diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 7b146d61f7..27739c32db 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -29,7 +29,7 @@ jobs: - run: cargo build --release -p agentos-native-sidecar - run: cargo build --release -p agentos-native-baseline - run: cargo build --release --target wasm32-wasip1 -p agentos-native-baseline - - run: make -C registry/native commands + - run: make -C toolchain commands - run: node packages/runtime-core/scripts/copy-wasm-commands.mjs --require - run: pnpm --dir packages/runtime-core build - run: pnpm --dir packages/runtime-benchmarks bench:check diff --git a/.github/workflows/ci-nightly.yml b/.github/workflows/ci-nightly.yml index 79bb6cdb3f..e0108161dc 100644 --- a/.github/workflows/ci-nightly.yml +++ b/.github/workflows/ci-nightly.yml @@ -23,7 +23,7 @@ jobs: with: targets: wasm32-wasip1 - run: pnpm install --frozen-lockfile - - run: make -C registry/native commands + - run: make -C toolchain commands - run: node packages/runtime-core/scripts/copy-wasm-commands.mjs --require - run: cargo check --workspace - run: cargo clippy --workspace --all-targets -- -D warnings diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69e0136cb9..c41b2f1a76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,7 @@ jobs: - run: node scripts/check-agentos-client-protocol-compat.mjs - run: node --test scripts/verify-fixed-versions.test.mjs - run: node scripts/verify-fixed-versions.mjs + - run: pnpm check-layout - run: cargo fmt --check - run: pnpm check-types - run: pnpm lint @@ -63,11 +64,11 @@ jobs: targets: wasm32-wasip1 - uses: Swatinem/rust-cache@v2 with: - workspaces: registry/native -> target + workspaces: toolchain -> toolchain/target cache-workspace-crates: true - key: wasm-commands-${{ hashFiles('registry/native/Cargo.lock') }} + key: wasm-commands-${{ hashFiles('toolchain/Cargo.lock') }} - run: pnpm install --frozen-lockfile --filter '@rivet-dev/agentos-runtime-core...' - - run: make -C registry/native commands + - run: make -C toolchain commands - run: node packages/runtime-core/scripts/copy-wasm-commands.mjs --require - uses: actions/upload-artifact@v4 with: @@ -104,9 +105,7 @@ jobs: - uses: actions/download-artifact@v4 with: name: wasm-commands - # Restore the canonical build output so registry packages can stage - # from the same path used by a local `just registry-native` build. - path: registry/native/target/wasm32-wasip1/release/commands + path: packages/runtime-core/commands - run: node packages/runtime-core/scripts/copy-wasm-commands.mjs --require - run: | if grep -qE '^[[:space:]]*-[[:space:]]*website[[:space:]]*$' pnpm-workspace.yaml; then diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index dd7cb0a5f7..512f308c13 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -66,11 +66,11 @@ jobs: targets: wasm32-wasip1 - uses: Swatinem/rust-cache@v2 with: - workspaces: registry/native -> target + workspaces: toolchain -> toolchain/target cache-workspace-crates: true - key: wasm-commands-${{ hashFiles('registry/native/Cargo.lock') }} + key: wasm-commands-${{ hashFiles('toolchain/Cargo.lock') }} - run: pnpm install --frozen-lockfile --filter '@rivet-dev/agentos-runtime-core...' - - run: make -C registry/native commands + - run: make -C toolchain commands - run: node packages/runtime-core/scripts/copy-wasm-commands.mjs --require - uses: actions/upload-artifact@v4 with: diff --git a/.gitignore b/.gitignore index ddccd47e77..e7d40a3268 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,19 @@ core.[0-9]* # Secrets secrets/**/* -# Registry build artifacts +# Generated WASM toolchain and software artifacts. These are rebuilt for tests +# and releases and must never be committed. +packages/runtime-core/commands/ +toolchain/vendor/ +toolchain/c/build/ +toolchain/c/vendor/ +toolchain/c/libs/ +toolchain/c/sysroot/ +toolchain/c/.cache/ +toolchain/std-patches/wasi-libc-overrides/*.o +software/*/bin/ + +# Legacy registry build paths retained for old worktrees. registry/native/target/ registry/native/vendor/ registry/native/c/build/ @@ -47,6 +59,7 @@ registry/native/c/.cache/ registry/native/c/sysroot/ registry/software/coreutils/bin/ registry/software/*/wasm/ +software/*/wasm/ registry/software/*/agentos-package.meta.json registry/.last-publish-hash registry/software/*/.last-publish-hash diff --git a/CLAUDE.md b/CLAUDE.md index b9172199b2..c5c872ea13 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -74,26 +74,55 @@ the guest — over inventing a softer fallback that hides the failure. only when the caller explicitly supplied them. - Agent adapters must use real upstream SDKs. Do not replace SDK adapters with direct API-call stubs. -- Native registry command binaries are generated artifacts. Do not commit - `registry/software/coreutils/bin/`; a fresh checkout intentionally has no - staged coreutils commands. Any VM, browser demo, or test that needs `sh` or - coreutils must build and stage the complete package from the repository root: +- WASM command binaries and every toolchain build output are generated + artifacts. Never commit `packages/runtime-core/commands/`, `software/*/bin/`, + `toolchain/vendor/`, `toolchain/c/{build,vendor,libs,sysroot,.cache}/`, or + `toolchain/std-patches/wasi-libc-overrides/*.o`. A fresh checkout intentionally + contains source and patches only. Rebuild and stage the complete default tool + set from the repository root with: ```bash pnpm install --frozen-lockfile - just registry-native - pnpm --filter @agentos-software/coreutils build:runtime + just tools-rebuild ``` - `just registry-native` compiles the patched Rust and C WASI sources into - `registry/native/target/wasm32-wasip1/release/commands/`; the pnpm command - then stages `bin/` and assembles `dist/package/`. `just registry-native-cmd - sh` builds only one development artifact and is not sufficient to assemble - the complete coreutils package. The ordinary `build` script may create an - empty placeholder for source-only repository checks; that placeholder is not - runnable and must never be treated as proof that coreutils was built. - Publishing coreutils must run the strict `build:runtime` lifecycle and fail - when the generated command set is absent or incomplete. + `just tools-rebuild` runs `just toolchain-build`, copies the canonical output + from `toolchain/target/wasm32-wasip1/release/commands/` into runtime staging, + and builds the `@agentos-software/*` packages. For focused development, + `just toolchain-cmd ` rebuilds one command, but it is not sufficient + for a release or complete package validation. Publish workflows must rebuild + and stage the complete command set and fail when it is absent or incomplete. + +## Software Build (WASM Toolchain) + +Registry software is **real upstream Linux software** (GNU coreutils, grep, sed, +gawk, real curl/sqlite/duckdb/vim, …) compiled to `wasm32-wasip1` against a +**sysroot we fully own** — a patched Rust std + libc whose gaps are filled by +custom host-syscall imports. Treat that target as **native POSIX**; +`wasm32-wasip1` is an implementation detail, not a feature ceiling. + +- **We do not depend on stock WASI / wasi-libc.** The sysroot is ours. A missing + libc/POSIX API (`getrlimit`/`RLIMIT_NOFILE`, `getgroups`, spawn, fd dup, …) is + never a blocker — implement it (real, or a sane stub) in the patched + std/libc/host-import layer. "WASI doesn't have X" is not a reason to stop; X is + ours to add. +- **Fix portability one layer down, in the sysroot** — a new std/libc patch or a + new host import — not with `cfg(target_*)` branches or shims in the tool's own + source. A WASM-specific branch in application code usually means the fix + belongs in the libc layer. +- **Patch the real upstream tool only as a fallback**, when the fix genuinely + cannot live in the sysroot. Patching the real tool is allowed; reimplementing + it is not. +- **"NOT POSSIBLE" is reserved for genuine impossibility** after exhausting both + sysroot patches and tool patches — never for a missing syscall we could + implement. Document the specific wall if you claim it. +- **Working in `software/`, you may (and should) fix the layer underneath.** When + a package behaves differently from real Linux, the root cause is usually not the + package — it's the runtime. It is in-scope and expected to fix the underlying + implementation: the Node-compat / bridge layer, the WASM execution runtime, the + kernel/VFS syscalls, or the patched sysroot/libc. Do **not** paper over a + Linux-deviating behavior in the package, its wrapper, or its test — chase it + down into whichever runtime layer owns it and make that layer match Linux. ## Publishing diff --git a/Cargo.lock b/Cargo.lock index 6c46128bd8..9a322ef02a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -189,7 +189,6 @@ dependencies = [ "rusqlite", "rustix 1.1.4", "rustls 0.23.41", - "rustls-native-certs", "rustls-pemfile", "scrypt", "serde", @@ -237,6 +236,7 @@ dependencies = [ "agentos-vm-config", "base64 0.22.1", "serde_json", + "webpki-root-certs", ] [[package]] @@ -2507,6 +2507,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2576,6 +2585,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", + "memoffset", ] [[package]] @@ -4273,6 +4283,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "0.26.11" diff --git a/Cargo.toml b/Cargo.toml index 8cc1efcefb..8bbe980838 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] resolver = "2" +exclude = ["software"] members = [ "crates/agentos-protocol", "crates/agentos-sidecar-core", diff --git a/README.md b/README.md index e9357918ca..1cf5b93e0f 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,6 @@ Browse pre-built agents, tools, filesystems, and software packages at the [agent | `@agentos-software/git` | git | git version control | rust | - | - | | `@agentos-software/grep` | grep | GNU grep pattern matching (grep, egrep, fgrep) | rust | - | - | | `@agentos-software/gzip` | gzip | GNU gzip compression (gzip, gunzip, zcat) | rust | - | - | -| `@agentos-software/http-get` | http-get | Minimal HTTP GET fetch helper | c | - | - | | `@agentos-software/jq` | jq | jq JSON processor | rust | - | - | | `@agentos-software/ripgrep` | ripgrep | ripgrep fast recursive search | rust | - | - | | `@agentos-software/sed` | sed | GNU sed stream editor | rust | - | - | @@ -165,7 +164,7 @@ Browse pre-built agents, tools, filesystems, and software packages at the [agent | `@agentos-software/tar` | tar | GNU tar archiver | rust | - | - | | `@agentos-software/tree` | tree | tree directory listing | rust | - | - | | `@agentos-software/unzip` | unzip | unzip archive extraction | c | - | - | -| `@agentos-software/wget` | wget | GNU wget HTTP client | c | - | - | +| `@agentos-software/wget` | wget | GNU Wget file downloader | c | - | - | | `@agentos-software/yq` | yq | yq YAML/JSON processor | rust | - | - | | `@agentos-software/zip` | zip | zip archive creation | c | - | - | @@ -175,7 +174,7 @@ Browse pre-built agents, tools, filesystems, and software packages at the [agent |---------|-------------|----------| | `@agentos-software/build-essential` | Build-essential WASM command set (standard + make + git + curl) | standard, make, git, curl | | `@agentos-software/common` | Common WASM command set (coreutils + sed + grep + gawk + findutils + diffutils + tar + gzip) | coreutils, sed, grep, gawk, findutils, diffutils, tar, gzip | -| `@agentos-software/everything` | All available WASM command packages in a single bundle | coreutils, sed, grep, gawk, findutils, diffutils, tar, gzip, curl, zip, unzip, jq, ripgrep, fd, tree, file, yq, codex-cli | +| `@agentos-software/everything` | All available WASM command packages in a single bundle | coreutils, sed, grep, gawk, findutils, diffutils, tar, gzip, curl, wget, zip, unzip, jq, ripgrep, fd, tree, file, yq, codex-cli | ## License diff --git a/crates/bridge/bridge-contract.json b/crates/bridge/bridge-contract.json index 12c3e68767..2417e9b0b3 100644 --- a/crates/bridge/bridge-contract.json +++ b/crates/bridge/bridge-contract.json @@ -189,12 +189,16 @@ "_childProcessSpawnStart", "_childProcessPoll", "_childProcessStdinWrite", - "_childProcessStdinClose", - "_childProcessKill", - "_processKill", - "_processSignalState" - ] - }, + "_childProcessStdinClose", + "_childProcessKill", + "_processKill", + "_processExec", + "_processExecFdImageCommit", + "_processSignalState", + "_processTakeSignal", + "_processWasmSyncRpc" + ] + }, { "convention": "syncPromise", "argumentTypes": [ @@ -244,6 +248,8 @@ "_upgradeSocketEndRaw", "_upgradeSocketDestroyRaw", "_networkDnsLookupSyncRaw", + "_netBindUnixRaw", + "_netBindConnectedUnixRaw", "_netSocketConnectRaw", "_netSocketPollRaw", "_netSocketReadRaw", @@ -260,6 +266,8 @@ "_netReleaseTcpPortRaw", "_netServerListenRaw", "_netServerAcceptRaw", + "_netServerCloseSyncRaw", + "_netSocketWaitConnectSyncRaw", "_dgramSocketCreateRaw", "_dgramSocketBindRaw", "_dgramSocketRecvRaw", @@ -757,6 +765,14 @@ "method": "net.release_tcp_port", "translateArgs": false }, + "_netBindConnectedUnixRaw": { + "method": "net.bind_connected_unix", + "translateArgs": false + }, + "_netBindUnixRaw": { + "method": "net.bind_unix", + "translateArgs": false + }, "_netReserveTcpPortRaw": { "method": "net.reserve_tcp_port", "translateArgs": false @@ -769,6 +785,10 @@ "method": "net.server_close", "translateArgs": false }, + "_netServerCloseSyncRaw": { + "method": "net.server_close", + "translateArgs": false + }, "_netServerListenRaw": { "method": "net.listen", "translateArgs": false @@ -817,6 +837,10 @@ "method": "net.socket_wait_connect", "translateArgs": false }, + "_netSocketWaitConnectSyncRaw": { + "method": "net.socket_wait_connect", + "translateArgs": false + }, "_netSocketWriteRaw": { "method": "net.write", "translateArgs": false @@ -945,13 +969,29 @@ "method": "process.kill", "translateArgs": false }, - "_processSignalState": { - "method": "process.signal_state", + "_processExec": { + "method": "process.exec", "translateArgs": false }, - "_ptySetRawMode": { - "method": "__pty_set_raw_mode", + "_processExecFdImageCommit": { + "method": "process.exec_fd_image_commit", "translateArgs": false + }, + "_processSignalState": { + "method": "process.signal_state", + "translateArgs": false + }, + "_processTakeSignal": { + "method": "process.take_signal", + "translateArgs": false + }, + "_processWasmSyncRpc": { + "method": "process.wasm_sync_rpc", + "translateArgs": false + }, + "_ptySetRawMode": { + "method": "__pty_set_raw_mode", + "translateArgs": false }, "_resolveModule": { "method": "__resolve_module", diff --git a/crates/bridge/src/queue_tracker.rs b/crates/bridge/src/queue_tracker.rs index f85d0dfd17..ccb0010572 100644 --- a/crates/bridge/src/queue_tracker.rs +++ b/crates/bridge/src/queue_tracker.rs @@ -72,6 +72,11 @@ pub enum TrackedLimit { SidecarStdoutFrames, CompletedSidecarResponses, PendingProcessEvents, + PendingProcessEventBytes, + PendingExecutionEvents, + PendingExecutionEventBytes, + PendingKernelStdinBytes, + PendingWasmSignals, PendingSidecarResponses, OutboundSidecarRequests, VmProcesses, @@ -104,6 +109,11 @@ impl TrackedLimit { TrackedLimit::SidecarStdoutFrames => "sidecar_stdout_frames", TrackedLimit::CompletedSidecarResponses => "completed_sidecar_responses", TrackedLimit::PendingProcessEvents => "pending_process_events", + TrackedLimit::PendingProcessEventBytes => "pending_process_event_bytes", + TrackedLimit::PendingExecutionEvents => "pending_execution_events", + TrackedLimit::PendingExecutionEventBytes => "pending_execution_event_bytes", + TrackedLimit::PendingKernelStdinBytes => "pending_kernel_stdin_bytes", + TrackedLimit::PendingWasmSignals => "pending_wasm_signals", TrackedLimit::PendingSidecarResponses => "pending_sidecar_responses", TrackedLimit::OutboundSidecarRequests => "outbound_sidecar_requests", TrackedLimit::VmProcesses => "vm_processes", @@ -134,6 +144,11 @@ impl TrackedLimit { | TrackedLimit::SidecarStdoutFrames | TrackedLimit::CompletedSidecarResponses | TrackedLimit::PendingProcessEvents + | TrackedLimit::PendingProcessEventBytes + | TrackedLimit::PendingExecutionEvents + | TrackedLimit::PendingExecutionEventBytes + | TrackedLimit::PendingKernelStdinBytes + | TrackedLimit::PendingWasmSignals | TrackedLimit::PendingSidecarResponses | TrackedLimit::OutboundSidecarRequests => LimitCategory::Queue, TrackedLimit::VmProcesses diff --git a/crates/client/src/config.rs b/crates/client/src/config.rs index a7a4203c55..49ad4ac010 100644 --- a/crates/client/src/config.rs +++ b/crates/client/src/config.rs @@ -261,6 +261,8 @@ pub struct AgentOsLimits { pub python: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub wasm: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub process: Option, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -589,6 +591,40 @@ pub struct WasmLimits { pub runner_heap_limit_mb: Option, } +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProcessLimits { + #[serde( + default, + rename = "maxSpawnFileActions", + skip_serializing_if = "Option::is_none" + )] + pub max_spawn_file_actions: Option, + #[serde( + default, + rename = "maxSpawnFileActionBytes", + skip_serializing_if = "Option::is_none" + )] + pub max_spawn_file_action_bytes: Option, + #[serde( + default, + rename = "pendingStdinBytes", + skip_serializing_if = "Option::is_none" + )] + pub pending_stdin_bytes: Option, + #[serde( + default, + rename = "pendingEventCount", + skip_serializing_if = "Option::is_none" + )] + pub pending_event_count: Option, + #[serde( + default, + rename = "pendingEventBytes", + skip_serializing_if = "Option::is_none" + )] + pub pending_event_bytes: Option, +} + // --------------------------------------------------------------------------- // Permissions tree (runtime.ts) // --------------------------------------------------------------------------- diff --git a/crates/client/tests/common/mod.rs b/crates/client/tests/common/mod.rs index 1013b35300..ca01e86829 100644 --- a/crates/client/tests/common/mod.rs +++ b/crates/client/tests/common/mod.rs @@ -149,8 +149,8 @@ fn wasm_command_mounts() -> Vec { /// Locate the materialized coreutils package under the in-repo registry build. pub fn coreutils_package_dir() -> Option { - let registry_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../../registry/software/coreutils/dist/package"); + let registry_dir = + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../software/coreutils/dist/package"); if registry_dir.join("agentos-package.json").is_file() { return std::fs::canonicalize(registry_dir).ok(); } @@ -175,7 +175,7 @@ pub fn coreutils_wasm_dir() -> Option { } let legacy_registry_dir = - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../registry/software/coreutils/wasm"); + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../software/coreutils/wasm"); if legacy_registry_dir.is_dir() { return std::fs::canonicalize(legacy_registry_dir).ok(); } @@ -244,6 +244,6 @@ pub async fn require_wasm_commands(os: &AgentOs, test_name: &str) -> bool { eprintln!("skipping {message}"); false } else { - panic!("{message}; run the registry/native command build or set AGENT_OS_CLIENT_ALLOW_E2E_SKIPS=1 for local skip-only runs"); + panic!("{message}; run the toolchain command build or set AGENT_OS_CLIENT_ALLOW_E2E_SKIPS=1 for local skip-only runs"); } } diff --git a/crates/client/tests/packages_aospkg_e2e.rs b/crates/client/tests/packages_aospkg_e2e.rs index 027f3a6824..fd9f104bcb 100644 --- a/crates/client/tests/packages_aospkg_e2e.rs +++ b/crates/client/tests/packages_aospkg_e2e.rs @@ -16,7 +16,7 @@ mod common; fn coreutils_aospkg() -> Option { for path in [ PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../../registry/software/coreutils/dist/package.aospkg"), + .join("../../software/coreutils/dist/package.aospkg"), PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../node_modules/@agentos-software/coreutils/dist/package.aospkg"), ] { diff --git a/crates/client/tests/pi_session_e2e.rs b/crates/client/tests/pi_session_e2e.rs index 9b15fd7fc9..2210301eba 100644 --- a/crates/client/tests/pi_session_e2e.rs +++ b/crates/client/tests/pi_session_e2e.rs @@ -9,7 +9,7 @@ //! - `AGENT_OS_PI_MODULE_CWD` env (a workspace with a built/installed `@agentos-software/pi`), else //! - the repo root, but only when the in-repo adapter is built //! (`node_modules/@agentos-software/pi/dist/adapter.js`). Build it with `pnpm --dir packages/core -//! build && pnpm --dir registry/agent/pi build` (core first for types). +//! build && pnpm --dir software/pi build` (core first for types). //! //! Background: a real agent SDK exercises module-loading patterns (tsc `__exportStar` CJS barrels, //! deep pnpm symlink graphs, `__dirname` package self-location) that mock ACP adapters never touch. @@ -55,11 +55,11 @@ fn pi_module_cwd() -> Option { fn pi_package_path() -> Option { // Prefer the packed .aospkg — the artifact the registry actually ships. - let aospkg = repo_root().join("registry/agent/pi/dist/package.aospkg"); + let aospkg = repo_root().join("software/pi/dist/package.aospkg"); if aospkg.is_file() { return std::fs::canonicalize(aospkg).ok(); } - let dir = repo_root().join("registry/agent/pi/dist/package"); + let dir = repo_root().join("software/pi/dist/package"); if dir.join("agentos-package.json").is_file() { std::fs::canonicalize(dir).ok() } else { diff --git a/crates/client/tests/shell_pty_packages_e2e.rs b/crates/client/tests/shell_pty_packages_e2e.rs index 746998a613..b057fdd5c6 100644 --- a/crates/client/tests/shell_pty_packages_e2e.rs +++ b/crates/client/tests/shell_pty_packages_e2e.rs @@ -17,7 +17,7 @@ fn coreutils_package_path() -> Option { // Prefer the packed .aospkg — the artifact registry packages actually ship. for aospkg in [ PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../../registry/software/coreutils/dist/package.aospkg"), + .join("../../software/coreutils/dist/package.aospkg"), PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../node_modules/@agentos-software/coreutils/dist/package.aospkg"), ] { @@ -26,8 +26,7 @@ fn coreutils_package_path() -> Option { } } for dir in [ - PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .join("../../registry/software/coreutils/dist/package"), + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../software/coreutils/dist/package"), PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../node_modules/@agentos-software/coreutils/dist/package"), ] { diff --git a/crates/execution/CLAUDE.md b/crates/execution/CLAUDE.md index 4bf08bd151..5aff12c315 100644 --- a/crates/execution/CLAUDE.md +++ b/crates/execution/CLAUDE.md @@ -91,7 +91,7 @@ ESM loader hooks (`loader.mjs`) and CJS `Module._load` patches (`runner.mjs`) ar - Node-facing `readdir` results must filter `.`/`..`. - Async methods should dispatch under `fs.promises.*`. - `fs.promises` methods that need real concurrency must use dedicated async bridge globals in `packages/build-tools/bridge-src/`; wrapping `fs.*Sync` inside `async` functions still serializes `Promise.all(...)` behind the first sidecar response. -- When adding WASI guest imports in `registry/native/crates/wasi-ext`, mirror the required module/object in `crates/execution/src/node_import_cache.rs`'s inline `NODE_WASM_RUNNER_SOURCE`; missing modules fail at `WebAssembly.instantiate()` before guest `main()` runs. +- When adding WASI guest imports in `toolchain/crates/wasi-ext`, mirror the required module/object in `crates/execution/src/node_import_cache.rs`'s inline `NODE_WASM_RUNNER_SOURCE`; missing modules fail at `WebAssembly.instantiate()` before guest `main()` runs. - Keep the embedded WASI shim in `crates/execution/src/wasm.rs` aligned with the patched wasi-libc surface used by the C command suite; overrides like `fcntl(F_SETFL)` now depend on `fd_fdstat_set_flags`, and missing imports fail at instantiation time before the guest command can do real work. - The shared-V8 WASM runner now resolves its own module loads plus internal guest `fs.openSync` / `fs.readSync` / `fs.writeSync` / `fs.closeSync` traffic inside `crates/execution/src/wasm.rs`; if the embedded runner gains more internal file syscalls, extend that internal sync-RPC handling there instead of surfacing those requests to callers or reintroducing a host-Node runtime path. - fd-based APIs (`open`, `read`, `write`, `close`, `fstat`) plus `createReadStream`/`createWriteStream` should ride the same bridge. diff --git a/crates/execution/assets/runners/python-runner.mjs b/crates/execution/assets/runners/python-runner.mjs index cbc835b830..50702cb77c 100644 --- a/crates/execution/assets/runners/python-runner.mjs +++ b/crates/execution/assets/runners/python-runner.mjs @@ -630,6 +630,7 @@ function createPythonBridgeRpcBridge() { subprocessRunSync( command, argsJson = '[]', + argv0 = null, cwd = null, envJson = '{}', shell = false, @@ -646,6 +647,7 @@ function createPythonBridgeRpcBridge() { return JSON.stringify(requestSync('subprocessRun', { command, args, + argv0, cwd, env, shell, @@ -852,6 +854,7 @@ function createPythonFdRpcBridge() { subprocessRunSync( command, argsJson = '[]', + argv0 = null, cwd = null, envJson = '{}', shell = false, @@ -868,6 +871,7 @@ function createPythonFdRpcBridge() { return JSON.stringify(requestSync('subprocessRun', { command, args, + argv0, cwd, env, shell, @@ -1393,16 +1397,35 @@ class _SecureExecCompletedProcess: self.stdout = stdout self.stderr = stderr -def _agentos_subprocess_run(args, *, capture_output=False, check=False, cwd=None, env=None, input=None, shell=False, text=False, encoding="utf-8", errors="strict", stdout=None, stderr=None, timeout=None, **kwargs): +def _agentos_subprocess_run(args, *, capture_output=False, check=False, cwd=None, env=None, input=None, shell=False, executable=None, text=False, encoding="utf-8", errors="strict", stdout=None, stderr=None, timeout=None, **kwargs): del kwargs, stdout, stderr, timeout if isinstance(args, (str, bytes)): - command = args.decode("utf-8") if isinstance(args, bytes) else args - argv = [] + original_command = args.decode("utf-8") if isinstance(args, bytes) else args + values = None else: values = list(args) if not values: raise ValueError("subprocess.run args must not be empty") - command = str(values[0]) + original_command = str(values[0]) + + bridge_shell = False + if shell: + # Python implements shell=True as shell -c command [argv...]. Invoke the + # shell explicitly so a string-valued executable selects the requested + # shell and sequence arguments retain their Linux $0/$1 positions. + command = str(executable) if executable is not None else "/bin/sh" + argv0 = command + if values is None: + argv = ["-c", original_command] + else: + argv = ["-c", original_command, *[str(value) for value in values[1:]]] + elif values is None: + command = str(executable) if executable is not None else original_command + argv0 = original_command + argv = [] + else: + command = str(executable) if executable is not None else original_command + argv0 = original_command argv = [str(value) for value in values[1:]] merged_env = dict(env or {}) resolved_cwd = cwd if cwd is not None else _agentos_os.environ.get("PWD") @@ -1413,9 +1436,10 @@ def _agentos_subprocess_run(args, *, capture_output=False, check=False, cwd=None _agentos_rpc.subprocessRunSync( command, _agentos_json.dumps(argv), + argv0, resolved_cwd, _agentos_json.dumps(merged_env), - bool(shell), + bridge_shell, ) ) except Exception as error: diff --git a/crates/execution/assets/runners/wasi-module.js b/crates/execution/assets/runners/wasi-module.js index 7dfe4d0384..769b0be992 100644 --- a/crates/execution/assets/runners/wasi-module.js +++ b/crates/execution/assets/runners/wasi-module.js @@ -40,6 +40,7 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = const __agentOSWasiErrnoNoent = 44; const __agentOSWasiErrnoNosys = 52; const __agentOSWasiErrnoNotdir = 54; + const __agentOSWasiErrnoNotempty = 55; const __agentOSWasiErrnoPipe = 64; const __agentOSWasiErrnoRofs = 69; const __agentOSWasiErrnoNotcapable = 76; @@ -54,6 +55,7 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = const __agentOSWasiOpenDirectory = 2; const __agentOSWasiOpenExclusive = 4; const __agentOSWasiOpenTruncate = 8; + const __agentOSWasiFdflagsAppend = 1; const __agentOSWasiRightFdRead = 1n << 1n; const __agentOSWasiRightFdWrite = 1n << 6n; const __agentOSWasiDefaultRightsBase = 0xffffffffffffffffn; @@ -396,6 +398,14 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = } } + _hasReadRights(rights) { + try { + return (BigInt(rights) & __agentOSWasiRightFdRead) !== 0n; + } catch { + return true; + } + } + _writeUint32(ptr, value) { try { this._memoryView().setUint32(Number(ptr) >>> 0, Number(value) >>> 0, true); @@ -779,6 +789,9 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = } _mapFsError(error) { + __agentOSWasiDebug( + `fs error code=${String(error?.code ?? "")} message=${String(error?.message ?? error)}`, + ); switch (error?.code) { case "EACCES": case "EPERM": @@ -787,6 +800,8 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = return __agentOSWasiErrnoNoent; case "ENOTDIR": return __agentOSWasiErrnoNotdir; + case "ENOTEMPTY": + return __agentOSWasiErrnoNotempty; case "EEXIST": return __agentOSWasiErrnoExist; case "EINVAL": @@ -827,6 +842,7 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = refCount: 1, open: true, readOnly: entry.readOnly === true, + append: entry.append === true, }; } @@ -1380,19 +1396,44 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); } } + const entry = this._descriptorEntry(descriptor); + const localHostPassthrough = + handle.kind === "host-passthrough" && + entry?.kind === "file" && + entry.realFd === handle.targetFd; + const position = handle.append + ? this._measureWasiPhase("appendFstat", () => + Number(__agentOSFs().fstatSync(handle.targetFd).size ?? 0) + ) + : localHostPassthrough + ? (entry.offset ?? 0) + : null; const written = this._measureWasiPhase("writeSync", () => __agentOSFs().writeSync( handle.targetFd, bytes, 0, bytes.length, - null, + position, ) ); + if (localHostPassthrough) { + if (handle.append) { + entry.offset = this._measureWasiPhase("appendFstat", () => + Number(__agentOSFs().fstatSync(handle.targetFd).size ?? 0) + ); + } else { + entry.offset = (entry.offset ?? 0) + written; + } + } return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); } if (handle?.kind === "guest-file" && typeof handle.targetFd === "number") { - const position = handle.append ? null : (handle.position ?? 0); + const position = handle.append + ? this._measureWasiPhase("appendFstat", () => + Number(__agentOSFs().fstatSync(handle.targetFd).size ?? 0) + ) + : (handle.position ?? 0); const written = this._measureWasiPhase("writeSync", () => __agentOSFs().writeSync( handle.targetFd, @@ -1469,7 +1510,11 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = } if (entry.kind === "file") { this._clearStatCache(); - const position = typeof entry.offset === "number" ? entry.offset : null; + const position = entry.append + ? this._measureWasiPhase("appendFstat", () => + Number(__agentOSFs().fstatSync(entry.realFd).size ?? 0) + ) + : (typeof entry.offset === "number" ? entry.offset : null); const written = this._measureWasiPhase("writeSync", () => __agentOSFs().writeSync( entry.realFd, @@ -1479,7 +1524,11 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = position, ) ); - if (typeof entry.offset === "number") { + if (entry.append) { + entry.offset = this._measureWasiPhase("appendFstat", () => + Number(__agentOSFs().fstatSync(entry.realFd).size ?? 0) + ); + } else if (typeof entry.offset === "number") { entry.offset += written; } return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); @@ -1715,6 +1764,11 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = (handle?.kind === "passthrough" || handle?.kind === "host-passthrough") && typeof handle.targetFd === "number" ) { + const localEntry = this._descriptorEntry(descriptor); + const localHostPassthrough = + handle.kind === "host-passthrough" && + localEntry?.kind === "file" && + localEntry.realFd === handle.targetFd; const totalLength = this._boundedReadLength(iovs, iovsLen); const buffer = Buffer.alloc(totalLength); const bytesRead = __agentOSFs().readSync( @@ -1722,8 +1776,11 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = buffer, 0, totalLength, - null, + localHostPassthrough ? (localEntry.offset ?? 0) : null, ); + if (localHostPassthrough) { + localEntry.offset = (localEntry.offset ?? 0) + bytesRead; + } const written = this._writeToIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)); return this._writeUint32(nreadPtr, written); } @@ -2206,8 +2263,12 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = this._clearStatCache(); } const fsConstants = __agentOSFs().constants ?? {}; + const requestedFdFlags = Number(_fdflags) >>> 0; + const append = (requestedFdFlags & __agentOSWasiFdflagsAppend) !== 0; let openFlags = requestedWriteAccess - ? fsConstants.O_RDWR ?? 2 + ? (this._hasReadRights(requestedRightsBase) + ? fsConstants.O_RDWR ?? 2 + : fsConstants.O_WRONLY ?? 1) : fsConstants.O_RDONLY ?? 0; if ((requestedFlags & __agentOSWasiOpenCreate) !== 0) { openFlags |= fsConstants.O_CREAT ?? 64; @@ -2218,6 +2279,9 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = if ((requestedFlags & __agentOSWasiOpenTruncate) !== 0) { openFlags |= fsConstants.O_TRUNC ?? 512; } + if (append) { + openFlags |= fsConstants.O_APPEND ?? 1024; + } if (openDirectory) { openFlags |= fsConstants.O_DIRECTORY ?? 0; } @@ -2235,10 +2299,13 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = hostPath: fsPath, readOnly: resolved.readOnly === true, realFd, - offset: 0, + offset: append + ? Number(__agentOSFs().fstatSync(realFd).size ?? 0) + : 0, + append, rightsBase: requestedRightsBase & allowedRightsInheriting, rightsInheriting: requestedRightsInheriting & allowedRightsInheriting, - fdFlags: (Number(_fdflags) >>> 0) & 0xffff, + fdFlags: requestedFdFlags & 0xffff, }); }); return this._measureWasiPhase("writeOpenedFd", () => this._writeUint32(openedFdPtr, openedFd)); diff --git a/crates/execution/assets/runners/wasm-runner.mjs b/crates/execution/assets/runners/wasm-runner.mjs index fb7e839855..18e3e958ce 100644 --- a/crates/execution/assets/runners/wasm-runner.mjs +++ b/crates/execution/assets/runners/wasm-runner.mjs @@ -17,29 +17,104 @@ const HOST_CWD = const WASI_ERRNO_SUCCESS = 0; const WASI_ERRNO_ACCES = 2; +const WASI_ERRNO_ADDRINUSE = 3; +const WASI_ERRNO_ADDRNOTAVAIL = 4; +const WASI_ERRNO_AFNOSUPPORT = 5; const WASI_ERRNO_AGAIN = 6; +const WASI_ERRNO_ALREADY = 7; const WASI_ERRNO_BADF = 8; -const WASI_ERRNO_CHILD = 10; +const WASI_ERRNO_CHILD = 12; +const WASI_ERRNO_CONNREFUSED = 14; +const WASI_ERRNO_CONNRESET = 15; +const WASI_ERRNO_DEADLK = 16; +const WASI_ERRNO_DESTADDRREQ = 17; +const WASI_ERRNO_EXIST = 20; +const WASI_ERRNO_FBIG = 22; +const WASI_ERRNO_ILSEQ = 25; +const WASI_ERRNO_INPROGRESS = 26; const WASI_ERRNO_INVAL = 28; +const WASI_ERRNO_INTR = 27; +const WASI_ERRNO_IO = 29; +const WASI_ERRNO_ISCONN = 30; +const WASI_ERRNO_LOOP = 32; +const WASI_ERRNO_MFILE = 33; +const WASI_ERRNO_MSGSIZE = 35; +const WASI_ERRNO_NETUNREACH = 40; +const WASI_ERRNO_NAMETOOLONG = 37; +const WASI_ERRNO_NOBUFS = 42; const WASI_ERRNO_NOENT = 44; +const WASI_ERRNO_NOEXEC = 45; +const WASI_ERRNO_HOSTUNREACH = 23; +const WASI_ERRNO_NOTDIR = 54; +const WASI_ERRNO_NOTCONN = 53; +const WASI_ERRNO_NOTSOCK = 57; +const WASI_ERRNO_NOTSUP = 58; +const WASI_ERRNO_NXIO = 60; +const WASI_ERRNO_PERM = 63; const WASI_ERRNO_PIPE = 64; +const WASI_ERRNO_PROTONOSUPPORT = 66; +const WASI_ERRNO_2BIG = 1; const WASI_ERRNO_ROFS = 69; const WASI_ERRNO_SPIPE = 70; const WASI_ERRNO_SRCH = 71; +const WASI_ERRNO_TIMEDOUT = 73; const WASI_ERRNO_FAULT = 21; const WASI_RIGHT_FD_WRITE = 64n; const WASI_FILETYPE_UNKNOWN = 0; const WASI_FILETYPE_CHARACTER_DEVICE = 2; +const WASI_FILETYPE_DIRECTORY = 3; const WASI_FILETYPE_REGULAR_FILE = 4; +const WASI_FILETYPE_SOCKET_DGRAM = 5; +const WASI_FILETYPE_SOCKET_STREAM = 6; const WASI_OFLAGS_CREAT = 1; const WASI_OFLAGS_DIRECTORY = 2; const WASI_OFLAGS_EXCL = 4; const WASI_OFLAGS_TRUNC = 8; const WASI_FDFLAGS_APPEND = 1; +const WASI_FDFLAGS_NONBLOCK = 4; +const KERNEL_O_WRONLY = 0o1; +const KERNEL_O_RDWR = 0o2; +const KERNEL_O_CREAT = 0o100; +const KERNEL_O_EXCL = 0o200; +const KERNEL_O_TRUNC = 0o1000; +const KERNEL_O_APPEND = 0o2000; +const KERNEL_O_NONBLOCK = 0o4000; +const KERNEL_O_DIRECTORY = 0o200000; +const KERNEL_O_NOFOLLOW = 0o400000; const WASI_WHENCE_SET = 0; const WASI_WHENCE_CUR = 1; const WASI_WHENCE_END = 2; const WASM_PAGE_BYTES = 65536; +// Linux exposes a separate numeric descriptor ceiling (`fs.nr_open`) in +// addition to the process' open-description limit. Keep the guest namespace +// below that ceiling so ordinary allocation can never collide with the +// private 0x40000000 pathname-preopen tag. +const LINUX_GUEST_FD_LIMIT = 1 << 20; +const LINUX_BINPRM_BUF_SIZE = 256; +const LINUX_MAX_INTERPRETER_DEPTH = 4; +const DEFAULT_WASM_MAX_MODULE_FILE_BYTES = 256 * 1024 * 1024; +const POSIX_SPAWN_RESETIDS = 1; +const POSIX_SPAWN_SETPGROUP = 2; +const POSIX_SPAWN_SETSIGDEF = 4; +const POSIX_SPAWN_SETSIGMASK = 8; +const LINUX_SA_NODEFER = 0x40000000; +const LINUX_SA_RESETHAND = 0x80000000; +const POSIX_SPAWN_SETSCHEDPARAM = 16; +const POSIX_SPAWN_SETSCHEDULER = 32; +const POSIX_SPAWN_USEVFORK = 64; +const POSIX_SPAWN_SETSID = 128; +const SUPPORTED_POSIX_SPAWN_FLAGS = + POSIX_SPAWN_RESETIDS | + POSIX_SPAWN_SETPGROUP | + POSIX_SPAWN_SETSIGDEF | + POSIX_SPAWN_SETSIGMASK | + POSIX_SPAWN_SETSCHEDPARAM | + POSIX_SPAWN_SETSCHEDULER | + POSIX_SPAWN_USEVFORK | + POSIX_SPAWN_SETSID; +const LINUX_SIGKILL = 9; +const LINUX_SIGSTOP = 19; +const INTERNAL_KERNEL_COMMAND_STUB = Buffer.from('#!/bin/sh\n# kernel command stub\n'); const DEFAULT_VIRTUAL_PID = 1; const DEFAULT_VIRTUAL_PPID = 0; const DEFAULT_VIRTUAL_UID = 0; @@ -60,6 +135,94 @@ function parseVirtualProcessString(value, fallback) { return typeof value === 'string' && value.length > 0 ? value : fallback; } +function parseInitialSignalSet(value, setting) { + if (typeof value !== 'string' || value.length === 0) { + return []; + } + let parsed; + try { + parsed = JSON.parse(value); + } catch (error) { + throw new Error(`${setting} must be a JSON signal-number array: ${error}`); + } + if (!Array.isArray(parsed) || parsed.length > 64) { + throw new Error(`${setting} must contain at most 64 signal numbers`); + } + const signals = new Set(); + for (const value of parsed) { + if (!Number.isInteger(value) || value <= 0 || value > 64) { + throw new Error(`${setting} contains invalid signal ${String(value)}`); + } + signals.add(value); + } + return [...signals]; +} + +function parseInitialKernelFdMappings(value, limit) { + if (typeof value !== 'string' || value.length === 0) { + return new Map(); + } + let parsed; + try { + parsed = JSON.parse(value); + } catch (error) { + throw new Error(`AGENTOS_WASM_INHERITED_FD_MAPPINGS must be a JSON pair array: ${error}`); + } + if (!Array.isArray(parsed) || parsed.length > limit) { + throw new Error( + `AGENTOS_WASM_INHERITED_FD_MAPPINGS exceeds the ${limit}-descriptor runtime limit`, + ); + } + const byKernelFd = new Map(); + const guestFds = new Set(); + for (const pair of parsed) { + if (!Array.isArray(pair) || pair.length !== 2) { + throw new Error('AGENTOS_WASM_INHERITED_FD_MAPPINGS entries must be [guestFd, kernelFd]'); + } + const guestFd = Number(pair[0]); + const kernelFd = Number(pair[1]); + if ( + !Number.isSafeInteger(guestFd) || guestFd < 0 || guestFd >= LINUX_GUEST_FD_LIMIT || + !Number.isSafeInteger(kernelFd) || kernelFd < 0 || kernelFd > 0xffffffff || + guestFds.has(guestFd) || byKernelFd.has(kernelFd) + ) { + throw new Error('AGENTOS_WASM_INHERITED_FD_MAPPINGS contains invalid or duplicate fds'); + } + guestFds.add(guestFd); + byKernelFd.set(kernelFd, guestFd); + } + return byKernelFd; +} + +function parseInitialClosedGuestFds(value, limit) { + if (typeof value !== 'string' || value.length === 0) { + return new Set(); + } + let parsed; + try { + parsed = JSON.parse(value); + } catch (error) { + throw new Error(`AGENTOS_WASM_CLOSED_INHERITED_FDS must be a JSON fd array: ${error}`); + } + if (!Array.isArray(parsed) || parsed.length > limit) { + throw new Error( + `AGENTOS_WASM_CLOSED_INHERITED_FDS exceeds the ${limit}-descriptor runtime limit`, + ); + } + const descriptors = new Set(); + for (const value of parsed) { + const fd = Number(value); + if ( + !Number.isSafeInteger(fd) || fd < 0 || fd >= LINUX_GUEST_FD_LIMIT || + descriptors.has(fd) + ) { + throw new Error('AGENTOS_WASM_CLOSED_INHERITED_FDS contains invalid or duplicate fds'); + } + descriptors.add(fd); + } + return descriptors; +} + function resolveVirtualPath(value, fallback) { const resolved = parseVirtualProcessString(value, fallback); return resolved.startsWith('/') ? path.posix.normalize(resolved) : fallback; @@ -330,8 +493,20 @@ function __agentOSWasmEmitPhaseMetrics(reason, extra = {}) { } } -const guestArgv = JSON.parse(process.env.AGENTOS_GUEST_ARGV ?? '[]'); -const guestEnv = JSON.parse(process.env.AGENTOS_GUEST_ENV ?? '{}'); +let guestArgv = JSON.parse(process.env.AGENTOS_GUEST_ARGV ?? '[]'); +let guestEnv = JSON.parse(process.env.AGENTOS_GUEST_ENV ?? '{}'); +const initialWasmSignalMask = parseInitialSignalSet( + process.env.AGENTOS_WASM_INITIAL_SIGNAL_MASK, + 'AGENTOS_WASM_INITIAL_SIGNAL_MASK', +).filter((signal) => signal !== LINUX_SIGKILL && signal !== LINUX_SIGSTOP); +const initialWasmSignalIgnores = parseInitialSignalSet( + process.env.AGENTOS_WASM_INITIAL_SIGNAL_IGNORES, + 'AGENTOS_WASM_INITIAL_SIGNAL_IGNORES', +); +const initialWasmPendingSignals = parseInitialSignalSet( + process.env.AGENTOS_WASM_INITIAL_PENDING_SIGNALS, + 'AGENTOS_WASM_INITIAL_PENDING_SIGNALS', +); const GUEST_PATH_MAPPINGS = parseGuestPathMappings(process.env.AGENTOS_GUEST_PATH_MAPPINGS); const permissionTier = process.env.AGENTOS_WASM_PERMISSION_TIER ?? 'full'; const prewarmOnly = process.env.AGENTOS_WASM_PREWARM_ONLY === '1'; @@ -339,11 +514,100 @@ const maxMemoryBytesValue = Number(process.env.AGENTOS_WASM_MAX_MEMORY_BYTES); const maxMemoryPages = Number.isFinite(maxMemoryBytesValue) ? Math.max(0, Math.floor(maxMemoryBytesValue / WASM_PAGE_BYTES)) : null; +const maxModuleFileBytesValue = Number(process.env.AGENTOS_WASM_MAX_MODULE_FILE_BYTES); +const maxModuleFileBytes = + Number.isFinite(maxModuleFileBytesValue) && maxModuleFileBytesValue >= 0 + ? Math.floor(maxModuleFileBytesValue) + : DEFAULT_WASM_MAX_MODULE_FILE_BYTES; +const maxSpawnFileActionsValue = Number(process.env.AGENTOS_WASM_MAX_SPAWN_FILE_ACTIONS); +const maxSpawnFileActions = + Number.isFinite(maxSpawnFileActionsValue) && maxSpawnFileActionsValue > 0 + ? Math.floor(maxSpawnFileActionsValue) + : 4096; +const maxSpawnFileActionBytesValue = Number( + process.env.AGENTOS_WASM_MAX_SPAWN_FILE_ACTION_BYTES, +); +const maxSpawnFileActionBytes = + Number.isFinite(maxSpawnFileActionBytesValue) && maxSpawnFileActionBytesValue > 0 + ? Math.floor(maxSpawnFileActionBytesValue) + : 1024 * 1024; +let warnedSpawnFileActions = false; +let warnedSpawnFileActionBytes = false; const maxStackBytesValue = Number(process.env.AGENTOS_WASM_MAX_STACK_BYTES); const maxStackBytes = Number.isFinite(maxStackBytesValue) && maxStackBytesValue > 0 ? Math.floor(maxStackBytesValue) : null; +const maxOpenFdsValue = Number(process.env.AGENTOS_WASM_MAX_OPEN_FDS); +const configuredMaxOpenFds = Number.isFinite(maxOpenFdsValue) && maxOpenFdsValue >= 0 + ? Math.floor(maxOpenFdsValue) + : 256; +const inheritedNofileHardValue = Number( + process.env.AGENTOS_WASM_RLIMIT_NOFILE_HARD, +); +let rlimitNofileHard = + Number.isSafeInteger(inheritedNofileHardValue) && + inheritedNofileHardValue >= 0 && + inheritedNofileHardValue <= configuredMaxOpenFds + ? inheritedNofileHardValue + : configuredMaxOpenFds; +const inheritedNofileSoftValue = Number( + process.env.AGENTOS_WASM_RLIMIT_NOFILE_SOFT, +); +let rlimitNofileSoft = + Number.isSafeInteger(inheritedNofileSoftValue) && + inheritedNofileSoftValue >= 0 && + inheritedNofileSoftValue <= rlimitNofileHard + ? inheritedNofileSoftValue + : rlimitNofileHard; + +function inheritedNofileBootstrapEnv() { + return { + AGENTOS_WASM_RLIMIT_NOFILE_SOFT: String(rlimitNofileSoft), + AGENTOS_WASM_RLIMIT_NOFILE_HARD: String(rlimitNofileHard), + }; +} +const maxSocketsValue = Number(process.env.AGENTOS_WASM_MAX_SOCKETS); +const maxSockets = Number.isFinite(maxSocketsValue) && maxSocketsValue >= 0 + ? Math.floor(maxSocketsValue) + : null; +const initialKernelFdMappings = parseInitialKernelFdMappings( + process.env.AGENTOS_WASM_INHERITED_FD_MAPPINGS, + configuredMaxOpenFds, +); +const initialHostNetDescriptions = parseInitialHostNetFds( + process.env.AGENTOS_WASM_INHERITED_HOSTNET_FDS, + configuredMaxOpenFds, + maxSockets, +); +const initialHostNetGuestFds = initialHostNetDescriptions + .flatMap((description) => description.guestFds.map((fd) => Number(fd) >>> 0)); +const initialMappedGuestFds = new Set([ + ...initialKernelFdMappings.values(), + ...initialHostNetGuestFds, +]); +if (initialMappedGuestFds.size !== initialKernelFdMappings.size + initialHostNetGuestFds.length) { + throw new Error('inherited kernel and host-network descriptors overlap in the guest fd table'); +} +if (initialMappedGuestFds.size > configuredMaxOpenFds) { + throw new Error( + `inherited descriptors exceed limits.resources.maxOpenFds (${configuredMaxOpenFds})`, + ); +} +const initialClosedGuestFds = parseInitialClosedGuestFds( + process.env.AGENTOS_WASM_CLOSED_INHERITED_FDS, + configuredMaxOpenFds, +); +traceHostProcess('kernel-fd-bootstrap', { + mappings: [...initialKernelFdMappings.entries()], + closedGuestFds: [...initialClosedGuestFds], + hostNetGuestFds: initialHostNetGuestFds, +}); +const maxBlockingReadMsValue = Number(process.env.AGENTOS_WASM_MAX_BLOCKING_READ_MS); +const maxBlockingReadMs = Number.isFinite(maxBlockingReadMsValue) && maxBlockingReadMsValue >= 0 + ? Math.floor(maxBlockingReadMsValue) + : null; +const unixConnectTimeoutMs = maxBlockingReadMs ?? 30_000; // A guest can drive WebAssembly into never-returning recursion. V8's default // native stack guard already traps that as a generic `RangeError`, but the @@ -368,7 +632,8 @@ function reportConfiguredStackLimitExceeded(error) { : ''; if (typeof process?.stderr?.write === 'function') { process.stderr.write( - `WebAssembly guest exceeded the configured stack byte limit of ${maxStackBytes} bytes${detail}\n`, + `WebAssembly guest exhausted its configured stack budget (${maxStackBytes} bytes); ` + + `raise limits.resources.maxWasmStackBytes to allow deeper guest call stacks${detail}.\n`, ); } } @@ -411,12 +676,15 @@ const KERNEL_STDIO_SYNC_RPC = process.env.AGENTOS_WASI_STDIO_SYNC_RPC === '1'; const SIDECAR_MANAGED_PROCESS = typeof process?.env?.AGENTOS_SANDBOX_ROOT === 'string' && process.env.AGENTOS_SANDBOX_ROOT.length > 0; +const SIDECAR_EXEC_COMMIT_RPC = process.env.AGENTOS_WASM_EXEC_COMMIT_RPC === '1'; let nextSyncRpcId = 1; let syncRpcResponseBuffer = ''; const spawnedChildren = new Map(); const spawnedChildrenById = new Map(); +let nextBlockingChildPumpIndex = 0; let nextSyntheticChildPid = 0x40000000; const syntheticFdEntries = new Map(); +const runnerCloexecFds = new Set(); const delegateManagedFdRefCounts = new Map(); const closedPassthroughFds = new Set(); globalThis.__agentOSWasiDelegateFdRefCount = (fd) => @@ -426,12 +694,293 @@ const passthroughHandles = new Map([ [1, { kind: 'passthrough', targetFd: 1, displayFd: 1, refCount: 0, open: true }], [2, { kind: 'passthrough', targetFd: 2, displayFd: 2, refCount: 0, open: true }], ]); +// POSIX spawn close actions are applied before the child runner starts. Node's +// WASI bootstrap still creates private 0/1/2 entries, so explicitly hide any +// stdio descriptor the child inherited as closed. +for (const fd of initialClosedGuestFds) { + if (fd <= 2) { + passthroughHandles.delete(fd); + closedPassthroughFds.add(fd); + } +} const retainedSyntheticHandlesByDisplayFd = new Map(); const retainedSpawnOutputHandlesByFd = new Map(); -let nextSyntheticFd = 64; +const FIRST_SYNTHETIC_FD = 3; +let nextSyntheticFd = FIRST_SYNTHETIC_FD; let nextSyntheticPipeId = 1; const syntheticWaitArray = new Int32Array(new SharedArrayBuffer(4)); let delegateWriteScratch = { base: 0, capacity: 0 }; +const EXEC_REPLACEMENT_MARKER = Symbol('agentos.wasm.exec-replacement'); +let warnedExecCloseFailure = false; + +function warnExecCloseFailure(fd, detail) { + if (warnedExecCloseFailure || typeof process?.stderr?.write !== 'function') return; + warnedExecCloseFailure = true; + process.stderr.write( + `[agentos] exec committed but closing FD_CLOEXEC descriptor ${fd} failed (${detail}); further close failures are suppressed\n`, + ); +} + +function isExecReplacement(error) { + return error && typeof error === 'object' && error.marker === EXEC_REPLACEMENT_MARKER; +} + +function readExecCloexecFds(ptr, count) { + const length = Number(count) >>> 0; + if (length > configuredMaxOpenFds) { + const error = new RangeError( + `proc_exec CLOEXEC descriptor count ${length} exceeds limits.resources.maxOpenFds (${configuredMaxOpenFds}); raise limits.resources.maxOpenFds if needed`, + ); + error.code = 'EINVAL'; + throw error; + } + if (!(instanceMemory instanceof WebAssembly.Memory)) { + throw new Error('WebAssembly memory is unavailable'); + } + const base = Number(ptr) >>> 0; + const byteLength = length * 4; + if (base + byteLength > instanceMemory.buffer.byteLength) { + const error = new RangeError('proc_exec CLOEXEC descriptor list is outside guest memory'); + error.code = 'EINVAL'; + throw error; + } + const view = new DataView(instanceMemory.buffer); + const fds = []; + for (let index = 0; index < length; index += 1) { + fds.push(view.getUint32(base + index * 4, true)); + } + return fds; +} + +function resolveExecModulePath(command) { + const raw = String(command); + const guestPath = raw.startsWith('/') + ? path.posix.normalize(raw) + : path.posix.resolve(HOST_FS_GUEST_CWD, raw); + return resolveModuleGuestPathToHostPath(guestPath) ?? guestPath; +} + +function execError(code, message) { + const error = new Error(message); + error.code = code; + return error; +} + +function isProjectedCommandGuestPath(subject) { + const raw = String(subject); + const guestPath = raw.startsWith('/') + ? path.posix.normalize(raw) + : path.posix.resolve(HOST_FS_GUEST_CWD, raw); + return ( + /^\/__secure_exec\/commands\/\d+(?:\/|$)/u.test(guestPath) || + /^\/opt\/agentos\/bin\/[^/]+$/u.test(guestPath) + ); +} + +function validateExecutableStat(stats, subject, projectedExecutable = false) { + if (typeof stats?.isFile !== 'function' || !stats.isFile()) { + throw execError('EACCES', `${subject} is not a regular executable file`); + } + if (!projectedExecutable && (Number(stats.mode) & 0o111) === 0) { + throw execError('EACCES', `${subject} does not have an executable mode bit`); + } + const size = Number(stats.size); + if (!Number.isSafeInteger(size) || size < 0) { + throw execError('EFBIG', `${subject} has an invalid executable image size`); + } + if (size > maxModuleFileBytes) { + throw execError( + 'EFBIG', + `${subject} is ${size} bytes, exceeding limits.wasm.maxModuleFileBytes (${maxModuleFileBytes}); raise limits.wasm.maxModuleFileBytes if needed`, + ); + } + return size; +} + +function readExecutableFdBytes(targetFd, stats, subject, projectedExecutable = false) { + const size = validateExecutableStat(stats, subject, projectedExecutable); + const bytes = Buffer.alloc(size); + let offset = 0; + while (offset < size) { + const count = fsModule.readSync(targetFd, bytes, offset, size - offset, offset); + if (!Number.isInteger(count) || count <= 0) { + throw execError('EIO', `${subject} changed while its executable image was read`); + } + offset += count; + } + return bytes; +} + +function readExecutablePathBytes(command) { + const hostPath = resolveExecModulePath(command); + const stats = fsModule.statSync(hostPath); + const size = validateExecutableStat( + stats, + String(command), + isProjectedCommandGuestPath(command), + ); + const bytes = fsModule.readFileSync(hostPath); + if (bytes.byteLength > size || bytes.byteLength > maxModuleFileBytes) { + throw execError( + 'EFBIG', + `${command} grew beyond limits.wasm.maxModuleFileBytes (${maxModuleFileBytes}) while being read; raise limits.wasm.maxModuleFileBytes if needed`, + ); + } + return bytes; +} + +function projectedCommandImageBytes(command) { + const name = path.posix.basename(String(command)); + if (!name || name === '.' || name === '/') return null; + for (const mapping of GUEST_PATH_MAPPINGS) { + if ( + !/^\/__secure_exec\/commands\/\d+$/u.test(mapping?.guestPath ?? '') && + mapping?.guestPath !== '/opt/agentos/bin' + ) continue; + const guestCandidate = path.posix.join(mapping.guestPath, name); + const hostCandidate = resolveExecModulePath(guestCandidate); + try { + const stats = fsModule.statSync(hostCandidate); + const size = validateExecutableStat(stats, guestCandidate, true); + const bytes = fsModule.readFileSync(hostCandidate); + traceHostProcess('projected-command-image', { + command, + guestCandidate, + hostCandidate, + byteLength: bytes.byteLength, + magic: Array.from(bytes.subarray(0, 4)), + }); + if (bytes.byteLength > size || bytes.byteLength > maxModuleFileBytes) { + throw execError('EFBIG', `${guestCandidate} grew beyond limits.wasm.maxModuleFileBytes while being read`); + } + return bytes; + } catch (error) { + if (error?.code !== 'ENOENT') throw error; + } + } + return null; +} + +function parseLinuxShebang(bytes) { + if (bytes.byteLength < 2 || bytes[0] !== 0x23 || bytes[1] !== 0x21) { + return null; + } + + const header = bytes.subarray(2, Math.min(bytes.byteLength, LINUX_BINPRM_BUF_SIZE)); + const newline = header.indexOf(0x0a); + let line = Buffer.from(newline >= 0 ? header.subarray(0, newline) : header).toString('utf8'); + line = line.replace(/[\t ]+$/u, ''); + const first = line.search(/[^\t ]/u); + if (first < 0) { + throw execError('ENOEXEC', 'shebang does not name an interpreter'); + } + line = line.slice(first); + const separator = line.search(/[\t ]/u); + if (newline < 0 && bytes.byteLength >= LINUX_BINPRM_BUF_SIZE && separator < 0) { + throw execError('ENOEXEC', 'shebang interpreter path exceeds the Linux header limit'); + } + if (separator < 0) { + return { interpreter: line, optionalArgument: null }; + } + const interpreter = line.slice(0, separator); + const optionalArgument = line.slice(separator).replace(/^[\t ]+|[\t ]+$/gu, ''); + return { + interpreter, + optionalArgument: optionalArgument.length > 0 ? optionalArgument : null, + }; +} + +function compileExecImage(bytes, subject, argv, interpreterDepth = 0) { + const shebang = parseLinuxShebang(bytes); + if (shebang) { + if (interpreterDepth >= LINUX_MAX_INTERPRETER_DEPTH) { + throw execError('ELOOP', `interpreter recursion for ${subject} exceeds the Linux limit`); + } + const interpreterArgv = [ + shebang.interpreter, + ...(shebang.optionalArgument === null ? [] : [shebang.optionalArgument]), + String(subject), + ...argv.slice(1), + ]; + return loadExecImageFromPath( + shebang.interpreter, + interpreterArgv, + interpreterDepth + 1, + ); + } + + try { + const binary = enforceMemoryLimit(bytes, maxMemoryPages); + return { module: new WebAssembly.Module(binary), argv }; + } catch (error) { + if ( + error instanceof WebAssembly.CompileError || + error?.message === 'module is not a valid WebAssembly binary' + ) { + throw execError('ENOEXEC', `${subject} is not a supported WebAssembly executable image`); + } + throw error; + } +} + +function loadExecImageFromPath(command, argv, interpreterDepth = 0) { + let bytes = readExecutablePathBytes(command); + traceHostProcess('exec-image-bytes', { + command, + byteLength: bytes.byteLength, + magic: Array.from(bytes.subarray(0, Math.min(16, bytes.byteLength))), + }); + if (bytes.equals(INTERNAL_KERNEL_COMMAND_STUB)) { + bytes = projectedCommandImageBytes(command); + if (bytes === null) { + throw execError('ENOENT', `registered command image for ${command} is unavailable`); + } + } + return compileExecImage( + bytes, + String(command), + argv, + interpreterDepth, + ); +} + +function executableTargetForHandle(handle) { + if (handle?.kind === 'guest-file' && typeof handle.targetFd === 'number') { + return handle.targetFd; + } + if (handle?.kind === 'kernel-fd' && typeof handle.targetFd === 'number') { + return handle.targetFd; + } + if (handle?.kind === 'passthrough' && typeof handle.ioFd === 'number') { + return handle.ioFd; + } + return null; +} + +function loadExecImageFromFd(fd, argv, closeFds) { + const descriptor = Number(fd) >>> 0; + const handle = lookupFdHandle(descriptor); + const targetFd = executableTargetForHandle(handle); + if (targetFd === null) { + throw execError('EBADF', `fexecve descriptor ${descriptor} is not an open file`); + } + const scriptRef = `/proc/self/fd/${descriptor}`; + const bytes = readExecutableFdBytes( + targetFd, + fsModule.fstatSync(targetFd), + scriptRef, + isProjectedCommandGuestPath(handle?.guestPath), + ); + if (parseLinuxShebang(bytes) && closeFds.includes(descriptor)) { + // Linux cannot hand a close-on-exec script descriptor to its interpreter, + // which subsequently opens the generated /proc/self/fd path. + throw execError('ENOENT', `${scriptRef} will be closed before its interpreter opens it`); + } + return { + ...compileExecImage(bytes, scriptRef, argv), + scriptRef, + }; +} function traceHostProcess(event, details) { const enabled = @@ -814,6 +1363,19 @@ function stdioFdIsKernelTty(fd) { // CPU (the guest thread blocks in Atomics.wait inside callSync). Keep each // slice under the 30s guest sync-RPC deadline. const KERNEL_WAIT_SLICE_MS = 10_000; +// A WASM descendant shares this runner's synchronous sidecar dispatch path. +// While one is active, return to the child event pump frequently enough to +// preserve the concurrent progress Linux gives separately scheduled processes. +const SPAWNED_CHILD_WAIT_SLICE_MS = 10; + +function hasActiveSpawnedChildren() { + for (const record of spawnedChildren.values()) { + if (record && typeof record.exitStatus !== 'number') { + return true; + } + } + return false; +} function readKernelStdinChunk(maxBytes) { const requestedLength = Math.max(1, Number(maxBytes) >>> 0); @@ -852,7 +1414,13 @@ if (prewarmOnly) { const WASI_PREOPENS = buildPreopens(); const WASI_PREOPEN_FD_BASE = 3; +// Patched wasi-libc tags descriptors returned by its absolute/cwd pathname +// resolver. The tag separates hidden WASI capability roots from the Linux +// guest descriptor namespace, where fd 3 is free to be closed or replaced. +const AGENTOS_HIDDEN_PREOPEN_FD_TAG = 0x40000000; +const AGENTOS_HIDDEN_PREOPEN_FD_MASK = 0x3fffffff; const WASI_PREOPEN_ENTRIES = Object.entries(WASI_PREOPENS); +const hiddenPreopenHandles = new Map(); const wasi = new WASI({ version: 'preview1', @@ -864,10 +1432,29 @@ const wasi = new WASI({ let instanceMemory = null; const wasiImport = { ...wasi.wasiImport }; -// node:wasi omits sock_shutdown; guest socket teardown happens via fd_close + host_net, so a -// success no-op is sufficient (needed for the cross-compiled X server / X clients). +// node:wasi omits sock_shutdown. Kernel-owned socketpair descriptors need a +// real half-close so readers observe Linux EOF; host-net transport teardown is +// still owned by net.destroy/fd_close. if (typeof wasiImport.sock_shutdown !== 'function') { - wasiImport.sock_shutdown = () => 0; + wasiImport.sock_shutdown = (fd, how) => { + try { + const numericFd = Number(fd) >>> 0; + const handle = lookupFdHandle(numericFd); + const kernelFd = handle?.kind === 'kernel-fd' + ? Number(handle.targetFd) >>> 0 + : delegateManagedFdRefCounts.has(numericFd) + ? numericFd + : null; + if (kernelFd == null) return WASI_ERRNO_SUCCESS; + const numericHow = Number(how) >>> 0; + const mode = numericHow === 1 ? 0 : numericHow === 2 ? 1 : numericHow === 3 ? 2 : null; + if (mode == null) return WASI_ERRNO_INVAL; + callSyncRpc('process.fd_socket_shutdown', [kernelFd, mode]); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + }; } const delegateClockTimeGet = typeof wasi.wasiImport.clock_time_get === 'function' @@ -897,6 +1484,10 @@ const delegateFdSync = typeof wasi.wasiImport.fd_sync === 'function' ? wasi.wasiImport.fd_sync.bind(wasi.wasiImport) : null; +const delegateFdDatasync = + typeof wasi.wasiImport.fd_datasync === 'function' + ? wasi.wasiImport.fd_datasync.bind(wasi.wasiImport) + : null; function decodeSignalMask(maskLo, maskHi) { const values = []; @@ -915,100 +1506,453 @@ function decodeSignalMask(maskLo, maskHi) { return values; } -function parseControlPipeFd(value) { - if (typeof value !== 'string' || value.trim() === '') { - return null; - } - - const parsed = Number.parseInt(value, 10); - return Number.isInteger(parsed) && parsed >= 3 ? parsed : null; -} - -function emitControlMessage(message) { - const emitSignalStateFallback = () => { - if ( - message?.type === 'signal_state' && - typeof process?.stdout?.write === 'function' - ) { - try { - process.stdout.write(`__AGENTOS_WASM_SIGNAL_STATE__:${JSON.stringify(message)}\n`); - } catch { - // Ignore signal-state bridge failures during teardown. - } +function encodeSignalMask(signals) { + let lo = 0; + let hi = 0; + for (const signal of signals) { + const numeric = Number(signal); + if (numeric >= 1 && numeric <= 32) { + lo = (lo | (1 << (numeric - 1))) >>> 0; + } else if (numeric >= 33 && numeric <= 64) { + hi = (hi | (1 << (numeric - 33))) >>> 0; } - }; - - if (CONTROL_PIPE_FD == null) { - emitSignalStateFallback(); - return; - } - - try { - writeSync(CONTROL_PIPE_FD, `${JSON.stringify(message)}\n`); - } catch { - emitSignalStateFallback(); } + return { lo, hi }; } -function isWorkspaceReadOnly() { - return permissionTier === 'read-only' || permissionTier === 'isolated'; +function spawnActionError(code, message) { + const error = new Error(message); + error.code = code; + return error; } -function hasWriteRights(rights) { - try { - return (BigInt(rights) & WASI_RIGHT_FD_WRITE) !== 0n; - } catch { - return true; +function spawnActionLimitError(message) { + if (typeof process?.stderr?.write === 'function') { + process.stderr.write(`[agentos] ${message}\n`); } + return spawnActionError('E2BIG', message); } -function hasReadRights(rights) { - try { - return (BigInt(rights) & WASI_RIGHT_FD_READ) !== 0n; - } catch { - return true; +function warnNearSpawnActionLimit(kind, current, limit) { + const countLimit = kind === 'actions'; + if ((countLimit ? warnedSpawnFileActions : warnedSpawnFileActionBytes) || + current < Math.ceil(limit * 0.9)) { + return; + } + if (countLimit) warnedSpawnFileActions = true; + else warnedSpawnFileActionBytes = true; + const setting = countLimit + ? 'limits.process.maxSpawnFileActions' + : 'limits.process.maxSpawnFileActionBytes'; + if (typeof process?.stderr?.write === 'function') { + process.stderr.write( + `[agentos] posix_spawn file-action ${kind} near ${setting} ` + + `(${current}/${limit}); raise ${setting} if needed\n`, + ); } } -function hasMutationOpenFlags(oflags) { - const normalized = Number(oflags) >>> 0; - return ( - (normalized & WASI_OFLAGS_CREAT) !== 0 || - (normalized & WASI_OFLAGS_EXCL) !== 0 || - (normalized & WASI_OFLAGS_TRUNC) !== 0 - ); -} - -function denyReadOnlyMutation() { - return WASI_ERRNO_ROFS; +function canonicalKernelFdForSpawnAction(fd) { + const numericFd = Number(fd) >>> 0; + const handle = lookupFdHandle(numericFd); + return handle?.kind === 'kernel-fd' ? Number(handle.targetFd) >>> 0 : numericFd; } -function guestPathForPreopenKey(key) { - if (key === '.') { - return HOST_FS_GUEST_CWD; - } - return path.posix.normalize(key); +function kernelCloexecFdsForCommit(closeFds) { + return closeFds.flatMap((fd) => { + const handle = lookupFdHandle(fd); + return handle?.kind === 'kernel-fd' ? [Number(handle.targetFd) >>> 0] : []; + }); } -function resolvePathOpenGuestPath(fd, pathPtr, pathLen) { - const target = readGuestString(pathPtr, pathLen); - if (target.startsWith('/')) { - return path.posix.normalize(target); - } - - const handle = lookupFdHandle(fd); - if (handle && typeof handle.guestPath === 'string') { - return path.posix.resolve(handle.guestPath, target); +function decodeSpawnActions(actionsPtr, actionsLen, initialCwd) { + const byteLength = Number(actionsLen) >>> 0; + if (byteLength > maxSpawnFileActionBytes) { + throw spawnActionLimitError( + `posix_spawn file-action payload is ${byteLength} bytes, exceeding ` + + `limits.process.maxSpawnFileActionBytes (${maxSpawnFileActionBytes}); ` + + 'raise limits.process.maxSpawnFileActionBytes if needed', + ); } - - const preopenIndex = (Number(fd) >>> 0) - WASI_PREOPEN_FD_BASE; - const preopen = WASI_PREOPEN_ENTRIES[preopenIndex]; - if (preopen) { - return path.posix.resolve(guestPathForPreopenKey(preopen[0]), target); + warnNearSpawnActionLimit('bytes', byteLength, maxSpawnFileActionBytes); + const bytes = readGuestBytes(actionsPtr, byteLength); + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + let scanOffset = 0; + let actionCount = 0; + while (scanOffset < bytes.byteLength) { + if (bytes.byteLength - scanOffset < 24) { + throw spawnActionError('EINVAL', 'truncated posix_spawn action header'); + } + const pathLength = view.getUint32(scanOffset + 20, true); + scanOffset += 24; + const pathEnd = scanOffset + pathLength; + if (pathEnd < scanOffset || pathEnd > bytes.byteLength) { + throw spawnActionError('EINVAL', 'truncated posix_spawn action path'); + } + scanOffset = pathEnd; + actionCount += 1; + if (actionCount > maxSpawnFileActions) { + throw spawnActionLimitError( + `posix_spawn has ${actionCount} file actions, exceeding ` + + `limits.process.maxSpawnFileActions (${maxSpawnFileActions}); ` + + 'raise limits.process.maxSpawnFileActions if needed', + ); + } + warnNearSpawnActionLimit('actions', actionCount, maxSpawnFileActions); } - - return null; -} + const stdio = [0, 1, 2]; + const closed = new Set(); + const actions = []; + const actionFdPaths = new Map(); + const actionFdSources = new Map(); + let offset = 0; + while (offset < bytes.byteLength) { + if (bytes.byteLength - offset < 24) { + throw spawnActionError('EINVAL', 'truncated posix_spawn action header'); + } + const command = view.getUint32(offset, true); + const fd = view.getInt32(offset + 4, true); + const sourceFd = view.getInt32(offset + 8, true); + const oflag = view.getInt32(offset + 12, true); + const mode = view.getUint32(offset + 16, true); + const pathLength = view.getUint32(offset + 20, true); + offset += 24; + const pathEnd = offset + pathLength; + if (pathEnd < offset || pathEnd > bytes.byteLength) { + throw spawnActionError('EINVAL', 'truncated posix_spawn action path'); + } + const actionPath = bytes.subarray(offset, pathEnd).toString('utf8'); + offset = pathEnd; + + if (command === 1) { + if (fd < 0) { + throw spawnActionError('EBADF', `posix_spawn close has invalid fd ${fd}`); + } + if ( + fd > 2 && + !actionFdPaths.has(fd) && + !actionFdSources.has(fd) && + !lookupFdHandle(fd) && + !hostNetSockets.has(fd) + ) { + throw spawnActionError('EBADF', `posix_spawn close references unopened fd ${fd}`); + } + closed.add(fd); + actionFdPaths.delete(fd); + actionFdSources.delete(fd); + if (fd <= 2) { + stdio[fd] = 0xffffffff; + } + actions.push({ + command, + guestFd: fd, + fd: canonicalKernelFdForSpawnAction(fd), + guestSourceFd: sourceFd, + sourceFd, + oflag, + mode, + path: '', + }); + continue; + } + if (command === 2) { + if (fd < 0 || sourceFd < 0 || closed.has(sourceFd)) { + throw spawnActionError('EBADF', 'posix_spawn dup2 references a closed fd'); + } + const source = sourceFd <= 2 ? stdio[sourceFd] : sourceFd; + if (source === 0xffffffff) { + throw spawnActionError('EBADF', 'posix_spawn dup2 references a closed fd'); + } + if ( + sourceFd > 2 && + !actionFdPaths.has(sourceFd) && + !actionFdSources.has(sourceFd) && + !lookupFdHandle(sourceFd) && + !hostNetSockets.has(sourceFd) + ) { + throw spawnActionError( + 'EBADF', + `posix_spawn dup2 references unopened fd ${sourceFd}`, + ); + } + if (fd <= 2) { + stdio[fd] = source; + } + closed.delete(fd); + if (actionFdPaths.has(sourceFd)) { + actionFdPaths.set(fd, actionFdPaths.get(sourceFd)); + actionFdSources.delete(fd); + } else if (actionFdSources.has(sourceFd)) { + actionFdPaths.delete(fd); + actionFdSources.set(fd, actionFdSources.get(sourceFd)); + } else { + actionFdPaths.delete(fd); + const sourceHandle = lookupFdHandle(sourceFd); + if (typeof sourceHandle?.guestPath === 'string') { + actionFdPaths.set(fd, sourceHandle.guestPath); + actionFdSources.delete(fd); + } else { + actionFdSources.set(fd, canonicalKernelFdForSpawnAction(sourceFd)); + } + } + actions.push({ + command, + guestFd: fd, + fd: canonicalKernelFdForSpawnAction(fd), + guestSourceFd: sourceFd, + sourceFd: canonicalKernelFdForSpawnAction(sourceFd), + oflag, + mode, + path: '', + }); + continue; + } + if (command === 3) { + if (fd < 0) { + throw spawnActionError('EBADF', `posix_spawn open has invalid fd ${fd}`); + } + if (actionPath.length === 0) { + throw spawnActionError('ENOENT', 'posix_spawn open path is empty'); + } + // Keep the guest pathname raw. Earlier chdir/fchdir actions can change + // its meaning through kernel symlinks, which only the sidecar can resolve. + closed.delete(fd); + actionFdPaths.set(fd, actionPath); + actionFdSources.delete(fd); + if (fd <= 2) { + stdio[fd] = fd; + } + actions.push({ + command, + guestFd: fd, + fd, + guestSourceFd: sourceFd, + sourceFd, + oflag, + mode, + path: actionPath, + }); + continue; + } + if (command === 4) { + if (actionPath.length === 0) { + throw spawnActionError('ENOENT', 'posix_spawn chdir path is empty'); + } + actions.push({ command, fd, sourceFd, oflag, mode, path: actionPath }); + continue; + } + if (command === 5) { + if (fd < 0 || closed.has(fd)) { + throw spawnActionError('EBADF', `posix_spawn fchdir has invalid fd ${fd}`); + } + if ( + !actionFdPaths.has(fd) && + !actionFdSources.has(fd) && + !lookupFdHandle(fd) && + !hostNetSockets.has(fd) + ) { + throw spawnActionError('EBADF', `posix_spawn fchdir references unopened fd ${fd}`); + } + actions.push({ + command, + guestFd: fd, + fd: canonicalKernelFdForSpawnAction(fd), + sourceFd, + oflag, + mode, + path: actionPath, + }); + continue; + } + if (command === 6) { + if (fd < 0) { + throw spawnActionError('EBADF', `posix_spawn closefrom has invalid fd ${fd}`); + } + // Snapshot the parent's live inheritable descriptors. Retained handles + // owned only by older children and Node-WASI's private backing table are + // deliberately excluded: neither is visible in the parent's fd table. + const inheritedGuestFds = new Set([ + ...syntheticFdEntries.keys(), + ...passthroughHandles.keys(), + ...delegateManagedFdRefCounts.keys(), + ...hostNetSockets.keys(), + ...actionFdPaths.keys(), + ...actionFdSources.keys(), + ...WASI_PREOPEN_ENTRIES.map((_, index) => WASI_PREOPEN_FD_BASE + index), + ]); + for (const guestFd of inheritedGuestFds) { + if (guestFd >= fd) { + closed.add(guestFd); + actionFdPaths.delete(guestFd); + actionFdSources.delete(guestFd); + } + } + for (let stdioFd = Math.max(fd, 0); stdioFd <= 2; stdioFd += 1) { + closed.add(stdioFd); + stdio[stdioFd] = 0xffffffff; + } + actions.push({ + command, + guestFd: fd, + fd, + guestSourceFd: sourceFd, + sourceFd, + oflag, + mode, + path: '', + closeFromGuestFds: [...inheritedGuestFds] + .filter((guestFd) => guestFd >= fd) + .sort((left, right) => left - right), + }); + continue; + } + throw spawnActionError('EINVAL', `unknown posix_spawn action opcode ${command}`); + } + return { stdio, cwd: initialCwd, actions }; +} + +function spawnActionsControlGuestFd(actions, fd) { + const guestFd = Number(fd) >>> 0; + return (actions ?? []).some( + (action) => { + const command = Number(action?.command); + const actionGuestFd = Number(action?.guestFd ?? action?.fd) >>> 0; + return ( + ([1, 2, 3].includes(command) && actionGuestFd === guestFd) || + (command === 6 && guestFd >= actionGuestFd) + ); + }, + ); +} + +function parseControlPipeFd(value) { + if (typeof value !== 'string' || value.trim() === '') { + return null; + } + + const parsed = Number.parseInt(value, 10); + return Number.isInteger(parsed) && parsed >= 3 ? parsed : null; +} + +function emitControlMessage(message) { + const emitSignalStateFallback = () => { + if ( + message?.type === 'signal_state' && + typeof process?.stdout?.write === 'function' + ) { + try { + process.stdout.write(`__AGENTOS_WASM_SIGNAL_STATE__:${JSON.stringify(message)}\n`); + } catch { + // Ignore signal-state bridge failures during teardown. + } + } + }; + + if (CONTROL_PIPE_FD == null) { + emitSignalStateFallback(); + return; + } + + try { + writeSync(CONTROL_PIPE_FD, `${JSON.stringify(message)}\n`); + } catch { + emitSignalStateFallback(); + } +} + +function isWorkspaceReadOnly() { + return permissionTier === 'read-only' || permissionTier === 'isolated'; +} + +function hasWriteRights(rights) { + try { + return (BigInt(rights) & WASI_RIGHT_FD_WRITE) !== 0n; + } catch { + return true; + } +} + +function hasReadRights(rights) { + try { + return (BigInt(rights) & WASI_RIGHT_FD_READ) !== 0n; + } catch { + return true; + } +} + +function hasMutationOpenFlags(oflags) { + const normalized = Number(oflags) >>> 0; + return ( + (normalized & WASI_OFLAGS_CREAT) !== 0 || + (normalized & WASI_OFLAGS_EXCL) !== 0 || + (normalized & WASI_OFLAGS_TRUNC) !== 0 + ); +} + +function kernelOpenFlagsFromWasi(oflags, rightsBase, fdflags, lookupflags) { + const wantsRead = hasReadRights(rightsBase); + const wantsWrite = hasWriteRights(rightsBase); + let flags = wantsWrite ? (wantsRead ? KERNEL_O_RDWR : KERNEL_O_WRONLY) : 0; + const normalizedOflags = Number(oflags) >>> 0; + const normalizedFdflags = Number(fdflags) >>> 0; + if ((normalizedOflags & WASI_OFLAGS_CREAT) !== 0) flags |= KERNEL_O_CREAT; + if ((normalizedOflags & WASI_OFLAGS_DIRECTORY) !== 0) flags |= KERNEL_O_DIRECTORY; + if ((normalizedOflags & WASI_OFLAGS_EXCL) !== 0) flags |= KERNEL_O_EXCL; + if ((normalizedOflags & WASI_OFLAGS_TRUNC) !== 0) flags |= KERNEL_O_TRUNC; + if ((normalizedFdflags & WASI_FDFLAGS_APPEND) !== 0) flags |= KERNEL_O_APPEND; + if ((normalizedFdflags & WASI_FDFLAGS_NONBLOCK) !== 0) flags |= KERNEL_O_NONBLOCK; + if (((Number(lookupflags) >>> 0) & WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) === 0) { + flags |= KERNEL_O_NOFOLLOW; + } + return flags; +} + +function denyReadOnlyMutation() { + return WASI_ERRNO_ROFS; +} + +function guestPathForPreopenKey(key) { + if (key === '.') { + return HOST_FS_GUEST_CWD; + } + return path.posix.normalize(key); +} + +function resolvePathOpenGuestPath(fd, pathPtr, pathLen) { + const target = readGuestString(pathPtr, pathLen); + if (target.startsWith('/')) { + return path.posix.normalize(target); + } + + const handle = lookupFdHandle(fd); + if (handle && typeof handle.guestPath === 'string') { + return path.posix.resolve(handle.guestPath, target); + } + if (handle?.kind === 'kernel-fd' && SIDECAR_MANAGED_PROCESS) { + try { + const base = callSyncRpc('process.fd_chdir_path', [Number(handle.targetFd) >>> 0]); + return path.posix.resolve(String(base), target); + } catch { + // The combined process.path_*_at RPC below returns the authoritative + // ENOTDIR/EBADF. Path resolution is also used by policy probes, which + // must not throw out of the WASI import and abort the entire runtime. + return null; + } + } + + const numericFd = Number(fd) >>> 0; + const preopenFd = + (numericFd & AGENTOS_HIDDEN_PREOPEN_FD_TAG) !== 0 + ? numericFd & AGENTOS_HIDDEN_PREOPEN_FD_MASK + : numericFd; + const preopenIndex = preopenFd - WASI_PREOPEN_FD_BASE; + const preopen = WASI_PREOPEN_ENTRIES[preopenIndex]; + if (preopen) { + return path.posix.resolve(guestPathForPreopenKey(preopen[0]), target); + } + + return null; +} function guestPathIsReadOnly(guestPath) { return GUEST_PATH_MAPPINGS.some( @@ -1160,19 +2104,59 @@ function fsOpenFlagForPathOpen(oflags, rightsBase, fdflags) { return 'r+'; } -function allocateSyntheticFd() { - let fd = nextSyntheticFd; - while ( +function runnerFdMappingInUse(fd) { + return ( syntheticFdEntries.has(fd) || passthroughHandles.has(fd) || - delegateManagedFdRefCounts.has(fd) + retainedSpawnOutputHandlesByFd.has(fd) || + retainedSyntheticHandlesByDisplayFd.has(fd) || + delegateManagedFdRefCounts.has(fd) || + hostNetSockets.has(fd) + ); +} + +function syntheticFdInUse(fd) { + return runnerFdMappingInUse(fd) || wasi?.fdTable?.has?.(fd) === true; +} + +function allocateSyntheticFd(minFd = nextSyntheticFd, reservedCapacity = false) { + if (!reservedCapacity && !hasRunnerOpenFdCapacity(1)) { + return null; + } + const numericMinimum = Number(minFd); + if ( + !Number.isSafeInteger(numericMinimum) || numericMinimum < 0 || + numericMinimum >= LINUX_GUEST_FD_LIMIT ) { + return null; + } + const descriptorLimit = Math.min(LINUX_GUEST_FD_LIMIT, rlimitNofileSoft); + let fd = Math.max(FIRST_SYNTHETIC_FD, numericMinimum); + while (fd < descriptorLimit && syntheticFdInUse(fd)) { fd += 1; } - nextSyntheticFd = fd + 1; + if (fd >= descriptorLimit) return null; + nextSyntheticFd = fd + 1 < descriptorLimit ? fd + 1 : FIRST_SYNTHETIC_FD; return fd; } +function allocateKernelGuestFd(minFd = 3) { + if (!hasRunnerOpenFdCapacity(1)) return null; + const numericMinimum = Number(minFd); + if ( + !Number.isSafeInteger(numericMinimum) || numericMinimum < 0 || + numericMinimum >= LINUX_GUEST_FD_LIMIT + ) { + return null; + } + const descriptorLimit = Math.min(LINUX_GUEST_FD_LIMIT, rlimitNofileSoft); + let fd = Math.max(3, numericMinimum); + while (fd < descriptorLimit && syntheticFdInUse(fd)) { + fd += 1; + } + return fd < descriptorLimit ? fd : null; +} + function openGuestFileForPathOpen(fd, pathPtr, pathLen, oflags, rightsBase, fdflags, openedFdPtr) { if (!pathOpenMayCreateTarget(oflags, rightsBase, fdflags)) { return null; @@ -1185,6 +2169,9 @@ function openGuestFileForPathOpen(fd, pathPtr, pathLen, oflags, rightsBase, fdfl if (typeof guestPath !== 'string') { return null; } + if (!hasRunnerOpenFdCapacity(1)) { + return WASI_ERRNO_MFILE; + } const append = (normalizedFdflags & WASI_FDFLAGS_APPEND) !== 0; const exclusive = (normalizedOflags & WASI_OFLAGS_EXCL) !== 0; @@ -1197,7 +2184,7 @@ function openGuestFileForPathOpen(fd, pathPtr, pathLen, oflags, rightsBase, fdfl fsOpenFlagForPathOpen(oflags, rightsBase, fdflags), 0o666, ); - const openedFd = allocateSyntheticFd(); + const openedFd = allocateSyntheticFd(nextSyntheticFd, true); syntheticFdEntries.set(openedFd, { kind: 'guest-file', targetFd, @@ -1211,6 +2198,61 @@ function openGuestFileForPathOpen(fd, pathPtr, pathLen, oflags, rightsBase, fdfl return writeGuestUint32(openedFdPtr, openedFd); } +function openProcSelfFdAlias(guestPath, oflags, rightsBase, lookupflags, openedFdPtr) { + const match = /^\/(?:proc\/self\/fd|dev\/fd)\/(\d+)$/u.exec(String(guestPath)); + if (!match) { + return null; + } + const sourceFd = Number(match[1]); + if (!Number.isSafeInteger(sourceFd) || sourceFd < 0) { + return WASI_ERRNO_NOENT; + } + const sourceHandle = lookupFdHandle(sourceFd); + const targetFd = executableTargetForHandle(sourceHandle); + if (targetFd === null) { + return WASI_ERRNO_NOENT; + } + if (((Number(lookupflags) >>> 0) & WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) === 0) { + return WASI_ERRNO_LOOP; + } + if ((Number(oflags) & WASI_OFLAGS_DIRECTORY) !== 0) { + return WASI_ERRNO_NOTDIR; + } + if (hasMutationOpenFlags(oflags) || hasWriteRights(rightsBase)) { + return WASI_ERRNO_ACCES; + } + if (!hasRunnerOpenFdCapacity(1)) { + return WASI_ERRNO_MFILE; + } + + sourceHandle.refCount += 1; + const openedFd = allocateSyntheticFd(nextSyntheticFd, true); + syntheticFdEntries.set(openedFd, { + kind: 'guest-file', + targetFd, + displayFd: openedFd, + refCount: 1, + open: true, + guestPath: String(guestPath), + position: 0, + append: false, + ownsTargetFd: false, + backingHandle: sourceHandle, + }); + return writeGuestUint32(openedFdPtr, openedFd); +} + +function kernelProcFdPathForGuestPath(guestPath) { + const match = /^\/proc\/self\/fd\/(\d+)$/u.exec(String(guestPath)); + if (!match) return guestPath; + const guestFd = Number(match[1]); + if (!Number.isSafeInteger(guestFd) || guestFd < 0) return guestPath; + const handle = lookupFdHandle(guestFd); + return handle?.kind === 'kernel-fd' + ? `/proc/self/fd/${Number(handle.targetFd) >>> 0}` + : guestPath; +} + function fsOpenNumericFlagsForManagedPath(rightsBase, fdflags) { const wantsRead = hasReadRights(rightsBase); const wantsWrite = hasWriteRights(rightsBase); @@ -1226,8 +2268,9 @@ function openManagedPathIoFd(guestPath, rightsBase, fdflags) { return null; } try { + const hostPath = resolveHostFsPath(guestPath) ?? guestPath; return fsModule.openSync( - guestPath, + hostPath, fsOpenNumericFlagsForManagedPath(rightsBase, fdflags), 0o666, ); @@ -1243,16 +2286,35 @@ function retainPathOpenDelegateFd(openedFdPtr, guestPath, fdflags, rightsBase) { try { const openedFd = new DataView(instanceMemory.buffer).getUint32(Number(openedFdPtr), true); + let retainedFd = openedFd; + if (openedFd > 2 && runnerFdMappingInUse(openedFd)) { + if (typeof delegateManagedFdRenumber !== 'function') { + return WASI_ERRNO_FAULT; + } + retainedFd = allocateSyntheticFd(openedFd + 1, true); + const renumberResult = delegateManagedFdRenumber(openedFd, retainedFd); + if (renumberResult !== WASI_ERRNO_SUCCESS) { + return renumberResult; + } + const writeResult = writeGuestUint32(openedFdPtr, retainedFd); + if (writeResult !== WASI_ERRNO_SUCCESS) { + return writeResult; + } + traceHostProcess('path-open-delegate-renumber', { + openedFd, + retainedFd, + }); + } const append = (Number(fdflags) & WASI_FDFLAGS_APPEND) !== 0; - retainDelegateFd(openedFd); - if (openedFd > 2 && !passthroughHandles.has(openedFd)) { + retainDelegateFd(retainedFd); + if (retainedFd > 2 && !passthroughHandles.has(retainedFd)) { const ioFd = openManagedPathIoFd(guestPath, rightsBase, fdflags); - closedPassthroughFds.delete(openedFd); - passthroughHandles.set(openedFd, { + closedPassthroughFds.delete(retainedFd); + passthroughHandles.set(retainedFd, { kind: 'passthrough', - targetFd: openedFd, + targetFd: retainedFd, ioFd, - displayFd: openedFd, + displayFd: retainedFd, refCount: 0, open: true, readOnly: @@ -1331,6 +2393,19 @@ function writeGuestFilestat(ptr, stats, filetype = WASI_FILETYPE_REGULAR_FILE) { } } +function wasiFiletypeFromStats(stats) { + if (typeof stats?.isDirectory === 'function' && stats.isDirectory()) { + return WASI_FILETYPE_DIRECTORY; + } + if (typeof stats?.isCharacterDevice === 'function' && stats.isCharacterDevice()) { + return WASI_FILETYPE_CHARACTER_DEVICE; + } + if (typeof stats?.isFile === 'function' && stats.isFile()) { + return WASI_FILETYPE_REGULAR_FILE; + } + return WASI_FILETYPE_UNKNOWN; +} + function writeGuestFdstat(ptr, filetype, flags, rightsBase, rightsInheriting) { if (!(instanceMemory instanceof WebAssembly.Memory)) { return WASI_ERRNO_FAULT; @@ -1354,14 +2429,111 @@ function mapSyntheticFsError(error) { case 'EBADF': return WASI_ERRNO_BADF; case 'EACCES': - case 'EPERM': return WASI_ERRNO_ACCES; + case 'EPERM': + return WASI_ERRNO_PERM; + case 'EROFS': + return WASI_ERRNO_ROFS; + case 'EEXIST': + return WASI_ERRNO_EXIST; + case 'ENOENT': + return WASI_ERRNO_NOENT; + case 'ENOEXEC': + return WASI_ERRNO_NOEXEC; case 'EINVAL': return WASI_ERRNO_INVAL; + case 'ENXIO': + return WASI_ERRNO_NXIO; + default: + return WASI_ERRNO_IO; + } +} + +function mapHostProcessError(error) { + switch (error?.code) { + case 'E2BIG': + return WASI_ERRNO_2BIG; + case 'EBADF': + return WASI_ERRNO_BADF; + case 'EACCES': + return WASI_ERRNO_ACCES; + case 'EADDRINUSE': + return WASI_ERRNO_ADDRINUSE; + case 'EADDRNOTAVAIL': + return WASI_ERRNO_ADDRNOTAVAIL; + case 'EAFNOSUPPORT': + return WASI_ERRNO_AFNOSUPPORT; + case 'EAGAIN': + case 'EWOULDBLOCK': + return WASI_ERRNO_AGAIN; + case 'EALREADY': + return WASI_ERRNO_ALREADY; + case 'EFBIG': + return WASI_ERRNO_FBIG; + case 'EEXIST': + return WASI_ERRNO_EXIST; + case 'ECONNREFUSED': + return WASI_ERRNO_CONNREFUSED; + case 'ECONNRESET': + return WASI_ERRNO_CONNRESET; + case 'EDEADLK': + return WASI_ERRNO_DEADLK; + case 'EDESTADDRREQ': + return WASI_ERRNO_DESTADDRREQ; + case 'EHOSTUNREACH': + return WASI_ERRNO_HOSTUNREACH; + case 'EINPROGRESS': + return WASI_ERRNO_INPROGRESS; + case 'EIO': + return WASI_ERRNO_IO; + case 'EILSEQ': + return WASI_ERRNO_ILSEQ; + case 'EISCONN': + return WASI_ERRNO_ISCONN; + case 'ELOOP': + return WASI_ERRNO_LOOP; + case 'ENOENT': + return WASI_ERRNO_NOENT; + case 'ENOEXEC': + return WASI_ERRNO_NOEXEC; + case 'ENOTDIR': + return WASI_ERRNO_NOTDIR; + case 'ENOTSUP': + return WASI_ERRNO_NOTSUP; + case 'EPERM': + return WASI_ERRNO_PERM; case 'EROFS': return WASI_ERRNO_ROFS; + case 'ESRCH': + return WASI_ERRNO_SRCH; + case 'ETIMEDOUT': + return WASI_ERRNO_TIMEDOUT; + case 'EINVAL': + return WASI_ERRNO_INVAL; + case 'EMFILE': + return WASI_ERRNO_MFILE; + case 'EMSGSIZE': + return WASI_ERRNO_MSGSIZE; + case 'ENAMETOOLONG': + return WASI_ERRNO_NAMETOOLONG; + case 'ENOBUFS': + return WASI_ERRNO_NOBUFS; + case 'ENETUNREACH': + return WASI_ERRNO_NETUNREACH; + case 'ENOTCONN': + return WASI_ERRNO_NOTCONN; + case 'ENOTSOCK': + return WASI_ERRNO_NOTSOCK; + case 'ENXIO': + return WASI_ERRNO_NXIO; + case 'EPIPE': + return WASI_ERRNO_PIPE; + case 'EPROTONOSUPPORT': + return WASI_ERRNO_PROTONOSUPPORT; default: - return WASI_ERRNO_FAULT; + return /command not found:/i.test(String(error?.message ?? error)) + ? WASI_ERRNO_NOENT + : WASI_ERRNO_FAULT; } } @@ -1373,7 +2545,12 @@ function seekGuestFileHandle(handle, offset, whence) { } else if (numericWhence === WASI_WHENCE_CUR) { base = BigInt(handle.position ?? 0); } else if (numericWhence === WASI_WHENCE_END) { - base = BigInt(Number(fsModule.fstatSync(handle.targetFd).size ?? 0)); + // Passthrough (read-only delegate) handles keep the real host fd in ioFd; + // targetFd is only a synthetic guest fd number and fstat'ing it reports + // size 0. Prefer ioFd so SEEK_END returns the true file size (e.g. mbedTLS + // sizing a CA bundle via fseek(SEEK_END)+ftell before reading it). + const sizeFd = typeof handle.ioFd === 'number' ? handle.ioFd : handle.targetFd; + base = BigInt(Number(fsModule.fstatSync(sizeFd).size ?? 0)); } else { return null; } @@ -1408,15 +2585,112 @@ function retainDelegateFd(fd) { delegateManagedFdRefCounts.set(numericFd, (delegateManagedFdRefCounts.get(numericFd) ?? 0) + 1); } -function releaseDelegateFd(fd) { - const numericFd = Number(fd) >>> 0; - const current = delegateManagedFdRefCounts.get(numericFd); - if (current == null) { - return false; +function registerKernelDelegateFd( + fd, + preferredGuestFd = null, + minimumGuestFd = 3, + shadowsInternalPreopen = false, +) { + const rawKernelFd = Number(fd); + if (!Number.isSafeInteger(rawKernelFd) || rawKernelFd < 0 || rawKernelFd > 0xffffffff) { + const error = new Error(`kernel returned invalid file descriptor ${fd}`); + error.code = 'EIO'; + throw error; } - if (current <= 1) { - delegateManagedFdRefCounts.delete(numericFd); - return true; + const kernelFd = rawKernelFd >>> 0; + const rejectRegistration = (error) => { + try { + callSyncRpc('process.fd_close', [kernelFd]); + } catch (closeError) { + error.message = `${error.message}; additionally failed to close kernel fd ${kernelFd}: ${closeError?.message ?? closeError}`; + } + throw error; + }; + let guestFd; + let replacesBootstrapStdio = false; + if (preferredGuestFd != null) { + const rawPreferredFd = Number(preferredGuestFd); + if ( + !Number.isSafeInteger(rawPreferredFd) || rawPreferredFd < 0 || + rawPreferredFd >= LINUX_GUEST_FD_LIMIT + ) { + const error = new Error(`invalid preferred guest file descriptor ${preferredGuestFd}`); + error.code = 'EINVAL'; + rejectRegistration(error); + } + guestFd = rawPreferredFd >>> 0; + // node:wasi pre-installs 0/1/2, but a spawned process may inherit kernel + // pipe/PTY descriptions at those exact Linux descriptor numbers. Let the + // kernel mapping shadow only those bootstrap stdio entries; every real + // runner mapping remains collision-protected. + const bootstrapStdioHandle = passthroughHandles.get(guestFd); + const shadowsRetainedBootstrapStdio = + guestFd <= 2 && + bootstrapStdioHandle == null && + closedPassthroughFds.has(guestFd) && + wasi?.fdTable?.has?.(guestFd) === true && + !syntheticFdEntries.has(guestFd) && + !retainedSpawnOutputHandlesByFd.has(guestFd); + const shadowsPrivatePreopen = + shadowsInternalPreopen || + shadowsRetainedBootstrapStdio || + (hiddenPreopenHandles.has(guestFd) && + (bootstrapStdioHandle == null || bootstrapStdioHandle.internalPreopen === true) && + !syntheticFdEntries.has(guestFd) && + !retainedSpawnOutputHandlesByFd.has(guestFd)); + replacesBootstrapStdio = + guestFd <= 2 && + bootstrapStdioHandle?.kind === 'passthrough' && + Number(bootstrapStdioHandle.targetFd) === guestFd && + Number(bootstrapStdioHandle.refCount) === 0; + if (syntheticFdInUse(guestFd) && !replacesBootstrapStdio && !shadowsPrivatePreopen) { + const error = new Error(`guest file descriptor ${guestFd} is already in use`); + error.code = 'EBUSY'; + rejectRegistration(error); + } + if (!replacesBootstrapStdio && !shadowsPrivatePreopen && !hasRunnerOpenFdCapacity(1)) { + const error = new Error('guest file descriptor limit reached'); + error.code = 'EMFILE'; + rejectRegistration(error); + } + } else { + guestFd = allocateKernelGuestFd(minimumGuestFd); + if (guestFd == null) { + const error = new Error('guest file descriptor limit reached'); + error.code = 'EMFILE'; + rejectRegistration(error); + } + } + + const handle = { + kind: 'kernel-fd', + targetFd: kernelFd, + displayFd: guestFd, + refCount: guestFd === kernelFd ? 0 : 1, + open: true, + }; + closedPassthroughFds.delete(guestFd); + if (replacesBootstrapStdio || passthroughHandles.get(guestFd)?.internalPreopen === true) { + passthroughHandles.delete(guestFd); + delegateManagedFdRefCounts.delete(guestFd); + } + if (guestFd === kernelFd) { + passthroughHandles.set(guestFd, handle); + } else { + syntheticFdEntries.set(guestFd, handle); + } + return guestFd; +} + +function releaseDelegateFd(fd) { + const numericFd = Number(fd) >>> 0; + const current = delegateManagedFdRefCounts.get(numericFd); + if (current == null) { + return false; + } + if (current <= 1) { + delegateManagedFdRefCounts.delete(numericFd); + return true; } delegateManagedFdRefCounts.set(numericFd, current - 1); return false; @@ -1424,6 +2698,9 @@ function releaseDelegateFd(fd) { function lookupFdHandle(fd) { const numericFd = Number(fd) >>> 0; + if ((numericFd & AGENTOS_HIDDEN_PREOPEN_FD_TAG) !== 0) { + return hiddenPreopenHandles.get(numericFd & AGENTOS_HIDDEN_PREOPEN_FD_MASK) ?? null; + } return ( syntheticFdEntries.get(numericFd) ?? retainedSpawnOutputHandlesByFd.get(numericFd)?.handle ?? @@ -1432,6 +2709,65 @@ function lookupFdHandle(fd) { ); } +function kernelFdMappingsForSpawn() { + const mappings = new Map(); + for (const [guestFd, handle] of [...passthroughHandles, ...syntheticFdEntries]) { + if (handle?.kind === 'kernel-fd' && handle.open !== false) { + mappings.set(Number(guestFd) >>> 0, Number(handle.targetFd) >>> 0); + } + } + if (mappings.size > configuredMaxOpenFds) { + const error = new Error( + `inherited kernel descriptor mappings exceed the ${configuredMaxOpenFds}-descriptor runtime limit`, + ); + error.code = 'EMFILE'; + throw error; + } + return [...mappings.entries()]; +} + +function hostNetFdsForSpawn() { + const descriptions = new Set(hostNetSockets.values()); + if (maxSockets != null && descriptions.size > maxSockets) { + const error = new Error( + `inherited host-network descriptions exceed limits.resources.maxSockets (${maxSockets}); ` + + 'raise limits.resources.maxSockets if needed', + ); + error.code = 'EMFILE'; + throw error; + } + const inherited = [...hostNetSockets.entries()].map(([guestFd, socket]) => ({ + guestFd: Number(guestFd) >>> 0, + closeOnExec: runnerCloexecFds.has(Number(guestFd) >>> 0), + socketId: socket.socketId ?? null, + serverId: socket.serverId ?? null, + udpSocketId: socket.udpSocketId ?? null, + metadata: { + domain: Number(socket.domain) >>> 0, + socketType: Number(socket.sockType) >>> 0, + protocol: Number(socket.protocol) >>> 0, + nonblocking: socket.nonblock === true, + recvTimeoutMs: socket.recvTimeoutMs ?? null, + bindOptions: socket.bindOptions ?? null, + localInfo: socket.localInfo ?? null, + localUnixAddress: socket.localUnixAddress ?? null, + localReservation: socket.localReservation ?? null, + remoteInfo: socket.remoteInfo ?? null, + remoteUnixAddress: socket.remoteUnixAddress ?? null, + listening: socket.listening === true, + }, + })); + if (inherited.length > configuredMaxOpenFds) { + const error = new Error( + `inherited host-network descriptors exceed limits.resources.maxOpenFds (${configuredMaxOpenFds}); ` + + 'raise limits.resources.maxOpenFds if needed', + ); + error.code = 'EMFILE'; + throw error; + } + return inherited; +} + function lookupSyntheticHandleByDisplayFd(fd, expectedKind = null) { const numericFd = Number(fd) >>> 0; for (const handle of syntheticFdEntries.values()) { @@ -1500,6 +2836,13 @@ function releaseFdHandle(handle) { if (handle.kind === 'passthrough') { handle.refCount = Math.max(0, handle.refCount - 1); + // Node's WASI preopens are capability roots used internally by libc to + // implement ordinary absolute-path opens. close(2)/closefrom(2) must make + // their guest descriptor numbers unusable without destroying those hidden + // roots, which do not exist as process FDs on native Linux. + if (handle.internalPreopen === true) { + return; + } if ( handle.refCount === 0 && handle.open && @@ -1511,9 +2854,7 @@ function releaseFdHandle(handle) { delegateManagedFdClose(handle.targetFd); } if (handle.refCount === 0 && handle.open && typeof handle.ioFd === 'number') { - try { - fsModule.closeSync(handle.ioFd); - } catch {} + fsModule.closeSync(handle.ioFd); handle.ioFd = null; } return; @@ -1523,7 +2864,24 @@ function releaseFdHandle(handle) { handle.refCount = Math.max(0, handle.refCount - 1); if (handle.refCount === 0 && handle.open) { handle.open = false; - fsModule.closeSync(handle.targetFd); + if (handle.ownsTargetFd === false) { + releaseFdHandle(handle.backingHandle); + } else { + fsModule.closeSync(handle.targetFd); + } + } + return; + } + + if (handle.kind === 'kernel-fd') { + handle.refCount = Math.max(0, handle.refCount - 1); + if ( + handle.refCount === 0 && + handle.open && + !passthroughHandleHasCanonicalMapping(handle) + ) { + handle.open = false; + callSyncRpc('process.fd_close', [Number(handle.targetFd) >>> 0]); } return; } @@ -1557,9 +2915,13 @@ function closeSyntheticFd(fd) { if (shouldRetainMapping) { retainSyntheticHandleByDisplayFd(handle); } - if (!shouldRetainMapping) { - syntheticFdEntries.delete(numericFd); - } + // The retained display mapping exists only so an already-spawned child can + // continue routing pipe data. It must never leave the descriptor visible to + // the parent after close(2). Mask the underlying Node-WASI/bootstrap slot as + // well; otherwise a later fstat(2) can fall through to a runtime-owned fd + // with the same number and make a second close appear to be the first. + syntheticFdEntries.delete(numericFd); + closedPassthroughFds.add(numericFd); releaseFdHandle(handle); if (shouldRetainMapping) { collectInactivePipeHandles(handle.pipe); @@ -1582,6 +2944,29 @@ function closePassthroughFd(fd) { return true; } +function forgetSidecarClosedKernelFd(fd) { + const numericFd = Number(fd) >>> 0; + const handle = lookupFdHandle(numericFd); + if (handle?.kind !== 'kernel-fd') { + return false; + } + if (syntheticFdEntries.get(numericFd) === handle) { + syntheticFdEntries.delete(numericFd); + } + if (passthroughHandles.get(numericFd) === handle) { + passthroughHandles.delete(numericFd); + } + handle.refCount = 0; + handle.open = false; + closedPassthroughFds.add(numericFd); + runnerCloexecFds.delete(numericFd); + traceHostProcess('exec-cloexec-kernel-fd-forgotten', { + fd: numericFd, + targetFd: Number(handle.targetFd) >>> 0, + }); + return true; +} + function rejectClosedPassthroughFd(fd) { return closedPassthroughFds.has(Number(fd) >>> 0); } @@ -1644,6 +3029,12 @@ function spawnStdinFdIsSyntheticPipe(fd) { return handle?.kind === 'pipe-read'; } +function spawnFdIsKernelBacked(fd) { + const numericFd = Number(fd) >>> 0; + return lookupFdHandle(numericFd)?.kind === 'kernel-fd' || + delegateManagedFdRefCounts.has(numericFd); +} + // Shell input redirects (`cmd < file`) reach proc_spawn as a plain file fd in // stdin_fd. The child cannot share that descriptor across the spawn boundary, // so the remaining file contents are materialized and written to the child's @@ -1776,6 +3167,149 @@ function writeBytesToGuestIovs(iovs, iovsLen, bytes) { return written >>> 0; } +function guestIovByteLength(iovs, iovsLen) { + if (!(instanceMemory instanceof WebAssembly.Memory)) { + throw new Error('WebAssembly memory is not available'); + } + + const view = new DataView(instanceMemory.buffer); + let total = 0; + for (let index = 0; index < (Number(iovsLen) >>> 0); index += 1) { + const entryOffset = (Number(iovs) >>> 0) + index * 8; + total += view.getUint32(entryOffset + 4, true); + } + return total >>> 0; +} + +function writeHostNetBytesToGuestIovs(iovs, iovsLen, bytes, nreadPtr) { + try { + return writeGuestUint32( + nreadPtr, + writeBytesToGuestIovs(iovs, iovsLen, bytes), + ); + } catch { + return WASI_ERRNO_FAULT; + } +} + +function readHostNetSocketToGuestIovs(socket, iovs, iovsLen, nreadPtr) { + try { + const requestedLength = guestIovByteLength(iovs, iovsLen); + if (requestedLength === 0) { + return writeGuestUint32(nreadPtr, 0); + } + + if (socket.nonblock) { + let queued = dequeueHostNetBytes(socket, requestedLength); + if (queued.length > 0) { + return writeHostNetBytesToGuestIovs(iovs, iovsLen, queued, nreadPtr); + } + if (socket.lastError) return mapHostProcessError(socket.lastError); + if (socket.readableEnded || socket.closed || !socket.socketId) { + return writeGuestUint32(nreadPtr, 0); + } + const result = readReadyHostNetSocket(socket, requestedLength, false, 0); + if (result?.kind === 'data' && result.bytes.length > 0) { + return writeHostNetBytesToGuestIovs(iovs, iovsLen, result.bytes, nreadPtr); + } + queued = dequeueHostNetBytes(socket, requestedLength); + if (queued.length > 0) { + return writeHostNetBytesToGuestIovs(iovs, iovsLen, queued, nreadPtr); + } + if (socket.readableEnded || socket.closed || !socket.socketId) { + return writeGuestUint32(nreadPtr, 0); + } + return WASI_ERRNO_AGAIN; + } + + const startedAt = Date.now(); + const receiveDeadline = socket.recvTimeoutMs == null + ? null + : startedAt + Math.max(0, socket.recvTimeoutMs); + const safeguardDeadline = startedAt + unixConnectTimeoutMs; + const warningAt = startedAt + Math.floor(unixConnectTimeoutMs * 0.8); + let warnedNearLimit = false; + while (true) { + if (dispatchPendingWasmSignals()) return WASI_ERRNO_INTR; + const queued = dequeueHostNetBytes(socket, requestedLength); + if (queued.length > 0) { + return writeHostNetBytesToGuestIovs(iovs, iovsLen, queued, nreadPtr); + } + if (socket.lastError) return mapHostProcessError(socket.lastError); + if (socket.readableEnded || socket.closed || !socket.socketId) { + return writeGuestUint32(nreadPtr, 0); + } + + const now = Date.now(); + if (receiveDeadline != null && now >= receiveDeadline) { + return WASI_ERRNO_AGAIN; + } + if (!warnedNearLimit && now >= warningAt) { + warnedNearLimit = true; + process.stderr.write( + `[agentos] blocking socket read is nearing limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms)\n`, + ); + } + if (now >= safeguardDeadline) { + process.stderr.write( + `[agentos] blocking socket read exceeded limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms); raise limits.resources.maxBlockingReadMs if needed\n`, + ); + return WASI_ERRNO_TIMEDOUT; + } + const nextDeadline = receiveDeadline == null + ? safeguardDeadline + : Math.min(receiveDeadline, safeguardDeadline); + // A blocking host-net read must not monopolize the VM dispatcher while a + // local child is the peer that will make it readable. Probe without an + // inline sidecar wait and drive the child between attempts, matching the + // existing kernel-fd and poll(2) descendant fairness paths. + const pumpsLocalChildren = hasActiveSpawnedChildren(); + const pollWaitMs = pumpsLocalChildren + ? 0 + : Math.max(0, Math.min(50, nextDeadline - now)); + const result = readReadyHostNetSocket(socket, requestedLength, false, pollWaitMs); + if (dispatchPendingWasmSignals()) return WASI_ERRNO_INTR; + if (result?.kind === 'data' && result.bytes.length > 0) { + return writeHostNetBytesToGuestIovs(iovs, iovsLen, result.bytes, nreadPtr); + } + if (pumpsLocalChildren) { + pumpSpawnedChildren(SPAWNED_CHILD_WAIT_SLICE_MS); + if (dispatchPendingWasmSignals()) return WASI_ERRNO_INTR; + } + if (receiveDeadline != null && Date.now() >= receiveDeadline) { + return WASI_ERRNO_AGAIN; + } + } + } catch (error) { + return mapHostProcessError(error); + } +} + +function writeHostNetSocketFromGuestIovs(socket, iovs, iovsLen, nwrittenPtr) { + if (!socket?.socketId || socket.closed) { + return WASI_ERRNO_BADF; + } + + let bytes; + try { + bytes = collectGuestIovBytes(iovs, iovsLen); + } catch { + return WASI_ERRNO_FAULT; + } + if (bytes.length === 0) { + return writeGuestUint32(nwrittenPtr, 0); + } + + try { + const written = Number( + callSyncRpc('net.write', [socket.socketId, bytes, socket.nonblock === true]), + ) >>> 0; + return writeGuestUint32(nwrittenPtr, written); + } catch (error) { + return mapHostProcessError(error); + } +} + function dequeuePipeBytes(pipe, maxBytes) { const requested = Math.max(0, Number(maxBytes) >>> 0); if (requested === 0 || pipe.chunks.length === 0) { @@ -1872,6 +3406,9 @@ function resolveChildInputPipe(record) { if (!record) { return null; } + if (record.directPosixStdin === true) { + return null; + } return ( record.stdinPipe ?? @@ -2002,14 +3539,52 @@ function closePipeConsumers(pipe) { return closed; } -function consumeSpawnOutputFd(fd) { - const numericFd = Number(fd) >>> 0; - const handle = syntheticFdEntries.get(numericFd); - if (handle?.kind === 'pipe-write' && handle.open) { - // Release the guest-owned write handle but retain the fd mapping so later - // child stdout/stderr events can still route into the synthetic pipe. - releaseFdHandle(handle); +function parseInitialHostNetFds(value, fdLimit, socketLimit) { + if (typeof value !== 'string' || value.length === 0) return []; + let parsed; + try { + parsed = JSON.parse(value); + } catch (error) { + throw new Error(`AGENTOS_WASM_INHERITED_HOSTNET_FDS must be JSON: ${error}`); + } + if (!Array.isArray(parsed)) { + throw new Error('AGENTOS_WASM_INHERITED_HOSTNET_FDS must be an array'); + } + if (socketLimit != null && parsed.length > socketLimit) { + throw new Error( + `AGENTOS_WASM_INHERITED_HOSTNET_FDS exceeds limits.resources.maxSockets (${socketLimit})`, + ); } + const guestFds = new Set(); + for (const entry of parsed) { + if (!entry || typeof entry !== 'object' || !Array.isArray(entry.guestFds)) { + throw new Error('inherited host-network entries require a guestFds array'); + } + const ids = [entry.socketId, entry.serverId, entry.udpSocketId] + .filter((id) => typeof id === 'string' && id.length > 0); + if (ids.length !== 1) { + throw new Error('inherited host-network entries require exactly one sidecar resource id'); + } + if (entry.guestFds.length === 0) { + throw new Error('inherited host-network entries require at least one guest fd'); + } + for (const rawFd of entry.guestFds) { + const fd = Number(rawFd); + if ( + !Number.isSafeInteger(fd) || fd < 0 || fd >= LINUX_GUEST_FD_LIMIT || + guestFds.has(fd) + ) { + throw new Error('inherited host-network entries contain invalid or duplicate guest fds'); + } + guestFds.add(fd); + } + } + if (guestFds.size > fdLimit) { + throw new Error( + `AGENTOS_WASM_INHERITED_HOSTNET_FDS exceeds limits.resources.maxOpenFds (${fdLimit})`, + ); + } + return parsed; } function routeChunkToFd(fd, bytes) { @@ -2148,12 +3723,18 @@ function routeChunkToDelegateFd(fd, bytes) { } } -function finalizeChildExit(record, exitCode, signal) { - const status = - signal == null - ? (Number(exitCode ?? 1) & 0xff) - : 128 + (signalNumberFromName(signal) & 0x7f); +function finalizeChildExit(record, exitCode, signal, coreDumped = false) { + const signalNumber = signal == null ? 0 : signalNumberFromName(signal) & 0x7f; + const rawExitCode = signalNumber === 0 ? Number(exitCode ?? 1) & 0xff : 0; + const status = signalNumber === 0 ? rawExitCode : 128 + signalNumber; + record.exitCode = rawExitCode; + record.exitSignal = signalNumber; + record.coreDumped = signalNumber !== 0 && coreDumped === true; record.exitStatus = status; + record.rawWaitStatus = + signalNumber === 0 + ? (rawExitCode << 8) >>> 0 + : (signalNumber | (record.coreDumped ? 0x80 : 0)) >>> 0; for (const fd of record.delegateRetainedFds ?? []) { if (releaseDelegateFd(fd) && typeof delegateManagedFdClose === 'function') { delegateManagedFdClose(fd); @@ -2312,22 +3893,25 @@ function createSyntheticChildRecord(result, stdinTarget, stdoutTarget, stderrTar stdoutPipe: null, stderrPipe: null, delegateRetainedFds: [], + exitCode: null, + exitSignal: null, exitStatus: null, + rawWaitStatus: null, + processGroup: 0, pendingEvents, synthetic: true, }; } -function emitSyntheticCommandOutput(record, stdoutFd, stderrFd, result) { +function emitSyntheticCommandOutput(record, result) { const syntheticOutputs = [ - ['stdout', stdoutFd, record.stdoutFd, result?.stdout], - ['stderr', stderrFd, record.stderrFd, result?.stderr], + ['stdout', record.stdoutFd, result?.stdout], + ['stderr', record.stderrFd, result?.stderr], ]; - for (const [stream, rawFd, targetFd, value] of syntheticOutputs) { + for (const [stream, targetFd, value] of syntheticOutputs) { const text = typeof value === 'string' ? value : ''; const pipe = registerPipeProducer(targetFd, record.childId, stream); - consumeSpawnOutputFd(rawFd); if (text.length > 0 && targetFd !== 0xffffffff) { routeChunkToFd(targetFd, Buffer.from(text, 'utf8')); } @@ -2348,6 +3932,57 @@ function reapSpawnedChild(record) { } } +function returnWaitedChild( + record, + retExitCodePtr, + retSignalPtr, + retPidPtr, + retCoreDumpedPtr, +) { + // A successful wait may reap the child that generated SIGCHLD. Linux runs + // the caught handler before returning the status without rewriting that + // successful wait result to EINTR. + dispatchPendingWasmSignals(); + if (writeGuestUint32(retExitCodePtr, record.exitCode ?? 0) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + if (writeGuestUint32(retSignalPtr, record.exitSignal ?? 0) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + if (writeGuestUint32(retCoreDumpedPtr, record.coreDumped ? 1 : 0) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + const writePidResult = writeGuestUint32(retPidPtr, record.pid); + if (writePidResult === WASI_ERRNO_SUCCESS) { + reapSpawnedChild(record); + } + return writePidResult; +} + +function returnLegacyWaitedChild(record, retStatusPtr, retPidPtr) { + dispatchPendingWasmSignals(); + if (writeGuestUint32(retStatusPtr, record.exitStatus ?? 0) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + const writePidResult = writeGuestUint32(retPidPtr, record.pid); + if (writePidResult === WASI_ERRNO_SUCCESS) { + reapSpawnedChild(record); + } + return writePidResult; +} + +function returnRawWaitedChild(record, retStatusPtr, retPidPtr) { + dispatchPendingWasmSignals(); + if (writeGuestUint32(retStatusPtr, record.rawWaitStatus ?? 0) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + const writePidResult = writeGuestUint32(retPidPtr, record.pid); + if (writePidResult === WASI_ERRNO_SUCCESS) { + reapSpawnedChild(record); + } + return writePidResult; +} + function processChildEvent(record, event) { if (!event) { return false; @@ -2388,24 +4023,10 @@ function processChildEvent(record, event) { typeof event.exitCode === 'number' ? Math.trunc(event.exitCode) : null; const signal = typeof event.signal === 'string' ? event.signal : null; - while (true) { - let trailingEvent = null; - try { - trailingEvent = pollChildEvent(record, 0); - } catch (error) { - if (isChildProcessGoneError(error)) { - break; - } - throw error; - } - if (!trailingEvent) { - break; - } - if (!processChildEvent(record, trailingEvent)) { - break; - } - } - finalizeChildExit(record, exitCode, signal); + // The child-process bridge emits exit only after both output streams have + // reached EOF. Do not infer EOF from one empty zero-time poll: that races + // delayed pipe delivery and can truncate the final output chunk. + finalizeChildExit(record, exitCode, signal, event.coreDumped === true); return true; } @@ -2498,13 +4119,29 @@ function pumpChildInputPipe(record, waitMs) { } function pumpSpawnedChildren(waitMs) { + const records = Array.from(spawnedChildren.values()).filter( + (record) => record && typeof record.exitStatus !== 'number', + ); + if (records.length === 0) { + return false; + } + + const boundedWaitMs = Math.max(0, Number(waitMs) || 0); + const startIndex = + boundedWaitMs > 0 ? nextBlockingChildPumpIndex % records.length : 0; + if (boundedWaitMs > 0) { + // One child receives the sweep's blocking quantum. Rotate that slot so a + // busy early child cannot permanently make later siblings zero-poll only. + nextBlockingChildPumpIndex = (startIndex + 1) % records.length; + } + let progressed = false; - for (const record of Array.from(spawnedChildren.values())) { - if (!record || typeof record.exitStatus === 'number') { - continue; - } + for (let offset = 0; offset < records.length; offset += 1) { + const record = records[(startIndex + offset) % records.length]; try { - const event = pollChildEvent(record, waitMs); + // Bound the whole sweep to one blocking poll instead of waitMs per + // child. Every other live child still receives a zero-time service pass. + const event = pollChildEvent(record, offset === 0 ? boundedWaitMs : 0); if (event) { processChildEvent(record, event); progressed = true; @@ -2519,6 +4156,15 @@ function pumpSpawnedChildren(waitMs) { return progressed; } +function pumpSpawnedChildrenOrWait(waitMs) { + const boundedWaitMs = Math.max(1, Number(waitMs) >>> 0); + const progressed = pumpSpawnedChildren(boundedWaitMs); + if (!progressed) { + Atomics.wait(syntheticWaitArray, 0, 0, boundedWaitMs); + } + return progressed; +} + function encodeGuestBytes(value) { return new TextEncoder().encode(String(value)); } @@ -2542,10 +4188,14 @@ function decodeNullSeparatedStrings(buffer) { return []; } - return buffer - .toString('utf8') - .split('\0') - .filter((entry) => entry.length > 0); + const entries = buffer.toString('utf8').split('\0'); + // The serializer terminates every string, including an empty string, with + // NUL. Remove exactly that framing terminator while preserving empty argv + // entries in every other position (Linux permits argv[i] == ""). + if (entries.at(-1) === '') { + entries.pop(); + } + return entries; } function parseSerializedEnv(buffer) { @@ -2652,6 +4302,14 @@ function readSyncRpcLine() { } } +// Standard (non-realtime) Linux signals coalesce while pending. A Set both +// matches that behavior and bounds guest-local pending state to the finite +// signal-number domain even if the host dispatch hook is spammed. +const pendingWasmSignals = new Set(initialWasmPendingSignals); +const wasmSignalRegistrations = new Map(); +const wasmBlockedSignals = new Set(initialWasmSignalMask); +let activeSpawnCallContext = null; + function callSyncRpc(method, args = []) { if ( globalThis.__agentOSSyncRpc && @@ -2659,7 +4317,7 @@ function callSyncRpc(method, args = []) { ) { const startedNs = __agentOSWasmNowNs(); try { - return globalThis.__agentOSSyncRpc.callSync(method, args); + return decodeSyncRpcValue(globalThis.__agentOSSyncRpc.callSync(method, args)); } finally { __agentOSWasiRecordSyncRpc(method, 'glue', startedNs); } @@ -2698,16 +4356,130 @@ function callSyncRpc(method, args = []) { } const hostNetSockets = new Map(); -let nextHostNetSocketFd = 0x40000000; +for (const inherited of initialHostNetDescriptions) { + const metadata = inherited.metadata && typeof inherited.metadata === 'object' + ? inherited.metadata + : {}; + const socket = { + domain: Number(metadata.domain) >>> 0, + sockType: Number(metadata.socketType) >>> 0, + protocol: Number(metadata.protocol) >>> 0, + bindOptions: metadata.bindOptions ?? null, + localInfo: metadata.localInfo ?? null, + localUnixAddress: metadata.localUnixAddress ?? null, + localReservation: metadata.localReservation ?? null, + remoteInfo: metadata.remoteInfo ?? null, + remoteUnixAddress: metadata.remoteUnixAddress ?? null, + listening: metadata.listening === true, + serverId: inherited.serverId ?? null, + socketId: inherited.socketId ?? null, + udpSocketId: inherited.udpSocketId ?? null, + pendingDatagram: null, + recvTimeoutMs: metadata.recvTimeoutMs ?? null, + readChunks: [], + pendingAccepts: [], + readableEnded: false, + closed: false, + lastError: null, + nonblock: metadata.nonblocking === true, + }; + for (const rawFd of inherited.guestFds) { + hostNetSockets.set(Number(rawFd) >>> 0, socket); + } +} +let warnedAboutOpenFdLimit = false; + +function runnerOpenFdSet() { + const openFds = new Set([0, 1, 2]); + for (const table of [ + syntheticFdEntries, + passthroughHandles, + retainedSpawnOutputHandlesByFd, + retainedSyntheticHandlesByDisplayFd, + delegateManagedFdRefCounts, + hostNetSockets, + wasi?.fdTable, + ]) { + if (!table || typeof table.keys !== 'function') continue; + for (const fd of table.keys()) openFds.add(Number(fd) >>> 0); + } + return openFds; +} + +function hasRunnerOpenFdCapacity(additionalFds) { + const openCount = runnerOpenFdSet().size; + const warnAt = Math.max(1, Math.floor(rlimitNofileSoft * 0.9)); + if (!warnedAboutOpenFdLimit && openCount >= warnAt) { + warnedAboutOpenFdLimit = true; + process.stderr.write( + `[agentos] WASM open fd usage ${openCount}/${rlimitNofileSoft} is near RLIMIT_NOFILE; raise the soft limit or limits.resources.maxOpenFds if needed\n`, + ); + } + return openCount + Math.max(0, Number(additionalFds) >>> 0) <= rlimitNofileSoft; +} +// Host-net socket fds must stay BELOW the guests' FD_SETSIZE (1024 in the +// wasi-libc sysroot): libcurl's select-based Curl_poll / curl_multi_fdset +// guard every socket with `s < FD_SETSIZE` and silently drop larger fds from +// the pollset, which stalls any transfer that has to WAIT for socket +// readiness (non-blocking TLS handshakes, >16 KiB uploads). Allocate the +// lowest free guest descriptor, as Linux does, while keeping it below both +// FD_SETSIZE and the process's current RLIMIT_NOFILE soft limit. +const HOST_NET_SOCKET_FD_MAX = 1023; const HOST_NET_TIMEOUT_SENTINEL = '__agentos_net_timeout__'; +const HOST_NET_MSG_PEEK = 0x0002; +const HOST_NET_MSG_DONTWAIT = 0x0040; +const HOST_NET_MSG_TRUNC = 0x0020; function getHostNetSocket(fd) { return hostNetSockets.get(Number(fd) >>> 0) ?? null; } +function validateHostNetSocketDescriptor(fd) { + const numericFd = Number(fd) >>> 0; + const socket = hostNetSockets.get(numericFd); + if (socket && !socket.closed) return WASI_ERRNO_SUCCESS; + if (lookupFdHandle(numericFd) || delegateManagedFdRefCounts.has(numericFd)) { + return WASI_ERRNO_NOTSOCK; + } + return WASI_ERRNO_BADF; +} + +function allocateHostNetSocketFd() { + if (maxSockets != null && hostNetSockets.size >= maxSockets) { + return null; + } + if (!hasRunnerOpenFdCapacity(1)) return null; + const openFds = runnerOpenFdSet(); + const descriptorLimit = Math.min(HOST_NET_SOCKET_FD_MAX + 1, rlimitNofileSoft); + for (let fd = FIRST_SYNTHETIC_FD; fd < descriptorLimit; fd += 1) { + if (!openFds.has(fd)) { + return fd; + } + } + return null; +} + +function allocateHostNetDuplicateFd(minimumFd = 0) { + const minimum = Number(minimumFd); + if (!Number.isSafeInteger(minimum) || minimum < 0 || minimum >= LINUX_GUEST_FD_LIMIT) { + return null; + } + if (!hasRunnerOpenFdCapacity(1)) return null; + const openFds = runnerOpenFdSet(); + const descriptorLimit = Math.min(LINUX_GUEST_FD_LIMIT, rlimitNofileSoft); + for (let fd = minimum; fd < descriptorLimit; fd += 1) { + if (!openFds.has(fd)) return fd; + } + return null; +} + function dequeueHostNetBytes(socket, maxBytes) { const requested = Math.max(0, Number(maxBytes) >>> 0); - if (requested === 0 || socket.readChunks.length === 0) { + if (requested === 0) { + return Buffer.alloc(0); + } + if (socket.readChunks.length === 0) { + socket.readableHint = false; return Buffer.alloc(0); } @@ -2727,29 +4499,109 @@ function dequeueHostNetBytes(socket, maxBytes) { remaining = 0; } + if (socket.readChunks.length === 0) { + socket.readableHint = false; + } + return Buffer.concat(parts); } -function pollHostNetSocket(socket, waitMs) { - if (!socket?.socketId || socket.closed) { - return null; +function peekHostNetBytes(socket, maxBytes) { + const requested = Math.max(0, Number(maxBytes) >>> 0); + if (requested === 0 || socket.readChunks.length === 0) { + return Buffer.alloc(0); } - const event = callSyncRpc('net.poll', [socket.socketId, Math.max(0, Number(waitMs) >>> 0)]); - if (!event) { - return null; + const parts = []; + let remaining = requested; + for (const chunk of socket.readChunks) { + if (remaining === 0) break; + const chunkLength = Math.min(chunk.length, remaining); + parts.push(chunk.subarray(0, chunkLength)); + remaining -= chunkLength; } - if (event.type === 'data') { - const chunk = decodeSyncRpcValue(event.data); - if (chunk?.length > 0) { - socket.readChunks.push(Buffer.from(chunk)); - } - return event; + return Buffer.concat(parts); +} + +function decodeHostNetSocketReadResult(result) { + if (result == null) { + return { kind: 'end' }; } - if (event.type === 'end' || event.type === 'close') { - socket.readableEnded = true; + if (result === HOST_NET_TIMEOUT_SENTINEL) { + return { kind: 'timeout' }; + } + + if (typeof result === 'string') { + if (result === HOST_NET_TIMEOUT_SENTINEL) { + return { kind: 'timeout' }; + } + return { kind: 'data', bytes: Buffer.from(result, 'base64') }; + } + + const decoded = decodeSyncRpcValue(result); + if (Buffer.isBuffer(decoded)) { + return { kind: 'data', bytes: decoded }; + } + if (decoded == null) { + return { kind: 'end' }; + } + if (decoded === HOST_NET_TIMEOUT_SENTINEL) { + return { kind: 'timeout' }; + } + return { kind: 'timeout' }; +} + +function readReadyHostNetSocket(socket, maxBytes = 64 * 1024, peek = false, waitMs = 0) { + if (!socket?.socketId || socket.closed) { + socket.readableEnded = true; + return null; + } + + const result = decodeHostNetSocketReadResult( + callSyncRpc('net.socket_read', [ + socket.socketId, + Math.max(0, Number(maxBytes) >>> 0), + peek === true, + Math.max(0, Number(waitMs) >>> 0), + ]), + ); + if (result.kind === 'data') { + socket.readableHint = peek === true && result.bytes.length > 0; + return result; + } + // poll(2) readiness is only a snapshot: another read may consume the data + // before recv(2), which then returns EAGAIN. Do not keep reporting POLLIN + // from a stale hint after a read attempt observed no bytes. + socket.readableHint = false; + if (result.kind === 'end') { + socket.readableEnded = true; + } + return result; +} + +function pollHostNetSocket(socket, waitMs) { + if (!socket?.socketId || socket.closed) { + return null; + } + + const event = callSyncRpc('net.poll', [socket.socketId, Math.max(0, Number(waitMs) >>> 0)]); + if (!event) { + return null; + } + + if (event.type === 'data') { + const chunk = decodeSyncRpcValue(event.data); + if (chunk?.length > 0) { + socket.readChunks.push(Buffer.from(chunk)); + } + socket.readableHint = true; + return event; + } + + if (event.type === 'end' || event.type === 'close') { + socket.readableEnded = true; if (event.type === 'close') { socket.closed = true; socket.socketId = null; @@ -2758,9 +4610,24 @@ function pollHostNetSocket(socket, waitMs) { } if (event.type === 'error') { - socket.lastError = String(event.message || event.code || 'socket error'); - socket.closed = true; - socket.socketId = null; + socket.lastError = { + code: String(event.code || 'EIO'), + message: String(event.message || event.code || 'socket error'), + }; + return event; + } + + if (event.readable === true || (Number(event.revents) & 0x001) !== 0) { + return readReadyHostNetSocket(socket); + } + + if (event.hangup === true) { + socket.readableEnded = true; + return event; + } + + if (event.error === true) { + socket.lastError = { code: 'EIO', message: 'socket error' }; return event; } @@ -2795,15 +4662,88 @@ function parseHostNetAddress(raw) { }; } +function parseHostNetUnixAddress(raw) { + const value = String(raw ?? ''); + if (value === 'unix-autobind') { + return { autobind: true }; + } + if (value.startsWith('unix-abstract:')) { + const abstractPathHex = value.slice('unix-abstract:'.length); + if (abstractPathHex.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(abstractPathHex)) { + throw new Error('invalid abstract host_net Unix address'); + } + return { abstractPathHex: abstractPathHex.toLowerCase() }; + } + if (value.startsWith('unix-path-hex:')) { + const pathHex = value.slice('unix-path-hex:'.length); + if (pathHex.length % 2 !== 0 || !/^[0-9a-f]*$/i.test(pathHex)) { + const error = new Error('invalid hexadecimal host_net Unix pathname'); + error.code = 'EINVAL'; + throw error; + } + const pathBytes = Buffer.from(pathHex, 'hex'); + const decoded = pathBytes.toString('utf8'); + if (!Buffer.from(decoded, 'utf8').equals(pathBytes)) { + const error = new Error('AF_UNIX pathname is not valid UTF-8'); + error.code = 'EILSEQ'; + throw error; + } + return { path: decoded }; + } + return value.startsWith('unix:') ? { path: value.slice(5) } : null; +} + +function formatHostNetUnixAddress(address) { + if (typeof address?.abstractPathHex === 'string') { + return `unix-abstract:${address.abstractPathHex.toLowerCase()}`; + } + if (typeof address?.path === 'string') { + return `unix:${address.path}`; + } + return 'unix-unnamed'; +} + +function hostNetUnixNodePath(address) { + if (typeof address?.abstractPathHex === 'string') { + return `\0${Buffer.from(address.abstractPathHex, 'hex').toString('utf8')}`; + } + return typeof address?.path === 'string' ? address.path : undefined; +} + +function unixAddressFromSidecarInfo(info, prefix) { + const abstractPathHex = info?.[`${prefix}AbstractPathHex`]; + if (typeof abstractPathHex === 'string') { + return { abstractPathHex }; + } + const path = info?.[`${prefix}Path`]; + return typeof path === 'string' ? { path } : null; +} + +function refreshHostNetUnixSocketInfo(socket) { + if (!socket?.socketId || Number(socket.domain) !== HOST_NET_AF_UNIX) return; + let info = callSyncRpc('net.socket_wait_connect', [socket.socketId]); + if (typeof info === 'string') info = JSON.parse(info); + const local = unixAddressFromSidecarInfo(info, 'local'); + const remote = unixAddressFromSidecarInfo(info, 'remote'); + socket.localUnixAddress = local + ? formatHostNetUnixAddress(local) + : 'unix-unnamed'; + socket.remoteUnixAddress = remote + ? formatHostNetUnixAddress(remote) + : 'unix-unnamed'; +} + function parseHostNetListenAddress(raw) { - const value = String(raw ?? '').trim(); - if (!value) { - throw new Error('host_net listen address is required'); + const value = String(raw ?? ''); + const unixAddress = parseHostNetUnixAddress(value); + if (unixAddress != null) { + return unixAddress; } - if (value.startsWith('/')) { - return { path: value }; + const inetValue = value.trim(); + if (!inetValue) { + throw new Error('host_net listen address is required'); } - const address = parseHostNetAddress(value); + const address = parseHostNetAddress(inetValue); return { host: address.host, port: address.port }; } @@ -2825,15 +4765,40 @@ function formatHostNetAddressInfo(info) { return `${address}:${port}`; } -const HOST_NET_AF_INET = 2; -const HOST_NET_AF_INET6 = 10; +// These are the AgentOS wasi-libc p1 ABI values, not Linux's numeric values. +// libc serializes Linux-compatible socket behavior over host_net, while the +// private guest/runner boundary retains wasi-libc's AF_INET=1, AF_INET6=2, +// AF_UNIX=3 assignments. +const HOST_NET_AF_INET = 1; +const HOST_NET_AF_INET6 = 2; +const HOST_NET_AF_UNIX = 3; const HOST_NET_SOCK_DGRAM = 5; +const HOST_NET_SOCK_STREAM = 6; const HOST_NET_SOCKET_TYPE_MASK = 0xf; +// wasi-libc : SOCK_NONBLOCK / SOCK_CLOEXEC bits OR'd into the +// socket(2) type argument (Linux-style socket(..., SOCK_STREAM | SOCK_NONBLOCK)). +const HOST_NET_SOCK_NONBLOCK = 0x4000; const HOST_NET_SOL_SOCKET = 1; const HOST_NET_WASI_SOL_SOCKET = 0x7fffffff; +const HOST_NET_SO_ERROR = 4; const HOST_NET_SO_RCVTIMEO_64 = 20; const HOST_NET_SO_RCVTIMEO_32 = 66; const HOST_NET_TIMEVAL_BYTES = 16; +// Performance/QoS socket options that guests may set but the host transport +// neither needs nor can honor per-socket: Node's net sockets already run +// with sensible defaults, and DSCP/traffic-class marking is not observable +// through the adapter. Accepted and ignored (values from the patched +// wasi-libc headers, matching Linux): setsockopt(2) succeeds, matching a +// Linux host where these are best-effort hints. OpenSSH sets all four on +// every connection (ssh_packet_set_tos / set_nodelay in opacket/misc) and +// treats failure as per-connection stderr noise. +const HOST_NET_SO_KEEPALIVE = 9; // SOL_SOCKET, socket(7) +const HOST_NET_IPPROTO_IP = 0; +const HOST_NET_IP_TOS = 1; // ip(7) +const HOST_NET_IPPROTO_TCP = 6; +const HOST_NET_TCP_NODELAY = 1; // tcp(7) +const HOST_NET_IPPROTO_IPV6 = 41; +const HOST_NET_IPV6_TCLASS = 67; // ipv6(7) function hostNetSocketBaseType(socket) { return Number(socket?.sockType ?? 0) & HOST_NET_SOCKET_TYPE_MASK; @@ -2843,6 +4808,35 @@ function hostNetSockoptKind(level, optname, optvalLen) { const normalizedLevel = Number(level) >>> 0; const normalizedOptname = Number(optname) >>> 0; const normalizedOptvalLen = Number(optvalLen) >>> 0; + // Accept-and-ignore QoS/keepalive/nagle hints (see constant block above). + // Option values are plain ints; accept any sane small buffer. + if (normalizedOptvalLen >= 1 && normalizedOptvalLen <= 16) { + if ( + (normalizedLevel === HOST_NET_SOL_SOCKET || + normalizedLevel === HOST_NET_WASI_SOL_SOCKET) && + normalizedOptname === HOST_NET_SO_KEEPALIVE + ) { + return 'ignore'; + } + if ( + normalizedLevel === HOST_NET_IPPROTO_TCP && + normalizedOptname === HOST_NET_TCP_NODELAY + ) { + return 'ignore'; + } + if ( + normalizedLevel === HOST_NET_IPPROTO_IP && + normalizedOptname === HOST_NET_IP_TOS + ) { + return 'ignore'; + } + if ( + normalizedLevel === HOST_NET_IPPROTO_IPV6 && + normalizedOptname === HOST_NET_IPV6_TCLASS + ) { + return 'ignore'; + } + } if ( normalizedLevel !== HOST_NET_SOL_SOCKET && normalizedLevel !== HOST_NET_WASI_SOL_SOCKET @@ -2898,6 +4892,68 @@ function ensureHostNetUdpSocket(socket) { return socket.udpSocketId; } +function pollHostNetDatagram(socket, waitMs) { + if (socket?.pendingDatagram) { + return socket.pendingDatagram; + } + if (!socket?.udpSocketId || socket.closed) { + return null; + } + const event = callSyncRpc('dgram.poll', [ + socket.udpSocketId, + Math.max(0, Number(waitMs) >>> 0), + ]); + if (event?.type === 'error') { + socket.lastError = event; + return event; + } + if (event?.type === 'message') { + // poll(2) must not consume a datagram. Keep exactly one bounded datagram + // until recv/recvfrom takes it, preserving Linux message boundaries. + socket.pendingDatagram = event; + return event; + } + return null; +} + +function hostNetDatagramBytes(event) { + if (event?.data && typeof event.data === 'object' && typeof event.data.base64 === 'string') { + return Buffer.from(event.data.base64, 'base64'); + } + return decodeFsBytesPayload(event?.data, 'host_net datagram data'); +} + +function receiveHostNetDatagramEvent(socket, flags) { + const recvFlags = Number(flags) >>> 0; + const nonblocking = socket.nonblock || (recvFlags & HOST_NET_MSG_DONTWAIT) !== 0; + const deadline = nonblocking + ? Date.now() + : socket.recvTimeoutMs == null + ? null + : Date.now() + Math.max(0, socket.recvTimeoutMs); + + while (true) { + const waitMs = nonblocking + ? 0 + : deadline == null + ? 50 + : Math.max(0, Math.min(50, deadline - Date.now())); + const event = pollHostNetDatagram(socket, waitMs); + if (event?.type === 'message') { + if ((recvFlags & HOST_NET_MSG_PEEK) === 0) { + socket.pendingDatagram = null; + } + return event; + } + if (event?.type === 'error' || socket.lastError) { + throw new Error(event?.message || socket.lastError?.message || 'UDP receive failed'); + } + if (nonblocking || (deadline != null && Date.now() >= deadline)) { + return null; + } + } +} + function signalNumberFromName(signal) { const mapped = LINUX_SIGNAL_NAMES.indexOf(String(signal)); if (mapped > 0) { @@ -2949,6 +5005,7 @@ const LINUX_SIGNAL_NAMES = [ 'SIGPWR', 'SIGSYS', ]; +const LINUX_MAX_SIGNAL_NUMBER = 64; function writeGuestBytes(ptr, maxLen, bytes, actualLenPtr) { if (!(instanceMemory instanceof WebAssembly.Memory)) { @@ -2969,7 +5026,8 @@ function writeGuestBytes(ptr, maxLen, bytes, actualLenPtr) { // Perform a single NON-BLOCKING accept on a listening host_net socket. On success it // registers the accepted connection as a new host_net socket and returns // { acceptedFd, address } (address is a Buffer: "host:port" for TCP, the peer path for -// AF_UNIX). Returns null when no connection is currently pending. Used by both net_poll +// AF_UNIX), or { error } when accepting the pending connection failed. Returns null when +// no connection is currently pending. Used by both net_poll // (to report accurate listener readiness) and net_accept (non-blocking semantics) so the // server never blocks inside accept() and starves already-connected clients. function tryHostNetAcceptOnce(socket) { @@ -2984,18 +5042,31 @@ function tryHostNetAcceptOnce(socket) { return null; } - const acceptedFd = nextHostNetSocketFd++; + const acceptedFd = allocateHostNetSocketFd(); + if (acceptedFd == null) { + callSyncRpc('net.destroy', [result.socketId]); + return { error: WASI_ERRNO_MFILE }; + } + const localUnix = unixAddressFromSidecarInfo(result.info, 'local') ?? + ((socket.bindOptions?.path != null || socket.bindOptions?.abstractPathHex != null) + ? socket.bindOptions + : null); + const remoteUnix = unixAddressFromSidecarInfo(result.info, 'remote'); hostNetSockets.set(acceptedFd, { domain: socket.domain, sockType: socket.sockType, protocol: socket.protocol, bindOptions: null, localInfo: normalizeHostNetAddressInfo(result.info?.localAddress, result.info?.localPort), + localUnixAddress: localUnix ? formatHostNetUnixAddress(localUnix) : null, localReservation: null, remoteInfo: normalizeHostNetAddressInfo(result.info?.remoteAddress, result.info?.remotePort), + remoteUnixAddress: remoteUnix ? formatHostNetUnixAddress(remoteUnix) : 'unix-unnamed', + listening: false, serverId: null, socketId: result.socketId, udpSocketId: null, + pendingDatagram: null, recvTimeoutMs: socket.recvTimeoutMs, readChunks: [], readableEnded: false, @@ -3010,11 +5081,28 @@ function tryHostNetAcceptOnce(socket) { port: result.info.remotePort, }), 'utf8'); } else { - address = Buffer.from(String(result.info?.remotePath ?? ''), 'utf8'); + address = Buffer.from(remoteUnix ? formatHostNetUnixAddress(remoteUnix) : 'unix-unnamed', 'utf8'); } return { acceptedFd, address }; } +function cleanupAcceptedHostNetSocket(accepted, reason) { + const acceptedFd = Number(accepted?.acceptedFd); + if (!Number.isInteger(acceptedFd)) return null; + const acceptedSocket = hostNetSockets.get(acceptedFd); + hostNetSockets.delete(acceptedFd); + if (!acceptedSocket?.socketId) return null; + try { + callSyncRpc('net.destroy', [acceptedSocket.socketId]); + return null; + } catch (error) { + process.stderr.write( + `[agentos] failed to destroy accepted socket during ${reason}: ${error instanceof Error ? error.message : String(error)}\n`, + ); + return error; + } +} + const hostNetImport = { // Poll an array of pollfd entries (8 bytes each: i32 fd, i16 events, i16 revents). // Connected sockets report POLLIN when data is queued; listening sockets report POLLIN @@ -3024,28 +5112,62 @@ const hostNetImport = { net_poll(fdsPtr, nfds, timeoutMs, retReadyPtr) { const n = Number(nfds) >>> 0; const base0 = Number(fdsPtr) >>> 0; - // The patched wasi sysroot's effective poll bits (bits/poll.h): POLLIN=POLLRDNORM=0x1, - // POLLOUT=POLLWRNORM=0x2 (NOT the 0x004 in legacy poll.h). Guests (X server + libxcb) use - // these, so net_poll must match or POLLOUT readiness is never reported and writers block. + // Match Linux's public poll(2) ABI in the owned sysroot exactly. const POLLIN = 0x001; - const POLLOUT = 0x002; + const POLLOUT = 0x004; const POLLERR = 0x008; const POLLHUP = 0x010; const POLLNVAL = 0x020; + const POLLRDNORM = 0x040; + const POLLWRNORM = 0x100; + const NORMAL_READ_EVENTS = POLLIN | POLLRDNORM; + const NORMAL_WRITE_EVENTS = POLLOUT | POLLWRNORM; const t = Number(timeoutMs) | 0; - const deadline = t < 0 ? null : Date.now() + Math.max(0, t); + const startedAt = Date.now(); + const deadline = t < 0 ? null : startedAt + Math.max(0, t); + const safeguardDeadline = startedAt + unixConnectTimeoutMs; + const safeguardApplies = deadline == null || safeguardDeadline < deadline; + const effectiveDeadline = safeguardApplies ? safeguardDeadline : deadline; + const warningAt = startedAt + Math.floor(unixConnectTimeoutMs * 0.8); + let warnedNearLimit = false; const kernelManagedStdio = KERNEL_STDIO_SYNC_RPC || (typeof process?.env?.AGENTOS_SANDBOX_ROOT === 'string' && process.env.AGENTOS_SANDBOX_ROOT.length > 0); try { while (true) { + if (safeguardApplies && !warnedNearLimit && Date.now() >= warningAt) { + warnedNearLimit = true; + process.stderr.write( + `[agentos] blocking poll is nearing limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms)\n`, + ); + } + if (dispatchPendingWasmSignals()) { + if (writeGuestUint32(retReadyPtr, 0) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + return WASI_ERRNO_INTR; + } + // A spawned WASM child is serviced through child_process.poll_event. + // Drive it while this process waits on inherited kernel pipes; otherwise + // parent poll(2) can starve the child that must make those pipes ready. + pumpSpawnedChildren(0); + // Child exit cleanup may have queued SIGCHLD while the poll-event RPC + // above was in flight. Drain again before observing fd readiness so an + // unblocked caught signal interrupts ppoll(2), as it does on Linux. + if (dispatchPendingWasmSignals()) { + if (writeGuestUint32(retReadyPtr, 0) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + return WASI_ERRNO_INTR; + } const view = new DataView(instanceMemory.buffer); let ready = 0; // fds the kernel owns (PTY/pipe stdio in sidecar-managed mode): their readiness // comes from a batched __kernel_poll below, which doubles as the wait slice. const kernelTargets = []; const kernelEntries = []; + let hasHostNetWaitTarget = false; for (let i = 0; i < n; i++) { const base = base0 + i * 8; const fd = view.getInt32(base, true); @@ -3054,27 +5176,67 @@ const hostNetImport = { const socket = getHostNetSocket(fd); const handle = fd >= 0 ? lookupFdHandle(fd >>> 0) : undefined; if (socket && !socket.closed) { + hasHostNetWaitTarget = true; if (socket.serverId) { - if (events & POLLIN) { + if (events & NORMAL_READ_EVENTS) { // Report the listener readable only when a connection is actually pending. if (!socket.pendingAccepts) socket.pendingAccepts = []; if (socket.pendingAccepts.length === 0) { const accepted = tryHostNetAcceptOnce(socket); if (accepted) socket.pendingAccepts.push(accepted); } - if (socket.pendingAccepts.length > 0) revents |= POLLIN; + if (socket.pendingAccepts.length > 0) { + revents |= events & NORMAL_READ_EVENTS; + } } } else if (socket.socketId) { - if (events & POLLIN && socket.readChunks && socket.readChunks.length > 0) { - revents |= POLLIN; + // poll(2) snapshots every target before returning. Probe the + // sidecar-backed socket without waiting even when another fd is + // already ready; otherwise a permanently ready kernel target + // (for example stdin POLLHUP after Git finishes writing to ssh) + // can starve the socket's queued exit-status/EOF forever. + if ( + (events & NORMAL_READ_EVENTS) !== 0 && + !socket.readableEnded && + !socket.lastError && + (!socket.readChunks || socket.readChunks.length === 0) && + socket.readableHint !== true + ) { + pollHostNetSocket(socket, 0); + } + if (events & NORMAL_READ_EVENTS && ( + (socket.readChunks && socket.readChunks.length > 0) || socket.readableHint === true + )) { + revents |= events & NORMAL_READ_EVENTS; } - if (events & POLLOUT) revents |= POLLOUT; + // poll(2) reports peer shutdown as POLLHUP even when it was not + // requested, and a read after the queued data drains must return + // EOF without blocking. OpenSSH waits on this transition before + // exiting after the remote command closes its connection. + // https://man7.org/linux/man-pages/man2/poll.2.html + if (socket.readableEnded) { + revents |= POLLHUP; + if (events & NORMAL_READ_EVENTS) { + revents |= events & NORMAL_READ_EVENTS; + } + } + if (socket.lastError) revents |= POLLERR; + revents |= events & NORMAL_WRITE_EVENTS; + } else if (socket.udpSocketId) { + if ( + events & NORMAL_READ_EVENTS && + pollHostNetDatagram(socket, 0)?.type === 'message' + ) { + revents |= events & NORMAL_READ_EVENTS; + } + if (socket.lastError) revents |= POLLERR; + revents |= events & NORMAL_WRITE_EVENTS; } } else if (handle?.kind === 'pipe-read') { - if (events & POLLIN) { + if (events & NORMAL_READ_EVENTS) { pumpPipeProducers(handle.pipe, 0); if (handle.pipe.chunks.length > 0) { - revents |= POLLIN; + revents |= events & NORMAL_READ_EVENTS; } else if ( handle.pipe.writeHandleCount === 0 && handle.pipe.producers.size === 0 @@ -3083,29 +5245,36 @@ const hostNetImport = { } } } else if (handle?.kind === 'pipe-write') { - if (events & POLLOUT) revents |= POLLOUT; - } else if ( - fd >= 0 && - fd <= 2 && - kernelManagedStdio && - (!handle || (handle.kind === 'passthrough' && handle.targetFd === fd)) - ) { - // Kernel-managed stdio (PTY slave / stdio pipes): ask the kernel, like a - // native poll(2) on the terminal fd. + revents |= events & NORMAL_WRITE_EVENTS; + } else if (handle?.kind === 'kernel-fd' || (fd >= 0 && kernelManagedStdio && ( + (!handle && fd <= 2) || + (handle?.kind === 'passthrough' && Number(handle.targetFd) >= 0 && + Number(handle.targetFd) <= 2) + ))) { + // poll(2): readiness means the requested operation will not block. + // https://man7.org/linux/man-pages/man2/poll.2.html + // Kernel-managed stdio (PTY slave / stdio pipes), including dup'd + // aliases: ask the kernel instead of treating a high alias like a + // regular file that is always ready. A false POLLIN here makes a + // guest block on an empty stdin pipe before it services another + // ready fd (for example OpenSSH flushing an exec request). + const kernelFd = handle?.kind === 'kernel-fd' || handle?.kind === 'passthrough' + ? Number(handle.targetFd) >>> 0 + : fd; kernelTargets.push({ - fd, + fd: kernelFd, events: - ((events & POLLIN) !== 0 ? KERNEL_POLLIN : 0) | - ((events & POLLOUT) !== 0 ? KERNEL_POLLOUT : 0), + ((events & NORMAL_READ_EVENTS) !== 0 ? KERNEL_POLLIN : 0) | + ((events & NORMAL_WRITE_EVENTS) !== 0 ? KERNEL_POLLOUT : 0), }); - kernelEntries.push({ base, fd, events }); + kernelEntries.push({ base, fd, kernelFd, events }); } else if (handle) { // Regular files / other VFS-backed fds: always ready, as on Linux. - revents |= events & (POLLIN | POLLOUT); + revents |= events & (NORMAL_READ_EVENTS | NORMAL_WRITE_EVENTS); } else if (fd >= 0 && fd <= 2) { // Non-kernel-managed stdio (plain runner stdio): report requested // readiness rather than blocking a guest forever on fds we cannot wait on. - revents |= events & (POLLIN | POLLOUT); + revents |= events & (NORMAL_READ_EVENTS | NORMAL_WRITE_EVENTS); } else if (fd >= 0) { revents |= POLLNVAL; } @@ -3115,27 +5284,42 @@ const hostNetImport = { if (kernelTargets.length > 0) { // If something is already ready (or this is a non-blocking poll), probe the - // kernel without waiting; otherwise let the kernel wait one slice for us. - const remaining = deadline == null ? Infinity : deadline - Date.now(); + // kernel without waiting. Mixed host-net + kernel polls must also keep + // this probe nonblocking: __kernel_poll cannot wake for a host socket, + // so sleeping here starves each queued SSH packet for a full 10s slice. + // The socket pump below supplies the bounded wait in that case. + const remaining = effectiveDeadline == null + ? Infinity + : effectiveDeadline - Date.now(); + const maxSliceMs = hasActiveSpawnedChildren() + ? SPAWNED_CHILD_WAIT_SLICE_MS + : KERNEL_WAIT_SLICE_MS; const sliceMs = - ready > 0 || t === 0 + ready > 0 || t === 0 || hasHostNetWaitTarget ? 0 - : Math.max(0, Math.min(KERNEL_WAIT_SLICE_MS, remaining)); + : Math.max(0, Math.min(maxSliceMs, remaining)); let response = null; try { response = callSyncRpc('__kernel_poll', [kernelTargets, sliceMs]); - } catch { - response = null; + } catch (error) { + traceHostProcess('kernel-poll-error', { + message: error instanceof Error ? error.message : String(error), + }); + return mapHostProcessError(error); } const responseEntries = Array.isArray(response?.fds) ? response.fds : []; for (const entry of kernelEntries) { const responseEntry = responseEntries.find( - (item) => (Number(item?.fd) >>> 0) === (entry.fd >>> 0), + (item) => (Number(item?.fd) >>> 0) === (entry.kernelFd >>> 0), ); const kernelRevents = Number(responseEntry?.revents) >>> 0; let revents = 0; - if (kernelRevents & KERNEL_POLLIN) revents |= POLLIN & entry.events; - if (kernelRevents & KERNEL_POLLOUT) revents |= POLLOUT & entry.events; + if (kernelRevents & KERNEL_POLLIN) { + revents |= NORMAL_READ_EVENTS & entry.events; + } + if (kernelRevents & KERNEL_POLLOUT) { + revents |= NORMAL_WRITE_EVENTS & entry.events; + } if (kernelRevents & KERNEL_POLLERR) revents |= POLLERR; if (kernelRevents & KERNEL_POLLHUP) revents |= POLLHUP; new DataView(instanceMemory.buffer).setUint16(entry.base + 6, revents, true); @@ -3144,27 +5328,51 @@ const hostNetImport = { } if (ready > 0 || t === 0 || (deadline != null && Date.now() >= deadline)) { - new DataView(instanceMemory.buffer).setUint32(Number(retReadyPtr) >>> 0, ready >>> 0, true); + if (dispatchPendingWasmSignals()) { + if (writeGuestUint32(retReadyPtr, 0) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + return WASI_ERRNO_INTR; + } + if (writeGuestUint32(retReadyPtr, ready) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } return 0; } + if (safeguardApplies && Date.now() >= safeguardDeadline) { + process.stderr.write( + `[agentos] blocking poll exceeded limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms); raise limits.resources.maxBlockingReadMs if needed\n`, + ); + if (writeGuestUint32(retReadyPtr, 0) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + return WASI_ERRNO_TIMEDOUT; + } let pumpedSocket = false; const v2 = new DataView(instanceMemory.buffer); for (let i = 0; i < n; i++) { const fd = v2.getInt32(base0 + i * 8, true); const s = getHostNetSocket(fd); - if (s && s.socketId && !s.serverId) { - pollHostNetSocket(s, 10); - pumpedSocket = true; + if (s && !s.serverId) { + if (s.socketId) { + pollHostNetSocket(s, 10); + pumpedSocket = true; + } else if (s.udpSocketId) { + pollHostNetDatagram(s, 10); + pumpedSocket = true; + } } } if (kernelTargets.length === 0 && !pumpedSocket) { // Nothing to wait on except time: sleep a slice instead of hot-spinning. - const remaining = deadline == null ? Infinity : deadline - Date.now(); + const remaining = effectiveDeadline == null + ? Infinity + : effectiveDeadline - Date.now(); Atomics.wait(syntheticWaitArray, 0, 0, Math.max(1, Math.min(10, remaining))); } } - } catch (_e) { - return WASI_ERRNO_FAULT; + } catch (error) { + return mapHostProcessError(error); } }, net_socket(domain, sockType, protocol, retFdPtr) { @@ -3172,26 +5380,50 @@ const hostNetImport = { const numericDomain = Number(domain) >>> 0; const numericType = Number(sockType) >>> 0; const numericProtocol = Number(protocol) >>> 0; + if ( + numericDomain === HOST_NET_AF_UNIX && + (numericType & HOST_NET_SOCKET_TYPE_MASK) !== HOST_NET_SOCK_STREAM + ) { + return WASI_ERRNO_NOTSUP; + } - const fd = nextHostNetSocketFd++; + const fd = allocateHostNetSocketFd(); + if (fd == null) { + return WASI_ERRNO_MFILE; + } hostNetSockets.set(fd, { domain: numericDomain, sockType: numericType, protocol: numericProtocol, bindOptions: null, localInfo: null, + localUnixAddress: numericDomain === HOST_NET_AF_UNIX ? 'unix-unnamed' : null, localReservation: null, remoteInfo: null, + remoteUnixAddress: null, + listening: false, serverId: null, socketId: null, udpSocketId: null, + pendingDatagram: null, recvTimeoutMs: null, readChunks: [], readableEnded: false, closed: false, lastError: null, + // Honor Linux-style socket(..., type | SOCK_NONBLOCK): guests like + // libcurl rely on O_NONBLOCK semantics (EAGAIN instead of blocking + // reads) to interleave send/recv on one connection. Dropping this bit + // deadlocks any upload larger than one TLS record: curl checks for an + // early server response mid-upload, and a blocking recv() waits on a + // server that is itself waiting for the rest of the request body. + nonblock: (numericType & HOST_NET_SOCK_NONBLOCK) !== 0, }); - return writeGuestUint32(retFdPtr, fd); + const copyout = writeGuestUint32(retFdPtr, fd); + if (copyout !== WASI_ERRNO_SUCCESS) { + hostNetSockets.delete(fd); + } + return copyout; } catch { return WASI_ERRNO_FAULT; } @@ -3207,7 +5439,13 @@ const hostNetImport = { net_connect(fd, addrPtr, addrLen) { const socket = getHostNetSocket(fd); if (!socket) { - return WASI_ERRNO_BADF; + return validateHostNetSocketDescriptor(fd); + } + if (socket.socketId != null) { + return WASI_ERRNO_ISCONN; + } + if (socket.listening === true) { + return WASI_ERRNO_INVAL; } try { @@ -3216,33 +5454,67 @@ const hostNetImport = { // padding; cut at the first NUL so the unix path is clean before classification. const nulAt = rawAddr.indexOf(String.fromCharCode(0)); if (nulAt >= 0) rawAddr = rawAddr.slice(0, nulAt); - rawAddr = rawAddr.trim(); - // AF_UNIX path connect (e.g. X11 /tmp/.X11-unix/X0): the C library passes the - // raw sun_path, the same '/'-prefixed string net_bind/net_listen receive. Route it - // to the sidecar's path-based net.connect, which dials the host-backed unix socket - // the listener bound under the VM sandbox root, so two guests in one VM can talk. - if (rawAddr.startsWith('/')) { + // AF_UNIX addresses use an explicit wire prefix so relative paths and paths containing ':' + // cannot be mistaken for TCP host:port strings. + const unixAddress = parseHostNetUnixAddress(rawAddr); + if (unixAddress != null) { + if (Number(socket.domain) !== HOST_NET_AF_UNIX) return WASI_ERRNO_AFNOSUPPORT; + if (unixAddress.autobind === true) { + return WASI_ERRNO_INVAL; + } + const request = { ...unixAddress }; + if (socket.serverId) { + request.boundServerId = socket.serverId; + } + const deadline = Date.now() + unixConnectTimeoutMs; + const warningAt = Date.now() + Math.floor(unixConnectTimeoutMs * 0.8); + let warnedNearLimit = false; let result; - try { - result = callSyncRpc('net.connect', [{ path: rawAddr }]); - } catch (e) { - try { process.stderr.write('[host_net] connect ' + rawAddr + ' failed: ' + (e && e.message ? e.message : String(e)) + '\n'); } catch (_) {} - return WASI_ERRNO_FAULT; + for (;;) { + if (dispatchPendingWasmSignals()) return WASI_ERRNO_INTR; + try { + result = callSyncRpc('net.connect', [request]); + break; + } catch (error) { + if (mapHostProcessError(error) !== WASI_ERRNO_AGAIN) throw error; + if (socket.nonblock) return WASI_ERRNO_AGAIN; + if (!warnedNearLimit && Date.now() >= warningAt) { + warnedNearLimit = true; + process.stderr.write( + `[agentos] blocking AF_UNIX connect is nearing limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms)\n`, + ); + } + if (Date.now() >= deadline) { + process.stderr.write( + `[agentos] blocking AF_UNIX connect exceeded limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms); raise limits.resources.maxBlockingReadMs if needed\n`, + ); + return WASI_ERRNO_TIMEDOUT; + } + pumpSpawnedChildrenOrWait(Math.min(10, Math.max(1, deadline - Date.now()))); + } } if (!result || typeof result.socketId !== 'string') { - try { process.stderr.write('[host_net] ' + rawAddr + ' returned no socketId\n'); } catch (_) {} return WASI_ERRNO_FAULT; } socket.socketId = result.socketId; socket.localInfo = null; + const localUnix = unixAddressFromSidecarInfo(result, 'local'); + socket.localUnixAddress = localUnix + ? formatHostNetUnixAddress(localUnix) + : socket.localUnixAddress ?? 'unix-unnamed'; socket.localReservation = null; socket.remoteInfo = null; + const remoteUnix = unixAddressFromSidecarInfo(result, 'remote'); + socket.remoteUnixAddress = formatHostNetUnixAddress(remoteUnix ?? unixAddress); + socket.serverId = null; + socket.listening = false; socket.readChunks.length = 0; socket.readableEnded = false; socket.closed = false; socket.lastError = null; return WASI_ERRNO_SUCCESS; } + if (Number(socket.domain) === HOST_NET_AF_UNIX) return WASI_ERRNO_INVAL; const { host, port } = parseHostNetAddress(rawAddr); if (!Number.isInteger(port) || port < 0 || port > 65535) { return WASI_ERRNO_FAULT; @@ -3273,8 +5545,8 @@ const hostNetImport = { socket.closed = false; socket.lastError = null; return WASI_ERRNO_SUCCESS; - } catch { - return WASI_ERRNO_FAULT; + } catch (error) { + return mapHostProcessError(error); } }, net_getaddrinfo(hostPtr, hostLen, portPtr, portLen, family, retAddrPtr, retAddrLenPtr) { @@ -3315,22 +5587,135 @@ const hostNetImport = { return WASI_ERRNO_FAULT; } }, + net_dns_query_rr_v1( + namePtr, + nameLen, + rrtype, + outPtr, + outCap, + retLenPtr, + retTtlPtr, + retFlagsPtr, + ) { + try { + const numericType = Number(rrtype) >>> 0; + const requestedType = numericType === 12 + ? 'PTR' + : numericType === 44 + ? 'SSHFP' + : null; + if (requestedType === null) { + return WASI_ERRNO_NOTSUP; + } + const response = callSyncRpc('dns.resolveRawRr', [{ + hostname: readGuestString(namePtr, nameLen), + rrtype: requestedType, + }]); + const status = String(response?.status ?? ''); + const records = response?.records; + if ( + !['ok', 'nxdomain', 'nodata'].includes(status) || + !Array.isArray(records) || + (status !== 'ok' && records.length !== 0) + ) { + return WASI_ERRNO_FAULT; + } + if (records.length > 4096) { + return WASI_ERRNO_NOBUFS; + } + + const rawRecords = []; + let ttl = 0; + for (const record of records) { + const encoded = typeof record?.data === 'string' ? record.data : ''; + if ( + !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(encoded) + ) { + return WASI_ERRNO_FAULT; + } + const raw = Buffer.from(encoded, 'base64'); + if ( + raw.toString('base64') !== encoded || + (requestedType === 'PTR' && raw.length === 0) || + (requestedType === 'SSHFP' && raw.length < 2) + ) { + return WASI_ERRNO_FAULT; + } + const recordTtl = Number(record?.ttl); + if (!Number.isInteger(recordTtl) || recordTtl < 0 || recordTtl > 0xffffffff) { + return WASI_ERRNO_FAULT; + } + ttl = rawRecords.length === 0 ? recordTtl : Math.min(ttl, recordTtl); + rawRecords.push(raw); + } + const payloadLength = rawRecords.reduce( + (total, record) => total + 4 + record.length, + 4, + ); + if (payloadLength > 64 * 1024) { + return WASI_ERRNO_NOBUFS; + } + if (writeGuestUint32(retLenPtr, payloadLength) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + if ((Number(outCap) >>> 0) < payloadLength) { + return WASI_ERRNO_NOBUFS; + } + + const payload = Buffer.alloc(payloadLength); + payload.writeUInt32LE(rawRecords.length, 0); + let offset = 4; + for (const record of rawRecords) { + payload.writeUInt32LE(record.length, offset); + offset += 4; + record.copy(payload, offset); + offset += record.length; + } + const memory = new Uint8Array(instanceMemory.buffer); + const outputOffset = Number(outPtr) >>> 0; + if (outputOffset > memory.length || payload.length > memory.length - outputOffset) { + return WASI_ERRNO_FAULT; + } + memory.set(payload, outputOffset); + const flags = status === 'nxdomain' ? 2 : status === 'nodata' ? 4 : 0; + if ( + writeGuestUint32(retTtlPtr, ttl) !== WASI_ERRNO_SUCCESS || + writeGuestUint32(retFlagsPtr, flags) !== WASI_ERRNO_SUCCESS + ) { + return WASI_ERRNO_FAULT; + } + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + }, net_bind(fd, addrPtr, addrLen) { const socket = getHostNetSocket(fd); if (!socket || socket.closed) { - return WASI_ERRNO_BADF; + return validateHostNetSocketDescriptor(fd); } try { + if (socket.bindOptions != null || socket.serverId != null) { + return WASI_ERRNO_INVAL; + } if (socket.localReservation != null) { callSyncRpc('net.release_tcp_port', [socket.localReservation]); socket.localReservation = null; } - socket.bindOptions = parseHostNetListenAddress(readGuestString(addrPtr, addrLen)); + const bindOptions = parseHostNetListenAddress(readGuestString(addrPtr, addrLen)); + const isUnixBind = bindOptions.path != null || + bindOptions.abstractPathHex != null || bindOptions.autobind === true; + if (isUnixBind && Number(socket.domain) !== HOST_NET_AF_UNIX) { + return WASI_ERRNO_AFNOSUPPORT; + } + if (!isUnixBind && Number(socket.domain) === HOST_NET_AF_UNIX) { + return WASI_ERRNO_INVAL; + } if (hostNetSocketBaseType(socket) === HOST_NET_SOCK_DGRAM) { - if (socket.bindOptions.path != null) { - return WASI_ERRNO_FAULT; + if (bindOptions.path != null || bindOptions.abstractPathHex != null) { + return WASI_ERRNO_NOTSUP; } const udpSocketId = ensureHostNetUdpSocket(socket); if (!udpSocketId) { @@ -3339,55 +5724,91 @@ const hostNetImport = { const result = callSyncRpc('dgram.bind', [ udpSocketId, { - address: socket.bindOptions.host, - port: socket.bindOptions.port, + address: bindOptions.host, + port: bindOptions.port, }, ]); - socket.localInfo = normalizeHostNetAddressInfo(result?.localAddress, result?.localPort); - return socket.localInfo ? WASI_ERRNO_SUCCESS : WASI_ERRNO_FAULT; + const localInfo = normalizeHostNetAddressInfo(result?.localAddress, result?.localPort); + if (!localInfo) return WASI_ERRNO_FAULT; + socket.bindOptions = bindOptions; + socket.localInfo = localInfo; + return WASI_ERRNO_SUCCESS; } - if (socket.bindOptions.path == null) { - const reservation = callSyncRpc('net.reserve_tcp_port', [socket.bindOptions]); + if (isUnixBind) { + if (socket.socketId != null) { + const result = callSyncRpc('net.bind_connected_unix', [{ + socketId: socket.socketId, + ...bindOptions, + }]); + const boundAddress = unixAddressFromSidecarInfo(result, 'local'); + if (!boundAddress) return WASI_ERRNO_FAULT; + socket.localUnixAddress = formatHostNetUnixAddress(boundAddress); + socket.bindOptions = boundAddress; + socket.localInfo = null; + return WASI_ERRNO_SUCCESS; + } + const result = callSyncRpc('net.bind_unix', [bindOptions]); + if (!result || typeof result.serverId !== 'string') { + return WASI_ERRNO_FAULT; + } + socket.serverId = result.serverId; + const boundAddress = unixAddressFromSidecarInfo(result, 'local'); + socket.localUnixAddress = boundAddress + ? formatHostNetUnixAddress(boundAddress) + : formatHostNetUnixAddress(bindOptions); + socket.bindOptions = boundAddress ?? bindOptions; + socket.localInfo = null; + } else { + if (socket.socketId != null) return WASI_ERRNO_INVAL; + const reservation = callSyncRpc('net.reserve_tcp_port', [bindOptions]); if ( !reservation || typeof reservation.reservationId !== 'string' || !Number.isInteger(Number(reservation.localPort)) ) { + if (typeof reservation?.reservationId === 'string') { + callSyncRpc('net.release_tcp_port', [reservation.reservationId]); + } return WASI_ERRNO_FAULT; } socket.localReservation = reservation.reservationId; socket.bindOptions = { - ...socket.bindOptions, - host: reservation.localAddress ?? socket.bindOptions.host, + ...bindOptions, + host: reservation.localAddress ?? bindOptions.host, port: Number(reservation.localPort), }; socket.localInfo = normalizeHostNetAddressInfo( socket.bindOptions.host ?? '127.0.0.1', socket.bindOptions.port, ); - } else { - socket.localInfo = null; } return WASI_ERRNO_SUCCESS; - } catch { - return WASI_ERRNO_FAULT; + } catch (error) { + return mapHostProcessError(error); } }, net_listen(fd, backlog) { const socket = getHostNetSocket(fd); if (!socket || socket.closed) { - return WASI_ERRNO_BADF; + return validateHostNetSocketDescriptor(fd); } - if (socket.serverId || !socket.bindOptions) { - return WASI_ERRNO_FAULT; + if (socket.socketId != null) { + return WASI_ERRNO_INVAL; + } + if (!socket.bindOptions) { + return WASI_ERRNO_INVAL; } try { const request = { - ...socket.bindOptions, backlog: Math.max(0, Number(backlog) >>> 0), }; + if (socket.serverId) { + request.boundServerId = socket.serverId; + } else { + Object.assign(request, socket.bindOptions); + } if (socket.localReservation != null) { request.localReservation = socket.localReservation; } @@ -3397,19 +5818,28 @@ const hostNetImport = { return WASI_ERRNO_FAULT; } socket.serverId = result.serverId; + const localUnix = unixAddressFromSidecarInfo(result, 'local'); + if (localUnix) { + socket.localUnixAddress = formatHostNetUnixAddress(localUnix); + socket.bindOptions = localUnix; + } socket.localReservation = null; socket.localInfo = normalizeHostNetAddressInfo(result.localAddress, result.localPort); + socket.listening = true; return WASI_ERRNO_SUCCESS; - } catch { - return WASI_ERRNO_FAULT; + } catch (error) { + return mapHostProcessError(error); } }, net_accept(fd, retFdPtr, retAddrPtr, retAddrLenPtr) { const socket = getHostNetSocket(fd); - if (!socket?.serverId || socket.closed) { - return WASI_ERRNO_BADF; + const validation = validateHostNetSocketDescriptor(fd); + if (validation !== WASI_ERRNO_SUCCESS) return validation; + if (!socket.serverId || socket.listening !== true) { + return WASI_ERRNO_INVAL; } + let accepted = null; try { // First drain a connection already buffered by net_poll's readiness probe; otherwise block // until one arrives (POSIX blocking-accept semantics, for guests that accept() without polling @@ -3417,55 +5847,132 @@ const hostNetImport = { // only when a connection is actually pending, so the X server only reaches accept() when there // is one to take, and otherwise services connected client fds instead. if (!socket.pendingAccepts) socket.pendingAccepts = []; - let accepted = socket.pendingAccepts.shift(); + accepted = socket.pendingAccepts.shift(); + const startedAt = Date.now(); + const safeguardDeadline = startedAt + unixConnectTimeoutMs; + const receiveTimeoutMs = Number(socket.recvTimeoutMs); + const receiveDeadline = Number.isFinite(receiveTimeoutMs) && receiveTimeoutMs > 0 + ? startedAt + receiveTimeoutMs + : null; + const warningAt = startedAt + Math.floor(unixConnectTimeoutMs * 0.8); + let warnedNearLimit = false; while (!accepted) { + if (dispatchPendingWasmSignals()) return WASI_ERRNO_INTR; accepted = tryHostNetAcceptOnce(socket); + if (!accepted && socket.nonblock) return WASI_ERRNO_AGAIN; if (!accepted) { - pumpSpawnedChildren(10); + const now = Date.now(); + if (receiveDeadline != null && now >= receiveDeadline) { + return WASI_ERRNO_AGAIN; + } + if (!warnedNearLimit && now >= warningAt) { + warnedNearLimit = true; + process.stderr.write( + `[agentos] blocking accept is nearing limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms)\n`, + ); + } + if (now >= safeguardDeadline) { + process.stderr.write( + `[agentos] blocking accept exceeded limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms); raise limits.resources.maxBlockingReadMs if needed\n`, + ); + return WASI_ERRNO_TIMEDOUT; + } + const nextDeadline = receiveDeadline == null + ? safeguardDeadline + : Math.min(receiveDeadline, safeguardDeadline); + pumpSpawnedChildrenOrWait(Math.min(10, Math.max(1, nextDeadline - now))); } } + if (accepted.error != null) { + return accepted.error; + } if (writeGuestUint32(retFdPtr, accepted.acceptedFd) !== WASI_ERRNO_SUCCESS) { + cleanupAcceptedHostNetSocket(accepted, 'accept fd copyout'); return WASI_ERRNO_FAULT; } - return writeGuestBytes(retAddrPtr, readGuestUint32(retAddrLenPtr), accepted.address, retAddrLenPtr); - } catch { - return WASI_ERRNO_FAULT; + const addressCopyout = writeGuestBytes( + retAddrPtr, + readGuestUint32(retAddrLenPtr), + accepted.address, + retAddrLenPtr, + ); + if (addressCopyout !== WASI_ERRNO_SUCCESS) { + cleanupAcceptedHostNetSocket(accepted, 'accept address copyout'); + } + return addressCopyout; + } catch (error) { + cleanupAcceptedHostNetSocket(accepted, 'accept failure'); + return mapHostProcessError(error); } }, + net_validate_socket(fd) { + return validateHostNetSocketDescriptor(fd); + }, + net_validate_accept(fd) { + const validation = validateHostNetSocketDescriptor(fd); + if (validation !== WASI_ERRNO_SUCCESS) return validation; + const socket = getHostNetSocket(fd); + return socket?.serverId && socket.listening === true + ? WASI_ERRNO_SUCCESS + : WASI_ERRNO_INVAL; + }, net_getsockname(fd, addrPtr, addrLenPtr) { const socket = getHostNetSocket(fd); if (!socket || socket.closed) { - return WASI_ERRNO_BADF; - } - if (!socket.localInfo) { - return WASI_ERRNO_INVAL; + return validateHostNetSocketDescriptor(fd); } - try { + refreshHostNetUnixSocketInfo(socket); + if (socket.localUnixAddress != null) { + const address = Buffer.from(socket.localUnixAddress, 'utf8'); + return writeGuestBytes(addrPtr, readGuestUint32(addrLenPtr), address, addrLenPtr); + } + if (!socket.localInfo) { + return WASI_ERRNO_INVAL; + } const address = Buffer.from(formatHostNetAddressInfo(socket.localInfo), 'utf8'); return writeGuestBytes(addrPtr, readGuestUint32(addrLenPtr), address, addrLenPtr); - } catch { - return WASI_ERRNO_FAULT; + } catch (error) { + return mapHostProcessError(error); } }, net_getpeername(fd, addrPtr, addrLenPtr) { const socket = getHostNetSocket(fd); if (!socket || socket.closed) { - return WASI_ERRNO_BADF; - } - if (!socket.remoteInfo) { - return WASI_ERRNO_INVAL; + return validateHostNetSocketDescriptor(fd); } - try { + refreshHostNetUnixSocketInfo(socket); + if (socket.remoteUnixAddress != null) { + const address = Buffer.from(socket.remoteUnixAddress, 'utf8'); + return writeGuestBytes(addrPtr, readGuestUint32(addrLenPtr), address, addrLenPtr); + } + if (!socket.remoteInfo) { + return WASI_ERRNO_NOTCONN; + } const address = Buffer.from(formatHostNetAddressInfo(socket.remoteInfo), 'utf8'); return writeGuestBytes(addrPtr, readGuestUint32(addrLenPtr), address, addrLenPtr); - } catch { - return WASI_ERRNO_FAULT; + } catch (error) { + return mapHostProcessError(error); } }, net_send(fd, bufPtr, bufLen, flags, retSentPtr) { const socket = getHostNetSocket(fd); + const handle = lookupFdHandle(Number(fd) >>> 0); + if (handle?.kind === 'kernel-fd') { + try { + const chunk = readGuestBytes(bufPtr, bufLen); + const written = Number(callSyncRpc('process.fd_sendmsg_rights', [ + Number(handle.targetFd) >>> 0, + chunk, + [], + Number(flags) >>> 0, + ])); + return writeGuestUint32(retSentPtr, written); + } catch (error) { + return mapHostProcessError(error); + } + } if (!socket?.socketId || socket.closed) { return WASI_ERRNO_BADF; } @@ -3475,37 +5982,96 @@ const hostNetImport = { if ((Number(flags) >>> 0) !== 0) { // Non-zero send flags are currently ignored in the WASM host_net shim. } - const written = Number(callSyncRpc('net.write', [socket.socketId, chunk])) >>> 0; + const written = Number( + callSyncRpc('net.write', [ + socket.socketId, + chunk, + socket.nonblock === true || + ((Number(flags) >>> 0) & HOST_NET_MSG_DONTWAIT) !== 0, + ]), + ) >>> 0; return writeGuestUint32(retSentPtr, written); - } catch { - return WASI_ERRNO_FAULT; + } catch (error) { + return mapHostProcessError(error); } }, net_recv(fd, bufPtr, bufLen, flags, retReceivedPtr) { const socket = getHostNetSocket(fd); + const handle = lookupFdHandle(Number(fd) >>> 0); + if (handle?.kind === 'kernel-fd') { + try { + const recvFlags = Number(flags) >>> 0; + const result = callSyncRpc('process.fd_recvmsg_rights', [ + Number(handle.targetFd) >>> 0, + Number(bufLen) >>> 0, + 0, + false, + (recvFlags & 0x0002) !== 0, + (recvFlags & 0x0040) !== 0, + (recvFlags & 0x0100) !== 0, + ]); + const bytes = Buffer.from(result?.data ?? []); + const write = writeGuestBytes(bufPtr, bufLen, bytes, retReceivedPtr); + if (write !== WASI_ERRNO_SUCCESS) return write; + if ((recvFlags & 0x0020) !== 0 && Number(result?.fullLength) > bytes.length) { + return writeGuestUint32(retReceivedPtr, Number(result.fullLength) >>> 0); + } + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + } if (!socket) { return WASI_ERRNO_BADF; } try { - if ((Number(flags) >>> 0) !== 0) { - // Non-zero recv flags are currently ignored in the WASM host_net shim. + const recvFlags = Number(flags) >>> 0; + if (hostNetSocketBaseType(socket) === HOST_NET_SOCK_DGRAM) { + const supportedFlags = HOST_NET_MSG_PEEK | HOST_NET_MSG_DONTWAIT | HOST_NET_MSG_TRUNC; + if ((recvFlags & ~supportedFlags) !== 0) { + return WASI_ERRNO_INVAL; + } + const udpSocketId = ensureHostNetUdpSocket(socket); + if (!udpSocketId) { + return WASI_ERRNO_BADF; + } + const event = receiveHostNetDatagramEvent(socket, recvFlags); + if (!event) { + return WASI_ERRNO_AGAIN; + } + const bytes = hostNetDatagramBytes(event); + const write = writeGuestBytes(bufPtr, bufLen, bytes, retReceivedPtr); + if (write !== WASI_ERRNO_SUCCESS) { + return write; + } + if ((recvFlags & HOST_NET_MSG_TRUNC) !== 0 && bytes.length > (Number(bufLen) >>> 0)) { + return writeGuestUint32(retReceivedPtr, bytes.length); + } + return WASI_ERRNO_SUCCESS; + } + if (!socket.socketId || socket.closed) { + return WASI_ERRNO_BADF; } + const peek = (recvFlags & HOST_NET_MSG_PEEK) !== 0; // Non-blocking sockets (O_NONBLOCK via net_set_nonblock, used by libxcb's poll_for_*): // pull whatever is queued, do ONE short readiness probe, and return EAGAIN if still empty // instead of blocking. libxcb assumes its "poll" reads never block on an empty socket. if (socket.nonblock) { - let queued = dequeueHostNetBytes(socket, bufLen); + let queued = peek ? peekHostNetBytes(socket, bufLen) : dequeueHostNetBytes(socket, bufLen); if (queued.length > 0) { return writeGuestBytes(bufPtr, bufLen, queued, retReceivedPtr); } - if (socket.lastError) return WASI_ERRNO_FAULT; + if (socket.lastError) return mapHostProcessError(socket.lastError); if (socket.readableEnded || socket.closed || !socket.socketId) { return writeGuestUint32(retReceivedPtr, 0); } - pollHostNetSocket(socket, 0); - queued = dequeueHostNetBytes(socket, bufLen); + const result = readReadyHostNetSocket(socket, bufLen, peek, 0); + if (result?.kind === 'data' && result.bytes.length > 0) { + return writeGuestBytes(bufPtr, bufLen, result.bytes, retReceivedPtr); + } + queued = peek ? peekHostNetBytes(socket, bufLen) : dequeueHostNetBytes(socket, bufLen); if (queued.length > 0) { return writeGuestBytes(bufPtr, bufLen, queued, retReceivedPtr); } @@ -3515,34 +6081,69 @@ const hostNetImport = { return WASI_ERRNO_AGAIN; } - const deadline = - socket.recvTimeoutMs == null ? null : Date.now() + Math.max(0, socket.recvTimeoutMs); + const startedAt = Date.now(); + const receiveDeadline = socket.recvTimeoutMs == null + ? null + : startedAt + Math.max(0, socket.recvTimeoutMs); + const safeguardDeadline = startedAt + unixConnectTimeoutMs; + const warningAt = startedAt + Math.floor(unixConnectTimeoutMs * 0.8); + let warnedNearLimit = false; while (true) { - const queued = dequeueHostNetBytes(socket, bufLen); + if (dispatchPendingWasmSignals()) return WASI_ERRNO_INTR; + const queued = peek ? peekHostNetBytes(socket, bufLen) : dequeueHostNetBytes(socket, bufLen); if (queued.length > 0) { return writeGuestBytes(bufPtr, bufLen, queued, retReceivedPtr); } if (socket.lastError) { - return WASI_ERRNO_FAULT; + return mapHostProcessError(socket.lastError); } if (socket.readableEnded || socket.closed || !socket.socketId) { return writeGuestUint32(retReceivedPtr, 0); } - const pollWaitMs = - deadline == null ? 50 : Math.max(0, Math.min(50, deadline - Date.now())); - if (deadline != null && pollWaitMs === 0) { + const now = Date.now(); + if (receiveDeadline != null && now >= receiveDeadline) { return WASI_ERRNO_AGAIN; } - pollHostNetSocket(socket, pollWaitMs); - if (deadline != null && Date.now() >= deadline) { + if (!warnedNearLimit && now >= warningAt) { + warnedNearLimit = true; + process.stderr.write( + `[agentos] blocking socket receive is nearing limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms)\n`, + ); + } + if (now >= safeguardDeadline) { + process.stderr.write( + `[agentos] blocking socket receive exceeded limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms); raise limits.resources.maxBlockingReadMs if needed\n`, + ); + return WASI_ERRNO_TIMEDOUT; + } + const nextDeadline = receiveDeadline == null + ? safeguardDeadline + : Math.min(receiveDeadline, safeguardDeadline); + // A child can be the peer that produces this read. Keep the sidecar + // probe nonblocking in that case and return to child_process.poll + // between attempts so the child's recv/send RPCs can run. + const pumpsLocalChildren = hasActiveSpawnedChildren(); + const pollWaitMs = pumpsLocalChildren + ? 0 + : Math.max(0, Math.min(50, nextDeadline - now)); + const result = readReadyHostNetSocket(socket, bufLen, peek, pollWaitMs); + if (dispatchPendingWasmSignals()) return WASI_ERRNO_INTR; + if (result?.kind === 'data' && result.bytes.length > 0) { + return writeGuestBytes(bufPtr, bufLen, result.bytes, retReceivedPtr); + } + if (pumpsLocalChildren) { + pumpSpawnedChildren(SPAWNED_CHILD_WAIT_SLICE_MS); + if (dispatchPendingWasmSignals()) return WASI_ERRNO_INTR; + } + if (receiveDeadline != null && Date.now() >= receiveDeadline) { return WASI_ERRNO_AGAIN; } } - } catch { - return WASI_ERRNO_FAULT; + } catch (error) { + return mapHostProcessError(error); } }, net_sendto(fd, bufPtr, bufLen, flags, addrPtr, addrLen, retSentPtr) { @@ -3581,71 +6182,50 @@ const hostNetImport = { } try { - if ((Number(flags) >>> 0) !== 0) { + const recvFlags = Number(flags) >>> 0; + const supportedFlags = HOST_NET_MSG_PEEK | HOST_NET_MSG_DONTWAIT | HOST_NET_MSG_TRUNC; + if ((recvFlags & ~supportedFlags) !== 0) { return WASI_ERRNO_INVAL; } const udpSocketId = ensureHostNetUdpSocket(socket); if (!udpSocketId) { return WASI_ERRNO_FAULT; } - - const deadline = - socket.recvTimeoutMs == null ? null : Date.now() + Math.max(0, socket.recvTimeoutMs); - while (true) { - const pollWaitMs = - deadline == null ? 50 : Math.max(0, Math.min(50, deadline - Date.now())); - if (deadline != null && pollWaitMs === 0) { - return WASI_ERRNO_AGAIN; - } - const event = callSyncRpc('dgram.poll', [udpSocketId, pollWaitMs]); - if (!event) { - if (deadline != null && Date.now() >= deadline) { - return WASI_ERRNO_AGAIN; - } - continue; - } - if (event.type === 'error') { - return WASI_ERRNO_FAULT; - } - if (event.type !== 'message') { - continue; - } - - let bytes; - if (event.data && typeof event.data === 'object' && typeof event.data.base64 === 'string') { - bytes = Buffer.from(event.data.base64, 'base64'); - } else { - try { - bytes = decodeFsBytesPayload(event.data, 'host_net recvfrom data'); - } catch { - return WASI_ERRNO_FAULT; - } - } - const dataResult = writeGuestBytes(bufPtr, bufLen, bytes, retReceivedPtr); - if (dataResult !== WASI_ERRNO_SUCCESS) { - return dataResult; - } - if (!event.remoteAddress || !Number.isInteger(Number(event.remotePort))) { - return WASI_ERRNO_BADF; - } - let address; - try { - address = Buffer.from(formatHostNetAddressInfo({ - address: event.remoteAddress, - port: event.remotePort, - }), 'utf8'); - } catch { - return WASI_ERRNO_INVAL; - } - let addressCapacity; - try { - addressCapacity = readGuestUint32(retAddrLenPtr); - } catch { - return WASI_ERRNO_FAULT; - } - const addressResult = writeGuestBytes(retAddrPtr, addressCapacity, address, retAddrLenPtr); + const event = receiveHostNetDatagramEvent(socket, recvFlags); + if (!event) { + return WASI_ERRNO_AGAIN; + } + const bytes = hostNetDatagramBytes(event); + const dataResult = writeGuestBytes(bufPtr, bufLen, bytes, retReceivedPtr); + if (dataResult !== WASI_ERRNO_SUCCESS) { + return dataResult; + } + if (!event.remoteAddress || !Number.isInteger(Number(event.remotePort))) { + return WASI_ERRNO_BADF; + } + let address; + try { + address = Buffer.from(formatHostNetAddressInfo({ + address: event.remoteAddress, + port: event.remotePort, + }), 'utf8'); + } catch { + return WASI_ERRNO_INVAL; + } + let addressCapacity; + try { + addressCapacity = readGuestUint32(retAddrLenPtr); + } catch { + return WASI_ERRNO_FAULT; + } + const addressResult = writeGuestBytes(retAddrPtr, addressCapacity, address, retAddrLenPtr); + if (addressResult !== WASI_ERRNO_SUCCESS) { return addressResult; } + if ((recvFlags & HOST_NET_MSG_TRUNC) !== 0 && bytes.length > (Number(bufLen) >>> 0)) { + return writeGuestUint32(retReceivedPtr, bytes.length); + } + return WASI_ERRNO_SUCCESS; } catch { return WASI_ERRNO_FAULT; } @@ -3659,6 +6239,9 @@ const hostNetImport = { if (sockoptKind == null) { return WASI_ERRNO_INVAL; } + if (sockoptKind === 'ignore') { + return WASI_ERRNO_SUCCESS; + } try { const timeoutMs = parseHostNetTimevalMs(readGuestBytes(optvalPtr, optvalLen)); if (timeoutMs == null && readGuestBytes(optvalPtr, optvalLen).some((byte) => byte !== 0)) { @@ -3672,6 +6255,32 @@ const hostNetImport = { } return WASI_ERRNO_SUCCESS; }, + net_getsockopt(fd, level, optname, optvalPtr, optvalLenPtr) { + const socket = getHostNetSocket(fd); + if (!socket || socket.closed) { + return WASI_ERRNO_BADF; + } + + try { + const optvalLen = readGuestUint32(optvalLenPtr); + const normalizedLevel = Number(level) >>> 0; + const normalizedOptname = Number(optname) >>> 0; + if ( + (normalizedLevel === HOST_NET_SOL_SOCKET || + normalizedLevel === HOST_NET_WASI_SOL_SOCKET) && + normalizedOptname === HOST_NET_SO_ERROR + ) { + if (optvalLen < 4) { + return WASI_ERRNO_INVAL; + } + new DataView(instanceMemory.buffer).setInt32(Number(optvalPtr) >>> 0, 0, true); + return writeGuestUint32(optvalLenPtr, 4); + } + return WASI_ERRNO_INVAL; + } catch { + return WASI_ERRNO_FAULT; + } + }, net_close(fd) { const numericFd = Number(fd) >>> 0; const socket = hostNetSockets.get(numericFd); @@ -3680,22 +6289,52 @@ const hostNetImport = { } hostNetSockets.delete(numericFd); - try { - if (socket.localReservation != null) { - callSyncRpc('net.release_tcp_port', [socket.localReservation]); + // dup/dup2 and inherited descriptors are aliases of one Linux open-file + // description. Closing one descriptor must not destroy the sidecar socket + // while another descriptor in this process still refers to it. + if ([...hostNetSockets.values()].some((candidate) => candidate === socket)) { + return WASI_ERRNO_SUCCESS; + } + let firstError = null; + const cleanup = (label, operation) => { + try { + operation(); + } catch (error) { + if (firstError == null) firstError = error; + process.stderr.write( + `[agentos] failed to ${label} while closing host_net fd ${numericFd}: ${error instanceof Error ? error.message : String(error)}\n`, + ); } - if (socket.socketId && !socket.closed) { - callSyncRpc('net.destroy', [socket.socketId]); + }; + if (Array.isArray(socket.pendingAccepts)) { + for (const accepted of socket.pendingAccepts.splice(0)) { + const error = cleanupAcceptedHostNetSocket(accepted, 'listener close'); + if (firstError == null && error != null) firstError = error; } - if (socket.udpSocketId) { + } + if (socket.localReservation != null) { + cleanup('release TCP port reservation', () => { + callSyncRpc('net.release_tcp_port', [socket.localReservation]); + }); + } + if (socket.socketId && !socket.closed) { + cleanup('destroy connected socket', () => { + callSyncRpc('net.destroy', [socket.socketId]); + }); + } + if (socket.serverId) { + cleanup('close listener', () => { + callSyncRpc('net.server_close', [socket.serverId]); + }); + } + if (socket.udpSocketId) { + cleanup('close datagram socket', () => { callSyncRpc('dgram.close', [socket.udpSocketId]); - } - return WASI_ERRNO_SUCCESS; - } catch { - return WASI_ERRNO_FAULT; + }); } + return firstError == null ? WASI_ERRNO_SUCCESS : mapHostProcessError(firstError); }, - net_tls_connect(fd, hostnamePtr, hostnameLen) { + net_tls_connect(fd, hostnamePtr, hostnameLen, flags = 0) { const socket = getHostNetSocket(fd); if (!socket?.socketId || socket.closed) { return WASI_ERRNO_BADF; @@ -3704,7 +6343,7 @@ const hostNetImport = { try { const servername = readGuestString(hostnamePtr, hostnameLen); const tlsOptions = { servername }; - if (guestEnv.NODE_TLS_REJECT_UNAUTHORIZED === '0') { + if ((Number(flags) & 1) === 1 || guestEnv.NODE_TLS_REJECT_UNAUTHORIZED === '0') { tlsOptions.rejectUnauthorized = false; } callSyncRpc('net.socket_upgrade_tls', [ @@ -3730,20 +6369,275 @@ const hostProcessImport = { cwdPtr, cwdLen, retPidPtr, + ) { + // Legacy ABI used by checked-in command modules. In this contract the + // executable is argv[0]; newer callers use proc_spawn_v2 so argv[0] + // can differ from the executable path. + try { + const argvBytes = readGuestBytes(argvPtr, argvLen); + const commandLength = argvBytes.indexOf(0); + if (commandLength <= 0) { + return WASI_ERRNO_FAULT; + } + return hostProcessImport.proc_spawn_v2( + argvPtr, + commandLength, + argvPtr, + argvLen, + envpPtr, + envpLen, + stdinFd, + stdoutFd, + stderrFd, + cwdPtr, + cwdLen, + retPidPtr, + ); + } catch (error) { + traceHostProcess('proc-spawn-legacy-fault', { + message: error instanceof Error ? error.message : String(error), + }); + return mapHostProcessError(error); + } + }, + proc_spawn_v3( + execPathPtr, + execPathLen, + argvPtr, + argvLen, + envpPtr, + envpLen, + actionsPtr, + actionsLen, + cwdPtr, + cwdLen, + attrFlags, + sigDefaultLo, + sigDefaultHi, + sigMaskLo, + sigMaskHi, + pgroup, + retPidPtr, + ) { + const flags = Number(attrFlags) >>> 0; + if ((flags & ~SUPPORTED_POSIX_SPAWN_FLAGS) !== 0) { + return WASI_ERRNO_NOTSUP; + } + if (activeSpawnCallContext !== null) { + return WASI_ERRNO_FAULT; + } + try { + const initialCwd = + Number(cwdLen) > 0 ? readGuestString(cwdPtr, cwdLen) : undefined; + const actions = decodeSpawnActions(actionsPtr, actionsLen, initialCwd); + const defaultSignals = + flags & POSIX_SPAWN_SETSIGDEF + ? decodeSignalMask(sigDefaultLo, sigDefaultHi) + : []; + const signalMask = decodeSignalMask(sigMaskLo, sigMaskHi).filter( + (signal) => signal !== LINUX_SIGKILL && signal !== LINUX_SIGSTOP, + ); + const inheritedIgnores = [...wasmSignalRegistrations.entries()] + .filter( + ([signal, registration]) => + registration.action === 'ignore' && !defaultSignals.includes(signal), + ) + .map(([signal]) => signal); + activeSpawnCallContext = { + internalBootstrapEnv: { + AGENTOS_WASM_INITIAL_SIGNAL_MASK: JSON.stringify(signalMask), + AGENTOS_WASM_INITIAL_SIGNAL_IGNORES: JSON.stringify(inheritedIgnores), + }, + attrFlags: flags, + pgroup: Number(pgroup) | 0, + signalDefaults: defaultSignals, + signalMask, + fileActions: actions.actions, + }; + return hostProcessImport.proc_spawn_v2( + execPathPtr, + execPathLen, + argvPtr, + argvLen, + envpPtr, + envpLen, + actions.stdio[0], + actions.stdio[1], + actions.stdio[2], + cwdPtr, + 0, + retPidPtr, + initialCwd, + ); + } catch (error) { + return mapHostProcessError(error); + } finally { + activeSpawnCallContext = null; + } + }, + proc_spawn_v4( + execPathPtr, + execPathLen, + argvPtr, + argvLen, + envpPtr, + envpLen, + actionsPtr, + actionsLen, + cwdPtr, + cwdLen, + searchPathPtr, + searchPathLen, + attrFlags, + sigDefaultLo, + sigDefaultHi, + sigMaskLo, + sigMaskHi, + pgroup, + schedPolicy, + schedPriority, + retPidPtr, + ) { + const flags = Number(attrFlags) >>> 0; + if ((flags & ~SUPPORTED_POSIX_SPAWN_FLAGS) !== 0) { + return WASI_ERRNO_NOTSUP; + } + if ( + (flags & POSIX_SPAWN_SETSID) !== 0 && + (flags & POSIX_SPAWN_SETPGROUP) !== 0 + ) { + // Linux rejects this combination because setsid() makes the child + // a process-group leader before the requested setpgid operation. + return WASI_ERRNO_PERM; + } + const requestedPolicy = Number(schedPolicy) | 0; + const requestedPriority = Number(schedPriority) | 0; + if ( + (flags & (POSIX_SPAWN_SETSCHEDPARAM | POSIX_SPAWN_SETSCHEDULER)) !== 0 && + requestedPriority !== 0 + ) { + // SCHED_OTHER has exactly one valid priority on Linux. + return WASI_ERRNO_INVAL; + } + if ( + (flags & POSIX_SPAWN_SETSCHEDULER) !== 0 && + requestedPolicy !== 0 + ) { + // AgentOS exposes SCHED_OTHER. Real-time policies require host + // scheduling privileges and are deliberately not virtualized. + return WASI_ERRNO_PERM; + } + if (activeSpawnCallContext !== null) { + return WASI_ERRNO_FAULT; + } + try { + const initialCwd = + Number(cwdLen) > 0 ? readGuestString(cwdPtr, cwdLen) : undefined; + const actions = decodeSpawnActions(actionsPtr, actionsLen, initialCwd); + // The pointer carries presence independently from the length: + // posix_spawn passes NULL/0, while posix_spawnp with PATH="" + // passes a non-NULL pointer and zero length. Linux treats that + // empty PATH as one current-directory entry. + const searchPath = + Number(searchPathPtr) !== 0 + ? readGuestString(searchPathPtr, searchPathLen) + : null; + const defaultSignals = + flags & POSIX_SPAWN_SETSIGDEF + ? decodeSignalMask(sigDefaultLo, sigDefaultHi) + : []; + const signalMask = decodeSignalMask(sigMaskLo, sigMaskHi).filter( + (signal) => signal !== LINUX_SIGKILL && signal !== LINUX_SIGSTOP, + ); + const inheritedIgnores = [...wasmSignalRegistrations.entries()] + .filter( + ([signal, registration]) => + registration.action === 'ignore' && !defaultSignals.includes(signal), + ) + .map(([signal]) => signal); + activeSpawnCallContext = { + internalBootstrapEnv: { + AGENTOS_WASM_INITIAL_SIGNAL_MASK: JSON.stringify(signalMask), + AGENTOS_WASM_INITIAL_SIGNAL_IGNORES: JSON.stringify(inheritedIgnores), + }, + attrFlags: flags, + exactExecPath: searchPath === null, + searchPath, + schedPolicy: requestedPolicy, + schedPriority: requestedPriority, + pgroup: Number(pgroup) | 0, + signalDefaults: defaultSignals, + signalMask, + fileActions: actions.actions, + }; + return hostProcessImport.proc_spawn_v2( + execPathPtr, + execPathLen, + argvPtr, + argvLen, + envpPtr, + envpLen, + actions.stdio[0], + actions.stdio[1], + actions.stdio[2], + cwdPtr, + 0, + retPidPtr, + initialCwd, + ); + } catch (error) { + return mapHostProcessError(error); + } finally { + activeSpawnCallContext = null; + } + }, + proc_spawn_v2( + execPathPtr, + execPathLen, + argvPtr, + argvLen, + envpPtr, + envpLen, + stdinFd, + stdoutFd, + stderrFd, + cwdPtr, + cwdLen, + retPidPtr, + resolvedCwdOverride, ) { if (permissionTier !== 'full') { return WASI_ERRNO_FAULT; } + traceHostProcess('proc-spawn-call', { + execPathPtr: Number(execPathPtr) >>> 0, + execPathLen: Number(execPathLen) >>> 0, + argvPtr: Number(argvPtr) >>> 0, + argvLen: Number(argvLen) >>> 0, + envpPtr: Number(envpPtr) >>> 0, + envpLen: Number(envpLen) >>> 0, + stdinFd: Number(stdinFd) >>> 0, + stdoutFd: Number(stdoutFd) >>> 0, + stderrFd: Number(stderrFd) >>> 0, + cwdPtr: Number(cwdPtr) >>> 0, + cwdLen: Number(cwdLen) >>> 0, + retPidPtr: Number(retPidPtr) >>> 0, + }); try { - const argv = decodeNullSeparatedStrings(readGuestBytes(argvPtr, argvLen)); - if (argv.length === 0) { - return WASI_ERRNO_FAULT; + const command = readGuestString(execPathPtr, execPathLen); + if (command.length === 0) { + return WASI_ERRNO_NOENT; } - - const [command, ...args] = argv; + const argv = decodeNullSeparatedStrings(readGuestBytes(argvPtr, argvLen)); + const argv0 = argv[0] ?? command; + const args = argv.slice(1); const env = parseSerializedEnv(readGuestBytes(envpPtr, envpLen)); const cwd = - Number(cwdLen) > 0 ? readGuestString(cwdPtr, cwdLen) : undefined; + typeof resolvedCwdOverride === 'string' + ? resolvedCwdOverride + : Number(cwdLen) > 0 + ? readGuestString(cwdPtr, cwdLen) + : undefined; const stdinTarget = resolveSpawnFd(stdinFd); const stdoutTarget = resolveSpawnFd(stdoutFd); const stderrTarget = resolveSpawnFd(stderrFd); @@ -3755,6 +6649,9 @@ const hostProcessImport = { stdoutTarget, stderrTarget, ); + record.processGroup = Number( + callSyncRpc('process.getpgid', [VIRTUAL_PID]), + ) >>> 0; spawnedChildren.set(record.pid, record); spawnedChildrenById.set(record.childId, record); traceHostProcess('proc-spawn-synthetic', { @@ -3763,11 +6660,12 @@ const hostProcessImport = { pid: record.pid, exitCode: syntheticResult.exitCode, }); - emitSyntheticCommandOutput(record, stdoutFd, stderrFd, syntheticResult); + emitSyntheticCommandOutput(record, syntheticResult); return writeGuestUint32(retPidPtr, record.pid); } traceHostProcess('proc-spawn-begin', { command, + argv0, args, cwd: cwd ?? null, stdinFd: Number(stdinFd) >>> 0, @@ -3776,12 +6674,20 @@ const hostProcessImport = { stdinTarget, stdoutTarget, stderrTarget, + kernelFdMappings: kernelFdMappingsForSpawn(), + internalBootstrapEnv: { + ...(activeSpawnCallContext?.internalBootstrapEnv ?? {}), + ...inheritedNofileBootstrapEnv(), + }, + spawnAttrFlags: activeSpawnCallContext?.attrFlags ?? 0, + spawnPgroup: activeSpawnCallContext?.pgroup ?? null, }); let stdinRedirectBytes = null; if ( stdinTarget > 2 && stdinTarget !== 0xffffffff && - !spawnStdinFdIsSyntheticPipe(stdinTarget) + !spawnStdinFdIsSyntheticPipe(stdinTarget) && + !spawnFdIsKernelBacked(stdinTarget) ) { stdinRedirectBytes = readSpawnStdinRedirectBytes(stdinTarget); if (stdinRedirectBytes == null) { @@ -3797,9 +6703,24 @@ const hostProcessImport = { command, args, options: { + argv0, cwd, env, - internalBootstrapEnv: {}, + internalBootstrapEnv: { + ...(activeSpawnCallContext?.internalBootstrapEnv ?? {}), + ...inheritedNofileBootstrapEnv(), + }, + spawnAttrFlags: activeSpawnCallContext?.attrFlags ?? 0, + spawnExactPath: activeSpawnCallContext?.exactExecPath ?? false, + spawnSearchPath: activeSpawnCallContext?.searchPath, + spawnSchedPolicy: activeSpawnCallContext?.schedPolicy, + spawnSchedPriority: activeSpawnCallContext?.schedPriority, + spawnPgroup: activeSpawnCallContext?.pgroup, + spawnSignalDefaults: activeSpawnCallContext?.signalDefaults ?? [], + spawnSignalMask: activeSpawnCallContext?.signalMask ?? [], + spawnFileActions: activeSpawnCallContext?.fileActions ?? [], + spawnFdMappings: kernelFdMappingsForSpawn(), + spawnHostNetFds: hostNetFdsForSpawn(), shell: false, stdio: [ stdinTarget === 0 @@ -3825,8 +6746,17 @@ const hostProcessImport = { if (!Number.isInteger(pid) || pid === 0 || typeof result?.childId !== 'string') { return WASI_ERRNO_FAULT; } + let processGroup = Number(result?.pgid) >>> 0; + if (processGroup === 0) { + processGroup = Number(callSyncRpc('process.getpgid', [pid])) >>> 0; + } - const stdinPipe = registerPipeConsumer(stdinTarget, result.childId, 'stdin'); + const directPosixStdin = + result?.directPosixStdin === true || + spawnActionsControlGuestFd(activeSpawnCallContext?.fileActions, 0); + const stdinPipe = directPosixStdin + ? null + : registerPipeConsumer(stdinTarget, result.childId, 'stdin'); const stdoutPipe = registerPipeProducer(stdoutTarget, result.childId, 'stdout'); const stderrPipe = registerPipeProducer(stderrTarget, result.childId, 'stderr'); const retainedSpawnOutputHandles = [stdoutTarget, stderrTarget] @@ -3846,6 +6776,7 @@ const hostProcessImport = { childId: result.childId, pid, stdinFd: stdinTarget, + directPosixStdin, stdoutFd: stdoutTarget, stderrFd: stderrTarget, stdinPipe, @@ -3854,14 +6785,20 @@ const hostProcessImport = { stdinReadyAtMs: Date.now() + 100, delegateRetainedFds, retainedSpawnOutputHandles, + exitCode: null, + exitSignal: null, exitStatus: null, - }; + rawWaitStatus: null, + processGroup, + }; spawnedChildren.set(pid, record); spawnedChildrenById.set(result.childId, record); traceHostProcess('proc-spawn-ready', { command, childId: result.childId, pid, + directPosixStdin, + resolvedArgs: result.args ?? null, }); if (stdinRedirectBytes != null) { if (stdinRedirectBytes.length > 0) { @@ -3872,108 +6809,498 @@ const hostProcessImport = { } callSyncRpc('child_process.close_stdin', [result.childId]); } - consumeSpawnOutputFd(stdoutFd); - consumeSpawnOutputFd(stderrFd); return writeGuestUint32(retPidPtr, pid); } catch (error) { traceHostProcess('proc-spawn-fault', { message: error instanceof Error ? error.message : String(error), }); - return WASI_ERRNO_FAULT; + return mapHostProcessError(error); } }, - proc_waitpid(pid, options, retStatusPtr, retPidPtr) { - const requestedPid = Number(pid) >>> 0; + proc_exec( + execPathPtr, + execPathLen, + argvPtr, + argvLen, + envpPtr, + envpLen, + cloexecFdsPtr, + cloexecFdsLen, + ) { if (permissionTier !== 'full') { - return requestedPid === 0xffffffff ? WASI_ERRNO_CHILD : WASI_ERRNO_SRCH; - } - const record = - requestedPid === 0xffffffff - ? spawnedChildren.values().next().value - : spawnedChildren.get(requestedPid); - if (!record) { - return requestedPid === 0xffffffff ? WASI_ERRNO_CHILD : WASI_ERRNO_SRCH; + return WASI_ERRNO_PERM; } - try { - const nonBlocking = (Number(options) >>> 0) !== 0; - traceHostProcess('proc-waitpid-begin', { - requestedPid, - childId: record.childId, - pid: record.pid, - }); - if (typeof record.exitStatus === 'number') { - if (writeGuestUint32(retStatusPtr, record.exitStatus) !== WASI_ERRNO_SUCCESS) { - return WASI_ERRNO_FAULT; + const command = readGuestString(execPathPtr, execPathLen); + if (command.length === 0) { + return WASI_ERRNO_NOENT; + } + const argv = decodeNullSeparatedStrings(readGuestBytes(argvPtr, argvLen)); + const env = parseSerializedEnv(readGuestBytes(envpPtr, envpLen)); + const closeFds = readExecCloexecFds(cloexecFdsPtr, cloexecFdsLen); + let replacement; + try { + replacement = loadExecImageFromPath(command, argv); + } catch (loadError) { + // The trusted sidecar owns AgentOS image selection. The local + // runner only replaces WASM->WASM after successful compilation; + // an otherwise valid non-WASM image is prepared and committed + // by the sidecar without ever resuming this image. + if (SIDECAR_EXEC_COMMIT_RPC && loadError?.code === 'ENOEXEC') { + const inheritedIgnores = [...wasmSignalRegistrations.entries()] + .filter(([, registration]) => registration.action === 'ignore') + .map(([signal]) => signal); + callSyncRpc('process.exec', [{ + command, + args: argv.slice(1), + options: { + argv0: argv[0] ?? command, + env, + shell: false, + cloexecFds: kernelCloexecFdsForCommit(closeFds), + localReplacement: false, + internalBootstrapEnv: { + AGENTOS_WASM_INITIAL_SIGNAL_MASK: JSON.stringify([...wasmBlockedSignals]), + AGENTOS_WASM_INITIAL_SIGNAL_IGNORES: JSON.stringify(inheritedIgnores), + AGENTOS_WASM_INITIAL_PENDING_SIGNALS: JSON.stringify([...pendingWasmSignals]), + ...inheritedNofileBootstrapEnv(), + }, + }, + }]); + const returned = new Error( + 'cross-runtime process.exec returned after committing the replacement image', + ); + returned.code = 'EIO'; + throw returned; } - const writePidResult = writeGuestUint32(retPidPtr, record.pid); - if (writePidResult !== WASI_ERRNO_SUCCESS) { - return writePidResult; + throw loadError; + } + let sidecarCommitted = false; + if (SIDECAR_EXEC_COMMIT_RPC) { + const result = callSyncRpc('process.exec', [{ + command, + args: replacement.argv.slice(1), + options: { + argv0: replacement.argv[0] ?? command, + env, + shell: false, + cloexecFds: kernelCloexecFdsForCommit(closeFds), + localReplacement: true, + internalBootstrapEnv: inheritedNofileBootstrapEnv(), + }, + }]); + if (result?.committed !== true) { + const error = new Error('process.exec did not confirm the sidecar commit'); + error.code = 'EIO'; + throw error; + } + sidecarCommitted = true; + } + throw { + marker: EXEC_REPLACEMENT_MARKER, + image: { + command, + module: replacement.module, + argv: replacement.argv, + env, + closeFds, + sidecarCommitted, + }, + }; + } catch (error) { + if (isExecReplacement(error)) throw error; + traceHostProcess('proc-exec-fault', { + code: error?.code ?? null, + message: error instanceof Error ? error.message : String(error), + }); + return mapHostProcessError(error); + } + }, + proc_fexec( + execFd, + argvPtr, + argvLen, + envpPtr, + envpLen, + cloexecFdsPtr, + cloexecFdsLen, + ) { + if (permissionTier !== 'full') { + return WASI_ERRNO_PERM; + } + try { + const descriptor = Number(execFd) >>> 0; + const argv = decodeNullSeparatedStrings(readGuestBytes(argvPtr, argvLen)); + const env = parseSerializedEnv(readGuestBytes(envpPtr, envpLen)); + const closeFds = readExecCloexecFds(cloexecFdsPtr, cloexecFdsLen); + const replacement = loadExecImageFromFd(descriptor, argv, closeFds); + let sidecarCommitted = false; + if (SIDECAR_EXEC_COMMIT_RPC) { + const result = callSyncRpc('process.exec_fd_image_commit', [{ + command: replacement.scriptRef, + args: replacement.argv.slice(1), + options: { + argv0: replacement.argv[0] ?? replacement.scriptRef, + env, + shell: false, + cloexecFds: kernelCloexecFdsForCommit(closeFds), + localReplacement: true, + executableFd: canonicalKernelFdForSpawnAction(descriptor), + internalBootstrapEnv: inheritedNofileBootstrapEnv(), + }, + }]); + if (result?.committed !== true) { + const error = new Error( + 'process.exec_fd_image_commit did not confirm the sidecar commit', + ); + error.code = 'EIO'; + throw error; } - reapSpawnedChild(record); - return writePidResult; + sidecarCommitted = true; } + throw { + marker: EXEC_REPLACEMENT_MARKER, + image: { + command: replacement.scriptRef, + module: replacement.module, + argv: replacement.argv, + env, + closeFds, + sidecarCommitted, + }, + }; + } catch (error) { + if (isExecReplacement(error)) throw error; + traceHostProcess('proc-fexec-fault', { + code: error?.code ?? null, + message: error instanceof Error ? error.message : String(error), + }); + return mapHostProcessError(error); + } + }, + proc_waitpid(pid, options, retStatusPtr, retPidPtr) { + const requestedPid = Number(pid) >>> 0; + if (permissionTier !== 'full') { + return WASI_ERRNO_CHILD; + } + const waitAny = requestedPid === 0xffffffff; + if (!waitAny && !spawnedChildren.has(requestedPid)) { + return WASI_ERRNO_CHILD; + } + if (spawnedChildren.size === 0) { + return WASI_ERRNO_CHILD; + } + try { + const normalizedOptions = Number(options) >>> 0; + const nonBlocking = (normalizedOptions & 1) !== 0; + if ((normalizedOptions & ~1) !== 0) { + return WASI_ERRNO_INVAL; + } while (true) { - const event = pollChildEvent( - record, - nonBlocking ? 0 : 10, - ); - if (!event) { - if (!pumpChildInputPipe(record, nonBlocking ? 0 : 10)) { - if (nonBlocking) { - return writeGuestUint32(retPidPtr, 0); - } + const records = waitAny + ? Array.from(spawnedChildren.values()) + : [spawnedChildren.get(requestedPid)].filter(Boolean); + if (records.length === 0) { + return WASI_ERRNO_CHILD; + } + for (const record of records) { + if (typeof record.exitStatus === 'number') { + return returnLegacyWaitedChild(record, retStatusPtr, retPidPtr); + } + pumpChildInputPipe(record, 0); + const event = pollChildEvent(record, 0); + if (event) { + processChildEvent(record, event); + } + if (typeof record.exitStatus === 'number') { + return returnLegacyWaitedChild(record, retStatusPtr, retPidPtr); } - continue; } - traceHostProcess('proc-waitpid-poll', { - requestedPid, - childId: record.childId, - type: event.type, - }); + if (nonBlocking) { + if (writeGuestUint32(retStatusPtr, 0) !== WASI_ERRNO_SUCCESS) { + return WASI_ERRNO_FAULT; + } + // WNOHANG must not acquire an all-sibling scheduling quantum: + // servicing a sibling's synchronous RPC can itself wait. The + // matching child received the zero-time probe above. + return writeGuestUint32(retPidPtr, 0); + } + // waitpid(pid) limits which child may be reaped; it does not + // stop sibling processes from running. All WASM descendants + // share this cooperative runner, so pump every live child while + // waiting for the selected one. Otherwise a selected child can + // deadlock waiting on output that only a sibling can produce. + pumpSpawnedChildren(SPAWNED_CHILD_WAIT_SLICE_MS); + const readyRecord = records.find( + (record) => typeof record.exitStatus === 'number', + ); + if (readyRecord) { + return returnLegacyWaitedChild(readyRecord, retStatusPtr, retPidPtr); + } + // A matching status wins over a simultaneously delivered + // signal. Otherwise a caught signal (including SIGCHLD from a + // non-selected sibling) interrupts blocking waitpid on Linux. + if (dispatchPendingWasmSignals()) { + return WASI_ERRNO_INTR; + } + } + } catch (error) { + traceHostProcess('proc-waitpid-legacy-fault', { + requestedPid, + message: error instanceof Error ? error.message : String(error), + }); + return WASI_ERRNO_FAULT; + } + }, + proc_waitpid_v2( + pid, + options, + retExitCodePtr, + retSignalPtr, + retPidPtr, + retCoreDumpedPtr, + ) { + const requestedPid = Number(pid) >>> 0; + if (permissionTier !== 'full') { + return WASI_ERRNO_CHILD; + } + const waitAny = requestedPid === 0xffffffff; + if (!waitAny && !spawnedChildren.has(requestedPid)) { + // Linux waitpid reports ECHILD when pid does not name a child of + // this process. ESRCH is reserved for operations such as kill(2). + return WASI_ERRNO_CHILD; + } + if (spawnedChildren.size === 0) { + return WASI_ERRNO_CHILD; + } + + try { + const normalizedOptions = Number(options) >>> 0; + const nonBlocking = (normalizedOptions & 1) !== 0; + if ((normalizedOptions & ~1) !== 0) { + return WASI_ERRNO_INVAL; + } + traceHostProcess('proc-waitpid-begin', { + requestedPid, + waitAny, + }); + + while (true) { + const records = waitAny + ? Array.from(spawnedChildren.values()) + : [spawnedChildren.get(requestedPid)].filter(Boolean); + if (records.length === 0) { + return WASI_ERRNO_CHILD; + } - if (event.type === 'stdout' && record.stdoutFd !== 0xffffffff) { - const chunk = decodeSyncRpcValue(event.data); - if (chunk?.length > 0) { - routeChunkToFd(record.stdoutFd, chunk); + for (const record of records) { + if (typeof record.exitStatus === 'number') { + return returnWaitedChild( + record, + retExitCodePtr, + retSignalPtr, + retPidPtr, + retCoreDumpedPtr, + ); } - continue; + + pumpChildInputPipe(record, 0); + const event = pollChildEvent(record, 0); + if (!event) { + continue; + } + traceHostProcess('proc-waitpid-poll', { + requestedPid, + childId: record.childId, + type: event.type, + }); + processChildEvent(record, event); + if (typeof record.exitStatus === 'number') { + return returnWaitedChild( + record, + retExitCodePtr, + retSignalPtr, + retPidPtr, + retCoreDumpedPtr, + ); + } + } + + if (nonBlocking) { + // Preserve WNOHANG's nonblocking contract. A zero-time sweep + // of every sibling can still block while servicing their + // internal synchronous RPCs. + return writeGuestUint32(retPidPtr, 0); + } + + // No child was ready. Block briefly on one child, then rescan all + // children so waitpid(-1) returns whichever one changes state + // first instead of waiting for map insertion order. + // Keep non-selected siblings scheduled while waitpid limits + // only which child status may be returned to the caller. + pumpSpawnedChildren(SPAWNED_CHILD_WAIT_SLICE_MS); + const readyRecord = records.find( + (record) => typeof record.exitStatus === 'number', + ); + if (readyRecord) { + return returnWaitedChild( + readyRecord, + retExitCodePtr, + retSignalPtr, + retPidPtr, + retCoreDumpedPtr, + ); + } + if (dispatchPendingWasmSignals()) { + return WASI_ERRNO_INTR; + } + } + } catch (error) { + traceHostProcess('proc-waitpid-fault', { + requestedPid, + message: error instanceof Error ? error.message : String(error), + }); + return WASI_ERRNO_FAULT; + } + }, + proc_waitpid_v3(pid, options, retStatusPtr, retPidPtr) { + const requestedPid = Number(pid) | 0; + if (permissionTier !== 'full') { + return WASI_ERRNO_CHILD; + } + const normalizedOptions = Number(options) >>> 0; + const waitNoHang = (normalizedOptions & 1) !== 0; + const waitUntraced = (normalizedOptions & 2) !== 0; + const waitContinued = (normalizedOptions & 8) !== 0; + if ((normalizedOptions & ~(1 | 2 | 8)) !== 0) { + return WASI_ERRNO_INVAL; + } + + try { + const callerProcessGroup = + requestedPid === 0 + ? Number(callSyncRpc('process.getpgid', [VIRTUAL_PID])) >>> 0 + : 0; + const matchingRecords = () => { + if (requestedPid > 0) { + return [spawnedChildren.get(requestedPid)].filter(Boolean); + } + if (requestedPid === -1) { + return Array.from(spawnedChildren.values()); + } + const selectedGroup = + requestedPid === 0 ? callerProcessGroup : Math.abs(requestedPid) >>> 0; + return Array.from(spawnedChildren.values()).filter( + (record) => record.processGroup === selectedGroup, + ); + }; + + if (matchingRecords().length === 0) { + return WASI_ERRNO_CHILD; + } + + while (true) { + const records = matchingRecords(); + if (records.length === 0) { + return WASI_ERRNO_CHILD; } - if (event.type === 'stderr' && record.stderrFd !== 0xffffffff) { - const chunk = decodeSyncRpcValue(event.data); - if (chunk?.length > 0) { - routeChunkToFd(record.stderrFd, chunk); + if ( + (waitUntraced || waitContinued) && + records.some((record) => record.synthetic !== true) + ) { + const transition = callSyncRpc('process.waitpid_transition', [ + requestedPid, + normalizedOptions, + ]); + if (transition && typeof transition.pid === 'number') { + const transitionedRecord = spawnedChildren.get( + Number(transition.pid) >>> 0, + ); + if (transitionedRecord && records.includes(transitionedRecord)) { + // Deliver the notification handler before returning the + // matching state change, without converting success to + // EINTR. This is the same ordering as child exit status. + dispatchPendingWasmSignals(); + if ( + writeGuestUint32(retStatusPtr, Number(transition.status) >>> 0) !== + WASI_ERRNO_SUCCESS + ) { + return WASI_ERRNO_FAULT; + } + return writeGuestUint32(retPidPtr, transitionedRecord.pid); + } } - continue; } - if (event.type === 'signal') { - processChildEvent(record, event); - continue; + for (const record of records) { + if (typeof record.rawWaitStatus === 'number') { + return returnRawWaitedChild(record, retStatusPtr, retPidPtr); + } + pumpChildInputPipe(record, 0); + const event = pollChildEvent(record, 0); + if (event) { + processChildEvent(record, event); + } + if (typeof record.rawWaitStatus === 'number') { + return returnRawWaitedChild(record, retStatusPtr, retPidPtr); + } } - if (event.type === 'exit') { - processChildEvent(record, event); - if (writeGuestUint32(retStatusPtr, record.exitStatus ?? 1) !== WASI_ERRNO_SUCCESS) { + if (waitNoHang) { + if (writeGuestUint32(retStatusPtr, 0) !== WASI_ERRNO_SUCCESS) { return WASI_ERRNO_FAULT; } - const writePidResult = writeGuestUint32(retPidPtr, record.pid); - if (writePidResult !== WASI_ERRNO_SUCCESS) { - return writePidResult; + // Do not turn WNOHANG into an O(children) RPC-service pass; + // the matching set received nonblocking probes above. + return writeGuestUint32(retPidPtr, 0); + } + + // Linux continues scheduling every child while the parent + // blocks in waitpid(pid). The cooperative WASM runner must do + // the same even though only a matching status can be reaped. + pumpSpawnedChildren(SPAWNED_CHILD_WAIT_SLICE_MS); + if ( + (waitUntraced || waitContinued) && + records.some((record) => record.synthetic !== true) + ) { + const transition = callSyncRpc('process.waitpid_transition', [ + requestedPid, + normalizedOptions, + ]); + if (transition && typeof transition.pid === 'number') { + const transitionedRecord = spawnedChildren.get( + Number(transition.pid) >>> 0, + ); + if (transitionedRecord && records.includes(transitionedRecord)) { + dispatchPendingWasmSignals(); + if ( + writeGuestUint32(retStatusPtr, Number(transition.status) >>> 0) !== + WASI_ERRNO_SUCCESS + ) { + return WASI_ERRNO_FAULT; + } + return writeGuestUint32(retPidPtr, transitionedRecord.pid); + } } - reapSpawnedChild(record); - return writePidResult; + } + const readyRecord = records.find( + (record) => typeof record.rawWaitStatus === 'number', + ); + if (readyRecord) { + return returnRawWaitedChild(readyRecord, retStatusPtr, retPidPtr); + } + if (dispatchPendingWasmSignals()) { + return WASI_ERRNO_INTR; } } - } catch { - traceHostProcess('proc-waitpid-fault', { + } catch (error) { + traceHostProcess('proc-waitpid-v3-fault', { requestedPid, - childId: record.childId, - pid: record.pid, + message: error instanceof Error ? error.message : String(error), }); - return WASI_ERRNO_FAULT; + return mapHostProcessError(error); } }, proc_kill(pid, signal) { @@ -3981,17 +7308,34 @@ const hostProcessImport = { return WASI_ERRNO_SRCH; } const targetPid = Number(pid) >>> 0; - const signalName = signalNameFromNumber(signal); + const numericSignal = Number(signal) >>> 0; + const signalName = signalNameFromNumber(numericSignal); try { if (targetPid === VIRTUAL_PID) { - callSyncRpc('process.kill', [VIRTUAL_PID, signalName]); - if ( - Number(signal) > 0 && - typeof instance?.exports?.__wasi_signal_trampoline === 'function' - ) { - instance.exports.__wasi_signal_trampoline(Number(signal) | 0); + // Signal zero only probes existence and permissions. Default + // dispositions must be enforced by the sidecar so termination, + // stop/continue, and wait status remain kernel-owned. A caught + // self-signal stays local so blocking, coalescing, sa_mask, + // SA_NODEFER, and SA_RESETHAND all use the same delivery path as + // externally delivered WASM signals. + if (numericSignal === 0) { + callSyncRpc('process.kill', [VIRTUAL_PID, signalName]); + return WASI_ERRNO_SUCCESS; + } + const registration = wasmSignalRegistrations.get(numericSignal); + if (registration?.action === 'ignore') { + return WASI_ERRNO_SUCCESS; } + if (registration?.action === 'user') { + if (wasmBlockedSignals.has(numericSignal)) { + pendingWasmSignals.add(numericSignal); + } else { + dispatchWasmSignal(numericSignal); + } + return WASI_ERRNO_SUCCESS; + } + callSyncRpc('process.kill', [VIRTUAL_PID, signalName]); return WASI_ERRNO_SUCCESS; } @@ -4016,47 +7360,199 @@ const hostProcessImport = { proc_getppid(retPidPtr) { return writeGuestUint32(retPidPtr, VIRTUAL_PPID); }, + proc_getrlimit(resource, retSoftPtr, retHardPtr) { + // Linux RLIMIT_NOFILE is resource 7. The typed per-execution value + // originates at limits.resources.maxOpenFds and is already the + // enforcement cap used by this runner's descriptor tables. + if ((Number(resource) >>> 0) !== 7) { + return WASI_ERRNO_NOTSUP; + } + const softResult = writeGuestUint64(retSoftPtr, rlimitNofileSoft); + if (softResult !== WASI_ERRNO_SUCCESS) { + return softResult; + } + return writeGuestUint64(retHardPtr, rlimitNofileHard); + }, + proc_setrlimit(resource, soft, hard) { + if ((Number(resource) >>> 0) !== 7) { + return WASI_ERRNO_NOTSUP; + } + const requestedSoft = BigInt.asUintN(64, BigInt(soft)); + const requestedHard = BigInt.asUintN(64, BigInt(hard)); + if (requestedSoft > requestedHard) { + return WASI_ERRNO_INVAL; + } + if (requestedHard > BigInt(rlimitNofileHard)) { + return WASI_ERRNO_PERM; + } + if ( + requestedSoft > BigInt(Number.MAX_SAFE_INTEGER) || + requestedHard > BigInt(Number.MAX_SAFE_INTEGER) + ) { + return WASI_ERRNO_INVAL; + } + rlimitNofileSoft = Number(requestedSoft); + rlimitNofileHard = Number(requestedHard); + warnedAboutOpenFdLimit = false; + return WASI_ERRNO_SUCCESS; + }, + proc_umask(mask, retPreviousPtr) { + try { + const previous = Number( + callSyncRpc('process.umask', [Number(mask) & 0o777]), + ) >>> 0; + return writeGuestUint32(retPreviousPtr, previous); + } catch (error) { + return mapHostProcessError(error); + } + }, + proc_itimer_real(operation, valueUs, intervalUs, retRemainingUsPtr, retIntervalUsPtr) { + try { + const numericOperation = Number(operation) >>> 0; + if (numericOperation > 1) { + return WASI_ERRNO_INVAL; + } + const value = Number(valueUs); + const interval = Number(intervalUs); + if ( + !Number.isSafeInteger(value) || + value < 0 || + !Number.isSafeInteger(interval) || + interval < 0 + ) { + return WASI_ERRNO_INVAL; + } + const result = callSyncRpc('process.itimer_real', [ + numericOperation, + value, + interval, + ]); + if ( + writeGuestUint64( + retRemainingUsPtr, + BigInt(result?.remainingUs ?? 0), + ) !== WASI_ERRNO_SUCCESS + ) { + return WASI_ERRNO_FAULT; + } + return writeGuestUint64( + retIntervalUsPtr, + BigInt(result?.intervalUs ?? 0), + ); + } catch (error) { + return mapHostProcessError(error); + } + }, + proc_getpgid(pid, retPgidPtr) { + if (permissionTier !== 'full') { + return WASI_ERRNO_SRCH; + } + const requestedPid = Number(pid) | 0; + if (requestedPid < 0) { + return WASI_ERRNO_SRCH; + } + try { + const targetPid = requestedPid === 0 ? VIRTUAL_PID : requestedPid; + const pgid = Number(callSyncRpc('process.getpgid', [targetPid])); + if (!Number.isSafeInteger(pgid) || pgid <= 0 || pgid > 0xffffffff) { + return WASI_ERRNO_FAULT; + } + return writeGuestUint32(retPgidPtr, pgid); + } catch (error) { + return mapHostProcessError(error); + } + }, + proc_setpgid(pid, pgid) { + if (permissionTier !== 'full') { + return WASI_ERRNO_SRCH; + } + const requestedPid = Number(pid) | 0; + const requestedPgid = Number(pgid) | 0; + if (requestedPid < 0 || requestedPgid < 0) { + return WASI_ERRNO_INVAL; + } + try { + const targetPid = requestedPid === 0 ? VIRTUAL_PID : requestedPid; + callSyncRpc('process.setpgid', [targetPid, requestedPgid]); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + }, fd_pipe(retReadFdPtr, retWriteFdPtr) { + let readFd = null; + let writeFd = null; try { - const pipe = { - id: nextSyntheticPipeId++, - chunks: [], - consumers: new Map(), - producers: new Map(), - readHandleCount: 0, - writeHandleCount: 0, - }; - const readFd = nextSyntheticFd++; - const writeFd = nextSyntheticFd++; - syntheticFdEntries.set(readFd, createPipeHandle('pipe-read', pipe, readFd)); - syntheticFdEntries.set(writeFd, createPipeHandle('pipe-write', pipe, writeFd)); + if (!hasRunnerOpenFdCapacity(2)) return WASI_ERRNO_MFILE; + if (!SIDECAR_MANAGED_PROCESS) { + const pipe = { + id: nextSyntheticPipeId++, + chunks: [], + consumers: new Map(), + producers: new Map(), + readHandleCount: 0, + writeHandleCount: 0, + }; + readFd = allocateSyntheticFd(nextSyntheticFd, true); + writeFd = allocateSyntheticFd(nextSyntheticFd, true); + if (readFd == null || writeFd == null) return WASI_ERRNO_MFILE; + syntheticFdEntries.set(readFd, createPipeHandle('pipe-read', pipe, readFd)); + syntheticFdEntries.set(writeFd, createPipeHandle('pipe-write', pipe, writeFd)); + } else { + const result = callSyncRpc('process.fd_pipe'); + readFd = registerKernelDelegateFd(result?.readFd); + writeFd = registerKernelDelegateFd(result?.writeFd); + } if (writeGuestUint32(retReadFdPtr, readFd) !== WASI_ERRNO_SUCCESS) { + wasiImport.fd_close(readFd); + wasiImport.fd_close(writeFd); return WASI_ERRNO_FAULT; } - return writeGuestUint32(retWriteFdPtr, writeFd); - } catch { - return WASI_ERRNO_FAULT; + if (writeGuestUint32(retWriteFdPtr, writeFd) !== WASI_ERRNO_SUCCESS) { + wasiImport.fd_close(readFd); + wasiImport.fd_close(writeFd); + return WASI_ERRNO_FAULT; + } + return WASI_ERRNO_SUCCESS; + } catch (error) { + if (readFd != null) wasiImport.fd_close(readFd); + if (writeFd != null) wasiImport.fd_close(writeFd); + return mapHostProcessError(error); } }, fd_dup(fd, retNewFdPtr) { try { + const hostNetSource = hostNetSockets.get(Number(fd) >>> 0); + if (hostNetSource) { + const duplicatedFd = allocateHostNetDuplicateFd(0); + if (duplicatedFd == null) return WASI_ERRNO_MFILE; + hostNetSockets.set(duplicatedFd, hostNetSource); + runnerCloexecFds.delete(duplicatedFd); + if (writeGuestUint32(retNewFdPtr, duplicatedFd) !== WASI_ERRNO_SUCCESS) { + hostNetImport.net_close(duplicatedFd); + return WASI_ERRNO_FAULT; + } + return WASI_ERRNO_SUCCESS; + } + const source = lookupFdHandle(fd); + if (source?.kind === 'kernel-fd') { + const duplicatedFd = registerKernelDelegateFd( + callSyncRpc('process.fd_dup', [Number(source.targetFd) >>> 0]), + ); + if (writeGuestUint32(retNewFdPtr, duplicatedFd) !== WASI_ERRNO_SUCCESS) { + wasiImport.fd_close(duplicatedFd); + return WASI_ERRNO_FAULT; + } + return WASI_ERRNO_SUCCESS; + } const handle = cloneFdHandle(fd); if (!handle) { return WASI_ERRNO_BADF; } - let duplicatedFd = 0; - while ( - duplicatedFd <= 2 && - ( - syntheticFdEntries.has(duplicatedFd) || - passthroughHandles.has(duplicatedFd) || - delegateManagedFdRefCounts.has(duplicatedFd) - ) - ) { - duplicatedFd += 1; - } - if (duplicatedFd > 2) { - duplicatedFd = nextSyntheticFd++; + const duplicatedFd = allocateSyntheticFd(0); + if (duplicatedFd == null) { + releaseFdHandle(handle); + return WASI_ERRNO_MFILE; } syntheticFdEntries.set(duplicatedFd, handle); traceHostProcess('fd-dup', { @@ -4075,8 +7571,11 @@ const hostProcessImport = { try { const sourceFd = Number(oldFd) >>> 0; const targetFd = Number(newFd) >>> 0; + if (sourceFd >= LINUX_GUEST_FD_LIMIT || targetFd >= LINUX_GUEST_FD_LIMIT) { + return WASI_ERRNO_BADF; + } if (sourceFd === targetFd) { - if (!lookupFdHandle(sourceFd)) { + if (!lookupFdHandle(sourceFd) && !hostNetSockets.has(sourceFd)) { return WASI_ERRNO_BADF; } traceHostProcess('fd-dup2-same-fd', { @@ -4085,11 +7584,88 @@ const hostProcessImport = { }); return WASI_ERRNO_SUCCESS; } + if (targetFd >= rlimitNofileSoft) { + return WASI_ERRNO_BADF; + } + + const hostNetSource = hostNetSockets.get(sourceFd); + if (hostNetSource) { + const targetWasOpen = runnerOpenFdSet().has(targetFd); + if (!targetWasOpen && !hasRunnerOpenFdCapacity(1)) { + return WASI_ERRNO_MFILE; + } + const closeResult = wasiImport.fd_close(targetFd); + if (closeResult !== WASI_ERRNO_SUCCESS && closeResult !== WASI_ERRNO_BADF) { + return closeResult; + } + hostNetSockets.set(targetFd, hostNetSource); + // dup2(2) always clears FD_CLOEXEC on the replacement descriptor. + runnerCloexecFds.delete(targetFd); + closedPassthroughFds.delete(targetFd); + return WASI_ERRNO_SUCCESS; + } + + const kernelSource = lookupFdHandle(sourceFd); + if (kernelSource?.kind === 'kernel-fd') { + const targetHandle = lookupFdHandle(targetFd); + const targetIsInternalPreopen = targetHandle?.internalPreopen === true; + const shadowsInternalPreopen = hiddenPreopenHandles.has(targetFd); + const targetPassthroughHasAliases = + targetHandle?.kind === 'passthrough' && + Number(targetHandle.refCount) > 0; + const replacesBootstrapStdio = + targetFd <= 2 && + targetHandle?.kind === 'passthrough' && + Number(targetHandle.targetFd) === targetFd && + Number(targetHandle.refCount) === 0; + if (!replacesBootstrapStdio) { + const closeResult = targetIsInternalPreopen + ? (() => { + // The node:wasi preopen is hidden runtime + // infrastructure, not the Linux descriptor being + // replaced. Remove only its guest-visible mirrors so + // kernel fd targetFd shadows it; retain fdTable/backing + // state for future path resolution. + passthroughHandles.delete(targetFd); + delegateManagedFdRefCounts.delete(targetFd); + return WASI_ERRNO_SUCCESS; + })() + : wasiImport.fd_close(targetFd); + if (closeResult !== WASI_ERRNO_SUCCESS && closeResult !== WASI_ERRNO_BADF) { + return closeResult; + } + if ( + !shadowsInternalPreopen && + !targetPassthroughHasAliases && + wasi?.fdTable?.has?.(targetFd) === true + ) { + // closePassthroughFd already closed the backing preopen fd. + // node:wasi retains a stale bookkeeping entry, which must be + // removed without attempting to close that resource twice. + // A runner-level dup alias keeps the same backing entry live, + // so never delete it while such aliases still exist. + wasi.fdTable.delete(targetFd); + } + } + // Guest and kernel fd numbers are separate namespaces. Allocate + // a fresh kernel descriptor, then install it at the exact guest + // destination; using the raw guest number as a kernel dup2 + // target can alias the source after preopen collisions. + const duplicatedKernelFd = callSyncRpc('process.fd_dup', [ + Number(kernelSource.targetFd) >>> 0, + ]); + registerKernelDelegateFd(duplicatedKernelFd, targetFd, 3, shadowsInternalPreopen); + return WASI_ERRNO_SUCCESS; + } const sourceHandle = cloneFdHandle(sourceFd); if (!sourceHandle) { return WASI_ERRNO_BADF; } + if (!syntheticFdInUse(targetFd) && !hasRunnerOpenFdCapacity(1)) { + releaseFdHandle(sourceHandle); + return WASI_ERRNO_MFILE; + } traceHostProcess('fd-dup2-begin', { oldFd: sourceFd, @@ -4100,16 +7676,30 @@ const hostProcessImport = { existingKind: syntheticFdEntries.get(targetFd)?.kind ?? passthroughHandles.get(targetFd)?.kind ?? null, }); + if (hostNetSockets.has(targetFd)) { + const closeResult = hostNetImport.net_close(targetFd); + if (closeResult !== WASI_ERRNO_SUCCESS) { + releaseFdHandle(sourceHandle); + return closeResult; + } + } closeSyntheticFd(targetFd); closePassthroughFd(targetFd); syntheticFdEntries.set(targetFd, sourceHandle); + closedPassthroughFds.delete(targetFd); traceHostProcess('fd-dup2-installed', { oldFd: sourceFd, newFd: targetFd, sourceKind: sourceHandle.kind, }); return WASI_ERRNO_SUCCESS; - } catch { + } catch (error) { + traceHostProcess('fd-dup2-fault', { + oldFd: Number(oldFd) >>> 0, + newFd: Number(newFd) >>> 0, + code: error?.code ?? null, + message: error instanceof Error ? error.message : String(error), + }); return WASI_ERRNO_FAULT; } }, @@ -4123,21 +7713,55 @@ const hostProcessImport = { if (!Number.isInteger(minimumFdNumber) || minimumFdNumber < 0) { return WASI_ERRNO_INVAL; } + if (minimumFdNumber >= LINUX_GUEST_FD_LIMIT) { + return WASI_ERRNO_INVAL; + } + if (minimumFdNumber >= rlimitNofileSoft) { + return WASI_ERRNO_INVAL; + } + + const hostNetSource = hostNetSockets.get(sourceFd); + if (hostNetSource) { + const duplicatedFd = allocateHostNetDuplicateFd(minimumFdNumber); + if (duplicatedFd == null) return WASI_ERRNO_MFILE; + hostNetSockets.set(duplicatedFd, hostNetSource); + runnerCloexecFds.delete(duplicatedFd); + if (writeGuestUint32(retNewFdPtr, duplicatedFd) !== WASI_ERRNO_SUCCESS) { + hostNetImport.net_close(duplicatedFd); + return WASI_ERRNO_FAULT; + } + return WASI_ERRNO_SUCCESS; + } + + const kernelSource = lookupFdHandle(sourceFd); + if (kernelSource?.kind === 'kernel-fd') { + // F_DUPFD's lower bound belongs to the guest descriptor table, + // not the sidecar kernel's private backing-fd namespace. Asking + // the kernel for fd >= minFd can exceed its bounded table for a + // perfectly valid guest request such as F_DUPFD(512). + const duplicatedFd = registerKernelDelegateFd( + callSyncRpc('process.fd_dup', [Number(kernelSource.targetFd) >>> 0]), + null, + minimumFdNumber, + ); + if (writeGuestUint32(retNewFdPtr, duplicatedFd) !== WASI_ERRNO_SUCCESS) { + wasiImport.fd_close(duplicatedFd); + return WASI_ERRNO_FAULT; + } + return WASI_ERRNO_SUCCESS; + } const handle = cloneFdHandle(sourceFd); if (!handle) { return WASI_ERRNO_BADF; } - let duplicatedFd = minimumFdNumber >>> 0; - while ( - syntheticFdEntries.has(duplicatedFd) || - passthroughHandles.has(duplicatedFd) || - delegateManagedFdRefCounts.has(duplicatedFd) - ) { - duplicatedFd += 1; + const duplicatedFd = allocateSyntheticFd(minimumFdNumber); + + if (duplicatedFd == null) { + releaseFdHandle(handle); + return WASI_ERRNO_MFILE; } - nextSyntheticFd = Math.max(nextSyntheticFd, duplicatedFd + 1); syntheticFdEntries.set(duplicatedFd, handle); traceHostProcess('fd-dup-min', { @@ -4149,13 +7773,502 @@ const hostProcessImport = { displayFd: handle.displayFd ?? null, }); return writeGuestUint32(retNewFdPtr, duplicatedFd); - } catch { - return WASI_ERRNO_FAULT; + } catch (error) { + traceHostProcess('fd-dup-min-fault', { + fd: Number(fd), + minimumFd: Number(minFd), + code: error?.code ?? null, + message: error instanceof Error ? error.message : String(error), + }); + return mapHostProcessError(error); + } + }, + fd_getfd(fd, retFlagsPtr) { + try { + const numericFd = Number(fd) >>> 0; + const handle = lookupFdHandle(numericFd); + let flags; + if (handle?.kind === 'kernel-fd') { + flags = Number(callSyncRpc('process.fd_getfd', [Number(handle.targetFd) >>> 0])); + } else if (handle || hostNetSockets.has(numericFd)) { + flags = runnerCloexecFds.has(numericFd) ? 1 : 0; + } else { + return WASI_ERRNO_BADF; + } + if (!Number.isSafeInteger(flags) || flags < 0 || flags > 0xffffffff) { + return WASI_ERRNO_IO; + } + return writeGuestUint32(retFlagsPtr, flags); + } catch (error) { + return mapHostProcessError(error); + } + }, + fd_setfd(fd, flags) { + try { + const numericFd = Number(fd) >>> 0; + const normalizedFlags = Number(flags) >>> 0; + const handle = lookupFdHandle(numericFd); + if (handle?.kind === 'kernel-fd') { + callSyncRpc('process.fd_setfd', [ + Number(handle.targetFd) >>> 0, + normalizedFlags, + ]); + } else if (!handle && !hostNetSockets.has(numericFd)) { + return WASI_ERRNO_BADF; + } + if ((normalizedFlags & 1) !== 0) { + runnerCloexecFds.add(numericFd); + } else { + runnerCloexecFds.delete(numericFd); + } + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + }, + fd_record_lock( + fd, + command, + lockType, + startOffset, + length, + retTypePtr, + retPidPtr, + retStartPtr, + retLengthPtr, + ) { + const numericCommand = Number(command) >>> 0; + let blockingWaitRegistered = false; + const cancelBlockingLockWait = () => { + callSyncRpc('process.fd_record_lock_cancel', []); + blockingWaitRegistered = false; + }; + try { + const numericFd = Number(fd) >>> 0; + const handle = lookupFdHandle(numericFd); + if (handle?.kind !== 'kernel-fd') { + // A runner-local or host-network descriptor has no stable VFS + // inode identity. Never report a lock that the kernel cannot + // enforce against other VM processes. + return handle || hostNetSockets.has(numericFd) + ? WASI_ERRNO_NOTSUP + : WASI_ERRNO_BADF; + } + const normalizedStart = BigInt(startOffset); + const normalizedLength = BigInt(length); + if (normalizedStart < 0n || normalizedLength < 0n) { + return WASI_ERRNO_INVAL; + } + const lockWaitStartedAt = Date.now(); + const lockWaitDeadline = lockWaitStartedAt + unixConnectTimeoutMs; + const lockWaitWarningAt = + lockWaitStartedAt + Math.floor(unixConnectTimeoutMs * 0.8); + let warnedNearLockWaitLimit = false; + let response; + while (true) { + try { + response = callSyncRpc('process.fd_record_lock', [ + Number(handle.targetFd) >>> 0, + numericCommand, + Number(lockType) >>> 0, + normalizedStart.toString(), + normalizedLength.toString(), + ]); + blockingWaitRegistered = false; + break; + } catch (error) { + if ( + numericCommand !== 14 || + (error?.code !== 'EAGAIN' && error?.code !== 'EWOULDBLOCK') + ) { + throw error; + } + blockingWaitRegistered = true; + const now = Date.now(); + if (!warnedNearLockWaitLimit && now >= lockWaitWarningAt) { + warnedNearLockWaitLimit = true; + process.stderr.write( + `[agentos] F_SETLKW is nearing limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms)\n`, + ); + } + if (now >= lockWaitDeadline) { + cancelBlockingLockWait(); + process.stderr.write( + `[agentos] F_SETLKW exceeded limits.resources.maxBlockingReadMs (${unixConnectTimeoutMs} ms); raise limits.resources.maxBlockingReadMs if needed\n`, + ); + return WASI_ERRNO_TIMEDOUT; + } + // The sidecar's sync-RPC dispatcher must stay nonblocking so + // another VM process can reach close/unlock. Suspend in the + // runner instead, advancing descendants and yielding briefly + // to independently scheduled processes between retries. + if (dispatchPendingWasmSignals()) { + cancelBlockingLockWait(); + return WASI_ERRNO_INTR; + } + pumpSpawnedChildrenOrWait(SPAWNED_CHILD_WAIT_SLICE_MS); + if (dispatchPendingWasmSignals()) { + cancelBlockingLockWait(); + return WASI_ERRNO_INTR; + } + } + } + if (!response || typeof response !== 'object') { + return WASI_ERRNO_IO; + } + if (numericCommand !== 12) { + return WASI_ERRNO_SUCCESS; + } + for (const errno of [ + writeGuestUint32(retTypePtr, Number(response.type) >>> 0), + writeGuestUint32(retPidPtr, Number(response.pid) >>> 0), + writeGuestUint64(retStartPtr, BigInt(response.start)), + writeGuestUint64(retLengthPtr, BigInt(response.length)), + ]) { + if (errno !== WASI_ERRNO_SUCCESS) return errno; + } + return WASI_ERRNO_SUCCESS; + } catch (error) { + if (blockingWaitRegistered) { + try { + cancelBlockingLockWait(); + } catch (cancelError) { + return mapHostProcessError(cancelError); + } + } + return mapHostProcessError(error); + } + }, + proc_closefrom(lowFd) { + const minimumFd = Number(lowFd) >>> 0; + const openVirtualFds = new Set([ + ...syntheticFdEntries.keys(), + ...passthroughHandles.keys(), + ...retainedSpawnOutputHandlesByFd.keys(), + ...retainedSyntheticHandlesByDisplayFd.keys(), + ...hostNetSockets.keys(), + ...delegateManagedFdRefCounts.keys(), + ...(wasi?.fdTable?.keys?.() ?? []), + ]); + let firstError = WASI_ERRNO_SUCCESS; + for (const fd of [...openVirtualFds].sort((left, right) => left - right)) { + if (fd < minimumFd) { + continue; + } + // WASI preopens are hidden path-resolution capabilities, not + // Linux process descriptors. Remove the public untagged alias, + // but retain the private tagged handle used by libc path lookup. + if ( + passthroughHandles.get(fd)?.internalPreopen === true || + hiddenPreopenHandles.has(fd) + ) { + passthroughHandles.delete(fd); + closedPassthroughFds.add(fd); + runnerCloexecFds.delete(fd); + continue; + } + const result = wasiImport.fd_close(fd); + if ( + result !== WASI_ERRNO_SUCCESS && + result !== WASI_ERRNO_BADF && + firstError === WASI_ERRNO_SUCCESS + ) { + firstError = result; + } + } + return firstError; + }, + fd_socketpair(socketKind, nonblocking, closeOnExec, retFirstPtr, retSecondPtr) { + let firstFd = null; + let secondFd = null; + try { + if (!hasRunnerOpenFdCapacity(2)) return WASI_ERRNO_MFILE; + const result = callSyncRpc('process.fd_socketpair', [ + Number(socketKind) >>> 0, + Number(nonblocking) !== 0, + Number(closeOnExec) !== 0, + ]); + firstFd = registerKernelDelegateFd(result?.firstFd); + secondFd = registerKernelDelegateFd(result?.secondFd); + const firstWrite = writeGuestUint32(retFirstPtr, firstFd); + const secondWrite = writeGuestUint32(retSecondPtr, secondFd); + if (firstWrite !== WASI_ERRNO_SUCCESS || secondWrite !== WASI_ERRNO_SUCCESS) { + wasiImport.fd_close(firstFd); + wasiImport.fd_close(secondFd); + return WASI_ERRNO_FAULT; + } + return WASI_ERRNO_SUCCESS; + } catch (error) { + if (firstFd != null) wasiImport.fd_close(firstFd); + if (secondFd != null) wasiImport.fd_close(secondFd); + return mapHostProcessError(error); + } + }, + fd_sendmsg_rights(socketFd, dataPtr, dataLen, rightsPtr, rightsLen, flags, retSentPtr) { + try { + if (!(instanceMemory instanceof WebAssembly.Memory)) return WASI_ERRNO_FAULT; + const byteLength = Number(dataLen) >>> 0; + const rightsLength = Number(rightsLen) >>> 0; + const memoryLength = instanceMemory.buffer.byteLength; + const dataOffset = Number(dataPtr) >>> 0; + const rightsOffset = Number(rightsPtr) >>> 0; + if (dataOffset > memoryLength || byteLength > memoryLength - dataOffset) { + return WASI_ERRNO_FAULT; + } + if ( + rightsLength > 253 || + rightsOffset > memoryLength || + rightsLength * 4 > memoryLength - rightsOffset + ) { + return rightsLength > 253 ? WASI_ERRNO_INVAL : WASI_ERRNO_FAULT; + } + const bytes = Buffer.from( + new Uint8Array(instanceMemory.buffer, dataOffset, byteLength), + ); + const view = new DataView(instanceMemory.buffer); + const rights = []; + for (let index = 0; index < rightsLength; index += 1) { + const guestFd = view.getUint32(rightsOffset + index * 4, true); + if (hostNetSockets.has(guestFd)) { + const socket = hostNetSockets.get(guestFd); + rights.push({ + kind: 'hostNet', + socketId: socket.socketId ?? null, + serverId: socket.serverId ?? null, + udpSocketId: socket.udpSocketId ?? null, + domain: Number(socket.domain) >>> 0, + socketType: Number(socket.sockType) >>> 0, + protocol: Number(socket.protocol) >>> 0, + nonblocking: socket.nonblock === true, + recvTimeoutMs: socket.recvTimeoutMs ?? null, + bindOptions: socket.bindOptions ?? null, + localInfo: socket.localInfo ?? null, + localUnixAddress: socket.localUnixAddress ?? null, + localReservation: socket.localReservation ?? null, + remoteInfo: socket.remoteInfo ?? null, + remoteUnixAddress: socket.remoteUnixAddress ?? null, + listening: socket.listening === true, + }); + continue; + } + const handle = lookupFdHandle(guestFd); + const kernelFd = handle?.kind === 'kernel-fd' + ? Number(handle.targetFd) >>> 0 + : delegateManagedFdRefCounts.has(guestFd) + ? guestFd + : null; + if (kernelFd == null) { + const error = new Error(`bad transferable file descriptor ${guestFd}`); + error.code = 'EBADF'; + throw error; + } + rights.push(kernelFd); + } + const sent = Number(callSyncRpc('process.fd_sendmsg_rights', [ + canonicalKernelFdForSpawnAction(socketFd), + bytes, + rights, + Number(flags) >>> 0, + ])); + traceHostProcess('fd-sendmsg-rights', { + guestSocketFd: Number(socketFd) >>> 0, + kernelSocketFd: canonicalKernelFdForSpawnAction(socketFd), + rights, + bytes: bytes.length, + sent, + }); + if (!Number.isSafeInteger(sent) || sent < 0 || sent > byteLength) { + return WASI_ERRNO_FAULT; + } + const writeResult = writeGuestUint32(retSentPtr, sent); + return writeResult === WASI_ERRNO_SUCCESS ? WASI_ERRNO_SUCCESS : WASI_ERRNO_FAULT; + } catch (error) { + return mapHostProcessError(error); + } + }, + fd_recvmsg_rights( + socketFd, + dataPtr, + dataLen, + rightsPtr, + rightsCapacity, + flags, + retReceivedPtr, + retRightsLenPtr, + retMessageFlagsPtr, + ) { + const installedFds = []; + try { + if (!(instanceMemory instanceof WebAssembly.Memory)) return WASI_ERRNO_FAULT; + const byteLength = Number(dataLen) >>> 0; + const rightsLength = Number(rightsCapacity) >>> 0; + const memoryLength = instanceMemory.buffer.byteLength; + const dataOffset = Number(dataPtr) >>> 0; + const rightsOffset = Number(rightsPtr) >>> 0; + if (dataOffset > memoryLength || byteLength > memoryLength - dataOffset) { + return WASI_ERRNO_FAULT; + } + if ( + rightsLength > 253 || + rightsOffset > memoryLength || + rightsLength * 4 > memoryLength - rightsOffset + ) { + return rightsLength > 253 ? WASI_ERRNO_INVAL : WASI_ERRNO_FAULT; + } + const numericFlags = Number(flags) >>> 0; + const kernelSocketFd = canonicalKernelFdForSpawnAction(socketFd); + const socketStat = callSyncRpc('process.fd_stat', [kernelSocketFd]); + const dontwait = + (numericFlags & 0x0040) !== 0 || + (Number(socketStat?.flags) & KERNEL_O_NONBLOCK) !== 0; + const requestArgs = [ + kernelSocketFd, + byteLength, + rightsLength, + (numericFlags & 0x40000000) !== 0, + (numericFlags & 0x0002) !== 0, + // A blocking sidecar recv monopolizes the dispatch loop and can + // deadlock against an inherited child whose sendmsg is waiting + // for that same loop. Poll the kernel non-blocking and pump child + // events between attempts. This preserves Linux blocking + // behavior while allowing the child send transaction to run. + true, + (numericFlags & 0x0100) !== 0, + ]; + const deadline = dontwait + ? Date.now() + : maxBlockingReadMs == null + ? null + : Date.now() + maxBlockingReadMs; + let result; + while (result == null) { + try { + result = callSyncRpc('process.fd_recvmsg_rights', requestArgs); + } catch (error) { + if (error?.code !== 'EAGAIN' && error?.code !== 'EWOULDBLOCK') { + throw error; + } + if (dontwait) { + throw error; + } + if (deadline != null && Date.now() >= deadline) { + const timeout = new Error( + 'blocking socket receive timed out; raise limits.resources.maxBlockingReadMs', + ); + timeout.code = 'EAGAIN'; + throw timeout; + } + const progressed = pumpSpawnedChildren(10); + dispatchPendingWasmSignals(); + if (!progressed && spawnedChildren.size === 0) { + Atomics.wait(syntheticWaitArray, 0, 0, 1); + } + } + } + traceHostProcess('fd-recvmsg-rights', { + guestSocketFd: Number(socketFd) >>> 0, + kernelSocketFd, + result, + }); + const bytes = Buffer.from(result?.data ?? []); + const receivedRights = Array.isArray(result?.rights) ? result.rights : []; + if (bytes.length > byteLength || receivedRights.length > rightsLength) { + return WASI_ERRNO_FAULT; + } + const memory = new Uint8Array(instanceMemory.buffer); + const view = new DataView(instanceMemory.buffer); + memory.set(bytes, dataOffset); + let installedCount = 0; + let localControlTruncated = result?.controlTruncated === true; + for (const received of receivedRights) { + let fd; + if (received?.kind === 'kernel') { + fd = registerKernelDelegateFd(received.fd); + } else if (received?.kind === 'hostNet') { + fd = allocateHostNetSocketFd(); + if (fd == null) { + localControlTruncated = true; + if (typeof received.socketId === 'string') { + callSyncRpc('net.destroy', [received.socketId]); + } + if (typeof received.serverId === 'string') { + callSyncRpc('net.server_close', [received.serverId]); + } + if (typeof received.udpSocketId === 'string') { + callSyncRpc('dgram.close', [received.udpSocketId]); + } + if (typeof received.localReservation === 'string') { + callSyncRpc('net.release_tcp_port', [received.localReservation]); + } + continue; + } + hostNetSockets.set(fd, { + domain: Number(received.domain) >>> 0, + sockType: Number(received.socketType) >>> 0, + protocol: Number(received.protocol) >>> 0, + bindOptions: received.bindOptions ?? null, + localInfo: received.localInfo ?? normalizeHostNetAddressInfo( + received.localAddress, + received.localPort, + ), + localUnixAddress: received.localUnixAddress ?? null, + localReservation: received.localReservation ?? null, + remoteInfo: received.remoteInfo ?? normalizeHostNetAddressInfo( + received.remoteAddress, + received.remotePort, + ), + remoteUnixAddress: received.remoteUnixAddress ?? null, + listening: received.listening === true, + serverId: received.serverId ?? null, + socketId: received.socketId ?? null, + udpSocketId: received.udpSocketId ?? null, + pendingDatagram: null, + recvTimeoutMs: received.recvTimeoutMs ?? null, + readChunks: [], + pendingAccepts: [], + readableEnded: false, + closed: false, + lastError: null, + nonblock: received.nonblocking === true, + }); + } else { + return WASI_ERRNO_FAULT; + } + installedFds.push(fd); + view.setUint32(rightsOffset + installedCount * 4, fd, true); + installedCount += 1; + } + const internalFlags = (result?.payloadTruncated === true ? 1 : 0) + | (localControlTruncated ? 2 : 0) + | ((Number(result?.fullLength) >>> 0) << 2); + for (const [ptr, value] of [ + [retReceivedPtr, bytes.length], + [retRightsLenPtr, installedCount], + [retMessageFlagsPtr, internalFlags], + ]) { + if (writeGuestUint32(ptr, value) !== WASI_ERRNO_SUCCESS) { + for (const fd of installedFds) wasiImport.fd_close(fd); + return WASI_ERRNO_FAULT; + } + } + return WASI_ERRNO_SUCCESS; + } catch (error) { + for (const fd of installedFds) wasiImport.fd_close(fd); + return mapHostProcessError(error); } }, sleep_ms(milliseconds) { try { - Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Number(milliseconds) >>> 0); + const waitArray = new Int32Array(new SharedArrayBuffer(4)); + const deadline = Date.now() + (Number(milliseconds) >>> 0); + while (Date.now() < deadline) { + // Keep guest sleeps interruptible by V8 termination during SIGTERM, + // SIGKILL, and VM disposal. Also drain handled Wasm signals at + // syscall boundaries so cooperative handlers run during sleeps. + dispatchPendingWasmSignals(); + Atomics.wait(waitArray, 0, 0, Math.max(1, Math.min(10, deadline - Date.now()))); + } + dispatchPendingWasmSignals(); return WASI_ERRNO_SUCCESS; } catch { return WASI_ERRNO_FAULT; @@ -4174,20 +8287,128 @@ const hostProcessImport = { mask: decodeSignalMask(maskLo, maskHi), flags: Number(flags) >>> 0, }; - emitControlMessage({ - type: 'signal_state', - signal: Number(signal) >>> 0, - registration, + callSyncRpc('process.signal_state', [ + Number(signal) >>> 0, + registration.action, + JSON.stringify(registration.mask), + registration.flags, + ]); + const numericSignal = Number(signal) >>> 0; + if (registration.action === 'default') { + wasmSignalRegistrations.delete(numericSignal); + } else { + wasmSignalRegistrations.set(numericSignal, registration); + } + traceHostProcess('proc-sigaction', { + signal: numericSignal, + action: registration.action, + mask: registration.mask, + flags: registration.flags, }); return WASI_ERRNO_SUCCESS; } catch { return WASI_ERRNO_FAULT; } }, + proc_signal_mask_v2(how, setLo, setHi, retOldLoPtr, retOldHiPtr) { + if (permissionTier !== 'full') { + return WASI_ERRNO_FAULT; + } + try { + const previous = encodeSignalMask(wasmBlockedSignals); + writeGuestUint32(retOldLoPtr, previous.lo); + writeGuestUint32(retOldHiPtr, previous.hi); + const operation = Number(how) >>> 0; + if (operation === 3) { + return WASI_ERRNO_SUCCESS; + } + if (operation > 2) { + return WASI_ERRNO_INVAL; + } + const requested = decodeSignalMask(setLo, setHi).filter( + (signal) => signal !== LINUX_SIGKILL && signal !== LINUX_SIGSTOP, + ); + if (operation === 0) { + for (const signal of requested) { + wasmBlockedSignals.add(signal); + } + } else if (operation === 1) { + for (const signal of requested) { + wasmBlockedSignals.delete(signal); + } + } else { + wasmBlockedSignals.clear(); + for (const signal of requested) { + wasmBlockedSignals.add(signal); + } + } + dispatchPendingWasmSignals(); + return WASI_ERRNO_SUCCESS; + } catch { + return WASI_ERRNO_FAULT; + } + }, + proc_ppoll_v1( + fdsPtr, + nfds, + timeoutSec, + timeoutNsec, + sigmaskLo, + sigmaskHi, + hasSigmask, + retReadyPtr, + ) { + if (permissionTier !== 'full') { + return WASI_ERRNO_PERM; + } + const previousMask = new Set(wasmBlockedSignals); + try { + const seconds = BigInt(timeoutSec); + const nanoseconds = BigInt(timeoutNsec); + let timeoutMs = -1; + if (seconds >= 0n || nanoseconds >= 0n) { + if (seconds < 0n || nanoseconds < 0n || nanoseconds >= 1_000_000_000n) { + return WASI_ERRNO_INVAL; + } + const milliseconds = seconds * 1000n + (nanoseconds + 999_999n) / 1_000_000n; + timeoutMs = Number(milliseconds > 2_147_483_647n ? 2_147_483_647n : milliseconds); + } + if ((Number(hasSigmask) >>> 0) !== 0) { + wasmBlockedSignals.clear(); + for (const signal of decodeSignalMask(sigmaskLo, sigmaskHi)) { + if (signal !== LINUX_SIGKILL && signal !== LINUX_SIGSTOP) { + wasmBlockedSignals.add(signal); + } + } + } + // No guest code runs between the mask swap and the first poll + // boundary. That boundary drains pending signals and returns + // EINTR when it invokes an unblocked caught handler. + return hostNetImport.net_poll(fdsPtr, nfds, timeoutMs, retReadyPtr); + } catch { + return WASI_ERRNO_FAULT; + } finally { + wasmBlockedSignals.clear(); + for (const signal of previousMask) { + wasmBlockedSignals.add(signal); + } + // A signal may have arrived while blocked only by ppoll's + // temporary mask. Linux runs that now-unblocked handler before + // returning to user code without rewriting a successful poll + // result, so drain after restoration rather than dropping it. + dispatchPendingWasmSignals(); + } + }, }; const limitedHostProcessImport = { fd_dup_min: hostProcessImport.fd_dup_min, + fd_getfd: hostProcessImport.fd_getfd, + fd_setfd: hostProcessImport.fd_setfd, + fd_record_lock: hostProcessImport.fd_record_lock, + proc_getrlimit: hostProcessImport.proc_getrlimit, + proc_setrlimit: hostProcessImport.proc_setrlimit, + proc_umask: hostProcessImport.proc_umask, }; const hostUserImport = { @@ -4229,18 +8450,65 @@ const HOST_FS_GUEST_CWD = for (let index = 0; index < WASI_PREOPEN_ENTRIES.length; index += 1) { const fd = WASI_PREOPEN_FD_BASE + index; const [guestPath, preopenSpec] = WASI_PREOPEN_ENTRIES[index]; - if (!passthroughHandles.has(fd)) { + const preopenHandle = { + kind: 'passthrough', + targetFd: fd, + displayFd: fd, + refCount: 0, + open: true, + guestPath: guestPathForPreopenKey(guestPath), + readOnly: preopenSpec?.readOnly === true, + internalPreopen: true, + }; + // node:wasi always owns this capability descriptor, even when the Linux + // guest namespace starts with the same descriptor closed or inherited from + // the kernel. Patched libc reaches that private descriptor through the + // tagged alias; only expose the untagged descriptor when it is actually free + // in the guest descriptor table. + hiddenPreopenHandles.set(fd, preopenHandle); + if (initialClosedGuestFds.has(fd)) { + // Keep the private tagged capability for libc path resolution, but make + // the same untagged Linux descriptor observably closed (including + // fd_prestat_get) after spawn close/closefrom actions. + closedPassthroughFds.add(fd); + } else if ( + !initialMappedGuestFds.has(fd) && + !passthroughHandles.has(fd) + ) { retainDelegateFd(fd); closedPassthroughFds.delete(fd); - passthroughHandles.set(fd, { - kind: 'passthrough', - targetFd: fd, - displayFd: fd, - refCount: 0, - open: true, - guestPath: guestPathForPreopenKey(guestPath), - readOnly: preopenSpec?.readOnly === true, - }); + passthroughHandles.set(fd, preopenHandle); + } +} + +if (SIDECAR_MANAGED_PROCESS) { + const inheritedEntries = callSyncRpc('process.fd_snapshot', []); + if (!Array.isArray(inheritedEntries) || inheritedEntries.length > configuredMaxOpenFds) { + throw new Error( + `kernel descriptor snapshot exceeds the ${configuredMaxOpenFds}-descriptor runtime limit`, + ); + } + inheritedEntries.sort((left, right) => Number(left?.fd) - Number(right?.fd)); + for (const entry of inheritedEntries) { + const kernelFd = Number(entry?.fd); + if (!Number.isSafeInteger(kernelFd) || kernelFd < 0 || kernelFd > 0xffffffff) { + throw new Error(`kernel descriptor snapshot contains invalid fd ${String(entry?.fd)}`); + } + const mappedGuestFd = initialKernelFdMappings.get(kernelFd); + // The kernel always has canonical stdio entries. Leave an unmapped entry + // on Node's bootstrap handle, but do not discard a POSIX-spawn dup2 that + // deliberately installed a pipe at kernel fd 0/1/2. The explicit inverse + // mapping is what distinguishes inherited transport from default stdio. + if (kernelFd <= 2 && mappedGuestFd == null) { + continue; + } + const guestFd = registerKernelDelegateFd( + kernelFd, + mappedGuestFd ?? null, + ); + if ((Number(entry?.fdFlags) & 1) !== 0) { + runnerCloexecFds.add(guestFd); + } } } @@ -4312,15 +8580,24 @@ function resolveHostFsMapping(value, fromGuestDir = HOST_FS_GUEST_CWD) { const hostFsImport = { fd_mode(fd) { const descriptor = Number(fd) >>> 0; - if (descriptor <= 2) { - return HOST_FS_MODE_CHARACTER; - } - const handle = lookupFdHandle(descriptor); if (handle?.kind === 'pipe-read' || handle?.kind === 'pipe-write') { return HOST_FS_MODE_FIFO; } + if (handle?.kind === 'kernel-fd') { + try { + const stat = callSyncRpc('process.fd_filestat', [Number(handle.targetFd) >>> 0]); + return Number(stat?.mode) >>> 0; + } catch { + return 0; + } + } + + if (descriptor <= 2) { + return HOST_FS_MODE_CHARACTER; + } + try { const targetFd = typeof handle?.ioFd === 'number' @@ -4337,6 +8614,10 @@ const hostFsImport = { const descriptor = Number(fd) >>> 0; try { const handle = lookupFdHandle(descriptor); + if (handle?.kind === 'kernel-fd') { + const stat = callSyncRpc('process.fd_filestat', [Number(handle.targetFd) >>> 0]); + return BigInt(stat?.size ?? -1); + } const rememberedSize = rememberedHostFsSize(handle?.guestPath); if (rememberedSize != null) { return rememberedSize; @@ -4418,14 +8699,14 @@ const hostFsImport = { try { const target = resolvePathOpenGuestPath(fd, pathPtr, pathLen); if (typeof target !== 'string') { - return 1; + return WASI_ERRNO_NOENT; } const mapping = resolveHostFsMapping(target); if (!mapping || typeof mapping.hostPath !== 'string') { - return 1; + return WASI_ERRNO_NOENT; } if (mapping.readOnly) { - return 1; + return WASI_ERRNO_ROFS; } traceHostProcess('host-fs-chmod', { target, @@ -4434,22 +8715,34 @@ const hostFsImport = { }); chmodMappedGuestPath(target, mapping.hostPath, Number(mode) >>> 0); return 0; - } catch { - traceHostProcess('host-fs-chmod-fault', {}); - return 1; + } catch (error) { + traceHostProcess('host-fs-chmod-fault', { + message: error instanceof Error ? error.message : String(error), + }); + return mapSyntheticFsError(error); } }, fchmod(fd, mode) { try { const descriptor = Number(fd) >>> 0; const handle = lookupFdHandle(descriptor); + if (handle?.kind === 'kernel-fd') { + callSyncRpc('process.fd_chmod', [ + Number(handle.targetFd) >>> 0, + Number(mode) >>> 0, + ]); + return WASI_ERRNO_SUCCESS; + } if (handle?.readOnly === true) { - return 1; + return WASI_ERRNO_ROFS; } if (typeof handle?.guestPath === 'string') { const mapping = resolveHostFsMapping(handle.guestPath); - if (!mapping || typeof mapping.hostPath !== 'string' || mapping.readOnly) { - return 1; + if (!mapping || typeof mapping.hostPath !== 'string') { + return WASI_ERRNO_NOENT; + } + if (mapping.readOnly) { + return WASI_ERRNO_ROFS; } chmodMappedGuestPath(handle.guestPath, mapping.hostPath, Number(mode) >>> 0); return 0; @@ -4458,9 +8751,52 @@ const hostFsImport = { typeof handle?.targetFd === 'number' ? Number(handle.targetFd) >>> 0 : descriptor; fsModule.fchmodSync(targetFd, Number(mode) >>> 0); return 0; - } catch { - traceHostProcess('host-fs-fchmod-fault', {}); - return 1; + } catch (error) { + traceHostProcess('host-fs-fchmod-fault', { + message: error instanceof Error ? error.message : String(error), + }); + return mapSyntheticFsError(error); + } + }, + chown(fd, pathPtr, pathLen, uid, gid, followSymlinks) { + try { + const operand = kernelPathOperand(fd, pathPtr, pathLen); + if (!operand) { + return WASI_ERRNO_BADF; + } + callSyncRpc('process.path_chown_at', [ + operand.dirFd, + operand.path, + Number(uid) >>> 0, + Number(gid) >>> 0, + Number(followSymlinks) !== 0, + ]); + return WASI_ERRNO_SUCCESS; + } catch (error) { + traceHostProcess('host-fs-chown-fault', { + message: error instanceof Error ? error.message : String(error), + }); + return mapSyntheticFsError(error); + } + }, + fchown(fd, uid, gid) { + try { + const descriptor = Number(fd) >>> 0; + const handle = lookupFdHandle(descriptor); + if (handle?.kind !== 'kernel-fd') { + return WASI_ERRNO_BADF; + } + callSyncRpc('process.fd_chown', [ + Number(handle.targetFd) >>> 0, + Number(uid) >>> 0, + Number(gid) >>> 0, + ]); + return WASI_ERRNO_SUCCESS; + } catch (error) { + traceHostProcess('host-fs-fchown-fault', { + message: error instanceof Error ? error.message : String(error), + }); + return mapSyntheticFsError(error); } }, ftruncate(fd, length) { @@ -4565,7 +8901,11 @@ if (delegatePathOpen) { const passthroughDirHandle = __agentOSWasiMeasurePhase('path_open', 'lookup_handle', () => lookupFdHandle(fd) ); - if (passthroughDirHandle && passthroughDirHandle.kind !== 'passthrough') { + if ( + passthroughDirHandle && + passthroughDirHandle.kind !== 'passthrough' && + passthroughDirHandle.kind !== 'kernel-fd' + ) { return WASI_ERRNO_BADF; } if (!passthroughDirHandle && rejectClosedPassthroughFd(fd)) { @@ -4586,6 +8926,57 @@ if (delegatePathOpen) { if (guestReadOnlyDenied) { return denyReadOnlyMutation(); } + const procFdResult = openProcSelfFdAlias( + guestPath, + oflags, + rightsBase, + dirflags, + openedFdPtr, + ); + if (procFdResult !== null) { + return procFdResult; + } + if (SIDECAR_MANAGED_PROCESS) { + if (typeof guestPath !== 'string') { + return WASI_ERRNO_BADF; + } + if (!hasRunnerOpenFdCapacity(1)) { + return WASI_ERRNO_MFILE; + } + try { + const kernelFd = Number( + passthroughDirHandle?.kind === 'kernel-fd' + ? callSyncRpc('process.path_open_at', [ + Number(passthroughDirHandle.targetFd) >>> 0, + readGuestString(pathPtr, pathLen), + kernelOpenFlagsFromWasi(oflags, rightsBase, fdflags, dirflags), + 0o666, + ]) + : callSyncRpc('process.fd_open', [ + guestPath, + kernelOpenFlagsFromWasi(oflags, rightsBase, fdflags, dirflags), + 0o666, + ]) + ) >>> 0; + if ((Number(oflags) & WASI_OFLAGS_DIRECTORY) !== 0) { + const stat = callSyncRpc('process.fd_stat', [kernelFd]); + if ((Number(stat?.filetype) >>> 0) !== WASI_FILETYPE_DIRECTORY) { + callSyncRpc('process.fd_close', [kernelFd]); + const error = new Error(`${guestPath} is not a directory`); + error.code = 'ENOTDIR'; + throw error; + } + } + const openedFd = registerKernelDelegateFd(kernelFd); + const writeResult = writeGuestUint32(openedFdPtr, openedFd); + if (writeResult !== WASI_ERRNO_SUCCESS) { + wasiImport.fd_close(openedFd); + } + return writeResult; + } catch (error) { + return mapHostProcessError(error); + } + } const mayCreateTarget = pathOpenMayCreateTarget(oflags, rightsBase, fdflags); if (!SIDECAR_MANAGED_PROCESS && mayCreateTarget) { try { @@ -4610,6 +9001,10 @@ if (delegatePathOpen) { } } + if (!hasRunnerOpenFdCapacity(1)) { + return WASI_ERRNO_MFILE; + } + let result = __agentOSWasiMeasurePhase( 'path_open', 'delegate_call', @@ -4685,6 +9080,164 @@ if (delegatePathOpen) { }; } +function delegatePathDirFd(fd) { + const numericFd = Number(fd) >>> 0; + const handle = lookupFdHandle(numericFd); + if (handle?.kind !== 'passthrough') { + return null; + } + return Number(handle.targetFd) >>> 0; +} + +function kernelPathOperand(fd, pathPtr, pathLen) { + const handle = lookupFdHandle(fd); + if (handle?.kind === 'kernel-fd') { + return { + dirFd: Number(handle.targetFd) >>> 0, + path: readGuestString(pathPtr, pathLen), + }; + } + const resolved = resolvePathOpenGuestPath(fd, pathPtr, pathLen); + return typeof resolved === 'string' ? { dirFd: 0, path: resolved } : null; +} + +const kernelPathOperationHandlers = { + path_create_directory(args) { + const operand = kernelPathOperand(args[0], args[1], args[2]); + if (!operand) return WASI_ERRNO_BADF; + callSyncRpc('process.path_mkdir_at', [operand.dirFd, operand.path]); + return WASI_ERRNO_SUCCESS; + }, + path_filestat_get(args) { + const operand = kernelPathOperand(args[0], args[2], args[3]); + if (!operand) return WASI_ERRNO_BADF; + const stat = callSyncRpc('process.path_stat_at', [ + operand.dirFd, + operand.path, + ((Number(args[1]) >>> 0) & WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0, + ]); + return writeGuestFilestat(args[4], stat, Number(stat?.filetype) >>> 0); + }, + path_filestat_set_times(args) { + const operand = kernelPathOperand(args[0], args[2], args[3]); + if (!operand) return WASI_ERRNO_BADF; + callSyncRpc('process.path_utimes_at', [ + operand.dirFd, + operand.path, + ((Number(args[1]) >>> 0) & WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0, + BigInt(args[4]).toString(), + BigInt(args[5]).toString(), + Number(args[6]) >>> 0, + ]); + return WASI_ERRNO_SUCCESS; + }, + path_link(args) { + const oldOperand = kernelPathOperand(args[0], args[2], args[3]); + const newOperand = kernelPathOperand(args[4], args[5], args[6]); + if (!oldOperand || !newOperand) return WASI_ERRNO_BADF; + callSyncRpc('process.path_link_at', [ + oldOperand.dirFd, + oldOperand.path, + newOperand.dirFd, + newOperand.path, + ((Number(args[1]) >>> 0) & WASI_LOOKUPFLAGS_SYMLINK_FOLLOW) !== 0, + ]); + return WASI_ERRNO_SUCCESS; + }, + path_readlink(args) { + const operand = kernelPathOperand(args[0], args[1], args[2]); + if (!operand) return WASI_ERRNO_BADF; + const target = Buffer.from(String(callSyncRpc('process.path_readlink_at', [ + operand.dirFd, + kernelProcFdPathForGuestPath(operand.path), + ]))); + return writeGuestBytes(args[3], args[4], target, args[5]); + }, + path_remove_directory(args) { + const operand = kernelPathOperand(args[0], args[1], args[2]); + if (!operand) return WASI_ERRNO_BADF; + callSyncRpc('process.path_remove_dir_at', [operand.dirFd, operand.path]); + return WASI_ERRNO_SUCCESS; + }, + path_rename(args) { + const oldOperand = kernelPathOperand(args[0], args[1], args[2]); + const newOperand = kernelPathOperand(args[3], args[4], args[5]); + if (!oldOperand || !newOperand) return WASI_ERRNO_BADF; + callSyncRpc('process.path_rename_at', [ + oldOperand.dirFd, + oldOperand.path, + newOperand.dirFd, + newOperand.path, + ]); + return WASI_ERRNO_SUCCESS; + }, + path_symlink(args) { + const operand = kernelPathOperand(args[2], args[3], args[4]); + if (!operand) return WASI_ERRNO_BADF; + callSyncRpc('process.path_symlink_at', [ + readGuestString(args[0], args[1]), + operand.dirFd, + operand.path, + ]); + return WASI_ERRNO_SUCCESS; + }, + path_unlink_file(args) { + const operand = kernelPathOperand(args[0], args[1], args[2]); + if (!operand) return WASI_ERRNO_BADF; + callSyncRpc('process.path_unlink_at', [operand.dirFd, operand.path]); + return WASI_ERRNO_SUCCESS; + }, +}; + +// All WASI path operations take one or more capability directory descriptors. +// Keep node:wasi's private preopens outside the Linux guest fd namespace by +// translating only libc's tagged aliases. An untagged fd that has been closed +// or replaced must never fall through to node:wasi, where it could otherwise +// accidentally name the private preopen at the same numeric descriptor. +function wrapPathDirFds(name, fdIndexes) { + const delegate = typeof wasiImport[name] === 'function' ? wasiImport[name].bind(wasiImport) : null; + if (!delegate) { + return; + } + wasiImport[name] = (...args) => { + const delegateArgs = [...args]; + // A managed guest has one filesystem source of truth: the sidecar kernel. + // In particular, a relative mkdir through libc's hidden cwd/root preopen + // must not be delegated into node:wasi's private host directory while a + // subsequent path_open is sent to the kernel. That split made a freshly + // created empty directory disappear until a child file happened to copy + // its ancestors into the kernel overlay. + if (SIDECAR_MANAGED_PROCESS) { + try { + return kernelPathOperationHandlers[name](delegateArgs); + } catch (error) { + return mapHostProcessError(error); + } + } + if (fdIndexes.some((index) => lookupFdHandle(delegateArgs[index])?.kind === 'kernel-fd')) { + return WASI_ERRNO_BADF; + } + for (const index of fdIndexes) { + const delegateFd = delegatePathDirFd(delegateArgs[index]); + if (delegateFd == null) { + return WASI_ERRNO_BADF; + } + delegateArgs[index] = delegateFd; + } + return delegate(...delegateArgs); + }; +} + +wrapPathDirFds('path_create_directory', [0]); +wrapPathDirFds('path_filestat_get', [0]); +wrapPathDirFds('path_filestat_set_times', [0]); +wrapPathDirFds('path_link', [0, 4]); +wrapPathDirFds('path_readlink', [0]); +wrapPathDirFds('path_remove_directory', [0]); +wrapPathDirFds('path_rename', [0, 3]); +wrapPathDirFds('path_symlink', [2]); +wrapPathDirFds('path_unlink_file', [0]); + function wrapReadOnlyPathMutation(name, shouldDeny) { const delegate = typeof wasiImport[name] === 'function' ? wasiImport[name].bind(wasiImport) : null; if (!delegate) { @@ -4796,6 +9349,10 @@ const delegateManagedFdFilestatGet = typeof wasiImport.fd_filestat_get === 'function' ? wasiImport.fd_filestat_get.bind(wasiImport) : null; +const delegateManagedFdReaddir = + typeof wasiImport.fd_readdir === 'function' + ? wasiImport.fd_readdir.bind(wasiImport) + : null; const delegateManagedFdFilestatSetSize = typeof wasiImport.fd_filestat_set_size === 'function' ? wasiImport.fd_filestat_set_size.bind(wasiImport) @@ -4804,6 +9361,10 @@ const delegateManagedFdClose = typeof wasiImport.fd_close === 'function' ? wasiImport.fd_close.bind(wasiImport) : null; +const delegateManagedFdRenumber = + typeof wasiImport.fd_renumber === 'function' + ? wasiImport.fd_renumber.bind(wasiImport) + : null; const delegateManagedFdPrestatGet = typeof wasiImport.fd_prestat_get === 'function' ? wasiImport.fd_prestat_get.bind(wasiImport) @@ -4823,9 +9384,71 @@ const KERNEL_POLLHUP = 0x0010; wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { const numericFd = Number(fd) >>> 0; + const hostNetSocket = getHostNetSocket(numericFd); + if (hostNetSocket) { + return readHostNetSocketToGuestIovs(hostNetSocket, iovs, iovsLen, nreadPtr); + } + const handle = __agentOSWasiMeasurePhase('fd_read', 'lookup_handle', () => lookupFdHandle(numericFd) ); + if (handle?.kind === 'kernel-fd') { + try { + const requestedLength = guestIovByteLength(iovs, iovsLen); + const kernelFd = Number(handle.targetFd) >>> 0; + let bytes; + const stat = callSyncRpc('process.fd_stat', [kernelFd]); + const nonblocking = (Number(stat?.flags) & KERNEL_O_NONBLOCK) !== 0; + const deadline = nonblocking + ? Date.now() + : maxBlockingReadMs == null + ? null + : Date.now() + maxBlockingReadMs; + while (bytes == null) { + try { + // A process with local descendants must return to its own event pump + // between zero-time probes. A leaf process can instead issue a + // bounded wait: the sidecar parks descendant reads by reply token, + // freeing the parent/sibling dispatcher until data or EOF arrives. + const pumpsLocalChildren = hasActiveSpawnedChildren(); + const remainingMs = deadline == null + ? KERNEL_WAIT_SLICE_MS + : Math.max(0, deadline - Date.now()); + const waitMs = nonblocking || pumpsLocalChildren + ? 0 + : Math.min(KERNEL_WAIT_SLICE_MS, remainingMs); + bytes = Buffer.from(callSyncRpc('process.fd_read', [ + kernelFd, + requestedLength, + waitMs, + ]) ?? []); + } catch (error) { + if (error?.code !== 'EAGAIN' && error?.code !== 'EWOULDBLOCK') { + throw error; + } + if (nonblocking) { + throw error; + } + if (deadline != null && Date.now() >= deadline) { + const timeout = new Error( + 'blocking file descriptor read timed out; raise limits.resources.maxBlockingReadMs', + ); + timeout.code = 'EAGAIN'; + throw timeout; + } + const progressed = pumpSpawnedChildren(SPAWNED_CHILD_WAIT_SLICE_MS); + dispatchPendingWasmSignals(); + if (!progressed && !hasActiveSpawnedChildren()) { + Atomics.wait(syntheticWaitArray, 0, 0, 1); + } + } + } + const written = writeBytesToGuestIovs(iovs, iovsLen, bytes); + return writeGuestUint32(nreadPtr, written); + } catch (error) { + return mapHostProcessError(error); + } + } if (handle?.kind === 'pipe-read') { try { const requestedLength = __agentOSWasiMeasurePhase('fd_read', 'iov_scan', () => { @@ -4947,11 +9570,14 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { } if ( - numericFd === 0 && handle?.kind === 'passthrough' && - handle.targetFd === 0 && - passthroughHandles.get(0) === handle + handle.targetFd === 0 ) { + // dup(2) aliases share the same open file description as fd 0. In a + // sidecar-managed process they must therefore read the kernel stdin pipe, + // not the runner process's unrelated host stdin. OpenSSH duplicates stdin + // before its poll/read loop, so splitting these paths loses pipe EOF. + // https://man7.org/linux/man-pages/man2/dup.2.html const sidecarManagedProcess = typeof process?.env?.AGENTOS_SANDBOX_ROOT === 'string' && process.env.AGENTOS_SANDBOX_ROOT.length > 0; @@ -4994,11 +9620,13 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { } if (handle?.kind === 'passthrough') { - return delegateManagedFdRead - ? __agentOSWasiMeasurePhase('fd_read', 'delegate_call', () => - delegateManagedFdRead(handle.targetFd, iovs, iovsLen, nreadPtr) - ) - : WASI_ERRNO_BADF; + if (!delegateManagedFdRead) { + return WASI_ERRNO_BADF; + } + const result = __agentOSWasiMeasurePhase('fd_read', 'delegate_call', () => + delegateManagedFdRead(handle.targetFd, iovs, iovsLen, nreadPtr) + ); + return result; } if (rejectClosedPassthroughFd(numericFd)) { @@ -5012,8 +9640,105 @@ wasiImport.fd_read = (fd, iovs, iovsLen, nreadPtr) => { : WASI_ERRNO_BADF; }; +wasiImport.fd_readdir = (fd, bufPtr, bufLen, cookie, bufUsedPtr) => { + const numericFd = Number(fd) >>> 0; + const handle = lookupFdHandle(numericFd); + if (handle?.kind === 'kernel-fd') { + if (!(instanceMemory instanceof WebAssembly.Memory)) { + return WASI_ERRNO_FAULT; + } + + const bufferOffset = Number(bufPtr) >>> 0; + const bufferLength = Number(bufLen) >>> 0; + const memoryBytes = new Uint8Array(instanceMemory.buffer); + if ( + bufferOffset > memoryBytes.length || + bufferLength > memoryBytes.length - bufferOffset + ) { + return WASI_ERRNO_FAULT; + } + const zeroResult = writeGuestUint32(bufUsedPtr, 0); + if (zeroResult !== WASI_ERRNO_SUCCESS || bufferLength === 0) { + return zeroResult; + } + + try { + const numericCookie = BigInt(cookie); + const maxEntries = Math.min( + 4096, + Math.max(1, Math.floor(bufferLength / 24) + 1), + ); + const entries = callSyncRpc('process.fd_readdir', [ + Number(handle.targetFd) >>> 0, + numericCookie.toString(), + maxEntries, + ]); + if (!Array.isArray(entries)) { + return WASI_ERRNO_IO; + } + + let bytesUsed = 0; + for (const entry of entries) { + const nameBytes = Buffer.from(String(entry?.name ?? ''), 'utf8'); + const record = new Uint8Array(24 + nameBytes.length); + const recordView = new DataView( + record.buffer, + record.byteOffset, + record.byteLength, + ); + recordView.setBigUint64(0, BigInt(entry?.next ?? 0), true); + recordView.setBigUint64(8, BigInt(entry?.ino ?? 0), true); + recordView.setUint32(16, nameBytes.length, true); + recordView.setUint8(20, Number(entry?.filetype) >>> 0); + record.set(nameBytes, 24); + + const writable = Math.min(record.length, bufferLength - bytesUsed); + memoryBytes.set(record.subarray(0, writable), bufferOffset + bytesUsed); + bytesUsed += writable; + if (writable < record.length || bytesUsed === bufferLength) { + break; + } + } + return writeGuestUint32(bufUsedPtr, bytesUsed); + } catch (error) { + return mapHostProcessError(error); + } + } + + if (handle?.kind === 'passthrough') { + return delegateManagedFdReaddir + ? delegateManagedFdReaddir( + handle.targetFd, + bufPtr, + bufLen, + cookie, + bufUsedPtr, + ) + : WASI_ERRNO_BADF; + } + if (rejectClosedPassthroughFd(numericFd)) { + return WASI_ERRNO_BADF; + } + return delegateManagedFdReaddir + ? delegateManagedFdReaddir(numericFd, bufPtr, bufLen, cookie, bufUsedPtr) + : WASI_ERRNO_BADF; +}; + wasiImport.fd_pread = (fd, iovs, iovsLen, offset, nreadPtr) => { const handle = lookupFdHandle(fd); + if (handle?.kind === 'kernel-fd') { + try { + const requestedLength = guestIovByteLength(iovs, iovsLen); + const bytes = Buffer.from(callSyncRpc('process.fd_pread', [ + Number(handle.targetFd) >>> 0, + requestedLength, + BigInt(offset).toString(), + ]) ?? []); + return writeGuestUint32(nreadPtr, writeBytesToGuestIovs(iovs, iovsLen, bytes)); + } catch (error) { + return mapHostProcessError(error); + } + } if (handle?.kind === 'guest-file') { try { const requestedLength = (() => { @@ -5072,9 +9797,10 @@ wasiImport.fd_pread = (fd, iovs, iovsLen, offset, nreadPtr) => { return mapSyntheticFsError(error); } } - return delegateFdPread - ? delegateFdPread(handle.targetFd, iovs, iovsLen, offset, nreadPtr) - : WASI_ERRNO_BADF; + if (!delegateFdPread) { + return WASI_ERRNO_BADF; + } + return delegateFdPread(handle.targetFd, iovs, iovsLen, offset, nreadPtr); } if (rejectClosedPassthroughFd(fd)) { @@ -5088,7 +9814,26 @@ wasiImport.fd_pread = (fd, iovs, iovsLen, offset, nreadPtr) => { wasiImport.fd_pwrite = (fd, iovs, iovsLen, offset, nwrittenPtr) => { const handle = lookupFdHandle(fd); + if (handle?.kind === 'kernel-fd') { + try { + const bytes = collectGuestIovBytes(iovs, iovsLen); + const written = Number(callSyncRpc('process.fd_pwrite', [ + Number(handle.targetFd) >>> 0, + bytes, + BigInt(offset).toString(), + ])); + if (!Number.isSafeInteger(written) || written < 0 || written > bytes.length) { + return WASI_ERRNO_IO; + } + return writeGuestUint32(nwrittenPtr, written); + } catch (error) { + return mapHostProcessError(error); + } + } if (handle?.kind === 'guest-file') { + if (handle.readOnly === true) { + return WASI_ERRNO_ROFS; + } try { const bytes = collectGuestIovBytes(iovs, iovsLen); const written = fsModule.writeSync( @@ -5143,6 +9888,14 @@ wasiImport.fd_pwrite = (fd, iovs, iovsLen, offset, nwrittenPtr) => { wasiImport.fd_sync = (fd) => { const handle = lookupFdHandle(fd); + if (handle?.kind === 'kernel-fd') { + try { + callSyncRpc('process.fd_sync', [Number(handle.targetFd) >>> 0]); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + } if (handle?.kind === 'guest-file') { return WASI_ERRNO_SUCCESS; } @@ -5158,8 +9911,50 @@ wasiImport.fd_sync = (fd) => { return delegateFdSync ? delegateFdSync(fd) : WASI_ERRNO_SUCCESS; }; +wasiImport.fd_datasync = (fd) => { + const handle = lookupFdHandle(fd); + if (handle?.kind === 'kernel-fd') { + try { + callSyncRpc('process.fd_datasync', [Number(handle.targetFd) >>> 0]); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + } + if (handle?.kind === 'guest-file') { + return WASI_ERRNO_SUCCESS; + } + if (handle?.kind === 'passthrough') { + return delegateFdDatasync + ? delegateFdDatasync(handle.targetFd) + : delegateFdSync + ? delegateFdSync(handle.targetFd) + : WASI_ERRNO_SUCCESS; + } + if (rejectClosedPassthroughFd(fd)) { + return WASI_ERRNO_BADF; + } + return delegateFdDatasync + ? delegateFdDatasync(fd) + : delegateFdSync + ? delegateFdSync(fd) + : WASI_ERRNO_SUCCESS; +}; + wasiImport.fd_seek = (fd, offset, whence, newOffsetPtr) => { const handle = lookupFdHandle(fd); + if (handle?.kind === 'kernel-fd') { + try { + const next = callSyncRpc('process.fd_seek', [ + Number(handle.targetFd) >>> 0, + BigInt(offset).toString(), + Number(whence) >>> 0, + ]); + return writeGuestUint64(newOffsetPtr, BigInt(next)); + } catch (error) { + return mapHostProcessError(error); + } + } if (handle?.kind === 'guest-file') { try { const next = seekGuestFileHandle(handle, offset, whence); @@ -5204,6 +9999,18 @@ wasiImport.fd_seek = (fd, offset, whence, newOffsetPtr) => { wasiImport.fd_tell = (fd, offsetPtr) => { const handle = lookupFdHandle(fd); + if (handle?.kind === 'kernel-fd') { + try { + const next = callSyncRpc('process.fd_seek', [ + Number(handle.targetFd) >>> 0, + '0', + WASI_WHENCE_CUR, + ]); + return writeGuestUint64(offsetPtr, BigInt(next)); + } catch (error) { + return mapHostProcessError(error); + } + } if (handle?.kind === 'guest-file') { return writeGuestUint64(offsetPtr, BigInt(handle.position ?? 0)); } @@ -5231,9 +10038,54 @@ wasiImport.fd_tell = (fd, offsetPtr) => { }; wasiImport.fd_fdstat_get = (fd, statPtr) => { + // Host-net sockets (curl/wget/git TLS transports): report a stream-socket + // fdstat with the current O_NONBLOCK state so guest fcntl(F_GETFL) works. + // Without this, fcntl-based non-blocking setup fails with EBADF and guests + // that expect EAGAIN semantics (libcurl mid-upload reads) block forever. + { + const hostNetSocket = getHostNetSocket(fd); + if (hostNetSocket && !hostNetSocket.closed) { + return writeGuestFdstat( + statPtr, + WASI_FILETYPE_SOCKET_STREAM, + hostNetSocket.nonblock ? WASI_FDFLAGS_NONBLOCK : 0, + WASI_RIGHT_FD_READ | + WASI_RIGHT_FD_WRITE | + WASI_RIGHT_FD_FDSTAT_SET_FLAGS | + WASI_RIGHT_FD_FILESTAT_GET | + WASI_RIGHT_POLL_FD_READWRITE, + 0n, + ); + } + } const handle = __agentOSWasiMeasurePhase('fd_fdstat_get', 'lookup_handle', () => lookupFdHandle(fd) ); + if (handle?.kind === 'kernel-fd') { + try { + const stat = callSyncRpc('process.fd_stat', [Number(handle.targetFd) >>> 0]); + const kernelFlags = Number(stat?.flags) >>> 0; + const wasiFlags = (kernelFlags & KERNEL_O_APPEND ? WASI_FDFLAGS_APPEND : 0) + | (kernelFlags & KERNEL_O_NONBLOCK ? WASI_FDFLAGS_NONBLOCK : 0); + const result = writeGuestFdstat( + statPtr, + Number(stat?.filetype) >>> 0, + wasiFlags, + WASI_RIGHT_FD_READ | + WASI_RIGHT_FD_WRITE | + WASI_RIGHT_FD_SEEK | + WASI_RIGHT_FD_TELL | + WASI_RIGHT_FD_FDSTAT_SET_FLAGS | + WASI_RIGHT_FD_FILESTAT_GET | + WASI_RIGHT_FD_SYNC | + WASI_RIGHT_POLL_FD_READWRITE, + 0n, + ); + return result; + } catch (error) { + return mapHostProcessError(error); + } + } // Kernel-PTY stdio must report CHARACTER_DEVICE so guest is_terminal()/ // isatty() see the TTY (the runner-process fds behind the delegate are // pipes). Resolve dup'd passthrough handles to their target fd first. @@ -5293,11 +10145,50 @@ wasiImport.fd_fdstat_get = (fd, statPtr) => { ); } + if (handle?.kind === 'guest-file') { + try { + const stat = fsModule.fstatSync(handle.targetFd); + return writeGuestFdstat( + statPtr, + wasiFiletypeFromStats(stat), + 0, + WASI_RIGHT_FD_READ | + WASI_RIGHT_FD_SEEK | + WASI_RIGHT_FD_TELL | + WASI_RIGHT_FD_FILESTAT_GET | + WASI_RIGHT_FD_WRITE | + WASI_RIGHT_FD_SYNC, + 0n, + ); + } catch (error) { + return mapSyntheticFsError(error); + } + } + if (handle && handle.kind !== 'passthrough') { return WASI_ERRNO_BADF; } if (handle?.kind === 'passthrough') { + if (typeof handle.ioFd === 'number') { + try { + const stat = fsModule.fstatSync(handle.ioFd); + return writeGuestFdstat( + statPtr, + wasiFiletypeFromStats(stat), + 0, + WASI_RIGHT_FD_READ | + WASI_RIGHT_FD_SEEK | + WASI_RIGHT_FD_TELL | + WASI_RIGHT_FD_FILESTAT_GET | + WASI_RIGHT_FD_WRITE | + WASI_RIGHT_FD_SYNC, + 0n, + ); + } catch (error) { + return mapSyntheticFsError(error); + } + } return delegateManagedFdFdstatGet ? __agentOSWasiMeasurePhase('fd_fdstat_get', 'delegate_call', () => delegateManagedFdFdstatGet(handle.targetFd, statPtr) @@ -5317,7 +10208,28 @@ wasiImport.fd_fdstat_get = (fd, statPtr) => { }; wasiImport.fd_fdstat_set_flags = (fd, flags) => { + // Host-net sockets: honor O_NONBLOCK (guest fcntl F_SETFL). net_recv/net_send + // consult `socket.nonblock` to return EAGAIN instead of blocking, which + // non-blocking clients like libcurl rely on to interleave send/recv. + { + const hostNetSocket = getHostNetSocket(fd); + if (hostNetSocket && !hostNetSocket.closed) { + hostNetSocket.nonblock = (Number(flags) & WASI_FDFLAGS_NONBLOCK) !== 0; + return WASI_ERRNO_SUCCESS; + } + } const handle = lookupFdHandle(fd); + if (handle?.kind === 'kernel-fd') { + try { + const wasiFlags = Number(flags) >>> 0; + const kernelFlags = (wasiFlags & WASI_FDFLAGS_APPEND ? KERNEL_O_APPEND : 0) + | (wasiFlags & WASI_FDFLAGS_NONBLOCK ? KERNEL_O_NONBLOCK : 0); + callSyncRpc('process.fd_set_flags', [Number(handle.targetFd) >>> 0, kernelFlags]); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + } if (handle && handle.kind !== 'passthrough') { return WASI_ERRNO_BADF; } @@ -5339,6 +10251,14 @@ wasiImport.fd_fdstat_set_flags = (fd, flags) => { wasiImport.fd_filestat_get = (fd, statPtr) => { const handle = lookupFdHandle(fd); + if (handle?.kind === 'kernel-fd') { + try { + const stat = callSyncRpc('process.fd_filestat', [Number(handle.targetFd) >>> 0]); + return writeGuestFilestat(statPtr, stat, Number(stat?.filetype) >>> 0); + } catch (error) { + return mapHostProcessError(error); + } + } if (handle?.kind === 'guest-file') { try { return writeGuestFilestat(statPtr, fsModule.fstatSync(handle.targetFd)); @@ -5346,6 +10266,16 @@ wasiImport.fd_filestat_get = (fd, statPtr) => { return mapSyntheticFsError(error); } } + if (handle?.kind === 'pipe-read' || handle?.kind === 'pipe-write') { + // WASI preview1 has no distinct FIFO filetype value. Preserve the same + // unknown-filetype contract used by fd_fdstat_get while still returning a + // stable identity for aliases of one pipe, as Linux fstat(2) does. + return writeGuestFilestat( + statPtr, + { ino: handle.pipe.id, nlink: 1, size: 0 }, + WASI_FILETYPE_UNKNOWN, + ); + } if (handle?.kind === 'passthrough') { if (typeof handle.ioFd === 'number') { @@ -5355,9 +10285,17 @@ wasiImport.fd_filestat_get = (fd, statPtr) => { return mapSyntheticFsError(error); } } - return delegateManagedFdFilestatGet - ? delegateManagedFdFilestatGet(handle.targetFd, statPtr) - : WASI_ERRNO_BADF; + if (!delegateManagedFdFilestatGet) { + return WASI_ERRNO_BADF; + } + return delegateManagedFdFilestatGet(handle.targetFd, statPtr); + } + + // Guest stdio always starts with an explicit mirror. If that mirror is + // absent, the guest descriptor was closed or renumbered; do not expose the + // runner's private Node-WASI bootstrap descriptor at the same number. + if (!handle && (Number(fd) >>> 0) <= 2) { + return WASI_ERRNO_BADF; } if (rejectClosedPassthroughFd(fd)) { @@ -5371,6 +10309,17 @@ wasiImport.fd_filestat_get = (fd, statPtr) => { wasiImport.fd_filestat_set_size = (fd, size) => { const handle = lookupFdHandle(fd); + if (handle?.kind === 'kernel-fd') { + try { + callSyncRpc('process.fd_truncate', [ + Number(handle.targetFd) >>> 0, + BigInt(size).toString(), + ]); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + } if (handle?.kind === 'guest-file') { try { const nextSize = Number(size); @@ -5477,10 +10426,30 @@ wasiImport.fd_prestat_dir_name = (fd, pathPtr, pathLen) => { }; wasiImport.fd_write = (fd, iovs, iovsLen, nwrittenPtr) => { + const numericFd = Number(fd) >>> 0; + const hostNetSocket = getHostNetSocket(numericFd); + if (hostNetSocket) { + return writeHostNetSocketFromGuestIovs(hostNetSocket, iovs, iovsLen, nwrittenPtr); + } + const handle = __agentOSWasiMeasurePhase('fd_write', 'lookup_handle', () => lookupFdHandle(fd) ); - const numericFd = Number(fd) >>> 0; + if (handle?.kind === 'kernel-fd') { + try { + const bytes = collectGuestIovBytes(iovs, iovsLen); + const written = Number(callSyncRpc('process.fd_write', [ + Number(handle.targetFd) >>> 0, + bytes, + ])); + if (!Number.isSafeInteger(written) || written < 0 || written > bytes.length) { + return WASI_ERRNO_FAULT; + } + return writeGuestUint32(nwrittenPtr, written); + } catch (error) { + return mapHostProcessError(error); + } + } if (handle?.kind === 'pipe-write') { try { const bytes = __agentOSWasiMeasurePhase('fd_write', 'guest_iov_collect', () => @@ -5502,6 +10471,9 @@ wasiImport.fd_write = (fd, iovs, iovsLen, nwrittenPtr) => { } if (handle?.kind === 'guest-file') { + if (handle.readOnly === true) { + return WASI_ERRNO_ROFS; + } try { const bytes = __agentOSWasiMeasurePhase('fd_write', 'guest_iov_collect', () => collectGuestIovBytes(iovs, iovsLen) @@ -5517,7 +10489,12 @@ wasiImport.fd_write = (fd, iovs, iovsLen, nwrittenPtr) => { } } - if (numericFd === 1 || numericFd === 2) { + const passthroughStdioTarget = + handle?.kind === 'passthrough' && + (Number(handle.targetFd) === 1 || Number(handle.targetFd) === 2) + ? Number(handle.targetFd) + : null; + if (passthroughStdioTarget != null) { try { const bytes = __agentOSWasiMeasurePhase('fd_write', 'guest_iov_collect', () => collectGuestIovBytes(iovs, iovsLen) @@ -5527,14 +10504,14 @@ wasiImport.fd_write = (fd, iovs, iovsLen, nwrittenPtr) => { process.env.AGENTOS_SANDBOX_ROOT.length > 0; if (sidecarManagedProcess || KERNEL_STDIO_SYNC_RPC) { const written = __agentOSWasiMeasurePhase('fd_write', 'sync_rpc', () => - Number(callSyncRpc('__kernel_stdio_write', [numericFd, bytes])) >>> 0 + Number(callSyncRpc('__kernel_stdio_write', [passthroughStdioTarget, bytes])) >>> 0 ); return __agentOSWasiMeasurePhase('fd_write', 'result_marshal', () => writeGuestUint32(nwrittenPtr, written) ); } __agentOSWasiMeasurePhase('fd_write', 'host_io', () => - (numericFd === 1 ? process.stdout : process.stderr).write(bytes) + (passthroughStdioTarget === 1 ? process.stdout : process.stderr).write(bytes) ); return __agentOSWasiMeasurePhase('fd_write', 'result_marshal', () => writeGuestUint32(nwrittenPtr, bytes.length) @@ -5595,25 +10572,55 @@ wasiImport.fd_write = (fd, iovs, iovsLen, nwrittenPtr) => { }; wasiImport.fd_close = (fd) => { + const numericFd = Number(fd) >>> 0; traceHostProcess('fd-close-begin', { - fd: Number(fd) >>> 0, - syntheticKind: syntheticFdEntries.get(Number(fd) >>> 0)?.kind ?? null, - passthroughKind: passthroughHandles.get(Number(fd) >>> 0)?.kind ?? null, + fd: numericFd, + syntheticKind: syntheticFdEntries.get(numericFd)?.kind ?? null, + passthroughKind: passthroughHandles.get(numericFd)?.kind ?? null, }); - if (__agentOSWasiMeasurePhase('fd_close', 'synthetic_close', () => closeSyntheticFd(fd))) { - traceHostProcess('fd-close-synthetic', { fd: Number(fd) >>> 0 }); - return WASI_ERRNO_SUCCESS; + if (hostNetSockets.has(numericFd)) { + const result = __agentOSWasiMeasurePhase('fd_close', 'host_socket_close', () => + hostNetImport.net_close(numericFd) + ); + // net_close consumes the runner fd even when a sidecar cleanup RPC fails, + // matching close(2)'s no-retry rule. Never let a reused fd inherit stale + // FD_CLOEXEC state from the consumed description. + runnerCloexecFds.delete(numericFd); + return result; + } + try { + if (__agentOSWasiMeasurePhase('fd_close', 'synthetic_close', () => closeSyntheticFd(fd))) { + runnerCloexecFds.delete(numericFd); + traceHostProcess('fd-close-synthetic', { fd: Number(fd) >>> 0 }); + return WASI_ERRNO_SUCCESS; + } + } catch (error) { + return mapHostProcessError(error); } const handle = __agentOSWasiMeasurePhase('fd_close', 'lookup_handle', () => lookupFdHandle(fd) ); + if (handle?.kind === 'kernel-fd') { + try { + traceHostProcess('fd-close-kernel', { + fd: Number(fd) >>> 0, + targetFd: handle.targetFd ?? null, + }); + closePassthroughFd(fd); + runnerCloexecFds.delete(numericFd); + return WASI_ERRNO_SUCCESS; + } catch (error) { + return mapHostProcessError(error); + } + } if (handle?.kind === 'passthrough') { - traceHostProcess('fd-close-passthrough', { + traceHostProcess('fd-close-mapped', { fd: Number(fd) >>> 0, targetFd: handle.targetFd ?? null, }); __agentOSWasiMeasurePhase('fd_close', 'fd_bookkeeping', () => closePassthroughFd(fd)); + runnerCloexecFds.delete(numericFd); return WASI_ERRNO_SUCCESS; } @@ -5635,17 +10642,95 @@ wasiImport.fd_close = (fd) => { remainingRefs: delegateManagedFdRefCounts.get(Number(fd) >>> 0) ?? 0, }); if (!shouldDelegateClose) { + runnerCloexecFds.delete(numericFd); return WASI_ERRNO_SUCCESS; } passthroughHandles.delete(Number(fd) >>> 0); } traceHostProcess('fd-close-delegate', { fd: Number(fd) >>> 0 }); - return delegateManagedFdClose + const result = delegateManagedFdClose ? __agentOSWasiMeasurePhase('fd_close', 'delegate_call', () => delegateManagedFdClose(fd) ) : WASI_ERRNO_BADF; + if (result === WASI_ERRNO_SUCCESS) runnerCloexecFds.delete(numericFd); + return result; +}; + +wasiImport.fd_renumber = (from, to) => { + try { + const sourceFd = Number(from) >>> 0; + const targetFd = Number(to) >>> 0; + if (sourceFd >= LINUX_GUEST_FD_LIMIT || targetFd >= LINUX_GUEST_FD_LIMIT) { + return WASI_ERRNO_BADF; + } + if (sourceFd === targetFd) { + return lookupFdHandle(sourceFd) || delegateManagedFdRefCounts.has(sourceFd) + ? WASI_ERRNO_SUCCESS + : WASI_ERRNO_BADF; + } + + const syntheticHandle = syntheticFdEntries.get(sourceFd); + const passthroughHandle = passthroughHandles.get(sourceFd); + const retainedSpawnOutputHandle = retainedSpawnOutputHandlesByFd.get(sourceFd); + if (!syntheticHandle && !passthroughHandle && !retainedSpawnOutputHandle) { + if (rejectClosedPassthroughFd(sourceFd)) { + return WASI_ERRNO_BADF; + } + return delegateManagedFdRenumber + ? delegateManagedFdRenumber(sourceFd, targetFd) + : WASI_ERRNO_BADF; + } + + if ( + syntheticFdEntries.has(targetFd) || + passthroughHandles.has(targetFd) || + retainedSpawnOutputHandlesByFd.has(targetFd) || + delegateManagedFdRefCounts.has(targetFd) + ) { + const closeResult = wasiImport.fd_close(targetFd); + if (closeResult !== WASI_ERRNO_SUCCESS) { + return closeResult; + } + } + + if (syntheticHandle) { + syntheticFdEntries.delete(sourceFd); + syntheticHandle.displayFd = targetFd; + syntheticFdEntries.set(targetFd, syntheticHandle); + } else if (passthroughHandle) { + passthroughHandles.delete(sourceFd); + passthroughHandle.displayFd = targetFd; + passthroughHandles.set(targetFd, passthroughHandle); + closedPassthroughFds.add(sourceFd); + closedPassthroughFds.delete(targetFd); + } else { + retainedSpawnOutputHandlesByFd.delete(sourceFd); + retainedSpawnOutputHandlesByFd.set(targetFd, retainedSpawnOutputHandle); + } + + // renumber(2) consumes the source descriptor and installs the same open + // description at the target. Preserve that guest-visible closure even + // when Node-WASI has a private/bootstrap descriptor at the source number. + closedPassthroughFds.add(sourceFd); + closedPassthroughFds.delete(targetFd); + + const sourceWasCloexec = runnerCloexecFds.delete(sourceFd); + runnerCloexecFds.delete(targetFd); + if (sourceWasCloexec) runnerCloexecFds.add(targetFd); + + nextSyntheticFd = Math.max(nextSyntheticFd, targetFd + 1); + traceHostProcess('fd-renumber', { + from: sourceFd, + to: targetFd, + syntheticKind: syntheticHandle?.kind ?? null, + passthroughKind: passthroughHandle?.kind ?? null, + }); + return WASI_ERRNO_SUCCESS; + } catch { + return WASI_ERRNO_FAULT; + } }; wasiImport.poll_oneoff = (inPtr, outPtr, nsubscriptions, neventsPtr) => { @@ -5703,7 +10788,9 @@ wasiImport.poll_oneoff = (inPtr, outPtr, nsubscriptions, neventsPtr) => { }); continue; } - if (handle && handle.kind !== 'passthrough') { + if (handle?.kind === 'kernel-fd') { + hasRemappedPassthroughSubscription = true; + } else if (handle && handle.kind !== 'passthrough') { hasSyntheticSubscription = true; } else if (handle?.kind === 'passthrough') { const targetFd = Number(handle.targetFd) >>> 0; @@ -5728,7 +10815,9 @@ wasiImport.poll_oneoff = (inPtr, outPtr, nsubscriptions, neventsPtr) => { }); } - if (!hasSyntheticSubscription && !hasRemappedPassthroughSubscription) { + const hasClockSubscription = subscriptions.some((subscription) => subscription.kind === 'clock'); + + if (!hasSyntheticSubscription && !hasRemappedPassthroughSubscription && !hasClockSubscription) { return delegateManagedPollOneoff ? delegateManagedPollOneoff(inPtr, outPtr, nsubscriptions, neventsPtr) : WASI_ERRNO_BADF; @@ -5754,6 +10843,9 @@ wasiImport.poll_oneoff = (inPtr, outPtr, nsubscriptions, neventsPtr) => { } return null; } + if (subscription.handle?.kind === 'kernel-fd') { + return Number(subscription.handle.targetFd) >>> 0; + } if (!subscription.handle && fd <= 2 && (sidecarManagedProcess || KERNEL_STDIO_SYNC_RPC)) { return fd; } @@ -5775,8 +10867,17 @@ wasiImport.poll_oneoff = (inPtr, outPtr, nsubscriptions, neventsPtr) => { pollTargets, Math.max(0, Number(waitMs) >>> 0), ]); - } catch { - return []; + } catch (error) { + traceHostProcess('kernel-poll-error', { + message: error instanceof Error ? error.message : String(error), + }); + return subscriptions.map((subscription) => ({ + userdata: subscription.userdata, + error: WASI_ERRNO_FAULT, + type: subscription.kind === 'fd_read' ? 1 : 2, + nbytes: 0, + flags: 0, + })); } const responseEntries = Array.isArray(response?.fds) ? response.fds : []; @@ -5812,6 +10913,11 @@ wasiImport.poll_oneoff = (inPtr, outPtr, nsubscriptions, neventsPtr) => { } while (readyEvents.length === 0) { + dispatchPendingWasmSignals(); + // poll_oneoff is also a process scheduling point. A long parked kernel + // poll here would otherwise starve descendants whose events are serviced + // by this runner. + pumpSpawnedChildren(0); for (const subscription of subscriptions) { if (subscription.error != null) { readyEvents.push({ @@ -5859,13 +10965,16 @@ wasiImport.poll_oneoff = (inPtr, outPtr, nsubscriptions, neventsPtr) => { // can be long. Synthetic (pipe) subscriptions still need short local // pumping, so only use the long slice when every subscription is // kernel-backed. + const maxKernelWaitMs = hasActiveSpawnedChildren() + ? SPAWNED_CHILD_WAIT_SLICE_MS + : KERNEL_WAIT_SLICE_MS; const kernelWaitMs = hasSyntheticSubscription ? deadline == null - ? 10 - : Math.max(0, Math.min(10, deadline - Date.now())) + ? SPAWNED_CHILD_WAIT_SLICE_MS + : Math.max(0, Math.min(SPAWNED_CHILD_WAIT_SLICE_MS, deadline - Date.now())) : deadline == null - ? KERNEL_WAIT_SLICE_MS - : Math.max(0, Math.min(KERNEL_WAIT_SLICE_MS, deadline - Date.now())); + ? maxKernelWaitMs + : Math.max(0, Math.min(maxKernelWaitMs, deadline - Date.now())); readyEvents.push(...collectKernelReadyEvents(kernelWaitMs)); if (readyEvents.length > 0) { break; @@ -5895,7 +11004,7 @@ wasiImport.poll_oneoff = (inPtr, outPtr, nsubscriptions, neventsPtr) => { ); } - if (readyEvents.length === 0 && subscriptions.some((subscription) => subscription.kind === 'clock')) { + if (readyEvents.length === 0 && hasClockSubscription) { const clockSubscription = subscriptions.find((subscription) => subscription.kind === 'clock'); readyEvents.push({ userdata: clockSubscription.userdata, @@ -6004,35 +11113,166 @@ if (__agentOSWasiSyscallPhasesEnabled) { } } -const instance = __agentOSWasmMeasurePhase('WebAssembly.Instance', () => new WebAssembly.Instance(module, { - wasi_snapshot_preview1: wasiImport, - wasi_unstable: wasiImport, - host_tty: hostTtyImport, - // Read-write commands like DuckDB need fd_dup_min from the patched - // wasi-libc surface, but broader host_process capabilities stay - // reserved for the full tier. - host_process: - permissionTier === 'full' - ? hostProcessImport - : permissionTier === 'isolated' - ? undefined - : limitedHostProcessImport, - host_net: permissionTier === 'full' ? hostNetImport : undefined, - host_user: hostUserImport, - host_fs: hostFsImport, -})); +function instantiateWasmModule(targetModule) { + return __agentOSWasmMeasurePhase('WebAssembly.Instance', () => new WebAssembly.Instance(targetModule, { + wasi_snapshot_preview1: wasiImport, + wasi_unstable: wasiImport, + host_tty: hostTtyImport, + // Read-write commands like DuckDB need fd_dup_min from the patched + // wasi-libc surface, but broader host_process capabilities stay + // reserved for the full tier. + host_process: + permissionTier === 'full' + ? hostProcessImport + : permissionTier === 'isolated' + ? undefined + : limitedHostProcessImport, + host_net: permissionTier === 'full' ? hostNetImport : undefined, + host_user: hostUserImport, + host_fs: hostFsImport, + })); +} + +let instance = instantiateWasmModule(module); if (instance.exports.memory instanceof WebAssembly.Memory) { instanceMemory = instance.exports.memory; } +function initializeSignalMaskForInstance(targetInstance) { + const mask = encodeSignalMask(wasmBlockedSignals); + if (mask.lo === 0 && mask.hi === 0) { + return; + } + const setter = targetInstance?.exports?.__agentos_set_initial_sigmask; + if (typeof setter !== 'function') { + throw new Error( + 'spawned WASM image cannot initialize its inherited signal mask; rebuild it with the current AgentOS sysroot', + ); + } + setter(mask.lo, mask.hi); +} + +initializeSignalMaskForInstance(instance); +for (const signal of initialWasmSignalIgnores) { + callSyncRpc('process.signal_state', [signal, 'ignore', '[]', 0]); + wasmSignalRegistrations.set(signal, { + action: 'ignore', + mask: [], + flags: 0, + }); +} + function dispatchWasmSignal(signal) { const numeric = Number(signal) | 0; - if ( - numeric > 0 && - typeof instance?.exports?.__wasi_signal_trampoline === 'function' - ) { + if (numeric <= 0) { + return false; + } + const registration = wasmSignalRegistrations.get(numeric); + if (registration?.action === 'ignore') { + return false; + } + if (registration?.action !== 'user') { + // The libc trampoline dispatches user handlers only. Default dispositions + // remain sidecar-owned so fatal signals terminate the VM and non-fatal + // defaults (SIGCHLD/SIGCONT/...) follow the kernel signal table. + callSyncRpc('process.kill', [VIRTUAL_PID, signalNameFromNumber(numeric)]); + return false; + } + if (typeof instance?.exports?.__wasi_signal_trampoline !== 'function') { + return false; + } + const previousMask = new Set(wasmBlockedSignals); + if (registration?.action === 'user') { + for (const maskedSignal of registration.mask) { + if (maskedSignal !== LINUX_SIGKILL && maskedSignal !== LINUX_SIGSTOP) { + wasmBlockedSignals.add(maskedSignal); + } + } + if ((registration.flags & LINUX_SA_NODEFER) === 0) { + wasmBlockedSignals.add(numeric); + } + if ((registration.flags & LINUX_SA_RESETHAND) !== 0) { + wasmSignalRegistrations.delete(numeric); + callSyncRpc('process.signal_state', [numeric, 'default', '[]', 0]); + } + } + let caught = false; + try { instance.exports.__wasi_signal_trampoline(numeric); + caught = true; + } finally { + wasmBlockedSignals.clear(); + for (const blockedSignal of previousMask) { + wasmBlockedSignals.add(blockedSignal); + } + caught = dispatchLocallyPendingWasmSignals() || caught; + } + return caught; +} + +function dispatchLocallyPendingWasmSignals() { + let caught = false; + for (const signal of [...pendingWasmSignals]) { + // A nested handler may drain another member of this snapshot. Do not + // dispatch that stale snapshot entry a second time. + if (!pendingWasmSignals.has(signal)) { + continue; + } + if (wasmBlockedSignals.has(signal)) { + continue; + } + pendingWasmSignals.delete(signal); + caught = dispatchWasmSignal(signal) || caught; + } + return caught; +} + +function dispatchPendingWasmSignals() { + let caught = dispatchLocallyPendingWasmSignals(); + // Standard signals coalesce, so at most one pending instance of each of the + // 64 supported signals can be transferred from the sidecar per boundary. + for (let index = 0; index < 64; index += 1) { + let signal; + try { + signal = callSyncRpc('process.take_signal', []); + } catch (error) { + if (error?.code === 'ERR_AGENTOS_WASM_SYNC_RPC_UNAVAILABLE') { + return caught; + } + throw error; + } + if (typeof signal !== 'number') { + return caught; + } + if (wasmBlockedSignals.has(signal)) { + pendingWasmSignals.add(signal); + } else { + caught = dispatchWasmSignal(signal) || caught; + } + } + return caught; +} + +function resetCaughtWasmSignalDispositionsForExec(sidecarCommitted) { + for (const [signal, registration] of wasmSignalRegistrations) { + if (registration.action !== 'user') { + continue; + } + if (!sidecarCommitted) { + try { + callSyncRpc('process.signal_state', [signal, 'default', '[]', 0]); + } catch (error) { + if (typeof process?.stderr?.write === 'function') { + process.stderr.write( + `[agentos] exec committed locally but failed to reset signal ${signal}: ${ + error instanceof Error ? error.message : String(error) + }\n`, + ); + } + } + } + wasmSignalRegistrations.delete(signal); } } @@ -6044,11 +11284,13 @@ Object.defineProperty(globalThis, '__secureExecWasmSignalDispatch', { typeof payload?.number === 'number' ? payload.number : signalNumberFromName(payload?.signal); - dispatchWasmSignal(signal); + if (signal > 0 && signal <= LINUX_MAX_SIGNAL_NUMBER) { + pendingWasmSignals.add(signal); + } }, }); -if (typeof instance.exports._start === 'function') { +while (typeof instance.exports._start === 'function') { // The `RuntimeError: unreachable` reports that used to point at // `WASI.start()` were caused by the host shim around guest startup, not by // V8 itself. Standalone runs must keep ordinary stdio on local process @@ -6060,6 +11302,41 @@ if (typeof instance.exports._start === 'function') { try { exitCode = __agentOSWasmMeasurePhase('wasi.start', () => wasi.start(instance)); } catch (error) { + if (isExecReplacement(error)) { + for (const fd of new Set(error.image.closeFds)) { + if ( + error.image.sidecarCommitted === true && + forgetSidecarClosedKernelFd(Number(fd) >>> 0) + ) { + continue; + } + try { + const result = wasiImport.fd_close(Number(fd) >>> 0); + if (result !== WASI_ERRNO_SUCCESS) { + warnExecCloseFailure(Number(fd) >>> 0, `WASI errno ${result}`); + } + } catch (closeError) { + // Linux commits a valid exec even when a close-on-exec close reports an error. + warnExecCloseFailure( + Number(fd) >>> 0, + closeError instanceof Error ? closeError.message : String(closeError), + ); + } + } + resetCaughtWasmSignalDispositionsForExec(error.image.sidecarCommitted === true); + guestArgv = error.image.argv; + guestEnv = error.image.env; + wasi.args = guestArgv.map((value) => String(value)); + wasi.env = Object.fromEntries( + Object.entries(guestEnv).map(([key, value]) => [String(key), String(value)]), + ); + instance = instantiateWasmModule(error.image.module); + instanceMemory = instance.exports.memory instanceof WebAssembly.Memory + ? instance.exports.memory + : null; + initializeSignalMaskForInstance(instance); + continue; + } __agentOSWasmEmitPhaseMetrics('wasi.start.error', { error: error && typeof error === 'object' && 'message' in error ? String(error.message) : String(error), }); @@ -6071,7 +11348,9 @@ if (typeof instance.exports._start === 'function') { } __agentOSWasmEmitPhaseMetrics('complete', { exitCode }); process.exit(typeof exitCode === 'number' ? exitCode : 0); -} else if (typeof instance.exports.run === 'function') { +} + +if (typeof instance.exports.run === 'function') { const result = await instance.exports.run(); if (typeof result !== 'undefined') { console.log(String(result)); diff --git a/crates/execution/src/benchmark.rs b/crates/execution/src/benchmark.rs index 90bc938731..5da12b1ac6 100644 --- a/crates/execution/src/benchmark.rs +++ b/crates/execution/src/benchmark.rs @@ -2532,6 +2532,7 @@ fn run_native_sample( let startup_started_at = Instant::now(); let execution = engine.start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-bench"), context_id: context.context_id, @@ -2657,6 +2658,7 @@ fn measure_transport_rtt( }); let mut execution = engine.start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-transport"), context_id: context.context_id, diff --git a/crates/execution/src/javascript.rs b/crates/execution/src/javascript.rs index 2bd0f3b7cf..ad298a68dc 100644 --- a/crates/execution/src/javascript.rs +++ b/crates/execution/src/javascript.rs @@ -509,6 +509,9 @@ pub struct StartJavascriptExecutionRequest { pub vm_id: String, pub context_id: String, pub argv: Vec, + /// Explicit process argv[0]. `Some("")` is distinct from `None` and must be + /// preserved for Node child_process compatibility. + pub argv0: Option, pub env: BTreeMap, pub cwd: PathBuf, /// Per-execution runtime limits (see [`JavascriptExecutionLimits`]). @@ -1554,6 +1557,7 @@ pub enum JavascriptExecutionError { ExpiredSyncRpcRequest(u64), RpcResponse(String), Terminate(std::io::Error), + Control(std::io::Error), StdinClosed, Stdin(std::io::Error), OutputBufferExceeded { stream: &'static str, limit: usize }, @@ -1598,6 +1602,7 @@ impl fmt::Display for JavascriptExecutionError { Self::Terminate(err) => { write!(f, "failed to terminate guest JavaScript runtime: {err}") } + Self::Control(err) => write!(f, "failed to control guest JavaScript runtime: {err}"), Self::StdinClosed => f.write_str("guest JavaScript stdin is already closed"), Self::Stdin(err) => write!(f, "failed to write guest stdin: {err}"), Self::OutputBufferExceeded { stream, limit } => { @@ -1624,6 +1629,10 @@ pub struct JavascriptExecution { kernel_stdin: Arc, _import_cache_guard: Arc, v8_session: V8SessionHandle, + /// Fully prepared V8 execute request. Cross-runtime execve prepares the + /// replacement isolate and its bridge before committing kernel process + /// state, but must not enqueue guest code until that commit is complete. + prepared_execute: Option, /// Host-direct module resolver state, used ONLY by the standalone `wait()` /// loop. The real VM runtime resolves modules against the kernel VFS on the /// sidecar service loop and never reaches this; but `wait()` runs without a @@ -1632,6 +1641,18 @@ pub struct JavascriptExecution { module_resolution: Mutex<(GuestPathTranslator, LocalModuleResolutionCache)>, } +#[derive(Debug)] +struct PreparedJavascriptExecute { + mode: u8, + file_path: String, + bridge_code: String, + post_restore_script: String, + userland_code: String, + high_resolution_time: bool, + user_code: String, + wasm_module_bytes: Option>>, +} + impl JavascriptExecution { pub fn execution_id(&self) -> &str { &self.execution_id @@ -1649,6 +1670,35 @@ impl JavascriptExecution { true } + /// Enqueue a replacement image that was fully prepared without running + /// guest code. This is the final step of an atomic cross-runtime execve and + /// must only be called after the kernel and sidecar process state commit. + pub fn start_prepared(&mut self) -> Result<(), JavascriptExecutionError> { + let prepared = self.prepared_execute.take().ok_or_else(|| { + JavascriptExecutionError::Spawn(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "JavaScript execution is not awaiting a prepared start", + )) + })?; + self.v8_session + .execute( + prepared.mode, + prepared.file_path, + prepared.bridge_code, + prepared.post_restore_script, + prepared.userland_code, + prepared.high_resolution_time, + prepared.user_code, + prepared.wasm_module_bytes, + ) + .map_err(JavascriptExecutionError::Spawn) + } + + #[doc(hidden)] + pub fn is_prepared_for_start(&self) -> bool { + self.prepared_execute.is_some() + } + pub fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), JavascriptExecutionError> { self.kernel_stdin.write(chunk)?; let payload = v8_runtime::json_to_cbor_payload(&json!({ @@ -1707,6 +1757,18 @@ impl JavascriptExecution { .map_err(JavascriptExecutionError::Terminate) } + pub fn pause(&self) -> Result<(), JavascriptExecutionError> { + self.v8_session + .pause() + .map_err(JavascriptExecutionError::Control) + } + + pub fn resume(&self) -> Result<(), JavascriptExecutionError> { + self.v8_session + .resume() + .map_err(JavascriptExecutionError::Control) + } + pub fn send_stream_event( &self, event_type: &str, @@ -1738,6 +1800,24 @@ impl JavascriptExecution { phase_start.elapsed(), ); + self.respond_claimed_sync_rpc_success(id, result) + } + + /// Atomically claim the exact pending sync RPC before a caller performs a + /// destructive operation on its behalf. A timed-out or replaced request + /// must not consume bytes that belong to the guest's next retry. + pub fn claim_sync_rpc_response(&mut self, id: u64) -> Result { + match self.clear_pending_sync_rpc(id)? { + PendingSyncRpcResolution::Pending => Ok(true), + PendingSyncRpcResolution::TimedOut | PendingSyncRpcResolution::Missing => Ok(false), + } + } + + pub fn respond_claimed_sync_rpc_success( + &mut self, + id: u64, + result: Value, + ) -> Result<(), JavascriptExecutionError> { let phase_start = Instant::now(); let payload = translate_legacy_bridge_value_to_v8(&result); record_sync_bridge_phase( @@ -1808,6 +1888,15 @@ impl JavascriptExecution { PendingSyncRpcResolution::Missing => {} } + self.respond_claimed_sync_rpc_error(id, code, message) + } + + pub fn respond_claimed_sync_rpc_error( + &mut self, + id: u64, + code: impl Into, + message: impl Into, + ) -> Result<(), JavascriptExecutionError> { let error_msg = format!("{}: {}", code.into(), message.into()); self.v8_session .send_bridge_response(id, 1, error_msg.into_bytes()) @@ -2126,6 +2215,18 @@ impl JavascriptExecutionEngine { context } + /// Dispose an execution context once its final start/prepare operation has + /// consumed the metadata. Live executions own their resolved runtime state + /// independently and do not consult this registry after creation. + pub fn dispose_context(&mut self, context_id: &str) -> bool { + self.contexts.remove(context_id).is_some() + } + + #[doc(hidden)] + pub fn context_count_for_test(&self) -> usize { + self.contexts.len() + } + pub fn start_execution( &mut self, request: StartJavascriptExecutionRequest, @@ -2133,6 +2234,13 @@ impl JavascriptExecutionEngine { self.start_execution_with_module_reader(request, None, None) } + pub fn prepare_execution( + &mut self, + request: StartJavascriptExecutionRequest, + ) -> Result { + self.prepare_execution_with_module_reader(request, None, None) + } + fn ensure_v8_host(&mut self) -> Result<(), JavascriptExecutionError> { let should_spawn_v8_host = match self.v8_host.as_mut() { Some(v8_host) => !v8_host @@ -2195,6 +2303,28 @@ impl JavascriptExecutionEngine { request: StartJavascriptExecutionRequest, module_reader: Option>, guest_reader: Option>, + ) -> Result { + self.create_execution_with_module_reader(request, module_reader, guest_reader, false) + } + + /// Prepare an execution through every fallible image-loading step without + /// enqueueing guest code in V8. Used by execve when the replacement runtime + /// differs from the current runtime. + pub fn prepare_execution_with_module_reader( + &mut self, + request: StartJavascriptExecutionRequest, + module_reader: Option>, + guest_reader: Option>, + ) -> Result { + self.create_execution_with_module_reader(request, module_reader, guest_reader, true) + } + + fn create_execution_with_module_reader( + &mut self, + request: StartJavascriptExecutionRequest, + module_reader: Option>, + guest_reader: Option>, + defer_execute: bool, ) -> Result { let context = self .contexts @@ -2349,6 +2479,7 @@ impl JavascriptExecutionEngine { user_code, &guest_entrypoint, &process_argv, + request.argv0.as_deref(), translator.guest_cwd(), &request.env, heap_limit_mb, @@ -2401,19 +2532,33 @@ impl JavascriptExecutionEngine { record_js_start_phase("js_start_install_module_reader", phase_start.elapsed()); let phase_start = Instant::now(); - // Execute bridge code + user code in the V8 isolate - v8_session - .execute( - if use_module_mode { 1 } else { 0 }, - guest_entrypoint.clone(), - V8RuntimeHost::bridge_code().to_owned(), - String::new(), - snapshot_userland_code, - request.guest_runtime.high_resolution_time, - user_code, - request.wasm_module_bytes.clone(), - ) - .map_err(JavascriptExecutionError::Spawn)?; + let prepared_execute = PreparedJavascriptExecute { + mode: if use_module_mode { 1 } else { 0 }, + file_path: guest_entrypoint.clone(), + bridge_code: V8RuntimeHost::bridge_code().to_owned(), + post_restore_script: String::new(), + userland_code: snapshot_userland_code, + high_resolution_time: request.guest_runtime.high_resolution_time, + user_code, + wasm_module_bytes: request.wasm_module_bytes.clone(), + }; + let prepared_execute = if defer_execute { + Some(prepared_execute) + } else { + v8_session + .execute( + prepared_execute.mode, + prepared_execute.file_path, + prepared_execute.bridge_code, + prepared_execute.post_restore_script, + prepared_execute.userland_code, + prepared_execute.high_resolution_time, + prepared_execute.user_code, + prepared_execute.wasm_module_bytes, + ) + .map_err(JavascriptExecutionError::Spawn)?; + None + }; registration_guard.disarm(); record_js_start_phase("js_start_send_execute", phase_start.elapsed()); @@ -2425,6 +2570,7 @@ impl JavascriptExecutionEngine { kernel_stdin, _import_cache_guard: import_cache_guard, v8_session, + prepared_execute, module_resolution: Mutex::new(( standalone_translator, LocalModuleResolutionCache::default(), @@ -2969,6 +3115,7 @@ fn prepend_v8_runtime_shim( user_code: String, entrypoint: &str, argv: &[String], + argv0: Option<&str>, cwd: &str, env: &BTreeMap, // V8 heap cap in MB (`0` = engine default). Threaded from the typed wire @@ -2980,6 +3127,8 @@ fn prepend_v8_runtime_shim( guest_runtime: &GuestRuntimeConfig, ) -> String { let argv_json = serde_json::to_string(argv).unwrap_or_else(|_| String::from("[\"node\"]")); + let argv0_json = serde_json::to_string(&argv0.unwrap_or("node")) + .unwrap_or_else(|_| String::from("\"node\"")); let entry_json = serde_json::to_string(entrypoint).unwrap_or_else(|_| String::from("\"/\"")); let cwd_json = serde_json::to_string(cwd).unwrap_or_else(|_| String::from("\"/\"")); @@ -3026,6 +3175,7 @@ fn prepend_v8_runtime_shim( writable: true, }}); const nextArgv = {argv_json}; + const nextArgv0 = {argv0_json}; const entryFile = {entry_json}; const nextCwd = {cwd_json}; const nextEnv = {env_json}; @@ -3043,6 +3193,7 @@ fn prepend_v8_runtime_shim( cwd: nextCwd, env: nextEnv, argv: nextArgv, + argv0: nextArgv0, high_resolution_time: nextHighResolutionTime, }}), writable: false, @@ -3060,7 +3211,7 @@ fn prepend_v8_runtime_shim( if (typeof process !== "undefined") {{ process.argv = nextArgv; - process.argv0 = nextArgv[0] || "node"; + process.argv0 = nextArgv0; process.env = {{ ...(process.env || {{}}), ...visibleEnv, @@ -3346,7 +3497,6 @@ fn spawn_v8_event_bridge( if method == "_log" { if !send_javascript_event( &sender, - &v8_session, &event_gauge, event_notify.as_deref(), JavascriptExecutionEvent::Stdout(output), @@ -3356,7 +3506,6 @@ fn spawn_v8_event_bridge( } else { if !send_javascript_event( &sender, - &v8_session, &event_gauge, event_notify.as_deref(), JavascriptExecutionEvent::Stderr(output), @@ -3449,7 +3598,6 @@ fn spawn_v8_event_bridge( }; if !send_javascript_event( &sender, - &v8_session, &event_gauge, event_notify.as_deref(), JavascriptExecutionEvent::Stderr(error_msg.into_bytes()), @@ -3484,13 +3632,7 @@ fn spawn_v8_event_bridge( } else { None }; - if !send_javascript_event( - &sender, - &v8_session, - &event_gauge, - event_notify.as_deref(), - event, - ) { + if !send_javascript_event(&sender, &event_gauge, event_notify.as_deref(), event) { break; } if let (Some((_, method)), Some(start)) = (sync_rpc, phase_start) { @@ -3509,7 +3651,6 @@ fn spawn_v8_event_bridge( let phase_start = Instant::now(); let sent = send_javascript_event( &sender, - &v8_session, &event_gauge, event_notify.as_deref(), JavascriptExecutionEvent::Exited(1), @@ -3525,16 +3666,51 @@ fn spawn_v8_event_bridge( fn send_javascript_event( sender: &tokio::sync::mpsc::Sender, - v8_session: &V8SessionHandle, gauge: &agentos_bridge::queue_tracker::QueueGauge, notify: Option<&Notify>, event: JavascriptExecutionEvent, ) -> bool { - if javascript_event_payload_len(&event) > JAVASCRIPT_EVENT_PAYLOAD_LIMIT_BYTES { - let _ = v8_session.destroy(); - return false; + match event { + JavascriptExecutionEvent::Stdout(chunk) + if chunk.len() > JAVASCRIPT_EVENT_PAYLOAD_LIMIT_BYTES => + { + for chunk in chunk.chunks(JAVASCRIPT_EVENT_PAYLOAD_LIMIT_BYTES) { + if !send_single_javascript_event( + sender, + gauge, + notify, + JavascriptExecutionEvent::Stdout(chunk.to_vec()), + ) { + return false; + } + } + true + } + JavascriptExecutionEvent::Stderr(chunk) + if chunk.len() > JAVASCRIPT_EVENT_PAYLOAD_LIMIT_BYTES => + { + for chunk in chunk.chunks(JAVASCRIPT_EVENT_PAYLOAD_LIMIT_BYTES) { + if !send_single_javascript_event( + sender, + gauge, + notify, + JavascriptExecutionEvent::Stderr(chunk.to_vec()), + ) { + return false; + } + } + true + } + event => send_single_javascript_event(sender, gauge, notify, event), } +} +fn send_single_javascript_event( + sender: &tokio::sync::mpsc::Sender, + gauge: &agentos_bridge::queue_tracker::QueueGauge, + notify: Option<&Notify>, + event: JavascriptExecutionEvent, +) -> bool { // Apply backpressure instead of self-destructing when the host consumer is // slow. A burst of guest events that briefly outpaces the host draining this // channel is normal; previously a single `try_send` returning `Full` tore the @@ -3559,17 +3735,6 @@ fn send_javascript_event( } } -fn javascript_event_payload_len(event: &JavascriptExecutionEvent) -> usize { - match event { - JavascriptExecutionEvent::Stdout(chunk) | JavascriptExecutionEvent::Stderr(chunk) => { - chunk.len() - } - JavascriptExecutionEvent::SyncRpcRequest(_) - | JavascriptExecutionEvent::SignalState { .. } - | JavascriptExecutionEvent::Exited(_) => 0, - } -} - /// Handle internal bridge calls that don't need to go to the sidecar. /// Returns Some(response) if handled locally, None if it should be forwarded. impl LocalBridgeState { @@ -7205,6 +7370,28 @@ mod tests { use std::time::{SystemTime, UNIX_EPOCH}; use tempfile::tempdir; + #[test] + fn dispose_context_reclaims_one_shot_metadata_without_reusing_ids() { + let mut engine = JavascriptExecutionEngine::default(); + let baseline = engine.context_count_for_test(); + let first = engine.create_context(CreateJavascriptContextRequest { + vm_id: String::from("vm-context-dispose"), + bootstrap_module: None, + compile_cache_root: None, + }); + assert_eq!(engine.context_count_for_test(), baseline + 1); + assert!(engine.dispose_context(&first.context_id)); + assert_eq!(engine.context_count_for_test(), baseline); + assert!(!engine.dispose_context(&first.context_id)); + + let second = engine.create_context(CreateJavascriptContextRequest { + vm_id: String::from("vm-context-dispose"), + bootstrap_module: None, + compile_cache_root: None, + }); + assert_ne!(first.context_id, second.context_id); + } + #[test] fn javascript_limits_are_read_from_typed_fields_and_env_is_inert() { // Misleading env values: a reader that still consulted `AGENTOS_*` would @@ -7232,6 +7419,7 @@ mod tests { ), ]); let request = StartJavascriptExecutionRequest { + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: String::from("ctx-js"), @@ -7279,6 +7467,7 @@ mod tests { #[test] fn javascript_limits_fall_back_to_defaults_when_unset() { let request = StartJavascriptExecutionRequest { + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: String::from("ctx-js"), @@ -7451,12 +7640,9 @@ mod tests { fn javascript_event_sender_reports_closed_receiver() { let (sender, receiver) = channel(1); drop(receiver); - let host = V8RuntimeHost::spawn().expect("spawn V8 runtime host"); - let session = host.session_handle(String::from("closed-event-sender-test")); let gauge = register_queue(TrackedLimit::JavascriptEventChannel, 1); assert!(!send_javascript_event( &sender, - &session, &gauge, None, JavascriptExecutionEvent::Exited(1) @@ -7479,7 +7665,6 @@ mod tests { let _session_receiver = host .register_session(&session_id) .expect("register event overflow session"); - let session = host.session_handle(session_id.clone()); let gauge = register_queue(TrackedLimit::JavascriptEventChannel, 1); let (sender, mut event_receiver) = channel(1); @@ -7499,7 +7684,6 @@ mod tests { for _ in 0..SENDS { assert!(send_javascript_event( &sender, - &session, &gauge, None, JavascriptExecutionEvent::Stdout(Vec::new()) @@ -7517,39 +7701,31 @@ mod tests { } #[test] - fn javascript_event_sender_destroys_session_when_event_is_oversized() { - let host = V8RuntimeHost::spawn().expect("spawn V8 runtime host"); - let session_id = format!( - "event-oversized-{}", - SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system time") - .as_nanos() - ); - let receiver = host - .register_session(&session_id) - .expect("register oversized event session"); - let session = host.session_handle(session_id.clone()); - let (sender, _event_receiver) = channel(JAVASCRIPT_EVENT_CHANNEL_CAPACITY); + fn javascript_event_sender_chunks_oversized_output_without_data_loss() { + let (sender, mut event_receiver) = channel(JAVASCRIPT_EVENT_CHANNEL_CAPACITY); let gauge = register_queue( TrackedLimit::JavascriptEventChannel, JAVASCRIPT_EVENT_CHANNEL_CAPACITY, ); + let payload = vec![b'x'; JAVASCRIPT_EVENT_PAYLOAD_LIMIT_BYTES + 17]; - assert!(!send_javascript_event( + assert!(send_javascript_event( &sender, - &session, &gauge, None, - JavascriptExecutionEvent::Stdout(vec![0; JAVASCRIPT_EVENT_PAYLOAD_LIMIT_BYTES + 1]) + JavascriptExecutionEvent::Stdout(payload.clone()) )); - drop(receiver); - let recovered = host - .register_session(&session_id) - .expect("oversized event should destroy and deregister the session"); - drop(recovered); - host.unregister_session(&session_id); + let first = event_receiver.blocking_recv().expect("first chunk"); + let second = event_receiver.blocking_recv().expect("second chunk"); + let joined = [first, second] + .into_iter() + .flat_map(|event| match event { + JavascriptExecutionEvent::Stdout(chunk) => chunk, + other => panic!("unexpected event: {other:?}"), + }) + .collect::>(); + assert_eq!(joined, payload); } #[test] @@ -7637,6 +7813,7 @@ mod tests { fn javascript_cpu_time_limit_defaults_to_bounded_value() { let request = StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js-default-cpu"), context_id: String::from("ctx-js-default-cpu"), @@ -7667,6 +7844,7 @@ mod tests { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-drop-cleanup"), context_id: context.context_id, @@ -7689,6 +7867,49 @@ mod tests { host.unregister_session(&session_id); } + #[test] + fn prepared_execution_does_not_enqueue_guest_code_until_started() { + let temp = tempdir().expect("create temp dir"); + let mut engine = JavascriptExecutionEngine::default(); + let context = engine.create_context(CreateJavascriptContextRequest { + vm_id: String::from("vm-deferred-exec"), + bootstrap_module: None, + compile_cache_root: None, + }); + + let mut execution = engine + .prepare_execution(StartJavascriptExecutionRequest { + limits: Default::default(), + argv0: None, + guest_runtime: Default::default(), + vm_id: String::from("vm-deferred-exec"), + context_id: context.context_id, + argv: vec![String::from("./entry.mjs")], + env: BTreeMap::new(), + cwd: temp.path().to_path_buf(), + wasm_module_bytes: None, + inline_code: Some(String::from("process.stdout.write('started\\n');")), + }) + .expect("prepare JavaScript execution"); + + assert!(execution.is_prepared_for_start()); + assert_eq!( + execution + .poll_event_blocking(Duration::ZERO) + .expect("poll prepared execution"), + None, + "preparation must not enqueue any guest code" + ); + + execution + .start_prepared() + .expect("start prepared execution"); + assert!(!execution.is_prepared_for_start()); + let result = execution.wait().expect("wait for prepared execution"); + assert_eq!(result.exit_code, 0); + assert_eq!(result.stdout, b"started\n"); + } + // --- Timer cancellation / cap regression tests (U4: H2 bridge timers, M3 // kernel timers). These assert the *safeguards firing* (delay clamped, timer // entry reclaimed, callback suppressed) and never spawn unbounded threads. --- diff --git a/crates/execution/src/node_import_cache.rs b/crates/execution/src/node_import_cache.rs index 4703e281ed..a5d4437bcb 100644 --- a/crates/execution/src/node_import_cache.rs +++ b/crates/execution/src/node_import_cache.rs @@ -15,7 +15,7 @@ const NODE_IMPORT_CACHE_PATH_ENV: &str = "AGENTOS_NODE_IMPORT_CACHE_PATH"; const NODE_IMPORT_CACHE_LOADER_PATH_ENV: &str = "AGENTOS_NODE_IMPORT_CACHE_LOADER_PATH"; const NODE_IMPORT_CACHE_SCHEMA_VERSION: &str = "1"; const NODE_IMPORT_CACHE_LOADER_VERSION: &str = "8"; -const NODE_IMPORT_CACHE_ASSET_VERSION: &str = "85"; +const NODE_IMPORT_CACHE_ASSET_VERSION: &str = "104"; const NODE_IMPORT_CACHE_DIR_PREFIX: &str = "agentos-node-import-cache"; const DEFAULT_NODE_IMPORT_CACHE_MATERIALIZE_TIMEOUT: Duration = Duration::from_secs(30); const PYODIDE_DIST_DIR: &str = "pyodide-dist"; @@ -4093,6 +4093,7 @@ function createRpcBackedChildProcessModule(fromGuestDir = '/') { : fromGuestDir, env: normalizeChildProcessEnv(options?.env), internalBootstrapEnv: createChildProcessInternalBootstrapEnv(), + argv0: options?.argv0 == null ? undefined : String(options.argv0), shell: shell || options?.shell === true || @@ -4187,15 +4188,16 @@ function createRpcBackedChildProcessModule(fromGuestDir = '/') { let exitCode = null; let signal = null; let error = null; + let timedOut = false; while (exitCode == null && signal == null) { if ( + !timedOut && normalizedOptions.timeout != null && Date.now() - startedAt > normalizedOptions.timeout ) { callKill(child.childId, normalizedOptions.killSignal); - signal = normalizedOptions.killSignal; + timedOut = true; error = createSpawnSyncTimeoutError(command); - break; } const event = callPoll(child.childId, RPC_POLL_WAIT_MS); @@ -4263,8 +4265,11 @@ function createRpcBackedChildProcessModule(fromGuestDir = '/') { stream.push(null); }; const emitChildLifecycleEvents = (child) => { + child.emit('exit', child.exitCode, child.signalCode); + // stdout/stderr have already received their EOF marker. Defer close one + // microtask so stream end/close observers run after exit, matching Node's + // exit -> stdio EOF -> close lifecycle without a timing-based quiet period. queueMicrotask(() => { - child.emit('exit', child.exitCode, child.signalCode); child.emit('close', child.exitCode, child.signalCode); }); }; diff --git a/crates/execution/src/python.rs b/crates/execution/src/python.rs index 0ca16f14a6..80d1e0551a 100644 --- a/crates/execution/src/python.rs +++ b/crates/execution/src/python.rs @@ -128,6 +128,8 @@ pub struct PythonVfsRpcRequest { pub socket_id: Option, pub command: Option, pub args: Vec, + /// Explicit child argv[0], kept separate from the executable path. + pub argv0: Option, pub cwd: Option, pub env: BTreeMap, pub shell: bool, @@ -238,6 +240,8 @@ struct PythonVfsBridgeRequestWire { #[serde(default)] args: Vec, #[serde(default)] + argv0: Option, + #[serde(default)] cwd: Option, #[serde(default)] env: BTreeMap, @@ -337,6 +341,7 @@ pub enum PythonExecutionError { StdinClosed, Stdin(std::io::Error), Kill(std::io::Error), + Control(std::io::Error), TimedOut(Duration), PendingVfsRpcRequest(u64), RpcResponse(String), @@ -385,6 +390,7 @@ impl fmt::Display for PythonExecutionError { Self::StdinClosed => f.write_str("guest Python stdin is already closed"), Self::Stdin(err) => write!(f, "failed to write guest stdin: {err}"), Self::Kill(err) => write!(f, "failed to kill guest Python runtime: {err}"), + Self::Control(err) => write!(f, "failed to control guest Python runtime: {err}"), Self::TimedOut(timeout) => write!( f, "guest Python runtime timed out after {}ms", @@ -470,6 +476,15 @@ impl PythonExecution { self.inner.uses_shared_v8_runtime() } + pub fn start_prepared(&mut self) -> Result<(), PythonExecutionError> { + self.inner.start_prepared().map_err(map_javascript_error) + } + + #[doc(hidden)] + pub fn is_prepared_for_start(&self) -> bool { + self.inner.is_prepared_for_start() + } + pub fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), PythonExecutionError> { self.inner .write_kernel_stdin_only(chunk) @@ -490,6 +505,14 @@ impl PythonExecution { self.inner.terminate().map_err(map_javascript_error) } + pub fn pause(&self) -> Result<(), PythonExecutionError> { + self.inner.pause().map_err(map_javascript_error) + } + + pub fn resume(&self) -> Result<(), PythonExecutionError> { + self.inner.resume().map_err(map_javascript_error) + } + pub fn respond_vfs_rpc_success( &mut self, id: u64, @@ -615,6 +638,25 @@ impl PythonExecution { .map_err(map_javascript_error) } + pub fn claim_javascript_sync_rpc_response( + &mut self, + id: u64, + ) -> Result { + self.inner + .claim_sync_rpc_response(id) + .map_err(map_javascript_error) + } + + pub fn respond_claimed_javascript_sync_rpc_success( + &mut self, + id: u64, + result: Value, + ) -> Result<(), PythonExecutionError> { + self.inner + .respond_claimed_sync_rpc_success(id, result) + .map_err(map_javascript_error) + } + pub fn respond_javascript_sync_rpc_error( &mut self, id: u64, @@ -626,6 +668,17 @@ impl PythonExecution { .map_err(map_javascript_error) } + pub fn respond_claimed_javascript_sync_rpc_error( + &mut self, + id: u64, + code: impl Into, + message: impl Into, + ) -> Result<(), PythonExecutionError> { + self.inner + .respond_claimed_sync_rpc_error(id, code, message) + .map_err(map_javascript_error) + } + pub async fn poll_event( &mut self, timeout: Duration, @@ -894,9 +947,45 @@ impl PythonExecutionEngine { context } + /// Dispose the Python context and its private JavaScript bridge context. + /// Live executions retain their own bridge session and are unaffected. + pub fn dispose_context(&mut self, context_id: &str) -> bool { + let removed = self.contexts.remove(context_id).is_some(); + if let Some(javascript_context_id) = self.javascript_context_ids.remove(context_id) { + self.javascript_engine + .dispose_context(&javascript_context_id); + } + removed + } + + #[doc(hidden)] + pub fn context_count_for_test(&self) -> usize { + self.contexts.len() + } + + #[doc(hidden)] + pub fn javascript_context_count_for_test(&self) -> usize { + self.javascript_engine.context_count_for_test() + } + pub fn start_execution( &mut self, request: StartPythonExecutionRequest, + ) -> Result { + self.create_execution(request, false) + } + + pub fn prepare_execution( + &mut self, + request: StartPythonExecutionRequest, + ) -> Result { + self.create_execution(request, true) + } + + fn create_execution( + &mut self, + request: StartPythonExecutionRequest, + defer_execute: bool, ) -> Result { ensure_pyodide_available()?; let context = self @@ -913,16 +1002,11 @@ impl PythonExecutionEngine { } let frozen_time_ms = frozen_time_ms(); - let javascript_context = - self.javascript_engine - .create_context(CreateJavascriptContextRequest { - vm_id: request.vm_id.clone(), - bootstrap_module: None, - compile_cache_root: None, - }); - let javascript_context_id = javascript_context.context_id.clone(); - self.javascript_context_ids - .insert(context.context_id.clone(), javascript_context_id.clone()); + let javascript_context_id = self + .javascript_context_ids + .get(&context.context_id) + .cloned() + .ok_or_else(|| PythonExecutionError::MissingContext(context.context_id.clone()))?; let warmup_metrics = { let import_cache = self.import_caches.entry(context.vm_id.clone()).or_default(); import_cache @@ -956,6 +1040,7 @@ impl PythonExecutionEngine { frozen_time_ms, prewarm_only: false, warmup_metrics: warmup_metrics.as_deref(), + defer_execute, }, )?; let pending_vfs_rpc = Arc::new(Mutex::new(None)); @@ -1020,6 +1105,7 @@ fn map_javascript_error(error: JavascriptExecutionError) -> PythonExecutionError PythonExecutionError::RpcResponse(message) } JavascriptExecutionError::Terminate(error) => PythonExecutionError::Kill(error), + JavascriptExecutionError::Control(error) => PythonExecutionError::Control(error), JavascriptExecutionError::StdinClosed => PythonExecutionError::StdinClosed, JavascriptExecutionError::Stdin(error) => PythonExecutionError::Stdin(error), JavascriptExecutionError::OutputBufferExceeded { stream, limit } => { @@ -1033,6 +1119,7 @@ struct PythonJavascriptExecutionOptions<'a> { frozen_time_ms: u128, prewarm_only: bool, warmup_metrics: Option<&'a [u8]>, + defer_execute: bool, } fn start_python_javascript_execution( @@ -1065,21 +1152,26 @@ fn start_python_javascript_execution( ..JavascriptExecutionLimits::default() }; - javascript_engine - .start_execution(StartJavascriptExecutionRequest { - vm_id: request.vm_id.clone(), - context_id: javascript_context_id.to_owned(), - argv: vec![import_cache.python_runner_path().display().to_string()], - env, - cwd: request.cwd.clone(), - limits: runner_limits, - // Forward the guest-runtime identity so the runner's shim sets - // process.* from typed config rather than env. - guest_runtime: request.guest_runtime.clone(), - wasm_module_bytes: None, - inline_code: Some(inline_code), - }) - .map_err(map_javascript_error) + let javascript_request = StartJavascriptExecutionRequest { + vm_id: request.vm_id.clone(), + context_id: javascript_context_id.to_owned(), + argv: vec![import_cache.python_runner_path().display().to_string()], + argv0: None, + env, + cwd: request.cwd.clone(), + limits: runner_limits, + // Forward the guest-runtime identity so the runner's shim sets + // process.* from typed config rather than env. + guest_runtime: request.guest_runtime.clone(), + wasm_module_bytes: None, + inline_code: Some(inline_code), + }; + if options.defer_execute { + javascript_engine.prepare_execution(javascript_request) + } else { + javascript_engine.start_execution(javascript_request) + } + .map_err(map_javascript_error) } fn build_python_internal_env( @@ -1304,6 +1396,7 @@ fn parse_python_bridge_sync_rpc_request( socket_id: wire.socket_id, command: wire.command, args: wire.args, + argv0: wire.argv0, cwd: wire.cwd, env: wire.env, shell: wire.shell, @@ -1440,6 +1533,7 @@ fn prewarm_python_path( frozen_time_ms, prewarm_only: true, warmup_metrics: None, + defer_execute: false, }, )?; let mut stdout = Vec::new(); @@ -2069,14 +2163,39 @@ fn warmup_metrics_line( #[cfg(test)] mod tests { use super::{ - python_managed_path_kind, PythonManagedPathKind, PYODIDE_CACHE_GUEST_ROOT, - PYODIDE_GUEST_ROOT, + python_managed_path_kind, CreatePythonContextRequest, PythonExecutionEngine, + PythonManagedPathKind, PYODIDE_CACHE_GUEST_ROOT, PYODIDE_GUEST_ROOT, }; use std::fs; #[cfg(unix)] use std::os::unix::fs::symlink; use tempfile::tempdir; + #[test] + fn dispose_context_reclaims_python_and_nested_javascript_metadata() { + let mut engine = PythonExecutionEngine::default(); + let baseline = ( + engine.context_count_for_test(), + engine.javascript_context_count_for_test(), + ); + let temp = tempdir().expect("create Pyodide fixture root"); + let context = engine.create_context(CreatePythonContextRequest { + vm_id: String::from("vm-python-context-dispose"), + pyodide_dist_path: temp.path().to_path_buf(), + }); + assert_eq!(engine.context_count_for_test(), baseline.0 + 1); + assert_eq!(engine.javascript_context_count_for_test(), baseline.1 + 1); + + assert!(engine.dispose_context(&context.context_id)); + assert_eq!( + ( + engine.context_count_for_test(), + engine.javascript_context_count_for_test(), + ), + baseline + ); + } + #[test] fn python_managed_guest_paths_normalize_dot_dot_inside_root() { let temp = tempdir().expect("create temp dir"); diff --git a/crates/execution/src/v8_host.rs b/crates/execution/src/v8_host.rs index fa111c6956..5b7703926a 100644 --- a/crates/execution/src/v8_host.rs +++ b/crates/execution/src/v8_host.rs @@ -288,6 +288,16 @@ impl V8SessionHandle { self.inner.terminate() } + /// Suspend guest execution while preserving the V8 stack and session state. + pub fn pause(&self) -> io::Result<()> { + self.inner.pause() + } + + /// Resume a session previously suspended with [`Self::pause`]. + pub fn resume(&self) -> io::Result<()> { + self.inner.resume() + } + /// Destroy this session in the embedded runtime and remove its receiver. pub fn destroy(&self) -> io::Result<()> { let _ = self.inner.terminate(); diff --git a/crates/execution/src/wasm.rs b/crates/execution/src/wasm.rs index 8a7a806dc2..7cefa1ae1b 100644 --- a/crates/execution/src/wasm.rs +++ b/crates/execution/src/wasm.rs @@ -37,6 +37,12 @@ const WASM_WARMUP_DEBUG_ENV: &str = "AGENTOS_WASM_WARMUP_DEBUG"; pub const WASM_MAX_FUEL_ENV: &str = "AGENTOS_WASM_MAX_FUEL"; pub const WASM_MAX_MEMORY_BYTES_ENV: &str = "AGENTOS_WASM_MAX_MEMORY_BYTES"; pub const WASM_MAX_STACK_BYTES_ENV: &str = "AGENTOS_WASM_MAX_STACK_BYTES"; +pub const WASM_MAX_MODULE_FILE_BYTES_ENV: &str = "AGENTOS_WASM_MAX_MODULE_FILE_BYTES"; +const WASM_MAX_OPEN_FDS_ENV: &str = "AGENTOS_WASM_MAX_OPEN_FDS"; +const WASM_MAX_SPAWN_FILE_ACTIONS_ENV: &str = "AGENTOS_WASM_MAX_SPAWN_FILE_ACTIONS"; +const WASM_MAX_SPAWN_FILE_ACTION_BYTES_ENV: &str = "AGENTOS_WASM_MAX_SPAWN_FILE_ACTION_BYTES"; +const WASM_MAX_SOCKETS_ENV: &str = "AGENTOS_WASM_MAX_SOCKETS"; +const WASM_MAX_BLOCKING_READ_MS_ENV: &str = "AGENTOS_WASM_MAX_BLOCKING_READ_MS"; const WASM_WARMUP_METRICS_PREFIX: &str = "__AGENTOS_WASM_WARMUP_METRICS__:"; const WASM_SIGNAL_STATE_PREFIX: &str = "__AGENTOS_WASM_SIGNAL_STATE__:"; const WASM_WARMUP_MARKER_VERSION: &str = "1"; @@ -110,14 +116,6 @@ const WASM_SIDECAR_ROUTED_FS_SYNC_METHODS: &[&str] = &[ "fs.writeFileSync", "fs.writeSync", ]; -const WASM_SIDECAR_ROUTED_KERNEL_SYNC_METHODS: &[&str] = &[ - "__kernel_isatty", - "__kernel_poll", - "__kernel_stdin_read", - "__kernel_stdio_write", - "__kernel_tty_size", - "__pty_set_raw_mode", -]; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WasmSignalDispositionAction { @@ -182,6 +180,19 @@ pub struct WasmExecutionLimits { /// fails closed; runtime V8 stack-limit enforcement is a follow-up — see /// [`resolve_wasm_stack_limit_bytes`]. pub max_stack_bytes: Option, + /// Maximum executable image bytes accepted for initial and replacement + /// modules. The trusted runner needs the typed value for fexecve preads. + pub max_module_file_bytes: Option, + /// Maximum number of file actions decoded for one posix_spawn call. + pub max_spawn_file_actions: Option, + /// Maximum serialized file-action bytes accepted for one posix_spawn call. + pub max_spawn_file_action_bytes: Option, + /// Maximum guest-visible open descriptors, including runner-owned sockets. + pub max_open_fds: Option, + /// Maximum runner-owned guest sockets. + pub max_sockets: Option, + /// Maximum time a blocking runner syscall may cooperatively wait. + pub max_blocking_read_ms: Option, /// Best-effort warmup/compile-cache timeout in ms. pub prewarm_timeout_ms: Option, /// V8 heap cap for the trusted JS runner isolate that hosts WASI/WASM. @@ -275,6 +286,7 @@ pub enum WasmExecutionError { stderr: String, }, Spawn(std::io::Error), + Control(std::io::Error), RpcResponse(String), StdinClosed, Stdin(std::io::Error), @@ -369,6 +381,7 @@ impl fmt::Display for WasmExecutionError { } } Self::Spawn(err) => write!(f, "failed to start guest WebAssembly runtime: {err}"), + Self::Control(err) => write!(f, "failed to control guest WebAssembly runtime: {err}"), Self::RpcResponse(message) => { write!( f, @@ -405,6 +418,8 @@ pub struct WasmExecution { pending_events: VecDeque, stdout_stream_buffer: Vec, stderr_stream_buffer: Vec, + max_stack_bytes: Option, + pending_v8_stack_overflow: Option>, } #[derive(Debug)] @@ -445,6 +460,17 @@ impl WasmExecution { self.inner.uses_shared_v8_runtime() } + pub fn start_prepared(&mut self) -> Result<(), WasmExecutionError> { + self.inner.start_prepared().map_err(map_javascript_error)?; + self.execution_started_at = Instant::now(); + Ok(()) + } + + #[doc(hidden)] + pub fn is_prepared_for_start(&self) -> bool { + self.inner.is_prepared_for_start() + } + pub fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), WasmExecutionError> { self.inner.write_stdin(chunk).map_err(map_javascript_error) } @@ -479,6 +505,14 @@ impl WasmExecution { self.inner.terminate().map_err(map_javascript_error) } + pub fn pause(&self) -> Result<(), WasmExecutionError> { + self.inner.pause().map_err(map_javascript_error) + } + + pub fn resume(&self) -> Result<(), WasmExecutionError> { + self.inner.resume().map_err(map_javascript_error) + } + pub fn respond_sync_rpc_success( &mut self, id: u64, @@ -489,6 +523,22 @@ impl WasmExecution { .map_err(map_javascript_error) } + pub fn claim_sync_rpc_response(&mut self, id: u64) -> Result { + self.inner + .claim_sync_rpc_response(id) + .map_err(map_javascript_error) + } + + pub fn respond_claimed_sync_rpc_success( + &mut self, + id: u64, + result: Value, + ) -> Result<(), WasmExecutionError> { + self.inner + .respond_claimed_sync_rpc_success(id, result) + .map_err(map_javascript_error) + } + pub fn respond_sync_rpc_raw_success( &mut self, id: u64, @@ -510,6 +560,17 @@ impl WasmExecution { .map_err(map_javascript_error) } + pub fn respond_claimed_sync_rpc_error( + &mut self, + id: u64, + code: impl Into, + message: impl Into, + ) -> Result<(), WasmExecutionError> { + self.inner + .respond_claimed_sync_rpc_error(id, code, message) + .map_err(map_javascript_error) + } + pub async fn poll_event( &mut self, timeout: Duration, @@ -689,7 +750,17 @@ impl WasmExecution { self.enqueue_stream_chunk(StreamChannel::Stdout, chunk)? } JavascriptExecutionEvent::Stderr(chunk) => { - self.enqueue_stream_chunk(StreamChannel::Stderr, chunk)? + if self.max_stack_bytes.is_some() && is_v8_stack_overflow_stderr(&chunk) { + let pending = self.pending_v8_stack_overflow.get_or_insert_with(Vec::new); + ensure_wasm_output_capacity( + pending.len(), + chunk.len(), + "pending stack-overflow stderr", + )?; + pending.extend_from_slice(&chunk); + } else { + self.enqueue_stream_chunk(StreamChannel::Stderr, chunk)? + } } JavascriptExecutionEvent::SyncRpcRequest(request) => { self.pending_events @@ -706,6 +777,18 @@ impl WasmExecution { }); } JavascriptExecutionEvent::Exited(code) => { + if let Some(original) = self.pending_v8_stack_overflow.take() { + let chunk = if code != 0 { + configured_wasm_stack_limit_error( + self.max_stack_bytes + .expect("stack-overflow buffering requires a configured limit"), + ) + .into_bytes() + } else { + original + }; + self.enqueue_stream_chunk(StreamChannel::Stderr, chunk)?; + } self.flush_stream_buffers(); self.pending_events .push_back(WasmExecutionEvent::Exited(code)); @@ -876,9 +959,46 @@ impl WasmExecutionEngine { context } + /// Dispose the WASM context and the private JavaScript bridge context that + /// belongs to it. A started execution has already cloned all required + /// runtime state and remains valid after this returns. + pub fn dispose_context(&mut self, context_id: &str) -> bool { + let removed = self.contexts.remove(context_id).is_some(); + if let Some(javascript_context_id) = self.javascript_context_ids.remove(context_id) { + self.javascript_engine + .dispose_context(&javascript_context_id); + } + removed + } + + #[doc(hidden)] + pub fn context_count_for_test(&self) -> usize { + self.contexts.len() + } + + #[doc(hidden)] + pub fn javascript_context_count_for_test(&self) -> usize { + self.javascript_engine.context_count_for_test() + } + pub fn start_execution( &mut self, request: StartWasmExecutionRequest, + ) -> Result { + self.create_execution(request, false) + } + + pub fn prepare_execution( + &mut self, + request: StartWasmExecutionRequest, + ) -> Result { + self.create_execution(request, true) + } + + fn create_execution( + &mut self, + request: StartWasmExecutionRequest, + defer_execute: bool, ) -> Result { let context = self .contexts @@ -944,6 +1064,7 @@ impl WasmExecutionEngine { frozen_time_ms, prewarm_only: false, warmup_metrics: warmup_metrics.as_deref(), + defer_execute, }, )?; let child_pid = javascript_execution.child_pid(); @@ -968,6 +1089,8 @@ impl WasmExecutionEngine { pending_events: VecDeque::new(), stdout_stream_buffer: Vec::new(), stderr_stream_buffer: Vec::new(), + max_stack_bytes: request.limits.max_stack_bytes, + pending_v8_stack_overflow: None, internal_sync_rpc: WasmInternalSyncRpc { module_guest_paths: wasm_guest_module_paths( &resolved_module.specifier, @@ -1019,6 +1142,7 @@ fn map_javascript_error(error: JavascriptExecutionError) -> WasmExecutionError { ), JavascriptExecutionError::RpcResponse(message) => WasmExecutionError::RpcResponse(message), JavascriptExecutionError::Terminate(error) => WasmExecutionError::Spawn(error), + JavascriptExecutionError::Control(error) => WasmExecutionError::Control(error), JavascriptExecutionError::StdinClosed => WasmExecutionError::StdinClosed, JavascriptExecutionError::Stdin(error) => WasmExecutionError::Stdin(error), JavascriptExecutionError::OutputBufferExceeded { stream, limit } => { @@ -1533,8 +1657,7 @@ fn wasm_sync_rpc_method_routes_through_sidecar_kernel( internal_sync_rpc: &WasmInternalSyncRpc, ) -> bool { internal_sync_rpc.route_fs_through_sidecar - && (WASM_SIDECAR_ROUTED_FS_SYNC_METHODS.contains(&request.method.as_str()) - || WASM_SIDECAR_ROUTED_KERNEL_SYNC_METHODS.contains(&request.method.as_str())) + && WASM_SIDECAR_ROUTED_FS_SYNC_METHODS.contains(&request.method.as_str()) } fn translate_wasm_guest_path( @@ -1950,6 +2073,20 @@ fn base64_decoded_len(base64: &str) -> Option { } } +fn is_v8_stack_overflow_stderr(chunk: &[u8]) -> bool { + std::str::from_utf8(chunk).is_ok_and(|message| { + message.starts_with("RangeError: Maximum call stack size exceeded") + && message.contains("wasm-function") + }) +} + +fn configured_wasm_stack_limit_error(limit: u64) -> String { + format!( + "WebAssembly guest exhausted its configured stack budget ({limit} bytes); \ +raise limits.resources.maxWasmStackBytes to allow deeper guest call stacks.\n" + ) +} + fn append_wasm_captured_output( buffer: &mut Vec, chunk: &[u8], @@ -2094,7 +2231,11 @@ fn translate_wasm_signal_state_sync_rpc_request( let flags = request .args .get(3) - .and_then(Value::as_u64) + .and_then(|value| { + value + .as_u64() + .or_else(|| value.as_i64().map(|signed| signed as u64)) + }) .unwrap_or_default() as u32; execution @@ -2173,6 +2314,7 @@ struct WasmJavascriptExecutionOptions<'a> { frozen_time_ms: u128, prewarm_only: bool, warmup_metrics: Option<&'a [u8]>, + defer_execute: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -2282,30 +2424,35 @@ fn start_wasm_javascript_execution( } }; - javascript_engine - .start_execution(StartJavascriptExecutionRequest { - vm_id: request.vm_id.clone(), - context_id: javascript_context_id.to_owned(), - argv: vec![String::from(WASM_INLINE_RUNNER_ENTRYPOINT)], - env, - cwd: request.cwd.clone(), - // Guest WASM fuel/memory caps are enforced from `request.limits`, - // and stack caps are validated there until runtime stack enforcement - // lands. These are separate from the runner's V8 heap: the trusted - // runner still has to compile the WASI runtime + guest module into - // its own isolate, which can overflow the 128 MiB per-guest default, - // so size the runner heap explicitly (operator-tunable). - limits: JavascriptExecutionLimits { - v8_heap_limit_mb: Some(wasm_runner_heap_limit_mb(request)), - ..JavascriptExecutionLimits::default() - }, - // Forward the guest-runtime identity so the runner's shim sets - // process.* from typed config rather than env. - guest_runtime, - inline_code: Some(inline_code), - wasm_module_bytes: Some(wasm_module_bytes), - }) - .map_err(map_javascript_error) + let javascript_request = StartJavascriptExecutionRequest { + vm_id: request.vm_id.clone(), + context_id: javascript_context_id.to_owned(), + argv: vec![String::from(WASM_INLINE_RUNNER_ENTRYPOINT)], + argv0: None, + env, + cwd: request.cwd.clone(), + // Guest WASM fuel/memory caps are enforced from `request.limits`, + // and stack caps are validated there until runtime stack enforcement + // lands. These are separate from the runner's V8 heap: the trusted + // runner still has to compile the WASI runtime + guest module into + // its own isolate, which can overflow the 128 MiB per-guest default, + // so size the runner heap explicitly (operator-tunable). + limits: JavascriptExecutionLimits { + v8_heap_limit_mb: Some(wasm_runner_heap_limit_mb(request)), + ..JavascriptExecutionLimits::default() + }, + // Forward the guest-runtime identity so the runner's shim sets + // process.* from typed config rather than env. + guest_runtime, + inline_code: Some(inline_code), + wasm_module_bytes: Some(wasm_module_bytes), + }; + if options.defer_execute { + javascript_engine.prepare_execution(javascript_request) + } else { + javascript_engine.start_execution(javascript_request) + } + .map_err(map_javascript_error) } struct WasmModuleBytesCache { @@ -2384,6 +2531,36 @@ fn build_wasm_internal_env( WASM_MAX_MEMORY_BYTES_ENV, request.limits.max_memory_bytes, ); + insert_optional_u64_env( + &mut internal_env, + WASM_MAX_MODULE_FILE_BYTES_ENV, + request.limits.max_module_file_bytes, + ); + insert_optional_u64_env( + &mut internal_env, + WASM_MAX_OPEN_FDS_ENV, + request.limits.max_open_fds, + ); + insert_optional_u64_env( + &mut internal_env, + WASM_MAX_SPAWN_FILE_ACTIONS_ENV, + request.limits.max_spawn_file_actions, + ); + insert_optional_u64_env( + &mut internal_env, + WASM_MAX_SPAWN_FILE_ACTION_BYTES_ENV, + request.limits.max_spawn_file_action_bytes, + ); + insert_optional_u64_env( + &mut internal_env, + WASM_MAX_SOCKETS_ENV, + request.limits.max_sockets, + ); + insert_optional_u64_env( + &mut internal_env, + WASM_MAX_BLOCKING_READ_MS_ENV, + request.limits.max_blocking_read_ms, + ); internal_env.insert( WASM_MODULE_PATH_ENV.to_string(), resolved_module.specifier.clone(), @@ -2448,6 +2625,12 @@ fn scrub_migrated_wasm_limit_env(env: &mut BTreeMap) { WASM_MAX_FUEL_ENV, WASM_MAX_MEMORY_BYTES_ENV, WASM_MAX_STACK_BYTES_ENV, + WASM_MAX_MODULE_FILE_BYTES_ENV, + WASM_MAX_OPEN_FDS_ENV, + WASM_MAX_SPAWN_FILE_ACTIONS_ENV, + WASM_MAX_SPAWN_FILE_ACTION_BYTES_ENV, + WASM_MAX_SOCKETS_ENV, + WASM_MAX_BLOCKING_READ_MS_ENV, "AGENTOS_WASM_PREWARM_TIMEOUT_MS", "AGENTOS_WASM_RUNNER_HEAP_LIMIT_MB", ] { @@ -2707,6 +2890,16 @@ if (typeof globalThis !== "undefined") {{ throw new Error("secure-exec WASM process kill bridge is unavailable"); }} return _processKill.applySync(void 0, args); + case "process.exec": + if (typeof _processExec === "undefined") {{ + throw new Error("secure-exec WASM process exec bridge is unavailable"); + }} + return _processExec.applySync(void 0, args); + case "process.exec_fd_image_commit": + if (typeof _processExecFdImageCommit === "undefined") {{ + throw new Error("secure-exec WASM process fd image commit bridge is unavailable"); + }} + return _processExecFdImageCommit.applySync(void 0, args); case "child_process.write_stdin": {{ if (typeof _childProcessStdinWrite === "undefined") {{ throw new Error("secure-exec WASM child_process stdin bridge is unavailable"); @@ -2727,6 +2920,16 @@ if (typeof globalThis !== "undefined") {{ throw new Error("secure-exec WASM net.connect bridge is unavailable"); }} return _netSocketConnectRaw.applySync(void 0, args); + case "net.bind_unix": + if (typeof _netBindUnixRaw === "undefined") {{ + throw new Error("secure-exec WASM net.bind_unix bridge is unavailable"); + }} + return _netBindUnixRaw.applySync(void 0, args); + case "net.bind_connected_unix": + if (typeof _netBindConnectedUnixRaw === "undefined") {{ + throw new Error("secure-exec WASM net.bind_connected_unix bridge is unavailable"); + }} + return _netBindConnectedUnixRaw.applySync(void 0, args); case "net.reserve_tcp_port": if (typeof _netReserveTcpPortRaw === "undefined") {{ throw new Error("secure-exec WASM net.reserve_tcp_port bridge is unavailable"); @@ -2747,11 +2950,26 @@ if (typeof globalThis !== "undefined") {{ throw new Error("secure-exec WASM net.server_accept bridge is unavailable"); }} return _netServerAcceptRaw.applySync(void 0, args); + case "net.server_close": + if (typeof _netServerCloseSyncRaw === "undefined") {{ + throw new Error("secure-exec WASM net.server_close bridge is unavailable"); + }} + return _netServerCloseSyncRaw.applySync(void 0, args); case "net.poll": if (typeof _netSocketPollRaw === "undefined") {{ throw new Error("secure-exec WASM net.poll bridge is unavailable"); }} return _netSocketPollRaw.applySync(void 0, args); + case "net.socket_read": + if (typeof _netSocketReadRaw === "undefined") {{ + throw new Error("secure-exec WASM net.socket_read bridge is unavailable"); + }} + return _netSocketReadRaw.applySync(void 0, args); + case "net.socket_wait_connect": + if (typeof _netSocketWaitConnectSyncRaw === "undefined") {{ + throw new Error("secure-exec WASM net.socket_wait_connect bridge is unavailable"); + }} + return _netSocketWaitConnectSyncRaw.applySync(void 0, args); case "net.write": if (typeof _netSocketWriteRaw === "undefined") {{ throw new Error("secure-exec WASM net.write bridge is unavailable"); @@ -2852,6 +3070,60 @@ if (typeof globalThis !== "undefined") {{ flags, ]); }} + case "process.take_signal": + if (typeof _processTakeSignal === "undefined") {{ + throw new Error("secure-exec WASM signal-drain bridge is unavailable"); + }} + return _processTakeSignal.applySync(void 0, args); + case "process.getpgid": + case "process.setpgid": + case "process.waitpid_transition": + case "process.itimer_real": + case "process.fd_pipe": + case "process.fd_open": + case "process.path_open_at": + case "process.path_mkdir_at": + case "process.path_stat_at": + case "process.path_utimes_at": + case "process.path_chown_at": + case "process.path_link_at": + case "process.path_readlink_at": + case "process.path_remove_dir_at": + case "process.path_rename_at": + case "process.path_symlink_at": + case "process.path_unlink_at": + case "process.fd_snapshot": + case "process.fd_read": + case "process.fd_pread": + case "process.fd_write": + case "process.fd_pwrite": + case "process.fd_sync": + case "process.fd_datasync": + case "process.fd_readdir": + case "process.fd_close": + case "process.fd_stat": + case "process.fd_filestat": + case "process.fd_chmod": + case "process.fd_chown": + case "process.fd_truncate": + case "process.fd_set_flags": + case "process.fd_getfd": + case "process.fd_setfd": + case "process.fd_record_lock": + case "process.fd_record_lock_cancel": + case "process.fd_dup": + case "process.fd_dup2": + case "process.fd_dup_min": + case "process.fd_seek": + case "process.fd_chdir_path": + case "process.fd_socketpair": + case "process.fd_sendmsg_rights": + case "process.fd_recvmsg_rights": + case "process.fd_socket_shutdown": + if (typeof _processWasmSyncRpc === "undefined") {{ + throw new Error("secure-exec WASM process-syscall bridge is unavailable"); + }} + return _processWasmSyncRpc.applySync(void 0, [method, ...args]); default: throw new Error(`secure-exec WASM sync RPC method not implemented in V8 runtime: ${{method}}`); }} @@ -2973,6 +3245,7 @@ fn prewarm_wasm_path( frozen_time_ms, prewarm_only: true, warmup_metrics: None, + defer_execute: false, }, ) .map_err(|error| match error { @@ -3845,14 +4118,14 @@ mod tests { wasm_read_only_filesystem_error, wasm_runner_base_env, wasm_runner_heap_limit_mb, wasm_sandbox_root, wasm_snapshot_runner_base_env, wasm_sync_read_length, wasm_sync_rpc_error_code, wasm_sync_rpc_method_routes_through_sidecar_kernel, - GuestRuntimeConfig, JavascriptSyncRpcRequest, ResolvedWasmModule, - StartWasmExecutionRequest, Value, WasmExecutionError, WasmExecutionLimits, - WasmInternalSyncRpc, WasmPermissionTier, DEFAULT_WASM_PREWARM_TIMEOUT_MS, - DEFAULT_WASM_RUNNER_HEAP_LIMIT_MB, NODE_WASI_MODULE_SOURCE, - WASM_CAPTURED_OUTPUT_LIMIT_BYTES, WASM_MAX_FUEL_ENV, WASM_MAX_MEMORY_BYTES_ENV, - WASM_MAX_STACK_BYTES_ENV, WASM_PAGE_BYTES, WASM_SANDBOX_ROOT_ENV, - WASM_SIDECAR_ROUTED_FS_SYNC_METHODS, WASM_SIDECAR_ROUTED_KERNEL_SYNC_METHODS, - WASM_SYNC_READ_LIMIT_BYTES, + CreateWasmContextRequest, GuestRuntimeConfig, JavascriptSyncRpcRequest, ResolvedWasmModule, + StartWasmExecutionRequest, Value, WasmExecutionEngine, WasmExecutionError, + WasmExecutionLimits, WasmInternalSyncRpc, WasmPermissionTier, + DEFAULT_WASM_PREWARM_TIMEOUT_MS, DEFAULT_WASM_RUNNER_HEAP_LIMIT_MB, + NODE_WASI_MODULE_SOURCE, WASM_CAPTURED_OUTPUT_LIMIT_BYTES, WASM_MAX_FUEL_ENV, + WASM_MAX_MEMORY_BYTES_ENV, WASM_MAX_MODULE_FILE_BYTES_ENV, WASM_MAX_SPAWN_FILE_ACTIONS_ENV, + WASM_MAX_SPAWN_FILE_ACTION_BYTES_ENV, WASM_MAX_STACK_BYTES_ENV, WASM_PAGE_BYTES, + WASM_SANDBOX_ROOT_ENV, WASM_SIDECAR_ROUTED_FS_SYNC_METHODS, WASM_SYNC_READ_LIMIT_BYTES, }; use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::fs; @@ -3861,6 +4134,30 @@ mod tests { use std::time::Duration; use tempfile::tempdir; + #[test] + fn dispose_context_reclaims_wasm_and_nested_javascript_metadata() { + let mut engine = WasmExecutionEngine::default(); + let baseline = ( + engine.context_count_for_test(), + engine.javascript_context_count_for_test(), + ); + let context = engine.create_context(CreateWasmContextRequest { + vm_id: String::from("vm-wasm-context-dispose"), + module_path: None, + }); + assert_eq!(engine.context_count_for_test(), baseline.0 + 1); + assert_eq!(engine.javascript_context_count_for_test(), baseline.1 + 1); + + assert!(engine.dispose_context(&context.context_id)); + assert_eq!( + ( + engine.context_count_for_test(), + engine.javascript_context_count_for_test(), + ), + baseline + ); + } + fn request_with_env(cwd: &Path, env: BTreeMap) -> StartWasmExecutionRequest { // Translate the legacy `AGENTOS_WASM_*` limit env keys these tests still // express into the typed limits the engine now reads (mirrors the @@ -3870,7 +4167,13 @@ mod tests { max_fuel: parse(WASM_MAX_FUEL_ENV), max_memory_bytes: parse(WASM_MAX_MEMORY_BYTES_ENV), max_stack_bytes: parse(WASM_MAX_STACK_BYTES_ENV), + max_module_file_bytes: None, + max_spawn_file_actions: None, + max_spawn_file_action_bytes: None, prewarm_timeout_ms: None, + max_open_fds: None, + max_sockets: None, + max_blocking_read_ms: None, runner_heap_limit_mb: None, }; StartWasmExecutionRequest { @@ -3945,6 +4248,14 @@ mod tests { String::from("AGENTOS_WASM_RUNNER_HEAP_LIMIT_MB"), String::from("999999"), ), + ( + String::from(WASM_MAX_SPAWN_FILE_ACTIONS_ENV), + String::from("999999"), + ), + ( + String::from(WASM_MAX_SPAWN_FILE_ACTION_BYTES_ENV), + String::from("999999"), + ), ]), cwd: PathBuf::from("/tmp"), permission_tier: WasmPermissionTier::Full, @@ -3957,7 +4268,13 @@ mod tests { max_fuel: Some(25), max_memory_bytes: Some(65_536), max_stack_bytes: Some(131_072), + max_module_file_bytes: Some(262_144), + max_spawn_file_actions: Some(7), + max_spawn_file_action_bytes: Some(321), prewarm_timeout_ms: Some(750), + max_open_fds: None, + max_sockets: None, + max_blocking_read_ms: None, runner_heap_limit_mb: Some(512), }); @@ -4020,7 +4337,13 @@ mod tests { max_fuel: Some(25), max_memory_bytes: Some(65_536), max_stack_bytes: Some(131_072), + max_module_file_bytes: Some(262_144), + max_spawn_file_actions: Some(7), + max_spawn_file_action_bytes: Some(321), prewarm_timeout_ms: Some(750), + max_open_fds: None, + max_sockets: None, + max_blocking_read_ms: None, runner_heap_limit_mb: Some(512), }); let resolved_module = ResolvedWasmModule { @@ -4035,6 +4358,18 @@ mod tests { internal_env.get(WASM_MAX_MEMORY_BYTES_ENV), Some(&String::from("65536")) ); + assert_eq!( + internal_env.get(WASM_MAX_MODULE_FILE_BYTES_ENV), + Some(&String::from("262144")) + ); + assert_eq!( + internal_env.get(WASM_MAX_SPAWN_FILE_ACTIONS_ENV), + Some(&String::from("7")) + ); + assert_eq!( + internal_env.get(WASM_MAX_SPAWN_FILE_ACTION_BYTES_ENV), + Some(&String::from("321")) + ); assert!(!internal_env.contains_key(WASM_MAX_STACK_BYTES_ENV)); assert!(!internal_env.contains_key(WASM_MAX_FUEL_ENV)); assert!(!internal_env.contains_key("AGENTOS_WASM_PREWARM_TIMEOUT_MS")); @@ -4047,7 +4382,13 @@ mod tests { max_fuel: Some(25), max_memory_bytes: Some(65_536), max_stack_bytes: Some(131_072), + max_module_file_bytes: Some(262_144), + max_spawn_file_actions: Some(7), + max_spawn_file_action_bytes: Some(321), prewarm_timeout_ms: Some(750), + max_open_fds: None, + max_sockets: None, + max_blocking_read_ms: None, runner_heap_limit_mb: Some(512), }); request @@ -4063,6 +4404,7 @@ mod tests { assert_eq!(env.get("AGENTOS_TRACE_ID"), Some(&String::from("kept"))); assert!(!env.contains_key(WASM_MAX_FUEL_ENV)); assert!(!env.contains_key(WASM_MAX_MEMORY_BYTES_ENV)); + assert!(!env.contains_key(WASM_MAX_MODULE_FILE_BYTES_ENV)); assert!(!env.contains_key(WASM_MAX_STACK_BYTES_ENV)); assert!(!env.contains_key("AGENTOS_WASM_PREWARM_TIMEOUT_MS")); assert!(!env.contains_key("AGENTOS_WASM_RUNNER_HEAP_LIMIT_MB")); @@ -4074,7 +4416,13 @@ mod tests { max_fuel: Some(25), max_memory_bytes: Some(65_536), max_stack_bytes: Some(131_072), + max_module_file_bytes: Some(262_144), + max_spawn_file_actions: Some(7), + max_spawn_file_action_bytes: Some(321), prewarm_timeout_ms: Some(750), + max_open_fds: None, + max_sockets: None, + max_blocking_read_ms: None, runner_heap_limit_mb: Some(512), }); request @@ -4630,7 +4978,7 @@ mod tests { } #[test] - fn wasm_sidecar_managed_methods_route_to_kernel_sync_rpc() { + fn wasm_sidecar_managed_fs_methods_route_to_kernel_sync_rpc() { let mut standalone = WasmInternalSyncRpc { module_guest_paths: Vec::new(), module_host_path: PathBuf::from("/tmp/module.wasm"), @@ -4668,18 +5016,6 @@ mod tests { ); } - for method in WASM_SIDECAR_ROUTED_KERNEL_SYNC_METHODS { - let request = wasm_sync_rpc_request(method); - assert!( - wasm_sync_rpc_method_routes_through_sidecar_kernel(&request, &sidecar_managed), - "{method} should route through the sidecar kernel for managed WASI executions" - ); - assert!( - !wasm_sync_rpc_method_routes_through_sidecar_kernel(&request, &standalone), - "{method} should stay local for standalone/prewarm WASI execution" - ); - } - standalone.route_fs_through_sidecar = true; let non_fs_request = wasm_sync_rpc_request("child_process.spawn"); assert!(!wasm_sync_rpc_method_routes_through_sidecar_kernel( @@ -4722,6 +5058,33 @@ mod tests { assert!(!bootstrap.contains("if (guestPath === \".\" || guestPath === \"/\") {")); } + #[test] + fn wasm_runner_bootstrap_exposes_unix_socket_sync_rpcs() { + let bootstrap = build_wasm_runner_bootstrap(&BTreeMap::new(), None); + + for (method, bridge) in [ + ("net.bind_unix", "_netBindUnixRaw.applySync"), + ( + "net.bind_connected_unix", + "_netBindConnectedUnixRaw.applySync", + ), + ("net.server_close", "_netServerCloseSyncRaw.applySync"), + ( + "net.socket_wait_connect", + "_netSocketWaitConnectSyncRaw.applySync", + ), + ] { + assert!( + bootstrap.contains(&format!("case \"{method}\":")), + "missing WASM sync RPC case for {method}" + ); + assert!( + bootstrap.contains(bridge), + "missing synchronous V8 bridge call for {method}" + ); + } + } + #[test] fn wasm_runner_bootstrap_reports_dot_preopen_to_wasi() { let bootstrap = build_wasm_runner_bootstrap(&BTreeMap::new(), None); diff --git a/crates/execution/tests/cjs_esm_interop.rs b/crates/execution/tests/cjs_esm_interop.rs index 70ac9b73e1..d0d6240b86 100644 --- a/crates/execution/tests/cjs_esm_interop.rs +++ b/crates/execution/tests/cjs_esm_interop.rs @@ -78,6 +78,7 @@ fn run_guest_result( let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, diff --git a/crates/execution/tests/javascript_v8.rs b/crates/execution/tests/javascript_v8.rs index 12fa95ec4f..eea4a17a38 100644 --- a/crates/execution/tests/javascript_v8.rs +++ b/crates/execution/tests/javascript_v8.rs @@ -12,6 +12,8 @@ use std::fs; use std::io::{Read, Write}; use std::os::unix::fs::symlink; use std::os::unix::fs::PermissionsExt; +use std::os::unix::process::CommandExt; +use std::os::unix::process::ExitStatusExt; use std::path::Path; use std::process::{Child, ChildStdin, Command, Stdio}; use std::sync::mpsc::{self, Receiver, Sender, TryRecvError}; @@ -86,6 +88,8 @@ fn write_fake_node_binary(path: &Path, log_path: &Path) { #[derive(Debug, Deserialize, Default)] #[serde(rename_all = "camelCase")] struct TestJavascriptChildProcessSpawnOptions { + #[serde(default)] + argv0: Option, #[serde(default)] cwd: Option, #[serde(default)] @@ -118,6 +122,8 @@ type TestJavascriptChildProcessSpawnSyncRequest = ( #[derive(Debug, Deserialize, Default)] #[serde(rename_all = "camelCase")] struct TestLegacyJavascriptChildProcessSpawnOptions { + #[serde(default)] + argv0: Option, #[serde(default)] cwd: Option, #[serde(default)] @@ -145,7 +151,7 @@ struct HostChildRecord { stdin: Option, output_events: Receiver, pending_events: VecDeque, - exit_status: Option, + exit_status: Option<(Option, Option)>, open_streams: usize, } @@ -196,6 +202,9 @@ impl HostChildProcessHarness { ); command }; + if let Some(argv0) = request.options.argv0.as_deref() { + command.arg0(argv0); + } let child_cwd = request .options @@ -275,16 +284,26 @@ impl HostChildProcessHarness { .try_wait() .map_err(|error| format!("try_wait {child_id} failed: {error}"))? { - child.exit_status = Some(status.code().unwrap_or(1)); + child.exit_status = Some(( + status.code(), + status.signal().map(|signal| match signal { + 9 => String::from("SIGKILL"), + 15 => String::from("SIGTERM"), + other => format!("SIG{other}"), + }), + )); } } - if let Some(exit_code) = child.exit_status { + if let Some((exit_code, signal)) = child.exit_status.as_ref() { if child.pending_events.is_empty() && child.open_streams == 0 { + let exit_code = *exit_code; + let signal = signal.clone(); self.children.remove(child_id); return Ok(json!({ "type": "exit", "exitCode": exit_code, + "signal": signal, })); } } @@ -314,6 +333,9 @@ impl HostChildProcessHarness { ); command }; + if let Some(argv0) = request.options.argv0.as_deref() { + command.arg0(argv0); + } let child_cwd = request .options @@ -345,6 +367,7 @@ impl HostChildProcessHarness { } child.stdin.take(); + let mut timed_out = false; if let Some(timeout_ms) = request.options.timeout { let deadline = Instant::now() + Duration::from_millis(timeout_ms); loop { @@ -353,22 +376,29 @@ impl HostChildProcessHarness { .map_err(|error| format!("try_wait for {} failed: {error}", request.command))? { Some(_) => break, - None if Instant::now() >= deadline => { - let _ = child.kill(); - let _ = child.wait(); + None if !timed_out && Instant::now() >= deadline => { let signal = request .options .kill_signal .clone() .unwrap_or_else(|| String::from("SIGTERM")); - return Ok(json!({ - "stdout": "", - "stderr": "", - "code": 1, - "signal": signal, - "timedOut": true, - "maxBufferExceeded": false, - })); + let status = Command::new("kill") + .arg(format!( + "-{}", + signal.strip_prefix("SIG").unwrap_or(&signal) + )) + .arg(child.id().to_string()) + .status() + .map_err(|error| { + format!("send {signal} to {} failed: {error}", request.command) + })?; + if !status.success() { + return Err(format!( + "send {signal} to {} failed with {status}", + request.command + )); + } + timed_out = true; } None => thread::sleep(Duration::from_millis(5)), } @@ -384,13 +414,18 @@ impl HostChildProcessHarness { let max_buffer = max_buffer.unwrap_or(1024 * 1024); let max_buffer_exceeded = output.stdout.len() > max_buffer || output.stderr.len() > max_buffer; + let signal = output.status.signal().map(|signal| match signal { + 9 => String::from("SIGKILL"), + 15 => String::from("SIGTERM"), + other => format!("SIG{other}"), + }); Ok(json!({ "stdout": stdout, "stderr": stderr, - "code": output.status.code().unwrap_or(1), - "signal": Value::Null, - "timedOut": false, + "code": output.status.code(), + "signal": signal, + "timedOut": timed_out, "maxBufferExceeded": max_buffer_exceeded, })) } @@ -438,10 +473,15 @@ impl HostChildProcessHarness { .children .get_mut(child_id) .ok_or_else(|| format!("unknown child process {child_id}"))?; - child - .child - .kill() + let signal = args.get(1).and_then(Value::as_str).unwrap_or("SIGTERM"); + let status = Command::new("kill") + .arg(format!("-{}", signal.strip_prefix("SIG").unwrap_or(signal))) + .arg(child.child.id().to_string()) + .status() .map_err(|error| format!("kill {child_id} failed: {error}"))?; + if !status.success() { + return Err(format!("kill {child_id} failed with {status}")); + } Ok(Value::Null) } @@ -601,6 +641,7 @@ fn parse_test_child_process_spawn_request( command: String::from(command), args: parsed_args, options: TestJavascriptChildProcessSpawnOptions { + argv0: parsed_options.argv0, cwd: parsed_options.cwd, env: parsed_options.env, internal_bootstrap_env: BTreeMap::new(), @@ -782,6 +823,7 @@ fn javascript_execution_uses_v8_runtime_without_spawning_guest_node_binary() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -826,6 +868,7 @@ fn javascript_execution_virtual_os_identity_comes_from_guest_runtime_not_env() { // os.* identity rides the typed guest_runtime; the env carries // contradictory values to prove the AGENTOS_VIRTUAL_OS_* knobs are // inert. + argv0: None, guest_runtime: agentos_execution::GuestRuntimeConfig { os_cpu_count: Some(7), os_totalmem: Some(8_000_000_000), @@ -890,6 +933,7 @@ fn javascript_execution_virtualizes_process_metadata_for_inline_v8_code() { limits: Default::default(), // Identity rides the typed guest_runtime; the env carries different // values to prove the `AGENTOS_VIRTUAL_PROCESS_*` knobs are inert. + argv0: Some(String::new()), guest_runtime: agentos_execution::GuestRuntimeConfig { virtual_pid: Some(4242), virtual_ppid: Some(41), @@ -914,6 +958,7 @@ fn javascript_execution_virtualizes_process_metadata_for_inline_v8_code() { r#" if (process.argv[1] !== "/root/entry.mjs") throw new Error(`argv=${process.argv[1]}`); if (process.argv[2] !== "alpha") throw new Error(`arg2=${process.argv[2]}`); +if (process.argv0 !== "") throw new Error(`argv0=${process.argv0}`); if (process.cwd() !== "/root") throw new Error(`cwd=${process.cwd()}`); if (process.pid !== 4242) throw new Error(`pid=${process.pid}`); if (process.ppid !== 41) throw new Error(`ppid=${process.ppid}`); @@ -941,6 +986,7 @@ fn javascript_execution_process_kill_rejects_invalid_pid_in_guest_js() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -995,6 +1041,7 @@ fn javascript_execution_preserves_binary_process_stdio_writes() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -1029,6 +1076,7 @@ fn javascript_execution_intl_number_format_does_not_require_host_icu() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -1073,6 +1121,7 @@ fn javascript_execution_to_locale_date_string_does_not_crash_embedded_v8() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -1126,6 +1175,7 @@ fn javascript_execution_stream_consumers_text_reads_live_stdin() { let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -1172,6 +1222,7 @@ fn javascript_execution_process_stdin_async_iterator_finishes_with_live_stdin() let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -1219,6 +1270,7 @@ fn javascript_execution_process_exit_from_live_stdin_listener_exits_without_wait let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -1286,6 +1338,7 @@ fn javascript_execution_process_exit_ignores_live_interval_handles() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -1343,6 +1396,7 @@ fn javascript_execution_process_exit_bypasses_promise_catch_handlers() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -1387,6 +1441,7 @@ fn javascript_execution_live_stdin_replays_end_after_late_listener_registration( let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -1439,6 +1494,7 @@ fn javascript_execution_file_url_to_path_accepts_guest_absolute_paths() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -1483,6 +1539,7 @@ fn javascript_execution_imports_node_events_without_hanging() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -1528,6 +1585,7 @@ fn javascript_execution_imports_node_process_without_hanging() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -1572,6 +1630,7 @@ fn javascript_execution_imports_node_fs_promises_without_hanging() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -1615,6 +1674,7 @@ fn javascript_execution_imports_node_perf_hooks_without_hanging() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -1670,6 +1730,7 @@ fn javascript_execution_high_resolution_time_opt_in_enables_sub_ms_hrtime() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: GuestRuntimeConfig { high_resolution_time: true, ..Default::default() @@ -1721,6 +1782,7 @@ fn javascript_execution_high_resolution_time_default_off_keeps_coarse_clock() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js-high-res-off"), context_id: context.context_id, @@ -1762,6 +1824,7 @@ fn javascript_execution_exposes_compatibility_shims_and_denies_escape_builtins() let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -1866,6 +1929,7 @@ console.log(JSON.stringify({ let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -1899,6 +1963,7 @@ fn javascript_execution_provides_async_hooks_and_diagnostics_channel_stubs() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -2044,6 +2109,7 @@ if (JSON.stringify(searchPaths) !== JSON.stringify(expectedPaths)) { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -2118,6 +2184,7 @@ fn javascript_execution_rejects_native_node_addons() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -2172,6 +2239,7 @@ fs.statSync("/workspace/note.txt"); let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -2250,6 +2318,7 @@ if (summary.rinfo.address !== "127.0.0.1" || summary.rinfo.port !== 7) { let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -2383,6 +2452,7 @@ fn javascript_execution_strips_hashbang_from_module_entrypoints() { let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -2461,6 +2531,7 @@ fn javascript_execution_resolves_pnpm_store_dependencies_from_symlinked_entrypoi let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -2540,6 +2611,7 @@ fn javascript_execution_resolves_dependencies_from_package_specific_symlink_moun let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -2588,6 +2660,7 @@ fn javascript_execution_v8_timer_callbacks_fire_and_clear_correctly() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id.clone(), @@ -2712,6 +2785,7 @@ fn javascript_execution_v8_timer_callbacks_fire_and_clear_correctly() { let only_immediate_execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id.clone(), @@ -2762,6 +2836,7 @@ fn javascript_execution_v8_readline_polyfill_emits_lines() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -2810,6 +2885,7 @@ fn javascript_execution_v8_builtin_wrappers_expose_common_named_exports() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -2854,12 +2930,21 @@ fs.writeFileSync("async-out.txt", Buffer.from("async:beta-async\n", "utf8")); const syncPiped = childProcess.spawnSync("/bin/cat", [], { input: Buffer.from("alpha-sync"), }); +const syncArgv0 = childProcess.spawnSync("/bin/sh", ["-c", "printf '%s' \"$0\""], { + argv0: "custom-agentos-argv0", + encoding: "utf8", +}); const syncError = childProcess.spawnSync("/bin/cat", ["definitely-missing-agentos-file"]); const syncTimeout = childProcess.spawnSync("/bin/sh", ["-c", "sleep 2"], { timeout: 50, killSignal: "SIGTERM", encoding: "utf8", }); +const syncCaughtTimeout = childProcess.spawnSync( + "/bin/sh", + ["-c", "trap 'exit 0' TERM; while :; do :; done"], + { timeout: 50, killSignal: "SIGTERM", encoding: "utf8" }, +); const stdinDestroyChild = childProcess.spawn("/bin/cat", [], { stdio: ["pipe", "pipe", "pipe"], }); @@ -2979,6 +3064,8 @@ console.log(JSON.stringify({ syncPipedStatus: syncPiped.status, syncPipedStdoutBase64: Buffer.from(syncPiped.stdout ?? []).toString("base64"), syncPipedStderrBase64: Buffer.from(syncPiped.stderr ?? []).toString("base64"), + syncArgv0Status: syncArgv0.status, + syncArgv0Stdout: syncArgv0.stdout, syncErrorStatus: syncError.status, syncErrorStdoutBase64: Buffer.from(syncError.stdout ?? []).toString("base64"), syncErrorStderrBase64: Buffer.from(syncError.stderr ?? []).toString("base64"), @@ -2988,6 +3075,9 @@ console.log(JSON.stringify({ syncTimeoutErrorMessage: syncTimeout.error?.message, syncTimeoutStdout: syncTimeout.stdout, syncTimeoutStderr: syncTimeout.stderr, + syncCaughtTimeoutStatus: syncCaughtTimeout.status, + syncCaughtTimeoutSignal: syncCaughtTimeout.signal, + syncCaughtTimeoutErrorCode: syncCaughtTimeout.error?.code, stdinDestroyStatus, stdinCallbackCode: stdinCallbackResult.code, stdinCallbackSignal: stdinCallbackResult.signal, @@ -3019,6 +3109,7 @@ console.log(JSON.stringify({ let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -3058,6 +3149,7 @@ fn javascript_execution_v8_web_stream_globals_support_basic_io() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -3117,6 +3209,7 @@ fn javascript_execution_v8_text_codec_streams_support_pipe_through() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -3207,6 +3300,7 @@ fn javascript_execution_v8_abort_controller_dispatches_abort() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -3248,6 +3342,7 @@ fn javascript_execution_v8_request_accepts_abort_signal() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -3291,6 +3386,7 @@ fn javascript_execution_v8_abort_signal_static_helpers_work() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -3362,6 +3458,7 @@ fn javascript_execution_v8_schedule_timer_bridge_resolves() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -3407,6 +3504,7 @@ fn javascript_execution_v8_kernel_poll_bridge_requests_multiple_fds() { let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -3489,6 +3587,7 @@ fn javascript_execution_v8_crypto_random_sources_use_local_secure_bridge() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -3566,6 +3665,7 @@ fn javascript_execution_v8_load_polyfill_returns_runtime_module_expressions() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -3706,6 +3806,7 @@ if (lifecycle.join(",") !== "write,finish,destroy") { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -3768,6 +3869,7 @@ if (!(file instanceof bufferModule.Blob)) { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -3828,6 +3930,7 @@ new tty.WriteStream(1); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -3876,6 +3979,7 @@ if (typeof sqlite.StatementSync !== "function") { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -3943,6 +4047,7 @@ try { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -4040,6 +4145,7 @@ if (!resolved.endsWith("/entry.cjs")) { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -4136,6 +4242,7 @@ if (!resolved.endsWith("/entry.cjs")) { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -4177,6 +4284,7 @@ console.log( let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -4243,6 +4351,7 @@ console.log(JSON.stringify(dep)); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -4299,6 +4408,7 @@ for (const [name, module] of Object.entries({ http, https })) { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -4355,6 +4465,7 @@ if (isWritable(socket)) { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -4405,6 +4516,7 @@ fn javascript_execution_v8_event_channel_backpressures_instead_of_destroying_ses let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -4554,6 +4666,7 @@ console.log("ORDER_OK:" + trace); let mut execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -4666,6 +4779,7 @@ console.log(JSON.stringify({ href, value: module.default.value })); let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -4704,6 +4818,7 @@ fn javascript_execution_v8_wasm_instantiate_streaming_never_hangs() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -4767,6 +4882,7 @@ fn javascript_execution_v8_structured_clone_rebinds_to_sandbox_realm() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -4909,6 +5025,7 @@ fn run_js_runtime_guest( let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -5077,6 +5194,7 @@ fn js_runtime_module_resolution_relative_allows_local_denies_bare() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -5151,6 +5269,7 @@ fn js_runtime_browser_loads_cjs_npm_package() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -5242,6 +5361,7 @@ fn javascript_infinite_loop_is_terminated_by_cpu_watchdog() { cpu_time_limit_ms: Some(750), ..Default::default() }, + argv0: None, guest_runtime: Default::default(), }); @@ -5329,6 +5449,7 @@ fn javascript_awaiting_guest_is_not_killed_by_cpu_budget() { cpu_time_limit_ms: Some(300), ..Default::default() }, + argv0: None, guest_runtime: Default::default(), }); @@ -5416,6 +5537,7 @@ fn javascript_default_cpu_budget_allows_short_cpu_work() { console.log('busy-done', n > 0);\n", )), limits: Default::default(), + argv0: None, guest_runtime: Default::default(), }); @@ -5512,6 +5634,7 @@ fn javascript_awaiting_guest_is_terminated_by_wall_clock_backstop() { wall_clock_limit_ms: Some(300), ..Default::default() }, + argv0: None, guest_runtime: Default::default(), }); @@ -5608,6 +5731,7 @@ fn javascript_cpu_budget_only_does_not_impose_wall_clock_limit() { cpu_time_limit_ms: Some(300), ..Default::default() }, + argv0: None, guest_runtime: Default::default(), }); @@ -5695,6 +5819,7 @@ fn javascript_no_time_limit_when_neither_env_set() { console.log('no-limit-ok');\n", )), limits: Default::default(), + argv0: None, guest_runtime: Default::default(), }); @@ -5798,6 +5923,7 @@ console.log("BOMB_COMPLETED_WITHOUT_CAP"); v8_heap_limit_mb: Some(32), ..Default::default() }, + argv0: None, guest_runtime: Default::default(), }); diff --git a/crates/execution/tests/permission_flags.rs b/crates/execution/tests/permission_flags.rs index 7890a7cc90..ff3517fd3b 100644 --- a/crates/execution/tests/permission_flags.rs +++ b/crates/execution/tests/permission_flags.rs @@ -98,6 +98,7 @@ fn node_permission_flags_allow_workers_for_internal_javascript_loader_runtime() let default_result = js_engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id.clone(), @@ -115,6 +116,7 @@ fn node_permission_flags_allow_workers_for_internal_javascript_loader_runtime() let worker_result = js_engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, @@ -177,6 +179,7 @@ fn node_permission_flags_only_propagate_nested_child_capabilities_when_parent_ex let denied_result = js_engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id.clone(), @@ -194,6 +197,7 @@ fn node_permission_flags_only_propagate_nested_child_capabilities_when_parent_ex let allowed_result = js_engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, diff --git a/crates/execution/tests/process.rs b/crates/execution/tests/process.rs index 69af9f3104..5c5693b813 100644 --- a/crates/execution/tests/process.rs +++ b/crates/execution/tests/process.rs @@ -48,6 +48,7 @@ fn embedded_runtime_process_keeps_host_pid_internal_for_javascript() { let execution = engine .start_execution(StartJavascriptExecutionRequest { limits: Default::default(), + argv0: None, guest_runtime: Default::default(), vm_id: String::from("vm-js"), context_id: context.context_id, diff --git a/crates/execution/tests/wasm.rs b/crates/execution/tests/wasm.rs index d7b28603c2..02536f5de8 100644 --- a/crates/execution/tests/wasm.rs +++ b/crates/execution/tests/wasm.rs @@ -212,7 +212,13 @@ fn wasm_limits_from_env(env: &BTreeMap) -> WasmExecutionLimits { max_fuel: parse(WASM_MAX_FUEL_ENV), max_memory_bytes: parse(WASM_MAX_MEMORY_BYTES_ENV), max_stack_bytes: parse(WASM_MAX_STACK_BYTES_ENV), + max_module_file_bytes: None, + max_spawn_file_actions: None, + max_spawn_file_action_bytes: None, prewarm_timeout_ms: None, + max_open_fds: None, + max_sockets: None, + max_blocking_read_ms: None, runner_heap_limit_mb: None, } } @@ -226,6 +232,18 @@ fn run_wasm_execution( permission_tier: WasmPermissionTier, ) -> (String, String, i32) { let limits = wasm_limits_from_env(&env); + run_wasm_execution_with_limits(engine, context_id, cwd, argv, env, permission_tier, limits) +} + +fn run_wasm_execution_with_limits( + engine: &mut WasmExecutionEngine, + context_id: String, + cwd: &Path, + argv: Vec, + env: BTreeMap, + permission_tier: WasmPermissionTier, + limits: WasmExecutionLimits, +) -> (String, String, i32) { let execution = engine .start_execution(StartWasmExecutionRequest { limits, @@ -272,6 +290,67 @@ fn wasm_stdout_module() -> Vec { .expect("compile wasm fixture") } +fn wasm_spawn_action_errno_module( + action_bytes: u32, + expected_errno: u32, + memory_pages: u32, +) -> Vec { + let initial_actions = "\\00".repeat(48); + wat::parse_str(format!( + r#" +(module + (type $spawn_t (func (param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32) (result i32))) + (type $exit_t (func (param i32))) + (import "host_process" "proc_spawn_v3" (func $spawn (type $spawn_t))) + (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (type $exit_t))) + (memory (export "memory") {memory_pages}) + (data (i32.const 128) "{initial_actions}") + (func $_start (export "_start") + (if + (i32.ne + (call $spawn + (i32.const 0) (i32.const 0) + (i32.const 0) (i32.const 0) + (i32.const 0) (i32.const 0) + (i32.const 128) (i32.const {action_bytes}) + (i32.const 0) (i32.const 0) + (i32.const 0) (i32.const 0) (i32.const 0) + (i32.const 0) (i32.const 0) (i32.const 0) + (i32.const 64)) + (i32.const {expected_errno})) + (then (call $proc_exit (i32.const 42)))))) +"#, + )) + .expect("compile spawn action errno fixture") +} + +fn wasm_getrlimit_nofile_module(expected_limit: u64) -> Vec { + wat::parse_str(format!( + r#" +(module + (type $getrlimit_t (func (param i32 i32 i32) (result i32))) + (type $exit_t (func (param i32))) + (import "host_process" "proc_getrlimit" (func $getrlimit (type $getrlimit_t))) + (import "wasi_snapshot_preview1" "proc_exit" (func $proc_exit (type $exit_t))) + (memory (export "memory") 1) + (func $_start (export "_start") + (if + (i32.ne + (call $getrlimit (i32.const 7) (i32.const 0) (i32.const 8)) + (i32.const 0)) + (then (call $proc_exit (i32.const 41)))) + (if + (i64.ne (i64.load (i32.const 0)) (i64.const {expected_limit})) + (then (call $proc_exit (i32.const 42)))) + (if + (i64.ne (i64.load (i32.const 8)) (i64.const {expected_limit})) + (then (call $proc_exit (i32.const 43))))) +) +"#, + )) + .expect("compile getrlimit nofile fixture") +} + fn wasm_stdin_echo_module() -> Vec { wat::parse_str( r#" @@ -932,6 +1011,104 @@ fn wasm_execution_runs_guest_module_through_v8() { assert!(stdout.contains("stdout:wasm-smoke")); } +fn wasm_spawn_action_decoder_enforces_typed_limits_with_e2big() { + assert_node_available(); + + let cases = [ + ( + "count", + wasm_spawn_action_errno_module(48, 1, 1), + WasmExecutionLimits { + max_spawn_file_actions: Some(1), + max_spawn_file_action_bytes: Some(128), + ..Default::default() + }, + "limits.process.maxSpawnFileActions", + ), + ( + "bytes", + wasm_spawn_action_errno_module(24, 1, 1), + WasmExecutionLimits { + max_spawn_file_actions: Some(10), + max_spawn_file_action_bytes: Some(23), + ..Default::default() + }, + "limits.process.maxSpawnFileActionBytes", + ), + ( + "raised-above-defaults", + wasm_spawn_action_errno_module(1_048_584, 28, 17), + WasmExecutionLimits { + max_spawn_file_actions: Some(44_000), + max_spawn_file_action_bytes: Some(1_100_000), + ..Default::default() + }, + "near limits.process.maxSpawnFileActionBytes", + ), + ]; + + for (name, module, limits, expected_stderr) in cases { + let temp = tempdir().expect("create temp dir"); + write_fixture(&temp.path().join("guest.wasm"), &module); + let mut engine = WasmExecutionEngine::default(); + let context = engine.create_context(CreateWasmContextRequest { + vm_id: String::from("vm-wasm"), + module_path: Some(String::from("./guest.wasm")), + }); + let (stdout, stderr, exit_code) = run_wasm_execution_with_limits( + &mut engine, + context.context_id, + temp.path(), + Vec::new(), + BTreeMap::new(), + WasmPermissionTier::Full, + limits, + ); + assert_eq!(exit_code, 0, "{name}: stdout={stdout} stderr={stderr}"); + assert!( + stderr.contains(expected_stderr), + "{name}: missing {expected_stderr:?} in stderr={stderr:?}" + ); + if name == "raised-above-defaults" { + assert!( + stderr.contains("near limits.process.maxSpawnFileActions"), + "{name}: missing count near-limit warning in stderr={stderr:?}" + ); + } + } +} + +fn wasm_getrlimit_nofile_reports_typed_fd_limit() { + assert_node_available(); + + let temp = tempdir().expect("create temp dir"); + write_fixture( + &temp.path().join("guest.wasm"), + &wasm_getrlimit_nofile_module(37), + ); + let mut engine = WasmExecutionEngine::default(); + let context = engine.create_context(CreateWasmContextRequest { + vm_id: String::from("vm-wasm"), + module_path: Some(String::from("./guest.wasm")), + }); + let (stdout, stderr, exit_code) = run_wasm_execution_with_limits( + &mut engine, + context.context_id, + temp.path(), + Vec::new(), + BTreeMap::new(), + WasmPermissionTier::Full, + WasmExecutionLimits { + max_open_fds: Some(37), + ..Default::default() + }, + ); + + assert_eq!(exit_code, 0, "stdout={stdout} stderr={stderr}"); + assert!(stdout.is_empty(), "unexpected stdout: {stdout:?}"); + assert!(stderr.is_empty(), "unexpected stderr: {stderr:?}"); +} + fn wasm_snapshot_runner_block_round_trips_twice() { assert_node_available(); let _mode = EnvVarGuard::set_value("AGENTOS_WASM_SNAPSHOT_RUNNER", "block"); @@ -2755,6 +2932,8 @@ fn wasm_suite() { wasm_contexts_preserve_vm_and_module_configuration(); wasm_execution_stays_inside_v8_runtime_without_host_node_launches(); wasm_execution_runs_guest_module_through_v8(); + wasm_spawn_action_decoder_enforces_typed_limits_with_e2big(); + wasm_getrlimit_nofile_reports_typed_fd_limit(); wasm_snapshot_runner_block_round_trips_twice(); wasm_snapshot_runner_warm_worker_pool_hits(); wasm_snapshot_runner_warm_worker_pool_disabled_falls_back(); diff --git a/crates/execution/tests/wasm_host_fs_errno_contract.rs b/crates/execution/tests/wasm_host_fs_errno_contract.rs new file mode 100644 index 0000000000..4785f6f617 --- /dev/null +++ b/crates/execution/tests/wasm_host_fs_errno_contract.rs @@ -0,0 +1,54 @@ +//! Contract checks for Linux filesystem errno propagation in the WASM runner. + +use std::{fs, path::PathBuf}; + +fn runner_source() -> String { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/runners/wasm-runner.mjs"); + fs::read_to_string(path).expect("read wasm runner") +} + +#[test] +fn existing_path_errors_reach_libc_as_eexist() { + let source = runner_source(); + let start = source + .find("function mapHostProcessError(") + .expect("host error mapper"); + let end = source[start..] + .find("\n}\n\nfunction seekGuestFileHandle") + .map(|offset| start + offset) + .expect("end of host error mapper"); + let error_map = &source[start..end]; + + assert!( + source.contains("const WASI_ERRNO_EXIST = 20;") + && error_map.contains("case 'EEXIST':\n return WASI_ERRNO_EXIST;"), + "host EEXIST must retain the preview1/WASI errno value instead of becoming EFAULT" + ); +} + +#[test] +fn path_open_preserves_directory_and_nofollow_before_kernel_mutation() { + let source = runner_source(); + + assert!( + source.contains("const KERNEL_O_DIRECTORY = 0o200000;") + && source.contains("const KERNEL_O_NOFOLLOW = 0o400000;"), + "the runner must expose the kernel's Linux-compatible open flag bits" + ); + assert!( + source.contains( + "if ((normalizedOflags & WASI_OFLAGS_DIRECTORY) !== 0) flags |= KERNEL_O_DIRECTORY;" + ) && source.contains("flags |= KERNEL_O_NOFOLLOW;"), + "path_open must pass O_DIRECTORY and a missing SYMLINK_FOLLOW lookup flag to the kernel" + ); + assert!( + source.contains("kernelOpenFlagsFromWasi(oflags, rightsBase, fdflags, dirflags)"), + "path_open must include its WASI lookup flags in the kernel conversion" + ); + assert!( + source.contains( + "function openProcSelfFdAlias(guestPath, oflags, rightsBase, lookupflags, openedFdPtr)" + ) && source.contains("return WASI_ERRNO_LOOP;"), + "runner-local /proc/self/fd and /dev/fd aliases must not bypass O_NOFOLLOW" + ); +} diff --git a/crates/execution/tests/wasm_host_net_errno_contract.rs b/crates/execution/tests/wasm_host_net_errno_contract.rs new file mode 100644 index 0000000000..771357d39f --- /dev/null +++ b/crates/execution/tests/wasm_host_net_errno_contract.rs @@ -0,0 +1,155 @@ +//! Pins the error boundary for WASI `fd_read`/`fd_write` on sidecar-backed +//! network sockets. Guest-memory faults are `EFAULT`; typed sidecar/RPC socket +//! errors must retain their Linux errno through `mapHostProcessError`. + +use std::fs; +use std::path::PathBuf; + +fn runner_source() -> String { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/runners/wasm-runner.mjs"); + fs::read_to_string(&path) + .unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display())) +} + +fn between<'a>(source: &'a str, start: &str, end: &str) -> &'a str { + let start_offset = source + .find(start) + .unwrap_or_else(|| panic!("missing runner source marker: {start}")); + let tail = &source[start_offset..]; + let end_offset = tail + .find(end) + .unwrap_or_else(|| panic!("missing runner source marker after {start}: {end}")); + &tail[..end_offset] +} + +#[test] +fn host_net_typed_errors_keep_linux_errno_mappings() { + let source = runner_source(); + let error_map = between( + &source, + "function mapHostProcessError(", + "function seekGuestFileHandle(", + ); + assert!( + error_map.contains("case 'EPIPE':\n return WASI_ERRNO_PIPE;") + && error_map.contains("case 'ECONNRESET':\n return WASI_ERRNO_CONNRESET;"), + "host-net RPC failures must preserve representative Linux socket errnos" + ); +} + +#[test] +fn host_net_fd_read_keeps_guest_faults_separate_from_socket_errors() { + let source = runner_source(); + let guest_marshal = between( + &source, + "function writeHostNetBytesToGuestIovs(", + "function readHostNetSocketToGuestIovs(", + ); + assert!( + guest_marshal.contains("writeBytesToGuestIovs(iovs, iovsLen, bytes)") + && guest_marshal.contains("catch {") + && guest_marshal.contains("return WASI_ERRNO_FAULT;"), + "host-net read guest-memory marshalling must return EFAULT" + ); + + let socket_read = between( + &source, + "function readHostNetSocketToGuestIovs(", + "function writeHostNetSocketFromGuestIovs(", + ); + assert!( + socket_read.contains("readReadyHostNetSocket(socket") + && socket_read.contains("catch (error) {") + && socket_read.contains("return mapHostProcessError(error);"), + "host-net fd_read must map typed net.socket_read RPC errors" + ); + assert!( + !socket_read.contains("writeBytesToGuestIovs("), + "host-net fd_read must isolate all fallible guest-memory writes in the EFAULT helper" + ); +} + +#[test] +fn host_net_fd_write_keeps_guest_faults_separate_from_socket_errors() { + let source = runner_source(); + let socket_write = between( + &source, + "function writeHostNetSocketFromGuestIovs(", + "function dequeuePipeBytes(", + ); + let guest_fault = socket_write + .find("bytes = collectGuestIovBytes(iovs, iovsLen);") + .expect("host-net fd_write must collect guest iovecs"); + let rpc = socket_write + .find("callSyncRpc('net.write'") + .expect("host-net fd_write must call the sidecar net.write RPC"); + let mapped_error = socket_write + .rfind("return mapHostProcessError(error);") + .expect("host-net fd_write must map typed net.write RPC errors"); + + assert!( + socket_write[guest_fault..rpc].contains("return WASI_ERRNO_FAULT;"), + "guest iovec reads must return EFAULT before the RPC boundary" + ); + assert!( + mapped_error > rpc, + "typed net.write RPC errors must be mapped after the RPC boundary" + ); + assert!( + !socket_write[rpc..].contains("return WASI_ERRNO_FAULT;"), + "the net.write RPC catch must not collapse typed errors to EFAULT" + ); +} + +#[test] +fn host_net_socket_families_match_the_owned_wasi_libc_abi() { + let source = runner_source(); + assert!( + source.contains("const HOST_NET_AF_INET = 1;") + && source.contains("const HOST_NET_AF_INET6 = 2;") + && source.contains("const HOST_NET_AF_UNIX = 3;"), + "host_net domain values must match the AgentOS wasi-libc p1 ABI" + ); + + let socket_import = between(&source, " net_socket(", " net_set_nonblock("); + assert!( + socket_import.contains("numericDomain === HOST_NET_AF_UNIX") + && socket_import.contains("localUnixAddress: numericDomain === HOST_NET_AF_UNIX"), + "net_socket must classify AF_UNIX by the owned libc ABI constant" + ); + assert!( + !socket_import.contains("numericDomain === 1"), + "AF_INET=1 must never be mistaken for AF_UNIX" + ); + + let connect_import = between(&source, " net_connect(", " net_bind("); + assert!( + connect_import.contains("Number(socket.domain) !== HOST_NET_AF_UNIX") + && connect_import.contains("Number(socket.domain) === HOST_NET_AF_UNIX"), + "net_connect must use the shared AF_UNIX ABI constant" + ); +} + +#[test] +fn host_net_empty_read_invalidates_cached_poll_readiness() { + let source = runner_source(); + let read_ready = between( + &source, + "function readReadyHostNetSocket(", + "function pollHostNetSocket(", + ); + let data_branch = read_ready + .find("if (result.kind === 'data')") + .expect("host-net read helper must classify data results"); + let invalidate = read_ready[data_branch..] + .find("socket.readableHint = false;") + .expect("empty host-net reads must invalidate cached poll readiness"); + let end_branch = read_ready[data_branch..] + .find("if (result.kind === 'end')") + .expect("host-net read helper must classify EOF"); + + assert!( + invalidate < end_branch, + "EAGAIN/timeout and EOF must clear a stale POLLIN hint before the next poll" + ); +} diff --git a/crates/kernel/Cargo.toml b/crates/kernel/Cargo.toml index 84dda807c3..103ee62852 100644 --- a/crates/kernel/Cargo.toml +++ b/crates/kernel/Cargo.toml @@ -10,7 +10,7 @@ description = "Shared kernel plane for secure-exec native and browser sidecars" agentos-bridge = { workspace = true } vfs = { workspace = true } base64 = "0.22" -hickory-proto = { version = "0.26.0-beta.3", default-features = false } +hickory-proto = { version = "=0.26.0-beta.3", default-features = false } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" # Drop-in std::time replacement that works on wasm32 (std SystemTime/Instant @@ -19,7 +19,7 @@ web-time = "1.1" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] getrandom = "0.2" -hickory-resolver = "0.26.0-beta.3" +hickory-resolver = "=0.26.0-beta.3" tokio = { version = "1", features = ["rt", "rt-multi-thread"] } [target.'cfg(target_arch = "wasm32")'.dependencies] diff --git a/crates/kernel/src/device_layer.rs b/crates/kernel/src/device_layer.rs index b0729f4ba8..3a92326bd8 100644 --- a/crates/kernel/src/device_layer.rs +++ b/crates/kernel/src/device_layer.rs @@ -233,6 +233,19 @@ impl VirtualFileSystem for DeviceLayer { self.inner.chown(path, uid, gid) } + fn chown_spec( + &mut self, + path: &str, + uid: u32, + gid: u32, + follow_symlinks: bool, + ) -> VfsResult<()> { + if is_device_path(path) { + return Ok(()); + } + self.inner.chown_spec(path, uid, gid, follow_symlinks) + } + fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> { if is_device_path(path) { return Ok(()); diff --git a/crates/kernel/src/dns.rs b/crates/kernel/src/dns.rs index d0111835b6..98ad364117 100644 --- a/crates/kernel/src/dns.rs +++ b/crates/kernel/src/dns.rs @@ -171,6 +171,8 @@ impl DnsRecordResolution { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum DnsResolverErrorKind { InvalidInput, + NxDomain, + NoData, LookupFailed, } @@ -195,6 +197,20 @@ impl DnsResolverError { } } + pub fn nx_domain(message: impl Into) -> Self { + Self { + kind: DnsResolverErrorKind::NxDomain, + message: message.into(), + } + } + + pub fn no_data(message: impl Into) -> Self { + Self { + kind: DnsResolverErrorKind::NoData, + message: message.into(), + } + } + pub const fn kind(&self) -> DnsResolverErrorKind { self.kind } @@ -462,13 +478,19 @@ fn worker_lookup_records( .lookup(&hostname, record_type) .await .map_err(|error| { - DnsResolverError::lookup_failed(format!( - "failed to resolve DNS {record_type} record {hostname}: {error}" - )) + let message = + format!("failed to resolve DNS {record_type} record {hostname}: {error}"); + if error.is_nx_domain() { + DnsResolverError::nx_domain(message) + } else if error.is_no_records_found() { + DnsResolverError::no_data(message) + } else { + DnsResolverError::lookup_failed(message) + } })?; let records = lookup.answers().to_vec(); if records.is_empty() { - return Err(DnsResolverError::lookup_failed(format!( + return Err(DnsResolverError::no_data(format!( "failed to resolve DNS {record_type} record {hostname}" ))); } @@ -586,7 +608,7 @@ pub fn resolve_dns_records( ); let records = resolver.lookup_records(&request)?; if records.is_empty() { - return Err(DnsResolverError::lookup_failed(format!( + return Err(DnsResolverError::no_data(format!( "failed to resolve DNS {record_type} record {normalized_hostname}" ))); } diff --git a/crates/kernel/src/fd_table.rs b/crates/kernel/src/fd_table.rs index 3e57be8ec9..4a4fd028f0 100644 --- a/crates/kernel/src/fd_table.rs +++ b/crates/kernel/src/fd_table.rs @@ -4,6 +4,8 @@ use std::fmt; use std::sync::atomic::{AtomicU32, AtomicU64, AtomicUsize, Ordering}; use std::sync::{Arc, Condvar, Mutex, MutexGuard}; +use crate::vfs::VirtualStat; + pub const MAX_FDS_PER_PROCESS: usize = 256; pub const O_RDONLY: u32 = 0; @@ -14,6 +16,8 @@ pub const O_EXCL: u32 = 0o200; pub const O_TRUNC: u32 = 0o1000; pub const O_APPEND: u32 = 0o2000; pub const O_NONBLOCK: u32 = 0o4000; +pub const O_DIRECTORY: u32 = 0o200000; +pub const O_NOFOLLOW: u32 = 0o400000; pub const F_DUPFD: u32 = 0; pub const F_GETFD: u32 = 1; pub const F_SETFD: u32 = 2; @@ -24,17 +28,107 @@ pub const LOCK_SH: u32 = 1; pub const LOCK_EX: u32 = 2; pub const LOCK_NB: u32 = 4; pub const LOCK_UN: u32 = 8; +const DEFAULT_MAX_RECORD_LOCKS: usize = 4096; pub const FILETYPE_UNKNOWN: u8 = 0; pub const FILETYPE_CHARACTER_DEVICE: u8 = 2; pub const FILETYPE_DIRECTORY: u8 = 3; pub const FILETYPE_REGULAR_FILE: u8 = 4; +pub const FILETYPE_SOCKET_DGRAM: u8 = 5; +pub const FILETYPE_SOCKET_STREAM: u8 = 6; pub const FILETYPE_PIPE: u8 = 6; pub const FILETYPE_SYMBOLIC_LINK: u8 = 7; pub type FdResult = Result; pub type SharedFileDescription = Arc; +// Every kernel subsystem keys pipes, PTYs, sockets, locks, and poll state by +// open-file-description identity. Allocate that identity from one global +// monotonic domain: per-subsystem counters can eventually overlap and make an +// unrelated regular file look like a stale socket or pipe. +static NEXT_FILE_DESCRIPTION_ID: AtomicU64 = AtomicU64::new(1); + +pub(crate) fn allocate_file_description_id() -> u64 { + NEXT_FILE_DESCRIPTION_ID + .fetch_update(Ordering::SeqCst, Ordering::SeqCst, |id| id.checked_add(1)) + .expect("open-file-description id space exhausted") +} + +#[derive(Debug, Default)] +pub struct AnonymousFileUsage { + bytes: AtomicU64, + inodes: AtomicUsize, +} + +impl AnonymousFileUsage { + pub fn bytes(&self) -> u64 { + self.bytes.load(Ordering::SeqCst) + } + + pub fn inodes(&self) -> usize { + self.inodes.load(Ordering::SeqCst) + } + + fn add_file(&self, size: u64) { + self.bytes.fetch_add(size, Ordering::SeqCst); + self.inodes.fetch_add(1, Ordering::SeqCst); + } + + fn remove_file(&self, size: u64) { + self.bytes.fetch_sub(size, Ordering::SeqCst); + self.inodes.fetch_sub(1, Ordering::SeqCst); + } + + fn resize_file(&self, old_size: u64, new_size: u64) { + if new_size >= old_size { + self.bytes + .fetch_add(new_size.saturating_sub(old_size), Ordering::SeqCst); + } else { + self.bytes + .fetch_sub(old_size.saturating_sub(new_size), Ordering::SeqCst); + } + } +} + +#[derive(Debug)] +pub struct AnonymousFile { + pub data: Vec, + pub stat: VirtualStat, + usage: Arc, +} + +impl AnonymousFile { + pub fn new(data: Vec, stat: VirtualStat, usage: Arc) -> Self { + usage.add_file(stat.size); + Self { data, stat, usage } + } +} + +impl Drop for AnonymousFile { + fn drop(&mut self) { + self.usage.remove_file(self.stat.size); + } +} + +pub type SharedAnonymousFile = Arc>; + +#[derive(Debug)] +enum FileBacking { + Path(String), + LinkedAlias { + former_path: String, + live_path: String, + }, + Anonymous { + former_path: String, + file: SharedAnonymousFile, + }, + DetachedDirectory { + former_path: String, + stat: VirtualStat, + }, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct FdTableError { code: &'static str, @@ -60,6 +154,13 @@ impl FdTableError { } } + fn no_memory(message: impl Into) -> Self { + Self { + code: "ENOMEM", + message: message.into(), + } + } + fn invalid_argument(message: impl Into) -> Self { Self { code: "EINVAL", @@ -73,6 +174,13 @@ impl FdTableError { message: message.into(), } } + + fn deadlock(message: impl Into) -> Self { + Self { + code: "EDEADLK", + message: message.into(), + } + } } impl fmt::Display for FdTableError { @@ -86,7 +194,7 @@ impl Error for FdTableError {} #[derive(Debug)] pub struct FileDescription { id: u64, - path: String, + backing: Mutex, lock_target: Option, cursor: AtomicU64, flags: AtomicU32, @@ -120,7 +228,7 @@ impl FileDescription { ) -> Self { Self { id, - path: path.into(), + backing: Mutex::new(FileBacking::Path(path.into())), lock_target, cursor: AtomicU64::new(0), flags: AtomicU32::new(flags), @@ -132,8 +240,230 @@ impl FileDescription { self.id } - pub fn path(&self) -> &str { - &self.path + pub fn path(&self) -> String { + match &*lock_or_recover(&self.backing) { + FileBacking::Path(path) => path.clone(), + FileBacking::LinkedAlias { live_path, .. } => live_path.clone(), + FileBacking::Anonymous { former_path, .. } => former_path.clone(), + FileBacking::DetachedDirectory { former_path, .. } => former_path.clone(), + } + } + + /// Linux renders an unlinked open file description through procfs with a + /// ` (deleted)` suffix while the description itself remains usable. + pub fn proc_display_path(&self) -> String { + match &*lock_or_recover(&self.backing) { + FileBacking::Path(path) => path.clone(), + FileBacking::LinkedAlias { former_path, .. } + | FileBacking::Anonymous { former_path, .. } + | FileBacking::DetachedDirectory { former_path, .. } => { + format!("{former_path} (deleted)") + } + } + } + + pub fn is_path_backed_by(&self, expected: &str) -> bool { + match &*lock_or_recover(&self.backing) { + FileBacking::Path(path) => path == expected, + FileBacking::LinkedAlias { live_path, .. } => live_path == expected, + FileBacking::Anonymous { .. } | FileBacking::DetachedDirectory { .. } => false, + } + } + + pub fn rename_path_prefix(&self, old_path: &str, new_path: &str) { + let mut backing = lock_or_recover(&self.backing); + let path = match &mut *backing { + FileBacking::Path(path) => path, + FileBacking::LinkedAlias { live_path, .. } => live_path, + FileBacking::Anonymous { .. } | FileBacking::DetachedDirectory { .. } => return, + }; + if path == old_path { + *path = new_path.to_string(); + } else if let Some(suffix) = path + .strip_prefix(old_path) + .filter(|value| value.starts_with('/')) + { + *path = format!("{new_path}{suffix}"); + } + } + + pub fn detach_path(&self, expected: &str, file: SharedAnonymousFile) -> bool { + let mut backing = lock_or_recover(&self.backing); + let former_path = match &*backing { + FileBacking::Path(path) if path == expected => expected.to_string(), + FileBacking::LinkedAlias { + former_path, + live_path, + } if live_path == expected => former_path.clone(), + _ => return false, + }; + *backing = FileBacking::Anonymous { former_path, file }; + true + } + + /// Keep an unlinked open description attached to the same inode through a + /// surviving hard-link name. The former name remains visible through + /// procfs, while descriptor operations use the live alias. + pub fn rebind_deleted_path(&self, expected: &str, live_path: &str) -> bool { + let mut backing = lock_or_recover(&self.backing); + let former_path = match &*backing { + FileBacking::Path(path) if path == expected => expected.to_string(), + FileBacking::LinkedAlias { + former_path, + live_path: current, + } if current == expected => former_path.clone(), + _ => return false, + }; + *backing = FileBacking::LinkedAlias { + former_path, + live_path: live_path.to_string(), + }; + true + } + + pub fn detach_directory(&self, _expected: &str, stat: VirtualStat) -> bool { + let mut backing = lock_or_recover(&self.backing); + let FileBacking::Path(former_path) = &*backing else { + return false; + }; + let former_path = former_path.clone(); + *backing = FileBacking::DetachedDirectory { former_path, stat }; + true + } + + pub fn detached_directory_stat(&self) -> Option { + match &*lock_or_recover(&self.backing) { + FileBacking::DetachedDirectory { stat, .. } => Some(stat.clone()), + FileBacking::Path(_) + | FileBacking::LinkedAlias { .. } + | FileBacking::Anonymous { .. } => None, + } + } + + pub fn anonymous_stat(&self) -> Option { + let file = match &*lock_or_recover(&self.backing) { + FileBacking::Anonymous { file, .. } => Arc::clone(file), + FileBacking::Path(_) + | FileBacking::LinkedAlias { .. } + | FileBacking::DetachedDirectory { .. } => return None, + }; + let stat = lock_or_recover(&file).stat.clone(); + Some(stat) + } + + pub fn anonymous_pread(&self, offset: u64, length: usize) -> Option> { + let file = match &*lock_or_recover(&self.backing) { + FileBacking::Anonymous { file, .. } => Arc::clone(file), + FileBacking::Path(_) + | FileBacking::LinkedAlias { .. } + | FileBacking::DetachedDirectory { .. } => return None, + }; + let file = lock_or_recover(&file); + let start = usize::try_from(offset).unwrap_or(usize::MAX); + if start >= file.data.len() { + return Some(Vec::new()); + } + let end = start.saturating_add(length).min(file.data.len()); + Some(file.data[start..end].to_vec()) + } + + pub fn anonymous_pwrite(&self, offset: u64, data: &[u8]) -> Option> { + let file = match &*lock_or_recover(&self.backing) { + FileBacking::Anonymous { file, .. } => Arc::clone(file), + FileBacking::Path(_) + | FileBacking::LinkedAlias { .. } + | FileBacking::DetachedDirectory { .. } => return None, + }; + let mut file = lock_or_recover(&file); + let Ok(start) = usize::try_from(offset) else { + return Some(Err(FdTableError::invalid_argument( + "anonymous file offset exceeds host address space", + ))); + }; + let Some(end) = start.checked_add(data.len()) else { + return Some(Err(FdTableError::invalid_argument( + "anonymous file write range overflows", + ))); + }; + if end > file.data.len() { + let additional = end - file.data.len(); + if file.data.try_reserve(additional).is_err() { + return Some(Err(FdTableError::no_memory( + "anonymous file write allocation failed", + ))); + } + file.data.resize(end, 0); + } + file.data[start..end].copy_from_slice(data); + let old_size = file.stat.size; + file.stat.size = file.data.len() as u64; + file.stat.blocks = file.stat.size.div_ceil(512); + file.usage.resize_file(old_size, file.stat.size); + Some(Ok(file.stat.size)) + } + + pub fn anonymous_truncate(&self, length: u64) -> Option> { + let file = match &*lock_or_recover(&self.backing) { + FileBacking::Anonymous { file, .. } => Arc::clone(file), + FileBacking::Path(_) + | FileBacking::LinkedAlias { .. } + | FileBacking::DetachedDirectory { .. } => return None, + }; + let Ok(length) = usize::try_from(length) else { + return Some(Err(FdTableError::invalid_argument( + "anonymous file length exceeds host address space", + ))); + }; + let mut file = lock_or_recover(&file); + let additional = length.saturating_sub(file.data.len()); + if additional > 0 && file.data.try_reserve(additional).is_err() { + return Some(Err(FdTableError::no_memory( + "anonymous file truncate allocation failed", + ))); + } + file.data.resize(length, 0); + let old_size = file.stat.size; + file.stat.size = length as u64; + file.stat.blocks = file.stat.size.div_ceil(512); + file.usage.resize_file(old_size, file.stat.size); + Some(Ok(())) + } + + pub fn detached_chmod(&self, mode: u32) -> bool { + let mut backing = lock_or_recover(&self.backing); + match &mut *backing { + FileBacking::Anonymous { file, .. } => { + let mut file = lock_or_recover(file); + file.stat.mode = (file.stat.mode & !0o7777) | (mode & 0o7777); + true + } + FileBacking::DetachedDirectory { stat, .. } => { + stat.mode = (stat.mode & !0o7777) | (mode & 0o7777); + true + } + FileBacking::Path(_) | FileBacking::LinkedAlias { .. } => false, + } + } + + pub fn detached_chown(&self, uid: u32, gid: u32, changed_mode: Option) -> bool { + let mut backing = lock_or_recover(&self.backing); + match &mut *backing { + FileBacking::Anonymous { file, .. } => { + let mut file = lock_or_recover(file); + file.stat.uid = uid; + file.stat.gid = gid; + if let Some(mode) = changed_mode { + file.stat.mode = mode; + } + true + } + FileBacking::DetachedDirectory { stat, .. } => { + stat.uid = uid; + stat.gid = gid; + true + } + FileBacking::Path(_) | FileBacking::LinkedAlias { .. } => false, + } } pub fn lock_target(&self) -> Option { @@ -199,6 +529,65 @@ pub struct FdEntry { pub filetype: u8, } +#[derive(Debug)] +pub struct TransferredFd { + description: SharedFileDescription, + status_flags: u32, + rights: u64, + filetype: u8, +} + +impl Clone for TransferredFd { + fn clone(&self) -> Self { + self.description.increment_ref_count(); + Self { + description: Arc::clone(&self.description), + status_flags: self.status_flags, + rights: self.rights, + filetype: self.filetype, + } + } +} + +impl PartialEq for TransferredFd { + fn eq(&self, other: &Self) -> bool { + self.description.id() == other.description.id() + && self.status_flags == other.status_flags + && self.rights == other.rights + && self.filetype == other.filetype + } +} + +impl Eq for TransferredFd {} + +impl Drop for TransferredFd { + fn drop(&mut self) { + self.description.decrement_ref_count(); + } +} + +impl TransferredFd { + pub(crate) fn description(&self) -> SharedFileDescription { + Arc::clone(&self.description) + } + + pub fn description_id(&self) -> u64 { + self.description.id() + } + + pub fn status_flags(&self) -> u32 { + self.status_flags + } + + pub fn rights(&self) -> u64 { + self.rights + } + + pub fn filetype(&self) -> u8 { + self.filetype + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct FdStat { pub filetype: u8, @@ -213,15 +602,11 @@ pub struct StdioOverride { } #[derive(Debug, Clone)] -struct DescriptionFactory { - next_description_id: Arc, -} +struct DescriptionFactory {} impl DescriptionFactory { - fn new(starting_id: u64) -> Self { - Self { - next_description_id: Arc::new(AtomicU64::new(starting_id)), - } + fn new(_starting_id: u64) -> Self { + Self {} } fn allocate(&self, path: &str, flags: u32) -> SharedFileDescription { @@ -234,7 +619,7 @@ impl DescriptionFactory { flags: u32, lock_target: Option, ) -> SharedFileDescription { - let next_id = self.next_description_id.fetch_add(1, Ordering::SeqCst); + let next_id = allocate_file_description_id(); Arc::new(FileDescription::new_with_lock( next_id, path, @@ -246,12 +631,17 @@ impl DescriptionFactory { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct FileLockTarget { + dev: u64, ino: u64, } impl FileLockTarget { - pub const fn new(ino: u64) -> Self { - Self { ino } + pub const fn new(dev: u64, ino: u64) -> Self { + Self { dev, ino } + } + + pub const fn dev(self) -> u64 { + self.dev } pub const fn ino(self) -> u64 { @@ -265,6 +655,45 @@ enum FileLockMode { Exclusive, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RecordLockType { + Read, + Write, + Unlock, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RecordLock { + pub lock_type: RecordLockType, + pub start: u64, + /// Exclusive end offset. `None` means through EOF, including future growth. + pub end: Option, + pub pid: u32, +} + +impl RecordLock { + pub fn new(lock_type: RecordLockType, start: u64, length: u64, pid: u32) -> FdResult { + let end = + if length == 0 { + None + } else { + Some(start.checked_add(length).ok_or_else(|| { + FdTableError::invalid_argument("record lock range exceeds u64") + })?) + }; + Ok(Self { + lock_type, + start, + end, + pid, + }) + } + + pub fn length(self) -> u64 { + self.end.map_or(0, |end| end - self.start) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FlockOperation { Shared { nonblocking: bool }, @@ -308,6 +737,10 @@ impl ProcessFdTable { self.max_fds } + pub fn available_fd_capacity(&self) -> usize { + self.max_fds.saturating_sub(self.entries.len()) + } + pub fn init_stdio( &mut self, stdin_desc: SharedFileDescription, @@ -460,10 +893,147 @@ impl ProcessFdTable { Ok(fd) } + pub fn open_pair_with_details( + &mut self, + first_path: &str, + second_path: &str, + status_flags: u32, + fd_flags: u32, + filetype: u8, + ) -> FdResult<(u32, u32, SharedFileDescription, SharedFileDescription)> { + if self.entries.len().saturating_add(2) > self.max_fds { + return Err(FdTableError::too_many_open_files()); + } + let first_fd = self.allocate_fd()?; + let second_fd = match self.allocate_fd() { + Ok(fd) => fd, + Err(error) => { + self.next_fd = first_fd; + return Err(error); + } + }; + let first = self.alloc_desc.allocate(first_path, O_RDWR); + let second = self.alloc_desc.allocate(second_path, O_RDWR); + for (fd, description) in [ + (first_fd, Arc::clone(&first)), + (second_fd, Arc::clone(&second)), + ] { + self.entries.insert( + fd, + FdEntry { + fd, + description, + status_flags: status_flags & ENTRY_STATUS_FLAG_MASK, + fd_flags: fd_flags & FD_CLOEXEC, + rights: 0, + filetype, + }, + ); + } + Ok((first_fd, second_fd, first, second)) + } + + pub fn transfer(&self, fd: u32) -> FdResult { + let entry = self + .entries + .get(&fd) + .ok_or_else(|| FdTableError::bad_file_descriptor(fd))?; + entry.description.increment_ref_count(); + Ok(TransferredFd { + description: Arc::clone(&entry.description), + status_flags: entry.status_flags, + rights: entry.rights, + filetype: entry.filetype, + }) + } + + /// Create an open-file-description transfer without consuming an fd slot. + /// + /// SCM_RIGHTS queues descriptions, not temporary descriptors in the + /// sending process. Keeping this separate from `open_with_details` avoids + /// spuriously returning EMFILE when the sender's descriptor table is full. + pub fn create_transfer(&self, path: &str, flags: u32, filetype: u8) -> TransferredFd { + TransferredFd { + description: self.alloc_desc.allocate(path, description_flags(flags)), + status_flags: status_flags(flags), + rights: 0, + filetype, + } + } + + pub fn install_transferred( + &mut self, + transfers: &[TransferredFd], + close_on_exec: bool, + ) -> FdResult> { + if self.entries.len().saturating_add(transfers.len()) > self.max_fds { + return Err(FdTableError::too_many_open_files()); + } + let mut candidates = Vec::with_capacity(transfers.len()); + let mut cursor = self.next_fd; + for _ in transfers { + let start = usize::try_from(cursor).unwrap_or(0) % self.max_fds; + let candidate = (0..self.max_fds) + .map(|offset| ((start + offset) % self.max_fds) as u32) + .find(|fd| !self.entries.contains_key(fd) && !candidates.contains(fd)) + .ok_or_else(FdTableError::too_many_open_files)?; + candidates.push(candidate); + cursor = candidate.saturating_add(1); + } + for (fd, transfer) in candidates.iter().copied().zip(transfers) { + transfer.description.increment_ref_count(); + self.entries.insert( + fd, + FdEntry { + fd, + description: Arc::clone(&transfer.description), + status_flags: transfer.status_flags, + fd_flags: if close_on_exec { FD_CLOEXEC } else { 0 }, + rights: transfer.rights, + filetype: transfer.filetype, + }, + ); + } + self.next_fd = cursor; + Ok(candidates) + } + + /// Install a transferred open file description at an exact descriptor. + /// This is used by spawn staging, where the post-file-action descriptor + /// numbers are already canonical and must not be allocated a second time. + pub(crate) fn install_transferred_at( + &mut self, + transfer: &TransferredFd, + fd: u32, + fd_flags: u32, + ) -> FdResult<()> { + self.validate_fd_bounds(fd)?; + if self.entries.contains_key(&fd) { + self.close(fd); + } + transfer.description.increment_ref_count(); + self.entries.insert( + fd, + FdEntry { + fd, + description: Arc::clone(&transfer.description), + status_flags: transfer.status_flags, + fd_flags: fd_flags & FD_CLOEXEC, + rights: transfer.rights, + filetype: transfer.filetype, + }, + ); + Ok(()) + } + pub fn get(&self, fd: u32) -> Option<&FdEntry> { self.entries.get(&fd) } + pub fn values(&self) -> Values<'_, u32, FdEntry> { + self.entries.values() + } + pub fn close(&mut self, fd: u32) -> bool { let Some(entry) = self.entries.remove(&fd) else { return false; @@ -472,6 +1042,17 @@ impl ProcessFdTable { true } + /// File descriptors that must be closed when the current process image is + /// replaced by exec(2). The caller performs the closes through the kernel + /// so pipe/socket lifecycle accounting receives the same notifications as + /// an explicit close(2). + pub fn close_on_exec_fds(&self) -> Vec { + self.entries + .iter() + .filter_map(|(fd, entry)| (entry.fd_flags & FD_CLOEXEC != 0).then_some(*fd)) + .collect() + } + pub fn dup(&mut self, fd: u32) -> FdResult { self.dup_with_status_flags(fd, None) } @@ -579,10 +1160,32 @@ impl ProcessFdTable { } pub fn fork(&self) -> Self { + self.fork_with_cloexec(false) + } + + /// Clone this descriptor table at the fork half of a deferred exec. + /// + /// Linux keeps `FD_CLOEXEC` descriptors across `fork(2)` so spawn file + /// actions can use them as sources. The exec half closes any descriptors + /// that remain marked after those actions have run. + pub fn fork_preserving_cloexec(&self) -> Self { + self.fork_with_cloexec(true) + } + + fn fork_with_cloexec(&self, preserve_cloexec: bool) -> Self { let mut child = Self::new(self.alloc_desc.clone(), self.max_fds); child.next_fd = self.next_fd; for (fd, entry) in &self.entries { + // Kernel process creation is spawn (fork + exec combined), so + // close-on-exec descriptors must not leak into the child. This + // matters for pipe write ends: an inherited writer keeps the + // pipe's writer refcount above zero forever, so a blocked reader + // (for example a grandchild sharing the parent's stdin pipe) + // would never observe EOF. + if !preserve_cloexec && entry.fd_flags & FD_CLOEXEC != 0 { + continue; + } entry.description.increment_ref_count(); child.entries.insert( *fd, @@ -810,15 +1413,36 @@ impl FdTableManager { } pub fn fork(&mut self, parent_pid: u32, child_pid: u32) -> &mut ProcessFdTable { + self.fork_with_cloexec(parent_pid, child_pid, false) + } + + pub fn fork_preserving_cloexec( + &mut self, + parent_pid: u32, + child_pid: u32, + ) -> &mut ProcessFdTable { + self.fork_with_cloexec(parent_pid, child_pid, true) + } + + fn fork_with_cloexec( + &mut self, + parent_pid: u32, + child_pid: u32, + preserve_cloexec: bool, + ) -> &mut ProcessFdTable { if !self.tables.contains_key(&parent_pid) { return self.create(child_pid); } - let child = self + let parent = self .tables .get(&parent_pid) - .expect("parent table presence was checked") - .fork(); + .expect("parent table presence was checked"); + let child = if preserve_cloexec { + parent.fork_preserving_cloexec() + } else { + parent.fork() + }; self.remove(child_pid); self.tables.insert(child_pid, child); self.tables @@ -866,15 +1490,37 @@ pub struct FileLockManager { inner: Arc, } -#[derive(Debug, Default)] +#[derive(Debug)] struct FileLockManagerInner { state: Mutex, wake: Condvar, + max_record_locks: usize, +} + +impl Default for FileLockManagerInner { + fn default() -> Self { + Self { + state: Mutex::new(FileLockState::default()), + wake: Condvar::new(), + max_record_locks: DEFAULT_MAX_RECORD_LOCKS, + } + } } #[derive(Debug, Default)] struct FileLockState { entries: BTreeMap, + record_locks: Vec<(FileLockTarget, RecordLock)>, + record_lock_waits: BTreeMap, + warned_near_record_lock_limit: bool, + warned_near_record_lock_wait_limit: bool, +} + +#[derive(Debug, Clone)] +struct RecordLockWait { + target: FileLockTarget, + request: RecordLock, + blockers: BTreeSet, } #[derive(Debug, Default)] @@ -885,7 +1531,17 @@ struct FileLockEntry { impl FileLockManager { pub fn new() -> Self { - Self::default() + Self::with_record_lock_limit(DEFAULT_MAX_RECORD_LOCKS) + } + + pub fn with_record_lock_limit(max_record_locks: usize) -> Self { + Self { + inner: Arc::new(FileLockManagerInner { + state: Mutex::new(FileLockState::default()), + wake: Condvar::new(), + max_record_locks: max_record_locks.max(1), + }), + } } pub fn apply( @@ -926,6 +1582,209 @@ impl FileLockManager { released } + pub fn query_record_lock( + &self, + target: FileLockTarget, + request: RecordLock, + ) -> Option { + let state = lock_or_recover(&self.inner.state); + state + .record_locks + .iter() + .filter_map(|(candidate_target, candidate)| { + (*candidate_target == target + && candidate.pid != request.pid + && record_locks_conflict(*candidate, request)) + .then_some(*candidate) + }) + .min_by_key(|candidate| (candidate.start, candidate.pid)) + } + + pub fn set_record_lock(&self, target: FileLockTarget, request: RecordLock) -> FdResult<()> { + self.set_record_lock_inner(target, request, false) + } + + pub fn set_blocking_record_lock( + &self, + target: FileLockTarget, + request: RecordLock, + ) -> FdResult<()> { + self.set_record_lock_inner(target, request, true) + } + + fn set_record_lock_inner( + &self, + target: FileLockTarget, + request: RecordLock, + blocking: bool, + ) -> FdResult<()> { + let mut state = lock_or_recover(&self.inner.state); + let blockers = if request.lock_type == RecordLockType::Unlock { + BTreeSet::new() + } else { + conflicting_record_lock_pids(&state.record_locks, target, request) + }; + if !blockers.is_empty() { + if blocking { + self.register_record_lock_wait(&mut state, target, request, blockers)?; + } else { + state.record_lock_waits.remove(&request.pid); + } + return Err(FdTableError::would_block( + "POSIX record lock is held by another process", + )); + } + + state.record_lock_waits.remove(&request.pid); + + let mut next = Vec::with_capacity(state.record_locks.len().saturating_add(2)); + for (candidate_target, candidate) in state.record_locks.iter().copied() { + if candidate_target != target + || candidate.pid != request.pid + || !record_ranges_overlap(candidate, request) + { + next.push((candidate_target, candidate)); + continue; + } + + if candidate.start < request.start { + next.push(( + target, + RecordLock { + end: Some(request.start), + ..candidate + }, + )); + } + if let Some(request_end) = request.end { + if candidate + .end + .is_none_or(|candidate_end| candidate_end > request_end) + { + next.push(( + target, + RecordLock { + start: request_end, + ..candidate + }, + )); + } + } + } + + if request.lock_type != RecordLockType::Unlock { + next.push((target, request)); + } + coalesce_record_locks(&mut next); + let warning_threshold = (self.inner.max_record_locks.saturating_mul(4) / 5).max(1); + if !state.warned_near_record_lock_limit && next.len() >= warning_threshold { + state.warned_near_record_lock_limit = true; + eprintln!( + "[agentos] POSIX record lock usage {}/{} is near the limit derived from limits.resources.maxOpenFds; raise limits.resources.maxOpenFds if needed", + next.len(), self.inner.max_record_locks + ); + } + if next.len() > self.inner.max_record_locks { + return Err(FdTableError { + code: "ENOLCK", + message: format!( + "POSIX record lock table limit ({}) derived from limits.resources.maxOpenFds reached; raise limits.resources.maxOpenFds if needed", + self.inner.max_record_locks + ), + }); + } + state.record_locks = next; + refresh_record_lock_waits(&mut state); + drop(state); + self.inner.wake.notify_all(); + Ok(()) + } + + pub fn cancel_record_lock_wait(&self, pid: u32) -> bool { + let mut state = lock_or_recover(&self.inner.state); + state.record_lock_waits.remove(&pid).is_some() + } + + /// POSIX process-associated locks are all discarded when the process + /// closes any descriptor referring to the same file. + pub fn release_process_target(&self, pid: u32, target: FileLockTarget) -> bool { + let mut state = lock_or_recover(&self.inner.state); + let previous = state.record_locks.len(); + state + .record_locks + .retain(|(candidate_target, lock)| *candidate_target != target || lock.pid != pid); + let wait_cancelled = state.record_lock_waits.remove(&pid).is_some(); + let released = state.record_locks.len() != previous; + if released || wait_cancelled { + refresh_record_lock_waits(&mut state); + } + drop(state); + if released || wait_cancelled { + self.inner.wake.notify_all(); + } + released || wait_cancelled + } + + pub fn release_process(&self, pid: u32) -> bool { + let mut state = lock_or_recover(&self.inner.state); + let previous = state.record_locks.len(); + state.record_locks.retain(|(_, lock)| lock.pid != pid); + let wait_cancelled = state.record_lock_waits.remove(&pid).is_some(); + let released = state.record_locks.len() != previous; + if released || wait_cancelled { + refresh_record_lock_waits(&mut state); + } + drop(state); + if released || wait_cancelled { + self.inner.wake.notify_all(); + } + released || wait_cancelled + } + + fn register_record_lock_wait( + &self, + state: &mut FileLockState, + target: FileLockTarget, + request: RecordLock, + blockers: BTreeSet, + ) -> FdResult<()> { + let new_waiter = !state.record_lock_waits.contains_key(&request.pid); + let waiter_count = state.record_lock_waits.len() + usize::from(new_waiter); + if new_waiter && waiter_count > self.inner.max_record_locks { + return Err(FdTableError { + code: "ENOLCK", + message: format!( + "POSIX record lock waiter limit ({}) derived from limits.resources.maxOpenFds reached; raise limits.resources.maxOpenFds if needed", + self.inner.max_record_locks + ), + }); + } + let warning_threshold = (self.inner.max_record_locks.saturating_mul(4) / 5).max(1); + if !state.warned_near_record_lock_wait_limit && waiter_count >= warning_threshold { + state.warned_near_record_lock_wait_limit = true; + eprintln!( + "[agentos] POSIX record lock waiter usage {}/{} is near the limit derived from limits.resources.maxOpenFds; raise limits.resources.maxOpenFds if needed", + waiter_count, + self.inner.max_record_locks + ); + } + state.record_lock_waits.insert( + request.pid, + RecordLockWait { + target, + request, + blockers, + }, + ); + if record_lock_wait_cycle(state, request.pid) { + state.record_lock_waits.remove(&request.pid); + return Err(FdTableError::deadlock( + "POSIX record lock wait would create a deadlock", + )); + } + Ok(()) + } + fn acquire( &self, owner_id: u64, @@ -952,6 +1811,85 @@ impl FileLockManager { } } +fn record_ranges_overlap(left: RecordLock, right: RecordLock) -> bool { + left.end.is_none_or(|end| right.start < end) && right.end.is_none_or(|end| left.start < end) +} + +fn record_locks_conflict(left: RecordLock, right: RecordLock) -> bool { + record_ranges_overlap(left, right) + && (left.lock_type == RecordLockType::Write || right.lock_type == RecordLockType::Write) +} + +fn conflicting_record_lock_pids( + locks: &[(FileLockTarget, RecordLock)], + target: FileLockTarget, + request: RecordLock, +) -> BTreeSet { + locks + .iter() + .filter_map(|(candidate_target, candidate)| { + (*candidate_target == target + && candidate.pid != request.pid + && record_locks_conflict(*candidate, request)) + .then_some(candidate.pid) + }) + .collect() +} + +fn refresh_record_lock_waits(state: &mut FileLockState) { + let FileLockState { + record_locks, + record_lock_waits, + .. + } = state; + for wait in record_lock_waits.values_mut() { + wait.blockers = conflicting_record_lock_pids(record_locks, wait.target, wait.request); + } +} + +fn record_lock_wait_cycle(state: &FileLockState, requester: u32) -> bool { + let Some(wait) = state.record_lock_waits.get(&requester) else { + return false; + }; + let mut pending = wait.blockers.iter().copied().collect::>(); + let mut visited = BTreeSet::new(); + while let Some(pid) = pending.pop() { + if pid == requester { + return true; + } + if !visited.insert(pid) { + continue; + } + if let Some(wait) = state.record_lock_waits.get(&pid) { + pending.extend(wait.blockers.iter().copied()); + } + } + false +} + +fn coalesce_record_locks(locks: &mut Vec<(FileLockTarget, RecordLock)>) { + locks.sort_by_key(|(target, lock)| (*target, lock.pid, lock.lock_type as u8, lock.start)); + let mut merged: Vec<(FileLockTarget, RecordLock)> = Vec::with_capacity(locks.len()); + for (target, lock) in locks.drain(..) { + if let Some((previous_target, previous)) = merged.last_mut() { + let touches = previous.end.is_none_or(|end| lock.start <= end); + if *previous_target == target + && previous.pid == lock.pid + && previous.lock_type == lock.lock_type + && touches + { + previous.end = match (previous.end, lock.end) { + (None, _) | (_, None) => None, + (Some(left), Some(right)) => Some(left.max(right)), + }; + continue; + } + } + merged.push((target, lock)); + } + *locks = merged; +} + impl FileLockEntry { fn can_grant(&self, owner_id: u64, mode: FileLockMode) -> bool { match mode { diff --git a/crates/kernel/src/kernel.rs b/crates/kernel/src/kernel.rs index 19e3d160f3..a4df8b0013 100644 --- a/crates/kernel/src/kernel.rs +++ b/crates/kernel/src/kernel.rs @@ -7,10 +7,12 @@ use crate::dns::{ SharedDnsResolver, }; use crate::fd_table::{ - FdEntry, FdStat, FdTableError, FdTableManager, FileDescription, FileLockManager, - FileLockTarget, FlockOperation, ProcessFdTable, FILETYPE_CHARACTER_DEVICE, FILETYPE_DIRECTORY, - FILETYPE_PIPE, FILETYPE_REGULAR_FILE, FILETYPE_SYMBOLIC_LINK, F_DUPFD, O_APPEND, O_CREAT, - O_EXCL, O_NONBLOCK, O_TRUNC, + AnonymousFile, AnonymousFileUsage, FdEntry, FdStat, FdTableError, FdTableManager, + FileDescription, FileLockManager, FileLockTarget, FlockOperation, ProcessFdTable, RecordLock, + RecordLockType, SharedAnonymousFile, TransferredFd, FD_CLOEXEC, FILETYPE_CHARACTER_DEVICE, + FILETYPE_DIRECTORY, FILETYPE_PIPE, FILETYPE_REGULAR_FILE, FILETYPE_SOCKET_DGRAM, + FILETYPE_SOCKET_STREAM, FILETYPE_SYMBOLIC_LINK, F_DUPFD, O_APPEND, O_CREAT, O_DIRECTORY, + O_EXCL, O_NOFOLLOW, O_NONBLOCK, O_RDONLY, O_TRUNC, O_WRONLY, }; use crate::mount_table::{MountEntry, MountOptions, MountTable, MountedFileSystem}; use crate::network_policy::format_tcp_resource; @@ -37,14 +39,14 @@ use crate::resource_accounting::{ }; use crate::root_fs::{RootFileSystem, RootFilesystemError, RootFilesystemSnapshot}; use crate::socket_table::{ - DatagramSocketOption, InetSocketAddress, ReceivedDatagram, SocketId, SocketMulticastMembership, - SocketReadiness, SocketRecord, SocketShutdown, SocketSpec, SocketState, SocketTable, - SocketTableError, SocketType, + DatagramSocketOption, InetSocketAddress, OpaqueTransferredRight, ReceivedDatagram, SocketId, + SocketMulticastMembership, SocketReadiness, SocketRecord, SocketShutdown, SocketSpec, + SocketState, SocketTable, SocketTableError, SocketType, TransferredSocketRight, }; use crate::user::{ProcessIdentity, UserConfig, UserManager}; use crate::vfs::{ normalize_path, VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat, - VirtualTimeSpec, VirtualUtimeSpec, + VirtualTimeSpec, VirtualUtimeSpec, MAX_PATH_LENGTH, }; use hickory_proto::rr::RecordType; use std::any::Any; @@ -65,6 +67,11 @@ pub const SEEK_CUR: u8 = 1; pub const SEEK_END: u8 = 2; const EXECUTABLE_PERMISSION_BITS: u32 = 0o111; const SHEBANG_LINE_MAX_BYTES: usize = 256; +const MAX_EXEC_INTERPRETER_DEPTH: usize = 4; +const MAX_UNIX_SOCKET_SYMLINKS: usize = 40; +const UNIX_SOCKET_FILE_TYPE: u32 = 0o140000; +const UNIX_DAC_WRITE: u32 = 0o2; +const UNIX_DAC_SEARCH: u32 = 0o1; #[derive(Debug, Clone, PartialEq, Eq)] pub struct KernelError { @@ -113,6 +120,50 @@ impl fmt::Display for KernelError { impl Error for KernelError {} +fn linux_shebang_interpreter(header: &[u8], path: &str) -> KernelResult> { + if !header.starts_with(b"#!") { + return Ok(None); + } + + let payload = &header[2..]; + let newline = payload.iter().position(|byte| *byte == b'\n'); + let line = newline.map_or(payload, |index| &payload[..index]); + let line_end = line + .iter() + .rposition(|byte| !matches!(*byte, b' ' | b'\t')) + .map(|index| index + 1) + .ok_or_else(|| KernelError::new("ENOEXEC", format!("invalid shebang line: {path}")))?; + let line = &line[..line_end]; + let interpreter_start = line + .iter() + .position(|byte| !matches!(*byte, b' ' | b'\t')) + .ok_or_else(|| KernelError::new("ENOEXEC", format!("invalid shebang line: {path}")))?; + let interpreter_tail = &line[interpreter_start..]; + let separator = interpreter_tail + .iter() + .position(|byte| matches!(*byte, b' ' | b'\t')); + + // Linux accepts a truncated optional argument, but it rejects a shebang + // whose interpreter pathname itself reaches the end of BINPRM_BUF_SIZE. + if newline.is_none() && header.len() >= SHEBANG_LINE_MAX_BYTES && separator.is_none() { + return Err(KernelError::new( + "ENOEXEC", + format!("shebang interpreter path exceeds the Linux header limit: {path}"), + )); + } + + let interpreter_end = separator.unwrap_or(interpreter_tail.len()); + let interpreter = std::str::from_utf8(&interpreter_tail[..interpreter_end]) + .map_err(|_| KernelError::new("ENOEXEC", format!("invalid shebang line: {path}")))?; + if interpreter.is_empty() { + return Err(KernelError::new( + "ENOEXEC", + format!("invalid shebang line: {path}"), + )); + } + Ok(Some(interpreter.to_owned())) +} + #[derive(Clone)] pub struct KernelVmConfig { pub vm_id: String, @@ -197,6 +248,108 @@ pub struct WaitPidEventResult { pub event: WaitPidEvent, } +#[derive(Debug)] +pub struct ReceivedFdMessage { + pub payload: Vec, + pub rights: Vec, + pub payload_truncated: bool, + pub control_truncated: bool, + pub full_length: usize, +} + +#[derive(Clone)] +pub enum FdTransferRequest { + Fd(u32), + Opaque(OpaqueTransferredRight), +} + +impl fmt::Debug for FdTransferRequest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Fd(fd) => f.debug_tuple("Fd").field(fd).finish(), + Self::Opaque(resource) => f + .debug_tuple("Opaque") + .field(&(Arc::as_ptr(resource) as *const ())) + .finish(), + } + } +} + +pub enum ReceivedFdRight { + Fd(u32), + Opaque(OpaqueTransferredRight), +} + +impl fmt::Debug for ReceivedFdRight { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Fd(fd) => f.debug_tuple("Fd").field(fd).finish(), + Self::Opaque(resource) => f + .debug_tuple("Opaque") + .field(&(Arc::as_ptr(resource) as *const ())) + .finish(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ProcessFdSnapshotEntry { + pub fd: u32, + pub fd_flags: u32, + pub status_flags: u32, + pub filetype: u8, + pub is_socket: bool, + pub is_pipe: bool, + pub is_pty: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProcessFdDirEntry { + pub name: String, + pub ino: u64, + pub is_directory: bool, + pub is_symbolic_link: bool, +} + +/// The canonical VFS object selected by Linux-style AF_UNIX pathname lookup. +/// +/// `canonical_path` preserves the actual directory reached through symlinks +/// while `stat` carries the `(dev, ino)` identity the sidecar must use to +/// distinguish socket nodes across mounts. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UnixSocketPathNode { + pub canonical_path: String, + pub stat: VirtualStat, +} + +struct UnixSocketBindTarget { + canonical_path: String, + parent: UnixSocketPathNode, + identity: ProcessIdentity, +} + +#[derive(Debug, Clone)] +struct FdSocketEntry { + description: Arc, + socket_id: SocketId, + mode: u32, + uid: u32, + gid: u32, +} + +type FdSocketRegistry = Arc>>; + +enum OpenFileRemovalBacking { + Anonymous { + descriptions: Vec>, + backing: SharedAnonymousFile, + }, + LinkedAlias { + descriptions: Vec>, + live_path: String, + }, +} + #[derive(Debug, Clone)] struct ResolvedSpawnCommand { command: String, @@ -299,10 +452,12 @@ pub struct KernelVm { pipes: PipeManager, ptys: PtyManager, sockets: SocketTable, + fd_sockets: FdSocketRegistry, poll_notifier: PollNotifier, users: UserManager, resources: ResourceAccountant, filesystem_usage_cache: Option, + anonymous_file_usage: Arc, file_locks: FileLockManager, driver_pids: Arc>>>, terminated: bool, @@ -314,6 +469,7 @@ fn cleanup_process_resources( pipes: &PipeManager, ptys: &PtyManager, sockets: &SocketTable, + fd_sockets: &FdSocketRegistry, driver_pids: &Mutex>>, pid: u32, ) { @@ -342,8 +498,17 @@ fn cleanup_process_resources( } for (description, filetype) in cleanup { - close_special_resource_if_needed(file_locks, pipes, ptys, &description, filetype); + close_special_resource_if_needed( + file_locks, + pipes, + ptys, + sockets, + fd_sockets, + &description, + filetype, + ); } + file_locks.release_process(pid); sockets.remove_all_for_pid(pid); @@ -363,6 +528,7 @@ fn dispose_kernel_vm_resources(kernel: &mut KernelVm) { &kernel.pipes, &kernel.ptys, &kernel.sockets, + &kernel.fd_sockets, kernel.driver_pids.as_ref(), pid, ); @@ -401,6 +567,8 @@ fn close_special_resource_if_needed( file_locks: &FileLockManager, pipes: &PipeManager, ptys: &PtyManager, + sockets: &SocketTable, + fd_sockets: &FdSocketRegistry, description: &Arc, filetype: u8, ) { @@ -417,6 +585,40 @@ fn close_special_resource_if_needed( if ptys.is_pty(description.id()) { ptys.close(description.id()); } + + prune_fd_sockets(sockets, fd_sockets); +} + +fn prune_fd_sockets(sockets: &SocketTable, fd_sockets: &FdSocketRegistry) { + loop { + let socket_ids = { + let mut registry = lock_or_recover(fd_sockets); + let closed = registry + .iter() + .filter_map(|(description_id, entry)| { + (entry.description.ref_count() == 0) + .then_some((*description_id, entry.socket_id)) + }) + .collect::>(); + for (description_id, _) in &closed { + registry.remove(description_id); + } + closed + .into_iter() + .map(|(_, socket_id)| socket_id) + .collect::>() + }; + if socket_ids.is_empty() { + return; + } + for socket_id in socket_ids { + if let Err(error) = sockets.remove(socket_id) { + eprintln!( + "[agentos] failed to remove closed descriptor-owned socket {socket_id}: {error}" + ); + } + } + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -448,13 +650,16 @@ impl KernelVm { let users = UserManager::from_config(config.user); let process_table = ProcessTable::with_zombie_ttl(config.zombie_ttl); let process_table_for_pty = process_table.clone(); - let fd_tables = Arc::new(Mutex::new(FdTableManager::with_max_fds( - config - .resources - .max_open_fds - .unwrap_or(DEFAULT_MAX_OPEN_FDS), - ))); - let file_locks = FileLockManager::new(); + let max_open_fds = config + .resources + .max_open_fds + .unwrap_or(DEFAULT_MAX_OPEN_FDS); + let fd_tables = Arc::new(Mutex::new(FdTableManager::with_max_fds(max_open_fds))); + // A descriptor may own several disjoint byte ranges. Keep the + // VM-wide table bounded and use the existing open-fd resource knob as + // the explicit way to raise that derived limit. + let file_locks = + FileLockManager::with_record_lock_limit(max_open_fds.saturating_mul(16).max(16)); let driver_pids = Arc::new(Mutex::new(BTreeMap::new())); let poll_notifier = PollNotifier::default(); let pipes = PipeManager::with_notifier(poll_notifier.clone()); @@ -465,6 +670,7 @@ impl KernelVm { poll_notifier.clone(), ); let sockets = SocketTable::new(); + let fd_sockets = Arc::new(Mutex::new(BTreeMap::new())); let fd_tables_for_exit = Arc::clone(&fd_tables); let file_locks_for_exit = file_locks.clone(); @@ -472,6 +678,7 @@ impl KernelVm { let pipes_for_exit = pipes.clone(); let ptys_for_exit = ptys.clone(); let sockets_for_exit = sockets.clone(); + let fd_sockets_for_exit = Arc::clone(&fd_sockets); process_table.set_on_process_exit(Some(Arc::new(move |pid| { cleanup_process_resources( fd_tables_for_exit.as_ref(), @@ -479,6 +686,7 @@ impl KernelVm { &pipes_for_exit, &ptys_for_exit, &sockets_for_exit, + &fd_sockets_for_exit, driver_pids_for_exit.as_ref(), pid, ); @@ -493,6 +701,7 @@ impl KernelVm { // `filesystem_usage()` through the RAW filesystem so no guest-attributable // permission check fires at construction (or ever) for quota bookkeeping. let filesystem_usage_cache = None; + let anonymous_file_usage = Arc::new(AnonymousFileUsage::default()); Self { vm_id: vm_id.clone(), @@ -511,10 +720,12 @@ impl KernelVm { pipes, ptys, sockets, + fd_sockets, poll_notifier, users, resources: ResourceAccountant::new(config.resources), filesystem_usage_cache, + anonymous_file_usage, file_locks, driver_pids, terminated: false, @@ -736,6 +947,7 @@ impl KernelVm { pub fn pread_file(&mut self, path: &str, offset: u64, length: usize) -> KernelResult> { self.assert_not_terminated()?; + self.reject_unix_socket_data_path(path, "ENXIO")?; self.resources.check_pread_length(length)?; Ok(VirtualFileSystem::pread( &mut self.filesystem, @@ -759,6 +971,7 @@ impl KernelVm { pub fn write_file(&mut self, path: &str, content: impl Into>) -> KernelResult<()> { self.assert_not_terminated()?; self.reject_read_only_resolved_write_path(path)?; + self.reject_unix_socket_data_path(path, "ENXIO")?; let content = content.into(); let new_size = content.len() as u64; let existing = self.storage_stat(path)?; @@ -768,6 +981,204 @@ impl KernelVm { Ok(()) } + /// Resolve the canonical candidate for an AF_UNIX pathname bind without + /// mutating the filesystem. + /// + /// This is the preflight used by sidecars to enforce host-mount policy on + /// the actual symlink-resolved destination before the socket inode is + /// created. The subsequent bind call repeats every lookup and DAC check. + pub fn resolve_unix_socket_bind_target_for_process( + &mut self, + requester_driver: &str, + pid: u32, + cwd: &str, + path: &str, + ) -> KernelResult { + Ok(self + .resolve_unix_socket_bind_target(requester_driver, pid, cwd, path)? + .canonical_path) + } + + fn resolve_unix_socket_bind_target( + &mut self, + requester_driver: &str, + pid: u32, + cwd: &str, + path: &str, + ) -> KernelResult { + self.assert_not_terminated()?; + let identity = self.process_identity(requester_driver, pid)?; + let (absolute_path, mut components, trailing_slash) = + unix_socket_absolute_components(cwd, path)?; + + let Some(basename) = components.pop_back() else { + return Err(unix_socket_address_in_use(&absolute_path)); + }; + + // A trailing slash or a final dot component asks pathname lookup for + // an existing directory. bind(2) cannot replace that entry: Linux + // reports EADDRINUSE when it resolves and the lookup error otherwise. + if trailing_slash || matches!(basename.as_str(), "." | "..") { + let (_, full_components, _) = unix_socket_absolute_components(cwd, path)?; + resolve_unix_socket_components( + self.raw_filesystem_mut(), + &identity, + full_components, + false, + false, + )?; + return Err(unix_socket_address_in_use(&absolute_path)); + } + + let parent = resolve_unix_socket_components( + self.raw_filesystem_mut(), + &identity, + components, + true, + true, + )?; + check_unix_dac( + &identity, + &parent.stat, + UNIX_DAC_WRITE | UNIX_DAC_SEARCH, + "bind", + &parent.canonical_path, + )?; + + let canonical_path = join_absolute_path(&parent.canonical_path, &basename); + self.reject_read_only_entry_write_path(&canonical_path)?; + self.filesystem + .check_virtual_path(FsOperation::Write, &canonical_path) + .map_err(KernelError::from)?; + + match self.raw_filesystem_mut().lstat(&canonical_path) { + Ok(_) => return Err(unix_socket_address_in_use(&canonical_path)), + Err(error) if error.code() == "ENOENT" => {} + Err(error) => return Err(error.into()), + } + + Ok(UnixSocketBindTarget { + canonical_path, + parent, + identity, + }) + } + + /// Perform Linux pathname lookup and materialize the persistent inode for + /// an AF_UNIX bind. + /// + /// Unlike the generic file helpers, this preserves raw `.`/`..` + /// traversal, follows symlinks only in the dirname, checks POSIX DAC with + /// the process's effective credentials, and never creates missing parent + /// directories. The returned canonical path and `(dev, ino)` identify the + /// exact dentry the sidecar must register. + pub fn bind_unix_socket_path_for_process( + &mut self, + requester_driver: &str, + pid: u32, + cwd: &str, + path: &str, + ) -> KernelResult { + let target = self.resolve_unix_socket_bind_target(requester_driver, pid, cwd, path)?; + let UnixSocketBindTarget { + canonical_path, + parent, + identity, + } = target; + let umask = self.processes.get_umask(pid)?; + + self.check_write_file_limits(&canonical_path, 0)?; + if let Err(error) = VirtualFileSystem::create_file_exclusive( + &mut self.filesystem, + &canonical_path, + Vec::new(), + ) { + return if error.code() == "EEXIST" { + Err(unix_socket_address_in_use(&canonical_path)) + } else { + Err(error.into()) + }; + } + + let mode = UNIX_SOCKET_FILE_TYPE | (0o777 & !(umask & 0o777)); + let gid = if parent.stat.mode & 0o2000 != 0 { + parent.stat.gid + } else { + identity.egid + }; + let metadata_result = (|| -> VfsResult { + let filesystem = self.raw_filesystem_mut(); + filesystem.chown(&canonical_path, identity.euid, gid)?; + filesystem.chmod(&canonical_path, mode)?; + filesystem.lstat(&canonical_path) + })(); + let stat = match metadata_result { + Ok(stat) => stat, + Err(error) => { + let cleanup = self.raw_filesystem_mut().remove_file(&canonical_path); + return match cleanup { + Ok(()) => Err(error.into()), + Err(cleanup_error) => Err(KernelError::new( + error.code(), + format!( + "failed to initialize Unix socket inode metadata: {error}; \ + rollback also failed: {cleanup_error}" + ), + )), + }; + } + }; + + self.update_filesystem_usage_cache_for_inode_create(0); + Ok(UnixSocketPathNode { + canonical_path, + stat, + }) + } + + /// Resolve an AF_UNIX connect target with Linux pathname and DAC rules. + /// The final symlink is followed, search permission is required on every + /// traversed directory, and the selected socket inode must be writable by + /// the process's effective credentials. + pub fn resolve_unix_socket_connect_target_for_process( + &mut self, + requester_driver: &str, + pid: u32, + cwd: &str, + path: &str, + ) -> KernelResult { + self.assert_not_terminated()?; + let identity = self.process_identity(requester_driver, pid)?; + let (_, components, trailing_slash) = unix_socket_absolute_components(cwd, path)?; + let target = resolve_unix_socket_components( + self.raw_filesystem_mut(), + &identity, + components, + true, + trailing_slash, + )?; + self.filesystem + .check_virtual_path(FsOperation::Write, &target.canonical_path) + .map_err(KernelError::from)?; + check_unix_dac( + &identity, + &target.stat, + UNIX_DAC_WRITE, + "connect", + &target.canonical_path, + )?; + if target.stat.mode & 0o170000 != UNIX_SOCKET_FILE_TYPE { + return Err(KernelError::new( + "ECONNREFUSED", + format!( + "Unix socket connect target is not a socket: {}", + target.canonical_path + ), + )); + } + Ok(target) + } + /// Writes `content` at `offset` within an existing file, growing (and /// zero-filling) it as needed. This is the positional counterpart to /// [`Self::pread_file`]: it lets a descriptor-based caller (the shared WASI @@ -784,6 +1195,7 @@ impl KernelVm { ) -> KernelResult<()> { self.assert_not_terminated()?; self.reject_read_only_resolved_write_path(path)?; + self.reject_unix_socket_data_path(path, "ENXIO")?; let content = content.into(); let existing = self.storage_stat(path)?; let existing_size = existing.as_ref().map(|stat| stat.size).unwrap_or(0); @@ -812,6 +1224,7 @@ impl KernelVm { let content = content.into(); let new_size = content.len() as u64; self.reject_read_only_resolved_write_path(path)?; + self.reject_unix_socket_data_path(path, "ENXIO")?; let existing = self.storage_stat(path)?; self.check_write_file_limits_with_existing(path, existing.as_ref(), new_size)?; VirtualFileSystem::write_file_with_mode(&mut self.filesystem, path, content, mode)?; @@ -1095,7 +1508,27 @@ impl KernelVm { self.assert_not_terminated()?; self.reject_read_only_entry_write_path(path)?; let removed = self.storage_lstat(path)?; + let detached = self.prepare_anonymous_file_backing(path, removed.as_ref())?; self.filesystem.remove_file(path)?; + match detached { + Some(OpenFileRemovalBacking::Anonymous { + descriptions, + backing, + }) => { + for description in descriptions { + description.detach_path(path, Arc::clone(&backing)); + } + } + Some(OpenFileRemovalBacking::LinkedAlias { + descriptions, + live_path, + }) => { + for description in descriptions { + description.rebind_deleted_path(path, &live_path); + } + } + None => {} + } self.update_filesystem_usage_cache_for_remove(removed.as_ref()); Ok(()) } @@ -1104,7 +1537,13 @@ impl KernelVm { self.assert_not_terminated()?; self.reject_read_only_entry_write_path(path)?; let removed = self.storage_lstat(path)?; + let detached = self.prepare_detached_directory_backing(path, removed.as_ref()); self.filesystem.remove_dir(path)?; + if let Some((descriptions, stat)) = detached { + for description in descriptions { + description.detach_directory(path, stat.clone()); + } + } if removed.as_ref().is_some_and(|stat| stat.is_directory) { self.update_filesystem_usage_cache_for_inode_delete(0); } @@ -1116,7 +1555,37 @@ impl KernelVm { self.reject_read_only_entry_write_path(old_path)?; self.reject_read_only_entry_write_path(new_path)?; self.check_rename_copy_up_limits(old_path, new_path)?; + let replaced = self.storage_lstat(new_path)?; + let detached_destination = + self.prepare_anonymous_file_backing(new_path, replaced.as_ref())?; + let detached_directory_destination = + self.prepare_detached_directory_backing(new_path, replaced.as_ref()); self.filesystem.rename(old_path, new_path)?; + match detached_destination { + Some(OpenFileRemovalBacking::Anonymous { + descriptions, + backing, + }) => { + for description in descriptions { + description.detach_path(new_path, Arc::clone(&backing)); + } + } + Some(OpenFileRemovalBacking::LinkedAlias { + descriptions, + live_path, + }) => { + for description in descriptions { + description.rebind_deleted_path(new_path, &live_path); + } + } + None => {} + } + if let Some((descriptions, stat)) = detached_directory_destination { + for description in descriptions { + description.detach_directory(new_path, stat.clone()); + } + } + self.rename_open_file_descriptions(old_path, new_path); // Rename can be a pure metadata move, a destination replacement, or an // overlay copy-up/removal with hard-link aliasing. Drop the cached root // usage because the local byte/inode delta is not knowable here. @@ -1183,6 +1652,37 @@ impl KernelVm { Ok(self.filesystem.chown(path, uid, gid)?) } + pub fn chown_for_process( + &mut self, + requester_driver: &str, + pid: u32, + path: &str, + uid: u32, + gid: u32, + follow_symlinks: bool, + ) -> KernelResult<()> { + self.assert_not_terminated()?; + let identity = self.process_identity(requester_driver, pid)?; + let stat = if follow_symlinks { + self.stat_for_process(requester_driver, pid, path)? + } else { + self.lstat_for_process(requester_driver, pid, path)? + }; + let (next_uid, next_gid) = validate_chown_request(&identity, &stat, uid, gid, path)?; + + if follow_symlinks { + self.reject_read_only_resolved_write_path(path)?; + } else { + self.reject_read_only_entry_write_path(path)?; + } + self.filesystem + .chown_spec(path, next_uid, next_gid, follow_symlinks)?; + if let Some(mode) = linux_chown_cleared_mode(&stat) { + self.filesystem.chmod(path, mode)?; + } + Ok(()) + } + pub fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> KernelResult<()> { self.utimes_spec( path, @@ -1233,6 +1733,7 @@ impl KernelVm { pub fn truncate(&mut self, path: &str, length: u64) -> KernelResult<()> { self.assert_not_terminated()?; self.reject_read_only_resolved_write_path(path)?; + self.reject_unix_socket_data_path(path, "EINVAL")?; let existing = self.storage_stat(path)?; self.check_truncate_limits_with_existing(path, existing.as_ref(), length)?; self.filesystem.truncate(path, length)?; @@ -1253,6 +1754,55 @@ impl KernelVm { command: &str, args: Vec, options: SpawnOptions, + ) -> KernelResult { + self.spawn_process_with_process_group(command, args, options, None) + } + + pub fn spawn_process_with_process_group( + &mut self, + command: &str, + args: Vec, + options: SpawnOptions, + requested_pgid: Option, + ) -> KernelResult { + self.spawn_process_with_process_group_and_cloexec( + command, + args, + options, + requested_pgid, + false, + ) + } + + /// Create the fork half of a process whose exec is deferred until the + /// caller has applied POSIX spawn file actions. + /// + /// Unlike ordinary combined spawn, this preserves `FD_CLOEXEC` sources. + /// The caller must invoke [`Self::close_process_cloexec_fds`] after all + /// file actions succeed and before exposing the new process image. + pub fn spawn_process_with_process_group_preserving_cloexec( + &mut self, + command: &str, + args: Vec, + options: SpawnOptions, + requested_pgid: Option, + ) -> KernelResult { + self.spawn_process_with_process_group_and_cloexec( + command, + args, + options, + requested_pgid, + true, + ) + } + + fn spawn_process_with_process_group_and_cloexec( + &mut self, + command: &str, + args: Vec, + options: SpawnOptions, + requested_pgid: Option, + preserve_cloexec: bool, ) -> KernelResult { self.assert_not_terminated()?; if let (Some(requester), Some(parent_pid)) = @@ -1289,6 +1839,10 @@ impl KernelVm { }; self.resources .check_process_spawn(&self.resource_snapshot(), inherited_fds)?; + let process_umask = match options.parent_pid { + Some(parent_pid) => self.processes.get_umask(parent_pid)?, + None => DEFAULT_PROCESS_UMASK, + }; self.register_process( resolved.driver.name().to_owned(), @@ -1299,57 +1853,239 @@ impl KernelVm { ppid: options.parent_pid.unwrap_or(0), env, cwd, - umask: DEFAULT_PROCESS_UMASK, + umask: process_umask, fds: Default::default(), identity: self.users.identity(), blocked_signals: SignalSet::empty(), pending_signals: SignalSet::empty(), }, options.requester_driver.as_deref(), + requested_pgid, + preserve_cloexec, ) } - pub fn create_virtual_process( + /// Replace a running process image without allocating a new PID or FD + /// table. This is the kernel half of execve(2): supplied argv/env replace + /// the old image, cwd and process relationships remain attached to the + /// same process, and only FD_CLOEXEC descriptors are closed. + pub fn exec_process( &mut self, requester_driver: &str, - driver: &str, + pid: u32, command: &str, args: Vec, - options: VirtualProcessOptions, - ) -> KernelResult { + env: BTreeMap, + cwd: String, + ) -> KernelResult<()> { + self.exec_process_retaining_internal_fds( + requester_driver, + pid, + command, + args, + env, + cwd, + &[], + &[], + None, + ) + } + + /// Validate the literal pathname supplied to execve(2) without committing + /// a process-image replacement. This preserves Linux pathname/type/mode + /// errno behavior for sidecars that launch the file through an internal + /// language-runtime driver. + pub fn validate_executable_path(&mut self, path: &str, cwd: &str) -> KernelResult { self.assert_not_terminated()?; - if let Some(parent_pid) = options.parent_pid { - self.assert_driver_owns(requester_driver, parent_pid)?; - } + self.resolve_executable_path(path, cwd)? + .ok_or_else(|| KernelError::command_not_found(path)) + } - let cwd = options.cwd.clone().unwrap_or_else(|| self.cwd.clone()); - self.resources.check_process_argv_bytes(command, &args)?; - self.resources - .check_process_env_bytes(&self.env, &options.env)?; + /// Validate the image chain for an in-place WASM exec replacement. Linux + /// applies the same pathname/type/mode checks to each `#!` interpreter as + /// it does to the originally requested script. The runner compiles the + /// resulting WASM image before asking the sidecar to commit, but the + /// trusted kernel remains responsible for enforcing those guest-visible + /// checks and errno values. + pub fn validate_wasm_exec_image(&mut self, path: &str, cwd: &str) -> KernelResult<()> { + self.assert_not_terminated()?; + self.validate_wasm_exec_image_inner(path, cwd, 0) + } - let mut env = self.env.clone(); - env.extend(options.env.clone()); + /// Sidecar variant of [`Self::exec_process`] which keeps host-only + /// plumbing descriptors that are stored in the process FD table as an + /// implementation detail. Those descriptors are never part of the guest's + /// Linux-visible FD set; all guest descriptors still obey FD_CLOEXEC. + pub fn exec_process_retaining_internal_fds( + &mut self, + requester_driver: &str, + pid: u32, + command: &str, + args: Vec, + env: BTreeMap, + _cwd: String, + retained_internal_fds: &[u32], + additional_cloexec_fds: &[u32], + image_command: Option<&str>, + ) -> KernelResult<()> { + self.assert_not_terminated()?; + self.assert_driver_owns(requester_driver, pid)?; + // execve has no cwd argument. Resolve the new image from the process's + // existing working directory and retain that directory across the + // image replacement regardless of what an internal caller supplied. + let cwd = self + .processes + .get(pid) + .ok_or_else(|| KernelError::no_such_process(pid))? + .cwd; + if let Some(image_command) = image_command { + // The sidecar launches the image through its runtime driver, but + // Linux still applies execve pathname checks to the guest file. + // Validate the literal path (including directory and execute-bit + // checks) before the commit point. + self.validate_executable_path(image_command, &cwd)?; + } + let resolved = self.resolve_spawn_command(command, &args, &cwd)?; + let image_command = image_command.unwrap_or(&resolved.command); + let (committed_argv0, committed_args) = resolved + .args + .split_first() + .map(|(argv0, args)| (argv0.clone(), args.to_vec())) + .unwrap_or_else(|| (String::new(), Vec::new())); + self.resources + .check_process_argv_bytes(&committed_argv0, &committed_args)?; + self.resources + .check_process_env_bytes(&BTreeMap::new(), &env)?; check_command_execution( &self.vm_id, &self.permissions, - command, - &args, + image_command, + &committed_args, Some(&cwd), &env, )?; - let inherited_fds = { - let tables = lock_or_recover(&self.fd_tables); - options - .parent_pid - .and_then(|pid| tables.get(pid).map(ProcessFdTable::len)) - .unwrap_or(3) - }; - self.resources - .check_process_spawn(&self.resource_snapshot(), inherited_fds)?; - - self.register_process( - String::from(driver), + // Keep the FD table locked across the only fallible commit operation. + // If ProcessTable::exec rejects the replacement, no descriptor has + // changed. Once it succeeds, removing entries already present in this + // table is infallible, so callers can safely treat Ok as the execve + // point of no return. + let closed_entries = { + let mut tables = lock_or_recover(&self.fd_tables); + let table = tables + .get_mut(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + let mut fds = table + .close_on_exec_fds() + .into_iter() + .collect::>(); + // libc also tracks CLOEXEC for runner-local descriptors. Close any + // forwarded descriptor that belongs to the kernel table and leave + // runner-local handles for the in-place image swap to close. + fds.extend( + additional_cloexec_fds + .iter() + .copied() + .filter(|fd| table.get(*fd).is_some()), + ); + + self.processes.exec( + pid, + resolved.driver.name().to_owned(), + committed_argv0, + committed_args, + env, + cwd, + )?; + + let mut closed_entries = Vec::with_capacity(fds.len()); + for fd in fds { + if retained_internal_fds.contains(&fd) { + continue; + } + if let Some(entry) = table.get(fd).cloned() { + // Presence was checked while holding the table lock. This + // cannot fail and therefore cannot leave exec half-committed. + let closed = table.close(fd); + debug_assert!(closed); + closed_entries.push((entry.description, entry.filetype)); + } + } + closed_entries + }; + for (description, filetype) in closed_entries { + if let Some(target) = description.lock_target() { + self.file_locks.release_process_target(pid, target); + } + self.close_special_resource_if_needed(&description, filetype); + } + Ok(()) + } + + pub fn create_virtual_process( + &mut self, + requester_driver: &str, + driver: &str, + command: &str, + args: Vec, + options: VirtualProcessOptions, + ) -> KernelResult { + self.create_virtual_process_with_process_group( + requester_driver, + driver, + command, + args, + options, + None, + ) + } + + pub fn create_virtual_process_with_process_group( + &mut self, + requester_driver: &str, + driver: &str, + command: &str, + args: Vec, + options: VirtualProcessOptions, + requested_pgid: Option, + ) -> KernelResult { + self.assert_not_terminated()?; + if let Some(parent_pid) = options.parent_pid { + self.assert_driver_owns(requester_driver, parent_pid)?; + } + + let cwd = options.cwd.clone().unwrap_or_else(|| self.cwd.clone()); + self.resources.check_process_argv_bytes(command, &args)?; + self.resources + .check_process_env_bytes(&self.env, &options.env)?; + + let mut env = self.env.clone(); + env.extend(options.env.clone()); + check_command_execution( + &self.vm_id, + &self.permissions, + command, + &args, + Some(&cwd), + &env, + )?; + + let inherited_fds = { + let tables = lock_or_recover(&self.fd_tables); + options + .parent_pid + .and_then(|pid| tables.get(pid).map(ProcessFdTable::len)) + .unwrap_or(3) + }; + self.resources + .check_process_spawn(&self.resource_snapshot(), inherited_fds)?; + let process_umask = match options.parent_pid { + Some(parent_pid) => self.processes.get_umask(parent_pid)?, + None => DEFAULT_PROCESS_UMASK, + }; + + self.register_process( + String::from(driver), String::from(command), args, ProcessContext { @@ -1357,13 +2093,15 @@ impl KernelVm { ppid: options.parent_pid.unwrap_or(0), env, cwd, - umask: DEFAULT_PROCESS_UMASK, + umask: process_umask, fds: Default::default(), identity: self.users.identity(), blocked_signals: SignalSet::empty(), pending_signals: SignalSet::empty(), }, Some(requester_driver), + requested_pgid, + false, ) } @@ -1413,29 +2151,56 @@ impl KernelVm { args: Vec, mut ctx: ProcessContext, requester_driver: Option<&str>, + requested_pgid: Option, + preserve_cloexec: bool, ) -> KernelResult { let pid = self.processes.allocate_pid()?; ctx.pid = pid; - { - let mut tables = lock_or_recover(&self.fd_tables); - if ctx.ppid != 0 { - let parent_pid = ctx.ppid; - tables.fork(parent_pid, pid); - } else { - tables.create(pid); + if let (Some(requester), Some(target_pgid)) = (requester_driver, requested_pgid) { + if target_pgid != 0 && target_pgid != pid { + if let Some(group_owner) = + self.processes + .list_processes() + .into_values() + .find(|process| { + process.pgid == target_pgid && process.status == ProcessStatus::Running + }) + { + if group_owner.driver != requester { + return Err(KernelError::permission_denied(format!( + "driver \"{requester}\" cannot join process group {target_pgid} owned by \"{}\"", + group_owner.driver + ))); + } + } } } let process = Arc::new(StubDriverProcess::default()); - self.processes.register( + self.processes.register_with_process_group( pid, driver_name.clone(), command, args, - ctx, + ctx.clone(), process.clone(), - ); + requested_pgid, + )?; + + { + let mut tables = lock_or_recover(&self.fd_tables); + if ctx.ppid != 0 { + let parent_pid = ctx.ppid; + if preserve_cloexec { + tables.fork_preserving_cloexec(parent_pid, pid); + } else { + tables.fork(parent_pid, pid); + } + } else { + tables.create(pid); + } + } let mut owners = lock_or_recover(&self.driver_pids); owners.entry(driver_name.clone()).or_default().insert(pid); @@ -1459,33 +2224,577 @@ impl KernelVm { Ok(WaitPidResult { pid, status }) } - pub fn waitpid_with_options( + pub fn waitpid_with_options( + &mut self, + requester_driver: &str, + waiter_pid: u32, + pid: i32, + flags: WaitPidFlags, + ) -> KernelResult> { + self.assert_driver_owns(requester_driver, waiter_pid)?; + let result = self.processes.waitpid_for(waiter_pid, pid, flags)?; + Ok(result.map(|result| self.finish_waitpid_event(result))) + } + + pub fn take_nonterminal_wait_event( + &self, + requester_driver: &str, + waiter_pid: u32, + pid: i32, + flags: WaitPidFlags, + ) -> KernelResult> { + self.assert_driver_owns(requester_driver, waiter_pid)?; + let result = self + .processes + .take_nonterminal_wait_event_for(waiter_pid, pid, flags)?; + Ok(result.map(|result| WaitPidEventResult { + pid: result.pid, + status: result.status, + event: result.event, + })) + } + + pub fn wait_and_reap(&mut self, pid: u32) -> KernelResult<(u32, i32)> { + let result = self.waitpid(pid)?; + Ok((result.pid, result.status)) + } + + pub fn open_pipe(&mut self, requester_driver: &str, pid: u32) -> KernelResult<(u32, u32)> { + self.assert_not_terminated()?; + self.assert_driver_owns(requester_driver, pid)?; + let identity = self.process_identity(requester_driver, pid)?; + self.resources + .check_pipe_allocation(&self.resource_snapshot())?; + let (read_fd, write_fd, read_description_id) = { + let mut tables = lock_or_recover(&self.fd_tables); + let table = tables + .get_mut(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + let (read_fd, write_fd) = self.pipes.create_pipe_fds(table)?; + let read_description_id = table + .get(read_fd) + .expect("new pipe read descriptor must exist") + .description + .id(); + (read_fd, write_fd, read_description_id) + }; + self.pipes + .set_owner(read_description_id, identity.euid, identity.egid)?; + Ok((read_fd, write_fd)) + } + + pub fn fd_snapshot( + &self, + requester_driver: &str, + pid: u32, + ) -> KernelResult> { + self.assert_driver_owns(requester_driver, pid)?; + let tables = lock_or_recover(&self.fd_tables); + let table = tables + .get(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + Ok(table + .values() + .map(|entry| ProcessFdSnapshotEntry { + fd: entry.fd, + fd_flags: entry.fd_flags, + status_flags: entry.status_flags | entry.description.flags(), + filetype: entry.filetype, + is_socket: self.fd_socket_id(&entry.description).is_some(), + is_pipe: self.pipes.is_pipe(entry.description.id()), + is_pty: self.ptys.is_pty(entry.description.id()), + }) + .collect()) + } + + /// Create a connected AF_UNIX socket pair whose endpoints live in the + /// process descriptor table. The socket records are owned by their open + /// file descriptions rather than by a PID so SCM_RIGHTS and spawn + /// inheritance preserve them after the creating process exits. + pub fn fd_socketpair( + &mut self, + requester_driver: &str, + pid: u32, + socket_type: SocketType, + nonblocking: bool, + close_on_exec: bool, + ) -> KernelResult<(u32, u32)> { + self.assert_not_terminated()?; + self.assert_driver_owns(requester_driver, pid)?; + let identity = self.process_identity(requester_driver, pid)?; + let spec = match socket_type { + SocketType::Stream => SocketSpec::unix_stream(), + SocketType::Datagram => SocketSpec::unix_datagram(), + SocketType::SeqPacket => SocketSpec::unix_seqpacket(), + }; + + let mut snapshot = self.resource_snapshot(); + self.resources.check_fd_allocation(&snapshot, 2)?; + for _ in 0..2 { + self.resources.check_socket_allocation(&snapshot)?; + snapshot.sockets = snapshot.sockets.saturating_add(1); + self.resources.check_socket_state_transition( + &snapshot, + SocketState::Created, + SocketState::Connected, + )?; + snapshot.socket_connections = snapshot.socket_connections.saturating_add(1); + } + + let status_flags = if nonblocking { O_NONBLOCK } else { 0 }; + let fd_flags = if close_on_exec { FD_CLOEXEC } else { 0 }; + let filetype = if socket_type == SocketType::Datagram { + FILETYPE_SOCKET_DGRAM + } else { + FILETYPE_SOCKET_STREAM + }; + let (first_fd, second_fd, first_description, second_description) = { + let mut tables = lock_or_recover(&self.fd_tables); + let table = tables + .get_mut(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + table.open_pair_with_details( + "socketpair:first", + "socketpair:second", + status_flags, + fd_flags, + filetype, + )? + }; + + // PID 0 is reserved for description-owned sockets. Process cleanup + // must not tear them down while another process or ancillary message + // still retains the open file description. + let first_socket = self.sockets.allocate(0, spec).id(); + let second_socket = self.sockets.allocate(0, spec).id(); + if let Err(error) = self.sockets.connect_pair(first_socket, second_socket) { + for socket_id in [first_socket, second_socket] { + if let Err(cleanup_error) = self.sockets.remove(socket_id) { + eprintln!( + "[agentos] failed to roll back socketpair socket {socket_id}: {cleanup_error}" + ); + } + } + let mut tables = lock_or_recover(&self.fd_tables); + if let Some(table) = tables.get_mut(pid) { + table.close(first_fd); + table.close(second_fd); + } + return Err(error.into()); + } + + { + let mut registry = lock_or_recover(&self.fd_sockets); + registry.insert( + first_description.id(), + FdSocketEntry { + description: first_description, + socket_id: first_socket, + mode: 0o777, + uid: identity.euid, + gid: identity.egid, + }, + ); + registry.insert( + second_description.id(), + FdSocketEntry { + description: second_description, + socket_id: second_socket, + mode: 0o777, + uid: identity.euid, + gid: identity.egid, + }, + ); + } + self.poll_notifier.notify(); + Ok((first_fd, second_fd)) + } + + /// Attach an existing kernel socket to a description-owned fd. Sidecar + /// transports use this when a raw socket becomes transferable through + /// SCM_RIGHTS; owner 0 keeps process teardown from destroying the socket + /// while the open description is queued in another process. + pub fn fd_adopt_socket( + &mut self, + requester_driver: &str, + pid: u32, + socket_id: SocketId, + status_flags: u32, + ) -> KernelResult { + self.assert_not_terminated()?; + self.assert_driver_owns(requester_driver, pid)?; + let identity = self.process_identity(requester_driver, pid)?; + let socket = self + .sockets + .get(socket_id) + .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; + if socket.owner_pid() != pid && socket.owner_pid() != 0 { + return Err(KernelError::permission_denied(format!( + "process {pid} does not own socket {socket_id}" + ))); + } + self.resources + .check_fd_allocation(&self.resource_snapshot(), 1)?; + let filetype = if socket.spec().socket_type == SocketType::Datagram { + FILETYPE_SOCKET_DGRAM + } else { + FILETYPE_SOCKET_STREAM + }; + let (fd, description) = { + let mut tables = lock_or_recover(&self.fd_tables); + let table = tables + .get_mut(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + let fd = table.open_with_details( + &format!("socket:{socket_id}"), + status_flags, + filetype, + None, + )?; + let description = Arc::clone( + &table + .get(fd) + .expect("newly adopted socket fd must exist") + .description, + ); + (fd, description) + }; + self.sockets.reassign_owner(socket_id, 0)?; + lock_or_recover(&self.fd_sockets).insert( + description.id(), + FdSocketEntry { + description, + socket_id, + mode: 0o777, + uid: identity.euid, + gid: identity.egid, + }, + ); + Ok(fd) + } + + /// Attach an existing kernel socket directly to a transferable open file + /// description. Unlike `fd_adopt_socket`, this does not allocate a + /// temporary descriptor in the sender, matching SCM_RIGHTS behavior when + /// the sender is already at its per-process fd limit. + pub fn fd_adopt_socket_transfer( + &mut self, + requester_driver: &str, + pid: u32, + socket_id: SocketId, + status_flags: u32, + ) -> KernelResult { + self.assert_not_terminated()?; + self.assert_driver_owns(requester_driver, pid)?; + let identity = self.process_identity(requester_driver, pid)?; + let socket = self + .sockets + .get(socket_id) + .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; + if socket.owner_pid() != pid && socket.owner_pid() != 0 { + return Err(KernelError::permission_denied(format!( + "process {pid} does not own socket {socket_id}" + ))); + } + let filetype = if socket.spec().socket_type == SocketType::Datagram { + FILETYPE_SOCKET_DGRAM + } else { + FILETYPE_SOCKET_STREAM + }; + let transfer = { + let tables = lock_or_recover(&self.fd_tables); + let table = tables + .get(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + table.create_transfer(&format!("socket:{socket_id}"), status_flags, filetype) + }; + self.sockets.reassign_owner(socket_id, 0)?; + lock_or_recover(&self.fd_sockets).insert( + transfer.description_id(), + FdSocketEntry { + description: transfer.description(), + socket_id, + mode: 0o777, + uid: identity.euid, + gid: identity.egid, + }, + ); + Ok(transfer) + } + + pub fn fd_transfer( + &self, + requester_driver: &str, + pid: u32, + fd: u32, + ) -> KernelResult { + self.assert_driver_owns(requester_driver, pid)?; + let tables = lock_or_recover(&self.fd_tables); + let table = tables + .get(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + Ok(table.transfer(fd)?) + } + + /// Install a transferred open file description at an exact descriptor in + /// another process. Unlike reopening `TransferredFd`'s path, this preserves + /// the same description identity, offset, status flags, and special-resource + /// ownership that existed when the transfer was captured. + pub fn fd_install_transfer_at( + &mut self, + requester_driver: &str, + pid: u32, + fd: u32, + fd_flags: u32, + transfer: &TransferredFd, + ) -> KernelResult<()> { + self.assert_not_terminated()?; + self.assert_driver_owns(requester_driver, pid)?; + let replaced = { + let mut tables = lock_or_recover(&self.fd_tables); + let table = tables + .get_mut(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + let replaced = table + .get(fd) + .map(|entry| (Arc::clone(&entry.description), entry.filetype)); + table.install_transferred_at(transfer, fd, fd_flags)?; + replaced + }; + if let Some((description, filetype)) = replaced { + self.close_special_resource_if_needed(&description, filetype); + } + Ok(()) + } + + pub fn fd_socket_sendmsg( + &mut self, + requester_driver: &str, + pid: u32, + socket_fd: u32, + data: &[u8], + rights_fds: &[u32], + ) -> KernelResult { + let rights = rights_fds + .iter() + .copied() + .map(FdTransferRequest::Fd) + .collect::>(); + self.fd_socket_sendmsg_transfers(requester_driver, pid, socket_fd, data, &rights) + } + + pub fn fd_socket_sendmsg_transfers( + &mut self, + requester_driver: &str, + pid: u32, + socket_fd: u32, + data: &[u8], + transfer_requests: &[FdTransferRequest], + ) -> KernelResult { + self.assert_not_terminated()?; + self.assert_driver_owns(requester_driver, pid)?; + let (socket_id, rights) = { + let tables = lock_or_recover(&self.fd_tables); + let table = tables + .get(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + let socket_entry = table + .get(socket_fd) + .ok_or_else(|| KernelError::bad_file_descriptor(socket_fd))?; + let socket_id = self + .fd_socket_id(&socket_entry.description) + .ok_or_else(|| KernelError::new("ENOTSOCK", "descriptor is not a socket"))?; + let rights = transfer_requests + .iter() + .map(|request| match request { + FdTransferRequest::Fd(fd) => table + .transfer(*fd) + .map(TransferredSocketRight::Fd) + .map_err(KernelError::from), + FdTransferRequest::Opaque(resource) => { + Ok(TransferredSocketRight::Opaque(Arc::clone(resource))) + } + }) + .collect::>>()?; + (socket_id, rights) + }; + + let socket = self + .sockets + .get(socket_id) + .ok_or_else(|| KernelError::bad_file_descriptor(socket_fd))?; + self.sockets.check_write(socket_id)?; + let snapshot = self.resource_snapshot(); + if socket.spec().socket_type == SocketType::Stream { + self.resources + .check_socket_buffer_growth(&snapshot, data.len())?; + } else { + self.resources + .check_socket_datagram_enqueue(&snapshot, data.len())?; + } + let written = self.sockets.send_message(socket_id, data, rights)?; + if written > 0 || socket.spec().socket_type != SocketType::Stream { + self.poll_notifier.notify(); + } + Ok(written) + } + + pub fn fd_socket_recvmsg( &mut self, requester_driver: &str, - waiter_pid: u32, - pid: i32, - flags: WaitPidFlags, - ) -> KernelResult> { - self.assert_driver_owns(requester_driver, waiter_pid)?; - let result = self.processes.waitpid_for(waiter_pid, pid, flags)?; - Ok(result.map(|result| self.finish_waitpid_event(result))) - } + pid: u32, + socket_fd: u32, + max_bytes: usize, + max_rights: usize, + close_on_exec: bool, + peek: bool, + dontwait: bool, + waitall: bool, + ) -> KernelResult> { + self.assert_not_terminated()?; + self.assert_driver_owns(requester_driver, pid)?; + let (socket_id, available_fds, nonblocking, socket_type) = { + let tables = lock_or_recover(&self.fd_tables); + let table = tables + .get(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + let socket_entry = table + .get(socket_fd) + .ok_or_else(|| KernelError::bad_file_descriptor(socket_fd))?; + let socket_id = self + .fd_socket_id(&socket_entry.description) + .ok_or_else(|| KernelError::new("ENOTSOCK", "descriptor is not a socket"))?; + ( + socket_id, + table.available_fd_capacity(), + (socket_entry.description.flags() | socket_entry.status_flags) & O_NONBLOCK != 0, + self.sockets + .get(socket_id) + .ok_or_else(|| KernelError::bad_file_descriptor(socket_fd))? + .spec() + .socket_type, + ) + }; - pub fn wait_and_reap(&mut self, pid: u32) -> KernelResult<(u32, i32)> { - let result = self.waitpid(pid)?; - Ok((result.pid, result.status)) + let deadline = (!nonblocking && !dontwait) + .then(|| { + self.blocking_read_timeout() + .map(|wait| Instant::now() + wait) + }) + .flatten(); + let mut message = loop { + let generation = self.poll_notifier.snapshot(); + let wait_for_full = waitall + && socket_type == SocketType::Stream + && !nonblocking + && !dontwait + && max_bytes > 0; + match self + .sockets + .recv_message(socket_id, max_bytes, peek || wait_for_full) + { + Ok(Some(message)) => { + let peer_closed = self + .sockets + .poll(socket_id, POLLHUP) + .map(|events| events.intersects(POLLHUP)) + .unwrap_or(false); + if wait_for_full && message.full_length < max_bytes && !peer_closed { + drop(message); + } else if wait_for_full && !peek { + break self.sockets.recv_message(socket_id, max_bytes, false)?; + } else { + break Some(message); + } + } + Ok(None) => break None, + Err(error) if error.code() == "EAGAIN" && !nonblocking && !dontwait => { + let remaining = + deadline.map(|deadline| deadline.saturating_duration_since(Instant::now())); + if matches!(remaining, Some(duration) if duration.is_zero()) + || !self.poll_notifier.wait_for_change(generation, remaining) + { + return Err(KernelError::new( + "EAGAIN", + "blocking socket receive timed out; raise limits.resources.maxBlockingReadMs", + )); + } + } + Err(error) => return Err(error.into()), + } + }; + let Some(mut message) = message.take() else { + return Ok(None); + }; + let mut kernel_fd_count = 0usize; + let mut install_count = 0usize; + for right in message.rights.iter().take(max_rights) { + if matches!(right, TransferredSocketRight::Fd(_)) { + if kernel_fd_count >= available_fds { + break; + } + kernel_fd_count += 1; + } + install_count += 1; + } + let discarded = message.rights.split_off(install_count); + let control_truncated = !discarded.is_empty(); + let mut fd_transfers = Vec::with_capacity(kernel_fd_count); + for right in &message.rights { + if let TransferredSocketRight::Fd(fd) = right { + fd_transfers.push(fd.clone()); + } + } + let installed_fds = if fd_transfers.is_empty() { + Vec::new() + } else { + let mut tables = lock_or_recover(&self.fd_tables); + let table = tables + .get_mut(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + table.install_transferred(&fd_transfers, close_on_exec)? + }; + let mut installed_fds = installed_fds.into_iter(); + let rights = message + .rights + .drain(..) + .map(|right| match right { + TransferredSocketRight::Fd(_) => ReceivedFdRight::Fd( + installed_fds + .next() + .expect("every retained fd transfer must be installed"), + ), + TransferredSocketRight::Opaque(resource) => ReceivedFdRight::Opaque(resource), + }) + .collect(); + debug_assert!(installed_fds.next().is_none()); + drop(discarded); + drop(fd_transfers); + prune_fd_sockets(&self.sockets, &self.fd_sockets); + self.poll_notifier.notify(); + Ok(Some(ReceivedFdMessage { + payload: message.payload, + rights, + payload_truncated: message.truncated, + control_truncated, + full_length: message.full_length, + })) } - pub fn open_pipe(&mut self, requester_driver: &str, pid: u32) -> KernelResult<(u32, u32)> { + pub fn fd_socket_shutdown( + &mut self, + requester_driver: &str, + pid: u32, + socket_fd: u32, + how: SocketShutdown, + ) -> KernelResult<()> { self.assert_not_terminated()?; self.assert_driver_owns(requester_driver, pid)?; - self.resources - .check_pipe_allocation(&self.resource_snapshot())?; - let mut tables = lock_or_recover(&self.fd_tables); - let table = tables - .get_mut(pid) - .ok_or_else(|| KernelError::no_such_process(pid))?; - Ok(self.pipes.create_pipe_fds(table)?) + let socket_id = self.fd_socket_id_for_fd(pid, socket_fd)?; + self.sockets.shutdown(socket_id, how)?; + prune_fd_sockets(&self.sockets, &self.fd_sockets); + self.poll_notifier.notify(); + Ok(()) } pub fn open_pty( @@ -1545,7 +2854,7 @@ impl KernelVm { .sockets .get(socket_id) .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {socket_id}" ))); @@ -1575,7 +2884,7 @@ impl KernelVm { .sockets .get(socket_id) .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {socket_id}" ))); @@ -1600,7 +2909,7 @@ impl KernelVm { .sockets .get(socket_id) .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {socket_id}" ))); @@ -1631,7 +2940,7 @@ impl KernelVm { let existing = self.sockets.get(listener_socket_id).ok_or_else(|| { KernelError::new("ENOENT", format!("no such socket {listener_socket_id}")) })?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {listener_socket_id}" ))); @@ -1654,7 +2963,7 @@ impl KernelVm { let existing = self.sockets.get(listener_socket_id).ok_or_else(|| { KernelError::new("ENOENT", format!("no such socket {listener_socket_id}")) })?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {listener_socket_id}" ))); @@ -1686,7 +2995,7 @@ impl KernelVm { .sockets .get(socket_id) .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {socket_id}" ))); @@ -1727,7 +3036,7 @@ impl KernelVm { .sockets .get(socket_id) .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {socket_id}" ))); @@ -1775,7 +3084,7 @@ impl KernelVm { .sockets .get(socket_id) .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {socket_id}" ))); @@ -1838,7 +3147,7 @@ impl KernelVm { .sockets .get(socket_id) .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {socket_id}" ))); @@ -1906,7 +3215,7 @@ impl KernelVm { .sockets .get(socket_id) .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {socket_id}" ))); @@ -1933,7 +3242,7 @@ impl KernelVm { .sockets .get(socket_id) .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {socket_id}" ))); @@ -1958,7 +3267,7 @@ impl KernelVm { .sockets .get(socket_id) .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {socket_id}" ))); @@ -1983,7 +3292,7 @@ impl KernelVm { .sockets .get(socket_id) .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {socket_id}" ))); @@ -2008,7 +3317,7 @@ impl KernelVm { .sockets .get(socket_id) .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {socket_id}" ))); @@ -2037,7 +3346,7 @@ impl KernelVm { .sockets .get(socket_id) .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {socket_id}" ))); @@ -2066,7 +3375,7 @@ impl KernelVm { .sockets .get(socket_id) .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {socket_id}" ))); @@ -2092,7 +3401,7 @@ impl KernelVm { .sockets .get(socket_id) .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {socket_id}" ))); @@ -2115,7 +3424,7 @@ impl KernelVm { .sockets .get(socket_id) .ok_or_else(|| KernelError::new("ENOENT", format!("no such socket {socket_id}")))?; - if existing.owner_pid() != pid { + if existing.owner_pid() != pid && existing.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {socket_id}" ))); @@ -2136,6 +3445,7 @@ impl KernelVm { ) -> KernelResult { self.assert_not_terminated()?; self.assert_driver_owns(requester_driver, pid)?; + self.validate_fd_open_flags(pid, path, flags)?; if let Some(existing_fd) = parse_dev_fd_path(path)? { { let tables = lock_or_recover(&self.fd_tables); @@ -2206,6 +3516,11 @@ impl KernelVm { false }; let (filetype, lock_target) = self.prepare_fd_open(path, flags, mode)?; + let description_path = if filetype == FILETYPE_DIRECTORY { + self.realpath_internal(Some(pid), path)? + } else { + path.to_owned() + }; if flags & O_CREAT != 0 && !existed { let umask = self.processes.get_umask(pid)?; self.apply_creation_mode(path, mode.unwrap_or(0o666), umask)?; @@ -2216,7 +3531,7 @@ impl KernelVm { let table = tables .get_mut(pid) .ok_or_else(|| KernelError::no_such_process(pid))?; - Ok(table.open_with_details(path, flags, filetype, lock_target)?) + Ok(table.open_with_details(&description_path, flags, filetype, lock_target)?) } pub fn fd_read( @@ -2249,11 +3564,46 @@ impl KernelVm { .ok_or_else(|| KernelError::bad_file_descriptor(fd))? }; + if let Some(socket_id) = self.fd_socket_id(&entry.description) { + self.resources.check_pread_length(length)?; + let nonblocking = (entry.description.flags() | entry.status_flags) & O_NONBLOCK != 0; + let wait = if nonblocking { + Some(Duration::ZERO) + } else { + timeout.or_else(|| self.blocking_read_timeout()) + }; + let deadline = wait.map(|wait| Instant::now() + wait); + let result = loop { + let generation = self.poll_notifier.snapshot(); + match self.sockets.read(socket_id, length) { + Ok(result) => break result, + Err(error) if error.code() == "EAGAIN" && !nonblocking => { + let remaining = deadline + .map(|deadline| deadline.saturating_duration_since(Instant::now())); + if matches!(remaining, Some(duration) if duration.is_zero()) + || !self.poll_notifier.wait_for_change(generation, remaining) + { + return Err(KernelError::new( + "EAGAIN", + "blocking socket read timed out; raise limits.resources.maxBlockingReadMs", + )); + } + } + Err(error) => return Err(error.into()), + } + }; + prune_fd_sockets(&self.sockets, &self.fd_sockets); + if result.is_some() { + self.poll_notifier.notify(); + } + return Ok(result); + } + if self.pipes.is_pipe(entry.description.id()) { return Ok(self.pipes.read_with_timeout( entry.description.id(), length, - if entry.status_flags & O_NONBLOCK != 0 { + if (entry.description.flags() | entry.status_flags) & O_NONBLOCK != 0 { Some(Duration::ZERO) } else { timeout.or_else(|| self.blocking_read_timeout()) @@ -2265,7 +3615,7 @@ impl KernelVm { return Ok(self.ptys.read_with_timeout( entry.description.id(), length, - if entry.status_flags & O_NONBLOCK != 0 { + if (entry.description.flags() | entry.status_flags) & O_NONBLOCK != 0 { Some(Duration::ZERO) } else { timeout.or_else(|| self.blocking_read_timeout()) @@ -2275,8 +3625,9 @@ impl KernelVm { self.resources.check_pread_length(length)?; - if is_proc_path(entry.description.path()) { - let bytes = self.proc_read_file_from_open_path(Some(pid), entry.description.path())?; + let path = entry.description.path(); + if is_proc_path(&path) { + let bytes = self.proc_read_file_from_open_path(Some(pid), &path)?; let start = entry.description.cursor() as usize; let end = start.saturating_add(length).min(bytes.len()); let chunk = if start >= bytes.len() { @@ -2294,12 +3645,11 @@ impl KernelVm { } let cursor = entry.description.cursor(); - let bytes = VirtualFileSystem::pread( - &mut self.filesystem, - entry.description.path(), - cursor, - length, - )?; + let bytes = if let Some(bytes) = entry.description.anonymous_pread(cursor, length) { + bytes + } else { + VirtualFileSystem::pread(&mut self.filesystem, &path, cursor, length)? + }; entry .description .set_cursor(cursor.saturating_add(bytes.len() as u64)); @@ -2324,11 +3674,32 @@ impl KernelVm { .ok_or_else(|| KernelError::bad_file_descriptor(fd))? }; + if let Some(socket_id) = self.fd_socket_id(&entry.description) { + let socket = self + .sockets + .get(socket_id) + .ok_or_else(|| KernelError::bad_file_descriptor(fd))?; + self.sockets.check_write(socket_id)?; + let snapshot = self.resource_snapshot(); + if socket.spec().socket_type == SocketType::Stream { + self.resources + .check_socket_buffer_growth(&snapshot, data.len())?; + } else { + self.resources + .check_socket_datagram_enqueue(&snapshot, data.len())?; + } + let written = self.sockets.write(socket_id, data)?; + if written > 0 || socket.spec().socket_type != SocketType::Stream { + self.poll_notifier.notify(); + } + return Ok(written); + } + if self.pipes.is_pipe(entry.description.id()) { return match self.pipes.write_with_mode( entry.description.id(), data, - entry.status_flags & O_NONBLOCK != 0, + (entry.description.flags() | entry.status_flags) & O_NONBLOCK != 0, ) { Ok(bytes) => Ok(bytes), Err(error) => { @@ -2344,9 +3715,26 @@ impl KernelVm { return Ok(self.ptys.write(entry.description.id(), data)?); } - self.reject_read_only_resolved_write_path(entry.description.path())?; - - let path = entry.description.path().to_owned(); + let path = entry.description.path(); + if let Some(stat) = entry.description.anonymous_stat() { + let cursor = if entry.description.flags() & O_APPEND != 0 { + stat.size + } else { + entry.description.cursor() + }; + let required_size = stat.size.max(checked_write_end(cursor, data.len())?); + self.check_path_resize_limits_with_existing(stat.size, required_size)?; + let new_size = entry + .description + .anonymous_pwrite(cursor, data) + .expect("anonymous stat and backing must agree")?; + debug_assert_eq!(new_size, required_size); + entry + .description + .set_cursor(cursor.saturating_add(data.len() as u64)); + return Ok(data.len()); + } + self.reject_read_only_resolved_write_path(&path)?; if is_virtual_device_storage_path(&path) { VirtualFileSystem::write_file(&mut self.filesystem, &path, data.to_vec())?; let cursor = entry.description.cursor(); @@ -2482,7 +3870,10 @@ impl KernelVm { .ok_or_else(|| KernelError::bad_file_descriptor(fd))? }; - if self.pipes.is_pipe(entry.description.id()) || self.ptys.is_pty(entry.description.id()) { + if self.pipes.is_pipe(entry.description.id()) + || self.ptys.is_pty(entry.description.id()) + || self.fd_socket_id(&entry.description).is_some() + { return Err(KernelError::new("ESPIPE", "illegal seek")); } @@ -2490,11 +3881,13 @@ impl KernelVm { SEEK_SET => 0_i128, SEEK_CUR => i128::from(entry.description.cursor()), SEEK_END => { - let size = if is_proc_path(entry.description.path()) { - self.proc_stat_from_open_path(Some(pid), entry.description.path())? - .size + let path = entry.description.path(); + let size = if let Some(stat) = entry.description.anonymous_stat() { + stat.size + } else if is_proc_path(&path) { + self.proc_stat_from_open_path(Some(pid), &path)?.size } else { - self.filesystem.stat(entry.description.path())?.size + self.filesystem.stat(&path)?.size }; i128::from(size) } @@ -2534,12 +3927,16 @@ impl KernelVm { .ok_or_else(|| KernelError::bad_file_descriptor(fd))? }; - if self.pipes.is_pipe(entry.description.id()) || self.ptys.is_pty(entry.description.id()) { + if self.pipes.is_pipe(entry.description.id()) + || self.ptys.is_pty(entry.description.id()) + || self.fd_socket_id(&entry.description).is_some() + { return Err(KernelError::new("ESPIPE", "illegal seek")); } - if is_proc_path(entry.description.path()) { - let bytes = self.proc_read_file_from_open_path(Some(pid), entry.description.path())?; + let path = entry.description.path(); + if is_proc_path(&path) { + let bytes = self.proc_read_file_from_open_path(Some(pid), &path)?; let start = usize::try_from(offset) .map_err(|_| KernelError::new("EINVAL", "pread offset out of range"))?; let end = start.saturating_add(length).min(bytes.len()); @@ -2550,9 +3947,12 @@ impl KernelVm { }); } + if let Some(bytes) = entry.description.anonymous_pread(offset, length) { + return Ok(bytes); + } Ok(VirtualFileSystem::pread( &mut self.filesystem, - entry.description.path(), + &path, offset, length, )?) @@ -2577,25 +3977,136 @@ impl KernelVm { .ok_or_else(|| KernelError::bad_file_descriptor(fd))? }; - if self.pipes.is_pipe(entry.description.id()) || self.ptys.is_pty(entry.description.id()) { + if self.pipes.is_pipe(entry.description.id()) + || self.ptys.is_pty(entry.description.id()) + || self.fd_socket_id(&entry.description).is_some() + { return Err(KernelError::new("ESPIPE", "illegal seek")); } - self.reject_read_only_resolved_write_path(entry.description.path())?; + let path = entry.description.path(); + if let Some(stat) = entry.description.anonymous_stat() { + let required_size = stat.size.max(checked_write_end(offset, data.len())?); + self.check_path_resize_limits_with_existing(stat.size, required_size)?; + let new_size = entry + .description + .anonymous_pwrite(offset, data) + .expect("anonymous stat and backing must agree")?; + debug_assert_eq!(new_size, required_size); + return Ok(data.len()); + } + self.reject_read_only_resolved_write_path(&path)?; - let current_size = self.current_storage_file_size(entry.description.path())?; + let current_size = self.current_storage_file_size(&path)?; let required_size = current_size.max(checked_write_end(offset, data.len())?); - self.check_path_resize_limits(entry.description.path(), required_size)?; - VirtualFileSystem::pwrite( - &mut self.filesystem, - entry.description.path(), - data.to_vec(), - offset, - )?; + self.check_path_resize_limits(&path, required_size)?; + VirtualFileSystem::pwrite(&mut self.filesystem, &path, data.to_vec(), offset)?; self.update_filesystem_usage_cache_for_resize(current_size, required_size); Ok(data.len()) } + pub fn fd_truncate( + &mut self, + requester_driver: &str, + pid: u32, + fd: u32, + length: u64, + ) -> KernelResult<()> { + let description = self.description_for_fd(requester_driver, pid, fd)?; + if let Some(stat) = description.anonymous_stat() { + self.check_path_resize_limits_with_existing(stat.size, length)?; + return Ok(description + .anonymous_truncate(length) + .expect("anonymous stat and backing must agree")?); + } + let path = description.path(); + self.truncate(&path, length) + } + + pub fn fd_chmod( + &mut self, + requester_driver: &str, + pid: u32, + fd: u32, + mode: u32, + ) -> KernelResult<()> { + let description = self.description_for_fd(requester_driver, pid, fd)?; + if description.detached_chmod(mode) { + return Ok(()); + } + if self.pipes.is_pipe(description.id()) { + self.pipes.chmod(description.id(), mode)?; + return Ok(()); + } + if let Some(socket) = lock_or_recover(&self.fd_sockets).get_mut(&description.id()) { + socket.mode = mode & 0o7777; + return Ok(()); + } + let path = description.path(); + self.chmod(&path, mode) + } + + pub fn fd_chmod_for_process( + &mut self, + requester_driver: &str, + pid: u32, + fd: u32, + mode: u32, + ) -> KernelResult<()> { + let identity = self.process_identity(requester_driver, pid)?; + let stat = self.dev_fd_stat(requester_driver, pid, fd)?; + if identity.euid != 0 && identity.euid != stat.uid { + return Err(KernelError::new( + "EPERM", + format!("operation not permitted, process does not own fd {fd}"), + )); + } + self.fd_chmod(requester_driver, pid, fd, mode) + } + + pub fn fd_chown_for_process( + &mut self, + requester_driver: &str, + pid: u32, + fd: u32, + uid: u32, + gid: u32, + ) -> KernelResult<()> { + let description = self.description_for_fd(requester_driver, pid, fd)?; + let identity = self.process_identity(requester_driver, pid)?; + let stat = self.dev_fd_stat(requester_driver, pid, fd)?; + let (next_uid, next_gid) = + validate_chown_request(&identity, &stat, uid, gid, &format!("fd {fd}"))?; + let changed_mode = linux_chown_cleared_mode(&stat); + if description.detached_chown(next_uid, next_gid, changed_mode) { + return Ok(()); + } + if self.pipes.is_pipe(description.id()) { + self.pipes.set_owner(description.id(), next_uid, next_gid)?; + return Ok(()); + } + if let Some(socket) = lock_or_recover(&self.fd_sockets).get_mut(&description.id()) { + socket.uid = next_uid; + socket.gid = next_gid; + return Ok(()); + } + + let fd_type = self.fd_stat(requester_driver, pid, fd)?.filetype; + if !matches!(fd_type, FILETYPE_REGULAR_FILE | FILETYPE_DIRECTORY) { + // Character devices have no mutable descriptor-owned inode in + // AgentOS. The fixed unprivileged guest can only reach this branch + // for the Linux no-op (-1, -1) request. + return Ok(()); + } + let path = description.path(); + self.reject_read_only_resolved_write_path(&path)?; + self.filesystem.chown(&path, next_uid, next_gid)?; + if let Some(mode) = changed_mode { + self.filesystem.chmod(&path, mode)?; + } + Ok(()) + } + pub fn fd_dup(&mut self, requester_driver: &str, pid: u32, fd: u32) -> KernelResult { self.assert_driver_owns(requester_driver, pid)?; { @@ -2656,6 +4167,9 @@ impl KernelVm { } if let Some(entry) = replaced { + if let Some(target) = entry.description.lock_target() { + self.file_locks.release_process_target(pid, target); + } self.close_special_resource_if_needed(&entry.description, entry.filetype); } Ok(()) @@ -2675,10 +4189,48 @@ impl KernelVm { table.close(fd); (entry.description, entry.filetype) }; + if let Some(target) = description.lock_target() { + self.file_locks.release_process_target(pid, target); + } self.close_special_resource_if_needed(&description, filetype); Ok(()) } + /// Commit the descriptor half of exec by closing every descriptor still + /// marked `FD_CLOEXEC` after POSIX spawn file actions have completed. + pub fn close_process_cloexec_fds( + &mut self, + requester_driver: &str, + pid: u32, + ) -> KernelResult<()> { + self.assert_driver_owns(requester_driver, pid)?; + let closed_entries = { + let mut tables = lock_or_recover(&self.fd_tables); + let table = tables + .get_mut(pid) + .ok_or_else(|| KernelError::no_such_process(pid))?; + let fds = table.close_on_exec_fds(); + let mut closed_entries = Vec::with_capacity(fds.len()); + for fd in fds { + let entry = table + .get(fd) + .cloned() + .expect("close-on-exec snapshot must reference an open descriptor"); + let closed = table.close(fd); + debug_assert!(closed); + closed_entries.push((entry.description, entry.filetype)); + } + closed_entries + }; + for (description, filetype) in closed_entries { + if let Some(target) = description.lock_target() { + self.file_locks.release_process_target(pid, target); + } + self.close_special_resource_if_needed(&description, filetype); + } + Ok(()) + } + pub fn fd_fcntl( &mut self, requester_driver: &str, @@ -2735,7 +4287,7 @@ impl KernelVm { .ok_or_else(|| KernelError::bad_file_descriptor(fd))? }; - if entry.filetype != FILETYPE_REGULAR_FILE { + if !matches!(entry.filetype, FILETYPE_REGULAR_FILE | FILETYPE_DIRECTORY) { return Err(KernelError::new( "EBADF", format!("file descriptor {fd} does not support advisory locking"), @@ -2754,6 +4306,118 @@ impl KernelVm { Ok(()) } + pub fn fd_record_lock( + &self, + requester_driver: &str, + pid: u32, + fd: u32, + lock_type: RecordLockType, + start: u64, + length: u64, + query: bool, + ) -> KernelResult> { + self.fd_record_lock_impl( + requester_driver, + pid, + fd, + lock_type, + start, + length, + query, + false, + ) + } + + pub fn fd_record_lock_wait( + &self, + requester_driver: &str, + pid: u32, + fd: u32, + lock_type: RecordLockType, + start: u64, + length: u64, + ) -> KernelResult<()> { + self.fd_record_lock_impl( + requester_driver, + pid, + fd, + lock_type, + start, + length, + false, + true, + ) + .map(|_| ()) + } + + pub fn fd_record_lock_cancel(&self, requester_driver: &str, pid: u32) -> KernelResult<()> { + self.assert_driver_owns(requester_driver, pid)?; + self.file_locks.cancel_record_lock_wait(pid); + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + fn fd_record_lock_impl( + &self, + requester_driver: &str, + pid: u32, + fd: u32, + lock_type: RecordLockType, + start: u64, + length: u64, + query: bool, + blocking: bool, + ) -> KernelResult> { + self.assert_driver_owns(requester_driver, pid)?; + let entry = { + let tables = lock_or_recover(&self.fd_tables); + tables + .get(pid) + .and_then(|table| table.get(fd)) + .cloned() + .ok_or_else(|| KernelError::bad_file_descriptor(fd))? + }; + if !matches!(entry.filetype, FILETYPE_REGULAR_FILE | FILETYPE_DIRECTORY) { + return Err(KernelError::new( + "EBADF", + format!("file descriptor {fd} does not support POSIX record locks"), + )); + } + if query && lock_type == RecordLockType::Unlock { + return Err(KernelError::new( + "EINVAL", + "F_GETLK requires a read or write lock type", + )); + } + if !query { + let access_mode = entry.description.flags() & 0o3; + if (lock_type == RecordLockType::Read && access_mode == O_WRONLY) + || (lock_type == RecordLockType::Write && access_mode == O_RDONLY) + { + return Err(KernelError::new( + "EBADF", + format!("file descriptor {fd} access mode is incompatible with record lock"), + )); + } + } + let target = entry.description.lock_target().ok_or_else(|| { + KernelError::new( + "EBADF", + format!("file descriptor {fd} is missing record lock metadata"), + ) + })?; + let request = RecordLock::new(lock_type, start, length, pid)?; + if query { + Ok(self.file_locks.query_record_lock(target, request)) + } else if blocking { + self.file_locks.set_blocking_record_lock(target, request)?; + Ok(None) + } else { + self.file_locks.set_record_lock(target, request)?; + Ok(None) + } + } + pub fn fd_stat(&self, requester_driver: &str, pid: u32, fd: u32) -> KernelResult { self.assert_driver_owns(requester_driver, pid)?; let tables = lock_or_recover(&self.fd_tables); @@ -2763,9 +4427,81 @@ impl KernelVm { .stat(fd)?) } + /// Synchronize a descriptor's committed data and metadata. The in-memory + /// VFS applies writes synchronously, so successful regular-file and + /// directory syncs require no extra flush. Descriptor validation and + /// Linux type errors still happen here rather than being silently ignored. + pub fn fd_sync(&self, requester_driver: &str, pid: u32, fd: u32) -> KernelResult<()> { + let stat = self.fd_stat(requester_driver, pid, fd)?; + match stat.filetype { + FILETYPE_REGULAR_FILE | FILETYPE_DIRECTORY => Ok(()), + _ => Err(KernelError::new( + "EINVAL", + format!("file descriptor {fd} cannot be synchronized"), + )), + } + } + + pub fn fd_read_dir_with_types( + &mut self, + requester_driver: &str, + pid: u32, + fd: u32, + ) -> KernelResult> { + let stat = self.fd_stat(requester_driver, pid, fd)?; + if stat.filetype != FILETYPE_DIRECTORY { + return Err(KernelError::new( + "ENOTDIR", + format!("file descriptor {fd} is not a directory"), + )); + } + let description = self.description_for_fd(requester_driver, pid, fd)?; + if description.detached_directory_stat().is_some() { + // Linux getdents64(2) on an open directory that has since been + // removed returns immediate EOF. The OFD remains valid for fstat, + // but the unlinked dentry no longer enumerates even synthetic dots. + return Ok(Vec::new()); + } + let path = self.fd_path(requester_driver, pid, fd)?; + let children = self.read_dir_with_types_for_process(requester_driver, pid, &path)?; + self.resources + .check_readdir_entries(children.len().saturating_add(2))?; + + // fd_readdir is the Linux-like descriptor traversal surface. Unlike + // path-based Node readdir, it exposes the synthetic current/parent + // entries and inode identities used by libc readdir/telldir/seekdir. + let current_stat = self.stat_internal(Some(pid), &path)?; + let parent = parent_path(&path); + let parent_stat = self.stat_internal(Some(pid), &parent)?; + let mut entries = Vec::with_capacity(children.len().saturating_add(2)); + entries.push(ProcessFdDirEntry { + name: String::from("."), + ino: required_dirent_ino(&path, current_stat.ino)?, + is_directory: true, + is_symbolic_link: false, + }); + entries.push(ProcessFdDirEntry { + name: String::from(".."), + ino: required_dirent_ino(&parent, parent_stat.ino)?, + is_directory: true, + is_symbolic_link: false, + }); + for child in children { + let child_path = join_child_path(&path, &child.name); + let child_stat = self.lstat_internal(Some(pid), &child_path)?; + entries.push(ProcessFdDirEntry { + name: child.name, + ino: required_dirent_ino(&child_path, child_stat.ino)?, + is_directory: child.is_directory, + is_symbolic_link: child.is_symbolic_link, + }); + } + Ok(entries) + } + pub fn fd_path(&self, requester_driver: &str, pid: u32, fd: u32) -> KernelResult { let description = self.description_for_fd(requester_driver, pid, fd)?; - Ok(description.path().to_owned()) + Ok(description.path()) } pub fn isatty(&self, requester_driver: &str, pid: u32, fd: u32) -> KernelResult { @@ -3044,15 +4780,41 @@ impl KernelVm { .ok_or_else(|| KernelError::bad_file_descriptor(fd))? }; - if self.pipes.is_pipe(entry.description.id()) || self.ptys.is_pty(entry.description.id()) { + if let Some(pipe_id) = self.pipes.pipe_id_for(entry.description.id()) { + let metadata = self + .pipes + .metadata(entry.description.id()) + .expect("live pipe description must retain inode metadata"); + let mut stat = synthetic_special_file_stat(pipe_id, 0o010000 | metadata.mode, 13); + stat.uid = metadata.uid; + stat.gid = metadata.gid; + return Ok(stat); + } + + if let Some(socket) = lock_or_recover(&self.fd_sockets).get(&entry.description.id()) { + let mut stat = + synthetic_special_file_stat(entry.description.id(), 0o140000 | socket.mode, 9); + stat.uid = socket.uid; + stat.gid = socket.gid; + return Ok(stat); + } + + if self.ptys.is_pty(entry.description.id()) { return Ok(synthetic_character_device_stat(entry.description.id())); } - if is_proc_path(entry.description.path()) { - return self.proc_stat_from_open_path(Some(pid), entry.description.path()); + if let Some(stat) = entry.description.anonymous_stat() { + return Ok(stat); + } + if let Some(stat) = entry.description.detached_directory_stat() { + return Ok(stat); + } + let path = entry.description.path(); + if is_proc_path(&path) { + return self.proc_stat_from_open_path(Some(pid), &path); } - Ok(self.filesystem.stat(entry.description.path())?) + Ok(self.filesystem.stat(&path)?) } pub fn dispose(&mut self) -> KernelResult<()> { @@ -3086,12 +4848,19 @@ impl KernelVm { let stat = VirtualFileSystem::stat(&mut self.filesystem, path)?; return Ok(( filetype_for_path(path, &stat), - Some(FileLockTarget::new(stat.ino)), + Some(FileLockTarget::new(stat.dev, stat.ino)), )); } let exists = self.filesystem.exists(path)?; if exists { + let existing_stat = VirtualFileSystem::stat(&mut self.filesystem, path)?; + if existing_stat.mode & 0o170000 == 0o140000 { + return Err(KernelError::new( + "ENXIO", + format!("cannot open Unix socket pathname '{path}'"), + )); + } if flags & O_TRUNC != 0 { let existing_size = self.current_storage_file_size(path)?; self.check_path_resize_limits_with_existing(existing_size, 0)?; @@ -3110,10 +4879,96 @@ impl KernelVm { let stat = VirtualFileSystem::stat(&mut self.filesystem, path)?; Ok(( filetype_for_path(path, &stat), - Some(FileLockTarget::new(stat.ino)), + Some(FileLockTarget::new(stat.dev, stat.ino)), )) } + fn validate_fd_open_flags(&mut self, pid: u32, path: &str, flags: u32) -> KernelResult<()> { + if flags & O_DIRECTORY != 0 { + if flags & O_CREAT != 0 { + return Err(KernelError::new( + "EINVAL", + format!("O_DIRECTORY and O_CREAT cannot be combined for '{path}'"), + )); + } + } + + if let Some(existing_fd) = parse_dev_fd_path(path)? { + let filetype = lock_or_recover(&self.fd_tables) + .get(pid) + .and_then(|table| table.get(existing_fd)) + .map(|entry| entry.filetype) + .ok_or_else(|| { + KernelError::new( + "ENOENT", + format!("no such file or directory, open '{path}'"), + ) + })?; + if flags & O_CREAT != 0 && flags & O_EXCL != 0 { + return Err(KernelError::new( + "EEXIST", + format!("file already exists, open '{path}'"), + )); + } + if flags & O_NOFOLLOW != 0 { + return Err(KernelError::new( + "ELOOP", + format!("symbolic link not followed, open '{path}'"), + )); + } + if flags & O_DIRECTORY != 0 { + if filetype != FILETYPE_DIRECTORY { + return Err(KernelError::new( + "ENOTDIR", + format!("not a directory, open '{path}'"), + )); + } + } + return Ok(()); + } + + if flags & O_DIRECTORY != 0 { + let stat = if flags & O_NOFOLLOW != 0 { + self.lstat_internal(Some(pid), path)? + } else { + self.stat_internal(Some(pid), path)? + }; + if !stat.is_directory || stat.is_symbolic_link { + return Err(KernelError::new( + "ENOTDIR", + format!("not a directory, open '{path}'"), + )); + } + } else if flags & O_NOFOLLOW != 0 && flags & (O_CREAT | O_EXCL) != (O_CREAT | O_EXCL) { + match self.lstat_internal(Some(pid), path) { + Ok(stat) if stat.is_symbolic_link => { + return Err(KernelError::new( + "ELOOP", + format!("symbolic link not followed, open '{path}'"), + )); + } + Ok(_) => {} + Err(error) if error.code() == "ENOENT" && flags & O_CREAT != 0 => {} + Err(error) => return Err(error.into()), + } + } + + Ok(()) + } + + fn reject_unix_socket_data_path(&mut self, path: &str, code: &'static str) -> KernelResult<()> { + if self + .storage_stat(path)? + .is_some_and(|stat| stat.mode & 0o170000 == 0o140000) + { + return Err(KernelError::new( + code, + format!("Unix socket pathname does not support file data I/O, '{path}'"), + )); + } + Ok(()) + } + fn reject_read_only_write_path(&mut self, path: &str) -> KernelResult<()> { if is_proc_path(path) { self.filesystem @@ -3295,7 +5150,7 @@ impl KernelVm { PollTarget::Socket(socket_id) => { let socket = self.sockets.get(socket_id); if let Some(socket) = socket { - if socket.owner_pid() != pid { + if socket.owner_pid() != pid && socket.owner_pid() != 0 { return Err(KernelError::permission_denied(format!( "process {pid} does not own socket {socket_id}" ))); @@ -3343,6 +5198,18 @@ impl KernelVm { entry: &crate::fd_table::FdEntry, requested: PollEvents, ) -> KernelResult { + if let Some(socket_id) = self.fd_socket_id(&entry.description) { + let socket = self + .sockets + .get(socket_id) + .ok_or_else(|| KernelError::bad_file_descriptor(entry.fd))?; + let mut events = self.sockets.poll(socket_id, requested)?; + if events.intersects(POLLOUT) && !self.socket_pollout_has_resource_capacity(&socket) { + events = PollEvents::from_bits(events.bits() & !POLLOUT.bits()); + } + return Ok(events); + } + if self.pipes.is_pipe(entry.description.id()) { return Ok(self.pipes.poll(entry.description.id(), requested)?); } @@ -3382,6 +5249,143 @@ impl KernelVm { .ok_or_else(|| KernelError::bad_file_descriptor(fd)) } + fn fd_socket_id(&self, description: &Arc) -> Option { + lock_or_recover(&self.fd_sockets) + .get(&description.id()) + .filter(|entry| Arc::ptr_eq(&entry.description, description)) + .map(|entry| entry.socket_id) + } + + fn fd_socket_id_for_fd(&self, pid: u32, fd: u32) -> KernelResult { + let description = { + let tables = lock_or_recover(&self.fd_tables); + tables + .get(pid) + .and_then(|table| table.get(fd)) + .map(|entry| Arc::clone(&entry.description)) + .ok_or_else(|| KernelError::bad_file_descriptor(fd))? + }; + self.fd_socket_id(&description) + .ok_or_else(|| KernelError::new("ENOTSOCK", "descriptor is not a socket")) + } + + fn open_file_descriptions(&self) -> Vec> { + let tables = lock_or_recover(&self.fd_tables); + let mut descriptions = BTreeMap::new(); + for pid in tables.pids() { + let Some(table) = tables.get(pid) else { + continue; + }; + for entry in table.values() { + descriptions + .entry(entry.description.id()) + .or_insert_with(|| Arc::clone(&entry.description)); + } + } + descriptions.into_values().collect() + } + + fn prepare_anonymous_file_backing( + &mut self, + path: &str, + stat: Option<&VirtualStat>, + ) -> KernelResult> { + let descriptions = self + .open_file_descriptions() + .into_iter() + .filter(|description| description.is_path_backed_by(path)) + .collect::>(); + if descriptions.is_empty() { + return Ok(None); + } + let stat = match stat { + Some(stat) if !stat.is_directory && !stat.is_symbolic_link => stat.clone(), + _ => return Ok(None), + }; + if stat.nlink > 1 { + if let Some(live_path) = self.find_surviving_hard_link(path, &stat)? { + return Ok(Some(OpenFileRemovalBacking::LinkedAlias { + descriptions, + live_path, + })); + } + } + let data = self.filesystem.read_file(path)?; + let backing: SharedAnonymousFile = Arc::new(Mutex::new(AnonymousFile::new( + data, + stat, + Arc::clone(&self.anonymous_file_usage), + ))); + Ok(Some(OpenFileRemovalBacking::Anonymous { + descriptions, + backing, + })) + } + + fn find_surviving_hard_link( + &mut self, + removed_path: &str, + target: &VirtualStat, + ) -> KernelResult> { + let removed_path = normalize_path(removed_path); + let mut queue = VecDeque::from([(String::from("/"), 0usize)]); + let mut entries = 0usize; + let per_directory_limit = self.resources.max_readdir_entries().unwrap_or(usize::MAX); + + while let Some((directory, depth)) = queue.pop_front() { + self.resources.check_recursive_fs_depth(depth)?; + let names = self + .raw_filesystem_mut() + .read_dir_limited(&directory, per_directory_limit)?; + self.resources.check_readdir_entries(names.len())?; + for name in names { + if matches!(name.as_str(), "." | "..") { + continue; + } + entries = entries.saturating_add(1); + self.resources.check_recursive_fs_entries(entries)?; + let path = join_child_path(&directory, &name); + let stat = match self.raw_filesystem_mut().lstat(&path) { + Ok(stat) => stat, + Err(error) if error.code() == "ENOENT" => continue, + Err(error) => return Err(error.into()), + }; + if path != removed_path && stat.dev == target.dev && stat.ino == target.ino { + return Ok(Some(path)); + } + if stat.is_directory && !stat.is_symbolic_link { + queue.push_back((path, depth.saturating_add(1))); + } + } + } + Ok(None) + } + + fn prepare_detached_directory_backing( + &self, + path: &str, + stat: Option<&VirtualStat>, + ) -> Option<(Vec>, VirtualStat)> { + let stat = stat.filter(|stat| stat.is_directory)?.clone(); + let descriptions = self + .open_file_descriptions() + .into_iter() + .filter(|description| { + description.is_path_backed_by(path) + || description + .lock_target() + .is_some_and(|target| target.ino() == stat.ino) + }) + .collect::>(); + (!descriptions.is_empty()).then_some((descriptions, stat)) + } + + fn rename_open_file_descriptions(&self, old_path: &str, new_path: &str) { + for description in self.open_file_descriptions() { + description.rename_path_prefix(old_path, new_path); + } + } + fn assert_not_terminated(&self) -> KernelResult<()> { if self.terminated { Err(KernelError::disposed()) @@ -3416,6 +5420,7 @@ impl KernelVm { &self.pipes, &self.ptys, &self.sockets, + &self.fd_sockets, self.driver_pids.as_ref(), pid, ); @@ -3485,7 +5490,12 @@ impl KernelVm { format!("permission denied, execute '{path}'"), )); } - if stat.mode & EXECUTABLE_PERMISSION_BITS == 0 { + // Registered command projections are executable kernel objects even + // when their host/package backing blob is stored as 0644. Ordinary + // VFS files must still carry a real execute bit, as Linux requires. + if stat.mode & EXECUTABLE_PERMISSION_BITS == 0 + && self.resolve_registered_command_path(&path).is_none() + { return Err(KernelError::new( "EACCES", format!("permission denied, execute '{path}'"), @@ -3494,6 +5504,45 @@ impl KernelVm { Ok(Some(path)) } + fn validate_wasm_exec_image_inner( + &mut self, + path: &str, + cwd: &str, + interpreter_depth: usize, + ) -> KernelResult<()> { + let resolved = self.validate_executable_path(path, cwd)?; + // `/bin/` and `/__secure_exec/commands/.../` may be + // kernel-owned command stubs whose backing bytes are a self-referential + // launcher rather than the projected WASM blob the runner loads. Once + // the exact path has resolved to a registered command, its trusted + // command driver is the validated executable image; parsing the stub's + // `#!` bytes would incorrectly report ELOOP for paths such as `/bin/sh`. + if self.resolve_registered_command_path(&resolved).is_some() { + return Ok(()); + } + let header = self + .filesystem + .pread(&resolved, 0, SHEBANG_LINE_MAX_BYTES)?; + if header.starts_with(b"\0asm") { + return Ok(()); + } + + let Some(interpreter) = linux_shebang_interpreter(&header, &resolved)? else { + return Err(KernelError::new( + "ENOEXEC", + format!("exec format error: {resolved}"), + )); + }; + if interpreter_depth >= MAX_EXEC_INTERPRETER_DEPTH { + return Err(KernelError::new( + "ELOOP", + format!("too many levels of symbolic links or interpreters: {resolved}"), + )); + } + + self.validate_wasm_exec_image_inner(&interpreter, cwd, interpreter_depth + 1) + } + fn resolve_registered_command_path(&self, path: &str) -> Option { let normalized = normalize_path(path); for prefix in ["/bin/", "/usr/bin/", "/usr/local/bin/"] { @@ -3614,6 +5663,7 @@ impl KernelVm { return self.proc_read_file(current_pid, &proc_node); } + self.reject_unix_socket_data_path(path, "ENXIO")?; Ok(self.filesystem.read_file(path)?) } @@ -4099,9 +6149,10 @@ impl KernelVm { match node { ProcNode::SelfLink { pid } => Ok(format!("/proc/{pid}")), ProcNode::PidCwdLink { pid } => Ok(self.proc_entry(*pid)?.cwd), - ProcNode::PidFdLink { pid, fd } => { - Ok(self.proc_fd_entry(*pid, *fd)?.description.path().to_owned()) - } + ProcNode::PidFdLink { pid, fd } => Ok(self + .proc_fd_entry(*pid, *fd)? + .description + .proc_display_path()), _ => Err(KernelError::new( "EINVAL", format!( @@ -4306,18 +6357,32 @@ impl KernelVm { } fn filesystem_usage(&mut self) -> KernelResult { - if let Some(usage) = self.filesystem_usage_cache.clone() { - return Ok(usage); + if let Some(linked_usage) = self.filesystem_usage_cache.clone() { + return Ok(FileSystemUsage { + total_bytes: linked_usage + .total_bytes + .saturating_add(self.anonymous_file_usage.bytes()), + inode_count: linked_usage + .inode_count + .saturating_add(self.anonymous_file_usage.inodes()), + }); } let filesystem = self.raw_filesystem_mut(); let filesystem_any = filesystem as &mut dyn Any; - let usage = if let Some(mount_table) = filesystem_any.downcast_mut::() { + let linked_usage = if let Some(mount_table) = filesystem_any.downcast_mut::() { mount_table.root_usage()? } else { measure_filesystem_usage(filesystem)? }; - self.filesystem_usage_cache = Some(usage.clone()); - Ok(usage) + self.filesystem_usage_cache = Some(linked_usage.clone()); + Ok(FileSystemUsage { + total_bytes: linked_usage + .total_bytes + .saturating_add(self.anonymous_file_usage.bytes()), + inode_count: linked_usage + .inode_count + .saturating_add(self.anonymous_file_usage.inodes()), + }) } fn invalidate_filesystem_usage_cache(&mut self) { @@ -4645,6 +6710,8 @@ impl KernelVm { &self.file_locks, &self.pipes, &self.ptys, + &self.sockets, + &self.fd_sockets, description, filetype, ); @@ -4799,6 +6866,286 @@ impl DriverProcess for StubDriverProcess { } } +fn unix_socket_absolute_components( + cwd: &str, + path: &str, +) -> KernelResult<(String, VecDeque, bool)> { + if path.is_empty() { + return Err(KernelError::new( + "ENOENT", + "Unix socket pathname must not be empty", + )); + } + if path.as_bytes().contains(&0) { + return Err(KernelError::new( + "EINVAL", + "Unix socket pathname contains a NUL byte", + )); + } + // Linux copies at most PATH_MAX bytes from the caller, including the + // terminating NUL. Check the raw pathname before any `..` processing so a + // long spelling cannot become valid merely by normalizing shorter. + if path.len() >= MAX_PATH_LENGTH { + return Err(KernelError::new( + "ENAMETOOLONG", + format!( + "Unix socket pathname is {} bytes; Linux permits at most {}", + path.len(), + MAX_PATH_LENGTH - 1 + ), + )); + } + if !path.starts_with('/') && !cwd.starts_with('/') { + return Err(KernelError::new( + "EINVAL", + format!("Unix socket cwd must be absolute: {cwd}"), + )); + } + + let absolute = if path.starts_with('/') { + path.to_owned() + } else if cwd == "/" { + format!("/{path}") + } else { + format!("{}/{path}", cwd.trim_end_matches('/')) + }; + let trailing_slash = absolute.len() > 1 && absolute.ends_with('/'); + let components = absolute + .split('/') + .filter(|component| !component.is_empty()) + .map(ToOwned::to_owned) + .collect(); + Ok((absolute, components, trailing_slash)) +} + +fn resolve_unix_socket_components( + filesystem: &mut F, + identity: &ProcessIdentity, + mut remaining: VecDeque, + follow_final_symlink: bool, + mut require_final_directory: bool, +) -> KernelResult { + let mut resolved = Vec::::new(); + let mut followed_symlinks = 0usize; + + if remaining.is_empty() { + let stat = filesystem.stat("/")?; + if require_final_directory && !stat.is_directory { + return Err(KernelError::new("ENOTDIR", "root is not a directory")); + } + return Ok(UnixSocketPathNode { + canonical_path: String::from("/"), + stat, + }); + } + + while let Some(component) = remaining.pop_front() { + let current_path = unix_socket_components_path(&resolved); + let current_stat = filesystem.stat(¤t_path)?; + if !current_stat.is_directory { + return Err(KernelError::new( + "ENOTDIR", + format!("not a directory while resolving Unix socket path: {current_path}"), + )); + } + check_unix_dac( + identity, + ¤t_stat, + UNIX_DAC_SEARCH, + "search", + ¤t_path, + )?; + + match component.as_str() { + "." => continue, + ".." => { + resolved.pop(); + continue; + } + _ => {} + } + + let candidate = join_absolute_path(¤t_path, &component); + let stat = filesystem.lstat(&candidate)?; + let is_final = remaining.is_empty(); + if stat.is_symbolic_link && (!is_final || follow_final_symlink) { + followed_symlinks = followed_symlinks.saturating_add(1); + if followed_symlinks > MAX_UNIX_SOCKET_SYMLINKS { + return Err(KernelError::new( + "ELOOP", + format!("too many symbolic links while resolving '{candidate}'"), + )); + } + let target = filesystem.read_link(&candidate)?; + if target.is_empty() { + return Err(KernelError::new( + "ENOENT", + format!("empty symbolic link target while resolving '{candidate}'"), + )); + } + if target.starts_with('/') { + resolved.clear(); + } + // A slash at the end of a symlink target carries the same + // directory requirement as a slash in the caller's pathname. + // Preserve it when the link supplies the final component. + if target.len() > 1 && target.ends_with('/') && remaining.is_empty() { + require_final_directory = true; + } + let target_components = target + .split('/') + .filter(|target_component| !target_component.is_empty()) + .map(ToOwned::to_owned) + .collect::>(); + for target_component in target_components.into_iter().rev() { + remaining.push_front(target_component); + } + continue; + } + + if is_final { + if require_final_directory && !stat.is_directory { + return Err(KernelError::new( + "ENOTDIR", + format!("not a directory while resolving Unix socket path: {candidate}"), + )); + } + return Ok(UnixSocketPathNode { + canonical_path: candidate, + stat, + }); + } + if !stat.is_directory { + return Err(KernelError::new( + "ENOTDIR", + format!("not a directory while resolving Unix socket path: {candidate}"), + )); + } + resolved.push(component); + } + + let canonical_path = unix_socket_components_path(&resolved); + let stat = filesystem.stat(&canonical_path)?; + if require_final_directory && !stat.is_directory { + return Err(KernelError::new( + "ENOTDIR", + format!("not a directory while resolving Unix socket path: {canonical_path}"), + )); + } + Ok(UnixSocketPathNode { + canonical_path, + stat, + }) +} + +fn unix_socket_components_path(components: &[String]) -> String { + if components.is_empty() { + String::from("/") + } else { + format!("/{}", components.join("/")) + } +} + +fn check_unix_dac( + identity: &ProcessIdentity, + stat: &VirtualStat, + requested: u32, + operation: &str, + path: &str, +) -> KernelResult<()> { + // AgentOS has no fsuid/fsgid or capability mutation. Match Linux's + // ordinary case with euid/egid, and model uid 0 as CAP_DAC_OVERRIDE for + // the write/search checks used by AF_UNIX pathname operations. + if identity.euid == 0 { + return Ok(()); + } + let shift = if identity.euid == stat.uid { + 6 + } else if identity.egid == stat.gid || identity.supplementary_gids.contains(&stat.gid) { + 3 + } else { + 0 + }; + let granted = (stat.mode >> shift) & 0o7; + if granted & requested == requested { + Ok(()) + } else { + Err(KernelError::new( + "EACCES", + format!("permission denied, {operation} Unix socket path '{path}'"), + )) + } +} + +fn validate_chown_request( + identity: &ProcessIdentity, + stat: &VirtualStat, + requested_uid: u32, + requested_gid: u32, + subject: &str, +) -> KernelResult<(u32, u32)> { + const UNCHANGED_ID: u32 = u32::MAX; + let next_uid = if requested_uid == UNCHANGED_ID { + stat.uid + } else { + requested_uid + }; + let next_gid = if requested_gid == UNCHANGED_ID { + stat.gid + } else { + requested_gid + }; + + if identity.euid == 0 || (requested_uid == UNCHANGED_ID && requested_gid == UNCHANGED_ID) { + return Ok((next_uid, next_gid)); + } + if identity.euid != stat.uid { + return Err(KernelError::new( + "EPERM", + format!("operation not permitted, process does not own '{subject}'"), + )); + } + if requested_uid != UNCHANGED_ID && requested_uid != stat.uid { + return Err(KernelError::new( + "EPERM", + format!("operation not permitted, cannot change owner of '{subject}'"), + )); + } + if requested_gid != UNCHANGED_ID + && requested_gid != identity.egid + && !identity.supplementary_gids.contains(&requested_gid) + { + return Err(KernelError::new( + "EPERM", + format!( + "operation not permitted, gid {requested_gid} is not a process group for '{subject}'" + ), + )); + } + Ok((next_uid, next_gid)) +} + +/// Linux clears S_ISUID on a regular file after chown, but preserves S_ISGID +/// when the group-execute bit is clear because that combination represents +/// mandatory-locking metadata rather than set-group-ID execution. +fn linux_chown_cleared_mode(stat: &VirtualStat) -> Option { + if stat.mode & 0o170000 != 0o100000 { + return None; + } + let mut mode = stat.mode & !0o4000; + if stat.mode & 0o0010 != 0 { + mode &= !0o2000; + } + (mode != stat.mode).then_some(mode) +} + +fn unix_socket_address_in_use(path: &str) -> KernelError { + KernelError::new( + "EADDRINUSE", + format!("Unix socket pathname is already in use: {path}"), + ) +} + impl From for KernelError { fn from(error: VfsError) -> Self { map_error(error.code(), error.to_string()) @@ -4883,6 +7230,8 @@ impl From for KernelError { fn map_dns_resolver_error(error: crate::dns::DnsResolverError) -> KernelError { let code = match error.kind() { DnsResolverErrorKind::InvalidInput => "EINVAL", + DnsResolverErrorKind::NxDomain => "ENOENT", + DnsResolverErrorKind::NoData => "ENODATA", DnsResolverErrorKind::LookupFailed => "EHOSTUNREACH", }; map_error(code, error.to_string()) @@ -4973,6 +7322,16 @@ fn parent_path(path: &str) -> String { } } +fn required_dirent_ino(path: &str, ino: u64) -> KernelResult { + if ino == 0 { + return Err(KernelError::new( + "EIO", + format!("filesystem returned an invalid zero inode for directory entry {path}"), + )); + } + Ok(ino) +} + fn join_absolute_path(parent: &str, child: &str) -> String { if parent == "/" { format!("/{child}") @@ -5023,6 +7382,8 @@ fn checked_write_end(offset: u64, len: usize) -> KernelResult { fn filetype_for_path(path: &str, stat: &VirtualStat) -> u8 { if stat.is_directory { FILETYPE_DIRECTORY + } else if stat.mode & 0o170000 == 0o140000 { + FILETYPE_SOCKET_STREAM } else if path.starts_with("/dev/") { FILETYPE_CHARACTER_DEVICE } else if stat.is_symbolic_link { @@ -5033,12 +7394,16 @@ fn filetype_for_path(path: &str, stat: &VirtualStat) -> u8 { } fn synthetic_character_device_stat(ino: u64) -> VirtualStat { + synthetic_special_file_stat(ino, 0o020666, 2) +} + +fn synthetic_special_file_stat(ino: u64, mode: u32, dev: u64) -> VirtualStat { let now = now_ms(); VirtualStat { - mode: 0o666, + mode, size: 0, blocks: 0, - dev: 2, + dev, rdev: 0, is_directory: false, is_symbolic_link: false, @@ -5209,10 +7574,373 @@ impl Drop for KernelVm { #[cfg(test)] mod tests { use super::*; + use crate::fd_table::{FD_CLOEXEC, F_GETFD, F_SETFD, O_RDONLY}; + use crate::process_table::SIGTERM; use crate::vfs::MemoryFileSystem; use std::panic::{catch_unwind, AssertUnwindSafe}; use std::thread; + fn kernel_with_process() -> (KernelVm, KernelProcessHandle) { + let mut config = KernelVmConfig::new("vm-fd-socket-test"); + config.permissions = Permissions::allow_all(); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel + .register_driver(CommandDriver::new("wasm", ["socket-test"])) + .expect("register wasm driver"); + let process = kernel + .spawn_process( + "socket-test", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from("wasm")), + ..SpawnOptions::default() + }, + ) + .expect("spawn socket test process"); + (kernel, process) + } + + #[test] + fn fd_socketpair_preserves_messages_and_transfers_descriptions() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + let (left, right) = kernel + .fd_socketpair("wasm", pid, SocketType::Stream, true, false) + .expect("create stream socketpair"); + assert_ne!(left, right); + assert_eq!( + kernel + .fd_stat("wasm", pid, left) + .expect("stat socket") + .filetype, + FILETYPE_SOCKET_STREAM + ); + assert_ne!( + kernel + .fd_stat("wasm", pid, left) + .expect("stat socket") + .flags + & O_NONBLOCK, + 0 + ); + + kernel + .fd_write("wasm", pid, left, b"hello") + .expect("write socketpair"); + assert_eq!( + kernel + .fd_read("wasm", pid, right, 32) + .expect("read socketpair"), + b"hello" + ); + + let (pipe_read, pipe_write) = kernel.open_pipe("wasm", pid).expect("open pipe"); + kernel + .fd_socket_sendmsg("wasm", pid, left, b"x", &[pipe_read]) + .expect("send pipe description"); + let received = kernel + .fd_socket_recvmsg("wasm", pid, right, 1, 1, true, false, false, false) + .expect("receive rights") + .expect("message available"); + assert_eq!(received.payload, b"x"); + assert!(!received.payload_truncated); + assert!(!received.control_truncated); + assert_eq!(received.rights.len(), 1); + let passed_read = match received.rights[0] { + ReceivedFdRight::Fd(fd) => fd, + ReceivedFdRight::Opaque(_) => panic!("expected transferred pipe fd"), + }; + assert_eq!( + kernel + .fd_fcntl("wasm", pid, passed_read, F_GETFD, 0) + .expect("get received fd flags"), + FD_CLOEXEC + ); + + kernel + .fd_close("wasm", pid, pipe_read) + .expect("close original pipe read end"); + kernel + .fd_write("wasm", pid, pipe_write, b"through-rights") + .expect("write pipe"); + assert_eq!( + kernel + .fd_read("wasm", pid, passed_read, 64) + .expect("read transferred pipe"), + b"through-rights" + ); + + for fd in [passed_read, pipe_write, left, right] { + kernel.fd_close("wasm", pid, fd).expect("close fd"); + } + assert_eq!(kernel.sockets.snapshot().sockets, 0); + assert!(lock_or_recover(&kernel.fd_sockets).is_empty()); + } + + #[test] + fn closed_socket_identity_cannot_poison_reused_regular_fd() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + let (left, right) = kernel + .fd_socketpair("wasm", pid, SocketType::Stream, false, false) + .expect("create socketpair"); + let stale_socket_entry = { + let tables = lock_or_recover(&kernel.fd_tables); + let description = &tables.get(pid).unwrap().get(left).unwrap().description; + lock_or_recover(&kernel.fd_sockets) + .get(&description.id()) + .cloned() + .expect("registered socket description") + }; + kernel + .fd_close("wasm", pid, left) + .expect("close left socket"); + kernel + .fd_close("wasm", pid, right) + .expect("close right socket"); + assert!(lock_or_recover(&kernel.fd_sockets).is_empty()); + + kernel + .write_file("/regular", b"regular-data".to_vec()) + .expect("seed regular file"); + let regular_fd = kernel + .fd_open("wasm", pid, "/regular", O_RDONLY, None) + .expect("open regular file after socket close"); + let regular_description = { + let tables = lock_or_recover(&kernel.fd_tables); + Arc::clone( + &tables + .get(pid) + .unwrap() + .get(regular_fd) + .unwrap() + .description, + ) + }; + assert_ne!( + regular_description.id(), + stale_socket_entry.description.id(), + "open-file-description ids must be globally unique" + ); + + // Defense in depth: even a corrupt/stale numeric registry key cannot + // classify a different description as a socket. + lock_or_recover(&kernel.fd_sockets).insert(regular_description.id(), stale_socket_entry); + assert_eq!( + kernel + .fd_read("wasm", pid, regular_fd, 32) + .expect("read regular file despite stale socket key"), + b"regular-data" + ); + kernel + .fd_close("wasm", pid, regular_fd) + .expect("close regular file"); + assert!(lock_or_recover(&kernel.fd_sockets).is_empty()); + } + + #[test] + fn fd_socketpair_datagram_reads_one_truncated_message_at_a_time() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + let (left, right) = kernel + .fd_socketpair("wasm", pid, SocketType::Datagram, false, false) + .expect("create datagram socketpair"); + kernel + .fd_write("wasm", pid, left, b"abcd") + .expect("write first datagram"); + kernel + .fd_write("wasm", pid, left, b"ef") + .expect("write second datagram"); + let truncated = kernel + .fd_socket_recvmsg("wasm", pid, right, 2, 0, false, false, false, false) + .unwrap() + .unwrap(); + assert_eq!(truncated.payload, b"ab"); + assert!(truncated.payload_truncated); + assert_eq!(truncated.full_length, 4); + assert_eq!(kernel.fd_read("wasm", pid, right, 8).unwrap(), b"ef"); + } + + #[test] + fn fd_socketpair_peek_duplicates_rights_without_consuming_message() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + let (left, right) = kernel + .fd_socketpair("wasm", pid, SocketType::Stream, false, false) + .unwrap(); + let (pipe_read, pipe_write) = kernel.open_pipe("wasm", pid).unwrap(); + kernel + .fd_socket_sendmsg("wasm", pid, left, b"hello", &[pipe_read]) + .unwrap(); + + let peeked = kernel + .fd_socket_recvmsg("wasm", pid, right, 2, 1, false, true, false, false) + .unwrap() + .unwrap(); + assert_eq!(peeked.payload, b"he"); + assert_eq!(peeked.full_length, 2); + let peeked_fd = match peeked.rights[0] { + ReceivedFdRight::Fd(fd) => fd, + ReceivedFdRight::Opaque(_) => panic!("expected fd right"), + }; + + let consumed = kernel + .fd_socket_recvmsg("wasm", pid, right, 5, 1, false, false, false, true) + .unwrap() + .unwrap(); + assert_eq!(consumed.payload, b"hello"); + let consumed_fd = match consumed.rights[0] { + ReceivedFdRight::Fd(fd) => fd, + ReceivedFdRight::Opaque(_) => panic!("expected fd right"), + }; + kernel.fd_close("wasm", pid, pipe_read).unwrap(); + kernel.fd_write("wasm", pid, pipe_write, b"ab").unwrap(); + assert_eq!(kernel.fd_read("wasm", pid, peeked_fd, 1).unwrap(), b"a"); + assert_eq!(kernel.fd_read("wasm", pid, consumed_fd, 1).unwrap(), b"b"); + for fd in [peeked_fd, consumed_fd, pipe_write, left, right] { + kernel.fd_close("wasm", pid, fd).unwrap(); + } + } + + #[test] + fn exact_fd_transfer_install_preserves_description_identity_and_offset() { + let (mut kernel, parent) = kernel_with_process(); + let parent_pid = parent.pid(); + kernel.write_file("/shared", b"abc").unwrap(); + let source_fd = kernel + .fd_open("wasm", parent_pid, "/shared", O_RDONLY, None) + .unwrap(); + kernel.fd_seek("wasm", parent_pid, source_fd, 1, 0).unwrap(); + let transfer = kernel.fd_transfer("wasm", parent_pid, source_fd).unwrap(); + + let child = kernel + .spawn_process( + "socket-test", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from("wasm")), + parent_pid: Some(parent_pid), + ..SpawnOptions::default() + }, + ) + .unwrap(); + kernel + .fd_install_transfer_at("wasm", child.pid(), 9, 0, &transfer) + .unwrap(); + assert_eq!( + kernel + .fd_transfer("wasm", child.pid(), 9) + .unwrap() + .description_id(), + transfer.description_id() + ); + assert_eq!(kernel.fd_read("wasm", child.pid(), 9, 1).unwrap(), b"b"); + assert_eq!( + kernel.fd_read("wasm", parent_pid, source_fd, 1).unwrap(), + b"c" + ); + + child.finish(0); + parent.finish(0); + kernel.waitpid(child.pid()).unwrap(); + kernel.waitpid(parent_pid).unwrap(); + } + + #[test] + fn dev_fd_stat_reports_linux_pipe_and_socket_modes() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + let (pipe_read, pipe_write) = kernel.open_pipe("wasm", pid).unwrap(); + let (socket_left, socket_right) = kernel + .fd_socketpair("wasm", pid, SocketType::Stream, false, false) + .unwrap(); + + for fd in [pipe_read, pipe_write] { + let stat = kernel.dev_fd_stat("wasm", pid, fd).unwrap(); + assert_eq!(stat.mode, 0o010600); + assert_eq!(stat.nlink, 1); + assert_eq!(stat.size, 0); + } + let read_stat = kernel.dev_fd_stat("wasm", pid, pipe_read).unwrap(); + let write_stat = kernel.dev_fd_stat("wasm", pid, pipe_write).unwrap(); + assert_eq!(read_stat.dev, write_stat.dev); + assert_eq!(read_stat.ino, write_stat.ino); + for fd in [socket_left, socket_right] { + let stat = kernel.dev_fd_stat("wasm", pid, fd).unwrap(); + assert_eq!(stat.mode, 0o140777); + assert_eq!(stat.nlink, 1); + assert_eq!(stat.size, 0); + } + } + + #[test] + fn adopted_kernel_socket_lives_while_queued_transfer_guard_exists() { + let (mut kernel, process) = kernel_with_process(); + let pid = process.pid(); + let socket_id = kernel + .socket_create("wasm", pid, SocketSpec::tcp()) + .expect("create transferable socket"); + let guard = kernel + .fd_adopt_socket_transfer("wasm", pid, socket_id, O_NONBLOCK) + .expect("retain socket description"); + assert_eq!( + kernel.sockets.get(socket_id).unwrap().owner_pid(), + 0, + "description-owned sockets must survive sender process cleanup" + ); + + let (left, right) = kernel + .fd_socketpair("wasm", pid, SocketType::Stream, false, false) + .expect("create rights channel"); + kernel + .fd_socket_sendmsg_transfers( + "wasm", + pid, + left, + b"x", + &[FdTransferRequest::Opaque(Arc::new(guard))], + ) + .expect("queue socket guard"); + kernel.fd_close("wasm", pid, left).unwrap(); + kernel.fd_close("wasm", pid, right).unwrap(); + assert!( + kernel.sockets.get(socket_id).is_none(), + "discarding the queued right must release and prune the adopted socket" + ); + } + + #[test] + fn adopting_socket_for_transfer_does_not_require_a_free_sender_fd() { + let mut config = KernelVmConfig::new("vm-full-fd-transfer-test"); + config.permissions = Permissions::allow_all(); + config.resources.max_open_fds = Some(3); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel + .register_driver(CommandDriver::new("wasm", ["socket-test"])) + .unwrap(); + let process = kernel + .spawn_process( + "socket-test", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from("wasm")), + ..SpawnOptions::default() + }, + ) + .unwrap(); + let pid = process.pid(); + let socket_id = kernel + .socket_create("wasm", pid, SocketSpec::tcp()) + .unwrap(); + + let guard = kernel + .fd_adopt_socket_transfer("wasm", pid, socket_id, 0) + .expect("SCM_RIGHTS must not need a temporary fd in a full sender table"); + assert_eq!(kernel.fd_snapshot("wasm", pid).unwrap().len(), 3); + assert_eq!(kernel.sockets.get(socket_id).unwrap().owner_pid(), 0); + drop(guard); + } + struct RetainedKernelResources { process: KernelProcessHandle, fd_tables: Arc>, @@ -5272,6 +8000,344 @@ mod tests { KernelVm::new(MemoryFileSystem::new(), config) } + #[test] + fn exec_process_replaces_image_without_replacing_linux_process_state() { + let mut config = KernelVmConfig::new("vm-exec-process-state"); + config.permissions = Permissions::allow_all(); + config.env = BTreeMap::from([(String::from("INHERITED"), String::from("old"))]); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel + .register_driver(CommandDriver::new("runtime", ["old", "new"])) + .expect("register runtime commands"); + + let parent = kernel + .spawn_process( + "old", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from("runtime")), + ..SpawnOptions::default() + }, + ) + .expect("spawn parent"); + let child = kernel + .spawn_process( + "old", + vec![String::from("old-argv")], + SpawnOptions { + requester_driver: Some(String::from("runtime")), + parent_pid: Some(parent.pid()), + cwd: Some(String::from("/before-exec")), + env: BTreeMap::from([(String::from("STALE"), String::from("value"))]), + }, + ) + .expect("spawn child"); + kernel + .setpgid("runtime", child.pid(), child.pid()) + .expect("put child in its own process group"); + kernel + .umask("runtime", child.pid(), Some(0o077)) + .expect("set process umask"); + let blocked = SignalSet::from_signal(SIGTERM).expect("signal set"); + kernel + .sigprocmask("runtime", child.pid(), SigmaskHow::Block, blocked) + .expect("block signal"); + kernel + .kill_process("runtime", child.pid(), SIGTERM) + .expect("queue blocked signal"); + + let (preserved_fd, cloexec_fd) = kernel + .open_pipe("runtime", child.pid()) + .expect("open process pipe"); + kernel + .fd_fcntl("runtime", child.pid(), cloexec_fd, F_SETFD, FD_CLOEXEC) + .expect("mark close-on-exec fd"); + let (forwarded_cloexec_fd, forwarded_peer_fd) = kernel + .open_pipe("runtime", child.pid()) + .expect("open runner-forwarded process pipe"); + + let before = kernel.processes.get(child.pid()).expect("child entry"); + let replacement_env = BTreeMap::from([(String::from("ONLY"), String::from("new"))]); + kernel + .mkdir("/literal", true) + .expect("create literal executable directory"); + kernel + .write_file("/literal/not-executable", b"wasm".to_vec()) + .expect("create non-executable replacement"); + kernel + .chmod("/literal/not-executable", 0o644) + .expect("clear replacement execute bits"); + let error = kernel + .exec_process_retaining_internal_fds( + "runtime", + child.pid(), + "new", + vec![String::new(), String::from("argument")], + replacement_env.clone(), + String::from("/must-not-change-cwd"), + &[], + &[forwarded_cloexec_fd], + Some("/literal/not-executable"), + ) + .expect_err("pathname validation must fail before exec commits"); + assert_eq!(error.code(), "EACCES"); + assert_eq!( + kernel.processes.get(child.pid()).expect("child entry"), + before, + "a pre-commit failure must not change process metadata" + ); + kernel + .fd_stat("runtime", child.pid(), cloexec_fd) + .expect("a pre-commit failure must not close CLOEXEC descriptors"); + kernel + .fd_stat("runtime", child.pid(), forwarded_cloexec_fd) + .expect("a pre-commit failure must not close forwarded descriptors"); + kernel + .write_file("/literal/new", b"wasm".to_vec()) + .expect("create executable replacement"); + kernel + .chmod("/literal/new", 0o755) + .expect("mark replacement executable"); + kernel + .exec_process_retaining_internal_fds( + "runtime", + child.pid(), + "new", + vec![String::new(), String::from("argument")], + replacement_env.clone(), + String::from("/must-not-change-cwd"), + &[], + &[forwarded_cloexec_fd], + Some("/literal/new"), + ) + .expect("replace process image"); + + let after = kernel.processes.get(child.pid()).expect("exec child entry"); + assert_eq!(after.pid, before.pid); + assert_eq!(after.ppid, before.ppid); + assert_eq!(after.pgid, before.pgid); + assert_eq!(after.sid, before.sid); + assert_eq!(after.identity, before.identity); + assert_eq!(after.umask, before.umask); + assert_eq!(after.cwd, before.cwd, "execve must preserve cwd"); + assert_eq!(after.command, ""); + assert_eq!(after.args, vec![String::from("argument")]); + assert_eq!( + kernel + .read_file_for_process( + "runtime", + child.pid(), + &format!("/proc/{}/cmdline", child.pid()), + ) + .expect("read post-exec cmdline"), + b"\0argument\0".to_vec(), + "procfs cmdline must contain argv exactly once, including empty argv0" + ); + assert_eq!(after.env, replacement_env, "envp must replace, not overlay"); + assert_eq!( + kernel + .sigprocmask( + "runtime", + child.pid(), + SigmaskHow::Block, + SignalSet::empty(), + ) + .expect("read signal mask"), + blocked, + "blocked signal mask must survive exec" + ); + assert!( + kernel + .sigpending("runtime", child.pid()) + .expect("read pending signals") + .contains(SIGTERM), + "pending signals must survive exec" + ); + kernel + .fd_stat("runtime", child.pid(), preserved_fd) + .expect("non-CLOEXEC fd must survive"); + assert_eq!( + kernel + .fd_stat("runtime", child.pid(), cloexec_fd) + .expect_err("CLOEXEC fd must close") + .code(), + "EBADF" + ); + assert_eq!( + kernel + .fd_stat("runtime", child.pid(), forwarded_cloexec_fd) + .expect_err("runner-forwarded CLOEXEC fd must close") + .code(), + "EBADF" + ); + kernel + .fd_stat("runtime", child.pid(), forwarded_peer_fd) + .expect("unmarked peer of runner-forwarded fd must survive"); + } + + #[test] + fn validate_executable_path_matches_linux_path_errors_and_symlinks() { + let mut config = KernelVmConfig::new("vm-exec-path-errors"); + config.permissions = Permissions::allow_all(); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + + assert_eq!( + kernel + .validate_executable_path("/missing", "/") + .expect_err("missing image must fail") + .code(), + "ENOENT" + ); + + kernel + .write_file("/plain", b"data".to_vec()) + .expect("create plain file"); + assert_eq!( + kernel + .validate_executable_path("/plain/child", "/") + .expect_err("non-directory path component must fail") + .code(), + "ENOTDIR" + ); + + kernel.mkdir("/directory", true).expect("create directory"); + assert_eq!( + kernel + .validate_executable_path("/directory", "/") + .expect_err("directory image must fail") + .code(), + "EACCES" + ); + assert_eq!( + kernel + .validate_executable_path("/plain", "/") + .expect_err("non-executable image must fail") + .code(), + "EACCES" + ); + + kernel + .chmod("/plain", 0o755) + .expect("mark target executable"); + kernel + .symlink("/plain", "/image-link") + .expect("create executable symlink"); + assert_eq!( + kernel + .validate_executable_path("/image-link", "/") + .expect("exec must follow final symlink"), + "/plain" + ); + + kernel + .symlink("/loop-b", "/loop-a") + .expect("create first loop link"); + kernel + .symlink("/loop-a", "/loop-b") + .expect("create second loop link"); + assert_eq!( + kernel + .validate_executable_path("/loop-a", "/") + .expect_err("symlink loop must fail") + .code(), + "ELOOP" + ); + } + + #[test] + fn validate_wasm_exec_image_follows_linux_shebang_chain() { + let mut config = KernelVmConfig::new("vm-exec-shebang"); + config.permissions = Permissions::allow_all(); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + + kernel + .register_driver(CommandDriver::new("shell", ["sh"])) + .expect("register projected shell command"); + kernel + .mkdir("/bin", true) + .expect("create command directory"); + kernel + .write_file("/bin/sh", b"#!/bin/sh\n".to_vec()) + .expect("write self-referential registered command stub"); + kernel + .write_file("/registered-script", b"#!/bin/sh\n".to_vec()) + .expect("write registered-interpreter script"); + kernel + .chmod("/registered-script", 0o755) + .expect("mark registered-interpreter script executable"); + kernel + .validate_wasm_exec_image("/registered-script", "/") + .expect("registered command stub must resolve as a runtime image"); + + kernel + .write_file("/interpreter.wasm", b"\0asm\x01\0\0\0".to_vec()) + .expect("write WASM interpreter"); + kernel + .chmod("/interpreter.wasm", 0o755) + .expect("mark interpreter executable"); + kernel + .write_file( + "/script", + b"#!/interpreter.wasm one optional argument\necho ignored\n".to_vec(), + ) + .expect("write executable script"); + kernel + .chmod("/script", 0o755) + .expect("mark script executable"); + kernel + .validate_wasm_exec_image("/script", "/") + .expect("WASM interpreter chain must validate"); + + kernel + .write_file("/missing-interpreter", b"#!/absent\n".to_vec()) + .expect("write missing-interpreter script"); + kernel + .chmod("/missing-interpreter", 0o755) + .expect("mark missing-interpreter script executable"); + assert_eq!( + kernel + .validate_wasm_exec_image("/missing-interpreter", "/") + .expect_err("missing interpreter must fail") + .code(), + "ENOENT" + ); + + kernel + .write_file("/not-executable", b"\0asm\x01\0\0\0".to_vec()) + .expect("write non-executable interpreter"); + kernel + .write_file("/denied-script", b"#!/not-executable\n".to_vec()) + .expect("write denied-interpreter script"); + kernel + .chmod("/denied-script", 0o755) + .expect("mark denied-interpreter script executable"); + assert_eq!( + kernel + .validate_wasm_exec_image("/denied-script", "/") + .expect_err("non-executable interpreter must fail") + .code(), + "EACCES" + ); + + for depth in 0..=MAX_EXEC_INTERPRETER_DEPTH { + let path = format!("/recursive-{depth}"); + let next = format!("/recursive-{}", depth + 1); + kernel + .write_file(&path, format!("#!{next}\n").into_bytes()) + .expect("write recursive interpreter"); + kernel + .chmod(&path, 0o755) + .expect("mark recursive interpreter executable"); + } + assert_eq!( + kernel + .validate_wasm_exec_image("/recursive-0", "/") + .expect_err("interpreter recursion must be bounded") + .code(), + "ELOOP" + ); + } + #[test] fn recursive_copy_preserves_tree_metadata_and_symlinks() { let mut kernel = recursive_fs_kernel(); @@ -5510,6 +8576,8 @@ mod tests { pending_signals: SignalSet::empty(), }, None, + None, + false, ) .expect("create virtual process"); let mask = @@ -5543,6 +8611,7 @@ mod tests { let pipes = PipeManager::new(); let ptys = PtyManager::new(); let sockets = SocketTable::new(); + let fd_sockets = Arc::new(Mutex::new(BTreeMap::new())); let driver_pids = Arc::new(Mutex::new(BTreeMap::from([( String::from("driver"), BTreeSet::from([41]), @@ -5590,6 +8659,7 @@ mod tests { &pipes_for_cleanup, &ptys, &sockets, + &fd_sockets, driver_pids_for_cleanup.as_ref(), 41, ); diff --git a/crates/kernel/src/permissions.rs b/crates/kernel/src/permissions.rs index 9dd2a389a9..7a4a5faa3f 100644 --- a/crates/kernel/src/permissions.rs +++ b/crates/kernel/src/permissions.rs @@ -634,6 +634,23 @@ impl VirtualFileSystem for PermissionedFileSystem { self.inner.chown(path, uid, gid) } + fn chown_spec( + &mut self, + path: &str, + uid: u32, + gid: u32, + follow_symlinks: bool, + ) -> VfsResult<()> { + if follow_symlinks { + self.check_subject(FsOperation::Chown, path)?; + } else { + validate_path(path)?; + let subject = self.resolved_destination_path(path)?; + self.check(FsOperation::Chown, &subject)?; + } + self.inner.chown_spec(path, uid, gid, follow_symlinks) + } + fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> { self.check_subject(FsOperation::Utimes, path)?; self.inner.utimes(path, atime_ms, mtime_ms) diff --git a/crates/kernel/src/pipe_manager.rs b/crates/kernel/src/pipe_manager.rs index 3a59b861ac..e09beb8051 100644 --- a/crates/kernel/src/pipe_manager.rs +++ b/crates/kernel/src/pipe_manager.rs @@ -1,6 +1,6 @@ use crate::fd_table::{ - FdResult, FileDescription, ProcessFdTable, SharedFileDescription, FILETYPE_PIPE, O_RDONLY, - O_WRONLY, + allocate_file_description_id, FdResult, FileDescription, ProcessFdTable, SharedFileDescription, + FILETYPE_PIPE, O_RDONLY, O_WRONLY, }; use crate::poll::{PollEvents, PollNotifier, POLLERR, POLLHUP, POLLIN, POLLOUT}; use std::collections::{BTreeMap, VecDeque}; @@ -86,12 +86,36 @@ struct PendingRead { result: Option>>, } -#[derive(Debug, Default)] +#[derive(Debug)] struct PipeState { buffer: VecDeque>, closed_read: bool, closed_write: bool, waiting_reads: VecDeque, + mode: u32, + uid: u32, + gid: u32, +} + +impl Default for PipeState { + fn default() -> Self { + Self { + buffer: VecDeque::new(), + closed_read: false, + closed_write: false, + waiting_reads: VecDeque::new(), + mode: 0o600, + uid: 0, + gid: 0, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PipeMetadata { + pub mode: u32, + pub uid: u32, + pub gid: u32, } #[derive(Debug)] @@ -100,7 +124,6 @@ struct PipeManagerState { desc_to_pipe: BTreeMap, waiters: BTreeMap, next_pipe_id: u64, - next_desc_id: u64, next_waiter_id: u64, } @@ -111,7 +134,6 @@ impl Default for PipeManagerState { desc_to_pipe: BTreeMap::new(), waiters: BTreeMap::new(), next_pipe_id: 1, - next_desc_id: 100_000, next_waiter_id: 1, } } @@ -158,10 +180,8 @@ impl PipeManager { let pipe_id = state.next_pipe_id; state.next_pipe_id += 1; - let read_id = state.next_desc_id; - state.next_desc_id += 1; - let write_id = state.next_desc_id; - state.next_desc_id += 1; + let read_id = allocate_file_description_id(); + let write_id = allocate_file_description_id(); state.pipes.insert(pipe_id, PipeState::default()); state.desc_to_pipe.insert( @@ -506,6 +526,48 @@ impl PipeManager { .map(|pipe_ref| pipe_ref.pipe_id) } + pub fn metadata(&self, description_id: u64) -> Option { + let state = lock_or_recover(&self.inner.state); + let pipe_id = state.desc_to_pipe.get(&description_id)?.pipe_id; + let pipe = state.pipes.get(&pipe_id)?; + Some(PipeMetadata { + mode: pipe.mode, + uid: pipe.uid, + gid: pipe.gid, + }) + } + + pub fn set_owner(&self, description_id: u64, uid: u32, gid: u32) -> PipeResult<()> { + let mut state = lock_or_recover(&self.inner.state); + let pipe_id = state + .desc_to_pipe + .get(&description_id) + .ok_or_else(|| PipeError::bad_file_descriptor("not a pipe end"))? + .pipe_id; + let pipe = state + .pipes + .get_mut(&pipe_id) + .ok_or_else(|| PipeError::bad_file_descriptor("pipe not found"))?; + pipe.uid = uid; + pipe.gid = gid; + Ok(()) + } + + pub fn chmod(&self, description_id: u64, mode: u32) -> PipeResult<()> { + let mut state = lock_or_recover(&self.inner.state); + let pipe_id = state + .desc_to_pipe + .get(&description_id) + .ok_or_else(|| PipeError::bad_file_descriptor("not a pipe end"))? + .pipe_id; + let pipe = state + .pipes + .get_mut(&pipe_id) + .ok_or_else(|| PipeError::bad_file_descriptor("pipe not found"))?; + pipe.mode = mode & 0o7777; + Ok(()) + } + pub fn pipe_count(&self) -> usize { lock_or_recover(&self.inner.state).pipes.len() } diff --git a/crates/kernel/src/process_table.rs b/crates/kernel/src/process_table.rs index a0459973e2..ef08897110 100644 --- a/crates/kernel/src/process_table.rs +++ b/crates/kernel/src/process_table.rs @@ -441,21 +441,56 @@ impl ProcessTable { ctx: ProcessContext, driver_process: Arc, ) -> ProcessEntry { - let (pgid, sid) = { - let state = self.inner.lock_state(); - match state.entries.get(&ctx.ppid) { - Some(parent) => (parent.entry.pgid, parent.entry.sid), - None => (pid, pid), - } + self.register_with_process_group(pid, driver, command, args, ctx, driver_process, None) + .expect("inheriting a process group cannot fail") + } + + pub fn register_with_process_group( + &self, + pid: u32, + driver: impl Into, + command: impl Into, + args: Vec, + ctx: ProcessContext, + driver_process: Arc, + requested_pgid: Option, + ) -> ProcessResult { + let driver = driver.into(); + let command = command.into(); + let mut state = self.inner.lock_state(); + let (inherited_pgid, sid) = match state.entries.get(&ctx.ppid) { + Some(parent) => (parent.entry.pgid, parent.entry.sid), + None => (pid, pid), }; + let pgid = requested_pgid.map_or(inherited_pgid, |pgid| if pgid == 0 { pid } else { pgid }); + if requested_pgid.is_some() && pgid != pid { + let mut group_exists = false; + for record in state.entries.values() { + if record.entry.pgid != pgid || record.entry.status == ProcessStatus::Exited { + continue; + } + if record.entry.sid != sid { + return Err(ProcessTableError::permission_denied( + "cannot join process group in different session", + )); + } + group_exists = true; + break; + } + if !group_exists { + return Err(ProcessTableError::permission_denied(format!( + "no such process group {pgid}" + ))); + } + } let entry = ProcessEntry { pid, ppid: ctx.ppid, pgid, sid, - driver: driver.into(), - command: command.into(), + driver, + command, args, status: ProcessStatus::Running, exit_code: None, @@ -466,27 +501,27 @@ impl ProcessTable { identity: ctx.identity, }; - let weak = Arc::downgrade(&self.inner); - driver_process.set_on_exit(Arc::new(move |code| { - if let Some(inner) = weak.upgrade() { - mark_exited_inner(&inner, pid, code); - } - })); - - let mut state = self.inner.lock_state(); state.next_pid = next_pid_after_registered(state.next_pid, pid); state.entries.insert( pid, ProcessRecord { entry: entry.clone(), - driver_process, + driver_process: driver_process.clone(), pending_wait_events: VecDeque::new(), blocked_signals: ctx.blocked_signals, pending_signals: ctx.pending_signals, }, ); + drop(state); + + let weak = Arc::downgrade(&self.inner); + driver_process.set_on_exit(Arc::new(move |code| { + if let Some(inner) = weak.upgrade() { + mark_exited_inner(&inner, pid, code); + } + })); - entry + Ok(entry) } pub fn get(&self, pid: u32) -> Option { @@ -497,6 +532,38 @@ impl ProcessTable { .map(|record| record.entry.clone()) } + /// Replace the userspace image metadata while retaining Linux process + /// identity (PID/PPID/PGID/SID), wait relationships, signal mask, pending + /// signals, and the driver process used to report the eventual exit. + pub fn exec( + &self, + pid: u32, + driver: impl Into, + command: impl Into, + args: Vec, + env: BTreeMap, + cwd: String, + ) -> ProcessResult<()> { + let mut state = self.inner.lock_state(); + let record = state + .entries + .get_mut(&pid) + .ok_or_else(|| ProcessTableError::no_such_process(pid))?; + if record.entry.status == ProcessStatus::Exited { + return Err(ProcessTableError::no_such_process(pid)); + } + record.entry.driver = driver.into(); + record.entry.command = command.into(); + record.entry.args = args; + record.entry.env = env; + record.entry.cwd = cwd; + record.entry.status = ProcessStatus::Running; + record.entry.exit_code = None; + record.entry.exit_time_ms = None; + self.inner.waiters.notify_all(); + Ok(()) + } + pub fn zombie_timer_count(&self) -> usize { self.reap_due_zombies(); self.inner.reaper.scheduled_count() @@ -606,6 +673,45 @@ impl ProcessTable { } } + /// Consume one waitable stopped/continued transition without observing or + /// reaping terminal state. Sidecar child-process bridges use this while + /// terminal reaping remains coupled to stdout/stderr EOF delivery. + pub fn take_nonterminal_wait_event_for( + &self, + waiter_pid: u32, + pid: i32, + flags: WaitPidFlags, + ) -> ProcessResult> { + let mut state = self.inner.lock_state(); + let selector = resolve_wait_selector(&state, waiter_pid, pid)?; + let matching_children = matching_child_pids(&state, waiter_pid, selector); + if matching_children.is_empty() { + return Err(ProcessTableError::no_matching_child(waiter_pid, pid)); + } + + for child_pid in matching_children { + let Some(record) = state.entries.get_mut(&child_pid) else { + continue; + }; + let Some(index) = record.pending_wait_events.iter().position(|event| { + event.event != ProcessWaitEvent::Exited && is_waitable_event(event.event, flags) + }) else { + continue; + }; + let event = record + .pending_wait_events + .remove(index) + .expect("pending nonterminal wait event should exist"); + return Ok(Some(ProcessWaitResult { + pid: child_pid, + status: event.status, + event: event.event, + })); + } + + Ok(None) + } + pub fn kill(&self, pid: i32, signal: i32) -> ProcessResult<()> { if !(0..=MAX_SIGNAL).contains(&signal) { return Err(ProcessTableError::invalid_signal(signal)); @@ -1536,6 +1642,20 @@ mod tests { } } + struct AlreadyExitedDriverProcess(i32); + + impl DriverProcess for AlreadyExitedDriverProcess { + fn kill(&self, _signal: i32) {} + + fn wait(&self, _timeout: Duration) -> Option { + Some(self.0) + } + + fn set_on_exit(&self, callback: ProcessExitCallback) { + callback(self.0); + } + } + fn context(ppid: u32) -> ProcessContext { ProcessContext { ppid, @@ -1543,6 +1663,101 @@ mod tests { } } + #[test] + fn register_accepts_synchronous_already_exited_callback() { + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + table.register( + 10, + "test", + "already-exited", + Vec::new(), + context(0), + Arc::new(AlreadyExitedDriverProcess(27)), + ); + + let entry = table.get(10).expect("registered process remains a zombie"); + assert_eq!(entry.status, ProcessStatus::Exited); + assert_eq!(entry.exit_code, Some(27)); + } + + #[test] + fn spawn_process_group_is_applied_atomically() { + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + table.register( + 10, + "test", + "parent", + Vec::new(), + context(0), + Arc::new(TestDriverProcess::default()), + ); + + let leader = table + .register_with_process_group( + 11, + "test", + "leader", + Vec::new(), + context(10), + Arc::new(TestDriverProcess::default()), + Some(0), + ) + .expect("spawn should create a new process group"); + assert_eq!(leader.pgid, 11); + + let peer = table + .register_with_process_group( + 12, + "test", + "peer", + Vec::new(), + context(10), + Arc::new(TestDriverProcess::default()), + Some(11), + ) + .expect("spawn should join an existing group in the same session"); + assert_eq!(peer.pgid, 11); + + let error = table + .register_with_process_group( + 13, + "test", + "invalid", + Vec::new(), + context(10), + Arc::new(TestDriverProcess::default()), + Some(999), + ) + .expect_err("spawn must reject a nonexistent process group"); + assert_eq!(error.code(), "EPERM"); + assert!( + table.get(13).is_none(), + "failed spawn must not register a child" + ); + + table.register( + 20, + "test", + "other-session", + Vec::new(), + context(0), + Arc::new(TestDriverProcess::default()), + ); + let error = table + .register_with_process_group( + 14, + "test", + "cross-session", + Vec::new(), + context(10), + Arc::new(TestDriverProcess::default()), + Some(20), + ) + .expect_err("spawn must reject a process group in another session"); + assert_eq!(error.code(), "EPERM"); + assert!(table.get(14).is_none(), "failed spawn must remain atomic"); + } + #[test] fn allocate_pid_wraps_without_reusing_live_or_zombie_processes() { let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); diff --git a/crates/kernel/src/pty.rs b/crates/kernel/src/pty.rs index c11f790e12..ffaa1a4a63 100644 --- a/crates/kernel/src/pty.rs +++ b/crates/kernel/src/pty.rs @@ -1,6 +1,6 @@ use crate::fd_table::{ - FdResult, FileDescription, ProcessFdTable, SharedFileDescription, FILETYPE_CHARACTER_DEVICE, - O_RDWR, + allocate_file_description_id, FdResult, FileDescription, ProcessFdTable, SharedFileDescription, + FILETYPE_CHARACTER_DEVICE, O_RDWR, }; use crate::poll::{PollEvents, PollNotifier, POLLHUP, POLLIN, POLLOUT}; use std::collections::{BTreeMap, VecDeque}; @@ -269,7 +269,6 @@ struct PtyManagerState { desc_to_pty: BTreeMap, waiters: BTreeMap, next_pty_id: u64, - next_desc_id: u64, next_waiter_id: u64, } @@ -280,7 +279,6 @@ impl Default for PtyManagerState { desc_to_pty: BTreeMap::new(), waiters: BTreeMap::new(), next_pty_id: 0, - next_desc_id: 200_000, next_waiter_id: 1, } } @@ -344,10 +342,8 @@ impl PtyManager { let pty_id = state.next_pty_id; state.next_pty_id += 1; - let master_id = state.next_desc_id; - state.next_desc_id += 1; - let slave_id = state.next_desc_id; - state.next_desc_id += 1; + let master_id = allocate_file_description_id(); + let slave_id = allocate_file_description_id(); let path = format!("/dev/pts/{pty_id}"); state.ptys.insert( diff --git a/crates/kernel/src/resource_accounting.rs b/crates/kernel/src/resource_accounting.rs index df00abe4be..2635024db7 100644 --- a/crates/kernel/src/resource_accounting.rs +++ b/crates/kernel/src/resource_accounting.rs @@ -23,7 +23,7 @@ pub const DEFAULT_MAX_SOCKETS: usize = 256; pub const DEFAULT_MAX_CONNECTIONS: usize = 256; pub const DEFAULT_MAX_SOCKET_BUFFERED_BYTES: usize = 4 * 1024 * 1024; pub const DEFAULT_MAX_SOCKET_DATAGRAM_QUEUE_LEN: usize = 1_024; -pub const DEFAULT_BLOCKING_READ_TIMEOUT_MS: u64 = 5_000; +pub const DEFAULT_BLOCKING_READ_TIMEOUT_MS: u64 = 30_000; pub const DEFAULT_MAX_PREAD_BYTES: usize = 64 * 1024 * 1024; pub const DEFAULT_MAX_FD_WRITE_BYTES: usize = 64 * 1024 * 1024; pub const DEFAULT_MAX_PROCESS_ARGV_BYTES: usize = 1024 * 1024; @@ -572,7 +572,7 @@ impl ResourceAccountant { if let Some(limit) = self.limits.max_filesystem_bytes { if resulting_bytes > limit { return Err(ResourceError::filesystem_full( - "maximum filesystem size limit reached", + "maximum filesystem size limit reached; raise limits.resources.maxFilesystemBytes if needed", )); } } @@ -580,7 +580,7 @@ impl ResourceAccountant { if let Some(limit) = self.limits.max_inode_count { if resulting_inodes > limit { return Err(ResourceError::filesystem_full( - "maximum inode count limit reached", + "maximum inode count limit reached; raise limits.resources.maxInodeCount if needed", )); } } diff --git a/crates/kernel/src/socket_table.rs b/crates/kernel/src/socket_table.rs index 6f958aca3f..28a70ccf16 100644 --- a/crates/kernel/src/socket_table.rs +++ b/crates/kernel/src/socket_table.rs @@ -1,5 +1,7 @@ +use crate::fd_table::TransferredFd; use crate::poll::{PollEvents, POLLERR, POLLHUP, POLLIN, POLLOUT}; use crate::vfs::normalize_path; +use std::any::Any; use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::error::Error; use std::fmt; @@ -134,6 +136,7 @@ pub enum SocketDomain { pub enum SocketType { Stream, Datagram, + SeqPacket, } #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] @@ -197,6 +200,10 @@ impl SocketSpec { pub const fn unix_datagram() -> Self { Self::new(SocketDomain::Unix, SocketType::Datagram) } + + pub const fn unix_seqpacket() -> Self { + Self::new(SocketDomain::Unix, SocketType::SeqPacket) + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -296,7 +303,13 @@ impl SocketRecord { self.datagram_state .as_ref() .map(|state| state.recv_queue.len()) - .unwrap_or(0) + .unwrap_or_else(|| { + self.connection_state + .as_ref() + .filter(|_| self.spec.socket_type != SocketType::Stream) + .map(|state| state.recv_buffer.len()) + .unwrap_or(0) + }) } pub fn queued_datagram_bytes(&self) -> usize { @@ -348,6 +361,46 @@ pub struct ReceivedDatagram { payload: Vec, } +pub type OpaqueTransferredRight = Arc; + +#[derive(Clone)] +pub enum TransferredSocketRight { + Fd(TransferredFd), + Opaque(OpaqueTransferredRight), +} + +impl fmt::Debug for TransferredSocketRight { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Fd(fd) => f.debug_tuple("Fd").field(&fd.description_id()).finish(), + Self::Opaque(resource) => f + .debug_tuple("Opaque") + .field(&(Arc::as_ptr(resource) as *const ())) + .finish(), + } + } +} + +impl PartialEq for TransferredSocketRight { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Fd(left), Self::Fd(right)) => left == right, + (Self::Opaque(left), Self::Opaque(right)) => Arc::ptr_eq(left, right), + _ => false, + } + } +} + +impl Eq for TransferredSocketRight {} + +#[derive(Debug)] +pub struct ReceivedSocketMessage { + pub payload: Vec, + pub rights: Vec, + pub truncated: bool, + pub full_length: usize, +} + impl ReceivedDatagram { pub fn source_address(&self) -> Option<&InetSocketAddress> { self.source_address.as_ref() @@ -490,38 +543,76 @@ struct ListenerState { #[derive(Debug, Clone, PartialEq, Eq, Default)] struct ConnectionState { peer_socket_id: Option, - recv_buffer: VecDeque>, + recv_buffer: VecDeque, recv_buffer_len: usize, read_shutdown: bool, write_shutdown: bool, peer_write_shutdown: bool, } +#[derive(Debug, Clone, PartialEq, Eq)] +struct RecvChunk { + data: Vec, + rights: Vec, +} + impl ConnectionState { fn buffered_len(&self) -> usize { self.recv_buffer_len } fn has_buffered_data(&self) -> bool { - self.recv_buffer_len > 0 + !self.recv_buffer.is_empty() } - fn push_recv(&mut self, data: &[u8]) { - if data.is_empty() { + fn push_recv(&mut self, data: &[u8], rights: Vec) { + if data.is_empty() && rights.is_empty() { return; } - self.recv_buffer.push_back(data.to_vec()); + self.recv_buffer.push_back(RecvChunk { + data: data.to_vec(), + rights, + }); self.recv_buffer_len = self.recv_buffer_len.saturating_add(data.len()); } - fn read_recv(&mut self, max_bytes: usize) -> Option> { - if max_bytes == 0 { - return Some(Vec::new()); - } - if self.recv_buffer_len == 0 { + fn read_recv(&mut self, max_bytes: usize, message_oriented: bool) -> Option> { + self.read_recv_message(max_bytes, message_oriented) + .map(|message| message.payload) + } + + fn read_recv_message( + &mut self, + max_bytes: usize, + message_oriented: bool, + ) -> Option { + if self.recv_buffer.is_empty() { return None; } + if message_oriented { + let chunk = self.recv_buffer.pop_front()?; + self.recv_buffer_len = self.recv_buffer_len.saturating_sub(chunk.data.len()); + let truncated = chunk.data.len() > max_bytes; + let full_length = chunk.data.len(); + return Some(ReceivedSocketMessage { + payload: chunk.data[..chunk.data.len().min(max_bytes)].to_vec(), + rights: chunk.rights, + truncated, + full_length, + }); + } + + if max_bytes == 0 { + let chunk = self.recv_buffer.front_mut()?; + return Some(ReceivedSocketMessage { + payload: Vec::new(), + rights: std::mem::take(&mut chunk.rights), + truncated: false, + full_length: 0, + }); + } + let read_len = self.recv_buffer_len.min(max_bytes); self.recv_buffer_len -= read_len; @@ -531,18 +622,23 @@ impl ConnectionState { .load(Ordering::Relaxed) .then(Instant::now); let mut out = Vec::with_capacity(read_len); + let mut rights = Vec::new(); while remaining > 0 { let mut chunk = self.recv_buffer.pop_front()?; chunks += 1; - if chunk.len() <= remaining { - remaining -= chunk.len(); - out.extend_from_slice(&chunk); + rights.append(&mut chunk.rights); + if chunk.data.len() <= remaining { + remaining -= chunk.data.len(); + out.extend_from_slice(&chunk.data); continue; } - let tail = chunk.split_off(remaining); - out.extend_from_slice(&chunk); - self.recv_buffer.push_front(tail); + let tail = chunk.data.split_off(remaining); + out.extend_from_slice(&chunk.data); + self.recv_buffer.push_front(RecvChunk { + data: tail, + rights: Vec::new(), + }); remaining = 0; } if let Some(started) = trace_started { @@ -561,7 +657,48 @@ impl ConnectionState { Ordering::Relaxed, ); } - Some(out) + Some(ReceivedSocketMessage { + payload: out, + rights, + truncated: false, + full_length: read_len, + }) + } + + fn peek_recv_message( + &self, + max_bytes: usize, + message_oriented: bool, + ) -> Option { + let first = self.recv_buffer.front()?; + if message_oriented { + return Some(ReceivedSocketMessage { + payload: first.data[..first.data.len().min(max_bytes)].to_vec(), + rights: first.rights.clone(), + truncated: first.data.len() > max_bytes, + full_length: first.data.len(), + }); + } + + let read_len = self.recv_buffer_len.min(max_bytes); + let mut payload = Vec::with_capacity(read_len); + let mut rights = Vec::new(); + let mut remaining = read_len; + for (index, chunk) in self.recv_buffer.iter().enumerate() { + if index > 0 && remaining == 0 { + break; + } + rights.extend(chunk.rights.iter().cloned()); + let take = remaining.min(chunk.data.len()); + payload.extend_from_slice(&chunk.data[..take]); + remaining -= take; + } + Some(ReceivedSocketMessage { + payload, + rights, + truncated: false, + full_length: read_len, + }) } fn clear_recv(&mut self) { @@ -683,6 +820,35 @@ impl SocketTable { .cloned() } + pub fn reassign_owner(&self, socket_id: SocketId, owner_pid: u32) -> SocketResult<()> { + let mut table = lock_or_recover(&self.inner.state); + let previous_owner = table + .sockets + .get(&socket_id) + .ok_or_else(|| SocketTableError::not_found(socket_id))? + .owner_pid; + if previous_owner == owner_pid { + return Ok(()); + } + if let Some(ids) = table.by_owner.get_mut(&previous_owner) { + ids.remove(&socket_id); + if ids.is_empty() { + table.by_owner.remove(&previous_owner); + } + } + table + .by_owner + .entry(owner_pid) + .or_default() + .insert(socket_id); + table + .sockets + .get_mut(&socket_id) + .expect("socket checked before owner reassignment") + .owner_pid = owner_pid; + Ok(()) + } + pub fn records_for_owner(&self, owner_pid: u32) -> Vec { let table = lock_or_recover(&self.inner.state); let Some(socket_ids) = table.by_owner.get(&owner_pid) else { @@ -1629,7 +1795,7 @@ impl SocketTable { } let was_empty = !peer_connection.has_buffered_data(); - peer_connection.push_recv(data); + peer_connection.push_recv(data, Vec::new()); (was_empty && !data.is_empty()).then_some(SocketReadiness { socket_id: peer_socket_id, kind: SocketReadinessKind::Data, @@ -1639,6 +1805,61 @@ impl SocketTable { Ok(data.len()) } + pub fn send_message( + &self, + socket_id: SocketId, + data: &[u8], + rights: Vec, + ) -> SocketResult { + let readiness = { + let mut table = lock_or_recover(&self.inner.state); + let record = table + .sockets + .get(&socket_id) + .cloned() + .ok_or_else(|| SocketTableError::not_found(socket_id))?; + let connection = record.connection_state.as_ref().ok_or_else(|| { + SocketTableError::not_connected(format!("socket {socket_id} is not connected")) + })?; + if record.state != SocketState::Connected { + return Err(SocketTableError::not_connected(format!( + "socket {socket_id} is not connected" + ))); + } + if connection.write_shutdown { + return Err(SocketTableError::broken_pipe(format!( + "socket {socket_id} write side is shut down" + ))); + } + if data.is_empty() && record.spec.socket_type == SocketType::Stream { + return Ok(0); + } + + let peer_socket_id = connection.peer_socket_id.ok_or_else(|| { + SocketTableError::broken_pipe(format!("socket {socket_id} peer is closed")) + })?; + let peer = table.sockets.get_mut(&peer_socket_id).ok_or_else(|| { + SocketTableError::broken_pipe(format!("socket {socket_id} peer is closed")) + })?; + let peer_connection = peer.connection_state.as_mut().ok_or_else(|| { + SocketTableError::broken_pipe(format!("socket {socket_id} peer is closed")) + })?; + if peer_connection.read_shutdown { + return Err(SocketTableError::broken_pipe(format!( + "socket {peer_socket_id} read side is shut down" + ))); + } + let was_empty = !peer_connection.has_buffered_data(); + peer_connection.push_recv(data, rights); + was_empty.then_some(SocketReadiness { + socket_id: peer_socket_id, + kind: SocketReadinessKind::Data, + }) + }; + self.emit_readiness(readiness); + Ok(data.len()) + } + pub fn check_write(&self, socket_id: SocketId) -> SocketResult<()> { let table = lock_or_recover(&self.inner.state); let record = table @@ -1720,7 +1941,9 @@ impl SocketTable { let connection = record.connection_state.as_mut().ok_or_else(|| { SocketTableError::not_connected(format!("socket {socket_id} is not connected")) })?; - return Ok(connection.read_recv(max_bytes)); + return Ok( + connection.read_recv(max_bytes, record.spec.socket_type != SocketType::Stream) + ); } let peer_open = connection @@ -1736,6 +1959,61 @@ impl SocketTable { ))) } + pub fn recv_message( + &self, + socket_id: SocketId, + max_bytes: usize, + peek: bool, + ) -> SocketResult> { + let mut table = lock_or_recover(&self.inner.state); + let record = table + .sockets + .get_mut(&socket_id) + .ok_or_else(|| SocketTableError::not_found(socket_id))?; + if record.state != SocketState::Connected { + return Err(SocketTableError::not_connected(format!( + "socket {socket_id} is not connected" + ))); + } + let message_oriented = record.spec.socket_type != SocketType::Stream; + let connection = record.connection_state.as_mut().ok_or_else(|| { + SocketTableError::not_connected(format!("socket {socket_id} is not connected")) + })?; + if connection.read_shutdown { + return Ok(None); + } + if connection.has_buffered_data() { + return Ok(if peek { + connection.peek_recv_message(max_bytes, message_oriented) + } else { + connection.read_recv_message(max_bytes, message_oriented) + }); + } + if connection.peer_write_shutdown { + return Ok(None); + } + Err(SocketTableError::would_block(format!( + "socket {socket_id} has no readable data" + ))) + } + + pub fn next_message_rights_count(&self, socket_id: SocketId) -> SocketResult { + let table = lock_or_recover(&self.inner.state); + let record = table + .sockets + .get(&socket_id) + .ok_or_else(|| SocketTableError::not_found(socket_id))?; + let connection = record.connection_state.as_ref().ok_or_else(|| { + SocketTableError::not_connected(format!("socket {socket_id} is not connected")) + })?; + Ok(connection + .recv_buffer + .iter() + .take(1) + .map(|chunk| chunk.rights.len()) + .sum()) + } + pub fn shutdown(&self, socket_id: SocketId, how: SocketShutdown) -> SocketResult { let mut table = lock_or_recover(&self.inner.state); let record = table @@ -1813,6 +2091,11 @@ impl SocketTable { snapshot.buffered_bytes = snapshot .buffered_bytes .saturating_add(connection.buffered_len()); + if record.spec.socket_type != SocketType::Stream { + snapshot.datagram_queue_len = snapshot + .datagram_queue_len + .saturating_add(connection.recv_buffer.len()); + } } if let Some(datagram_state) = &record.datagram_state { snapshot.datagram_queue_len = snapshot @@ -1853,6 +2136,12 @@ fn validate_state_transition(current: SocketState, next: SocketState) -> SocketR } fn validate_connect_pair(socket: &SocketRecord, peer: &SocketRecord) -> SocketResult<()> { + if socket.spec != peer.spec { + return Err(SocketTableError::invalid_argument(format!( + "socket {} and peer {} have incompatible types", + socket.id, peer.id + ))); + } if !supports_connection_lifecycle(socket.spec) { return Err(SocketTableError::invalid_argument(format!( "socket {} does not support stream connections", @@ -1898,6 +2187,11 @@ fn default_datagram_state(spec: SocketSpec) -> Option { fn supports_connection_lifecycle(spec: SocketSpec) -> bool { matches!(spec.socket_type, SocketType::Stream) + || (spec.domain == SocketDomain::Unix + && matches!( + spec.socket_type, + SocketType::Datagram | SocketType::SeqPacket + )) } fn supports_listener_lifecycle(spec: SocketSpec) -> bool { diff --git a/crates/kernel/tests/api_surface.rs b/crates/kernel/tests/api_surface.rs index 23d7651670..ceb9ae3d97 100644 --- a/crates/kernel/tests/api_surface.rs +++ b/crates/kernel/tests/api_surface.rs @@ -1,7 +1,8 @@ use agentos_kernel::command_registry::CommandDriver; use agentos_kernel::fd_table::{ - FD_CLOEXEC, F_DUPFD, F_GETFD, F_GETFL, F_SETFD, F_SETFL, LOCK_EX, LOCK_NB, LOCK_SH, LOCK_UN, - O_APPEND, O_CREAT, O_EXCL, O_NONBLOCK, O_RDWR, + RecordLockType, FD_CLOEXEC, F_DUPFD, F_GETFD, F_GETFL, F_SETFD, F_SETFL, LOCK_EX, LOCK_NB, + LOCK_SH, LOCK_UN, O_APPEND, O_CREAT, O_DIRECTORY, O_EXCL, O_NOFOLLOW, O_NONBLOCK, O_RDONLY, + O_RDWR, O_TRUNC, }; use agentos_kernel::kernel::{ ExecOptions, KernelVm, KernelVmConfig, OpenShellOptions, SpawnOptions, WaitPidFlags, @@ -11,6 +12,7 @@ use agentos_kernel::mount_table::{MountOptions, MountTable}; use agentos_kernel::permissions::Permissions; use agentos_kernel::pipe_manager::MAX_PIPE_BUFFER_BYTES; use agentos_kernel::process_table::{ProcessWaitEvent, SIGWINCH}; +use agentos_kernel::socket_table::SocketType; use agentos_kernel::vfs::{ MemoryFileSystem, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat, MAX_PATH_LENGTH, }; @@ -261,6 +263,28 @@ fn kernel_fd_surface_supports_open_seek_positional_io_dup_and_dev_fd_views() { kernel .fd_write("shell", process.pid(), created_fd, b"created") .expect("write created file"); + kernel + .fd_sync("shell", process.pid(), created_fd) + .expect("sync regular file"); + let directory_fd = kernel + .fd_open("shell", process.pid(), "/tmp", 0, None) + .expect("open directory for sync"); + kernel + .fd_sync("shell", process.pid(), directory_fd) + .expect("sync directory"); + let directory_entries = kernel + .fd_read_dir_with_types("shell", process.pid(), directory_fd) + .expect("read directory through fd"); + assert!(directory_entries.iter().any(|entry| { + entry.name == "data.txt" && !entry.is_directory && !entry.is_symbolic_link + })); + assert!(directory_entries.iter().any(|entry| { + entry.name == "created.txt" && !entry.is_directory && !entry.is_symbolic_link + })); + assert_kernel_error_code( + kernel.fd_read_dir_with_types("shell", process.pid(), created_fd), + "ENOTDIR", + ); assert_eq!( kernel .filesystem_mut() @@ -337,6 +361,17 @@ fn kernel_fd_surface_supports_open_seek_positional_io_dup_and_dev_fd_views() { assert!(!file_stat.is_directory); let (read_fd, write_fd) = kernel.open_pipe("shell", process.pid()).expect("open pipe"); + assert_kernel_error_code(kernel.fd_sync("shell", process.pid(), read_fd), "EINVAL"); + let (socket_fd, peer_fd) = kernel + .fd_socketpair("shell", process.pid(), SocketType::Stream, false, false) + .expect("open socketpair for sync validation"); + assert_kernel_error_code(kernel.fd_sync("shell", process.pid(), socket_fd), "EINVAL"); + kernel + .fd_close("shell", process.pid(), socket_fd) + .expect("close first socket"); + kernel + .fd_close("shell", process.pid(), peer_fd) + .expect("close second socket"); kernel .fd_write("shell", process.pid(), write_fd, b"pipe-data") .expect("write pipe"); @@ -357,12 +392,233 @@ fn kernel_fd_surface_supports_open_seek_positional_io_dup_and_dev_fd_views() { let pipe_stat = kernel .dev_fd_stat("shell", process.pid(), read_fd) .expect("stat pipe fd"); - assert_eq!(pipe_stat.mode, 0o666); + let write_pipe_stat = kernel + .dev_fd_stat("shell", process.pid(), write_fd) + .expect("stat pipe write fd"); + assert_eq!(pipe_stat.mode, 0o010600); assert_eq!(pipe_stat.size, 0); assert_eq!(pipe_stat.blocks, 0); - assert_eq!(pipe_stat.dev, 2); + assert_ne!(pipe_stat.dev, 0); + assert_eq!(pipe_stat.nlink, 1); + assert_eq!(write_pipe_stat.dev, pipe_stat.dev); + assert_eq!(write_pipe_stat.ino, pipe_stat.ino); assert!(!pipe_stat.is_directory); + kernel + .fd_close("shell", process.pid(), directory_fd) + .expect("close directory fd"); + assert_kernel_error_code( + kernel.fd_read_dir_with_types("shell", process.pid(), directory_fd), + "EBADF", + ); + kernel + .fd_close("shell", process.pid(), created_fd) + .expect("close synced file fd"); + assert_kernel_error_code(kernel.fd_sync("shell", process.pid(), created_fd), "EBADF"); + + process.finish(0); + kernel.waitpid(process.pid()).expect("wait for shell"); +} + +#[test] +fn fd_open_directory_truncate_rejects_regular_file_without_truncating_it() { + let mut config = KernelVmConfig::new("vm-open-directory-truncate"); + config.permissions = Permissions::allow_all(); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel + .register_driver(CommandDriver::new("shell", ["sh"])) + .expect("register shell"); + kernel + .write_file("/regular", b"payload") + .expect("seed regular file"); + let process = spawn_shell(&mut kernel); + + assert_kernel_error_code( + kernel.fd_open( + "shell", + process.pid(), + "/regular", + O_RDONLY | O_DIRECTORY | O_TRUNC, + None, + ), + "ENOTDIR", + ); + assert_eq!( + kernel.read_file("/regular").expect("read regular file"), + b"payload" + ); +} + +#[test] +fn fd_open_nofollow_truncate_rejects_symlink_without_truncating_target() { + let mut config = KernelVmConfig::new("vm-open-nofollow-truncate"); + config.permissions = Permissions::allow_all(); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel + .register_driver(CommandDriver::new("shell", ["sh"])) + .expect("register shell"); + kernel + .write_file("/target", b"payload") + .expect("seed target file"); + kernel + .symlink("/target", "/target-link") + .expect("create target symlink"); + let process = spawn_shell(&mut kernel); + + assert_kernel_error_code( + kernel.fd_open( + "shell", + process.pid(), + "/target-link", + O_RDONLY | O_NOFOLLOW | O_TRUNC, + None, + ), + "ELOOP", + ); + assert_eq!( + kernel.read_file("/target").expect("read target file"), + b"payload" + ); +} + +#[test] +fn fd_open_directory_create_rejects_missing_path_without_creating_it() { + let mut config = KernelVmConfig::new("vm-open-directory-create"); + config.permissions = Permissions::allow_all(); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel + .register_driver(CommandDriver::new("shell", ["sh"])) + .expect("register shell"); + let process = spawn_shell(&mut kernel); + + assert_kernel_error_code( + kernel.fd_open( + "shell", + process.pid(), + "/missing", + O_RDONLY | O_DIRECTORY | O_CREAT, + None, + ), + "EINVAL", + ); + assert!(!kernel.exists("/missing").expect("query missing path")); +} + +#[test] +fn fd_open_nofollow_rejects_proc_and_dev_fd_symlink_aliases() { + let mut config = KernelVmConfig::new("vm-open-nofollow-special-links"); + config.permissions = Permissions::allow_all(); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel + .register_driver(CommandDriver::new("shell", ["sh"])) + .expect("register shell"); + kernel + .write_file("/target", b"payload") + .expect("seed target file"); + let process = spawn_shell(&mut kernel); + let target_fd = kernel + .fd_open("shell", process.pid(), "/target", O_RDONLY, None) + .expect("open target file"); + + for path in [ + String::from("/proc/self"), + String::from("/proc/self/cwd"), + format!("/proc/self/fd/{target_fd}"), + format!("/dev/fd/{target_fd}"), + ] { + assert_kernel_error_code( + kernel.fd_open("shell", process.pid(), &path, O_RDONLY | O_NOFOLLOW, None), + "ELOOP", + ); + } + + for path in ["/proc/self/fd/999999", "/dev/fd/999999"] { + assert_kernel_error_code( + kernel.fd_open("shell", process.pid(), path, O_RDONLY | O_NOFOLLOW, None), + "ENOENT", + ); + } +} + +#[test] +fn open_file_descriptions_survive_unlink_and_follow_rename() { + let mut config = KernelVmConfig::new("vm-api-open-file-description"); + config.permissions = Permissions::allow_all(); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel + .register_driver(CommandDriver::new("shell", ["sh"])) + .expect("register shell"); + kernel + .write_file("/tmp/unlinked.txt", b"hello".to_vec()) + .expect("seed unlink target"); + kernel + .write_file("/tmp/renamed.txt", b"rename".to_vec()) + .expect("seed rename target"); + + let process = spawn_shell(&mut kernel); + let unlinked_fd = kernel + .fd_open("shell", process.pid(), "/tmp/unlinked.txt", O_RDWR, None) + .expect("open unlink target"); + kernel + .remove_file("/tmp/unlinked.txt") + .expect("unlink open file"); + assert_eq!( + kernel + .read_link_for_process( + "shell", + process.pid(), + &format!("/proc/self/fd/{unlinked_fd}"), + ) + .expect("read unlinked file proc fd target"), + "/tmp/unlinked.txt (deleted)" + ); + + assert!(!kernel + .exists("/tmp/unlinked.txt") + .expect("check removed path")); + assert_eq!( + kernel + .fd_pread("shell", process.pid(), unlinked_fd, 5, 0) + .expect("read unlinked file description"), + b"hello".to_vec() + ); + kernel + .fd_pwrite("shell", process.pid(), unlinked_fd, b"X", 1) + .expect("write unlinked file description"); + kernel + .fd_chmod("shell", process.pid(), unlinked_fd, 0o600) + .expect("chmod unlinked file description"); + let unlinked_stat = kernel + .dev_fd_stat("shell", process.pid(), unlinked_fd) + .expect("stat unlinked file description"); + assert_eq!(unlinked_stat.size, 5); + assert_eq!(unlinked_stat.mode & 0o777, 0o600); + assert_eq!( + kernel + .fd_pread("shell", process.pid(), unlinked_fd, 5, 0) + .expect("read updated unlinked file description"), + b"hXllo".to_vec() + ); + + let renamed_fd = kernel + .fd_open("shell", process.pid(), "/tmp/renamed.txt", O_RDWR, None) + .expect("open rename target"); + kernel + .rename("/tmp/renamed.txt", "/tmp/moved.txt") + .expect("rename open file"); + assert_eq!( + kernel + .fd_path("shell", process.pid(), renamed_fd) + .expect("resolve renamed fd path"), + "/tmp/moved.txt" + ); + assert_eq!( + kernel + .fd_pread("shell", process.pid(), renamed_fd, 6, 0) + .expect("read renamed file description"), + b"rename".to_vec() + ); + process.finish(0); kernel.waitpid(process.pid()).expect("wait for shell"); } @@ -417,6 +673,43 @@ fn kernel_process_umask_applies_to_created_files_and_directories() { let dir_stat = kernel.stat("/tmp/private-dir").expect("stat private dir"); assert_eq!(dir_stat.mode & 0o777, 0o750); + let child = kernel + .spawn_process( + "sh", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from("shell")), + parent_pid: Some(process.pid()), + ..SpawnOptions::default() + }, + ) + .expect("spawn child with inherited umask"); + assert_eq!( + kernel + .umask("shell", child.pid(), None) + .expect("read child umask"), + 0o027 + ); + let child_fd = kernel + .fd_open( + "shell", + child.pid(), + "/tmp/child-umask-file.txt", + O_CREAT | O_RDWR, + Some(0o666), + ) + .expect("create file with inherited child umask"); + kernel + .fd_close("shell", child.pid(), child_fd) + .expect("close child-created file"); + let child_stat = kernel + .stat("/tmp/child-umask-file.txt") + .expect("stat child umask file"); + assert_eq!(child_stat.mode & 0o777, 0o640); + + child.finish(0); + kernel.waitpid(child.pid()).expect("wait for child"); + process.finish(0); kernel.waitpid(process.pid()).expect("wait for shell"); } @@ -469,6 +762,130 @@ fn read_dir_with_types_for_process_reports_entries_and_enforces_driver_ownership kernel.waitpid(process.pid()).expect("wait for shell"); } +#[test] +fn descriptor_readdir_reports_linux_dot_order_and_inode_identity() { + let mut config = KernelVmConfig::new("vm-api-fd-readdir-inodes"); + config.permissions = Permissions::allow_all(); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel + .register_driver(CommandDriver::new("shell", ["sh"])) + .expect("register shell"); + kernel.mkdir("/real", true).expect("create real root"); + kernel + .mkdir("/real/target", true) + .expect("create target directory"); + kernel.mkdir("/links", true).expect("create link root"); + kernel + .write_file("/real/target/child", b"child") + .expect("create child"); + kernel + .symlink("/real/target", "/links/target-link") + .expect("create directory symlink"); + + let process = spawn_shell(&mut kernel); + let directory_fd = kernel + .fd_open("shell", process.pid(), "/links/target-link", 0, None) + .expect("open directory through symlink"); + let entries = kernel + .fd_read_dir_with_types("shell", process.pid(), directory_fd) + .expect("read descriptor directory"); + assert_eq!(entries[0].name, "."); + assert_eq!(entries[1].name, ".."); + assert!(entries.iter().all(|entry| entry.ino != 0)); + assert_eq!(entries[0].ino, kernel.stat("/real/target").unwrap().ino); + assert_eq!(entries[1].ino, kernel.stat("/real").unwrap().ino); + assert_ne!(entries[1].ino, kernel.stat("/links").unwrap().ino); + assert_eq!( + entries + .iter() + .find(|entry| entry.name == "child") + .unwrap() + .ino, + kernel.lstat("/real/target/child").unwrap().ino + ); + assert_eq!( + entries, + kernel + .fd_read_dir_with_types("shell", process.pid(), directory_fd) + .expect("repeat descriptor directory read") + ); + + let root_fd = kernel + .fd_open("shell", process.pid(), "/", 0, None) + .expect("open root directory"); + let root_entries = kernel + .fd_read_dir_with_types("shell", process.pid(), root_fd) + .expect("read root directory"); + assert_eq!(root_entries[0].name, "."); + assert_eq!(root_entries[1].name, ".."); + assert_ne!(root_entries[0].ino, 0); + assert_eq!(root_entries[0].ino, root_entries[1].ino); + assert_eq!(root_entries[0].ino, kernel.stat("/").unwrap().ino); + + kernel + .mkdir("/renamed-before", true) + .expect("create directory before rename"); + kernel + .write_file("/renamed-before/child", b"child") + .expect("create renamed child"); + let renamed_fd = kernel + .fd_open("shell", process.pid(), "/renamed-before", 0, None) + .expect("open directory before rename"); + let renamed_ino = kernel.stat("/renamed-before").unwrap().ino; + kernel + .rename("/renamed-before", "/renamed-after") + .expect("rename open directory"); + assert_eq!( + kernel + .fd_path("shell", process.pid(), renamed_fd) + .expect("descriptor path follows rename"), + "/renamed-after" + ); + let renamed_entries = kernel + .fd_read_dir_with_types("shell", process.pid(), renamed_fd) + .expect("read renamed directory descriptor"); + assert_eq!(renamed_entries[0].ino, renamed_ino); + assert!(renamed_entries.iter().any(|entry| entry.name == "child")); + + kernel + .mkdir("/removed-open", true) + .expect("create directory to remove while open"); + let removed_fd = kernel + .fd_open("shell", process.pid(), "/removed-open", 0, None) + .expect("open directory before removal"); + let removed_stat = kernel + .dev_fd_stat("shell", process.pid(), removed_fd) + .expect("stat open directory before removal"); + kernel + .remove_dir("/removed-open") + .expect("remove open empty directory"); + assert_eq!( + kernel + .read_link_for_process( + "shell", + process.pid(), + &format!("/proc/self/fd/{removed_fd}"), + ) + .expect("read detached directory proc fd target"), + "/removed-open (deleted)" + ); + let detached_stat = kernel + .dev_fd_stat("shell", process.pid(), removed_fd) + .expect("fstat detached directory"); + assert_eq!(detached_stat.ino, removed_stat.ino); + assert_eq!(detached_stat.mode, removed_stat.mode); + assert!( + kernel + .fd_read_dir_with_types("shell", process.pid(), removed_fd) + .expect("read detached directory") + .is_empty(), + "Linux getdents returns EOF for an unlinked open directory" + ); + + process.finish(0); + kernel.waitpid(process.pid()).expect("wait for shell"); +} + #[test] fn kernel_fd_surface_reads_exact_byte_counts_from_device_nodes() { let mut config = KernelVmConfig::new("vm-api-fd-devices"); @@ -871,6 +1288,220 @@ fn kernel_fd_surface_shares_advisory_locks_across_fork_inherited_fds() { kernel.waitpid(contender.pid()).expect("wait contender"); } +#[test] +fn kernel_fd_surface_enforces_posix_record_lock_ranges_and_close_semantics() { + let mut config = KernelVmConfig::new("vm-api-record-locks"); + config.permissions = Permissions::allow_all(); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel + .register_driver(CommandDriver::new("shell", ["sh"])) + .expect("register shell"); + kernel + .filesystem_mut() + .write_file("/tmp/record-lock.txt", vec![0; 64]) + .expect("seed record-lock file"); + + let owner = spawn_shell(&mut kernel); + let contender = spawn_shell(&mut kernel); + let owner_fd = kernel + .fd_open("shell", owner.pid(), "/tmp/record-lock.txt", O_RDWR, None) + .expect("owner opens file"); + let owner_dup = kernel + .fd_dup("shell", owner.pid(), owner_fd) + .expect("duplicate owner fd"); + let contender_fd = kernel + .fd_open( + "shell", + contender.pid(), + "/tmp/record-lock.txt", + O_RDWR, + None, + ) + .expect("contender opens file"); + + kernel + .fd_record_lock( + "shell", + owner.pid(), + owner_fd, + RecordLockType::Write, + 10, + 20, + false, + ) + .expect("owner locks [10, 30)"); + let conflict = kernel + .fd_record_lock( + "shell", + contender.pid(), + contender_fd, + RecordLockType::Read, + 0, + 64, + true, + ) + .expect("query conflicting lock") + .expect("write lock should conflict"); + assert_eq!(conflict.pid, owner.pid()); + assert_eq!(conflict.lock_type, RecordLockType::Write); + assert_eq!((conflict.start, conflict.length()), (10, 20)); + assert_kernel_error_code( + kernel + .fd_record_lock( + "shell", + contender.pid(), + contender_fd, + RecordLockType::Write, + 20, + 2, + false, + ) + .map(|_| ()), + "EWOULDBLOCK", + ); + kernel + .fd_record_lock( + "shell", + contender.pid(), + contender_fd, + RecordLockType::Write, + 30, + 10, + false, + ) + .expect("adjacent range does not conflict"); + + kernel + .fd_record_lock( + "shell", + owner.pid(), + owner_fd, + RecordLockType::Unlock, + 15, + 10, + false, + ) + .expect("split owner range"); + kernel + .fd_record_lock( + "shell", + contender.pid(), + contender_fd, + RecordLockType::Write, + 15, + 10, + false, + ) + .expect("unlocked middle range is available"); + + // Linux process locks are released when any descriptor for the inode is + // closed, not only when the last duplicate closes. + kernel + .fd_close("shell", owner.pid(), owner_dup) + .expect("close owner duplicate"); + assert!(kernel + .fd_record_lock( + "shell", + contender.pid(), + contender_fd, + RecordLockType::Write, + 0, + 15, + false, + ) + .is_ok()); + + owner.finish(0); + contender.finish(0); + kernel.waitpid(owner.pid()).expect("wait owner"); + kernel.waitpid(contender.pid()).expect("wait contender"); +} + +#[test] +fn kernel_fd_record_lock_wait_detects_deadlock_and_cleans_up_waiters() { + let mut config = KernelVmConfig::new("vm-api-record-lock-deadlock"); + config.permissions = Permissions::allow_all(); + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + kernel + .register_driver(CommandDriver::new("shell", ["sh"])) + .expect("register shell"); + kernel + .filesystem_mut() + .write_file("/tmp/record-lock-a", vec![0; 8]) + .expect("seed first record-lock file"); + kernel + .filesystem_mut() + .write_file("/tmp/record-lock-b", vec![0; 8]) + .expect("seed second record-lock file"); + + let first = spawn_shell(&mut kernel); + let second = spawn_shell(&mut kernel); + let first_a = kernel + .fd_open("shell", first.pid(), "/tmp/record-lock-a", O_RDWR, None) + .expect("first process opens first file"); + let first_b = kernel + .fd_open("shell", first.pid(), "/tmp/record-lock-b", O_RDWR, None) + .expect("first process opens second file"); + let second_a = kernel + .fd_open("shell", second.pid(), "/tmp/record-lock-a", O_RDWR, None) + .expect("second process opens first file"); + let second_b = kernel + .fd_open("shell", second.pid(), "/tmp/record-lock-b", O_RDWR, None) + .expect("second process opens second file"); + + kernel + .fd_record_lock( + "shell", + first.pid(), + first_a, + RecordLockType::Write, + 0, + 8, + false, + ) + .expect("first process locks first file"); + kernel + .fd_record_lock( + "shell", + second.pid(), + second_b, + RecordLockType::Write, + 0, + 8, + false, + ) + .expect("second process locks second file"); + + assert_kernel_error_code( + kernel.fd_record_lock_wait("shell", first.pid(), first_b, RecordLockType::Write, 0, 8), + "EWOULDBLOCK", + ); + assert_kernel_error_code( + kernel.fd_record_lock_wait("shell", second.pid(), second_a, RecordLockType::Write, 0, 8), + "EDEADLK", + ); + + kernel + .fd_record_lock_cancel("shell", first.pid()) + .expect("cancel first process wait"); + assert_kernel_error_code( + kernel.fd_record_lock_wait("shell", second.pid(), second_a, RecordLockType::Write, 0, 8), + "EWOULDBLOCK", + ); + kernel + .fd_close("shell", second.pid(), second_a) + .expect("close cancels second process wait"); + + second.finish(0); + kernel.waitpid(second.pid()).expect("wait second process"); + kernel + .fd_record_lock_wait("shell", first.pid(), first_b, RecordLockType::Write, 0, 8) + .expect("process exit releases lock and wait edge"); + + first.finish(0); + kernel.waitpid(first.pid()).expect("wait first process"); +} + #[test] fn waitpid_returns_structured_result_and_process_introspection_works() { let mut config = KernelVmConfig::new("vm-api-proc"); diff --git a/crates/kernel/tests/dns_resolution.rs b/crates/kernel/tests/dns_resolution.rs index 9dd8b11851..a101c31873 100644 --- a/crates/kernel/tests/dns_resolution.rs +++ b/crates/kernel/tests/dns_resolution.rs @@ -16,6 +16,7 @@ struct MockDnsResolver { requests: Arc>>, record_requests: Arc>>, response: Vec, + record_error: Option, } impl MockDnsResolver { @@ -24,9 +25,15 @@ impl MockDnsResolver { requests: Arc::new(Mutex::new(Vec::new())), record_requests: Arc::new(Mutex::new(Vec::new())), response, + record_error: None, } } + fn with_record_error(mut self, error: DnsResolverError) -> Self { + self.record_error = Some(error); + self + } + fn requests(&self) -> Vec { self.requests.lock().expect("mock requests").clone() } @@ -56,7 +63,9 @@ impl DnsResolver for MockDnsResolver { .lock() .expect("mock record requests") .push(request.clone()); - Ok(Vec::new()) + self.record_error + .clone() + .map_or_else(|| Ok(Vec::new()), Err) } } @@ -205,3 +214,29 @@ fn kernel_dns_resolution_denies_by_default_before_resolver_lookup() { "permission denial should happen before record resolver lookup" ); } + +#[test] +fn kernel_dns_record_resolution_preserves_nxdomain_and_nodata() { + for (resolver, expected_code) in [ + (MockDnsResolver::new(Vec::new()), "ENODATA"), + ( + MockDnsResolver::new(Vec::new()) + .with_record_error(DnsResolverError::nx_domain("name does not exist")), + "ENOENT", + ), + ] { + let mut config = KernelVmConfig::new(format!("vm-dns-{expected_code}")); + config.permissions = Permissions::allow_all(); + config.dns_resolver = Arc::new(resolver); + let kernel = new_kernel(config); + + let error = kernel + .resolve_dns_records( + "missing.example.test", + RecordType::SSHFP, + DnsLookupPolicy::CheckPermissions, + ) + .expect_err("empty DNS record answer must retain its negative status"); + assert_eq!(error.code(), expected_code); + } +} diff --git a/crates/kernel/tests/fd_table.rs b/crates/kernel/tests/fd_table.rs index 1c7e54aa40..fd1e608948 100644 --- a/crates/kernel/tests/fd_table.rs +++ b/crates/kernel/tests/fd_table.rs @@ -1,8 +1,8 @@ use agentos_kernel::fd_table::{ FdResult, FdTableManager, FileDescription, FileLockManager, FileLockTarget, FlockOperation, - FD_CLOEXEC, FILETYPE_CHARACTER_DEVICE, FILETYPE_REGULAR_FILE, F_DUPFD, F_GETFD, F_GETFL, - F_SETFD, F_SETFL, LOCK_EX, LOCK_NB, LOCK_SH, LOCK_UN, MAX_FDS_PER_PROCESS, O_APPEND, - O_NONBLOCK, O_RDONLY, O_WRONLY, + RecordLock, RecordLockType, FD_CLOEXEC, FILETYPE_CHARACTER_DEVICE, FILETYPE_REGULAR_FILE, + F_DUPFD, F_GETFD, F_GETFL, F_SETFD, F_SETFL, LOCK_EX, LOCK_NB, LOCK_SH, LOCK_UN, + MAX_FDS_PER_PROCESS, O_APPEND, O_NONBLOCK, O_RDONLY, O_WRONLY, }; use std::fmt::Debug; use std::sync::Arc; @@ -419,7 +419,7 @@ fn flock_operation_parser_accepts_supported_modes() { #[test] fn flock_manager_enforces_shared_and_exclusive_conflicts() { let locks = FileLockManager::new(); - let target = FileLockTarget::new(42); + let target = FileLockTarget::new(1, 42); locks .apply(1, target, FlockOperation::Shared { nonblocking: false }) @@ -445,7 +445,7 @@ fn flock_manager_enforces_shared_and_exclusive_conflicts() { #[test] fn flock_manager_treats_reacquire_on_same_description_as_non_conflicting() { let locks = FileLockManager::new(); - let target = FileLockTarget::new(7); + let target = FileLockTarget::new(1, 7); locks .apply(99, target, FlockOperation::Exclusive { nonblocking: false }) @@ -460,3 +460,120 @@ fn flock_manager_treats_reacquire_on_same_description_as_non_conflicting() { let shared = locks.apply(100, target, FlockOperation::Shared { nonblocking: true }); shared.expect("downgrade should allow other shared holders"); } + +#[test] +fn lock_manager_distinguishes_equal_inodes_on_different_devices() { + let locks = FileLockManager::new(); + let first = FileLockTarget::new(1, 42); + let second = FileLockTarget::new(2, 42); + + locks + .apply(1, first, FlockOperation::Exclusive { nonblocking: true }) + .expect("lock inode on first device"); + locks + .apply(2, second, FlockOperation::Exclusive { nonblocking: true }) + .expect("same inode number on another device must not conflict"); + + locks + .set_record_lock( + first, + RecordLock::new(RecordLockType::Write, 0, 0, 10).expect("first record lock"), + ) + .expect("record lock inode on first device"); + locks + .set_record_lock( + second, + RecordLock::new(RecordLockType::Write, 0, 0, 20).expect("second record lock"), + ) + .expect("record lock on equal inode from another device must not conflict"); +} + +#[test] +fn record_lock_wait_graph_detects_cycles_and_clears_cancelled_waits() { + let locks = FileLockManager::new(); + let first = FileLockTarget::new(1, 100); + let second = FileLockTarget::new(1, 200); + let write = |target, pid| { + RecordLock::new(RecordLockType::Write, 0, 8, pid) + .map(|request| (target, request)) + .expect("valid record lock") + }; + + let (target, request) = write(first, 10); + locks + .set_record_lock(target, request) + .expect("first process locks first file"); + let (target, request) = write(second, 20); + locks + .set_record_lock(target, request) + .expect("second process locks second file"); + + let (target, request) = write(second, 10); + assert_error_code( + locks.set_blocking_record_lock(target, request), + "EWOULDBLOCK", + ); + let (target, request) = write(first, 20); + assert_error_code(locks.set_blocking_record_lock(target, request), "EDEADLK"); + + assert!(locks.cancel_record_lock_wait(10)); + let (target, request) = write(first, 20); + assert_error_code( + locks.set_blocking_record_lock(target, request), + "EWOULDBLOCK", + ); +} + +#[test] +fn record_lock_wait_graph_refreshes_after_unlock_and_success() { + let locks = FileLockManager::new(); + let first = FileLockTarget::new(1, 300); + let second = FileLockTarget::new(1, 400); + let request = + |lock_type, pid| RecordLock::new(lock_type, 0, 8, pid).expect("valid record lock"); + + locks + .set_record_lock(first, request(RecordLockType::Write, 10)) + .expect("first process locks first file"); + locks + .set_record_lock(second, request(RecordLockType::Write, 20)) + .expect("second process locks second file"); + assert_error_code( + locks.set_blocking_record_lock(second, request(RecordLockType::Write, 10)), + "EWOULDBLOCK", + ); + + locks + .set_record_lock(second, request(RecordLockType::Unlock, 20)) + .expect("second process unlocks second file"); + locks + .set_blocking_record_lock(second, request(RecordLockType::Write, 10)) + .expect("first process acquires released lock"); + + assert_error_code( + locks.set_blocking_record_lock(first, request(RecordLockType::Write, 20)), + "EWOULDBLOCK", + ); + assert!(locks.release_process_target(20, first)); + assert!(!locks.cancel_record_lock_wait(20)); +} + +#[test] +fn record_lock_wait_graph_enforces_its_bounded_capacity() { + let locks = FileLockManager::with_record_lock_limit(1); + let target = FileLockTarget::new(1, 500); + let request = + |pid| RecordLock::new(RecordLockType::Write, 0, 8, pid).expect("valid record lock"); + + locks + .set_record_lock(target, request(10)) + .expect("owner acquires lock"); + assert_error_code( + locks.set_blocking_record_lock(target, request(20)), + "EWOULDBLOCK", + ); + assert_error_code( + locks.set_blocking_record_lock(target, request(30)), + "ENOLCK", + ); +} diff --git a/crates/kernel/tests/ownership.rs b/crates/kernel/tests/ownership.rs new file mode 100644 index 0000000000..5452644f35 --- /dev/null +++ b/crates/kernel/tests/ownership.rs @@ -0,0 +1,239 @@ +use agentos_kernel::fd_table::{O_DIRECTORY, O_RDONLY}; +use agentos_kernel::kernel::{KernelVm, KernelVmConfig, VirtualProcessOptions}; +use agentos_kernel::mount_table::MountTable; +use agentos_kernel::permissions::Permissions; +use agentos_kernel::root_fs::{RootFileSystem, RootFilesystemDescriptor, RootFilesystemMode}; +use agentos_kernel::socket_table::SocketType; +use agentos_kernel::user::UserConfig; +use agentos_kernel::vfs::MemoryFileSystem; + +const DRIVER: &str = "ownership-driver"; +const UNCHANGED: u32 = u32::MAX; + +fn kernel_and_pid() -> (KernelVm, u32) { + let mut config = KernelVmConfig::new("vm-ownership"); + config.permissions = Permissions::allow_all(); + config.user = UserConfig { + supplementary_gids: vec![44], + ..UserConfig::default() + }; + let mut kernel = KernelVm::new(MemoryFileSystem::new(), config); + let process = kernel + .create_virtual_process( + DRIVER, + DRIVER, + "ownership-check", + Vec::new(), + VirtualProcessOptions::default(), + ) + .expect("create process"); + (kernel, process.pid()) +} + +#[test] +fn unprivileged_chown_matches_linux_owner_and_group_rules() { + let (mut kernel, pid) = kernel_and_pid(); + kernel + .write_file_for_process(DRIVER, pid, "/file", b"data", Some(0o6755)) + .expect("create owned file"); + + kernel + .chown_for_process(DRIVER, pid, "/file", UNCHANGED, 44, true) + .expect("owner may select supplementary group"); + let stat = kernel.stat("/file").expect("stat changed file"); + assert_eq!((stat.uid, stat.gid), (1000, 44)); + assert_eq!(stat.mode & 0o7777, 0o755, "chown clears set-id bits"); + + let error = kernel + .chown_for_process(DRIVER, pid, "/file", 1001, UNCHANGED, true) + .expect_err("unprivileged owner change must fail"); + assert_eq!(error.code(), "EPERM"); + let error = kernel + .chown_for_process(DRIVER, pid, "/file", UNCHANGED, 55, true) + .expect_err("unprivileged foreign group change must fail"); + assert_eq!(error.code(), "EPERM"); + + kernel + .chown("/file", 2000, 2000) + .expect("seed foreign owner"); + kernel + .chown_for_process(DRIVER, pid, "/file", UNCHANGED, UNCHANGED, true) + .expect("Linux accepts an all-unchanged request on a foreign inode"); + let error = kernel + .chown_for_process(DRIVER, pid, "/file", 2000, UNCHANGED, true) + .expect_err("specifying even the existing uid requires ownership"); + assert_eq!(error.code(), "EPERM"); +} + +#[test] +fn chown_preserves_non_executable_setgid_like_linux() { + let (mut kernel, pid) = kernel_and_pid(); + kernel + .write_file_for_process(DRIVER, pid, "/mandatory-lock", b"data", Some(0o6745)) + .expect("create set-id file without group execute"); + + kernel + .chown_for_process(DRIVER, pid, "/mandatory-lock", UNCHANGED, 44, true) + .expect("change file group"); + let stat = kernel.stat("/mandatory-lock").expect("stat changed file"); + assert_eq!((stat.uid, stat.gid), (1000, 44)); + assert_eq!( + stat.mode & 0o7777, + 0o2745, + "Linux clears setuid but preserves setgid without group execute" + ); +} + +#[test] +fn chown_follows_but_lchown_mutates_the_symlink_inode() { + let (mut kernel, pid) = kernel_and_pid(); + kernel + .write_file_for_process(DRIVER, pid, "/target", b"data", None) + .expect("create target"); + kernel.symlink("/target", "/link").expect("create link"); + + kernel + .chown_for_process(DRIVER, pid, "/link", UNCHANGED, 44, false) + .expect("lchown link"); + assert_eq!(kernel.lstat("/link").expect("lstat link").gid, 44); + assert_eq!(kernel.stat("/target").expect("stat target").gid, 1000); + + kernel + .chown_for_process(DRIVER, pid, "/link", UNCHANGED, 44, true) + .expect("chown target through link"); + assert_eq!(kernel.stat("/target").expect("stat target").gid, 44); +} + +#[test] +fn fchown_updates_an_open_file_after_unlink() { + let (mut kernel, pid) = kernel_and_pid(); + kernel + .write_file_for_process(DRIVER, pid, "/open", b"data", Some(0o6755)) + .expect("create file"); + let fd = kernel + .fd_open(DRIVER, pid, "/open", O_RDONLY, None) + .expect("open file"); + kernel.remove_file("/open").expect("unlink open file"); + + kernel + .fd_chown_for_process(DRIVER, pid, fd, UNCHANGED, 44) + .expect("fchown detached file"); + let stat = kernel + .dev_fd_stat(DRIVER, pid, fd) + .expect("fstat detached file"); + assert_eq!((stat.uid, stat.gid), (1000, 44)); + assert_eq!(stat.mode & 0o7777, 0o755); +} + +#[test] +fn fchown_after_unlink_updates_the_surviving_hard_link_inode() { + let (mut kernel, pid) = kernel_and_pid(); + kernel + .write_file_for_process(DRIVER, pid, "/original", b"data", Some(0o6745)) + .expect("create hard-link source"); + kernel + .link("/original", "/alias") + .expect("create surviving hard link"); + let fd = kernel + .fd_open(DRIVER, pid, "/original", O_RDONLY, None) + .expect("open source"); + let original = kernel.stat("/original").expect("stat source"); + + kernel.remove_file("/original").expect("unlink source name"); + kernel + .fd_chown_for_process(DRIVER, pid, fd, UNCHANGED, 44) + .expect("fchown through deleted name"); + + let descriptor = kernel + .dev_fd_stat(DRIVER, pid, fd) + .expect("fstat open hard link"); + let alias = kernel.stat("/alias").expect("stat surviving alias"); + assert_eq!((descriptor.dev, descriptor.ino), (alias.dev, alias.ino)); + assert_eq!((alias.dev, alias.ino), (original.dev, original.ino)); + assert_eq!((descriptor.uid, descriptor.gid), (1000, 44)); + assert_eq!((alias.uid, alias.gid), (1000, 44)); + assert_eq!(descriptor.mode & 0o7777, 0o2745); + assert_eq!(alias.mode & 0o7777, 0o2745); +} + +#[test] +fn fchmod_mutates_detached_directories_pipes_and_sockets() { + let (mut kernel, pid) = kernel_and_pid(); + kernel.mkdir("/detached", false).expect("create directory"); + let directory_fd = kernel + .fd_open(DRIVER, pid, "/detached", O_RDONLY | O_DIRECTORY, None) + .expect("open directory"); + kernel + .remove_dir("/detached") + .expect("remove open directory"); + kernel + .fd_chmod_for_process(DRIVER, pid, directory_fd, 0o701) + .expect("fchmod detached directory"); + assert_eq!( + kernel + .dev_fd_stat(DRIVER, pid, directory_fd) + .expect("fstat detached directory") + .mode + & 0o7777, + 0o701 + ); + + let (pipe_read, pipe_write) = kernel.open_pipe(DRIVER, pid).expect("open pipe"); + kernel + .fd_chmod_for_process(DRIVER, pid, pipe_read, 0o640) + .expect("fchmod pipe"); + for fd in [pipe_read, pipe_write] { + let stat = kernel.dev_fd_stat(DRIVER, pid, fd).expect("fstat pipe"); + assert_eq!(stat.mode, 0o010640); + assert_eq!((stat.uid, stat.gid), (1000, 1000)); + } + + let (socket_left, socket_right) = kernel + .fd_socketpair(DRIVER, pid, SocketType::Stream, false, false) + .expect("open socketpair"); + kernel + .fd_chmod_for_process(DRIVER, pid, socket_left, 0o601) + .expect("fchmod socket"); + let left = kernel + .dev_fd_stat(DRIVER, pid, socket_left) + .expect("fstat changed socket"); + let right = kernel + .dev_fd_stat(DRIVER, pid, socket_right) + .expect("fstat peer socket"); + assert_eq!(left.mode, 0o140601); + assert_eq!(right.mode, 0o140777); + assert_eq!((left.uid, left.gid), (1000, 1000)); +} + +#[test] +fn lchown_reaches_the_ephemeral_root_overlay_without_following() { + let mut config = KernelVmConfig::new("vm-root-ownership"); + config.permissions = Permissions::allow_all(); + let root = RootFileSystem::from_descriptor(RootFilesystemDescriptor { + mode: RootFilesystemMode::Ephemeral, + disable_default_base_layer: true, + lowers: Vec::new(), + bootstrap_entries: Vec::new(), + }) + .expect("build root filesystem"); + let mut kernel = KernelVm::new(MountTable::new(root), config); + let process = kernel + .create_virtual_process( + DRIVER, + DRIVER, + "root-ownership-check", + Vec::new(), + VirtualProcessOptions::default(), + ) + .expect("create process"); + let pid = process.pid(); + kernel + .write_file_for_process(DRIVER, pid, "/target", b"data", None) + .expect("create target"); + kernel.symlink("/target", "/link").expect("create link"); + + kernel + .chown_for_process(DRIVER, pid, "/link", UNCHANGED, UNCHANGED, false) + .expect("lchown through mounted root overlay"); + assert!(kernel.lstat("/link").expect("lstat link").is_symbolic_link); +} diff --git a/crates/kernel/tests/process_table.rs b/crates/kernel/tests/process_table.rs index 671a5e9732..34bc0381e7 100644 --- a/crates/kernel/tests/process_table.rs +++ b/crates/kernel/tests/process_table.rs @@ -540,6 +540,62 @@ fn waitpid_for_reports_stopped_and_continued_children_once() { assert_eq!(parent.kills(), vec![SIGCHLD, SIGCHLD]); } +#[test] +fn nonterminal_wait_query_never_reaps_an_exited_child() { + let table = ProcessTable::with_zombie_ttl(Duration::from_secs(3600)); + let parent = MockDriverProcess::new(); + let child = MockDriverProcess::new(); + let parent_pid = allocate_pid(&table); + let child_pid = allocate_pid(&table); + table.register( + parent_pid, + "wasmvm", + "parent", + Vec::new(), + create_context(0), + parent, + ); + table.register( + child_pid, + "wasmvm", + "child", + Vec::new(), + create_context(parent_pid), + child.clone(), + ); + + table.mark_stopped(child_pid, SIGSTOP); + child.exit(17); + assert_eq!( + table + .take_nonterminal_wait_event_for(parent_pid, child_pid as i32, WaitPidFlags::WUNTRACED,) + .expect("nonterminal wait should succeed"), + Some(agentos_kernel::process_table::ProcessWaitResult { + pid: child_pid, + status: SIGSTOP, + event: ProcessWaitEvent::Stopped, + }) + ); + assert_eq!( + table + .get(child_pid) + .expect("transition-only query must preserve zombie") + .status, + ProcessStatus::Exited + ); + assert_eq!( + table + .waitpid_for(parent_pid, child_pid as i32, WaitPidFlags::WNOHANG) + .expect("terminal wait should succeed"), + Some(agentos_kernel::process_table::ProcessWaitResult { + pid: child_pid, + status: 17, + event: ProcessWaitEvent::Exited, + }) + ); + assert!(table.get(child_pid).is_none()); +} + #[test] fn kill_routes_signals_and_validates_process_existence() { let table = ProcessTable::new(); diff --git a/crates/kernel/tests/resource_accounting.rs b/crates/kernel/tests/resource_accounting.rs index a9fc44a112..7670603d06 100644 --- a/crates/kernel/tests/resource_accounting.rs +++ b/crates/kernel/tests/resource_accounting.rs @@ -5,9 +5,9 @@ use agentos_kernel::mount_table::{MountOptions, MountTable}; use agentos_kernel::permissions::Permissions; use agentos_kernel::pty::LineDisciplineConfig; use agentos_kernel::resource_accounting::{ - ResourceLimits, DEFAULT_MAX_CONNECTIONS, DEFAULT_MAX_OPEN_FDS, DEFAULT_MAX_PIPES, - DEFAULT_MAX_PROCESSES, DEFAULT_MAX_PTYS, DEFAULT_MAX_SOCKETS, - DEFAULT_MAX_SOCKET_BUFFERED_BYTES, DEFAULT_MAX_SOCKET_DATAGRAM_QUEUE_LEN, + ResourceLimits, DEFAULT_BLOCKING_READ_TIMEOUT_MS, DEFAULT_MAX_CONNECTIONS, + DEFAULT_MAX_OPEN_FDS, DEFAULT_MAX_PIPES, DEFAULT_MAX_PROCESSES, DEFAULT_MAX_PTYS, + DEFAULT_MAX_SOCKETS, DEFAULT_MAX_SOCKET_BUFFERED_BYTES, DEFAULT_MAX_SOCKET_DATAGRAM_QUEUE_LEN, DEFAULT_VIRTUAL_CPU_COUNT, }; use agentos_kernel::root_fs::{ @@ -100,6 +100,11 @@ fn resource_limits_default_to_bounded_values() { limits.max_socket_datagram_queue_len, Some(DEFAULT_MAX_SOCKET_DATAGRAM_QUEUE_LEN) ); + assert_eq!( + limits.max_blocking_read_ms, + Some(DEFAULT_BLOCKING_READ_TIMEOUT_MS) + ); + assert_eq!(DEFAULT_BLOCKING_READ_TIMEOUT_MS, 30_000); } #[test] @@ -535,6 +540,155 @@ fn filesystem_limits_reject_fd_pwrite_before_resizing_file() { kernel.wait_and_reap(process.pid()).expect("reap shell"); } +#[test] +fn filesystem_limits_charge_rename_over_open_files_until_the_last_fd_closes() { + let mut config = KernelVmConfig::new("vm-open-file-rename-accounting"); + config.permissions = Permissions::allow_all(); + config.resources = ResourceLimits { + // The registered `sh` command contributes a 32-byte executable stub; + // the three 4-byte data files fill the remaining quota exactly. + max_filesystem_bytes: Some(44), + ..ResourceLimits::default() + }; + + let mut filesystem = MemoryFileSystem::new(); + filesystem + .write_file("/tmp/dst.bin", b"dest".to_vec()) + .expect("seed original destination"); + filesystem + .write_file("/tmp/src-one.bin", b"one!".to_vec()) + .expect("seed first source"); + filesystem + .write_file("/tmp/src-two.bin", b"two!".to_vec()) + .expect("seed second source"); + let mut kernel = KernelVm::new(filesystem, config); + kernel + .register_driver(CommandDriver::new("shell", ["sh"])) + .expect("register shell"); + + let process = kernel + .spawn_process( + "sh", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from("shell")), + ..SpawnOptions::default() + }, + ) + .expect("spawn shell"); + let original_destination = kernel + .fd_open("shell", process.pid(), "/tmp/dst.bin", O_RDWR, None) + .expect("open original destination"); + kernel + .rename("/tmp/src-one.bin", "/tmp/dst.bin") + .expect("replace first open destination"); + let first_replacement = kernel + .fd_open("shell", process.pid(), "/tmp/dst.bin", O_RDWR, None) + .expect("open first replacement"); + let first_replacement_dup = kernel + .fd_dup("shell", process.pid(), first_replacement) + .expect("duplicate first replacement fd"); + kernel + .rename("/tmp/src-two.bin", "/tmp/dst.bin") + .expect("replace second open destination"); + + let full_error = kernel + .write_file("/tmp/extra.bin", b"x".to_vec()) + .expect_err("both anonymous destinations must remain charged"); + assert_eq!(full_error.code(), "ENOSPC"); + assert!(full_error + .to_string() + .contains("limits.resources.maxFilesystemBytes")); + + kernel + .fd_close("shell", process.pid(), original_destination) + .expect("close original destination"); + let duplicate_still_charged = kernel + .write_file("/tmp/extra.bin", b"12345".to_vec()) + .expect_err("open duplicate must retain the anonymous destination charge"); + assert_eq!(duplicate_still_charged.code(), "ENOSPC"); + + kernel + .fd_close("shell", process.pid(), first_replacement) + .expect("close first replacement fd"); + let last_duplicate_still_charged = kernel + .write_file("/tmp/extra.bin", b"12345".to_vec()) + .expect_err("charge must remain until the last duplicate closes"); + assert_eq!(last_duplicate_still_charged.code(), "ENOSPC"); + + kernel + .fd_close("shell", process.pid(), first_replacement_dup) + .expect("close last replacement duplicate"); + kernel + .write_file("/tmp/extra.bin", b"12345678".to_vec()) + .expect("last close must release anonymous-file quota"); + + process.finish(0); + kernel.wait_and_reap(process.pid()).expect("reap shell"); +} + +#[test] +fn filesystem_limits_reject_anonymous_fd_growth_with_typed_limit_errors() { + let mut config = KernelVmConfig::new("vm-anonymous-fd-growth-limit"); + config.permissions = Permissions::allow_all(); + config.resources = ResourceLimits { + // The registered `sh` stub (32 bytes) plus the 3-byte file leave two + // bytes of headroom before anonymous growth must fail. + max_filesystem_bytes: Some(37), + ..ResourceLimits::default() + }; + + let mut filesystem = MemoryFileSystem::new(); + filesystem + .write_file("/tmp/data.bin", b"abc".to_vec()) + .expect("seed file"); + let mut kernel = KernelVm::new(filesystem, config); + kernel + .register_driver(CommandDriver::new("shell", ["sh"])) + .expect("register shell"); + let process = kernel + .spawn_process( + "sh", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from("shell")), + ..SpawnOptions::default() + }, + ) + .expect("spawn shell"); + let fd = kernel + .fd_open("shell", process.pid(), "/tmp/data.bin", O_RDWR, None) + .expect("open data file"); + kernel + .remove_file("/tmp/data.bin") + .expect("unlink data file"); + + let pwrite_error = kernel + .fd_pwrite("shell", process.pid(), fd, b"z", 5) + .expect_err("anonymous pwrite growth must respect byte limit"); + assert_eq!(pwrite_error.code(), "ENOSPC"); + assert!(pwrite_error + .to_string() + .contains("limits.resources.maxFilesystemBytes")); + + let truncate_error = kernel + .fd_truncate("shell", process.pid(), fd, 6) + .expect_err("anonymous truncate growth must respect byte limit"); + assert_eq!(truncate_error.code(), "ENOSPC"); + assert!(truncate_error + .to_string() + .contains("limits.resources.maxFilesystemBytes")); + assert_eq!( + kernel + .fd_pread("shell", process.pid(), fd, 3, 0) + .expect("rejected growth must leave anonymous file unchanged"), + b"abc".to_vec() + ); + + process.finish(0); + kernel.wait_and_reap(process.pid()).expect("reap shell"); +} + #[test] fn filesystem_limits_ignore_read_only_mount_usage() { let mut config = KernelVmConfig::new("vm-mounted-filesystem-limits"); diff --git a/crates/kernel/tests/unix_socket_path_permissions.rs b/crates/kernel/tests/unix_socket_path_permissions.rs new file mode 100644 index 0000000000..86adcc1d19 --- /dev/null +++ b/crates/kernel/tests/unix_socket_path_permissions.rs @@ -0,0 +1,505 @@ +use agentos_kernel::command_registry::CommandDriver; +use agentos_kernel::kernel::{KernelVm, KernelVmConfig, SpawnOptions}; +use agentos_kernel::permissions::Permissions; +use agentos_kernel::resource_accounting::{measure_filesystem_usage, ResourceLimits}; +use agentos_kernel::user::UserConfig; +use agentos_kernel::vfs::{ + MemoryFileSystem, VfsError, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat, + MAX_PATH_LENGTH, +}; + +const DRIVER: &str = "shell"; + +fn configured_user(euid: u32, egid: u32, supplementary_gids: Vec) -> UserConfig { + UserConfig { + uid: Some(euid), + gid: Some(egid), + euid: Some(euid), + egid: Some(egid), + supplementary_gids, + ..UserConfig::default() + } +} + +fn kernel_with_filesystem( + filesystem: MemoryFileSystem, + user: UserConfig, +) -> KernelVm { + let mut config = KernelVmConfig::new("vm-unix-socket-path-permissions"); + config.permissions = Permissions::allow_all(); + config.user = user; + let mut kernel = KernelVm::new(filesystem, config); + kernel + .register_driver(CommandDriver::new(DRIVER, ["sh"])) + .expect("register shell driver"); + kernel +} + +fn spawn_shell(kernel: &mut KernelVm) -> u32 { + kernel + .spawn_process( + "sh", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(DRIVER)), + ..SpawnOptions::default() + }, + ) + .expect("spawn shell") + .pid() +} + +fn mkdir_with_metadata( + filesystem: &mut MemoryFileSystem, + path: &str, + mode: u32, + uid: u32, + gid: u32, +) { + filesystem + .mkdir(path, true) + .expect("create fixture directory"); + filesystem + .chmod(path, mode) + .expect("chmod fixture directory"); + filesystem + .chown(path, uid, gid) + .expect("chown fixture directory"); +} + +struct MetadataFailureFileSystem { + inner: MemoryFileSystem, + fail_chown_path: String, + fail_chown_once: bool, +} + +impl MetadataFailureFileSystem { + fn new(inner: MemoryFileSystem, fail_chown_path: impl Into) -> Self { + Self { + inner, + fail_chown_path: fail_chown_path.into(), + fail_chown_once: true, + } + } +} + +impl VirtualFileSystem for MetadataFailureFileSystem { + fn read_file(&mut self, path: &str) -> VfsResult> { + self.inner.read_file(path) + } + + fn read_dir(&mut self, path: &str) -> VfsResult> { + self.inner.read_dir(path) + } + + fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult> { + self.inner.read_dir_limited(path, max_entries) + } + + fn read_dir_with_types(&mut self, path: &str) -> VfsResult> { + self.inner.read_dir_with_types(path) + } + + fn write_file(&mut self, path: &str, content: impl Into>) -> VfsResult<()> { + self.inner.write_file(path, content) + } + + fn create_file_exclusive(&mut self, path: &str, content: impl Into>) -> VfsResult<()> { + self.inner.create_file_exclusive(path, content) + } + + fn append_file(&mut self, path: &str, content: impl Into>) -> VfsResult { + self.inner.append_file(path, content) + } + + fn create_dir(&mut self, path: &str) -> VfsResult<()> { + self.inner.create_dir(path) + } + + fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> { + self.inner.mkdir(path, recursive) + } + + fn exists(&self, path: &str) -> bool { + self.inner.exists(path) + } + + fn stat(&mut self, path: &str) -> VfsResult { + self.inner.stat(path) + } + + fn remove_file(&mut self, path: &str) -> VfsResult<()> { + self.inner.remove_file(path) + } + + fn remove_dir(&mut self, path: &str) -> VfsResult<()> { + self.inner.remove_dir(path) + } + + fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> { + self.inner.rename(old_path, new_path) + } + + fn realpath(&self, path: &str) -> VfsResult { + self.inner.realpath(path) + } + + fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> { + self.inner.symlink(target, link_path) + } + + fn read_link(&self, path: &str) -> VfsResult { + self.inner.read_link(path) + } + + fn lstat(&self, path: &str) -> VfsResult { + self.inner.lstat(path) + } + + fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> { + self.inner.link(old_path, new_path) + } + + fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> { + self.inner.chmod(path, mode) + } + + fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> { + if path == self.fail_chown_path && self.fail_chown_once { + self.fail_chown_once = false; + return Err(VfsError::new( + "EIO", + format!("injected chown failure for '{path}'"), + )); + } + self.inner.chown(path, uid, gid) + } + + fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> { + self.inner.utimes(path, atime_ms, mtime_ms) + } + + fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> { + self.inner.truncate(path, length) + } + + fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult> { + self.inner.pread(path, offset, length) + } +} + +#[test] +fn unix_socket_bind_applies_umask_effective_owner_and_setgid_parent_group() { + let mut filesystem = MemoryFileSystem::new(); + mkdir_with_metadata(&mut filesystem, "/setgid", 0o2770, 99, 900); + mkdir_with_metadata(&mut filesystem, "/effective-gid", 0o700, 700, 999); + let mut kernel = kernel_with_filesystem(filesystem, configured_user(700, 701, vec![900])); + let pid = spawn_shell(&mut kernel); + kernel + .umask(DRIVER, pid, Some(0o027)) + .expect("set process umask"); + + let inherited = kernel + .bind_unix_socket_path_for_process(DRIVER, pid, "/", "/setgid/inherited.sock") + .expect("bind socket under setgid parent"); + assert_eq!(inherited.canonical_path, "/setgid/inherited.sock"); + assert_eq!(inherited.stat.mode, 0o140750); + assert_eq!(inherited.stat.uid, 700); + assert_eq!(inherited.stat.gid, 900); + assert_ne!(inherited.stat.ino, 0); + + let effective = kernel + .bind_unix_socket_path_for_process(DRIVER, pid, "/", "/effective-gid/effective.sock") + .expect("bind socket under ordinary parent"); + assert_eq!(effective.stat.mode, 0o140750); + assert_eq!(effective.stat.uid, 700); + assert_eq!(effective.stat.gid, 701); + + let duplicate = kernel + .bind_unix_socket_path_for_process(DRIVER, pid, "/", "/setgid/inherited.sock") + .expect_err("duplicate socket node must fail"); + assert_eq!(duplicate.code(), "EADDRINUSE"); +} + +#[test] +fn unix_socket_bind_checks_search_write_and_raw_parent_components() { + let mut filesystem = MemoryFileSystem::new(); + mkdir_with_metadata(&mut filesystem, "/no-search", 0o600, 700, 701); + mkdir_with_metadata(&mut filesystem, "/no-write", 0o500, 700, 701); + mkdir_with_metadata(&mut filesystem, "/blocked", 0o700, 999, 999); + mkdir_with_metadata(&mut filesystem, "/blocked/child", 0o777, 700, 701); + mkdir_with_metadata(&mut filesystem, "/group", 0o730, 999, 900); + mkdir_with_metadata(&mut filesystem, "/base", 0o700, 700, 701); + let mut kernel = kernel_with_filesystem(filesystem, configured_user(700, 701, vec![900])); + let pid = spawn_shell(&mut kernel); + + for path in [ + "/no-search/denied.sock", + "/no-write/denied.sock", + "/blocked/child/denied.sock", + ] { + let error = kernel + .bind_unix_socket_path_for_process(DRIVER, pid, "/", path) + .expect_err("directory DAC must reject bind"); + assert_eq!(error.code(), "EACCES", "unexpected error for {path}"); + } + + kernel + .bind_unix_socket_path_for_process(DRIVER, pid, "/", "/group/allowed.sock") + .expect("supplementary group write/search must allow bind"); + + let raw_parent_error = kernel + .bind_unix_socket_path_for_process(DRIVER, pid, "/base", "missing/../must-not-exist.sock") + .expect_err("missing raw component must not be normalized away"); + assert_eq!(raw_parent_error.code(), "ENOENT"); + assert!(!kernel + .exists("/base/must-not-exist.sock") + .expect("inspect failed bind destination")); + + let ownership_error = kernel + .bind_unix_socket_path_for_process("foreign-driver", pid, "/", "/group/foreign.sock") + .expect_err("foreign driver must not act for process"); + assert_eq!(ownership_error.code(), "EPERM"); +} + +#[test] +fn unix_socket_bind_follows_parent_symlinks_but_not_the_final_name() { + let mut filesystem = MemoryFileSystem::new(); + mkdir_with_metadata(&mut filesystem, "/real", 0o777, 700, 701); + mkdir_with_metadata(&mut filesystem, "/real/nested", 0o777, 700, 701); + filesystem + .symlink("/real/nested", "/alias") + .expect("create parent alias"); + filesystem + .symlink("/missing-target", "/real/dangling") + .expect("create dangling final symlink"); + let mut kernel = kernel_with_filesystem(filesystem, configured_user(700, 701, Vec::new())); + let pid = spawn_shell(&mut kernel); + + let preflight = kernel + .resolve_unix_socket_bind_target_for_process(DRIVER, pid, "/", "/alias/../service.sock") + .expect("preflight bind through parent symlink"); + assert_eq!(preflight, "/real/service.sock"); + assert!(!kernel + .exists(&preflight) + .expect("preflight must not create the marker")); + + let node = kernel + .bind_unix_socket_path_for_process(DRIVER, pid, "/", "/alias/../service.sock") + .expect("bind through parent symlink and raw parent component"); + assert_eq!(node.canonical_path, "/real/service.sock"); + + let dangling = kernel + .bind_unix_socket_path_for_process(DRIVER, pid, "/", "/real/dangling") + .expect_err("bind must not follow an existing final symlink"); + assert_eq!(dangling.code(), "EADDRINUSE"); + + let missing_parent = kernel + .bind_unix_socket_path_for_process(DRIVER, pid, "/", "/absent/child.sock") + .expect_err("bind must not create missing parents"); + assert_eq!(missing_parent.code(), "ENOENT"); + assert!(!kernel + .exists("/absent") + .expect("inspect missing parent after failed bind")); +} + +#[test] +fn unix_socket_connect_follows_final_symlink_and_checks_target_write_permission() { + let mut filesystem = MemoryFileSystem::new(); + mkdir_with_metadata(&mut filesystem, "/real", 0o777, 700, 701); + filesystem + .write_file("/real/plain", Vec::new()) + .expect("create regular connect target"); + filesystem + .chmod("/real/plain", 0o222) + .expect("make regular target writable"); + let mut kernel = kernel_with_filesystem(filesystem, configured_user(700, 701, vec![900])); + let pid = spawn_shell(&mut kernel); + let bound = kernel + .bind_unix_socket_path_for_process(DRIVER, pid, "/", "/real/server.sock") + .expect("create socket target"); + kernel + .symlink("/real/server.sock", "/server-alias") + .expect("create final socket symlink"); + kernel + .symlink("/missing-socket", "/dangling-socket") + .expect("create dangling socket symlink"); + kernel + .symlink("/real/server.sock/", "/socket-target-with-trailing-slash") + .expect("create socket symlink whose target has a trailing slash"); + + let resolved = kernel + .resolve_unix_socket_connect_target_for_process(DRIVER, pid, "/", "/server-alias") + .expect("connect lookup must follow final symlink"); + assert_eq!(resolved.canonical_path, "/real/server.sock"); + assert_eq!( + (resolved.stat.dev, resolved.stat.ino), + (bound.stat.dev, bound.stat.ino) + ); + + kernel + .chmod("/real/server.sock", 0o140400) + .expect("remove socket write permission"); + let denied = kernel + .resolve_unix_socket_connect_target_for_process(DRIVER, pid, "/", "/server-alias") + .expect_err("socket write permission must be enforced"); + assert_eq!(denied.code(), "EACCES"); + + kernel + .chown("/real/server.sock", 999, 900) + .expect("move socket to supplementary group"); + kernel + .chmod("/real/server.sock", 0o140020) + .expect("grant group-only socket write permission"); + kernel + .resolve_unix_socket_connect_target_for_process(DRIVER, pid, "/", "/server-alias") + .expect("supplementary group write must allow connect lookup"); + + let regular = kernel + .resolve_unix_socket_connect_target_for_process(DRIVER, pid, "/", "/real/plain") + .expect_err("regular file is not a Unix socket target"); + assert_eq!(regular.code(), "ECONNREFUSED"); + + let dangling = kernel + .resolve_unix_socket_connect_target_for_process(DRIVER, pid, "/", "/dangling-socket") + .expect_err("dangling final symlink must be ENOENT"); + assert_eq!(dangling.code(), "ENOENT"); + + let trailing = kernel + .resolve_unix_socket_connect_target_for_process(DRIVER, pid, "/", "/real/server.sock/") + .expect_err("trailing slash requires a directory"); + assert_eq!(trailing.code(), "ENOTDIR"); + + let symlink_target_trailing = kernel + .resolve_unix_socket_connect_target_for_process( + DRIVER, + pid, + "/", + "/socket-target-with-trailing-slash", + ) + .expect_err("a trailing slash in the final symlink target requires a directory"); + assert_eq!(symlink_target_trailing.code(), "ENOTDIR"); +} + +#[test] +fn unix_socket_path_walk_is_bounded_and_root_bypasses_dac() { + let mut filesystem = MemoryFileSystem::new(); + mkdir_with_metadata(&mut filesystem, "/locked", 0o000, 999, 999); + for index in 0..=40 { + let target = if index == 40 { + String::from("/locked") + } else { + format!("/link-{}", index + 1) + }; + filesystem + .symlink(&target, &format!("/link-{index}")) + .expect("create bounded symlink chain"); + } + let mut kernel = kernel_with_filesystem(filesystem, configured_user(0, 0, Vec::new())); + let pid = spawn_shell(&mut kernel); + + let loop_error = kernel + .bind_unix_socket_path_for_process(DRIVER, pid, "/", "/link-0/too-deep.sock") + .expect_err("more than 40 followed symlinks must fail"); + assert_eq!(loop_error.code(), "ELOOP"); + + let node = kernel + .bind_unix_socket_path_for_process(DRIVER, pid, "/", "/locked/root.sock") + .expect("root effective uid must bypass directory DAC"); + assert_eq!(node.stat.uid, 0); + assert_eq!(node.stat.gid, 0); + kernel + .chmod("/locked/root.sock", 0o140000) + .expect("remove every socket permission bit"); + kernel + .resolve_unix_socket_connect_target_for_process(DRIVER, pid, "/", "/locked/root.sock") + .expect("root effective uid must bypass socket write DAC"); +} + +#[test] +fn unix_socket_path_validation_uses_raw_linux_length_and_allows_control_bytes() { + let mut filesystem = MemoryFileSystem::new(); + mkdir_with_metadata(&mut filesystem, "/real", 0o700, 700, 701); + let mut kernel = kernel_with_filesystem(filesystem, configured_user(700, 701, Vec::new())); + let pid = spawn_shell(&mut kernel); + + let control_path = "/real/control\nbyte.sock"; + let control_node = kernel + .bind_unix_socket_path_for_process(DRIVER, pid, "/", control_path) + .expect("Linux permits non-NUL control bytes in Unix socket pathnames"); + assert_eq!(control_node.canonical_path, control_path); + + let raw_too_long = format!("/real/{}socket", "a/../".repeat(MAX_PATH_LENGTH)); + assert!(raw_too_long.len() >= MAX_PATH_LENGTH); + let length_error = kernel + .resolve_unix_socket_bind_target_for_process(DRIVER, pid, "/", &raw_too_long) + .expect_err("raw pathname length must be checked before resolving dot components"); + assert_eq!(length_error.code(), "ENAMETOOLONG"); + + let nul_error = kernel + .resolve_unix_socket_bind_target_for_process(DRIVER, pid, "/", "/real/nul\0byte.sock") + .expect_err("embedded NUL must remain invalid"); + assert_eq!(nul_error.code(), "EINVAL"); +} + +#[test] +fn unix_socket_bind_rolls_back_metadata_failure_without_charging_inode_quota() { + let mut filesystem = MemoryFileSystem::new(); + mkdir_with_metadata(&mut filesystem, "/sockets", 0o700, 700, 701); + // Preseed the driver's projected command so registration does not change + // the inode baseline after the quota is configured. + filesystem + .write_file("/bin/sh", Vec::new()) + .expect("preseed projected shell command"); + let initial_usage = measure_filesystem_usage(&mut filesystem).expect("measure fixture usage"); + + let mut config = KernelVmConfig::new("vm-unix-socket-bind-rollback"); + config.permissions = Permissions::allow_all(); + config.user = configured_user(700, 701, Vec::new()); + config.resources = ResourceLimits { + max_inode_count: Some(initial_usage.inode_count + 2), + ..ResourceLimits::default() + }; + let mut kernel = KernelVm::new( + MetadataFailureFileSystem::new(filesystem, "/sockets/fail.sock"), + config, + ); + kernel + .register_driver(CommandDriver::new(DRIVER, ["sh"])) + .expect("register shell driver"); + let pid = kernel + .spawn_process( + "sh", + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(DRIVER)), + ..SpawnOptions::default() + }, + ) + .expect("spawn shell") + .pid(); + + let metadata_error = kernel + .bind_unix_socket_path_for_process(DRIVER, pid, "/", "/sockets/fail.sock") + .expect_err("injected metadata failure must propagate"); + assert_eq!( + metadata_error.code(), + "EIO", + "unexpected bind failure: {metadata_error}" + ); + assert!(!kernel + .exists("/sockets/fail.sock") + .expect("inspect rolled-back marker")); + + kernel + .bind_unix_socket_path_for_process(DRIVER, pid, "/", "/sockets/ok.sock") + .expect("rolled-back bind must not consume the remaining inode quota"); + let second_node = kernel + .bind_unix_socket_path_for_process(DRIVER, pid, "/", "/sockets/last.sock") + .expect("the second available inode remains usable after rollback"); + assert_eq!(second_node.canonical_path, "/sockets/last.sock"); + let quota_error = kernel + .bind_unix_socket_path_for_process(DRIVER, pid, "/", "/sockets/over-limit.sock") + .expect_err("the successful markers must consume the remaining inode quota"); + assert_eq!(quota_error.code(), "ENOSPC"); +} diff --git a/crates/native-sidecar-browser/src/service.rs b/crates/native-sidecar-browser/src/service.rs index 56d2793985..943fd2fc18 100644 --- a/crates/native-sidecar-browser/src/service.rs +++ b/crates/native-sidecar-browser/src/service.rs @@ -1992,6 +1992,10 @@ mod tests { PersistenceBridge, RandomBridge, }; use agentos_kernel::kernel::KernelVmConfig; + use agentos_kernel::permissions::Permissions; + use agentos_native_sidecar_core::ca::{ + CA_CERTIFICATES_BUNDLE, CA_CERTIFICATES_GUEST_PATH, CA_CERTIFICATES_SYMLINK_PATH, + }; use std::time::SystemTime; #[derive(Debug, Clone, PartialEq, Eq)] @@ -2196,6 +2200,57 @@ mod tests { } } + #[test] + fn browser_vm_bootstraps_ca_before_locking_read_only_root() { + let mut sidecar = BrowserSidecar::new( + TerminateFailingBridge::default(), + BrowserSidecarConfig::default(), + ); + let custom_cert_pem = "browser custom cert.pem\n"; + let mut kernel_config = KernelVmConfig::new("vm-ca"); + kernel_config.permissions = Permissions::allow_all(); + sidecar + .create_vm_with_root_filesystem( + kernel_config, + RootFilesystemConfig { + mode: agentos_vm_config::RootFilesystemMode::ReadOnly, + disable_default_base_layer: true, + bootstrap_entries: vec![agentos_vm_config::RootFilesystemEntry { + path: CA_CERTIFICATES_SYMLINK_PATH.to_string(), + kind: agentos_vm_config::RootFilesystemEntryKind::File, + mode: None, + uid: None, + gid: None, + content: Some(custom_cert_pem.to_string()), + encoding: Some(agentos_vm_config::RootFilesystemEntryEncoding::Utf8), + target: None, + executable: false, + }], + ..RootFilesystemConfig::default() + }, + ) + .expect("create browser VM with read-only root"); + + assert_eq!( + sidecar + .read_file("vm-ca", CA_CERTIFICATES_GUEST_PATH) + .expect("read browser VM CA bundle"), + CA_CERTIFICATES_BUNDLE + ); + assert_eq!( + sidecar + .read_file("vm-ca", CA_CERTIFICATES_SYMLINK_PATH) + .expect("read custom regular cert.pem"), + custom_cert_pem.as_bytes() + ); + assert!( + sidecar + .write_file("vm-ca", CA_CERTIFICATES_GUEST_PATH, b"replacement") + .is_err(), + "browser service must finish read-only bootstrap before returning" + ); + } + // A mid-dispose worker-termination failure must still drain the VM, context, // and execution bookkeeping for that id — otherwise the VmState (holding a // BrowserKernel) and ContextState leak for the process lifetime. diff --git a/crates/native-sidecar-core/Cargo.toml b/crates/native-sidecar-core/Cargo.toml index 1d6150cb7c..d153e0991b 100644 --- a/crates/native-sidecar-core/Cargo.toml +++ b/crates/native-sidecar-core/Cargo.toml @@ -14,3 +14,7 @@ agentos-vm-config = { workspace = true } base64 = "0.22" serde_json = "1.0" vfs = { workspace = true } + +[build-dependencies] +base64 = "=0.22.1" +webpki-root-certs = "=1.0.8" diff --git a/crates/native-sidecar-core/build.rs b/crates/native-sidecar-core/build.rs new file mode 100644 index 0000000000..bfb554cf3e --- /dev/null +++ b/crates/native-sidecar-core/build.rs @@ -0,0 +1,33 @@ +use base64::{engine::general_purpose::STANDARD, Engine as _}; +use std::{env, fmt::Write as _, fs, path::PathBuf}; +use webpki_root_certs::TLS_SERVER_ROOT_CERTS; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + + let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR must be set")); + let destination = out_dir.join("ca-certificates.crt"); + let mut pem = String::new(); + + for certificate in TLS_SERVER_ROOT_CERTS { + pem.push_str("-----BEGIN CERTIFICATE-----\n"); + let encoded = STANDARD.encode(certificate.as_ref()); + for line in encoded.as_bytes().chunks(64) { + writeln!( + pem, + "{}", + std::str::from_utf8(line).expect("base64 must be UTF-8") + ) + .expect("writing to a String must succeed"); + } + pem.push_str("-----END CERTIFICATE-----\n"); + } + + assert!(!pem.is_empty(), "Mozilla CA root set must not be empty"); + fs::write(&destination, pem).unwrap_or_else(|error| { + panic!( + "failed to write generated CA bundle to {}: {error}", + destination.display() + ) + }); +} diff --git a/crates/native-sidecar-core/src/ca.rs b/crates/native-sidecar-core/src/ca.rs new file mode 100644 index 0000000000..1f16060ae2 --- /dev/null +++ b/crates/native-sidecar-core/src/ca.rs @@ -0,0 +1,51 @@ +//! The versioned default trust store installed in every AgentOS VM root. + +use agentos_kernel::root_fs::{FilesystemEntry, FilesystemEntryKind, RootFilesystemSnapshot}; + +/// Mozilla trust-store snapshot generated from the exact-pinned build dependency. +pub const CA_CERTIFICATES_BUNDLE: &[u8] = + include_bytes!(concat!(env!("OUT_DIR"), "/ca-certificates.crt")); +pub const CA_CERTIFICATES_GUEST_PATH: &str = "/etc/ssl/certs/ca-certificates.crt"; +pub const CA_CERTIFICATES_SYMLINK_PATH: &str = "/etc/ssl/cert.pem"; +pub const CA_CERTIFICATES_SYMLINK_TARGET: &str = "certs/ca-certificates.crt"; + +/// Return the lowest-precedence CA layer. User lowers and bootstrap entries +/// remain authoritative when they provide either conventional trust path. +pub(crate) fn default_ca_snapshot( + omit_bundle: bool, + omit_cert_pem_symlink: bool, +) -> RootFilesystemSnapshot { + assert!( + !CA_CERTIFICATES_BUNDLE.is_empty(), + "embedded Mozilla CA certificate bundle must not be empty" + ); + + let mut entries = vec![ + FilesystemEntry::directory("/etc/ssl"), + FilesystemEntry::directory("/etc/ssl/certs"), + ]; + if !omit_bundle { + entries.push(FilesystemEntry { + path: CA_CERTIFICATES_GUEST_PATH.to_string(), + kind: FilesystemEntryKind::File, + mode: 0o644, + uid: 0, + gid: 0, + content: Some(CA_CERTIFICATES_BUNDLE.to_vec()), + target: None, + }); + } + if !omit_cert_pem_symlink { + entries.push(FilesystemEntry { + path: CA_CERTIFICATES_SYMLINK_PATH.to_string(), + kind: FilesystemEntryKind::Symlink, + mode: 0o777, + uid: 0, + gid: 0, + content: None, + target: Some(CA_CERTIFICATES_SYMLINK_TARGET.to_string()), + }); + } + + RootFilesystemSnapshot { entries } +} diff --git a/crates/native-sidecar-core/src/lib.rs b/crates/native-sidecar-core/src/lib.rs index 929a33e183..e351166c72 100644 --- a/crates/native-sidecar-core/src/lib.rs +++ b/crates/native-sidecar-core/src/lib.rs @@ -3,6 +3,7 @@ //! Backend-agnostic sidecar logic shared by native and browser shells. pub mod bridge_bytes; +pub mod ca; pub mod diagnostics; pub mod frames; pub mod guest_fs; @@ -15,6 +16,7 @@ pub mod net; pub mod permissions; pub mod root_fs; pub mod router; +pub mod services; pub mod signals; pub mod tools; pub mod vm_fetch; diff --git a/crates/native-sidecar-core/src/limits.rs b/crates/native-sidecar-core/src/limits.rs index 48a62eef83..2477a99a4d 100644 --- a/crates/native-sidecar-core/src/limits.rs +++ b/crates/native-sidecar-core/src/limits.rs @@ -49,6 +49,11 @@ pub const DEFAULT_WASM_CAPTURED_OUTPUT_LIMIT_BYTES: usize = 16 * 1024 * 1024; pub const DEFAULT_WASM_SYNC_READ_LIMIT_BYTES: usize = 16 * 1024 * 1024; pub const DEFAULT_WASM_PREWARM_TIMEOUT_MS: u64 = 30_000; pub const DEFAULT_WASM_RUNNER_HEAP_LIMIT_MB: u32 = 2048; +pub const DEFAULT_PROCESS_PENDING_STDIN_BYTES: usize = 64 * 1024 * 1024; +pub const DEFAULT_PROCESS_MAX_SPAWN_FILE_ACTIONS: usize = 4096; +pub const DEFAULT_PROCESS_MAX_SPAWN_FILE_ACTION_BYTES: usize = 1024 * 1024; +pub const DEFAULT_PROCESS_PENDING_EVENT_COUNT: usize = 10_000; +pub const DEFAULT_PROCESS_PENDING_EVENT_BYTES: usize = 64 * 1024 * 1024; /// All operator-tunable VM-scoped limits. Fields are concrete values; the `Default` impls own the /// numbers and equal today's hardcoded constants, so unset operator config leaves behavior @@ -64,6 +69,7 @@ pub struct VmLimits { pub js_runtime: JsRuntimeLimits, pub python: PythonLimits, pub wasm: WasmLimits, + pub process: ProcessLimits, } pub fn virtual_os_cpu_count(resource_limits: &ResourceLimits) -> usize { @@ -157,6 +163,20 @@ pub struct WasmLimits { pub runner_heap_limit_mb: u32, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProcessLimits { + /// Maximum file actions decoded for one posix_spawn request. + pub max_spawn_file_actions: usize, + /// Maximum serialized file-action bytes accepted for one spawn request. + pub max_spawn_file_action_bytes: usize, + /// Host-side bytes accepted for pipe-backed stdin but not yet written. + pub pending_stdin_bytes: usize, + /// Maximum queued process events at each sidecar queue stage. + pub pending_event_count: usize, + /// Maximum aggregate payload bytes retained at each process-event stage. + pub pending_event_bytes: usize, +} + impl Default for HttpLimits { fn default() -> Self { Self { @@ -239,6 +259,18 @@ impl Default for WasmLimits { } } +impl Default for ProcessLimits { + fn default() -> Self { + Self { + max_spawn_file_actions: DEFAULT_PROCESS_MAX_SPAWN_FILE_ACTIONS, + max_spawn_file_action_bytes: DEFAULT_PROCESS_MAX_SPAWN_FILE_ACTION_BYTES, + pending_stdin_bytes: DEFAULT_PROCESS_PENDING_STDIN_BYTES, + pending_event_count: DEFAULT_PROCESS_PENDING_EVENT_COUNT, + pending_event_bytes: DEFAULT_PROCESS_PENDING_EVENT_BYTES, + } + } +} + pub fn vm_limits_from_config( config: Option<&VmLimitsConfig>, sidecar_max_frame_bytes: usize, @@ -416,6 +448,33 @@ pub fn vm_limits_from_config( .map_err(|_| integer_too_large("limits.wasm.runnerHeapLimitMb", value))?; } } + if let Some(process) = config.process.as_ref() { + set_usize( + &mut limits.process.max_spawn_file_actions, + process.max_spawn_file_actions, + "limits.process.maxSpawnFileActions", + )?; + set_usize( + &mut limits.process.max_spawn_file_action_bytes, + process.max_spawn_file_action_bytes, + "limits.process.maxSpawnFileActionBytes", + )?; + set_usize( + &mut limits.process.pending_stdin_bytes, + process.pending_stdin_bytes, + "limits.process.pendingStdinBytes", + )?; + set_usize( + &mut limits.process.pending_event_count, + process.pending_event_count, + "limits.process.pendingEventCount", + )?; + set_usize( + &mut limits.process.pending_event_bytes, + process.pending_event_bytes, + "limits.process.pendingEventBytes", + )?; + } validate_vm_limits(&limits, sidecar_max_frame_bytes)?; Ok(limits) @@ -566,6 +625,61 @@ fn integer_too_large(key: &str, value: u64) -> SidecarCoreError { SidecarCoreError::new(format!("{key} value {value} does not fit this platform")) } +#[cfg(test)] +mod tests { + use super::{ + vm_limits_from_config, DEFAULT_PROCESS_MAX_SPAWN_FILE_ACTIONS, + DEFAULT_PROCESS_MAX_SPAWN_FILE_ACTION_BYTES, + }; + use agentos_vm_config::{ProcessLimitsConfig, VmLimitsConfig}; + + #[test] + fn process_spawn_action_limits_have_bounded_defaults_and_accept_overrides() { + let defaults = vm_limits_from_config(None, 64 * 1024 * 1024).expect("default limits"); + assert_eq!( + defaults.process.max_spawn_file_actions, + DEFAULT_PROCESS_MAX_SPAWN_FILE_ACTIONS + ); + assert_eq!( + defaults.process.max_spawn_file_action_bytes, + DEFAULT_PROCESS_MAX_SPAWN_FILE_ACTION_BYTES + ); + + let config = VmLimitsConfig { + process: Some(ProcessLimitsConfig { + max_spawn_file_actions: Some(7), + max_spawn_file_action_bytes: Some(321), + ..ProcessLimitsConfig::default() + }), + ..VmLimitsConfig::default() + }; + let overridden = vm_limits_from_config(Some(&config), 64 * 1024 * 1024).expect("overrides"); + assert_eq!(overridden.process.max_spawn_file_actions, 7); + assert_eq!(overridden.process.max_spawn_file_action_bytes, 321); + + for (max_actions, max_bytes, field) in [ + (Some(0), Some(321), "limits.process.max_spawn_file_actions"), + ( + Some(7), + Some(0), + "limits.process.max_spawn_file_action_bytes", + ), + ] { + let config = VmLimitsConfig { + process: Some(ProcessLimitsConfig { + max_spawn_file_actions: max_actions, + max_spawn_file_action_bytes: max_bytes, + ..ProcessLimitsConfig::default() + }), + ..VmLimitsConfig::default() + }; + let error = vm_limits_from_config(Some(&config), 64 * 1024 * 1024) + .expect_err("zero process spawn limit must be rejected"); + assert!(error.to_string().contains(field), "{error}"); + } + } +} + /// Cross-field validation. Fail-by-default: reject any configuration that would deadlock or /// violate the wire frame budget with an explicit, actionable message. pub fn validate_vm_limits( @@ -591,7 +705,7 @@ pub fn validate_vm_limits( ))); } - let nonzero_usize: [(&str, usize); 13] = [ + let nonzero_usize: [(&str, usize); 18] = [ ( "limits.tools.max_registered_toolkits", limits.tools.max_registered_toolkits, @@ -644,6 +758,26 @@ pub fn validate_vm_limits( "limits.wasm.captured_output_limit_bytes", limits.wasm.captured_output_limit_bytes, ), + ( + "limits.process.max_spawn_file_actions", + limits.process.max_spawn_file_actions, + ), + ( + "limits.process.max_spawn_file_action_bytes", + limits.process.max_spawn_file_action_bytes, + ), + ( + "limits.process.pending_stdin_bytes", + limits.process.pending_stdin_bytes, + ), + ( + "limits.process.pending_event_count", + limits.process.pending_event_count, + ), + ( + "limits.process.pending_event_bytes", + limits.process.pending_event_bytes, + ), ]; for (key, value) in nonzero_usize { if value == 0 { diff --git a/crates/native-sidecar-core/src/root_fs.rs b/crates/native-sidecar-core/src/root_fs.rs index 72ff61c779..03763d115d 100644 --- a/crates/native-sidecar-core/src/root_fs.rs +++ b/crates/native-sidecar-core/src/root_fs.rs @@ -1,3 +1,5 @@ +use crate::ca::default_ca_snapshot; +use crate::services::default_services_snapshot; use agentos_bridge::FilesystemSnapshot; use agentos_kernel::mount_table::MountTable; use agentos_kernel::root_fs::{ @@ -107,7 +109,7 @@ pub fn build_root_filesystem_with_loaded_snapshot( limits: &impl RootFilesystemResourceLimits, ) -> Result { let import_limits = RootFilesystemImportLimits::from_resource_limits(limits); - let descriptor = if let Some(restored) = supported_loaded_snapshot(loaded_snapshot) { + let mut descriptor = if let Some(restored) = supported_loaded_snapshot(loaded_snapshot) { KernelRootFilesystemDescriptor { mode: root_filesystem_mode_from_config(config.mode), disable_default_base_layer: true, @@ -127,6 +129,56 @@ pub fn build_root_filesystem_with_loaded_snapshot( } else { root_filesystem_descriptor_from_config_with_import_limits(config, &import_limits)? }; + // Adding any explicit lower disables the kernel's automatic minimal lower. + // Preserve that scaffold when the CA is the only explicit layer. + if descriptor.disable_default_base_layer && descriptor.lowers.is_empty() { + let mut minimal_root = RootFileSystem::from_descriptor_with_import_limits( + KernelRootFilesystemDescriptor { + mode: KernelRootFilesystemMode::Ephemeral, + disable_default_base_layer: true, + lowers: Vec::new(), + bootstrap_entries: Vec::new(), + }, + &import_limits, + ) + .map_err(|error| { + SidecarCoreError::new(format!("build minimal root filesystem: {error}")) + })?; + descriptor.lowers.push( + minimal_root.snapshot().map_err(|error| { + SidecarCoreError::new(format!("snapshot minimal root: {error}")) + })?, + ); + } + + // This is the final (lowest-precedence) AgentOS lower. Overlay lookup checks + // user lowers first, and bootstrap entries are applied to the upper, so an + // explicit trust store always replaces these defaults. + let bootstrap_replaces_bundle = descriptor + .bootstrap_entries + .iter() + .any(|entry| normalize_path(&entry.path) == crate::ca::CA_CERTIFICATES_GUEST_PATH); + let bootstrap_replaces_cert_pem = descriptor + .bootstrap_entries + .iter() + .any(|entry| normalize_path(&entry.path) == crate::ca::CA_CERTIFICATES_SYMLINK_PATH); + // `write_file` follows a lower-layer symlink during copy-up. When an + // explicit bootstrap node replaces one of the trust paths, remove that + // exact lower entry first so a regular file replaces the symlink itself + // instead of overwriting its target. This also handles restored snapshots + // that captured an older default CA layout. + for lower in &mut descriptor.lowers { + lower.entries.retain(|entry| { + let path = normalize_path(&entry.path); + !(bootstrap_replaces_bundle && path == crate::ca::CA_CERTIFICATES_GUEST_PATH) + && !(bootstrap_replaces_cert_pem && path == crate::ca::CA_CERTIFICATES_SYMLINK_PATH) + }); + } + descriptor.lowers.push(default_services_snapshot()); + descriptor.lowers.push(default_ca_snapshot( + bootstrap_replaces_bundle, + bootstrap_replaces_cert_pem, + )); RootFileSystem::from_descriptor_with_import_limits(descriptor, &import_limits) .map_err(|error| SidecarCoreError::new(format!("build root filesystem: {error}"))) } @@ -449,6 +501,11 @@ fn snapshot_entry_content(content: Vec) -> (String, ProtocolRootFilesystemEn #[cfg(test)] mod tests { use super::*; + use crate::ca::{ + CA_CERTIFICATES_BUNDLE, CA_CERTIFICATES_GUEST_PATH, CA_CERTIFICATES_SYMLINK_PATH, + CA_CERTIFICATES_SYMLINK_TARGET, + }; + use crate::services::{BASELINE_SERVICES, SERVICES_GUEST_PATH}; use agentos_kernel::resource_accounting::ResourceLimits; use agentos_kernel::vfs::VirtualFileSystem; @@ -494,6 +551,24 @@ mod tests { ); } + #[test] + fn installs_default_linux_services_database() { + let mut root = build_root_filesystem( + &vm_config::RootFilesystemConfig { + disable_default_base_layer: true, + ..vm_config::RootFilesystemConfig::default() + }, + &ResourceLimits::default(), + ) + .expect("build root filesystem"); + + assert_eq!( + root.read_file(SERVICES_GUEST_PATH) + .expect("read default services"), + BASELINE_SERVICES.as_bytes() + ); + } + #[test] fn decodes_base64_root_filesystem_entries() { let mut root = build_root_filesystem( @@ -712,4 +787,176 @@ mod tests { "restored snapshots should replace configured lowers" ); } + + #[test] + fn installs_default_ca_in_read_only_roots_before_bootstrap_finishes() { + let mut root = build_root_filesystem( + &vm_config::RootFilesystemConfig { + mode: vm_config::RootFilesystemMode::ReadOnly, + disable_default_base_layer: true, + ..vm_config::RootFilesystemConfig::default() + }, + &ResourceLimits::default(), + ) + .expect("build read-only root filesystem"); + + assert_eq!( + root.read_file(CA_CERTIFICATES_GUEST_PATH) + .expect("read default CA bundle"), + CA_CERTIFICATES_BUNDLE + ); + assert_eq!( + root.read_link(CA_CERTIFICATES_SYMLINK_PATH) + .expect("read default CA symlink"), + CA_CERTIFICATES_SYMLINK_TARGET + ); + assert!( + root.exists("/usr/bin/env"), + "adding the CA lower must preserve the kernel's minimal root scaffold" + ); + + root.finish_bootstrap(); + assert_eq!( + root.read_file(CA_CERTIFICATES_SYMLINK_PATH) + .expect("read CA through symlink after read-only lock"), + CA_CERTIFICATES_BUNDLE + ); + assert!( + root.write_file(CA_CERTIFICATES_GUEST_PATH, b"replacement") + .is_err(), + "read-only root must lock the seeded trust store with all other writes" + ); + } + + #[test] + fn installs_default_ca_in_ephemeral_bundled_roots() { + let mut root = build_root_filesystem( + &vm_config::RootFilesystemConfig::default(), + &ResourceLimits::default(), + ) + .expect("build default ephemeral root filesystem"); + + assert_eq!( + root.read_file(CA_CERTIFICATES_GUEST_PATH) + .expect("read default CA bundle from bundled root"), + CA_CERTIFICATES_BUNDLE + ); + assert_eq!( + root.read_file(CA_CERTIFICATES_SYMLINK_PATH) + .expect("read default CA through bundled-root symlink"), + CA_CERTIFICATES_BUNDLE + ); + } + + #[test] + fn explicit_ca_entries_replace_defaults_across_lower_and_bootstrap_layers() { + let custom_bundle = "custom lower bundle\n"; + let custom_cert_pem = "custom bootstrap cert.pem\n"; + let mut root = build_root_filesystem( + &vm_config::RootFilesystemConfig { + disable_default_base_layer: true, + lowers: vec![vm_config::RootFilesystemLowerDescriptor::Snapshot { + entries: vec![vm_config::RootFilesystemEntry { + path: CA_CERTIFICATES_GUEST_PATH.to_string(), + kind: vm_config::RootFilesystemEntryKind::File, + mode: None, + uid: None, + gid: None, + content: Some(custom_bundle.to_string()), + encoding: Some(vm_config::RootFilesystemEntryEncoding::Utf8), + target: None, + executable: false, + }], + }], + bootstrap_entries: vec![vm_config::RootFilesystemEntry { + path: CA_CERTIFICATES_SYMLINK_PATH.to_string(), + kind: vm_config::RootFilesystemEntryKind::File, + mode: None, + uid: None, + gid: None, + content: Some(custom_cert_pem.to_string()), + encoding: Some(vm_config::RootFilesystemEntryEncoding::Utf8), + target: None, + executable: false, + }], + ..vm_config::RootFilesystemConfig::default() + }, + &ResourceLimits::default(), + ) + .expect("build root filesystem with custom trust"); + + assert_eq!( + root.read_file(CA_CERTIFICATES_GUEST_PATH) + .expect("read custom lower bundle"), + custom_bundle.as_bytes() + ); + assert_eq!( + root.read_file(CA_CERTIFICATES_SYMLINK_PATH) + .expect("read regular custom cert.pem"), + custom_cert_pem.as_bytes() + ); + assert!( + !root + .lstat(CA_CERTIFICATES_SYMLINK_PATH) + .expect("lstat custom cert.pem") + .is_symbolic_link, + "a regular custom cert.pem must replace the default symlink" + ); + } + + #[test] + fn restored_snapshot_ca_replaces_default_ca() { + let restored_bundle = b"restored trust bundle\n"; + let restored = RootFilesystemSnapshot { + entries: vec![ + FilesystemEntry::file(CA_CERTIFICATES_GUEST_PATH, restored_bundle), + FilesystemEntry::symlink( + CA_CERTIFICATES_SYMLINK_PATH, + CA_CERTIFICATES_SYMLINK_TARGET, + ), + ], + }; + let loaded_snapshot = FilesystemSnapshot { + format: String::from(agentos_kernel::root_fs::ROOT_FILESYSTEM_SNAPSHOT_FORMAT), + bytes: agentos_kernel::root_fs::encode_snapshot(&restored) + .expect("encode restored snapshot"), + }; + let mut root = build_root_filesystem_with_loaded_snapshot( + &vm_config::RootFilesystemConfig { + disable_default_base_layer: true, + bootstrap_entries: vec![vm_config::RootFilesystemEntry { + path: CA_CERTIFICATES_SYMLINK_PATH.to_string(), + kind: vm_config::RootFilesystemEntryKind::File, + mode: None, + uid: None, + gid: None, + content: Some("restored custom cert.pem\n".to_string()), + encoding: Some(vm_config::RootFilesystemEntryEncoding::Utf8), + target: None, + executable: false, + }], + ..vm_config::RootFilesystemConfig::default() + }, + Some(&loaded_snapshot), + &ResourceLimits::default(), + ) + .expect("build root filesystem from restored snapshot"); + + assert_eq!( + root.read_file(CA_CERTIFICATES_GUEST_PATH) + .expect("read restored CA bundle"), + restored_bundle + ); + assert_eq!( + root.read_file(CA_CERTIFICATES_SYMLINK_PATH) + .expect("read restored custom regular cert.pem"), + b"restored custom cert.pem\n" + ); + assert!( + !root + .lstat(CA_CERTIFICATES_SYMLINK_PATH) + .expect("lstat restored custom cert.pem") + .is_symbolic_link + ); + } } diff --git a/crates/native-sidecar-core/src/services.rs b/crates/native-sidecar-core/src/services.rs new file mode 100644 index 0000000000..4803a13f7b --- /dev/null +++ b/crates/native-sidecar-core/src/services.rs @@ -0,0 +1,70 @@ +use agentos_kernel::root_fs::{FilesystemEntry, RootFilesystemSnapshot}; + +pub const SERVICES_GUEST_PATH: &str = "/etc/services"; + +/* Baseline IANA names shipped by ordinary minimal Linux images. This is a + * lowest-precedence root layer: callers may replace it with a distro-specific + * /etc/services through a lower or bootstrap entry. */ +pub const BASELINE_SERVICES: &str = "\ +tcpmux 1/tcp\n\ +echo 7/tcp\n\ +echo 7/udp\n\ +discard 9/tcp\n\ +discard 9/udp\n\ +systat 11/tcp\n\ +daytime 13/tcp\n\ +daytime 13/udp\n\ +netstat 15/tcp\n\ +qotd 17/tcp quote\n\ +chargen 19/tcp ttytst source\n\ +chargen 19/udp ttytst source\n\ +ftp-data 20/tcp\n\ +ftp 21/tcp\n\ +ssh 22/tcp\n\ +telnet 23/tcp\n\ +smtp 25/tcp mail\n\ +time 37/tcp timserver\n\ +time 37/udp timserver\n\ +domain 53/tcp\n\ +domain 53/udp\n\ +bootps 67/udp\n\ +bootpc 68/udp\n\ +tftp 69/udp\n\ +http 80/tcp www\n\ +kerberos 88/tcp kerberos5 krb5\n\ +kerberos 88/udp kerberos5 krb5\n\ +pop3 110/tcp pop-3\n\ +sunrpc 111/tcp portmapper\n\ +sunrpc 111/udp portmapper\n\ +auth 113/tcp authentication tap ident\n\ +nntp 119/tcp readnews untp\n\ +ntp 123/udp\n\ +imap 143/tcp imap2\n\ +snmp 161/udp\n\ +snmptrap 162/udp snmp-trap\n\ +bgp 179/tcp\n\ +ldap 389/tcp\n\ +ldap 389/udp\n\ +https 443/tcp\n\ +https 443/udp\n\ +shell 514/tcp cmd\n\ +syslog 514/udp\n\ +submission 587/tcp msa\n\ +ldaps 636/tcp\n\ +rsync 873/tcp\n\ +imaps 993/tcp\n\ +pop3s 995/tcp\n\ +socks 1080/tcp\n\ +mysql 3306/tcp\n\ +postgresql 5432/tcp postgres\n\ +redis 6379/tcp\n\ +http-alt 8080/tcp webcache\n"; + +pub fn default_services_snapshot() -> RootFilesystemSnapshot { + RootFilesystemSnapshot { + entries: vec![FilesystemEntry::file( + SERVICES_GUEST_PATH, + BASELINE_SERVICES.as_bytes().to_vec(), + )], + } +} diff --git a/crates/native-sidecar/Cargo.toml b/crates/native-sidecar/Cargo.toml index c2f7826540..7099814361 100644 --- a/crates/native-sidecar/Cargo.toml +++ b/crates/native-sidecar/Cargo.toml @@ -34,18 +34,17 @@ filetime = "0.2" h2 = "0.4" http = "1" hmac = "0.12" -hickory-resolver = "0.26.0-beta.3" +hickory-resolver = "=0.26.0-beta.3" jsonwebtoken = "8.3.0" log = "0.4" md-5 = "0.10" -nix = { version = "0.29", features = ["fs", "poll", "process", "signal", "user"] } +nix = { version = "0.29", features = ["fs", "poll", "process", "signal", "socket", "user"] } # `vendored` builds OpenSSL from source → self-contained binary with no # system-openssl discovery on any runner (ubuntu/macOS). openssl backs the # guest crypto runtime (RSA/EC/DH/AES), so it cannot be a TLS-only stack. openssl = { version = "0.10", features = ["vendored"] } pbkdf2 = "0.12" rustls = { version = "0.23.37", default-features = false, features = ["aws_lc_rs", "std", "tls12"] } -rustls-native-certs = "0.8" rustls-pemfile = "2.2" tokio-rustls = { version = "0.26", default-features = false, features = ["aws_lc_rs", "tls12"] } rusqlite = { version = "0.32", features = ["bundled"] } diff --git a/crates/native-sidecar/assets/base-filesystem.json b/crates/native-sidecar/assets/base-filesystem.json index 64e03bfa53..e641efddd3 100644 --- a/crates/native-sidecar/assets/base-filesystem.json +++ b/crates/native-sidecar/assets/base-filesystem.json @@ -7,7 +7,8 @@ "transforms": [ "Normalize HOSTNAME to secure-exec", "Preserve the captured user-level environment and filesystem layout as the secure-exec base layer", - "Add the non-Alpine /workspace directory (default agent working directory) owned by the base user" + "Add the non-Alpine /workspace directory (default agent working directory) owned by the base user", + "Restore Alpine 3.22's /etc/services database and add the VM's Docker-style /etc/hosts entries for libc lookup parity" ] }, "environment": { @@ -93,6 +94,14 @@ "gid": 0, "content": "secure-exec\n" }, + { + "path": "/etc/hosts", + "type": "file", + "mode": "644", + "uid": 0, + "gid": 0, + "content": "127.0.0.1 localhost localhost.localdomain\n::1 localhost localhost.localdomain ip6-localhost ip6-loopback\nfe00:: ip6-localnet\nff00:: ip6-mcastprefix\nff02::1 ip6-allnodes\nff02::2 ip6-allrouters\n127.0.1.1 secure-exec\n" + }, { "path": "/etc/logrotate.d", "type": "directory", @@ -197,6 +206,14 @@ "gid": 0, "content": "# valid login shells\n/bin/sh\n/bin/ash\n" }, + { + "path": "/etc/services", + "type": "file", + "mode": "644", + "uid": 0, + "gid": 0, + "content": "# Network services, Internet style\n#\n# Updated from https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml .\n#\n# New ports will be added on request if they have been officially assigned\n# by IANA and used in the real-world or are needed by a debian package.\n# If you need a huge list of used numbers please install the nmap package.\n\ntcpmux\t\t1/tcp\t\t\t\t# TCP port service multiplexer\necho\t\t7/tcp\necho\t\t7/udp\ndiscard\t\t9/tcp\t\tsink null\ndiscard\t\t9/udp\t\tsink null\nsystat\t\t11/tcp\t\tusers\ndaytime\t\t13/tcp\ndaytime\t\t13/udp\nnetstat\t\t15/tcp\nqotd\t\t17/tcp\t\tquote\nchargen\t\t19/tcp\t\tttytst source\nchargen\t\t19/udp\t\tttytst source\nftp-data\t20/tcp\nftp\t\t21/tcp\nfsp\t\t21/udp\t\tfspd\nssh\t\t22/tcp\t\t\t\t# SSH Remote Login Protocol\ntelnet\t\t23/tcp\nsmtp\t\t25/tcp\t\tmail\ntime\t\t37/tcp\t\ttimserver\ntime\t\t37/udp\t\ttimserver\nwhois\t\t43/tcp\t\tnicname\ntacacs\t\t49/tcp\t\t\t\t# Login Host Protocol (TACACS)\ntacacs\t\t49/udp\ndomain\t\t53/tcp\t\t\t\t# Domain Name Server\ndomain\t\t53/udp\nbootps\t\t67/udp\nbootpc\t\t68/udp\ntftp\t\t69/udp\ngopher\t\t70/tcp\t\t\t\t# Internet Gopher\nfinger\t\t79/tcp\nhttp\t\t80/tcp\t\twww\t\t# WorldWideWeb HTTP\nkerberos\t88/tcp\t\tkerberos5 krb5 kerberos-sec\t# Kerberos v5\nkerberos\t88/udp\t\tkerberos5 krb5 kerberos-sec\t# Kerberos v5\niso-tsap\t102/tcp\t\ttsap\t\t# part of ISODE\nacr-nema\t104/tcp\t\tdicom\t\t# Digital Imag. & Comm. 300\npop3\t\t110/tcp\t\tpop-3\t\t# POP version 3\nsunrpc\t\t111/tcp\t\tportmapper\t# RPC 4.0 portmapper\nsunrpc\t\t111/udp\t\tportmapper\nauth\t\t113/tcp\t\tauthentication tap ident\nnntp\t\t119/tcp\t\treadnews untp\t# USENET News Transfer Protocol\nntp\t\t123/udp\t\t\t\t# Network Time Protocol\nepmap\t\t135/tcp\t\tloc-srv\t\t# DCE endpoint resolution\nnetbios-ns\t137/udp\t\t\t\t# NETBIOS Name Service\nnetbios-dgm\t138/udp\t\t\t\t# NETBIOS Datagram Service\nnetbios-ssn\t139/tcp\t\t\t\t# NETBIOS session service\nimap2\t\t143/tcp\t\timap\t\t# Interim Mail Access P 2 and 4\nsnmp\t\t161/tcp\t\t\t\t# Simple Net Mgmt Protocol\nsnmp\t\t161/udp\nsnmp-trap\t162/tcp\t\tsnmptrap\t# Traps for SNMP\nsnmp-trap\t162/udp\t\tsnmptrap\ncmip-man\t163/tcp\t\t\t\t# ISO mgmt over IP (CMOT)\ncmip-man\t163/udp\ncmip-agent\t164/tcp\ncmip-agent\t164/udp\nmailq\t\t174/tcp\t\t\t# Mailer transport queue for Zmailer\nxdmcp\t\t177/udp\t\t\t# X Display Manager Control Protocol\nbgp\t\t179/tcp\t\t\t\t# Border Gateway Protocol\nsmux\t\t199/tcp\t\t\t\t# SNMP Unix Multiplexer\nqmtp\t\t209/tcp\t\t\t\t# Quick Mail Transfer Protocol\nz3950\t\t210/tcp\t\twais\t\t# NISO Z39.50 database\nipx\t\t213/udp\t\t\t\t# IPX [RFC1234]\nptp-event\t319/udp\nptp-general\t320/udp\npawserv\t\t345/tcp\t\t\t\t# Perf Analysis Workbench\nzserv\t\t346/tcp\t\t\t\t# Zebra server\nrpc2portmap\t369/tcp\nrpc2portmap\t369/udp\t\t\t\t# Coda portmapper\ncodaauth2\t370/tcp\ncodaauth2\t370/udp\t\t\t\t# Coda authentication server\nclearcase\t371/udp\t\tClearcase\nldap\t\t389/tcp\t\t\t# Lightweight Directory Access Protocol\nldap\t\t389/udp\nsvrloc\t\t427/tcp\t\t\t\t# Server Location\nsvrloc\t\t427/udp\nhttps\t\t443/tcp\t\t\t\t# http protocol over TLS/SSL\nhttps\t\t443/udp\t\t\t\t# HTTP/3\nsnpp\t\t444/tcp\t\t\t\t# Simple Network Paging Protocol\nmicrosoft-ds\t445/tcp\t\t\t\t# Microsoft Naked CIFS\nkpasswd\t\t464/tcp\nkpasswd\t\t464/udp\nsubmissions\t465/tcp\t\tssmtp smtps urd # Submission over TLS [RFC8314]\nsaft\t\t487/tcp\t\t\t# Simple Asynchronous File Transfer\nisakmp\t\t500/udp\t\t\t\t# IPSEC key management\nrtsp\t\t554/tcp\t\t\t# Real Time Stream Control Protocol\nrtsp\t\t554/udp\nnqs\t\t607/tcp\t\t\t\t# Network Queuing system\nasf-rmcp\t623/udp\t\t# ASF Remote Management and Control Protocol\nqmqp\t\t628/tcp\nipp\t\t631/tcp\t\t\t\t# Internet Printing Protocol\nldp\t\t646/tcp\t\t\t\t# Label Distribution Protocol\nldp\t\t646/udp\n#\n# UNIX specific services\n#\nexec\t\t512/tcp\nbiff\t\t512/udp\t\tcomsat\nlogin\t\t513/tcp\nwho\t\t513/udp\t\twhod\nshell\t\t514/tcp\t\tcmd syslog\t# no passwords used\nsyslog\t\t514/udp\nprinter\t\t515/tcp\t\tspooler\t\t# line printer spooler\ntalk\t\t517/udp\nntalk\t\t518/udp\nroute\t\t520/udp\t\trouter routed\t# RIP\ngdomap\t\t538/tcp\t\t\t\t# GNUstep distributed objects\ngdomap\t\t538/udp\nuucp\t\t540/tcp\t\tuucpd\t\t# uucp daemon\nklogin\t\t543/tcp\t\t\t\t# Kerberized `rlogin' (v5)\nkshell\t\t544/tcp\t\tkrcmd\t\t# Kerberized `rsh' (v5)\ndhcpv6-client\t546/udp\ndhcpv6-server\t547/udp\nafpovertcp\t548/tcp\t\t\t\t# AFP over TCP\nnntps\t\t563/tcp\t\tsnntp\t\t# NNTP over SSL\nsubmission\t587/tcp\t\t\t\t# Submission [RFC4409]\nldaps\t\t636/tcp\t\t\t\t# LDAP over SSL\nldaps\t\t636/udp\ntinc\t\t655/tcp\t\t\t\t# tinc control port\ntinc\t\t655/udp\nsilc\t\t706/tcp\nkerberos-adm\t749/tcp\t\t\t\t# Kerberos `kadmin' (v5)\n#\ndomain-s\t853/tcp\t\t\t\t# DNS over TLS [RFC7858]\ndomain-s\t853/udp\t\t\t\t# DNS over DTLS [RFC8094]\nrsync\t\t873/tcp\nftps-data\t989/tcp\t\t\t\t# FTP over SSL (data)\nftps\t\t990/tcp\ntelnets\t\t992/tcp\t\t\t\t# Telnet over SSL\nimaps\t\t993/tcp\t\t\t\t# IMAP over SSL\npop3s\t\t995/tcp\t\t\t\t# POP-3 over SSL\n#\n# From ``Assigned Numbers'':\n#\n#> The Registered Ports are not controlled by the IANA and on most systems\n#> can be used by ordinary user processes or programs executed by ordinary\n#> users.\n#\n#> Ports are used in the TCP [45,106] to name the ends of logical\n#> connections which carry long term conversations. For the purpose of\n#> providing services to unknown callers, a service contact port is\n#> defined. This list specifies the port used by the server process as its\n#> contact port. While the IANA can not control uses of these ports it\n#> does register or list uses of these ports as a convienence to the\n#> community.\n#\nsocks\t\t1080/tcp\t\t\t# socks proxy server\nproofd\t\t1093/tcp\nrootd\t\t1094/tcp\nopenvpn\t\t1194/tcp\nopenvpn\t\t1194/udp\nrmiregistry\t1099/tcp\t\t\t# Java RMI Registry\nlotusnote\t1352/tcp\tlotusnotes\t# Lotus Note\nms-sql-s\t1433/tcp\t\t\t# Microsoft SQL Server\nms-sql-m\t1434/udp\t\t\t# Microsoft SQL Monitor\ningreslock\t1524/tcp\ndatametrics\t1645/tcp\told-radius\ndatametrics\t1645/udp\told-radius\nsa-msg-port\t1646/tcp\told-radacct\nsa-msg-port\t1646/udp\told-radacct\nkermit\t\t1649/tcp\ngroupwise\t1677/tcp\nl2f\t\t1701/udp\tl2tp\nradius\t\t1812/tcp\nradius\t\t1812/udp\nradius-acct\t1813/tcp\tradacct\t\t# Radius Accounting\nradius-acct\t1813/udp\tradacct\ncisco-sccp\t2000/tcp\t\t\t# Cisco SCCP\nnfs\t\t2049/tcp\t\t\t# Network File System\nnfs\t\t2049/udp\t\t\t# Network File System\ngnunet\t\t2086/tcp\ngnunet\t\t2086/udp\nrtcm-sc104\t2101/tcp\t\t\t# RTCM SC-104 IANA 1/29/99\nrtcm-sc104\t2101/udp\ngsigatekeeper\t2119/tcp\ngris\t\t2135/tcp\t\t# Grid Resource Information Server\ncvspserver\t2401/tcp\t\t\t# CVS client/server operations\nvenus\t\t2430/tcp\t\t\t# codacon port\nvenus\t\t2430/udp\t\t\t# Venus callback/wbc interface\nvenus-se\t2431/tcp\t\t\t# tcp side effects\nvenus-se\t2431/udp\t\t\t# udp sftp side effect\ncodasrv\t\t2432/tcp\t\t\t# not used\ncodasrv\t\t2432/udp\t\t\t# server port\ncodasrv-se\t2433/tcp\t\t\t# tcp side effects\ncodasrv-se\t2433/udp\t\t\t# udp sftp side effect\nmon\t\t2583/tcp\t\t\t# MON traps\nmon\t\t2583/udp\ndict\t\t2628/tcp\t\t\t# Dictionary server\nf5-globalsite\t2792/tcp\ngsiftp\t\t2811/tcp\ngpsd\t\t2947/tcp\ngds-db\t\t3050/tcp\tgds_db\t\t# InterBase server\nicpv2\t\t3130/udp\ticp\t\t# Internet Cache Protocol\nisns\t\t3205/tcp\t\t\t# iSNS Server Port\nisns\t\t3205/udp\t\t\t# iSNS Server Port\niscsi-target\t3260/tcp\nmysql\t\t3306/tcp\nms-wbt-server\t3389/tcp\nnut\t\t3493/tcp\t\t\t# Network UPS Tools\nnut\t\t3493/udp\ndistcc\t\t3632/tcp\t\t\t# distributed compiler\ndaap\t\t3689/tcp\t\t\t# Digital Audio Access Protocol\nsvn\t\t3690/tcp\tsubversion\t# Subversion protocol\nsuucp\t\t4031/tcp\t\t\t# UUCP over SSL\nsysrqd\t\t4094/tcp\t\t\t# sysrq daemon\nsieve\t\t4190/tcp\t\t\t# ManageSieve Protocol\nepmd\t\t4369/tcp\t\t\t# Erlang Port Mapper Daemon\nremctl\t\t4373/tcp\t\t# Remote Authenticated Command Service\nf5-iquery\t4353/tcp\t\t\t# F5 iQuery\nntske\t\t4460/tcp\t# Network Time Security Key Establishment\nipsec-nat-t\t4500/udp\t\t\t# IPsec NAT-Traversal [RFC3947]\niax\t\t4569/udp\t\t\t# Inter-Asterisk eXchange\nmtn\t\t4691/tcp\t\t\t# monotone Netsync Protocol\nradmin-port\t4899/tcp\t\t\t# RAdmin Port\nsip\t\t5060/tcp\t\t\t# Session Initiation Protocol\nsip\t\t5060/udp\nsip-tls\t\t5061/tcp\nsip-tls\t\t5061/udp\nxmpp-client\t5222/tcp\tjabber-client\t# Jabber Client Connection\nxmpp-server\t5269/tcp\tjabber-server\t# Jabber Server Connection\ncfengine\t5308/tcp\nmdns\t\t5353/udp\t\t\t# Multicast DNS\npostgresql\t5432/tcp\tpostgres\t# PostgreSQL Database\nfreeciv\t\t5556/tcp\trptp\t\t# Freeciv gameplay\namqps\t\t5671/tcp\t\t\t# AMQP protocol over TLS/SSL\namqp\t\t5672/tcp\namqp\t\t5672/sctp\nx11\t\t6000/tcp\tx11-0\t\t# X Window System\nx11-1\t\t6001/tcp\nx11-2\t\t6002/tcp\nx11-3\t\t6003/tcp\nx11-4\t\t6004/tcp\nx11-5\t\t6005/tcp\nx11-6\t\t6006/tcp\nx11-7\t\t6007/tcp\ngnutella-svc\t6346/tcp\t\t\t# gnutella\ngnutella-svc\t6346/udp\ngnutella-rtr\t6347/tcp\t\t\t# gnutella\ngnutella-rtr\t6347/udp\nredis\t\t6379/tcp\nsge-qmaster\t6444/tcp\tsge_qmaster\t# Grid Engine Qmaster Service\nsge-execd\t6445/tcp\tsge_execd\t# Grid Engine Execution Service\nmysql-proxy\t6446/tcp\t\t\t# MySQL Proxy\nbabel\t\t6696/udp\t\t\t# Babel Routing Protocol\nircs-u\t\t6697/tcp\t\t# Internet Relay Chat via TLS/SSL\nbbs\t\t7000/tcp\nafs3-fileserver 7000/udp\nafs3-callback\t7001/udp\t\t\t# callbacks to cache managers\nafs3-prserver\t7002/udp\t\t\t# users & groups database\nafs3-vlserver\t7003/udp\t\t\t# volume location database\nafs3-kaserver\t7004/udp\t\t\t# AFS/Kerberos authentication\nafs3-volser\t7005/udp\t\t\t# volume managment server\nafs3-bos\t7007/udp\t\t\t# basic overseer process\nafs3-update\t7008/udp\t\t\t# server-to-server updater\nafs3-rmtsys\t7009/udp\t\t\t# remote cache manager service\nfont-service\t7100/tcp\txfs\t\t# X Font Service\nhttp-alt\t8080/tcp\twebcache\t# WWW caching service\npuppet\t\t8140/tcp\t\t\t# The Puppet master service\nbacula-dir\t9101/tcp\t\t\t# Bacula Director\nbacula-fd\t9102/tcp\t\t\t# Bacula File Daemon\nbacula-sd\t9103/tcp\t\t\t# Bacula Storage Daemon\nxmms2\t\t9667/tcp\t# Cross-platform Music Multiplexing System\nnbd\t\t10809/tcp\t\t\t# Linux Network Block Device\nzabbix-agent\t10050/tcp\t\t\t# Zabbix Agent\nzabbix-trapper\t10051/tcp\t\t\t# Zabbix Trapper\namanda\t\t10080/tcp\t\t\t# amanda backup services\ndicom\t\t11112/tcp\nhkp\t\t11371/tcp\t\t\t# OpenPGP HTTP Keyserver\ndb-lsp\t\t17500/tcp\t\t\t# Dropbox LanSync Protocol\ndcap\t\t22125/tcp\t\t\t# dCache Access Protocol\ngsidcap\t\t22128/tcp\t\t\t# GSI dCache Access Protocol\nwnn6\t\t22273/tcp\t\t\t# wnn6\n\n#\n# Datagram Delivery Protocol services\n#\nrtmp\t\t1/ddp\t\t\t# Routing Table Maintenance Protocol\nnbp\t\t2/ddp\t\t\t# Name Binding Protocol\necho\t\t4/ddp\t\t\t# AppleTalk Echo Protocol\nzip\t\t6/ddp\t\t\t# Zone Information Protocol\n\n#=========================================================================\n# The remaining port numbers are not as allocated by IANA.\n#=========================================================================\n\n# Kerberos (Project Athena/MIT) services\nkerberos4\t750/udp\t\tkerberos-iv kdc\t# Kerberos (server)\nkerberos4\t750/tcp\t\tkerberos-iv kdc\nkerberos-master\t751/udp\t\tkerberos_master\t# Kerberos authentication\nkerberos-master\t751/tcp\npasswd-server\t752/udp\t\tpasswd_server\t# Kerberos passwd server\nkrb-prop\t754/tcp\t\tkrb_prop krb5_prop hprop # Kerberos slave propagation\nzephyr-srv\t2102/udp\t\t\t# Zephyr server\nzephyr-clt\t2103/udp\t\t\t# Zephyr serv-hm connection\nzephyr-hm\t2104/udp\t\t\t# Zephyr hostmanager\niprop\t\t2121/tcp\t\t\t# incremental propagation\nsupfilesrv\t871/tcp\t\t\t# Software Upgrade Protocol server\nsupfiledbg\t1127/tcp\t\t# Software Upgrade Protocol debugging\n\n#\n# Services added for the Debian GNU/Linux distribution\n#\npoppassd\t106/tcp\t\t\t\t# Eudora\nmoira-db\t775/tcp\t\tmoira_db\t# Moira database\nmoira-update\t777/tcp\t\tmoira_update\t# Moira update protocol\nmoira-ureg\t779/udp\t\tmoira_ureg\t# Moira user registration\nspamd\t\t783/tcp\t\t\t\t# spamassassin daemon\nskkserv\t\t1178/tcp\t\t\t# skk jisho server port\npredict\t\t1210/udp\t\t\t# predict -- satellite tracking\nrmtcfg\t\t1236/tcp\t\t\t# Gracilis Packeten remote config server\nxtel\t\t1313/tcp\t\t\t# french minitel\nxtelw\t\t1314/tcp\t\t\t# french minitel\nzebrasrv\t2600/tcp\t\t\t# zebra service\nzebra\t\t2601/tcp\t\t\t# zebra vty\nripd\t\t2602/tcp\t\t\t# ripd vty (zebra)\nripngd\t\t2603/tcp\t\t\t# ripngd vty (zebra)\nospfd\t\t2604/tcp\t\t\t# ospfd vty (zebra)\nbgpd\t\t2605/tcp\t\t\t# bgpd vty (zebra)\nospf6d\t\t2606/tcp\t\t\t# ospf6d vty (zebra)\nospfapi\t\t2607/tcp\t\t\t# OSPF-API\nisisd\t\t2608/tcp\t\t\t# ISISd vty (zebra)\nfax\t\t4557/tcp\t\t\t# FAX transmission service (old)\nhylafax\t\t4559/tcp\t\t\t# HylaFAX client-server protocol (new)\nmunin\t\t4949/tcp\tlrrd\t\t# Munin\nrplay\t\t5555/udp\t\t\t# RPlay audio service\nnrpe\t\t5666/tcp\t\t\t# Nagios Remote Plugin Executor\nnsca\t\t5667/tcp\t\t\t# Nagios Agent - NSCA\ncanna\t\t5680/tcp\t\t\t# cannaserver\nsyslog-tls\t6514/tcp\t\t\t# Syslog over TLS [RFC5425]\nsane-port\t6566/tcp\tsane saned\t# SANE network scanner daemon\nircd\t\t6667/tcp\t\t\t# Internet Relay Chat\nzope-ftp\t8021/tcp\t\t\t# zope management by ftp\ntproxy\t\t8081/tcp\t\t\t# Transparent Proxy\nomniorb\t\t8088/tcp\t\t\t# OmniORB\nclc-build-daemon 8990/tcp\t\t\t# Common lisp build daemon\nxinetd\t\t9098/tcp\ngit\t\t9418/tcp\t\t\t# Git Version Control System\nzope\t\t9673/tcp\t\t\t# zope server\nwebmin\t\t10000/tcp\nkamanda\t\t10081/tcp\t\t\t# amanda backup services (Kerberos)\namandaidx\t10082/tcp\t\t\t# amanda backup services\namidxtape\t10083/tcp\t\t\t# amanda backup services\nsgi-cmsd\t17001/udp\t\t# Cluster membership services daemon\nsgi-crsd\t17002/udp\nsgi-gcd\t\t17003/udp\t\t\t# SGI Group membership daemon\nsgi-cad\t\t17004/tcp\t\t\t# Cluster Admin daemon\nbinkp\t\t24554/tcp\t\t\t# binkp fidonet protocol\nasp\t\t27374/tcp\t\t\t# Address Search Protocol\nasp\t\t27374/udp\ncsync2\t\t30865/tcp\t\t\t# cluster synchronization tool\ndircproxy\t57000/tcp\t\t\t# Detachable IRC Proxy\ntfido\t\t60177/tcp\t\t\t# fidonet EMSI over telnet\nfido\t\t60179/tcp\t\t\t# fidonet EMSI over TCP\n\n# Local services\n" + }, { "path": "/etc/ssl", "type": "directory", diff --git a/crates/native-sidecar/src/execution.rs b/crates/native-sidecar/src/execution.rs index 049d7565ed..6c88e309a2 100644 --- a/crates/native-sidecar/src/execution.rs +++ b/crates/native-sidecar/src/execution.rs @@ -14,13 +14,14 @@ use crate::protocol::{ GuestKernelResultResponse, GuestRuntimeKind, JavascriptChildProcessSpawnOptions, JavascriptChildProcessSpawnRequest, JavascriptDgramBindRequest, JavascriptDgramCreateSocketRequest, JavascriptDgramSendRequest, JavascriptDnsLookupRequest, - JavascriptDnsResolveRequest, JavascriptNetConnectRequest, JavascriptNetListenRequest, - JavascriptNetReserveTcpPortRequest, KillProcessRequest, OwnershipScope, ProcessExitedEvent, - ProcessOutputEvent, ProcessSnapshotEntry, ProcessSnapshotStatus, PtyResizedResponse, - QueueSnapshotEntry, RequestFrame, ResizePtyRequest, ResourceSnapshotResponse, ResponseFrame, - ResponsePayload, SidecarRequestPayload, SignalDispositionAction, SignalHandlerRegistration, - SocketStateEntry, StreamChannel, VmFetchRequest, VmFetchResponse, WasmPermissionTier, - WriteStdinRequest, + JavascriptDnsResolveRequest, JavascriptNetBindConnectedUnixRequest, + JavascriptNetConnectRequest, JavascriptNetListenRequest, JavascriptNetReserveTcpPortRequest, + JavascriptPosixSpawnFileAction, JavascriptSpawnHostNetFd, KillProcessRequest, OwnershipScope, + ProcessExitedEvent, ProcessOutputEvent, ProcessSnapshotEntry, ProcessSnapshotStatus, + PtyResizedResponse, QueueSnapshotEntry, RequestFrame, ResizePtyRequest, + ResourceSnapshotResponse, ResponseFrame, ResponsePayload, SidecarRequestPayload, + SignalDispositionAction, SignalHandlerRegistration, SocketStateEntry, StreamChannel, + VmFetchRequest, VmFetchResponse, WasmPermissionTier, WriteStdinRequest, }; use crate::service::{ audit_fields, dirname, emit_security_audit_event, emit_structured_event, javascript_error, @@ -31,23 +32,29 @@ use crate::service::{ use crate::state::{ ActiveCipherSession, ActiveDhSession, ActiveDiffieHellmanSession, ActiveEcdhSession, ActiveExecution, ActiveExecutionEvent, ActiveHttp2Server, ActiveHttp2Session, - ActiveHttp2Stream, ActiveHttpServer, ActiveMappedHostFd, ActiveProcess, ActiveSqliteDatabase, - ActiveSqliteStatement, ActiveTcpListener, ActiveTcpSocket, ActiveTlsState, ActiveTlsStream, - ActiveUdpSocket, ActiveUnixListener, ActiveUnixSocket, BridgeError, ExitedProcessSnapshot, - Http2BridgeEvent, Http2RuntimeSnapshot, Http2SessionCommand, Http2SessionSnapshot, - Http2SocketSnapshot, JavascriptHttpLoopbackTarget, JavascriptSocketEventPusher, - JavascriptSocketFamily, JavascriptSocketPathContext, JavascriptTcpListenerEvent, - JavascriptTcpSocketEvent, JavascriptTlsBridgeOptions, JavascriptTlsClientHello, - JavascriptTlsDataValue, JavascriptTlsMaterial, JavascriptUdpFamily, JavascriptUdpSocketEvent, + ActiveHttp2Stream, ActiveHttpServer, ActiveMappedHostFd, ActiveProcess, + ActiveRealIntervalTimer, ActiveSqliteDatabase, ActiveSqliteStatement, ActiveTcpListener, + ActiveTcpSocket, ActiveTlsState, ActiveTlsStream, ActiveUdpSocket, ActiveUnixListener, + ActiveUnixSocket, BridgeError, ExitedProcessSnapshot, GuestUnixAddress, + GuestUnixAddressRegistry, GuestUnixAddressRegistryEntry, GuestUnixConnectionState, + HostNetTransferDescription, HostNetTransferDescriptionRegistry, Http2BridgeEvent, + Http2RuntimeSnapshot, Http2SessionCommand, Http2SessionSnapshot, Http2SocketSnapshot, + JavascriptHttpLoopbackTarget, JavascriptSocketEventPusher, JavascriptSocketFamily, + JavascriptSocketPathContext, JavascriptTcpListenerEvent, JavascriptTcpSocketEvent, + JavascriptTlsBridgeOptions, JavascriptTlsClientHello, JavascriptTlsDataValue, + JavascriptTlsMaterial, JavascriptUdpFamily, JavascriptUdpSocketEvent, JavascriptUnixListenerEvent, KernelSocketReadinessEvent, KernelSocketReadinessRegistry, KernelSocketReadinessTarget, LoopbackTlsPendingWriteHandle, LoopbackTlsPendingWriteState, - NetworkResourceCounts, PendingTcpSocket, PendingUnixSocket, ProcNetEntry, ProcessEventEnvelope, - PythonHostSocket, ResolvedChildProcessExecution, ResolvedTcpConnectAddr, SharedBridge, - SharedSidecarRequestClient, SidecarKernel, SocketQueryKind, ToolExecution, VmDnsConfig, - VmListenPolicy, VmState, DEFAULT_JAVASCRIPT_NET_BACKLOG, EXECUTION_DRIVER_NAME, + NetworkResourceCounts, PendingKernelStdin, PendingTcpSocket, PendingUnixConnectionGuard, + PendingUnixSocket, ProcNetEntry, ProcessEventEnvelope, PythonHostSocket, + ResolvedChildProcessExecution, ResolvedTcpConnectAddr, ShadowNodeType, + ShadowSyncInventoryEntry, SharedBridge, SharedSidecarRequestClient, SidecarKernel, + SocketQueryKind, ToolExecution, UnixSocketReadState, VmDnsConfig, VmListenPolicy, + VmPendingByteBudget, VmState, DEFAULT_JAVASCRIPT_NET_BACKLOG, EXECUTION_DRIVER_NAME, EXECUTION_SANDBOX_ROOT_ENV, JAVASCRIPT_COMMAND, LOOPBACK_EXEMPT_PORTS_ENV, MAPPED_HOST_FD_START, PYTHON_COMMAND, TOOL_DRIVER_NAME, - VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY, WASM_COMMAND, WASM_STDIO_SYNC_RPC_ENV, + VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY, WASM_COMMAND, WASM_EXEC_COMMIT_RPC_ENV, + WASM_STDIO_SYNC_RPC_ENV, }; use crate::tools::{ format_tool_failure_output, is_tool_command, normalized_tool_command_name, @@ -100,7 +107,10 @@ use agentos_execution::{ use agentos_kernel::dns::{ DnsLookupPolicy, DnsRecordResolution, DnsResolutionSource as KernelDnsResolutionSource, }; -use agentos_kernel::kernel::{KernelProcessHandle, SpawnOptions, VirtualProcessOptions}; +use agentos_kernel::fd_table::TransferredFd; +use agentos_kernel::kernel::{ + FdTransferRequest, KernelProcessHandle, ReceivedFdRight, SpawnOptions, VirtualProcessOptions, +}; pub(crate) use agentos_kernel::network_policy::format_tcp_resource; use agentos_kernel::network_policy::{ is_loopback_ip, loopback_cidr, restricted_non_loopback_ip_range, @@ -109,13 +119,17 @@ use agentos_kernel::permissions::NetworkOperation; use agentos_kernel::poll::{PollEvents, PollFd, PollTargetEntry, POLLERR, POLLHUP, POLLIN}; use agentos_kernel::process_table::{ProcessStatus, WaitPidFlags, SIGKILL, SIGTERM}; use agentos_kernel::pty::MAX_PTY_BUFFER_BYTES; -use agentos_kernel::resource_accounting::ResourceLimits; +use agentos_kernel::resource_accounting::{ + ResourceLimits, DEFAULT_BLOCKING_READ_TIMEOUT_MS, DEFAULT_MAX_SOCKET_BUFFERED_BYTES, +}; use agentos_kernel::root_fs::RootFilesystemMode; use agentos_kernel::socket_table::{ reset_socket_read_trace, set_socket_read_trace_enabled, socket_read_trace_snapshot, InetSocketAddress, SocketDomain, SocketId, SocketShutdown as KernelSocketShutdown, SocketSpec, SocketState, SocketType, }; +use agentos_kernel::vfs::{VirtualTimeSpec, VirtualUtimeSpec}; +use agentos_native_sidecar_core::ca::CA_CERTIFICATES_GUEST_PATH; use agentos_native_sidecar_core::{ apply_process_signal_state_update, bound_udp_snapshot_response, bridge_buffer_value, decode_base64, decode_bridge_buffer_value, decode_encoded_bytes_value, encoded_bytes_value, @@ -129,6 +143,9 @@ use agentos_native_sidecar_core::{ SharedProcessSnapshotEntry, SharedProcessSnapshotStatus, SidecarCoreError, VM_FETCH_BUFFER_LIMIT_BYTES, }; +use nix::sys::socket::{ + bind as bind_socket, connect as connect_socket, send as send_socket, MsgFlags, UnixAddr, +}; use rusqlite::types::ValueRef as SqliteValueRef; use rusqlite::{ Connection as SqliteConnection, OpenFlags as SqliteOpenFlags, Statement as SqliteStatement, @@ -145,7 +162,7 @@ use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; use sha1::Sha1; use sha2::{digest::Digest, Sha224, Sha256, Sha384, Sha512}; -use socket2::{SockRef, TcpKeepalive}; +use socket2::{Domain, SockAddr, SockRef, Socket, TcpKeepalive, Type}; use std::collections::VecDeque; use std::collections::{BTreeMap, BTreeSet}; use std::fmt; @@ -155,14 +172,16 @@ use std::net::{ IpAddr, Ipv4Addr, Ipv6Addr, Shutdown, SocketAddr, TcpListener, TcpStream, ToSocketAddrs, UdpSocket, }; -use std::os::fd::{AsFd, BorrowedFd}; +use std::os::fd::{AsFd, AsRawFd, BorrowedFd}; +#[cfg(target_os = "linux")] +use std::os::linux::net::SocketAddrExt; use std::os::unix::fs::{MetadataExt, PermissionsExt}; use std::os::unix::net::{SocketAddr as UnixSocketAddr, UnixListener, UnixStream}; use std::path::{Path, PathBuf}; use std::pin::Pin; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; use std::sync::mpsc::{self, RecvTimeoutError, Sender}; -use std::sync::{Arc, Mutex, OnceLock, Weak}; +use std::sync::{Arc, Condvar, Mutex, OnceLock, Weak}; use std::thread; use std::time::{Duration, Instant}; use tokio::io::{AsyncRead, AsyncWrite}; @@ -179,12 +198,18 @@ const PYTHON_PYODIDE_CACHE_GUEST_ROOT: &str = "/__agentos_pyodide_cache"; const TCP_SOCKET_POLL_TIMEOUT: Duration = Duration::from_millis(100); const TLS_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5); const LOOPBACK_TLS_HANDSHAKE_POLL_TIMEOUT: Duration = Duration::from_millis(1); +// Descendant WASM processes share one cooperative event dispatcher. Batch a +// small, bounded run of invisible sync RPCs before returning to the parent +// pump: yielding after every syscall makes process-heavy tools unusably slow, +// while an unbounded batch can starve a sibling that must make the child +// runnable. The time bound models a short scheduler quantum; the count bound +// also guarantees fairness when individual RPCs are exceptionally cheap. +const DESCENDANT_INTERNAL_RPC_QUANTUM: Duration = Duration::from_millis(10); +const MAX_DESCENDANT_INTERNAL_RPCS_PER_POLL: usize = 1024; const LOOPBACK_TLS_PENDING_WRITE_BUFFER_BYTES: usize = 4 * 1024 * 1024; const LOOPBACK_TLS_PENDING_WRITE_WARNING_BYTES: usize = LOOPBACK_TLS_PENDING_WRITE_BUFFER_BYTES * 4 / 5; const HTTP_LOOPBACK_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); -const PROCESS_EXIT_DRAIN_INITIAL_QUIET: Duration = Duration::from_millis(1); -const PROCESS_EXIT_DRAIN_TRAILING_QUIET: Duration = Duration::from_millis(25); struct NetTcpTraceCounters { socket_read_calls: AtomicU64, @@ -286,25 +311,57 @@ fn http_loopback_request_timeout() -> Duration { /// stall as the fixed sleeps this replaced, and only acceptable because the /// guest net path always polls with wait == 0. Keep deadlines bounded and do /// not add wait > 0 callers on paths that service concurrent VM traffic. -fn wait_fd_readable_until(fd: BorrowedFd<'_>, deadline: Instant) -> bool { - let remaining = deadline.saturating_duration_since(Instant::now()); - if remaining.is_zero() { - return false; +fn wait_fd_readable_until(fd: BorrowedFd<'_>, deadline: Instant) -> std::io::Result { + loop { + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Ok(false); + } + + let timeout_ms = remaining.as_millis().saturating_add(u128::from( + !remaining.subsec_nanos().is_multiple_of(1_000_000), + )); + let timeout = + PollTimeout::try_from(timeout_ms.min(i32::MAX as u128)).unwrap_or(PollTimeout::MAX); + let mut fds = [NixPollFd::new(fd, PollFlags::POLLIN)]; + match poll(&mut fds, timeout) { + Ok(0) => return Ok(false), + Ok(_) => { + return Ok(fds[0] + .revents() + .unwrap_or_else(PollFlags::empty) + .intersects(PollFlags::POLLIN | PollFlags::POLLHUP | PollFlags::POLLERR)); + } + Err(nix::errno::Errno::EINTR) => continue, + Err(error) => return Err(std::io::Error::from_raw_os_error(error as i32)), + } } +} - let timeout_ms = remaining.as_millis().saturating_add(u128::from( - !remaining.subsec_nanos().is_multiple_of(1_000_000), - )); - let timeout = - PollTimeout::try_from(timeout_ms.min(i32::MAX as u128)).unwrap_or(PollTimeout::MAX); - let mut fds = [NixPollFd::new(fd, PollFlags::POLLIN)]; - match poll(&mut fds, timeout) { - Ok(0) => false, - Ok(_) => fds[0] - .revents() - .unwrap_or_else(PollFlags::empty) - .intersects(PollFlags::POLLIN | PollFlags::POLLHUP | PollFlags::POLLERR), - Err(_) => true, +fn wait_fd_writable_until(fd: BorrowedFd<'_>, deadline: Instant) -> std::io::Result { + loop { + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Ok(false); + } + + let timeout_ms = remaining.as_millis().saturating_add(u128::from( + !remaining.subsec_nanos().is_multiple_of(1_000_000), + )); + let timeout = + PollTimeout::try_from(timeout_ms.min(i32::MAX as u128)).unwrap_or(PollTimeout::MAX); + let mut fds = [NixPollFd::new(fd, PollFlags::POLLOUT)]; + match poll(&mut fds, timeout) { + Ok(0) => return Ok(false), + Ok(_) => { + return Ok(fds[0] + .revents() + .unwrap_or_else(PollFlags::empty) + .intersects(PollFlags::POLLOUT | PollFlags::POLLHUP | PollFlags::POLLERR)); + } + Err(nix::errno::Errno::EINTR) => continue, + Err(error) => return Err(std::io::Error::from_raw_os_error(error as i32)), + } } } @@ -460,6 +517,51 @@ impl ServerCertVerifier for InsecureTlsVerifier { } } +/// Ownership of VM-wide retained-byte accounting for an event temporarily +/// removed from a process queue. Keeping this reservation alive across a +/// capacity check prevents a concurrent producer from consuming the bytes an +/// already-accepted event needs if that event must be put back. +#[derive(Debug)] +struct PendingExecutionEventReservation { + budget: Arc, + bytes: usize, +} + +impl PendingExecutionEventReservation { + fn transfer_to_queue(mut self) { + self.bytes = 0; + } +} + +impl Drop for PendingExecutionEventReservation { + fn drop(&mut self) { + self.budget.release(self.bytes); + } +} + +#[derive(Debug)] +struct PolledExecutionEvent { + event: ActiveExecutionEvent, + reservation: Option, +} + +impl PolledExecutionEvent { + fn unreserved(event: ActiveExecutionEvent) -> Self { + Self { + event, + reservation: None, + } + } + + fn event(&self) -> &ActiveExecutionEvent { + &self.event + } + + fn into_event(self) -> ActiveExecutionEvent { + self.event + } +} + impl ActiveProcess { pub(crate) fn new( kernel_pid: u32, @@ -467,10 +569,20 @@ impl ActiveProcess { runtime: GuestRuntimeKind, execution: ActiveExecution, ) -> Self { + let start_real_timer = runtime == GuestRuntimeKind::WebAssembly; Self { kernel_pid, kernel_handle, kernel_stdin_writer_fd: None, + pending_kernel_stdin: PendingKernelStdin::default(), + pending_kernel_stdin_gauge: queue_tracker::register_queue( + queue_tracker::TrackedLimit::PendingKernelStdinBytes, + agentos_native_sidecar_core::limits::DEFAULT_PROCESS_PENDING_STDIN_BYTES, + ), + vm_pending_stdin_bytes_budget: VmPendingByteBudget::new( + agentos_native_sidecar_core::limits::DEFAULT_PROCESS_PENDING_STDIN_BYTES, + queue_tracker::TrackedLimit::PendingKernelStdinBytes, + ), tty_master_fd: None, runtime, detached: false, @@ -482,7 +594,32 @@ impl ActiveProcess { mapped_host_fds: BTreeMap::new(), next_mapped_host_fd: MAPPED_HOST_FD_START, pending_execution_events: VecDeque::new(), + pending_execution_event_bytes: 0, + pending_execution_event_count_limit: + agentos_native_sidecar_core::limits::DEFAULT_PROCESS_PENDING_EVENT_COUNT, + pending_execution_event_bytes_limit: + agentos_native_sidecar_core::limits::DEFAULT_PROCESS_PENDING_EVENT_BYTES, + pending_execution_event_count_gauge: queue_tracker::register_queue( + queue_tracker::TrackedLimit::PendingExecutionEvents, + agentos_native_sidecar_core::limits::DEFAULT_PROCESS_PENDING_EVENT_COUNT, + ), + pending_execution_event_bytes_gauge: queue_tracker::register_queue( + queue_tracker::TrackedLimit::PendingExecutionEventBytes, + agentos_native_sidecar_core::limits::DEFAULT_PROCESS_PENDING_EVENT_BYTES, + ), + vm_pending_event_bytes_budget: VmPendingByteBudget::new( + agentos_native_sidecar_core::limits::DEFAULT_PROCESS_PENDING_EVENT_BYTES, + queue_tracker::TrackedLimit::PendingExecutionEventBytes, + ), pending_self_signal_exit: None, + exit_signal: None, + exit_core_dumped: false, + pending_wasm_signals: BTreeSet::new(), + pending_wasm_signals_gauge: queue_tracker::register_queue( + queue_tracker::TrackedLimit::PendingWasmSignals, + 64, + ), + real_interval_timer: ActiveRealIntervalTimer::new(start_real_timer), child_processes: BTreeMap::new(), next_child_process_id: 0, http_servers: BTreeMap::new(), @@ -521,10 +658,245 @@ impl ActiveProcess { &mut self, event: ActiveExecutionEvent, ) -> Result<(), SidecarError> { - if self.pending_execution_events.len() >= MAX_PROCESS_EVENT_QUEUE { - return Err(process_event_queue_overflow_error()); + self.try_queue_pending_execution_event(event) + .map_err(|(error, _event)| error) + } + + fn try_queue_pending_execution_event( + &mut self, + event: ActiveExecutionEvent, + ) -> Result<(), (SidecarError, ActiveExecutionEvent)> { + let event_bytes = event.retained_bytes(); + if self.pending_execution_events.len() >= self.pending_execution_event_count_limit { + return Err(( + SidecarError::InvalidState(format!( + "process execution event queue exceeded {} events (limits.process.pendingEventCount); raise limits.process.pendingEventCount", + self.pending_execution_event_count_limit + )), + event, + )); + } + if self + .pending_execution_event_bytes + .saturating_add(event_bytes) + > self.pending_execution_event_bytes_limit + { + return Err(( + SidecarError::InvalidState(format!( + "process execution event queue exceeded {} retained bytes (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes", + self.pending_execution_event_bytes_limit + )), + event, + )); } + if !self.vm_pending_event_bytes_budget.try_reserve(event_bytes) { + return Err(( + SidecarError::InvalidState(format!( + "VM process execution event queues exceeded {} retained bytes \ + (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes", + self.vm_pending_event_bytes_budget.limit() + )), + event, + )); + } + self.pending_execution_event_bytes = self + .pending_execution_event_bytes + .saturating_add(event_bytes); self.pending_execution_events.push_back(event); + self.pending_execution_event_count_gauge + .observe_depth(self.pending_execution_events.len()); + self.pending_execution_event_bytes_gauge + .observe_depth(self.pending_execution_event_bytes); + Ok(()) + } + + fn try_queue_pending_execution_envelope( + &mut self, + envelope: ProcessEventEnvelope, + ) -> Result<(), (SidecarError, ProcessEventEnvelope)> { + let ProcessEventEnvelope { + connection_id, + session_id, + vm_id, + process_id, + event, + } = envelope; + self.try_queue_pending_execution_event(event) + .map_err(|(error, event)| { + ( + error, + ProcessEventEnvelope { + connection_id, + session_id, + vm_id, + process_id, + event, + }, + ) + }) + } + + fn lease_pending_execution_event(&mut self) -> Option { + let event = self.pending_execution_events.pop_front()?; + let event_bytes = event.retained_bytes(); + self.pending_execution_event_bytes = self + .pending_execution_event_bytes + .saturating_sub(event_bytes); + self.pending_execution_event_count_gauge + .observe_depth(self.pending_execution_events.len()); + self.pending_execution_event_bytes_gauge + .observe_depth(self.pending_execution_event_bytes); + Some(PolledExecutionEvent { + event, + reservation: Some(PendingExecutionEventReservation { + budget: Arc::clone(&self.vm_pending_event_bytes_budget), + bytes: event_bytes, + }), + }) + } + + #[cfg(test)] + pub(crate) fn pop_pending_execution_event(&mut self) -> Option { + self.lease_pending_execution_event() + .map(PolledExecutionEvent::into_event) + } + + fn requeue_pending_execution_event( + &mut self, + polled: PolledExecutionEvent, + ) -> Result<(), SidecarError> { + let PolledExecutionEvent { event, reservation } = polled; + let event_bytes = event.retained_bytes(); + if self.pending_execution_events.len() >= self.pending_execution_event_count_limit { + return Err(SidecarError::InvalidState(format!( + "process execution event queue exceeded {} events (limits.process.pendingEventCount); raise limits.process.pendingEventCount", + self.pending_execution_event_count_limit + ))); + } + if self + .pending_execution_event_bytes + .saturating_add(event_bytes) + > self.pending_execution_event_bytes_limit + { + return Err(SidecarError::InvalidState(format!( + "process execution event queue exceeded {} retained bytes (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes", + self.pending_execution_event_bytes_limit + ))); + } + + let reservation = match reservation { + Some(reservation) => { + if reservation.bytes != event_bytes + || !Arc::ptr_eq(&reservation.budget, &self.vm_pending_event_bytes_budget) + { + return Err(SidecarError::InvalidState(String::from( + "process execution event reservation no longer matches its VM queue; event requeue aborted", + ))); + } + Some(reservation) + } + None => { + if !self.vm_pending_event_bytes_budget.try_reserve(event_bytes) { + return Err(SidecarError::InvalidState(format!( + "VM process execution event queues exceeded {} retained bytes \ + (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes", + self.vm_pending_event_bytes_budget.limit() + ))); + } + None + } + }; + + self.pending_execution_event_bytes = self + .pending_execution_event_bytes + .saturating_add(event_bytes); + self.pending_execution_events.push_front(event); + self.pending_execution_event_count_gauge + .observe_depth(self.pending_execution_events.len()); + self.pending_execution_event_bytes_gauge + .observe_depth(self.pending_execution_event_bytes); + if let Some(reservation) = reservation { + reservation.transfer_to_queue(); + } + Ok(()) + } + + async fn poll_execution_event( + &mut self, + timeout: Duration, + ) -> Result, SidecarError> { + if let ActiveExecution::Tool(execution) = &mut self.execution { + return poll_tool_process_event_leased(execution); + } + self.execution + .poll_event(timeout) + .await + .map(|event| event.map(PolledExecutionEvent::unreserved)) + } + + fn poll_execution_event_blocking( + &mut self, + timeout: Duration, + ) -> Result, SidecarError> { + if let ActiveExecution::Tool(execution) = &mut self.execution { + return poll_tool_process_event_leased(execution); + } + self.execution + .poll_event_blocking(timeout) + .map(|event| event.map(PolledExecutionEvent::unreserved)) + } + + pub(crate) fn with_process_event_limits( + mut self, + limits: &agentos_native_sidecar_core::limits::ProcessLimits, + ) -> Self { + self.pending_execution_event_count_limit = limits.pending_event_count; + self.pending_execution_event_bytes_limit = limits.pending_event_bytes; + if let ActiveExecution::Tool(execution) = &self.execution { + execution + .pending_event_count_limit + .store(limits.pending_event_count, Ordering::Release); + execution + .pending_event_bytes_limit + .store(limits.pending_event_bytes, Ordering::Release); + } + self.pending_kernel_stdin_gauge = queue_tracker::register_queue( + queue_tracker::TrackedLimit::PendingKernelStdinBytes, + limits.pending_stdin_bytes, + ); + self.pending_execution_event_count_gauge = queue_tracker::register_queue( + queue_tracker::TrackedLimit::PendingExecutionEvents, + limits.pending_event_count, + ); + self.pending_execution_event_bytes_gauge = queue_tracker::register_queue( + queue_tracker::TrackedLimit::PendingExecutionEventBytes, + limits.pending_event_bytes, + ); + self + } + + pub(crate) fn with_vm_pending_byte_budgets( + mut self, + stdin: Arc, + events: Arc, + ) -> Self { + debug_assert_eq!(self.pending_kernel_stdin.total, 0); + debug_assert_eq!(self.pending_execution_event_bytes, 0); + self.vm_pending_stdin_bytes_budget = stdin; + self.vm_pending_event_bytes_budget = Arc::clone(&events); + if let ActiveExecution::Tool(execution) = &mut self.execution { + if !Arc::ptr_eq(&execution.vm_pending_event_bytes_budget, &events) { + debug_assert_eq!(execution.pending_event_bytes.load(Ordering::Acquire), 0); + execution.vm_pending_event_bytes_budget = events; + } + } + self + } + + pub(crate) fn queue_pending_wasm_signal(&mut self, signal: i32) -> Result<(), SidecarError> { + self.pending_wasm_signals.insert(signal); + self.pending_wasm_signals_gauge + .observe_depth(self.pending_wasm_signals.len()); Ok(()) } @@ -637,123 +1009,404 @@ impl ActiveProcess { format!("udp-socket-{}", self.next_udp_socket_id) } + #[allow(dead_code)] pub(crate) fn network_resource_counts(&self) -> NetworkResourceCounts { - let mut counts = NetworkResourceCounts { - sockets: self.http_servers.len() - + self.tcp_listeners.len() - + self.tcp_sockets.len() - + self.unix_listeners.len() - + self.unix_sockets.len() - + self.udp_sockets.len() - + self.python_sockets.len(), - connections: self.tcp_sockets.len() + self.unix_sockets.len(), - }; - if let Ok(http2) = self.http2.shared.lock() { - counts.sockets += http2.servers.len() + http2.sessions.len(); - counts.connections += http2.sessions.len(); - } - - for child in self.child_processes.values() { - let child_counts = child.network_resource_counts(); - counts.sockets += child_counts.sockets; - counts.connections += child_counts.connections; - } - + let mut counts = NetworkResourceCounts::default(); + let mut descriptions = BTreeMap::new(); + self.collect_network_resource_counts(false, &mut descriptions, &mut counts); + add_host_net_description_counts(&descriptions, &mut counts); counts } - fn sidecar_only_network_resource_counts(&self) -> NetworkResourceCounts { - let mut counts = NetworkResourceCounts { - sockets: self.http_servers.len() - + self - .tcp_listeners - .values() - .filter(|listener| listener.kernel_socket_id.is_none()) - .count() - + self - .tcp_sockets - .values() - .filter(|socket| socket.kernel_socket_id.is_none()) - .count() - + self.unix_listeners.len() - + self.unix_sockets.len() - + self - .udp_sockets - .values() - .filter(|socket| socket.kernel_socket_id.is_none()) - .count() - + self.python_sockets.len(), - connections: self - .tcp_sockets - .values() - .filter(|socket| socket.kernel_socket_id.is_none()) - .count() - + self.unix_sockets.len(), - }; - if let Ok(http2) = self.http2.shared.lock() { - counts.sockets += http2.servers.len() + http2.sessions.len(); - counts.connections += http2.sessions.len(); - } + fn collect_network_resource_counts( + &self, + sidecar_only: bool, + descriptions: &mut BTreeMap, + counts: &mut NetworkResourceCounts, + ) { + counts.sockets += self.http_servers.len() + self.python_sockets.len(); + let http2 = self + .http2 + .shared + .lock() + .unwrap_or_else(|error| error.into_inner()); + counts.sockets += http2.servers.len() + http2.sessions.len(); + counts.connections += http2.sessions.len(); + drop(http2); + for listener in self.tcp_listeners.values() { + if !sidecar_only || listener.kernel_socket_id.is_none() { + descriptions + .entry(Arc::as_ptr(&listener.description_handles) as usize) + .or_insert(false); + } + } + for socket in self.tcp_sockets.values() { + if !sidecar_only || socket.kernel_socket_id.is_none() { + descriptions.insert(Arc::as_ptr(&socket.description_handles) as usize, true); + } + } + for listener in self.unix_listeners.values() { + descriptions + .entry(Arc::as_ptr(&listener.description_handles) as usize) + .or_insert(false); + } + for socket in self.unix_sockets.values() { + descriptions.insert(Arc::as_ptr(&socket.description_handles) as usize, true); + } + for socket in self.udp_sockets.values() { + if !sidecar_only || socket.kernel_socket_id.is_none() { + descriptions + .entry(Arc::as_ptr(&socket.description_handles) as usize) + .or_insert(false); + } + } for child in self.child_processes.values() { - let child_counts = child.sidecar_only_network_resource_counts(); - counts.sockets += child_counts.sockets; - counts.connections += child_counts.connections; + child.collect_network_resource_counts(sidecar_only, descriptions, counts); } - - counts } } -fn poll_tool_process_event( - execution: &ToolExecution, -) -> Result, SidecarError> { - let event = execution - .pending_events - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()) - .pop_front(); - if event.is_some() { - return Ok(event); - } - if execution.events_overflowed.load(Ordering::Relaxed) { - return Err(process_event_queue_overflow_error()); +impl Drop for ActiveProcess { + fn drop(&mut self) { + let pending_stdin_bytes = self.pending_kernel_stdin.total; + self.vm_pending_stdin_bytes_budget + .release(pending_stdin_bytes); + self.pending_kernel_stdin.clear(); + self.pending_kernel_stdin_gauge.observe_depth(0); + + self.vm_pending_event_bytes_budget + .release(self.pending_execution_event_bytes); + self.pending_execution_events.clear(); + self.pending_execution_event_bytes = 0; + self.pending_execution_event_count_gauge.observe_depth(0); + self.pending_execution_event_bytes_gauge.observe_depth(0); } - Ok(None) } -fn descendant_pending_execution_event_capacity( - root: &ActiveProcess, - child_path: &[&str], -) -> Option { - let mut child = root; - for child_process_id in child_path { - child = child.child_processes.get(*child_process_id)?; +#[cfg(test)] +mod pending_event_reservation_tests { + use super::{ + queue_tracker, send_tool_process_event, ActiveExecution, ActiveExecutionEvent, + ActiveProcess, GuestRuntimeKind, SidecarKernel, SignalDispositionAction, + SignalHandlerRegistration, ToolExecution, VmPendingByteBudget, EXECUTION_DRIVER_NAME, + WASM_COMMAND, + }; + use agentos_kernel::command_registry::CommandDriver; + use agentos_kernel::kernel::{KernelVmConfig, SpawnOptions}; + use agentos_kernel::mount_table::MountTable; + use agentos_kernel::permissions::Permissions; + use agentos_kernel::vfs::MemoryFileSystem; + use std::sync::Arc; + use std::thread; + + #[test] + fn checked_out_event_keeps_vm_bytes_reserved_until_requeue_or_consumption() { + let event = ActiveExecutionEvent::Stdout(vec![0x5a; 32]); + let event_bytes = event.retained_bytes(); + let budget = VmPendingByteBudget::new( + event_bytes, + queue_tracker::TrackedLimit::PendingExecutionEventBytes, + ); + + let mut config = KernelVmConfig::new("vm-pending-event-reservation"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .expect("register execution driver"); + let handle = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn process"); + let mut process = ActiveProcess::new( + handle.pid(), + handle, + GuestRuntimeKind::WebAssembly, + ActiveExecution::Tool(ToolExecution::default()), + ) + .with_vm_pending_byte_budgets( + VmPendingByteBudget::new( + event_bytes, + queue_tracker::TrackedLimit::PendingKernelStdinBytes, + ), + Arc::clone(&budget), + ); + + process + .queue_pending_execution_event(event) + .expect("initial event fits the VM aggregate"); + let checked_out = process + .lease_pending_execution_event() + .expect("pop accepted event with its reservation"); + + let sibling_budget = Arc::clone(&budget); + let sibling = thread::spawn(move || sibling_budget.try_reserve(event_bytes)); + assert!( + !sibling.join().expect("sibling producer thread"), + "a sibling producer must not steal a checked-out event's reservation" + ); + + process + .requeue_pending_execution_event(checked_out) + .expect("requeue must reuse the existing reservation"); + let consumed = process + .pop_pending_execution_event() + .expect("requeued event remains available"); + assert!(matches!(consumed, ActiveExecutionEvent::Stdout(bytes) if bytes == vec![0x5a; 32])); + + assert!( + budget.try_reserve(event_bytes), + "real consumption must release the VM aggregate reservation" + ); + budget.release(event_bytes); + + let ActiveExecution::Tool(tool) = &process.execution else { + panic!("test process must use the tool event queue"); + }; + assert!(send_tool_process_event( + &tool.cancelled, + &tool.pending_events, + &tool.event_overflow_reason, + &tool.pending_event_bytes, + &tool.pending_event_count_limit, + &tool.pending_event_bytes_limit, + &tool.vm_pending_event_bytes_budget, + ActiveExecutionEvent::Stdout(vec![0x6b; 32]), + )); + let checked_out = process + .poll_execution_event_blocking(std::time::Duration::ZERO) + .expect("poll tool event queue") + .expect("tool event available with its reservation"); + let sibling_budget = Arc::clone(&budget); + let sibling = thread::spawn(move || sibling_budget.try_reserve(event_bytes)); + assert!( + !sibling.join().expect("second sibling producer thread"), + "polling the tool queue must also retain its aggregate reservation" + ); + process + .requeue_pending_execution_event(checked_out) + .expect("tool event requeue must transfer its existing reservation"); + assert!(matches!( + process.pop_pending_execution_event(), + Some(ActiveExecutionEvent::Stdout(bytes)) if bytes == vec![0x6b; 32] + )); + assert!(budget.try_reserve(event_bytes)); + budget.release(event_bytes); + + // Detached internal events are consumed by a handler that can resume + // the guest immediately. The lease conversion must free the current + // event before that handler lets the producer enqueue its response. + let internal_event = ActiveExecutionEvent::SignalState { + signal: 10, + registration: SignalHandlerRegistration { + action: SignalDispositionAction::User, + mask: Vec::new(), + flags: 0, + }, + }; + let internal_bytes = internal_event.retained_bytes(); + let internal_budget = VmPendingByteBudget::new( + internal_bytes, + queue_tracker::TrackedLimit::PendingExecutionEventBytes, + ); + process = process.with_vm_pending_byte_budgets( + VmPendingByteBudget::new( + internal_bytes, + queue_tracker::TrackedLimit::PendingKernelStdinBytes, + ), + Arc::clone(&internal_budget), + ); + process + .queue_pending_execution_event(internal_event) + .expect("internal event fills aggregate budget"); + let consumed_internal = process + .lease_pending_execution_event() + .expect("lease internal event") + .into_event(); + assert!(matches!( + consumed_internal, + ActiveExecutionEvent::SignalState { signal: 10, .. } + )); + assert!( + internal_budget.try_reserve(internal_bytes), + "consuming an internal event must release before its handler resumes the producer" + ); + internal_budget.release(internal_bytes); + + process.kernel_handle.finish(0); + kernel.waitpid(process.kernel_pid).expect("reap process"); } - Some(MAX_PROCESS_EVENT_QUEUE.saturating_sub(child.pending_execution_events.len())) } -fn poll_child_execution_after_exit( - child: &mut ActiveProcess, - wait: Duration, -) -> Result, SidecarError> { - match child.execution.poll_event_blocking(wait) { - Ok(event) => Ok(event), - Err(SidecarError::Execution(message)) - if child.runtime == GuestRuntimeKind::WebAssembly - && message == WasmExecutionError::EventChannelClosed.to_string() => - { - Ok(None) - } - Err(error) => Err(error), +impl ToolExecution { + pub(crate) fn with_vm_pending_event_bytes_budget( + mut self, + budget: Arc, + ) -> Self { + debug_assert_eq!(self.pending_event_bytes.load(Ordering::Acquire), 0); + debug_assert!(self + .pending_events + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .is_empty()); + self.vm_pending_event_bytes_budget = budget; + self } } -fn closed_javascript_event_channel(message: &str) -> bool { - message == "guest JavaScript event channel closed unexpectedly" +impl Drop for ToolExecution { + fn drop(&mut self) { + // Stop a background callback producer before reclaiming the queue. The + // producer checks this flag while holding the same queue lock, so it + // cannot enqueue after the retained-byte total is released here. + self.cancelled.store(true, Ordering::Release); + let mut pending_events = self + .pending_events + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + pending_events.clear(); + let pending_bytes = self.pending_event_bytes.swap(0, Ordering::AcqRel); + self.vm_pending_event_bytes_budget.release(pending_bytes); + } } -fn closed_python_event_channel(message: &str) -> bool { +fn add_host_net_description_counts( + descriptions: &BTreeMap, + counts: &mut NetworkResourceCounts, +) { + counts.sockets += descriptions.len(); + counts.connections += descriptions + .values() + .filter(|connected| **connected) + .count(); +} + +fn add_live_host_net_transfer_descriptions( + registry: &HostNetTransferDescriptionRegistry, + descriptions: &mut BTreeMap, +) { + let mut transfers = registry.lock().unwrap_or_else(|error| error.into_inner()); + transfers.retain(|description_id, transfer| { + let alive = transfer.handles.upgrade().is_some(); + if alive { + descriptions + .entry(*description_id) + .and_modify(|connected| *connected |= transfer.connected) + .or_insert(transfer.connected); + } + alive + }); +} + +fn process_network_resource_counts_with_transfers( + kernel: &SidecarKernel, + process: &ActiveProcess, + registry: &HostNetTransferDescriptionRegistry, +) -> NetworkResourceCounts { + let snapshot = kernel.resource_snapshot(); + let mut counts = NetworkResourceCounts { + sockets: snapshot.sockets, + connections: snapshot.socket_connections, + }; + let mut descriptions = BTreeMap::new(); + process.collect_network_resource_counts(true, &mut descriptions, &mut counts); + add_live_host_net_transfer_descriptions(registry, &mut descriptions); + add_host_net_description_counts(&descriptions, &mut counts); + counts +} + +impl ActiveExecutionEvent { + pub(crate) fn retained_bytes(&self) -> usize { + match self { + Self::Stdout(bytes) | Self::Stderr(bytes) => { + std::mem::size_of::().saturating_add(bytes.len()) + } + // Internal RPC events are serviced eagerly rather than retained; + // account a conservative fixed envelope if they are briefly + // deferred. Their wire payload is independently frame-bounded. + Self::JavascriptSyncRpcRequest(_) | Self::PythonVfsRpcRequest(_) => 4 * 1024, + Self::SignalState { .. } | Self::Exited(_) => std::mem::size_of::(), + } + } +} + +impl ProcessEventEnvelope { + pub(crate) fn retained_bytes(&self) -> usize { + self.connection_id + .len() + .saturating_add(self.session_id.len()) + .saturating_add(self.vm_id.len()) + .saturating_add(self.process_id.len()) + .saturating_add(self.event.retained_bytes()) + } +} + +fn poll_tool_process_event( + execution: &ToolExecution, +) -> Result, SidecarError> { + poll_tool_process_event_leased(execution) + .map(|event| event.map(PolledExecutionEvent::into_event)) +} + +fn poll_tool_process_event_leased( + execution: &ToolExecution, +) -> Result, SidecarError> { + let event = execution + .pending_events + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .pop_front(); + if let Some(event) = event { + let event_bytes = event.retained_bytes(); + execution + .pending_event_bytes + .fetch_sub(event_bytes, Ordering::AcqRel); + return Ok(Some(PolledExecutionEvent { + event, + reservation: Some(PendingExecutionEventReservation { + budget: Arc::clone(&execution.vm_pending_event_bytes_budget), + bytes: event_bytes, + }), + })); + } + if let Some(reason) = execution + .event_overflow_reason + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) + .clone() + { + return Err(SidecarError::InvalidState(reason)); + } + Ok(None) +} + +fn descendant_pending_execution_event_capacity( + root: &ActiveProcess, + child_path: &[&str], +) -> Option { + let mut child = root; + for child_process_id in child_path { + child = child.child_processes.get(*child_process_id)?; + } + Some( + child + .pending_execution_event_count_limit + .saturating_sub(child.pending_execution_events.len()), + ) +} + +fn closed_javascript_event_channel(message: &str) -> bool { + message == "guest JavaScript event channel closed unexpectedly" +} + +fn closed_python_event_channel(message: &str) -> bool { message == "guest Python event channel closed unexpectedly" } @@ -812,6 +1465,26 @@ fn is_javascript_child_process_gone_error(error: &SidecarError) -> bool { ) } +fn missing_javascript_child_kill_result( + next_child_process_id: usize, + child_process_id: &str, +) -> Result<(), SidecarError> { + let previously_allocated = child_process_id + .strip_prefix("child-") + .and_then(|value| value.parse::().ok()) + .is_some_and(|sequence| { + sequence != 0 + && sequence <= next_child_process_id + && child_process_id == format!("child-{sequence}") + }); + if previously_allocated { + return Ok(()); + } + Err(SidecarError::InvalidState(format!( + "unknown child process {child_process_id} during kill" + ))) +} + fn loopback_tls_transport_registry( ) -> &'static Mutex>> { static REGISTRY: OnceLock< @@ -946,12 +1619,17 @@ impl LoopbackTlsPendingWriteHandle { } fn failure_error(&self) -> SidecarError { - let message = self - .state - .lock() - .ok() - .and_then(|state| state.failure_message.clone()) - .unwrap_or_else(|| String::from("loopback TLS pending write failed")); + let message = match self.state.lock() { + Ok(state) => state + .failure_message + .clone() + .unwrap_or_else(|| String::from("loopback TLS pending write failed")), + Err(_) => { + return SidecarError::InvalidState(String::from( + "loopback TLS pending write state lock poisoned", + )); + } + }; SidecarError::Execution(message) } @@ -1477,807 +2155,1461 @@ struct JavascriptHttp2SyncRpcServiceRequest<'a, B> { network_counts: NetworkResourceCounts, } -impl ActiveTcpSocket { - fn connect(request: ActiveTcpConnectRequest<'_, B>) -> Result - where - B: NativeSidecarBridge + Send + 'static, - BridgeError: fmt::Debug + Send + Sync + 'static, - { - let ActiveTcpConnectRequest { - bridge, - kernel, - kernel_pid, - vm_id, - dns, - host, - port, - local_address, - local_port, - local_reservation, - context, - } = request; - let resolved = resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, context)?; - if resolved.use_kernel_loopback { - let family = JavascriptSocketFamily::from_ip(resolved.guest_remote_addr.ip()); - let requested_local_port = local_port.unwrap_or(0); - let local_port = if requested_local_port != 0 - && local_reservation == Some((family, requested_local_port)) - { - requested_local_port - } else { - allocate_guest_listen_port( - requested_local_port, - family, - &context.used_tcp_guest_ports, - context.listen_policy, - )? - }; - let local_ip = match (family, local_address) { - (JavascriptSocketFamily::Ipv4, Some("0.0.0.0")) => { - IpAddr::V4(Ipv4Addr::UNSPECIFIED) - } - (JavascriptSocketFamily::Ipv4, Some("127.0.0.1") | Some("localhost") | None) => { - IpAddr::V4(Ipv4Addr::LOCALHOST) - } - (JavascriptSocketFamily::Ipv6, Some("::")) => IpAddr::V6(Ipv6Addr::UNSPECIFIED), - (JavascriptSocketFamily::Ipv6, Some("::1") | Some("localhost") | None) => { - IpAddr::V6(Ipv6Addr::LOCALHOST) - } - (JavascriptSocketFamily::Ipv4, Some(other)) => { - return Err(SidecarError::Execution(format!( - "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}" - ))); - } - (JavascriptSocketFamily::Ipv6, Some(other)) => { - return Err(SidecarError::Execution(format!( - "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}" - ))); - } - }; - let local_addr = SocketAddr::new(local_ip, local_port); - let spec = match family { - JavascriptSocketFamily::Ipv4 => SocketSpec::tcp(), - JavascriptSocketFamily::Ipv6 => { - SocketSpec::new(SocketDomain::Inet6, SocketType::Stream) - } - }; - let socket_id = kernel - .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec) - .map_err(kernel_error)?; - kernel - .socket_bind_inet( - EXECUTION_DRIVER_NAME, - kernel_pid, - socket_id, - InetSocketAddress::new(local_ip.to_string(), local_port), - ) - .map_err(kernel_error)?; - kernel - .socket_connect_inet_loopback( - EXECUTION_DRIVER_NAME, - kernel_pid, - socket_id, - InetSocketAddress::new( - resolved.guest_remote_addr.ip().to_string(), - resolved.guest_remote_addr.port(), - ), - ) - .map_err(kernel_error)?; - return Ok(Self::from_kernel( - socket_id, - None, - local_addr, - resolved.guest_remote_addr, - )); +#[derive(Debug)] +enum TransferredHostNetSocket { + Tcp { + socket: ActiveTcpSocket, + metadata: TransferredHostNetMetadata, + }, + TcpListener { + listener: ActiveTcpListener, + metadata: TransferredHostNetMetadata, + }, + Udp { + socket: ActiveUdpSocket, + metadata: TransferredHostNetMetadata, + }, + Unix { + socket: ActiveUnixSocket, + metadata: TransferredHostNetMetadata, + }, + UnixListener { + listener: ActiveUnixListener, + metadata: TransferredHostNetMetadata, + }, + Pending { + metadata: TransferredHostNetMetadata, + description_handles: Arc<()>, + }, +} + +impl TransferredHostNetSocket { + fn class(&self) -> &'static str { + match self { + Self::Tcp { .. } => "tcp", + Self::TcpListener { .. } => "listener", + Self::Udp { .. } => "udp", + Self::Unix { .. } => "unix", + Self::UnixListener { .. } => "unix-listener", + Self::Pending { .. } => "pending", } - - let stream = TcpStream::connect_timeout(&resolved.actual_addr, Duration::from_secs(30)) - .map_err(sidecar_net_error)?; - let guest_local_addr = stream.local_addr().map_err(sidecar_net_error)?; - Self::from_stream(stream, None, guest_local_addr, resolved.guest_remote_addr) - } - - fn from_stream( - stream: TcpStream, - listener_id: Option, - guest_local_addr: SocketAddr, - guest_remote_addr: SocketAddr, - ) -> Result { - let read_stream = stream.try_clone().map_err(sidecar_net_error)?; - read_stream - .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT)) - .map_err(sidecar_net_error)?; - let stream = Arc::new(Mutex::new(stream)); - let pending_read_stream = Arc::new(Mutex::new(Some(read_stream))); - let (sender, events) = mpsc::channel(); - let tls_mode = Arc::new(AtomicBool::new(false)); - let tls_stream = Arc::new(Mutex::new(None)); - let tls_state = Arc::new(Mutex::new(None)); - let loopback_tls_pending_write = Arc::new(Mutex::new(None)); - let saw_local_shutdown = Arc::new(AtomicBool::new(false)); - let saw_remote_end = Arc::new(AtomicBool::new(false)); - let close_notified = Arc::new(AtomicBool::new(false)); - - Ok(Self { - stream: Some(stream), - pending_read_stream: Some(pending_read_stream), - events: Some(events), - event_sender: Some(sender), - event_pusher: Arc::new(Mutex::new(None)), - kernel_socket_id: None, - no_delay: false, - keep_alive: false, - keep_alive_initial_delay_secs: None, - guest_local_addr, - guest_remote_addr, - listener_id, - tls_mode, - tls_stream, - tls_state, - loopback_tls_pending_write, - saw_local_shutdown, - saw_remote_end, - close_notified, - }) } - fn from_kernel( - socket_id: SocketId, - listener_id: Option, - guest_local_addr: SocketAddr, - guest_remote_addr: SocketAddr, - ) -> Self { - let (sender, events) = mpsc::channel(); - Self { - stream: None, - pending_read_stream: None, - events: Some(events), - event_sender: Some(sender), - event_pusher: Arc::new(Mutex::new(None)), - kernel_socket_id: Some(socket_id), - no_delay: false, - keep_alive: false, - keep_alive_initial_delay_secs: None, - guest_local_addr, - guest_remote_addr, - listener_id, - tls_mode: Arc::new(AtomicBool::new(false)), - tls_stream: Arc::new(Mutex::new(None)), - tls_state: Arc::new(Mutex::new(None)), - loopback_tls_pending_write: Arc::new(Mutex::new(None)), - saw_local_shutdown: Arc::new(AtomicBool::new(false)), - saw_remote_end: Arc::new(AtomicBool::new(false)), - close_notified: Arc::new(AtomicBool::new(false)), + fn metadata(&self) -> &TransferredHostNetMetadata { + match self { + Self::Tcp { metadata, .. } + | Self::TcpListener { metadata, .. } + | Self::Udp { metadata, .. } + | Self::Unix { metadata, .. } + | Self::UnixListener { metadata, .. } + | Self::Pending { metadata, .. } => metadata, } } - fn set_event_pusher(&self, session: Option, socket_id: String) { - let Some(session) = session else { - return; - }; - if let Ok(mut pusher) = self.event_pusher.lock() { - *pusher = Some(JavascriptSocketEventPusher { session, socket_id }); + fn description_identity(&self) -> (&Arc<()>, bool, bool) { + match self { + Self::Tcp { socket, .. } => ( + &socket.description_handles, + true, + socket.kernel_socket_id.is_some(), + ), + Self::TcpListener { listener, .. } => ( + &listener.description_handles, + false, + listener.kernel_socket_id.is_some(), + ), + Self::Udp { socket, .. } => ( + &socket.description_handles, + false, + socket.kernel_socket_id.is_some(), + ), + Self::Unix { socket, .. } => (&socket.description_handles, true, false), + Self::UnixListener { listener, .. } => (&listener.description_handles, false, false), + Self::Pending { + description_handles, + .. + } => (description_handles, false, false), } } - fn poll( - &mut self, - kernel: &mut SidecarKernel, - kernel_pid: u32, - wait: Duration, - trace_enabled: bool, - ) -> Result, SidecarError> { - if self.tls_mode.load(Ordering::SeqCst) { - self.ensure_tcp_reader()?; - return match self - .events - .as_ref() - .ok_or_else(|| { - SidecarError::InvalidState(String::from("TCP socket event channel missing")) - })? - .recv_timeout(wait) - { - Ok(event) => Ok(Some(event)), - Err(RecvTimeoutError::Timeout) => Ok(None), - Err(RecvTimeoutError::Disconnected) => Ok(None), - }; + fn clone_for_fd_transfer(&self) -> Result { + match self { + Self::Tcp { socket, metadata } => Ok(Self::Tcp { + socket: socket.clone_for_fd_transfer(), + metadata: metadata.clone(), + }), + Self::TcpListener { listener, metadata } => Ok(Self::TcpListener { + listener: listener.clone_for_fd_transfer()?, + metadata: metadata.clone(), + }), + Self::Udp { socket, metadata } => Ok(Self::Udp { + socket: socket.clone_for_fd_transfer()?, + metadata: metadata.clone(), + }), + Self::Unix { socket, metadata } => Ok(Self::Unix { + socket: socket.clone_for_fd_transfer(), + metadata: metadata.clone(), + }), + Self::UnixListener { listener, metadata } => Ok(Self::UnixListener { + listener: listener.clone_for_fd_transfer()?, + metadata: metadata.clone(), + }), + Self::Pending { + metadata, + description_handles, + } => Ok(Self::Pending { + metadata: metadata.clone(), + description_handles: Arc::clone(description_handles), + }), } + } +} - if let Some(socket_id) = self.kernel_socket_id { - let poll_started = Instant::now(); - let result = kernel - .poll_targets( - EXECUTION_DRIVER_NAME, - kernel_pid, - vec![PollTargetEntry::socket( - socket_id, - POLLIN | POLLHUP | POLLERR, - )], - i32::try_from(wait.as_millis()).unwrap_or(i32::MAX), - ) - .map_err(kernel_error)?; - let poll_elapsed = poll_started.elapsed(); - let revents = result - .targets - .first() - .map(|entry| entry.revents) - .unwrap_or_else(PollEvents::empty); - record_net_tcp_kernel_poll(trace_enabled, wait, poll_elapsed, revents); - if revents.is_empty() { - return Ok(None); +fn register_host_net_transfer_description( + registry: &HostNetTransferDescriptionRegistry, + resource: &TransferredHostNetSocket, +) { + let (handles, connected, kernel_backed) = resource.description_identity(); + // Adopted kernel sockets remain present in the kernel resource snapshot + // while queued. Only sidecar-only descriptions need this weak queue lease. + if kernel_backed { + return; + } + let description_id = Arc::as_ptr(handles) as usize; + let mut descriptions = registry.lock().unwrap_or_else(|error| error.into_inner()); + descriptions.retain(|_, description| description.handles.upgrade().is_some()); + descriptions + .entry(description_id) + .and_modify(|description| description.connected |= connected) + .or_insert_with(|| HostNetTransferDescription { + handles: Arc::downgrade(handles), + connected, + }); +} + +#[derive(Debug, Clone)] +struct TransferredHostNetMetadata { + domain: u32, + socket_type: u32, + protocol: u32, + nonblocking: bool, + recv_timeout_ms: Option, + bind_options: Option, + local_info: Option, + local_unix_address: Option, + local_reservation: Option, + remote_info: Option, + remote_unix_address: Option, + listening: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct HostNetOpenDescriptionOptions { + nonblocking: bool, + recv_timeout_ms: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +enum SpawnHostNetSource { + Tcp(String), + TcpListener(String), + Udp(String), +} + +#[derive(Debug, Clone, Copy)] +enum ResolvedHostNetSourceClass { + Tcp, + Unix, + TcpListener, + UnixListener, + Udp, +} + +#[derive(Debug)] +struct PreparedSpawnHostNetDescription { + guest_fds: Vec, + resource: TransferredHostNetSocket, + metadata: Value, +} + +#[derive(Debug, Default)] +struct PreparedSpawnHostNetFds { + descriptions: Vec, + kernel_actions: Vec, +} + +#[derive(Debug, Clone, Copy)] +struct SpawnHostNetFdState { + description: usize, + close_on_exec: bool, +} + +const HOST_NET_AF_INET: u32 = 1; +const HOST_NET_AF_INET6: u32 = 2; +const HOST_NET_AF_UNIX: u32 = 3; +const HOST_NET_SOCK_DGRAM: u32 = 5; +const HOST_NET_SOCK_STREAM: u32 = 6; +const HOST_NET_SOCKET_TYPE_MASK: u32 = 0x0f; +const HOST_NET_SOCK_CLOEXEC: u32 = 0x2000; +const HOST_NET_SOCK_NONBLOCK: u32 = 0x4000; +const HOST_NET_IPPROTO_TCP: u32 = 6; +const HOST_NET_IPPROTO_UDP: u32 = 17; +const HOST_NET_METADATA_MAX_BYTES: usize = 16 * 1024; +const HOST_NET_METADATA_MAX_STRING_BYTES: usize = 4 * 1024; +const HOST_NET_RECV_TIMEOUT_MAX_MS: u64 = u32::MAX as u64; +const LINUX_SCM_MAX_FD: usize = 253; + +fn validate_host_net_metadata_size(value: &Value, label: &str) -> Result<(), SidecarError> { + let encoded_len = serde_json::to_vec(value) + .map_err(|error| { + SidecarError::InvalidState(format!("EINVAL: invalid {label} metadata: {error}")) + })? + .len(); + if encoded_len > HOST_NET_METADATA_MAX_BYTES { + return Err(SidecarError::InvalidState(format!( + "E2BIG: {label} metadata is {encoded_len} bytes, exceeding the {HOST_NET_METADATA_MAX_BYTES}-byte limit" + ))); + } + fn validate_strings(value: &Value, label: &str) -> Result<(), SidecarError> { + match value { + Value::String(value) if value.len() > HOST_NET_METADATA_MAX_STRING_BYTES => { + Err(SidecarError::InvalidState(format!( + "ENAMETOOLONG: {label} metadata string exceeds {HOST_NET_METADATA_MAX_STRING_BYTES} bytes" + ))) } - if revents.intersects(POLLIN) { - let read_started = Instant::now(); - let read_result = - kernel.socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024); - if trace_enabled { - NET_TCP_TRACE_COUNTERS.socket_read_kernel_us.fetch_add( - duration_micros_u64(read_started.elapsed()), - Ordering::Relaxed, - ); + Value::Array(values) => { + for value in values { + validate_strings(value, label)?; } - return match read_result { - Ok(Some(bytes)) if !bytes.is_empty() => { - if trace_enabled { - NET_TCP_TRACE_COUNTERS - .socket_read_data_events - .fetch_add(1, Ordering::Relaxed); - NET_TCP_TRACE_COUNTERS.socket_read_bytes.fetch_add( - u64::try_from(bytes.len()).unwrap_or(u64::MAX), - Ordering::Relaxed, - ); - } - Ok(Some(JavascriptTcpSocketEvent::Data(bytes))) - } - Ok(Some(_)) => { - if trace_enabled { - NET_TCP_TRACE_COUNTERS - .socket_read_data_events - .fetch_add(1, Ordering::Relaxed); - } - Ok(Some(JavascriptTcpSocketEvent::Data(Vec::new()))) - } - Ok(None) => { - if trace_enabled { - NET_TCP_TRACE_COUNTERS - .socket_read_end_events - .fetch_add(1, Ordering::Relaxed); - } - self.saw_remote_end.store(true, Ordering::SeqCst); - Ok(Some(JavascriptTcpSocketEvent::End)) - } - Err(error) if error.code() == "EAGAIN" => { - if trace_enabled { - NET_TCP_TRACE_COUNTERS - .socket_read_eagain - .fetch_add(1, Ordering::Relaxed); - } - Ok(None) - } - Err(error) => { - if trace_enabled { - NET_TCP_TRACE_COUNTERS - .socket_read_errors - .fetch_add(1, Ordering::Relaxed); - } - Ok(Some(JavascriptTcpSocketEvent::Error { - code: Some(error.code().to_string()), - message: error.to_string(), - })) - } - }; - } - if revents.intersects(POLLHUP) { - self.saw_remote_end.store(true, Ordering::SeqCst); - return Ok(Some(JavascriptTcpSocketEvent::End)); + Ok(()) } - if revents.intersects(POLLERR) { - return Ok(Some(JavascriptTcpSocketEvent::Error { - code: Some(String::from("EPIPE")), - message: String::from("kernel TCP socket reported POLLERR"), - })); + Value::Object(values) => { + for (key, value) in values { + if key.len() > HOST_NET_METADATA_MAX_STRING_BYTES { + return Err(SidecarError::InvalidState(format!( + "ENAMETOOLONG: {label} metadata key exceeds {HOST_NET_METADATA_MAX_STRING_BYTES} bytes" + ))); + } + validate_strings(value, label)?; + } + Ok(()) } - return Ok(None); - } - - self.ensure_tcp_reader()?; - match self - .events - .as_ref() - .ok_or_else(|| { - SidecarError::InvalidState(String::from("TCP socket event channel missing")) - })? - .recv_timeout(wait) - { - Ok(event) => Ok(Some(event)), - Err(RecvTimeoutError::Timeout) => Ok(None), - Err(RecvTimeoutError::Disconnected) => Ok(None), + _ => Ok(()), } } + validate_strings(value, label) +} - fn ensure_tcp_reader(&self) -> Result<(), SidecarError> { - if self.kernel_socket_id.is_some() { - return Ok(()); - } - if self.tls_mode.load(Ordering::SeqCst) { - return Ok(()); +fn host_net_open_description_options( + value: &Value, + label: &str, +) -> Result { + validate_host_net_metadata_size(value, label)?; + let object = value.as_object().ok_or_else(|| { + SidecarError::InvalidState(String::from( + "EINVAL: host-network metadata must be an object", + )) + })?; + let nonblocking = match object.get("nonblocking") { + None => false, + Some(Value::Bool(value)) => *value, + Some(_) => { + return Err(SidecarError::InvalidState(format!( + "EINVAL: {label} metadata nonblocking must be boolean" + ))) } - let read_stream = self - .pending_read_stream - .as_ref() - .ok_or_else(|| { - SidecarError::InvalidState(String::from("TCP socket reader handle missing")) - })? - .lock() - .map_err(|_| { - SidecarError::InvalidState(String::from("TCP socket reader lock poisoned")) - })? - .take(); - if let Some(read_stream) = read_stream { - spawn_tcp_socket_reader( - read_stream, - self.event_sender - .as_ref() - .ok_or_else(|| { - SidecarError::InvalidState(String::from("TCP socket event sender missing")) - })? - .clone(), - Arc::clone(&self.event_pusher), - Arc::clone(&self.tls_mode), - Arc::clone(&self.saw_local_shutdown), - Arc::clone(&self.saw_remote_end), - Arc::clone(&self.close_notified), - ); + }; + let recv_timeout_ms = match object.get("recvTimeoutMs") { + None | Some(Value::Null) => None, + Some(value) => { + let timeout = value.as_u64().ok_or_else(|| { + SidecarError::InvalidState(format!( + "EINVAL: {label} metadata recvTimeoutMs must be a non-negative integer or null" + )) + })?; + if timeout > HOST_NET_RECV_TIMEOUT_MAX_MS { + return Err(SidecarError::InvalidState(format!( + "EINVAL: {label} metadata recvTimeoutMs exceeds {HOST_NET_RECV_TIMEOUT_MAX_MS}" + ))); + } + Some(timeout) } - Ok(()) + }; + Ok(HostNetOpenDescriptionOptions { + nonblocking, + recv_timeout_ms, + }) +} + +fn host_net_domain(address: &SocketAddr) -> u32 { + if address.is_ipv4() { + HOST_NET_AF_INET + } else { + HOST_NET_AF_INET6 } +} - fn socket_info(&self) -> Value { - tcp_socket_info_value(&self.guest_local_addr, &self.guest_remote_addr) +fn host_net_address_info(address: SocketAddr) -> Value { + json!({ + "address": address.ip().to_string(), + "port": address.port(), + }) +} + +fn host_net_bind_options(address: SocketAddr) -> Value { + json!({ + "host": address.ip().to_string(), + "port": address.port(), + }) +} + +fn host_net_unix_options(path: Option<&str>, abstract_path_hex: Option<&str>) -> Option { + if let Some(abstract_path_hex) = abstract_path_hex { + Some(json!({ "abstractPathHex": abstract_path_hex })) + } else { + path.map(|path| json!({ "path": path })) } +} - fn set_no_delay(&mut self, enable: bool) -> Result<(), SidecarError> { - self.no_delay = enable; - if self.kernel_socket_id.is_some() { - return Ok(()); - } - let stream = self - .stream - .as_ref() - .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))? - .lock() - .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?; - stream.set_nodelay(enable).map_err(sidecar_net_error) +fn host_net_unix_address(path: Option<&str>, abstract_path_hex: Option<&str>) -> String { + if let Some(abstract_path_hex) = abstract_path_hex { + format!("unix-abstract:{}", abstract_path_hex.to_ascii_lowercase()) + } else if let Some(path) = path { + format!("unix:{path}") + } else { + String::from("unix-unnamed") } +} - fn set_keep_alive( - &mut self, - enable: bool, - initial_delay_secs: Option, - ) -> Result<(), SidecarError> { - self.keep_alive = enable; - self.keep_alive_initial_delay_secs = initial_delay_secs; - if self.kernel_socket_id.is_some() { - return Ok(()); - } - let stream = self - .stream - .as_ref() - .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))? - .lock() - .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?; - let socket = SockRef::from(&*stream); - socket.set_keepalive(enable).map_err(sidecar_net_error)?; - if enable { - if let Some(delay_secs) = initial_delay_secs.filter(|delay_secs| *delay_secs > 0) { - socket - .set_tcp_keepalive( - &TcpKeepalive::new().with_time(Duration::from_secs(delay_secs)), - ) - .map_err(sidecar_net_error)?; - } +impl TransferredHostNetMetadata { + fn tcp_socket(socket: &ActiveTcpSocket, options: HostNetOpenDescriptionOptions) -> Self { + Self { + domain: host_net_domain(&socket.guest_remote_addr), + socket_type: HOST_NET_SOCK_STREAM, + protocol: HOST_NET_IPPROTO_TCP, + nonblocking: options.nonblocking, + recv_timeout_ms: options.recv_timeout_ms, + bind_options: None, + local_info: Some(host_net_address_info(socket.guest_local_addr)), + local_unix_address: None, + local_reservation: None, + remote_info: Some(host_net_address_info(socket.guest_remote_addr)), + remote_unix_address: None, + listening: false, + } + } + + fn tcp_listener(listener: &ActiveTcpListener, options: HostNetOpenDescriptionOptions) -> Self { + let local = listener.guest_local_addr(); + Self { + domain: host_net_domain(&local), + socket_type: HOST_NET_SOCK_STREAM, + protocol: HOST_NET_IPPROTO_TCP, + nonblocking: options.nonblocking, + recv_timeout_ms: options.recv_timeout_ms, + bind_options: Some(host_net_bind_options(local)), + local_info: Some(host_net_address_info(local)), + local_unix_address: None, + local_reservation: None, + remote_info: None, + remote_unix_address: None, + listening: true, + } + } + + fn udp_socket(socket: &ActiveUdpSocket, options: HostNetOpenDescriptionOptions) -> Self { + let domain = match socket.family { + JavascriptUdpFamily::Ipv4 => HOST_NET_AF_INET, + JavascriptUdpFamily::Ipv6 => HOST_NET_AF_INET6, + }; + Self { + domain, + socket_type: HOST_NET_SOCK_DGRAM, + protocol: HOST_NET_IPPROTO_UDP, + nonblocking: options.nonblocking, + recv_timeout_ms: options.recv_timeout_ms, + bind_options: socket.guest_local_addr.map(host_net_bind_options), + local_info: socket.guest_local_addr.map(host_net_address_info), + local_unix_address: None, + local_reservation: None, + remote_info: None, + remote_unix_address: None, + listening: false, + } + } + + fn unix_socket(socket: &ActiveUnixSocket, options: HostNetOpenDescriptionOptions) -> Self { + Self { + domain: HOST_NET_AF_UNIX, + socket_type: HOST_NET_SOCK_STREAM, + protocol: 0, + nonblocking: options.nonblocking, + recv_timeout_ms: options.recv_timeout_ms, + bind_options: host_net_unix_options( + socket.local_path.as_deref(), + socket.local_abstract_path_hex.as_deref(), + ), + local_info: None, + local_unix_address: Some(host_net_unix_address( + socket.local_path.as_deref(), + socket.local_abstract_path_hex.as_deref(), + )), + local_reservation: None, + remote_info: None, + remote_unix_address: Some(host_net_unix_address( + socket.remote_path.as_deref(), + socket.remote_abstract_path_hex.as_deref(), + )), + listening: false, + } + } + + fn unix_listener( + listener: &ActiveUnixListener, + options: HostNetOpenDescriptionOptions, + ) -> Self { + Self { + domain: HOST_NET_AF_UNIX, + socket_type: HOST_NET_SOCK_STREAM, + protocol: 0, + nonblocking: options.nonblocking, + recv_timeout_ms: options.recv_timeout_ms, + bind_options: host_net_unix_options( + Some(listener.path.as_str()), + listener.abstract_path_hex.as_deref(), + ), + local_info: None, + local_unix_address: Some(host_net_unix_address( + Some(listener.path.as_str()), + listener.abstract_path_hex.as_deref(), + )), + local_reservation: None, + remote_info: None, + remote_unix_address: None, + listening: listener.listener.is_some(), + } + } + + fn pending( + value: &Value, + options: HostNetOpenDescriptionOptions, + label: &str, + ) -> Result { + let object = value + .as_object() + .expect("open-description options validated object"); + let domain = required_host_net_u32(object, "domain", label)?; + let raw_socket_type = required_host_net_u32(object, "socketType", label)?; + if raw_socket_type + & !(HOST_NET_SOCKET_TYPE_MASK | HOST_NET_SOCK_NONBLOCK | HOST_NET_SOCK_CLOEXEC) + != 0 + { + return Err(SidecarError::InvalidState(format!( + "EINVAL: {label} metadata socketType contains unsupported flags" + ))); } - Ok(()) + let socket_type = raw_socket_type & HOST_NET_SOCKET_TYPE_MASK; + let requested_protocol = required_host_net_u32(object, "protocol", label)?; + let protocol = match (domain, socket_type, requested_protocol) { + (HOST_NET_AF_INET | HOST_NET_AF_INET6, HOST_NET_SOCK_STREAM, 0 | HOST_NET_IPPROTO_TCP) => { + HOST_NET_IPPROTO_TCP + } + (HOST_NET_AF_INET | HOST_NET_AF_INET6, HOST_NET_SOCK_DGRAM, 0 | HOST_NET_IPPROTO_UDP) => { + HOST_NET_IPPROTO_UDP + } + (HOST_NET_AF_UNIX, HOST_NET_SOCK_STREAM, 0) => 0, + _ => { + return Err(SidecarError::InvalidState(format!( + "EPROTONOSUPPORT: {label} metadata does not describe a supported unconnected socket" + ))) + } + }; + let metadata = Self { + domain, + socket_type, + protocol, + nonblocking: options.nonblocking, + recv_timeout_ms: options.recv_timeout_ms, + bind_options: None, + local_info: None, + local_unix_address: (domain == HOST_NET_AF_UNIX).then(|| String::from("unix-unnamed")), + local_reservation: None, + remote_info: None, + remote_unix_address: None, + listening: false, + }; + validate_host_net_metadata(value, &metadata, "pending", label)?; + Ok(metadata) } - fn upgrade_tls( - &self, - vm_id: &str, - kernel: &SidecarKernel, - options: JavascriptTlsBridgeOptions, - ) -> Result<(), SidecarError> { - if self.tls_mode.load(Ordering::SeqCst) { - return Ok(()); + fn as_value(&self) -> Value { + json!({ + "domain": self.domain, + "socketType": self.socket_type, + "protocol": self.protocol, + "nonblocking": self.nonblocking, + "recvTimeoutMs": self.recv_timeout_ms, + "bindOptions": self.bind_options, + "localInfo": self.local_info, + "localUnixAddress": self.local_unix_address, + "localReservation": self.local_reservation, + "remoteInfo": self.remote_info, + "remoteUnixAddress": self.remote_unix_address, + "listening": self.listening, + }) + } +} + +fn required_host_net_u32( + object: &Map, + name: &str, + label: &str, +) -> Result { + object + .get(name) + .and_then(Value::as_u64) + .and_then(|value| u32::try_from(value).ok()) + .ok_or_else(|| { + SidecarError::InvalidState(format!("EINVAL: {label} metadata field {name} must be u32")) + }) +} + +fn validate_host_net_metadata( + value: &Value, + expected: &TransferredHostNetMetadata, + expected_class: &str, + label: &str, +) -> Result<(), SidecarError> { + let options = host_net_open_description_options(value, label)?; + if options.nonblocking != expected.nonblocking + || options.recv_timeout_ms != expected.recv_timeout_ms + { + return Err(host_net_metadata_mismatch(label, "open-description state")); + } + let object = value.as_object().ok_or_else(|| { + SidecarError::InvalidState(format!("EINVAL: {label} metadata must be an object")) + })?; + let domain = required_host_net_u32(object, "domain", label)?; + if domain != expected.domain { + return Err(host_net_metadata_mismatch(label, "domain")); + } + let socket_type = required_host_net_u32(object, "socketType", label)?; + if socket_type & !(HOST_NET_SOCKET_TYPE_MASK | HOST_NET_SOCK_NONBLOCK | HOST_NET_SOCK_CLOEXEC) + != 0 + || socket_type & HOST_NET_SOCKET_TYPE_MASK != expected.socket_type + { + return Err(host_net_metadata_mismatch(label, "socketType")); + } + let protocol = required_host_net_u32(object, "protocol", label)?; + if protocol != 0 && protocol != expected.protocol { + return Err(host_net_metadata_mismatch(label, "protocol")); + } + if let Some(class) = object.get("class") { + if class.as_str() != Some(expected_class) { + return Err(host_net_metadata_mismatch(label, "class")); + } + } + let expected_value = expected.as_value(); + let expected_object = expected_value + .as_object() + .expect("canonical host-network metadata is an object"); + for name in [ + "bindOptions", + "localInfo", + "localUnixAddress", + "localReservation", + "remoteInfo", + "remoteUnixAddress", + "listening", + ] { + let actual = object.get(name).unwrap_or(&Value::Null); + let canonical = expected_object.get(name).unwrap_or(&Value::Null); + if actual != canonical { + return Err(host_net_metadata_mismatch(label, name)); } + } + Ok(()) +} - let client_hello = if options.is_server { - self.peek_tls_client_hello(vm_id, kernel)? - } else { - None +fn host_net_metadata_mismatch(label: &str, field: &str) -> SidecarError { + SidecarError::InvalidState(format!( + "EINVAL: {label} metadata {field} does not match the sidecar-owned socket" + )) +} + +fn spawn_host_net_source( + fd: &JavascriptSpawnHostNetFd, +) -> Result { + let mut sources = Vec::new(); + if let Some(id) = fd.socket_id.as_deref().filter(|id| !id.is_empty()) { + validate_host_net_resource_id(id, "inherited socket id")?; + sources.push(SpawnHostNetSource::Tcp(id.to_owned())); + } + if let Some(id) = fd.server_id.as_deref().filter(|id| !id.is_empty()) { + validate_host_net_resource_id(id, "inherited listener id")?; + sources.push(SpawnHostNetSource::TcpListener(id.to_owned())); + } + if let Some(id) = fd.udp_socket_id.as_deref().filter(|id| !id.is_empty()) { + validate_host_net_resource_id(id, "inherited UDP socket id")?; + sources.push(SpawnHostNetSource::Udp(id.to_owned())); + } + if sources.len() != 1 { + return Err(SidecarError::InvalidState(String::from( + "EINVAL: inherited host-network fd requires exactly one resource id", + ))); + } + Ok(sources.pop().expect("one source checked")) +} + +fn validate_host_net_resource_id(id: &str, label: &str) -> Result<(), SidecarError> { + if id.len() > 256 { + return Err(SidecarError::InvalidState(format!( + "ENAMETOOLONG: {label} exceeds 256 bytes" + ))); + } + Ok(()) +} + +fn scm_rights_host_net_source(value: &Value) -> Result, SidecarError> { + validate_host_net_metadata_size(value, "SCM_RIGHTS host-network")?; + let object = value.as_object().ok_or_else(|| { + SidecarError::InvalidState(String::from( + "EINVAL: SCM_RIGHTS host-network entry must be an object", + )) + })?; + let mut sources = Vec::new(); + for (name, source) in [("socketId", 0u8), ("serverId", 1u8), ("udpSocketId", 2u8)] { + let Some(value) = object.get(name) else { + continue; }; + if value.is_null() { + continue; + } + let id = value.as_str().filter(|id| !id.is_empty()).ok_or_else(|| { + SidecarError::InvalidState(format!( + "EINVAL: SCM_RIGHTS host-network {name} must be a non-empty string or null" + )) + })?; + validate_host_net_resource_id(id, name)?; + sources.push(match source { + 0 => SpawnHostNetSource::Tcp(id.to_owned()), + 1 => SpawnHostNetSource::TcpListener(id.to_owned()), + 2 => SpawnHostNetSource::Udp(id.to_owned()), + _ => unreachable!(), + }); + } + if sources.len() > 1 { + return Err(SidecarError::InvalidState(String::from( + "EINVAL: SCM_RIGHTS host-network entry requires at most one resource id", + ))); + } + Ok(sources.pop()) +} - let (tls_stream, loopback_pending_write) = if let Some(socket_id) = self.kernel_socket_id { - let peer_socket_id = wait_for_loopback_peer_socket_id(kernel, socket_id) - .ok_or_else(|| { - SidecarError::Execution(format!( - "ERR_NOT_IMPLEMENTED: kernel-backed loopback socket {socket_id} has no peer for TLS upgrade" - )) - })?; - let endpoint = loopback_tls_endpoint(vm_id, socket_id, peer_socket_id)?; - let pending_write = LoopbackTlsPendingWriteHandle::new(&endpoint); - let tls_stream = if options.is_server { - ActiveTlsStream::LoopbackServer(build_server_loopback_tls_stream( - endpoint, &options, - )?) - } else { - ActiveTlsStream::LoopbackClient(build_client_loopback_tls_stream( - endpoint, &options, - )?) - }; - (tls_stream, Some(pending_write)) - } else { - self.pending_read_stream - .as_ref() - .ok_or_else(|| { - SidecarError::InvalidState(String::from("TCP socket reader handle missing")) - })? - .lock() - .map_err(|_| { - SidecarError::InvalidState(String::from("TCP socket reader lock poisoned")) - })? - .take(); - let stream = self - .stream - .as_ref() - .ok_or_else(|| { - SidecarError::InvalidState(String::from("TCP socket stream missing")) - })? - .lock() - .map_err(|_| { - SidecarError::InvalidState(String::from("TCP socket lock poisoned")) - })?; - let cloned = stream.try_clone().map_err(sidecar_net_error)?; - drop(stream); +fn posix_spawn_action_guest_fd( + action: &JavascriptPosixSpawnFileAction, + label: &str, +) -> Result { + u32::try_from(action.guest_fd.unwrap_or(action.fd)).map_err(|_| { + SidecarError::InvalidState(format!( + "EBADF: invalid posix_spawn {label} fd {}", + action.guest_fd.unwrap_or(action.fd) + )) + }) +} - if options.is_server { - ( - ActiveTlsStream::Server(build_server_tls_stream(cloned, &options)?), - None, - ) - } else { - ( - ActiveTlsStream::Client(build_client_tls_stream(cloned, &options)?), - None, - ) +fn posix_spawn_action_guest_source_fd( + action: &JavascriptPosixSpawnFileAction, +) -> Result { + u32::try_from(action.guest_source_fd.unwrap_or(action.source_fd)).map_err(|_| { + SidecarError::InvalidState(format!( + "EBADF: invalid posix_spawn dup2 source {}", + action.guest_source_fd.unwrap_or(action.source_fd) + )) + }) +} + +impl PreparedSpawnHostNetFds { + fn inherited_fd_count(&self) -> usize { + self.descriptions + .iter() + .map(|description| description.guest_fds.len()) + .sum() + } + + fn bootstrap_json(&self) -> Value { + Value::Array( + self.descriptions + .iter() + .enumerate() + .map(|(index, description)| { + let mut value = json!({ + "guestFds": description.guest_fds, + "metadata": description.metadata, + }); + let object = value + .as_object_mut() + .expect("spawn host-network bootstrap value is an object"); + let (key, id) = match description.resource { + TransferredHostNetSocket::Tcp { .. } => { + ("socketId", format!("spawn-tcp-{index}")) + } + TransferredHostNetSocket::TcpListener { .. } => { + ("serverId", format!("spawn-listener-{index}")) + } + TransferredHostNetSocket::Udp { .. } => { + ("udpSocketId", format!("spawn-udp-{index}")) + } + TransferredHostNetSocket::Unix { .. } => { + ("socketId", format!("spawn-unix-{index}")) + } + TransferredHostNetSocket::UnixListener { .. } => { + ("serverId", format!("spawn-unix-listener-{index}")) + } + TransferredHostNetSocket::Pending { .. } => unreachable!( + "pending host-network descriptions are rejected before spawn" + ), + }; + object.insert(key.to_owned(), Value::String(id)); + value + }) + .collect(), + ) + } + + fn install(self, child: &mut ActiveProcess) { + for (index, description) in self.descriptions.into_iter().enumerate() { + match description.resource { + TransferredHostNetSocket::Tcp { mut socket, .. } => { + socket.listener_id = None; + child + .tcp_sockets + .insert(format!("spawn-tcp-{index}"), socket); + } + TransferredHostNetSocket::TcpListener { listener, .. } => { + child + .tcp_listeners + .insert(format!("spawn-listener-{index}"), listener); + } + TransferredHostNetSocket::Udp { socket, .. } => { + child + .udp_sockets + .insert(format!("spawn-udp-{index}"), socket); + } + TransferredHostNetSocket::Unix { mut socket, .. } => { + socket.listener_id = None; + child + .unix_sockets + .insert(format!("spawn-unix-{index}"), socket); + } + TransferredHostNetSocket::UnixListener { listener, .. } => { + child + .unix_listeners + .insert(format!("spawn-unix-listener-{index}"), listener); + } + TransferredHostNetSocket::Pending { .. } => { + unreachable!("pending host-network descriptions are rejected before spawn") + } } - }; + } + } +} - let tls_state = ActiveTlsState { - client_hello, - local_certificates: tls_local_certificates(&options)?, - session_reused: false, - }; +fn transferred_hostnet_value( + class: &str, + metadata: TransferredHostNetMetadata, + id: Option<(&str, String)>, + local: Option, + remote: Option, +) -> Value { + let mut value = json!({ + "kind": "hostNet", + "class": class, + "domain": metadata.domain, + "socketType": metadata.socket_type, + "protocol": metadata.protocol, + "nonblocking": metadata.nonblocking, + "recvTimeoutMs": metadata.recv_timeout_ms, + "bindOptions": metadata.bind_options, + "localInfo": metadata.local_info, + "localUnixAddress": metadata.local_unix_address, + "localReservation": metadata.local_reservation, + "remoteInfo": metadata.remote_info, + "remoteUnixAddress": metadata.remote_unix_address, + "listening": metadata.listening, + }); + let object = value + .as_object_mut() + .expect("transferred host-net value is an object"); + if let Some((key, id)) = id { + object.insert(key.to_owned(), Value::String(id)); + } + if let Some(local) = local { + object.insert( + String::from("localAddress"), + Value::String(local.ip().to_string()), + ); + object.insert(String::from("localPort"), Value::from(local.port())); + } + if let Some(remote) = remote { + object.insert( + String::from("remoteAddress"), + Value::String(remote.ip().to_string()), + ); + object.insert(String::from("remotePort"), Value::from(remote.port())); + } + value +} - self.tls_mode.store(true, Ordering::SeqCst); - { - let mut state = self - .tls_state - .lock() - .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?; - *state = Some(tls_state); +fn adopt_kernel_socket_transfer_guard( + kernel: &mut SidecarKernel, + pid: u32, + socket_id: SocketId, + nonblocking: bool, +) -> Result { + let flags = if nonblocking { + agentos_kernel::fd_table::O_NONBLOCK + } else { + 0 + }; + kernel + .fd_adopt_socket_transfer(EXECUTION_DRIVER_NAME, pid, socket_id, flags) + .map_err(kernel_error) +} + +fn prepare_transferred_host_net_resource( + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + source: &SpawnHostNetSource, + value: &Value, + label: &str, +) -> Result { + // Resolve the sidecar-owned resource before reading any guest-controlled + // metadata. Metadata may describe open-description flags, but it never + // selects the resource class or lifecycle. + let resolved_class = match source { + SpawnHostNetSource::Tcp(socket_id) if process.tcp_sockets.contains_key(socket_id) => { + ResolvedHostNetSourceClass::Tcp + } + SpawnHostNetSource::Tcp(socket_id) if process.unix_sockets.contains_key(socket_id) => { + ResolvedHostNetSourceClass::Unix + } + SpawnHostNetSource::Tcp(socket_id) => { + return Err(SidecarError::InvalidState(format!( + "EBADF: unknown transferable socket {socket_id}" + ))) } + SpawnHostNetSource::TcpListener(listener_id) + if process.tcp_listeners.contains_key(listener_id) => { - let mut stream = self.tls_stream.lock().map_err(|_| { - SidecarError::InvalidState(String::from("TLS stream lock poisoned")) - })?; - *stream = Some(tls_stream); + ResolvedHostNetSourceClass::TcpListener } + SpawnHostNetSource::TcpListener(listener_id) + if process.unix_listeners.contains_key(listener_id) => { - let mut pending = self.loopback_tls_pending_write.lock().map_err(|_| { - SidecarError::InvalidState(String::from( - "loopback TLS pending write handle lock poisoned", + ResolvedHostNetSourceClass::UnixListener + } + SpawnHostNetSource::TcpListener(listener_id) => { + return Err(SidecarError::InvalidState(format!( + "EBADF: unknown transferable listener {listener_id}" + ))) + } + SpawnHostNetSource::Udp(socket_id) if process.udp_sockets.contains_key(socket_id) => { + ResolvedHostNetSourceClass::Udp + } + SpawnHostNetSource::Udp(socket_id) => { + return Err(SidecarError::InvalidState(format!( + "EBADF: unknown transferable UDP socket {socket_id}" + ))) + } + }; + let options = host_net_open_description_options(value, label)?; + let resource = match (source, resolved_class) { + (SpawnHostNetSource::Tcp(socket_id), ResolvedHostNetSourceClass::Tcp) => { + let socket = process + .tcp_sockets + .get_mut(socket_id) + .expect("resolved TCP socket remains present"); + let metadata = TransferredHostNetMetadata::tcp_socket(socket, options); + validate_host_net_metadata(value, &metadata, "tcp", label)?; + if socket.kernel_transfer_guard.is_none() { + if let Some(kernel_socket_id) = socket.kernel_socket_id { + socket.kernel_transfer_guard = Some(adopt_kernel_socket_transfer_guard( + kernel, + process.kernel_pid, + kernel_socket_id, + options.nonblocking, + )?); + } + } + TransferredHostNetSocket::Tcp { + socket: socket.clone_for_fd_transfer(), + metadata, + } + } + (SpawnHostNetSource::Tcp(socket_id), ResolvedHostNetSourceClass::Unix) => { + let socket = process + .unix_sockets + .get(socket_id) + .expect("resolved Unix socket remains present"); + let metadata = TransferredHostNetMetadata::unix_socket(socket, options); + validate_host_net_metadata(value, &metadata, "unix", label)?; + TransferredHostNetSocket::Unix { + socket: socket.clone_for_fd_transfer(), + metadata, + } + } + (SpawnHostNetSource::TcpListener(listener_id), ResolvedHostNetSourceClass::TcpListener) => { + let listener = process + .tcp_listeners + .get_mut(listener_id) + .expect("resolved TCP listener remains present"); + let metadata = TransferredHostNetMetadata::tcp_listener(listener, options); + validate_host_net_metadata(value, &metadata, "listener", label)?; + if listener.kernel_transfer_guard.is_none() { + if let Some(kernel_socket_id) = listener.kernel_socket_id { + listener.kernel_transfer_guard = Some(adopt_kernel_socket_transfer_guard( + kernel, + process.kernel_pid, + kernel_socket_id, + options.nonblocking, + )?); + } + } + TransferredHostNetSocket::TcpListener { + listener: listener.clone_for_fd_transfer()?, + metadata, + } + } + ( + SpawnHostNetSource::TcpListener(listener_id), + ResolvedHostNetSourceClass::UnixListener, + ) => { + let listener = process + .unix_listeners + .get(listener_id) + .expect("resolved Unix listener remains present"); + let metadata = TransferredHostNetMetadata::unix_listener(listener, options); + validate_host_net_metadata(value, &metadata, "unix-listener", label)?; + TransferredHostNetSocket::UnixListener { + listener: listener.clone_for_fd_transfer()?, + metadata, + } + } + (SpawnHostNetSource::Udp(socket_id), ResolvedHostNetSourceClass::Udp) => { + let socket = process.udp_sockets.get_mut(socket_id).ok_or_else(|| { + SidecarError::InvalidState(format!( + "EBADF: unknown transferable UDP socket {socket_id}" )) })?; - *pending = loopback_pending_write.clone(); + let metadata = TransferredHostNetMetadata::udp_socket(socket, options); + validate_host_net_metadata(value, &metadata, "udp", label)?; + if socket.kernel_transfer_guard.is_none() { + if let Some(kernel_socket_id) = socket.kernel_socket_id { + socket.kernel_transfer_guard = Some(adopt_kernel_socket_transfer_guard( + kernel, + process.kernel_pid, + kernel_socket_id, + options.nonblocking, + )?); + } + } + TransferredHostNetSocket::Udp { + socket: socket.clone_for_fd_transfer()?, + metadata, + } } + _ => unreachable!("resource source and resolved class must agree"), + }; + Ok(resource) +} - spawn_tls_socket_reader( - Arc::clone(&self.tls_stream), - loopback_pending_write, - self.event_sender - .as_ref() - .ok_or_else(|| { - SidecarError::InvalidState(String::from("TCP socket event sender missing")) - })? - .clone(), - Arc::clone(&self.event_pusher), - Arc::clone(&self.saw_local_shutdown), - Arc::clone(&self.saw_remote_end), - Arc::clone(&self.close_notified), - ); - Ok(()) - } - - fn peek_tls_client_hello( - &self, - vm_id: &str, - kernel: &SidecarKernel, - ) -> Result, SidecarError> { - if let Some(socket_id) = self.kernel_socket_id { - let Some(peer_socket_id) = kernel - .socket_get(socket_id) - .and_then(|record| record.peer_socket_id()) - else { - return Ok(None); +impl ActiveTcpSocket { + fn connect(request: ActiveTcpConnectRequest<'_, B>) -> Result + where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, + { + let ActiveTcpConnectRequest { + bridge, + kernel, + kernel_pid, + vm_id, + dns, + host, + port, + local_address, + local_port, + local_reservation, + context, + } = request; + let resolved = resolve_tcp_connect_addr(bridge, kernel, vm_id, dns, host, port, context)?; + if resolved.use_kernel_loopback { + let family = JavascriptSocketFamily::from_ip(resolved.guest_remote_addr.ip()); + let requested_local_port = local_port.unwrap_or(0); + let local_port = if requested_local_port != 0 + && local_reservation == Some((family, requested_local_port)) + { + requested_local_port + } else { + allocate_guest_listen_port( + requested_local_port, + family, + &context.used_tcp_guest_ports, + context.listen_policy, + )? }; - return peek_loopback_tls_client_hello(vm_id, socket_id, peer_socket_id); + let local_ip = match (family, local_address) { + (JavascriptSocketFamily::Ipv4, Some("0.0.0.0")) => { + IpAddr::V4(Ipv4Addr::UNSPECIFIED) + } + (JavascriptSocketFamily::Ipv4, Some("127.0.0.1") | Some("localhost") | None) => { + IpAddr::V4(Ipv4Addr::LOCALHOST) + } + (JavascriptSocketFamily::Ipv6, Some("::")) => IpAddr::V6(Ipv6Addr::UNSPECIFIED), + (JavascriptSocketFamily::Ipv6, Some("::1") | Some("localhost") | None) => { + IpAddr::V6(Ipv6Addr::LOCALHOST) + } + (JavascriptSocketFamily::Ipv4, Some(other)) => { + return Err(SidecarError::Execution(format!( + "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}" + ))); + } + (JavascriptSocketFamily::Ipv6, Some(other)) => { + return Err(SidecarError::Execution(format!( + "EACCES: TCP sockets must bind to loopback or unspecified addresses, got {other}" + ))); + } + }; + let local_addr = SocketAddr::new(local_ip, local_port); + let spec = match family { + JavascriptSocketFamily::Ipv4 => SocketSpec::tcp(), + JavascriptSocketFamily::Ipv6 => { + SocketSpec::new(SocketDomain::Inet6, SocketType::Stream) + } + }; + let socket_id = kernel + .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec) + .map_err(kernel_error)?; + kernel + .socket_bind_inet( + EXECUTION_DRIVER_NAME, + kernel_pid, + socket_id, + InetSocketAddress::new(local_ip.to_string(), local_port), + ) + .map_err(kernel_error)?; + kernel + .socket_connect_inet_loopback( + EXECUTION_DRIVER_NAME, + kernel_pid, + socket_id, + InetSocketAddress::new( + resolved.guest_remote_addr.ip().to_string(), + resolved.guest_remote_addr.port(), + ), + ) + .map_err(kernel_error)?; + return Ok(Self::from_kernel( + socket_id, + None, + local_addr, + resolved.guest_remote_addr, + )); } - let stream = self - .stream - .as_ref() - .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))? - .lock() - .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?; - let mut buffer = vec![0_u8; 16 * 1024]; - let bytes = match stream.peek(&mut buffer) { - Ok(0) => return Ok(None), - Ok(bytes) => bytes, - Err(error) - if matches!( - error.kind(), - std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut - ) => - { - return Ok(None); - } - Err(error) => return Err(sidecar_net_error(error)), - }; - parse_tls_client_hello_from_bytes(&buffer[..bytes]) + let stream = TcpStream::connect_timeout(&resolved.actual_addr, Duration::from_secs(30)) + .map_err(sidecar_net_error)?; + let guest_local_addr = stream.local_addr().map_err(sidecar_net_error)?; + Self::from_stream(stream, None, guest_local_addr, resolved.guest_remote_addr) } - fn tls_client_hello_json( - &self, - vm_id: &str, - kernel: &SidecarKernel, - ) -> Result { - if let Some(client_hello) = self - .tls_state - .lock() - .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))? - .as_ref() - .and_then(|state| state.client_hello.clone()) - { - return javascript_net_json_string( - serde_json::to_value(client_hello).map_err(|error| { - SidecarError::InvalidState(format!( - "failed to serialize TLS client hello: {error}" - )) - })?, - "net.socket_get_tls_client_hello", - ); + fn from_stream( + stream: TcpStream, + listener_id: Option, + guest_local_addr: SocketAddr, + guest_remote_addr: SocketAddr, + ) -> Result { + let read_stream = stream.try_clone().map_err(sidecar_net_error)?; + read_stream + .set_read_timeout(Some(TCP_SOCKET_POLL_TIMEOUT)) + .map_err(sidecar_net_error)?; + let stream = Arc::new(Mutex::new(stream)); + let pending_read_stream = Arc::new(Mutex::new(Some(read_stream))); + let (sender, events) = mpsc::channel(); + let tls_mode = Arc::new(AtomicBool::new(false)); + let tls_stream = Arc::new(Mutex::new(None)); + let tls_state = Arc::new(Mutex::new(None)); + let loopback_tls_pending_write = Arc::new(Mutex::new(None)); + let saw_local_shutdown = Arc::new(AtomicBool::new(false)); + let saw_remote_end = Arc::new(AtomicBool::new(false)); + let close_notified = Arc::new(AtomicBool::new(false)); + + Ok(Self { + stream: Some(stream), + pending_read_stream: Some(pending_read_stream), + events: Some(Arc::new(Mutex::new(events))), + event_sender: Some(sender), + event_pusher: Arc::new(Mutex::new(None)), + kernel_socket_id: None, + no_delay: false, + keep_alive: false, + keep_alive_initial_delay_secs: None, + guest_local_addr, + guest_remote_addr, + listener_id, + tls_mode, + tls_stream, + tls_state, + loopback_tls_pending_write, + saw_local_shutdown, + saw_remote_end, + close_notified, + read_buffer: Arc::new(Mutex::new(VecDeque::new())), + description_handles: Arc::new(()), + kernel_transfer_guard: None, + }) + } + + fn from_kernel( + socket_id: SocketId, + listener_id: Option, + guest_local_addr: SocketAddr, + guest_remote_addr: SocketAddr, + ) -> Self { + let (sender, events) = mpsc::channel(); + Self { + stream: None, + pending_read_stream: None, + events: Some(Arc::new(Mutex::new(events))), + event_sender: Some(sender), + event_pusher: Arc::new(Mutex::new(None)), + kernel_socket_id: Some(socket_id), + no_delay: false, + keep_alive: false, + keep_alive_initial_delay_secs: None, + guest_local_addr, + guest_remote_addr, + listener_id, + tls_mode: Arc::new(AtomicBool::new(false)), + tls_stream: Arc::new(Mutex::new(None)), + tls_state: Arc::new(Mutex::new(None)), + loopback_tls_pending_write: Arc::new(Mutex::new(None)), + saw_local_shutdown: Arc::new(AtomicBool::new(false)), + saw_remote_end: Arc::new(AtomicBool::new(false)), + close_notified: Arc::new(AtomicBool::new(false)), + read_buffer: Arc::new(Mutex::new(VecDeque::new())), + description_handles: Arc::new(()), + kernel_transfer_guard: None, } + } - javascript_net_json_string( - serde_json::to_value( - self.peek_tls_client_hello(vm_id, kernel)? - .unwrap_or_default(), - ) - .map_err(|error| { - SidecarError::InvalidState(format!("failed to serialize TLS client hello: {error}")) - })?, - "net.socket_get_tls_client_hello", - ) + fn clone_for_fd_transfer(&self) -> Self { + Self { + stream: self.stream.as_ref().map(Arc::clone), + pending_read_stream: self.pending_read_stream.as_ref().map(Arc::clone), + events: self.events.as_ref().map(Arc::clone), + event_sender: self.event_sender.clone(), + event_pusher: Arc::clone(&self.event_pusher), + kernel_socket_id: self.kernel_socket_id, + no_delay: self.no_delay, + keep_alive: self.keep_alive, + keep_alive_initial_delay_secs: self.keep_alive_initial_delay_secs, + guest_local_addr: self.guest_local_addr, + guest_remote_addr: self.guest_remote_addr, + listener_id: self.listener_id.clone(), + tls_mode: Arc::clone(&self.tls_mode), + tls_stream: Arc::clone(&self.tls_stream), + tls_state: Arc::clone(&self.tls_state), + loopback_tls_pending_write: Arc::clone(&self.loopback_tls_pending_write), + saw_local_shutdown: Arc::clone(&self.saw_local_shutdown), + saw_remote_end: Arc::clone(&self.saw_remote_end), + close_notified: Arc::clone(&self.close_notified), + read_buffer: Arc::clone(&self.read_buffer), + description_handles: Arc::clone(&self.description_handles), + kernel_transfer_guard: self.kernel_transfer_guard.clone(), + } + } + + fn is_final_description_handle(&self) -> bool { + Arc::strong_count(&self.description_handles) == 1 } - fn tls_query(&self, query: &str, detailed: bool) -> Result { - let state = self - .tls_state - .lock() - .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))? - .clone(); - let mut tls_stream = self - .tls_stream - .lock() - .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?; - let Some(stream) = tls_stream.as_mut() else { - return javascript_net_json_string( - tls_bridge_undefined_value(), - "net.socket_tls_query", - ); - }; - - let payload = match query { - "getSession" => tls_bridge_undefined_value(), - "isSessionReused" => Value::Bool( - state - .as_ref() - .is_some_and(|tls_state| tls_state.session_reused), - ), - "getPeerCertificate" => { - let certificate = stream - .peer_certificates() - .and_then(|certificates| certificates.first()) - .map(|certificate| { - tls_certificate_bridge_value(certificate.as_ref(), detailed) - }); - certificate.unwrap_or_else(tls_bridge_undefined_value) - } - "getCertificate" => state - .as_ref() - .and_then(|tls_state| tls_state.local_certificates.first()) - .map(|certificate| tls_certificate_bridge_value(certificate, detailed)) - .unwrap_or_else(tls_bridge_undefined_value), - "getProtocol" => stream - .protocol_version() - .map(tls_protocol_name) - .map(Value::String) - .unwrap_or(Value::Null), - "getCipher" => stream - .negotiated_cipher_suite() - .map(tls_cipher_bridge_value) - .unwrap_or_else(tls_bridge_undefined_value), - other => { - return Err(SidecarError::InvalidState(format!( - "unsupported TLS query {other}" - ))); - } - }; - javascript_net_json_string(payload, "net.socket_tls_query") + fn set_event_pusher(&self, session: Option, socket_id: String) { + if let Ok(mut pusher) = self.event_pusher.lock() { + *pusher = session.map(|session| JavascriptSocketEventPusher { session, socket_id }); + } } - fn write_all( - &self, + fn poll( + &mut self, kernel: &mut SidecarKernel, kernel_pid: u32, - contents: &[u8], - ) -> Result { + wait: Duration, + trace_enabled: bool, + ) -> Result, SidecarError> { if self.tls_mode.load(Ordering::SeqCst) { - let loopback_pending_write = self - .loopback_tls_pending_write + self.ensure_tcp_reader()?; + return match self + .events + .as_ref() + .ok_or_else(|| { + SidecarError::InvalidState(String::from("TCP socket event channel missing")) + })? .lock() .map_err(|_| { - SidecarError::InvalidState(String::from( - "loopback TLS pending write handle lock poisoned", - )) + SidecarError::InvalidState(String::from("TCP socket event lock poisoned")) })? - .clone(); - if let Some(pending_write) = loopback_pending_write.as_ref() { - if pending_write.should_buffer_write()? { - pending_write.append_write(contents)?; - return Ok(contents.len()); - } - pending_write.interrupt_own_reader(); - } - let mut tls_stream = self.tls_stream.lock().map_err(|_| { - SidecarError::InvalidState(String::from("TLS stream lock poisoned")) - })?; - let stream = tls_stream.as_mut().ok_or_else(|| { - SidecarError::InvalidState(String::from("TLS stream missing for upgraded socket")) - })?; - stream.write_all(contents)?; - return Ok(contents.len()); + .recv_timeout(wait) + { + Ok(event) => Ok(Some(event)), + Err(RecvTimeoutError::Timeout) => Ok(None), + Err(RecvTimeoutError::Disconnected) => Ok(None), + }; } + if let Some(socket_id) = self.kernel_socket_id { - return kernel - .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, contents) - .map_err(kernel_error); + let poll_started = Instant::now(); + let result = kernel + .poll_targets( + EXECUTION_DRIVER_NAME, + kernel_pid, + vec![PollTargetEntry::socket( + socket_id, + POLLIN | POLLHUP | POLLERR, + )], + i32::try_from(wait.as_millis()).unwrap_or(i32::MAX), + ) + .map_err(kernel_error)?; + let poll_elapsed = poll_started.elapsed(); + let revents = result + .targets + .first() + .map(|entry| entry.revents) + .unwrap_or_else(PollEvents::empty); + record_net_tcp_kernel_poll(trace_enabled, wait, poll_elapsed, revents); + if revents.is_empty() { + return Ok(None); + } + if revents.intersects(POLLIN) { + let read_started = Instant::now(); + let read_result = + kernel.socket_read(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, 64 * 1024); + if trace_enabled { + NET_TCP_TRACE_COUNTERS.socket_read_kernel_us.fetch_add( + duration_micros_u64(read_started.elapsed()), + Ordering::Relaxed, + ); + } + return match read_result { + Ok(Some(bytes)) if !bytes.is_empty() => { + if trace_enabled { + NET_TCP_TRACE_COUNTERS + .socket_read_data_events + .fetch_add(1, Ordering::Relaxed); + NET_TCP_TRACE_COUNTERS.socket_read_bytes.fetch_add( + u64::try_from(bytes.len()).unwrap_or(u64::MAX), + Ordering::Relaxed, + ); + } + Ok(Some(JavascriptTcpSocketEvent::Data(bytes))) + } + Ok(Some(_)) => { + if trace_enabled { + NET_TCP_TRACE_COUNTERS + .socket_read_data_events + .fetch_add(1, Ordering::Relaxed); + } + Ok(Some(JavascriptTcpSocketEvent::Data(Vec::new()))) + } + Ok(None) => { + if trace_enabled { + NET_TCP_TRACE_COUNTERS + .socket_read_end_events + .fetch_add(1, Ordering::Relaxed); + } + self.saw_remote_end.store(true, Ordering::SeqCst); + Ok(Some(JavascriptTcpSocketEvent::End)) + } + Err(error) if error.code() == "EAGAIN" => { + if trace_enabled { + NET_TCP_TRACE_COUNTERS + .socket_read_eagain + .fetch_add(1, Ordering::Relaxed); + } + Ok(None) + } + Err(error) => { + if trace_enabled { + NET_TCP_TRACE_COUNTERS + .socket_read_errors + .fetch_add(1, Ordering::Relaxed); + } + Ok(Some(JavascriptTcpSocketEvent::Error { + code: Some(error.code().to_string()), + message: error.to_string(), + })) + } + }; + } + if revents.intersects(POLLHUP) { + self.saw_remote_end.store(true, Ordering::SeqCst); + return Ok(Some(JavascriptTcpSocketEvent::End)); + } + if revents.intersects(POLLERR) { + return Ok(Some(JavascriptTcpSocketEvent::Error { + code: Some(String::from("EPIPE")), + message: String::from("kernel TCP socket reported POLLERR"), + })); + } + return Ok(None); } - let mut stream = self - .stream + self.ensure_tcp_reader()?; + match self + .events .as_ref() - .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))? + .ok_or_else(|| { + SidecarError::InvalidState(String::from("TCP socket event channel missing")) + })? .lock() - .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?; - stream.write_all(contents).map_err(sidecar_net_error)?; - Ok(contents.len()) + .map_err(|_| { + SidecarError::InvalidState(String::from("TCP socket event lock poisoned")) + })? + .recv_timeout(wait) + { + Ok(event) => Ok(Some(event)), + Err(RecvTimeoutError::Timeout) => Ok(None), + Err(RecvTimeoutError::Disconnected) => Ok(None), + } } - fn shutdown_write( + fn buffered_read_event( &self, + max_bytes: usize, + peek: bool, + ) -> Result, SidecarError> { + let mut buffer = self.read_buffer.lock().map_err(|_| { + SidecarError::InvalidState(String::from("TCP shared read buffer lock poisoned")) + })?; + if buffer.is_empty() && max_bytes != 0 { + return Ok(None); + } + let take = buffer.len().min(max_bytes); + let bytes = if peek { + buffer.iter().take(take).copied().collect() + } else { + buffer.drain(..take).collect() + }; + Ok(Some(JavascriptTcpSocketEvent::Data(bytes))) + } + + fn poll_readable( + &mut self, kernel: &mut SidecarKernel, kernel_pid: u32, - ) -> Result<(), SidecarError> { - if self.tls_mode.load(Ordering::SeqCst) { - let loopback_pending_write = self - .loopback_tls_pending_write - .lock() - .map_err(|_| { - SidecarError::InvalidState(String::from( - "loopback TLS pending write handle lock poisoned", - )) - })? - .clone(); - if let Some(pending_write) = loopback_pending_write.as_ref() { - if pending_write.should_buffer_write()? { - pending_write.defer_shutdown_write()?; - self.saw_local_shutdown.store(true, Ordering::SeqCst); - return Ok(()); - } - pending_write.interrupt_own_reader(); - } - if let Some(stream) = self - .tls_stream - .lock() - .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))? - .as_mut() - { - let _ = stream.send_close_notify(); - let _ = stream.shutdown_write(); - } - if self.kernel_socket_id.is_some() { - self.saw_local_shutdown.store(true, Ordering::SeqCst); - return Ok(()); + wait: Duration, + max_bytes: usize, + peek: bool, + trace_enabled: bool, + ) -> Result, SidecarError> { + if let Some(event) = self.buffered_read_event(max_bytes, peek)? { + return Ok(Some(event)); + } + match self.poll(kernel, kernel_pid, wait, trace_enabled)? { + Some(JavascriptTcpSocketEvent::Data(bytes)) => { + self.read_buffer + .lock() + .map_err(|_| { + SidecarError::InvalidState(String::from( + "TCP shared read buffer lock poisoned", + )) + })? + .extend(bytes); + self.buffered_read_event(max_bytes, peek) } + other => Ok(other), } - if let Some(socket_id) = self.kernel_socket_id { - self.saw_local_shutdown.store(true, Ordering::SeqCst); - match kernel.socket_shutdown( - EXECUTION_DRIVER_NAME, - kernel_pid, - socket_id, - KernelSocketShutdown::Write, - ) { - Ok(()) => {} - Err(error) if error.code() == "ENOENT" => {} - Err(error) => return Err(kernel_error(error)), + } + + fn poll_readiness( + &mut self, + kernel: &mut SidecarKernel, + kernel_pid: u32, + wait: Duration, + trace_enabled: bool, + ) -> Result, SidecarError> { + if !self + .read_buffer + .lock() + .map_err(|_| { + SidecarError::InvalidState(String::from("TCP shared read buffer lock poisoned")) + })? + .is_empty() + { + return Ok(Some(JavascriptTcpSocketEvent::Data(Vec::new()))); + } + match self.poll(kernel, kernel_pid, wait, trace_enabled)? { + Some(JavascriptTcpSocketEvent::Data(bytes)) => { + self.read_buffer + .lock() + .map_err(|_| { + SidecarError::InvalidState(String::from( + "TCP shared read buffer lock poisoned", + )) + })? + .extend(bytes); + Ok(Some(JavascriptTcpSocketEvent::Data(Vec::new()))) } + other => Ok(other), + } + } + + fn ensure_tcp_reader(&self) -> Result<(), SidecarError> { + if self.kernel_socket_id.is_some() { return Ok(()); } - let stream = self - .stream + if self.tls_mode.load(Ordering::SeqCst) { + return Ok(()); + } + let read_stream = self + .pending_read_stream .as_ref() - .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))? + .ok_or_else(|| { + SidecarError::InvalidState(String::from("TCP socket reader handle missing")) + })? .lock() - .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?; - self.saw_local_shutdown.store(true, Ordering::SeqCst); - match stream.shutdown(Shutdown::Write) { - Ok(()) => {} - Err(error) if error.kind() == std::io::ErrorKind::NotConnected => {} - Err(error) => return Err(sidecar_net_error(error)), - } - if self.saw_remote_end.load(Ordering::SeqCst) - && !self.close_notified.swap(true, Ordering::SeqCst) - { - let _ = self - .event_sender - .as_ref() - .ok_or_else(|| { - SidecarError::InvalidState(String::from("TCP socket event sender missing")) - })? - .send(JavascriptTcpSocketEvent::Close { had_error: false }); + .map_err(|_| { + SidecarError::InvalidState(String::from("TCP socket reader lock poisoned")) + })? + .take(); + if let Some(read_stream) = read_stream { + spawn_tcp_socket_reader( + read_stream, + self.event_sender + .as_ref() + .ok_or_else(|| { + SidecarError::InvalidState(String::from("TCP socket event sender missing")) + })? + .clone(), + Arc::clone(&self.event_pusher), + Arc::clone(&self.tls_mode), + Arc::clone(&self.saw_local_shutdown), + Arc::clone(&self.saw_remote_end), + Arc::clone(&self.close_notified), + ); } Ok(()) } - fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> { - if self.tls_mode.load(Ordering::SeqCst) { - if let Some(pending_write) = self - .loopback_tls_pending_write - .lock() - .map_err(|_| { - SidecarError::InvalidState(String::from( - "loopback TLS pending write handle lock poisoned", - )) - })? - .take() - { - pending_write.clear_for_close(); - pending_write.interrupt_own_reader(); - } - if let Some(stream) = self - .tls_stream - .lock() - .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))? - .as_mut() - { - let _ = stream.send_close_notify(); - let _ = stream.close(); - } - } - if let Some(socket_id) = self.kernel_socket_id { - return close_kernel_socket_idempotent(kernel, kernel_pid, socket_id); + fn socket_info(&self) -> Value { + tcp_socket_info_value(&self.guest_local_addr, &self.guest_remote_addr) + } + + fn set_no_delay(&mut self, enable: bool) -> Result<(), SidecarError> { + self.no_delay = enable; + if self.kernel_socket_id.is_some() { + return Ok(()); } let stream = self .stream @@ -2285,7147 +3617,6257 @@ impl ActiveTcpSocket { .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))? .lock() .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?; - stream.shutdown(Shutdown::Both).map_err(sidecar_net_error) - } -} - -fn close_kernel_socket_idempotent( - kernel: &mut SidecarKernel, - kernel_pid: u32, - socket_id: SocketId, -) -> Result<(), SidecarError> { - match kernel.socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) { - Ok(()) => Ok(()), - Err(error) if error.code() == "ENOENT" => Ok(()), - Err(error) => Err(kernel_error(error)), - } -} - -fn register_kernel_readiness_target( - registry: &KernelSocketReadinessRegistry, - kernel_socket_id: Option, - session: Option, - target_id: String, - event: KernelSocketReadinessEvent, -) { - let (Some(kernel_socket_id), Some(session)) = (kernel_socket_id, session) else { - return; - }; - if let Ok(mut targets) = registry.lock() { - targets.insert( - kernel_socket_id, - KernelSocketReadinessTarget { - session, - target_id, - event, - }, - ); - } -} - -fn unregister_kernel_readiness_target( - registry: &KernelSocketReadinessRegistry, - kernel_socket_id: Option, -) { - let Some(kernel_socket_id) = kernel_socket_id else { - return; - }; - if let Ok(mut targets) = registry.lock() { - targets.remove(&kernel_socket_id); + stream.set_nodelay(enable).map_err(sidecar_net_error) } -} -fn release_tcp_socket_handle( - process: &mut ActiveProcess, - socket_id: &str, - socket: ActiveTcpSocket, - kernel: &mut SidecarKernel, - kernel_readiness: &KernelSocketReadinessRegistry, -) { - unregister_kernel_readiness_target(kernel_readiness, socket.kernel_socket_id); - if let Some(listener_id) = socket.listener_id.as_deref() { - if let Some(listener) = process.tcp_listeners.get_mut(listener_id) { - listener.release_connection(socket_id); + fn set_keep_alive( + &mut self, + enable: bool, + initial_delay_secs: Option, + ) -> Result<(), SidecarError> { + self.keep_alive = enable; + self.keep_alive_initial_delay_secs = initial_delay_secs; + if self.kernel_socket_id.is_some() { + return Ok(()); } - } - let _ = socket.close(kernel, process.kernel_pid); -} - -fn release_unix_socket_handle( - process: &mut ActiveProcess, - socket_id: &str, - socket: ActiveUnixSocket, -) { - if let Some(listener_id) = socket.listener_id.as_deref() { - if let Some(listener) = process.unix_listeners.get_mut(listener_id) { - listener.release_connection(socket_id); + let stream = self + .stream + .as_ref() + .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))? + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?; + let socket = SockRef::from(&*stream); + socket.set_keepalive(enable).map_err(sidecar_net_error)?; + if enable { + if let Some(delay_secs) = initial_delay_secs.filter(|delay_secs| *delay_secs > 0) { + socket + .set_tcp_keepalive( + &TcpKeepalive::new().with_time(Duration::from_secs(delay_secs)), + ) + .map_err(sidecar_net_error)?; + } } - } - let _ = socket.close(); -} - -impl ActiveTlsStream { - fn is_loopback(&self) -> bool { - matches!(self, Self::LoopbackClient(_) | Self::LoopbackServer(_)) + Ok(()) } - fn is_handshaking(&self) -> bool { - match self { - Self::Client(stream) => stream.conn.is_handshaking(), - Self::Server(stream) => stream.conn.is_handshaking(), - Self::LoopbackClient(stream) => stream.conn.is_handshaking(), - Self::LoopbackServer(stream) => stream.conn.is_handshaking(), + fn upgrade_tls( + &self, + vm_id: &str, + kernel: &mut SidecarKernel, + options: JavascriptTlsBridgeOptions, + ) -> Result<(), SidecarError> { + if self.tls_mode.load(Ordering::SeqCst) { + return Ok(()); } - } - fn set_loopback_poll_timeout(&mut self, timeout: Duration) { - match self { - Self::LoopbackClient(stream) => stream.sock.set_poll_timeout(timeout), - Self::LoopbackServer(stream) => stream.sock.set_poll_timeout(timeout), - Self::Client(_) | Self::Server(_) => {} - } - } + let client_hello = if options.is_server { + self.peek_tls_client_hello(vm_id, kernel)? + } else { + None + }; - fn write_all(&mut self, contents: &[u8]) -> Result<(), SidecarError> { - match self { - Self::Client(stream) => { - stream.write_all(contents).map_err(sidecar_net_error)?; - stream.flush().map_err(sidecar_net_error) - } - Self::Server(stream) => { - stream.write_all(contents).map_err(sidecar_net_error)?; - stream.flush().map_err(sidecar_net_error) - } - Self::LoopbackClient(stream) => { - stream.write_all(contents).map_err(sidecar_net_error)?; - stream.flush().map_err(sidecar_net_error) - } - Self::LoopbackServer(stream) => { - stream.write_all(contents).map_err(sidecar_net_error)?; - stream.flush().map_err(sidecar_net_error) + let default_ca_bundle = vm_default_ca_bundle_for_tls_options(kernel, &options)?; + let (tls_stream, loopback_pending_write) = if let Some(socket_id) = self.kernel_socket_id { + let peer_socket_id = wait_for_loopback_peer_socket_id(kernel, socket_id) + .ok_or_else(|| { + SidecarError::Execution(format!( + "ERR_NOT_IMPLEMENTED: kernel-backed loopback socket {socket_id} has no peer for TLS upgrade" + )) + })?; + let endpoint = loopback_tls_endpoint(vm_id, socket_id, peer_socket_id)?; + let pending_write = LoopbackTlsPendingWriteHandle::new(&endpoint); + let tls_stream = if options.is_server { + ActiveTlsStream::LoopbackServer(build_server_loopback_tls_stream( + endpoint, &options, + )?) + } else { + ActiveTlsStream::LoopbackClient(build_client_loopback_tls_stream( + endpoint, + &options, + &default_ca_bundle, + )?) + }; + (tls_stream, Some(pending_write)) + } else { + self.pending_read_stream + .as_ref() + .ok_or_else(|| { + SidecarError::InvalidState(String::from("TCP socket reader handle missing")) + })? + .lock() + .map_err(|_| { + SidecarError::InvalidState(String::from("TCP socket reader lock poisoned")) + })? + .take(); + let stream = self + .stream + .as_ref() + .ok_or_else(|| { + SidecarError::InvalidState(String::from("TCP socket stream missing")) + })? + .lock() + .map_err(|_| { + SidecarError::InvalidState(String::from("TCP socket lock poisoned")) + })?; + let cloned = stream.try_clone().map_err(sidecar_net_error)?; + drop(stream); + + if options.is_server { + ( + ActiveTlsStream::Server(build_server_tls_stream(cloned, &options)?), + None, + ) + } else { + ( + ActiveTlsStream::Client(build_client_tls_stream( + cloned, + &options, + &default_ca_bundle, + )?), + None, + ) } - } - } + }; - fn read(&mut self, buffer: &mut [u8]) -> std::io::Result { - match self { - Self::Client(stream) => stream.read(buffer), - Self::Server(stream) => stream.read(buffer), - Self::LoopbackClient(stream) => stream.read(buffer), - Self::LoopbackServer(stream) => stream.read(buffer), - } - } + let tls_state = ActiveTlsState { + client_hello, + local_certificates: tls_local_certificates(&options)?, + session_reused: false, + }; - fn send_close_notify(&mut self) -> Result<(), SidecarError> { - match self { - Self::Client(stream) => { - stream.conn.send_close_notify(); - let _ = stream.conn.complete_io(&mut stream.sock); - } - Self::Server(stream) => { - stream.conn.send_close_notify(); - let _ = stream.conn.complete_io(&mut stream.sock); - } - Self::LoopbackClient(stream) => { - stream.conn.send_close_notify(); - let _ = stream.conn.complete_io(&mut stream.sock); - } - Self::LoopbackServer(stream) => { - stream.conn.send_close_notify(); - let _ = stream.conn.complete_io(&mut stream.sock); - } + self.tls_mode.store(true, Ordering::SeqCst); + { + let mut state = self + .tls_state + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))?; + *state = Some(tls_state); } - Ok(()) - } - - fn shutdown_write(&mut self) -> Result<(), SidecarError> { - match self { - Self::Client(stream) => stream - .sock - .shutdown(Shutdown::Write) - .map_err(sidecar_net_error), - Self::Server(stream) => stream - .sock - .shutdown(Shutdown::Write) - .map_err(sidecar_net_error), - Self::LoopbackClient(stream) => stream.sock.shutdown_write(), - Self::LoopbackServer(stream) => stream.sock.shutdown_write(), + { + let mut stream = self.tls_stream.lock().map_err(|_| { + SidecarError::InvalidState(String::from("TLS stream lock poisoned")) + })?; + *stream = Some(tls_stream); } - } - - fn close(&mut self) -> Result<(), SidecarError> { - match self { - Self::Client(stream) => stream - .sock - .shutdown(Shutdown::Both) - .map_err(sidecar_net_error), - Self::Server(stream) => stream - .sock - .shutdown(Shutdown::Both) - .map_err(sidecar_net_error), - Self::LoopbackClient(stream) => stream.sock.close_endpoint(), - Self::LoopbackServer(stream) => stream.sock.close_endpoint(), + { + let mut pending = self.loopback_tls_pending_write.lock().map_err(|_| { + SidecarError::InvalidState(String::from( + "loopback TLS pending write handle lock poisoned", + )) + })?; + *pending = loopback_pending_write.clone(); } - } - fn peer_certificates(&self) -> Option<&[CertificateDer<'static>]> { - match self { - Self::Client(stream) => stream.conn.peer_certificates(), - Self::Server(stream) => stream.conn.peer_certificates(), - Self::LoopbackClient(stream) => stream.conn.peer_certificates(), - Self::LoopbackServer(stream) => stream.conn.peer_certificates(), - } + spawn_tls_socket_reader( + Arc::clone(&self.tls_stream), + loopback_pending_write, + self.event_sender + .as_ref() + .ok_or_else(|| { + SidecarError::InvalidState(String::from("TCP socket event sender missing")) + })? + .clone(), + Arc::clone(&self.event_pusher), + Arc::clone(&self.saw_local_shutdown), + Arc::clone(&self.saw_remote_end), + Arc::clone(&self.close_notified), + ); + Ok(()) } - fn negotiated_cipher_suite(&self) -> Option { - match self { - Self::Client(stream) => stream.conn.negotiated_cipher_suite(), - Self::Server(stream) => stream.conn.negotiated_cipher_suite(), - Self::LoopbackClient(stream) => stream.conn.negotiated_cipher_suite(), - Self::LoopbackServer(stream) => stream.conn.negotiated_cipher_suite(), + fn peek_tls_client_hello( + &self, + vm_id: &str, + kernel: &SidecarKernel, + ) -> Result, SidecarError> { + if let Some(socket_id) = self.kernel_socket_id { + let Some(peer_socket_id) = kernel + .socket_get(socket_id) + .and_then(|record| record.peer_socket_id()) + else { + return Ok(None); + }; + return peek_loopback_tls_client_hello(vm_id, socket_id, peer_socket_id); } - } - fn protocol_version(&self) -> Option { - match self { - Self::Client(stream) => stream.conn.protocol_version(), - Self::Server(stream) => stream.conn.protocol_version(), - Self::LoopbackClient(stream) => stream.conn.protocol_version(), - Self::LoopbackServer(stream) => stream.conn.protocol_version(), - } + let stream = self + .stream + .as_ref() + .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))? + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?; + let mut buffer = vec![0_u8; 16 * 1024]; + let bytes = match stream.peek(&mut buffer) { + Ok(0) => return Ok(None), + Ok(bytes) => bytes, + Err(error) + if matches!( + error.kind(), + std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut + ) => + { + return Ok(None); + } + Err(error) => return Err(sidecar_net_error(error)), + }; + parse_tls_client_hello_from_bytes(&buffer[..bytes]) } -} - -// ActiveTcpListener moved to crate::state -// Unix socket types moved to crate::state + fn tls_client_hello_json( + &self, + vm_id: &str, + kernel: &SidecarKernel, + ) -> Result { + if let Some(client_hello) = self + .tls_state + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))? + .as_ref() + .and_then(|state| state.client_hello.clone()) + { + return javascript_net_json_string( + serde_json::to_value(client_hello).map_err(|error| { + SidecarError::InvalidState(format!( + "failed to serialize TLS client hello: {error}" + )) + })?, + "net.socket_get_tls_client_hello", + ); + } -impl ActiveUnixSocket { - fn connect(host_path: &Path, guest_path: &str) -> Result { - let stream = UnixStream::connect(host_path).map_err(sidecar_net_error)?; - Self::from_stream(stream, None, None, Some(guest_path.to_owned())) + javascript_net_json_string( + serde_json::to_value( + self.peek_tls_client_hello(vm_id, kernel)? + .unwrap_or_default(), + ) + .map_err(|error| { + SidecarError::InvalidState(format!("failed to serialize TLS client hello: {error}")) + })?, + "net.socket_get_tls_client_hello", + ) } - fn from_stream( - stream: UnixStream, - listener_id: Option, - local_path: Option, - remote_path: Option, - ) -> Result { - let read_stream = stream.try_clone().map_err(sidecar_net_error)?; - let stream = Arc::new(Mutex::new(stream)); - let (sender, events) = mpsc::channel(); - let event_pusher = Arc::new(Mutex::new(None)); - let saw_local_shutdown = Arc::new(AtomicBool::new(false)); - let saw_remote_end = Arc::new(AtomicBool::new(false)); - let close_notified = Arc::new(AtomicBool::new(false)); - spawn_unix_socket_reader( - read_stream, - sender.clone(), - Arc::clone(&event_pusher), - Arc::clone(&saw_local_shutdown), - Arc::clone(&saw_remote_end), - Arc::clone(&close_notified), - ); - - Ok(Self { - stream, - events, - event_sender: sender, - event_pusher, - listener_id, - local_path, - remote_path, - saw_local_shutdown, - saw_remote_end, - close_notified, - }) - } + fn tls_query(&self, query: &str, detailed: bool) -> Result { + let state = self + .tls_state + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("TLS state lock poisoned")))? + .clone(); + let mut tls_stream = self + .tls_stream + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))?; + let Some(stream) = tls_stream.as_mut() else { + return javascript_net_json_string( + tls_bridge_undefined_value(), + "net.socket_tls_query", + ); + }; - fn set_event_pusher(&self, session: Option, socket_id: String) { - let Some(session) = session else { - return; + let payload = match query { + "getSession" => tls_bridge_undefined_value(), + "isSessionReused" => Value::Bool( + state + .as_ref() + .is_some_and(|tls_state| tls_state.session_reused), + ), + "getPeerCertificate" => { + let certificate = stream + .peer_certificates() + .and_then(|certificates| certificates.first()) + .map(|certificate| { + tls_certificate_bridge_value(certificate.as_ref(), detailed) + }); + certificate.unwrap_or_else(tls_bridge_undefined_value) + } + "getCertificate" => state + .as_ref() + .and_then(|tls_state| tls_state.local_certificates.first()) + .map(|certificate| tls_certificate_bridge_value(certificate, detailed)) + .unwrap_or_else(tls_bridge_undefined_value), + "getProtocol" => stream + .protocol_version() + .map(tls_protocol_name) + .map(Value::String) + .unwrap_or(Value::Null), + "getCipher" => stream + .negotiated_cipher_suite() + .map(tls_cipher_bridge_value) + .unwrap_or_else(tls_bridge_undefined_value), + other => { + return Err(SidecarError::InvalidState(format!( + "unsupported TLS query {other}" + ))); + } }; - if let Ok(mut pusher) = self.event_pusher.lock() { - *pusher = Some(JavascriptSocketEventPusher { session, socket_id }); - } + javascript_net_json_string(payload, "net.socket_tls_query") } - fn poll(&mut self, wait: Duration) -> Result, SidecarError> { - match self.events.recv_timeout(wait) { - Ok(event) => Ok(Some(event)), - Err(RecvTimeoutError::Timeout) => Ok(None), - Err(RecvTimeoutError::Disconnected) => Ok(None), + fn write_all( + &self, + kernel: &mut SidecarKernel, + kernel_pid: u32, + contents: &[u8], + ) -> Result { + if self.tls_mode.load(Ordering::SeqCst) { + let loopback_pending_write = self + .loopback_tls_pending_write + .lock() + .map_err(|_| { + SidecarError::InvalidState(String::from( + "loopback TLS pending write handle lock poisoned", + )) + })? + .clone(); + if let Some(pending_write) = loopback_pending_write.as_ref() { + if pending_write.should_buffer_write()? { + pending_write.append_write(contents)?; + return Ok(contents.len()); + } + pending_write.interrupt_own_reader(); + } + let mut tls_stream = self.tls_stream.lock().map_err(|_| { + SidecarError::InvalidState(String::from("TLS stream lock poisoned")) + })?; + let stream = tls_stream.as_mut().ok_or_else(|| { + SidecarError::InvalidState(String::from("TLS stream missing for upgraded socket")) + })?; + stream.write_all(contents)?; + return Ok(contents.len()); + } + if let Some(socket_id) = self.kernel_socket_id { + return kernel + .socket_write(EXECUTION_DRIVER_NAME, kernel_pid, socket_id, contents) + .map_err(kernel_error); } - } - - fn socket_info(&self) -> Value { - unix_socket_info_value(self.local_path.as_deref(), self.remote_path.as_deref()) - } - fn write_all(&self, contents: &[u8]) -> Result { let mut stream = self .stream + .as_ref() + .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))? .lock() - .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?; + .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?; stream.write_all(contents).map_err(sidecar_net_error)?; Ok(contents.len()) } - fn shutdown_write(&self) -> Result<(), SidecarError> { + fn shutdown_write( + &self, + kernel: &mut SidecarKernel, + kernel_pid: u32, + ) -> Result<(), SidecarError> { + if self.tls_mode.load(Ordering::SeqCst) { + let loopback_pending_write = self + .loopback_tls_pending_write + .lock() + .map_err(|_| { + SidecarError::InvalidState(String::from( + "loopback TLS pending write handle lock poisoned", + )) + })? + .clone(); + if let Some(pending_write) = loopback_pending_write.as_ref() { + if pending_write.should_buffer_write()? { + pending_write.defer_shutdown_write()?; + self.saw_local_shutdown.store(true, Ordering::SeqCst); + return Ok(()); + } + pending_write.interrupt_own_reader(); + } + if let Some(stream) = self + .tls_stream + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))? + .as_mut() + { + let _ = stream.send_close_notify(); + let _ = stream.shutdown_write(); + } + if self.kernel_socket_id.is_some() { + self.saw_local_shutdown.store(true, Ordering::SeqCst); + return Ok(()); + } + } + if let Some(socket_id) = self.kernel_socket_id { + self.saw_local_shutdown.store(true, Ordering::SeqCst); + match kernel.socket_shutdown( + EXECUTION_DRIVER_NAME, + kernel_pid, + socket_id, + KernelSocketShutdown::Write, + ) { + Ok(()) => {} + Err(error) if error.code() == "ENOENT" => {} + Err(error) => return Err(kernel_error(error)), + } + return Ok(()); + } let stream = self .stream + .as_ref() + .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))? .lock() - .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?; + .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?; self.saw_local_shutdown.store(true, Ordering::SeqCst); - stream - .shutdown(Shutdown::Write) - .map_err(sidecar_net_error)?; + match stream.shutdown(Shutdown::Write) { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotConnected => {} + Err(error) => return Err(sidecar_net_error(error)), + } if self.saw_remote_end.load(Ordering::SeqCst) && !self.close_notified.swap(true, Ordering::SeqCst) { let _ = self .event_sender + .as_ref() + .ok_or_else(|| { + SidecarError::InvalidState(String::from("TCP socket event sender missing")) + })? .send(JavascriptTcpSocketEvent::Close { had_error: false }); } Ok(()) } - fn close(&self) -> Result<(), SidecarError> { + fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> { + if self.tls_mode.load(Ordering::SeqCst) { + if let Some(pending_write) = self + .loopback_tls_pending_write + .lock() + .map_err(|_| { + SidecarError::InvalidState(String::from( + "loopback TLS pending write handle lock poisoned", + )) + })? + .take() + { + pending_write.clear_for_close(); + pending_write.interrupt_own_reader(); + } + if let Some(stream) = self + .tls_stream + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("TLS stream lock poisoned")))? + .as_mut() + { + let _ = stream.send_close_notify(); + let _ = stream.close(); + } + } + if let Some(socket_id) = self.kernel_socket_id { + return close_kernel_socket_idempotent(kernel, kernel_pid, socket_id); + } let stream = self .stream + .as_ref() + .ok_or_else(|| SidecarError::InvalidState(String::from("TCP socket stream missing")))? .lock() - .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?; + .map_err(|_| SidecarError::InvalidState(String::from("TCP socket lock poisoned")))?; stream.shutdown(Shutdown::Both).map_err(sidecar_net_error) } } -// ActiveUnixListener moved to crate::state - -impl ActiveUnixListener { - fn bind( - host_path: &Path, - guest_path: &str, - backlog: Option, - ) -> Result { - if let Some(parent) = host_path.parent() { - fs::create_dir_all(parent).map_err(sidecar_net_error)?; - } - let listener = UnixListener::bind(host_path).map_err(sidecar_net_error)?; - listener.set_nonblocking(true).map_err(sidecar_net_error)?; - Ok(Self { - listener, - path: guest_path.to_owned(), - backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG)) - .expect("default backlog fits within usize"), - active_connection_ids: BTreeSet::new(), - }) +fn close_kernel_socket_idempotent( + kernel: &mut SidecarKernel, + kernel_pid: u32, + socket_id: SocketId, +) -> Result<(), SidecarError> { + match kernel.socket_close(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) { + Ok(()) => Ok(()), + Err(error) if error.code() == "ENOENT" => Ok(()), + Err(error) => Err(kernel_error(error)), } +} - fn path(&self) -> &str { - &self.path +fn register_kernel_readiness_target( + registry: &KernelSocketReadinessRegistry, + kernel_socket_id: Option, + session: Option, + target_id: String, + event: KernelSocketReadinessEvent, +) { + let (Some(kernel_socket_id), Some(session)) = (kernel_socket_id, session) else { + return; + }; + if let Ok(mut targets) = registry.lock() { + targets.insert( + kernel_socket_id, + KernelSocketReadinessTarget { + session, + target_id, + event, + }, + ); } +} - fn poll( - &mut self, - wait: Duration, - ) -> Result, SidecarError> { - let deadline = Instant::now() + wait; - loop { - match self.listener.accept() { - Ok((stream, remote_addr)) => { - if self.active_connection_ids.len() >= self.backlog { - let _ = stream.shutdown(Shutdown::Both); - if wait.is_zero() || Instant::now() >= deadline { - return Ok(None); - } - continue; - } - - let local_path = Some(self.path.clone()); - let remote_path = unix_socket_path(&remote_addr); - return Ok(Some(JavascriptUnixListenerEvent::Connection( - PendingUnixSocket { - stream, - local_path, - remote_path, - }, - ))); - } - Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { - if wait.is_zero() || Instant::now() >= deadline { - return Ok(None); - } - if !wait_fd_readable_until(self.listener.as_fd(), deadline) { - return Ok(None); - } - } - Err(error) => { - return Ok(Some(JavascriptUnixListenerEvent::Error { - code: io_error_code(&error), - message: error.to_string(), - })); - } - } - } +fn unregister_kernel_readiness_target( + registry: &KernelSocketReadinessRegistry, + kernel_socket_id: Option, +) { + let Some(kernel_socket_id) = kernel_socket_id else { + return; + }; + if let Ok(mut targets) = registry.lock() { + targets.remove(&kernel_socket_id); } +} - fn close(&self) -> Result<(), SidecarError> { - Ok(()) - } +fn rebind_process_runtime_event_targets( + process: &mut ActiveProcess, + kernel_readiness: &KernelSocketReadinessRegistry, +) { + let session = process.execution.javascript_v8_session_handle(); - fn active_connection_count(&self) -> usize { - self.active_connection_ids.len() + for (socket_id, socket) in &process.tcp_sockets { + socket.set_event_pusher(session.clone(), socket_id.clone()); + unregister_kernel_readiness_target(kernel_readiness, socket.kernel_socket_id); + register_kernel_readiness_target( + kernel_readiness, + socket.kernel_socket_id, + session.clone(), + socket_id.clone(), + KernelSocketReadinessEvent::Data, + ); } - - fn register_connection(&mut self, socket_id: &str) { - self.active_connection_ids.insert(socket_id.to_string()); + for (socket_id, socket) in &process.unix_sockets { + socket.set_event_pusher(session.clone(), socket_id.clone()); + } + for (listener_id, listener) in &process.tcp_listeners { + unregister_kernel_readiness_target(kernel_readiness, listener.kernel_socket_id); + register_kernel_readiness_target( + kernel_readiness, + listener.kernel_socket_id, + session.clone(), + listener_id.clone(), + KernelSocketReadinessEvent::Accept, + ); + } + for (socket_id, socket) in &process.udp_sockets { + unregister_kernel_readiness_target(kernel_readiness, socket.kernel_socket_id); + register_kernel_readiness_target( + kernel_readiness, + socket.kernel_socket_id, + session.clone(), + socket_id.clone(), + KernelSocketReadinessEvent::Datagram, + ); + } + if let Ok(mut http2) = process.http2.shared.lock() { + http2.event_session = session; } +} - fn release_connection(&mut self, socket_id: &str) { - self.active_connection_ids.remove(socket_id); +fn release_tcp_socket_handle( + process: &mut ActiveProcess, + socket_id: &str, + socket: ActiveTcpSocket, + kernel: &mut SidecarKernel, + kernel_readiness: &KernelSocketReadinessRegistry, +) { + if !socket.is_final_description_handle() { + return; + } + unregister_kernel_readiness_target(kernel_readiness, socket.kernel_socket_id); + if let Some(listener_id) = socket.listener_id.as_deref() { + if let Some(listener) = process.tcp_listeners.get_mut(listener_id) { + listener.release_connection(socket_id); + } + } + if let Err(error) = socket.close(kernel, process.kernel_pid) { + eprintln!("failed to close TCP socket {socket_id}: {error}"); } } -impl ActiveTcpListener { - fn bind( - bind_host: &str, - guest_host: &str, - guest_port: u16, - backlog: Option, - ) -> Result { - let bind_addr = resolve_tcp_bind_addr(bind_host, 0)?; - let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?; - let listener = TcpListener::bind(bind_addr).map_err(sidecar_net_error)?; - listener.set_nonblocking(true).map_err(sidecar_net_error)?; - let local_addr = listener.local_addr().map_err(sidecar_net_error)?; - Ok(Self { - listener: Some(listener), - kernel_socket_id: None, - local_addr: Some(local_addr), - guest_local_addr: guest_addr, - backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG)) - .expect("default backlog fits within usize"), - active_connection_ids: BTreeSet::new(), - }) +fn release_unix_socket_handle( + process: &mut ActiveProcess, + socket_id: &str, + mut socket: ActiveUnixSocket, + socket_paths: &JavascriptSocketPathContext, +) { + if !socket.is_final_description_handle() { + return; + } + if socket.listener_id.is_some() { + if let Err(error) = socket.cache_remote_peer_metadata(&socket_paths.unix_bound_addresses) { + eprintln!("failed to cache Unix peer metadata for {socket_id}: {error}"); + } + if let Some(state) = socket.connection_state.as_ref() { + state.accepted_peer_open.store(false, Ordering::SeqCst); + } + } + if let Some(listener_id) = socket.listener_id.as_deref() { + if let Some(listener) = process.unix_listeners.get_mut(listener_id) { + listener.release_connection(socket_id); + } } + if let Err(error) = socket.close() { + eprintln!("failed to close Unix socket {socket_id}: {error}"); + } + if let Some(binding_id) = socket.local_registry_binding_id.as_deref() { + if let Err(error) = + release_guest_unix_binding(&socket_paths.unix_bound_addresses, binding_id) + { + eprintln!("failed to release Unix address metadata for {socket_id}: {error}"); + } + } +} - fn bind_kernel( - kernel: &mut SidecarKernel, - kernel_pid: u32, - guest_host: &str, - guest_port: u16, - backlog: Option, - ) -> Result { - let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?; - let spec = match guest_addr { - SocketAddr::V4(_) => SocketSpec::tcp(), - SocketAddr::V6(_) => SocketSpec::new(SocketDomain::Inet6, SocketType::Stream), - }; - let socket_id = kernel - .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec) - .map_err(kernel_error)?; - kernel - .socket_bind_inet( - EXECUTION_DRIVER_NAME, - kernel_pid, - socket_id, - InetSocketAddress::new(guest_addr.ip().to_string(), guest_addr.port()), - ) - .map_err(kernel_error)?; - kernel - .socket_listen( - EXECUTION_DRIVER_NAME, - kernel_pid, - socket_id, - usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG)) - .expect("default backlog fits within usize"), - ) - .map_err(kernel_error)?; - Ok(Self { - listener: None, - kernel_socket_id: Some(socket_id), - local_addr: Some(guest_addr), - guest_local_addr: guest_addr, - backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG)) - .expect("default backlog fits within usize"), - active_connection_ids: BTreeSet::new(), - }) +impl ActiveTlsStream { + fn is_loopback(&self) -> bool { + matches!(self, Self::LoopbackClient(_) | Self::LoopbackServer(_)) } - pub(crate) fn local_addr(&self) -> SocketAddr { - self.local_addr.unwrap_or(self.guest_local_addr) + fn is_handshaking(&self) -> bool { + match self { + Self::Client(stream) => stream.conn.is_handshaking(), + Self::Server(stream) => stream.conn.is_handshaking(), + Self::LoopbackClient(stream) => stream.conn.is_handshaking(), + Self::LoopbackServer(stream) => stream.conn.is_handshaking(), + } } - fn guest_local_addr(&self) -> SocketAddr { - self.guest_local_addr + fn set_loopback_poll_timeout(&mut self, timeout: Duration) { + match self { + Self::LoopbackClient(stream) => stream.sock.set_poll_timeout(timeout), + Self::LoopbackServer(stream) => stream.sock.set_poll_timeout(timeout), + Self::Client(_) | Self::Server(_) => {} + } } - fn poll( - &mut self, - kernel: &mut SidecarKernel, - kernel_pid: u32, - wait: Duration, - trace_enabled: bool, - ) -> Result, SidecarError> { - if let Some(socket_id) = self.kernel_socket_id { - let poll_started = Instant::now(); - let result = kernel - .poll_targets( - EXECUTION_DRIVER_NAME, - kernel_pid, - vec![PollTargetEntry::socket(socket_id, POLLIN)], - i32::try_from(wait.as_millis()).unwrap_or(i32::MAX), - ) - .map_err(kernel_error)?; - let poll_elapsed = poll_started.elapsed(); - let revents = result - .targets - .first() - .map(|entry| entry.revents) - .unwrap_or_else(PollEvents::empty); - record_net_tcp_kernel_poll(trace_enabled, wait, poll_elapsed, revents); - if revents.is_empty() { - return Ok(None); + fn write_all(&mut self, contents: &[u8]) -> Result<(), SidecarError> { + match self { + Self::Client(stream) => { + stream.write_all(contents).map_err(sidecar_net_error)?; + stream.flush().map_err(sidecar_net_error) } - let accepted_socket_id = - match kernel.socket_accept(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) { - Ok(accepted_socket_id) => accepted_socket_id, - Err(error) if error.code() == "EAGAIN" => { - if trace_enabled { - NET_TCP_TRACE_COUNTERS - .server_accept_eagain - .fetch_add(1, Ordering::Relaxed); - } - return Ok(None); - } - Err(error) => { - if trace_enabled { - NET_TCP_TRACE_COUNTERS - .server_accept_errors - .fetch_add(1, Ordering::Relaxed); - } - return Ok(Some(JavascriptTcpListenerEvent::Error { - code: Some(error.code().to_string()), - message: error.to_string(), - })); - } - }; - let accepted = kernel.socket_get(accepted_socket_id).ok_or_else(|| { - SidecarError::InvalidState(format!( - "accepted kernel TCP socket {accepted_socket_id} is missing" - )) - })?; - let local_addr = accepted.local_address().ok_or_else(|| { - SidecarError::InvalidState(format!( - "accepted kernel TCP socket {accepted_socket_id} missing local address" - )) - })?; - let remote_addr = accepted.peer_address().ok_or_else(|| { - SidecarError::InvalidState(format!( - "accepted kernel TCP socket {accepted_socket_id} missing peer address" - )) - })?; - if trace_enabled { - NET_TCP_TRACE_COUNTERS - .server_accept_connections - .fetch_add(1, Ordering::Relaxed); + Self::Server(stream) => { + stream.write_all(contents).map_err(sidecar_net_error)?; + stream.flush().map_err(sidecar_net_error) + } + Self::LoopbackClient(stream) => { + stream.write_all(contents).map_err(sidecar_net_error)?; + stream.flush().map_err(sidecar_net_error) + } + Self::LoopbackServer(stream) => { + stream.write_all(contents).map_err(sidecar_net_error)?; + stream.flush().map_err(sidecar_net_error) } - return Ok(Some(JavascriptTcpListenerEvent::Connection( - PendingTcpSocket { - stream: None, - kernel_socket_id: Some(accepted_socket_id), - preallocated: true, - guest_local_addr: resolve_tcp_bind_addr(local_addr.host(), local_addr.port())?, - guest_remote_addr: resolve_tcp_bind_addr( - remote_addr.host(), - remote_addr.port(), - )?, - }, - ))); } + } - let deadline = Instant::now() + wait; - loop { - match self - .listener - .as_ref() - .ok_or_else(|| { - SidecarError::InvalidState(String::from("TCP listener socket missing")) - })? - .accept() - { - Ok((stream, remote_addr)) => { - if self.active_connection_ids.len() >= self.backlog { - let _ = stream.shutdown(Shutdown::Both); - if wait.is_zero() || Instant::now() >= deadline { - return Ok(None); - } - continue; - } - return Ok(Some(JavascriptTcpListenerEvent::Connection( - PendingTcpSocket { - stream: Some(stream), - kernel_socket_id: None, - preallocated: false, - guest_local_addr: self.guest_local_addr, - guest_remote_addr: SocketAddr::new( - remote_addr.ip(), - remote_addr.port(), - ), - }, - ))); - } - Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { - if wait.is_zero() || Instant::now() >= deadline { - return Ok(None); - } - if !wait_fd_readable_until( - self.listener - .as_ref() - .expect("TCP listener checked before accept") - .as_fd(), - deadline, - ) { - return Ok(None); - } - } - Err(error) => { - return Ok(Some(JavascriptTcpListenerEvent::Error { - code: io_error_code(&error), - message: error.to_string(), - })); - } - } + fn read(&mut self, buffer: &mut [u8]) -> std::io::Result { + match self { + Self::Client(stream) => stream.read(buffer), + Self::Server(stream) => stream.read(buffer), + Self::LoopbackClient(stream) => stream.read(buffer), + Self::LoopbackServer(stream) => stream.read(buffer), } } - fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> { - if let Some(socket_id) = self.kernel_socket_id { - close_kernel_socket_idempotent(kernel, kernel_pid, socket_id)?; + fn send_close_notify(&mut self) -> Result<(), SidecarError> { + match self { + Self::Client(stream) => { + stream.conn.send_close_notify(); + let _ = stream.conn.complete_io(&mut stream.sock); + } + Self::Server(stream) => { + stream.conn.send_close_notify(); + let _ = stream.conn.complete_io(&mut stream.sock); + } + Self::LoopbackClient(stream) => { + stream.conn.send_close_notify(); + let _ = stream.conn.complete_io(&mut stream.sock); + } + Self::LoopbackServer(stream) => { + stream.conn.send_close_notify(); + let _ = stream.conn.complete_io(&mut stream.sock); + } } Ok(()) } - fn active_connection_count(&self) -> usize { - self.active_connection_ids.len() + fn shutdown_write(&mut self) -> Result<(), SidecarError> { + match self { + Self::Client(stream) => stream + .sock + .shutdown(Shutdown::Write) + .map_err(sidecar_net_error), + Self::Server(stream) => stream + .sock + .shutdown(Shutdown::Write) + .map_err(sidecar_net_error), + Self::LoopbackClient(stream) => stream.sock.shutdown_write(), + Self::LoopbackServer(stream) => stream.sock.shutdown_write(), + } } - fn register_connection(&mut self, socket_id: &str) { - self.active_connection_ids.insert(socket_id.to_string()); + fn close(&mut self) -> Result<(), SidecarError> { + match self { + Self::Client(stream) => stream + .sock + .shutdown(Shutdown::Both) + .map_err(sidecar_net_error), + Self::Server(stream) => stream + .sock + .shutdown(Shutdown::Both) + .map_err(sidecar_net_error), + Self::LoopbackClient(stream) => stream.sock.close_endpoint(), + Self::LoopbackServer(stream) => stream.sock.close_endpoint(), + } } - fn release_connection(&mut self, socket_id: &str) { - self.active_connection_ids.remove(socket_id); + fn peer_certificates(&self) -> Option<&[CertificateDer<'static>]> { + match self { + Self::Client(stream) => stream.conn.peer_certificates(), + Self::Server(stream) => stream.conn.peer_certificates(), + Self::LoopbackClient(stream) => stream.conn.peer_certificates(), + Self::LoopbackServer(stream) => stream.conn.peer_certificates(), + } + } + + fn negotiated_cipher_suite(&self) -> Option { + match self { + Self::Client(stream) => stream.conn.negotiated_cipher_suite(), + Self::Server(stream) => stream.conn.negotiated_cipher_suite(), + Self::LoopbackClient(stream) => stream.conn.negotiated_cipher_suite(), + Self::LoopbackServer(stream) => stream.conn.negotiated_cipher_suite(), + } + } + + fn protocol_version(&self) -> Option { + match self { + Self::Client(stream) => stream.conn.protocol_version(), + Self::Server(stream) => stream.conn.protocol_version(), + Self::LoopbackClient(stream) => stream.conn.protocol_version(), + Self::LoopbackServer(stream) => stream.conn.protocol_version(), + } } } -// UDP types moved to crate::state +// ActiveTcpListener moved to crate::state -impl ActiveUdpSocket { - fn new( - kernel: &mut SidecarKernel, - kernel_pid: u32, - family: JavascriptUdpFamily, - ) -> Result { - let spec = match family { - JavascriptUdpFamily::Ipv4 => SocketSpec::udp(), - JavascriptUdpFamily::Ipv6 => SocketSpec::new(SocketDomain::Inet6, SocketType::Datagram), - }; - let socket_id = kernel - .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec) - .map_err(kernel_error)?; - Ok(Self { - family, - socket: None, - kernel_socket_id: Some(socket_id), - guest_local_addr: None, - recv_buffer_size: 0, - send_buffer_size: 0, - }) +// Unix socket types moved to crate::state + +fn decode_abstract_unix_name(hex: &str) -> Result, SidecarError> { + if hex.len() % 2 != 0 || hex.len() > 214 || !hex.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err(SidecarError::InvalidState(String::from( + "abstract Unix socket names must be at most 107 bytes of hexadecimal data", + ))); } + hex.as_bytes() + .chunks_exact(2) + .map(|pair| { + let high = (pair[0] as char).to_digit(16).expect("validated hex digit"); + let low = (pair[1] as char).to_digit(16).expect("validated hex digit"); + Ok(((high << 4) | low) as u8) + }) + .collect() +} - fn local_addr(&self) -> Option { - self.guest_local_addr +fn abstract_unix_node_path(name: &[u8]) -> String { + let mut path = String::with_capacity(1 + name.len()); + path.push('\0'); + path.push_str(&String::from_utf8_lossy(name)); + path +} + +fn abstract_unix_name_hex(name: &[u8]) -> String { + let mut hex = String::with_capacity(name.len() * 2); + for byte in name { + use std::fmt::Write as _; + write!(&mut hex, "{byte:02x}").expect("writing to String cannot fail"); } + hex +} - fn socket(&self) -> Result<&UdpSocket, SidecarError> { - self.socket - .as_ref() - .ok_or_else(|| SidecarError::Execution(String::from("EBADF: bad file descriptor"))) +fn abstract_unix_host_address_key(name: &[u8]) -> String { + format!("abstract:{}", abstract_unix_name_hex(name)) +} + +fn pathname_unix_host_address_key(path: &Path) -> String { + format!("pathname:{}", path.to_string_lossy()) +} + +fn unix_host_address_key(address: &UnixSocketAddr) -> Option { + if let Some(path) = address.as_pathname() { + return Some(pathname_unix_host_address_key(path)); + } + #[cfg(target_os = "linux")] + if let Some(name) = address.as_abstract_name() { + return Some(abstract_unix_host_address_key(name)); } + None +} - fn bind( - &mut self, - kernel: &mut SidecarKernel, - kernel_pid: u32, - host: Option<&str>, - port: u16, - context: &JavascriptSocketPathContext, - ) -> Result { - if self.socket.is_some() || self.guest_local_addr.is_some() { - return Err(SidecarError::Execution(String::from( - "EINVAL: secure-exec dgram socket is already bound", - ))); - } +static NEXT_GUEST_UNIX_BINDING_GENERATION: AtomicU64 = AtomicU64::new(1); - let (bind_host, guest_host, guest_family) = normalize_udp_bind_host(host, self.family)?; - let guest_port = allocate_guest_listen_port( - port, - guest_family, - &context.used_udp_guest_ports, - context.listen_policy, - )?; - let local_addr = resolve_udp_bind_addr(guest_host, guest_port, self.family)?; - if let Some(socket_id) = self.kernel_socket_id { - kernel - .socket_bind_inet( - EXECUTION_DRIVER_NAME, - kernel_pid, - socket_id, - InetSocketAddress::new(local_addr.ip().to_string(), local_addr.port()), - ) - .map_err(kernel_error)?; - } else { - let bind_addr = resolve_udp_bind_addr(bind_host, 0, self.family)?; - let socket = UdpSocket::bind(bind_addr).map_err(sidecar_net_error)?; - socket.set_nonblocking(true).map_err(sidecar_net_error)?; - self.socket = Some(socket); - } - self.guest_local_addr = Some(local_addr); - Ok(local_addr) +fn guest_unix_binding_id(kernel_pid: u32, local_id: &str) -> String { + format!("{kernel_pid}:{local_id}") +} + +fn register_guest_unix_binding( + registry: &GuestUnixAddressRegistry, + binding_id: &str, + host_address_key: &str, + address: GuestUnixAddress, + guest_device_inode: Option<(u64, u64)>, + host_path: Option, +) -> Result<(), SidecarError> { + let mut registry = registry + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix address registry poisoned")))?; + if registry.contains_key(binding_id) { + return Err(SidecarError::InvalidState(format!( + "duplicate Unix binding id {binding_id}" + ))); } + registry.insert( + binding_id.to_owned(), + GuestUnixAddressRegistryEntry { + host_address_key: host_address_key.to_owned(), + address, + guest_device_inode, + host_path, + generation: NEXT_GUEST_UNIX_BINDING_GENERATION.fetch_add(1, Ordering::Relaxed), + active_bindings: 1, + queued_by_target: BTreeMap::new(), + pending_connections: VecDeque::new(), + }, + ); + Ok(()) +} - fn ensure_bound_for_send( - &mut self, - kernel: &mut SidecarKernel, - kernel_pid: u32, - context: &JavascriptSocketPathContext, - ) -> Result { - if let Some(local_addr) = self.local_addr() { - return Ok(local_addr); - } +fn guest_unix_path_target( + context: &JavascriptSocketPathContext, + guest_device_inode: (u64, u64), +) -> Result, SidecarError> { + let registry = context + .unix_bound_addresses + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix address registry poisoned")))?; + Ok(registry.iter().find_map(|(binding_id, entry)| { + (entry.active_bindings > 0 && entry.guest_device_inode == Some(guest_device_inode)).then( + || { + entry + .host_path + .clone() + .map(|path| (path, binding_id.clone(), entry.address.clone())) + }, + )? + })) +} - self.bind(kernel, kernel_pid, None, 0, context) - } +fn guest_unix_address_for_host_key( + registry: &GuestUnixAddressRegistry, + host_address_key: &str, +) -> Result, SidecarError> { + let registry = registry + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix address registry poisoned")))?; + Ok(registry + .values() + .filter(|entry| entry.host_address_key == host_address_key) + .max_by_key(|entry| entry.generation) + .map(|entry| entry.address.clone())) +} - fn send_to( - &mut self, - request: ActiveUdpSendToRequest<'_, B>, - ) -> Result<(usize, SocketAddr), SidecarError> - where - B: NativeSidecarBridge + Send + 'static, - BridgeError: fmt::Debug + Send + Sync + 'static, - { - let ActiveUdpSendToRequest { - bridge, - kernel, - kernel_pid, - vm_id, - dns, - host, - port, - context, - contents, - } = request; - let remote_addr = resolve_udp_addr(UdpRemoteAddrRequest { - bridge, - kernel, - vm_id, - dns, - host, - port, - family: self.family, - context, - })?; - let local_addr = self.ensure_bound_for_send(kernel, kernel_pid, context)?; - let written = if let Some(socket_id) = self.kernel_socket_id { - if is_loopback_ip(remote_addr.ip()) && remote_addr.port() == port { - kernel - .socket_send_to_inet_loopback( - EXECUTION_DRIVER_NAME, - kernel_pid, - socket_id, - InetSocketAddress::new(remote_addr.ip().to_string(), remote_addr.port()), - contents, - ) - .map_err(kernel_error)? - } else { - return Err(SidecarError::Execution(String::from( - "ERR_NOT_IMPLEMENTED: external UDP datagrams are not yet supported by the kernel-backed V8 bridge", - ))); - } - } else { - let socket = self.socket.as_ref().ok_or_else(|| { - SidecarError::InvalidState(String::from("UDP socket is not initialized")) - })?; - socket - .send_to(contents, remote_addr) - .map_err(sidecar_net_error)? - }; - Ok((written, local_addr)) - } +fn guest_unix_binding_for_host_key( + registry: &GuestUnixAddressRegistry, + host_address_key: &str, +) -> Result, SidecarError> { + let registry = registry + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix address registry poisoned")))?; + Ok(registry + .iter() + .filter(|(_, entry)| { + entry.active_bindings > 0 && entry.host_address_key == host_address_key + }) + .max_by_key(|(_, entry)| entry.generation) + .map(|(binding_id, entry)| (binding_id.clone(), entry.address.clone()))) +} - fn poll( - &self, - kernel: &mut SidecarKernel, - kernel_pid: u32, - wait: Duration, - ) -> Result, SidecarError> { - if let Some(socket_id) = self.kernel_socket_id { - let result = kernel - .poll_targets( - EXECUTION_DRIVER_NAME, - kernel_pid, - vec![PollTargetEntry::socket(socket_id, POLLIN)], - i32::try_from(wait.as_millis()).unwrap_or(i32::MAX), - ) - .map_err(kernel_error)?; - let revents = result - .targets - .first() - .map(|entry| entry.revents) - .unwrap_or_else(PollEvents::empty); - if revents.is_empty() { - return Ok(None); - } - return match kernel.socket_recv_datagram( - EXECUTION_DRIVER_NAME, - kernel_pid, - socket_id, - 64 * 1024, - ) { - Ok(Some(datagram)) => { - let (source_address, payload) = datagram.into_parts(); - let remote_addr = source_address - .map(|source| { - resolve_udp_bind_addr(source.host(), source.port(), self.family) - }) - .transpose()? - .unwrap_or_else(|| match self.family { - JavascriptUdpFamily::Ipv4 => { - SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0) - } - JavascriptUdpFamily::Ipv6 => { - SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0) - } - }); - Ok(Some(JavascriptUdpSocketEvent::Message { - data: payload, - remote_addr, - })) - } - Ok(None) => Ok(None), - Err(error) if error.code() == "EAGAIN" => Ok(None), - Err(error) => Ok(Some(JavascriptUdpSocketEvent::Error { - code: Some(error.code().to_string()), - message: error.to_string(), - })), - }; - } - let socket = self.socket()?; - let deadline = Instant::now() + wait; - let mut buffer = vec![0_u8; 64 * 1024]; +fn queue_guest_unix_peer( + registry: &GuestUnixAddressRegistry, + source_binding_id: &str, + target_binding_id: &str, +) -> Result<(), SidecarError> { + let mut registry = registry + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix address registry poisoned")))?; + let entry = registry.get_mut(source_binding_id).ok_or_else(|| { + SidecarError::InvalidState(format!( + "missing bound Unix address metadata for {source_binding_id}" + )) + })?; + let queued = entry + .queued_by_target + .entry(target_binding_id.to_owned()) + .or_default(); + *queued = queued.saturating_add(1); + Ok(()) +} - loop { - match socket.recv_from(&mut buffer) { - Ok((bytes_read, remote_addr)) => { - return Ok(Some(JavascriptUdpSocketEvent::Message { - data: buffer[..bytes_read].to_vec(), - remote_addr, - })); - } - Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { - if wait.is_zero() || Instant::now() >= deadline { - return Ok(None); - } - if !wait_fd_readable_until(socket.as_fd(), deadline) { - return Ok(None); - } - } - Err(error) => { - return Ok(Some(JavascriptUdpSocketEvent::Error { - code: io_error_code(&error), - message: error.to_string(), - })); - } - } +fn register_guest_unix_connection( + registry: &GuestUnixAddressRegistry, + target_binding_id: &str, +) -> Result, SidecarError> { + let mut registry = registry + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix address registry poisoned")))?; + let target = registry.get_mut(target_binding_id).ok_or_else(|| { + SidecarError::InvalidState(format!( + "missing target Unix address metadata for {target_binding_id}" + )) + })?; + let state = Arc::new(GuestUnixConnectionState { + accepted_peer_open: AtomicBool::new(true), + }); + target.pending_connections.push_back(Arc::clone(&state)); + Ok(state) +} + +fn accept_guest_unix_connection( + registry: &GuestUnixAddressRegistry, + target_binding_id: &str, +) -> Result, SidecarError> { + let mut registry = registry + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix address registry poisoned")))?; + registry + .get_mut(target_binding_id) + .and_then(|target| target.pending_connections.pop_front()) + .ok_or_else(|| { + SidecarError::InvalidState(format!( + "missing pending Unix connection metadata for {target_binding_id}" + )) + }) +} + +fn close_pending_guest_unix_connections( + registry: &GuestUnixAddressRegistry, + target_binding_id: &str, +) -> Result<(), SidecarError> { + let mut registry = registry + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix address registry poisoned")))?; + if let Some(target) = registry.get_mut(target_binding_id) { + for state in target.pending_connections.drain(..) { + state.accepted_peer_open.store(false, Ordering::SeqCst); } } + Ok(()) +} - fn close(&mut self, kernel: &mut SidecarKernel, kernel_pid: u32) { - if let Some(socket_id) = self.kernel_socket_id { - let _ = close_kernel_socket_idempotent(kernel, kernel_pid, socket_id); +fn guest_unix_connection_peer_open(state: Option<&Arc>) -> bool { + state.is_some_and(|state| state.accepted_peer_open.load(Ordering::SeqCst)) +} + +fn consume_guest_unix_peer( + registry: &GuestUnixAddressRegistry, + source_host_address_key: &str, + target_binding_id: &str, +) -> Result, SidecarError> { + let mut registry = registry + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix address registry poisoned")))?; + let source_binding_id = registry + .iter() + .filter(|(_, entry)| { + entry.host_address_key == source_host_address_key + && entry + .queued_by_target + .get(target_binding_id) + .copied() + .unwrap_or_default() + > 0 + }) + .max_by_key(|(_, entry)| entry.generation) + .map(|(binding_id, _)| binding_id.clone()); + let Some(source_binding_id) = source_binding_id else { + return Ok(None); + }; + let entry = registry + .get_mut(&source_binding_id) + .expect("selected Unix binding remains registered"); + let address = entry.address.clone(); + if let Some(queued) = entry.queued_by_target.get_mut(target_binding_id) { + *queued = queued.saturating_sub(1); + if *queued == 0 { + entry.queued_by_target.remove(target_binding_id); } - self.socket.take(); - self.guest_local_addr = None; } + if entry.active_bindings == 0 && entry.queued_by_target.is_empty() { + registry.remove(&source_binding_id); + } + Ok(Some(address)) +} - fn set_buffer_size(&mut self, which: &str, size: usize) -> Result<(), SidecarError> { - match which { - "recv" => self.recv_buffer_size = size, - "send" => self.send_buffer_size = size, - other => { - return Err(SidecarError::InvalidState(format!( - "unsupported UDP buffer size kind {other}" - ))); - } - } - if self.kernel_socket_id.is_some() { - return Ok(()); - } - let socket = self.socket()?; - let socket = SockRef::from(socket); - match which { - "recv" => socket.set_recv_buffer_size(size).map_err(sidecar_net_error), - "send" => socket.set_send_buffer_size(size).map_err(sidecar_net_error), - other => Err(SidecarError::InvalidState(format!( - "unsupported UDP buffer size kind {other}" - ))), - } +fn release_guest_unix_binding( + registry: &GuestUnixAddressRegistry, + binding_id: &str, +) -> Result<(), SidecarError> { + let mut registry = registry + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix address registry poisoned")))?; + let Some(entry) = registry.get_mut(binding_id) else { + return Ok(()); + }; + entry.active_bindings = entry.active_bindings.saturating_sub(1); + if entry.active_bindings == 0 && entry.queued_by_target.is_empty() { + registry.remove(binding_id); } + Ok(()) +} - fn get_buffer_size(&self, which: &str) -> Result { - if self.kernel_socket_id.is_some() { - return Ok(match which { - "recv" => self.recv_buffer_size, - "send" => self.send_buffer_size, - other => { - return Err(SidecarError::InvalidState(format!( - "unsupported UDP buffer size kind {other}" - ))); - } - }); - } - let socket = self.socket()?; - let socket = SockRef::from(socket); - match which { - "recv" => socket.recv_buffer_size().map_err(sidecar_net_error), - "send" => socket.send_buffer_size().map_err(sidecar_net_error), - other => Err(SidecarError::InvalidState(format!( - "unsupported UDP buffer size kind {other}" - ))), - } +fn rollback_guest_unix_binding( + registry: &GuestUnixAddressRegistry, + binding_id: &str, +) -> Result<(), SidecarError> { + let mut registry = registry + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix address registry poisoned")))?; + registry.remove(binding_id); + Ok(()) +} + +fn rollback_guest_unix_path_binding( + registry: &GuestUnixAddressRegistry, + binding_id: &str, + kernel: &mut SidecarKernel, + guest_path: &str, + host_path: &Path, +) -> Result<(), SidecarError> { + let registry_error = rollback_guest_unix_binding(registry, binding_id).err(); + cleanup_private_unix_socket_path(host_path); + let marker_error = kernel.remove_file(guest_path).err().map(kernel_error); + match (registry_error, marker_error) { + (None, None) => Ok(()), + (Some(error), None) | (None, Some(error)) => Err(error), + (Some(registry_error), Some(marker_error)) => Err(SidecarError::Execution(format!( + "failed to roll back Unix socket metadata: {registry_error}; \ + failed to remove Unix socket node {guest_path}: {marker_error}" + ))), } } -// ActiveExecution, ActiveExecutionEvent, SocketQueryKind moved to crate::state +fn purge_guest_unix_target( + registry: &GuestUnixAddressRegistry, + target_binding_id: &str, +) -> Result<(), SidecarError> { + let mut registry = registry + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix address registry poisoned")))?; + registry.retain(|_, entry| { + entry.queued_by_target.remove(target_binding_id); + entry.active_bindings != 0 || !entry.queued_by_target.is_empty() + }); + Ok(()) +} -impl ActiveExecution { - pub(crate) fn uses_shared_v8_runtime(&self) -> bool { - match self { - Self::Javascript(execution) => execution.uses_shared_v8_runtime(), - Self::Python(execution) => execution.uses_shared_v8_runtime(), - Self::Wasm(execution) => execution.uses_shared_v8_runtime(), - Self::Tool(_) => false, - } +#[cfg(test)] +mod guest_unix_metadata_tests { + use super::*; + + fn registry() -> GuestUnixAddressRegistry { + Arc::new(Mutex::new(BTreeMap::new())) } - pub(crate) fn child_pid(&self) -> u32 { - match self { - Self::Javascript(execution) => execution.child_pid(), - Self::Python(execution) => execution.child_pid(), - Self::Wasm(execution) => execution.child_pid(), - Self::Tool(_) => 0, - } + fn register_abstract(registry: &GuestUnixAddressRegistry, binding_id: &str, host_key: &str) { + register_guest_unix_binding( + registry, + binding_id, + host_key, + GuestUnixAddress { + path: format!("\0{binding_id}"), + abstract_path_hex: Some(String::from("61")), + }, + None, + None, + ) + .expect("register Unix address"); } - pub(crate) fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), SidecarError> { - match self { - Self::Javascript(execution) => execution - .write_stdin(chunk) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .write_stdin(chunk) - .map_err(|error| SidecarError::Execution(error.to_string())), - // Sidecar wasm always runs with kernel-managed stdio - // (AGENTOS_WASI_STDIO_SYNC_RPC=1): the guest reads fd 0 via - // `__kernel_stdin_read`, so skip the V8 `stdin` stream event — - // it is never consumed and would flood the session's deferred - // message queue while the guest blocks in a sync read. - Self::Wasm(execution) => execution - .write_stdin_kernel_only(chunk) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Tool(_) => Ok(()), + #[test] + fn pending_connection_guard_closes_rejected_peer() { + let state = Arc::new(GuestUnixConnectionState { + accepted_peer_open: AtomicBool::new(true), + }); + { + let _guard = PendingUnixConnectionGuard { + state: Some(Arc::clone(&state)), + }; } + assert!(!guest_unix_connection_peer_open(Some(&state))); } - pub(crate) fn close_stdin(&mut self) -> Result<(), SidecarError> { - match self { - Self::Javascript(execution) => execution - .close_stdin() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .close_stdin() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .close_stdin() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Tool(_) => Ok(()), - } + #[test] + fn listener_close_marks_unaccepted_connections_closed() { + let registry = registry(); + register_abstract(®istry, "target", "abstract:target"); + let state = register_guest_unix_connection(®istry, "target") + .expect("register pending connection"); + close_pending_guest_unix_connections(®istry, "target") + .expect("close pending connections"); + assert!(!guest_unix_connection_peer_open(Some(&state))); + assert!(registry + .lock() + .expect("registry") + .get("target") + .expect("target") + .pending_connections + .is_empty()); } - pub(crate) fn respond_python_vfs_rpc_success( - &mut self, - id: u64, - payload: PythonVfsRpcResponsePayload, - ) -> Result<(), SidecarError> { - match self { - Self::Python(execution) => execution - .respond_vfs_rpc_success(id, payload) - .map_err(|error| SidecarError::Execution(error.to_string())), - _ => Err(SidecarError::InvalidState(String::from( - "only Python executions can service Python VFS RPC responses", - ))), - } + #[test] + fn consumed_late_peer_metadata_returns_registry_to_baseline() { + let registry = registry(); + register_abstract(®istry, "target", "abstract:target"); + register_abstract(®istry, "source", "abstract:source"); + queue_guest_unix_peer(®istry, "source", "target").expect("queue peer metadata"); + release_guest_unix_binding(®istry, "source").expect("release source binding"); + + let address = consume_guest_unix_peer(®istry, "abstract:source", "target") + .expect("consume peer metadata"); + assert!(address.is_some()); + let registry = registry.lock().expect("registry"); + assert!(registry.contains_key("target")); + assert!(!registry.contains_key("source")); } +} - pub(crate) fn respond_python_vfs_rpc_error( - &mut self, - id: u64, - code: impl Into, - message: impl Into, - ) -> Result<(), SidecarError> { - match self { - Self::Python(execution) => execution - .respond_vfs_rpc_error(id, code, message) - .map_err(|error| SidecarError::Execution(error.to_string())), - _ => Err(SidecarError::InvalidState(String::from( - "only Python executions can service Python VFS RPC responses", - ))), - } +fn host_abstract_unix_name(context: &JavascriptSocketPathContext, guest_name: &[u8]) -> [u8; 32] { + let mut digest = Sha256::new(); + digest.update(b"agentos-unix-abstract-v1\0"); + digest.update(context.unix_abstract_namespace); + digest.update(guest_name); + digest.finalize().into() +} + +fn guest_autobind_unix_name(kernel_pid: u32, listener_id: &str, nonce: u32) -> [u8; 5] { + let digest = Sha256::digest(format!( + "agentos-unix-autobind-v1\0{kernel_pid}\0{listener_id}\0{nonce}" + )); + let value = + ((u32::from(digest[0]) << 12) | (u32::from(digest[1]) << 4) | (u32::from(digest[2]) >> 4)) + & 0x000f_ffff; + let encoded = format!("{value:05x}"); + encoded + .as_bytes() + .try_into() + .expect("five hexadecimal digits") +} + +impl ActiveUnixSocket { + fn connect( + host_path: &Path, + guest_path: &str, + max_buffered_bytes: usize, + ) -> Result { + let socket = Socket::new(Domain::UNIX, Type::STREAM, None).map_err(sidecar_net_error)?; + socket.set_nonblocking(true).map_err(sidecar_net_error)?; + let address = SockAddr::unix(host_path).map_err(sidecar_net_error)?; + socket + .connect(&address) + .map_err(nonblocking_unix_connect_error)?; + socket.set_nonblocking(false).map_err(sidecar_net_error)?; + let stream: UnixStream = socket.into(); + Self::from_stream( + stream, + None, + None, + Some(guest_path.to_owned()), + None, + None, + None, + None, + max_buffered_bytes, + ) } - pub(crate) fn send_javascript_stream_event( - &self, - event_type: &str, - payload: Value, - ) -> Result<(), SidecarError> { - match self { - Self::Javascript(execution) => execution - .send_stream_event(event_type, payload) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .send_stream_event(event_type, payload) - .map_err(|error| SidecarError::Execution(error.to_string())), - _ => Err(SidecarError::InvalidState(String::from( - "only embedded V8 executions can receive JavaScript stream events", - ))), - } + #[cfg(target_os = "linux")] + fn connect_abstract( + host_name: &[u8], + guest_name: &[u8], + max_buffered_bytes: usize, + ) -> Result { + let socket = Socket::new(Domain::UNIX, Type::STREAM, None).map_err(sidecar_net_error)?; + socket.set_nonblocking(true).map_err(sidecar_net_error)?; + let address = UnixAddr::new_abstract(host_name) + .map_err(|error| sidecar_net_error(std::io::Error::from_raw_os_error(error as i32)))?; + connect_socket(socket.as_raw_fd(), &address).map_err(|error| { + nonblocking_unix_connect_error(std::io::Error::from_raw_os_error(error as i32)) + })?; + socket.set_nonblocking(false).map_err(sidecar_net_error)?; + let stream: UnixStream = socket.into(); + Self::from_stream( + stream, + None, + None, + Some(abstract_unix_node_path(guest_name)), + None, + Some(abstract_unix_name_hex(guest_name)), + None, + None, + max_buffered_bytes, + ) } - pub(crate) fn javascript_v8_session_handle(&self) -> Option { - match self { - Self::Javascript(execution) => Some(execution.v8_session_handle()), - Self::Wasm(execution) => Some(execution.v8_session_handle()), - _ => None, - } + #[cfg(not(target_os = "linux"))] + fn connect_abstract( + _host_name: &[u8], + _guest_name: &[u8], + _max_buffered_bytes: usize, + ) -> Result { + Err(SidecarError::Unsupported(String::from( + "abstract Unix sockets require a Linux sidecar host", + ))) } - pub(crate) fn terminate(&mut self) -> Result<(), SidecarError> { - match self { - Self::Javascript(execution) => execution - .terminate() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .kill() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .terminate() - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Tool(_) => Ok(()), - } + fn from_stream( + stream: UnixStream, + listener_id: Option, + local_path: Option, + remote_path: Option, + local_abstract_path_hex: Option, + remote_abstract_path_hex: Option, + local_registry_binding_id: Option, + private_host_path: Option, + max_buffered_bytes: usize, + ) -> Result { + let read_stream = stream.try_clone().map_err(sidecar_net_error)?; + let stream = Arc::new(Mutex::new(stream)); + let read_state = Arc::new(( + Mutex::new(UnixSocketReadState { + bytes: VecDeque::new(), + terminal_events: VecDeque::new(), + max_buffered_bytes: max_buffered_bytes.max(1), + warned_near_limit: false, + closed: false, + }), + Condvar::new(), + )); + let event_pusher = Arc::new(Mutex::new(None)); + let saw_local_shutdown = Arc::new(AtomicBool::new(false)); + let saw_remote_end = Arc::new(AtomicBool::new(false)); + let close_notified = Arc::new(AtomicBool::new(false)); + spawn_unix_socket_reader( + read_stream, + Arc::clone(&read_state), + Arc::clone(&event_pusher), + Arc::clone(&saw_local_shutdown), + Arc::clone(&saw_remote_end), + Arc::clone(&close_notified), + ); + + Ok(Self { + stream, + read_state, + event_pusher, + listener_id, + local_path, + remote_path, + local_abstract_path_hex, + remote_abstract_path_hex, + local_registry_binding_id, + remote_registry_binding_id: None, + connection_state: None, + private_host_path, + saw_local_shutdown, + saw_remote_end, + close_notified, + description_handles: Arc::new(()), + }) } - pub(crate) fn respond_javascript_sync_rpc_success( - &mut self, - id: u64, - result: Value, - ) -> Result<(), SidecarError> { - match self { - Self::Javascript(execution) => execution - .respond_sync_rpc_success(id, result) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .respond_javascript_sync_rpc_success(id, result) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .respond_sync_rpc_success(id, result) - .map_err(|error| SidecarError::Execution(error.to_string())), - _ => Err(SidecarError::InvalidState(String::from( - "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses", - ))), + fn clone_for_fd_transfer(&self) -> Self { + Self { + stream: Arc::clone(&self.stream), + read_state: Arc::clone(&self.read_state), + event_pusher: Arc::clone(&self.event_pusher), + listener_id: self.listener_id.clone(), + local_path: self.local_path.clone(), + remote_path: self.remote_path.clone(), + local_abstract_path_hex: self.local_abstract_path_hex.clone(), + remote_abstract_path_hex: self.remote_abstract_path_hex.clone(), + local_registry_binding_id: self.local_registry_binding_id.clone(), + remote_registry_binding_id: self.remote_registry_binding_id.clone(), + connection_state: self.connection_state.clone(), + private_host_path: self.private_host_path.clone(), + saw_local_shutdown: Arc::clone(&self.saw_local_shutdown), + saw_remote_end: Arc::clone(&self.saw_remote_end), + close_notified: Arc::clone(&self.close_notified), + description_handles: Arc::clone(&self.description_handles), } } - pub(crate) fn respond_javascript_sync_rpc_raw_success( - &mut self, - id: u64, - payload: Vec, - ) -> Result<(), SidecarError> { - match self { - Self::Javascript(execution) => execution - .respond_sync_rpc_raw_success(id, payload) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .respond_sync_rpc_raw_success(id, payload) - .map_err(|error| SidecarError::Execution(error.to_string())), - _ => Err(SidecarError::InvalidState(String::from( - "only embedded V8 executions can service raw JavaScript sync RPC responses", - ))), + fn is_final_description_handle(&self) -> bool { + Arc::strong_count(&self.description_handles) == 1 + } + + fn set_event_pusher(&self, session: Option, socket_id: String) { + if let Ok(mut pusher) = self.event_pusher.lock() { + *pusher = session.map(|session| JavascriptSocketEventPusher { session, socket_id }); } } - pub(crate) fn respond_javascript_sync_rpc_response( + fn poll_readable( &mut self, - id: u64, - response: JavascriptSyncRpcServiceResponse, - ) -> Result<(), SidecarError> { - match response { - JavascriptSyncRpcServiceResponse::Json(result) => { - self.respond_javascript_sync_rpc_success(id, result) - } - JavascriptSyncRpcServiceResponse::Raw(payload) => { - self.respond_javascript_sync_rpc_raw_success(id, payload) + wait: Duration, + max_bytes: usize, + peek: bool, + ) -> Result, SidecarError> { + let deadline = Instant::now() + wait; + let (state_lock, ready) = &*self.read_state; + let mut state = state_lock.lock().map_err(|_| { + SidecarError::InvalidState(String::from("Unix socket read state lock poisoned")) + })?; + loop { + if max_bytes == 0 || !state.bytes.is_empty() { + let take = state.bytes.len().min(max_bytes); + let bytes = if peek { + state.bytes.iter().take(take).copied().collect() + } else { + let bytes = state.bytes.drain(..take).collect(); + ready.notify_all(); + bytes + }; + return Ok(Some(JavascriptTcpSocketEvent::Data(bytes))); + } + if let Some(event) = state.terminal_events.pop_front() { + return Ok(Some(event)); + } + if wait.is_zero() { + return Ok(None); + } + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Ok(None); + } + let (next_state, timeout) = ready.wait_timeout(state, remaining).map_err(|_| { + SidecarError::InvalidState(String::from("Unix socket read state lock poisoned")) + })?; + state = next_state; + if timeout.timed_out() && state.bytes.is_empty() && state.terminal_events.is_empty() { + return Ok(None); } } } - pub(crate) fn respond_javascript_sync_rpc_error( + fn poll_readiness( &mut self, - id: u64, - code: impl Into, - message: impl Into, - ) -> Result<(), SidecarError> { - match self { - Self::Javascript(execution) => execution - .respond_sync_rpc_error(id, code, message) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .respond_javascript_sync_rpc_error(id, code, message) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .respond_sync_rpc_error(id, code, message) - .map_err(|error| SidecarError::Execution(error.to_string())), - _ => Err(SidecarError::InvalidState(String::from( - "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses", - ))), + wait: Duration, + ) -> Result, SidecarError> { + let deadline = Instant::now() + wait; + let (state_lock, ready) = &*self.read_state; + let mut state = state_lock.lock().map_err(|_| { + SidecarError::InvalidState(String::from("Unix socket read state lock poisoned")) + })?; + loop { + if !state.bytes.is_empty() { + return Ok(Some(JavascriptTcpSocketEvent::Data(Vec::new()))); + } + if let Some(event) = state.terminal_events.pop_front() { + return Ok(Some(event)); + } + if wait.is_zero() { + return Ok(None); + } + let remaining = deadline.saturating_duration_since(Instant::now()); + if remaining.is_zero() { + return Ok(None); + } + let (next_state, timeout) = ready.wait_timeout(state, remaining).map_err(|_| { + SidecarError::InvalidState(String::from("Unix socket read state lock poisoned")) + })?; + state = next_state; + if timeout.timed_out() && state.bytes.is_empty() && state.terminal_events.is_empty() { + return Ok(None); + } } } - pub(crate) async fn poll_event( - &mut self, - timeout: Duration, - ) -> Result, SidecarError> { - match self { - Self::Javascript(execution) => execution - .poll_event(timeout) - .await - .map(|event| { - event.map(|event| match event { - JavascriptExecutionEvent::Stdout(chunk) => { - ActiveExecutionEvent::Stdout(chunk) - } - JavascriptExecutionEvent::Stderr(chunk) => { - ActiveExecutionEvent::Stderr(chunk) - } - JavascriptExecutionEvent::SyncRpcRequest(request) => { - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) - } - JavascriptExecutionEvent::SignalState { - signal, - registration, - } => ActiveExecutionEvent::SignalState { - signal, - registration: map_node_signal_registration(registration), - }, - JavascriptExecutionEvent::Exited(code) => { - ActiveExecutionEvent::Exited(code) - } - }) - }) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .poll_event(timeout) - .await - .map(|event| { - event.map(|event| match event { - PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk), - PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk), - PythonExecutionEvent::JavascriptSyncRpcRequest(request) => { - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) - } - PythonExecutionEvent::VfsRpcRequest(request) => { - ActiveExecutionEvent::PythonVfsRpcRequest(request) - } - PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code), - }) - }) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .poll_event(timeout) - .await - .map(|event| { - event.map(|event| match event { - WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk), - WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk), - WasmExecutionEvent::SyncRpcRequest(request) => { - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) - } - WasmExecutionEvent::SignalState { - signal, - registration, - } => ActiveExecutionEvent::SignalState { - signal, - registration: map_wasm_signal_registration(registration), - }, - WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code), - }) - }) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Tool(execution) => { - let _ = timeout; - poll_tool_process_event(execution) - } + fn socket_info(&mut self, registry: &GuestUnixAddressRegistry) -> Result { + let stream = self + .stream + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?; + let local_address = stream.local_addr().map_err(sidecar_net_error)?; + let live_local = local_address + .as_pathname() + .map(pathname_unix_host_address_key) + .or_else(|| { + #[cfg(target_os = "linux")] + { + local_address + .as_abstract_name() + .map(abstract_unix_host_address_key) + } + #[cfg(not(target_os = "linux"))] + { + None + } + }); + let remote_address = stream.peer_addr().map_err(sidecar_net_error)?; + let live_remote = remote_address + .as_pathname() + .map(pathname_unix_host_address_key) + .or_else(|| { + #[cfg(target_os = "linux")] + { + remote_address + .as_abstract_name() + .map(abstract_unix_host_address_key) + } + #[cfg(not(target_os = "linux"))] + { + None + } + }); + drop(stream); + let local = live_local + .as_deref() + .map(|key| guest_unix_address_for_host_key(registry, key)) + .transpose()? + .flatten(); + let remote = if let (Some(key), Some(target_binding_id)) = ( + live_remote.as_deref(), + self.remote_registry_binding_id.as_deref(), + ) { + consume_guest_unix_peer(registry, key, target_binding_id)? + .or(guest_unix_address_for_host_key(registry, key)?) + } else { + live_remote + .as_deref() + .map(|key| guest_unix_address_for_host_key(registry, key)) + .transpose()? + .flatten() + }; + if let Some(address) = remote.as_ref() { + self.remote_path = Some(address.path.clone()); + self.remote_abstract_path_hex = address.abstract_path_hex.clone(); + } + let local_path = local + .as_ref() + .map(|address| address.path.as_str()) + .or(self.local_path.as_deref()); + let remote_path = remote + .as_ref() + .map(|address| address.path.as_str()) + .or(self.remote_path.as_deref()); + let mut info = unix_socket_info_value(local_path, remote_path); + if let Some(object) = info.as_object_mut() { + object.insert( + String::from("localAbstractPathHex"), + json!(local + .as_ref() + .and_then(|address| address.abstract_path_hex.as_deref()) + .or(self.local_abstract_path_hex.as_deref())), + ); + object.insert( + String::from("remoteAbstractPathHex"), + json!(remote + .as_ref() + .and_then(|address| address.abstract_path_hex.as_deref()) + .or(self.remote_abstract_path_hex.as_deref())), + ); } + Ok(info) } - pub(crate) fn poll_event_blocking( + fn cache_remote_peer_metadata( &mut self, - timeout: Duration, - ) -> Result, SidecarError> { - match self { - Self::Javascript(execution) => execution - .poll_event_blocking(timeout) - .map(|event| { - event.map(|event| match event { - JavascriptExecutionEvent::Stdout(chunk) => { - ActiveExecutionEvent::Stdout(chunk) - } - JavascriptExecutionEvent::Stderr(chunk) => { - ActiveExecutionEvent::Stderr(chunk) - } - JavascriptExecutionEvent::SyncRpcRequest(request) => { - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) - } - JavascriptExecutionEvent::SignalState { - signal, - registration, - } => ActiveExecutionEvent::SignalState { - signal, - registration: map_node_signal_registration(registration), - }, - JavascriptExecutionEvent::Exited(code) => { - ActiveExecutionEvent::Exited(code) - } - }) - }) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Python(execution) => execution - .poll_event_blocking(timeout) - .map(|event| { - event.map(|event| match event { - PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk), - PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk), - PythonExecutionEvent::JavascriptSyncRpcRequest(request) => { - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) - } - PythonExecutionEvent::VfsRpcRequest(request) => { - ActiveExecutionEvent::PythonVfsRpcRequest(request) - } - PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code), - }) - }) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Wasm(execution) => execution - .poll_event_blocking(timeout) - .map(|event| { - event.map(|event| match event { - WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk), - WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk), - WasmExecutionEvent::SyncRpcRequest(request) => { - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) - } - WasmExecutionEvent::SignalState { - signal, - registration, - } => ActiveExecutionEvent::SignalState { - signal, - registration: map_wasm_signal_registration(registration), - }, - WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code), - }) - }) - .map_err(|error| SidecarError::Execution(error.to_string())), - Self::Tool(execution) => { - let _ = timeout; - poll_tool_process_event(execution) - } + registry: &GuestUnixAddressRegistry, + ) -> Result<(), SidecarError> { + if self.remote_registry_binding_id.is_none() || self.remote_path.is_some() { + return Ok(()); } + self.socket_info(registry)?; + Ok(()) } -} -struct ToolProcessEventRequest { - sidecar_requests: SharedSidecarRequestClient, - connection_id: String, - session_id: String, - vm_id: String, - tool_resolution: ToolCommandResolution, - cancelled: Arc, - pending_events: Arc>>, - events_overflowed: Arc, -} + fn bind_path( + &mut self, + host_path: &Path, + guest_path: &str, + binding_id: &str, + ) -> Result<(), SidecarError> { + if let Some(parent) = host_path.parent() { + fs::create_dir_all(parent).map_err(sidecar_net_error)?; + } + let stream = self + .stream + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?; + let address = UnixAddr::new(host_path) + .map_err(|error| sidecar_net_error(std::io::Error::from_raw_os_error(error as i32)))?; + bind_socket(stream.as_raw_fd(), &address) + .map_err(|error| sidecar_net_error(std::io::Error::from_raw_os_error(error as i32)))?; + drop(stream); + self.local_path = Some(guest_path.to_owned()); + self.local_abstract_path_hex = None; + self.local_registry_binding_id = Some(binding_id.to_owned()); + self.private_host_path = Some(host_path.to_path_buf()); + Ok(()) + } -pub(crate) fn send_tool_process_event( - pending_events: &Arc>>, - events_overflowed: &AtomicBool, - event: ActiveExecutionEvent, -) -> bool { - let mut pending_events = pending_events - .lock() - .unwrap_or_else(|poisoned| poisoned.into_inner()); - if pending_events.len() >= MAX_PROCESS_EVENT_QUEUE { - events_overflowed.store(true, Ordering::Relaxed); - return false; + #[cfg(target_os = "linux")] + fn bind_abstract( + &mut self, + host_name: &[u8], + guest_name: &[u8], + binding_id: &str, + ) -> Result<(), SidecarError> { + let stream = self + .stream + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?; + let address = UnixAddr::new_abstract(host_name) + .map_err(|error| sidecar_net_error(std::io::Error::from_raw_os_error(error as i32)))?; + bind_socket(stream.as_raw_fd(), &address) + .map_err(|error| sidecar_net_error(std::io::Error::from_raw_os_error(error as i32)))?; + drop(stream); + self.local_path = Some(abstract_unix_node_path(guest_name)); + self.local_abstract_path_hex = Some(abstract_unix_name_hex(guest_name)); + self.local_registry_binding_id = Some(binding_id.to_owned()); + Ok(()) } - pending_events.push_back(event); - true -} -fn spawn_tool_process_events(request: ToolProcessEventRequest) { - let ToolProcessEventRequest { - sidecar_requests, - connection_id, - session_id, - vm_id, - tool_resolution, - cancelled, - pending_events, - events_overflowed, - } = request; - std::thread::spawn(move || match tool_resolution { - ToolCommandResolution::Failure(message) => { - if !send_tool_process_event( - &pending_events, - &events_overflowed, - ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)), - ) { - return; - } - let _ = send_tool_process_event( - &pending_events, - &events_overflowed, - ActiveExecutionEvent::Exited(1), - ); + fn write( + &self, + contents: &[u8], + nonblocking: bool, + max_wait: Duration, + ) -> Result { + if contents.is_empty() { + return Ok(0); } - ToolCommandResolution::Invoke { request, timeout } => { - let response = sidecar_requests.invoke( - OwnershipScope::vm(connection_id.clone(), session_id.clone(), vm_id.clone()), - SidecarRequestPayload::HostCallback(request.clone()), - timeout, - ); - if cancelled.load(Ordering::Relaxed) { - return; + let stream = self + .stream + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?; + let started = Instant::now(); + let deadline = started + max_wait; + let warning_at = started + max_wait.mul_f64(0.8); + let mut warned = false; + loop { + let mut flags = MsgFlags::MSG_DONTWAIT; + #[cfg(any(target_os = "android", target_os = "linux"))] + { + flags |= MsgFlags::MSG_NOSIGNAL; } - - match response { - Ok(crate::protocol::SidecarResponsePayload::HostCallbackResult(result)) => { - if let Some(value) = result.result { - let value: serde_json::Value = serde_json::from_str(&value) - .unwrap_or(serde_json::Value::String(value)); - let stdout = serde_json::to_vec(&json!({ - "ok": true, - "result": value, - })) - .unwrap_or_else(|error| { - format_tool_failure_output(&format!( - "failed to serialize tool result: {error}" - )) - }); - if !send_tool_process_event( - &pending_events, - &events_overflowed, - ActiveExecutionEvent::Stdout(stdout), - ) { - return; - } - let _ = send_tool_process_event( - &pending_events, - &events_overflowed, - ActiveExecutionEvent::Exited(0), - ); - } else { - let message = result - .error - .unwrap_or_else(|| String::from("tool invocation returned no result")); - if !send_tool_process_event( - &pending_events, - &events_overflowed, - ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)), - ) { - return; - } - let _ = send_tool_process_event( - &pending_events, - &events_overflowed, - ActiveExecutionEvent::Exited(1), - ); + match send_socket(stream.as_raw_fd(), contents, flags) { + Ok(written) => return Ok(written), + Err(error) => { + let error = std::io::Error::from_raw_os_error(error as i32); + if error.kind() == std::io::ErrorKind::Interrupted { + continue; } - } - Ok(_) => { - if !send_tool_process_event( - &pending_events, - &events_overflowed, - ActiveExecutionEvent::Stderr(format_tool_failure_output( - "unexpected sidecar tool response", - )), - ) { - return; + if error.kind() != std::io::ErrorKind::WouldBlock { + return Err(sidecar_net_error(error)); } - let _ = send_tool_process_event( - &pending_events, - &events_overflowed, - ActiveExecutionEvent::Exited(1), - ); } + } + if nonblocking { + return Err(sidecar_net_error(std::io::Error::from_raw_os_error( + libc::EAGAIN, + ))); + } + + let now = Instant::now(); + if !warned && now >= warning_at { + warned = true; + eprintln!( + "[agentos] blocking AF_UNIX write is nearing limits.resources.maxBlockingReadMs ({} ms)", + max_wait.as_millis() + ); + } + if now >= deadline { + eprintln!( + "[agentos] blocking AF_UNIX write exceeded limits.resources.maxBlockingReadMs ({} ms); raise limits.resources.maxBlockingReadMs if needed", + max_wait.as_millis() + ); + return Err(sidecar_net_error(std::io::Error::from_raw_os_error( + libc::ETIMEDOUT, + ))); + } + let poll_deadline = deadline.min(now + JAVASCRIPT_NET_POLL_MAX_WAIT); + wait_fd_writable_until(stream.as_fd(), poll_deadline).map_err(sidecar_net_error)?; + } + } + + fn shutdown_write(&self) -> Result<(), SidecarError> { + let stream = self + .stream + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?; + self.saw_local_shutdown.store(true, Ordering::SeqCst); + stream + .shutdown(Shutdown::Write) + .map_err(sidecar_net_error)?; + if self.saw_remote_end.load(Ordering::SeqCst) + && !self.close_notified.swap(true, Ordering::SeqCst) + { + let (state_lock, ready) = &*self.read_state; + let mut state = state_lock.lock().map_err(|_| { + SidecarError::InvalidState(String::from("Unix socket read state lock poisoned")) + })?; + state + .terminal_events + .push_back(JavascriptTcpSocketEvent::Close { had_error: false }); + ready.notify_all(); + } + Ok(()) + } + + fn close(&self) -> Result<(), SidecarError> { + let (state_lock, ready) = &*self.read_state; + let mut state = state_lock.lock().map_err(|_| { + SidecarError::InvalidState(String::from("Unix socket read state lock poisoned")) + })?; + state.closed = true; + ready.notify_all(); + drop(state); + let stream = self + .stream + .lock() + .map_err(|_| SidecarError::InvalidState(String::from("Unix socket lock poisoned")))?; + stream.shutdown(Shutdown::Both).map_err(sidecar_net_error) + } +} + +#[cfg(test)] +mod unix_socket_io_safeguard_tests { + use super::ActiveUnixSocket; + use socket2::SockRef; + use std::time::Duration; + + #[test] + #[ignore = "expensive saturation safeguard: fills a host AF_UNIX send buffer"] + fn stalled_unix_peer_times_out_instead_of_blocking_the_sync_rpc_forever() { + let (stream, peer) = std::os::unix::net::UnixStream::pair().expect("Unix stream pair"); + SockRef::from(&stream) + .set_send_buffer_size(4 * 1024) + .expect("shrink send buffer"); + let socket = ActiveUnixSocket::from_stream( + stream, + None, + None, + None, + None, + None, + None, + None, + 64 * 1024, + ) + .expect("active Unix socket"); + let payload = vec![0xa5; 4 * 1024 * 1024]; + let mut offset = 0; + let mut timeout = None; + while offset < payload.len() { + match socket.write(&payload[offset..], false, Duration::from_millis(20)) { + Ok(0) => panic!("AF_UNIX write made no progress"), + Ok(written) => offset += written, Err(error) => { - if !send_tool_process_event( - &pending_events, - &events_overflowed, - ActiveExecutionEvent::Stderr(format_tool_failure_output( - &error.to_string(), - )), - ) { - return; - } - let _ = send_tool_process_event( - &pending_events, - &events_overflowed, - ActiveExecutionEvent::Exited(1), - ); + timeout = Some(error.to_string()); + break; } } } - }); + assert!( + timeout + .as_deref() + .is_some_and(|error| error.starts_with("ETIMEDOUT:")), + "stalled write must surface typed ETIMEDOUT, got {timeout:?} after {offset} bytes" + ); + socket.close().expect("close saturated socket"); + drop(peer); + } } -impl NativeSidecar -where - B: NativeSidecarBridge + Send + 'static, - BridgeError: fmt::Debug + Send + Sync + 'static, -{ - pub(crate) async fn execute( - &mut self, - request: &RequestFrame, - payload: ExecuteRequest, - ) -> Result { - let execute_total_start = Instant::now(); - let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; - self.require_owned_vm(&connection_id, &session_id, &vm_id)?; +// ActiveUnixListener moved to crate::state - let vm = self - .vms - .get_mut(&vm_id) - .ok_or_else(|| missing_vm_error(&vm_id))?; - if vm.active_processes.contains_key(&payload.process_id) { - return Err(SidecarError::InvalidState(format!( - "VM {vm_id} already has an active process with id {}", - payload.process_id +impl ActiveUnixListener { + fn bind_unlistened(host_path: &Path, guest_path: &str) -> Result { + if let Some(parent) = host_path.parent() { + fs::create_dir_all(parent).map_err(sidecar_net_error)?; + } + let socket = Socket::new(Domain::UNIX, Type::STREAM, None).map_err(sidecar_net_error)?; + let address = SockAddr::unix(host_path).map_err(sidecar_net_error)?; + socket.bind(&address).map_err(sidecar_net_error)?; + Ok(Self { + listener: None, + bound_socket: Some(socket), + path: guest_path.to_owned(), + abstract_path_hex: None, + registry_binding_id: String::new(), + private_host_path: None, + guest_node_path: None, + backlog: DEFAULT_JAVASCRIPT_NET_BACKLOG as usize, + active_connection_ids: BTreeSet::new(), + description_handles: Arc::new(()), + }) + } + + fn bind( + host_path: &Path, + guest_path: &str, + backlog: Option, + ) -> Result { + let mut listener = Self::bind_unlistened(host_path, guest_path)?; + listener.listen(backlog)?; + Ok(listener) + } + + #[cfg(target_os = "linux")] + fn bind_abstract_unlistened(host_name: &[u8], guest_name: &[u8]) -> Result { + let socket = Socket::new(Domain::UNIX, Type::STREAM, None).map_err(sidecar_net_error)?; + let address = UnixAddr::new_abstract(host_name) + .map_err(|error| sidecar_net_error(std::io::Error::from_raw_os_error(error as i32)))?; + bind_socket(socket.as_raw_fd(), &address) + .map_err(|error| sidecar_net_error(std::io::Error::from_raw_os_error(error as i32)))?; + Ok(Self { + listener: None, + bound_socket: Some(socket), + path: abstract_unix_node_path(guest_name), + abstract_path_hex: Some(abstract_unix_name_hex(guest_name)), + registry_binding_id: String::new(), + private_host_path: None, + guest_node_path: None, + backlog: DEFAULT_JAVASCRIPT_NET_BACKLOG as usize, + active_connection_ids: BTreeSet::new(), + description_handles: Arc::new(()), + }) + } + + #[cfg(not(target_os = "linux"))] + fn bind_abstract_unlistened( + _host_name: &[u8], + _guest_name: &[u8], + ) -> Result { + Err(SidecarError::Unsupported(String::from( + "abstract Unix sockets require a Linux sidecar host", + ))) + } + + fn bind_abstract( + host_name: &[u8], + guest_name: &[u8], + backlog: Option, + ) -> Result { + let mut listener = Self::bind_abstract_unlistened(host_name, guest_name)?; + listener.listen(backlog)?; + Ok(listener) + } + + fn listen(&mut self, backlog: Option) -> Result<(), SidecarError> { + let backlog = usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG)) + .expect("default backlog fits within usize"); + if let Some(socket) = self.bound_socket.as_ref() { + socket + .listen(i32::try_from(backlog).unwrap_or(i32::MAX)) + .map_err(sidecar_net_error)?; + socket.set_nonblocking(true).map_err(sidecar_net_error)?; + let socket = self + .bound_socket + .take() + .expect("bound socket remains owned until listen succeeds"); + self.listener = Some(socket.into()); + } else if let Some(listener) = self.listener.as_ref() { + SockRef::from(listener) + .listen(i32::try_from(backlog).unwrap_or(i32::MAX)) + .map_err(sidecar_net_error)?; + } else { + return Err(sidecar_net_error(std::io::Error::from_raw_os_error( + libc::EINVAL, ))); } + self.backlog = backlog; + Ok(()) + } - if let Some(command) = payload.command.as_deref() { - if let Some(tool_resolution) = - resolve_tool_command(vm, command, &payload.args, payload.cwd.as_deref())? - { - let guest_cwd = payload - .cwd - .as_deref() - .map(normalize_path) - .unwrap_or_else(|| vm.guest_cwd.clone()); - let kernel_handle = vm - .kernel - .create_virtual_process( - EXECUTION_DRIVER_NAME, - TOOL_DRIVER_NAME, - command, - std::iter::once(command.to_owned()) - .chain(payload.args.iter().cloned()) - .collect(), - VirtualProcessOptions { - env: vm.guest_env.clone(), - cwd: Some(guest_cwd.clone()), - ..VirtualProcessOptions::default() + fn connect_bound_path( + &mut self, + host_path: &Path, + guest_path: &str, + binding_id: &str, + max_buffered_bytes: usize, + ) -> Result { + let socket = self + .bound_socket + .as_ref() + .ok_or_else(|| sidecar_net_error(std::io::Error::from_raw_os_error(libc::EINVAL)))?; + socket.set_nonblocking(true).map_err(sidecar_net_error)?; + let address = SockAddr::unix(host_path).map_err(sidecar_net_error)?; + socket + .connect(&address) + .map_err(nonblocking_unix_connect_error)?; + socket.set_nonblocking(false).map_err(sidecar_net_error)?; + let stream: UnixStream = self + .bound_socket + .take() + .expect("bound socket exists after successful connect") + .into(); + ActiveUnixSocket::from_stream( + stream, + None, + Some(self.path.clone()), + Some(guest_path.to_owned()), + self.abstract_path_hex.clone(), + None, + Some(binding_id.to_owned()), + self.private_host_path.take(), + max_buffered_bytes, + ) + } + + #[cfg(target_os = "linux")] + fn connect_bound_abstract( + &mut self, + host_name: &[u8], + guest_name: &[u8], + binding_id: &str, + max_buffered_bytes: usize, + ) -> Result { + let socket = self + .bound_socket + .as_ref() + .ok_or_else(|| sidecar_net_error(std::io::Error::from_raw_os_error(libc::EINVAL)))?; + socket.set_nonblocking(true).map_err(sidecar_net_error)?; + let address = UnixAddr::new_abstract(host_name) + .map_err(|error| sidecar_net_error(std::io::Error::from_raw_os_error(error as i32)))?; + connect_socket(socket.as_raw_fd(), &address).map_err(|error| { + nonblocking_unix_connect_error(std::io::Error::from_raw_os_error(error as i32)) + })?; + socket.set_nonblocking(false).map_err(sidecar_net_error)?; + let stream: UnixStream = self + .bound_socket + .take() + .expect("bound socket exists after successful connect") + .into(); + ActiveUnixSocket::from_stream( + stream, + None, + Some(self.path.clone()), + Some(abstract_unix_node_path(guest_name)), + self.abstract_path_hex.clone(), + Some(abstract_unix_name_hex(guest_name)), + Some(binding_id.to_owned()), + self.private_host_path.take(), + max_buffered_bytes, + ) + } + + #[cfg(not(target_os = "linux"))] + fn connect_bound_abstract( + &mut self, + _host_name: &[u8], + _guest_name: &[u8], + _binding_id: &str, + _max_buffered_bytes: usize, + ) -> Result { + Err(SidecarError::Unsupported(String::from( + "abstract Unix sockets require a Linux sidecar host", + ))) + } + + fn path(&self) -> &str { + &self.path + } + + fn clone_for_fd_transfer(&self) -> Result { + Ok(Self { + listener: self + .listener + .as_ref() + .map(UnixListener::try_clone) + .transpose() + .map_err(sidecar_net_error)?, + bound_socket: self + .bound_socket + .as_ref() + .map(Socket::try_clone) + .transpose() + .map_err(sidecar_net_error)?, + path: self.path.clone(), + abstract_path_hex: self.abstract_path_hex.clone(), + registry_binding_id: self.registry_binding_id.clone(), + private_host_path: self.private_host_path.clone(), + guest_node_path: self.guest_node_path.clone(), + backlog: self.backlog, + active_connection_ids: self.active_connection_ids.clone(), + description_handles: Arc::clone(&self.description_handles), + }) + } + + fn is_final_description_handle(&self) -> bool { + Arc::strong_count(&self.description_handles) == 1 + } + + fn poll( + &mut self, + wait: Duration, + context: &JavascriptSocketPathContext, + target_binding_id: &str, + ) -> Result, SidecarError> { + let listener = self + .listener + .as_ref() + .ok_or_else(|| sidecar_net_error(std::io::Error::from_raw_os_error(libc::EINVAL)))?; + let deadline = Instant::now() + wait; + loop { + match listener.accept() { + Ok((stream, remote_addr)) => { + let connection_state = accept_guest_unix_connection( + &context.unix_bound_addresses, + target_binding_id, + )?; + let local_path = Some(self.path.clone()); + let remote = match unix_host_address_key(&remote_addr) { + Some(key) => consume_guest_unix_peer( + &context.unix_bound_addresses, + &key, + target_binding_id, + )?, + None => None, + }; + let remote_path = remote.as_ref().map(|address| address.path.clone()); + return Ok(Some(JavascriptUnixListenerEvent::Connection( + PendingUnixSocket { + stream, + local_path, + remote_path, + local_abstract_path_hex: self.abstract_path_hex.clone(), + remote_abstract_path_hex: remote + .and_then(|address| address.abstract_path_hex), + connection_guard: PendingUnixConnectionGuard { + state: Some(connection_state), + }, }, - ) - .map_err(kernel_error)?; - let kernel_pid = kernel_handle.pid(); - let tool_execution = ToolExecution::default(); - let cancelled = tool_execution.cancelled.clone(); - let pending_events = tool_execution.pending_events.clone(); - let events_overflowed = tool_execution.events_overflowed.clone(); - vm.active_processes.insert( - payload.process_id.clone(), - ActiveProcess::new( - kernel_pid, - kernel_handle, - GuestRuntimeKind::JavaScript, - ActiveExecution::Tool(tool_execution), - ) - .with_guest_cwd(guest_cwd.clone()) - .with_host_cwd(resolve_vm_guest_path_to_host(vm, &guest_cwd)), - ); - self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?; - spawn_tool_process_events(ToolProcessEventRequest { - sidecar_requests: self.sidecar_requests.clone(), - connection_id: connection_id.clone(), - session_id: session_id.clone(), - vm_id: vm_id.clone(), - tool_resolution, - cancelled, - pending_events, - events_overflowed, - }); + ))); + } + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { + if wait.is_zero() || Instant::now() >= deadline { + return Ok(None); + } + if !wait_fd_readable_until(listener.as_fd(), deadline) + .map_err(sidecar_net_error)? + { + return Ok(None); + } + } + Err(error) => { + return Ok(Some(JavascriptUnixListenerEvent::Error { + code: io_error_code(&error), + message: error.to_string(), + })); + } + } + } + } - return Ok(DispatchResult { - response: process_started_response( - request, - payload.process_id, - Some(kernel_pid), - ), - events: Vec::new(), - }); + fn close(&self) -> Result<(), SidecarError> { + Ok(()) + } + + fn active_connection_count(&self) -> usize { + self.active_connection_ids.len() + } + + fn register_connection(&mut self, socket_id: &str) { + self.active_connection_ids.insert(socket_id.to_string()); + } + + fn release_connection(&mut self, socket_id: &str) { + self.active_connection_ids.remove(socket_id); + } +} + +fn cleanup_private_unix_socket_path(path: &Path) { + match fs::remove_file(path) { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => eprintln!( + "failed to remove private Unix socket {}: {error}", + path.display() + ), + } +} + +impl Drop for PendingUnixConnectionGuard { + fn drop(&mut self) { + if let Some(state) = self.state.as_ref() { + state.accepted_peer_open.store(false, Ordering::SeqCst); + } + } +} + +impl Drop for ActiveUnixSocket { + fn drop(&mut self) { + if !self.is_final_description_handle() { + return; + } + if self.listener_id.is_some() { + if let Some(state) = self.connection_state.as_ref() { + state.accepted_peer_open.store(false, Ordering::SeqCst); } } + if let Some(path) = self.private_host_path.as_deref() { + cleanup_private_unix_socket_path(path); + } + } +} - let requested_tty = payload - .env - .get(EXECUTION_REQUEST_TTY_ENV) - .is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true")); - let phase_start = Instant::now(); - let mut resolved = resolve_execute_request(vm, &payload)?; - stage_agentos_package_command(vm, &mut resolved)?; - let resolved = resolved; - record_execute_phase("resolve_execute_request", phase_start.elapsed()); - let phase_start = Instant::now(); - let mut env = resolved.env.clone(); - env.remove(EXECUTION_REQUEST_TTY_ENV); - let sandbox_root = normalize_host_path(&vm.cwd); - env.insert( - String::from(EXECUTION_SANDBOX_ROOT_ENV), - sandbox_root.to_string_lossy().into_owned(), - ); - if resolved.runtime == GuestRuntimeKind::JavaScript { - env.insert(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1")); - // A TTY guest-node process reads stdin through the kernel PTY: host - // input is written to the PTY master (write_kernel_process_stdin), - // line discipline runs (echo / VERASE / ICRNL / VEOF), and the - // sidecar drains the cooked bytes from the slave and forwards them - // to the isolate's stream-stdin dispatch - // (forward_tty_slave_input_to_javascript). The in-isolate - // `_kernelStdinRead` bridge stays local; no RPC forwarding is - // needed because the isolate never reads kernel fd 0 itself. - } else if resolved.runtime == GuestRuntimeKind::WebAssembly { - env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1")); +impl Drop for ActiveUnixListener { + fn drop(&mut self) { + if !self.is_final_description_handle() { + return; } - let launch_entrypoint = if resolved.runtime == GuestRuntimeKind::JavaScript { - resolve_agentos_package_javascript_launch_entrypoint(vm, &mut env) - .unwrap_or_else(|| resolved.entrypoint.clone()) - } else { - resolved.entrypoint.clone() + if let Some(path) = self.private_host_path.as_deref() { + cleanup_private_unix_socket_path(path); + } + } +} + +impl ActiveTcpListener { + fn bind( + bind_host: &str, + guest_host: &str, + guest_port: u16, + backlog: Option, + ) -> Result { + let bind_addr = resolve_tcp_bind_addr(bind_host, 0)?; + let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?; + let listener = TcpListener::bind(bind_addr).map_err(sidecar_net_error)?; + listener.set_nonblocking(true).map_err(sidecar_net_error)?; + let local_addr = listener.local_addr().map_err(sidecar_net_error)?; + Ok(Self { + listener: Some(listener), + kernel_socket_id: None, + local_addr: Some(local_addr), + guest_local_addr: guest_addr, + backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG)) + .expect("default backlog fits within usize"), + active_connection_ids: BTreeSet::new(), + description_handles: Arc::new(()), + kernel_transfer_guard: None, + }) + } + + fn clone_for_fd_transfer(&self) -> Result { + Ok(Self { + listener: self + .listener + .as_ref() + .map(TcpListener::try_clone) + .transpose() + .map_err(sidecar_net_error)?, + kernel_socket_id: self.kernel_socket_id, + local_addr: self.local_addr, + guest_local_addr: self.guest_local_addr, + backlog: self.backlog, + active_connection_ids: self.active_connection_ids.clone(), + description_handles: Arc::clone(&self.description_handles), + kernel_transfer_guard: self.kernel_transfer_guard.clone(), + }) + } + + fn is_final_description_handle(&self) -> bool { + Arc::strong_count(&self.description_handles) == 1 + } + + fn bind_kernel( + kernel: &mut SidecarKernel, + kernel_pid: u32, + guest_host: &str, + guest_port: u16, + backlog: Option, + ) -> Result { + let guest_addr = resolve_tcp_bind_addr(guest_host, guest_port)?; + let spec = match guest_addr { + SocketAddr::V4(_) => SocketSpec::tcp(), + SocketAddr::V6(_) => SocketSpec::new(SocketDomain::Inet6, SocketType::Stream), }; - let argv = std::iter::once(launch_entrypoint.clone()) - .chain(resolved.execution_args.iter().cloned()) - .collect::>(); - record_execute_phase("env_argv_setup", phase_start.elapsed()); - let phase_start = Instant::now(); - let kernel_handle = vm - .kernel - .spawn_process( - &resolved.command, - argv, - SpawnOptions { - requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), - cwd: Some(resolved.guest_cwd.clone()), - ..SpawnOptions::default() - }, + let socket_id = kernel + .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec) + .map_err(kernel_error)?; + kernel + .socket_bind_inet( + EXECUTION_DRIVER_NAME, + kernel_pid, + socket_id, + InetSocketAddress::new(guest_addr.ip().to_string(), guest_addr.port()), ) .map_err(kernel_error)?; - let kernel_pid = kernel_handle.pid(); - record_execute_phase("kernel_spawn_process", phase_start.elapsed()); - let tty_master_fd = if requested_tty { - let (master_fd, slave_fd, _) = vm - .kernel - .open_pty(EXECUTION_DRIVER_NAME, kernel_pid) - .map_err(kernel_error)?; - vm.kernel - .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 0) - .map_err(kernel_error)?; - vm.kernel - .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 1) - .map_err(kernel_error)?; - vm.kernel - .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 2) - .map_err(kernel_error)?; - vm.kernel - .pty_set_foreground_pgid(EXECUTION_DRIVER_NAME, kernel_pid, master_fd, kernel_pid) + kernel + .socket_listen( + EXECUTION_DRIVER_NAME, + kernel_pid, + socket_id, + usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG)) + .expect("default backlog fits within usize"), + ) + .map_err(kernel_error)?; + Ok(Self { + listener: None, + kernel_socket_id: Some(socket_id), + local_addr: Some(guest_addr), + guest_local_addr: guest_addr, + backlog: usize::try_from(backlog.unwrap_or(DEFAULT_JAVASCRIPT_NET_BACKLOG)) + .expect("default backlog fits within usize"), + active_connection_ids: BTreeSet::new(), + description_handles: Arc::new(()), + kernel_transfer_guard: None, + }) + } + + pub(crate) fn local_addr(&self) -> SocketAddr { + self.local_addr.unwrap_or(self.guest_local_addr) + } + + fn guest_local_addr(&self) -> SocketAddr { + self.guest_local_addr + } + + fn poll( + &mut self, + kernel: &mut SidecarKernel, + kernel_pid: u32, + wait: Duration, + trace_enabled: bool, + ) -> Result, SidecarError> { + if let Some(socket_id) = self.kernel_socket_id { + let poll_started = Instant::now(); + let result = kernel + .poll_targets( + EXECUTION_DRIVER_NAME, + kernel_pid, + vec![PollTargetEntry::socket(socket_id, POLLIN)], + i32::try_from(wait.as_millis()).unwrap_or(i32::MAX), + ) .map_err(kernel_error)?; - if let Some((cols, rows)) = requested_pty_window_size(&env) { - vm.kernel - .pty_resize(EXECUTION_DRIVER_NAME, kernel_pid, master_fd, cols, rows) - .map_err(kernel_error)?; + let poll_elapsed = poll_started.elapsed(); + let revents = result + .targets + .first() + .map(|entry| entry.revents) + .unwrap_or_else(PollEvents::empty); + record_net_tcp_kernel_poll(trace_enabled, wait, poll_elapsed, revents); + if revents.is_empty() { + return Ok(None); } - Some(master_fd) - } else { - None - }; - - let (execution, process_env) = match resolved.runtime { - GuestRuntimeKind::JavaScript => { - let phase_start = Instant::now(); - let inline_code = load_javascript_entrypoint_source( - vm, - &resolved.host_cwd, - &launch_entrypoint, - &env, - ); - record_execute_phase("js_load_entrypoint_source", phase_start.elapsed()); - let phase_start = Instant::now(); - prepare_javascript_shadow(vm, &resolved, &env)?; - record_execute_phase("js_prepare_shadow", phase_start.elapsed()); + let accepted_socket_id = + match kernel.socket_accept(EXECUTION_DRIVER_NAME, kernel_pid, socket_id) { + Ok(accepted_socket_id) => accepted_socket_id, + Err(error) if error.code() == "EAGAIN" => { + if trace_enabled { + NET_TCP_TRACE_COUNTERS + .server_accept_eagain + .fetch_add(1, Ordering::Relaxed); + } + return Ok(None); + } + Err(error) => { + if trace_enabled { + NET_TCP_TRACE_COUNTERS + .server_accept_errors + .fetch_add(1, Ordering::Relaxed); + } + return Ok(Some(JavascriptTcpListenerEvent::Error { + code: Some(error.code().to_string()), + message: error.to_string(), + })); + } + }; + let accepted = kernel.socket_get(accepted_socket_id).ok_or_else(|| { + SidecarError::InvalidState(format!( + "accepted kernel TCP socket {accepted_socket_id} is missing" + )) + })?; + let local_addr = accepted.local_address().ok_or_else(|| { + SidecarError::InvalidState(format!( + "accepted kernel TCP socket {accepted_socket_id} missing local address" + )) + })?; + let remote_addr = accepted.peer_address().ok_or_else(|| { + SidecarError::InvalidState(format!( + "accepted kernel TCP socket {accepted_socket_id} missing peer address" + )) + })?; + if trace_enabled { + NET_TCP_TRACE_COUNTERS + .server_accept_connections + .fetch_add(1, Ordering::Relaxed); + } + return Ok(Some(JavascriptTcpListenerEvent::Connection( + PendingTcpSocket { + stream: None, + kernel_socket_id: Some(accepted_socket_id), + preallocated: true, + guest_local_addr: resolve_tcp_bind_addr(local_addr.host(), local_addr.port())?, + guest_remote_addr: resolve_tcp_bind_addr( + remote_addr.host(), + remote_addr.port(), + )?, + }, + ))); + } - let phase_start = Instant::now(); - let context = - self.javascript_engine - .create_context(CreateJavascriptContextRequest { - vm_id: vm_id.clone(), - bootstrap_module: None, - compile_cache_root: Some(self.cache_root.join("node-compile-cache")), - }); - record_execute_phase("js_create_context", phase_start.elapsed()); - let phase_start = Instant::now(); - let built_reader = build_module_reader(vm, &resolved); - let guest_reader = built_reader.clone().map(|reader| { - Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader)) - as Box - }); - let module_reader = - built_reader.map(|reader| Box::new(reader) as Box); - record_execute_phase("js_build_module_reader", phase_start.elapsed()); - let phase_start = Instant::now(); - let execution = self - .javascript_engine - .start_execution_with_module_reader( - StartJavascriptExecutionRequest { - guest_runtime: guest_runtime_identity(vm, None, None), - vm_id: vm_id.clone(), - context_id: context.context_id, - argv: std::iter::once(launch_entrypoint.clone()) - .chain(resolved.execution_args.iter().cloned()) - .collect(), - env: env.clone(), - cwd: resolved.host_cwd.clone(), - limits: javascript_execution_limits(vm), - inline_code, - wasm_module_bytes: None, + let deadline = Instant::now() + wait; + loop { + match self + .listener + .as_ref() + .ok_or_else(|| { + SidecarError::InvalidState(String::from("TCP listener socket missing")) + })? + .accept() + { + Ok((stream, remote_addr)) => { + if self.active_connection_ids.len() >= self.backlog { + if let Err(error) = stream.shutdown(Shutdown::Both) { + eprintln!("failed to reject excess Unix socket connection: {error}"); + } + if wait.is_zero() || Instant::now() >= deadline { + return Ok(None); + } + continue; + } + return Ok(Some(JavascriptTcpListenerEvent::Connection( + PendingTcpSocket { + stream: Some(stream), + kernel_socket_id: None, + preallocated: false, + guest_local_addr: self.guest_local_addr, + guest_remote_addr: SocketAddr::new( + remote_addr.ip(), + remote_addr.port(), + ), }, - module_reader, - guest_reader, + ))); + } + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { + if wait.is_zero() || Instant::now() >= deadline { + return Ok(None); + } + if !wait_fd_readable_until( + self.listener + .as_ref() + .expect("TCP listener checked before accept") + .as_fd(), + deadline, ) - .map_err(javascript_error)?; - record_execute_phase("js_start_execution", phase_start.elapsed()); - (ActiveExecution::Javascript(execution), env.clone()) + .map_err(sidecar_net_error)? + { + return Ok(None); + } + } + Err(error) => { + return Ok(Some(JavascriptTcpListenerEvent::Error { + code: io_error_code(&error), + message: error.to_string(), + })); + } } - GuestRuntimeKind::Python => { - // The `python` command path (marked by AGENTOS_PYTHON_ARGV) is - // explicit about file mode via AGENTOS_PYTHON_FILE, so a `-c` code - // string that happens to end in `.py` is never mistaken for a path. - // The low-level execute API keeps the `.py`-suffix heuristic. - let python_file_path = if resolved.env.contains_key("AGENTOS_PYTHON_ARGV") { - resolved.env.get("AGENTOS_PYTHON_FILE").map(PathBuf::from) - } else { - python_file_entrypoint(&resolved.entrypoint) - }; - let pyodide_dist_path = self - .python_engine - .bundled_pyodide_dist_path_for_vm(&vm_id) - .map_err(python_error)?; - let pyodide_cache_path = pyodide_dist_path - .parent() - .and_then(Path::parent) - .unwrap_or(pyodide_dist_path.as_path()) - .join("pyodide-package-cache"); - add_runtime_guest_path_mapping( - &mut env, - PYTHON_PYODIDE_GUEST_ROOT, - &pyodide_dist_path, - ); - add_runtime_guest_path_mapping( - &mut env, - PYTHON_PYODIDE_CACHE_GUEST_ROOT, - &pyodide_cache_path, - ); - add_runtime_host_access_path( - &mut env, - "AGENTOS_EXTRA_FS_READ_PATHS", - &pyodide_dist_path, - true, - ); - add_runtime_host_access_path( - &mut env, - "AGENTOS_EXTRA_FS_READ_PATHS", - &pyodide_cache_path, - true, - ); - add_runtime_host_access_path( - &mut env, - "AGENTOS_EXTRA_FS_WRITE_PATHS", - &pyodide_cache_path, - false, - ); - let context = self - .python_engine - .create_context(CreatePythonContextRequest { - vm_id: vm_id.clone(), - pyodide_dist_path, - }); - let execution = self - .python_engine - .start_execution(StartPythonExecutionRequest { - vm_id: vm_id.clone(), - context_id: context.context_id, - code: resolved.entrypoint.clone(), - file_path: python_file_path, - env: env.clone(), - cwd: resolved.host_cwd.clone(), - limits: python_execution_limits(vm), - guest_runtime: guest_runtime_identity(vm, None, None), - }) - .map_err(python_error)?; - (ActiveExecution::Python(execution), env.clone()) - } - GuestRuntimeKind::WebAssembly => { - let wasm_limits = wasm_execution_limits(vm); - let wasm_guest_runtime = - guest_runtime_identity(vm, Some(u64::from(kernel_pid)), Some(0)); - let wasm_permission_tier = resolved.wasm_permission_tier.unwrap_or_else(|| { - resolve_wasm_permission_tier( - vm, - Some(&resolved.command), - None, - &resolved.entrypoint, - ) - }); - let context = self.wasm_engine.create_context(CreateWasmContextRequest { - vm_id: vm_id.clone(), - module_path: Some(resolved.entrypoint.clone()), - }); - let execution = self - .wasm_engine - .start_execution(StartWasmExecutionRequest { - vm_id: vm_id.clone(), - context_id: context.context_id, - argv: resolved.process_args.clone(), - env: env.clone(), - cwd: resolved.host_cwd.clone(), - permission_tier: execution_wasm_permission_tier(wasm_permission_tier), - limits: wasm_limits, - guest_runtime: wasm_guest_runtime, - }) - .map_err(wasm_error)?; - (ActiveExecution::Wasm(Box::new(execution)), env) - } - }; - let child_pid = execution.child_pid(); - let phase_start = Instant::now(); - let kernel_stdin_writer_fd = if let Some(master_fd) = tty_master_fd { - master_fd - } else { - install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)? - }; - vm.active_processes.insert( - payload.process_id.clone(), - ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution) - .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd) - .with_tty_master_fd(tty_master_fd) - .with_guest_cwd(resolved.guest_cwd.clone()) - .with_env(process_env) - .with_host_cwd(resolved.host_cwd.clone()), - ); - self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?; - mark_execute_response_ready(&vm_id, &payload.process_id); - record_execute_phase("process_register_and_lifecycle", phase_start.elapsed()); - record_execute_phase("execute_total", execute_total_start.elapsed()); + } + } - Ok(DispatchResult { - response: process_started_response( - request, - payload.process_id, - Some(if child_pid == 0 { - kernel_pid - } else { - child_pid - }), - ), - events: Vec::new(), - }) + fn close(&self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> { + if !self.is_final_description_handle() { + return Ok(()); + } + if let Some(socket_id) = self.kernel_socket_id { + close_kernel_socket_idempotent(kernel, kernel_pid, socket_id)?; + } + Ok(()) } - pub(crate) async fn resize_pty( - &mut self, - request: &RequestFrame, - payload: ResizePtyRequest, - ) -> Result { - let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; - self.require_owned_vm(&connection_id, &session_id, &vm_id)?; + fn active_connection_count(&self) -> usize { + self.active_connection_ids.len() + } - let vm = self - .vms - .get_mut(&vm_id) - .ok_or_else(|| missing_vm_error(&vm_id))?; - let process = vm - .active_processes - .get(&payload.process_id) - .ok_or_else(|| { - SidecarError::InvalidState(format!( - "VM {vm_id} has no active process {}", - payload.process_id - )) - })?; - let Some(writer_fd) = process.kernel_stdin_writer_fd else { - return Err(SidecarError::InvalidState(format!( - "process {} does not have a PTY", - payload.process_id - ))); + fn register_connection(&mut self, socket_id: &str) { + self.active_connection_ids.insert(socket_id.to_string()); + } + + fn release_connection(&mut self, socket_id: &str) { + self.active_connection_ids.remove(socket_id); + } +} + +// UDP types moved to crate::state + +impl ActiveUdpSocket { + fn new( + kernel: &mut SidecarKernel, + kernel_pid: u32, + family: JavascriptUdpFamily, + ) -> Result { + let spec = match family { + JavascriptUdpFamily::Ipv4 => SocketSpec::udp(), + JavascriptUdpFamily::Ipv6 => SocketSpec::new(SocketDomain::Inet6, SocketType::Datagram), }; - vm.kernel - .pty_resize( - EXECUTION_DRIVER_NAME, - process.kernel_pid, - writer_fd, - payload.cols, - payload.rows, - ) - .map_err(kernel_error)?; - // The kernel records SIGWINCH for every member of the terminal's - // foreground process group. Embedded V8 executions cannot observe that - // process-table state directly, so mirror the same group delivery into - // every tracked runtime session, including nested Vim/readline children. - let foreground_pgid = vm - .kernel - .tcgetpgrp(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd) + let socket_id = kernel + .socket_create(EXECUTION_DRIVER_NAME, kernel_pid, spec) .map_err(kernel_error)?; - let foreground_pids = vm - .kernel - .list_processes() - .into_values() - .filter(|info| info.pgid == foreground_pgid && info.status != ProcessStatus::Exited) - .map(|info| info.pid) - .collect::>(); - for active in vm.active_processes.values() { - dispatch_v8_signal_to_tracked_processes(active, &foreground_pids, libc::SIGWINCH)?; - } + Ok(Self { + family, + socket: None, + kernel_socket_id: Some(socket_id), + guest_local_addr: None, + recv_buffer_size: 0, + send_buffer_size: 0, + description_handles: Arc::new(()), + kernel_transfer_guard: None, + }) + } - Ok(DispatchResult { - response: self.respond( - request, - ResponsePayload::PtyResized(PtyResizedResponse { - process_id: payload.process_id, - cols: payload.cols, - rows: payload.rows, - }), - ), - events: Vec::new(), + fn clone_for_fd_transfer(&self) -> Result { + Ok(Self { + family: self.family, + socket: self + .socket + .as_ref() + .map(UdpSocket::try_clone) + .transpose() + .map_err(sidecar_net_error)?, + kernel_socket_id: self.kernel_socket_id, + guest_local_addr: self.guest_local_addr, + recv_buffer_size: self.recv_buffer_size, + send_buffer_size: self.send_buffer_size, + description_handles: Arc::clone(&self.description_handles), + kernel_transfer_guard: self.kernel_transfer_guard.clone(), }) } - pub(crate) async fn write_stdin( - &mut self, - request: &RequestFrame, - payload: WriteStdinRequest, - ) -> Result { - let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; - self.require_owned_vm(&connection_id, &session_id, &vm_id)?; + fn is_final_description_handle(&self) -> bool { + Arc::strong_count(&self.description_handles) == 1 + } - let vm = self - .vms - .get_mut(&vm_id) - .ok_or_else(|| missing_vm_error(&vm_id))?; - let process = vm - .active_processes - .get_mut(&payload.process_id) - .ok_or_else(|| { - SidecarError::InvalidState(format!( - "VM {vm_id} has no active process {}", - payload.process_id - )) - })?; - // For a TTY JavaScript process, host stdin must go ONLY to the kernel PTY - // master (so line discipline + echo apply); feeding the in-process local - // stdin bridge as well would double-deliver the input. Non-TTY JS (piped - // stdin) still uses the local bridge; wasm/python always take the - // streaming/no-op `write_stdin` path plus the kernel master write below. - let tty_js = - process.runtime == GuestRuntimeKind::JavaScript && process.tty_master_fd.is_some(); - if !tty_js { - process.execution.write_stdin(&payload.chunk)?; - } - write_kernel_process_stdin(&mut vm.kernel, process, &payload.chunk)?; + fn local_addr(&self) -> Option { + self.guest_local_addr + } - Ok(DispatchResult { - response: stdin_written_response( - request, - payload.process_id, - payload.chunk.len() as u64, - ), - events: Vec::new(), - }) + fn socket(&self) -> Result<&UdpSocket, SidecarError> { + self.socket + .as_ref() + .ok_or_else(|| SidecarError::Execution(String::from("EBADF: bad file descriptor"))) } - pub(crate) async fn close_stdin( + fn bind( &mut self, - request: &RequestFrame, - payload: CloseStdinRequest, - ) -> Result { - let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; - self.require_owned_vm(&connection_id, &session_id, &vm_id)?; - - let vm = self - .vms - .get_mut(&vm_id) - .ok_or_else(|| missing_vm_error(&vm_id))?; - let process = vm - .active_processes - .get_mut(&payload.process_id) - .ok_or_else(|| { - SidecarError::InvalidState(format!( - "VM {vm_id} has no active process {}", - payload.process_id - )) - })?; - process.execution.close_stdin()?; - close_kernel_process_stdin(&mut vm.kernel, process)?; - - Ok(DispatchResult { - response: stdin_closed_response(request, payload.process_id), - events: Vec::new(), - }) - } - - pub(crate) async fn kill_process( - &mut self, - request: &RequestFrame, - payload: KillProcessRequest, - ) -> Result { - let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; - self.require_owned_vm(&connection_id, &session_id, &vm_id)?; - self.kill_process_internal(&vm_id, &payload.process_id, &payload.signal)?; - - Ok(DispatchResult { - response: process_killed_response(request, payload.process_id), - events: Vec::new(), - }) - } + kernel: &mut SidecarKernel, + kernel_pid: u32, + host: Option<&str>, + port: u16, + context: &JavascriptSocketPathContext, + ) -> Result { + if self.socket.is_some() || self.guest_local_addr.is_some() { + return Err(SidecarError::Execution(String::from( + "EINVAL: secure-exec dgram socket is already bound", + ))); + } - pub(crate) async fn find_listener( - &mut self, - request: &RequestFrame, - payload: FindListenerRequest, - ) -> Result { - let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; - self.require_owned_vm(&connection_id, &session_id, &vm_id)?; - require_vm_inspection_permission( - &self.bridge, - &vm_id, - "network.inspect", - "network", - &socket_query_resource(SocketQueryKind::TcpListener, &payload), + let (bind_host, guest_host, guest_family) = normalize_udp_bind_host(host, self.family)?; + let guest_port = allocate_guest_listen_port( + port, + guest_family, + &context.used_udp_guest_ports, + context.listen_policy, )?; - - let listener = - find_socket_state_entry(self.vms.get(&vm_id), SocketQueryKind::TcpListener, &payload)?; - - Ok(DispatchResult { - response: listener_snapshot_response(request, listener), - events: Vec::new(), - }) + let local_addr = resolve_udp_bind_addr(guest_host, guest_port, self.family)?; + if let Some(socket_id) = self.kernel_socket_id { + kernel + .socket_bind_inet( + EXECUTION_DRIVER_NAME, + kernel_pid, + socket_id, + InetSocketAddress::new(local_addr.ip().to_string(), local_addr.port()), + ) + .map_err(kernel_error)?; + } else { + let bind_addr = resolve_udp_bind_addr(bind_host, 0, self.family)?; + let socket = UdpSocket::bind(bind_addr).map_err(sidecar_net_error)?; + socket.set_nonblocking(true).map_err(sidecar_net_error)?; + self.socket = Some(socket); + } + self.guest_local_addr = Some(local_addr); + Ok(local_addr) } - pub(crate) async fn get_process_snapshot( + fn ensure_bound_for_send( &mut self, - request: &RequestFrame, - _payload: GetProcessSnapshotRequest, - ) -> Result { - let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; - self.require_owned_vm(&connection_id, &session_id, &vm_id)?; - require_vm_inspection_permission( - &self.bridge, - &vm_id, - "process.inspect", - "process", - "process://snapshot", - )?; - - let processes = self - .vms - .get_mut(&vm_id) - .map(|vm| { - prune_exited_process_snapshots(vm); - snapshot_vm_processes(vm) - }) - .unwrap_or_default(); + kernel: &mut SidecarKernel, + kernel_pid: u32, + context: &JavascriptSocketPathContext, + ) -> Result { + if let Some(local_addr) = self.local_addr() { + return Ok(local_addr); + } - Ok(DispatchResult { - response: process_snapshot_response(request, processes), - events: Vec::new(), - }) + self.bind(kernel, kernel_pid, None, 0, context) } - pub(crate) async fn guest_kernel_call( + fn send_to( &mut self, - request: &RequestFrame, - payload: GuestKernelCallRequest, - ) -> Result { - let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; - self.require_owned_vm(&connection_id, &session_id, &vm_id)?; - - let vm = self.vms.get_mut(&vm_id).ok_or_else(|| { - SidecarError::InvalidState(format!("VM {vm_id} no longer exists for guest kernel call")) + request: ActiveUdpSendToRequest<'_, B>, + ) -> Result<(usize, SocketAddr), SidecarError> + where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, + { + let ActiveUdpSendToRequest { + bridge, + kernel, + kernel_pid, + vm_id, + dns, + host, + port, + context, + contents, + } = request; + let remote_addr = resolve_udp_addr(UdpRemoteAddrRequest { + bridge, + kernel, + vm_id, + dns, + host, + port, + family: self.family, + context, })?; - let kernel_pid = vm - .active_processes - .get(&payload.execution_id) - .map(|process| process.kernel_pid) - .ok_or_else(|| { - SidecarError::InvalidState(format!( - "VM {vm_id} has no active process {} for guest kernel call", - payload.execution_id - )) + let local_addr = self.ensure_bound_for_send(kernel, kernel_pid, context)?; + let written = if let Some(socket_id) = self.kernel_socket_id { + if is_loopback_ip(remote_addr.ip()) && remote_addr.port() == port { + kernel + .socket_send_to_inet_loopback( + EXECUTION_DRIVER_NAME, + kernel_pid, + socket_id, + InetSocketAddress::new(remote_addr.ip().to_string(), remote_addr.port()), + contents, + ) + .map_err(kernel_error)? + } else { + return Err(SidecarError::Execution(String::from( + "ERR_NOT_IMPLEMENTED: external UDP datagrams are not yet supported by the kernel-backed V8 bridge", + ))); + } + } else { + let socket = self.socket.as_ref().ok_or_else(|| { + SidecarError::InvalidState(String::from("UDP socket is not initialized")) })?; - - let response = agentos_native_sidecar_core::handle_guest_kernel_call( - &mut vm.kernel, - kernel_pid, - EXECUTION_DRIVER_NAME, - &payload.operation, - &payload.payload, - ) - .map_err(guest_kernel_core_error)?; - - Ok(DispatchResult { - response: self.respond( - request, - ResponsePayload::GuestKernelResult(GuestKernelResultResponse { payload: response }), - ), - events: Vec::new(), - }) + socket + .send_to(contents, remote_addr) + .map_err(sidecar_net_error)? + }; + Ok((written, local_addr)) } - pub(crate) async fn get_resource_snapshot( - &mut self, - request: &RequestFrame, - _payload: GetResourceSnapshotRequest, - ) -> Result { - let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; - self.require_owned_vm(&connection_id, &session_id, &vm_id)?; - require_vm_inspection_permission( - &self.bridge, - &vm_id, - "process.inspect", - "process", - "process://resources", - )?; - - let snapshot = self - .vms - .get(&vm_id) - .map(|vm| vm.kernel.resource_snapshot()) - .unwrap_or_default(); - let queue_snapshots = queue_tracker::queue_snapshot() - .into_iter() - .map(|queue| QueueSnapshotEntry { - name: queue.name.as_str().to_owned(), - category: queue.category.as_str().to_owned(), - depth: queue.depth as u64, - high_water: queue.high_water as u64, - capacity: queue.capacity as u64, - fill_percent: queue.fill_percent as u64, - }) - .collect(); - - Ok(DispatchResult { - response: self.respond( - request, - ResponsePayload::ResourceSnapshot(ResourceSnapshotResponse { - running_processes: snapshot.running_processes as u64, - exited_processes: snapshot.exited_processes as u64, - fd_tables: snapshot.fd_tables as u64, - open_fds: snapshot.open_fds as u64, - pipes: snapshot.pipes as u64, - pipe_buffered_bytes: snapshot.pipe_buffered_bytes as u64, - ptys: snapshot.ptys as u64, - pty_buffered_input_bytes: snapshot.pty_buffered_input_bytes as u64, - pty_buffered_output_bytes: snapshot.pty_buffered_output_bytes as u64, - sockets: snapshot.sockets as u64, - socket_listeners: snapshot.socket_listeners as u64, - socket_connections: snapshot.socket_connections as u64, - socket_buffered_bytes: snapshot.socket_buffered_bytes as u64, - socket_datagram_queue_len: snapshot.socket_datagram_queue_len as u64, - queue_snapshots, - }), - ), - events: Vec::new(), - }) + fn poll( + &self, + kernel: &mut SidecarKernel, + kernel_pid: u32, + wait: Duration, + ) -> Result, SidecarError> { + if let Some(socket_id) = self.kernel_socket_id { + let result = kernel + .poll_targets( + EXECUTION_DRIVER_NAME, + kernel_pid, + vec![PollTargetEntry::socket(socket_id, POLLIN)], + i32::try_from(wait.as_millis()).unwrap_or(i32::MAX), + ) + .map_err(kernel_error)?; + let revents = result + .targets + .first() + .map(|entry| entry.revents) + .unwrap_or_else(PollEvents::empty); + if revents.is_empty() { + return Ok(None); + } + return match kernel.socket_recv_datagram( + EXECUTION_DRIVER_NAME, + kernel_pid, + socket_id, + 64 * 1024, + ) { + Ok(Some(datagram)) => { + let (source_address, payload) = datagram.into_parts(); + let remote_addr = source_address + .map(|source| { + resolve_udp_bind_addr(source.host(), source.port(), self.family) + }) + .transpose()? + .unwrap_or_else(|| match self.family { + JavascriptUdpFamily::Ipv4 => { + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 0) + } + JavascriptUdpFamily::Ipv6 => { + SocketAddr::new(IpAddr::V6(Ipv6Addr::LOCALHOST), 0) + } + }); + Ok(Some(JavascriptUdpSocketEvent::Message { + data: payload, + remote_addr, + })) + } + Ok(None) => Ok(None), + Err(error) if error.code() == "EAGAIN" => Ok(None), + Err(error) => Ok(Some(JavascriptUdpSocketEvent::Error { + code: Some(error.code().to_string()), + message: error.to_string(), + })), + }; + } + let socket = self.socket()?; + let deadline = Instant::now() + wait; + let mut buffer = vec![0_u8; 64 * 1024]; + + loop { + match socket.recv_from(&mut buffer) { + Ok((bytes_read, remote_addr)) => { + return Ok(Some(JavascriptUdpSocketEvent::Message { + data: buffer[..bytes_read].to_vec(), + remote_addr, + })); + } + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { + if wait.is_zero() || Instant::now() >= deadline { + return Ok(None); + } + if !wait_fd_readable_until(socket.as_fd(), deadline) + .map_err(sidecar_net_error)? + { + return Ok(None); + } + } + Err(error) => { + return Ok(Some(JavascriptUdpSocketEvent::Error { + code: io_error_code(&error), + message: error.to_string(), + })); + } + } + } } - pub(crate) async fn find_bound_udp( - &mut self, - request: &RequestFrame, - payload: FindBoundUdpRequest, - ) -> Result { - let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; - self.require_owned_vm(&connection_id, &session_id, &vm_id)?; + fn close(&mut self, kernel: &mut SidecarKernel, kernel_pid: u32) -> Result<(), SidecarError> { + if !self.is_final_description_handle() { + return Ok(()); + } + let close_result = self + .kernel_socket_id + .map(|socket_id| close_kernel_socket_idempotent(kernel, kernel_pid, socket_id)) + .transpose(); + self.socket.take(); + self.guest_local_addr = None; + close_result.map(|_| ()) + } - let lookup_request = FindListenerRequest { - host: payload.host, - port: payload.port, - path: None, - }; - require_vm_inspection_permission( - &self.bridge, - &vm_id, - "network.inspect", - "network", - &socket_query_resource(SocketQueryKind::UdpBound, &lookup_request), - )?; - let socket = find_socket_state_entry( - self.vms.get(&vm_id), - SocketQueryKind::UdpBound, - &lookup_request, - )?; + fn set_buffer_size(&mut self, which: &str, size: usize) -> Result<(), SidecarError> { + match which { + "recv" => self.recv_buffer_size = size, + "send" => self.send_buffer_size = size, + other => { + return Err(SidecarError::InvalidState(format!( + "unsupported UDP buffer size kind {other}" + ))); + } + } + if self.kernel_socket_id.is_some() { + return Ok(()); + } + let socket = self.socket()?; + let socket = SockRef::from(socket); + match which { + "recv" => socket.set_recv_buffer_size(size).map_err(sidecar_net_error), + "send" => socket.set_send_buffer_size(size).map_err(sidecar_net_error), + other => Err(SidecarError::InvalidState(format!( + "unsupported UDP buffer size kind {other}" + ))), + } + } - Ok(DispatchResult { - response: bound_udp_snapshot_response(request, socket), - events: Vec::new(), - }) + fn get_buffer_size(&self, which: &str) -> Result { + if self.kernel_socket_id.is_some() { + return Ok(match which { + "recv" => self.recv_buffer_size, + "send" => self.send_buffer_size, + other => { + return Err(SidecarError::InvalidState(format!( + "unsupported UDP buffer size kind {other}" + ))); + } + }); + } + let socket = self.socket()?; + let socket = SockRef::from(socket); + match which { + "recv" => socket.recv_buffer_size().map_err(sidecar_net_error), + "send" => socket.send_buffer_size().map_err(sidecar_net_error), + other => Err(SidecarError::InvalidState(format!( + "unsupported UDP buffer size kind {other}" + ))), + } } +} - pub(crate) async fn vm_fetch( - &mut self, - request: &RequestFrame, - payload: VmFetchRequest, - ) -> Result { - let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; - self.require_owned_vm(&connection_id, &session_id, &vm_id)?; +// ActiveExecution, ActiveExecutionEvent, SocketQueryKind moved to crate::state - let vm = self - .vms - .get_mut(&vm_id) - .ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?; - let target_path = if payload.path.starts_with('/') { - payload.path.clone() - } else { - format!("/{}", payload.path) - }; - let request_url = Url::parse(&format!("http://127.0.0.1:{}{target_path}", payload.port)) - .map_err(|error| { - SidecarError::InvalidState(format!( - "invalid vm.fetch target {target_path:?}: {error}" - )) - })?; - let header_values: BTreeMap = serde_json::from_str(&payload.headers_json) - .map_err(|error| { - SidecarError::InvalidState(format!( - "vm.fetch headers_json must be valid JSON: {error}" - )) - })?; - let options = JavascriptHttpRequestOptions { - method: Some(payload.method), - headers: header_values, - body: payload.body, - reject_unauthorized: None, - }; - let headers = parse_http_header_collection(&options.headers, "vm.fetch headers")?; - let target_process_id = find_kernel_http_listener_process(vm, payload.port); - if let Some(target_process_id) = target_process_id { - let max_fetch_response_bytes = vm.limits.http.max_fetch_response_bytes; - let response_json = match dispatch_kernel_http_fetch( - &self.bridge, - &vm_id, - vm, - &target_process_id, - payload.port, - &target_path, - &options, - &headers, - max_fetch_response_bytes, - ) { - Ok(response_json) => response_json, - Err(error) => { - if let Some(exit_code) = kernel_http_fetch_target_exit_code(&error) { - let _ = vm; - self.finish_active_process_exit(&vm_id, &target_process_id, exit_code)?; - } - return Err(error); - } - }; - let response = self.respond( - request, - ResponsePayload::VmFetchResult(VmFetchResponse { response_json }), - ); - ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?; +impl ActiveExecution { + pub(crate) fn is_prepared_for_start(&self) -> bool { + match self { + Self::Javascript(execution) => execution.is_prepared_for_start(), + Self::Python(execution) => execution.is_prepared_for_start(), + Self::Wasm(execution) => execution.is_prepared_for_start(), + Self::Tool(_) => false, + } + } - return Ok(DispatchResult { - response, - events: Vec::new(), - }); + pub(crate) fn start_prepared(&mut self) -> Result<(), SidecarError> { + match self { + Self::Javascript(execution) => execution + .start_prepared() + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Python(execution) => execution + .start_prepared() + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Wasm(execution) => execution + .start_prepared() + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Tool(_) => Err(SidecarError::InvalidState(String::from( + "tool execution cannot be a prepared execve image", + ))), } + } - let Some((target_process_id, server_id)) = - vm.active_processes - .iter() - .find_map(|(process_id, process)| { - process - .http_servers - .iter() - .find(|(_, server)| server.guest_local_addr.port() == payload.port) - .map(|(server_id, _)| (process_id.clone(), *server_id)) - }) - else { - return Err(SidecarError::Execution(format!( - "vm.fetch could not find a guest HTTP listener on port {}", - payload.port - ))); - }; - let socket_paths = build_javascript_socket_path_context(vm)?; - let resource_limits = vm.kernel.resource_limits().clone(); - let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); - let process = vm - .active_processes - .get_mut(&target_process_id) - .ok_or_else(|| { - SidecarError::InvalidState(format!( - "vm.fetch target process disappeared: {target_process_id}" - )) - })?; - let request_json = serialize_http_loopback_request(&request_url, &options, &headers)?; - let response_json = dispatch_loopback_http_request(LoopbackHttpDispatchRequest { - bridge: &self.bridge, - vm_id: &vm_id, - dns: &vm.dns, - socket_paths: &socket_paths, - kernel: &mut vm.kernel, - kernel_readiness, - process, - resource_limits: &resource_limits, - server_id, - request_json: &request_json, - })?; + pub(crate) fn uses_shared_v8_runtime(&self) -> bool { + match self { + Self::Javascript(execution) => execution.uses_shared_v8_runtime(), + Self::Python(execution) => execution.uses_shared_v8_runtime(), + Self::Wasm(execution) => execution.uses_shared_v8_runtime(), + Self::Tool(_) => false, + } + } - let response = self.respond( - request, - ResponsePayload::VmFetchResult(VmFetchResponse { response_json }), - ); - ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?; + pub(crate) fn child_pid(&self) -> u32 { + match self { + Self::Javascript(execution) => execution.child_pid(), + Self::Python(execution) => execution.child_pid(), + Self::Wasm(execution) => execution.child_pid(), + Self::Tool(_) => 0, + } + } - Ok(DispatchResult { - response, - events: Vec::new(), - }) + pub(crate) fn write_stdin(&mut self, chunk: &[u8]) -> Result<(), SidecarError> { + match self { + Self::Javascript(execution) => execution + .write_stdin(chunk) + .map_err(|error| SidecarError::Execution(error.to_string())), + // Sidecar Python and WASM always read fd 0 from the sidecar kernel + // pipe. Their in-process LocalKernelStdinBridge is not consumed in + // this mode; duplicating input into it creates a hidden 16 MiB cap. + Self::Python(_) | Self::Wasm(_) => Ok(()), + Self::Tool(_) => Ok(()), + } } - pub(crate) async fn get_signal_state( + pub(crate) fn close_stdin(&mut self) -> Result<(), SidecarError> { + match self { + Self::Javascript(execution) => execution + .close_stdin() + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Python(_) | Self::Wasm(_) => Ok(()), + Self::Tool(_) => Ok(()), + } + } + + pub(crate) fn respond_python_vfs_rpc_success( &mut self, - request: &RequestFrame, - payload: GetSignalStateRequest, - ) -> Result { - let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; - self.require_owned_vm(&connection_id, &session_id, &vm_id)?; + id: u64, + payload: PythonVfsRpcResponsePayload, + ) -> Result<(), SidecarError> { + match self { + Self::Python(execution) => execution + .respond_vfs_rpc_success(id, payload) + .map_err(|error| SidecarError::Execution(error.to_string())), + _ => Err(SidecarError::InvalidState(String::from( + "only Python executions can service Python VFS RPC responses", + ))), + } + } - let handlers = self - .vms - .get(&vm_id) - .and_then(|vm| vm.signal_states.get(&payload.process_id)) - .cloned() - .unwrap_or_default(); + pub(crate) fn respond_python_vfs_rpc_error( + &mut self, + id: u64, + code: impl Into, + message: impl Into, + ) -> Result<(), SidecarError> { + match self { + Self::Python(execution) => execution + .respond_vfs_rpc_error(id, code, message) + .map_err(|error| SidecarError::Execution(error.to_string())), + _ => Err(SidecarError::InvalidState(String::from( + "only Python executions can service Python VFS RPC responses", + ))), + } + } - Ok(DispatchResult { - response: signal_state_response(request, payload.process_id, handlers), - events: Vec::new(), - }) + pub(crate) fn send_javascript_stream_event( + &self, + event_type: &str, + payload: Value, + ) -> Result<(), SidecarError> { + match self { + Self::Javascript(execution) => execution + .send_stream_event(event_type, payload) + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Wasm(execution) => execution + .send_stream_event(event_type, payload) + .map_err(|error| SidecarError::Execution(error.to_string())), + _ => Err(SidecarError::InvalidState(String::from( + "only embedded V8 executions can receive JavaScript stream events", + ))), + } } - pub(crate) async fn get_zombie_timer_count( - &mut self, - request: &RequestFrame, - _payload: GetZombieTimerCountRequest, - ) -> Result { - let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; - self.require_owned_vm(&connection_id, &session_id, &vm_id)?; + pub(crate) fn javascript_v8_session_handle(&self) -> Option { + match self { + Self::Javascript(execution) => Some(execution.v8_session_handle()), + Self::Wasm(execution) => Some(execution.v8_session_handle()), + _ => None, + } + } - let count = self - .vms - .get(&vm_id) - .map(|vm| vm.kernel.zombie_timer_count() as u64) - .unwrap_or_default(); + pub(crate) fn terminate(&mut self) -> Result<(), SidecarError> { + match self { + Self::Javascript(execution) => execution + .terminate() + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Python(execution) => execution + .kill() + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Wasm(execution) => execution + .terminate() + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Tool(_) => Ok(()), + } + } - Ok(DispatchResult { - response: zombie_timer_count_response(request, count), - events: Vec::new(), - }) + pub(crate) fn pause(&self) -> Result<(), SidecarError> { + match self { + Self::Javascript(execution) => execution + .pause() + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Python(execution) => execution + .pause() + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Wasm(execution) => execution + .pause() + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Tool(_) => Ok(()), + } } - pub(crate) fn kill_process_internal( + pub(crate) fn resume(&self) -> Result<(), SidecarError> { + match self { + Self::Javascript(execution) => execution + .resume() + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Python(execution) => execution + .resume() + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Wasm(execution) => execution + .resume() + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Tool(_) => Ok(()), + } + } + + pub(crate) fn respond_javascript_sync_rpc_success( &mut self, - vm_id: &str, - process_id: &str, - signal: &str, + id: u64, + result: Value, ) -> Result<(), SidecarError> { - let signal_name = signal.to_owned(); - let signal = parse_signal(signal)?; - let vm = self - .vms - .get_mut(vm_id) - .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?; - let process = vm.active_processes.get_mut(process_id).ok_or_else(|| { - SidecarError::InvalidState(format!("VM {vm_id} has no active process {process_id}")) - })?; - let kernel_pid = process.kernel_pid; - if !matches!(signal, 0 | libc::SIGCONT) { - // A guest parked in a deferred kernel-wait sync RPC is blocked in a - // native bridge wait the kill cannot interrupt; answer the parked - // RPC first so the termination can take effect. - flush_parked_kernel_wait_rpc(process); + match self { + Self::Javascript(execution) => execution + .respond_sync_rpc_success(id, result) + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Python(execution) => execution + .respond_javascript_sync_rpc_success(id, result) + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Wasm(execution) => execution + .respond_sync_rpc_success(id, result) + .map_err(|error| SidecarError::Execution(error.to_string())), + _ => Err(SidecarError::InvalidState(String::from( + "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses", + ))), } + } - enum KillBehavior { - Tool, - SharedV8StateOnly, - SharedV8Continue, - SharedV8Terminate, - SharedV8DispatchOrTerminate, - Noop, - HostPid(u32), + pub(crate) fn claim_javascript_sync_rpc_response( + &mut self, + id: u64, + ) -> Result { + match self { + Self::Javascript(execution) => execution + .claim_sync_rpc_response(id) + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Python(execution) => execution + .claim_javascript_sync_rpc_response(id) + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Wasm(execution) => execution + .claim_sync_rpc_response(id) + .map_err(|error| SidecarError::Execution(error.to_string())), + _ => Err(SidecarError::InvalidState(String::from( + "only JavaScript, Python, and WebAssembly executions can claim JavaScript sync RPC responses", + ))), } + } - let behavior = match &process.execution { - ActiveExecution::Tool(_) => KillBehavior::Tool, - ActiveExecution::Javascript(execution) - if execution.uses_shared_v8_runtime() && matches!(signal, 0 | libc::SIGSTOP) => - { - KillBehavior::SharedV8StateOnly - } - ActiveExecution::Javascript(execution) - if execution.uses_shared_v8_runtime() && signal == libc::SIGCONT => - { - KillBehavior::SharedV8Continue - } - ActiveExecution::Wasm(execution) - if execution.uses_shared_v8_runtime() - && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) => - { - KillBehavior::SharedV8StateOnly - } - ActiveExecution::Python(execution) - if execution.uses_shared_v8_runtime() - && matches!(signal, 0 | libc::SIGSTOP | libc::SIGCONT) => - { - KillBehavior::SharedV8StateOnly - } - ActiveExecution::Javascript(execution) - if execution.uses_shared_v8_runtime() && signal == SIGKILL => - { - KillBehavior::SharedV8Terminate - } - ActiveExecution::Wasm(execution) - if execution.uses_shared_v8_runtime() && signal == SIGKILL => - { - KillBehavior::SharedV8Terminate - } - ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime() => { - KillBehavior::SharedV8DispatchOrTerminate - } - ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() => { - KillBehavior::SharedV8Terminate - } - ActiveExecution::Python(execution) if execution.uses_shared_v8_runtime() => { - KillBehavior::SharedV8Terminate - } - ActiveExecution::Javascript(execution) if execution.child_pid() == 0 => { - KillBehavior::Noop - } - _ => KillBehavior::HostPid(process.execution.child_pid()), - }; - - match behavior { - KillBehavior::Tool => { - let ActiveExecution::Tool(execution) = &process.execution else { - unreachable!("kill behavior must match tool execution"); - }; - if signal != 0 { - execution.cancelled.store(true, Ordering::Relaxed); - process.queue_pending_execution_event(ActiveExecutionEvent::Exited( - 128 + signal, - ))?; - } - } - KillBehavior::SharedV8StateOnly => { - if matches!(signal, libc::SIGSTOP | libc::SIGCONT) { - vm.kernel - .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal) - .map_err(kernel_error)?; - } - } - KillBehavior::SharedV8Continue => { - vm.kernel - .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal) - .map_err(kernel_error)?; - if signal != 0 && !dispatch_v8_process_signal(process, signal)? { - process.execution.terminate()?; - } - } - KillBehavior::SharedV8Terminate => { - if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) { - close_kernel_process_stdin(&mut vm.kernel, process)?; - } - process.execution.terminate()?; - let needs_synthetic_exit = matches!(process.execution, ActiveExecution::Wasm(_)) - || (signal == SIGKILL - && matches!(process.execution, ActiveExecution::Javascript(_))); - if signal != 0 && needs_synthetic_exit { - process.queue_pending_execution_event(ActiveExecutionEvent::Exited( - 128 + signal, - ))?; - } - } - KillBehavior::SharedV8DispatchOrTerminate => { - if signal != 0 && !dispatch_v8_process_signal(process, signal)? { - process.execution.terminate()?; - } - } - KillBehavior::Noop => {} - KillBehavior::HostPid(pid) => { - if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) { - close_kernel_process_stdin(&mut vm.kernel, process)?; - } - signal_runtime_process(pid, signal)?; - } + pub(crate) fn respond_claimed_javascript_sync_rpc_success( + &mut self, + id: u64, + result: Value, + ) -> Result<(), SidecarError> { + match self { + Self::Javascript(execution) => execution + .respond_claimed_sync_rpc_success(id, result) + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Python(execution) => execution + .respond_claimed_javascript_sync_rpc_success(id, result) + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Wasm(execution) => execution + .respond_claimed_sync_rpc_success(id, result) + .map_err(|error| SidecarError::Execution(error.to_string())), + _ => Err(SidecarError::InvalidState(String::from( + "only JavaScript, Python, and WebAssembly executions can service claimed JavaScript sync RPC responses", + ))), } - emit_security_audit_event( - &self.bridge, - vm_id, - "security.process.kill", - audit_fields([ - (String::from("source"), String::from("control_plane")), - (String::from("source_pid"), String::from("0")), - (String::from("target_pid"), process.kernel_pid.to_string()), - (String::from("process_id"), process_id.to_owned()), - (String::from("signal"), signal_name), - ( - String::from("host_pid"), - process.execution.child_pid().to_string(), - ), - ]), - ); - Ok(()) } - pub async fn pump_process_events( + pub(crate) fn respond_javascript_sync_rpc_raw_success( &mut self, - ownership: &OwnershipScope, - ) -> Result { - let mut emitted_any = false; - - let mut queued_envelopes = Vec::new(); - { - let pending_capacity = self.pending_process_event_capacity(); - let receiver = self.process_event_receiver.as_mut().ok_or_else(|| { - SidecarError::InvalidState(String::from("process event receiver unavailable")) - })?; - loop { - if queued_envelopes.len() >= pending_capacity { - if receiver.is_empty() { - break; - } - return Err(process_event_queue_overflow_error()); - } - match receiver.try_recv() { - Ok(envelope) => { - queued_envelopes.push(envelope); - emitted_any = true; - } - Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break, - Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break, - } - } - } - for envelope in queued_envelopes { - self.queue_pending_process_event(envelope)?; + id: u64, + payload: Vec, + ) -> Result<(), SidecarError> { + match self { + Self::Javascript(execution) => execution + .respond_sync_rpc_raw_success(id, payload) + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Wasm(execution) => execution + .respond_sync_rpc_raw_success(id, payload) + .map_err(|error| SidecarError::Execution(error.to_string())), + _ => Err(SidecarError::InvalidState(String::from( + "only embedded V8 executions can service raw JavaScript sync RPC responses", + ))), } + } - let vm_ids = self.vm_ids_for_scope(ownership)?; - for vm_id in vm_ids { - while let Some(vm) = self.vms.get(&vm_id) { - let connection_id = vm.connection_id.clone(); - let session_id = vm.session_id.clone(); - let process_ids = self - .vms - .get(&vm_id) - .map(|vm| vm.active_processes.keys().cloned().collect::>()) - .unwrap_or_default(); - let mut emitted_this_pass = false; - - for process_id in process_ids { - if self - .vms - .get(&vm_id) - .is_some_and(|vm| vm.detached_child_processes.contains(&process_id)) - { - continue; - } - enum ProcessPollResult { - Event(Box>), - RecoverClosedChannel, - } - let poll_result = { - let Some(vm) = self.vms.get_mut(&vm_id) else { - continue; - }; - let Some(process) = vm.active_processes.get_mut(&process_id) else { - continue; - }; - if let Some(event) = process.pending_execution_events.pop_front() { - ProcessPollResult::Event(Box::new(Some(event))) - } else { - match process.execution.poll_event(Duration::ZERO).await { - Ok(event) => ProcessPollResult::Event(Box::new(event)), - Err(SidecarError::Execution(message)) - if (process.runtime == GuestRuntimeKind::JavaScript - && closed_javascript_event_channel(&message)) - || (process.runtime == GuestRuntimeKind::Python - && closed_python_event_channel(&message)) - || (process.runtime == GuestRuntimeKind::WebAssembly - && closed_wasm_event_channel(&message)) => - { - ProcessPollResult::RecoverClosedChannel - } - Err(other) => return Err(other), - } - } - }; - let event = match poll_result { - ProcessPollResult::Event(event) => *event, - ProcessPollResult::RecoverClosedChannel => { - self.recover_closed_root_runtime_process_event(&vm_id, &process_id)? - } - }; - - let Some(event) = event else { - continue; - }; - if matches!(&event, ActiveExecutionEvent::Exited(_)) { - record_execute_response_to_exit_milestone( - "execute_response_to_exit_event_polled", - &vm_id, - &process_id, - ); - } - - if Self::internal_execution_event(&event) { - // These events are sidecar work items, not client-facing - // process events. Handle them immediately so a sibling - // process can service sync RPCs while another request - // waits on VM-local networking. - self.handle_execution_event(&vm_id, &process_id, event)?; - } else { - self.queue_pending_process_event(ProcessEventEnvelope { - connection_id: connection_id.clone(), - session_id: session_id.clone(), - vm_id: vm_id.clone(), - process_id: process_id.clone(), - event, - })?; - } - emitted_any = true; - emitted_this_pass = true; - } - - if !emitted_this_pass { - break; - } + pub(crate) fn respond_javascript_sync_rpc_response( + &mut self, + id: u64, + response: JavascriptSyncRpcServiceResponse, + ) -> Result<(), SidecarError> { + match response { + JavascriptSyncRpcServiceResponse::Json(result) => { + self.respond_javascript_sync_rpc_success(id, result) } - - if self.pump_detached_child_process_events(&vm_id)? { - emitted_any = true; + JavascriptSyncRpcServiceResponse::Raw(payload) => { + self.respond_javascript_sync_rpc_raw_success(id, payload) } } - - Ok(emitted_any) } - fn internal_execution_event(event: &ActiveExecutionEvent) -> bool { - matches!( - event, - ActiveExecutionEvent::JavascriptSyncRpcRequest(_) - | ActiveExecutionEvent::PythonVfsRpcRequest(_) - | ActiveExecutionEvent::SignalState { .. } - ) - } - - fn recover_closed_root_runtime_process_event( + pub(crate) fn respond_javascript_sync_rpc_error( &mut self, - vm_id: &str, - process_id: &str, - ) -> Result, SidecarError> { - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(None); - }; - let Some(process) = vm.active_processes.get(process_id) else { - return Ok(None); - }; - if process.execution.uses_shared_v8_runtime() { - return Ok(None); - } - if process.runtime != GuestRuntimeKind::JavaScript - && process.runtime != GuestRuntimeKind::Python - && process.runtime != GuestRuntimeKind::WebAssembly - { - return Ok(None); - } - let runtime_child_pid = process.execution.child_pid(); - if runtime_child_pid == 0 { - return Ok(None); - } - if let Some(status) = runtime_child_exit_status(runtime_child_pid)? { - return Ok(Some(ActiveExecutionEvent::Exited(status))); - } - if runtime_child_is_alive(runtime_child_pid)? { - return Ok(None); + id: u64, + code: impl Into, + message: impl Into, + ) -> Result<(), SidecarError> { + match self { + Self::Javascript(execution) => execution + .respond_sync_rpc_error(id, code, message) + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Python(execution) => execution + .respond_javascript_sync_rpc_error(id, code, message) + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Wasm(execution) => execution + .respond_sync_rpc_error(id, code, message) + .map_err(|error| SidecarError::Execution(error.to_string())), + _ => Err(SidecarError::InvalidState(String::from( + "only JavaScript, Python, and WebAssembly executions can service JavaScript sync RPC responses", + ))), } - Ok(Some(ActiveExecutionEvent::Exited(0))) } - fn active_process_by_path<'a>( - process: &'a ActiveProcess, - child_path: &[&str], - ) -> Option<&'a ActiveProcess> { - let mut current = process; - for child_id in child_path { - current = current.child_processes.get(*child_id)?; + pub(crate) fn respond_claimed_javascript_sync_rpc_error( + &mut self, + id: u64, + code: impl Into, + message: impl Into, + ) -> Result<(), SidecarError> { + let code = code.into(); + let message = message.into(); + match self { + Self::Javascript(execution) => execution + .respond_claimed_sync_rpc_error(id, code, message) + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Python(execution) => execution + .respond_claimed_javascript_sync_rpc_error(id, code, message) + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Wasm(execution) => execution + .respond_claimed_sync_rpc_error(id, code, message) + .map_err(|error| SidecarError::Execution(error.to_string())), + _ => Err(SidecarError::InvalidState(String::from( + "only JavaScript, Python, and WebAssembly executions can service claimed JavaScript sync RPC errors", + ))), } - Some(current) } - fn active_process_by_path_mut<'a>( - process: &'a mut ActiveProcess, - child_path: &[&str], - ) -> Option<&'a mut ActiveProcess> { - let mut current = process; - for child_id in child_path { - current = current.child_processes.get_mut(*child_id)?; + pub(crate) async fn poll_event( + &mut self, + timeout: Duration, + ) -> Result, SidecarError> { + match self { + Self::Javascript(execution) => execution + .poll_event(timeout) + .await + .map(|event| { + event.map(|event| match event { + JavascriptExecutionEvent::Stdout(chunk) => { + ActiveExecutionEvent::Stdout(chunk) + } + JavascriptExecutionEvent::Stderr(chunk) => { + ActiveExecutionEvent::Stderr(chunk) + } + JavascriptExecutionEvent::SyncRpcRequest(request) => { + ActiveExecutionEvent::JavascriptSyncRpcRequest(request) + } + JavascriptExecutionEvent::SignalState { + signal, + registration, + } => ActiveExecutionEvent::SignalState { + signal, + registration: map_node_signal_registration(registration), + }, + JavascriptExecutionEvent::Exited(code) => { + ActiveExecutionEvent::Exited(code) + } + }) + }) + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Python(execution) => execution + .poll_event(timeout) + .await + .map(|event| { + event.map(|event| match event { + PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk), + PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk), + PythonExecutionEvent::JavascriptSyncRpcRequest(request) => { + ActiveExecutionEvent::JavascriptSyncRpcRequest(request) + } + PythonExecutionEvent::VfsRpcRequest(request) => { + ActiveExecutionEvent::PythonVfsRpcRequest(request) + } + PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code), + }) + }) + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Wasm(execution) => execution + .poll_event(timeout) + .await + .map(|event| { + event.map(|event| match event { + WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk), + WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk), + WasmExecutionEvent::SyncRpcRequest(request) => { + ActiveExecutionEvent::JavascriptSyncRpcRequest(request) + } + WasmExecutionEvent::SignalState { + signal, + registration, + } => ActiveExecutionEvent::SignalState { + signal, + registration: map_wasm_signal_registration(registration), + }, + WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code), + }) + }) + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Tool(execution) => { + let _ = timeout; + poll_tool_process_event(execution) + } } - Some(current) } - fn active_process_by_owned_path_mut<'a>( - process: &'a mut ActiveProcess, - child_path: &[String], - ) -> Option<&'a mut ActiveProcess> { - let mut current = process; - for child_id in child_path { - current = current.child_processes.get_mut(child_id)?; + pub(crate) fn poll_event_blocking( + &mut self, + timeout: Duration, + ) -> Result, SidecarError> { + match self { + Self::Javascript(execution) => execution + .poll_event_blocking(timeout) + .map(|event| { + event.map(|event| match event { + JavascriptExecutionEvent::Stdout(chunk) => { + ActiveExecutionEvent::Stdout(chunk) + } + JavascriptExecutionEvent::Stderr(chunk) => { + ActiveExecutionEvent::Stderr(chunk) + } + JavascriptExecutionEvent::SyncRpcRequest(request) => { + ActiveExecutionEvent::JavascriptSyncRpcRequest(request) + } + JavascriptExecutionEvent::SignalState { + signal, + registration, + } => ActiveExecutionEvent::SignalState { + signal, + registration: map_node_signal_registration(registration), + }, + JavascriptExecutionEvent::Exited(code) => { + ActiveExecutionEvent::Exited(code) + } + }) + }) + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Python(execution) => execution + .poll_event_blocking(timeout) + .map(|event| { + event.map(|event| match event { + PythonExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk), + PythonExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk), + PythonExecutionEvent::JavascriptSyncRpcRequest(request) => { + ActiveExecutionEvent::JavascriptSyncRpcRequest(request) + } + PythonExecutionEvent::VfsRpcRequest(request) => { + ActiveExecutionEvent::PythonVfsRpcRequest(request) + } + PythonExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code), + }) + }) + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Wasm(execution) => execution + .poll_event_blocking(timeout) + .map(|event| { + event.map(|event| match event { + WasmExecutionEvent::Stdout(chunk) => ActiveExecutionEvent::Stdout(chunk), + WasmExecutionEvent::Stderr(chunk) => ActiveExecutionEvent::Stderr(chunk), + WasmExecutionEvent::SyncRpcRequest(request) => { + ActiveExecutionEvent::JavascriptSyncRpcRequest(request) + } + WasmExecutionEvent::SignalState { + signal, + registration, + } => ActiveExecutionEvent::SignalState { + signal, + registration: map_wasm_signal_registration(registration), + }, + WasmExecutionEvent::Exited(code) => ActiveExecutionEvent::Exited(code), + }) + }) + .map_err(|error| SidecarError::Execution(error.to_string())), + Self::Tool(execution) => { + let _ = timeout; + poll_tool_process_event(execution) + } } - Some(current) } +} - fn active_process_path_by_kernel_pid( - process: &ActiveProcess, - kernel_pid: u32, - ) -> Option> { - if process.kernel_pid == kernel_pid { - return Some(Vec::new()); - } +struct ToolProcessEventRequest { + sidecar_requests: SharedSidecarRequestClient, + connection_id: String, + session_id: String, + vm_id: String, + tool_resolution: ToolCommandResolution, + cancelled: Arc, + pending_events: Arc>>, + event_overflow_reason: Arc>>, + pending_event_bytes: Arc, + pending_event_count_limit: Arc, + pending_event_bytes_limit: Arc, + vm_pending_event_bytes_budget: Arc, +} - for (child_id, child) in &process.child_processes { - let Some(mut path) = Self::active_process_path_by_kernel_pid(child, kernel_pid) else { - continue; - }; - path.insert(0, child_id.clone()); - return Some(path); - } - - None +pub(crate) fn send_tool_process_event( + cancelled: &AtomicBool, + pending_events: &Arc>>, + event_overflow_reason: &Mutex>, + pending_event_bytes: &AtomicUsize, + pending_event_count_limit: &AtomicUsize, + pending_event_bytes_limit: &AtomicUsize, + vm_pending_event_bytes_budget: &VmPendingByteBudget, + event: ActiveExecutionEvent, +) -> bool { + let mut pending_events = pending_events + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + if cancelled.load(Ordering::Acquire) { + return false; } - - fn descendant_parent_process<'a>( - vm: &'a VmState, - process_id: &str, - child_path: &[&str], - ) -> Option<&'a ActiveProcess> { - let root = vm.active_processes.get(process_id)?; - Self::active_process_by_path(root, child_path) + let count_limit = pending_event_count_limit + .load(Ordering::Acquire) + .min(MAX_PROCESS_EVENT_QUEUE); + let event_bytes = event.retained_bytes(); + let bytes = pending_event_bytes.load(Ordering::Acquire); + if pending_events.len() >= count_limit { + let mut reason = event_overflow_reason + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + reason.get_or_insert_with(|| { + format!( + "process execution event queue exceeded {count_limit} events \ + (limits.process.pendingEventCount); raise limits.process.pendingEventCount" + ) + }); + return false; } - - fn descendant_parent_process_mut<'a>( - vm: &'a mut VmState, - process_id: &str, - child_path: &[&str], - ) -> Option<&'a mut ActiveProcess> { - let root = vm.active_processes.get_mut(process_id)?; - Self::active_process_by_path_mut(root, child_path) + let byte_limit = pending_event_bytes_limit.load(Ordering::Acquire); + if bytes.saturating_add(event_bytes) > byte_limit { + let mut reason = event_overflow_reason + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + reason.get_or_insert_with(|| { + format!( + "process execution event queue exceeded {byte_limit} bytes \ + (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes" + ) + }); + return false; } - - fn child_process_path_label(process_id: &str, child_path: &[&str]) -> String { - if child_path.is_empty() { - process_id.to_owned() - } else { - format!("{process_id}/{}", child_path.join("/")) - } + if !vm_pending_event_bytes_budget.try_reserve(event_bytes) { + let mut reason = event_overflow_reason + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); + reason.get_or_insert_with(|| { + format!( + "VM process execution event queues exceeded {} bytes \ + (limits.process.pendingEventBytes); raise limits.process.pendingEventBytes", + vm_pending_event_bytes_budget.limit() + ) + }); + return false; } + pending_events.push_back(event); + pending_event_bytes.fetch_add(event_bytes, Ordering::AcqRel); + true +} - fn adopt_detached_child_processes( - current_process_id: &str, - process: &mut ActiveProcess, - ) -> Vec<(String, ActiveProcess)> { - let mut adopted = Vec::new(); - let child_ids = process.child_processes.keys().cloned().collect::>(); - for child_id in child_ids { - let child_process_id = format!("{current_process_id}/{child_id}"); - let Some(mut child) = process.child_processes.remove(&child_id) else { - continue; - }; - if child.detached { - adopted.push((child_process_id, child)); - continue; +fn spawn_tool_process_events(request: ToolProcessEventRequest) { + let ToolProcessEventRequest { + sidecar_requests, + connection_id, + session_id, + vm_id, + tool_resolution, + cancelled, + pending_events, + event_overflow_reason, + pending_event_bytes, + pending_event_count_limit, + pending_event_bytes_limit, + vm_pending_event_bytes_budget, + } = request; + std::thread::spawn(move || match tool_resolution { + ToolCommandResolution::Failure(message) => { + if !send_tool_process_event( + &cancelled, + &pending_events, + &event_overflow_reason, + &pending_event_bytes, + &pending_event_count_limit, + &pending_event_bytes_limit, + &vm_pending_event_bytes_budget, + ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)), + ) { + return; } - - adopted.extend(Self::adopt_detached_child_processes( - &child_process_id, - &mut child, - )); - process.child_processes.insert(child_id, child); - } - adopted - } - - fn child_process_signal_key<'a>(process_id: &'a str, child_path: &[&'a str]) -> &'a str { - child_path.last().copied().unwrap_or(process_id) - } - - fn resolve_detached_child_process_path( - vm: &VmState, - detached_process_id: &str, - ) -> Option<(String, Vec)> { - let root_process_id = vm - .active_processes - .keys() - .filter(|candidate| { - detached_process_id == candidate.as_str() - || detached_process_id - .strip_prefix(candidate.as_str()) - .is_some_and(|remainder| remainder.starts_with('/')) - }) - .max_by_key(|candidate| candidate.len())? - .clone(); - - let remainder = detached_process_id - .strip_prefix(root_process_id.as_str()) - .unwrap_or_default(); - if remainder.is_empty() { - return Some((root_process_id, Vec::new())); + let _ = send_tool_process_event( + &cancelled, + &pending_events, + &event_overflow_reason, + &pending_event_bytes, + &pending_event_count_limit, + &pending_event_bytes_limit, + &vm_pending_event_bytes_budget, + ActiveExecutionEvent::Exited(1), + ); } + ToolCommandResolution::Invoke { request, timeout } => { + let response = sidecar_requests.invoke( + OwnershipScope::vm(connection_id.clone(), session_id.clone(), vm_id.clone()), + SidecarRequestPayload::HostCallback(request.clone()), + timeout, + ); + if cancelled.load(Ordering::Relaxed) { + return; + } - Some(( - root_process_id, - remainder - .trim_start_matches('/') - .split('/') - .map(str::to_owned) - .collect(), - )) - } - - fn pump_detached_child_process_events(&mut self, vm_id: &str) -> Result { - let detached_process_ids = self - .vms - .get(vm_id) - .map(|vm| { - vm.detached_child_processes - .iter() - .cloned() - .collect::>() - }) - .unwrap_or_default(); - let mut emitted_any = false; - for detached_process_id in detached_process_ids { - let Some((root_process_id, child_path)) = self - .vms - .get(vm_id) - .and_then(|vm| Self::resolve_detached_child_process_path(vm, &detached_process_id)) - else { - if let Some(vm) = self.vms.get_mut(vm_id) { - vm.detached_child_processes.remove(&detached_process_id); - } - continue; - }; - if child_path.is_empty() { - loop { - enum ProcessPollResult { - Event(Box>), - RecoverClosedChannel, - } - let poll_result = { - let Some(vm) = self.vms.get_mut(vm_id) else { - break; - }; - let Some(process) = vm.active_processes.get_mut(&root_process_id) else { - break; - }; - if let Some(event) = process.pending_execution_events.pop_front() { - ProcessPollResult::Event(Box::new(Some(event))) - } else { - match process.execution.poll_event_blocking(Duration::ZERO) { - Ok(event) => ProcessPollResult::Event(Box::new(event)), - Err(SidecarError::Execution(message)) - if (process.runtime == GuestRuntimeKind::JavaScript - && closed_javascript_event_channel(&message)) - || (process.runtime == GuestRuntimeKind::Python - && closed_python_event_channel(&message)) - || (process.runtime == GuestRuntimeKind::WebAssembly - && closed_wasm_event_channel(&message)) => - { - ProcessPollResult::RecoverClosedChannel - } - Err(error) => return Err(error), - } + match response { + Ok(crate::protocol::SidecarResponsePayload::HostCallbackResult(result)) => { + if let Some(value) = result.result { + let value: serde_json::Value = serde_json::from_str(&value) + .unwrap_or(serde_json::Value::String(value)); + let stdout = serde_json::to_vec(&json!({ + "ok": true, + "result": value, + })) + .unwrap_or_else(|error| { + format_tool_failure_output(&format!( + "failed to serialize tool result: {error}" + )) + }); + if !send_tool_process_event( + &cancelled, + &pending_events, + &event_overflow_reason, + &pending_event_bytes, + &pending_event_count_limit, + &pending_event_bytes_limit, + &vm_pending_event_bytes_budget, + ActiveExecutionEvent::Stdout(stdout), + ) { + return; } - }; - let event = match poll_result { - ProcessPollResult::Event(event) => *event, - ProcessPollResult::RecoverClosedChannel => { - self.recover_closed_root_runtime_process_event(vm_id, &root_process_id)? + let _ = send_tool_process_event( + &cancelled, + &pending_events, + &event_overflow_reason, + &pending_event_bytes, + &pending_event_count_limit, + &pending_event_bytes_limit, + &vm_pending_event_bytes_budget, + ActiveExecutionEvent::Exited(0), + ); + } else { + let message = result + .error + .unwrap_or_else(|| String::from("tool invocation returned no result")); + if !send_tool_process_event( + &cancelled, + &pending_events, + &event_overflow_reason, + &pending_event_bytes, + &pending_event_count_limit, + &pending_event_bytes_limit, + &vm_pending_event_bytes_budget, + ActiveExecutionEvent::Stderr(format_tool_failure_output(&message)), + ) { + return; } - }; - let Some(event) = event else { - break; - }; - if matches!(&event, ActiveExecutionEvent::Exited(_)) { - record_execute_response_to_exit_milestone( - "execute_response_to_detached_exit_event_polled", - vm_id, - &detached_process_id, + let _ = send_tool_process_event( + &cancelled, + &pending_events, + &event_overflow_reason, + &pending_event_bytes, + &pending_event_count_limit, + &pending_event_bytes_limit, + &vm_pending_event_bytes_budget, + ActiveExecutionEvent::Exited(1), ); } - let Some((connection_id, session_id)) = self - .vms - .get(vm_id) - .map(|vm| (vm.connection_id.clone(), vm.session_id.clone())) - else { - break; - }; - match event { - ActiveExecutionEvent::Stdout(chunk) => { - self.queue_pending_process_event(ProcessEventEnvelope { - connection_id, - session_id, - vm_id: vm_id.to_owned(), - process_id: detached_process_id.clone(), - event: ActiveExecutionEvent::Stdout(chunk), - })?; - emitted_any = true; - } - ActiveExecutionEvent::Stderr(chunk) => { - self.queue_pending_process_event(ProcessEventEnvelope { - connection_id, - session_id, - vm_id: vm_id.to_owned(), - process_id: detached_process_id.clone(), - event: ActiveExecutionEvent::Stderr(chunk), - })?; - emitted_any = true; - } - ActiveExecutionEvent::Exited(exit_code) => { - if let Some(vm) = self.vms.get_mut(vm_id) { - vm.detached_child_processes.remove(&detached_process_id); - } - self.queue_pending_process_event(ProcessEventEnvelope { - connection_id, - session_id, - vm_id: vm_id.to_owned(), - process_id: detached_process_id.clone(), - event: ActiveExecutionEvent::Exited(exit_code), - })?; - emitted_any = true; - break; - } - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => { - self.handle_javascript_sync_rpc_request( - vm_id, - &root_process_id, - request, - )?; - } - ActiveExecutionEvent::PythonVfsRpcRequest(request) => { - self.handle_python_vfs_rpc_request(vm_id, &root_process_id, *request)?; - } - ActiveExecutionEvent::SignalState { - signal, - registration, - } => { - if let Some(vm) = self.vms.get_mut(vm_id) { - vm.signal_states - .entry(root_process_id.clone()) - .or_default() - .insert(signal, registration); - } - } - } } - continue; - } - - let parent_path = child_path[..child_path.len() - 1] - .iter() - .map(String::as_str) - .collect::>(); - let child_process_id = child_path.last().expect("child path cannot be empty"); - - loop { - let event = match self.poll_descendant_javascript_child_process( - vm_id, - &root_process_id, - &parent_path, - child_process_id, - 0, - ) { - Ok(event) => event, - Err(SidecarError::InvalidState(message)) - if message.contains("unknown child process") - || message.contains("unknown child process path") => - { - if let Some(vm) = self.vms.get_mut(vm_id) { - vm.detached_child_processes.remove(&detached_process_id); - } - break; - } - Err(error) if is_javascript_child_process_gone_error(&error) => { - if let Some(vm) = self.vms.get_mut(vm_id) { - vm.detached_child_processes.remove(&detached_process_id); - } - break; + Ok(_) => { + if !send_tool_process_event( + &cancelled, + &pending_events, + &event_overflow_reason, + &pending_event_bytes, + &pending_event_count_limit, + &pending_event_bytes_limit, + &vm_pending_event_bytes_budget, + ActiveExecutionEvent::Stderr(format_tool_failure_output( + "unexpected sidecar tool response", + )), + ) { + return; } - Err(error) => return Err(error), - }; - - let Some(event_type) = event.get("type").and_then(Value::as_str) else { - break; - }; - let Some((connection_id, session_id)) = self - .vms - .get(vm_id) - .map(|vm| (vm.connection_id.clone(), vm.session_id.clone())) - else { - break; - }; - - let envelope = match event_type { - "stdout" => Some(ProcessEventEnvelope { - connection_id: connection_id.clone(), - session_id: session_id.clone(), - vm_id: vm_id.to_owned(), - process_id: detached_process_id.clone(), - event: ActiveExecutionEvent::Stdout(javascript_sync_rpc_bytes_arg( - &[event.get("data").cloned().unwrap_or(Value::Null)], - 0, - "detached child_process stdout", - )?), - }), - "stderr" => Some(ProcessEventEnvelope { - connection_id: connection_id.clone(), - session_id: session_id.clone(), - vm_id: vm_id.to_owned(), - process_id: detached_process_id.clone(), - event: ActiveExecutionEvent::Stderr(javascript_sync_rpc_bytes_arg( - &[event.get("data").cloned().unwrap_or(Value::Null)], - 0, - "detached child_process stderr", - )?), - }), - "exit" => { - if let Some(vm) = self.vms.get_mut(vm_id) { - vm.detached_child_processes.remove(&detached_process_id); - } - Some(ProcessEventEnvelope { - connection_id, - session_id, - vm_id: vm_id.to_owned(), - process_id: detached_process_id.clone(), - event: ActiveExecutionEvent::Exited( - event - .get("exitCode") - .and_then(Value::as_i64) - .map(|value| value as i32) - .unwrap_or(1), - ), - }) + let _ = send_tool_process_event( + &cancelled, + &pending_events, + &event_overflow_reason, + &pending_event_bytes, + &pending_event_count_limit, + &pending_event_bytes_limit, + &vm_pending_event_bytes_budget, + ActiveExecutionEvent::Exited(1), + ); + } + Err(error) => { + if !send_tool_process_event( + &cancelled, + &pending_events, + &event_overflow_reason, + &pending_event_bytes, + &pending_event_count_limit, + &pending_event_bytes_limit, + &vm_pending_event_bytes_budget, + ActiveExecutionEvent::Stderr(format_tool_failure_output( + &error.to_string(), + )), + ) { + return; } - _ => None, - }; - - let Some(envelope) = envelope else { - break; - }; - self.queue_pending_process_event(envelope)?; - emitted_any = true; - - if event_type == "exit" { - break; + let _ = send_tool_process_event( + &cancelled, + &pending_events, + &event_overflow_reason, + &pending_event_bytes, + &pending_event_count_limit, + &pending_event_bytes_limit, + &vm_pending_event_bytes_budget, + ActiveExecutionEvent::Exited(1), + ); } } } + }); +} - Ok(emitted_any) - } - pub(crate) fn drain_queued_descendant_javascript_child_process_events( +impl NativeSidecar +where + B: NativeSidecarBridge + Send + 'static, + BridgeError: fmt::Debug + Send + Sync + 'static, +{ + pub(crate) async fn execute( &mut self, - vm_id: &str, - process_id: &str, - child_path: &[&str], - ) -> Result<(), SidecarError> { - if child_path.is_empty() { - return Ok(()); - } - let target_process_id = Self::child_process_path_label(process_id, child_path); - let mut child_capacity = self - .vms - .get(vm_id) - .and_then(|vm| vm.active_processes.get(process_id)) - .and_then(|root| descendant_pending_execution_event_capacity(root, child_path)); - - let mut deferred = VecDeque::new(); - while let Some(envelope) = self.pending_process_events.pop_front() { - if envelope.vm_id == vm_id && envelope.process_id == target_process_id { - if matches!(child_capacity, Some(0)) { - self.pending_process_events.push_front(envelope); - while let Some(deferred_envelope) = deferred.pop_back() { - self.pending_process_events.push_front(deferred_envelope); - } - return Err(process_event_queue_overflow_error()); - } - if let Some(vm) = self.vms.get_mut(vm_id) { - if let Some(root) = vm.active_processes.get_mut(process_id) { - if let Some(child) = Self::active_process_by_path_mut(root, child_path) { - child.queue_pending_execution_event(envelope.event)?; - child_capacity = child_capacity.map(|capacity| capacity - 1); - continue; - } - } - } - } - deferred.push_back(envelope); - } - self.pending_process_events = deferred; + request: &RequestFrame, + payload: ExecuteRequest, + ) -> Result { + let execute_total_start = Instant::now(); + let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; + self.require_owned_vm(&connection_id, &session_id, &vm_id)?; - let mut queued = Vec::new(); - { - let transfer_capacity = self - .pending_process_event_capacity() - .min(child_capacity.unwrap_or(usize::MAX)); - let receiver = self.process_event_receiver.as_mut().ok_or_else(|| { - SidecarError::InvalidState(String::from("process event receiver unavailable")) - })?; - loop { - if queued.len() >= transfer_capacity { - if receiver.is_empty() { - break; - } - return Err(process_event_queue_overflow_error()); - } - match receiver.try_recv() { - Ok(envelope) => queued.push(envelope), - Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break, - Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break, - } - } - } - for envelope in queued { - if envelope.vm_id == vm_id && envelope.process_id == target_process_id { - if let Some(vm) = self.vms.get_mut(vm_id) { - if let Some(root) = vm.active_processes.get_mut(process_id) { - if let Some(child) = Self::active_process_by_path_mut(root, child_path) { - child.queue_pending_execution_event(envelope.event)?; - continue; - } - } - } - } - self.queue_pending_process_event(envelope)?; + let vm = self + .vms + .get_mut(&vm_id) + .ok_or_else(|| missing_vm_error(&vm_id))?; + let process_event_limits = vm.limits.process.clone(); + let vm_pending_stdin_bytes_budget = Arc::clone(&vm.pending_stdin_bytes_budget); + let vm_pending_event_bytes_budget = Arc::clone(&vm.pending_event_bytes_budget); + if vm.active_processes.contains_key(&payload.process_id) { + return Err(SidecarError::InvalidState(format!( + "VM {vm_id} already has an active process with id {}", + payload.process_id + ))); } - Ok(()) - } - - pub(crate) fn handle_execution_event( - &mut self, - vm_id: &str, - process_id: &str, - event: ActiveExecutionEvent, - ) -> Result, SidecarError> { - let Some(vm) = self.vms.get(vm_id) else { - log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch"); - return Ok(None); - }; - if !vm.active_processes.contains_key(process_id) { - log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch"); - return Ok(None); - } - let (connection_id, session_id) = { (vm.connection_id.clone(), vm.session_id.clone()) }; - let ownership = OwnershipScope::vm(&connection_id, &session_id, vm_id); - - if self.capture_extension_process_output_event(vm_id, process_id, &event) { - return Ok(None); - } - - match event { - ActiveExecutionEvent::Stdout(chunk) => Ok(Some(EventFrame::new( - ownership, - EventPayload::ProcessOutput(ProcessOutputEvent { - process_id: process_id.to_owned(), - channel: StreamChannel::Stdout, - chunk, - }), - ))), - ActiveExecutionEvent::Stderr(chunk) => Ok(Some(EventFrame::new( - ownership, - EventPayload::ProcessOutput(ProcessOutputEvent { - process_id: process_id.to_owned(), - channel: StreamChannel::Stderr, - chunk, - }), - ))), - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => { - self.handle_javascript_sync_rpc_request(vm_id, process_id, request)?; - Ok(None) - } - ActiveExecutionEvent::PythonVfsRpcRequest(request) => { - self.handle_python_vfs_rpc_request(vm_id, process_id, *request)?; - Ok(None) - } - ActiveExecutionEvent::SignalState { - signal, - registration, - } => { - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(None); - }; - if !vm.active_processes.contains_key(process_id) { - return Ok(None); - } - vm.signal_states - .entry(process_id.to_owned()) - .or_default() - .insert(signal, registration); - Ok(None) - } - ActiveExecutionEvent::Exited(exit_code) => { - record_execute_response_to_exit_milestone( - "execute_response_to_exit_event_handle", - vm_id, - process_id, + if let Some(command) = payload.command.as_deref() { + if let Some(tool_resolution) = + resolve_tool_command(vm, command, &payload.args, payload.cwd.as_deref())? + { + let guest_cwd = payload + .cwd + .as_deref() + .map(normalize_path) + .unwrap_or_else(|| vm.guest_cwd.clone()); + let kernel_handle = vm + .kernel + .create_virtual_process( + EXECUTION_DRIVER_NAME, + TOOL_DRIVER_NAME, + command, + std::iter::once(command.to_owned()) + .chain(payload.args.iter().cloned()) + .collect(), + VirtualProcessOptions { + env: vm.guest_env.clone(), + cwd: Some(guest_cwd.clone()), + ..VirtualProcessOptions::default() + }, + ) + .map_err(kernel_error)?; + let kernel_pid = kernel_handle.pid(); + let tool_execution = ToolExecution::default() + .with_vm_pending_event_bytes_budget(Arc::clone(&vm_pending_event_bytes_budget)); + let cancelled = tool_execution.cancelled.clone(); + let pending_events = tool_execution.pending_events.clone(); + let event_overflow_reason = tool_execution.event_overflow_reason.clone(); + let pending_event_bytes = tool_execution.pending_event_bytes.clone(); + let pending_event_count_limit = tool_execution.pending_event_count_limit.clone(); + let pending_event_bytes_limit = tool_execution.pending_event_bytes_limit.clone(); + vm.active_processes.insert( + payload.process_id.clone(), + ActiveProcess::new( + kernel_pid, + kernel_handle, + GuestRuntimeKind::JavaScript, + ActiveExecution::Tool(tool_execution), + ) + .with_process_event_limits(&process_event_limits) + .with_vm_pending_byte_budgets( + Arc::clone(&vm_pending_stdin_bytes_budget), + Arc::clone(&vm_pending_event_bytes_budget), + ) + .with_guest_cwd(guest_cwd.clone()) + .with_host_cwd(resolve_vm_guest_path_to_host(vm, &guest_cwd)), ); - record_execute_response_to_exit(vm_id, process_id); - let phase_start = Instant::now(); - let became_idle = self - .finish_active_process_exit(vm_id, process_id, exit_code)? - .unwrap_or(false); - record_execute_phase("process_exit_cleanup", phase_start.elapsed()); - - let phase_start = Instant::now(); - if became_idle { - self.bridge.emit_lifecycle(vm_id, LifecycleState::Ready)?; - } - record_execute_phase("process_exit_lifecycle_emit", phase_start.elapsed()); - - Ok(Some(EventFrame::new( - ownership, - EventPayload::ProcessExited(ProcessExitedEvent { - process_id: process_id.to_owned(), - exit_code, - }), - ))) - } - } - } - - pub(crate) fn finish_active_process_exit( - &mut self, - vm_id: &str, - process_id: &str, - exit_code: i32, - ) -> Result, SidecarError> { - let Some(vm) = self.vms.get_mut(vm_id) else { - log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup"); - return Ok(None); - }; - if !vm.active_processes.contains_key(process_id) { - log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup"); - return Ok(None); - } + self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?; + spawn_tool_process_events(ToolProcessEventRequest { + sidecar_requests: self.sidecar_requests.clone(), + connection_id: connection_id.clone(), + session_id: session_id.clone(), + vm_id: vm_id.clone(), + tool_resolution, + cancelled, + pending_events, + event_overflow_reason, + pending_event_bytes, + pending_event_count_limit, + pending_event_bytes_limit, + vm_pending_event_bytes_budget: Arc::clone(&vm_pending_event_bytes_budget), + }); - let phase_start = Instant::now(); - prune_exited_process_snapshots(vm); - record_execute_phase( - "process_exit_cleanup_prune_snapshots", - phase_start.elapsed(), - ); - let phase_start = Instant::now(); - let process_table = vm.kernel.list_processes(); - record_execute_phase("process_exit_cleanup_list_processes", phase_start.elapsed()); - let phase_start = Instant::now(); - let Some(mut process) = vm.active_processes.remove(process_id) else { - return Ok(None); - }; - record_execute_phase("process_exit_cleanup_remove_active", phase_start.elapsed()); - let phase_start = Instant::now(); - if let Some(info) = process_table.get(&process.kernel_pid) { - vm.exited_process_snapshots - .push_back(ExitedProcessSnapshot { - captured_at: Instant::now(), - process: build_process_snapshot_entry( - process_id, - &process, - info, - Some(exit_code), + return Ok(DispatchResult { + response: process_started_response( + request, + payload.process_id, + Some(kernel_pid), ), + events: Vec::new(), }); + } } - record_execute_phase("process_exit_cleanup_build_snapshot", phase_start.elapsed()); - let phase_start = Instant::now(); - let detached_children = Self::adopt_detached_child_processes(process_id, &mut process); - record_execute_phase("process_exit_cleanup_adopt_detached", phase_start.elapsed()); - let phase_start = Instant::now(); - let should_sync_host_writes = process.host_write_dirty_recursive() - || !process.clean_host_writes_are_observable_recursive(); - if should_sync_host_writes { - sync_process_host_writes_to_kernel(vm, &process)?; - } else { - record_execute_phase( - "process_exit_cleanup_sync_host_writes_clean_skip", - Duration::ZERO, - ); - } - record_execute_phase( - "process_exit_cleanup_sync_host_writes", - phase_start.elapsed(), - ); - release_inherited_child_raw_mode(&mut vm.kernel, &process)?; - let phase_start = Instant::now(); - let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); - terminate_child_process_tree(&mut vm.kernel, &mut process, &kernel_readiness); - record_execute_phase( - "process_exit_cleanup_terminate_child_tree", - phase_start.elapsed(), - ); - let phase_start = Instant::now(); - process.kernel_handle.finish(exit_code); - record_execute_phase("process_exit_cleanup_kernel_finish", phase_start.elapsed()); + + let requested_tty = payload + .env + .get(EXECUTION_REQUEST_TTY_ENV) + .is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true")); let phase_start = Instant::now(); - let _ = vm.kernel.wait_and_reap(process.kernel_pid); - record_execute_phase("process_exit_cleanup_wait_and_reap", phase_start.elapsed()); + let mut resolved = resolve_execute_request(vm, &payload)?; + stage_agentos_package_command(vm, &mut resolved)?; + let resolved = resolved; + record_execute_phase("resolve_execute_request", phase_start.elapsed()); let phase_start = Instant::now(); - vm.signal_states.remove(process_id); - record_execute_phase( - "process_exit_cleanup_signal_state_remove", - phase_start.elapsed(), + let mut env = resolved.env.clone(); + env.remove(EXECUTION_REQUEST_TTY_ENV); + let sandbox_root = normalize_host_path(&vm.cwd); + env.insert( + String::from(EXECUTION_SANDBOX_ROOT_ENV), + sandbox_root.to_string_lossy().into_owned(), ); - let phase_start = Instant::now(); - for (detached_process_id, detached_child) in detached_children { - vm.detached_child_processes - .insert(detached_process_id.clone()); - vm.active_processes - .insert(detached_process_id, detached_child); + if resolved.runtime == GuestRuntimeKind::JavaScript { + env.insert(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1")); + // A TTY guest-node process reads stdin through the kernel PTY: host + // input is written to the PTY master (write_kernel_process_stdin), + // line discipline runs (echo / VERASE / ICRNL / VEOF), and the + // sidecar drains the cooked bytes from the slave and forwards them + // to the isolate's stream-stdin dispatch + // (forward_tty_slave_input_to_javascript). The in-isolate + // `_kernelStdinRead` bridge stays local; no RPC forwarding is + // needed because the isolate never reads kernel fd 0 itself. + } else if resolved.runtime == GuestRuntimeKind::WebAssembly { + env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1")); + env.insert(String::from(WASM_EXEC_COMMIT_RPC_ENV), String::from("1")); } - record_execute_phase( - "process_exit_cleanup_reinsert_detached", - phase_start.elapsed(), - ); + let launch_entrypoint = if resolved.runtime == GuestRuntimeKind::JavaScript { + resolve_agentos_package_javascript_launch_entrypoint(vm, &mut env) + .unwrap_or_else(|| resolved.entrypoint.clone()) + } else { + resolved.entrypoint.clone() + }; + let argv = std::iter::once(launch_entrypoint.clone()) + .chain(resolved.execution_args.iter().cloned()) + .collect::>(); + record_execute_phase("env_argv_setup", phase_start.elapsed()); let phase_start = Instant::now(); - let became_idle = vm.active_processes.is_empty(); - record_execute_phase("process_exit_cleanup_became_idle", phase_start.elapsed()); - let phase_start = Instant::now(); - self.prune_extension_process_resource(process_id); - record_execute_phase("process_exit_cleanup_prune_resource", phase_start.elapsed()); - - Ok(Some(became_idle)) - } + let kernel_handle = vm + .kernel + .spawn_process( + &resolved.command, + argv, + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + cwd: Some(resolved.guest_cwd.clone()), + ..SpawnOptions::default() + }, + ) + .map_err(kernel_error)?; + let kernel_pid = kernel_handle.pid(); + record_execute_phase("kernel_spawn_process", phase_start.elapsed()); + let tty_master_fd = if requested_tty { + let (master_fd, slave_fd, _) = vm + .kernel + .open_pty(EXECUTION_DRIVER_NAME, kernel_pid) + .map_err(kernel_error)?; + vm.kernel + .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 0) + .map_err(kernel_error)?; + vm.kernel + .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 1) + .map_err(kernel_error)?; + vm.kernel + .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, slave_fd, 2) + .map_err(kernel_error)?; + vm.kernel + .pty_set_foreground_pgid(EXECUTION_DRIVER_NAME, kernel_pid, master_fd, kernel_pid) + .map_err(kernel_error)?; + if let Some((cols, rows)) = requested_pty_window_size(&env) { + vm.kernel + .pty_resize(EXECUTION_DRIVER_NAME, kernel_pid, master_fd, cols, rows) + .map_err(kernel_error)?; + } + Some(master_fd) + } else { + None + }; - pub(crate) fn drain_process_events_blocking_with_limit( - &mut self, - vm_id: &str, - process_id: &str, - max_events: usize, - ) -> Result, SidecarError> { - let mut events = Vec::new(); - if max_events == 0 { - return Ok(events); - } - let mut deadline = Instant::now() + PROCESS_EXIT_DRAIN_INITIAL_QUIET; + let (execution, process_env) = match resolved.runtime { + GuestRuntimeKind::JavaScript => { + let phase_start = Instant::now(); + let inline_code = load_javascript_entrypoint_source( + vm, + &resolved.host_cwd, + &launch_entrypoint, + &env, + ); + record_execute_phase("js_load_entrypoint_source", phase_start.elapsed()); + let phase_start = Instant::now(); + prepare_javascript_shadow(vm, &resolved, &env)?; + record_execute_phase("js_prepare_shadow", phase_start.elapsed()); - loop { - if events.len() >= max_events { - break; + let phase_start = Instant::now(); + let context = + self.javascript_engine + .create_context(CreateJavascriptContextRequest { + vm_id: vm_id.clone(), + bootstrap_module: None, + compile_cache_root: Some(self.cache_root.join("node-compile-cache")), + }); + let context_id = context.context_id; + record_execute_phase("js_create_context", phase_start.elapsed()); + let phase_start = Instant::now(); + let built_reader = build_module_reader(vm, &resolved); + let guest_reader = built_reader.clone().map(|reader| { + Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader)) + as Box + }); + let module_reader = + built_reader.map(|reader| Box::new(reader) as Box); + record_execute_phase("js_build_module_reader", phase_start.elapsed()); + let phase_start = Instant::now(); + let execution_result = self.javascript_engine.start_execution_with_module_reader( + StartJavascriptExecutionRequest { + guest_runtime: guest_runtime_identity(vm, None, None), + vm_id: vm_id.clone(), + context_id: context_id.clone(), + argv: std::iter::once(launch_entrypoint.clone()) + .chain(resolved.execution_args.iter().cloned()) + .collect(), + argv0: None, + env: env.clone(), + cwd: resolved.host_cwd.clone(), + limits: javascript_execution_limits(vm), + inline_code, + wasm_module_bytes: None, + }, + module_reader, + guest_reader, + ); + self.javascript_engine.dispose_context(&context_id); + let execution = execution_result.map_err(javascript_error)?; + record_execute_phase("js_start_execution", phase_start.elapsed()); + (ActiveExecution::Javascript(execution), env.clone()) } - let event = { - let Some(vm) = self.vms.get_mut(vm_id) else { - break; - }; - let Some(process) = vm.active_processes.get_mut(process_id) else { - break; - }; - if let Some(event) = process.pending_execution_events.pop_front() { - Some(event) + GuestRuntimeKind::Python => { + // The `python` command path (marked by AGENTOS_PYTHON_ARGV) is + // explicit about file mode via AGENTOS_PYTHON_FILE, so a `-c` code + // string that happens to end in `.py` is never mistaken for a path. + // The low-level execute API keeps the `.py`-suffix heuristic. + let python_file_path = if resolved.env.contains_key("AGENTOS_PYTHON_ARGV") { + resolved.env.get("AGENTOS_PYTHON_FILE").map(PathBuf::from) } else { - match process.execution.poll_event_blocking(Duration::ZERO) { - Ok(event) => event, - Err(SidecarError::Execution(_)) => None, - Err(other) => return Err(other), - } - } - }; - - let Some(event) = event else { - if Instant::now() >= deadline { - break; - } - let blocking_wait = deadline.saturating_duration_since(Instant::now()); - if blocking_wait.is_zero() { - break; - } - if events.len() >= max_events { - break; - } - let delayed_event = { - let Some(vm) = self.vms.get_mut(vm_id) else { - break; - }; - let Some(process) = vm.active_processes.get_mut(process_id) else { - break; - }; - if let Some(event) = process.pending_execution_events.pop_front() { - Some(event) - } else { - match process.execution.poll_event_blocking(blocking_wait) { - Ok(event) => event, - Err(SidecarError::Execution(_)) => None, - Err(other) => return Err(other), - } - } - }; - let Some(event) = delayed_event else { - break; + python_file_entrypoint(&resolved.entrypoint) }; - events.push(event); - deadline = Instant::now() + PROCESS_EXIT_DRAIN_TRAILING_QUIET; - continue; - }; - events.push(event); - deadline = Instant::now() + PROCESS_EXIT_DRAIN_TRAILING_QUIET; - } + let pyodide_dist_path = self + .python_engine + .bundled_pyodide_dist_path_for_vm(&vm_id) + .map_err(python_error)?; + let pyodide_cache_path = pyodide_dist_path + .parent() + .and_then(Path::parent) + .unwrap_or(pyodide_dist_path.as_path()) + .join("pyodide-package-cache"); + add_runtime_guest_path_mapping( + &mut env, + PYTHON_PYODIDE_GUEST_ROOT, + &pyodide_dist_path, + ); + add_runtime_guest_path_mapping( + &mut env, + PYTHON_PYODIDE_CACHE_GUEST_ROOT, + &pyodide_cache_path, + ); + add_runtime_host_access_path( + &mut env, + "AGENTOS_EXTRA_FS_READ_PATHS", + &pyodide_dist_path, + true, + ); + add_runtime_host_access_path( + &mut env, + "AGENTOS_EXTRA_FS_READ_PATHS", + &pyodide_cache_path, + true, + ); + add_runtime_host_access_path( + &mut env, + "AGENTOS_EXTRA_FS_WRITE_PATHS", + &pyodide_cache_path, + false, + ); + let context = self + .python_engine + .create_context(CreatePythonContextRequest { + vm_id: vm_id.clone(), + pyodide_dist_path, + }); + let context_id = context.context_id; + let execution_result = + self.python_engine + .start_execution(StartPythonExecutionRequest { + vm_id: vm_id.clone(), + context_id: context_id.clone(), + code: resolved.entrypoint.clone(), + file_path: python_file_path, + env: env.clone(), + cwd: resolved.host_cwd.clone(), + limits: python_execution_limits(vm), + guest_runtime: guest_runtime_identity(vm, None, None), + }); + self.python_engine.dispose_context(&context_id); + let execution = execution_result.map_err(python_error)?; + (ActiveExecution::Python(execution), env.clone()) + } + GuestRuntimeKind::WebAssembly => { + let wasm_limits = wasm_execution_limits(vm); + let wasm_guest_runtime = + guest_runtime_identity(vm, Some(u64::from(kernel_pid)), Some(0)); + let wasm_permission_tier = resolved.wasm_permission_tier.unwrap_or_else(|| { + resolve_wasm_permission_tier( + vm, + Some(&resolved.command), + None, + &resolved.entrypoint, + ) + }); + let context = self.wasm_engine.create_context(CreateWasmContextRequest { + vm_id: vm_id.clone(), + module_path: Some(resolved.entrypoint.clone()), + }); + let context_id = context.context_id; + let execution_result = + self.wasm_engine.start_execution(StartWasmExecutionRequest { + vm_id: vm_id.clone(), + context_id: context_id.clone(), + argv: resolved.process_args.clone(), + env: env.clone(), + cwd: resolved.host_cwd.clone(), + permission_tier: execution_wasm_permission_tier(wasm_permission_tier), + limits: wasm_limits, + guest_runtime: wasm_guest_runtime, + }); + self.wasm_engine.dispose_context(&context_id); + let execution = execution_result.map_err(wasm_error)?; + (ActiveExecution::Wasm(Box::new(execution)), env) + } + }; + let child_pid = execution.child_pid(); + let phase_start = Instant::now(); + let kernel_stdin_writer_fd = if let Some(master_fd) = tty_master_fd { + master_fd + } else { + install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)? + }; + vm.active_processes.insert( + payload.process_id.clone(), + ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution) + .with_process_event_limits(&process_event_limits) + .with_vm_pending_byte_budgets( + vm_pending_stdin_bytes_budget, + vm_pending_event_bytes_budget, + ) + .with_kernel_stdin_writer_fd(kernel_stdin_writer_fd) + .with_tty_master_fd(tty_master_fd) + .with_guest_cwd(resolved.guest_cwd.clone()) + .with_env(process_env) + .with_host_cwd(resolved.host_cwd.clone()), + ); + self.bridge.emit_lifecycle(&vm_id, LifecycleState::Busy)?; + mark_execute_response_ready(&vm_id, &payload.process_id); + record_execute_phase("process_register_and_lifecycle", phase_start.elapsed()); + record_execute_phase("execute_total", execute_total_start.elapsed()); - Ok(events) + Ok(DispatchResult { + response: process_started_response( + request, + payload.process_id, + Some(if child_pid == 0 { + kernel_pid + } else { + child_pid + }), + ), + events: Vec::new(), + }) } - pub(crate) fn handle_python_vfs_rpc_request( + pub(crate) async fn resize_pty( &mut self, - vm_id: &str, - process_id: &str, - request: PythonVfsRpcRequest, - ) -> Result<(), SidecarError> { - match request.method { - PythonVfsRpcMethod::Read - | PythonVfsRpcMethod::Write - | PythonVfsRpcMethod::Stat - | PythonVfsRpcMethod::Lstat - | PythonVfsRpcMethod::ReadDir - | PythonVfsRpcMethod::Mkdir - | PythonVfsRpcMethod::Unlink - | PythonVfsRpcMethod::Rmdir - | PythonVfsRpcMethod::Rename - | PythonVfsRpcMethod::Symlink - | PythonVfsRpcMethod::ReadLink - | PythonVfsRpcMethod::Setattr => { - filesystem_handle_python_vfs_rpc_request(self, vm_id, process_id, request) - } - PythonVfsRpcMethod::HttpRequest => { - self.handle_python_http_rpc_request(vm_id, process_id, request) - } - PythonVfsRpcMethod::DnsLookup => { - self.handle_python_dns_rpc_request(vm_id, process_id, request) - } - PythonVfsRpcMethod::SubprocessRun => { - self.handle_python_subprocess_rpc_request(vm_id, process_id, request) - } - PythonVfsRpcMethod::SocketConnect - | PythonVfsRpcMethod::SocketSend - | PythonVfsRpcMethod::SocketRecv - | PythonVfsRpcMethod::SocketClose - | PythonVfsRpcMethod::UdpCreate - | PythonVfsRpcMethod::UdpSendto - | PythonVfsRpcMethod::UdpRecvfrom => { - self.handle_python_socket_rpc_request(vm_id, process_id, request) - } - } - } + request: &RequestFrame, + payload: ResizePtyRequest, + ) -> Result { + let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; + self.require_owned_vm(&connection_id, &session_id, &vm_id)?; - fn handle_python_http_rpc_request( - &mut self, - vm_id: &str, - process_id: &str, - request: PythonVfsRpcRequest, - ) -> Result<(), SidecarError> { - let Some(vm) = self.vms.get(vm_id) else { - return Ok(()); + let vm = self + .vms + .get_mut(&vm_id) + .ok_or_else(|| missing_vm_error(&vm_id))?; + let process = vm + .active_processes + .get(&payload.process_id) + .ok_or_else(|| { + SidecarError::InvalidState(format!( + "VM {vm_id} has no active process {}", + payload.process_id + )) + })?; + let Some(writer_fd) = process.kernel_stdin_writer_fd else { + return Err(SidecarError::InvalidState(format!( + "process {} does not have a PTY", + payload.process_id + ))); }; - if !vm.active_processes.contains_key(process_id) { - return Ok(()); + vm.kernel + .pty_resize( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + writer_fd, + payload.cols, + payload.rows, + ) + .map_err(kernel_error)?; + // The kernel records SIGWINCH for every member of the terminal's + // foreground process group. Embedded V8 executions cannot observe that + // process-table state directly, so mirror the same group delivery into + // every tracked runtime session, including nested Vim/readline children. + let foreground_pgid = vm + .kernel + .tcgetpgrp(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd) + .map_err(kernel_error)?; + let foreground_pids = vm + .kernel + .list_processes() + .into_values() + .filter(|info| info.pgid == foreground_pgid && info.status != ProcessStatus::Exited) + .map(|info| info.pid) + .collect::>(); + for active in vm.active_processes.values() { + dispatch_v8_signal_to_tracked_processes(active, &foreground_pids, libc::SIGWINCH)?; } - let response = (|| { - let url_text = request.url.as_deref().ok_or_else(|| { - SidecarError::InvalidState(String::from("python httpRequest requires a url")) - })?; - let url = Url::parse(url_text) - .map_err(|error| SidecarError::Execution(format!("ERR_INVALID_URL: {error}")))?; - let host = url.host_str().ok_or_else(|| { - SidecarError::Execution(String::from("ERR_INVALID_URL: missing host")) - })?; - let port = url.port_or_known_default().ok_or_else(|| { - SidecarError::Execution(String::from("ERR_INVALID_URL: missing port")) - })?; - self.bridge.require_network_access( - vm_id, - NetworkOperation::Http, - format_tcp_resource(host, port), - )?; - // Pin the outbound connection to the IP addresses that pass the - // egress range guard at resolution time. A literal IP is validated - // directly; a hostname is resolved once here and the resulting - // address set is pinned into the HTTP client's resolver below so a - // rebinding DNS server cannot make the second (TLS/TCP) lookup land - // on a private/link-local/metadata IP that this check rejected. - let pinned_addresses = if let Ok(literal_ip) = host.parse::() { - filter_dns_safe_ip_addrs(vec![literal_ip], host)? - } else { - filter_dns_safe_ip_addrs( - resolve_dns_ip_addrs( - &self.bridge, - &vm.kernel, - vm_id, - &vm.dns, - host, - DnsLookupPolicy::SkipPermissions, - )?, - host, - )? - }; - let mut headers = BTreeMap::new(); - for (name, value) in &request.headers { - headers.insert(name.clone(), Value::String(value.clone())); - } - let options = JavascriptHttpRequestOptions { - method: Some( - request - .http_method - .clone() - .unwrap_or_else(|| String::from("GET")), - ), - headers, - body: request.body_base64.as_deref().map(|body| { - String::from_utf8( - base64::engine::general_purpose::STANDARD - .decode(body) - .unwrap_or_default(), - ) - .unwrap_or_default() + + Ok(DispatchResult { + response: self.respond( + request, + ResponsePayload::PtyResized(PtyResizedResponse { + process_id: payload.process_id, + cols: payload.cols, + rows: payload.rows, }), - reject_unauthorized: None, - }; - let headers = - parse_http_header_collection(&options.headers, "python httpRequest headers")?; - let response = - issue_outbound_http_request(&url, &options, &headers, &pinned_addresses)?; - let payload_json = response.as_str().ok_or_else(|| { - SidecarError::Execution(String::from( - "python httpRequest returned a non-string response payload", + ), + events: Vec::new(), + }) + } + + pub(crate) async fn write_stdin( + &mut self, + request: &RequestFrame, + payload: WriteStdinRequest, + ) -> Result { + let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; + self.require_owned_vm(&connection_id, &session_id, &vm_id)?; + + let vm = self + .vms + .get_mut(&vm_id) + .ok_or_else(|| missing_vm_error(&vm_id))?; + let pending_stdin_limit = vm.limits.process.pending_stdin_bytes; + let process = vm + .active_processes + .get_mut(&payload.process_id) + .ok_or_else(|| { + SidecarError::InvalidState(format!( + "VM {vm_id} has no active process {}", + payload.process_id )) })?; - let payload: Value = serde_json::from_str(payload_json).map_err(|error| { - SidecarError::Execution(format!( - "python httpRequest response must be valid JSON: {error}" + // For a TTY JavaScript process, host stdin must go ONLY to the kernel PTY + // master (so line discipline + echo apply); feeding the in-process local + // stdin bridge as well would double-deliver the input. Non-TTY JS (piped + // stdin) still uses the local bridge; wasm/python always take the + // streaming/no-op `write_stdin` path plus the kernel master write below. + let tty_js = + process.runtime == GuestRuntimeKind::JavaScript && process.tty_master_fd.is_some(); + if !tty_js { + process.execution.write_stdin(&payload.chunk)?; + } + write_kernel_process_stdin(&mut vm.kernel, process, &payload.chunk, pending_stdin_limit)?; + + Ok(DispatchResult { + response: stdin_written_response( + request, + payload.process_id, + payload.chunk.len() as u64, + ), + events: Vec::new(), + }) + } + + pub(crate) async fn close_stdin( + &mut self, + request: &RequestFrame, + payload: CloseStdinRequest, + ) -> Result { + let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; + self.require_owned_vm(&connection_id, &session_id, &vm_id)?; + + let vm = self + .vms + .get_mut(&vm_id) + .ok_or_else(|| missing_vm_error(&vm_id))?; + let process = vm + .active_processes + .get_mut(&payload.process_id) + .ok_or_else(|| { + SidecarError::InvalidState(format!( + "VM {vm_id} has no active process {}", + payload.process_id )) })?; - let header_map = payload - .get("headers") - .and_then(Value::as_array) - .map(|entries| { - let mut normalized = BTreeMap::>::new(); - for entry in entries { - let Some(pair) = entry.as_array() else { - continue; - }; - let Some(name) = pair.first().and_then(Value::as_str) else { - continue; - }; - let Some(value) = pair.get(1).and_then(Value::as_str) else { - continue; - }; - normalized - .entry(name.to_owned()) - .or_default() - .push(value.to_owned()); - } - normalized - }) - .unwrap_or_default(); - Ok(PythonVfsRpcResponsePayload::Http { - status: payload - .get("status") - .and_then(Value::as_u64) - .map(|value| value as u16) - .unwrap_or_default(), - reason: payload - .get("statusText") - .and_then(Value::as_str) - .unwrap_or_default() - .to_owned(), - url: payload - .get("url") - .and_then(Value::as_str) - .unwrap_or(url_text) - .to_owned(), - headers: header_map, - body_base64: payload - .get("body") - .and_then(Value::as_str) - .unwrap_or_default() - .to_owned(), - }) - })(); + process.execution.close_stdin()?; + close_kernel_process_stdin(&mut vm.kernel, process)?; - self.respond_python_rpc(vm_id, process_id, request.id, response) + Ok(DispatchResult { + response: stdin_closed_response(request, payload.process_id), + events: Vec::new(), + }) } - fn handle_python_dns_rpc_request( + pub(crate) async fn kill_process( &mut self, - vm_id: &str, - process_id: &str, - request: PythonVfsRpcRequest, - ) -> Result<(), SidecarError> { - let Some(vm) = self.vms.get(vm_id) else { - return Ok(()); - }; - if !vm.active_processes.contains_key(process_id) { - return Ok(()); - } - let response = (|| { - let hostname = request.hostname.as_deref().ok_or_else(|| { - SidecarError::InvalidState(String::from("python dnsLookup requires a hostname")) - })?; - let mut addresses = filter_dns_safe_ip_addrs( - resolve_dns_ip_addrs( - &self.bridge, - &vm.kernel, - vm_id, - &vm.dns, - hostname, - DnsLookupPolicy::CheckPermissions, - )?, - hostname, - )?; - if let Some(family) = request.family { - addresses.retain(|address| { - matches!((family, address), (4, IpAddr::V4(_)) | (6, IpAddr::V6(_))) - }); - } - Ok(PythonVfsRpcResponsePayload::DnsLookup { - addresses: addresses - .into_iter() - .map(|address| address.to_string()) - .collect(), - }) - })(); + request: &RequestFrame, + payload: KillProcessRequest, + ) -> Result { + let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; + self.require_owned_vm(&connection_id, &session_id, &vm_id)?; + self.kill_process_internal(&vm_id, &payload.process_id, &payload.signal)?; - self.respond_python_rpc(vm_id, process_id, request.id, response) + Ok(DispatchResult { + response: process_killed_response(request, payload.process_id), + events: Vec::new(), + }) } - fn handle_python_subprocess_rpc_request( + pub(crate) async fn find_listener( &mut self, - vm_id: &str, - process_id: &str, - request: PythonVfsRpcRequest, - ) -> Result<(), SidecarError> { - let command = request.command.clone().ok_or_else(|| { - SidecarError::InvalidState(String::from("python subprocessRun requires a command")) - })?; - let (internal_bootstrap_env, cwd) = { - let Some(vm) = self.vms.get(vm_id) else { - return Ok(()); - }; - let Some(process) = vm.active_processes.get(process_id) else { - return Ok(()); - }; - let virtual_home = guest_virtual_home(vm); - let cwd = request.cwd.clone().or_else(|| { - guest_runtime_path_for_host_path( - &vm.guest_env, - &virtual_home, - &vm.host_cwd, - &process.host_cwd.to_string_lossy(), - ) - }); - ( - sanitize_javascript_child_process_internal_bootstrap_env(&vm.guest_env), - cwd, - ) - }; - let response = self - .spawn_javascript_child_process_sync( - vm_id, - process_id, - JavascriptChildProcessSpawnRequest { - command, - args: request.args.clone(), - options: JavascriptChildProcessSpawnOptions { - cwd, - env: request.env.clone(), - input: None, - internal_bootstrap_env, - shell: request.shell, - detached: false, - stdio: vec![ - String::from("pipe"), - String::from("pipe"), - String::from("pipe"), - ], - timeout: None, - kill_signal: None, - }, - }, - request.max_buffer, - ) - .map(|payload| PythonVfsRpcResponsePayload::SubprocessRun { - exit_code: payload - .get("code") - .and_then(Value::as_i64) - .map(|value| value as i32) - .unwrap_or(1), - stdout: payload - .get("stdout") - .and_then(Value::as_str) - .unwrap_or_default() - .to_owned(), - stderr: payload - .get("stderr") - .and_then(Value::as_str) - .unwrap_or_default() - .to_owned(), - max_buffer_exceeded: payload - .get("maxBufferExceeded") - .and_then(Value::as_bool) - .unwrap_or(false), - }); + request: &RequestFrame, + payload: FindListenerRequest, + ) -> Result { + let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; + self.require_owned_vm(&connection_id, &session_id, &vm_id)?; + require_vm_inspection_permission( + &self.bridge, + &vm_id, + "network.inspect", + "network", + &socket_query_resource(SocketQueryKind::TcpListener, &payload), + )?; - self.respond_python_rpc(vm_id, process_id, request.id, response) + let listener = + find_socket_state_entry(self.vms.get(&vm_id), SocketQueryKind::TcpListener, &payload)?; + + Ok(DispatchResult { + response: listener_snapshot_response(request, listener), + events: Vec::new(), + }) } - fn handle_python_socket_rpc_request( + pub(crate) async fn get_process_snapshot( &mut self, - vm_id: &str, - process_id: &str, - request: PythonVfsRpcRequest, - ) -> Result<(), SidecarError> { - if !self.vms.contains_key(vm_id) { - return Ok(()); - } - let response = self.python_socket_op(vm_id, process_id, &request); - self.respond_python_rpc(vm_id, process_id, request.id, response) + request: &RequestFrame, + _payload: GetProcessSnapshotRequest, + ) -> Result { + let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; + self.require_owned_vm(&connection_id, &session_id, &vm_id)?; + require_vm_inspection_permission( + &self.bridge, + &vm_id, + "process.inspect", + "process", + "process://snapshot", + )?; + + let processes = self + .vms + .get_mut(&vm_id) + .map(|vm| { + prune_exited_process_snapshots(vm); + snapshot_vm_processes(vm) + }) + .unwrap_or_default(); + + Ok(DispatchResult { + response: process_snapshot_response(request, processes), + events: Vec::new(), + }) } - fn python_socket_op( + pub(crate) async fn guest_kernel_call( &mut self, - vm_id: &str, - process_id: &str, - request: &PythonVfsRpcRequest, - ) -> Result { - match request.method { - PythonVfsRpcMethod::SocketConnect => { - let host = python_socket_host(request)?; - let port = python_socket_port(request)?; - self.check_python_socket_limit(vm_id)?; - self.bridge.require_network_access( - vm_id, - NetworkOperation::Http, - format_tcp_resource(&host, port), - )?; - let pinned = self.python_socket_pinned_addrs(vm_id, &host, port)?; - let stream = python_connect_tcp(&pinned, port)?; - stream - .set_read_timeout(Some(PYTHON_SOCKET_READ_POLL)) - .map_err(python_socket_io_error)?; - // Bound writes too: without a write timeout `write_all` on a - // stalled peer would block the shared event loop indefinitely. - stream - .set_write_timeout(Some(PYTHON_SOCKET_WRITE_TIMEOUT)) - .map_err(python_socket_io_error)?; - let socket_id = - self.store_python_socket(vm_id, process_id, PythonHostSocket::Tcp(stream))?; - Ok(PythonVfsRpcResponsePayload::SocketCreated { socket_id }) - } - PythonVfsRpcMethod::SocketSend => { - let data = python_socket_payload(request)?; - let socket = self.python_socket_mut(vm_id, process_id, request)?; - let PythonHostSocket::Tcp(stream) = socket else { - return Err(python_socket_kind_error("send", "TCP")); - }; - stream.write_all(&data).map_err(python_socket_io_error)?; - Ok(PythonVfsRpcResponsePayload::SocketSent { - bytes_sent: data.len(), - }) - } - PythonVfsRpcMethod::SocketRecv => { - let max = python_socket_recv_len(request); - let socket = self.python_socket_mut(vm_id, process_id, request)?; - let PythonHostSocket::Tcp(stream) = socket else { - return Err(python_socket_kind_error("recv", "TCP")); - }; - let mut buf = vec![0u8; max]; - match stream.read(&mut buf) { - Ok(0) => Ok(PythonVfsRpcResponsePayload::SocketReceived { - data_base64: String::new(), - closed: true, - timed_out: false, - }), - Ok(n) => Ok(PythonVfsRpcResponsePayload::SocketReceived { - data_base64: base64::engine::general_purpose::STANDARD.encode(&buf[..n]), - closed: false, - timed_out: false, - }), - Err(error) if python_socket_would_block(&error) => { - Ok(PythonVfsRpcResponsePayload::SocketReceived { - data_base64: String::new(), - closed: false, - timed_out: true, - }) - } - Err(error) => Err(python_socket_io_error(error)), - } - } - PythonVfsRpcMethod::SocketClose => { - self.remove_python_socket(vm_id, process_id, request); - Ok(PythonVfsRpcResponsePayload::Empty) - } - PythonVfsRpcMethod::UdpCreate => { - self.check_python_socket_limit(vm_id)?; - let socket = UdpSocket::bind("0.0.0.0:0").map_err(python_socket_io_error)?; - socket - .set_read_timeout(Some(PYTHON_SOCKET_READ_POLL)) - .map_err(python_socket_io_error)?; - let socket_id = - self.store_python_socket(vm_id, process_id, PythonHostSocket::Udp(socket))?; - Ok(PythonVfsRpcResponsePayload::SocketCreated { socket_id }) - } - PythonVfsRpcMethod::UdpSendto => { - let host = python_socket_host(request)?; - let port = python_socket_port(request)?; - let data = python_socket_payload(request)?; - self.bridge.require_network_access( - vm_id, - NetworkOperation::Http, - format_tcp_resource(&host, port), - )?; - let pinned = self.python_socket_pinned_addrs(vm_id, &host, port)?; - let target = pinned - .first() - .map(|ip| SocketAddr::new(*ip, port)) - .ok_or_else(|| { - SidecarError::Execution(format!("EAI_NONAME: cannot resolve {host}")) - })?; - let socket = self.python_socket_mut(vm_id, process_id, request)?; - let PythonHostSocket::Udp(udp) = socket else { - return Err(python_socket_kind_error("sendto", "UDP")); - }; - let sent = udp.send_to(&data, target).map_err(python_socket_io_error)?; - Ok(PythonVfsRpcResponsePayload::SocketSent { bytes_sent: sent }) - } - PythonVfsRpcMethod::UdpRecvfrom => { - let max = python_socket_recv_len(request); - let socket = self.python_socket_mut(vm_id, process_id, request)?; - let PythonHostSocket::Udp(udp) = socket else { - return Err(python_socket_kind_error("recvfrom", "UDP")); - }; - let mut buf = vec![0u8; max]; - match udp.recv_from(&mut buf) { - Ok((n, addr)) => Ok(PythonVfsRpcResponsePayload::UdpReceived { - data_base64: base64::engine::general_purpose::STANDARD.encode(&buf[..n]), - host: addr.ip().to_string(), - port: addr.port(), - timed_out: false, - }), - Err(error) if python_socket_would_block(&error) => { - Ok(PythonVfsRpcResponsePayload::UdpReceived { - data_base64: String::new(), - host: String::new(), - port: 0, - timed_out: true, - }) - } - Err(error) => Err(python_socket_io_error(error)), - } - } - _ => Err(SidecarError::InvalidState(String::from( - "non-socket python RPC reached the socket dispatcher unexpectedly", - ))), - } - } - - /// Resolve `host` to the egress-guard-approved IP set, then apply the same - /// loopback-connect gate the JS raw-TCP path uses (`filter_tcp_connect_ip_addrs`) - /// so a rebinding DNS server can't map an allowlisted hostname onto a - /// sidecar-local loopback port the VM policy didn't open. - fn python_socket_pinned_addrs( - &self, - vm_id: &str, - host: &str, - port: u16, - ) -> Result, SidecarError> { - let Some(vm) = self.vms.get(vm_id) else { - return Err(SidecarError::InvalidState(String::from( - "python socket op for unknown vm", - ))); - }; - let context = build_javascript_socket_path_context(vm)?; - if let Ok(literal_ip) = host.parse::() { - filter_tcp_connect_ip_addrs(vec![literal_ip], host, port, &context) - } else { - filter_tcp_connect_ip_addrs( - resolve_dns_ip_addrs( - &self.bridge, - &vm.kernel, - vm_id, - &vm.dns, - host, - DnsLookupPolicy::SkipPermissions, - )?, - host, - port, - &context, - ) - } - } - - /// Enforce the VM's `max_sockets` resource limit before opening another - /// host socket for the guest (the registry is otherwise unbounded — a - /// hostile guest could exhaust the sidecar's fds/memory). - fn check_python_socket_limit(&self, vm_id: &str) -> Result<(), SidecarError> { - let Some(vm) = self.vms.get(vm_id) else { - return Ok(()); - }; - let limit = vm.kernel.resource_limits().max_sockets; - let current = vm_network_resource_counts(vm).sockets; - check_network_resource_limit(limit, current, 1, "socket") - } + request: &RequestFrame, + payload: GuestKernelCallRequest, + ) -> Result { + let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; + self.require_owned_vm(&connection_id, &session_id, &vm_id)?; - fn store_python_socket( - &mut self, - vm_id: &str, - process_id: &str, - socket: PythonHostSocket, - ) -> Result { - let process = self - .vms - .get_mut(vm_id) - .and_then(|vm| vm.active_processes.get_mut(process_id)) + let vm = self.vms.get_mut(&vm_id).ok_or_else(|| { + SidecarError::InvalidState(format!("VM {vm_id} no longer exists for guest kernel call")) + })?; + let kernel_pid = vm + .active_processes + .get(&payload.execution_id) + .map(|process| process.kernel_pid) .ok_or_else(|| { - SidecarError::InvalidState(String::from("python socket op for reaped vm/process")) + SidecarError::InvalidState(format!( + "VM {vm_id} has no active process {} for guest kernel call", + payload.execution_id + )) })?; - let socket_id = process.next_python_socket_id; - process.next_python_socket_id = process.next_python_socket_id.wrapping_add(1); - process.python_sockets.insert(socket_id, socket); - Ok(socket_id) - } - fn python_socket_mut( - &mut self, - vm_id: &str, - process_id: &str, - request: &PythonVfsRpcRequest, - ) -> Result<&mut PythonHostSocket, SidecarError> { - let socket_id = request.socket_id.ok_or_else(|| { - SidecarError::InvalidState(String::from("python socket op requires socketId")) - })?; - self.vms - .get_mut(vm_id) - .and_then(|vm| vm.active_processes.get_mut(process_id)) - .and_then(|process| process.python_sockets.get_mut(&socket_id)) - .ok_or_else(|| { - SidecarError::Execution(format!("EBADF: unknown python socket {socket_id}")) - }) + let response = agentos_native_sidecar_core::handle_guest_kernel_call( + &mut vm.kernel, + kernel_pid, + EXECUTION_DRIVER_NAME, + &payload.operation, + &payload.payload, + ) + .map_err(guest_kernel_core_error)?; + + Ok(DispatchResult { + response: self.respond( + request, + ResponsePayload::GuestKernelResult(GuestKernelResultResponse { payload: response }), + ), + events: Vec::new(), + }) } - fn remove_python_socket( + pub(crate) async fn get_resource_snapshot( &mut self, - vm_id: &str, - process_id: &str, - request: &PythonVfsRpcRequest, - ) { - let Some(socket_id) = request.socket_id else { - return; - }; - if let Some(process) = self + request: &RequestFrame, + _payload: GetResourceSnapshotRequest, + ) -> Result { + let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; + self.require_owned_vm(&connection_id, &session_id, &vm_id)?; + require_vm_inspection_permission( + &self.bridge, + &vm_id, + "process.inspect", + "process", + "process://resources", + )?; + + let snapshot = self .vms - .get_mut(vm_id) - .and_then(|vm| vm.active_processes.get_mut(process_id)) - { - process.python_sockets.remove(&socket_id); - } - } + .get(&vm_id) + .map(|vm| vm.kernel.resource_snapshot()) + .unwrap_or_default(); + let queue_snapshots = queue_tracker::queue_snapshot() + .into_iter() + .map(|queue| QueueSnapshotEntry { + name: queue.name.as_str().to_owned(), + category: queue.category.as_str().to_owned(), + depth: queue.depth as u64, + high_water: queue.high_water as u64, + capacity: queue.capacity as u64, + fill_percent: queue.fill_percent as u64, + }) + .collect(); - fn respond_python_rpc( - &mut self, - vm_id: &str, - process_id: &str, - request_id: u64, - response: Result, - ) -> Result<(), SidecarError> { - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(()); - }; - let Some(process) = vm.active_processes.get_mut(process_id) else { - return Ok(()); - }; - let result = match response { - Ok(payload) => process - .execution - .respond_python_vfs_rpc_success(request_id, payload), - Err(error) => process.execution.respond_python_vfs_rpc_error( - request_id, - "ERR_AGENTOS_PYTHON_VFS_RPC", - error.to_string(), + Ok(DispatchResult { + response: self.respond( + request, + ResponsePayload::ResourceSnapshot(ResourceSnapshotResponse { + running_processes: snapshot.running_processes as u64, + exited_processes: snapshot.exited_processes as u64, + fd_tables: snapshot.fd_tables as u64, + open_fds: snapshot.open_fds as u64, + pipes: snapshot.pipes as u64, + pipe_buffered_bytes: snapshot.pipe_buffered_bytes as u64, + ptys: snapshot.ptys as u64, + pty_buffered_input_bytes: snapshot.pty_buffered_input_bytes as u64, + pty_buffered_output_bytes: snapshot.pty_buffered_output_bytes as u64, + sockets: snapshot.sockets as u64, + socket_listeners: snapshot.socket_listeners as u64, + socket_connections: snapshot.socket_connections as u64, + socket_buffered_bytes: snapshot.socket_buffered_bytes as u64, + socket_datagram_queue_len: snapshot.socket_datagram_queue_len as u64, + queue_snapshots, + }), ), + events: Vec::new(), + }) + } + + pub(crate) async fn find_bound_udp( + &mut self, + request: &RequestFrame, + payload: FindBoundUdpRequest, + ) -> Result { + let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; + self.require_owned_vm(&connection_id, &session_id, &vm_id)?; + + let lookup_request = FindListenerRequest { + host: payload.host, + port: payload.port, + path: None, }; - match result { - Ok(()) => Ok(()), - Err(error) if is_broken_pipe_error(&error) => Ok(()), - Err(error) => Err(error), - } + require_vm_inspection_permission( + &self.bridge, + &vm_id, + "network.inspect", + "network", + &socket_query_resource(SocketQueryKind::UdpBound, &lookup_request), + )?; + let socket = find_socket_state_entry( + self.vms.get(&vm_id), + SocketQueryKind::UdpBound, + &lookup_request, + )?; + + Ok(DispatchResult { + response: bound_udp_snapshot_response(request, socket), + events: Vec::new(), + }) } - pub(crate) fn resolve_javascript_child_process_execution( - &self, - vm: &VmState, - parent_env: &BTreeMap, - parent_guest_cwd: &str, - parent_host_cwd: &Path, - request: &JavascriptChildProcessSpawnRequest, - ) -> Result { - let mut runtime_env = parent_env.clone(); - runtime_env.extend(request.options.internal_bootstrap_env.clone()); - let (guest_cwd, host_cwd_override) = request - .options - .cwd - .as_deref() - .map(|cwd| { - let normalized_parent_host_cwd = normalize_host_path(parent_host_cwd); - let requested_host_cwd = normalize_host_path(Path::new(cwd)); - if path_is_within_root(&requested_host_cwd, &normalized_parent_host_cwd) { - let relative = requested_host_cwd - .strip_prefix(&normalized_parent_host_cwd) - .unwrap_or_else(|_| Path::new("")); - let relative = relative.to_string_lossy().replace('\\', "/"); - let guest_cwd = if relative.is_empty() { - parent_guest_cwd.to_owned() - } else { - normalize_path(&format!("{parent_guest_cwd}/{relative}")) - }; - (guest_cwd, Some(requested_host_cwd)) - } else if Path::new(cwd).is_relative() { - ( - normalize_path(&format!("{parent_guest_cwd}/{cwd}")), - Some(normalize_host_path(&parent_host_cwd.join(cwd))), - ) - } else { - (normalize_path(cwd), None) - } - }) - .unwrap_or_else(|| (parent_guest_cwd.to_owned(), None)); - let inherited_host_cwd = (host_cwd_override.is_none() && guest_cwd == parent_guest_cwd) - .then(|| normalize_host_path(parent_host_cwd)); - let host_cwd = host_cwd_override - .or(inherited_host_cwd) - .or_else(|| { - host_runtime_path_for_guest_path_with_env( - vm, - &runtime_env, - &guest_cwd, - parent_host_cwd, - ) - }) - .unwrap_or_else(|| { - let candidate = PathBuf::from(&guest_cwd); - if guest_cwd == parent_guest_cwd { - normalize_host_path(parent_host_cwd) - } else if candidate.is_absolute() { - shadow_path_for_guest(vm, &guest_cwd) - } else { - vm.host_cwd.clone() - } - }); - let mut env = parent_env.clone(); - env.extend(request.options.env.clone()); - // Child JavaScript executions must resolve their own entrypoint/eval state. - // Reusing the parent's values makes the sidecar load the wrong source file. - env.remove("AGENTOS_GUEST_ENTRYPOINT"); - env.remove("AGENTOS_NODE_EVAL"); + pub(crate) async fn vm_fetch( + &mut self, + request: &RequestFrame, + payload: VmFetchRequest, + ) -> Result { + let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; + self.require_owned_vm(&connection_id, &session_id, &vm_id)?; - let (command, process_args) = if request.options.shell { - let tokens = tokenize_shell_free_command(&request.command); - let requires_shell = command_requires_shell(&request.command) - || tokens.first().is_some_and(|command| { - is_posix_shell_builtin(command) || shell_first_token_requires_shell(command) - }); - if requires_shell { - if !vm.command_guest_paths.contains_key("sh") { - return Err(SidecarError::InvalidState(format!( - "shell-mode child_process command requires /bin/sh, which is not \ - installed in this VM (install a software package that provides sh, \ - for example @agentos-software/coreutils): {}", - request.command - ))); - } - ( - String::from("sh"), - vec![String::from("-c"), request.command.clone()], - ) - } else { - let Some((command, args)) = tokens.split_first() else { - return Err(SidecarError::InvalidState(String::from( - "child_process shell command must not be empty", - ))); - }; - (command.clone(), args.to_vec()) - } + let vm = self + .vms + .get_mut(&vm_id) + .ok_or_else(|| SidecarError::InvalidState(String::from("unknown sidecar VM")))?; + let target_path = if payload.path.starts_with('/') { + payload.path.clone() } else { - (request.command.clone(), request.args.clone()) + format!("/{}", payload.path) }; - let process_args = apply_shell_cwd_prefix(&command, process_args, &guest_cwd); - if is_tool_command(vm, &command) { - let command = normalized_tool_command_name(&command).unwrap_or(command); - return Ok(ResolvedChildProcessExecution { - command: command.clone(), - process_args: std::iter::once(command.clone()) - .chain(process_args.iter().cloned()) - .collect(), - runtime: GuestRuntimeKind::JavaScript, - entrypoint: command, - execution_args: process_args, - env, - guest_cwd, - host_cwd, - wasm_permission_tier: None, - tool_command: true, + let request_url = Url::parse(&format!("http://127.0.0.1:{}{target_path}", payload.port)) + .map_err(|error| { + SidecarError::InvalidState(format!( + "invalid vm.fetch target {target_path:?}: {error}" + )) + })?; + let header_values: BTreeMap = serde_json::from_str(&payload.headers_json) + .map_err(|error| { + SidecarError::InvalidState(format!( + "vm.fetch headers_json must be valid JSON: {error}" + )) + })?; + let options = JavascriptHttpRequestOptions { + method: Some(payload.method), + headers: header_values, + body: payload.body, + reject_unauthorized: None, + }; + let headers = parse_http_header_collection(&options.headers, "vm.fetch headers")?; + let target_process_id = find_kernel_http_listener_process(vm, payload.port); + if let Some(target_process_id) = target_process_id { + let max_fetch_response_bytes = vm.limits.http.max_fetch_response_bytes; + let response_json = match dispatch_kernel_http_fetch( + &self.bridge, + &vm_id, + vm, + &target_process_id, + payload.port, + &target_path, + &options, + &headers, + max_fetch_response_bytes, + ) { + Ok(response_json) => response_json, + Err(error) => { + if let Some(exit_code) = kernel_http_fetch_target_exit_code(&error) { + let _ = vm; + self.finish_active_process_exit(&vm_id, &target_process_id, exit_code)?; + } + return Err(error); + } + }; + let response = self.respond( + request, + ResponsePayload::VmFetchResult(VmFetchResponse { response_json }), + ); + ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?; + + return Ok(DispatchResult { + response, + events: Vec::new(), }); } - if is_path_like_specifier(&command) - && matches!( - Path::new(&command).extension().and_then(|ext| ext.to_str()), - Some("js" | "mjs" | "cjs" | "ts" | "mts" | "cts") - ) - { - let guest_entrypoint = if command.starts_with('/') { - normalize_path(&command) - } else if command.starts_with("file:") { - normalize_path(command.trim_start_matches("file:")) - } else { - normalize_path(&format!("{guest_cwd}/{command}")) - }; - let host_entrypoint = if command.starts_with("./") || command.starts_with("../") { - normalize_host_path(&host_cwd.join(&command)) - } else { - host_runtime_path_for_guest_path_with_env( - vm, - &runtime_env, - &guest_entrypoint, - parent_host_cwd, - ) - .unwrap_or_else(|| { - let candidate = PathBuf::from(&guest_entrypoint); - if candidate.is_absolute() { - candidate - } else { - host_cwd.join(&guest_entrypoint) - } + let Some((target_process_id, server_id)) = + vm.active_processes + .iter() + .find_map(|(process_id, process)| { + process + .http_servers + .iter() + .find(|(_, server)| server.guest_local_addr.port() == payload.port) + .map(|(server_id, _)| (process_id.clone(), *server_id)) }) - }; - env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint); - let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned(); - prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?; - - return Ok(ResolvedChildProcessExecution { - command: command.clone(), - process_args: std::iter::once(command) - .chain(process_args.iter().cloned()) - .collect(), - runtime: GuestRuntimeKind::JavaScript, - entrypoint: host_entrypoint.to_string_lossy().into_owned(), - execution_args: process_args, - env, - guest_cwd, - host_cwd, - wasm_permission_tier: None, - tool_command: false, - }); - } + else { + return Err(SidecarError::Execution(format!( + "vm.fetch could not find a guest HTTP listener on port {}", + payload.port + ))); + }; + let socket_paths = build_javascript_socket_path_context(vm)?; + let resource_limits = vm.kernel.resource_limits().clone(); + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + let process = vm + .active_processes + .get_mut(&target_process_id) + .ok_or_else(|| { + SidecarError::InvalidState(format!( + "vm.fetch target process disappeared: {target_process_id}" + )) + })?; + let request_json = serialize_http_loopback_request(&request_url, &options, &headers)?; + let response_json = dispatch_loopback_http_request(LoopbackHttpDispatchRequest { + bridge: &self.bridge, + vm_id: &vm_id, + dns: &vm.dns, + socket_paths: &socket_paths, + kernel: &mut vm.kernel, + kernel_readiness, + process, + resource_limits: &resource_limits, + server_id, + request_json: &request_json, + })?; - if is_node_runtime_command(&command) { - if let Some(cli) = resolve_host_node_cli_entrypoint(&command) { - env.insert( - String::from("AGENTOS_NODE_EVAL"), - build_host_node_cli_eval(&cli), - ); - prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?; - add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root); - add_runtime_host_access_path( - &mut env, - "AGENTOS_EXTRA_FS_READ_PATHS", - &cli.package_root, - true, - ); + let response = self.respond( + request, + ResponsePayload::VmFetchResult(VmFetchResponse { response_json }), + ); + ensure_vm_fetch_response_frame_within_limit(&response, self.config.max_frame_bytes)?; - return Ok(ResolvedChildProcessExecution { - command: command.clone(), - process_args: std::iter::once(command.clone()) - .chain(process_args.iter().cloned()) - .collect(), - runtime: GuestRuntimeKind::JavaScript, - entrypoint: String::from("-e"), - execution_args: std::iter::once(cli.guest_entrypoint.clone()) - .chain(process_args.iter().cloned()) - .collect(), - env, - guest_cwd, - host_cwd, - wasm_permission_tier: None, - tool_command: false, - }); - } + Ok(DispatchResult { + response, + events: Vec::new(), + }) + } - if process_args.is_empty() { - env.insert(String::from("AGENTOS_NODE_EVAL"), String::new()); - prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?; + pub(crate) async fn get_signal_state( + &mut self, + request: &RequestFrame, + payload: GetSignalStateRequest, + ) -> Result { + let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; + self.require_owned_vm(&connection_id, &session_id, &vm_id)?; - return Ok(ResolvedChildProcessExecution { - command: command.clone(), - process_args: vec![command.clone()], - runtime: GuestRuntimeKind::JavaScript, - entrypoint: String::from("-e"), - execution_args: Vec::new(), - env, - guest_cwd, - host_cwd, - wasm_permission_tier: None, - tool_command: false, - }); - } + self.drain_root_signal_state_events(&vm_id, &payload.process_id)?; - if let Some((entrypoint, execution_args)) = - resolve_special_node_cli_invocation(&process_args, &mut env) - { - prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?; + let handlers = self + .vms + .get(&vm_id) + .and_then(|vm| vm.signal_states.get(&payload.process_id)) + .cloned() + .unwrap_or_default(); - return Ok(ResolvedChildProcessExecution { - command: command.clone(), - process_args: std::iter::once(command.clone()) - .chain(process_args.iter().cloned()) - .collect(), - runtime: GuestRuntimeKind::JavaScript, - entrypoint, - execution_args, - env, - guest_cwd, - host_cwd, - wasm_permission_tier: None, - tool_command: false, - }); - } + Ok(DispatchResult { + response: signal_state_response(request, payload.process_id, handlers), + events: Vec::new(), + }) + } - let Some(entrypoint_specifier) = process_args.first() else { - return Err(SidecarError::InvalidState(format!( - "{command} child_process spawn requires an entrypoint" - ))); - }; + fn drain_root_signal_state_events( + &mut self, + vm_id: &str, + process_id: &str, + ) -> Result<(), SidecarError> { + let mut deferred = VecDeque::new(); - let (entrypoint, execution_args) = if is_path_like_specifier(entrypoint_specifier) { - let guest_entrypoint = if entrypoint_specifier.starts_with('/') { - normalize_path(entrypoint_specifier) - } else if entrypoint_specifier.starts_with("file:") { - normalize_path(entrypoint_specifier.trim_start_matches("file:")) - } else { - normalize_path(&format!("{guest_cwd}/{entrypoint_specifier}")) + loop { + let event = { + let Some(vm) = self.vms.get_mut(vm_id) else { + break; }; - let host_entrypoint = if entrypoint_specifier.starts_with("./") - || entrypoint_specifier.starts_with("../") - { - normalize_host_path(&host_cwd.join(entrypoint_specifier)) + let Some(process) = vm.active_processes.get_mut(process_id) else { + break; + }; + if let Some(event) = process.lease_pending_execution_event() { + Some(event) } else { - host_runtime_path_for_guest_path_with_env( - vm, - &runtime_env, - &guest_entrypoint, - parent_host_cwd, - ) - .unwrap_or_else(|| { - let candidate = PathBuf::from(&guest_entrypoint); - if candidate.is_absolute() { - candidate - } else { - host_cwd.join(&guest_entrypoint) + match process.poll_execution_event_blocking(Duration::ZERO) { + Ok(event) => event, + Err(SidecarError::Execution(message)) + if (process.runtime == GuestRuntimeKind::JavaScript + && closed_javascript_event_channel(&message)) + || (process.runtime == GuestRuntimeKind::Python + && closed_python_event_channel(&message)) + || (process.runtime == GuestRuntimeKind::WebAssembly + && closed_wasm_event_channel(&message)) => + { + None } - }) - }; - env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint); - ( - host_entrypoint.to_string_lossy().into_owned(), - process_args.iter().skip(1).cloned().collect(), - ) - } else { - ( - entrypoint_specifier.clone(), - process_args.iter().skip(1).cloned().collect(), - ) + Err(error) => return Err(error), + } + } }; - let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned(); - prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?; - return Ok(ResolvedChildProcessExecution { - command: command.clone(), - process_args: std::iter::once(command) - .chain(process_args.iter().cloned()) - .collect(), - runtime: GuestRuntimeKind::JavaScript, - entrypoint, - execution_args, - env, - guest_cwd, - host_cwd, - wasm_permission_tier: None, - tool_command: false, - }); - } + let Some(event) = event else { + break; + }; - if is_python_runtime_command(&command) { - return resolve_python_command_execution( - vm, - &command, - &process_args, - env, - guest_cwd, - host_cwd, - ); + match event.event() { + ActiveExecutionEvent::SignalState { + signal, + registration, + } => { + let signal = *signal; + let registration = registration.clone(); + drop(event); + if let Some(vm) = self.vms.get_mut(vm_id) { + apply_process_signal_state_update( + &mut vm.signal_states, + process_id, + signal, + registration, + ); + } + } + _ => deferred.push_back(event), + } } - let guest_entrypoint = resolve_guest_command_entrypoint( - vm, - &guest_cwd, - &command, - env.get("PATH").map(String::as_str), - ) - .ok_or_else(|| SidecarError::InvalidState(format!("command not found: {command}")))?; - let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint); - let wasm_permission_tier = vm.command_permissions.get(&command).copied().or_else(|| { - Path::new(&guest_entrypoint) - .file_name() - .and_then(|name| name.to_str()) - .and_then(|name| vm.command_permissions.get(name).copied()) - }); - if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) = - resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint) - { - prepare_guest_runtime_env( - vm, - &mut env, - &guest_cwd, - &host_cwd, - Some(javascript_guest_entrypoint), - )?; - - return Ok(ResolvedChildProcessExecution { - command: command.clone(), - process_args: std::iter::once(command) - .chain(process_args.iter().cloned()) - .collect(), - runtime: GuestRuntimeKind::JavaScript, - entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(), - execution_args: process_args, - env, - guest_cwd, - host_cwd, - wasm_permission_tier: None, - tool_command: false, - }); + if let Some(vm) = self.vms.get_mut(vm_id) { + if let Some(process) = vm.active_processes.get_mut(process_id) { + for event in deferred.into_iter().rev() { + if process.pending_execution_events.len() + >= process.pending_execution_event_count_limit + { + return Err(process_event_queue_overflow_error()); + } + process.requeue_pending_execution_event(event)?; + } + } } - prepare_guest_runtime_env( - vm, - &mut env, - &guest_cwd, - &host_cwd, - Some(guest_entrypoint.clone()), - )?; - Ok(ResolvedChildProcessExecution { - command: command.clone(), - process_args: std::iter::once(command) - .chain(process_args.iter().cloned()) - .collect(), - runtime: GuestRuntimeKind::WebAssembly, - entrypoint: host_entrypoint.to_string_lossy().into_owned(), - execution_args: process_args, - env, - guest_cwd, - host_cwd, - wasm_permission_tier, - tool_command: false, - }) + Ok(()) } - fn resolve_javascript_child_process_with_shebang( + pub(crate) async fn get_zombie_timer_count( &mut self, - vm_id: &str, - parent_env: &BTreeMap, - parent_guest_cwd: &str, - parent_host_cwd: &Path, - request: &mut JavascriptChildProcessSpawnRequest, - ) -> Result { - const MAX_SHEBANG_REDIRECTS: usize = 4; - - let mut resolved = { - let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; - self.resolve_javascript_child_process_execution( - vm, - parent_env, - parent_guest_cwd, - parent_host_cwd, - request, - )? - }; + request: &RequestFrame, + _payload: GetZombieTimerCountRequest, + ) -> Result { + let (connection_id, session_id, vm_id) = self.vm_scope_for(&request.ownership)?; + self.require_owned_vm(&connection_id, &session_id, &vm_id)?; - for redirects in 0..=MAX_SHEBANG_REDIRECTS { - let redirected = { - let vm = self - .vms - .get_mut(vm_id) - .ok_or_else(|| missing_vm_error(vm_id))?; - rewrite_javascript_shebang_request(vm, &resolved, request)? - }; - if !redirected { - return Ok(resolved); - } - if redirects == MAX_SHEBANG_REDIRECTS { - return Err(SidecarError::Execution(format!( - "ELOOP: exceeded {MAX_SHEBANG_REDIRECTS} shebang redirects" - ))); - } - resolved = { - let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; - self.resolve_javascript_child_process_execution( - vm, - parent_env, - parent_guest_cwd, - parent_host_cwd, - request, - )? - }; - } + let count = self + .vms + .get(&vm_id) + .map(|vm| vm.kernel.zombie_timer_count() as u64) + .unwrap_or_default(); - Ok(resolved) + Ok(DispatchResult { + response: zombie_timer_count_response(request, count), + events: Vec::new(), + }) } - pub(crate) fn spawn_javascript_child_process( + pub(crate) fn kill_process_internal( &mut self, vm_id: &str, process_id: &str, - request: JavascriptChildProcessSpawnRequest, - ) -> Result { - let mut request = request; - let total_start = Instant::now(); - let phase_start = Instant::now(); - let (parent_env, parent_guest_cwd, parent_host_cwd) = { - let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; - let parent = vm - .active_processes - .get(process_id) - .ok_or_else(|| missing_process_error(vm_id, process_id))?; - ( - parent.env.clone(), - parent.guest_cwd.clone(), - parent.host_cwd.clone(), - ) - }; - let mut resolved = self.resolve_javascript_child_process_with_shebang( - vm_id, - &parent_env, - &parent_guest_cwd, - &parent_host_cwd, - &mut request, - )?; - { - let vm = self - .vms - .get_mut(vm_id) - .ok_or_else(|| missing_vm_error(vm_id))?; - stage_agentos_package_command(vm, &mut resolved)?; - } - let resolved = resolved; - record_execute_phase("child_process_resolve_execution", phase_start.elapsed()); - let (parent_kernel_pid, child_process_id) = { - let vm = self - .vms - .get_mut(vm_id) - .ok_or_else(|| missing_vm_error(vm_id))?; - let process = vm - .active_processes - .get_mut(process_id) - .ok_or_else(|| missing_process_error(vm_id, process_id))?; - (process.kernel_pid, process.allocate_child_process_id()) - }; - let sidecar_requests = self.sidecar_requests.clone(); + signal: &str, + ) -> Result<(), SidecarError> { + let signal_name = signal.to_owned(); + let signal = parse_signal(signal)?; let vm = self .vms .get_mut(vm_id) - .ok_or_else(|| missing_vm_error(vm_id))?; - let phase_start = Instant::now(); - let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved - .tool_command - { - let tool_resolution = resolve_tool_command( - vm, - &resolved.command, - &resolved.execution_args, - Some(&resolved.guest_cwd), - )? - .ok_or_else(|| { - SidecarError::InvalidState(format!( - "tool command no longer resolves: {}", - resolved.command - )) - })?; - let kernel_handle = vm - .kernel - .create_virtual_process( - EXECUTION_DRIVER_NAME, - TOOL_DRIVER_NAME, - &resolved.command, - resolved.process_args.clone(), - VirtualProcessOptions { - parent_pid: Some(parent_kernel_pid), - env: resolved.env.clone(), - cwd: Some(resolved.guest_cwd.clone()), - }, - ) - .map_err(kernel_error)?; - let kernel_pid = kernel_handle.pid(); - let tool_execution = ToolExecution::default(); - let cancelled = tool_execution.cancelled.clone(); - let pending_events = tool_execution.pending_events.clone(); - let events_overflowed = tool_execution.events_overflowed.clone(); - spawn_tool_process_events(ToolProcessEventRequest { - sidecar_requests: sidecar_requests.clone(), - connection_id: vm.connection_id.clone(), - session_id: vm.session_id.clone(), - vm_id: vm_id.to_owned(), - tool_resolution, - cancelled, - pending_events, - events_overflowed, - }); - ( - kernel_pid, - kernel_handle, - ActiveExecution::Tool(tool_execution), - None, - ) - } else { - let kernel_command = match resolved.runtime { - GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND, - GuestRuntimeKind::WebAssembly => WASM_COMMAND, - GuestRuntimeKind::Python => PYTHON_COMMAND, - }; - let kernel_handle = vm - .kernel - .spawn_process( - kernel_command, - resolved.process_args.clone(), - SpawnOptions { - requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), - parent_pid: Some(parent_kernel_pid), - env: resolved.env.clone(), - cwd: Some(resolved.guest_cwd.clone()), - }, - ) - .map_err(kernel_error)?; - let kernel_pid = kernel_handle.pid(); - if request.options.detached { - vm.kernel - .setsid(EXECUTION_DRIVER_NAME, kernel_pid) - .map_err(kernel_error)?; - } - let mut execution_env = resolved.env.clone(); - execution_env.insert( - String::from(EXECUTION_SANDBOX_ROOT_ENV), - normalize_host_path(&vm.cwd).to_string_lossy().into_owned(), - ); + .ok_or_else(|| SidecarError::InvalidState(format!("unknown sidecar VM {vm_id}")))?; + let signal_action = vm + .signal_states + .get(process_id) + .and_then(|handlers| handlers.get(&(signal as u32))) + .map(|registration| registration.action.clone()) + .unwrap_or(SignalDispositionAction::Default); + let process = vm.active_processes.get_mut(process_id).ok_or_else(|| { + SidecarError::InvalidState(format!("VM {vm_id} has no active process {process_id}")) + })?; + let kernel_pid = process.kernel_pid; + if !matches!(signal, 0 | libc::SIGCONT) { + // A guest parked in a deferred kernel-wait sync RPC is blocked in a + // native bridge wait the kill cannot interrupt; answer the parked + // RPC first so the termination can take effect. + flush_parked_kernel_wait_rpc(process); + } - let execution = match resolved.runtime { - GuestRuntimeKind::JavaScript => { - execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env( - &request.options.internal_bootstrap_env, - )); - execution_env - .insert(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1")); - let launch_entrypoint = resolve_agentos_package_javascript_launch_entrypoint( - vm, - &mut execution_env, - ) - .unwrap_or_else(|| resolved.entrypoint.clone()); - let context = - self.javascript_engine - .create_context(CreateJavascriptContextRequest { - vm_id: vm_id.to_owned(), - bootstrap_module: None, - compile_cache_root: Some( - self.cache_root.join("node-compile-cache"), - ), - }); - let inline_code = load_javascript_entrypoint_source( - vm, - &resolved.host_cwd, - &launch_entrypoint, - &execution_env, - ); - prepare_javascript_shadow(vm, &resolved, &execution_env)?; + enum KillBehavior { + Tool, + SharedV8StateOnly, + SharedV8Pause, + SharedV8Continue, + SharedV8Terminate, + SharedV8DispatchOrTerminate, + Noop, + HostPid(u32), + } - let built_reader = build_module_reader(vm, &resolved); - let guest_reader = built_reader.clone().map(|reader| { - Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader)) - as Box - }); - let module_reader = built_reader - .map(|reader| Box::new(reader) as Box); - let execution = self - .javascript_engine - .start_execution_with_module_reader( - StartJavascriptExecutionRequest { - guest_runtime: guest_runtime_identity( - vm, - Some(u64::from(kernel_pid)), - Some(u64::from(parent_kernel_pid)), - ), - vm_id: vm_id.to_owned(), - context_id: context.context_id, - argv: std::iter::once(launch_entrypoint) - .chain(resolved.execution_args.clone()) - .collect(), - env: execution_env, - cwd: resolved.host_cwd.clone(), - limits: javascript_execution_limits(vm), - inline_code, - wasm_module_bytes: None, - }, - module_reader, - guest_reader, - ) - .map_err(javascript_error)?; - ActiveExecution::Javascript(execution) - } - GuestRuntimeKind::WebAssembly => { - execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1")); - let wasm_limits = wasm_execution_limits(vm); - let wasm_guest_runtime = guest_runtime_identity( - vm, - Some(u64::from(kernel_pid)), - Some(u64::from(parent_kernel_pid)), - ); - let context = self.wasm_engine.create_context(CreateWasmContextRequest { - vm_id: vm_id.to_owned(), - module_path: Some(resolved.entrypoint.clone()), - }); - let execution = self - .wasm_engine - .start_execution(StartWasmExecutionRequest { - vm_id: vm_id.to_owned(), - context_id: context.context_id, - argv: resolved.process_args.clone(), - env: execution_env, - cwd: resolved.host_cwd.clone(), - permission_tier: execution_wasm_permission_tier( - resolved - .wasm_permission_tier - .unwrap_or(WasmPermissionTier::Full), - ), - limits: wasm_limits, - guest_runtime: wasm_guest_runtime, - }) - .map_err(wasm_error)?; - ActiveExecution::Wasm(Box::new(execution)) + let behavior = match &process.execution { + ActiveExecution::Tool(_) => KillBehavior::Tool, + _ if process.execution.uses_shared_v8_runtime() && signal == 0 => { + KillBehavior::SharedV8StateOnly + } + _ if process.execution.uses_shared_v8_runtime() + && matches!( + signal, + libc::SIGSTOP | libc::SIGTSTP | libc::SIGTTIN | libc::SIGTTOU + ) + && (signal == libc::SIGSTOP + || signal_action == SignalDispositionAction::Default) => + { + KillBehavior::SharedV8Pause + } + _ if process.execution.uses_shared_v8_runtime() + && matches!(signal, libc::SIGTSTP | libc::SIGTTIN | libc::SIGTTOU) + && signal_action == SignalDispositionAction::Ignore => + { + KillBehavior::Noop + } + _ if process.execution.uses_shared_v8_runtime() && signal == libc::SIGCONT => { + KillBehavior::SharedV8Continue + } + ActiveExecution::Javascript(execution) + if execution.uses_shared_v8_runtime() && signal == SIGKILL => + { + KillBehavior::SharedV8Terminate + } + ActiveExecution::Wasm(execution) + if execution.uses_shared_v8_runtime() && signal == SIGKILL => + { + KillBehavior::SharedV8Terminate + } + ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime() => { + KillBehavior::SharedV8DispatchOrTerminate + } + ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() => { + KillBehavior::SharedV8DispatchOrTerminate + } + ActiveExecution::Python(execution) + if execution.uses_shared_v8_runtime() + && signal_action != SignalDispositionAction::Default => + { + KillBehavior::SharedV8DispatchOrTerminate + } + ActiveExecution::Python(execution) if execution.uses_shared_v8_runtime() => { + KillBehavior::SharedV8Terminate + } + ActiveExecution::Javascript(execution) if execution.child_pid() == 0 => { + KillBehavior::Noop + } + _ => KillBehavior::HostPid(process.execution.child_pid()), + }; + + match behavior { + KillBehavior::Tool => { + let ActiveExecution::Tool(execution) = &process.execution else { + unreachable!("kill behavior must match tool execution"); + }; + if signal != 0 { + execution.cancelled.store(true, Ordering::Relaxed); + process.exit_signal = Some(signal); + process.exit_core_dumped = false; + process.queue_pending_execution_event(ActiveExecutionEvent::Exited( + 128 + signal, + ))?; } - GuestRuntimeKind::Python => { - // Nested `python` child_process: set up the Pyodide context the - // same way the top-level execute path does, so a guest shell or - // node parent can spawn `python` exactly like `node`. - let python_file_path = if execution_env.contains_key("AGENTOS_PYTHON_ARGV") { - execution_env.get("AGENTOS_PYTHON_FILE").map(PathBuf::from) + } + KillBehavior::SharedV8StateOnly => { + vm.kernel + .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal) + .map_err(kernel_error)?; + } + KillBehavior::SharedV8Pause => { + process.execution.pause()?; + vm.kernel + .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal) + .map_err(kernel_error)?; + } + KillBehavior::SharedV8Continue => { + process.execution.resume()?; + vm.kernel + .kill_process(EXECUTION_DRIVER_NAME, kernel_pid, signal) + .map_err(kernel_error)?; + if matches!(&process.execution, ActiveExecution::Javascript(_)) { + if !dispatch_v8_process_signal(process, signal)? { + return Err(SidecarError::InvalidState(format!( + "unsupported guest SIGCONT handler delivery for pid {kernel_pid}" + ))); + } + } else if signal_action == SignalDispositionAction::User { + if matches!(&process.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()) + { + process.queue_pending_wasm_signal(signal)?; } else { - python_file_entrypoint(&resolved.entrypoint) - }; - let pyodide_dist_path = self - .python_engine - .bundled_pyodide_dist_path_for_vm(vm_id) - .map_err(python_error)?; - let pyodide_cache_path = pyodide_dist_path - .parent() - .and_then(Path::parent) - .unwrap_or(pyodide_dist_path.as_path()) - .join("pyodide-package-cache"); - add_runtime_guest_path_mapping( - &mut execution_env, - PYTHON_PYODIDE_GUEST_ROOT, - &pyodide_dist_path, - ); - add_runtime_guest_path_mapping( - &mut execution_env, - PYTHON_PYODIDE_CACHE_GUEST_ROOT, - &pyodide_cache_path, - ); - add_runtime_host_access_path( - &mut execution_env, - "AGENTOS_EXTRA_FS_READ_PATHS", - &pyodide_dist_path, - true, - ); - add_runtime_host_access_path( - &mut execution_env, - "AGENTOS_EXTRA_FS_READ_PATHS", - &pyodide_cache_path, - true, - ); - add_runtime_host_access_path( - &mut execution_env, - "AGENTOS_EXTRA_FS_WRITE_PATHS", - &pyodide_cache_path, - false, + return Err(SidecarError::InvalidState(format!( + "unsupported guest SIGCONT handler delivery for pid {kernel_pid}" + ))); + } + } + } + KillBehavior::SharedV8Terminate => { + if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) { + close_kernel_process_stdin(&mut vm.kernel, process)?; + } + process.exit_signal = (signal != 0).then_some(signal); + process.exit_core_dumped = false; + process.execution.terminate()?; + let needs_synthetic_exit = matches!(process.execution, ActiveExecution::Wasm(_)) + || (signal == SIGKILL + && matches!(process.execution, ActiveExecution::Javascript(_))); + if signal != 0 && needs_synthetic_exit { + process.queue_pending_execution_event(ActiveExecutionEvent::Exited( + 128 + signal, + ))?; + } + } + KillBehavior::SharedV8DispatchOrTerminate => { + if signal != 0 { + let is_shared_wasm = matches!( + &process.execution, + ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime() ); - let context = self - .python_engine - .create_context(CreatePythonContextRequest { - vm_id: vm_id.to_owned(), - pyodide_dist_path, - }); - let execution = self - .python_engine - .start_execution(StartPythonExecutionRequest { - vm_id: vm_id.to_owned(), - context_id: context.context_id, - code: resolved.entrypoint.clone(), - file_path: python_file_path, - env: execution_env, - cwd: resolved.host_cwd.clone(), - limits: python_execution_limits(vm), - guest_runtime: guest_runtime_identity( - vm, - Some(u64::from(kernel_pid)), - Some(u64::from(parent_kernel_pid)), - ), - }) - .map_err(python_error)?; - ActiveExecution::Python(execution) + if is_shared_wasm { + let action = vm + .signal_states + .get(process_id) + .and_then(|handlers| handlers.get(&(signal as u32))) + .map(|registration| ®istration.action); + match action { + Some(SignalDispositionAction::Ignore) => {} + Some(SignalDispositionAction::User) => { + process.queue_pending_wasm_signal(signal)?; + } + Some(SignalDispositionAction::Default) | None => { + if !matches!( + canonical_signal_name(signal), + Some("SIGWINCH" | "SIGCHLD" | "SIGURG") + ) { + process.exit_signal = Some(signal); + process.exit_core_dumped = false; + process.execution.terminate()?; + process.queue_pending_execution_event( + ActiveExecutionEvent::Exited(128 + signal), + )?; + } + } + } + } else if matches!(process.execution, ActiveExecution::Python(_)) { + match signal_action { + SignalDispositionAction::Ignore => {} + SignalDispositionAction::User => { + return Err(SidecarError::InvalidState(format!( + "unsupported guest signal handler delivery for pid {kernel_pid}" + ))); + } + SignalDispositionAction::Default => { + process.exit_signal = Some(signal); + process.exit_core_dumped = false; + process.execution.terminate()?; + } + } + } else if !dispatch_v8_process_signal(process, signal)? { + process.exit_signal = Some(signal); + process.exit_core_dumped = false; + process.execution.terminate()?; + } } - }; - let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) { - "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?), - "ignore" => { - vm.kernel - .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0) - .map_err(kernel_error)?; - None + } + KillBehavior::Noop => {} + KillBehavior::HostPid(pid) => { + if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) { + close_kernel_process_stdin(&mut vm.kernel, process)?; } - "inherit" => None, - _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?), - }; - (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) - }; - record_execute_phase( - "child_process_spawn_and_start_execution", - phase_start.elapsed(), - ); - - let phase_start = Instant::now(); - // Shared-terminal detection: when the child's kernel fd 1 is a PTY (the - // slave inherited from a TTY shell), record who owns the host-facing - // master so the child's stdio writes surface through master drains - // instead of child stdout events (see `tty_master_owner`). - let child_fd1_is_tty = vm - .kernel - .isatty(EXECUTION_DRIVER_NAME, kernel_pid, 1) - .unwrap_or(false); - let process = vm - .active_processes - .get(process_id) - .ok_or_else(|| missing_process_error(vm_id, process_id))?; - let inherited_tty_master_owner = if child_fd1_is_tty { - process - .tty_master_fd - .map(|master_fd| (process.kernel_pid, master_fd)) - .or(process.tty_master_owner) - } else { - None - }; - let process = vm - .active_processes - .get_mut(process_id) - .ok_or_else(|| missing_process_error(vm_id, process_id))?; - process.child_processes.insert( - child_process_id.clone(), - ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution) - .with_detached(request.options.detached) - .with_guest_cwd(resolved.guest_cwd.clone()) - .with_env(resolved.env.clone()) - .with_host_cwd(resolved.host_cwd.clone()), - ); - { - let child = process - .child_processes - .get_mut(&child_process_id) - .ok_or_else(|| { - SidecarError::InvalidState(format!( - "child process {child_process_id} disappeared during spawn" - )) - })?; - child.tty_master_owner = inherited_tty_master_owner; - if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd { - child.kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd); + signal_runtime_process(pid, signal)?; } } - record_execute_phase("child_process_register", phase_start.elapsed()); - record_execute_phase("child_process_spawn_total", total_start.elapsed()); - Ok(json!({ - "childId": child_process_id, - "pid": kernel_pid, - "command": resolved.command, - "args": resolved.process_args, - })) + emit_security_audit_event( + &self.bridge, + vm_id, + "security.process.kill", + audit_fields([ + (String::from("source"), String::from("control_plane")), + (String::from("source_pid"), String::from("0")), + (String::from("target_pid"), process.kernel_pid.to_string()), + (String::from("process_id"), process_id.to_owned()), + (String::from("signal"), signal_name), + ( + String::from("host_pid"), + process.execution.child_pid().to_string(), + ), + ]), + ); + Ok(()) } - pub(crate) fn spawn_javascript_child_process_sync( + pub async fn pump_process_events( &mut self, - vm_id: &str, - process_id: &str, - request: JavascriptChildProcessSpawnRequest, - max_buffer: Option, - ) -> Result { - let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?; - let timeout_deadline = request - .options - .timeout - .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms)); - let timeout_signal = request - .options - .kill_signal - .clone() - .unwrap_or_else(|| String::from("SIGTERM")); - let spawned = self.spawn_javascript_child_process(vm_id, process_id, request)?; - let child_process_id = spawned - .get("childId") - .and_then(Value::as_str) - .ok_or_else(|| { - SidecarError::InvalidState(String::from( - "child_process.spawn_sync response is missing childId", - )) - })? - .to_owned(); - - if let Some(input) = sync_input.as_deref() { - self.write_javascript_child_process_stdin(vm_id, process_id, &child_process_id, input)?; - } - self.close_javascript_child_process_stdin(vm_id, process_id, &child_process_id)?; + ownership: &OwnershipScope, + ) -> Result { + let mut emitted_any = false; - let max_buffer = max_buffer.unwrap_or(1024 * 1024); - let mut stdout = Vec::new(); - let mut stderr = Vec::new(); - let mut max_buffer_exceeded = false; - let mut kill_sent = false; - let mut timed_out = false; - - let exit_code = loop { - let wait_ms = if let Some(deadline) = timeout_deadline { - let now = Instant::now(); - if now >= deadline { - if !kill_sent { - timed_out = true; - self.kill_javascript_child_process( - vm_id, - process_id, - &child_process_id, - &timeout_signal, - )?; - kill_sent = true; + let mut queued_envelopes = Vec::new(); + { + let pending_capacity = self.pending_process_event_capacity(); + let receiver = self.process_event_receiver.as_mut().ok_or_else(|| { + SidecarError::InvalidState(String::from("process event receiver unavailable")) + })?; + loop { + if queued_envelopes.len() >= pending_capacity { + if receiver.is_empty() { + break; } - 0 - } else { - u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50)) - .unwrap_or(50) + return Err(process_event_queue_overflow_error()); + } + match receiver.try_recv() { + Ok(envelope) => { + queued_envelopes.push(envelope); + emitted_any = true; + } + Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break, + Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break, } - } else { - 50 - }; - let event = - self.poll_javascript_child_process(vm_id, process_id, &child_process_id, wait_ms)?; - if event.is_null() { - continue; } + } + for envelope in queued_envelopes { + self.queue_pending_process_event(envelope)?; + } - match event.get("type").and_then(Value::as_str) { - Some("stdout") => { - let chunk = javascript_sync_rpc_bytes_arg( - &[event.get("data").cloned().unwrap_or(Value::Null)], - 0, - "child_process.spawn_sync stdout", - )?; - stdout.extend_from_slice(&chunk); - if stdout.len() > max_buffer && !kill_sent { - max_buffer_exceeded = true; - self.kill_javascript_child_process( - vm_id, - process_id, - &child_process_id, - "SIGTERM", - )?; - kill_sent = true; + let vm_ids = self.vm_ids_for_scope(ownership)?; + for vm_id in vm_ids { + while let Some(vm) = self.vms.get(&vm_id) { + let connection_id = vm.connection_id.clone(); + let session_id = vm.session_id.clone(); + let process_ids = self + .vms + .get(&vm_id) + .map(|vm| vm.active_processes.keys().cloned().collect::>()) + .unwrap_or_default(); + let mut emitted_this_pass = false; + + for process_id in process_ids { + if self + .vms + .get(&vm_id) + .is_some_and(|vm| vm.detached_child_processes.contains(&process_id)) + { + continue; } - } - Some("stderr") => { - let chunk = javascript_sync_rpc_bytes_arg( - &[event.get("data").cloned().unwrap_or(Value::Null)], - 0, - "child_process.spawn_sync stderr", - )?; - stderr.extend_from_slice(&chunk); - if stderr.len() > max_buffer && !kill_sent { - max_buffer_exceeded = true; - self.kill_javascript_child_process( - vm_id, - process_id, - &child_process_id, - "SIGTERM", - )?; - kill_sent = true; + enum ProcessPollResult { + Event(Box>), + RecoverClosedChannel, + } + let poll_result = { + let Some(vm) = self.vms.get_mut(&vm_id) else { + continue; + }; + let Some(process) = vm.active_processes.get_mut(&process_id) else { + continue; + }; + if let Some(event) = process.lease_pending_execution_event() { + ProcessPollResult::Event(Box::new(Some(event))) + } else { + match process.poll_execution_event(Duration::ZERO).await { + Ok(event) => ProcessPollResult::Event(Box::new(event)), + Err(SidecarError::Execution(message)) + if (process.runtime == GuestRuntimeKind::JavaScript + && closed_javascript_event_channel(&message)) + || (process.runtime == GuestRuntimeKind::Python + && closed_python_event_channel(&message)) + || (process.runtime == GuestRuntimeKind::WebAssembly + && closed_wasm_event_channel(&message)) => + { + ProcessPollResult::RecoverClosedChannel + } + Err(other) => return Err(other), + } + } + }; + let event = match poll_result { + ProcessPollResult::Event(event) => *event, + ProcessPollResult::RecoverClosedChannel => self + .recover_closed_root_runtime_process_event(&vm_id, &process_id)? + .map(PolledExecutionEvent::unreserved), + }; + + let Some(event) = event else { + continue; + }; + if matches!(event.event(), ActiveExecutionEvent::Exited(_)) { + record_execute_response_to_exit_milestone( + "execute_response_to_exit_event_polled", + &vm_id, + &process_id, + ); } + + if Self::internal_execution_event(event.event()) { + // These events are sidecar work items, not client-facing + // process events. Handle them immediately so a sibling + // process can service sync RPCs while another request + // waits on VM-local networking. + self.handle_execution_event(&vm_id, &process_id, event.into_event())?; + } else { + let PolledExecutionEvent { event, reservation } = event; + let envelope = ProcessEventEnvelope { + connection_id: connection_id.clone(), + session_id: session_id.clone(), + vm_id: vm_id.clone(), + process_id: process_id.clone(), + event, + }; + if let Err(error) = self.check_pending_process_event_capacity(&envelope) { + if let Some(process) = self + .vms + .get_mut(&vm_id) + .and_then(|vm| vm.active_processes.get_mut(&process_id)) + { + process.requeue_pending_execution_event(PolledExecutionEvent { + event: envelope.event, + reservation, + })?; + } + return Err(error); + } + self.queue_pending_process_event(envelope)?; + drop(reservation); + } + emitted_any = true; + emitted_this_pass = true; } - Some("exit") => { - break event - .get("exitCode") - .and_then(Value::as_i64) - .map(|value| value as i32) - .unwrap_or(1); + + if !emitted_this_pass { + break; } - _ => {} } - }; - Ok(json!({ - "stdout": String::from_utf8_lossy(&stdout), - "stderr": String::from_utf8_lossy(&stderr), - "code": exit_code, - "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null }, - "timedOut": timed_out, - "maxBufferExceeded": max_buffer_exceeded, - })) + if self.pump_detached_child_process_events(&vm_id)? { + emitted_any = true; + } + } + + Ok(emitted_any) } - fn spawn_descendant_javascript_child_process( + fn internal_execution_event(event: &ActiveExecutionEvent) -> bool { + matches!( + event, + ActiveExecutionEvent::JavascriptSyncRpcRequest(_) + | ActiveExecutionEvent::PythonVfsRpcRequest(_) + | ActiveExecutionEvent::SignalState { .. } + ) + } + + fn recover_closed_root_runtime_process_event( &mut self, vm_id: &str, process_id: &str, - current_process_path: &[&str], - request: JavascriptChildProcessSpawnRequest, - ) -> Result { - let mut request = request; - let total_start = Instant::now(); - let current_process_label = - Self::child_process_path_label(process_id, current_process_path); - let phase_start = Instant::now(); - let (parent_env, parent_guest_cwd, parent_host_cwd, parent_kernel_pid) = { - let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; - let root = vm - .active_processes - .get(process_id) - .ok_or_else(|| missing_process_error(vm_id, process_id))?; - let parent = - Self::active_process_by_path(root, current_process_path).ok_or_else(|| { - SidecarError::InvalidState(format!( - "unknown child process path {current_process_label} during nested spawn" - )) - })?; - ( - parent.env.clone(), - parent.guest_cwd.clone(), - parent.host_cwd.clone(), - parent.kernel_pid, - ) + ) -> Result, SidecarError> { + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(None); }; - let mut resolved = self.resolve_javascript_child_process_with_shebang( - vm_id, - &parent_env, - &parent_guest_cwd, - &parent_host_cwd, - &mut request, - )?; + let Some(process) = vm.active_processes.get_mut(process_id) else { + return Ok(None); + }; + if process.execution.uses_shared_v8_runtime() { + return Ok(None); + } + if process.runtime != GuestRuntimeKind::JavaScript + && process.runtime != GuestRuntimeKind::Python + && process.runtime != GuestRuntimeKind::WebAssembly { - let vm = self - .vms - .get_mut(vm_id) - .ok_or_else(|| missing_vm_error(vm_id))?; - stage_agentos_package_command(vm, &mut resolved)?; + return Ok(None); } - let resolved = resolved; - record_execute_phase("child_process_resolve_execution", phase_start.elapsed()); + let runtime_child_pid = process.execution.child_pid(); + if runtime_child_pid == 0 { + return Ok(None); + } + match runtime_child_exit_status(runtime_child_pid)? { + RuntimeChildStatusObservation::Exited(status) => { + process.exit_signal = status.signal; + process.exit_core_dumped = status.core_dumped; + return Ok(Some(ActiveExecutionEvent::Exited(status.status))); + } + RuntimeChildStatusObservation::Running => return Ok(None), + RuntimeChildStatusObservation::NotWaitable => { + return Err(SidecarError::Execution(format!( + "ECHILD: guest runtime process {runtime_child_pid} exited without an observable wait status" + ))); + } + } + } - let sidecar_requests = self.sidecar_requests.clone(); - let vm = self - .vms - .get_mut(vm_id) - .ok_or_else(|| missing_vm_error(vm_id))?; - let phase_start = Instant::now(); - let child_process_id = { - let root = vm - .active_processes - .get_mut(process_id) - .ok_or_else(|| missing_process_error(vm_id, process_id))?; - let parent = - Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| { - SidecarError::InvalidState(format!( - "unknown child process path {current_process_label} during nested spawn" - )) - })?; - parent.allocate_child_process_id() - }; - let mut child_path = current_process_path.to_vec(); - child_path.push(child_process_id.as_str()); - let (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) = if resolved - .tool_command - { - let tool_resolution = resolve_tool_command( - vm, - &resolved.command, - &resolved.execution_args, - Some(&resolved.guest_cwd), - )? - .ok_or_else(|| { - SidecarError::InvalidState(format!( - "tool command no longer resolves: {}", - resolved.command - )) - })?; - let kernel_handle = vm - .kernel - .create_virtual_process( - EXECUTION_DRIVER_NAME, - TOOL_DRIVER_NAME, - &resolved.command, - resolved.process_args.clone(), - VirtualProcessOptions { - parent_pid: Some(parent_kernel_pid), - env: resolved.env.clone(), - cwd: Some(resolved.guest_cwd.clone()), - }, - ) - .map_err(kernel_error)?; - let kernel_pid = kernel_handle.pid(); - let tool_execution = ToolExecution::default(); - let cancelled = tool_execution.cancelled.clone(); - let pending_events = tool_execution.pending_events.clone(); - let events_overflowed = tool_execution.events_overflowed.clone(); - spawn_tool_process_events(ToolProcessEventRequest { - sidecar_requests: sidecar_requests.clone(), - connection_id: vm.connection_id.clone(), - session_id: vm.session_id.clone(), - vm_id: vm_id.to_owned(), - tool_resolution, - cancelled, - pending_events, - events_overflowed, - }); - ( - kernel_pid, - kernel_handle, - ActiveExecution::Tool(tool_execution), - None, - ) - } else { - let kernel_command = match resolved.runtime { - GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND, - GuestRuntimeKind::WebAssembly => WASM_COMMAND, - GuestRuntimeKind::Python => PYTHON_COMMAND, - }; - let kernel_handle = vm - .kernel - .spawn_process( - kernel_command, - resolved.process_args.clone(), - SpawnOptions { - requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), - parent_pid: Some(parent_kernel_pid), - env: resolved.env.clone(), - cwd: Some(resolved.guest_cwd.clone()), - }, - ) - .map_err(kernel_error)?; - let kernel_pid = kernel_handle.pid(); - if request.options.detached { - vm.kernel - .setsid(EXECUTION_DRIVER_NAME, kernel_pid) - .map_err(kernel_error)?; - } - let mut execution_env = resolved.env.clone(); - execution_env.insert( - String::from(EXECUTION_SANDBOX_ROOT_ENV), - normalize_host_path(&vm.cwd).to_string_lossy().into_owned(), - ); - let execution = match resolved.runtime { - GuestRuntimeKind::JavaScript => { - execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env( - &request.options.internal_bootstrap_env, - )); - execution_env - .insert(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1")); - let launch_entrypoint = resolve_agentos_package_javascript_launch_entrypoint( - vm, - &mut execution_env, - ) - .unwrap_or_else(|| resolved.entrypoint.clone()); - let context = - self.javascript_engine - .create_context(CreateJavascriptContextRequest { - vm_id: vm_id.to_owned(), - bootstrap_module: None, - compile_cache_root: Some( - self.cache_root.join("node-compile-cache"), - ), - }); - let inline_code = load_javascript_entrypoint_source( - vm, - &resolved.host_cwd, - &launch_entrypoint, - &execution_env, - ); - prepare_javascript_shadow(vm, &resolved, &execution_env)?; - - let built_reader = build_module_reader(vm, &resolved); - let guest_reader = built_reader.clone().map(|reader| { - Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader)) - as Box - }); - let module_reader = built_reader - .map(|reader| Box::new(reader) as Box); - let execution = self - .javascript_engine - .start_execution_with_module_reader( - StartJavascriptExecutionRequest { - guest_runtime: guest_runtime_identity( - vm, - Some(u64::from(kernel_pid)), - Some(u64::from(parent_kernel_pid)), - ), - vm_id: vm_id.to_owned(), - context_id: context.context_id, - argv: std::iter::once(launch_entrypoint) - .chain(resolved.execution_args.clone()) - .collect(), - env: execution_env, - cwd: resolved.host_cwd.clone(), - limits: javascript_execution_limits(vm), - inline_code, - wasm_module_bytes: None, - }, - module_reader, - guest_reader, - ) - .map_err(javascript_error)?; - ActiveExecution::Javascript(execution) - } - GuestRuntimeKind::WebAssembly => { - execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1")); - let wasm_limits = wasm_execution_limits(vm); - let wasm_guest_runtime = guest_runtime_identity( - vm, - Some(u64::from(kernel_pid)), - Some(u64::from(parent_kernel_pid)), - ); - let context = self.wasm_engine.create_context(CreateWasmContextRequest { - vm_id: vm_id.to_owned(), - module_path: Some(resolved.entrypoint.clone()), - }); - let execution = self - .wasm_engine - .start_execution(StartWasmExecutionRequest { - vm_id: vm_id.to_owned(), - context_id: context.context_id, - argv: resolved.process_args.clone(), - env: execution_env, - cwd: resolved.host_cwd.clone(), - permission_tier: execution_wasm_permission_tier( - resolved - .wasm_permission_tier - .unwrap_or(WasmPermissionTier::Full), - ), - limits: wasm_limits, - guest_runtime: wasm_guest_runtime, - }) - .map_err(wasm_error)?; - ActiveExecution::Wasm(Box::new(execution)) - } - GuestRuntimeKind::Python => { - // Nested `python` child_process: set up the Pyodide context the - // same way the top-level execute path does, so a guest shell or - // node parent can spawn `python` exactly like `node`. - let python_file_path = if execution_env.contains_key("AGENTOS_PYTHON_ARGV") { - execution_env.get("AGENTOS_PYTHON_FILE").map(PathBuf::from) - } else { - python_file_entrypoint(&resolved.entrypoint) - }; - let pyodide_dist_path = self - .python_engine - .bundled_pyodide_dist_path_for_vm(vm_id) - .map_err(python_error)?; - let pyodide_cache_path = pyodide_dist_path - .parent() - .and_then(Path::parent) - .unwrap_or(pyodide_dist_path.as_path()) - .join("pyodide-package-cache"); - add_runtime_guest_path_mapping( - &mut execution_env, - PYTHON_PYODIDE_GUEST_ROOT, - &pyodide_dist_path, - ); - add_runtime_guest_path_mapping( - &mut execution_env, - PYTHON_PYODIDE_CACHE_GUEST_ROOT, - &pyodide_cache_path, - ); - add_runtime_host_access_path( - &mut execution_env, - "AGENTOS_EXTRA_FS_READ_PATHS", - &pyodide_dist_path, - true, - ); - add_runtime_host_access_path( - &mut execution_env, - "AGENTOS_EXTRA_FS_READ_PATHS", - &pyodide_cache_path, - true, - ); - add_runtime_host_access_path( - &mut execution_env, - "AGENTOS_EXTRA_FS_WRITE_PATHS", - &pyodide_cache_path, - false, - ); - let context = self - .python_engine - .create_context(CreatePythonContextRequest { - vm_id: vm_id.to_owned(), - pyodide_dist_path, - }); - let execution = self - .python_engine - .start_execution(StartPythonExecutionRequest { - vm_id: vm_id.to_owned(), - context_id: context.context_id, - code: resolved.entrypoint.clone(), - file_path: python_file_path, - env: execution_env, - cwd: resolved.host_cwd.clone(), - limits: python_execution_limits(vm), - guest_runtime: guest_runtime_identity( - vm, - Some(u64::from(kernel_pid)), - Some(u64::from(parent_kernel_pid)), - ), - }) - .map_err(python_error)?; - ActiveExecution::Python(execution) - } - }; - let kernel_stdin_writer_fd = match javascript_child_process_stdin_mode(&request) { - "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?), - "ignore" => { - vm.kernel - .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0) - .map_err(kernel_error)?; - None - } - "inherit" => None, - _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?), - }; - (kernel_pid, kernel_handle, execution, kernel_stdin_writer_fd) - }; - record_execute_phase( - "child_process_spawn_and_start_execution", - phase_start.elapsed(), - ); + fn active_process_by_path<'a>( + process: &'a ActiveProcess, + child_path: &[&str], + ) -> Option<&'a ActiveProcess> { + let mut current = process; + for child_id in child_path { + current = current.child_processes.get(*child_id)?; + } + Some(current) + } - let phase_start = Instant::now(); - let child_fd1_is_tty = vm - .kernel - .isatty(EXECUTION_DRIVER_NAME, kernel_pid, 1) - .unwrap_or(false); - let root = vm - .active_processes - .get(process_id) - .ok_or_else(|| missing_process_error(vm_id, process_id))?; - let parent = Self::active_process_by_path(root, current_process_path).ok_or_else(|| { - SidecarError::InvalidState(format!( - "unknown child process path {current_process_label} during nested spawn" - )) - })?; - let inherited_tty_master_owner = if child_fd1_is_tty { - parent - .tty_master_fd - .map(|master_fd| (parent.kernel_pid, master_fd)) - .or(parent.tty_master_owner) - } else { - None - }; - let root = vm - .active_processes - .get_mut(process_id) - .ok_or_else(|| missing_process_error(vm_id, process_id))?; - let parent = - Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| { - SidecarError::InvalidState(format!( - "unknown child process path {current_process_label} during nested spawn" - )) - })?; - parent.child_processes.insert( - child_process_id.clone(), - ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution) - .with_detached(request.options.detached) - .with_guest_cwd(resolved.guest_cwd.clone()) - .with_env(resolved.env.clone()) - .with_host_cwd(resolved.host_cwd.clone()), - ); - { - let child = parent - .child_processes - .get_mut(&child_process_id) - .ok_or_else(|| { - SidecarError::InvalidState(format!( - "child process {child_process_id} disappeared during nested spawn" - )) - })?; - child.tty_master_owner = inherited_tty_master_owner; - if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd { - child.kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd); - } + fn active_process_by_path_mut<'a>( + process: &'a mut ActiveProcess, + child_path: &[&str], + ) -> Option<&'a mut ActiveProcess> { + let mut current = process; + for child_id in child_path { + current = current.child_processes.get_mut(*child_id)?; } - record_execute_phase("child_process_register", phase_start.elapsed()); - record_execute_phase("child_process_spawn_total", total_start.elapsed()); - Ok(json!({ - "childId": child_process_id, - "pid": kernel_pid, - "command": resolved.command, - "args": resolved.process_args, - })) + Some(current) } - fn spawn_descendant_javascript_child_process_sync( - &mut self, - vm_id: &str, - process_id: &str, - current_process_path: &[&str], - request: JavascriptChildProcessSpawnRequest, - max_buffer: Option, - ) -> Result { - let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?; - let timeout_deadline = request - .options - .timeout - .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms)); - let timeout_signal = request - .options - .kill_signal - .clone() - .unwrap_or_else(|| String::from("SIGTERM")); - let spawned = self.spawn_descendant_javascript_child_process( - vm_id, - process_id, - current_process_path, - request, - )?; - let child_process_id = spawned - .get("childId") - .and_then(Value::as_str) - .ok_or_else(|| { - SidecarError::InvalidState(String::from( - "child_process.spawn_sync response is missing childId", - )) - })? - .to_owned(); + fn active_process_by_owned_path_mut<'a>( + process: &'a mut ActiveProcess, + child_path: &[String], + ) -> Option<&'a mut ActiveProcess> { + let mut current = process; + for child_id in child_path { + current = current.child_processes.get_mut(child_id)?; + } + Some(current) + } - if let Some(input) = sync_input.as_deref() { - self.write_descendant_javascript_child_process_stdin( - vm_id, - process_id, - current_process_path, - &child_process_id, - input, - )?; + fn active_process_path_by_kernel_pid( + process: &ActiveProcess, + kernel_pid: u32, + ) -> Option> { + if process.kernel_pid == kernel_pid { + return Some(Vec::new()); } - self.close_descendant_javascript_child_process_stdin( - vm_id, - process_id, - current_process_path, - &child_process_id, - )?; - let max_buffer = max_buffer.unwrap_or(1024 * 1024); - let mut stdout = Vec::new(); - let mut stderr = Vec::new(); - let mut max_buffer_exceeded = false; - let mut kill_sent = false; - let mut timed_out = false; - - let exit_code = loop { - let wait_ms = if let Some(deadline) = timeout_deadline { - let now = Instant::now(); - if now >= deadline { - if !kill_sent { - timed_out = true; - self.kill_descendant_javascript_child_process( - vm_id, - process_id, - current_process_path, - &child_process_id, - &timeout_signal, - )?; - kill_sent = true; - } - 0 - } else { - u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50)) - .unwrap_or(50) - } - } else { - 50 - }; - let event = self.poll_descendant_javascript_child_process( - vm_id, - process_id, - current_process_path, - &child_process_id, - wait_ms, - )?; - if event.is_null() { + for (child_id, child) in &process.child_processes { + let Some(mut path) = Self::active_process_path_by_kernel_pid(child, kernel_pid) else { continue; - } + }; + path.insert(0, child_id.clone()); + return Some(path); + } - match event.get("type").and_then(Value::as_str) { - Some("stdout") => { - let chunk = javascript_sync_rpc_bytes_arg( - &[event.get("data").cloned().unwrap_or(Value::Null)], - 0, - "child_process.spawn_sync stdout", - )?; - stdout.extend_from_slice(&chunk); - if stdout.len() > max_buffer && !kill_sent { - max_buffer_exceeded = true; - self.kill_descendant_javascript_child_process( - vm_id, - process_id, - current_process_path, - &child_process_id, - "SIGTERM", - )?; - kill_sent = true; - } - } - Some("stderr") => { - let chunk = javascript_sync_rpc_bytes_arg( - &[event.get("data").cloned().unwrap_or(Value::Null)], - 0, - "child_process.spawn_sync stderr", - )?; - stderr.extend_from_slice(&chunk); - if stderr.len() > max_buffer && !kill_sent { - max_buffer_exceeded = true; - self.kill_descendant_javascript_child_process( - vm_id, - process_id, - current_process_path, - &child_process_id, - "SIGTERM", - )?; - kill_sent = true; - } - } - Some("exit") => { - break event - .get("exitCode") - .and_then(Value::as_i64) - .map(|value| value as i32) - .unwrap_or(1); - } - _ => {} - } - }; + None + } - Ok(json!({ - "stdout": String::from_utf8_lossy(&stdout), - "stderr": String::from_utf8_lossy(&stderr), - "code": exit_code, - "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null }, - "timedOut": timed_out, - "maxBufferExceeded": max_buffer_exceeded, - })) + fn descendant_parent_process<'a>( + vm: &'a VmState, + process_id: &str, + child_path: &[&str], + ) -> Option<&'a ActiveProcess> { + let root = vm.active_processes.get(process_id)?; + Self::active_process_by_path(root, child_path) } - fn handle_descendant_javascript_child_process_rpc( - &mut self, - vm_id: &str, + fn descendant_parent_process_mut<'a>( + vm: &'a mut VmState, process_id: &str, - current_process_path: &[&str], - request: &JavascriptSyncRpcRequest, - ) -> Result { - match request.method.as_str() { - "child_process.spawn" => { - let Some(vm) = self.vms.get(vm_id) else { - return Ok(Value::Null); - }; - let (payload, _) = parse_javascript_child_process_spawn_request(vm, &request.args)?; - self.spawn_descendant_javascript_child_process( - vm_id, - process_id, - current_process_path, - payload, - ) - } - "child_process.spawn_sync" => { - let Some(vm) = self.vms.get(vm_id) else { - return Ok(Value::Null); - }; - let (payload, max_buffer) = - parse_javascript_child_process_spawn_request(vm, &request.args)?; - self.spawn_descendant_javascript_child_process_sync( - vm_id, - process_id, - current_process_path, - payload, - max_buffer, - ) - } - "child_process.poll" => { - let child_process_id = - javascript_sync_rpc_arg_str(&request.args, 0, "child_process.poll child id")?; - let wait_ms = javascript_sync_rpc_arg_u64_optional( - &request.args, - 1, - "child_process.poll wait ms", - )? - .unwrap_or_default(); - self.poll_descendant_javascript_child_process( - vm_id, - process_id, - current_process_path, - child_process_id, - wait_ms, - ) - } - "child_process.write_stdin" => { - let child_process_id = javascript_sync_rpc_arg_str( - &request.args, - 0, - "child_process.write_stdin child id", - )?; - let chunk = javascript_sync_rpc_bytes_arg( - &request.args, - 1, - "child_process.write_stdin chunk", - )?; - self.write_descendant_javascript_child_process_stdin( - vm_id, - process_id, - current_process_path, - child_process_id, - &chunk, - )?; - Ok(Value::Null) - } - "child_process.close_stdin" => { - let child_process_id = javascript_sync_rpc_arg_str( - &request.args, - 0, - "child_process.close_stdin child id", - )?; - self.close_descendant_javascript_child_process_stdin( - vm_id, - process_id, - current_process_path, - child_process_id, - )?; - Ok(Value::Null) - } - "child_process.kill" => { - let child_process_id = - javascript_sync_rpc_arg_str(&request.args, 0, "child_process.kill child id")?; - let signal = - javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?; - self.kill_descendant_javascript_child_process( - vm_id, - process_id, - current_process_path, - child_process_id, - signal, - )?; - Ok(Value::Null) + child_path: &[&str], + ) -> Option<&'a mut ActiveProcess> { + let root = vm.active_processes.get_mut(process_id)?; + Self::active_process_by_path_mut(root, child_path) + } + + fn child_process_path_label(process_id: &str, child_path: &[&str]) -> String { + if child_path.is_empty() { + process_id.to_owned() + } else { + format!("{process_id}/{}", child_path.join("/")) + } + } + + fn adopt_detached_child_processes( + current_process_id: &str, + process: &mut ActiveProcess, + ) -> Vec<(String, ActiveProcess)> { + let mut adopted = Vec::new(); + let child_ids = process.child_processes.keys().cloned().collect::>(); + for child_id in child_ids { + let child_process_id = format!("{current_process_id}/{child_id}"); + let Some(mut child) = process.child_processes.remove(&child_id) else { + continue; + }; + if child.detached { + adopted.push((child_process_id, child)); + continue; } - _ => Err(SidecarError::InvalidState(format!( - "unsupported nested child process RPC method {}", - request.method - ))), + + adopted.extend(Self::adopt_detached_child_processes( + &child_process_id, + &mut child, + )); + process.child_processes.insert(child_id, child); } + adopted } - /// Deferred servicing for a CHILD's `__kernel_stdin_read` / `__kernel_poll` - /// inside the child-event pump: probe readiness with a zero timeout, reply - /// when ready / expired / non-blocking, otherwise park the RPC on the child - /// (reply-by-token). The pump loop re-checks the parked RPC every - /// iteration, so the dispatch loop never blocks in a kernel wait on the - /// child's behalf. Returns false when the RPC must be serviced inline - /// (non-TTY JavaScript local stdin bridge). - fn service_child_kernel_wait_rpc( - &mut self, - vm_id: &str, - process_id: &str, - current_process_path: &[&str], - child_process_id: &str, - request: &JavascriptSyncRpcRequest, - ) -> Result { - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(true); - }; - let kernel = &mut vm.kernel; - let Some(root) = vm.active_processes.get_mut(process_id) else { - return Ok(true); - }; - let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else { - return Ok(true); - }; - let Some(child) = parent.child_processes.get_mut(child_process_id) else { - return Ok(true); - }; - if request.method == "__kernel_stdin_read" - && matches!(child.execution, ActiveExecution::Javascript(_)) - && child.tty_master_fd.is_none() - { - return Ok(false); - } - let now = Instant::now(); - let requested_timeout_ms = match request.method.as_str() { - "__kernel_stdin_read" => parse_kernel_stdin_read_args(request)?.1, - _ => u64::try_from(parse_kernel_poll_args(request)?.1).unwrap_or(0), - }; - let deadline = match &child.deferred_kernel_wait_rpc { - Some((parked, parked_deadline)) if parked.id == request.id => *parked_deadline, - _ => now + Duration::from_millis(requested_timeout_ms), - }; - let kernel_pid = child.kernel_pid; - let probe = match request.method.as_str() { - "__kernel_stdin_read" => { - let (max_bytes, _) = parse_kernel_stdin_read_args(request)?; - kernel_stdin_read_response(kernel, kernel_pid, max_bytes, Duration::ZERO) - } - _ => { - let (fd_requests, _) = parse_kernel_poll_args(request)?; - kernel_poll_response(kernel, kernel_pid, &fd_requests, 0) - } - }; - let probe = match probe { - Ok(value) => value, - Err(error) => { - child.deferred_kernel_wait_rpc = None; - child - .execution - .respond_javascript_sync_rpc_error( - request.id, - javascript_sync_rpc_error_code(&error), - error.to_string(), - ) - .or_else(ignore_stale_javascript_sync_rpc_response)?; - return Ok(true); - } - }; - let ready = match request.method.as_str() { - "__kernel_stdin_read" => !probe.is_null(), - _ => probe.get("readyCount").and_then(Value::as_u64).unwrap_or(0) > 0, - }; - if ready || requested_timeout_ms == 0 || now >= deadline { - child.deferred_kernel_wait_rpc = None; - child - .execution - .respond_javascript_sync_rpc_response(request.id, probe.into()) - .or_else(ignore_stale_javascript_sync_rpc_response)?; - return Ok(true); - } - child.deferred_kernel_wait_rpc = Some((request.clone(), deadline)); - Ok(true) + fn child_process_signal_key<'a>(process_id: &'a str, child_path: &[&'a str]) -> &'a str { + process_signal_state_key(process_id, child_path) } - /// Service `__kernel_stdio_write` for a process writing to the shared PTY. - /// Apply the slave line discipline once, then enqueue the drained bytes on - /// the owning process before acknowledging the write. The owner's local - /// queue is drained before its next runtime event, keeping child output - /// ahead of the next shell prompt. - pub(crate) fn service_shared_tty_stdio_write( - &mut self, - vm_id: &str, - writer_kernel_pid: u32, - owner: (u32, u32), - request: &JavascriptSyncRpcRequest, - ) -> Result { - let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?; - let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?; - if fd != 1 && fd != 2 { - return Err(SidecarError::InvalidState(format!( - "__kernel_stdio_write only supports fd 1/2, got {fd}" - ))); - } - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(json!(chunk.len())); - }; - let written = if fd == 1 { - vm.kernel - .write_process_stdout(EXECUTION_DRIVER_NAME, writer_kernel_pid, &chunk) - .map_err(kernel_error)? - } else { - vm.kernel - .write_process_stderr(EXECUTION_DRIVER_NAME, writer_kernel_pid, &chunk) - .map_err(kernel_error)? - }; - let (owner_pid, master_fd) = owner; - let mut drained = Vec::new(); - loop { - match vm.kernel.fd_read_with_timeout_result( - EXECUTION_DRIVER_NAME, - owner_pid, - master_fd, - MAX_PTY_BUFFER_BYTES, - Some(Duration::ZERO), - ) { - Ok(Some(bytes)) if !bytes.is_empty() => drained.extend(bytes), - Ok(_) => break, - Err(error) if error.code() == "EAGAIN" => break, - Err(error) => return Err(kernel_error(error)), - } - } - if !drained.is_empty() { - let Some(owner_process) = vm - .active_processes - .values_mut() - .find(|process| process.kernel_pid == owner_pid) - else { - return Err(SidecarError::InvalidState(format!( - "shared PTY owner pid {owner_pid} is not active" - ))); - }; - owner_process.queue_pending_execution_event(ActiveExecutionEvent::Stdout(drained))?; - } - Ok(json!(written)) - } + fn resolve_detached_child_process_path( + vm: &VmState, + detached_process_id: &str, + ) -> Option<(String, Vec)> { + let root_process_id = vm + .active_processes + .keys() + .filter(|candidate| { + detached_process_id == candidate.as_str() + || detached_process_id + .strip_prefix(candidate.as_str()) + .is_some_and(|remainder| remainder.starts_with('/')) + }) + .max_by_key(|candidate| candidate.len())? + .clone(); - /// Re-check a child's parked kernel-wait RPC (see - /// `service_child_kernel_wait_rpc`); called once per pump-loop iteration. - fn recheck_child_deferred_kernel_wait_rpc( - &mut self, - vm_id: &str, - process_id: &str, - current_process_path: &[&str], - child_process_id: &str, - ) -> Result<(), SidecarError> { - let parked = { - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(()); - }; - let Some(root) = vm.active_processes.get_mut(process_id) else { - return Ok(()); - }; - let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else { - return Ok(()); - }; - let Some(child) = parent.child_processes.get_mut(child_process_id) else { - return Ok(()); - }; - child - .deferred_kernel_wait_rpc - .as_ref() - .map(|(request, _)| request.clone()) - }; - if let Some(request) = parked { - let _ = self.service_child_kernel_wait_rpc( - vm_id, - process_id, - current_process_path, - child_process_id, - &request, - )?; + let remainder = detached_process_id + .strip_prefix(root_process_id.as_str()) + .unwrap_or_default(); + if remainder.is_empty() { + return Some((root_process_id, Vec::new())); } - Ok(()) - } - fn poll_descendant_javascript_child_process( - &mut self, - vm_id: &str, - process_id: &str, - current_process_path: &[&str], - child_process_id: &str, - wait_ms: u64, - ) -> Result { - let mut child_path = current_process_path.to_vec(); - child_path.push(child_process_id); - let child_gone_error = || javascript_child_process_gone_error(process_id, &child_path); - let deadline = Instant::now() + Duration::from_millis(wait_ms); - let mut polled_once = false; + Some(( + root_process_id, + remainder + .trim_start_matches('/') + .split('/') + .map(str::to_owned) + .collect(), + )) + } - loop { - self.drain_queued_descendant_javascript_child_process_events( - vm_id, - process_id, - &child_path, - )?; - self.recheck_child_deferred_kernel_wait_rpc( - vm_id, - process_id, - current_process_path, - child_process_id, - )?; - enum ChildPollResult { - Event(Box>), - RecoverRuntimeExit, - Timeout, - } - let wait = if wait_ms == 0 { - Duration::ZERO - } else { - deadline.saturating_duration_since(Instant::now()) + fn pump_detached_child_process_events(&mut self, vm_id: &str) -> Result { + let detached_process_ids = self + .vms + .get(vm_id) + .map(|vm| { + vm.detached_child_processes + .iter() + .cloned() + .collect::>() + }) + .unwrap_or_default(); + let mut emitted_any = false; + for detached_process_id in detached_process_ids { + let Some((root_process_id, child_path)) = self + .vms + .get(vm_id) + .and_then(|vm| Self::resolve_detached_child_process_path(vm, &detached_process_id)) + else { + if let Some(vm) = self.vms.get_mut(vm_id) { + vm.detached_child_processes.remove(&detached_process_id); + } + continue; }; - let poll_result = { - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(Value::Null); - }; - let Some(parent) = - Self::descendant_parent_process_mut(vm, process_id, current_process_path) - else { - return Err(child_gone_error()); - }; - let Some(child) = parent.child_processes.get_mut(child_process_id) else { - return Err(child_gone_error()); - }; - if let Some(event) = child.pending_execution_events.pop_front() { - ChildPollResult::Event(Box::new(Some(event))) - } else if polled_once && wait.is_zero() { - ChildPollResult::Timeout - } else { - polled_once = true; - match child.execution.poll_event_blocking(wait) { - Ok(Some(event)) => ChildPollResult::Event(Box::new(Some(event))), - Ok(None) => ChildPollResult::RecoverRuntimeExit, - Err(SidecarError::Execution(message)) - if (child.runtime == GuestRuntimeKind::JavaScript - && closed_javascript_event_channel(&message)) - || (child.runtime == GuestRuntimeKind::Python - && closed_python_event_channel(&message)) - || (child.runtime == GuestRuntimeKind::WebAssembly - && closed_wasm_event_channel(&message)) => - { - ChildPollResult::RecoverRuntimeExit - } - Err(error) => return Err(error), + if child_path.is_empty() { + loop { + enum ProcessPollResult { + Event(Box>), + RecoverClosedChannel, } - } - }; - let event = match poll_result { - ChildPollResult::Event(event) => *event, - ChildPollResult::Timeout => return Ok(Value::Null), - ChildPollResult::RecoverRuntimeExit => self - .recover_descendant_runtime_child_process_event( - vm_id, - process_id, - current_process_path, - child_process_id, - wait.as_millis().try_into().unwrap_or(u64::MAX), - )?, - }; - - let Some(event) = event else { - return Ok(Value::Null); - }; - - match event { - ActiveExecutionEvent::Stdout(chunk) => { - return Ok(json!({ - "type": "stdout", - "data": javascript_sync_rpc_bytes_value(&chunk), - })); - } - ActiveExecutionEvent::Stderr(chunk) => { - return Ok(json!({ - "type": "stderr", - "data": javascript_sync_rpc_bytes_value(&chunk), - })); - } - ActiveExecutionEvent::Exited(exit_code) => { - let cleanup_start = Instant::now(); - let had_trailing_events = { + let poll_result = { let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(Value::Null); - }; - let Some(parent) = Self::descendant_parent_process_mut( - vm, - process_id, - current_process_path, - ) else { - return Ok(Value::Null); + break; }; - let Some(child) = parent.child_processes.get_mut(child_process_id) else { - return Ok(Value::Null); + let Some(process) = vm.active_processes.get_mut(&root_process_id) else { + break; }; - let mut quiet_deadline = Instant::now() + PROCESS_EXIT_DRAIN_INITIAL_QUIET; - loop { - let wait = quiet_deadline.saturating_duration_since(Instant::now()); - let next = poll_child_execution_after_exit(child, wait)?; - let Some(next) = next else { - break; - }; - if matches!(next, ActiveExecutionEvent::Exited(_)) { - continue; - } - child.queue_pending_execution_event(next)?; - quiet_deadline = Instant::now() + PROCESS_EXIT_DRAIN_TRAILING_QUIET; - } - if !child.pending_execution_events.is_empty() { - child.queue_pending_execution_event(ActiveExecutionEvent::Exited( - exit_code, - ))?; - true + if let Some(event) = process.lease_pending_execution_event() { + ProcessPollResult::Event(Box::new(Some(event))) } else { - false + match process.poll_execution_event_blocking(Duration::ZERO) { + Ok(event) => ProcessPollResult::Event(Box::new(event)), + Err(SidecarError::Execution(message)) + if (process.runtime == GuestRuntimeKind::JavaScript + && closed_javascript_event_channel(&message)) + || (process.runtime == GuestRuntimeKind::Python + && closed_python_event_channel(&message)) + || (process.runtime == GuestRuntimeKind::WebAssembly + && closed_wasm_event_channel(&message)) => + { + ProcessPollResult::RecoverClosedChannel + } + Err(error) => return Err(error), + } } }; - if had_trailing_events { - continue; - } - - let parent_signal_key = - Self::child_process_signal_key(process_id, current_process_path); - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(Value::Null); - }; - let signal_name = { - let Some(parent) = Self::descendant_parent_process_mut( - vm, - process_id, - current_process_path, - ) else { - return Ok(Value::Null); - }; - let Some(child) = parent.child_processes.get_mut(child_process_id) else { - return Ok(Value::Null); - }; - child.pending_self_signal_exit.take().and_then(|signal| { - if exit_code == 128 + signal { - canonical_signal_name(signal).map(str::to_owned) - } else { - None - } - }) + let event = match poll_result { + ProcessPollResult::Event(event) => *event, + ProcessPollResult::RecoverClosedChannel => self + .recover_closed_root_runtime_process_event(vm_id, &root_process_id)? + .map(PolledExecutionEvent::unreserved), }; - let (parent_runtime_pid, parent_v8_signal_session, should_signal_parent) = { - let Some(parent) = - Self::descendant_parent_process(vm, process_id, current_process_path) - else { - return Ok(Value::Null); - }; - ( - parent.execution.child_pid(), - parent.execution.javascript_v8_session_handle().filter(|_| { - matches!( - &parent.execution, - ActiveExecution::Javascript(execution) - if execution.uses_shared_v8_runtime() - ) - }), - vm.signal_states - .get(parent_signal_key) - .and_then(|handlers| handlers.get(&(libc::SIGCHLD as u32))) - .is_some_and(|registration| { - registration.action != SignalDispositionAction::Default - }), - ) + let Some(event) = event else { + break; }; - let Some(parent) = - Self::descendant_parent_process_mut(vm, process_id, current_process_path) + if matches!(event.event(), ActiveExecutionEvent::Exited(_)) { + record_execute_response_to_exit_milestone( + "execute_response_to_detached_exit_event_polled", + vm_id, + &detached_process_id, + ); + } + let Some((connection_id, session_id)) = self + .vms + .get(vm_id) + .map(|vm| (vm.connection_id.clone(), vm.session_id.clone())) else { - return Ok(Value::Null); - }; - let Some(mut child) = parent.child_processes.remove(child_process_id) else { - return Ok(Value::Null); + break; }; - let child_process_label = - Self::child_process_path_label(process_id, &child_path); - let detached_children = - Self::adopt_detached_child_processes(&child_process_label, &mut child); - sync_process_host_writes_to_kernel(vm, &child)?; - release_inherited_child_raw_mode(&mut vm.kernel, &child)?; - let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); - terminate_child_process_tree(&mut vm.kernel, &mut child, &kernel_readiness); - child.kernel_handle.finish(exit_code); - let _ = vm.kernel.wait_and_reap(child.kernel_pid); - vm.signal_states.remove(child_process_id); - for (detached_process_id, detached_child) in detached_children { - vm.detached_child_processes - .insert(detached_process_id.clone()); - vm.active_processes - .insert(detached_process_id, detached_child); - } - if should_signal_parent { - if let Some(session) = parent_v8_signal_session { - dispatch_v8_session_signal_async(session, libc::SIGCHLD); - } else { - signal_runtime_process(parent_runtime_pid, libc::SIGCHLD)?; - } - } - let mut payload = Map::new(); - payload.insert(String::from("type"), Value::String(String::from("exit"))); - payload.insert(String::from("exitCode"), Value::from(exit_code)); - if let Some(signal_name) = signal_name { - payload.insert(String::from("signal"), Value::String(signal_name)); - } - record_execute_phase("child_process_exit_cleanup", cleanup_start.elapsed()); - return Ok(Value::Object(payload)); - } - ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => { - let mut current_child_path = current_process_path.to_vec(); - current_child_path.push(child_process_id); - if matches!( - request.method.as_str(), - "__kernel_stdin_read" | "__kernel_poll" - ) && self.service_child_kernel_wait_rpc( - vm_id, - process_id, - current_process_path, - child_process_id, - &request, - )? { - // Replied immediately or parked on the child; the pump - // loop re-checks parked RPCs every iteration. - continue; - } - if request.method == "__kernel_stdio_write" { - let shared_tty = { - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(Value::Null); + let PolledExecutionEvent { event, reservation } = event; + match event { + ActiveExecutionEvent::Stdout(chunk) => { + let envelope = ProcessEventEnvelope { + connection_id, + session_id, + vm_id: vm_id.to_owned(), + process_id: detached_process_id.clone(), + event: ActiveExecutionEvent::Stdout(chunk), }; - let Some(root) = vm.active_processes.get_mut(process_id) else { - return Ok(Value::Null); + if let Err(error) = self.check_pending_process_event_capacity(&envelope) + { + if let Some(process) = self + .vms + .get_mut(vm_id) + .and_then(|vm| vm.active_processes.get_mut(&root_process_id)) + { + process.requeue_pending_execution_event( + PolledExecutionEvent { + event: envelope.event, + reservation, + }, + )?; + } + return Err(error); + } + self.queue_pending_process_event(envelope)?; + drop(reservation); + emitted_any = true; + } + ActiveExecutionEvent::Stderr(chunk) => { + let envelope = ProcessEventEnvelope { + connection_id, + session_id, + vm_id: vm_id.to_owned(), + process_id: detached_process_id.clone(), + event: ActiveExecutionEvent::Stderr(chunk), }; - let Some(parent) = - Self::active_process_by_path_mut(root, current_process_path) - else { - return Ok(Value::Null); + if let Err(error) = self.check_pending_process_event_capacity(&envelope) + { + if let Some(process) = self + .vms + .get_mut(vm_id) + .and_then(|vm| vm.active_processes.get_mut(&root_process_id)) + { + process.requeue_pending_execution_event( + PolledExecutionEvent { + event: envelope.event, + reservation, + }, + )?; + } + return Err(error); + } + self.queue_pending_process_event(envelope)?; + drop(reservation); + emitted_any = true; + } + ActiveExecutionEvent::Exited(exit_code) => { + let envelope = ProcessEventEnvelope { + connection_id, + session_id, + vm_id: vm_id.to_owned(), + process_id: detached_process_id.clone(), + event: ActiveExecutionEvent::Exited(exit_code), }; - parent - .child_processes - .get(child_process_id) - .and_then(|child| { - child - .tty_master_owner - .map(|owner| (child.kernel_pid, owner)) - }) - }; - if let Some((child_kernel_pid, owner)) = shared_tty { - let response = self.service_shared_tty_stdio_write( + if let Err(error) = self.check_pending_process_event_capacity(&envelope) + { + if let Some(process) = self + .vms + .get_mut(vm_id) + .and_then(|vm| vm.active_processes.get_mut(&root_process_id)) + { + process.requeue_pending_execution_event( + PolledExecutionEvent { + event: envelope.event, + reservation, + }, + )?; + } + return Err(error); + } + if let Some(vm) = self.vms.get_mut(vm_id) { + vm.detached_child_processes.remove(&detached_process_id); + } + self.queue_pending_process_event(envelope)?; + drop(reservation); + emitted_any = true; + break; + } + internal @ (ActiveExecutionEvent::JavascriptSyncRpcRequest(_) + | ActiveExecutionEvent::PythonVfsRpcRequest(_) + | ActiveExecutionEvent::SignalState { .. }) => { + // This event is now being consumed. Convert it back + // through the lease API before servicing the RPC: + // the handler can resume the guest, whose next + // output must reserve from this same VM budget. + self.handle_execution_event( vm_id, - child_kernel_pid, - owner, - &request, - ); - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(Value::Null); - }; - let Some(root) = vm.active_processes.get_mut(process_id) else { - return Ok(Value::Null); - }; - let Some(parent) = - Self::active_process_by_path_mut(root, current_process_path) - else { - return Ok(Value::Null); - }; - let Some(child) = parent.child_processes.get_mut(child_process_id) - else { - return Ok(Value::Null); - }; - match response { - Ok(result) => child - .execution - .respond_javascript_sync_rpc_response(request.id, result.into()) - .or_else(ignore_stale_javascript_sync_rpc_response)?, - Err(error) => child - .execution - .respond_javascript_sync_rpc_error( - request.id, - javascript_sync_rpc_error_code(&error), - error.to_string(), - ) - .or_else(ignore_stale_javascript_sync_rpc_response)?, - } - continue; + &root_process_id, + PolledExecutionEvent { + event: internal, + reservation, + } + .into_event(), + )?; } } - let response = if request.method == "process.signal_state" { - let (signal, registration) = - parse_process_signal_state_request(&request.args) - .map_err(|error| SidecarError::InvalidState(error.to_string()))?; - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(Value::Null); - }; - let signal_key = - Self::child_process_signal_key(process_id, ¤t_child_path) - .to_owned(); - apply_process_signal_state_update( - &mut vm.signal_states, - &signal_key, - signal, - registration, - ); - Ok(Value::Null.into()) - } else if request.method == "process.kill" { - self.handle_descendant_process_kill_rpc( - vm_id, - process_id, - current_process_path, - child_process_id, - &request, - ) - .map(Into::into) - } else if request.method.starts_with("child_process.") { - self.handle_descendant_javascript_child_process_rpc( - vm_id, - process_id, - ¤t_child_path, - &request, - ) - .map(Into::into) - } else { - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(Value::Null); - }; - let resource_limits = vm.kernel.resource_limits().clone(); - let network_counts = vm_network_resource_counts(vm); - let socket_paths = build_javascript_socket_path_context(vm)?; - let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); - let Some(root) = vm.active_processes.get_mut(process_id) else { - return Ok(Value::Null); - }; - let Some(parent) = - Self::active_process_by_path_mut(root, current_process_path) - else { - return Ok(Value::Null); - }; - let Some(child) = parent.child_processes.get_mut(child_process_id) else { - return Ok(Value::Null); - }; - service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest { - bridge: &self.bridge, - vm_id, - dns: &vm.dns, - socket_paths: &socket_paths, - kernel: &mut vm.kernel, - kernel_readiness, - process: child, - sync_request: &request, - resource_limits: &resource_limits, - network_counts, - }) - }; + } + continue; + } - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(Value::Null); - }; - let Some(parent) = - Self::descendant_parent_process_mut(vm, process_id, current_process_path) - else { - return Ok(Value::Null); - }; - let Some(child) = parent.child_processes.get_mut(child_process_id) else { - return Ok(Value::Null); - }; - let parent_signal_event = response - .as_ref() - .ok() - .and_then(JavascriptSyncRpcServiceResponse::as_json) - .and_then(|result| { - let target_path_label = - Self::child_process_path_label(process_id, current_process_path); - if request.method != "process.kill" - || result.get("action").and_then(Value::as_str) != Some("user") - || result.get("targetProcessPath").and_then(Value::as_str) - != Some(target_path_label.as_str()) - { - return None; + let parent_path = child_path[..child_path.len() - 1] + .iter() + .map(String::as_str) + .collect::>(); + let child_process_id = child_path.last().expect("child path cannot be empty"); + + loop { + let event = match self.poll_descendant_javascript_child_process( + vm_id, + &root_process_id, + &parent_path, + child_process_id, + 0, + ) { + Ok(event) => event, + Err(SidecarError::InvalidState(message)) + if message.contains("unknown child process") + || message.contains("unknown child process path") => + { + if let Some(vm) = self.vms.get_mut(vm_id) { + vm.detached_child_processes.remove(&detached_process_id); } - Some(json!({ - "type": "signal", - "signal": result.get("signal").and_then(Value::as_str).unwrap_or_default(), - "number": result.get("number").and_then(Value::as_i64).unwrap_or_default(), - })) - }); - match response { - Ok(result) => child - .execution - .respond_javascript_sync_rpc_response(request.id, result) - .or_else(ignore_stale_javascript_sync_rpc_response)?, - Err(error) => child - .execution - .respond_javascript_sync_rpc_error( - request.id, - javascript_sync_rpc_error_code(&error), - error.to_string(), - ) - .or_else(ignore_stale_javascript_sync_rpc_response)?, + break; } - if let Some(event) = parent_signal_event { - return Ok(event); + Err(error) if is_javascript_child_process_gone_error(&error) => { + if let Some(vm) = self.vms.get_mut(vm_id) { + vm.detached_child_processes.remove(&detached_process_id); + } + break; } - } - ActiveExecutionEvent::PythonVfsRpcRequest(request) => { - // The kernel-VFS bridge is wired for top-level Python - // executions; a nested Python child (spawned by a JS/Python - // parent) cannot service VFS RPCs through this child-event - // path. Respond with a recoverable error instead of aborting - // the child, so its runner falls back to the in-isolate FS - // for the nested process — top-level Python keeps the full - // VFS root. - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(Value::Null); - }; - let Some(parent) = - Self::descendant_parent_process_mut(vm, process_id, current_process_path) - else { - return Ok(Value::Null); - }; - let Some(child) = parent.child_processes.get_mut(child_process_id) else { - return Ok(Value::Null); - }; - // Best-effort: deliver the "unavailable" error so the child's - // pending VFS RPC resolves and its runner falls back to the - // in-isolate FS. If delivery fails the child has already gone - // away (broken pipe / no-longer-pending), so dropping the - // result is correct here — there is nothing left to hang. - let _ = child.execution.respond_python_vfs_rpc_error( - request.id, - "ERR_AGENTOS_PYTHON_VFS_UNAVAILABLE", - "python VFS is not available for nested child processes", - ); - } - ActiveExecutionEvent::SignalState { - signal, - registration, - } => { - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(Value::Null); - }; - let signal_key = - Self::child_process_signal_key(process_id, &child_path).to_owned(); - apply_process_signal_state_update( - &mut vm.signal_states, - &signal_key, - signal, - registration.clone(), - ); - return Ok(json!({ - "type": "signal_state", - "signal": signal, - "registration": registration, - })); - } - } - } - } + Err(error) => return Err(error), + }; - fn recover_descendant_runtime_child_process_event( - &mut self, - vm_id: &str, - process_id: &str, - current_process_path: &[&str], - child_process_id: &str, - wait_ms: u64, - ) -> Result, SidecarError> { - let ( - parent_kernel_pid, - child_kernel_pid, - child_runtime_pid, - child_runtime, - child_shared_runtime, - ) = { - let mut child_path = current_process_path.to_vec(); - child_path.push(child_process_id); - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(None); - }; - let Some(parent) = - Self::descendant_parent_process_mut(vm, process_id, current_process_path) - else { - return Err(javascript_child_process_gone_error(process_id, &child_path)); - }; - let Some(child) = parent.child_processes.get_mut(child_process_id) else { - return Err(javascript_child_process_gone_error(process_id, &child_path)); - }; - ( - parent.kernel_pid, - child.kernel_pid, - child.execution.child_pid(), - child.runtime.clone(), - child.execution.uses_shared_v8_runtime(), - ) - }; - if child_runtime != GuestRuntimeKind::JavaScript - && child_runtime != GuestRuntimeKind::Python - && child_runtime != GuestRuntimeKind::WebAssembly - { - return Ok(None); - } - let wait_deadline = Instant::now() + Duration::from_millis(wait_ms.min(25)); - loop { - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(None); - }; - if let Some(process_info) = vm.kernel.list_processes().get(&child_kernel_pid) { - if process_info.status == ProcessStatus::Exited { - return Ok(Some(ActiveExecutionEvent::Exited( - process_info.exit_code.unwrap_or(0), - ))); - } - } - if let Some(wait_result) = vm - .kernel - .waitpid_with_options( - EXECUTION_DRIVER_NAME, - parent_kernel_pid, - child_kernel_pid as i32, - WaitPidFlags::WNOHANG, - ) - .map_err(kernel_error)? - { - return Ok(Some(ActiveExecutionEvent::Exited(wait_result.status))); - } + let Some(event_type) = event.get("type").and_then(Value::as_str) else { + break; + }; + let Some((connection_id, session_id)) = self + .vms + .get(vm_id) + .map(|vm| (vm.connection_id.clone(), vm.session_id.clone())) + else { + break; + }; - if !child_shared_runtime && child_runtime_pid != 0 { - if let Some(status) = runtime_child_exit_status(child_runtime_pid)? { - return Ok(Some(ActiveExecutionEvent::Exited(status))); - } - if !runtime_child_is_alive(child_runtime_pid)? { - return Ok(Some(ActiveExecutionEvent::Exited(0))); + let envelope = match event_type { + "stdout" => Some(ProcessEventEnvelope { + connection_id: connection_id.clone(), + session_id: session_id.clone(), + vm_id: vm_id.to_owned(), + process_id: detached_process_id.clone(), + event: ActiveExecutionEvent::Stdout(javascript_sync_rpc_bytes_arg( + &[event.get("data").cloned().unwrap_or(Value::Null)], + 0, + "detached child_process stdout", + )?), + }), + "stderr" => Some(ProcessEventEnvelope { + connection_id: connection_id.clone(), + session_id: session_id.clone(), + vm_id: vm_id.to_owned(), + process_id: detached_process_id.clone(), + event: ActiveExecutionEvent::Stderr(javascript_sync_rpc_bytes_arg( + &[event.get("data").cloned().unwrap_or(Value::Null)], + 0, + "detached child_process stderr", + )?), + }), + "exit" => { + if let Some(vm) = self.vms.get_mut(vm_id) { + vm.detached_child_processes.remove(&detached_process_id); + } + Some(ProcessEventEnvelope { + connection_id, + session_id, + vm_id: vm_id.to_owned(), + process_id: detached_process_id.clone(), + event: ActiveExecutionEvent::Exited( + event + .get("exitCode") + .and_then(Value::as_i64) + .map(|value| value as i32) + .unwrap_or(1), + ), + }) + } + _ => None, + }; + + let Some(envelope) = envelope else { + break; + }; + self.queue_pending_process_event(envelope)?; + emitted_any = true; + + if event_type == "exit" { + break; } } - if Instant::now() >= wait_deadline { - return Ok(None); - } - std::thread::sleep(Duration::from_millis(5)); } - } - fn write_descendant_javascript_child_process_stdin( + Ok(emitted_any) + } + pub(crate) fn drain_queued_descendant_javascript_child_process_events( &mut self, vm_id: &str, process_id: &str, - current_process_path: &[&str], - child_process_id: &str, - chunk: &[u8], + child_path: &[&str], ) -> Result<(), SidecarError> { - let mut child_path = current_process_path.to_vec(); - child_path.push(child_process_id); - let Some(vm) = self.vms.get_mut(vm_id) else { - return Err(javascript_child_process_gone_error(process_id, &child_path)); - }; - let Some(root) = vm.active_processes.get_mut(process_id) else { - return Err(javascript_child_process_gone_error(process_id, &child_path)); - }; - let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else { - return Err(javascript_child_process_gone_error(process_id, &child_path)); - }; - let Some(child) = parent.child_processes.get_mut(child_process_id) else { - return Err(javascript_child_process_gone_error(process_id, &child_path)); - }; - if let Err(error) = child.execution.write_stdin(chunk) { - if is_broken_pipe_error(&error) { - return Ok(()); + if child_path.is_empty() { + return Ok(()); + } + let target_process_id = Self::child_process_path_label(process_id, child_path); + let mut child_capacity = self + .vms + .get(vm_id) + .and_then(|vm| vm.active_processes.get(process_id)) + .and_then(|root| descendant_pending_execution_event_capacity(root, child_path)); + + let mut deferred = VecDeque::new(); + while let Some(envelope) = self.pending_process_events.pop_front() { + if envelope.vm_id == vm_id && envelope.process_id == target_process_id { + if matches!(child_capacity, Some(0)) { + self.pending_process_events.push_front(envelope); + while let Some(deferred_envelope) = deferred.pop_back() { + self.pending_process_events.push_front(deferred_envelope); + } + self.observe_pending_process_event_depth(); + return Err(process_event_queue_overflow_error()); + } + if let Some(vm) = self.vms.get_mut(vm_id) { + if let Some(root) = vm.active_processes.get_mut(process_id) { + if let Some(child) = Self::active_process_by_path_mut(root, child_path) { + match child.try_queue_pending_execution_envelope(envelope) { + Ok(()) => { + child_capacity = child_capacity.map(|capacity| capacity - 1); + continue; + } + Err((error, envelope)) => { + self.pending_process_events.push_front(envelope); + while let Some(deferred_envelope) = deferred.pop_back() { + self.pending_process_events.push_front(deferred_envelope); + } + self.observe_pending_process_event_depth(); + return Err(error); + } + } + } + } + } } - return Err(error); + deferred.push_back(envelope); } - write_kernel_process_stdin(&mut vm.kernel, child, chunk) - } + self.pending_process_events = deferred; + self.observe_pending_process_event_depth(); - fn close_descendant_javascript_child_process_stdin( - &mut self, - vm_id: &str, - process_id: &str, - current_process_path: &[&str], - child_process_id: &str, - ) -> Result<(), SidecarError> { - let mut child_path = current_process_path.to_vec(); - child_path.push(child_process_id); - let Some(vm) = self.vms.get_mut(vm_id) else { - return Err(javascript_child_process_gone_error(process_id, &child_path)); - }; - let Some(root) = vm.active_processes.get_mut(process_id) else { - return Err(javascript_child_process_gone_error(process_id, &child_path)); - }; - let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else { - return Err(javascript_child_process_gone_error(process_id, &child_path)); - }; - let Some(child) = parent.child_processes.get_mut(child_process_id) else { - return Err(javascript_child_process_gone_error(process_id, &child_path)); - }; - child.execution.close_stdin()?; - close_kernel_process_stdin(&mut vm.kernel, child) + let mut queued = VecDeque::new(); + { + let transfer_capacity = self + .pending_process_event_capacity() + .min(child_capacity.unwrap_or(usize::MAX)); + let receiver = self.process_event_receiver.as_mut().ok_or_else(|| { + SidecarError::InvalidState(String::from("process event receiver unavailable")) + })?; + loop { + if queued.len() >= transfer_capacity { + if receiver.is_empty() { + break; + } + self.pending_process_events.append(&mut queued); + self.observe_pending_process_event_depth(); + return Err(process_event_queue_overflow_error()); + } + match receiver.try_recv() { + Ok(envelope) => queued.push_back(envelope), + Err(tokio::sync::mpsc::error::TryRecvError::Empty) => break, + Err(tokio::sync::mpsc::error::TryRecvError::Disconnected) => break, + } + } + } + while let Some(envelope) = queued.pop_front() { + if envelope.vm_id == vm_id && envelope.process_id == target_process_id { + if let Some(vm) = self.vms.get_mut(vm_id) { + if let Some(root) = vm.active_processes.get_mut(process_id) { + if let Some(child) = Self::active_process_by_path_mut(root, child_path) { + match child.try_queue_pending_execution_envelope(envelope) { + Ok(()) => continue, + Err((error, envelope)) => { + self.pending_process_events.push_back(envelope); + self.pending_process_events.append(&mut queued); + self.observe_pending_process_event_depth(); + return Err(error); + } + } + } + } + } + } + if let Err((error, envelope)) = self.try_queue_pending_process_event(envelope) { + self.pending_process_events.push_back(envelope); + self.pending_process_events.append(&mut queued); + self.observe_pending_process_event_depth(); + return Err(error); + } + } + + Ok(()) } - fn kill_descendant_javascript_child_process( + pub(crate) fn handle_execution_event( &mut self, vm_id: &str, process_id: &str, - current_process_path: &[&str], - child_process_id: &str, - signal: &str, - ) -> Result<(), SidecarError> { - let signal_name = signal.to_owned(); - let signal = parse_signal(signal)?; + event: ActiveExecutionEvent, + ) -> Result, SidecarError> { let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(()); - }; - let Some(root) = vm.active_processes.get_mut(process_id) else { - return Ok(()); - }; - let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else { - return Ok(()); - }; - let source_pid = parent.kernel_pid; - let Some(child) = parent.child_processes.get_mut(child_process_id) else { - return Ok(()); - }; - terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?; - let child_process_label = if current_process_path.is_empty() { - child_process_id.to_owned() - } else { - format!("{}/{}", current_process_path.join("/"), child_process_id) + log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch"); + return Ok(None); }; - emit_security_audit_event( - &self.bridge, - vm_id, - "security.process.kill", - audit_fields([ - (String::from("source"), String::from("guest_child_process")), - (String::from("source_pid"), source_pid.to_string()), - (String::from("target_pid"), child.kernel_pid.to_string()), - (String::from("process_id"), process_id.to_owned()), - (String::from("child_process_id"), child_process_label), - (String::from("signal"), signal_name), - ]), - ); - Ok(()) - } - - fn handle_descendant_process_kill_rpc( - &mut self, - vm_id: &str, - process_id: &str, - current_process_path: &[&str], - child_process_id: &str, - request: &JavascriptSyncRpcRequest, - ) -> Result { - let target_pid = javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?; - let signal_name = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?; - let signal = parse_signal(signal_name)?; + if !vm.active_processes.contains_key(process_id) { + log_stale_process_event(&self.bridge, vm_id, process_id, "execution event dispatch"); + return Ok(None); + } + let (connection_id, session_id) = { (vm.connection_id.clone(), vm.session_id.clone()) }; + let ownership = OwnershipScope::vm(&connection_id, &session_id, vm_id); - let mut source_path = current_process_path.to_vec(); - source_path.push(child_process_id); + if self.capture_extension_process_output_event(vm_id, process_id, &event) { + return Ok(None); + } - if signal != 0 && target_pid < 0 { - let pgid = target_pid.unsigned_abs(); - let caller_kernel_pid = { - let Some(vm) = self.vms.get(vm_id) else { - return Err(SidecarError::InvalidState(String::from( - "ESRCH: unknown VM during process.kill", - ))); - }; - let Some(root) = vm.active_processes.get(process_id) else { - return Err(SidecarError::InvalidState(format!( - "ESRCH: unknown process {process_id} during process.kill", - ))); - }; - let Some(source) = Self::active_process_by_path(root, &source_path) else { - return Err(SidecarError::InvalidState(format!( - "ESRCH: unknown child process {child_process_id} during process.kill", - ))); + match event { + ActiveExecutionEvent::Stdout(chunk) => Ok(Some(EventFrame::new( + ownership, + EventPayload::ProcessOutput(ProcessOutputEvent { + process_id: process_id.to_owned(), + channel: StreamChannel::Stdout, + chunk, + }), + ))), + ActiveExecutionEvent::Stderr(chunk) => Ok(Some(EventFrame::new( + ownership, + EventPayload::ProcessOutput(ProcessOutputEvent { + process_id: process_id.to_owned(), + channel: StreamChannel::Stderr, + chunk, + }), + ))), + ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => { + self.handle_javascript_sync_rpc_request(vm_id, process_id, request)?; + Ok(None) + } + ActiveExecutionEvent::PythonVfsRpcRequest(request) => { + self.handle_python_vfs_rpc_request(vm_id, process_id, *request)?; + Ok(None) + } + ActiveExecutionEvent::SignalState { + signal, + registration, + } => { + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(None); }; - source.kernel_pid - }; - let caller_is_member = - self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal_name)?; - if !caller_is_member { - return Ok(Value::Null); + if !vm.active_processes.contains_key(process_id) { + return Ok(None); + } + vm.signal_states + .entry(process_id.to_owned()) + .or_default() + .insert(signal, registration); + Ok(None) } - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(Value::Null); - }; - let Some(root) = vm.active_processes.get_mut(process_id) else { - return Ok(Value::Null); - }; - let Some(source) = Self::active_process_by_path_mut(root, &source_path) else { - return Ok(Value::Null); - }; - source.pending_self_signal_exit = None; - if !matches!( - canonical_signal_name(signal), - Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG") - ) { - source.pending_self_signal_exit = Some(signal); + ActiveExecutionEvent::Exited(exit_code) => { + record_execute_response_to_exit_milestone( + "execute_response_to_exit_event_handle", + vm_id, + process_id, + ); + record_execute_response_to_exit(vm_id, process_id); + let phase_start = Instant::now(); + let became_idle = self + .finish_active_process_exit(vm_id, process_id, exit_code)? + .unwrap_or(false); + record_execute_phase("process_exit_cleanup", phase_start.elapsed()); + + let phase_start = Instant::now(); + if became_idle { + self.bridge.emit_lifecycle(vm_id, LifecycleState::Ready)?; + } + record_execute_phase("process_exit_lifecycle_emit", phase_start.elapsed()); + + Ok(Some(EventFrame::new( + ownership, + EventPayload::ProcessExited(ProcessExitedEvent { + process_id: process_id.to_owned(), + exit_code, + }), + ))) } - return Ok(json!({ - "self": true, - "action": "default", - })); } + } + pub(crate) fn finish_active_process_exit( + &mut self, + vm_id: &str, + process_id: &str, + exit_code: i32, + ) -> Result, SidecarError> { let Some(vm) = self.vms.get_mut(vm_id) else { - return Err(SidecarError::InvalidState(String::from( - "ESRCH: unknown VM during process.kill", - ))); + log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup"); + return Ok(None); }; - - if signal == 0 { - vm.kernel - .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal) - .map_err(kernel_error)?; - return Ok(Value::Null); + if !vm.active_processes.contains_key(process_id) { + log_stale_process_event(&self.bridge, vm_id, process_id, "process exit cleanup"); + return Ok(None); } - let target_kernel_pid = u32::try_from(target_pid).map_err(|_| { - SidecarError::InvalidState(format!("EINVAL: invalid process pid {target_pid}")) - })?; - let (source_pid, located_target_path) = { - let Some(root) = vm.active_processes.get(process_id) else { - return Err(SidecarError::InvalidState(format!( - "ESRCH: unknown process {process_id} during process.kill", - ))); - }; - let Some(source) = Self::active_process_by_path(root, &source_path) else { - return Err(SidecarError::InvalidState(format!( - "ESRCH: unknown child process {child_process_id} during process.kill", - ))); - }; - vm.kernel - .signal_process(EXECUTION_DRIVER_NAME, target_pid, 0) - .map_err(kernel_error)?; - ( - source.kernel_pid, - Self::active_process_path_by_kernel_pid(root, target_kernel_pid), - ) - }; - let Some(target_path) = located_target_path else { - // The target is alive but not part of this root's process tree. - // Resolve it VM-wide so cross-tree pids and untracked kernel - // processes still receive the signal. - self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal_name)?; - return Ok(Value::Null); - }; - let Some(vm) = self.vms.get_mut(vm_id) else { - return Err(SidecarError::InvalidState(String::from( - "ESRCH: unknown VM during process.kill", - ))); + let phase_start = Instant::now(); + prune_exited_process_snapshots(vm); + record_execute_phase( + "process_exit_cleanup_prune_snapshots", + phase_start.elapsed(), + ); + let phase_start = Instant::now(); + let process_table = vm.kernel.list_processes(); + record_execute_phase("process_exit_cleanup_list_processes", phase_start.elapsed()); + let phase_start = Instant::now(); + let Some(mut process) = vm.active_processes.remove(process_id) else { + return Ok(None); }; - - if source_pid == target_kernel_pid { - let Some(root) = vm.active_processes.get_mut(process_id) else { - return Ok(Value::Null); - }; - let Some(source) = Self::active_process_by_path_mut(root, &source_path) else { - return Ok(Value::Null); - }; - source.pending_self_signal_exit = None; - if !matches!( - canonical_signal_name(signal), - Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG") - ) { - source.pending_self_signal_exit = Some(signal); - } - return Ok(json!({ - "self": true, - "action": "default", - })); + record_execute_phase("process_exit_cleanup_remove_active", phase_start.elapsed()); + let phase_start = Instant::now(); + if let Some(info) = process_table.get(&process.kernel_pid) { + vm.exited_process_snapshots + .push_back(ExitedProcessSnapshot { + captured_at: Instant::now(), + process: build_process_snapshot_entry( + process_id, + &process, + info, + Some(exit_code), + ), + }); } - - let signal_key = target_path.last().map(String::as_str).unwrap_or(process_id); - let registration = vm - .signal_states - .get(signal_key) - .and_then(|handlers| handlers.get(&(signal as u32))) - .cloned(); - - let action = match registration - .as_ref() - .map(|registration| ®istration.action) - { - Some(SignalDispositionAction::Ignore) => "ignore", - Some(SignalDispositionAction::User) => { - let Some(root) = vm.active_processes.get_mut(process_id) else { - return Ok(Value::Null); - }; - let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path) - else { - return Err(SidecarError::InvalidState(format!( - "ESRCH: unknown process pid {target_pid}" - ))); - }; - if let Some(session) = target.execution.javascript_v8_session_handle().filter( - |_| matches!(&target.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime()) - || matches!(&target.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()), - ) { - dispatch_v8_session_signal_async(session, signal); - } else if !dispatch_v8_process_signal(target, signal)? { - return Err(SidecarError::InvalidState(format!( - "unsupported guest signal delivery for pid {target_pid}" - ))); - } - "user" - } - Some(SignalDispositionAction::Default) | None - if matches!( - canonical_signal_name(signal), - Some("SIGWINCH" | "SIGCHLD" | "SIGURG") - ) => - { - "ignore" - } - Some(SignalDispositionAction::Default) | None => { - let Some(root) = vm.active_processes.get_mut(process_id) else { - return Ok(Value::Null); - }; - let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path) - else { - return Err(SidecarError::InvalidState(format!( - "ESRCH: unknown process pid {target_pid}" - ))); - }; - apply_active_process_default_signal(&mut vm.kernel, target, signal)?; - "default" - } - }; - - let target_path_label = Self::child_process_path_label( - process_id, - &target_path.iter().map(String::as_str).collect::>(), + record_execute_phase("process_exit_cleanup_build_snapshot", phase_start.elapsed()); + let phase_start = Instant::now(); + let detached_children = Self::adopt_detached_child_processes(process_id, &mut process); + record_execute_phase("process_exit_cleanup_adopt_detached", phase_start.elapsed()); + let phase_start = Instant::now(); + let should_sync_host_writes = process.host_write_dirty_recursive() + || !process.clean_host_writes_are_observable_recursive(); + if should_sync_host_writes { + sync_process_host_writes_to_kernel(vm, &process)?; + } else { + record_execute_phase( + "process_exit_cleanup_sync_host_writes_clean_skip", + Duration::ZERO, + ); + } + record_execute_phase( + "process_exit_cleanup_sync_host_writes", + phase_start.elapsed(), ); - emit_security_audit_event( - &self.bridge, - vm_id, - "security.process.kill", - audit_fields([ - (String::from("source"), String::from("guest_process")), - (String::from("source_pid"), source_pid.to_string()), - (String::from("target_pid"), target_pid.to_string()), - (String::from("process_id"), process_id.to_owned()), - ( - String::from("target_process_path"), - target_path_label.clone(), - ), - (String::from("signal"), signal_name.to_owned()), - ]), + release_inherited_child_raw_mode(&mut vm.kernel, &process)?; + let phase_start = Instant::now(); + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + terminate_child_process_tree( + &mut vm.kernel, + &mut process, + &kernel_readiness, + &vm.unix_address_registry, ); + record_execute_phase( + "process_exit_cleanup_terminate_child_tree", + phase_start.elapsed(), + ); + let phase_start = Instant::now(); + process.kernel_handle.finish(exit_code); + record_execute_phase("process_exit_cleanup_kernel_finish", phase_start.elapsed()); + let phase_start = Instant::now(); + let _ = vm.kernel.wait_and_reap(process.kernel_pid); + record_execute_phase("process_exit_cleanup_wait_and_reap", phase_start.elapsed()); + let phase_start = Instant::now(); + vm.signal_states.remove(process_id); + record_execute_phase( + "process_exit_cleanup_signal_state_remove", + phase_start.elapsed(), + ); + let phase_start = Instant::now(); + for (detached_process_id, detached_child) in detached_children { + vm.detached_child_processes + .insert(detached_process_id.clone()); + vm.active_processes + .insert(detached_process_id, detached_child); + } + record_execute_phase( + "process_exit_cleanup_reinsert_detached", + phase_start.elapsed(), + ); + let phase_start = Instant::now(); + let became_idle = vm.active_processes.is_empty(); + record_execute_phase("process_exit_cleanup_became_idle", phase_start.elapsed()); + let phase_start = Instant::now(); + self.prune_extension_process_resource(process_id); + record_execute_phase("process_exit_cleanup_prune_resource", phase_start.elapsed()); - Ok(json!({ - "self": false, - "action": action, - "signal": signal_name, - "number": signal, - "targetProcessPath": target_path_label, - })) - } - - pub(crate) fn poll_javascript_child_process( - &mut self, - vm_id: &str, - process_id: &str, - child_process_id: &str, - wait_ms: u64, - ) -> Result { - self.poll_descendant_javascript_child_process( - vm_id, - process_id, - &[], - child_process_id, - wait_ms, - ) + Ok(Some(became_idle)) } - pub(crate) fn write_javascript_child_process_stdin( + pub(crate) fn handle_python_vfs_rpc_request( &mut self, vm_id: &str, process_id: &str, - child_process_id: &str, - chunk: &[u8], + request: PythonVfsRpcRequest, ) -> Result<(), SidecarError> { - let Some(vm) = self.vms.get_mut(vm_id) else { - return Err(javascript_child_process_gone_error( - process_id, - &[child_process_id], - )); - }; - let Some(child) = vm - .active_processes - .get_mut(process_id) - .ok_or_else(|| missing_process_error(vm_id, process_id))? - .child_processes - .get_mut(child_process_id) - else { - return Err(javascript_child_process_gone_error( - process_id, - &[child_process_id], - )); - }; - if let Err(error) = child.execution.write_stdin(chunk) { - if is_broken_pipe_error(&error) { - return Ok(()); + match request.method { + PythonVfsRpcMethod::Read + | PythonVfsRpcMethod::Write + | PythonVfsRpcMethod::Stat + | PythonVfsRpcMethod::Lstat + | PythonVfsRpcMethod::ReadDir + | PythonVfsRpcMethod::Mkdir + | PythonVfsRpcMethod::Unlink + | PythonVfsRpcMethod::Rmdir + | PythonVfsRpcMethod::Rename + | PythonVfsRpcMethod::Symlink + | PythonVfsRpcMethod::ReadLink + | PythonVfsRpcMethod::Setattr => { + filesystem_handle_python_vfs_rpc_request(self, vm_id, process_id, request) + } + PythonVfsRpcMethod::HttpRequest => { + self.handle_python_http_rpc_request(vm_id, process_id, request) + } + PythonVfsRpcMethod::DnsLookup => { + self.handle_python_dns_rpc_request(vm_id, process_id, request) + } + PythonVfsRpcMethod::SubprocessRun => { + self.handle_python_subprocess_rpc_request(vm_id, process_id, request) + } + PythonVfsRpcMethod::SocketConnect + | PythonVfsRpcMethod::SocketSend + | PythonVfsRpcMethod::SocketRecv + | PythonVfsRpcMethod::SocketClose + | PythonVfsRpcMethod::UdpCreate + | PythonVfsRpcMethod::UdpSendto + | PythonVfsRpcMethod::UdpRecvfrom => { + self.handle_python_socket_rpc_request(vm_id, process_id, request) } - return Err(error); } - write_kernel_process_stdin(&mut vm.kernel, child, chunk) - } - - pub(crate) fn close_javascript_child_process_stdin( - &mut self, - vm_id: &str, - process_id: &str, - child_process_id: &str, - ) -> Result<(), SidecarError> { - let Some(vm) = self.vms.get_mut(vm_id) else { - return Err(javascript_child_process_gone_error( - process_id, - &[child_process_id], - )); - }; - let Some(child) = vm - .active_processes - .get_mut(process_id) - .ok_or_else(|| missing_process_error(vm_id, process_id))? - .child_processes - .get_mut(child_process_id) - else { - return Err(javascript_child_process_gone_error( - process_id, - &[child_process_id], - )); - }; - child.execution.close_stdin()?; - close_kernel_process_stdin(&mut vm.kernel, child) } - pub(crate) fn kill_javascript_child_process( + fn handle_python_http_rpc_request( &mut self, vm_id: &str, process_id: &str, - child_process_id: &str, - signal: &str, + request: PythonVfsRpcRequest, ) -> Result<(), SidecarError> { - let signal_name = signal.to_owned(); - let signal = parse_signal(signal)?; let Some(vm) = self.vms.get_mut(vm_id) else { return Ok(()); }; - let process = vm - .active_processes - .get_mut(process_id) - .ok_or_else(|| missing_process_error(vm_id, process_id))?; - let source_pid = process.kernel_pid; - let child = process - .child_processes - .get_mut(child_process_id) - .ok_or_else(|| { - SidecarError::InvalidState(format!( - "unknown child process {child_process_id} during kill" - )) + if !vm.active_processes.contains_key(process_id) { + return Ok(()); + } + let response = (|| { + let url_text = request.url.as_deref().ok_or_else(|| { + SidecarError::InvalidState(String::from("python httpRequest requires a url")) })?; - terminate_tracked_child_process_for_signal(&mut vm.kernel, child, signal)?; - emit_security_audit_event( - &self.bridge, - vm_id, - "security.process.kill", - audit_fields([ - (String::from("source"), String::from("guest_child_process")), - (String::from("source_pid"), source_pid.to_string()), - (String::from("target_pid"), child.kernel_pid.to_string()), - (String::from("process_id"), process_id.to_owned()), - ( - String::from("child_process_id"), - child_process_id.to_owned(), - ), - (String::from("signal"), signal_name), - ]), - ); - Ok(()) - } + let url = Url::parse(url_text) + .map_err(|error| SidecarError::Execution(format!("ERR_INVALID_URL: {error}")))?; + let host = url.host_str().ok_or_else(|| { + SidecarError::Execution(String::from("ERR_INVALID_URL: missing host")) + })?; + let port = url.port_or_known_default().ok_or_else(|| { + SidecarError::Execution(String::from("ERR_INVALID_URL: missing port")) + })?; + self.bridge.require_network_access( + vm_id, + NetworkOperation::Http, + format_tcp_resource(host, port), + )?; + // Pin the outbound connection to the IP addresses that pass the + // egress range guard at resolution time. A literal IP is validated + // directly; a hostname is resolved once here and the resulting + // address set is pinned into the HTTP client's resolver below so a + // rebinding DNS server cannot make the second (TLS/TCP) lookup land + // on a private/link-local/metadata IP that this check rejected. + let pinned_addresses = if let Ok(literal_ip) = host.parse::() { + filter_dns_safe_ip_addrs(vec![literal_ip], host)? + } else { + filter_dns_safe_ip_addrs( + resolve_dns_ip_addrs( + &self.bridge, + &vm.kernel, + vm_id, + &vm.dns, + host, + DnsLookupPolicy::SkipPermissions, + )?, + host, + )? + }; + let mut headers = BTreeMap::new(); + for (name, value) in &request.headers { + headers.insert(name.clone(), Value::String(value.clone())); + } + let options = JavascriptHttpRequestOptions { + method: Some( + request + .http_method + .clone() + .unwrap_or_else(|| String::from("GET")), + ), + headers, + body: request.body_base64.as_deref().map(|body| { + String::from_utf8( + base64::engine::general_purpose::STANDARD + .decode(body) + .unwrap_or_default(), + ) + .unwrap_or_default() + }), + reject_unauthorized: None, + }; + let headers = + parse_http_header_collection(&options.headers, "python httpRequest headers")?; + let default_ca_bundle = if url.scheme() == "https" { + read_vm_default_ca_bundle(&mut vm.kernel)? + } else { + Vec::new() + }; + let response = issue_outbound_http_request( + &url, + &options, + &headers, + &pinned_addresses, + &default_ca_bundle, + )?; + let payload_json = response.as_str().ok_or_else(|| { + SidecarError::Execution(String::from( + "python httpRequest returned a non-string response payload", + )) + })?; + let payload: Value = serde_json::from_str(payload_json).map_err(|error| { + SidecarError::Execution(format!( + "python httpRequest response must be valid JSON: {error}" + )) + })?; + let header_map = payload + .get("headers") + .and_then(Value::as_array) + .map(|entries| { + let mut normalized = BTreeMap::>::new(); + for entry in entries { + let Some(pair) = entry.as_array() else { + continue; + }; + let Some(name) = pair.first().and_then(Value::as_str) else { + continue; + }; + let Some(value) = pair.get(1).and_then(Value::as_str) else { + continue; + }; + normalized + .entry(name.to_owned()) + .or_default() + .push(value.to_owned()); + } + normalized + }) + .unwrap_or_default(); + Ok(PythonVfsRpcResponsePayload::Http { + status: payload + .get("status") + .and_then(Value::as_u64) + .map(|value| value as u16) + .unwrap_or_default(), + reason: payload + .get("statusText") + .and_then(Value::as_str) + .unwrap_or_default() + .to_owned(), + url: payload + .get("url") + .and_then(Value::as_str) + .unwrap_or(url_text) + .to_owned(), + headers: header_map, + body_base64: payload + .get("body") + .and_then(Value::as_str) + .unwrap_or_default() + .to_owned(), + }) + })(); - /// Delivers a signal to one kernel pid inside a VM, resolving the target - /// through the active-process tree first so tracked sidecar executions get - /// the same termination handling as a direct `child_process.kill`. - /// Untracked kernel processes (for example WASM subprocess trees) receive - /// the signal through the kernel process table directly. - pub(crate) fn signal_vm_kernel_pid( + self.respond_python_rpc(vm_id, process_id, request.id, response) + } + + fn handle_python_dns_rpc_request( &mut self, vm_id: &str, - target_kernel_pid: u32, - signal_name: &str, + process_id: &str, + request: PythonVfsRpcRequest, ) -> Result<(), SidecarError> { - let signal = parse_signal(signal_name)?; - let located = { - let Some(vm) = self.vms.get(vm_id) else { - return Err(SidecarError::InvalidState(String::from( - "ESRCH: unknown VM during process.kill", - ))); - }; - let alive = vm - .kernel - .list_processes() - .get(&target_kernel_pid) - .is_some_and(|info| info.status != ProcessStatus::Exited); - if !alive { - return Err(SidecarError::InvalidState(format!( - "ESRCH: no such process {target_kernel_pid}" - ))); - } - vm.active_processes.iter().find_map(|(process_id, root)| { - Self::active_process_path_by_kernel_pid(root, target_kernel_pid) - .map(|path| (process_id.clone(), path)) - }) + let Some(vm) = self.vms.get(vm_id) else { + return Ok(()); }; - - match located { - Some((process_id, path)) if path.is_empty() => { - self.kill_process_internal(vm_id, &process_id, signal_name) - } - Some((process_id, path)) => { - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(()); - }; - let Some(root) = vm.active_processes.get_mut(&process_id) else { - return Ok(()); - }; - let Some(target) = Self::active_process_by_owned_path_mut(root, &path) else { - return Err(SidecarError::InvalidState(format!( - "ESRCH: no such process {target_kernel_pid}" - ))); - }; - terminate_tracked_child_process_for_signal(&mut vm.kernel, target, signal)?; - emit_security_audit_event( - &self.bridge, - vm_id, - "security.process.kill", - audit_fields([ - (String::from("source"), String::from("guest_process")), - (String::from("target_pid"), target_kernel_pid.to_string()), - (String::from("process_id"), process_id), - (String::from("signal"), signal_name.to_owned()), - ]), - ); - Ok(()) - } - None => { - let Some(vm) = self.vms.get_mut(vm_id) else { - return Ok(()); - }; - let target_pid = i32::try_from(target_kernel_pid).map_err(|_| { - SidecarError::InvalidState(format!( - "EINVAL: invalid process pid {target_kernel_pid}" - )) - })?; - vm.kernel - .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal) - .map_err(kernel_error)?; - emit_security_audit_event( + if !vm.active_processes.contains_key(process_id) { + return Ok(()); + } + let response = (|| { + let hostname = request.hostname.as_deref().ok_or_else(|| { + SidecarError::InvalidState(String::from("python dnsLookup requires a hostname")) + })?; + let mut addresses = filter_dns_safe_ip_addrs( + resolve_dns_ip_addrs( &self.bridge, + &vm.kernel, vm_id, - "security.process.kill", - audit_fields([ - (String::from("source"), String::from("guest_process")), - (String::from("target_pid"), target_kernel_pid.to_string()), - (String::from("signal"), signal_name.to_owned()), - ]), - ); - Ok(()) + &vm.dns, + hostname, + DnsLookupPolicy::CheckPermissions, + )?, + hostname, + )?; + if let Some(family) = request.family { + addresses.retain(|address| { + matches!((family, address), (4, IpAddr::V4(_)) | (6, IpAddr::V6(_))) + }); } - } + Ok(PythonVfsRpcResponsePayload::DnsLookup { + addresses: addresses + .into_iter() + .map(|address| address.to_string()) + .collect(), + }) + })(); + + self.respond_python_rpc(vm_id, process_id, request.id, response) } - /// Delivers a signal to every live member of a VM process group, matching - /// Linux `kill(-pgid, sig)` semantics. Returns whether the caller itself - /// is a member of the group so entry points can apply self-signal - /// delivery; the caller is intentionally skipped here. - pub(crate) fn signal_vm_process_group( + fn handle_python_subprocess_rpc_request( &mut self, vm_id: &str, - caller_kernel_pid: u32, - pgid: u32, - signal_name: &str, - ) -> Result { - parse_signal(signal_name)?; - let members = { - let Some(vm) = self.vms.get(vm_id) else { - return Err(SidecarError::InvalidState(String::from( - "ESRCH: unknown VM during process.kill", - ))); - }; - vm.kernel - .list_processes() - .into_iter() - .filter(|(_, info)| info.pgid == pgid && info.status != ProcessStatus::Exited) - .map(|(pid, _)| pid) - .collect::>() + process_id: &str, + request: PythonVfsRpcRequest, + ) -> Result<(), SidecarError> { + let command = request.command.clone().ok_or_else(|| { + SidecarError::InvalidState(String::from("python subprocessRun requires a command")) + })?; + let (internal_bootstrap_env, cwd) = { + let Some(vm) = self.vms.get(vm_id) else { + return Ok(()); + }; + let Some(process) = vm.active_processes.get(process_id) else { + return Ok(()); + }; + let virtual_home = guest_virtual_home(vm); + let cwd = request.cwd.clone().or_else(|| { + guest_runtime_path_for_host_path( + &vm.guest_env, + &virtual_home, + &vm.host_cwd, + &process.host_cwd.to_string_lossy(), + ) + }); + ( + sanitize_javascript_child_process_internal_bootstrap_env(&vm.guest_env), + cwd, + ) }; - if members.is_empty() { - return Err(SidecarError::InvalidState(format!( - "ESRCH: no such process group {pgid}" - ))); - } - - let mut caller_is_member = false; - for member_pid in members { - if member_pid == caller_kernel_pid { - caller_is_member = true; - continue; - } - match self.signal_vm_kernel_pid(vm_id, member_pid, signal_name) { - Ok(()) => {} - // Group members can exit while the group is being signaled. A - // vanished member is not an error for the group kill overall. - Err(error) if sidecar_error_is_esrch(&error) => {} - Err(error) => return Err(error), - } - } - Ok(caller_is_member) - } -} - -/// Applies a kill signal to a tracked child execution. Shared-runtime -/// executions for lethal signals are terminated directly with a synthetic -/// signal exit so child polls observe a prompt close; everything else routes -/// through the kernel process table. -fn terminate_tracked_child_process_for_signal( - kernel: &mut SidecarKernel, - child: &mut ActiveProcess, - signal: i32, -) -> Result<(), SidecarError> { - let should_terminate_shared_runtime = child.execution.uses_shared_v8_runtime() - && signal != 0 - && !matches!( - signal, - libc::SIGHUP - | libc::SIGINT - | libc::SIGTERM - | libc::SIGCHLD - | libc::SIGWINCH - | libc::SIGSTOP - | libc::SIGCONT - ); - if should_terminate_shared_runtime { - child.execution.terminate()?; - child.pending_self_signal_exit = Some(signal); - child.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?; - } else { - kernel - .kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, signal) - .map_err(kernel_error)?; - } - Ok(()) -} - -fn sidecar_error_is_esrch(error: &SidecarError) -> bool { - error.to_string().contains("ESRCH") -} - -fn apply_active_process_default_signal( - kernel: &mut SidecarKernel, - process: &mut ActiveProcess, - signal: i32, -) -> Result<(), SidecarError> { - if matches!(signal, libc::SIGSTOP | libc::SIGCONT) { - return kernel - .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal) - .map_err(kernel_error); - } + let response = self + .spawn_javascript_child_process_sync( + vm_id, + process_id, + JavascriptChildProcessSpawnRequest { + command, + args: request.args.clone(), + options: JavascriptChildProcessSpawnOptions { + argv0: request.argv0.clone(), + cloexec_fds: Vec::new(), + local_replacement: false, + executable_fd: None, + cwd, + env: request.env.clone(), + input: None, + internal_bootstrap_env, + spawn_attr_flags: 0, + spawn_exact_path: false, + spawn_search_path: None, + spawn_sched_policy: None, + spawn_sched_priority: None, + spawn_pgroup: None, + spawn_signal_defaults: Vec::new(), + spawn_signal_mask: Vec::new(), + spawn_file_actions: Vec::new(), + spawn_fd_mappings: Vec::new(), + spawn_host_net_fds: Vec::new(), + shell: request.shell, + detached: false, + stdio: vec![ + String::from("pipe"), + String::from("pipe"), + String::from("pipe"), + ], + timeout: None, + kill_signal: None, + }, + }, + request.max_buffer, + ) + .map(|payload| PythonVfsRpcResponsePayload::SubprocessRun { + exit_code: payload + .get("code") + .and_then(Value::as_i64) + .map(|value| value as i32) + .unwrap_or(1), + stdout: payload + .get("stdout") + .and_then(Value::as_str) + .unwrap_or_default() + .to_owned(), + stderr: payload + .get("stderr") + .and_then(Value::as_str) + .unwrap_or_default() + .to_owned(), + max_buffer_exceeded: payload + .get("maxBufferExceeded") + .and_then(Value::as_bool) + .unwrap_or(false), + }); - if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) { - close_kernel_process_stdin(kernel, process)?; + self.respond_python_rpc(vm_id, process_id, request.id, response) } - if process.execution.uses_shared_v8_runtime() { - process.execution.terminate()?; - if signal != 0 && matches!(process.execution, ActiveExecution::Wasm(_)) { - process.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?; + fn handle_python_socket_rpc_request( + &mut self, + vm_id: &str, + process_id: &str, + request: PythonVfsRpcRequest, + ) -> Result<(), SidecarError> { + if !self.vms.contains_key(vm_id) { + return Ok(()); } - return Ok(()); + let response = self.python_socket_op(vm_id, process_id, &request); + self.respond_python_rpc(vm_id, process_id, request.id, response) } - kernel - .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal) - .map_err(kernel_error) -} - -fn map_wasm_signal_registration( - registration: agentos_execution::wasm::WasmSignalHandlerRegistration, -) -> SignalHandlerRegistration { - SignalHandlerRegistration { - action: match registration.action { - agentos_execution::wasm::WasmSignalDispositionAction::Default => { - crate::protocol::SignalDispositionAction::Default + fn python_socket_op( + &mut self, + vm_id: &str, + process_id: &str, + request: &PythonVfsRpcRequest, + ) -> Result { + match request.method { + PythonVfsRpcMethod::SocketConnect => { + let host = python_socket_host(request)?; + let port = python_socket_port(request)?; + self.check_python_socket_limit(vm_id)?; + self.bridge.require_network_access( + vm_id, + NetworkOperation::Http, + format_tcp_resource(&host, port), + )?; + let pinned = self.python_socket_pinned_addrs(vm_id, &host, port)?; + let stream = python_connect_tcp(&pinned, port)?; + stream + .set_read_timeout(Some(PYTHON_SOCKET_READ_POLL)) + .map_err(python_socket_io_error)?; + // Bound writes too: without a write timeout `write_all` on a + // stalled peer would block the shared event loop indefinitely. + stream + .set_write_timeout(Some(PYTHON_SOCKET_WRITE_TIMEOUT)) + .map_err(python_socket_io_error)?; + let socket_id = + self.store_python_socket(vm_id, process_id, PythonHostSocket::Tcp(stream))?; + Ok(PythonVfsRpcResponsePayload::SocketCreated { socket_id }) } - agentos_execution::wasm::WasmSignalDispositionAction::Ignore => { - crate::protocol::SignalDispositionAction::Ignore + PythonVfsRpcMethod::SocketSend => { + let data = python_socket_payload(request)?; + let socket = self.python_socket_mut(vm_id, process_id, request)?; + let PythonHostSocket::Tcp(stream) = socket else { + return Err(python_socket_kind_error("send", "TCP")); + }; + stream.write_all(&data).map_err(python_socket_io_error)?; + Ok(PythonVfsRpcResponsePayload::SocketSent { + bytes_sent: data.len(), + }) } - agentos_execution::wasm::WasmSignalDispositionAction::User => { - crate::protocol::SignalDispositionAction::User + PythonVfsRpcMethod::SocketRecv => { + let max = python_socket_recv_len(request); + let socket = self.python_socket_mut(vm_id, process_id, request)?; + let PythonHostSocket::Tcp(stream) = socket else { + return Err(python_socket_kind_error("recv", "TCP")); + }; + let mut buf = vec![0u8; max]; + match stream.read(&mut buf) { + Ok(0) => Ok(PythonVfsRpcResponsePayload::SocketReceived { + data_base64: String::new(), + closed: true, + timed_out: false, + }), + Ok(n) => Ok(PythonVfsRpcResponsePayload::SocketReceived { + data_base64: base64::engine::general_purpose::STANDARD.encode(&buf[..n]), + closed: false, + timed_out: false, + }), + Err(error) if python_socket_would_block(&error) => { + Ok(PythonVfsRpcResponsePayload::SocketReceived { + data_base64: String::new(), + closed: false, + timed_out: true, + }) + } + Err(error) => Err(python_socket_io_error(error)), + } } - }, - mask: registration.mask, - flags: registration.flags, - } -} - -fn map_node_signal_registration( - registration: NodeSignalHandlerRegistration, -) -> SignalHandlerRegistration { - SignalHandlerRegistration { - action: match registration.action { - NodeSignalDispositionAction::Default => SignalDispositionAction::Default, - NodeSignalDispositionAction::Ignore => SignalDispositionAction::Ignore, - NodeSignalDispositionAction::User => SignalDispositionAction::User, - }, - mask: registration.mask, - flags: registration.flags, - } -} - -fn javascript_child_process_sync_input_bytes( - value: Option<&Value>, -) -> Result>, SidecarError> { - let Some(value) = value else { - return Ok(None); - }; - - match value { - Value::Null => Ok(None), - Value::String(text) => Ok(Some(text.as_bytes().to_vec())), - other => javascript_sync_rpc_bytes_arg( - std::slice::from_ref(other), - 0, - "child_process.spawn_sync input", - ) - .map(Some), + PythonVfsRpcMethod::SocketClose => { + self.remove_python_socket(vm_id, process_id, request); + Ok(PythonVfsRpcResponsePayload::Empty) + } + PythonVfsRpcMethod::UdpCreate => { + self.check_python_socket_limit(vm_id)?; + let socket = UdpSocket::bind("0.0.0.0:0").map_err(python_socket_io_error)?; + socket + .set_read_timeout(Some(PYTHON_SOCKET_READ_POLL)) + .map_err(python_socket_io_error)?; + let socket_id = + self.store_python_socket(vm_id, process_id, PythonHostSocket::Udp(socket))?; + Ok(PythonVfsRpcResponsePayload::SocketCreated { socket_id }) + } + PythonVfsRpcMethod::UdpSendto => { + let host = python_socket_host(request)?; + let port = python_socket_port(request)?; + let data = python_socket_payload(request)?; + self.bridge.require_network_access( + vm_id, + NetworkOperation::Http, + format_tcp_resource(&host, port), + )?; + let pinned = self.python_socket_pinned_addrs(vm_id, &host, port)?; + let target = pinned + .first() + .map(|ip| SocketAddr::new(*ip, port)) + .ok_or_else(|| { + SidecarError::Execution(format!("EAI_NONAME: cannot resolve {host}")) + })?; + let socket = self.python_socket_mut(vm_id, process_id, request)?; + let PythonHostSocket::Udp(udp) = socket else { + return Err(python_socket_kind_error("sendto", "UDP")); + }; + let sent = udp.send_to(&data, target).map_err(python_socket_io_error)?; + Ok(PythonVfsRpcResponsePayload::SocketSent { bytes_sent: sent }) + } + PythonVfsRpcMethod::UdpRecvfrom => { + let max = python_socket_recv_len(request); + let socket = self.python_socket_mut(vm_id, process_id, request)?; + let PythonHostSocket::Udp(udp) = socket else { + return Err(python_socket_kind_error("recvfrom", "UDP")); + }; + let mut buf = vec![0u8; max]; + match udp.recv_from(&mut buf) { + Ok((n, addr)) => Ok(PythonVfsRpcResponsePayload::UdpReceived { + data_base64: base64::engine::general_purpose::STANDARD.encode(&buf[..n]), + host: addr.ip().to_string(), + port: addr.port(), + timed_out: false, + }), + Err(error) if python_socket_would_block(&error) => { + Ok(PythonVfsRpcResponsePayload::UdpReceived { + data_base64: String::new(), + host: String::new(), + port: 0, + timed_out: true, + }) + } + Err(error) => Err(python_socket_io_error(error)), + } + } + _ => Err(SidecarError::InvalidState(String::from( + "non-socket python RPC reached the socket dispatcher unexpectedly", + ))), + } } -} -// bridge_permissions moved to crate::bridge + /// Resolve `host` to the egress-guard-approved IP set, then apply the same + /// loopback-connect gate the JS raw-TCP path uses (`filter_tcp_connect_ip_addrs`) + /// so a rebinding DNS server can't map an allowlisted hostname onto a + /// sidecar-local loopback port the VM policy didn't open. + fn python_socket_pinned_addrs( + &self, + vm_id: &str, + host: &str, + port: u16, + ) -> Result, SidecarError> { + let Some(vm) = self.vms.get(vm_id) else { + return Err(SidecarError::InvalidState(String::from( + "python socket op for unknown vm", + ))); + }; + let context = build_javascript_socket_path_context(vm)?; + if let Ok(literal_ip) = host.parse::() { + filter_tcp_connect_ip_addrs(vec![literal_ip], host, port, &context) + } else { + filter_tcp_connect_ip_addrs( + resolve_dns_ip_addrs( + &self.bridge, + &vm.kernel, + vm_id, + &vm.dns, + host, + DnsLookupPolicy::SkipPermissions, + )?, + host, + port, + &context, + ) + } + } -// reconcile_mounts, resolve_cwd moved to crate::vm + /// Enforce the VM's `max_sockets` resource limit before opening another + /// host socket for the guest (the registry is otherwise unbounded — a + /// hostile guest could exhaust the sidecar's fds/memory). + fn check_python_socket_limit(&self, vm_id: &str) -> Result<(), SidecarError> { + let Some(vm) = self.vms.get(vm_id) else { + return Ok(()); + }; + let limit = vm.kernel.resource_limits().max_sockets; + let current = vm_network_resource_counts(vm).sockets; + check_network_resource_limit(limit, current, 1, "socket") + } -fn resolve_execute_request( - vm: &VmState, - payload: &ExecuteRequest, -) -> Result { - let payload_env: BTreeMap = payload - .env - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(); - if let Some(command) = payload.command.as_deref() { - return resolve_command_execution( - vm, - command, - &payload.args, - &payload_env, - payload.cwd.as_deref(), - payload.wasm_permission_tier, - ); + fn store_python_socket( + &mut self, + vm_id: &str, + process_id: &str, + socket: PythonHostSocket, + ) -> Result { + let process = self + .vms + .get_mut(vm_id) + .and_then(|vm| vm.active_processes.get_mut(process_id)) + .ok_or_else(|| { + SidecarError::InvalidState(String::from("python socket op for reaped vm/process")) + })?; + let socket_id = process.next_python_socket_id; + process.next_python_socket_id = process.next_python_socket_id.wrapping_add(1); + process.python_sockets.insert(socket_id, socket); + Ok(socket_id) } - let runtime = payload.runtime.clone().ok_or_else(|| { - SidecarError::InvalidState(String::from("execute requires either command or runtime")) - })?; - let entrypoint = payload.entrypoint.clone().ok_or_else(|| { - SidecarError::InvalidState(String::from( - "execute requires either command or entrypoint", - )) - })?; - let (guest_cwd, host_cwd, allow_host_path_overrides) = - resolve_execution_cwds(vm, payload.cwd.as_deref()); - let mut env = vm.guest_env.clone(); - env.extend(payload_env.clone()); + fn python_socket_mut( + &mut self, + vm_id: &str, + process_id: &str, + request: &PythonVfsRpcRequest, + ) -> Result<&mut PythonHostSocket, SidecarError> { + let socket_id = request.socket_id.ok_or_else(|| { + SidecarError::InvalidState(String::from("python socket op requires socketId")) + })?; + self.vms + .get_mut(vm_id) + .and_then(|vm| vm.active_processes.get_mut(process_id)) + .and_then(|process| process.python_sockets.get_mut(&socket_id)) + .ok_or_else(|| { + SidecarError::Execution(format!("EBADF: unknown python socket {socket_id}")) + }) + } - let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint); - if requested_host_entrypoint.is_some() && !allow_host_path_overrides { - let requested_cwd = payload.cwd.as_deref().unwrap_or(guest_cwd.as_str()); - return Err(SidecarError::InvalidState(format!( - "execution cwd {requested_cwd} is outside sandbox root {}", - vm.host_cwd.to_string_lossy() - ))); + fn remove_python_socket( + &mut self, + vm_id: &str, + process_id: &str, + request: &PythonVfsRpcRequest, + ) { + let Some(socket_id) = request.socket_id else { + return; + }; + if let Some(process) = self + .vms + .get_mut(vm_id) + .and_then(|vm| vm.active_processes.get_mut(process_id)) + { + process.python_sockets.remove(&socket_id); + } } - let host_entrypoint_override = allow_host_path_overrides - .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint)) - .flatten(); - let guest_entrypoint = host_entrypoint_override - .as_ref() - .map(|(guest_entrypoint, _)| guest_entrypoint.clone()) - .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, &entrypoint)); - prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?; + fn respond_python_rpc( + &mut self, + vm_id: &str, + process_id: &str, + request_id: u64, + response: Result, + ) -> Result<(), SidecarError> { + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(()); + }; + let Some(process) = vm.active_processes.get_mut(process_id) else { + return Ok(()); + }; + let result = match response { + Ok(payload) => process + .execution + .respond_python_vfs_rpc_success(request_id, payload), + Err(error) => process.execution.respond_python_vfs_rpc_error( + request_id, + "ERR_AGENTOS_PYTHON_VFS_RPC", + error.to_string(), + ), + }; + match result { + Ok(()) => Ok(()), + Err(error) if is_broken_pipe_error(&error) => Ok(()), + Err(error) => Err(error), + } + } - Ok(ResolvedChildProcessExecution { - command: match runtime { - GuestRuntimeKind::JavaScript => String::from(JAVASCRIPT_COMMAND), - GuestRuntimeKind::Python => String::from(PYTHON_COMMAND), - GuestRuntimeKind::WebAssembly => String::from(WASM_COMMAND), - }, - process_args: std::iter::once(entrypoint.clone()) - .chain(payload.args.iter().cloned()) - .collect(), - runtime, - entrypoint: host_entrypoint_override - .map(|(_, host_entrypoint)| host_entrypoint) - .unwrap_or(entrypoint), - execution_args: payload.args.clone(), - env, - guest_cwd, - host_cwd, - wasm_permission_tier: payload.wasm_permission_tier, - tool_command: false, - }) -} - -fn resolve_command_execution( - vm: &VmState, - command: &str, - args: &[String], - extra_env: &BTreeMap, - cwd: Option<&str>, - explicit_wasm_permission_tier: Option, -) -> Result { - let (guest_cwd, host_cwd, allow_host_path_overrides) = resolve_execution_cwds(vm, cwd); - let mut env = vm.guest_env.clone(); - env.extend(extra_env.clone()); - let args = apply_shell_cwd_prefix(command, args.to_vec(), &guest_cwd); - - if is_tool_command(vm, command) { - let command = normalized_tool_command_name(command).unwrap_or_else(|| command.to_owned()); - return Ok(ResolvedChildProcessExecution { - command: command.clone(), - process_args: std::iter::once(command.clone()) - .chain(args.iter().cloned()) - .collect(), - runtime: GuestRuntimeKind::JavaScript, - entrypoint: command, - execution_args: args, - env, - guest_cwd, - host_cwd, - wasm_permission_tier: None, - tool_command: true, - }); - } - - if is_python_runtime_command(command) { - return resolve_python_command_execution(vm, command, &args, env, guest_cwd, host_cwd); + #[allow(dead_code)] + pub(crate) fn resolve_javascript_child_process_execution( + &self, + vm: &VmState, + parent_env: &BTreeMap, + parent_guest_cwd: &str, + parent_host_cwd: &Path, + request: &JavascriptChildProcessSpawnRequest, + ) -> Result { + self.resolve_javascript_child_process_execution_with_mode( + vm, + parent_env, + parent_guest_cwd, + parent_host_cwd, + request, + false, + None, + ) } - if is_node_runtime_command(command) { - if let Some(cli) = resolve_host_node_cli_entrypoint(command) { - env.insert( - String::from("AGENTOS_NODE_EVAL"), - build_host_node_cli_eval(&cli), - ); - prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?; - add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root); - add_runtime_host_access_path( - &mut env, - "AGENTOS_EXTRA_FS_READ_PATHS", - &cli.package_root, - true, - ); - - return Ok(ResolvedChildProcessExecution { - command: String::from(JAVASCRIPT_COMMAND), - process_args: std::iter::once(command.to_owned()) - .chain(args.iter().cloned()) - .collect(), - runtime: GuestRuntimeKind::JavaScript, - entrypoint: String::from("-e"), - execution_args: std::iter::once(cli.guest_entrypoint.clone()) - .chain(args.iter().cloned()) - .collect(), - env, - guest_cwd, - host_cwd, - wasm_permission_tier: None, - tool_command: false, - }); + pub(crate) fn resolve_javascript_child_process_execution_with_mode( + &self, + vm: &VmState, + parent_env: &BTreeMap, + parent_guest_cwd: &str, + parent_host_cwd: &Path, + request: &JavascriptChildProcessSpawnRequest, + exact_exec_path: bool, + search_path_override: Option<&str>, + ) -> Result { + if exact_exec_path && search_path_override.is_some() { + return Err(SidecarError::InvalidState(String::from( + "EINVAL: exact spawn path cannot also request PATH search", + ))); } + let mut runtime_env = parent_env.clone(); + runtime_env.extend(request.options.internal_bootstrap_env.clone()); + let (guest_cwd, host_cwd_override) = request + .options + .cwd + .as_deref() + .map(|cwd| { + let normalized_parent_host_cwd = normalize_host_path(parent_host_cwd); + let requested_host_cwd = normalize_host_path(Path::new(cwd)); + if path_is_within_root(&requested_host_cwd, &normalized_parent_host_cwd) { + let relative = requested_host_cwd + .strip_prefix(&normalized_parent_host_cwd) + .unwrap_or_else(|_| Path::new("")); + let relative = relative.to_string_lossy().replace('\\', "/"); + let guest_cwd = if relative.is_empty() { + parent_guest_cwd.to_owned() + } else { + normalize_path(&format!("{parent_guest_cwd}/{relative}")) + }; + (guest_cwd, Some(requested_host_cwd)) + } else if Path::new(cwd).is_relative() { + ( + normalize_path(&format!("{parent_guest_cwd}/{cwd}")), + Some(normalize_host_path(&parent_host_cwd.join(cwd))), + ) + } else { + (normalize_path(cwd), None) + } + }) + .unwrap_or_else(|| (parent_guest_cwd.to_owned(), None)); + let inherited_host_cwd = (host_cwd_override.is_none() && guest_cwd == parent_guest_cwd) + .then(|| normalize_host_path(parent_host_cwd)); + let host_cwd = host_cwd_override + .or(inherited_host_cwd) + .or_else(|| { + host_runtime_path_for_guest_path_with_env( + vm, + &runtime_env, + &guest_cwd, + parent_host_cwd, + ) + }) + .unwrap_or_else(|| { + let candidate = PathBuf::from(&guest_cwd); + if guest_cwd == parent_guest_cwd { + normalize_host_path(parent_host_cwd) + } else if candidate.is_absolute() { + shadow_path_for_guest(vm, &guest_cwd) + } else { + vm.host_cwd.clone() + } + }); + let mut env = parent_env.clone(); + env.extend(request.options.env.clone()); + // Child JavaScript executions must resolve their own entrypoint/eval state. + // Reusing the parent's values makes the sidecar load the wrong source file. + env.remove("AGENTOS_GUEST_ENTRYPOINT"); + env.remove("AGENTOS_NODE_EVAL"); - if args.is_empty() { - env.insert(String::from("AGENTOS_NODE_EVAL"), String::new()); - prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?; - + let (command, process_args) = if request.options.shell { + let tokens = tokenize_shell_free_command(&request.command); + let requires_shell = request.options.argv0.is_some() + || command_requires_shell(&request.command) + || tokens.first().is_some_and(|command| { + is_posix_shell_builtin(command) || shell_first_token_requires_shell(command) + }); + if requires_shell { + if !vm.command_guest_paths.contains_key("sh") { + return Err(SidecarError::InvalidState(format!( + "shell-mode child_process command requires /bin/sh, which is not \ + installed in this VM (install a software package that provides sh, \ + for example @agentos-software/coreutils): {}", + request.command + ))); + } + ( + String::from("sh"), + vec![String::from("-c"), request.command.clone()], + ) + } else { + let Some((command, args)) = tokens.split_first() else { + return Err(SidecarError::InvalidState(String::from( + "child_process shell command must not be empty", + ))); + }; + (command.clone(), args.to_vec()) + } + } else { + (request.command.clone(), request.args.clone()) + }; + let process_args = apply_shell_cwd_prefix(&command, process_args, &guest_cwd); + if !exact_exec_path && is_tool_command(vm, &command) { + let command = normalized_tool_command_name(&command).unwrap_or(command); return Ok(ResolvedChildProcessExecution { - command: String::from(JAVASCRIPT_COMMAND), - process_args: vec![command.to_owned()], + command: command.clone(), + process_args: std::iter::once(command.clone()) + .chain(process_args.iter().cloned()) + .collect(), runtime: GuestRuntimeKind::JavaScript, - entrypoint: String::from("-e"), - execution_args: Vec::new(), + entrypoint: command, + execution_args: process_args, env, guest_cwd, host_cwd, wasm_permission_tier: None, - tool_command: false, + tool_command: true, }); } - if let Some((entrypoint, execution_args)) = - resolve_special_node_cli_invocation(&args, &mut env) + if is_path_like_specifier(&command) + && matches!( + Path::new(&command).extension().and_then(|ext| ext.to_str()), + Some("js" | "mjs" | "cjs" | "ts" | "mts" | "cts") + ) { - prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?; + let guest_entrypoint = if command.starts_with('/') { + normalize_path(&command) + } else if command.starts_with("file:") { + normalize_path(command.trim_start_matches("file:")) + } else { + normalize_path(&format!("{guest_cwd}/{command}")) + }; + let host_entrypoint = if command.starts_with("./") || command.starts_with("../") { + normalize_host_path(&host_cwd.join(&command)) + } else { + host_runtime_path_for_guest_path_with_env( + vm, + &runtime_env, + &guest_entrypoint, + parent_host_cwd, + ) + .unwrap_or_else(|| { + let candidate = PathBuf::from(&guest_entrypoint); + if candidate.is_absolute() { + candidate + } else { + host_cwd.join(&guest_entrypoint) + } + }) + }; + env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint); + let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned(); + prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?; return Ok(ResolvedChildProcessExecution { - command: String::from(JAVASCRIPT_COMMAND), - process_args: std::iter::once(command.to_owned()) - .chain(args.iter().cloned()) + command: command.clone(), + process_args: std::iter::once(command) + .chain(process_args.iter().cloned()) .collect(), runtime: GuestRuntimeKind::JavaScript, - entrypoint, - execution_args, + entrypoint: host_entrypoint.to_string_lossy().into_owned(), + execution_args: process_args, env, guest_cwd, host_cwd, @@ -9434,2229 +9876,9922 @@ fn resolve_command_execution( }); } - let Some(entrypoint_specifier) = args.first() else { - return Err(SidecarError::InvalidState(format!( - "{command} execution requires an entrypoint" - ))); - }; + if !exact_exec_path && is_node_runtime_command(&command) { + if let Some(cli) = resolve_host_node_cli_entrypoint(&command) { + env.insert( + String::from("AGENTOS_NODE_EVAL"), + build_host_node_cli_eval(&cli), + ); + prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?; + add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root); + add_runtime_host_access_path( + &mut env, + "AGENTOS_EXTRA_FS_READ_PATHS", + &cli.package_root, + true, + ); - let (entrypoint, execution_args, guest_entrypoint) = { - let requested_host_entrypoint = - resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier); - if requested_host_entrypoint.is_some() && !allow_host_path_overrides { - let requested_cwd = cwd.unwrap_or(guest_cwd.as_str()); + return Ok(ResolvedChildProcessExecution { + command: command.clone(), + process_args: std::iter::once(command.clone()) + .chain(process_args.iter().cloned()) + .collect(), + runtime: GuestRuntimeKind::JavaScript, + entrypoint: String::from("-e"), + execution_args: std::iter::once(cli.guest_entrypoint.clone()) + .chain(process_args.iter().cloned()) + .collect(), + env, + guest_cwd, + host_cwd, + wasm_permission_tier: None, + tool_command: false, + }); + } + + if process_args.is_empty() { + env.insert(String::from("AGENTOS_NODE_EVAL"), String::new()); + prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?; + + return Ok(ResolvedChildProcessExecution { + command: command.clone(), + process_args: vec![command.clone()], + runtime: GuestRuntimeKind::JavaScript, + entrypoint: String::from("-e"), + execution_args: Vec::new(), + env, + guest_cwd, + host_cwd, + wasm_permission_tier: None, + tool_command: false, + }); + } + + if let Some((entrypoint, execution_args)) = + resolve_special_node_cli_invocation(&process_args, &mut env) + { + prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?; + + return Ok(ResolvedChildProcessExecution { + command: command.clone(), + process_args: std::iter::once(command.clone()) + .chain(process_args.iter().cloned()) + .collect(), + runtime: GuestRuntimeKind::JavaScript, + entrypoint, + execution_args, + env, + guest_cwd, + host_cwd, + wasm_permission_tier: None, + tool_command: false, + }); + } + + let Some(entrypoint_specifier) = process_args.first() else { return Err(SidecarError::InvalidState(format!( - "execution cwd {requested_cwd} is outside sandbox root {}", - vm.host_cwd.to_string_lossy() + "{command} child_process spawn requires an entrypoint" ))); - } - let host_entrypoint_override = allow_host_path_overrides - .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier)) - .flatten(); - let guest_entrypoint = host_entrypoint_override - .as_ref() - .map(|(guest_entrypoint, _)| guest_entrypoint.clone()) - .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, entrypoint_specifier)); - let entrypoint = host_entrypoint_override.map_or_else( - || { - guest_entrypoint.as_ref().map_or_else( - || entrypoint_specifier.clone(), - |guest_entrypoint| { - resolve_vm_guest_path_to_host(vm, guest_entrypoint) - .to_string_lossy() - .into_owned() - }, + }; + + let (entrypoint, execution_args) = if is_path_like_specifier(entrypoint_specifier) { + let guest_entrypoint = if entrypoint_specifier.starts_with('/') { + normalize_path(entrypoint_specifier) + } else if entrypoint_specifier.starts_with("file:") { + normalize_path(entrypoint_specifier.trim_start_matches("file:")) + } else { + normalize_path(&format!("{guest_cwd}/{entrypoint_specifier}")) + }; + let host_entrypoint = if entrypoint_specifier.starts_with("./") + || entrypoint_specifier.starts_with("../") + { + normalize_host_path(&host_cwd.join(entrypoint_specifier)) + } else { + host_runtime_path_for_guest_path_with_env( + vm, + &runtime_env, + &guest_entrypoint, + parent_host_cwd, ) - }, - |(_, host_entrypoint)| host_entrypoint, - ); - ( + .unwrap_or_else(|| { + let candidate = PathBuf::from(&guest_entrypoint); + if candidate.is_absolute() { + candidate + } else { + host_cwd.join(&guest_entrypoint) + } + }) + }; + env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint); + ( + host_entrypoint.to_string_lossy().into_owned(), + process_args.iter().skip(1).cloned().collect(), + ) + } else { + ( + entrypoint_specifier.clone(), + process_args.iter().skip(1).cloned().collect(), + ) + }; + let guest_entrypoint = env.get("AGENTOS_GUEST_ENTRYPOINT").cloned(); + prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?; + + return Ok(ResolvedChildProcessExecution { + command: command.clone(), + process_args: std::iter::once(command) + .chain(process_args.iter().cloned()) + .collect(), + runtime: GuestRuntimeKind::JavaScript, entrypoint, - args.iter().skip(1).cloned().collect(), - guest_entrypoint, + execution_args, + env, + guest_cwd, + host_cwd, + wasm_permission_tier: None, + tool_command: false, + }); + } + + if !exact_exec_path && is_python_runtime_command(&command) { + return resolve_python_command_execution( + vm, + &command, + &process_args, + env, + guest_cwd, + host_cwd, + ); + } + + let guest_entrypoint = if exact_exec_path { + resolve_exact_guest_command_entrypoint(vm, &guest_cwd, &command) + } else { + resolve_guest_command_entrypoint( + vm, + &guest_cwd, + &command, + search_path_override.or_else(|| env.get("PATH").map(String::as_str)), ) - }; + } + .ok_or_else(|| SidecarError::InvalidState(format!("command not found: {command}")))?; + let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint); + let wasm_permission_tier = vm.command_permissions.get(&command).copied().or_else(|| { + Path::new(&guest_entrypoint) + .file_name() + .and_then(|name| name.to_str()) + .and_then(|name| vm.command_permissions.get(name).copied()) + }); + if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) = + resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint) + { + prepare_guest_runtime_env( + vm, + &mut env, + &guest_cwd, + &host_cwd, + Some(javascript_guest_entrypoint), + )?; - prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?; + return Ok(ResolvedChildProcessExecution { + command: command.clone(), + process_args: std::iter::once(command) + .chain(process_args.iter().cloned()) + .collect(), + runtime: GuestRuntimeKind::JavaScript, + entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(), + execution_args: process_args, + env, + guest_cwd, + host_cwd, + wasm_permission_tier: None, + tool_command: false, + }); + } + prepare_guest_runtime_env( + vm, + &mut env, + &guest_cwd, + &host_cwd, + Some(guest_entrypoint.clone()), + )?; - return Ok(ResolvedChildProcessExecution { - command: String::from(JAVASCRIPT_COMMAND), - process_args: std::iter::once(command.to_owned()) - .chain(args.iter().cloned()) + Ok(ResolvedChildProcessExecution { + command: command.clone(), + process_args: std::iter::once(command) + .chain(process_args.iter().cloned()) .collect(), - runtime: GuestRuntimeKind::JavaScript, - entrypoint, - execution_args, + runtime: GuestRuntimeKind::WebAssembly, + entrypoint: host_entrypoint.to_string_lossy().into_owned(), + execution_args: process_args, env, guest_cwd, host_cwd, - wasm_permission_tier: None, + wasm_permission_tier, tool_command: false, - }); + }) } - if command.ends_with(".js") || command.ends_with(".mjs") || command.ends_with(".cjs") { - let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, command); - if requested_host_entrypoint.is_some() && !allow_host_path_overrides { - let requested_cwd = cwd.unwrap_or(guest_cwd.as_str()); - return Err(SidecarError::InvalidState(format!( - "execution cwd {requested_cwd} is outside sandbox root {}", - vm.host_cwd.to_string_lossy() - ))); - } - let host_entrypoint_override = allow_host_path_overrides - .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, command)) - .flatten(); - let guest_entrypoint = host_entrypoint_override - .as_ref() - .map(|(guest_entrypoint, _)| guest_entrypoint.clone()) - .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, command)); - let entrypoint = host_entrypoint_override.map_or_else( - || { - guest_entrypoint.as_ref().map_or_else( - || command.to_owned(), - |guest_entrypoint| { - resolve_vm_guest_path_to_host(vm, guest_entrypoint) - .to_string_lossy() - .into_owned() - }, - ) - }, - |(_, host_entrypoint)| host_entrypoint, - ); - prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?; - - return Ok(ResolvedChildProcessExecution { - command: String::from(JAVASCRIPT_COMMAND), - process_args: std::iter::once(command.to_owned()) - .chain(args.iter().cloned()) - .collect(), - runtime: GuestRuntimeKind::JavaScript, - entrypoint, - execution_args: args.to_vec(), - env, - guest_cwd, - host_cwd, - wasm_permission_tier: None, - tool_command: false, - }); - } + fn resolve_javascript_child_process_with_shebang( + &mut self, + vm_id: &str, + parent_env: &BTreeMap, + parent_guest_cwd: &str, + parent_host_cwd: &Path, + request: &mut JavascriptChildProcessSpawnRequest, + ) -> Result { + const MAX_SHEBANG_REDIRECTS: usize = 4; - let guest_entrypoint = resolve_guest_command_entrypoint( - vm, - &guest_cwd, - command, - env.get("PATH").map(String::as_str), - ) - .ok_or_else(|| { - SidecarError::InvalidState(format!( - "command not found on native sidecar path: {command}" - )) - })?; - let wasm_permission_tier = explicit_wasm_permission_tier - .or_else(|| vm.command_permissions.get(command).copied()) - .or_else(|| { - Path::new(&guest_entrypoint) - .file_name() - .and_then(|name| name.to_str()) - .and_then(|name| vm.command_permissions.get(name).copied()) - }); + let mut resolved = { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + self.resolve_javascript_child_process_execution_with_mode( + vm, + parent_env, + parent_guest_cwd, + parent_host_cwd, + request, + false, + None, + )? + }; - let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint); - if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) = - resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint) - { - prepare_guest_runtime_env( - vm, - &mut env, - &guest_cwd, - &host_cwd, - Some(javascript_guest_entrypoint), - )?; + for redirects in 0..=MAX_SHEBANG_REDIRECTS { + let redirected = { + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + rewrite_javascript_shebang_request(vm, &resolved, request)? + }; + if !redirected { + return Ok(resolved); + } + if redirects == MAX_SHEBANG_REDIRECTS { + return Err(SidecarError::Execution(format!( + "ELOOP: exceeded {MAX_SHEBANG_REDIRECTS} shebang redirects" + ))); + } + resolved = { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + self.resolve_javascript_child_process_execution_with_mode( + vm, + parent_env, + parent_guest_cwd, + parent_host_cwd, + request, + false, + None, + )? + }; + } - return Ok(ResolvedChildProcessExecution { - command: command.to_owned(), - process_args: std::iter::once(command.to_owned()) - .chain(args.iter().cloned()) - .collect(), - runtime: GuestRuntimeKind::JavaScript, - entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(), - execution_args: args.to_vec(), - env, - guest_cwd, - host_cwd, - wasm_permission_tier: None, - tool_command: false, - }); + Ok(resolved) } - prepare_guest_runtime_env( - vm, - &mut env, - &guest_cwd, - &host_cwd, - Some(guest_entrypoint.clone()), - )?; - - Ok(ResolvedChildProcessExecution { - command: command.to_owned(), - process_args: std::iter::once(command.to_owned()) - .chain(args.iter().cloned()) - .collect(), - runtime: GuestRuntimeKind::WebAssembly, - entrypoint: host_entrypoint.to_string_lossy().into_owned(), - execution_args: args.to_vec(), - env, - guest_cwd, - host_cwd, - wasm_permission_tier, - tool_command: false, - }) -} -const MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH: usize = 4; - -fn resolve_javascript_command_entrypoint( - vm: &VmState, - guest_entrypoint: &str, - host_entrypoint: &Path, -) -> Option<(String, PathBuf)> { - // agentOS package content is served guest-native (tar + single-symlink - // mounts) and is never materialized on the host, so the shebang-reading - // fallback below (which reads the host path) cannot classify these - // entrypoints. Within the package mount the only runtimes are WebAssembly - // (`*.wasm`) and JavaScript, and `bin/` launchers are frequently - // extensionless — so classify by extension here: `.wasm` is WASM (fall - // through), everything else in the mount is JavaScript. - if guest_path_is_within_agentos_package_mount(vm, guest_entrypoint) { - let extension = Path::new(guest_entrypoint) - .extension() - .and_then(|extension| extension.to_str()); - if extension != Some("wasm") { - return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf())); + pub(crate) fn spawn_javascript_child_process( + &mut self, + vm_id: &str, + process_id: &str, + mut request: JavascriptChildProcessSpawnRequest, + ) -> Result { + let spawn_attributes = javascript_spawn_attributes(&request.options)?; + let requested_pgid = spawn_attributes.process_group; + let parent_sync_roots = { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + let parent = vm + .active_processes + .get(process_id) + .ok_or_else(|| missing_process_error(vm_id, process_id))?; + (parent.host_write_dirty_recursive() + || !parent.clean_host_writes_are_observable_recursive()) + .then(|| (parent.host_cwd.clone(), parent.guest_cwd.clone())) + }; + if let Some((host_cwd, guest_cwd)) = parent_sync_roots { + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + sync_process_host_roots_to_kernel(vm, &host_cwd, &guest_cwd)?; + } + let prepared_host_net_fds = { + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + let current_network_counts = vm_spawn_host_net_resource_counts(vm); + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let parent = active_processes + .get_mut(process_id) + .ok_or_else(|| missing_process_error(vm_id, process_id))?; + prepare_spawn_host_net_fds( + kernel, + parent, + current_network_counts, + &request.options.spawn_host_net_fds, + &request.options.spawn_fd_mappings, + &request.options.spawn_file_actions, + )? + }; + let prepared_spawn_actions = if !prepared_host_net_fds.kernel_actions.is_empty() { + let (parent_pid, parent_cwd) = { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + let parent = vm + .active_processes + .get(process_id) + .ok_or_else(|| missing_process_error(vm_id, process_id))?; + let initial_cwd = request + .options + .cwd + .as_deref() + .map(|cwd| { + if cwd.starts_with('/') { + normalize_path(cwd) + } else { + normalize_path(&format!("{}/{cwd}", parent.guest_cwd)) + } + }) + .unwrap_or_else(|| parent.guest_cwd.clone()); + (parent.kernel_pid, initial_cwd) + }; + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + Some(preapply_posix_spawn_file_actions( + &mut vm.kernel, + parent_pid, + &parent_cwd, + requested_pgid, + &request.options.spawn_fd_mappings, + &prepared_host_net_fds.kernel_actions, + )?) + } else { + None + }; + if let Some(prepared) = prepared_spawn_actions.as_ref() { + request.options.cwd = Some(prepared.cwd.clone()); + } + { + let parent_guest_cwd = self + .vms + .get(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))? + .active_processes + .get(process_id) + .ok_or_else(|| missing_process_error(vm_id, process_id))? + .guest_cwd + .clone(); + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + resolve_posix_spawn_program(vm, &parent_guest_cwd, &mut request)?; + } + let total_start = Instant::now(); + let phase_start = Instant::now(); + let (parent_env, parent_guest_cwd, parent_host_cwd) = { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + let parent = vm + .active_processes + .get(process_id) + .ok_or_else(|| missing_process_error(vm_id, process_id))?; + ( + parent.env.clone(), + parent.guest_cwd.clone(), + parent.host_cwd.clone(), + ) + }; + let mut resolved = + if !request.options.spawn_exact_path && request.options.spawn_search_path.is_none() { + self.resolve_javascript_child_process_with_shebang( + vm_id, + &parent_env, + &parent_guest_cwd, + &parent_host_cwd, + &mut request, + )? + } else { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + self.resolve_javascript_child_process_execution_with_mode( + vm, + &parent_env, + &parent_guest_cwd, + &parent_host_cwd, + &request, + request.options.spawn_exact_path, + request.options.spawn_search_path.as_deref(), + )? + }; + apply_child_process_argv0(&mut resolved, request.options.argv0.as_deref()); + { + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + stage_agentos_package_command(vm, &mut resolved)?; + } + let resolved = resolved; + if prepared_host_net_fds.inherited_fd_count() != 0 + && (resolved.runtime != GuestRuntimeKind::WebAssembly || resolved.tool_command) + { + return Err(SidecarError::InvalidState(String::from( + "ENOTSUP: inherited host-network fds require a WebAssembly child runtime", + ))); + } + record_execute_phase("child_process_resolve_execution", phase_start.elapsed()); + let (parent_kernel_pid, child_process_id) = { + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + let process = vm + .active_processes + .get_mut(process_id) + .ok_or_else(|| missing_process_error(vm_id, process_id))?; + (process.kernel_pid, process.allocate_child_process_id()) + }; + let sidecar_requests = self.sidecar_requests.clone(); + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + let vm_pending_stdin_bytes_budget = Arc::clone(&vm.pending_stdin_bytes_budget); + let vm_pending_event_bytes_budget = Arc::clone(&vm.pending_event_bytes_budget); + let phase_start = Instant::now(); + let mut pending_kernel_handle = None; + let spawn_result = (|| { + let spawned = if resolved.tool_command { + let tool_resolution = resolve_tool_command( + vm, + &resolved.command, + &resolved.execution_args, + Some(&resolved.guest_cwd), + )? + .ok_or_else(|| { + SidecarError::InvalidState(format!( + "tool command no longer resolves: {}", + resolved.command + )) + })?; + let kernel_handle = vm + .kernel + .create_virtual_process_with_process_group( + EXECUTION_DRIVER_NAME, + TOOL_DRIVER_NAME, + &resolved.command, + resolved.process_args.clone(), + VirtualProcessOptions { + parent_pid: Some(parent_kernel_pid), + env: resolved.env.clone(), + cwd: Some(resolved.guest_cwd.clone()), + }, + requested_pgid, + ) + .map_err(kernel_error)?; + let kernel_pid = kernel_handle.pid(); + if let Some(prepared) = prepared_spawn_actions { + install_preapplied_posix_spawn_file_actions( + &mut vm.kernel, + &kernel_handle, + prepared, + )?; + } else { + apply_posix_spawn_file_actions_or_rollback( + &mut vm.kernel, + &kernel_handle, + &resolved.guest_cwd, + &request.options.spawn_fd_mappings, + &prepared_host_net_fds.kernel_actions, + )?; + } + apply_spawn_session_or_rollback( + &mut vm.kernel, + &kernel_handle, + spawn_attributes.new_session || request.options.detached, + )?; + pending_kernel_handle = Some(kernel_handle.clone()); + let tool_execution = ToolExecution::default() + .with_vm_pending_event_bytes_budget(Arc::clone(&vm_pending_event_bytes_budget)); + let cancelled = tool_execution.cancelled.clone(); + let pending_events = tool_execution.pending_events.clone(); + let event_overflow_reason = tool_execution.event_overflow_reason.clone(); + let pending_event_bytes = tool_execution.pending_event_bytes.clone(); + let pending_event_count_limit = tool_execution.pending_event_count_limit.clone(); + let pending_event_bytes_limit = tool_execution.pending_event_bytes_limit.clone(); + spawn_tool_process_events(ToolProcessEventRequest { + sidecar_requests: sidecar_requests.clone(), + connection_id: vm.connection_id.clone(), + session_id: vm.session_id.clone(), + vm_id: vm_id.to_owned(), + tool_resolution, + cancelled, + pending_events, + event_overflow_reason, + pending_event_bytes, + pending_event_count_limit, + pending_event_bytes_limit, + vm_pending_event_bytes_budget: Arc::clone(&vm_pending_event_bytes_budget), + }); + ( + kernel_pid, + kernel_handle, + ActiveExecution::Tool(tool_execution), + None, + false, + ) + } else { + let kernel_command = match resolved.runtime { + GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND, + GuestRuntimeKind::WebAssembly => WASM_COMMAND, + GuestRuntimeKind::Python => PYTHON_COMMAND, + }; + let kernel_handle = vm + .kernel + .spawn_process_with_process_group( + kernel_command, + resolved.process_args.clone(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(parent_kernel_pid), + env: resolved.env.clone(), + cwd: Some(resolved.guest_cwd.clone()), + }, + requested_pgid, + ) + .map_err(kernel_error)?; + let kernel_pid = kernel_handle.pid(); + let applied_spawn_actions = if let Some(prepared) = prepared_spawn_actions { + install_preapplied_posix_spawn_file_actions( + &mut vm.kernel, + &kernel_handle, + prepared, + )? + } else { + apply_posix_spawn_file_actions_or_rollback( + &mut vm.kernel, + &kernel_handle, + &resolved.guest_cwd, + &request.options.spawn_fd_mappings, + &prepared_host_net_fds.kernel_actions, + )? + }; + let posix_spawn_controls_stdin = !request.options.spawn_file_actions.is_empty() + && (applied_spawn_actions + .fd_mappings + .iter() + .any(|mapping| mapping[0] == 0) + || applied_spawn_actions.closed_guest_fds.contains(&0) + || prepared_host_net_fds + .descriptions + .iter() + .any(|description| description.guest_fds.contains(&0))); + apply_spawn_session_or_rollback( + &mut vm.kernel, + &kernel_handle, + spawn_attributes.new_session || request.options.detached, + )?; + pending_kernel_handle = Some(kernel_handle.clone()); + let mut execution_env = resolved.env.clone(); + if resolved.runtime == GuestRuntimeKind::WebAssembly { + execution_env.insert( + String::from("AGENTOS_WASM_INHERITED_FD_MAPPINGS"), + serde_json::to_string(&applied_spawn_actions.fd_mappings).map_err( + |error| { + SidecarError::InvalidState(format!( + "failed to serialize inherited WASM fd mappings: {error}" + )) + }, + )?, + ); + execution_env.insert( + String::from("AGENTOS_WASM_CLOSED_INHERITED_FDS"), + serde_json::to_string(&applied_spawn_actions.closed_guest_fds).map_err( + |error| { + SidecarError::InvalidState(format!( + "failed to serialize closed inherited WASM fds: {error}" + )) + }, + )?, + ); + execution_env.insert( + String::from("AGENTOS_WASM_INHERITED_HOSTNET_FDS"), + serde_json::to_string(&prepared_host_net_fds.bootstrap_json()).map_err( + |error| { + SidecarError::InvalidState(format!( + "failed to serialize inherited WASM host-network fds: {error}" + )) + }, + )?, + ); + } + execution_env.insert( + String::from(EXECUTION_SANDBOX_ROOT_ENV), + normalize_host_path(&vm.cwd).to_string_lossy().into_owned(), + ); + + let execution = match resolved.runtime { + GuestRuntimeKind::JavaScript => { + execution_env.extend( + sanitize_javascript_child_process_internal_bootstrap_env( + &request.options.internal_bootstrap_env, + ), + ); + execution_env + .insert(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1")); + let launch_entrypoint = + resolve_agentos_package_javascript_launch_entrypoint( + vm, + &mut execution_env, + ) + .unwrap_or_else(|| resolved.entrypoint.clone()); + let inline_code = load_javascript_entrypoint_source( + vm, + &resolved.host_cwd, + &launch_entrypoint, + &execution_env, + ); + prepare_javascript_shadow(vm, &resolved, &execution_env)?; + + let built_reader = build_module_reader(vm, &resolved); + let guest_reader = built_reader.clone().map(|reader| { + Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader)) + as Box + }); + let module_reader = built_reader + .map(|reader| Box::new(reader) as Box); + let context = + self.javascript_engine + .create_context(CreateJavascriptContextRequest { + vm_id: vm_id.to_owned(), + bootstrap_module: None, + compile_cache_root: Some( + self.cache_root.join("node-compile-cache"), + ), + }); + let context_id = context.context_id; + let execution_result = + self.javascript_engine.start_execution_with_module_reader( + StartJavascriptExecutionRequest { + guest_runtime: guest_runtime_identity( + vm, + Some(u64::from(kernel_pid)), + Some(u64::from(parent_kernel_pid)), + ), + vm_id: vm_id.to_owned(), + context_id: context_id.clone(), + argv: std::iter::once(launch_entrypoint) + .chain(resolved.execution_args.clone()) + .collect(), + argv0: request.options.argv0.clone(), + env: execution_env, + cwd: resolved.host_cwd.clone(), + limits: javascript_execution_limits(vm), + inline_code, + wasm_module_bytes: None, + }, + module_reader, + guest_reader, + ); + self.javascript_engine.dispose_context(&context_id); + let execution = execution_result.map_err(javascript_error)?; + ActiveExecution::Javascript(execution) + } + GuestRuntimeKind::WebAssembly => { + // These values configure the trusted WASM runner, not the + // guest's Linux-visible envp. The execution layer filters + // every AGENTOS_* key from AGENTOS_GUEST_ENV before WASI + // constructs environ, while still making the inherited + // signal state available during runner bootstrap. + execution_env.extend( + sanitize_javascript_child_process_internal_bootstrap_env( + &request.options.internal_bootstrap_env, + ), + ); + execution_env + .insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1")); + execution_env + .insert(String::from(WASM_EXEC_COMMIT_RPC_ENV), String::from("1")); + let wasm_limits = wasm_execution_limits(vm); + let wasm_guest_runtime = guest_runtime_identity( + vm, + Some(u64::from(kernel_pid)), + Some(u64::from(parent_kernel_pid)), + ); + let context = self.wasm_engine.create_context(CreateWasmContextRequest { + vm_id: vm_id.to_owned(), + module_path: Some(resolved.entrypoint.clone()), + }); + let context_id = context.context_id; + let execution_result = + self.wasm_engine.start_execution(StartWasmExecutionRequest { + vm_id: vm_id.to_owned(), + context_id: context_id.clone(), + argv: resolved.process_args.clone(), + env: execution_env, + cwd: resolved.host_cwd.clone(), + permission_tier: execution_wasm_permission_tier( + resolved + .wasm_permission_tier + .unwrap_or(WasmPermissionTier::Full), + ), + limits: wasm_limits, + guest_runtime: wasm_guest_runtime, + }); + self.wasm_engine.dispose_context(&context_id); + let execution = execution_result.map_err(wasm_error)?; + ActiveExecution::Wasm(Box::new(execution)) + } + GuestRuntimeKind::Python => { + // Nested `python` child_process: set up the Pyodide context the + // same way the top-level execute path does, so a guest shell or + // node parent can spawn `python` exactly like `node`. + let python_file_path = if execution_env.contains_key("AGENTOS_PYTHON_ARGV") + { + execution_env.get("AGENTOS_PYTHON_FILE").map(PathBuf::from) + } else { + python_file_entrypoint(&resolved.entrypoint) + }; + let pyodide_dist_path = self + .python_engine + .bundled_pyodide_dist_path_for_vm(vm_id) + .map_err(python_error)?; + let pyodide_cache_path = pyodide_dist_path + .parent() + .and_then(Path::parent) + .unwrap_or(pyodide_dist_path.as_path()) + .join("pyodide-package-cache"); + add_runtime_guest_path_mapping( + &mut execution_env, + PYTHON_PYODIDE_GUEST_ROOT, + &pyodide_dist_path, + ); + add_runtime_guest_path_mapping( + &mut execution_env, + PYTHON_PYODIDE_CACHE_GUEST_ROOT, + &pyodide_cache_path, + ); + add_runtime_host_access_path( + &mut execution_env, + "AGENTOS_EXTRA_FS_READ_PATHS", + &pyodide_dist_path, + true, + ); + add_runtime_host_access_path( + &mut execution_env, + "AGENTOS_EXTRA_FS_READ_PATHS", + &pyodide_cache_path, + true, + ); + add_runtime_host_access_path( + &mut execution_env, + "AGENTOS_EXTRA_FS_WRITE_PATHS", + &pyodide_cache_path, + false, + ); + let context = + self.python_engine + .create_context(CreatePythonContextRequest { + vm_id: vm_id.to_owned(), + pyodide_dist_path, + }); + let context_id = context.context_id; + let execution_result = + self.python_engine + .start_execution(StartPythonExecutionRequest { + vm_id: vm_id.to_owned(), + context_id: context_id.clone(), + code: resolved.entrypoint.clone(), + file_path: python_file_path, + env: execution_env, + cwd: resolved.host_cwd.clone(), + limits: python_execution_limits(vm), + guest_runtime: guest_runtime_identity( + vm, + Some(u64::from(kernel_pid)), + Some(u64::from(parent_kernel_pid)), + ), + }); + self.python_engine.dispose_context(&context_id); + let execution = execution_result.map_err(python_error)?; + ActiveExecution::Python(execution) + } + }; + let kernel_stdin_writer_fd = if posix_spawn_controls_stdin { + // POSIX spawn file actions run after fork and before exec. If + // they explicitly installed or closed fd 0, that exact final + // state is authoritative; adding the generic child-process + // stdin pipe here would replace it and keep an unrelated + // writer alive, so the guest could never observe Linux EOF. + None + } else { + match javascript_child_process_stdin_mode(&request) { + "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?), + "ignore" => { + vm.kernel + .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0) + .map_err(kernel_error)?; + None + } + "inherit" => None, + _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?), + } + }; + ( + kernel_pid, + kernel_handle, + execution, + kernel_stdin_writer_fd, + posix_spawn_controls_stdin, + ) + }; + Ok::<_, SidecarError>(spawned) + })(); + let (kernel_pid, kernel_handle, mut execution, kernel_stdin_writer_fd, direct_posix_stdin) = + match spawn_result { + Ok(spawned) => spawned, + Err(error) => { + if let Some(process) = pending_kernel_handle.take() { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &process, + None, + "child_process.spawn", + ); + } + return Err(error); + } + }; + record_execute_phase( + "child_process_spawn_and_start_execution", + phase_start.elapsed(), + ); + + let phase_start = Instant::now(); + // Shared-terminal detection: when the child's kernel fd 1 is a PTY (the + // slave inherited from a TTY shell), record who owns the host-facing + // master so the child's stdio writes surface through master drains + // instead of child stdout events (see `tty_master_owner`). + let child_fd1_is_tty = vm + .kernel + .isatty(EXECUTION_DRIVER_NAME, kernel_pid, 1) + .unwrap_or(false); + let child_process_group = match vm.kernel.getpgid(EXECUTION_DRIVER_NAME, kernel_pid) { + Ok(process_group) => process_group, + Err(error) => { + if let Some(process) = pending_kernel_handle.take() { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &process, + Some(&mut execution), + "child_process.spawn", + ); + } + return Err(kernel_error(error)); + } + }; + let process_event_limits = vm.limits.process.clone(); + let process = match vm.active_processes.get_mut(process_id) { + Some(process) => process, + None => { + let error = missing_process_error(vm_id, process_id); + if let Some(child) = pending_kernel_handle.take() { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &child, + Some(&mut execution), + "child_process.spawn", + ); + } + return Err(error); + } + }; + let inherited_tty_master_owner = if child_fd1_is_tty { + process + .tty_master_fd + .map(|master_fd| (process.kernel_pid, master_fd)) + .or(process.tty_master_owner) + } else { + None + }; + pending_kernel_handle.take(); + process.child_processes.insert( + child_process_id.clone(), + ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution) + .with_process_event_limits(&process_event_limits) + .with_vm_pending_byte_budgets( + Arc::clone(&vm_pending_stdin_bytes_budget), + Arc::clone(&vm_pending_event_bytes_budget), + ) + .with_detached(request.options.detached) + .with_guest_cwd(resolved.guest_cwd.clone()) + .with_env(resolved.env.clone()) + .with_host_cwd(resolved.host_cwd.clone()), + ); + { + let child = process + .child_processes + .get_mut(&child_process_id) + .expect("inserted child process exists during spawn registration"); + child.tty_master_owner = inherited_tty_master_owner; + if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd { + child.kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd); + } + prepared_host_net_fds.install(child); + } + record_execute_phase("child_process_register", phase_start.elapsed()); + record_execute_phase("child_process_spawn_total", total_start.elapsed()); + Ok(json!({ + "childId": child_process_id, + "pid": kernel_pid, + "pgid": child_process_group, + "directPosixStdin": direct_posix_stdin, + "command": resolved.command, + "args": resolved.process_args, + })) + } + + /// Replace a running guest process image for execve(2) without creating a + /// child. Resolution is deliberately performed against an empty inherited + /// environment: execve's supplied envp replaces the old environment rather + /// than being overlaid on it. The existing PID, process tree, cwd, stdio, + /// and non-CLOEXEC kernel descriptors remain attached to `ActiveProcess`. + pub(crate) fn exec_javascript_process_image( + &mut self, + vm_id: &str, + root_process_id: &str, + process_path: &[&str], + mut request: JavascriptChildProcessSpawnRequest, + ) -> Result<(), SidecarError> { + if request.options.executable_fd.is_some() { + return Err(SidecarError::InvalidState(String::from( + "EINVAL: executableFd is only valid for process.exec_fd_image_commit", + ))); + } + if request.options.shell || request.options.detached { + return Err(SidecarError::InvalidState(String::from( + "EINVAL: execve does not accept shell or detached process options", + ))); + } + + let (guest_cwd, host_cwd, kernel_pid, parent_kernel_pid, current_runtime) = { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + let root = vm + .active_processes + .get(root_process_id) + .ok_or_else(|| missing_process_error(vm_id, root_process_id))?; + let process = Self::active_process_by_path(root, process_path).ok_or_else(|| { + SidecarError::InvalidState(format!( + "unknown process path {} during execve", + Self::child_process_path_label(root_process_id, process_path) + )) + })?; + let parent_kernel_pid = vm + .kernel + .list_processes() + .get(&process.kernel_pid) + .map(|entry| entry.ppid) + .unwrap_or_default(); + ( + process.guest_cwd.clone(), + process.host_cwd.clone(), + process.kernel_pid, + parent_kernel_pid, + process.runtime.clone(), + ) + }; + + if request.command.is_empty() { + return Err(SidecarError::InvalidState(String::from( + "ENOENT: execve path is empty", + ))); + } + // execve resolves a relative pathname from cwd; it never searches + // PATH. Making the command explicitly path-like keeps the shared child + // resolver from taking its spawnp/PATH branch for a bare relative name. + request.command = if request.command.starts_with('/') { + normalize_path(&request.command) + } else { + normalize_path(&format!("{guest_cwd}/{}", request.command)) + }; + let literal_exec_path = request.command.clone(); + request.command = { + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + vm.kernel + .validate_executable_path(&literal_exec_path, &guest_cwd) + .map_err(kernel_error)? + }; + request.options.cwd = None; + request.options.shell = false; + request.options.detached = false; + + let mut resolved = { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + self.resolve_javascript_child_process_execution_with_mode( + vm, + &BTreeMap::new(), + &guest_cwd, + &host_cwd, + &request, + true, + None, + )? + }; + apply_child_process_argv0(&mut resolved, request.options.argv0.as_deref()); + if resolved.tool_command { + return Err(SidecarError::InvalidState(format!( + "ENOEXEC: exec format error: {}", + request.command + ))); + } + if request.options.local_replacement + && current_runtime == GuestRuntimeKind::WebAssembly + && resolved.runtime == GuestRuntimeKind::WebAssembly + { + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + // The runner has already compiled the replacement before it asks + // the sidecar to commit. Recursively validate the original image + // and every `#!` interpreter in the kernel so scripts retain + // Linux pathname, mode, format, and recursion errors without + // requiring the script itself to contain raw WASM bytes. + vm.kernel + .validate_wasm_exec_image(&request.command, &guest_cwd) + .map_err(kernel_error)?; + } else { + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + validate_exact_exec_image_format(vm, &request.command, &resolved.runtime)?; + } + { + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + stage_agentos_package_command(vm, &mut resolved)?; + } + // Keep guest-visible envp separate from executor bootstrap variables + // added during resolution. Both local and separate-runtime exec paths + // must publish and inherit exactly the supplied environment. + let replacement_guest_env = request.options.env.clone(); + + if request.options.local_replacement { + if current_runtime != GuestRuntimeKind::WebAssembly + || resolved.runtime != GuestRuntimeKind::WebAssembly + { + return Err(SidecarError::InvalidState(format!( + "ENOEXEC: in-place exec only supports WebAssembly images: {}", + literal_exec_path + ))); + } + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + // execve's envp is a complete replacement. `resolved.env` also + // contains host/runtime control variables injected while locating + // the module; those must never leak into the process's Linux-visible + // environment or become inherited by its future children. + let retained_internal_fds = Self::active_process_by_path( + vm.active_processes + .get(root_process_id) + .ok_or_else(|| missing_process_error(vm_id, root_process_id))?, + process_path, + ) + .and_then(|process| process.kernel_stdin_writer_fd) + .into_iter() + .collect::>(); + vm.kernel + .exec_process_retaining_internal_fds( + EXECUTION_DRIVER_NAME, + kernel_pid, + WASM_COMMAND, + resolved.process_args.clone(), + replacement_guest_env.clone(), + resolved.guest_cwd.clone(), + &retained_internal_fds, + &request.options.cloexec_fds, + Some(&literal_exec_path), + ) + .map_err(kernel_error)?; + + let root = vm + .active_processes + .get_mut(root_process_id) + .ok_or_else(|| missing_process_error(vm_id, root_process_id))?; + let process = + Self::active_process_by_path_mut(root, process_path).ok_or_else(|| { + SidecarError::InvalidState(format!( + "process disappeared during execve: {}", + Self::child_process_path_label(root_process_id, process_path) + )) + })?; + process.guest_cwd = resolved.guest_cwd; + process.host_cwd = resolved.host_cwd; + process.env = replacement_guest_env; + process.exit_signal = None; + process.exit_core_dumped = false; + process.deferred_kernel_wait_rpc = None; + process.module_resolution_cache = Default::default(); + discard_replaced_image_pending_events(process); + + // POSIX exec resets caught dispositions to default, preserves + // ignored dispositions, and preserves the signal mask/pending set. + reset_caught_signal_dispositions_after_exec( + &mut vm.signal_states, + root_process_id, + process_path, + ); + return Ok(()); + } + + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + let mut execution_env = resolved.env.clone(); + execution_env.insert( + String::from(EXECUTION_SANDBOX_ROOT_ENV), + normalize_host_path(&vm.cwd).to_string_lossy().into_owned(), + ); + let replacement = match resolved.runtime { + GuestRuntimeKind::JavaScript => { + execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env( + &request.options.internal_bootstrap_env, + )); + execution_env.insert(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1")); + let launch_entrypoint = + resolve_agentos_package_javascript_launch_entrypoint(vm, &mut execution_env) + .unwrap_or_else(|| resolved.entrypoint.clone()); + let inline_code = load_javascript_entrypoint_source( + vm, + &resolved.host_cwd, + &launch_entrypoint, + &execution_env, + ); + prepare_javascript_shadow(vm, &resolved, &execution_env)?; + let built_reader = build_module_reader(vm, &resolved); + let guest_reader = built_reader.clone().map(|reader| { + Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader)) + as Box + }); + let module_reader = + built_reader.map(|reader| Box::new(reader) as Box); + let context = + self.javascript_engine + .create_context(CreateJavascriptContextRequest { + vm_id: vm_id.to_owned(), + bootstrap_module: None, + compile_cache_root: Some(self.cache_root.join("node-compile-cache")), + }); + let context_id = context.context_id; + let replacement_result = + self.javascript_engine.prepare_execution_with_module_reader( + StartJavascriptExecutionRequest { + guest_runtime: guest_runtime_identity( + vm, + Some(u64::from(kernel_pid)), + Some(u64::from(parent_kernel_pid)), + ), + vm_id: vm_id.to_owned(), + context_id: context_id.clone(), + argv: std::iter::once(launch_entrypoint) + .chain(resolved.execution_args.clone()) + .collect(), + argv0: request.options.argv0.clone(), + env: execution_env, + cwd: resolved.host_cwd.clone(), + limits: javascript_execution_limits(vm), + inline_code, + wasm_module_bytes: None, + }, + module_reader, + guest_reader, + ); + self.javascript_engine.dispose_context(&context_id); + ActiveExecution::Javascript(replacement_result.map_err(javascript_error)?) + } + GuestRuntimeKind::WebAssembly => { + execution_env.extend(sanitize_javascript_child_process_internal_bootstrap_env( + &request.options.internal_bootstrap_env, + )); + execution_env.insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1")); + execution_env.insert(String::from(WASM_EXEC_COMMIT_RPC_ENV), String::from("1")); + let context = self.wasm_engine.create_context(CreateWasmContextRequest { + vm_id: vm_id.to_owned(), + module_path: Some(resolved.entrypoint.clone()), + }); + let context_id = context.context_id; + let replacement_result = + self.wasm_engine + .prepare_execution(StartWasmExecutionRequest { + vm_id: vm_id.to_owned(), + context_id: context_id.clone(), + argv: resolved.process_args.clone(), + env: execution_env, + cwd: resolved.host_cwd.clone(), + permission_tier: execution_wasm_permission_tier( + resolved + .wasm_permission_tier + .unwrap_or(WasmPermissionTier::Full), + ), + limits: wasm_execution_limits(vm), + guest_runtime: guest_runtime_identity( + vm, + Some(u64::from(kernel_pid)), + Some(u64::from(parent_kernel_pid)), + ), + }); + self.wasm_engine.dispose_context(&context_id); + ActiveExecution::Wasm(Box::new(replacement_result.map_err(wasm_error)?)) + } + GuestRuntimeKind::Python => { + let python_file_path = if execution_env.contains_key("AGENTOS_PYTHON_ARGV") { + execution_env.get("AGENTOS_PYTHON_FILE").map(PathBuf::from) + } else { + python_file_entrypoint(&resolved.entrypoint) + }; + let pyodide_dist_path = self + .python_engine + .bundled_pyodide_dist_path_for_vm(vm_id) + .map_err(python_error)?; + let pyodide_cache_path = pyodide_dist_path + .parent() + .and_then(Path::parent) + .unwrap_or(pyodide_dist_path.as_path()) + .join("pyodide-package-cache"); + add_runtime_guest_path_mapping( + &mut execution_env, + PYTHON_PYODIDE_GUEST_ROOT, + &pyodide_dist_path, + ); + add_runtime_guest_path_mapping( + &mut execution_env, + PYTHON_PYODIDE_CACHE_GUEST_ROOT, + &pyodide_cache_path, + ); + add_runtime_host_access_path( + &mut execution_env, + "AGENTOS_EXTRA_FS_READ_PATHS", + &pyodide_dist_path, + true, + ); + add_runtime_host_access_path( + &mut execution_env, + "AGENTOS_EXTRA_FS_READ_PATHS", + &pyodide_cache_path, + true, + ); + add_runtime_host_access_path( + &mut execution_env, + "AGENTOS_EXTRA_FS_WRITE_PATHS", + &pyodide_cache_path, + false, + ); + let context = self + .python_engine + .create_context(CreatePythonContextRequest { + vm_id: vm_id.to_owned(), + pyodide_dist_path, + }); + let context_id = context.context_id; + let replacement_result = + self.python_engine + .prepare_execution(StartPythonExecutionRequest { + vm_id: vm_id.to_owned(), + context_id: context_id.clone(), + code: resolved.entrypoint.clone(), + file_path: python_file_path, + env: execution_env, + cwd: resolved.host_cwd.clone(), + limits: python_execution_limits(vm), + guest_runtime: guest_runtime_identity( + vm, + Some(u64::from(kernel_pid)), + Some(u64::from(parent_kernel_pid)), + ), + }); + self.python_engine.dispose_context(&context_id); + ActiveExecution::Python(replacement_result.map_err(python_error)?) + } + }; + + // Hard production invariant: a cross-runtime exec image must still + // own its deferred execute payload here. This check makes it + // impossible to accidentally regress to the old start-before-commit + // path without failing execve before kernel state is mutated. + if !replacement.is_prepared_for_start() { + return Err(SidecarError::InvalidState(String::from( + "EIO: cross-runtime execve replacement started before kernel commit", + ))); + } + + let kernel_command = match resolved.runtime { + GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND, + GuestRuntimeKind::WebAssembly => WASM_COMMAND, + GuestRuntimeKind::Python => PYTHON_COMMAND, + }; + let retained_internal_fds = Self::active_process_by_path( + vm.active_processes + .get(root_process_id) + .ok_or_else(|| missing_process_error(vm_id, root_process_id))?, + process_path, + ) + .and_then(|process| process.kernel_stdin_writer_fd) + .into_iter() + .collect::>(); + if let Err(error) = vm.kernel.exec_process_retaining_internal_fds( + EXECUTION_DRIVER_NAME, + kernel_pid, + kernel_command, + resolved.process_args.clone(), + replacement_guest_env.clone(), + resolved.guest_cwd.clone(), + &retained_internal_fds, + &request.options.cloexec_fds, + Some(&literal_exec_path), + ) { + let mut replacement = replacement; + if let Err(terminate_error) = replacement.terminate() { + tracing::warn!( + vm_id, + kernel_pid, + error = %terminate_error, + "failed to terminate prepared replacement after execve kernel commit was rejected" + ); + } + return Err(kernel_error(error)); + } + + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + let root = vm + .active_processes + .get_mut(root_process_id) + .ok_or_else(|| missing_process_error(vm_id, root_process_id))?; + let process = Self::active_process_by_path_mut(root, process_path).ok_or_else(|| { + SidecarError::InvalidState(format!( + "process disappeared during execve: {}", + Self::child_process_path_label(root_process_id, process_path) + )) + })?; + let mut old_execution = std::mem::replace(&mut process.execution, replacement); + process.runtime = resolved.runtime; + process.guest_cwd = resolved.guest_cwd; + process.host_cwd = resolved.host_cwd; + process.env = replacement_guest_env; + process.exit_signal = None; + process.exit_core_dumped = false; + process.deferred_kernel_wait_rpc = None; + process.module_resolution_cache = Default::default(); + discard_replaced_image_pending_events(process); + rebind_process_runtime_event_targets(process, &kernel_readiness); + + // POSIX exec resets caught dispositions to default but preserves + // dispositions explicitly set to ignore. + let signal_key = reset_caught_signal_dispositions_after_exec( + &mut vm.signal_states, + root_process_id, + process_path, + ); + // The replacement isolate was registered and fully loaded before the + // kernel commit, but no guest code was enqueued. Only start it now, + // after both kernel-visible process state and sidecar-owned descriptors + // and event targets point at the replacement image. + #[cfg(test)] + let replacement_start_error = if std::mem::take(&mut self.fail_next_exec_start_after_commit) + { + Some(SidecarError::Execution(String::from( + "injected post-commit execve start failure", + ))) + } else { + process.execution.start_prepared().err() + }; + #[cfg(not(test))] + let replacement_start_error = process.execution.start_prepared().err(); + if let Some(error) = replacement_start_error.as_ref() { + let message = format!("execve replacement runtime failed to start: {error}\n"); + if let Err(queue_error) = process + .queue_pending_execution_event(ActiveExecutionEvent::Stderr(message.into_bytes())) + .and_then(|_| { + process.queue_pending_execution_event(ActiveExecutionEvent::Exited(127)) + }) + { + tracing::error!( + vm_id, + process_id = %signal_key, + error = %queue_error, + "failed to queue fatal post-commit execve start failure" + ); + } + process.kernel_handle.finish(127); + if let Err(terminate_error) = process.execution.terminate() { + tracing::error!( + vm_id, + process_id = %signal_key, + error = %terminate_error, + "failed to terminate replacement runtime after post-commit execve start failure" + ); + } + } + // The old image is blocked in the exec RPC. Terminating it after the + // atomic state swap ensures no success response can resume old code. + if let Err(error) = old_execution.terminate() { + tracing::warn!( + vm_id, + process_id = %signal_key, + error = %error, + "execve committed but the replaced runtime image reported a termination error" + ); + } + if let Some(error) = replacement_start_error { + // The kernel execve commit is irrevocable. Linux does not return an + // errno into the old image when a post-commit loader/start failure + // occurs; the new process image dies. Log the typed host failure, + // leave the queued exit for normal cleanup, and report committed + // success to the service loop so it never replies to old code. + tracing::error!( + vm_id, + process_id = %signal_key, + error = %error, + "execve replacement failed after commit; terminating the replacement process" + ); + } + Ok(()) + } + + /// Commit metadata for an fexecve image that the trusted WASM runner has + /// already read and compiled from its live private descriptor. This route + /// deliberately performs no pathname resolution or reopen: the descriptor + /// may name an unlinked file, and the runner owns that open-file identity. + pub(crate) fn commit_wasm_fd_process_image( + &mut self, + vm_id: &str, + root_process_id: &str, + process_path: &[&str], + request: JavascriptChildProcessSpawnRequest, + ) -> Result<(), SidecarError> { + if !request.options.local_replacement || request.options.executable_fd.is_none() { + return Err(SidecarError::InvalidState(String::from( + "EINVAL: fd-image exec commit requires localReplacement and executableFd", + ))); + } + if request.options.shell || request.options.detached || request.options.cwd.is_some() { + return Err(SidecarError::InvalidState(String::from( + "EINVAL: fexecve does not accept shell, detached, or cwd options", + ))); + } + + let (kernel_pid, retained_internal_fds) = { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + let root = vm + .active_processes + .get(root_process_id) + .ok_or_else(|| missing_process_error(vm_id, root_process_id))?; + let process = Self::active_process_by_path(root, process_path).ok_or_else(|| { + SidecarError::InvalidState(format!( + "unknown process path {} during fexecve commit", + Self::child_process_path_label(root_process_id, process_path) + )) + })?; + if process.runtime != GuestRuntimeKind::WebAssembly { + return Err(SidecarError::InvalidState(String::from( + "ENOEXEC: fd-image exec commit requires a WebAssembly process", + ))); + } + ( + process.kernel_pid, + process + .kernel_stdin_writer_fd + .into_iter() + .collect::>(), + ) + }; + + let mut argv = Vec::with_capacity(request.args.len().saturating_add(1)); + argv.push( + request + .options + .argv0 + .clone() + .unwrap_or_else(|| request.command.clone()), + ); + argv.extend(request.args); + let replacement_guest_env = request.options.env; + + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + vm.kernel + .exec_process_retaining_internal_fds( + EXECUTION_DRIVER_NAME, + kernel_pid, + WASM_COMMAND, + argv, + replacement_guest_env.clone(), + String::new(), + &retained_internal_fds, + &request.options.cloexec_fds, + None, + ) + .map_err(kernel_error)?; + + let root = vm + .active_processes + .get_mut(root_process_id) + .ok_or_else(|| missing_process_error(vm_id, root_process_id))?; + let process = Self::active_process_by_path_mut(root, process_path).ok_or_else(|| { + SidecarError::InvalidState(format!( + "process disappeared during fexecve commit: {}", + Self::child_process_path_label(root_process_id, process_path) + )) + })?; + process.env = replacement_guest_env; + process.exit_signal = None; + process.exit_core_dumped = false; + process.deferred_kernel_wait_rpc = None; + process.module_resolution_cache = Default::default(); + discard_replaced_image_pending_events(process); + reset_caught_signal_dispositions_after_exec( + &mut vm.signal_states, + root_process_id, + process_path, + ); + Ok(()) + } + + pub(crate) fn spawn_javascript_child_process_sync( + &mut self, + vm_id: &str, + process_id: &str, + request: JavascriptChildProcessSpawnRequest, + max_buffer: Option, + ) -> Result { + let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?; + let timeout_deadline = request + .options + .timeout + .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms)); + let timeout_signal = request + .options + .kill_signal + .clone() + .unwrap_or_else(|| String::from("SIGTERM")); + let spawned = self.spawn_javascript_child_process(vm_id, process_id, request)?; + let child_process_id = spawned + .get("childId") + .and_then(Value::as_str) + .ok_or_else(|| { + SidecarError::InvalidState(String::from( + "child_process.spawn_sync response is missing childId", + )) + })? + .to_owned(); + + if let Some(input) = sync_input.as_deref() { + self.write_javascript_child_process_stdin(vm_id, process_id, &child_process_id, input)?; + } + self.close_javascript_child_process_stdin(vm_id, process_id, &child_process_id)?; + + let max_buffer = max_buffer.unwrap_or(1024 * 1024); + let mut stdout = Vec::new(); + let mut stderr = Vec::new(); + let mut max_buffer_exceeded = false; + let mut kill_sent = false; + let mut timed_out = false; + + let exit_code = loop { + let wait_ms = if let Some(deadline) = timeout_deadline { + let now = Instant::now(); + if now >= deadline { + if !kill_sent { + timed_out = true; + self.kill_javascript_child_process( + vm_id, + process_id, + &child_process_id, + &timeout_signal, + )?; + kill_sent = true; + } + 0 + } else { + u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50)) + .unwrap_or(50) + } + } else { + 50 + }; + let event = + self.poll_javascript_child_process(vm_id, process_id, &child_process_id, wait_ms)?; + if event.is_null() { + continue; + } + + match event.get("type").and_then(Value::as_str) { + Some("stdout") => { + let chunk = javascript_sync_rpc_bytes_arg( + &[event.get("data").cloned().unwrap_or(Value::Null)], + 0, + "child_process.spawn_sync stdout", + )?; + stdout.extend_from_slice(&chunk); + if stdout.len() > max_buffer && !kill_sent { + max_buffer_exceeded = true; + self.kill_javascript_child_process( + vm_id, + process_id, + &child_process_id, + "SIGTERM", + )?; + kill_sent = true; + } + } + Some("stderr") => { + let chunk = javascript_sync_rpc_bytes_arg( + &[event.get("data").cloned().unwrap_or(Value::Null)], + 0, + "child_process.spawn_sync stderr", + )?; + stderr.extend_from_slice(&chunk); + if stderr.len() > max_buffer && !kill_sent { + max_buffer_exceeded = true; + self.kill_javascript_child_process( + vm_id, + process_id, + &child_process_id, + "SIGTERM", + )?; + kill_sent = true; + } + } + Some("exit") => { + break event + .get("exitCode") + .and_then(Value::as_i64) + .map(|value| value as i32) + .unwrap_or(1); + } + _ => {} + } + }; + + Ok(json!({ + "stdout": String::from_utf8_lossy(&stdout), + "stderr": String::from_utf8_lossy(&stderr), + "code": exit_code, + "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null }, + "timedOut": timed_out, + "maxBufferExceeded": max_buffer_exceeded, + })) + } + + fn spawn_descendant_javascript_child_process( + &mut self, + vm_id: &str, + process_id: &str, + current_process_path: &[&str], + mut request: JavascriptChildProcessSpawnRequest, + ) -> Result { + let spawn_attributes = javascript_spawn_attributes(&request.options)?; + let requested_pgid = spawn_attributes.process_group; + let current_process_label = + Self::child_process_path_label(process_id, current_process_path); + let parent_sync_roots = { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + let root = vm + .active_processes + .get(process_id) + .ok_or_else(|| missing_process_error(vm_id, process_id))?; + let parent = + Self::active_process_by_path(root, current_process_path).ok_or_else(|| { + SidecarError::InvalidState(format!( + "unknown child process path {current_process_label} during nested spawn" + )) + })?; + (parent.host_write_dirty_recursive() + || !parent.clean_host_writes_are_observable_recursive()) + .then(|| (parent.host_cwd.clone(), parent.guest_cwd.clone())) + }; + if let Some((host_cwd, guest_cwd)) = parent_sync_roots { + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + sync_process_host_roots_to_kernel(vm, &host_cwd, &guest_cwd)?; + } + let prepared_host_net_fds = { + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + let current_network_counts = vm_spawn_host_net_resource_counts(vm); + let (kernel, active_processes) = (&mut vm.kernel, &mut vm.active_processes); + let root = active_processes + .get_mut(process_id) + .ok_or_else(|| missing_process_error(vm_id, process_id))?; + let parent = + Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| { + SidecarError::InvalidState(format!( + "unknown child process path {} during host-network fd inheritance", + Self::child_process_path_label(process_id, current_process_path) + )) + })?; + prepare_spawn_host_net_fds( + kernel, + parent, + current_network_counts, + &request.options.spawn_host_net_fds, + &request.options.spawn_fd_mappings, + &request.options.spawn_file_actions, + )? + }; + let prepared_spawn_actions = if !prepared_host_net_fds.kernel_actions.is_empty() { + let (parent_pid, parent_cwd) = { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + let root = vm + .active_processes + .get(process_id) + .ok_or_else(|| missing_process_error(vm_id, process_id))?; + let parent = + Self::active_process_by_path(root, current_process_path).ok_or_else(|| { + SidecarError::InvalidState(format!( + "unknown child process path {} during spawn file actions", + Self::child_process_path_label(process_id, current_process_path) + )) + })?; + let initial_cwd = request + .options + .cwd + .as_deref() + .map(|cwd| { + if cwd.starts_with('/') { + normalize_path(cwd) + } else { + normalize_path(&format!("{}/{cwd}", parent.guest_cwd)) + } + }) + .unwrap_or_else(|| parent.guest_cwd.clone()); + (parent.kernel_pid, initial_cwd) + }; + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + Some(preapply_posix_spawn_file_actions( + &mut vm.kernel, + parent_pid, + &parent_cwd, + requested_pgid, + &request.options.spawn_fd_mappings, + &prepared_host_net_fds.kernel_actions, + )?) + } else { + None + }; + if let Some(prepared) = prepared_spawn_actions.as_ref() { + request.options.cwd = Some(prepared.cwd.clone()); + } + { + let parent_guest_cwd = { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + let root = vm + .active_processes + .get(process_id) + .ok_or_else(|| missing_process_error(vm_id, process_id))?; + Self::active_process_by_path(root, current_process_path) + .ok_or_else(|| { + SidecarError::InvalidState(format!( + "unknown child process path {} during program resolution", + Self::child_process_path_label(process_id, current_process_path) + )) + })? + .guest_cwd + .clone() + }; + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + resolve_posix_spawn_program(vm, &parent_guest_cwd, &mut request)?; + } + let total_start = Instant::now(); + let phase_start = Instant::now(); + let (parent_env, parent_guest_cwd, parent_host_cwd, parent_kernel_pid) = { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + let root = vm + .active_processes + .get(process_id) + .ok_or_else(|| missing_process_error(vm_id, process_id))?; + let parent = + Self::active_process_by_path(root, current_process_path).ok_or_else(|| { + SidecarError::InvalidState(format!( + "unknown child process path {current_process_label} during nested spawn" + )) + })?; + ( + parent.env.clone(), + parent.guest_cwd.clone(), + parent.host_cwd.clone(), + parent.kernel_pid, + ) + }; + let mut resolved = + if !request.options.spawn_exact_path && request.options.spawn_search_path.is_none() { + self.resolve_javascript_child_process_with_shebang( + vm_id, + &parent_env, + &parent_guest_cwd, + &parent_host_cwd, + &mut request, + )? + } else { + let vm = self.vms.get(vm_id).ok_or_else(|| missing_vm_error(vm_id))?; + self.resolve_javascript_child_process_execution_with_mode( + vm, + &parent_env, + &parent_guest_cwd, + &parent_host_cwd, + &request, + request.options.spawn_exact_path, + request.options.spawn_search_path.as_deref(), + )? + }; + apply_child_process_argv0(&mut resolved, request.options.argv0.as_deref()); + { + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + stage_agentos_package_command(vm, &mut resolved)?; + } + let resolved = resolved; + if prepared_host_net_fds.inherited_fd_count() != 0 + && (resolved.runtime != GuestRuntimeKind::WebAssembly || resolved.tool_command) + { + return Err(SidecarError::InvalidState(String::from( + "ENOTSUP: inherited host-network fds require a WebAssembly child runtime", + ))); + } + record_execute_phase("child_process_resolve_execution", phase_start.elapsed()); + let sidecar_requests = self.sidecar_requests.clone(); + let vm = self + .vms + .get_mut(vm_id) + .ok_or_else(|| missing_vm_error(vm_id))?; + let vm_pending_stdin_bytes_budget = Arc::clone(&vm.pending_stdin_bytes_budget); + let vm_pending_event_bytes_budget = Arc::clone(&vm.pending_event_bytes_budget); + let phase_start = Instant::now(); + let child_process_id = { + let root = vm + .active_processes + .get_mut(process_id) + .ok_or_else(|| missing_process_error(vm_id, process_id))?; + let parent = + Self::active_process_by_path_mut(root, current_process_path).ok_or_else(|| { + SidecarError::InvalidState(format!( + "unknown child process path {current_process_label} during nested spawn" + )) + })?; + parent.allocate_child_process_id() + }; + let mut child_path = current_process_path.to_vec(); + child_path.push(child_process_id.as_str()); + let mut pending_kernel_handle = None; + let spawn_result = (|| { + let spawned = if resolved.tool_command { + let tool_resolution = resolve_tool_command( + vm, + &resolved.command, + &resolved.execution_args, + Some(&resolved.guest_cwd), + )? + .ok_or_else(|| { + SidecarError::InvalidState(format!( + "tool command no longer resolves: {}", + resolved.command + )) + })?; + let kernel_handle = vm + .kernel + .create_virtual_process_with_process_group( + EXECUTION_DRIVER_NAME, + TOOL_DRIVER_NAME, + &resolved.command, + resolved.process_args.clone(), + VirtualProcessOptions { + parent_pid: Some(parent_kernel_pid), + env: resolved.env.clone(), + cwd: Some(resolved.guest_cwd.clone()), + }, + requested_pgid, + ) + .map_err(kernel_error)?; + let kernel_pid = kernel_handle.pid(); + if let Some(prepared) = prepared_spawn_actions { + install_preapplied_posix_spawn_file_actions( + &mut vm.kernel, + &kernel_handle, + prepared, + )?; + } else { + apply_posix_spawn_file_actions_or_rollback( + &mut vm.kernel, + &kernel_handle, + &resolved.guest_cwd, + &request.options.spawn_fd_mappings, + &prepared_host_net_fds.kernel_actions, + )?; + } + apply_spawn_session_or_rollback( + &mut vm.kernel, + &kernel_handle, + spawn_attributes.new_session || request.options.detached, + )?; + pending_kernel_handle = Some(kernel_handle.clone()); + let tool_execution = ToolExecution::default() + .with_vm_pending_event_bytes_budget(Arc::clone(&vm_pending_event_bytes_budget)); + let cancelled = tool_execution.cancelled.clone(); + let pending_events = tool_execution.pending_events.clone(); + let event_overflow_reason = tool_execution.event_overflow_reason.clone(); + let pending_event_bytes = tool_execution.pending_event_bytes.clone(); + let pending_event_count_limit = tool_execution.pending_event_count_limit.clone(); + let pending_event_bytes_limit = tool_execution.pending_event_bytes_limit.clone(); + spawn_tool_process_events(ToolProcessEventRequest { + sidecar_requests: sidecar_requests.clone(), + connection_id: vm.connection_id.clone(), + session_id: vm.session_id.clone(), + vm_id: vm_id.to_owned(), + tool_resolution, + cancelled, + pending_events, + event_overflow_reason, + pending_event_bytes, + pending_event_count_limit, + pending_event_bytes_limit, + vm_pending_event_bytes_budget: Arc::clone(&vm_pending_event_bytes_budget), + }); + ( + kernel_pid, + kernel_handle, + ActiveExecution::Tool(tool_execution), + None, + false, + ) + } else { + let kernel_command = match resolved.runtime { + GuestRuntimeKind::JavaScript => JAVASCRIPT_COMMAND, + GuestRuntimeKind::WebAssembly => WASM_COMMAND, + GuestRuntimeKind::Python => PYTHON_COMMAND, + }; + let kernel_handle = vm + .kernel + .spawn_process_with_process_group( + kernel_command, + resolved.process_args.clone(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(parent_kernel_pid), + env: resolved.env.clone(), + cwd: Some(resolved.guest_cwd.clone()), + }, + requested_pgid, + ) + .map_err(kernel_error)?; + let kernel_pid = kernel_handle.pid(); + let applied_spawn_actions = if let Some(prepared) = prepared_spawn_actions { + install_preapplied_posix_spawn_file_actions( + &mut vm.kernel, + &kernel_handle, + prepared, + )? + } else { + apply_posix_spawn_file_actions_or_rollback( + &mut vm.kernel, + &kernel_handle, + &resolved.guest_cwd, + &request.options.spawn_fd_mappings, + &prepared_host_net_fds.kernel_actions, + )? + }; + let posix_spawn_controls_stdin = !request.options.spawn_file_actions.is_empty() + && (applied_spawn_actions + .fd_mappings + .iter() + .any(|mapping| mapping[0] == 0) + || applied_spawn_actions.closed_guest_fds.contains(&0) + || prepared_host_net_fds + .descriptions + .iter() + .any(|description| description.guest_fds.contains(&0))); + apply_spawn_session_or_rollback( + &mut vm.kernel, + &kernel_handle, + spawn_attributes.new_session || request.options.detached, + )?; + pending_kernel_handle = Some(kernel_handle.clone()); + let mut execution_env = resolved.env.clone(); + if resolved.runtime == GuestRuntimeKind::WebAssembly { + execution_env.insert( + String::from("AGENTOS_WASM_INHERITED_FD_MAPPINGS"), + serde_json::to_string(&applied_spawn_actions.fd_mappings).map_err( + |error| { + SidecarError::InvalidState(format!( + "failed to serialize inherited WASM fd mappings: {error}" + )) + }, + )?, + ); + execution_env.insert( + String::from("AGENTOS_WASM_CLOSED_INHERITED_FDS"), + serde_json::to_string(&applied_spawn_actions.closed_guest_fds).map_err( + |error| { + SidecarError::InvalidState(format!( + "failed to serialize closed inherited WASM fds: {error}" + )) + }, + )?, + ); + execution_env.insert( + String::from("AGENTOS_WASM_INHERITED_HOSTNET_FDS"), + serde_json::to_string(&prepared_host_net_fds.bootstrap_json()).map_err( + |error| { + SidecarError::InvalidState(format!( + "failed to serialize inherited WASM host-network fds: {error}" + )) + }, + )?, + ); + } + execution_env.insert( + String::from(EXECUTION_SANDBOX_ROOT_ENV), + normalize_host_path(&vm.cwd).to_string_lossy().into_owned(), + ); + let execution = match resolved.runtime { + GuestRuntimeKind::JavaScript => { + execution_env.extend( + sanitize_javascript_child_process_internal_bootstrap_env( + &request.options.internal_bootstrap_env, + ), + ); + execution_env + .insert(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1")); + let launch_entrypoint = + resolve_agentos_package_javascript_launch_entrypoint( + vm, + &mut execution_env, + ) + .unwrap_or_else(|| resolved.entrypoint.clone()); + let inline_code = load_javascript_entrypoint_source( + vm, + &resolved.host_cwd, + &launch_entrypoint, + &execution_env, + ); + prepare_javascript_shadow(vm, &resolved, &execution_env)?; + + let built_reader = build_module_reader(vm, &resolved); + let guest_reader = built_reader.clone().map(|reader| { + Box::new(crate::plugins::host_dir::SessionModuleReader::new(reader)) + as Box + }); + let module_reader = built_reader + .map(|reader| Box::new(reader) as Box); + let context = + self.javascript_engine + .create_context(CreateJavascriptContextRequest { + vm_id: vm_id.to_owned(), + bootstrap_module: None, + compile_cache_root: Some( + self.cache_root.join("node-compile-cache"), + ), + }); + let context_id = context.context_id; + let execution_result = + self.javascript_engine.start_execution_with_module_reader( + StartJavascriptExecutionRequest { + guest_runtime: guest_runtime_identity( + vm, + Some(u64::from(kernel_pid)), + Some(u64::from(parent_kernel_pid)), + ), + vm_id: vm_id.to_owned(), + context_id: context_id.clone(), + argv: std::iter::once(launch_entrypoint) + .chain(resolved.execution_args.clone()) + .collect(), + argv0: request.options.argv0.clone(), + env: execution_env, + cwd: resolved.host_cwd.clone(), + limits: javascript_execution_limits(vm), + inline_code, + wasm_module_bytes: None, + }, + module_reader, + guest_reader, + ); + self.javascript_engine.dispose_context(&context_id); + let execution = execution_result.map_err(javascript_error)?; + ActiveExecution::Javascript(execution) + } + GuestRuntimeKind::WebAssembly => { + execution_env.extend( + sanitize_javascript_child_process_internal_bootstrap_env( + &request.options.internal_bootstrap_env, + ), + ); + execution_env + .insert(String::from(WASM_STDIO_SYNC_RPC_ENV), String::from("1")); + execution_env + .insert(String::from(WASM_EXEC_COMMIT_RPC_ENV), String::from("1")); + let wasm_limits = wasm_execution_limits(vm); + let wasm_guest_runtime = guest_runtime_identity( + vm, + Some(u64::from(kernel_pid)), + Some(u64::from(parent_kernel_pid)), + ); + let context = self.wasm_engine.create_context(CreateWasmContextRequest { + vm_id: vm_id.to_owned(), + module_path: Some(resolved.entrypoint.clone()), + }); + let context_id = context.context_id; + let execution_result = + self.wasm_engine.start_execution(StartWasmExecutionRequest { + vm_id: vm_id.to_owned(), + context_id: context_id.clone(), + argv: resolved.process_args.clone(), + env: execution_env, + cwd: resolved.host_cwd.clone(), + permission_tier: execution_wasm_permission_tier( + resolved + .wasm_permission_tier + .unwrap_or(WasmPermissionTier::Full), + ), + limits: wasm_limits, + guest_runtime: wasm_guest_runtime, + }); + self.wasm_engine.dispose_context(&context_id); + let execution = execution_result.map_err(wasm_error)?; + ActiveExecution::Wasm(Box::new(execution)) + } + GuestRuntimeKind::Python => { + // Nested `python` child_process: set up the Pyodide context the + // same way the top-level execute path does, so a guest shell or + // node parent can spawn `python` exactly like `node`. + let python_file_path = if execution_env.contains_key("AGENTOS_PYTHON_ARGV") + { + execution_env.get("AGENTOS_PYTHON_FILE").map(PathBuf::from) + } else { + python_file_entrypoint(&resolved.entrypoint) + }; + let pyodide_dist_path = self + .python_engine + .bundled_pyodide_dist_path_for_vm(vm_id) + .map_err(python_error)?; + let pyodide_cache_path = pyodide_dist_path + .parent() + .and_then(Path::parent) + .unwrap_or(pyodide_dist_path.as_path()) + .join("pyodide-package-cache"); + add_runtime_guest_path_mapping( + &mut execution_env, + PYTHON_PYODIDE_GUEST_ROOT, + &pyodide_dist_path, + ); + add_runtime_guest_path_mapping( + &mut execution_env, + PYTHON_PYODIDE_CACHE_GUEST_ROOT, + &pyodide_cache_path, + ); + add_runtime_host_access_path( + &mut execution_env, + "AGENTOS_EXTRA_FS_READ_PATHS", + &pyodide_dist_path, + true, + ); + add_runtime_host_access_path( + &mut execution_env, + "AGENTOS_EXTRA_FS_READ_PATHS", + &pyodide_cache_path, + true, + ); + add_runtime_host_access_path( + &mut execution_env, + "AGENTOS_EXTRA_FS_WRITE_PATHS", + &pyodide_cache_path, + false, + ); + let context = + self.python_engine + .create_context(CreatePythonContextRequest { + vm_id: vm_id.to_owned(), + pyodide_dist_path, + }); + let context_id = context.context_id; + let execution_result = + self.python_engine + .start_execution(StartPythonExecutionRequest { + vm_id: vm_id.to_owned(), + context_id: context_id.clone(), + code: resolved.entrypoint.clone(), + file_path: python_file_path, + env: execution_env, + cwd: resolved.host_cwd.clone(), + limits: python_execution_limits(vm), + guest_runtime: guest_runtime_identity( + vm, + Some(u64::from(kernel_pid)), + Some(u64::from(parent_kernel_pid)), + ), + }); + self.python_engine.dispose_context(&context_id); + let execution = execution_result.map_err(python_error)?; + ActiveExecution::Python(execution) + } + }; + let kernel_stdin_writer_fd = if posix_spawn_controls_stdin { + None + } else { + match javascript_child_process_stdin_mode(&request) { + "pipe" => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?), + "ignore" => { + vm.kernel + .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, 0) + .map_err(kernel_error)?; + None + } + "inherit" => None, + _ => Some(install_kernel_stdin_pipe(&mut vm.kernel, kernel_pid)?), + } + }; + ( + kernel_pid, + kernel_handle, + execution, + kernel_stdin_writer_fd, + posix_spawn_controls_stdin, + ) + }; + Ok::<_, SidecarError>(spawned) + })(); + let (kernel_pid, kernel_handle, mut execution, kernel_stdin_writer_fd, direct_posix_stdin) = + match spawn_result { + Ok(spawned) => spawned, + Err(error) => { + if let Some(process) = pending_kernel_handle.take() { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &process, + None, + "nested child_process.spawn", + ); + } + return Err(error); + } + }; + record_execute_phase( + "child_process_spawn_and_start_execution", + phase_start.elapsed(), + ); + + let phase_start = Instant::now(); + let child_fd1_is_tty = vm + .kernel + .isatty(EXECUTION_DRIVER_NAME, kernel_pid, 1) + .unwrap_or(false); + let child_process_group = match vm.kernel.getpgid(EXECUTION_DRIVER_NAME, kernel_pid) { + Ok(process_group) => process_group, + Err(error) => { + if let Some(process) = pending_kernel_handle.take() { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &process, + Some(&mut execution), + "nested child_process.spawn", + ); + } + return Err(kernel_error(error)); + } + }; + let process_event_limits = vm.limits.process.clone(); + let root = match vm.active_processes.get_mut(process_id) { + Some(root) => root, + None => { + let error = missing_process_error(vm_id, process_id); + if let Some(child) = pending_kernel_handle.take() { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &child, + Some(&mut execution), + "nested child_process.spawn", + ); + } + return Err(error); + } + }; + let parent = match Self::active_process_by_path_mut(root, current_process_path) { + Some(parent) => parent, + None => { + let error = SidecarError::InvalidState(format!( + "unknown child process path {current_process_label} during nested spawn" + )); + if let Some(child) = pending_kernel_handle.take() { + rollback_unregistered_spawn_child( + &mut vm.kernel, + &child, + Some(&mut execution), + "nested child_process.spawn", + ); + } + return Err(error); + } + }; + let inherited_tty_master_owner = if child_fd1_is_tty { + parent + .tty_master_fd + .map(|master_fd| (parent.kernel_pid, master_fd)) + .or(parent.tty_master_owner) + } else { + None + }; + pending_kernel_handle.take(); + parent.child_processes.insert( + child_process_id.clone(), + ActiveProcess::new(kernel_pid, kernel_handle, resolved.runtime, execution) + .with_process_event_limits(&process_event_limits) + .with_vm_pending_byte_budgets( + Arc::clone(&vm_pending_stdin_bytes_budget), + Arc::clone(&vm_pending_event_bytes_budget), + ) + .with_detached(request.options.detached) + .with_guest_cwd(resolved.guest_cwd.clone()) + .with_env(resolved.env.clone()) + .with_host_cwd(resolved.host_cwd.clone()), + ); + { + let child = parent + .child_processes + .get_mut(&child_process_id) + .expect("inserted nested child exists during spawn registration"); + child.tty_master_owner = inherited_tty_master_owner; + if let Some(kernel_stdin_writer_fd) = kernel_stdin_writer_fd { + child.kernel_stdin_writer_fd = Some(kernel_stdin_writer_fd); + } + prepared_host_net_fds.install(child); + } + record_execute_phase("child_process_register", phase_start.elapsed()); + record_execute_phase("child_process_spawn_total", total_start.elapsed()); + Ok(json!({ + "childId": child_process_id, + "pid": kernel_pid, + "pgid": child_process_group, + "directPosixStdin": direct_posix_stdin, + "command": resolved.command, + "args": resolved.process_args, + })) + } + + #[cfg(test)] + #[allow(dead_code)] + pub(crate) fn spawn_descendant_javascript_child_process_for_test( + &mut self, + vm_id: &str, + process_id: &str, + current_process_path: &[&str], + request: JavascriptChildProcessSpawnRequest, + ) -> Result { + self.spawn_descendant_javascript_child_process( + vm_id, + process_id, + current_process_path, + request, + ) + } + + fn spawn_descendant_javascript_child_process_sync( + &mut self, + vm_id: &str, + process_id: &str, + current_process_path: &[&str], + request: JavascriptChildProcessSpawnRequest, + max_buffer: Option, + ) -> Result { + let sync_input = javascript_child_process_sync_input_bytes(request.options.input.as_ref())?; + let timeout_deadline = request + .options + .timeout + .map(|timeout_ms| Instant::now() + Duration::from_millis(timeout_ms)); + let timeout_signal = request + .options + .kill_signal + .clone() + .unwrap_or_else(|| String::from("SIGTERM")); + let spawned = self.spawn_descendant_javascript_child_process( + vm_id, + process_id, + current_process_path, + request, + )?; + let child_process_id = spawned + .get("childId") + .and_then(Value::as_str) + .ok_or_else(|| { + SidecarError::InvalidState(String::from( + "child_process.spawn_sync response is missing childId", + )) + })? + .to_owned(); + + if let Some(input) = sync_input.as_deref() { + self.write_descendant_javascript_child_process_stdin( + vm_id, + process_id, + current_process_path, + &child_process_id, + input, + )?; + } + self.close_descendant_javascript_child_process_stdin( + vm_id, + process_id, + current_process_path, + &child_process_id, + )?; + + let max_buffer = max_buffer.unwrap_or(1024 * 1024); + let mut stdout = Vec::new(); + let mut stderr = Vec::new(); + let mut max_buffer_exceeded = false; + let mut kill_sent = false; + let mut timed_out = false; + + let exit_code = loop { + let wait_ms = if let Some(deadline) = timeout_deadline { + let now = Instant::now(); + if now >= deadline { + if !kill_sent { + timed_out = true; + self.kill_descendant_javascript_child_process( + vm_id, + process_id, + current_process_path, + &child_process_id, + &timeout_signal, + )?; + kill_sent = true; + } + 0 + } else { + u64::try_from(deadline.saturating_duration_since(now).as_millis().min(50)) + .unwrap_or(50) + } + } else { + 50 + }; + let event = self.poll_descendant_javascript_child_process( + vm_id, + process_id, + current_process_path, + &child_process_id, + wait_ms, + )?; + if event.is_null() { + continue; + } + + match event.get("type").and_then(Value::as_str) { + Some("stdout") => { + let chunk = javascript_sync_rpc_bytes_arg( + &[event.get("data").cloned().unwrap_or(Value::Null)], + 0, + "child_process.spawn_sync stdout", + )?; + stdout.extend_from_slice(&chunk); + if stdout.len() > max_buffer && !kill_sent { + max_buffer_exceeded = true; + self.kill_descendant_javascript_child_process( + vm_id, + process_id, + current_process_path, + &child_process_id, + "SIGTERM", + )?; + kill_sent = true; + } + } + Some("stderr") => { + let chunk = javascript_sync_rpc_bytes_arg( + &[event.get("data").cloned().unwrap_or(Value::Null)], + 0, + "child_process.spawn_sync stderr", + )?; + stderr.extend_from_slice(&chunk); + if stderr.len() > max_buffer && !kill_sent { + max_buffer_exceeded = true; + self.kill_descendant_javascript_child_process( + vm_id, + process_id, + current_process_path, + &child_process_id, + "SIGTERM", + )?; + kill_sent = true; + } + } + Some("exit") => { + break event + .get("exitCode") + .and_then(Value::as_i64) + .map(|value| value as i32) + .unwrap_or(1); + } + _ => {} + } + }; + + Ok(json!({ + "stdout": String::from_utf8_lossy(&stdout), + "stderr": String::from_utf8_lossy(&stderr), + "code": exit_code, + "signal": if timed_out { Value::String(timeout_signal) } else { Value::Null }, + "timedOut": timed_out, + "maxBufferExceeded": max_buffer_exceeded, + })) + } + + fn handle_descendant_javascript_child_process_rpc( + &mut self, + vm_id: &str, + process_id: &str, + current_process_path: &[&str], + request: &JavascriptSyncRpcRequest, + ) -> Result { + match request.method.as_str() { + "child_process.spawn" => { + let Some(vm) = self.vms.get(vm_id) else { + return Ok(Value::Null); + }; + let (payload, _) = parse_javascript_child_process_spawn_request(vm, &request.args)?; + self.spawn_descendant_javascript_child_process( + vm_id, + process_id, + current_process_path, + payload, + ) + } + "child_process.spawn_sync" => { + let Some(vm) = self.vms.get(vm_id) else { + return Ok(Value::Null); + }; + let (payload, max_buffer) = + parse_javascript_child_process_spawn_request(vm, &request.args)?; + self.spawn_descendant_javascript_child_process_sync( + vm_id, + process_id, + current_process_path, + payload, + max_buffer, + ) + } + "child_process.poll" => { + let child_process_id = + javascript_sync_rpc_arg_str(&request.args, 0, "child_process.poll child id")?; + let wait_ms = javascript_sync_rpc_arg_u64_optional( + &request.args, + 1, + "child_process.poll wait ms", + )? + .unwrap_or_default(); + self.poll_descendant_javascript_child_process( + vm_id, + process_id, + current_process_path, + child_process_id, + wait_ms, + ) + } + "child_process.write_stdin" => { + let child_process_id = javascript_sync_rpc_arg_str( + &request.args, + 0, + "child_process.write_stdin child id", + )?; + let chunk = javascript_sync_rpc_bytes_arg( + &request.args, + 1, + "child_process.write_stdin chunk", + )?; + self.write_descendant_javascript_child_process_stdin( + vm_id, + process_id, + current_process_path, + child_process_id, + &chunk, + )?; + Ok(Value::Null) + } + "child_process.close_stdin" => { + let child_process_id = javascript_sync_rpc_arg_str( + &request.args, + 0, + "child_process.close_stdin child id", + )?; + self.close_descendant_javascript_child_process_stdin( + vm_id, + process_id, + current_process_path, + child_process_id, + )?; + Ok(Value::Null) + } + "child_process.kill" => { + let child_process_id = + javascript_sync_rpc_arg_str(&request.args, 0, "child_process.kill child id")?; + let signal = + javascript_sync_rpc_arg_str(&request.args, 1, "child_process.kill signal")?; + self.kill_descendant_javascript_child_process( + vm_id, + process_id, + current_process_path, + child_process_id, + signal, + )?; + Ok(Value::Null) + } + _ => Err(SidecarError::InvalidState(format!( + "unsupported nested child process RPC method {}", + request.method + ))), + } + } + + /// Deferred servicing for a CHILD's kernel read / poll sync RPCs + /// inside the child-event pump: probe readiness with a zero timeout, reply + /// when ready / expired / non-blocking, otherwise park the RPC on the child + /// (reply-by-token). The pump loop re-checks the parked RPC every + /// iteration, so the dispatch loop never blocks in a kernel wait on the + /// child's behalf. Returns false when the RPC must be serviced inline + /// (non-TTY JavaScript local stdin bridge). + fn service_child_kernel_wait_rpc( + &mut self, + vm_id: &str, + process_id: &str, + current_process_path: &[&str], + child_process_id: &str, + request: &JavascriptSyncRpcRequest, + ) -> Result { + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(true); + }; + let kernel = &mut vm.kernel; + let Some(root) = vm.active_processes.get_mut(process_id) else { + return Ok(true); + }; + let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else { + return Ok(true); + }; + let Some(child) = parent.child_processes.get_mut(child_process_id) else { + return Ok(true); + }; + if request.method == "__kernel_stdin_read" + && matches!(child.execution, ActiveExecution::Javascript(_)) + && child.tty_master_fd.is_none() + { + return Ok(false); + } + // Top off the child's stdin pipe from any host-side backlog before + // probing readiness: the child draining the pipe is exactly what frees + // capacity for the next queued stdin bytes (and, once the backlog is + // empty, executes a deferred stdin close so EOF arrives in order). + flush_pending_kernel_stdin(kernel, child)?; + let now = Instant::now(); + let requested_timeout_ms = match request.method.as_str() { + "__kernel_stdin_read" => parse_kernel_stdin_read_args(request)?.1, + "__kernel_poll" => u64::try_from(parse_kernel_poll_args(request)?.1).unwrap_or(0), + "process.fd_read" => { + javascript_sync_rpc_arg_u64_optional(&request.args, 2, "fd_read timeout ms")? + .unwrap_or(DEFAULT_KERNEL_STDIN_READ_TIMEOUT_MS) + } + _ => return Ok(false), + }; + let deadline = match &child.deferred_kernel_wait_rpc { + Some((parked, parked_deadline)) if parked.id == request.id => *parked_deadline, + _ => now + Duration::from_millis(requested_timeout_ms), + }; + let kernel_pid = child.kernel_pid; + let mut fd_read = None; + let probe = match request.method.as_str() { + "__kernel_stdin_read" => { + let (max_bytes, _) = parse_kernel_stdin_read_args(request)?; + kernel_stdin_read_response(kernel, kernel_pid, max_bytes, Duration::ZERO) + .map(|value| (value, true)) + } + "__kernel_poll" => { + let (fd_requests, _) = parse_kernel_poll_args(request)?; + kernel_poll_response(kernel, kernel_pid, &fd_requests, 0).map(|value| (value, true)) + } + "process.fd_read" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_read fd")?; + let length = usize::try_from(javascript_sync_rpc_arg_u64( + &request.args, + 1, + "fd_read length", + )?) + .map_err(|_| SidecarError::InvalidState("fd_read length is too large".into()))?; + fd_read = Some((fd, length)); + kernel + .poll_fds( + EXECUTION_DRIVER_NAME, + kernel_pid, + vec![PollFd::new(fd, POLLIN)], + 0, + ) + .map(|result| (Value::Null, result.ready_count > 0)) + .map_err(kernel_error) + } + _ => unreachable!("unsupported deferred kernel wait method"), + }; + let (probe, fd_read_ready) = match probe { + Ok(probe) => probe, + Err(error) => { + child.deferred_kernel_wait_rpc = None; + child + .execution + .respond_javascript_sync_rpc_error( + request.id, + javascript_sync_rpc_error_code(&error), + error.to_string(), + ) + .or_else(ignore_stale_javascript_sync_rpc_response)?; + return Ok(true); + } + }; + let ready = match request.method.as_str() { + "__kernel_stdin_read" => !probe.is_null(), + "__kernel_poll" => probe.get("readyCount").and_then(Value::as_u64).unwrap_or(0) > 0, + "process.fd_read" => fd_read_ready, + _ => unreachable!("unsupported deferred kernel wait method"), + }; + if ready || requested_timeout_ms == 0 || now >= deadline { + child.deferred_kernel_wait_rpc = None; + if let Some((fd, length)) = fd_read { + // A deferred read's V8 reply token can time out independently + // of this sidecar poll. Claim it before the destructive read so + // a stale request can never consume bytes intended for a later + // read on the same descriptor. + let claimed = child + .execution + .claim_javascript_sync_rpc_response(request.id)?; + if !claimed { + return Ok(true); + } + let read_result = kernel.fd_read_with_timeout_result( + EXECUTION_DRIVER_NAME, + kernel_pid, + fd, + length, + Some(Duration::ZERO), + ); + match read_result { + Ok(Some(bytes)) => child + .execution + .respond_claimed_javascript_sync_rpc_success( + request.id, + javascript_sync_rpc_bytes_value(&bytes), + )?, + Ok(None) => child + .execution + .respond_claimed_javascript_sync_rpc_success( + request.id, + javascript_sync_rpc_bytes_value(&[]), + )?, + Err(error) => { + let error = kernel_error(error); + child.execution.respond_claimed_javascript_sync_rpc_error( + request.id, + javascript_sync_rpc_error_code(&error), + error.to_string(), + )?; + } + } + return Ok(true); + } + child + .execution + .respond_javascript_sync_rpc_response(request.id, probe.into()) + .or_else(ignore_stale_javascript_sync_rpc_response)?; + return Ok(true); + } + child.deferred_kernel_wait_rpc = Some((request.clone(), deadline)); + Ok(true) + } + + /// Re-probe every parked descriptor read in the VM after a successful + /// operation that can make a descriptor readable. Linux wakes blocked + /// readers as part of the write/close transition; relying only on a later + /// child-process pump can otherwise strand a reader until its deadline. + /// + /// The walk is bounded by the VM's enforced process limit: every visited + /// `ActiveProcess` has a corresponding kernel process accounted against + /// `ResourceLimits::max_processes`. + pub(crate) fn wake_ready_deferred_fd_reads(vm: &mut VmState) -> Result<(), SidecarError> { + let kernel = &mut vm.kernel; + for process in vm.active_processes.values_mut() { + recheck_ready_deferred_fd_reads(kernel, process)?; + } + Ok(()) + } + + /// Service `__kernel_stdio_write` for a process writing to the shared PTY. + /// Apply the slave line discipline once, then enqueue the drained bytes on + /// the owning process before acknowledging the write. The owner's local + /// queue is drained before its next runtime event, keeping child output + /// ahead of the next shell prompt. + pub(crate) fn service_shared_tty_stdio_write( + &mut self, + vm_id: &str, + writer_kernel_pid: u32, + owner: (u32, u32), + request: &JavascriptSyncRpcRequest, + ) -> Result { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "__kernel_stdio_write fd")?; + let chunk = javascript_sync_rpc_bytes_arg(&request.args, 1, "__kernel_stdio_write chunk")?; + if fd != 1 && fd != 2 { + return Err(SidecarError::InvalidState(format!( + "__kernel_stdio_write only supports fd 1/2, got {fd}" + ))); + } + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(json!(chunk.len())); + }; + let written = if fd == 1 { + vm.kernel + .write_process_stdout(EXECUTION_DRIVER_NAME, writer_kernel_pid, &chunk) + .map_err(kernel_error)? + } else { + vm.kernel + .write_process_stderr(EXECUTION_DRIVER_NAME, writer_kernel_pid, &chunk) + .map_err(kernel_error)? + }; + let (owner_pid, master_fd) = owner; + let mut drained = Vec::new(); + loop { + match vm.kernel.fd_read_with_timeout_result( + EXECUTION_DRIVER_NAME, + owner_pid, + master_fd, + MAX_PTY_BUFFER_BYTES, + Some(Duration::ZERO), + ) { + Ok(Some(bytes)) if !bytes.is_empty() => drained.extend(bytes), + Ok(_) => break, + Err(error) if error.code() == "EAGAIN" => break, + Err(error) => return Err(kernel_error(error)), + } + } + if !drained.is_empty() { + let Some(owner_process) = vm + .active_processes + .values_mut() + .find(|process| process.kernel_pid == owner_pid) + else { + return Err(SidecarError::InvalidState(format!( + "shared PTY owner pid {owner_pid} is not active" + ))); + }; + owner_process.queue_pending_execution_event(ActiveExecutionEvent::Stdout(drained))?; + } + Ok(json!(written)) + } + + /// Re-check a child's parked kernel-wait RPC (see + /// `service_child_kernel_wait_rpc`); called once per pump-loop iteration. + fn recheck_child_deferred_kernel_wait_rpc( + &mut self, + vm_id: &str, + process_id: &str, + current_process_path: &[&str], + child_process_id: &str, + ) -> Result<(), SidecarError> { + let parked = { + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(()); + }; + let Some(root) = vm.active_processes.get_mut(process_id) else { + return Ok(()); + }; + let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else { + return Ok(()); + }; + let Some(child) = parent.child_processes.get_mut(child_process_id) else { + return Ok(()); + }; + child + .deferred_kernel_wait_rpc + .as_ref() + .map(|(request, _)| request.clone()) + }; + if let Some(request) = parked { + let _ = self.service_child_kernel_wait_rpc( + vm_id, + process_id, + current_process_path, + child_process_id, + &request, + )?; + } + Ok(()) + } + + fn poll_descendant_javascript_child_process( + &mut self, + vm_id: &str, + process_id: &str, + current_process_path: &[&str], + child_process_id: &str, + wait_ms: u64, + ) -> Result { + let mut child_path = current_process_path.to_vec(); + child_path.push(child_process_id); + let child_gone_error = || javascript_child_process_gone_error(process_id, &child_path); + let has_live_sibling = self + .vms + .get(vm_id) + .and_then(|vm| Self::descendant_parent_process(vm, process_id, current_process_path)) + .is_some_and(|parent| { + parent + .child_processes + .keys() + .any(|sibling_id| sibling_id != child_process_id) + }); + let deadline = Instant::now() + Duration::from_millis(wait_ms); + let mut internal_rpc_deadline = None; + let mut internal_rpcs_serviced = 0usize; + let mut internal_rpc_quantum_exhausted = || { + if wait_ms == 0 { + return true; + } + internal_rpcs_serviced += 1; + let rpc_deadline = internal_rpc_deadline + .get_or_insert_with(|| Instant::now() + DESCENDANT_INTERNAL_RPC_QUANTUM); + (has_live_sibling && internal_rpcs_serviced >= MAX_DESCENDANT_INTERNAL_RPCS_PER_POLL) + || Instant::now() >= *rpc_deadline + }; + + loop { + self.drain_queued_descendant_javascript_child_process_events( + vm_id, + process_id, + &child_path, + )?; + self.recheck_child_deferred_kernel_wait_rpc( + vm_id, + process_id, + current_process_path, + child_process_id, + )?; + enum ChildPollResult { + Event(Box>), + RecoverRuntimeExit, + } + let wait = if wait_ms == 0 { + Duration::ZERO + } else { + deadline.saturating_duration_since(Instant::now()) + }; + let poll_result = { + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(Value::Null); + }; + let Some(parent) = + Self::descendant_parent_process_mut(vm, process_id, current_process_path) + else { + return Err(child_gone_error()); + }; + let Some(child) = parent.child_processes.get_mut(child_process_id) else { + return Err(child_gone_error()); + }; + if let Some(event) = child.lease_pending_execution_event() { + ChildPollResult::Event(Box::new(Some(event))) + } else { + match child.poll_execution_event_blocking(wait) { + Ok(Some(event)) => ChildPollResult::Event(Box::new(Some(event))), + Ok(None) => ChildPollResult::RecoverRuntimeExit, + Err(SidecarError::Execution(message)) + if (child.runtime == GuestRuntimeKind::JavaScript + && closed_javascript_event_channel(&message)) + || (child.runtime == GuestRuntimeKind::Python + && closed_python_event_channel(&message)) + || (child.runtime == GuestRuntimeKind::WebAssembly + && closed_wasm_event_channel(&message)) => + { + ChildPollResult::RecoverRuntimeExit + } + Err(error) => return Err(error), + } + } + }; + let event = match poll_result { + ChildPollResult::Event(event) => *event, + ChildPollResult::RecoverRuntimeExit => self + .recover_descendant_runtime_child_process_event( + vm_id, + process_id, + current_process_path, + child_process_id, + wait.as_millis().try_into().unwrap_or(u64::MAX), + )? + .map(PolledExecutionEvent::unreserved), + }; + + let Some(event) = event else { + return Ok(Value::Null); + }; + + match event.into_event() { + ActiveExecutionEvent::Stdout(chunk) => { + return Ok(json!({ + "type": "stdout", + "data": javascript_sync_rpc_bytes_value(&chunk), + })); + } + ActiveExecutionEvent::Stderr(chunk) => { + return Ok(json!({ + "type": "stderr", + "data": javascript_sync_rpc_bytes_value(&chunk), + })); + } + ActiveExecutionEvent::Exited(mut exit_code) => { + let cleanup_start = Instant::now(); + // Native runner exits carry the authoritative terminating + // signal/core bit. Do not infer a signal solely from + // 128+N: a program may legitimately exit with that value. + { + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(Value::Null); + }; + let Some(parent) = Self::descendant_parent_process_mut( + vm, + process_id, + current_process_path, + ) else { + return Ok(Value::Null); + }; + let Some(child) = parent.child_processes.get_mut(child_process_id) else { + return Ok(Value::Null); + }; + let runtime_pid = child.execution.child_pid(); + if runtime_pid != 0 && !child.execution.uses_shared_v8_runtime() { + if let RuntimeChildStatusObservation::Exited(status) = + runtime_child_exit_status(runtime_pid)? + { + exit_code = status.status; + child.exit_signal = status.signal; + child.exit_core_dumped = status.core_dumped; + } + } + } + let parent_signal_key = + Self::child_process_signal_key(process_id, current_process_path); + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(Value::Null); + }; + let (signal_name, core_dumped) = { + let Some(parent) = Self::descendant_parent_process_mut( + vm, + process_id, + current_process_path, + ) else { + return Ok(Value::Null); + }; + let Some(child) = parent.child_processes.get_mut(child_process_id) else { + return Ok(Value::Null); + }; + let actual_signal = child.exit_signal.take(); + child.pending_self_signal_exit = None; + ( + actual_signal + .and_then(canonical_signal_name) + .map(str::to_owned), + child.exit_core_dumped, + ) + }; + let ( + parent_runtime_pid, + parent_v8_signal_session, + parent_is_wasm, + should_signal_parent, + ) = { + let Some(parent) = + Self::descendant_parent_process(vm, process_id, current_process_path) + else { + return Ok(Value::Null); + }; + ( + parent.execution.child_pid(), + parent.execution.javascript_v8_session_handle().filter(|_| { + matches!( + &parent.execution, + ActiveExecution::Javascript(execution) + if execution.uses_shared_v8_runtime() + ) + }), + matches!(&parent.execution, ActiveExecution::Wasm(_)), + vm.signal_states + .get(parent_signal_key) + .and_then(|handlers| handlers.get(&(libc::SIGCHLD as u32))) + .is_some_and(|registration| { + registration.action != SignalDispositionAction::Default + }), + ) + }; + let (mut child, detached_children) = { + let Some(parent) = Self::descendant_parent_process_mut( + vm, + process_id, + current_process_path, + ) else { + return Ok(Value::Null); + }; + let Some(mut child) = parent.child_processes.remove(child_process_id) + else { + return Ok(Value::Null); + }; + let child_process_label = + Self::child_process_path_label(process_id, &child_path); + let detached_children = + Self::adopt_detached_child_processes(&child_process_label, &mut child); + (child, detached_children) + }; + sync_process_host_writes_to_kernel(vm, &child)?; + release_inherited_child_raw_mode(&mut vm.kernel, &child)?; + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + terminate_child_process_tree( + &mut vm.kernel, + &mut child, + &kernel_readiness, + &vm.unix_address_registry, + ); + child.kernel_handle.finish(exit_code); + let _ = vm.kernel.wait_and_reap(child.kernel_pid); + vm.signal_states.remove(child_process_id); + for (detached_process_id, detached_child) in detached_children { + vm.detached_child_processes + .insert(detached_process_id.clone()); + vm.active_processes + .insert(detached_process_id, detached_child); + } + if should_signal_parent { + if parent_is_wasm { + // WASM guests do not install a host-OS signal handler. + // Their runner drains this process-owned queue at syscall + // boundaries, including ppoll(2) and waitpid(2). Sending + // SIGCHLD to the runner PID bypasses that cooperative path. + let Some(parent) = Self::descendant_parent_process_mut( + vm, + process_id, + current_process_path, + ) else { + return Ok(Value::Null); + }; + parent.queue_pending_wasm_signal(libc::SIGCHLD)?; + } else if let Some(session) = parent_v8_signal_session { + dispatch_v8_session_signal_async(session, libc::SIGCHLD); + } else { + signal_runtime_process(parent_runtime_pid, libc::SIGCHLD)?; + } + } + let mut payload = Map::new(); + payload.insert(String::from("type"), Value::String(String::from("exit"))); + payload.insert(String::from("exitCode"), Value::from(exit_code)); + payload.insert(String::from("coreDumped"), Value::from(core_dumped)); + if let Some(signal_name) = signal_name { + payload.insert(String::from("signal"), Value::String(signal_name)); + } + record_execute_phase("child_process_exit_cleanup", cleanup_start.elapsed()); + return Ok(Value::Object(payload)); + } + ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => { + let mut current_child_path = current_process_path.to_vec(); + current_child_path.push(child_process_id); + if let Some(wait_request) = deferred_child_kernel_wait_request(&request)? { + if self.service_child_kernel_wait_rpc( + vm_id, + process_id, + current_process_path, + child_process_id, + &wait_request, + )? { + // Replied immediately or parked on the child; the + // pump loop re-checks parked RPCs every iteration. + return Ok(Value::Null); + } + } + if request.method == "__kernel_stdio_write" { + let shared_tty = { + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(Value::Null); + }; + let Some(root) = vm.active_processes.get_mut(process_id) else { + return Ok(Value::Null); + }; + let Some(parent) = + Self::active_process_by_path_mut(root, current_process_path) + else { + return Ok(Value::Null); + }; + parent + .child_processes + .get(child_process_id) + .and_then(|child| { + child + .tty_master_owner + .map(|owner| (child.kernel_pid, owner)) + }) + }; + if let Some((child_kernel_pid, owner)) = shared_tty { + let response = self.service_shared_tty_stdio_write( + vm_id, + child_kernel_pid, + owner, + &request, + ); + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(Value::Null); + }; + let Some(root) = vm.active_processes.get_mut(process_id) else { + return Ok(Value::Null); + }; + let Some(parent) = + Self::active_process_by_path_mut(root, current_process_path) + else { + return Ok(Value::Null); + }; + let Some(child) = parent.child_processes.get_mut(child_process_id) + else { + return Ok(Value::Null); + }; + match response { + Ok(result) => child + .execution + .respond_javascript_sync_rpc_response(request.id, result.into()) + .or_else(ignore_stale_javascript_sync_rpc_response)?, + Err(error) => child + .execution + .respond_javascript_sync_rpc_error( + request.id, + javascript_sync_rpc_error_code(&error), + error.to_string(), + ) + .or_else(ignore_stale_javascript_sync_rpc_response)?, + } + return Ok(Value::Null); + } + } + let response = if request.method == "process.exec_fd_image_commit" { + let payload = { + let Some(vm) = self.vms.get(vm_id) else { + return Ok(Value::Null); + }; + parse_javascript_child_process_spawn_request(vm, &request.args)?.0 + }; + self.commit_wasm_fd_process_image( + vm_id, + process_id, + ¤t_child_path, + payload, + )?; + Ok(json!({ "committed": true }).into()) + } else if request.method == "process.exec" { + let payload = { + let Some(vm) = self.vms.get(vm_id) else { + return Ok(Value::Null); + }; + parse_javascript_child_process_spawn_request(vm, &request.args)?.0 + }; + let local_replacement = payload.options.local_replacement; + match self.exec_javascript_process_image( + vm_id, + process_id, + ¤t_child_path, + payload, + ) { + Ok(()) if local_replacement => Ok(json!({ "committed": true }).into()), + // Separate-runtime replacement destroys the old + // image and therefore receives no response. + Ok(()) => return Ok(Value::Null), + Err(error) => Err(error), + } + } else if request.method == "process.signal_state" { + let (signal, registration) = + parse_process_signal_state_request(&request.args) + .map_err(|error| SidecarError::InvalidState(error.to_string()))?; + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(Value::Null); + }; + let signal_key = + Self::child_process_signal_key(process_id, ¤t_child_path) + .to_owned(); + apply_process_signal_state_update( + &mut vm.signal_states, + &signal_key, + signal, + registration, + ); + Ok(Value::Null.into()) + } else if request.method == "process.kill" { + self.handle_descendant_process_kill_rpc( + vm_id, + process_id, + current_process_path, + child_process_id, + &request, + ) + .map(Into::into) + } else if request.method.starts_with("child_process.") { + self.handle_descendant_javascript_child_process_rpc( + vm_id, + process_id, + ¤t_child_path, + &request, + ) + .map(Into::into) + } else { + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(Value::Null); + }; + let resource_limits = vm.kernel.resource_limits().clone(); + let network_counts = vm_network_resource_counts(vm); + let socket_paths = build_javascript_socket_path_context(vm)?; + let kernel_readiness = Arc::clone(&vm.kernel_socket_readiness); + let Some(root) = vm.active_processes.get_mut(process_id) else { + return Ok(Value::Null); + }; + let Some(parent) = + Self::active_process_by_path_mut(root, current_process_path) + else { + return Ok(Value::Null); + }; + let Some(child) = parent.child_processes.get_mut(child_process_id) else { + return Ok(Value::Null); + }; + service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest { + bridge: &self.bridge, + vm_id, + dns: &vm.dns, + socket_paths: &socket_paths, + kernel: &mut vm.kernel, + kernel_readiness, + process: child, + sync_request: &request, + resource_limits: &resource_limits, + network_counts, + }) + }; + + if response.is_ok() && javascript_sync_rpc_may_make_fd_readable(&request) { + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(Value::Null); + }; + Self::wake_ready_deferred_fd_reads(vm)?; + } + + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(Value::Null); + }; + let Some(parent) = + Self::descendant_parent_process_mut(vm, process_id, current_process_path) + else { + return Ok(Value::Null); + }; + let Some(child) = parent.child_processes.get_mut(child_process_id) else { + return Ok(Value::Null); + }; + let parent_signal_event = response + .as_ref() + .ok() + .and_then(JavascriptSyncRpcServiceResponse::as_json) + .and_then(|result| { + let target_path_label = + Self::child_process_path_label(process_id, current_process_path); + if request.method != "process.kill" + || result.get("action").and_then(Value::as_str) != Some("user") + || result.get("targetProcessPath").and_then(Value::as_str) + != Some(target_path_label.as_str()) + { + return None; + } + Some(json!({ + "type": "signal", + "signal": result.get("signal").and_then(Value::as_str).unwrap_or_default(), + "number": result.get("number").and_then(Value::as_i64).unwrap_or_default(), + })) + }); + match response { + Ok(result) => child + .execution + .respond_javascript_sync_rpc_response(request.id, result) + .or_else(ignore_stale_javascript_sync_rpc_response)?, + Err(error) => child + .execution + .respond_javascript_sync_rpc_error( + request.id, + javascript_sync_rpc_error_code(&error), + error.to_string(), + ) + .or_else(ignore_stale_javascript_sync_rpc_response)?, + } + if let Some(event) = parent_signal_event { + return Ok(event); + } + // A guest can issue another synchronous RPC immediately + // after each reply (network poll loops do exactly that). + // Service a short bounded batch before yielding: Linux + // schedules a process for a time quantum, not one syscall, + // while still preventing a busy child from starving a + // sibling that must make it runnable. + if internal_rpc_quantum_exhausted() { + return Ok(Value::Null); + } + continue; + } + ActiveExecutionEvent::PythonVfsRpcRequest(request) => { + // The kernel-VFS bridge is wired for top-level Python + // executions; a nested Python child (spawned by a JS/Python + // parent) cannot service VFS RPCs through this child-event + // path. Respond with a recoverable error instead of aborting + // the child, so its runner falls back to the in-isolate FS + // for the nested process — top-level Python keeps the full + // VFS root. + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(Value::Null); + }; + let Some(parent) = + Self::descendant_parent_process_mut(vm, process_id, current_process_path) + else { + return Ok(Value::Null); + }; + let Some(child) = parent.child_processes.get_mut(child_process_id) else { + return Ok(Value::Null); + }; + // Best-effort: deliver the "unavailable" error so the child's + // pending VFS RPC resolves and its runner falls back to the + // in-isolate FS. If delivery fails the child has already gone + // away (broken pipe / no-longer-pending), so dropping the + // result is correct here — there is nothing left to hang. + let _ = child.execution.respond_python_vfs_rpc_error( + request.id, + "ERR_AGENTOS_PYTHON_VFS_UNAVAILABLE", + "python VFS is not available for nested child processes", + ); + return Ok(Value::Null); + } + ActiveExecutionEvent::SignalState { + signal, + registration, + } => { + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(Value::Null); + }; + let signal_key = + Self::child_process_signal_key(process_id, &child_path).to_owned(); + apply_process_signal_state_update( + &mut vm.signal_states, + &signal_key, + signal, + registration.clone(), + ); + return Ok(json!({ + "type": "signal_state", + "signal": signal, + "registration": registration, + })); + } + } + } + } + + fn recover_descendant_runtime_child_process_event( + &mut self, + vm_id: &str, + process_id: &str, + current_process_path: &[&str], + child_process_id: &str, + wait_ms: u64, + ) -> Result, SidecarError> { + let ( + parent_kernel_pid, + child_kernel_pid, + child_runtime_pid, + child_runtime, + child_shared_runtime, + ) = { + let mut child_path = current_process_path.to_vec(); + child_path.push(child_process_id); + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(None); + }; + let Some(parent) = + Self::descendant_parent_process_mut(vm, process_id, current_process_path) + else { + return Err(javascript_child_process_gone_error(process_id, &child_path)); + }; + let Some(child) = parent.child_processes.get_mut(child_process_id) else { + return Err(javascript_child_process_gone_error(process_id, &child_path)); + }; + ( + parent.kernel_pid, + child.kernel_pid, + child.execution.child_pid(), + child.runtime.clone(), + child.execution.uses_shared_v8_runtime(), + ) + }; + if child_runtime != GuestRuntimeKind::JavaScript + && child_runtime != GuestRuntimeKind::Python + && child_runtime != GuestRuntimeKind::WebAssembly + { + return Ok(None); + } + let wait_deadline = Instant::now() + Duration::from_millis(wait_ms.min(25)); + loop { + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(None); + }; + if let Some(process_info) = vm.kernel.list_processes().get(&child_kernel_pid) { + if process_info.status == ProcessStatus::Exited { + return Ok(Some(ActiveExecutionEvent::Exited( + process_info.exit_code.unwrap_or(0), + ))); + } + } + if let Some(wait_result) = vm + .kernel + .waitpid_with_options( + EXECUTION_DRIVER_NAME, + parent_kernel_pid, + child_kernel_pid as i32, + WaitPidFlags::WNOHANG, + ) + .map_err(kernel_error)? + { + return Ok(Some(ActiveExecutionEvent::Exited(wait_result.status))); + } + + if !child_shared_runtime && child_runtime_pid != 0 { + match runtime_child_exit_status(child_runtime_pid)? { + RuntimeChildStatusObservation::Exited(status) => { + let Some(root) = vm.active_processes.get_mut(process_id) else { + return Ok(None); + }; + let Some(parent) = + Self::active_process_by_path_mut(root, current_process_path) + else { + return Ok(None); + }; + let Some(child) = parent.child_processes.get_mut(child_process_id) else { + return Ok(None); + }; + child.exit_signal = status.signal; + child.exit_core_dumped = status.core_dumped; + return Ok(Some(ActiveExecutionEvent::Exited(status.status))); + } + RuntimeChildStatusObservation::Running => {} + RuntimeChildStatusObservation::NotWaitable => { + return Err(SidecarError::Execution(format!( + "ECHILD: guest runtime process {child_runtime_pid} exited without an observable wait status" + ))); + } + } + } + if Instant::now() >= wait_deadline { + return Ok(None); + } + std::thread::sleep(Duration::from_millis(5)); + } + } + + fn write_descendant_javascript_child_process_stdin( + &mut self, + vm_id: &str, + process_id: &str, + current_process_path: &[&str], + child_process_id: &str, + chunk: &[u8], + ) -> Result<(), SidecarError> { + let mut child_path = current_process_path.to_vec(); + child_path.push(child_process_id); + let Some(vm) = self.vms.get_mut(vm_id) else { + return Err(javascript_child_process_gone_error(process_id, &child_path)); + }; + let pending_stdin_limit = vm.limits.process.pending_stdin_bytes; + let Some(root) = vm.active_processes.get_mut(process_id) else { + return Err(javascript_child_process_gone_error(process_id, &child_path)); + }; + let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else { + return Err(javascript_child_process_gone_error(process_id, &child_path)); + }; + let Some(child) = parent.child_processes.get_mut(child_process_id) else { + return Err(javascript_child_process_gone_error(process_id, &child_path)); + }; + child.execution.write_stdin(chunk)?; + write_kernel_process_stdin(&mut vm.kernel, child, chunk, pending_stdin_limit) + } + + fn close_descendant_javascript_child_process_stdin( + &mut self, + vm_id: &str, + process_id: &str, + current_process_path: &[&str], + child_process_id: &str, + ) -> Result<(), SidecarError> { + let mut child_path = current_process_path.to_vec(); + child_path.push(child_process_id); + let Some(vm) = self.vms.get_mut(vm_id) else { + return Err(javascript_child_process_gone_error(process_id, &child_path)); + }; + let Some(root) = vm.active_processes.get_mut(process_id) else { + return Err(javascript_child_process_gone_error(process_id, &child_path)); + }; + let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else { + return Err(javascript_child_process_gone_error(process_id, &child_path)); + }; + let Some(child) = parent.child_processes.get_mut(child_process_id) else { + return Err(javascript_child_process_gone_error(process_id, &child_path)); + }; + child.execution.close_stdin()?; + close_kernel_process_stdin(&mut vm.kernel, child) + } + + fn kill_descendant_javascript_child_process( + &mut self, + vm_id: &str, + process_id: &str, + current_process_path: &[&str], + child_process_id: &str, + signal: &str, + ) -> Result<(), SidecarError> { + let signal_name = signal.to_owned(); + let signal = parse_signal(signal)?; + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(()); + }; + let registration = vm + .signal_states + .get(child_process_id) + .and_then(|handlers| handlers.get(&(signal as u32))) + .cloned(); + let Some(root) = vm.active_processes.get_mut(process_id) else { + return Ok(()); + }; + let Some(parent) = Self::active_process_by_path_mut(root, current_process_path) else { + return Ok(()); + }; + let source_pid = parent.kernel_pid; + let Some(child) = parent.child_processes.get_mut(child_process_id) else { + return Ok(()); + }; + terminate_tracked_child_process_for_signal( + &mut vm.kernel, + child, + signal, + registration.as_ref(), + )?; + let child_process_label = if current_process_path.is_empty() { + child_process_id.to_owned() + } else { + format!("{}/{}", current_process_path.join("/"), child_process_id) + }; + emit_security_audit_event( + &self.bridge, + vm_id, + "security.process.kill", + audit_fields([ + (String::from("source"), String::from("guest_child_process")), + (String::from("source_pid"), source_pid.to_string()), + (String::from("target_pid"), child.kernel_pid.to_string()), + (String::from("process_id"), process_id.to_owned()), + (String::from("child_process_id"), child_process_label), + (String::from("signal"), signal_name), + ]), + ); + Ok(()) + } + + fn handle_descendant_process_kill_rpc( + &mut self, + vm_id: &str, + process_id: &str, + current_process_path: &[&str], + child_process_id: &str, + request: &JavascriptSyncRpcRequest, + ) -> Result { + let target_pid = javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?; + let signal_name = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?; + let signal = parse_signal(signal_name)?; + + let mut source_path = current_process_path.to_vec(); + source_path.push(child_process_id); + + if signal != 0 && target_pid < 0 { + let pgid = target_pid.unsigned_abs(); + let caller_kernel_pid = { + let Some(vm) = self.vms.get(vm_id) else { + return Err(SidecarError::InvalidState(String::from( + "ESRCH: unknown VM during process.kill", + ))); + }; + let Some(root) = vm.active_processes.get(process_id) else { + return Err(SidecarError::InvalidState(format!( + "ESRCH: unknown process {process_id} during process.kill", + ))); + }; + let Some(source) = Self::active_process_by_path(root, &source_path) else { + return Err(SidecarError::InvalidState(format!( + "ESRCH: unknown child process {child_process_id} during process.kill", + ))); + }; + source.kernel_pid + }; + let caller_is_member = + self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal_name)?; + if !caller_is_member { + return Ok(Value::Null); + } + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(Value::Null); + }; + let Some(root) = vm.active_processes.get_mut(process_id) else { + return Ok(Value::Null); + }; + let Some(source) = Self::active_process_by_path_mut(root, &source_path) else { + return Ok(Value::Null); + }; + if !matches!( + canonical_signal_name(signal), + Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG") + ) { + apply_active_process_default_signal(&mut vm.kernel, source, signal)?; + } + return Ok(json!({ + "self": true, + "action": "default", + })); + } + + let Some(vm) = self.vms.get_mut(vm_id) else { + return Err(SidecarError::InvalidState(String::from( + "ESRCH: unknown VM during process.kill", + ))); + }; + + if signal == 0 { + vm.kernel + .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal) + .map_err(kernel_error)?; + return Ok(Value::Null); + } + + let target_kernel_pid = u32::try_from(target_pid).map_err(|_| { + SidecarError::InvalidState(format!("EINVAL: invalid process pid {target_pid}")) + })?; + let (source_pid, located_target_path) = { + let Some(root) = vm.active_processes.get(process_id) else { + return Err(SidecarError::InvalidState(format!( + "ESRCH: unknown process {process_id} during process.kill", + ))); + }; + let Some(source) = Self::active_process_by_path(root, &source_path) else { + return Err(SidecarError::InvalidState(format!( + "ESRCH: unknown child process {child_process_id} during process.kill", + ))); + }; + vm.kernel + .signal_process(EXECUTION_DRIVER_NAME, target_pid, 0) + .map_err(kernel_error)?; + ( + source.kernel_pid, + Self::active_process_path_by_kernel_pid(root, target_kernel_pid), + ) + }; + let Some(target_path) = located_target_path else { + // The target is alive but not part of this root's process tree. + // Resolve it VM-wide so cross-tree pids and untracked kernel + // processes still receive the signal. + self.signal_vm_kernel_pid(vm_id, target_kernel_pid, signal_name)?; + return Ok(Value::Null); + }; + let Some(vm) = self.vms.get_mut(vm_id) else { + return Err(SidecarError::InvalidState(String::from( + "ESRCH: unknown VM during process.kill", + ))); + }; + + if source_pid == target_kernel_pid { + let Some(root) = vm.active_processes.get_mut(process_id) else { + return Ok(Value::Null); + }; + let Some(source) = Self::active_process_by_path_mut(root, &source_path) else { + return Ok(Value::Null); + }; + if !matches!( + canonical_signal_name(signal), + Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG") + ) { + apply_active_process_default_signal(&mut vm.kernel, source, signal)?; + } + return Ok(json!({ + "self": true, + "action": "default", + })); + } + + let signal_key = target_path.last().map(String::as_str).unwrap_or(process_id); + let registration = vm + .signal_states + .get(signal_key) + .and_then(|handlers| handlers.get(&(signal as u32))) + .cloned(); + + let action = match registration + .as_ref() + .map(|registration| ®istration.action) + { + Some(SignalDispositionAction::Ignore) => "ignore", + Some(SignalDispositionAction::User) => { + let Some(root) = vm.active_processes.get_mut(process_id) else { + return Ok(Value::Null); + }; + let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path) + else { + return Err(SidecarError::InvalidState(format!( + "ESRCH: unknown process pid {target_pid}" + ))); + }; + if matches!(&target.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()) { + target.queue_pending_wasm_signal(signal)?; + } else if let Some(session) = target.execution.javascript_v8_session_handle().filter( + |_| matches!(&target.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime()), + ) { + dispatch_v8_session_signal_async(session, signal); + } else if !dispatch_v8_process_signal(target, signal)? { + return Err(SidecarError::InvalidState(format!( + "unsupported guest signal delivery for pid {target_pid}" + ))); + } + "user" + } + Some(SignalDispositionAction::Default) | None + if matches!( + canonical_signal_name(signal), + Some("SIGWINCH" | "SIGCHLD" | "SIGURG") + ) => + { + "ignore" + } + Some(SignalDispositionAction::Default) | None => { + let Some(root) = vm.active_processes.get_mut(process_id) else { + return Ok(Value::Null); + }; + let Some(target) = Self::active_process_by_owned_path_mut(root, &target_path) + else { + return Err(SidecarError::InvalidState(format!( + "ESRCH: unknown process pid {target_pid}" + ))); + }; + apply_active_process_default_signal(&mut vm.kernel, target, signal)?; + "default" + } + }; + + let target_path_label = Self::child_process_path_label( + process_id, + &target_path.iter().map(String::as_str).collect::>(), + ); + emit_security_audit_event( + &self.bridge, + vm_id, + "security.process.kill", + audit_fields([ + (String::from("source"), String::from("guest_process")), + (String::from("source_pid"), source_pid.to_string()), + (String::from("target_pid"), target_pid.to_string()), + (String::from("process_id"), process_id.to_owned()), + ( + String::from("target_process_path"), + target_path_label.clone(), + ), + (String::from("signal"), signal_name.to_owned()), + ]), + ); + + Ok(json!({ + "self": false, + "action": action, + "signal": signal_name, + "number": signal, + "targetProcessPath": target_path_label, + })) + } + + pub(crate) fn poll_javascript_child_process( + &mut self, + vm_id: &str, + process_id: &str, + child_process_id: &str, + wait_ms: u64, + ) -> Result { + self.poll_descendant_javascript_child_process( + vm_id, + process_id, + &[], + child_process_id, + wait_ms, + ) + } + + pub(crate) fn write_javascript_child_process_stdin( + &mut self, + vm_id: &str, + process_id: &str, + child_process_id: &str, + chunk: &[u8], + ) -> Result<(), SidecarError> { + let Some(vm) = self.vms.get_mut(vm_id) else { + return Err(javascript_child_process_gone_error( + process_id, + &[child_process_id], + )); + }; + let pending_stdin_limit = vm.limits.process.pending_stdin_bytes; + let Some(child) = vm + .active_processes + .get_mut(process_id) + .ok_or_else(|| missing_process_error(vm_id, process_id))? + .child_processes + .get_mut(child_process_id) + else { + return Err(javascript_child_process_gone_error( + process_id, + &[child_process_id], + )); + }; + child.execution.write_stdin(chunk)?; + write_kernel_process_stdin(&mut vm.kernel, child, chunk, pending_stdin_limit) + } + + pub(crate) fn close_javascript_child_process_stdin( + &mut self, + vm_id: &str, + process_id: &str, + child_process_id: &str, + ) -> Result<(), SidecarError> { + let Some(vm) = self.vms.get_mut(vm_id) else { + return Err(javascript_child_process_gone_error( + process_id, + &[child_process_id], + )); + }; + let Some(child) = vm + .active_processes + .get_mut(process_id) + .ok_or_else(|| missing_process_error(vm_id, process_id))? + .child_processes + .get_mut(child_process_id) + else { + return Err(javascript_child_process_gone_error( + process_id, + &[child_process_id], + )); + }; + child.execution.close_stdin()?; + close_kernel_process_stdin(&mut vm.kernel, child) + } + + pub(crate) fn kill_javascript_child_process( + &mut self, + vm_id: &str, + process_id: &str, + child_process_id: &str, + signal: &str, + ) -> Result<(), SidecarError> { + let signal_name = signal.to_owned(); + let signal = parse_signal(signal)?; + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(()); + }; + let registration = vm + .signal_states + .get(child_process_id) + .and_then(|handlers| handlers.get(&(signal as u32))) + .cloned(); + let process = vm + .active_processes + .get_mut(process_id) + .ok_or_else(|| missing_process_error(vm_id, process_id))?; + let source_pid = process.kernel_pid; + let Some(child) = process.child_processes.get_mut(child_process_id) else { + // Child IDs are monotonically allocated per parent. If this ID was + // allocated but is no longer in the tree, its exit event already + // reaped it (or its spawn rolled back). Treat cleanup kills like + // Linux's already-completed child cleanup: idempotent and local. + // Never-allocated IDs remain errors so caller bugs are not hidden. + return missing_javascript_child_kill_result( + process.next_child_process_id, + child_process_id, + ); + }; + terminate_tracked_child_process_for_signal( + &mut vm.kernel, + child, + signal, + registration.as_ref(), + )?; + emit_security_audit_event( + &self.bridge, + vm_id, + "security.process.kill", + audit_fields([ + (String::from("source"), String::from("guest_child_process")), + (String::from("source_pid"), source_pid.to_string()), + (String::from("target_pid"), child.kernel_pid.to_string()), + (String::from("process_id"), process_id.to_owned()), + ( + String::from("child_process_id"), + child_process_id.to_owned(), + ), + (String::from("signal"), signal_name), + ]), + ); + Ok(()) + } + + /// Delivers a signal to one kernel pid inside a VM, resolving the target + /// through the active-process tree first so tracked sidecar executions get + /// the same termination handling as a direct `child_process.kill`. + /// Untracked kernel processes (for example WASM subprocess trees) receive + /// the signal through the kernel process table directly. + pub(crate) fn signal_vm_kernel_pid( + &mut self, + vm_id: &str, + target_kernel_pid: u32, + signal_name: &str, + ) -> Result<(), SidecarError> { + let signal = parse_signal(signal_name)?; + let located = { + let Some(vm) = self.vms.get(vm_id) else { + return Err(SidecarError::InvalidState(String::from( + "ESRCH: unknown VM during process.kill", + ))); + }; + let alive = vm + .kernel + .list_processes() + .get(&target_kernel_pid) + .is_some_and(|info| info.status != ProcessStatus::Exited); + if !alive { + return Err(SidecarError::InvalidState(format!( + "ESRCH: no such process {target_kernel_pid}" + ))); + } + vm.active_processes.iter().find_map(|(process_id, root)| { + Self::active_process_path_by_kernel_pid(root, target_kernel_pid) + .map(|path| (process_id.clone(), path)) + }) + }; + + match located { + Some((process_id, path)) if path.is_empty() => { + self.kill_process_internal(vm_id, &process_id, signal_name) + } + Some((process_id, path)) => { + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(()); + }; + let signal_key = path.last().map(String::as_str).unwrap_or(&process_id); + let registration = vm + .signal_states + .get(signal_key) + .and_then(|handlers| handlers.get(&(signal as u32))) + .cloned(); + let Some(root) = vm.active_processes.get_mut(&process_id) else { + return Ok(()); + }; + let Some(target) = Self::active_process_by_owned_path_mut(root, &path) else { + return Err(SidecarError::InvalidState(format!( + "ESRCH: no such process {target_kernel_pid}" + ))); + }; + terminate_tracked_child_process_for_signal( + &mut vm.kernel, + target, + signal, + registration.as_ref(), + )?; + emit_security_audit_event( + &self.bridge, + vm_id, + "security.process.kill", + audit_fields([ + (String::from("source"), String::from("guest_process")), + (String::from("target_pid"), target_kernel_pid.to_string()), + (String::from("process_id"), process_id), + (String::from("signal"), signal_name.to_owned()), + ]), + ); + Ok(()) + } + None => { + let Some(vm) = self.vms.get_mut(vm_id) else { + return Ok(()); + }; + let target_pid = i32::try_from(target_kernel_pid).map_err(|_| { + SidecarError::InvalidState(format!( + "EINVAL: invalid process pid {target_kernel_pid}" + )) + })?; + vm.kernel + .signal_process(EXECUTION_DRIVER_NAME, target_pid, signal) + .map_err(kernel_error)?; + emit_security_audit_event( + &self.bridge, + vm_id, + "security.process.kill", + audit_fields([ + (String::from("source"), String::from("guest_process")), + (String::from("target_pid"), target_kernel_pid.to_string()), + (String::from("signal"), signal_name.to_owned()), + ]), + ); + Ok(()) + } + } + } + + /// Delivers a signal to every live member of a VM process group, matching + /// Linux `kill(-pgid, sig)` semantics. Returns whether the caller itself + /// is a member of the group so entry points can apply self-signal + /// delivery; the caller is intentionally skipped here. + pub(crate) fn signal_vm_process_group( + &mut self, + vm_id: &str, + caller_kernel_pid: u32, + pgid: u32, + signal_name: &str, + ) -> Result { + parse_signal(signal_name)?; + let members = { + let Some(vm) = self.vms.get(vm_id) else { + return Err(SidecarError::InvalidState(String::from( + "ESRCH: unknown VM during process.kill", + ))); + }; + vm.kernel + .list_processes() + .into_iter() + .filter(|(_, info)| info.pgid == pgid && info.status != ProcessStatus::Exited) + .map(|(pid, _)| pid) + .collect::>() + }; + if members.is_empty() { + return Err(SidecarError::InvalidState(format!( + "ESRCH: no such process group {pgid}" + ))); + } + + let mut caller_is_member = false; + for member_pid in members { + if member_pid == caller_kernel_pid { + caller_is_member = true; + continue; + } + match self.signal_vm_kernel_pid(vm_id, member_pid, signal_name) { + Ok(()) => {} + // Group members can exit while the group is being signaled. A + // vanished member is not an error for the group kill overall. + Err(error) if sidecar_error_is_esrch(&error) => {} + Err(error) => return Err(error), + } + } + Ok(caller_is_member) + } +} + +/// Applies a kill signal to a tracked child execution. Shared-runtime +/// executions for lethal signals are terminated directly with a synthetic +/// signal exit so child polls observe a prompt close; everything else routes +/// through the kernel process table. +fn terminate_tracked_child_process_for_signal( + kernel: &mut SidecarKernel, + child: &mut ActiveProcess, + signal: i32, + registration: Option<&SignalHandlerRegistration>, +) -> Result<(), SidecarError> { + if signal == 0 { + return kernel + .kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, signal) + .map_err(kernel_error); + } + + if signal == libc::SIGCONT { + apply_active_process_default_signal(kernel, child, signal)?; + match registration.map(|registration| ®istration.action) { + Some(SignalDispositionAction::User) => { + if matches!(&child.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()) + { + child.queue_pending_wasm_signal(signal)?; + } else if let Some(session) = child.execution.javascript_v8_session_handle() { + dispatch_v8_session_signal_async(session, signal); + } else if !dispatch_v8_process_signal(child, signal)? { + return Err(SidecarError::InvalidState(format!( + "unsupported guest SIGCONT handler delivery for pid {}", + child.kernel_pid + ))); + } + } + Some(SignalDispositionAction::Default | SignalDispositionAction::Ignore) | None => {} + } + return Ok(()); + } + + // SIGKILL and SIGSTOP are uncatchable. For every other signal, honor the + // guest-installed disposition before applying the default action. + if !matches!(signal, libc::SIGKILL | libc::SIGSTOP) { + match registration.map(|registration| ®istration.action) { + Some(SignalDispositionAction::Ignore) => return Ok(()), + Some(SignalDispositionAction::User) => { + if matches!(&child.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()) + { + return child.queue_pending_wasm_signal(signal); + } + if let Some(session) = child.execution.javascript_v8_session_handle().filter(|_| { + matches!(&child.execution, ActiveExecution::Javascript(execution) if execution.uses_shared_v8_runtime()) + }) { + dispatch_v8_session_signal_async(session, signal); + return Ok(()); + } + if dispatch_v8_process_signal(child, signal)? { + return Ok(()); + } + return Err(SidecarError::InvalidState(format!( + "unsupported guest signal handler delivery for pid {}", + child.kernel_pid + ))); + } + Some(SignalDispositionAction::Default) | None => {} + } + } + + if matches!( + canonical_signal_name(signal), + Some("SIGWINCH" | "SIGCHLD" | "SIGURG") + ) { + return Ok(()); + } + apply_active_process_default_signal(kernel, child, signal) +} + +fn sidecar_error_is_esrch(error: &SidecarError) -> bool { + error.to_string().contains("ESRCH") +} + +pub(crate) fn apply_active_process_default_signal( + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, + signal: i32, +) -> Result<(), SidecarError> { + if matches!( + signal, + libc::SIGSTOP | libc::SIGTSTP | libc::SIGTTIN | libc::SIGTTOU + ) { + if process.execution.uses_shared_v8_runtime() { + process.execution.pause()?; + } else { + signal_runtime_process(process.execution.child_pid(), signal)?; + } + return kernel + .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal) + .map_err(kernel_error); + } + if signal == libc::SIGCONT { + // Linux resumes a stopped process on SIGCONT even when the signal is + // blocked, ignored, or has a user handler. Handler delivery, if any, + // is layered on top by the caller after the process is runnable. + if process.execution.uses_shared_v8_runtime() { + process.execution.resume()?; + } else { + signal_runtime_process(process.execution.child_pid(), signal)?; + } + return kernel + .kill_process(EXECUTION_DRIVER_NAME, process.kernel_pid, signal) + .map_err(kernel_error); + } + + if signal != 0 && matches!(process.execution, ActiveExecution::Python(_)) { + close_kernel_process_stdin(kernel, process)?; + } + + if process.execution.uses_shared_v8_runtime() { + process.exit_signal = (signal != 0).then_some(signal); + process.exit_core_dumped = false; + process.execution.terminate()?; + if signal != 0 && matches!(process.execution, ActiveExecution::Wasm(_)) { + process.queue_pending_execution_event(ActiveExecutionEvent::Exited(128 + signal))?; + } + return Ok(()); + } + + signal_runtime_process(process.execution.child_pid(), signal) +} + +fn map_wasm_signal_registration( + registration: agentos_execution::wasm::WasmSignalHandlerRegistration, +) -> SignalHandlerRegistration { + SignalHandlerRegistration { + action: match registration.action { + agentos_execution::wasm::WasmSignalDispositionAction::Default => { + crate::protocol::SignalDispositionAction::Default + } + agentos_execution::wasm::WasmSignalDispositionAction::Ignore => { + crate::protocol::SignalDispositionAction::Ignore + } + agentos_execution::wasm::WasmSignalDispositionAction::User => { + crate::protocol::SignalDispositionAction::User + } + }, + mask: registration.mask, + flags: registration.flags, + } +} + +fn map_node_signal_registration( + registration: NodeSignalHandlerRegistration, +) -> SignalHandlerRegistration { + SignalHandlerRegistration { + action: match registration.action { + NodeSignalDispositionAction::Default => SignalDispositionAction::Default, + NodeSignalDispositionAction::Ignore => SignalDispositionAction::Ignore, + NodeSignalDispositionAction::User => SignalDispositionAction::User, + }, + mask: registration.mask, + flags: registration.flags, + } +} + +fn process_signal_state_key<'a>(process_id: &'a str, child_path: &[&'a str]) -> &'a str { + child_path.last().copied().unwrap_or(process_id) +} + +fn reset_caught_signal_dispositions_after_exec( + signal_states: &mut BTreeMap>, + process_id: &str, + child_path: &[&str], +) -> String { + let signal_key = process_signal_state_key(process_id, child_path).to_owned(); + if let Some(registrations) = signal_states.get_mut(&signal_key) { + registrations + .retain(|_, registration| registration.action == SignalDispositionAction::Ignore); + } + signal_key +} + +fn discard_replaced_image_pending_events(process: &mut ActiveProcess) { + // Bytes written before exec remain observable through the same pipe on + // Linux. Retain those output events, but discard old-image RPCs, signal + // registrations, and exit notifications that cannot apply to the new + // image. Pending signals live in `pending_wasm_signals` and deliberately + // remain untouched. + let previous_pending_bytes = process.pending_execution_event_bytes; + process.pending_execution_events.retain(|event| { + matches!( + event, + ActiveExecutionEvent::Stdout(_) | ActiveExecutionEvent::Stderr(_) + ) + }); + process.pending_execution_event_bytes = process + .pending_execution_events + .iter() + .map(ActiveExecutionEvent::retained_bytes) + .fold(0usize, usize::saturating_add); + process + .vm_pending_event_bytes_budget + .release(previous_pending_bytes.saturating_sub(process.pending_execution_event_bytes)); + process + .pending_execution_event_count_gauge + .observe_depth(process.pending_execution_events.len()); + process + .pending_execution_event_bytes_gauge + .observe_depth(process.pending_execution_event_bytes); +} + +fn javascript_child_process_sync_input_bytes( + value: Option<&Value>, +) -> Result>, SidecarError> { + let Some(value) = value else { + return Ok(None); + }; + + match value { + Value::Null => Ok(None), + Value::String(text) => Ok(Some(text.as_bytes().to_vec())), + other => javascript_sync_rpc_bytes_arg( + std::slice::from_ref(other), + 0, + "child_process.spawn_sync input", + ) + .map(Some), + } +} + +const POSIX_SPAWN_SETPGROUP: u32 = 1 << 1; +const POSIX_SPAWN_SETSCHEDPARAM: u32 = 1 << 4; +const POSIX_SPAWN_SETSCHEDULER: u32 = 1 << 5; +const POSIX_SPAWN_SETSID: u32 = 1 << 7; +const SUPPORTED_POSIX_SPAWN_FLAGS: u32 = (1 << 0) + | POSIX_SPAWN_SETPGROUP + | (1 << 2) + | (1 << 3) + | POSIX_SPAWN_SETSCHEDPARAM + | POSIX_SPAWN_SETSCHEDULER + | (1 << 6) + | POSIX_SPAWN_SETSID; + +fn kernel_open_flags_from_wasi(oflag: i32) -> u32 { + let oflag = oflag as u32; + let mut flags = if oflag & 0x1000_0000 != 0 { + if oflag & 0x0400_0000 != 0 { + agentos_kernel::fd_table::O_RDWR + } else { + agentos_kernel::fd_table::O_WRONLY + } + } else { + agentos_kernel::fd_table::O_RDONLY + }; + if oflag & 0x0000_0001 != 0 { + flags |= agentos_kernel::fd_table::O_APPEND; + } + if oflag & 0x0000_0004 != 0 { + flags |= agentos_kernel::fd_table::O_NONBLOCK; + } + if oflag & (1 << 12) != 0 { + flags |= agentos_kernel::fd_table::O_CREAT; + } + if oflag & (2 << 12) != 0 { + flags |= agentos_kernel::fd_table::O_DIRECTORY; + } + if oflag & (4 << 12) != 0 { + flags |= agentos_kernel::fd_table::O_EXCL; + } + if oflag & (8 << 12) != 0 { + flags |= agentos_kernel::fd_table::O_TRUNC; + } + if oflag & 0x0100_0000 != 0 { + flags |= agentos_kernel::fd_table::O_NOFOLLOW; + } + flags +} + +#[cfg(test)] +mod posix_spawn_open_flag_tests { + use super::kernel_open_flags_from_wasi; + use agentos_kernel::fd_table::{O_DIRECTORY, O_NOFOLLOW, O_TRUNC}; + + #[test] + fn guest_directory_and_nofollow_flags_reach_kernel_open() { + let flags = kernel_open_flags_from_wasi(((2 | 8) << 12) | 0x0100_0000); + assert_eq!( + flags & (O_DIRECTORY | O_NOFOLLOW | O_TRUNC), + O_DIRECTORY | O_NOFOLLOW | O_TRUNC + ); + } +} + +#[derive(Default)] +struct AppliedPosixSpawnFileActions { + fd_mappings: Vec<[u32; 2]>, + closed_guest_fds: Vec, +} + +struct PreparedPosixSpawnFd { + fd: u32, + fd_flags: u32, + transfer: TransferredFd, +} + +struct PreparedPosixSpawnFileActions { + applied: AppliedPosixSpawnFileActions, + fds: Vec, + cwd: String, +} + +fn prepare_spawn_host_net_fds( + kernel: &mut SidecarKernel, + parent: &mut ActiveProcess, + current_network_counts: NetworkResourceCounts, + inherited_fds: &[JavascriptSpawnHostNetFd], + inherited_kernel_mappings: &[[u32; 2]], + actions: &[JavascriptPosixSpawnFileAction], +) -> Result { + const LINUX_GUEST_FD_LIMIT: u32 = 1 << 20; + if let Some(limit) = kernel.resource_limits().max_open_fds { + if inherited_fds.len() > limit { + return Err(SidecarError::InvalidState(format!( + "EMFILE: inherited host-network fd list has {} entries, exceeding limits.resources.maxOpenFds ({limit}); raise limits.resources.maxOpenFds", + inherited_fds.len() + ))); + } + } + + let inherited_kernel_guest_fds = inherited_kernel_mappings + .iter() + .map(|mapping| mapping[0]) + .collect::>(); + let mut fd_states = BTreeMap::::new(); + let mut source_descriptions = BTreeMap::::new(); + let mut description_metadata = Vec::::new(); + let mut description_resources = Vec::>::new(); + + for inherited in inherited_fds { + if inherited.guest_fd >= LINUX_GUEST_FD_LIMIT { + return Err(SidecarError::InvalidState(format!( + "EBADF: inherited host-network guest fd {} exceeds the Linux descriptor limit", + inherited.guest_fd + ))); + } + if inherited_kernel_guest_fds.contains(&inherited.guest_fd) + || fd_states.contains_key(&inherited.guest_fd) + { + return Err(SidecarError::InvalidState(format!( + "EINVAL: duplicate inherited guest fd {}", + inherited.guest_fd + ))); + } + + let source = spawn_host_net_source(inherited)?; + let description = if let Some(index) = source_descriptions.get(&source).copied() { + let existing = description_resources[index] + .as_ref() + .expect("spawn host-network description resource exists"); + if validate_host_net_metadata( + &inherited.metadata, + existing.metadata(), + existing.class(), + "spawn host-network", + ) + .is_err() + { + return Err(SidecarError::InvalidState(String::from( + "EINVAL: aliases of one inherited host-network description disagree on metadata", + ))); + } + index + } else { + let resource = prepare_transferred_host_net_resource( + kernel, + parent, + &source, + &inherited.metadata, + "spawn host-network", + )?; + let index = description_resources.len(); + source_descriptions.insert(source, index); + description_metadata.push(resource.metadata().as_value()); + description_resources.push(Some(resource)); + index + }; + fd_states.insert( + inherited.guest_fd, + SpawnHostNetFdState { + description, + close_on_exec: inherited.close_on_exec, + }, + ); + } + + let kernel_actions = apply_spawn_host_net_file_actions(&mut fd_states, actions)?; + fd_states.retain(|_, state| !state.close_on_exec); + + // fork/exec inheritance installs new descriptor references to the same + // Linux open-file descriptions. maxOpenFds bounds those references; the + // socket/connection limits continue to count each description once. + check_spawn_host_net_resource_limit( + kernel.resource_limits().max_sockets, + current_network_counts.sockets, + 0, + "EMFILE", + "socket descriptions", + "maxSockets", + )?; + check_spawn_host_net_resource_limit( + kernel.resource_limits().max_connections, + current_network_counts.connections, + 0, + "EAGAIN", + "connected socket descriptions", + "maxConnections", + )?; + + let mut final_guest_fds = vec![Vec::new(); description_resources.len()]; + for (guest_fd, state) in fd_states { + final_guest_fds[state.description].push(guest_fd); + } + let mut descriptions = Vec::new(); + for (index, guest_fds) in final_guest_fds.into_iter().enumerate() { + if guest_fds.is_empty() { + continue; + } + descriptions.push(PreparedSpawnHostNetDescription { + guest_fds, + resource: description_resources[index] + .take() + .expect("spawn host-network description resource exists"), + metadata: description_metadata[index].clone(), + }); + } + Ok(PreparedSpawnHostNetFds { + descriptions, + kernel_actions, + }) +} + +fn check_spawn_host_net_resource_limit( + limit: Option, + current: usize, + additional: usize, + errno: &str, + label: &str, + config_name: &str, +) -> Result<(), SidecarError> { + let Some(limit) = limit else { + return Ok(()); + }; + let requested = current.saturating_add(additional); + if additional > 0 && requested > limit { + return Err(SidecarError::InvalidState(format!( + "{errno}: inheriting {additional} host-network {label} would raise recursive VM usage from {current} to {requested}, exceeding limits.resources.{config_name} ({limit}); raise limits.resources.{config_name}" + ))); + } + Ok(()) +} + +#[cfg(test)] +fn spawn_host_net_network_counts( + fd_states: &BTreeMap, + description_connections: &[bool], +) -> NetworkResourceCounts { + let surviving_descriptions = fd_states + .values() + .map(|state| state.description) + .collect::>(); + NetworkResourceCounts { + sockets: surviving_descriptions.len(), + connections: surviving_descriptions + .iter() + .filter(|description| description_connections[**description]) + .count(), + } +} + +fn apply_spawn_host_net_file_actions( + fd_states: &mut BTreeMap, + actions: &[JavascriptPosixSpawnFileAction], +) -> Result, SidecarError> { + let mut kernel_actions = Vec::with_capacity(actions.len()); + for action in actions { + match action.command { + 1 => { + let guest_fd = posix_spawn_action_guest_fd(action, "close")?; + let removed_host_net = fd_states.remove(&guest_fd).is_some(); + if !removed_host_net || guest_fd <= 2 { + kernel_actions.push(action.clone()); + } + } + 2 => { + let guest_fd = posix_spawn_action_guest_fd(action, "dup2 target")?; + let source_fd = posix_spawn_action_guest_source_fd(action)?; + if let Some(mut state) = fd_states.get(&source_fd).copied() { + // POSIX spawn dup2 actions clear FD_CLOEXEC even for a + // same-fd action; direct dup2(2) remains a no-op. + state.close_on_exec = false; + if guest_fd == source_fd { + fd_states.insert(guest_fd, state); + continue; + } + fd_states.insert(guest_fd, state); + let mut close_target = action.clone(); + close_target.command = 1; + close_target.guest_fd = Some(i32::try_from(guest_fd).map_err(|_| { + SidecarError::InvalidState(format!( + "EBADF: posix_spawn dup2 target {guest_fd} exceeds i32" + )) + })?); + close_target.source_fd = 0; + close_target.guest_source_fd = None; + close_target.path.clear(); + close_target.close_from_guest_fds.clear(); + kernel_actions.push(close_target); + } else { + fd_states.remove(&guest_fd); + kernel_actions.push(action.clone()); + } + } + 3 => { + let guest_fd = posix_spawn_action_guest_fd(action, "open")?; + fd_states.remove(&guest_fd); + kernel_actions.push(action.clone()); + } + 4 => kernel_actions.push(action.clone()), + 5 => { + let guest_fd = posix_spawn_action_guest_fd(action, "fchdir")?; + if fd_states.contains_key(&guest_fd) { + return Err(SidecarError::InvalidState(format!( + "ENOTDIR: posix_spawn fchdir fd {guest_fd} is a socket" + ))); + } + kernel_actions.push(action.clone()); + } + 6 => { + let low_fd = posix_spawn_action_guest_fd(action, "closefrom")?; + fd_states.retain(|guest_fd, _| *guest_fd < low_fd); + kernel_actions.push(action.clone()); + } + command => { + return Err(SidecarError::InvalidState(format!( + "EINVAL: unknown posix_spawn file action {command}" + ))); + } + } + } + Ok(kernel_actions) +} + +fn apply_posix_spawn_file_actions( + kernel: &mut SidecarKernel, + pid: u32, + initial_cwd: &str, + inherited_mappings: &[[u32; 2]], + actions: &[JavascriptPosixSpawnFileAction], +) -> Result<(AppliedPosixSpawnFileActions, String), SidecarError> { + let inherited_kernel_fds = kernel + .fd_snapshot(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_error)? + .into_iter() + .map(|entry| entry.fd) + .collect::>(); + let mut mappings = BTreeMap::new(); + let mut mapped_kernel_fds = BTreeSet::new(); + let mut closed_guest_fds = BTreeSet::new(); + let mut cwd = kernel + .realpath_for_process(EXECUTION_DRIVER_NAME, pid, initial_cwd) + .map(|path| normalize_path(&path)) + .map_err(kernel_error)?; + for [guest_fd, kernel_fd] in inherited_mappings { + // Runner-local descriptors can appear in the guest mapping without a + // kernel entry. Kernel-backed FD_CLOEXEC descriptors remain present + // until file actions finish, matching fork + exec ordering on Linux. + if !inherited_kernel_fds.contains(kernel_fd) { + continue; + } + if mappings.insert(*guest_fd, *kernel_fd).is_some() || !mapped_kernel_fds.insert(*kernel_fd) + { + return Err(SidecarError::InvalidState(String::from( + "EINVAL: duplicate posix_spawn guest/kernel fd mapping", + ))); + } + } + + let action_guest_fd = |action: &JavascriptPosixSpawnFileAction| { + u32::try_from(action.guest_fd.unwrap_or(action.fd)).map_err(|_| { + SidecarError::InvalidState(format!( + "EBADF: invalid posix_spawn guest fd {}", + action.guest_fd.unwrap_or(action.fd) + )) + }) + }; + for action in actions { + match action.command { + 1 => { + let guest_fd = action_guest_fd(action)?; + closed_guest_fds.insert(guest_fd); + let fd = if let Some(fd) = mappings.remove(&guest_fd) { + Some(fd) + } else if action.guest_fd.is_some() && guest_fd > 2 { + // WASI preopens and other runner-local descriptors do not + // exist in the kernel namespace. Closing them must never + // fall through to an unrelated canonical fd with the same + // number; the child runner suppresses them from its local + // descriptor table instead. + None + } else { + let raw_fd = u32::try_from(action.fd).map_err(|_| { + SidecarError::InvalidState(format!( + "EBADF: invalid posix_spawn close fd {}", + action.fd + )) + })?; + if mapped_kernel_fds.contains(&raw_fd) { + return Err(SidecarError::InvalidState(format!( + "EBADF: posix_spawn guest fd {guest_fd} collides with another mapped descriptor" + ))); + } + Some(raw_fd) + }; + if let Some(fd) = fd { + mapped_kernel_fds.remove(&fd); + kernel + .fd_close(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_error)?; + } + } + 2 => { + let guest_fd = action_guest_fd(action)?; + let guest_source_fd = u32::try_from( + action.guest_source_fd.unwrap_or(action.source_fd), + ) + .map_err(|_| { + SidecarError::InvalidState(format!( + "EBADF: invalid posix_spawn dup2 source {}", + action.guest_source_fd.unwrap_or(action.source_fd) + )) + })?; + if guest_source_fd == guest_fd { + let fd = if let Some(fd) = mappings.get(&guest_source_fd).copied() { + fd + } else if action.guest_source_fd.is_some() && guest_source_fd > 2 { + return Err(SidecarError::InvalidState(format!( + "EBADF: posix_spawn dup2 source guest fd {guest_source_fd} is not kernel-backed" + ))); + } else { + u32::try_from(action.source_fd).map_err(|_| { + SidecarError::InvalidState(format!( + "EBADF: invalid posix_spawn dup2 source {}", + action.source_fd + )) + })? + }; + // POSIX requires dup2(fd, fd) to clear FD_CLOEXEC when it + // is executed as a spawn file action. It is not the same + // operation as a direct dup2(2) no-op: the file action is + // specified to behave as though the descriptor had first + // been duplicated onto itself without O_CLOEXEC. + kernel + .fd_fcntl( + EXECUTION_DRIVER_NAME, + pid, + fd, + agentos_kernel::fd_table::F_SETFD, + 0, + ) + .map_err(kernel_error)?; + closed_guest_fds.remove(&guest_fd); + continue; + } + let source_fd = if let Some(fd) = mappings.get(&guest_source_fd).copied() { + fd + } else if action.guest_source_fd.is_some() && guest_source_fd > 2 { + return Err(SidecarError::InvalidState(format!( + "EBADF: posix_spawn dup2 source guest fd {guest_source_fd} is not kernel-backed" + ))); + } else { + let raw_fd = u32::try_from(action.source_fd).map_err(|_| { + SidecarError::InvalidState(format!( + "EBADF: invalid posix_spawn dup2 source {}", + action.source_fd + )) + })?; + if mapped_kernel_fds.contains(&raw_fd) { + return Err(SidecarError::InvalidState(format!( + "EBADF: posix_spawn dup2 source guest fd {guest_source_fd} collides with another mapped descriptor" + ))); + } + raw_fd + }; + let fd = if let Some(fd) = mappings.get(&guest_fd).copied() { + kernel + .fd_dup2(EXECUTION_DRIVER_NAME, pid, source_fd, fd) + .map_err(kernel_error)?; + fd + } else { + kernel + .fd_dup(EXECUTION_DRIVER_NAME, pid, source_fd) + .map_err(kernel_error)? + }; + mappings.insert(guest_fd, fd); + mapped_kernel_fds.insert(fd); + closed_guest_fds.remove(&guest_fd); + } + 3 => { + let guest_fd = action_guest_fd(action)?; + if let Some(fd) = mappings.remove(&guest_fd) { + mapped_kernel_fds.remove(&fd); + kernel + .fd_close(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_error)?; + } + let action_path = resolve_posix_spawn_action_path(&cwd, &action.path); + let opened_fd = kernel + .fd_open( + EXECUTION_DRIVER_NAME, + pid, + &action_path, + kernel_open_flags_from_wasi(action.oflag), + Some(action.mode), + ) + .map_err(kernel_error)?; + if action.oflag as u32 & (2 << 12) != 0 { + let stat = kernel + .fd_stat(EXECUTION_DRIVER_NAME, pid, opened_fd) + .map_err(kernel_error)?; + if stat.filetype != agentos_kernel::fd_table::FILETYPE_DIRECTORY { + kernel + .fd_close(EXECUTION_DRIVER_NAME, pid, opened_fd) + .map_err(kernel_error)?; + return Err(SidecarError::InvalidState(format!( + "ENOTDIR: posix_spawn open path is not a directory: {}", + action.path + ))); + } + } + mappings.insert(guest_fd, opened_fd); + mapped_kernel_fds.insert(opened_fd); + closed_guest_fds.remove(&guest_fd); + } + 4 => { + let action_path = resolve_posix_spawn_action_path(&cwd, &action.path); + let stat = kernel + .stat_for_process(EXECUTION_DRIVER_NAME, pid, &action_path) + .map_err(kernel_error)?; + if !stat.is_directory { + return Err(SidecarError::InvalidState(format!( + "ENOTDIR: posix_spawn chdir path is not a directory: {}", + action.path + ))); + } + cwd = kernel + .realpath_for_process(EXECUTION_DRIVER_NAME, pid, &action_path) + .map(|path| normalize_path(&path)) + .map_err(kernel_error)?; + } + 5 => { + let guest_fd = action_guest_fd(action)?; + let fd = if let Some(fd) = mappings.get(&guest_fd).copied() { + fd + } else if action.guest_fd.is_some() && guest_fd > 2 { + return Err(SidecarError::InvalidState(format!( + "EBADF: posix_spawn fchdir guest fd {guest_fd} is not kernel-backed" + ))); + } else { + u32::try_from(action.fd).map_err(|_| { + SidecarError::InvalidState(format!( + "EBADF: invalid posix_spawn fchdir fd {}", + action.fd + )) + })? + }; + let stat = kernel + .fd_stat(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_error)?; + if stat.filetype != agentos_kernel::fd_table::FILETYPE_DIRECTORY { + return Err(SidecarError::InvalidState(format!( + "ENOTDIR: posix_spawn fchdir fd {guest_fd} is not a directory" + ))); + } + cwd = normalize_path( + &kernel + .fd_path(EXECUTION_DRIVER_NAME, pid, fd) + .map_err(kernel_error)?, + ); + } + 6 => { + let low_fd = action_guest_fd(action)?; + if let Some(limit) = kernel.resource_limits().max_open_fds { + if action.close_from_guest_fds.len() > limit { + return Err(SidecarError::InvalidState(format!( + "EMFILE: posix_spawn closefrom guest fd list has {} entries, exceeding limits.resources.maxOpenFds ({limit}); raise limits.resources.maxOpenFds", + action.close_from_guest_fds.len() + ))); + } + } + for guest_fd in &action.close_from_guest_fds { + if *guest_fd < low_fd { + return Err(SidecarError::InvalidState(format!( + "EINVAL: posix_spawn closefrom guest fd {guest_fd} is below cutoff {low_fd}" + ))); + } + closed_guest_fds.insert(*guest_fd); + } + let open_kernel_fds = kernel + .fd_snapshot(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_error)? + .into_iter() + .map(|entry| entry.fd) + .collect::>(); + + // File actions operate in the guest descriptor namespace. + // A guest fd may map to a different kernel fd, so applying a + // raw kernel `fd >= low_fd` cutoff would close the wrong end + // whenever (for example) guest stdout maps to kernel fd 8. + let mapped_fds = mappings + .iter() + .map(|(guest_fd, kernel_fd)| (*guest_fd, *kernel_fd)) + .collect::>(); + let mut to_close = BTreeMap::new(); + for (guest_fd, kernel_fd) in mapped_fds { + if guest_fd >= low_fd && open_kernel_fds.contains(&kernel_fd) { + to_close.insert(guest_fd, kernel_fd); + } + } + for kernel_fd in open_kernel_fds { + if !mapped_kernel_fds.contains(&kernel_fd) && kernel_fd >= low_fd { + to_close.insert(kernel_fd, kernel_fd); + } + } + + for (guest_fd, kernel_fd) in to_close { + closed_guest_fds.insert(guest_fd); + if mappings.remove(&guest_fd).is_some() { + mapped_kernel_fds.remove(&kernel_fd); + } + kernel + .fd_close(EXECUTION_DRIVER_NAME, pid, kernel_fd) + .map_err(kernel_error)?; + } + } + command => { + return Err(SidecarError::InvalidState(format!( + "EINVAL: unknown posix_spawn file action {command}" + ))); + } + } + } + let child_kernel_fds = kernel + .fd_snapshot(EXECUTION_DRIVER_NAME, pid) + .map_err(kernel_error)? + .into_iter() + .map(|entry| entry.fd) + .collect::>(); + Ok(( + AppliedPosixSpawnFileActions { + fd_mappings: mappings + .into_iter() + .filter(|(_, kernel_fd)| child_kernel_fds.contains(kernel_fd)) + .map(|(guest_fd, kernel_fd)| [guest_fd, kernel_fd]) + .collect(), + closed_guest_fds: closed_guest_fds.into_iter().collect(), + }, + cwd, + )) +} + +fn resolve_posix_spawn_action_path(cwd: &str, action_path: &str) -> String { + // File actions are ordered. Resolve each relative pathname against the + // canonical cwd produced by all preceding chdir/fchdir actions. + if action_path.starts_with('/') { + normalize_path(action_path) + } else { + normalize_path(&format!("{cwd}/{action_path}")) + } +} + +fn apply_posix_spawn_file_actions_or_rollback( + kernel: &mut SidecarKernel, + process: &KernelProcessHandle, + cwd: &str, + inherited_mappings: &[[u32; 2]], + actions: &[JavascriptPosixSpawnFileAction], +) -> Result { + match apply_posix_spawn_file_actions(kernel, process.pid(), cwd, inherited_mappings, actions) { + Ok((mappings, _)) => Ok(mappings), + Err(error) => { + process.finish(127); + if let Err(cleanup_error) = kernel.waitpid(process.pid()) { + eprintln!( + "[agentos] failed to reap rejected posix_spawn child {}: {}", + process.pid(), + cleanup_error + ); + } + Err(error) + } + } +} + +fn rollback_unregistered_spawn_child( + kernel: &mut SidecarKernel, + process: &KernelProcessHandle, + execution: Option<&mut ActiveExecution>, + context: &str, +) { + if let Some(execution) = execution { + if let ActiveExecution::Tool(tool) = execution { + tool.cancelled.store(true, Ordering::Relaxed); + } else if let Err(error) = execution.terminate() { + eprintln!( + "[agentos] failed to terminate rejected {context} runtime for PID {}: {error}", + process.pid() + ); + } + } + process.finish(127); + if let Err(error) = kernel.waitpid(process.pid()) { + eprintln!( + "[agentos] failed to reap rejected {context} child {}: {error}", + process.pid() + ); + } +} + +fn apply_spawn_session_or_rollback( + kernel: &mut SidecarKernel, + process: &KernelProcessHandle, + create_session: bool, +) -> Result<(), SidecarError> { + if !create_session { + return Ok(()); + } + if let Err(error) = kernel.setsid(EXECUTION_DRIVER_NAME, process.pid()) { + process.finish(127); + if let Err(cleanup_error) = kernel.waitpid(process.pid()) { + eprintln!( + "[agentos] failed to reap rejected setsid child {}: {}", + process.pid(), + cleanup_error + ); + } + return Err(kernel_error(error)); + } + Ok(()) +} + +fn preapply_posix_spawn_file_actions( + kernel: &mut SidecarKernel, + parent_pid: u32, + cwd: &str, + requested_pgid: Option, + inherited_mappings: &[[u32; 2]], + actions: &[JavascriptPosixSpawnFileAction], +) -> Result { + // Linux runs file actions once before attempting the exec/PATH search. + // Stage them in a short-lived child FD table, then retain transfers of the + // resulting open descriptions. The eventual runtime child installs those + // exact descriptions; it never reopens a path or repeats O_EXCL/O_TRUNC. + let process = kernel + .spawn_process_with_process_group_preserving_cloexec( + WASM_COMMAND, + vec![String::from("posix-spawn-file-actions")], + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(parent_pid), + env: BTreeMap::new(), + cwd: Some(cwd.to_owned()), + }, + requested_pgid, + ) + .map_err(kernel_error)?; + let prepared = (|| { + let (mut applied, cwd) = apply_posix_spawn_file_actions( + kernel, + process.pid(), + cwd, + inherited_mappings, + actions, + )?; + kernel + .close_process_cloexec_fds(EXECUTION_DRIVER_NAME, process.pid()) + .map_err(kernel_error)?; + let snapshot = kernel + .fd_snapshot(EXECUTION_DRIVER_NAME, process.pid()) + .map_err(kernel_error)?; + let surviving_fds = snapshot + .iter() + .map(|entry| entry.fd) + .collect::>(); + applied + .fd_mappings + .retain(|mapping| surviving_fds.contains(&mapping[1])); + let mut fds = Vec::with_capacity(snapshot.len()); + for entry in snapshot { + fds.push(PreparedPosixSpawnFd { + fd: entry.fd, + fd_flags: entry.fd_flags, + transfer: kernel + .fd_transfer(EXECUTION_DRIVER_NAME, process.pid(), entry.fd) + .map_err(kernel_error)?, + }); + } + Ok(PreparedPosixSpawnFileActions { applied, fds, cwd }) + })(); + process.finish(if prepared.is_ok() { 0 } else { 127 }); + let reap_result = kernel.waitpid(process.pid()).map_err(kernel_error); + match prepared { + Ok(prepared) => { + reap_result?; + Ok(prepared) + } + Err(error) => { + if let Err(reap_error) = reap_result { + eprintln!( + "[agentos] failed to reap rejected preapplied posix_spawn child {}: {}", + process.pid(), + reap_error + ); + } + Err(error) + } + } +} + +fn install_preapplied_posix_spawn_file_actions( + kernel: &mut SidecarKernel, + process: &KernelProcessHandle, + prepared: PreparedPosixSpawnFileActions, +) -> Result { + let result = (|| { + let inherited_fds = kernel + .fd_snapshot(EXECUTION_DRIVER_NAME, process.pid()) + .map_err(kernel_error)?; + for entry in inherited_fds { + kernel + .fd_close(EXECUTION_DRIVER_NAME, process.pid(), entry.fd) + .map_err(kernel_error)?; + } + for entry in &prepared.fds { + kernel + .fd_install_transfer_at( + EXECUTION_DRIVER_NAME, + process.pid(), + entry.fd, + entry.fd_flags, + &entry.transfer, + ) + .map_err(kernel_error)?; + } + Ok(prepared.applied) + })(); + if result.is_err() { + process.finish(127); + if let Err(cleanup_error) = kernel.waitpid(process.pid()) { + eprintln!( + "[agentos] failed to reap rejected preapplied posix_spawn target {}: {}", + process.pid(), + cleanup_error + ); + } + } + result +} + +#[derive(Debug)] +struct JavascriptSpawnAttributes { + process_group: Option, + new_session: bool, +} + +fn javascript_spawn_attributes( + options: &JavascriptChildProcessSpawnOptions, +) -> Result { + if options.spawn_attr_flags & !SUPPORTED_POSIX_SPAWN_FLAGS != 0 { + return Err(SidecarError::InvalidState(format!( + "unsupported POSIX spawn attribute flags: {:#x}", + options.spawn_attr_flags & !SUPPORTED_POSIX_SPAWN_FLAGS + ))); + } + for signal in options + .spawn_signal_defaults + .iter() + .chain(options.spawn_signal_mask.iter()) + { + if !(1..=64).contains(signal) { + return Err(SidecarError::InvalidState(format!( + "invalid POSIX spawn signal number {signal}" + ))); + } + } + + let new_session = options.spawn_attr_flags & POSIX_SPAWN_SETSID != 0; + if new_session && options.spawn_attr_flags & POSIX_SPAWN_SETPGROUP != 0 { + return Err(SidecarError::InvalidState(String::from( + "EPERM: POSIX_SPAWN_SETSID cannot be combined with POSIX_SPAWN_SETPGROUP", + ))); + } + if new_session && options.detached { + return Err(SidecarError::InvalidState(String::from( + "EINVAL: POSIX_SPAWN_SETSID cannot be combined with detached child-process mode", + ))); + } + if options.spawn_attr_flags & (POSIX_SPAWN_SETSCHEDPARAM | POSIX_SPAWN_SETSCHEDULER) != 0 + && options.spawn_sched_priority.unwrap_or_default() != 0 + { + return Err(SidecarError::InvalidState(String::from( + "EINVAL: SCHED_OTHER requires scheduling priority zero", + ))); + } + if options.spawn_attr_flags & POSIX_SPAWN_SETSCHEDULER != 0 + && options.spawn_sched_policy.unwrap_or_default() != 0 + { + return Err(SidecarError::InvalidState(String::from( + "EPERM: requested POSIX spawn scheduler policy requires host privilege", + ))); + } + + if options.spawn_attr_flags & POSIX_SPAWN_SETPGROUP == 0 { + if options.spawn_pgroup.unwrap_or(0) != 0 { + return Err(SidecarError::InvalidState(String::from( + "spawnPgroup requires POSIX_SPAWN_SETPGROUP", + ))); + } + return Ok(JavascriptSpawnAttributes { + process_group: None, + new_session, + }); + } + if options.detached { + return Err(SidecarError::InvalidState(String::from( + "POSIX_SPAWN_SETPGROUP cannot be combined with detached child-process mode", + ))); + } + let pgid = options.spawn_pgroup.unwrap_or(0); + let process_group = u32::try_from(pgid).map_err(|_| { + SidecarError::InvalidState(format!("invalid POSIX spawn process group {pgid}")) + })?; + Ok(JavascriptSpawnAttributes { + process_group: Some(process_group), + new_session, + }) +} + +fn apply_child_process_argv0(resolved: &mut ResolvedChildProcessExecution, argv0: Option<&str>) { + let Some(argv0) = argv0 else { + return; + }; + if resolved.process_args.is_empty() { + resolved.process_args.push(argv0.to_owned()); + } else { + // Empty argv0 is valid on Linux and observable through procfs/runtime + // argv APIs. Do not coerce it back to the executable name. + resolved.process_args[0] = argv0.to_owned(); + } +} + +// bridge_permissions moved to crate::bridge + +// reconcile_mounts, resolve_cwd moved to crate::vm + +fn resolve_execute_request( + vm: &VmState, + payload: &ExecuteRequest, +) -> Result { + let payload_env: BTreeMap = payload + .env + .iter() + .map(|(k, v)| (k.clone(), v.clone())) + .collect(); + if let Some(command) = payload.command.as_deref() { + return resolve_command_execution( + vm, + command, + &payload.args, + &payload_env, + payload.cwd.as_deref(), + payload.wasm_permission_tier, + ); + } + + let runtime = payload.runtime.clone().ok_or_else(|| { + SidecarError::InvalidState(String::from("execute requires either command or runtime")) + })?; + let entrypoint = payload.entrypoint.clone().ok_or_else(|| { + SidecarError::InvalidState(String::from( + "execute requires either command or entrypoint", + )) + })?; + let (guest_cwd, host_cwd, allow_host_path_overrides) = + resolve_execution_cwds(vm, payload.cwd.as_deref()); + let mut env = vm.guest_env.clone(); + env.extend(payload_env.clone()); + + let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint); + if requested_host_entrypoint.is_some() && !allow_host_path_overrides { + let requested_cwd = payload.cwd.as_deref().unwrap_or(guest_cwd.as_str()); + return Err(SidecarError::InvalidState(format!( + "execution cwd {requested_cwd} is outside sandbox root {}", + vm.host_cwd.to_string_lossy() + ))); + } + let host_entrypoint_override = allow_host_path_overrides + .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, &entrypoint)) + .flatten(); + + let guest_entrypoint = host_entrypoint_override + .as_ref() + .map(|(guest_entrypoint, _)| guest_entrypoint.clone()) + .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, &entrypoint)); + prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?; + + Ok(ResolvedChildProcessExecution { + command: match runtime { + GuestRuntimeKind::JavaScript => String::from(JAVASCRIPT_COMMAND), + GuestRuntimeKind::Python => String::from(PYTHON_COMMAND), + GuestRuntimeKind::WebAssembly => String::from(WASM_COMMAND), + }, + process_args: std::iter::once(entrypoint.clone()) + .chain(payload.args.iter().cloned()) + .collect(), + runtime, + entrypoint: host_entrypoint_override + .map(|(_, host_entrypoint)| host_entrypoint) + .unwrap_or(entrypoint), + execution_args: payload.args.clone(), + env, + guest_cwd, + host_cwd, + wasm_permission_tier: payload.wasm_permission_tier, + tool_command: false, + }) +} + +fn resolve_command_execution( + vm: &VmState, + command: &str, + args: &[String], + extra_env: &BTreeMap, + cwd: Option<&str>, + explicit_wasm_permission_tier: Option, +) -> Result { + let (guest_cwd, host_cwd, allow_host_path_overrides) = resolve_execution_cwds(vm, cwd); + let mut env = vm.guest_env.clone(); + env.extend(extra_env.clone()); + let args = apply_shell_cwd_prefix(command, args.to_vec(), &guest_cwd); + + if is_tool_command(vm, command) { + let command = normalized_tool_command_name(command).unwrap_or_else(|| command.to_owned()); + return Ok(ResolvedChildProcessExecution { + command: command.clone(), + process_args: std::iter::once(command.clone()) + .chain(args.iter().cloned()) + .collect(), + runtime: GuestRuntimeKind::JavaScript, + entrypoint: command, + execution_args: args, + env, + guest_cwd, + host_cwd, + wasm_permission_tier: None, + tool_command: true, + }); + } + + if is_python_runtime_command(command) { + return resolve_python_command_execution(vm, command, &args, env, guest_cwd, host_cwd); + } + + if is_node_runtime_command(command) { + if let Some(cli) = resolve_host_node_cli_entrypoint(command) { + env.insert( + String::from("AGENTOS_NODE_EVAL"), + build_host_node_cli_eval(&cli), + ); + prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?; + add_runtime_guest_path_mapping(&mut env, &cli.guest_root, &cli.package_root); + add_runtime_host_access_path( + &mut env, + "AGENTOS_EXTRA_FS_READ_PATHS", + &cli.package_root, + true, + ); + + return Ok(ResolvedChildProcessExecution { + command: String::from(JAVASCRIPT_COMMAND), + process_args: std::iter::once(command.to_owned()) + .chain(args.iter().cloned()) + .collect(), + runtime: GuestRuntimeKind::JavaScript, + entrypoint: String::from("-e"), + execution_args: std::iter::once(cli.guest_entrypoint.clone()) + .chain(args.iter().cloned()) + .collect(), + env, + guest_cwd, + host_cwd, + wasm_permission_tier: None, + tool_command: false, + }); + } + + if args.is_empty() { + env.insert(String::from("AGENTOS_NODE_EVAL"), String::new()); + prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?; + + return Ok(ResolvedChildProcessExecution { + command: String::from(JAVASCRIPT_COMMAND), + process_args: vec![command.to_owned()], + runtime: GuestRuntimeKind::JavaScript, + entrypoint: String::from("-e"), + execution_args: Vec::new(), + env, + guest_cwd, + host_cwd, + wasm_permission_tier: None, + tool_command: false, + }); + } + + if let Some((entrypoint, execution_args)) = + resolve_special_node_cli_invocation(&args, &mut env) + { + prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, None)?; + + return Ok(ResolvedChildProcessExecution { + command: String::from(JAVASCRIPT_COMMAND), + process_args: std::iter::once(command.to_owned()) + .chain(args.iter().cloned()) + .collect(), + runtime: GuestRuntimeKind::JavaScript, + entrypoint, + execution_args, + env, + guest_cwd, + host_cwd, + wasm_permission_tier: None, + tool_command: false, + }); + } + + let Some(entrypoint_specifier) = args.first() else { + return Err(SidecarError::InvalidState(format!( + "{command} execution requires an entrypoint" + ))); + }; + + let (entrypoint, execution_args, guest_entrypoint) = { + let requested_host_entrypoint = + resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier); + if requested_host_entrypoint.is_some() && !allow_host_path_overrides { + let requested_cwd = cwd.unwrap_or(guest_cwd.as_str()); + return Err(SidecarError::InvalidState(format!( + "execution cwd {requested_cwd} is outside sandbox root {}", + vm.host_cwd.to_string_lossy() + ))); + } + let host_entrypoint_override = allow_host_path_overrides + .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, entrypoint_specifier)) + .flatten(); + let guest_entrypoint = host_entrypoint_override + .as_ref() + .map(|(guest_entrypoint, _)| guest_entrypoint.clone()) + .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, entrypoint_specifier)); + let entrypoint = host_entrypoint_override.map_or_else( + || { + guest_entrypoint.as_ref().map_or_else( + || entrypoint_specifier.clone(), + |guest_entrypoint| { + resolve_vm_guest_path_to_host(vm, guest_entrypoint) + .to_string_lossy() + .into_owned() + }, + ) + }, + |(_, host_entrypoint)| host_entrypoint, + ); + ( + entrypoint, + args.iter().skip(1).cloned().collect(), + guest_entrypoint, + ) + }; + + prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?; + + return Ok(ResolvedChildProcessExecution { + command: String::from(JAVASCRIPT_COMMAND), + process_args: std::iter::once(command.to_owned()) + .chain(args.iter().cloned()) + .collect(), + runtime: GuestRuntimeKind::JavaScript, + entrypoint, + execution_args, + env, + guest_cwd, + host_cwd, + wasm_permission_tier: None, + tool_command: false, + }); + } + + if command.ends_with(".js") || command.ends_with(".mjs") || command.ends_with(".cjs") { + let requested_host_entrypoint = resolve_host_entrypoint_within_vm_host_cwd(vm, command); + if requested_host_entrypoint.is_some() && !allow_host_path_overrides { + let requested_cwd = cwd.unwrap_or(guest_cwd.as_str()); + return Err(SidecarError::InvalidState(format!( + "execution cwd {requested_cwd} is outside sandbox root {}", + vm.host_cwd.to_string_lossy() + ))); + } + let host_entrypoint_override = allow_host_path_overrides + .then(|| resolve_host_entrypoint_within_vm_host_cwd(vm, command)) + .flatten(); + let guest_entrypoint = host_entrypoint_override + .as_ref() + .map(|(guest_entrypoint, _)| guest_entrypoint.clone()) + .or_else(|| guest_entrypoint_for_specifier(&guest_cwd, command)); + let entrypoint = host_entrypoint_override.map_or_else( + || { + guest_entrypoint.as_ref().map_or_else( + || command.to_owned(), + |guest_entrypoint| { + resolve_vm_guest_path_to_host(vm, guest_entrypoint) + .to_string_lossy() + .into_owned() + }, + ) + }, + |(_, host_entrypoint)| host_entrypoint, + ); + prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?; + + return Ok(ResolvedChildProcessExecution { + command: String::from(JAVASCRIPT_COMMAND), + process_args: std::iter::once(command.to_owned()) + .chain(args.iter().cloned()) + .collect(), + runtime: GuestRuntimeKind::JavaScript, + entrypoint, + execution_args: args.to_vec(), + env, + guest_cwd, + host_cwd, + wasm_permission_tier: None, + tool_command: false, + }); + } + + let guest_entrypoint = resolve_guest_command_entrypoint( + vm, + &guest_cwd, + command, + env.get("PATH").map(String::as_str), + ) + .ok_or_else(|| { + SidecarError::InvalidState(format!( + "command not found on native sidecar path: {command}" + )) + })?; + let wasm_permission_tier = explicit_wasm_permission_tier + .or_else(|| vm.command_permissions.get(command).copied()) + .or_else(|| { + Path::new(&guest_entrypoint) + .file_name() + .and_then(|name| name.to_str()) + .and_then(|name| vm.command_permissions.get(name).copied()) + }); + + let host_entrypoint = resolve_vm_guest_path_to_host(vm, &guest_entrypoint); + if let Some((javascript_guest_entrypoint, javascript_host_entrypoint)) = + resolve_javascript_command_entrypoint(vm, &guest_entrypoint, &host_entrypoint) + { + prepare_guest_runtime_env( + vm, + &mut env, + &guest_cwd, + &host_cwd, + Some(javascript_guest_entrypoint), + )?; + + return Ok(ResolvedChildProcessExecution { + command: command.to_owned(), + process_args: std::iter::once(command.to_owned()) + .chain(args.iter().cloned()) + .collect(), + runtime: GuestRuntimeKind::JavaScript, + entrypoint: javascript_host_entrypoint.to_string_lossy().into_owned(), + execution_args: args.to_vec(), + env, + guest_cwd, + host_cwd, + wasm_permission_tier: None, + tool_command: false, + }); + } + prepare_guest_runtime_env( + vm, + &mut env, + &guest_cwd, + &host_cwd, + Some(guest_entrypoint.clone()), + )?; + + Ok(ResolvedChildProcessExecution { + command: command.to_owned(), + process_args: std::iter::once(command.to_owned()) + .chain(args.iter().cloned()) + .collect(), + runtime: GuestRuntimeKind::WebAssembly, + entrypoint: host_entrypoint.to_string_lossy().into_owned(), + execution_args: args.to_vec(), + env, + guest_cwd, + host_cwd, + wasm_permission_tier, + tool_command: false, + }) +} + +const MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH: usize = 4; + +fn resolve_javascript_command_entrypoint( + vm: &VmState, + guest_entrypoint: &str, + host_entrypoint: &Path, +) -> Option<(String, PathBuf)> { + // agentOS package content is served guest-native (tar + single-symlink + // mounts) and is never materialized on the host, so the shebang-reading + // fallback below (which reads the host path) cannot classify these + // entrypoints. Within the package mount the only runtimes are WebAssembly + // (`*.wasm`) and JavaScript, and `bin/` launchers are frequently + // extensionless — so classify by extension here: `.wasm` is WASM (fall + // through), everything else in the mount is JavaScript. + if guest_path_is_within_agentos_package_mount(vm, guest_entrypoint) { + let extension = Path::new(guest_entrypoint) + .extension() + .and_then(|extension| extension.to_str()); + if extension != Some("wasm") { + return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf())); + } + return None; + } + + resolve_javascript_command_entrypoint_inner( + vm, + guest_entrypoint, + host_entrypoint, + MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH, + ) +} + +fn resolve_javascript_command_entrypoint_inner( + vm: &VmState, + guest_entrypoint: &str, + host_entrypoint: &Path, + redirects_remaining: usize, +) -> Option<(String, PathBuf)> { + if redirects_remaining > 0 { + let symlink_target = fs::symlink_metadata(host_entrypoint) + .ok() + .filter(|metadata| metadata.file_type().is_symlink()) + .and_then(|_| fs::read_link(host_entrypoint).ok()); + if let Some(symlink_target) = symlink_target { + let guest_parent = Path::new(guest_entrypoint) + .parent() + .and_then(|path| path.to_str()) + .unwrap_or("/"); + let symlink_guest_entrypoint = if symlink_target.is_absolute() { + normalize_path(&symlink_target.to_string_lossy()) + } else { + normalize_path(&format!( + "{guest_parent}/{}", + symlink_target.to_string_lossy().replace('\\', "/") + )) + }; + let symlink_host_entrypoint = + resolve_vm_guest_path_to_host(vm, &symlink_guest_entrypoint); + return resolve_javascript_command_entrypoint_inner( + vm, + &symlink_guest_entrypoint, + &symlink_host_entrypoint, + redirects_remaining - 1, + ); + } + } + + let script = load_executable_script_preview(host_entrypoint)?; + let interpreter = parse_script_interpreter_name(&script); + + if interpreter.is_none() && is_probable_javascript_entrypoint(host_entrypoint, &script) { + return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf())); + } + + let interpreter = interpreter?; + if interpreter == "node" { + return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf())); + } + + if redirects_remaining == 0 || !matches!(interpreter.as_str(), "sh" | "bash" | "dash") { + return None; + } + + let shim_target = parse_node_shell_shim_target(&script)?; + let guest_parent = Path::new(guest_entrypoint) + .parent() + .and_then(|path| path.to_str()) + .unwrap_or("/"); + let shim_guest_entrypoint = normalize_path(&format!("{guest_parent}/{shim_target}")); + let shim_host_entrypoint = resolve_vm_guest_path_to_host(vm, &shim_guest_entrypoint); + resolve_javascript_command_entrypoint_inner( + vm, + &shim_guest_entrypoint, + &shim_host_entrypoint, + redirects_remaining - 1, + ) +} + +fn load_executable_script_preview(path: &Path) -> Option { + let bytes = fs::read(path).ok()?; + let preview_len = bytes.len().min(16 * 1024); + Some(String::from_utf8_lossy(&bytes[..preview_len]).into_owned()) +} + +fn parse_script_interpreter_name(script: &str) -> Option { + let shebang = script.lines().next()?.strip_prefix("#!")?.trim(); + let mut tokens = shebang.split_whitespace(); + let command = tokens.next()?; + let command_name = Path::new(command).file_name()?.to_str()?; + if command_name == "env" { + for token in tokens { + if token.starts_with('-') { + continue; + } + return Path::new(token) + .file_name() + .and_then(|name| name.to_str()) + .map(ToOwned::to_owned); + } + return None; + } + + Some(command_name.to_owned()) +} + +fn parse_node_shell_shim_target(script: &str) -> Option { + for line in script.lines() { + let trimmed = line.trim(); + if !trimmed.starts_with("exec ") { + continue; + } + + let mut remaining = trimmed; + while let Some(start) = remaining.find("\"$basedir/") { + let after_prefix = &remaining[start + "\"$basedir/".len()..]; + let end = after_prefix.find('"')?; + let candidate = &after_prefix[..end]; + remaining = &after_prefix[end + 1..]; + + if candidate.is_empty() || candidate == "node" || candidate.ends_with("/node") { + continue; + } + + return Some(candidate.to_owned()); + } + } + + None +} + +fn is_probable_javascript_entrypoint(path: &Path, script: &str) -> bool { + let extension = path + .extension() + .and_then(|value| value.to_str()) + .unwrap_or_default(); + if matches!(extension, "js" | "cjs" | "mjs") { + return true; + } + + if !path + .components() + .any(|component| component.as_os_str() == "node_modules") + { + return false; + } + + let preview = script.trim_start_matches('\u{feff}').trim_start(); + !preview.is_empty() + && !preview.starts_with("#!") + && (preview.starts_with("\"use strict\"") + || preview.starts_with("'use strict'") + || preview.starts_with("import ") + || preview.starts_with("export ") + || preview.starts_with("const ") + || preview.starts_with("let ") + || preview.starts_with("var ") + || preview.starts_with("Object.defineProperty(exports") + || preview.starts_with("module.exports") + || preview.starts_with("require(")) +} + +fn resolve_guest_execution_cwd(vm: &VmState, value: Option<&str>) -> String { + value + .map(normalize_path) + .unwrap_or_else(|| vm.guest_cwd.clone()) +} + +fn resolve_execution_cwds(vm: &VmState, value: Option<&str>) -> (String, PathBuf, bool) { + if let Some(raw_cwd) = value { + let normalized_vm_host_cwd = normalize_host_path(&vm.host_cwd); + let requested_host_cwd = normalize_host_path(Path::new(raw_cwd)); + if path_is_within_root(&requested_host_cwd, &normalized_vm_host_cwd) { + let relative = requested_host_cwd + .strip_prefix(&normalized_vm_host_cwd) + .unwrap_or_else(|_| Path::new("")); + let relative = relative.to_string_lossy().replace('\\', "/"); + let guest_cwd = if relative.is_empty() { + String::from("/") + } else { + normalize_path(&format!("/{relative}")) + }; + return (guest_cwd, requested_host_cwd, true); + } + } + + let guest_cwd = resolve_guest_execution_cwd(vm, value); + let host_cwd = if value.is_none() { + vm.host_cwd.clone() + } else { + resolve_vm_guest_path_to_host(vm, &guest_cwd) + }; + (guest_cwd, host_cwd, value.is_none()) +} + +fn resolve_vm_guest_path_to_host(vm: &VmState, guest_path: &str) -> PathBuf { + host_mount_path_for_guest_path(vm, guest_path) + .unwrap_or_else(|| shadow_path_for_guest(vm, guest_path)) +} + +fn shadow_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf { + let normalized = normalize_path(guest_path); + let relative = normalized.trim_start_matches('/'); + if relative.is_empty() { + return vm.cwd.clone(); + } + vm.cwd.join(relative) +} + +fn apply_shell_cwd_prefix(command: &str, mut args: Vec, guest_cwd: &str) -> Vec { + if guest_cwd == "/" || !is_shell_command(command) { + return args; + } + + let Some(flag) = args.first() else { + return args; + }; + if !matches!(flag.as_str(), "-c" | "-lc") || args.len() < 2 { + return args; + } + + let command_text = args[1].clone(); + let quoted_cwd = shell_single_quote(guest_cwd); + args[1] = format!("cd {quoted_cwd} && {command_text}"); + args +} + +fn is_shell_command(command: &str) -> bool { + Path::new(command) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(command) + .trim_end_matches(".exe") + .eq("sh") + || Path::new(command) + .file_name() + .and_then(|name| name.to_str()) + .unwrap_or(command) + .trim_end_matches(".exe") + .eq("bash") +} + +fn shell_single_quote(value: &str) -> String { + if value.is_empty() { + return String::from("''"); + } + format!("'{}'", value.replace('\'', "'\"'\"'")) +} + +pub(crate) fn sync_active_process_host_writes_to_kernel( + vm: &mut VmState, +) -> Result<(), SidecarError> { + if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly { + sync_vm_shadow_root_to_kernel(vm)?; + } + + let normalized_vm_root = normalize_host_path(&vm.cwd); + let extra_roots = collect_active_process_host_sync_roots(vm, &normalized_vm_root); + for (host_cwd, guest_cwd) in extra_roots { + sync_host_directory_tree_to_kernel(vm, &host_cwd, &guest_cwd)?; + } + + Ok(()) +} + +/// Syncs the VM shadow root into the kernel VFS and reconciles deletions. +/// +/// The walk itself is additive (it copies shadow entries into the kernel), so +/// on its own it can never express a guest deletion performed directly on the +/// shadow tree — a removed file would be resurrected forever. To get real +/// Linux semantics the walk records every guest path it sees and diffs that +/// set against the previous walk's inventory: paths that were present in the +/// shadow before and are gone now are removed from the kernel VFS as well. +/// +/// Deletion propagation is scoped to the guest-writable shadow region: it only +/// runs for the VM shadow root walk (never for external host roots), re-checks +/// the protected/mount skip list, never touches the POSIX bootstrap skeleton, +/// and removes directories non-recursively so a kernel directory that still +/// has kernel-only content (for example a mount point or a path that was never +/// materialized into the shadow) is left in place with a warning. +fn sync_vm_shadow_root_to_kernel(vm: &mut VmState) -> Result<(), SidecarError> { + let shadow_root = normalize_host_path(&vm.cwd); + if host_sync_root_is_filesystem_root(&shadow_root) { + tracing::warn!("skipping host shadow sync rooted at the host filesystem root"); + return Ok(()); + } + let mut snapshot = collect_shadow_sync_snapshot(&shadow_root)?; + snapshot + .entries + .retain(|path, _| !should_skip_shadow_sync_path(vm, path)); + + // An unreadable subtree is not an empty subtree. Preserve the previous + // inventory below it so a transient EACCES cannot be misinterpreted as a + // request to delete all of its kernel children. + for (path, entry) in &vm.shadow_sync_inventory { + if snapshot + .incomplete_subtrees + .iter() + .any(|prefix| guest_path_is_at_or_below(path, prefix)) + { + snapshot.entries.entry(path.clone()).or_insert(*entry); + } + } + + let failed_replacements = propagate_shadow_deletions_to_kernel(vm, &mut snapshot.entries); + let mut synced_file_times = BTreeMap::new(); + sync_host_directory_tree_to_kernel_inner( + vm, + &shadow_root, + &shadow_root, + "/", + &mut synced_file_times, + Some(&snapshot.entries), + Some(&failed_replacements), + )?; + vm.shadow_sync_inventory = snapshot.entries; + Ok(()) +} + +#[derive(Default)] +struct ShadowSyncSnapshot { + entries: BTreeMap, + incomplete_subtrees: BTreeSet, +} + +/// Capture the shadow root before any additive kernel writes. The separate +/// inventory pass lets deletion and type-replacement reconciliation happen +/// first, which is essential when a stale symlink or directory occupies the +/// pathname that is about to become a regular file. +pub(crate) fn initial_shadow_sync_inventory( + shadow_root: &Path, +) -> Result, SidecarError> { + Ok(collect_shadow_sync_snapshot(shadow_root)?.entries) +} + +fn collect_shadow_sync_snapshot(shadow_root: &Path) -> Result { + let mut snapshot = ShadowSyncSnapshot::default(); + collect_shadow_sync_snapshot_inner(shadow_root, shadow_root, "/", &mut snapshot)?; + Ok(snapshot) +} + +fn collect_shadow_sync_snapshot_inner( + shadow_root: &Path, + current_host_dir: &Path, + current_guest_dir: &str, + snapshot: &mut ShadowSyncSnapshot, +) -> Result<(), SidecarError> { + let entries = match fs::read_dir(current_host_dir) { + Ok(entries) => entries, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(error) + if error.kind() == std::io::ErrorKind::PermissionDenied + || error.raw_os_error() == Some(libc::EPERM) => + { + let guest_path = normalize_path(current_guest_dir); + snapshot.incomplete_subtrees.insert(guest_path.clone()); + tracing::warn!( + path = %current_host_dir.display(), + guest_path = %guest_path, + "shadow inventory is incomplete because a host directory is unreadable" + ); + return Ok(()); + } + Err(error) => { + return Err(SidecarError::Io(format!( + "failed to inventory host shadow directory {}: {error}", + current_host_dir.display() + ))); + } + }; + + for entry in entries { + let entry = match entry { + Ok(entry) => entry, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, + Err(error) => { + return Err(SidecarError::Io(format!( + "failed to inventory host shadow entry in {}: {error}", + current_host_dir.display() + ))); + } + }; + let host_path = entry.path(); + let file_type = match entry.file_type() { + Ok(file_type) => file_type, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, + Err(error) => { + return Err(SidecarError::Io(format!( + "failed to inventory host shadow entry {}: {error}", + host_path.display() + ))); + } + }; + let relative_path = host_path.strip_prefix(shadow_root).map_err(|error| { + SidecarError::InvalidState(format!( + "failed to relativize host shadow path {} against {}: {error}", + host_path.display(), + shadow_root.display() + )) + })?; + let guest_path = normalize_path(&format!( + "/{}", + relative_path.to_string_lossy().replace('\\', "/") + )); + let node_type = if file_type.is_dir() { + ShadowNodeType::Directory + } else if file_type.is_file() { + ShadowNodeType::File + } else if file_type.is_symlink() { + ShadowNodeType::Symlink + } else { + continue; + }; + snapshot.entries.insert( + guest_path.clone(), + ShadowSyncInventoryEntry::present(node_type), + ); + if node_type == ShadowNodeType::Directory { + collect_shadow_sync_snapshot_inner(shadow_root, &host_path, &guest_path, snapshot)?; + } + } + Ok(()) +} + +/// Removes kernel paths whose shadow copy disappeared since the last walk. +/// +/// Best-effort by design: a failure to reconcile one stale path must not +/// poison the guest filesystem operation that triggered the sync, so failures +/// are surfaced as host-visible warnings instead of errors. Children sort +/// after their parents in the `BTreeSet`, so the reverse iteration removes +/// leaves before the directories that contain them. +fn propagate_shadow_deletions_to_kernel( + vm: &mut VmState, + current: &mut BTreeMap, +) -> BTreeSet { + let stale = vm + .shadow_sync_inventory + .iter() + .rev() + .filter_map(|(path, previous)| { + let replacement = current.get(path); + (previous.deletion_pending + || replacement.is_none() + || replacement.is_some_and(|entry| entry.node_type != previous.node_type)) + .then(|| (path.clone(), *previous)) + }) + .collect::>(); + let mut failed = BTreeSet::new(); + for (path, previous) in stale { + if path == "/" || should_skip_shadow_sync_path(vm, &path) || is_shadow_bootstrap_dir(&path) + { + continue; + } + let stat = match vm.kernel.lstat(&path) { + Ok(stat) => stat, + Err(error) if error.code() == "ENOENT" => continue, + Err(error) => { + tracing::warn!( + path = %path, + error = %error, + "failed to inspect a stale shadow path; deletion will be retried" + ); + retain_shadow_deletion_tombstone(current, &path, previous); + failed.insert(path); + continue; + } + }; + let result = if stat.is_directory && !stat.is_symbolic_link { + vm.kernel.remove_dir(&path) + } else { + vm.kernel.remove_file(&path) + }; + if let Err(error) = result { + if error.code() != "ENOENT" { + tracing::warn!( + path = %path, + error = %error, + "failed to propagate guest shadow deletion into the kernel VFS; deletion will be retried" + ); + retain_shadow_deletion_tombstone(current, &path, previous); + failed.insert(path); + } + } + } + failed +} + +fn retain_shadow_deletion_tombstone( + current: &mut BTreeMap, + path: &str, + previous: ShadowSyncInventoryEntry, +) { + current + .entry(path.to_owned()) + .and_modify(|entry| entry.deletion_pending = true) + .or_insert(ShadowSyncInventoryEntry { + node_type: previous.node_type, + deletion_pending: true, + }); +} + +fn collect_active_process_host_sync_roots( + vm: &VmState, + normalized_vm_root: &Path, +) -> Vec<(PathBuf, String)> { + let mut roots = Vec::new(); + let mut seen = BTreeSet::new(); + + for process in vm.active_processes.values() { + collect_process_host_sync_roots(process, normalized_vm_root, &mut seen, &mut roots); + } + + roots +} + +fn collect_process_host_sync_roots( + process: &ActiveProcess, + normalized_vm_root: &Path, + seen: &mut BTreeSet<(PathBuf, String)>, + roots: &mut Vec<(PathBuf, String)>, +) { + let normalized_host_cwd = normalize_host_path(&process.host_cwd); + if !path_is_within_root(&normalized_host_cwd, normalized_vm_root) { + let guest_cwd = normalize_path(&process.guest_cwd); + if seen.insert((normalized_host_cwd.clone(), guest_cwd.clone())) { + roots.push((normalized_host_cwd, guest_cwd)); + } + } + + for child in process.child_processes.values() { + collect_process_host_sync_roots(child, normalized_vm_root, seen, roots); + } +} + +fn sync_process_host_writes_to_kernel( + vm: &mut VmState, + process: &ActiveProcess, +) -> Result<(), SidecarError> { + sync_process_host_roots_to_kernel(vm, &process.host_cwd, &process.guest_cwd) +} + +fn sync_process_host_roots_to_kernel( + vm: &mut VmState, + process_host_cwd: &Path, + process_guest_cwd: &str, +) -> Result<(), SidecarError> { + if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly { + sync_vm_shadow_root_to_kernel(vm)?; + } + + if !path_is_within_root( + &normalize_host_path(process_host_cwd), + &normalize_host_path(&vm.cwd), + ) { + sync_host_directory_tree_to_kernel(vm, process_host_cwd, process_guest_cwd)?; + } + + Ok(()) +} + +fn host_sync_root_is_filesystem_root(host_root: &Path) -> bool { + normalize_host_path(host_root) == Path::new("/") +} + +fn sync_host_directory_tree_to_kernel( + vm: &mut VmState, + host_root: &Path, + guest_root: &str, +) -> Result<(), SidecarError> { + let normalized_host_root = normalize_host_path(host_root); + let normalized_guest_root = normalize_path(guest_root); + if host_sync_root_is_filesystem_root(host_root) { + // A process tracked with host cwd "/" would pull the entire host + // filesystem into the kernel VFS (until the size/inode caps fire). + // No sanctioned flow shadows the host root wholesale; host access is + // scoped through mounts. + tracing::warn!("skipping host shadow sync rooted at the host filesystem root"); + return Ok(()); + } + let mut synced_file_times = BTreeMap::new(); + sync_host_directory_tree_to_kernel_inner( + vm, + &normalized_host_root, + &normalized_host_root, + &normalized_guest_root, + &mut synced_file_times, + None, + None, + ) +} + +fn sync_host_directory_tree_to_kernel_inner( + vm: &mut VmState, + host_root: &Path, + current_host_dir: &Path, + guest_root: &str, + synced_file_times: &mut BTreeMap<(u64, u64), (u64, u64)>, + expected_inventory: Option<&BTreeMap>, + failed_replacements: Option<&BTreeSet>, +) -> Result<(), SidecarError> { + let entries = match fs::read_dir(current_host_dir) { + Ok(entries) => entries, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => { + // Host dirs the sidecar user cannot read (e.g. root-owned + // /lost+found under a host-root mount) are skipped rather than + // failing the whole shadow sync; the guest just won't see them. + tracing::warn!( + path = %current_host_dir.display(), + "skipping unreadable host shadow directory" + ); + return Ok(()); + } + Err(error) => { + return Err(SidecarError::Io(format!( + "failed to read host shadow directory {}: {error}", + current_host_dir.display() + ))); + } + }; + + for entry in entries { + let entry = entry.map_err(|error| { + SidecarError::Io(format!( + "failed to read host shadow entry in {}: {error}", + current_host_dir.display() + )) + })?; + let host_path = entry.path(); + let file_type = entry.file_type().map_err(|error| { + SidecarError::Io(format!( + "failed to stat host shadow entry {}: {error}", + host_path.display() + )) + })?; + let relative_path = host_path + .strip_prefix(host_root) + .map_err(|error| { + SidecarError::InvalidState(format!( + "failed to relativize host shadow path {} against {}: {error}", + host_path.display(), + host_root.display() + )) + })? + .to_string_lossy() + .replace('\\', "/"); + let guest_path = if guest_root == "/" { + normalize_path(&format!("/{relative_path}")) + } else { + normalize_path(&format!( + "{}/{}", + guest_root.trim_end_matches('/'), + relative_path + )) + }; + + if should_skip_shadow_sync_path(vm, &guest_path) { + continue; + } + if expected_inventory.is_some_and(|inventory| !inventory.contains_key(&guest_path)) { + // The entry appeared after the inventory pass. Pick it up on the + // next sync rather than mixing two different shadow snapshots. + continue; + } + if failed_replacements.is_some_and(|failed| failed.contains(&guest_path)) { + // Never write through a stale object whose removal failed. The + // retained tombstone will retry before a future additive write. + continue; + } + + if file_type.is_dir() { + ensure_kernel_shadow_node_type(vm, &guest_path, ShadowNodeType::Directory)?; + let metadata = entry.metadata().map_err(|error| { + SidecarError::Io(format!( + "failed to read host shadow metadata {}: {error}", + host_path.display() + )) + })?; + if !is_shadow_bootstrap_dir(&guest_path) { + if !vm.kernel.exists(&guest_path).unwrap_or(false) { + vm.kernel.mkdir(&guest_path, true).map_err(|error| { + SidecarError::InvalidState(format!( + "failed to sync host shadow directory {} to guest {}: {}", + host_path.display(), + guest_path, + kernel_error(error) + )) + })?; + } + vm.kernel + .chmod(&guest_path, host_shadow_mode(&metadata)) + .map_err(|error| { + SidecarError::InvalidState(format!( + "failed to sync host shadow directory mode {} to guest {}: {}", + host_path.display(), + guest_path, + kernel_error(error) + )) + })?; + } + sync_host_directory_tree_to_kernel_inner( + vm, + host_root, + &host_path, + guest_root, + synced_file_times, + expected_inventory, + failed_replacements, + )?; + continue; + } + + if file_type.is_file() { + ensure_kernel_shadow_node_type(vm, &guest_path, ShadowNodeType::File)?; + let metadata = entry.metadata().map_err(|error| { + SidecarError::Io(format!( + "failed to read host shadow metadata {}: {error}", + host_path.display() + )) + })?; + let timestamp_key = (metadata.dev(), metadata.ino()); + let (atime_ms, mtime_ms) = + *synced_file_times.entry(timestamp_key).or_insert_with(|| { + ( + metadata_time_ms(metadata.atime(), metadata.atime_nsec()), + metadata_time_ms(metadata.mtime(), metadata.mtime_nsec()), + ) + }); + let desired_mode = host_shadow_mode(&metadata); + // Fast path: skip the expensive re-read + re-write when the kernel already + // holds a copy of this shadow file that matches on size, mode, and mtime. + // + // Every read-side fs op (exists/stat/readFile/...) triggers a full + // shadow-tree reconciliation walk. Without this skip the walk re-reads every + // file's bytes from the host and re-writes them into the kernel VFS on every + // op -- O(whole tree) per op, and super-linear as the VM's shadow grows, + // which is a dominant source of session-creation/runtime latency on + // populated VMs. + // + // This is a (size, mode, mtime) quick-check, the same heuristic rsync uses + // by default. It needs no separate cache to invalidate -- it compares against + // the kernel's own stat, so a kernel reset (e.g. a layer swap) or any host + // change that moves size/mode/mtime forces a resync. Limitation: mtime is + // compared at the millisecond granularity the kernel stores (utimes truncates + // to ms), so a host-side rewrite that preserves byte length AND mode AND lands + // in the same wall-clock millisecond can be skipped and leave stale bytes. + // That window is sub-millisecond same-length edits; if it ever matters here, + // upgrade this to a content digest (or full-precision mtime) for files whose + // mtime is within the last few ms of `now`. + if let Ok(existing) = vm.kernel.lstat(&guest_path) { + if !existing.is_directory + && !existing.is_symbolic_link + && existing.size == metadata.len() + && (existing.mode & 0o7777) == (desired_mode & 0o7777) + && existing.mtime_ms == mtime_ms + { + continue; + } + } + let bytes = match read_host_shadow_file(&host_path, desired_mode) { + Ok(bytes) => bytes, + // The host entry vanished between the walk and the read + // (short-lived files churn constantly — editor swap files, + // temp files). Skipping matches native semantics; failing + // here would poison EVERY subsequent fs op on the VM. + Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, + // Same tolerance for entries the sidecar user cannot read + // (root-owned files under a host-root mount): skip them + // rather than poisoning the whole sync. + Err(error) + if error.kind() == std::io::ErrorKind::PermissionDenied + || error.raw_os_error() == Some(libc::EPERM) => + { + tracing::warn!( + path = %host_path.display(), + "skipping unreadable host shadow file" + ); + continue; + } + Err(error) => { + return Err(SidecarError::Io(format!( + "failed to read host shadow file {}: {error}", + host_path.display() + ))); + } + }; + match vm.kernel.write_file(&guest_path, bytes) { + Ok(()) => {} + // ENOENT here means the guest-side path cannot currently + // receive the write (e.g. it is a symlink whose target was + // just unlinked by the guest — vim's swap-file dance). The + // entry is mid-churn; skip it rather than failing the VM. + Err(error) if error.code() == "ENOENT" => continue, + Err(error) => { + return Err(SidecarError::InvalidState(format!( + "failed to sync host shadow file {} to guest {}: {}", + host_path.display(), + guest_path, + kernel_error(error) + ))); + } + } + vm.kernel + .chmod(&guest_path, desired_mode) + .map_err(|error| { + SidecarError::InvalidState(format!( + "failed to sync host shadow file mode {} to guest {}: {}", + host_path.display(), + guest_path, + kernel_error(error) + )) + })?; + vm.kernel + .utimes(&guest_path, atime_ms, mtime_ms) + .map_err(|error| { + SidecarError::InvalidState(format!( + "failed to sync host shadow file times {} to guest {}: {}", + host_path.display(), + guest_path, + kernel_error(error) + )) + })?; + continue; } - return None; - } - - resolve_javascript_command_entrypoint_inner( - vm, - guest_entrypoint, - host_entrypoint, - MAX_JAVASCRIPT_COMMAND_REDIRECT_DEPTH, - ) -} -fn resolve_javascript_command_entrypoint_inner( - vm: &VmState, - guest_entrypoint: &str, - host_entrypoint: &Path, - redirects_remaining: usize, -) -> Option<(String, PathBuf)> { - if redirects_remaining > 0 { - let symlink_target = fs::symlink_metadata(host_entrypoint) - .ok() - .filter(|metadata| metadata.file_type().is_symlink()) - .and_then(|_| fs::read_link(host_entrypoint).ok()); - if let Some(symlink_target) = symlink_target { - let guest_parent = Path::new(guest_entrypoint) - .parent() - .and_then(|path| path.to_str()) - .unwrap_or("/"); - let symlink_guest_entrypoint = if symlink_target.is_absolute() { - normalize_path(&symlink_target.to_string_lossy()) - } else { - normalize_path(&format!( - "{guest_parent}/{}", - symlink_target.to_string_lossy().replace('\\', "/") - )) + if file_type.is_symlink() { + ensure_kernel_shadow_node_type(vm, &guest_path, ShadowNodeType::Symlink)?; + let target = match fs::read_link(&host_path) { + Ok(target) => target, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, + Err(error) => { + return Err(SidecarError::Io(format!( + "failed to read host shadow symlink {}: {error}", + host_path.display() + ))); + } }; - let symlink_host_entrypoint = - resolve_vm_guest_path_to_host(vm, &symlink_guest_entrypoint); - return resolve_javascript_command_entrypoint_inner( - vm, - &symlink_guest_entrypoint, - &symlink_host_entrypoint, - redirects_remaining - 1, - ); + replace_kernel_symlink(vm, &guest_path, &target.to_string_lossy())?; } } - let script = load_executable_script_preview(host_entrypoint)?; - let interpreter = parse_script_interpreter_name(&script); + Ok(()) +} - if interpreter.is_none() && is_probable_javascript_entrypoint(host_entrypoint, &script) { - return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf())); +fn ensure_kernel_shadow_node_type( + vm: &mut VmState, + guest_path: &str, + desired: ShadowNodeType, +) -> Result<(), SidecarError> { + let existing = match vm.kernel.lstat(guest_path) { + Ok(existing) => existing, + Err(error) if error.code() == "ENOENT" => return Ok(()), + Err(error) => return Err(kernel_error(error)), + }; + let existing_type = if existing.is_symbolic_link { + ShadowNodeType::Symlink + } else if existing.is_directory { + ShadowNodeType::Directory + } else { + ShadowNodeType::File + }; + if existing_type == desired { + return Ok(()); } - let interpreter = interpreter?; - if interpreter == "node" { - return Some((guest_entrypoint.to_owned(), host_entrypoint.to_path_buf())); + let result = if existing_type == ShadowNodeType::Directory { + vm.kernel.remove_dir(guest_path) + } else { + // `remove_file` unlinks the directory entry itself. It must run before + // `write_file`, otherwise that write would follow a stale symlink. + vm.kernel.remove_file(guest_path) + }; + result.map_err(|error| { + SidecarError::InvalidState(format!( + "failed to replace shadow path {guest_path} from {existing_type:?} to {desired:?}: {}", + kernel_error(error) + )) + }) +} + +fn replace_kernel_symlink( + vm: &mut VmState, + guest_path: &str, + target: &str, +) -> Result<(), SidecarError> { + if vm.kernel.symlink(target, guest_path).is_ok() { + return Ok(()); } - if redirects_remaining == 0 || !matches!(interpreter.as_str(), "sh" | "bash" | "dash") { - return None; + if let Ok(existing_target) = vm.kernel.read_link(guest_path) { + if existing_target == target { + return Ok(()); + } } - let shim_target = parse_node_shell_shim_target(&script)?; - let guest_parent = Path::new(guest_entrypoint) - .parent() - .and_then(|path| path.to_str()) - .unwrap_or("/"); - let shim_guest_entrypoint = normalize_path(&format!("{guest_parent}/{shim_target}")); - let shim_host_entrypoint = resolve_vm_guest_path_to_host(vm, &shim_guest_entrypoint); - resolve_javascript_command_entrypoint_inner( - vm, - &shim_guest_entrypoint, - &shim_host_entrypoint, - redirects_remaining - 1, - ) + let _ = vm.kernel.remove_file(guest_path); + let _ = vm.kernel.remove_dir(guest_path); + vm.kernel + .symlink(target, guest_path) + .map_err(kernel_error)?; + Ok(()) } -fn load_executable_script_preview(path: &Path) -> Option { - let bytes = fs::read(path).ok()?; - let preview_len = bytes.len().min(16 * 1024); - Some(String::from_utf8_lossy(&bytes[..preview_len]).into_owned()) +fn host_shadow_mode(metadata: &fs::Metadata) -> u32 { + metadata.permissions().mode() & 0o7777 } -fn parse_script_interpreter_name(script: &str) -> Option { - let shebang = script.lines().next()?.strip_prefix("#!")?.trim(); - let mut tokens = shebang.split_whitespace(); - let command = tokens.next()?; - let command_name = Path::new(command).file_name()?.to_str()?; - if command_name == "env" { - for token in tokens { - if token.starts_with('-') { - continue; - } - return Path::new(token) - .file_name() - .and_then(|name| name.to_str()) - .map(ToOwned::to_owned); +/// Reads a shadow-root file back into the kernel even when guest-visible mode +/// bits make it unreadable for the host user. The sidecar is the kernel for +/// this tree, so guest permission bits (for example a 0o200 write-only file +/// produced by `chmod` plus a shell append redirect) must not break the +/// exit-time shadow sync. The original mode is restored after the read. +fn read_host_shadow_file(host_path: &Path, mode: u32) -> std::io::Result> { + match fs::read(host_path) { + Ok(bytes) => Ok(bytes), + Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => { + fs::set_permissions(host_path, fs::Permissions::from_mode(mode | 0o400))?; + let result = fs::read(host_path); + fs::set_permissions(host_path, fs::Permissions::from_mode(mode))?; + result } - return None; + Err(error) => Err(error), } - - Some(command_name.to_owned()) } -fn parse_node_shell_shim_target(script: &str) -> Option { - for line in script.lines() { - let trimmed = line.trim(); - if !trimmed.starts_with("exec ") { - continue; - } - - let mut remaining = trimmed; - while let Some(start) = remaining.find("\"$basedir/") { - let after_prefix = &remaining[start + "\"$basedir/".len()..]; - let end = after_prefix.find('"')?; - let candidate = &after_prefix[..end]; - remaining = &after_prefix[end + 1..]; - - if candidate.is_empty() || candidate == "node" || candidate.ends_with("/node") { - continue; - } - - return Some(candidate.to_owned()); - } - } +fn metadata_time_ms(seconds: i64, nanos: i64) -> u64 { + let seconds = seconds.max(0) as u64; + let nanos = nanos.max(0) as u64; + seconds + .saturating_mul(1_000) + .saturating_add(nanos / 1_000_000) +} - None +fn is_shadow_bootstrap_dir(path: &str) -> bool { + matches!( + path, + "/dev" + | "/proc" + | "/tmp" + | "/bin" + | "/lib" + | "/sbin" + | "/boot" + | "/etc" + | "/root" + | "/run" + | "/srv" + | "/sys" + | "/opt" + | "/mnt" + | "/media" + | "/home" + | "/home/agentos" + | "/usr" + | "/usr/bin" + | "/usr/games" + | "/usr/include" + | "/usr/lib" + | "/usr/libexec" + | "/usr/man" + | "/usr/local" + | "/usr/local/bin" + | "/usr/sbin" + | "/usr/share" + | "/usr/share/man" + | "/var" + | "/var/cache" + | "/var/empty" + | "/var/lib" + | "/var/lock" + | "/var/log" + | "/var/run" + | "/var/spool" + | "/var/tmp" + | "/etc/agentos" + | "/workspace" + ) } -fn is_probable_javascript_entrypoint(path: &Path, script: &str) -> bool { - let extension = path - .extension() - .and_then(|value| value.to_str()) - .unwrap_or_default(); - if matches!(extension, "js" | "cjs" | "mjs") { - return true; - } +#[cfg(test)] +mod shadow_sync_tests { + use super::{is_protected_agentos_shadow_sync_path, is_shadow_bootstrap_dir}; - if !path - .components() - .any(|component| component.as_os_str() == "node_modules") - { - return false; + #[test] + fn shadow_bootstrap_sync_skips_virtual_home_tree() { + assert!(is_shadow_bootstrap_dir("/home")); + assert!(is_shadow_bootstrap_dir("/home/agentos")); } - let preview = script.trim_start_matches('\u{feff}').trim_start(); - !preview.is_empty() - && !preview.starts_with("#!") - && (preview.starts_with("\"use strict\"") - || preview.starts_with("'use strict'") - || preview.starts_with("import ") - || preview.starts_with("export ") - || preview.starts_with("const ") - || preview.starts_with("let ") - || preview.starts_with("var ") - || preview.starts_with("Object.defineProperty(exports") - || preview.starts_with("module.exports") - || preview.starts_with("require(")) + #[test] + fn protected_agentos_paths_are_not_shadow_synced() { + assert!(is_protected_agentos_shadow_sync_path("/etc/agentos")); + assert!(is_protected_agentos_shadow_sync_path( + "/etc/agentos/instructions.md" + )); + assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos-copy")); + assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos.md")); + } } -fn resolve_guest_execution_cwd(vm: &VmState, value: Option<&str>) -> String { - value - .map(normalize_path) - .unwrap_or_else(|| vm.guest_cwd.clone()) +fn is_kernel_owned_shadow_sync_path(path: &str) -> bool { + matches!(path, "/dev" | "/proc" | "/sys") + || path.starts_with("/dev/") + || path.starts_with("/proc/") + || path.starts_with("/sys/") } -fn resolve_execution_cwds(vm: &VmState, value: Option<&str>) -> (String, PathBuf, bool) { - if let Some(raw_cwd) = value { - let normalized_vm_host_cwd = normalize_host_path(&vm.host_cwd); - let requested_host_cwd = normalize_host_path(Path::new(raw_cwd)); - if path_is_within_root(&requested_host_cwd, &normalized_vm_host_cwd) { - let relative = requested_host_cwd - .strip_prefix(&normalized_vm_host_cwd) - .unwrap_or_else(|_| Path::new("")); - let relative = relative.to_string_lossy().replace('\\', "/"); - let guest_cwd = if relative.is_empty() { - String::from("/") - } else { - normalize_path(&format!("/{relative}")) - }; - return (guest_cwd, requested_host_cwd, true); - } - } - - let guest_cwd = resolve_guest_execution_cwd(vm, value); - let host_cwd = if value.is_none() { - vm.host_cwd.clone() - } else { - resolve_vm_guest_path_to_host(vm, &guest_cwd) - }; - (guest_cwd, host_cwd, value.is_none()) +pub(crate) fn is_protected_agentos_shadow_sync_path(path: &str) -> bool { + path == "/etc/agentos" || path.starts_with("/etc/agentos/") } -fn resolve_vm_guest_path_to_host(vm: &VmState, guest_path: &str) -> PathBuf { - host_mount_path_for_guest_path(vm, guest_path) - .unwrap_or_else(|| shadow_path_for_guest(vm, guest_path)) +fn should_skip_shadow_sync_path(vm: &VmState, guest_path: &str) -> bool { + is_kernel_owned_shadow_sync_path(guest_path) + || is_protected_agentos_shadow_sync_path(guest_path) + // Never reconcile the covered parent filesystem through a live mount. + // This applies to every plugin (host_dir, js_bridge, package tar, + // synthetic leaves, and future plugins), not just mounts with a host + // backing path. + || vm.kernel.mounted_filesystems().iter().any(|mount| { + let mount_path = normalize_path(&mount.path); + mount_path != "/" && guest_path_is_at_or_below(guest_path, &mount_path) + }) + // agentOS package content is served guest-native from read-only tar + // mounts; it is already present in the guest and cannot be written, so a + // host->guest shadow sync would fail with EROFS. Skip it. + || guest_path_is_within_agentos_package_mount(vm, guest_path) + || host_mount_path_for_guest_path_from_mounts(&vm.configuration.mounts, guest_path) + .is_some() } -fn shadow_path_for_guest(vm: &VmState, guest_path: &str) -> PathBuf { - let normalized = normalize_path(guest_path); - let relative = normalized.trim_start_matches('/'); - if relative.is_empty() { - return vm.cwd.clone(); - } - vm.cwd.join(relative) +fn guest_path_is_at_or_below(path: &str, prefix: &str) -> bool { + let path = normalize_path(path); + let prefix = normalize_path(prefix); + prefix == "/" || path == prefix || path.starts_with(&format!("{prefix}/")) } -fn apply_shell_cwd_prefix(command: &str, mut args: Vec, guest_cwd: &str) -> Vec { - if guest_cwd == "/" || !is_shell_command(command) { - return args; +fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String { + if specifier.starts_with("file://") { + normalize_path(specifier.trim_start_matches("file://")) + } else if specifier.starts_with("file:") { + normalize_path(specifier.trim_start_matches("file:")) + } else if specifier.starts_with('/') { + normalize_path(specifier) + } else { + normalize_path(&format!("{cwd}/{specifier}")) } +} - let Some(flag) = args.first() else { - return args; - }; - if !matches!(flag.as_str(), "-c" | "-lc") || args.len() < 2 { - return args; - } +fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option { + is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier)) +} - let command_text = args[1].clone(); - let quoted_cwd = shell_single_quote(guest_cwd); - args[1] = format!("cd {quoted_cwd} && {command_text}"); - args +fn is_node_runtime_command(command: &str) -> bool { + matches!(command, "node" | "npm" | "npx") + || Path::new(command) + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| matches!(name, "node" | "npm" | "npx")) } -fn is_shell_command(command: &str) -> bool { +fn python_command_base_name(command: &str) -> &str { Path::new(command) .file_name() .and_then(|name| name.to_str()) .unwrap_or(command) - .trim_end_matches(".exe") - .eq("sh") - || Path::new(command) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(command) - .trim_end_matches(".exe") - .eq("bash") } -fn shell_single_quote(value: &str) -> String { - if value.is_empty() { - return String::from("''"); - } - format!("'{}'", value.replace('\'', "'\"'\"'")) +/// `python` / `python3` (and `pip` / `pip3`, which map to `python -m pip`) are +/// served by the embedded Pyodide runtime, mirroring how `node` is served by the +/// embedded V8 runtime. +fn is_python_runtime_command(command: &str) -> bool { + matches!( + python_command_base_name(command), + "python" | "python3" | "pip" | "pip3" + ) } -pub(crate) fn sync_active_process_host_writes_to_kernel( - vm: &mut VmState, -) -> Result<(), SidecarError> { - if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly { - let shadow_root = vm.cwd.clone(); - sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?; +/// Parse a `python` / `pip` command line into a Pyodide execution. Supports the +/// CPython program selectors `-c CODE`, `-m MODULE`, a `SCRIPT` path, `-` / +/// piped stdin programs, and a bare interpreter (interactive REPL). The chosen +/// mode plus `sys.argv` are forwarded to the runner as `AGENTOS_PYTHON_*` control +/// env, which the runner consumes and never exposes in the guest `os.environ`. +fn resolve_python_command_execution( + vm: &VmState, + command: &str, + args: &[String], + mut env: BTreeMap, + guest_cwd: String, + host_cwd: PathBuf, +) -> Result { + let base_name = python_command_base_name(command); + let is_pip = matches!(base_name, "pip" | "pip3"); + + let mut entrypoint = String::new(); + let mut argv: Vec = Vec::new(); + let mut module: Option = None; + let mut stdin_program = false; + let mut interactive = false; + let mut guest_entrypoint: Option = None; + + if is_pip { + module = Some(String::from("pip")); + argv.push(String::from("pip")); + argv.extend(args.iter().cloned()); + } else { + // Skip the value-less interpreter flags we can safely ignore so they do + // not get mistaken for a script path. + let mut idx = 0; + while let Some(flag) = args.get(idx) { + match flag.as_str() { + "-B" | "-E" | "-I" | "-O" | "-OO" | "-q" | "-s" | "-S" | "-u" | "-v" | "-b" + | "-d" | "-x" => idx += 1, + _ => break, + } + } + let rest = &args[idx..]; + match rest.first().map(String::as_str) { + Some("-c") => { + entrypoint = rest.get(1).cloned().ok_or_else(|| { + SidecarError::InvalidState(String::from("argument expected for the -c option")) + })?; + argv.push(String::from("-c")); + argv.extend(rest.iter().skip(2).cloned()); + } + Some("-m") => { + let name = rest.get(1).cloned().ok_or_else(|| { + SidecarError::InvalidState(String::from("argument expected for the -m option")) + })?; + module = Some(name); + argv.push(String::from("-m")); + argv.extend(rest.iter().skip(2).cloned()); + } + Some("-") => { + stdin_program = true; + argv.push(String::from("-")); + argv.extend(rest.iter().skip(1).cloned()); + } + Some(spec) if !spec.starts_with('-') => { + let resolved_guest = guest_entrypoint_for_specifier(&guest_cwd, spec) + .unwrap_or_else(|| spec.to_string()); + entrypoint = resolved_guest.clone(); + env.insert(String::from("AGENTOS_PYTHON_FILE"), resolved_guest.clone()); + guest_entrypoint = Some(resolved_guest); + argv.push(spec.to_string()); + argv.extend(rest.iter().skip(1).cloned()); + } + Some(other) => { + return Err(SidecarError::InvalidState(format!( + "unsupported python option: {other}" + ))); + } + None => { + interactive = true; + argv.push(String::new()); + } + } } - let normalized_vm_root = normalize_host_path(&vm.cwd); - let extra_roots = collect_active_process_host_sync_roots(vm, &normalized_vm_root); - for (host_cwd, guest_cwd) in extra_roots { - sync_host_directory_tree_to_kernel(vm, &host_cwd, &guest_cwd)?; + env.insert( + String::from("AGENTOS_PYTHON_ARGV"), + serde_json::to_string(&argv).unwrap_or_else(|_| String::from("[]")), + ); + if let Some(module) = &module { + env.insert(String::from("AGENTOS_PYTHON_MODULE"), module.clone()); + } + if stdin_program { + env.insert( + String::from("AGENTOS_PYTHON_STDIN_PROGRAM"), + String::from("1"), + ); + } + if interactive { + env.insert( + String::from("AGENTOS_PYTHON_INTERACTIVE"), + String::from("1"), + ); + } + + prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?; + + Ok(ResolvedChildProcessExecution { + command: String::from(PYTHON_COMMAND), + process_args: std::iter::once(command.to_owned()) + .chain(args.iter().cloned()) + .collect(), + runtime: GuestRuntimeKind::Python, + entrypoint, + execution_args: args.to_vec(), + env, + guest_cwd, + host_cwd, + wasm_permission_tier: None, + tool_command: false, + }) +} + +fn resolve_special_node_cli_invocation( + args: &[String], + env: &mut BTreeMap, +) -> Option<(String, Vec)> { + let first = args.first()?; + match first.as_str() { + "-e" | "--eval" => { + env.insert( + String::from("AGENTOS_NODE_EVAL"), + args.get(1).cloned().unwrap_or_default(), + ); + Some((first.clone(), args.iter().skip(2).cloned().collect())) + } + "-v" | "--version" => { + env.insert( + String::from("AGENTOS_NODE_EVAL"), + String::from("console.log(process.version);"), + ); + Some((String::from("-e"), args.to_vec())) + } + _ => None, } - - Ok(()) } -fn collect_active_process_host_sync_roots( - vm: &VmState, - normalized_vm_root: &Path, -) -> Vec<(PathBuf, String)> { - let mut roots = Vec::new(); - let mut seen = BTreeSet::new(); - - for process in vm.active_processes.values() { - collect_process_host_sync_roots(process, normalized_vm_root, &mut seen, &mut roots); - } - - roots +fn node_runtime_command_name(command: &str) -> Option<&str> { + let name = Path::new(command) + .file_name() + .and_then(|name| name.to_str())?; + matches!(name, "node" | "npm" | "npx").then_some(name) } -fn collect_process_host_sync_roots( - process: &ActiveProcess, - normalized_vm_root: &Path, - seen: &mut BTreeSet<(PathBuf, String)>, - roots: &mut Vec<(PathBuf, String)>, -) { - let normalized_host_cwd = normalize_host_path(&process.host_cwd); - if !path_is_within_root(&normalized_host_cwd, normalized_vm_root) { - let guest_cwd = normalize_path(&process.guest_cwd); - if seen.insert((normalized_host_cwd.clone(), guest_cwd.clone())) { - roots.push((normalized_host_cwd, guest_cwd)); - } - } - - for child in process.child_processes.values() { - collect_process_host_sync_roots(child, normalized_vm_root, seen, roots); - } +struct ResolvedHostNodeCliEntrypoint { + command_name: String, + guest_root: String, + guest_entrypoint: String, + package_root: PathBuf, } -fn sync_process_host_writes_to_kernel( - vm: &mut VmState, - process: &ActiveProcess, -) -> Result<(), SidecarError> { - if vm.root_filesystem_mode != RootFilesystemMode::ReadOnly { - let shadow_root = vm.cwd.clone(); - sync_host_directory_tree_to_kernel(vm, &shadow_root, "/")?; +fn resolve_host_node_cli_entrypoint(command: &str) -> Option { + let command_name = node_runtime_command_name(command)?; + if !matches!(command_name, "npm" | "npx") { + return None; } - if !path_is_within_root( - &normalize_host_path(&process.host_cwd), - &normalize_host_path(&vm.cwd), - ) { - sync_host_directory_tree_to_kernel(vm, &process.host_cwd, &process.guest_cwd)?; + let path = std::env::var_os("PATH")?; + for root in std::env::split_paths(&path) { + let candidate = root.join(command_name); + if !candidate.is_file() { + continue; + } + let entrypoint = candidate.canonicalize().ok().unwrap_or(candidate); + let package_root = entrypoint.parent()?.parent()?.to_path_buf(); + let guest_root = format!("/__secure_exec/node-runtime/{command_name}"); + let relative_entrypoint = entrypoint.strip_prefix(&package_root).ok()?; + let guest_entrypoint = normalize_path(&format!( + "{guest_root}/{}", + relative_entrypoint.to_string_lossy().replace('\\', "/") + )); + return Some(ResolvedHostNodeCliEntrypoint { + command_name: command_name.to_owned(), + guest_root, + guest_entrypoint, + package_root, + }); } - Ok(()) -} - -fn host_sync_root_is_filesystem_root(host_root: &Path) -> bool { - normalize_host_path(host_root) == Path::new("/") + None } -fn sync_host_directory_tree_to_kernel( - vm: &mut VmState, - host_root: &Path, - guest_root: &str, -) -> Result<(), SidecarError> { - let normalized_host_root = normalize_host_path(host_root); - let normalized_guest_root = normalize_path(guest_root); - if host_sync_root_is_filesystem_root(host_root) { - // A process tracked with host cwd "/" would pull the entire host - // filesystem into the kernel VFS (until the size/inode caps fire). - // No sanctioned flow shadows the host root wholesale; host access is - // scoped through mounts. - tracing::warn!("skipping host shadow sync rooted at the host filesystem root"); - return Ok(()); +fn build_host_node_cli_eval(cli: &ResolvedHostNodeCliEntrypoint) -> String { + let guest_npm_main = normalize_path(&format!("{}/lib/npm.js", cli.guest_root)); + let guest_npm_cli = normalize_path(&format!("{}/bin/npm-cli.js", cli.guest_root)); + let guest_package_json = normalize_path(&format!("{}/package.json", cli.guest_root)); + let guest_display_module = normalize_path(&format!("{}/lib/utils/display.js", cli.guest_root)); + let guest_log_file_module = + normalize_path(&format!("{}/lib/utils/log-file.js", cli.guest_root)); + let debug_preamble = "const __agentOSDebugNpmCli = !!process.env.CODEX_DEBUG_NPM_CLI; const __agentOSDebugLog = (...args) => { if (__agentOSDebugNpmCli) { console.error('[secure-exec npm debug]', ...args); } }; const __agentOSIsProcessExitError = (error) => !!(error && typeof error === 'object' && (error._isProcessExit === true || error.name === 'ProcessExitError')); const __agentOSResolveExitCode = (code) => Number.isFinite(code) ? code : (Number.isFinite(process.exitCode) ? process.exitCode : 0); const __agentOSFinish = (code) => { process.exitCode = __agentOSResolveExitCode(code); }; if (__agentOSDebugNpmCli) { const __agentOSWrapAsyncFsMethod = (__agentOSTarget, __agentOSMethod) => { const __agentOSOriginal = __agentOSTarget[__agentOSMethod]; if (typeof __agentOSOriginal !== 'function' || __agentOSOriginal.__agentOSDebugWrapped) { return; } const __agentOSWrapped = async (...args) => { const target = args.length > 0 ? args[0] : ''; __agentOSDebugLog(`fs.${__agentOSMethod}:start`, String(target)); try { const result = await __agentOSOriginal.apply(__agentOSTarget, args); __agentOSDebugLog(`fs.${__agentOSMethod}:done`, String(target)); return result; } catch (error) { __agentOSDebugLog(`fs.${__agentOSMethod}:error`, String(target), error && error.stack ? error.stack : String(error)); throw error; } }; __agentOSWrapped.__agentOSDebugWrapped = true; __agentOSTarget[__agentOSMethod] = __agentOSWrapped; }; const __agentOSWrapSyncFsMethod = (__agentOSTarget, __agentOSMethod) => { const __agentOSOriginal = __agentOSTarget[__agentOSMethod]; if (typeof __agentOSOriginal !== 'function' || __agentOSOriginal.__agentOSDebugWrapped) { return; } const __agentOSWrapped = (...args) => { const target = args.length > 0 ? args[0] : ''; __agentOSDebugLog(`fs.${__agentOSMethod}:start`, String(target)); try { const result = __agentOSOriginal.apply(__agentOSTarget, args); __agentOSDebugLog(`fs.${__agentOSMethod}:done`, String(target)); return result; } catch (error) { __agentOSDebugLog(`fs.${__agentOSMethod}:error`, String(target), error && error.stack ? error.stack : String(error)); throw error; } }; __agentOSWrapped.__agentOSDebugWrapped = true; __agentOSTarget[__agentOSMethod] = __agentOSWrapped; }; const __agentOSFsPromiseModules = [require('fs/promises'), require('node:fs/promises')]; for (const __agentOSFsPromises of __agentOSFsPromiseModules) { for (const __agentOSMethod of ['access', 'lstat', 'mkdir', 'open', 'readFile', 'readdir', 'readlink', 'realpath', 'rename', 'rm', 'rmdir', 'stat', 'symlink', 'unlink', 'writeFile']) { __agentOSWrapAsyncFsMethod(__agentOSFsPromises, __agentOSMethod); } } const __agentOSFsModules = [require('fs'), require('node:fs')]; for (const __agentOSFs of __agentOSFsModules) { for (const __agentOSMethod of ['accessSync', 'existsSync', 'lstatSync', 'mkdirSync', 'openSync', 'readFileSync', 'readdirSync', 'readlinkSync', 'realpathSync', 'renameSync', 'rmSync', 'rmdirSync', 'statSync', 'symlinkSync', 'unlinkSync', 'writeFileSync']) { __agentOSWrapSyncFsMethod(__agentOSFs, __agentOSMethod); } } }"; + let display_stub = format!( + "const __agentOSDisplayModulePath = require.resolve({display_module}); const __agentOSLogFileModulePath = require.resolve({log_file_module}); const __agentOSColorPassthrough = new Proxy((value) => value, {{ get: () => __agentOSColorPassthrough, apply: (_target, _thisArg, args) => args[0] }}); class __AgentOSNpmDisplayStub {{ constructor() {{ this.chalk = {{ noColor: __agentOSColorPassthrough, stdout: __agentOSColorPassthrough, stderr: __agentOSColorPassthrough }}; this._logPaused = true; this._logBuffer = []; this._outputBuffer = []; this._write = (stream, values) => {{ if (!Array.isArray(values) || values.length === 0) {{ return; }} const text = values.map((value) => typeof value === 'string' ? value : String(value)).join(' '); if (text.length === 0) {{ return; }} const normalized = text.replace(/\\r\\n/g, '\\n'); if (/^\\n?> npx\\n> /u.test(normalized)) {{ return; }} stream.write(text.endsWith('\\n') ? text : `${{text}}\\n`); }}; this._inputHandler = (level, ...args) => {{ if (level !== 'read') {{ return; }} const [resolve, reject, callback] = args; Promise.resolve().then(() => callback()).then(resolve, reject); }}; this._logHandler = (level, ...args) => {{ if (level === 'resume') {{ this._logPaused = false; for (const entry of this._logBuffer.splice(0)) {{ this._write(process.stderr, entry); }} return; }} if (level === 'pause') {{ this._logPaused = true; return; }} if (this._logPaused) {{ this._logBuffer.push(args); return; }} this._write(process.stderr, args); }}; this._outputHandler = (level, ...args) => {{ if (level === 'buffer') {{ this._outputBuffer.push(['standard', args]); return; }} if (level === 'flush') {{ for (const [bufferLevel, bufferArgs] of this._outputBuffer.splice(0)) {{ this._write(bufferLevel === 'error' ? process.stderr : process.stdout, bufferArgs); }} return; }} this._write(level === 'error' ? process.stderr : process.stdout, args); }}; process.on('input', this._inputHandler); process.on('log', this._logHandler); process.on('output', this._outputHandler); }} async load() {{ process.emit('log', 'resume'); process.emit('output', 'flush'); }} off() {{ if (this._inputHandler) {{ process.off('input', this._inputHandler); }} if (this._logHandler) {{ process.off('log', this._logHandler); }} if (this._outputHandler) {{ process.off('output', this._outputHandler); }} this._logBuffer.length = 0; this._outputBuffer.length = 0; }} }} class __AgentOSNpmLogFileStub {{ constructor() {{ this.files = []; }} async load() {{ return []; }} off() {{}} }} globalThis._moduleCache[__agentOSDisplayModulePath] = {{ exports: __AgentOSNpmDisplayStub }}; globalThis._moduleCache[__agentOSLogFileModulePath] = {{ exports: __AgentOSNpmLogFileStub }};", + display_module = serde_json::to_string(&guest_display_module) + .unwrap_or_else(|_| format!("\"{guest_display_module}\"")), + log_file_module = serde_json::to_string(&guest_log_file_module) + .unwrap_or_else(|_| format!("\"{guest_log_file_module}\"")), + ); + let registry_fetch_stub = "const { createRequire: __agentOSCreateRequire } = require('module'); const __agentOSNpmRequire = __agentOSCreateRequire(require.resolve(__AGENTOS_NPM_MAIN__)); try { const __agentOSMinipassFetchPath = __agentOSNpmRequire.resolve('minipass-fetch'); const __agentOSMinipassFetch = __agentOSNpmRequire(__agentOSMinipassFetchPath); const { FetchError: __agentOSFetchError, Headers: __agentOSFetchHeaders, Request: __agentOSFetchRequest, Response: __agentOSFetchResponse, AbortError: __agentOSAbortError } = __agentOSMinipassFetch; const { Minipass: __agentOSMinipass } = __agentOSNpmRequire('minipass'); const __agentOSCreateBinaryMinipass = () => new __agentOSMinipass({ objectMode: false, encoding: null }); const __agentOSCloneBuffer = (buffer) => Buffer.isBuffer(buffer) ? Buffer.from(buffer) : Buffer.from(buffer ?? []); const __agentOSBufferToArrayBuffer = (buffer) => { const bytes = __agentOSCloneBuffer(buffer); return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); }; const __agentOSAttachBufferedBodyMethods = (response, responseBuffer) => { const __agentOSReadBuffer = async () => __agentOSCloneBuffer(responseBuffer); response.__agentOSBufferedBody = __agentOSCloneBuffer(responseBuffer); response.buffer = __agentOSReadBuffer; response.text = async () => (await __agentOSReadBuffer()).toString('utf8'); response.json = async () => JSON.parse(await response.text()); response.arrayBuffer = async () => __agentOSBufferToArrayBuffer(await __agentOSReadBuffer()); response.clone = () => { const clonedBody = __agentOSCreateBinaryMinipass(); const clonedBuffer = __agentOSCloneBuffer(responseBuffer); clonedBody.end(clonedBuffer); const clonedResponse = new __agentOSFetchResponse(clonedBody, { url: response.url, status: response.status, statusText: response.statusText, headers: response.headers, size: response.size, timeout: response.timeout, counter: response.counter, trailer: response.trailer }); return __agentOSAttachBufferedBodyMethods(clonedResponse, clonedBuffer); }; return response; }; const __agentOSNormalizeHeaders = (__agentOSHeaders) => { const normalized = {}; __agentOSHeaders.forEach((value, key) => { if (normalized[key] === undefined) { normalized[key] = value; return; } if (Array.isArray(normalized[key])) { normalized[key].push(value); return; } normalized[key] = [normalized[key], value]; }); return normalized; }; const __agentOSPatchedMinipassFetch = async (input, opts = {}) => { const request = input instanceof __agentOSFetchRequest ? input : new __agentOSFetchRequest(input, opts); const __agentOSController = !request.signal && typeof AbortController === 'function' ? new AbortController() : null; const __agentOSSignal = request.signal ?? __agentOSController?.signal; let __agentOSTimer = null; if (__agentOSController && Number.isFinite(request.timeout) && request.timeout > 0) { __agentOSTimer = setTimeout(() => __agentOSController.abort(new Error(`network timeout at: ${request.url}`)), request.timeout); __agentOSTimer.unref?.(); } try { const requestHeaders = {}; request.headers.forEach((value, key) => { requestHeaders[key] = value; }); const response = await fetch(request.url, { method: request.method, headers: requestHeaders, body: request.body ?? undefined, redirect: request.redirect ?? opts.redirect ?? 'follow', signal: __agentOSSignal, ...(request.body ? { duplex: 'half' } : {}) }); const responseBody = __agentOSCreateBinaryMinipass(); const contentType = String(response.headers.get('content-type') || '').toLowerCase(); const responseBuffer = contentType.includes('json') ? Buffer.from(JSON.stringify(await response.json())) : contentType.startsWith('text/') ? Buffer.from(await response.text()) : Buffer.from(await response.arrayBuffer()); responseBody.end(responseBuffer); return __agentOSAttachBufferedBodyMethods(new __agentOSFetchResponse(responseBody, { url: response.url, status: response.status, statusText: response.statusText, headers: __agentOSNormalizeHeaders(response.headers), size: request.size, timeout: request.timeout, counter: request.counter ?? opts.counter ?? 0, trailer: Promise.resolve(new __agentOSFetchHeaders()) }), responseBuffer); } catch (error) { if (error instanceof Error) { throw error; } throw new __agentOSFetchError(String(error), 'system', error); } finally { if (__agentOSTimer) { clearTimeout(__agentOSTimer); } } }; globalThis.__agentOSPatchedMinipassFetch = __agentOSPatchedMinipassFetch; __agentOSPatchedMinipassFetch.isRedirect = typeof __agentOSMinipassFetch.isRedirect === 'function' ? __agentOSMinipassFetch.isRedirect.bind(__agentOSMinipassFetch) : (code) => code === 301 || code === 302 || code === 303 || code === 307 || code === 308; __agentOSPatchedMinipassFetch.FetchError = __agentOSFetchError; __agentOSPatchedMinipassFetch.Headers = __agentOSFetchHeaders; __agentOSPatchedMinipassFetch.Request = __agentOSFetchRequest; __agentOSPatchedMinipassFetch.Response = __agentOSFetchResponse; __agentOSPatchedMinipassFetch.AbortError = __agentOSAbortError; globalThis._moduleCache[__agentOSMinipassFetchPath] = { exports: __agentOSPatchedMinipassFetch }; __agentOSDebugLog('patched-minipass-fetch', __agentOSMinipassFetchPath); const __agentOSCheckResponsePath = __agentOSNpmRequire.resolve('npm-registry-fetch/lib/check-response.js'); const __agentOSCheckResponse = __agentOSNpmRequire(__agentOSCheckResponsePath); const __agentOSEnsureResponseBodyStream = (response) => { if (!response || (response.body && typeof response.body.on === 'function')) { return response; } const body = __agentOSCreateBinaryMinipass(); const finishWithError = (error) => body.emit('error', error instanceof Error ? error : new Error(String(error))); try { if (typeof response.buffer === 'function') { Promise.resolve(response.buffer()).then((buffer) => body.end(buffer), finishWithError); } else if (Buffer.isBuffer(response.body) || typeof response.body === 'string') { body.end(response.body); } else if (response.body && typeof response.body[Symbol.asyncIterator] === 'function') { (async () => { try { for await (const chunk of response.body) { body.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } body.end(); } catch (error) { finishWithError(error); body.end(); } })(); } else { body.end(); } } catch (error) { finishWithError(error); body.end(); } return new __agentOSFetchResponse(body, response); }; globalThis._moduleCache[__agentOSCheckResponsePath] = { exports: (payload) => { const normalized = { ...payload, res: __agentOSEnsureResponseBodyStream(payload.res) }; __agentOSDebugLog('check-response-body', normalized.res && normalized.res.status, typeof (normalized.res && normalized.res.body), normalized.res && normalized.res.body && typeof normalized.res.body.on, normalized.res && normalized.res.body && normalized.res.body.constructor && normalized.res.body.constructor.name, !!(normalized.res && normalized.res.__agentOSBufferedBody), normalized.res && typeof normalized.res.json); return __agentOSCheckResponse(normalized); } }; __agentOSDebugLog('patched-check-response', __agentOSCheckResponsePath); } catch (error) { __agentOSDebugLog('patch-minipass-fetch-failed', error && error.stack ? error.stack : String(error)); } try { const __agentOSRegistryFetchPath = __agentOSNpmRequire.resolve('npm-registry-fetch'); const __agentOSRegistryFetch = __agentOSNpmRequire(__agentOSRegistryFetchPath); const __agentOSWrapRegistryFetch = (fn) => { const wrapResult = (promise) => Promise.resolve(promise).then((res) => { __agentOSDebugLog('registry-fetch-result', res && res.status, typeof (res && res.body), res && res.body && typeof res.body.on, res && res.body && res.body.constructor && res.body.constructor.name, !!(res && res.__agentOSBufferedBody), res && typeof res.json); return res; }); const wrapped = (uri, opts = {}) => wrapResult(globalThis.__agentOSPatchedMinipassFetch(uri, { method: opts.method, headers: opts.headers, body: opts.body, redirect: opts.redirect, signal: opts.signal, timeout: opts.timeout, size: opts.size, counter: opts.counter })); if (typeof fn.json === 'function') { wrapped.json = (uri, opts = {}) => wrapped(uri, opts).then((res) => res.json()); } if (fn.json && typeof fn.json.stream === 'function') { wrapped.json = wrapped.json || {}; wrapped.json.stream = (uri, path, opts = {}) => fn.json.stream(uri, path, { ...opts, agent: false }); } if (typeof fn.pickRegistry === 'function') { wrapped.pickRegistry = fn.pickRegistry.bind(fn); } if (typeof fn.getAuth === 'function') { wrapped.getAuth = fn.getAuth.bind(fn); } return wrapped; }; globalThis._moduleCache[__agentOSRegistryFetchPath] = { exports: __agentOSWrapRegistryFetch(__agentOSRegistryFetch) }; __agentOSDebugLog('patched-npm-registry-fetch', __agentOSRegistryFetchPath); } catch (error) { __agentOSDebugLog('patch-npm-registry-fetch-failed', error && error.stack ? error.stack : String(error)); }"; + match cli.command_name.as_str() { + "npx" => format!( + "{debug_preamble} {display_stub} {registry_fetch_stub} process.argv[1] = require.resolve({npm_cli}); process.argv.splice(2, 0, 'exec'); __agentOSDebugLog('argv', JSON.stringify(process.argv), 'cwd', process.cwd()); (async () => {{ const pkg = require({package_json}); if (process.argv.includes('--version') || process.argv.includes('-v')) {{ __agentOSDebugLog('version-shortcut'); console.log(pkg.version); __agentOSFinish(0); return; }} const Npm = require({npm_main}); const npm = new Npm(); __agentOSDebugLog('before-load'); const loaded = await npm.load(); __agentOSDebugLog('after-load', loaded && loaded.command, JSON.stringify(loaded && loaded.args)); if (!loaded.exec) {{ __agentOSDebugLog('no-exec'); __agentOSFinish(); return; }} if (!loaded.command) {{ __agentOSDebugLog('no-command'); const {{ output }} = require('proc-log'); output.standard(npm.usage); __agentOSFinish(1); return; }} __agentOSDebugLog('before-exec', loaded.command, JSON.stringify(loaded.args)); await npm.exec(loaded.command, loaded.args); __agentOSDebugLog('after-exec', __agentOSResolveExitCode()); __agentOSFinish(); }})().catch((error) => {{ if (__agentOSIsProcessExitError(error)) {{ __agentOSDebugLog('process-exit-error', __agentOSResolveExitCode(error.code)); __agentOSFinish(error.code); return; }} console.error(error && error.stack ? error.stack : String(error)); __agentOSFinish(error && typeof error === 'object' && Number.isFinite(error.exitCode) ? error.exitCode : 1); }});", + debug_preamble = debug_preamble, + display_stub = display_stub, + registry_fetch_stub = registry_fetch_stub.replace( + "__AGENTOS_NPM_MAIN__", + &serde_json::to_string(&guest_npm_main) + .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")), + ), + npm_main = serde_json::to_string(&guest_npm_main) + .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")), + npm_cli = serde_json::to_string(&guest_npm_cli) + .unwrap_or_else(|_| format!("\"{guest_npm_cli}\"")), + package_json = serde_json::to_string(&guest_package_json) + .unwrap_or_else(|_| format!("\"{guest_package_json}\"")), + ), + _ => format!( + "{debug_preamble} {display_stub} {registry_fetch_stub} __agentOSDebugLog('argv', JSON.stringify(process.argv), 'cwd', process.cwd()); (async () => {{ const pkg = require({package_json}); if (process.argv.includes('--version') || process.argv.includes('-v')) {{ __agentOSDebugLog('version-shortcut'); console.log(pkg.version); __agentOSFinish(0); return; }} const Npm = require({npm_main}); const npm = new Npm(); __agentOSDebugLog('before-load'); const loaded = await npm.load(); __agentOSDebugLog('after-load', loaded && loaded.command, JSON.stringify(loaded && loaded.args)); if (!loaded.exec) {{ __agentOSDebugLog('no-exec'); __agentOSFinish(); return; }} if (!loaded.command) {{ __agentOSDebugLog('no-command'); const {{ output }} = require('proc-log'); output.standard(npm.usage); __agentOSFinish(1); return; }} __agentOSDebugLog('before-exec', loaded.command, JSON.stringify(loaded.args)); await npm.exec(loaded.command, loaded.args); __agentOSDebugLog('after-exec', __agentOSResolveExitCode()); __agentOSFinish(); }})().catch((error) => {{ if (__agentOSIsProcessExitError(error)) {{ __agentOSDebugLog('process-exit-error', __agentOSResolveExitCode(error.code)); __agentOSFinish(error.code); return; }} console.error(error && error.stack ? error.stack : String(error)); __agentOSFinish(error && typeof error === 'object' && Number.isFinite(error.exitCode) ? error.exitCode : 1); }});", + debug_preamble = debug_preamble, + display_stub = display_stub, + registry_fetch_stub = registry_fetch_stub.replace( + "__AGENTOS_NPM_MAIN__", + &serde_json::to_string(&guest_npm_main) + .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")), + ), + npm_main = serde_json::to_string(&guest_npm_main) + .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")), + package_json = serde_json::to_string(&guest_package_json) + .unwrap_or_else(|_| format!("\"{guest_package_json}\"")), + ), } - let mut synced_file_times = BTreeMap::new(); - sync_host_directory_tree_to_kernel_inner( - vm, - &normalized_host_root, - &normalized_host_root, - &normalized_guest_root, - &mut synced_file_times, - ) } -fn sync_host_directory_tree_to_kernel_inner( +fn rewrite_javascript_shebang_request( vm: &mut VmState, - host_root: &Path, - current_host_dir: &Path, - guest_root: &str, - synced_file_times: &mut BTreeMap<(u64, u64), (u64, u64)>, -) -> Result<(), SidecarError> { - let entries = match fs::read_dir(current_host_dir) { - Ok(entries) => entries, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), - Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => { - // Host dirs the sidecar user cannot read (e.g. root-owned - // /lost+found under a host-root mount) are skipped rather than - // failing the whole shadow sync; the guest just won't see them. - tracing::warn!( - path = %current_host_dir.display(), - "skipping unreadable host shadow directory" - ); - return Ok(()); - } - Err(error) => { - return Err(SidecarError::Io(format!( - "failed to read host shadow directory {}: {error}", - current_host_dir.display() + resolved: &ResolvedChildProcessExecution, + request: &mut JavascriptChildProcessSpawnRequest, +) -> Result { + const MAX_SHEBANG_LINE_BYTES: usize = 256; + + if !matches!(resolved.runtime, GuestRuntimeKind::WebAssembly) { + return Ok(false); + } + let Some(script_path) = resolved + .env + .get("AGENTOS_GUEST_ENTRYPOINT") + .filter(|path| path.starts_with('/')) + .map(|path| normalize_path(path)) + else { + return Ok(false); + }; + let is_registered_command = vm + .command_guest_paths + .values() + .any(|path| normalize_path(path) == script_path); + if !is_registered_command { + let stat = vm.kernel.stat(&script_path).map_err(kernel_error)?; + if stat.is_directory || stat.mode & 0o111 == 0 { + return Err(SidecarError::Execution(format!( + "EACCES: permission denied, execute '{script_path}'" ))); } + } + let header = vm + .kernel + .pread_file(&script_path, 0, MAX_SHEBANG_LINE_BYTES + 1) + .map_err(kernel_error)?; + let Some((command, args)) = + parse_javascript_shebang(&script_path, &header, &resolved.execution_args)? + else { + return Ok(false); }; + request.command = command; + request.args = args; + request.options.shell = false; + Ok(true) +} - for entry in entries { - let entry = entry.map_err(|error| { - SidecarError::Io(format!( - "failed to read host shadow entry in {}: {error}", - current_host_dir.display() - )) - })?; - let host_path = entry.path(); - let file_type = entry.file_type().map_err(|error| { - SidecarError::Io(format!( - "failed to stat host shadow entry {}: {error}", - host_path.display() - )) - })?; - let relative_path = host_path - .strip_prefix(host_root) - .map_err(|error| { - SidecarError::InvalidState(format!( - "failed to relativize host shadow path {} against {}: {error}", - host_path.display(), - host_root.display() - )) - })? - .to_string_lossy() - .replace('\\', "/"); - let guest_path = if guest_root == "/" { - normalize_path(&format!("/{relative_path}")) - } else { - normalize_path(&format!( - "{}/{}", - guest_root.trim_end_matches('/'), - relative_path - )) - }; +fn parse_javascript_shebang( + script_path: &str, + header: &[u8], + execution_args: &[String], +) -> Result)>, SidecarError> { + const MAX_SHEBANG_LINE_BYTES: usize = 256; - if should_skip_shadow_sync_path(vm, &guest_path) { - continue; - } + if !header.starts_with(b"#!") { + return Ok(None); + } - if file_type.is_dir() { - let metadata = entry.metadata().map_err(|error| { - SidecarError::Io(format!( - "failed to read host shadow metadata {}: {error}", - host_path.display() + let line_end = match header.iter().position(|byte| *byte == b'\n') { + Some(index) if index > MAX_SHEBANG_LINE_BYTES => { + return Err(SidecarError::Execution(format!( + "ENOEXEC: shebang line exceeds {MAX_SHEBANG_LINE_BYTES} bytes: {script_path}" + ))); + } + Some(index) => index, + None if header.len() > MAX_SHEBANG_LINE_BYTES => { + return Err(SidecarError::Execution(format!( + "ENOEXEC: shebang line exceeds {MAX_SHEBANG_LINE_BYTES} bytes: {script_path}" + ))); + } + None => header.len(), + }; + let line = header[2..line_end] + .strip_suffix(b"\r") + .unwrap_or(&header[2..line_end]); + let text = std::str::from_utf8(line).map_err(|_| { + SidecarError::Execution(format!("ENOEXEC: invalid shebang line: {script_path}")) + })?; + let text = text.trim_start_matches(|ch: char| ch.is_ascii_whitespace()); + let (interpreter, optional_arg) = text + .find(|ch: char| ch.is_ascii_whitespace()) + .map(|index| { + ( + &text[..index], + text[index..].trim_matches(|ch: char| ch.is_ascii_whitespace()), + ) + }) + .map(|(interpreter, optional_arg)| { + ( + interpreter, + (!optional_arg.is_empty()).then_some(optional_arg), + ) + }) + .unwrap_or((text, None)); + if interpreter.is_empty() { + return Err(SidecarError::Execution(format!( + "ENOEXEC: invalid shebang line: {script_path}" + ))); + } + let (command, mut interpreter_args) = if matches!(interpreter, "/usr/bin/env" | "/bin/env") { + let optional_arg = optional_arg.ok_or_else(|| { + SidecarError::Execution(format!( + "ENOENT: missing interpreter after {interpreter} in shebang: {script_path}" + )) + })?; + if let Some(split_string) = optional_arg + .strip_prefix("-S") + .filter(|rest| rest.starts_with(|ch: char| ch.is_ascii_whitespace())) + { + let mut words = shlex::split(split_string.trim()).ok_or_else(|| { + SidecarError::Execution(format!( + "ENOEXEC: invalid /usr/bin/env -S quoting in shebang: {script_path}" )) })?; - if !is_shadow_bootstrap_dir(&guest_path) - && !vm.kernel.exists(&guest_path).unwrap_or(false) + if words.is_empty() { + return Err(SidecarError::Execution(format!( + "ENOENT: missing interpreter after /usr/bin/env -S in shebang: {script_path}" + ))); + } + let command = words.remove(0); + (command, words) + } else { + if optional_arg.starts_with('-') + || optional_arg.chars().any(|ch| ch.is_ascii_whitespace()) { - vm.kernel.mkdir(&guest_path, true).map_err(|error| { - SidecarError::InvalidState(format!( - "failed to sync host shadow directory {} to guest {}: {}", - host_path.display(), - guest_path, - kernel_error(error) - )) - })?; - vm.kernel - .chmod(&guest_path, host_shadow_mode(&metadata)) - .map_err(|error| { - SidecarError::InvalidState(format!( - "failed to sync host shadow directory mode {} to guest {}: {}", - host_path.display(), - guest_path, - kernel_error(error) - )) - })?; + return Err(SidecarError::Execution(format!( + "ENOEXEC: /usr/bin/env shebang arguments require -S: {script_path}" + ))); } - sync_host_directory_tree_to_kernel_inner( - vm, - host_root, - &host_path, - guest_root, - synced_file_times, - )?; - continue; + (optional_arg.to_owned(), Vec::new()) } + } else { + ( + interpreter.to_owned(), + optional_arg + .map(|arg| vec![arg.to_owned()]) + .unwrap_or_default(), + ) + }; + interpreter_args.push(script_path.to_owned()); + interpreter_args.extend(execution_args.iter().cloned()); + Ok(Some((command, interpreter_args))) +} - if file_type.is_file() { - let metadata = entry.metadata().map_err(|error| { - SidecarError::Io(format!( - "failed to read host shadow metadata {}: {error}", - host_path.display() - )) - })?; - let timestamp_key = (metadata.dev(), metadata.ino()); - let (atime_ms, mtime_ms) = - *synced_file_times.entry(timestamp_key).or_insert_with(|| { - ( - metadata_time_ms(metadata.atime(), metadata.atime_nsec()), - metadata_time_ms(metadata.mtime(), metadata.mtime_nsec()), - ) - }); - let desired_mode = host_shadow_mode(&metadata); - // Fast path: skip the expensive re-read + re-write when the kernel already - // holds a copy of this shadow file that matches on size, mode, and mtime. - // - // Every read-side fs op (exists/stat/readFile/...) triggers a full - // shadow-tree reconciliation walk. Without this skip the walk re-reads every - // file's bytes from the host and re-writes them into the kernel VFS on every - // op -- O(whole tree) per op, and super-linear as the VM's shadow grows, - // which is a dominant source of session-creation/runtime latency on - // populated VMs. - // - // This is a (size, mode, mtime) quick-check, the same heuristic rsync uses - // by default. It needs no separate cache to invalidate -- it compares against - // the kernel's own stat, so a kernel reset (e.g. a layer swap) or any host - // change that moves size/mode/mtime forces a resync. Limitation: mtime is - // compared at the millisecond granularity the kernel stores (utimes truncates - // to ms), so a host-side rewrite that preserves byte length AND mode AND lands - // in the same wall-clock millisecond can be skipped and leave stale bytes. - // That window is sub-millisecond same-length edits; if it ever matters here, - // upgrade this to a content digest (or full-precision mtime) for files whose - // mtime is within the last few ms of `now`. - if let Ok(existing) = vm.kernel.lstat(&guest_path) { - if !existing.is_directory - && !existing.is_symbolic_link - && existing.size == metadata.len() - && (existing.mode & 0o7777) == (desired_mode & 0o7777) - && existing.mtime_ms == mtime_ms - { - continue; - } - } - let bytes = match read_host_shadow_file(&host_path, desired_mode) { - Ok(bytes) => bytes, - // The host entry vanished between the walk and the read - // (short-lived files churn constantly — editor swap files, - // temp files). Skipping matches native semantics; failing - // here would poison EVERY subsequent fs op on the VM. - Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, - // Same tolerance for entries the sidecar user cannot read - // (root-owned files under a host-root mount): skip them - // rather than poisoning the whole sync. - Err(error) - if error.kind() == std::io::ErrorKind::PermissionDenied - || error.raw_os_error() == Some(libc::EPERM) => - { - tracing::warn!( - path = %host_path.display(), - "skipping unreadable host shadow file" - ); - continue; - } - Err(error) => { - return Err(SidecarError::Io(format!( - "failed to read host shadow file {}: {error}", - host_path.display() - ))); - } - }; - match vm.kernel.write_file(&guest_path, bytes) { - Ok(()) => {} - // ENOENT here means the guest-side path cannot currently - // receive the write (e.g. it is a symlink whose target was - // just unlinked by the guest — vim's swap-file dance). The - // entry is mid-churn; skip it rather than failing the VM. - Err(error) if error.code() == "ENOENT" => continue, - Err(error) => { - return Err(SidecarError::InvalidState(format!( - "failed to sync host shadow file {} to guest {}: {}", - host_path.display(), - guest_path, - kernel_error(error) - ))); - } +#[cfg(test)] +mod javascript_shebang_tests { + use super::parse_javascript_shebang; + + fn strings(values: &[&str]) -> Vec { + values.iter().map(|value| (*value).to_owned()).collect() + } + + #[test] + fn preserves_linux_optional_argument_and_crlf() { + let parsed = parse_javascript_shebang( + "/workspace/test.sh", + b"#!/bin/sh -e -x\r\necho ignored", + &strings(&["one", "two"]), + ) + .expect("parse direct shebang") + .expect("shebang should be detected"); + + assert_eq!(parsed.0, "/bin/sh"); + assert_eq!( + parsed.1, + strings(&["-e -x", "/workspace/test.sh", "one", "two"]) + ); + } + + #[test] + fn parses_env_and_quoted_env_split_strings() { + let env = parse_javascript_shebang("/workspace/env.sh", b"#!/usr/bin/env sh\n", &[]) + .expect("parse env shebang") + .expect("env shebang should be detected"); + assert_eq!(env, (String::from("sh"), strings(&["/workspace/env.sh"]))); + + let env_split = parse_javascript_shebang( + "/workspace/env-s.sh", + b"#! /usr/bin/env -S sh -c 'printf \"%s\" \"$1\"' shell\n", + &strings(&["tail"]), + ) + .expect("parse env -S shebang") + .expect("env -S shebang should be detected"); + assert_eq!( + env_split, + ( + String::from("sh"), + strings(&[ + "-c", + "printf \"%s\" \"$1\"", + "shell", + "/workspace/env-s.sh", + "tail" + ]) + ) + ); + } + + #[test] + fn rejects_invalid_or_unbounded_shebangs() { + assert!( + parse_javascript_shebang("/workspace/plain", b"plain text", &[]) + .expect("parse plain file") + .is_none() + ); + + let missing = parse_javascript_shebang("/workspace/missing", b"#!/usr/bin/env\n", &[]) + .expect_err("env without interpreter must fail"); + assert!(missing.to_string().contains("ENOENT")); + + let malformed = parse_javascript_shebang( + "/workspace/malformed", + b"#!/usr/bin/env -S sh 'unterminated\n", + &[], + ) + .expect_err("unterminated env -S quote must fail"); + assert!(malformed.to_string().contains("ENOEXEC")); + + let overlong = format!("#!/{}\n", "x".repeat(257)); + let too_long = parse_javascript_shebang("/workspace/long", overlong.as_bytes(), &[]) + .expect_err("overlong shebang must fail"); + assert!(too_long.to_string().contains("ENOEXEC")); + } +} + +fn resolve_guest_command_entrypoint( + vm: &VmState, + guest_cwd: &str, + command: &str, + path_env: Option<&str>, +) -> Option { + if !is_path_like_specifier(command) { + if let Some(entrypoint) = vm.command_guest_paths.get(command) { + return Some(entrypoint.clone()); + } + + for search_dir in guest_command_search_dirs(vm, guest_cwd, path_env) { + let candidate = normalize_path(&format!("{search_dir}/{command}")); + if let Some(entrypoint) = resolve_guest_command_path_candidate(vm, &candidate) { + return Some(entrypoint); } - vm.kernel - .chmod(&guest_path, desired_mode) - .map_err(|error| { - SidecarError::InvalidState(format!( - "failed to sync host shadow file mode {} to guest {}: {}", - host_path.display(), - guest_path, - kernel_error(error) - )) - })?; - vm.kernel - .utimes(&guest_path, atime_ms, mtime_ms) - .map_err(|error| { - SidecarError::InvalidState(format!( - "failed to sync host shadow file times {} to guest {}: {}", - host_path.display(), - guest_path, - kernel_error(error) - )) - })?; - continue; } - if file_type.is_symlink() { - let target = match fs::read_link(&host_path) { - Ok(target) => target, - Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue, - Err(error) => { - return Err(SidecarError::Io(format!( - "failed to read host shadow symlink {}: {error}", - host_path.display() - ))); - } - }; - replace_kernel_symlink(vm, &guest_path, &target.to_string_lossy())?; + return None; + } + + let normalized = resolve_path_like_guest_specifier(guest_cwd, command); + resolve_guest_command_path_candidate(vm, &normalized).or_else(|| { + // Some guest shells materialize PATH lookups into absolute candidate paths. + // If that path points into a searched directory but does not exist, fall + // back to the command basename so the sidecar can remap VM command packages. + let parent_dir = Path::new(&normalized).parent()?.to_str()?; + if !guest_command_search_dirs(vm, guest_cwd, path_env) + .iter() + .any(|search_dir| normalize_path(search_dir) == normalize_path(parent_dir)) + { + return None; } + + let file_name = Path::new(&normalized).file_name()?.to_str()?; + vm.command_guest_paths.get(file_name).cloned() + }) +} + +/// Resolve the literal pathname supplied to execve(2). Unlike the spawnp +/// resolver above, this never searches PATH and never substitutes a registered +/// command merely because a missing pathname has the same basename. +fn resolve_exact_guest_command_entrypoint( + vm: &VmState, + guest_cwd: &str, + command: &str, +) -> Option { + if !is_path_like_specifier(command) { + return None; } - Ok(()) + let normalized = resolve_path_like_guest_specifier(guest_cwd, command); + if let Some(name) = registered_command_name_for_path(vm, &normalized) { + return vm.command_guest_paths.get(&name).cloned(); + } + if vm + .kernel + .exists(&normalized) + .ok() + .is_some_and(|exists| exists) + { + // execve follows the final symlink. Returning the real path also lets + // projected package commands select their actual JS/WASM entrypoint. + return vm + .kernel + .realpath(&normalized) + .ok() + .map(|path| normalize_path(&path)) + .or(Some(normalized)); + } + + resolve_vm_guest_path_to_host(vm, &normalized) + .is_file() + .then_some(normalized) } -fn replace_kernel_symlink( - vm: &mut VmState, - guest_path: &str, - target: &str, -) -> Result<(), SidecarError> { - if vm.kernel.symlink(target, guest_path).is_ok() { - return Ok(()); +fn registered_command_name_for_path(vm: &VmState, path: &str) -> Option { + let normalized = normalize_path(path); + let name = ["/bin/", "/usr/bin/", "/usr/local/bin/", "/opt/agentos/bin/"] + .into_iter() + .find_map(|prefix| normalized.strip_prefix(prefix)) + .or_else(|| { + normalized + .strip_prefix("/__secure_exec/commands/") + .and_then(|suffix| suffix.rsplit('/').next()) + })?; + (!name.is_empty() && !name.contains('/') && vm.kernel.commands().contains_key(name)) + .then(|| name.to_owned()) +} + +const LINUX_BINPRM_BUF_SIZE: usize = 256; +const LINUX_MAX_INTERPRETER_DEPTH: usize = 4; + +struct LinuxShebang { + interpreter: String, + optional_argument: Option, +} + +fn parse_linux_shebang(header: &[u8], path: &str) -> Result, SidecarError> { + if !header.starts_with(b"#!") { + return Ok(None); } - if let Ok(existing_target) = vm.kernel.read_link(guest_path) { - if existing_target == target { - return Ok(()); - } + let payload = &header[2..]; + let newline = payload.iter().position(|byte| *byte == b'\n'); + let line = newline.map_or(payload, |index| &payload[..index]); + let line_end = line + .iter() + .rposition(|byte| !matches!(*byte, b' ' | b'\t')) + .map(|index| index + 1) + .ok_or_else(|| SidecarError::Kernel(format!("ENOEXEC: invalid shebang line: {path}")))?; + let line = &line[..line_end]; + let interpreter_start = line + .iter() + .position(|byte| !matches!(*byte, b' ' | b'\t')) + .ok_or_else(|| SidecarError::Kernel(format!("ENOEXEC: invalid shebang line: {path}")))?; + let interpreter_tail = &line[interpreter_start..]; + let separator = interpreter_tail + .iter() + .position(|byte| matches!(*byte, b' ' | b'\t')); + if newline.is_none() && header.len() >= LINUX_BINPRM_BUF_SIZE && separator.is_none() { + return Err(SidecarError::Kernel(format!( + "ENOEXEC: shebang interpreter path exceeds the Linux header limit: {path}" + ))); } - let _ = vm.kernel.remove_file(guest_path); - let _ = vm.kernel.remove_dir(guest_path); - vm.kernel - .symlink(target, guest_path) - .map_err(kernel_error)?; - Ok(()) + let interpreter_end = separator.unwrap_or(interpreter_tail.len()); + let interpreter = std::str::from_utf8(&interpreter_tail[..interpreter_end]) + .map_err(|_| SidecarError::Kernel(format!("ENOEXEC: invalid shebang line: {path}")))?; + if interpreter.is_empty() { + return Err(SidecarError::Kernel(format!( + "ENOEXEC: invalid shebang line: {path}" + ))); + } + let optional_argument = separator + .map(|index| &interpreter_tail[index..]) + .map(|value| { + let start = value + .iter() + .position(|byte| !matches!(*byte, b' ' | b'\t')) + .unwrap_or(value.len()); + let end = value + .iter() + .rposition(|byte| !matches!(*byte, b' ' | b'\t')) + .map(|index| index + 1) + .unwrap_or(start); + &value[start..end] + }) + .filter(|value| !value.is_empty()) + .map(|value| { + std::str::from_utf8(value) + .map(str::to_owned) + .map_err(|_| SidecarError::Kernel(format!("ENOEXEC: invalid shebang line: {path}"))) + }) + .transpose()?; + + Ok(Some(LinuxShebang { + interpreter: interpreter.to_owned(), + optional_argument, + })) } -fn host_shadow_mode(metadata: &fs::Metadata) -> u32 { - metadata.permissions().mode() & 0o7777 +struct SpawnPathCandidate { + lookup_path: String, + script_argument: String, } -/// Reads a shadow-root file back into the kernel even when guest-visible mode -/// bits make it unreadable for the host user. The sidecar is the kernel for -/// this tree, so guest permission bits (for example a 0o200 write-only file -/// produced by `chmod` plus a shell append redirect) must not break the -/// exit-time shadow sync. The original mode is restored after the read. -fn read_host_shadow_file(host_path: &Path, mode: u32) -> std::io::Result> { - match fs::read(host_path) { - Ok(bytes) => Ok(bytes), - Err(error) if error.kind() == std::io::ErrorKind::PermissionDenied => { - fs::set_permissions(host_path, fs::Permissions::from_mode(mode | 0o400))?; - let result = fs::read(host_path); - fs::set_permissions(host_path, fs::Permissions::from_mode(mode))?; - result +fn spawn_request_guest_cwd( + parent_guest_cwd: &str, + request: &JavascriptChildProcessSpawnRequest, +) -> String { + request + .options + .cwd + .as_deref() + .map(|cwd| { + if cwd.starts_with('/') { + normalize_path(cwd) + } else { + normalize_path(&format!("{parent_guest_cwd}/{cwd}")) + } + }) + .unwrap_or_else(|| parent_guest_cwd.to_owned()) +} + +/// Resolve a bare `posix_spawnp` name with the same candidate selection rules +/// as Linux `execvpe`: the caller's PATH is authoritative, empty entries name +/// the current working directory, permission-denied candidates are skipped in +/// case a later entry succeeds, and EACCES wins if every usable candidate was +/// denied. `script_argument` preserves the candidate spelling Linux places in +/// argv when the selected image is a shebang script (notably `name`, rather +/// than `./name`, for an empty PATH entry). +fn resolve_posix_spawn_path_candidate( + vm: &mut VmState, + guest_cwd: &str, + command: &str, + search_path: &str, +) -> Result { + if command.is_empty() { + return Err(SidecarError::Kernel(String::from( + "ENOENT: posix_spawnp command is empty", + ))); + } + + let mut permission_error = None; + for segment in search_path.split(':') { + // PATH entries are literal. Do not trim whitespace: a directory whose + // name starts or ends with a space is valid on Linux. + let script_argument = if segment.is_empty() { + command.to_owned() + } else { + format!("{segment}/{command}") + }; + let lookup_path = if segment.is_empty() { + format!("./{command}") + } else { + script_argument.clone() + }; + match vm.kernel.validate_executable_path(&lookup_path, guest_cwd) { + Ok(_) => { + return Ok(SpawnPathCandidate { + lookup_path, + script_argument, + }); + } + Err(error) if error.code() == "EACCES" => permission_error = Some(error), + Err(error) if matches!(error.code(), "ENOENT" | "ENOTDIR") => {} + Err(error) => return Err(kernel_error(error)), } - Err(error) => Err(error), } -} -fn metadata_time_ms(seconds: i64, nanos: i64) -> u64 { - let seconds = seconds.max(0) as u64; - let nanos = nanos.max(0) as u64; - seconds - .saturating_mul(1_000) - .saturating_add(nanos / 1_000_000) + if let Some(error) = permission_error { + Err(kernel_error(error)) + } else { + Err(SidecarError::Kernel(format!( + "ENOENT: posix_spawnp command not found in PATH: {command}" + ))) + } } -fn is_shadow_bootstrap_dir(path: &str) -> bool { - matches!( - path, - "/dev" - | "/proc" - | "/tmp" - | "/bin" - | "/lib" - | "/sbin" - | "/boot" - | "/etc" - | "/root" - | "/run" - | "/srv" - | "/sys" - | "/opt" - | "/mnt" - | "/media" - | "/home" - | "/home/agentos" - | "/usr" - | "/usr/bin" - | "/usr/games" - | "/usr/include" - | "/usr/lib" - | "/usr/libexec" - | "/usr/man" - | "/usr/local" - | "/usr/local/bin" - | "/usr/sbin" - | "/usr/share" - | "/usr/share/man" - | "/var" - | "/var/cache" - | "/var/empty" - | "/var/lib" - | "/var/lock" - | "/var/log" - | "/var/run" - | "/var/spool" - | "/var/tmp" - | "/etc/agentos" - | "/workspace" +/// Finish pathname and shebang resolution after POSIX file actions have run. +/// `posix_spawnp` searches PATH exactly once in this staged child state, then +/// follows the same recursive shebang rules as literal `posix_spawn`. +fn resolve_posix_spawn_program( + vm: &mut VmState, + parent_guest_cwd: &str, + request: &mut JavascriptChildProcessSpawnRequest, +) -> Result<(), SidecarError> { + if request.options.spawn_exact_path { + return resolve_spawn_shebang(vm, parent_guest_cwd, request, None); + } + + let Some(search_path) = request.options.spawn_search_path.clone() else { + // Ordinary Node child_process resolution retains its existing package + // and runtime-command behavior. Only proc_spawn_v4/posix_spawnp sends + // spawnSearchPath and requests Linux execvpe semantics here. + return Ok(()); + }; + + if is_path_like_specifier(&request.command) { + // POSIX specifies that a name containing '/' bypasses PATH search. + request.options.spawn_exact_path = true; + request.options.spawn_search_path = None; + return resolve_spawn_shebang(vm, parent_guest_cwd, request, None); + } + + let guest_cwd = spawn_request_guest_cwd(parent_guest_cwd, request); + let candidate = + resolve_posix_spawn_path_candidate(vm, &guest_cwd, &request.command, &search_path)?; + request.command = candidate.lookup_path; + request.options.spawn_exact_path = true; + request.options.spawn_search_path = None; + resolve_spawn_shebang( + vm, + parent_guest_cwd, + request, + Some(candidate.script_argument), ) } -#[cfg(test)] -mod shadow_sync_tests { - use super::{is_protected_agentos_shadow_sync_path, is_shadow_bootstrap_dir}; +fn resolve_spawn_shebang( + vm: &mut VmState, + parent_guest_cwd: &str, + request: &mut JavascriptChildProcessSpawnRequest, + mut initial_script_argument: Option, +) -> Result<(), SidecarError> { + let guest_cwd = spawn_request_guest_cwd(parent_guest_cwd, request); + let mut interpreter_depth = 0; - #[test] - fn shadow_bootstrap_sync_skips_virtual_home_tree() { - assert!(is_shadow_bootstrap_dir("/home")); - assert!(is_shadow_bootstrap_dir("/home/agentos")); - } + loop { + let script_argument = initial_script_argument + .take() + .unwrap_or_else(|| request.command.clone()); + let resolved_path = vm + .kernel + .validate_executable_path(&request.command, &guest_cwd) + .map_err(kernel_error)?; + if registered_command_name_for_path(vm, &resolved_path).is_some() { + return Ok(()); + } - #[test] - fn protected_agentos_paths_are_not_shadow_synced() { - assert!(is_protected_agentos_shadow_sync_path("/etc/agentos")); - assert!(is_protected_agentos_shadow_sync_path( - "/etc/agentos/instructions.md" - )); - assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos-copy")); - assert!(!is_protected_agentos_shadow_sync_path("/etc/agentos.md")); + let header = vm + .kernel + .pread_file(&resolved_path, 0, LINUX_BINPRM_BUF_SIZE) + .map_err(kernel_error)?; + if header.starts_with(b"\0asm") { + return Ok(()); + } + let Some(shebang) = parse_linux_shebang(&header, &resolved_path)? else { + return Err(SidecarError::Kernel(format!( + "ENOEXEC: exec format error: {resolved_path}" + ))); + }; + if interpreter_depth >= LINUX_MAX_INTERPRETER_DEPTH { + return Err(SidecarError::Kernel(format!( + "ELOOP: interpreter recursion for {resolved_path} exceeds the Linux limit" + ))); + } + interpreter_depth += 1; + + let mut interpreter_args = Vec::with_capacity(request.args.len() + 2); + if let Some(argument) = shebang.optional_argument { + interpreter_args.push(argument); + } + interpreter_args.push(script_argument); + interpreter_args.append(&mut request.args); + request.command = shebang.interpreter; + request.args = interpreter_args; + // Linux discards the caller-supplied script argv[0]. The final + // interpreter pathname becomes argv[0], including across a nested + // shebang chain. + request.options.argv0 = Some(request.command.clone()); } } -fn is_kernel_owned_shadow_sync_path(path: &str) -> bool { - matches!(path, "/dev" | "/proc" | "/sys") - || path.starts_with("/dev/") - || path.starts_with("/proc/") - || path.starts_with("/sys/") +fn validate_exact_exec_image_format( + vm: &mut VmState, + path: &str, + runtime: &GuestRuntimeKind, +) -> Result<(), SidecarError> { + let header = vm.kernel.pread_file(path, 0, 4).map_err(kernel_error)?; + let valid = exact_exec_image_header_is_valid(runtime, &header); + if valid { + Ok(()) + } else { + Err(SidecarError::InvalidState(format!( + "ENOEXEC: exec format error: {path}" + ))) + } } -pub(crate) fn is_protected_agentos_shadow_sync_path(path: &str) -> bool { - path == "/etc/agentos" || path.starts_with("/etc/agentos/") +fn exact_exec_image_header_is_valid(runtime: &GuestRuntimeKind, header: &[u8]) -> bool { + match runtime { + GuestRuntimeKind::WebAssembly => header == b"\0asm", + // Linux recognizes scripts through their shebang, not their filename + // extension. Runtime resolution has already checked that the shebang + // selects the corresponding supported interpreter. + GuestRuntimeKind::JavaScript | GuestRuntimeKind::Python => header.starts_with(b"#!"), + } } -fn should_skip_shadow_sync_path(vm: &VmState, guest_path: &str) -> bool { - is_kernel_owned_shadow_sync_path(guest_path) - || is_protected_agentos_shadow_sync_path(guest_path) - // agentOS package content is served guest-native from read-only tar - // mounts; it is already present in the guest and cannot be written, so a - // host->guest shadow sync would fail with EROFS. Skip it. - || guest_path_is_within_agentos_package_mount(vm, guest_path) - || host_mount_path_for_guest_path_from_mounts(&vm.configuration.mounts, guest_path) - .is_some() -} +fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&str>) -> Vec { + let mut search_dirs = Vec::new(); + let mut seen = BTreeSet::new(); -fn resolve_path_like_guest_specifier(cwd: &str, specifier: &str) -> String { - if specifier.starts_with("file://") { - normalize_path(specifier.trim_start_matches("file://")) - } else if specifier.starts_with("file:") { - normalize_path(specifier.trim_start_matches("file:")) - } else if specifier.starts_with('/') { - normalize_path(specifier) - } else { - normalize_path(&format!("{cwd}/{specifier}")) + if let Some(path) = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)) { + for segment in path.split(':') { + let trimmed = segment.trim(); + if trimmed.is_empty() { + continue; + } + let normalized = if trimmed.starts_with('/') { + normalize_path(trimmed) + } else { + normalize_path(&format!("{guest_cwd}/{trimmed}")) + }; + if seen.insert(normalized.clone()) { + search_dirs.push(normalized); + } + } } -} -fn guest_entrypoint_for_specifier(cwd: &str, specifier: &str) -> Option { - is_path_like_specifier(specifier).then(|| resolve_path_like_guest_specifier(cwd, specifier)) + for fallback in ["/bin", "/usr/bin", "/usr/local/bin"] { + let normalized = String::from(fallback); + if seen.insert(normalized.clone()) { + search_dirs.push(normalized); + } + } + + search_dirs } -fn is_node_runtime_command(command: &str) -> bool { - matches!(command, "node" | "npm" | "npx") - || Path::new(command) +fn resolve_guest_command_path_candidate(vm: &VmState, candidate: &str) -> Option { + if candidate.starts_with(&format!("{}/", crate::package_projection::OPT_AGENTOS_BIN)) { + if let Ok(realpath) = vm.kernel.realpath(candidate) { + return Some(normalize_path(&realpath)); + } + } + + if candidate.starts_with("/bin/") + || candidate.starts_with("/usr/bin/") + || candidate.starts_with("/usr/local/bin/") + || candidate.starts_with(&format!("{}/", crate::package_projection::OPT_AGENTOS_BIN)) + || candidate.starts_with("/__secure_exec/commands/") + { + if let Some(file_name) = Path::new(candidate) .file_name() .and_then(|name| name.to_str()) - .is_some_and(|name| matches!(name, "node" | "npm" | "npx")) -} + { + if let Some(guest_entrypoint) = vm.command_guest_paths.get(file_name) { + return Some(guest_entrypoint.clone()); + } + } + } -fn python_command_base_name(command: &str) -> &str { - Path::new(command) - .file_name() - .and_then(|name| name.to_str()) - .unwrap_or(command) -} + if vm + .kernel + .exists(candidate) + .ok() + .is_some_and(|exists| exists) + { + return Some(normalize_path(candidate)); + } -/// `python` / `python3` (and `pip` / `pip3`, which map to `python -m pip`) are -/// served by the embedded Pyodide runtime, mirroring how `node` is served by the -/// embedded V8 runtime. -fn is_python_runtime_command(command: &str) -> bool { - matches!( - python_command_base_name(command), - "python" | "python3" | "pip" | "pip3" - ) + resolve_vm_guest_path_to_host(vm, candidate) + .is_file() + .then(|| normalize_path(candidate)) } -/// Parse a `python` / `pip` command line into a Pyodide execution. Supports the -/// CPython program selectors `-c CODE`, `-m MODULE`, a `SCRIPT` path, `-` / -/// piped stdin programs, and a bare interpreter (interactive REPL). The chosen -/// mode plus `sys.argv` are forwarded to the runner as `AGENTOS_PYTHON_*` control -/// env, which the runner consumes and never exposes in the guest `os.environ`. -fn resolve_python_command_execution( +fn resolve_host_entrypoint_within_vm_host_cwd( vm: &VmState, - command: &str, - args: &[String], - mut env: BTreeMap, - guest_cwd: String, - host_cwd: PathBuf, -) -> Result { - let base_name = python_command_base_name(command); - let is_pip = matches!(base_name, "pip" | "pip3"); + specifier: &str, +) -> Option<(String, String)> { + let candidate = Path::new(specifier); + if !candidate.is_absolute() { + return None; + } - let mut entrypoint = String::new(); - let mut argv: Vec = Vec::new(); - let mut module: Option = None; - let mut stdin_program = false; - let mut interactive = false; - let mut guest_entrypoint: Option = None; + let normalized_entrypoint = normalize_host_path(candidate); + let normalized_host_cwd = normalize_host_path(&vm.host_cwd); + if !path_is_within_root(&normalized_entrypoint, &normalized_host_cwd) { + return None; + } - if is_pip { - module = Some(String::from("pip")); - argv.push(String::from("pip")); - argv.extend(args.iter().cloned()); + let relative = normalized_entrypoint + .strip_prefix(&normalized_host_cwd) + .ok()? + .to_string_lossy() + .replace('\\', "/"); + let guest_entrypoint = if relative.is_empty() { + String::from("/") } else { - // Skip the value-less interpreter flags we can safely ignore so they do - // not get mistaken for a script path. - let mut idx = 0; - while let Some(flag) = args.get(idx) { - match flag.as_str() { - "-B" | "-E" | "-I" | "-O" | "-OO" | "-q" | "-s" | "-S" | "-u" | "-v" | "-b" - | "-d" | "-x" => idx += 1, - _ => break, - } - } - let rest = &args[idx..]; - match rest.first().map(String::as_str) { - Some("-c") => { - entrypoint = rest.get(1).cloned().ok_or_else(|| { - SidecarError::InvalidState(String::from("argument expected for the -c option")) - })?; - argv.push(String::from("-c")); - argv.extend(rest.iter().skip(2).cloned()); - } - Some("-m") => { - let name = rest.get(1).cloned().ok_or_else(|| { - SidecarError::InvalidState(String::from("argument expected for the -m option")) - })?; - module = Some(name); - argv.push(String::from("-m")); - argv.extend(rest.iter().skip(2).cloned()); - } - Some("-") => { - stdin_program = true; - argv.push(String::from("-")); - argv.extend(rest.iter().skip(1).cloned()); - } - Some(spec) if !spec.starts_with('-') => { - let resolved_guest = guest_entrypoint_for_specifier(&guest_cwd, spec) - .unwrap_or_else(|| spec.to_string()); - entrypoint = resolved_guest.clone(); - env.insert(String::from("AGENTOS_PYTHON_FILE"), resolved_guest.clone()); - guest_entrypoint = Some(resolved_guest); - argv.push(spec.to_string()); - argv.extend(rest.iter().skip(1).cloned()); - } - Some(other) => { - return Err(SidecarError::InvalidState(format!( - "unsupported python option: {other}" - ))); - } - None => { - interactive = true; - argv.push(String::new()); - } - } - } + normalize_path(&format!("/{relative}")) + }; + Some(( + guest_entrypoint, + normalized_entrypoint.to_string_lossy().into_owned(), + )) +} + +fn prepare_guest_runtime_env( + vm: &VmState, + env: &mut BTreeMap, + guest_cwd: &str, + host_cwd: &Path, + guest_entrypoint: Option, +) -> Result<(), SidecarError> { + let user = vm.kernel.user_profile(); + let path_mappings = runtime_guest_path_mappings(vm); + let read_paths = expand_host_access_paths( + std::iter::once(vm.cwd.clone()) + .chain( + path_mappings + .iter() + .map(|mapping| PathBuf::from(&mapping.host_path)), + ) + .chain(std::iter::once(host_cwd.to_path_buf())) + .collect::>() + .as_slice(), + ); + let write_paths = dedupe_host_paths( + std::iter::once(vm.cwd.clone()) + .chain(std::iter::once(host_cwd.to_path_buf())) + .chain(runtime_guest_writable_host_paths(vm)) + .collect::>() + .as_slice(), + ); + let allowed_node_builtins = configured_allowed_node_builtins(vm); + let loopback_exempt_ports = configured_loopback_exempt_ports(vm); env.insert( - String::from("AGENTOS_PYTHON_ARGV"), - serde_json::to_string(&argv).unwrap_or_else(|_| String::from("[]")), + String::from("AGENTOS_GUEST_PATH_MAPPINGS"), + serde_json::to_string(&path_mappings).map_err(|error| { + SidecarError::InvalidState(format!("failed to encode guest path mappings: {error}")) + })?, ); - if let Some(module) = &module { - env.insert(String::from("AGENTOS_PYTHON_MODULE"), module.clone()); + env.entry(String::from(EXECUTION_SANDBOX_ROOT_ENV)) + .or_insert_with(|| normalize_host_path(&vm.cwd).to_string_lossy().into_owned()); + env.insert( + String::from("AGENTOS_EXTRA_FS_READ_PATHS"), + serde_json::to_string( + &read_paths + .iter() + .map(|path| path.to_string_lossy().into_owned()) + .collect::>(), + ) + .map_err(|error| { + SidecarError::InvalidState(format!("failed to encode read paths: {error}")) + })?, + ); + env.insert( + String::from("AGENTOS_EXTRA_FS_WRITE_PATHS"), + serde_json::to_string( + &write_paths + .iter() + .map(|path| path.to_string_lossy().into_owned()) + .collect::>(), + ) + .map_err(|error| { + SidecarError::InvalidState(format!("failed to encode write paths: {error}")) + })?, + ); + env.insert( + String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), + serde_json::to_string(&allowed_node_builtins).map_err(|error| { + SidecarError::InvalidState(format!("failed to encode allowed builtins: {error}")) + })?, + ); + // The guest JS host platform drives subtractive global scrubbing in the + // per-execution runtime shim (see prepend_v8_runtime_shim). + env.insert( + String::from("AGENTOS_JS_PLATFORM"), + js_runtime_platform_env(vm).to_owned(), + ); + // Module-resolution mode (omitted when full Node resolution / the default). + if let Some(resolution) = js_runtime_module_resolution_env(vm) { + env.insert( + String::from("AGENTOS_JS_MODULE_RESOLUTION"), + resolution.to_owned(), + ); } - if stdin_program { + // Builtin allow-list gate for the live resolver. Present only when builtins + // should be restricted (non-node platform => deny all; node + explicit + // allow-list => exactly those). Absent => unrestricted (node default). + if let Some(allowlist) = js_runtime_enforced_builtins(vm) { env.insert( - String::from("AGENTOS_PYTHON_STDIN_PROGRAM"), - String::from("1"), + String::from("AGENTOS_JS_BUILTIN_ALLOWLIST"), + serde_json::to_string(&allowlist).map_err(|error| { + SidecarError::InvalidState(format!( + "failed to encode jsRuntime builtin allow-list: {error}" + )) + })?, ); } - if interactive { + // Virtual OS identity (os.cpus/totalmem/freemem/homedir/userInfo/...) now + // rides the typed `guest_runtime` (see `guest_runtime_identity`), exposed to + // the guest as the `__agentOSVirtualOs` structured global by the runtime + // shim — no longer the `AGENTOS_VIRTUAL_OS_*` env vars. + // Virtual process uid/gid now ride the typed `guest_runtime` identity + // (see `guest_runtime_identity`), not the `AGENTOS_VIRTUAL_PROCESS_*` env. + env.entry(String::from("HOME")) + .or_insert_with(|| user.homedir.clone()); + env.entry(String::from("USER")) + .or_insert_with(|| user.username.clone()); + env.entry(String::from("LOGNAME")) + .or_insert_with(|| user.username.clone()); + env.entry(String::from("SHELL")) + .or_insert_with(|| user.shell.clone()); + env.entry(String::from("PATH")).or_insert_with(|| { + vm.guest_env + .get("PATH") + .cloned() + .unwrap_or_else(|| crate::vm::DEFAULT_GUEST_PATH_ENV.to_owned()) + }); + env.entry(String::from("TMPDIR")) + .or_insert_with(|| String::from("/tmp")); + env.insert(String::from("PWD"), guest_cwd.to_owned()); + if !loopback_exempt_ports.is_empty() { env.insert( - String::from("AGENTOS_PYTHON_INTERACTIVE"), - String::from("1"), + String::from(LOOPBACK_EXEMPT_PORTS_ENV), + serde_json::to_string(&loopback_exempt_ports).map_err(|error| { + SidecarError::InvalidState(format!("failed to encode loopback exemptions: {error}")) + })?, ); } - - prepare_guest_runtime_env(vm, &mut env, &guest_cwd, &host_cwd, guest_entrypoint)?; - - Ok(ResolvedChildProcessExecution { - command: String::from(PYTHON_COMMAND), - process_args: std::iter::once(command.to_owned()) - .chain(args.iter().cloned()) - .collect(), - runtime: GuestRuntimeKind::Python, - entrypoint, - execution_args: args.to_vec(), - env, - guest_cwd, - host_cwd, - wasm_permission_tier: None, - tool_command: false, - }) -} - -fn resolve_special_node_cli_invocation( - args: &[String], - env: &mut BTreeMap, -) -> Option<(String, Vec)> { - let first = args.first()?; - match first.as_str() { - "-e" | "--eval" => { - env.insert( - String::from("AGENTOS_NODE_EVAL"), - args.get(1).cloned().unwrap_or_default(), - ); - Some((first.clone(), args.iter().skip(2).cloned().collect())) - } - "-v" | "--version" => { - env.insert( - String::from("AGENTOS_NODE_EVAL"), - String::from("console.log(process.version);"), - ); - Some((String::from("-e"), args.to_vec())) - } - _ => None, + if let Some(guest_entrypoint) = guest_entrypoint { + env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint); } + Ok(()) } -fn node_runtime_command_name(command: &str) -> Option<&str> { - let name = Path::new(command) - .file_name() - .and_then(|name| name.to_str())?; - matches!(name, "node" | "npm" | "npx").then_some(name) -} - -struct ResolvedHostNodeCliEntrypoint { - command_name: String, - guest_root: String, - guest_entrypoint: String, - package_root: PathBuf, +/// Build the typed per-execution JavaScript limits from the per-VM `VmLimits` +/// (sourced from `CreateVmConfig` on the BARE wire). These ride the execution +/// request, not `AGENTOS_*` env vars — see the env-vs-wire rule in +/// `crates/sidecar/CLAUDE.md`. +fn javascript_execution_limits(vm: &VmState) -> JavascriptExecutionLimits { + JavascriptExecutionLimits { + v8_heap_limit_mb: vm.limits.js_runtime.v8_heap_limit_mb, + sync_rpc_wait_timeout_ms: vm.limits.js_runtime.sync_rpc_wait_timeout_ms, + cpu_time_limit_ms: Some(vm.limits.js_runtime.cpu_time_limit_ms), + wall_clock_limit_ms: Some(vm.limits.js_runtime.wall_clock_limit_ms), + import_cache_materialize_timeout_ms: Some( + vm.limits.js_runtime.import_cache_materialize_timeout_ms, + ), + } } -fn resolve_host_node_cli_entrypoint(command: &str) -> Option { - let command_name = node_runtime_command_name(command)?; - if !matches!(command_name, "npm" | "npx") { - return None; +/// Build the typed per-execution guest-runtime identity (virtual `process.*`) +/// from kernel state. Replaces the `AGENTOS_VIRTUAL_PROCESS_{UID,GID,PID,PPID}` +/// env round-trip: the runtime shim reads these from `guest_runtime`, not env. +/// `uid`/`gid` come from the VM user profile (applied to every guest); +/// `pid`/`ppid` are per-process and only set for paths that assigned them. +fn guest_runtime_identity( + vm: &VmState, + virtual_pid: Option, + virtual_ppid: Option, +) -> GuestRuntimeConfig { + let user = vm.kernel.user_profile(); + let resource_limits = vm.kernel.resource_limits(); + let identity = shared_guest_runtime_identity(&user, resource_limits, virtual_pid, virtual_ppid); + GuestRuntimeConfig { + virtual_uid: Some(identity.virtual_uid), + virtual_gid: Some(identity.virtual_gid), + virtual_pid: identity.virtual_pid, + virtual_ppid: identity.virtual_ppid, + virtual_exec_path: None, + os_cpu_count: Some(identity.os_cpu_count), + os_totalmem: Some(identity.os_totalmem), + os_freemem: Some(identity.os_freemem), + os_homedir: Some(identity.os_homedir), + os_hostname: Some(identity.os_hostname), + os_tmpdir: Some(identity.os_tmpdir), + os_type: Some(identity.os_type), + os_release: Some(identity.os_release), + os_version: Some(identity.os_version), + os_machine: Some(identity.os_machine), + os_shell: Some(identity.os_shell), + os_user: Some(identity.os_user), + high_resolution_time: vm + .configuration + .js_runtime + .as_ref() + .is_some_and(|cfg| cfg.high_resolution_time.unwrap_or(false)), + // Userland bundle to bake into the per-sidecar snapshot. The sidecar + // derives this from configured agent packages with `agent.snapshot`. + snapshot_userland_code: vm.configuration.snapshot_userland_code.clone(), } +} - let path = std::env::var_os("PATH")?; - for root in std::env::split_paths(&path) { - let candidate = root.join(command_name); - if !candidate.is_file() { - continue; - } - let entrypoint = candidate.canonicalize().ok().unwrap_or(candidate); - let package_root = entrypoint.parent()?.parent()?.to_path_buf(); - let guest_root = format!("/__secure_exec/node-runtime/{command_name}"); - let relative_entrypoint = entrypoint.strip_prefix(&package_root).ok()?; - let guest_entrypoint = normalize_path(&format!( - "{guest_root}/{}", - relative_entrypoint.to_string_lossy().replace('\\', "/") - )); - return Some(ResolvedHostNodeCliEntrypoint { - command_name: command_name.to_owned(), - guest_root, - guest_entrypoint, - package_root, - }); +/// The guest's virtual home directory, sourced from the VM user profile (the +/// same value carried to the guest as `os.homedir()` via `guest_runtime`). Used +/// by sidecar-internal `~`-path resolution; falls back to `/root` for a +/// non-absolute profile value. +fn guest_virtual_home(vm: &VmState) -> String { + let homedir = vm.kernel.user_profile().homedir; + if homedir.starts_with('/') { + homedir + } else { + String::from("/root") } +} - None +/// Build the typed per-execution Python limits from the per-VM `VmLimits`. +fn python_execution_limits(vm: &VmState) -> PythonExecutionLimits { + PythonExecutionLimits { + output_buffer_max_bytes: Some(vm.limits.python.output_buffer_max_bytes), + execution_timeout_ms: Some(vm.limits.python.execution_timeout_ms), + max_old_space_mb: Some(vm.limits.python.max_old_space_mb), + vfs_rpc_timeout_ms: Some(vm.limits.python.vfs_rpc_timeout_ms), + } } -fn build_host_node_cli_eval(cli: &ResolvedHostNodeCliEntrypoint) -> String { - let guest_npm_main = normalize_path(&format!("{}/lib/npm.js", cli.guest_root)); - let guest_npm_cli = normalize_path(&format!("{}/bin/npm-cli.js", cli.guest_root)); - let guest_package_json = normalize_path(&format!("{}/package.json", cli.guest_root)); - let guest_display_module = normalize_path(&format!("{}/lib/utils/display.js", cli.guest_root)); - let guest_log_file_module = - normalize_path(&format!("{}/lib/utils/log-file.js", cli.guest_root)); - let debug_preamble = "const __agentOSDebugNpmCli = !!process.env.CODEX_DEBUG_NPM_CLI; const __agentOSDebugLog = (...args) => { if (__agentOSDebugNpmCli) { console.error('[secure-exec npm debug]', ...args); } }; const __agentOSIsProcessExitError = (error) => !!(error && typeof error === 'object' && (error._isProcessExit === true || error.name === 'ProcessExitError')); const __agentOSResolveExitCode = (code) => Number.isFinite(code) ? code : (Number.isFinite(process.exitCode) ? process.exitCode : 0); const __agentOSFinish = (code) => { process.exitCode = __agentOSResolveExitCode(code); }; if (__agentOSDebugNpmCli) { const __agentOSWrapAsyncFsMethod = (__agentOSTarget, __agentOSMethod) => { const __agentOSOriginal = __agentOSTarget[__agentOSMethod]; if (typeof __agentOSOriginal !== 'function' || __agentOSOriginal.__agentOSDebugWrapped) { return; } const __agentOSWrapped = async (...args) => { const target = args.length > 0 ? args[0] : ''; __agentOSDebugLog(`fs.${__agentOSMethod}:start`, String(target)); try { const result = await __agentOSOriginal.apply(__agentOSTarget, args); __agentOSDebugLog(`fs.${__agentOSMethod}:done`, String(target)); return result; } catch (error) { __agentOSDebugLog(`fs.${__agentOSMethod}:error`, String(target), error && error.stack ? error.stack : String(error)); throw error; } }; __agentOSWrapped.__agentOSDebugWrapped = true; __agentOSTarget[__agentOSMethod] = __agentOSWrapped; }; const __agentOSWrapSyncFsMethod = (__agentOSTarget, __agentOSMethod) => { const __agentOSOriginal = __agentOSTarget[__agentOSMethod]; if (typeof __agentOSOriginal !== 'function' || __agentOSOriginal.__agentOSDebugWrapped) { return; } const __agentOSWrapped = (...args) => { const target = args.length > 0 ? args[0] : ''; __agentOSDebugLog(`fs.${__agentOSMethod}:start`, String(target)); try { const result = __agentOSOriginal.apply(__agentOSTarget, args); __agentOSDebugLog(`fs.${__agentOSMethod}:done`, String(target)); return result; } catch (error) { __agentOSDebugLog(`fs.${__agentOSMethod}:error`, String(target), error && error.stack ? error.stack : String(error)); throw error; } }; __agentOSWrapped.__agentOSDebugWrapped = true; __agentOSTarget[__agentOSMethod] = __agentOSWrapped; }; const __agentOSFsPromiseModules = [require('fs/promises'), require('node:fs/promises')]; for (const __agentOSFsPromises of __agentOSFsPromiseModules) { for (const __agentOSMethod of ['access', 'lstat', 'mkdir', 'open', 'readFile', 'readdir', 'readlink', 'realpath', 'rename', 'rm', 'rmdir', 'stat', 'symlink', 'unlink', 'writeFile']) { __agentOSWrapAsyncFsMethod(__agentOSFsPromises, __agentOSMethod); } } const __agentOSFsModules = [require('fs'), require('node:fs')]; for (const __agentOSFs of __agentOSFsModules) { for (const __agentOSMethod of ['accessSync', 'existsSync', 'lstatSync', 'mkdirSync', 'openSync', 'readFileSync', 'readdirSync', 'readlinkSync', 'realpathSync', 'renameSync', 'rmSync', 'rmdirSync', 'statSync', 'symlinkSync', 'unlinkSync', 'writeFileSync']) { __agentOSWrapSyncFsMethod(__agentOSFs, __agentOSMethod); } } }"; - let display_stub = format!( - "const __agentOSDisplayModulePath = require.resolve({display_module}); const __agentOSLogFileModulePath = require.resolve({log_file_module}); const __agentOSColorPassthrough = new Proxy((value) => value, {{ get: () => __agentOSColorPassthrough, apply: (_target, _thisArg, args) => args[0] }}); class __AgentOSNpmDisplayStub {{ constructor() {{ this.chalk = {{ noColor: __agentOSColorPassthrough, stdout: __agentOSColorPassthrough, stderr: __agentOSColorPassthrough }}; this._logPaused = true; this._logBuffer = []; this._outputBuffer = []; this._write = (stream, values) => {{ if (!Array.isArray(values) || values.length === 0) {{ return; }} const text = values.map((value) => typeof value === 'string' ? value : String(value)).join(' '); if (text.length === 0) {{ return; }} const normalized = text.replace(/\\r\\n/g, '\\n'); if (/^\\n?> npx\\n> /u.test(normalized)) {{ return; }} stream.write(text.endsWith('\\n') ? text : `${{text}}\\n`); }}; this._inputHandler = (level, ...args) => {{ if (level !== 'read') {{ return; }} const [resolve, reject, callback] = args; Promise.resolve().then(() => callback()).then(resolve, reject); }}; this._logHandler = (level, ...args) => {{ if (level === 'resume') {{ this._logPaused = false; for (const entry of this._logBuffer.splice(0)) {{ this._write(process.stderr, entry); }} return; }} if (level === 'pause') {{ this._logPaused = true; return; }} if (this._logPaused) {{ this._logBuffer.push(args); return; }} this._write(process.stderr, args); }}; this._outputHandler = (level, ...args) => {{ if (level === 'buffer') {{ this._outputBuffer.push(['standard', args]); return; }} if (level === 'flush') {{ for (const [bufferLevel, bufferArgs] of this._outputBuffer.splice(0)) {{ this._write(bufferLevel === 'error' ? process.stderr : process.stdout, bufferArgs); }} return; }} this._write(level === 'error' ? process.stderr : process.stdout, args); }}; process.on('input', this._inputHandler); process.on('log', this._logHandler); process.on('output', this._outputHandler); }} async load() {{ process.emit('log', 'resume'); process.emit('output', 'flush'); }} off() {{ if (this._inputHandler) {{ process.off('input', this._inputHandler); }} if (this._logHandler) {{ process.off('log', this._logHandler); }} if (this._outputHandler) {{ process.off('output', this._outputHandler); }} this._logBuffer.length = 0; this._outputBuffer.length = 0; }} }} class __AgentOSNpmLogFileStub {{ constructor() {{ this.files = []; }} async load() {{ return []; }} off() {{}} }} globalThis._moduleCache[__agentOSDisplayModulePath] = {{ exports: __AgentOSNpmDisplayStub }}; globalThis._moduleCache[__agentOSLogFileModulePath] = {{ exports: __AgentOSNpmLogFileStub }};", - display_module = serde_json::to_string(&guest_display_module) - .unwrap_or_else(|_| format!("\"{guest_display_module}\"")), - log_file_module = serde_json::to_string(&guest_log_file_module) - .unwrap_or_else(|_| format!("\"{guest_log_file_module}\"")), - ); - let registry_fetch_stub = "const { createRequire: __agentOSCreateRequire } = require('module'); const __agentOSNpmRequire = __agentOSCreateRequire(require.resolve(__AGENTOS_NPM_MAIN__)); try { const __agentOSMinipassFetchPath = __agentOSNpmRequire.resolve('minipass-fetch'); const __agentOSMinipassFetch = __agentOSNpmRequire(__agentOSMinipassFetchPath); const { FetchError: __agentOSFetchError, Headers: __agentOSFetchHeaders, Request: __agentOSFetchRequest, Response: __agentOSFetchResponse, AbortError: __agentOSAbortError } = __agentOSMinipassFetch; const { Minipass: __agentOSMinipass } = __agentOSNpmRequire('minipass'); const __agentOSCreateBinaryMinipass = () => new __agentOSMinipass({ objectMode: false, encoding: null }); const __agentOSCloneBuffer = (buffer) => Buffer.isBuffer(buffer) ? Buffer.from(buffer) : Buffer.from(buffer ?? []); const __agentOSBufferToArrayBuffer = (buffer) => { const bytes = __agentOSCloneBuffer(buffer); return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); }; const __agentOSAttachBufferedBodyMethods = (response, responseBuffer) => { const __agentOSReadBuffer = async () => __agentOSCloneBuffer(responseBuffer); response.__agentOSBufferedBody = __agentOSCloneBuffer(responseBuffer); response.buffer = __agentOSReadBuffer; response.text = async () => (await __agentOSReadBuffer()).toString('utf8'); response.json = async () => JSON.parse(await response.text()); response.arrayBuffer = async () => __agentOSBufferToArrayBuffer(await __agentOSReadBuffer()); response.clone = () => { const clonedBody = __agentOSCreateBinaryMinipass(); const clonedBuffer = __agentOSCloneBuffer(responseBuffer); clonedBody.end(clonedBuffer); const clonedResponse = new __agentOSFetchResponse(clonedBody, { url: response.url, status: response.status, statusText: response.statusText, headers: response.headers, size: response.size, timeout: response.timeout, counter: response.counter, trailer: response.trailer }); return __agentOSAttachBufferedBodyMethods(clonedResponse, clonedBuffer); }; return response; }; const __agentOSNormalizeHeaders = (__agentOSHeaders) => { const normalized = {}; __agentOSHeaders.forEach((value, key) => { if (normalized[key] === undefined) { normalized[key] = value; return; } if (Array.isArray(normalized[key])) { normalized[key].push(value); return; } normalized[key] = [normalized[key], value]; }); return normalized; }; const __agentOSPatchedMinipassFetch = async (input, opts = {}) => { const request = input instanceof __agentOSFetchRequest ? input : new __agentOSFetchRequest(input, opts); const __agentOSController = !request.signal && typeof AbortController === 'function' ? new AbortController() : null; const __agentOSSignal = request.signal ?? __agentOSController?.signal; let __agentOSTimer = null; if (__agentOSController && Number.isFinite(request.timeout) && request.timeout > 0) { __agentOSTimer = setTimeout(() => __agentOSController.abort(new Error(`network timeout at: ${request.url}`)), request.timeout); __agentOSTimer.unref?.(); } try { const requestHeaders = {}; request.headers.forEach((value, key) => { requestHeaders[key] = value; }); const response = await fetch(request.url, { method: request.method, headers: requestHeaders, body: request.body ?? undefined, redirect: request.redirect ?? opts.redirect ?? 'follow', signal: __agentOSSignal, ...(request.body ? { duplex: 'half' } : {}) }); const responseBody = __agentOSCreateBinaryMinipass(); const contentType = String(response.headers.get('content-type') || '').toLowerCase(); const responseBuffer = contentType.includes('json') ? Buffer.from(JSON.stringify(await response.json())) : contentType.startsWith('text/') ? Buffer.from(await response.text()) : Buffer.from(await response.arrayBuffer()); responseBody.end(responseBuffer); return __agentOSAttachBufferedBodyMethods(new __agentOSFetchResponse(responseBody, { url: response.url, status: response.status, statusText: response.statusText, headers: __agentOSNormalizeHeaders(response.headers), size: request.size, timeout: request.timeout, counter: request.counter ?? opts.counter ?? 0, trailer: Promise.resolve(new __agentOSFetchHeaders()) }), responseBuffer); } catch (error) { if (error instanceof Error) { throw error; } throw new __agentOSFetchError(String(error), 'system', error); } finally { if (__agentOSTimer) { clearTimeout(__agentOSTimer); } } }; globalThis.__agentOSPatchedMinipassFetch = __agentOSPatchedMinipassFetch; __agentOSPatchedMinipassFetch.isRedirect = typeof __agentOSMinipassFetch.isRedirect === 'function' ? __agentOSMinipassFetch.isRedirect.bind(__agentOSMinipassFetch) : (code) => code === 301 || code === 302 || code === 303 || code === 307 || code === 308; __agentOSPatchedMinipassFetch.FetchError = __agentOSFetchError; __agentOSPatchedMinipassFetch.Headers = __agentOSFetchHeaders; __agentOSPatchedMinipassFetch.Request = __agentOSFetchRequest; __agentOSPatchedMinipassFetch.Response = __agentOSFetchResponse; __agentOSPatchedMinipassFetch.AbortError = __agentOSAbortError; globalThis._moduleCache[__agentOSMinipassFetchPath] = { exports: __agentOSPatchedMinipassFetch }; __agentOSDebugLog('patched-minipass-fetch', __agentOSMinipassFetchPath); const __agentOSCheckResponsePath = __agentOSNpmRequire.resolve('npm-registry-fetch/lib/check-response.js'); const __agentOSCheckResponse = __agentOSNpmRequire(__agentOSCheckResponsePath); const __agentOSEnsureResponseBodyStream = (response) => { if (!response || (response.body && typeof response.body.on === 'function')) { return response; } const body = __agentOSCreateBinaryMinipass(); const finishWithError = (error) => body.emit('error', error instanceof Error ? error : new Error(String(error))); try { if (typeof response.buffer === 'function') { Promise.resolve(response.buffer()).then((buffer) => body.end(buffer), finishWithError); } else if (Buffer.isBuffer(response.body) || typeof response.body === 'string') { body.end(response.body); } else if (response.body && typeof response.body[Symbol.asyncIterator] === 'function') { (async () => { try { for await (const chunk of response.body) { body.write(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } body.end(); } catch (error) { finishWithError(error); body.end(); } })(); } else { body.end(); } } catch (error) { finishWithError(error); body.end(); } return new __agentOSFetchResponse(body, response); }; globalThis._moduleCache[__agentOSCheckResponsePath] = { exports: (payload) => { const normalized = { ...payload, res: __agentOSEnsureResponseBodyStream(payload.res) }; __agentOSDebugLog('check-response-body', normalized.res && normalized.res.status, typeof (normalized.res && normalized.res.body), normalized.res && normalized.res.body && typeof normalized.res.body.on, normalized.res && normalized.res.body && normalized.res.body.constructor && normalized.res.body.constructor.name, !!(normalized.res && normalized.res.__agentOSBufferedBody), normalized.res && typeof normalized.res.json); return __agentOSCheckResponse(normalized); } }; __agentOSDebugLog('patched-check-response', __agentOSCheckResponsePath); } catch (error) { __agentOSDebugLog('patch-minipass-fetch-failed', error && error.stack ? error.stack : String(error)); } try { const __agentOSRegistryFetchPath = __agentOSNpmRequire.resolve('npm-registry-fetch'); const __agentOSRegistryFetch = __agentOSNpmRequire(__agentOSRegistryFetchPath); const __agentOSWrapRegistryFetch = (fn) => { const wrapResult = (promise) => Promise.resolve(promise).then((res) => { __agentOSDebugLog('registry-fetch-result', res && res.status, typeof (res && res.body), res && res.body && typeof res.body.on, res && res.body && res.body.constructor && res.body.constructor.name, !!(res && res.__agentOSBufferedBody), res && typeof res.json); return res; }); const wrapped = (uri, opts = {}) => wrapResult(globalThis.__agentOSPatchedMinipassFetch(uri, { method: opts.method, headers: opts.headers, body: opts.body, redirect: opts.redirect, signal: opts.signal, timeout: opts.timeout, size: opts.size, counter: opts.counter })); if (typeof fn.json === 'function') { wrapped.json = (uri, opts = {}) => wrapped(uri, opts).then((res) => res.json()); } if (fn.json && typeof fn.json.stream === 'function') { wrapped.json = wrapped.json || {}; wrapped.json.stream = (uri, path, opts = {}) => fn.json.stream(uri, path, { ...opts, agent: false }); } if (typeof fn.pickRegistry === 'function') { wrapped.pickRegistry = fn.pickRegistry.bind(fn); } if (typeof fn.getAuth === 'function') { wrapped.getAuth = fn.getAuth.bind(fn); } return wrapped; }; globalThis._moduleCache[__agentOSRegistryFetchPath] = { exports: __agentOSWrapRegistryFetch(__agentOSRegistryFetch) }; __agentOSDebugLog('patched-npm-registry-fetch', __agentOSRegistryFetchPath); } catch (error) { __agentOSDebugLog('patch-npm-registry-fetch-failed', error && error.stack ? error.stack : String(error)); }"; - match cli.command_name.as_str() { - "npx" => format!( - "{debug_preamble} {display_stub} {registry_fetch_stub} process.argv[1] = require.resolve({npm_cli}); process.argv.splice(2, 0, 'exec'); __agentOSDebugLog('argv', JSON.stringify(process.argv), 'cwd', process.cwd()); (async () => {{ const pkg = require({package_json}); if (process.argv.includes('--version') || process.argv.includes('-v')) {{ __agentOSDebugLog('version-shortcut'); console.log(pkg.version); __agentOSFinish(0); return; }} const Npm = require({npm_main}); const npm = new Npm(); __agentOSDebugLog('before-load'); const loaded = await npm.load(); __agentOSDebugLog('after-load', loaded && loaded.command, JSON.stringify(loaded && loaded.args)); if (!loaded.exec) {{ __agentOSDebugLog('no-exec'); __agentOSFinish(); return; }} if (!loaded.command) {{ __agentOSDebugLog('no-command'); const {{ output }} = require('proc-log'); output.standard(npm.usage); __agentOSFinish(1); return; }} __agentOSDebugLog('before-exec', loaded.command, JSON.stringify(loaded.args)); await npm.exec(loaded.command, loaded.args); __agentOSDebugLog('after-exec', __agentOSResolveExitCode()); __agentOSFinish(); }})().catch((error) => {{ if (__agentOSIsProcessExitError(error)) {{ __agentOSDebugLog('process-exit-error', __agentOSResolveExitCode(error.code)); __agentOSFinish(error.code); return; }} console.error(error && error.stack ? error.stack : String(error)); __agentOSFinish(error && typeof error === 'object' && Number.isFinite(error.exitCode) ? error.exitCode : 1); }});", - debug_preamble = debug_preamble, - display_stub = display_stub, - registry_fetch_stub = registry_fetch_stub.replace( - "__AGENTOS_NPM_MAIN__", - &serde_json::to_string(&guest_npm_main) - .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")), - ), - npm_main = serde_json::to_string(&guest_npm_main) - .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")), - npm_cli = serde_json::to_string(&guest_npm_cli) - .unwrap_or_else(|_| format!("\"{guest_npm_cli}\"")), - package_json = serde_json::to_string(&guest_package_json) - .unwrap_or_else(|_| format!("\"{guest_package_json}\"")), - ), - _ => format!( - "{debug_preamble} {display_stub} {registry_fetch_stub} __agentOSDebugLog('argv', JSON.stringify(process.argv), 'cwd', process.cwd()); (async () => {{ const pkg = require({package_json}); if (process.argv.includes('--version') || process.argv.includes('-v')) {{ __agentOSDebugLog('version-shortcut'); console.log(pkg.version); __agentOSFinish(0); return; }} const Npm = require({npm_main}); const npm = new Npm(); __agentOSDebugLog('before-load'); const loaded = await npm.load(); __agentOSDebugLog('after-load', loaded && loaded.command, JSON.stringify(loaded && loaded.args)); if (!loaded.exec) {{ __agentOSDebugLog('no-exec'); __agentOSFinish(); return; }} if (!loaded.command) {{ __agentOSDebugLog('no-command'); const {{ output }} = require('proc-log'); output.standard(npm.usage); __agentOSFinish(1); return; }} __agentOSDebugLog('before-exec', loaded.command, JSON.stringify(loaded.args)); await npm.exec(loaded.command, loaded.args); __agentOSDebugLog('after-exec', __agentOSResolveExitCode()); __agentOSFinish(); }})().catch((error) => {{ if (__agentOSIsProcessExitError(error)) {{ __agentOSDebugLog('process-exit-error', __agentOSResolveExitCode(error.code)); __agentOSFinish(error.code); return; }} console.error(error && error.stack ? error.stack : String(error)); __agentOSFinish(error && typeof error === 'object' && Number.isFinite(error.exitCode) ? error.exitCode : 1); }});", - debug_preamble = debug_preamble, - display_stub = display_stub, - registry_fetch_stub = registry_fetch_stub.replace( - "__AGENTOS_NPM_MAIN__", - &serde_json::to_string(&guest_npm_main) - .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")), - ), - npm_main = serde_json::to_string(&guest_npm_main) - .unwrap_or_else(|_| format!("\"{guest_npm_main}\"")), - package_json = serde_json::to_string(&guest_package_json) - .unwrap_or_else(|_| format!("\"{guest_package_json}\"")), - ), +/// Build the typed per-execution WebAssembly limits from the per-VM kernel +/// `ResourceLimits`. Replaces the old `apply_wasm_limit_env` env round-trip; +/// notably this is the path that finally enforces the stack cap that the +/// `AGENTOS_WASM_MAX_STACK_BYTES` env knob set but no reader consumed. +fn wasm_execution_limits(vm: &VmState) -> WasmExecutionLimits { + let resource_limits = vm.kernel.resource_limits(); + WasmExecutionLimits { + max_fuel: resource_limits.max_wasm_fuel, + max_memory_bytes: resource_limits.max_wasm_memory_bytes, + max_stack_bytes: resource_limits + .max_wasm_stack_bytes + .map(|value| value as u64), + max_module_file_bytes: Some(vm.limits.wasm.max_module_file_bytes), + max_spawn_file_actions: Some(vm.limits.process.max_spawn_file_actions as u64), + max_spawn_file_action_bytes: Some(vm.limits.process.max_spawn_file_action_bytes as u64), + max_open_fds: resource_limits.max_open_fds.map(|value| value as u64), + max_sockets: resource_limits.max_sockets.map(|value| value as u64), + max_blocking_read_ms: resource_limits.max_blocking_read_ms, + prewarm_timeout_ms: Some(vm.limits.wasm.prewarm_timeout_ms), + runner_heap_limit_mb: Some(vm.limits.wasm.runner_heap_limit_mb), } } -fn rewrite_javascript_shebang_request( - vm: &mut VmState, - resolved: &ResolvedChildProcessExecution, - request: &mut JavascriptChildProcessSpawnRequest, -) -> Result { - const MAX_SHEBANG_LINE_BYTES: usize = 256; +/// The guest JavaScript host platform configured for this VM, defaulting to +/// full Node.js emulation when no `jsRuntime` config was supplied at create. +fn js_runtime_platform(vm: &VmState) -> vm_config::JsRuntimePlatform { + vm.configuration + .js_runtime + .as_ref() + .map(|cfg| cfg.platform) + .unwrap_or(vm_config::JsRuntimePlatform::Node) +} - if !matches!(resolved.runtime, GuestRuntimeKind::WebAssembly) { - return Ok(false); - } - let Some(script_path) = resolved - .env - .get("AGENTOS_GUEST_ENTRYPOINT") - .filter(|path| path.starts_with('/')) - .map(|path| normalize_path(path)) - else { - return Ok(false); - }; - let is_registered_command = vm - .command_guest_paths - .values() - .any(|path| normalize_path(path) == script_path); - if !is_registered_command { - let stat = vm.kernel.stat(&script_path).map_err(kernel_error)?; - if stat.is_directory || stat.mode & 0o111 == 0 { - return Err(SidecarError::Execution(format!( - "EACCES: permission denied, execute '{script_path}'" - ))); - } +/// Lowercase wire name for the configured platform, mirroring the serde +/// representation of `vm_config::JsRuntimePlatform`. +fn js_runtime_platform_env(vm: &VmState) -> &'static str { + match js_runtime_platform(vm) { + vm_config::JsRuntimePlatform::Node => "node", + vm_config::JsRuntimePlatform::Browser => "browser", + vm_config::JsRuntimePlatform::Neutral => "neutral", + vm_config::JsRuntimePlatform::Bare => "bare", } - let header = vm - .kernel - .pread_file(&script_path, 0, MAX_SHEBANG_LINE_BYTES + 1) - .map_err(kernel_error)?; - let Some((command, args)) = - parse_javascript_shebang(&script_path, &header, &resolved.execution_args)? - else { - return Ok(false); - }; - request.command = command; - request.args = args; - request.options.shell = false; - Ok(true) } -fn parse_javascript_shebang( - script_path: &str, - header: &[u8], - execution_args: &[String], -) -> Result)>, SidecarError> { - const MAX_SHEBANG_LINE_BYTES: usize = 256; - - if !header.starts_with(b"#!") { - return Ok(None); +/// Wire name for the configured module-resolution mode, or `None` when it is the +/// full-Node default (which the live resolver also assumes when the env is unset). +fn js_runtime_module_resolution_env(vm: &VmState) -> Option<&'static str> { + let resolution = vm + .configuration + .js_runtime + .as_ref() + .map(|cfg| cfg.module_resolution) + .unwrap_or(vm_config::JsModuleResolution::Node); + match resolution { + vm_config::JsModuleResolution::Node => None, + vm_config::JsModuleResolution::Relative => Some("relative"), + vm_config::JsModuleResolution::None => Some("none"), } +} - let line_end = match header.iter().position(|byte| *byte == b'\n') { - Some(index) if index > MAX_SHEBANG_LINE_BYTES => { - return Err(SidecarError::Execution(format!( - "ENOEXEC: shebang line exceeds {MAX_SHEBANG_LINE_BYTES} bytes: {script_path}" - ))); - } - Some(index) => index, - None if header.len() > MAX_SHEBANG_LINE_BYTES => { - return Err(SidecarError::Execution(format!( - "ENOEXEC: shebang line exceeds {MAX_SHEBANG_LINE_BYTES} bytes: {script_path}" - ))); - } - None => header.len(), - }; - let line = header[2..line_end] - .strip_suffix(b"\r") - .unwrap_or(&header[2..line_end]); - let text = std::str::from_utf8(line).map_err(|_| { - SidecarError::Execution(format!("ENOEXEC: invalid shebang line: {script_path}")) - })?; - let text = text.trim_start_matches(|ch: char| ch.is_ascii_whitespace()); - let (interpreter, optional_arg) = text - .find(|ch: char| ch.is_ascii_whitespace()) - .map(|index| { - ( - &text[..index], - text[index..].trim_matches(|ch: char| ch.is_ascii_whitespace()), - ) - }) - .map(|(interpreter, optional_arg)| { - ( - interpreter, - (!optional_arg.is_empty()).then_some(optional_arg), - ) - }) - .unwrap_or((text, None)); - if interpreter.is_empty() { - return Err(SidecarError::Execution(format!( - "ENOEXEC: invalid shebang line: {script_path}" - ))); +/// The builtin allow-list the live resolver should enforce, or `None` to leave +/// builtins unrestricted (full Node default — preserving today's behavior). +/// Non-node platforms enforce an empty list (deny all builtins). +fn js_runtime_enforced_builtins(vm: &VmState) -> Option> { + if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node { + return Some(Vec::new()); } - let (command, mut interpreter_args) = if matches!(interpreter, "/usr/bin/env" | "/bin/env") { - let optional_arg = optional_arg.ok_or_else(|| { - SidecarError::Execution(format!( - "ENOENT: missing interpreter after {interpreter} in shebang: {script_path}" - )) - })?; - if let Some(split_string) = optional_arg - .strip_prefix("-S") - .filter(|rest| rest.starts_with(|ch: char| ch.is_ascii_whitespace())) - { - let mut words = shlex::split(split_string.trim()).ok_or_else(|| { - SidecarError::Execution(format!( - "ENOEXEC: invalid /usr/bin/env -S quoting in shebang: {script_path}" - )) - })?; - if words.is_empty() { - return Err(SidecarError::Execution(format!( - "ENOENT: missing interpreter after /usr/bin/env -S in shebang: {script_path}" - ))); - } - let command = words.remove(0); - (command, words) - } else { - if optional_arg.starts_with('-') - || optional_arg.chars().any(|ch| ch.is_ascii_whitespace()) - { - return Err(SidecarError::Execution(format!( - "ENOEXEC: /usr/bin/env shebang arguments require -S: {script_path}" - ))); - } - (optional_arg.to_owned(), Vec::new()) - } - } else { - ( - interpreter.to_owned(), - optional_arg - .map(|arg| vec![arg.to_owned()]) - .unwrap_or_default(), - ) + vm.configuration + .js_runtime + .as_ref() + .and_then(|cfg| cfg.allowed_builtins.clone()) +} + +fn configured_allowed_node_builtins(vm: &VmState) -> Vec { + // Non-node platforms expose no Node builtin modules at all. + if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node { + return Vec::new(); + } + // Under the node platform an explicit allow-list wins — including an explicit + // empty list, which means deny all. Absence falls back to the engine default. + let configured = match vm + .configuration + .js_runtime + .as_ref() + .and_then(|cfg| cfg.allowed_builtins.as_ref()) + { + Some(list) => list.clone(), + None => DEFAULT_ALLOWED_NODE_BUILTINS + .iter() + .map(|value| (*value).to_owned()) + .collect::>(), }; - interpreter_args.push(script_path.to_owned()); - interpreter_args.extend(execution_args.iter().cloned()); - Ok(Some((command, interpreter_args))) + dedupe_strings(&configured) } -#[cfg(test)] -mod javascript_shebang_tests { - use super::parse_javascript_shebang; - - fn strings(values: &[&str]) -> Vec { - values.iter().map(|value| (*value).to_owned()).collect() +fn configured_loopback_exempt_ports(vm: &VmState) -> Vec { + if !vm.configuration.loopback_exempt_ports.is_empty() { + return vm + .configuration + .loopback_exempt_ports + .iter() + .map(ToString::to_string) + .collect(); } - #[test] - fn preserves_linux_optional_argument_and_crlf() { - let parsed = parse_javascript_shebang( - "/workspace/test.sh", - b"#!/bin/sh -e -x\r\necho ignored", - &strings(&["one", "two"]), - ) - .expect("parse direct shebang") - .expect("shebang should be detected"); + vm.create_loopback_exempt_ports + .iter() + .map(ToString::to_string) + .collect() +} - assert_eq!(parsed.0, "/bin/sh"); - assert_eq!( - parsed.1, - strings(&["-e -x", "/workspace/test.sh", "one", "two"]) - ); - } +/// Extract the `hostPath` string from a mount plugin's JSON-encoded config. +fn mount_config_host_path(config: &str) -> Option { + serde_json::from_str::(config) + .ok()? + .get("hostPath") + .and_then(Value::as_str) + .map(str::to_owned) +} - #[test] - fn parses_env_and_quoted_env_split_strings() { - let env = parse_javascript_shebang("/workspace/env.sh", b"#!/usr/bin/env sh\n", &[]) - .expect("parse env shebang") - .expect("env shebang should be detected"); - assert_eq!(env, (String::from("sh"), strings(&["/workspace/env.sh"]))); +/// Host path backing a mount for HOST-SIDE resolution (entrypoint launch, import +/// cache location). `agentos_packages` is deliberately excluded by callers: +/// package tar mounts are guest-native and resolve through the kernel VFS. +fn mount_config_host_backing_path(config: &str) -> Option { + let value = serde_json::from_str::(config).ok()?; + value + .get("hostPath") + .and_then(Value::as_str) + .map(str::to_owned) +} - let env_split = parse_javascript_shebang( - "/workspace/env-s.sh", - b"#! /usr/bin/env -S sh -c 'printf \"%s\" \"$1\"' shell\n", - &strings(&["tail"]), - ) - .expect("parse env -S shebang") - .expect("env -S shebang should be detected"); - assert_eq!( - env_split, - ( - String::from("sh"), - strings(&[ - "-c", - "printf \"%s\" \"$1\"", - "shell", - "/workspace/env-s.sh", - "tail" - ]) - ) - ); - } +fn runtime_guest_writable_host_paths(vm: &VmState) -> Vec { + vm.configuration + .mounts + .iter() + .filter(|mount| !mount.read_only) + .filter_map(|mount| { + ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access")) + .then(|| mount_config_host_path(&mount.plugin.config)) + .flatten() + .map(PathBuf::from) + }) + .collect() +} - #[test] - fn rejects_invalid_or_unbounded_shebangs() { - assert!( - parse_javascript_shebang("/workspace/plain", b"plain text", &[]) - .expect("parse plain file") - .is_none() - ); +fn runtime_guest_path_mappings(vm: &VmState) -> Vec { + let mut mappings = vm + .configuration + .mounts + .iter() + .filter_map(|mount| { + ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access")) + .then(|| { + mount_config_host_path(&mount.plugin.config).map(|host_path| { + RuntimeGuestPathMapping { + guest_path: normalize_path(&mount.guest_path), + host_path, + read_only: mount.read_only, + } + }) + }) + .flatten() + }) + .collect::>(); + let mut command_root_mappings = vm + .command_guest_paths + .values() + .filter_map(|guest_path| { + Path::new(guest_path) + .parent() + .and_then(|parent| parent.to_str()) + .map(normalize_path) + }) + .collect::>() + .into_iter() + .map(|guest_path| RuntimeGuestPathMapping { + host_path: resolve_vm_guest_path_to_host(vm, &guest_path) + .to_string_lossy() + .into_owned(), + guest_path, + read_only: false, + }) + .collect::>(); + mappings.append(&mut command_root_mappings); + let mut extra_node_modules_roots = mappings + .iter() + .filter(|mapping| mapping.guest_path.starts_with("/root/node_modules/")) + .filter_map(|mapping| { + host_node_modules_root(Path::new(&mapping.host_path)).map(|host_root| { + RuntimeGuestPathMapping { + guest_path: String::from("/root/node_modules"), + host_path: host_root.to_string_lossy().into_owned(), + read_only: mapping.read_only, + } + }) + }) + .collect::>(); + mappings.append(&mut extra_node_modules_roots); + mappings.push(RuntimeGuestPathMapping { + guest_path: String::from("/"), + host_path: vm.cwd.to_string_lossy().into_owned(), + read_only: false, + }); + mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len())); + mappings.dedup_by(|left, right| { + left.guest_path == right.guest_path && left.host_path == right.host_path + }); + mappings +} + +/// Build a `Send`-able, read-only VFS module reader over the VM's read-only +/// `host_dir`/`module_access` mounts (and the derived `/root/node_modules` root +/// for nested mounts). When present, the V8 bridge thread resolves modules +/// inline against this reader — concurrently with the service loop — so a large +/// cold-start module graph never serializes behind / starves an in-flight ACP +/// `session/new` bootstrap on the single service-loop thread. The reader reads +/// the same mounted tree the guest sees (anchored resolve-beneath, escaping-symlink +/// refusal), never the host-direct path translator. Returns `None` when the VM +/// has no usable read-only mount, so resolution falls back to the service-loop +/// kernel reader. +fn build_module_reader( + vm: &VmState, + resolved: &ResolvedChildProcessExecution, +) -> Option { + let mut pairs: Vec<(String, PathBuf)> = vm + .configuration + .mounts + .iter() + .filter(|mount| mount.read_only) + .filter(|mount| (mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access")) + .filter_map(|mount| { + mount_config_host_path(&mount.plugin.config) + .map(|host_path| (normalize_path(&mount.guest_path), PathBuf::from(host_path))) + }) + .collect(); + + // Packed package-version leaves: module resolution reads packed + // `node_modules` content straight from the `.aospkg` mount index (shared + // mmap cache; no kernel access), mirroring what the guest sees through the + // kernel tar mount. `(guest_path, aospkg_path, tar_root)` triples. + let mut package_tars: Vec<(String, String, String)> = vm + .configuration + .mounts + .iter() + .filter(|mount| mount.plugin.id == "agentos_packages") + .filter_map(|mount| { + let config = serde_json::from_str::(&mount.plugin.config).ok()?; + if config.get("kind").and_then(Value::as_str) != Some("tar") { + return None; + } + let tar_path = config.get("tarPath").and_then(Value::as_str)?.to_owned(); + let root = config + .get("root") + .and_then(Value::as_str) + .unwrap_or("/") + .to_owned(); + Some((normalize_path(&mount.guest_path), tar_path, root)) + }) + .collect(); + // `/current -> ` symlink leaves: alias the current prefix to + // the same tar so modules that self-locate through `current` (rather than + // the realpathed version dir) still resolve. + let current_aliases: Vec<(String, String, String)> = vm + .configuration + .mounts + .iter() + .filter(|mount| mount.plugin.id == "agentos_packages") + .filter_map(|mount| { + let config = serde_json::from_str::(&mount.plugin.config).ok()?; + if config.get("kind").and_then(Value::as_str) != Some("singleSymlink") { + return None; + } + let link_path = normalize_path(&mount.guest_path); + let target = config.get("target").and_then(Value::as_str)?; + let resolved_target = if target.starts_with('/') { + normalize_path(target) + } else { + let parent = Path::new(&link_path).parent()?.to_str()?; + normalize_path(&format!("{parent}/{target}")) + }; + package_tars + .iter() + .find(|(guest, _, _)| *guest == resolved_target) + .map(|(_, tar_path, root)| (link_path, tar_path.clone(), root.clone())) + }) + .collect(); + package_tars.extend(current_aliases); - let missing = parse_javascript_shebang("/workspace/missing", b"#!/usr/bin/env\n", &[]) - .expect_err("env without interpreter must fail"); - assert!(missing.to_string().contains("ENOENT")); + let guest_entrypoint = resolved + .env + .get("AGENTOS_GUEST_ENTRYPOINT") + .map(|path| normalize_path(path)); + if let Some(guest_entrypoint) = guest_entrypoint.as_deref() { + // Package entrypoints may still carry their pre-realpath launch path + // (`/opt/agentos/bin/` or `/current/...` symlink leaves), so + // gate on EVERY agentos_packages mount prefix, not just the tar leaves. + let package_mount_prefixes: Vec = vm + .configuration + .mounts + .iter() + .filter(|mount| mount.plugin.id == "agentos_packages") + .map(|mount| normalize_path(&mount.guest_path)) + .collect(); + let entrypoint_in_read_only_mount = pairs + .iter() + .map(|(guest_path, _)| guest_path) + .chain(package_mount_prefixes.iter()) + .any(|guest_path| { + guest_entrypoint == guest_path + || guest_entrypoint.starts_with(&format!("{guest_path}/")) + }); + if !entrypoint_in_read_only_mount { + return None; + } + } - let malformed = parse_javascript_shebang( - "/workspace/malformed", - b"#!/usr/bin/env -S sh 'unterminated\n", - &[], - ) - .expect_err("unterminated env -S quote must fail"); - assert!(malformed.to_string().contains("ENOEXEC")); + // Mirror runtime_guest_path_mappings: a mount nested under + // `/root/node_modules/` implies a `/root/node_modules` root the resolver + // walks, so expose that root too (e.g. software-package mounts). + let extra_roots: Vec<(String, PathBuf)> = pairs + .iter() + .filter(|(guest_path, _)| guest_path.starts_with("/root/node_modules/")) + .filter_map(|(_, host_path)| { + host_node_modules_root(host_path).map(|root| (String::from("/root/node_modules"), root)) + }) + .collect(); + pairs.extend(extra_roots); - let overlong = format!("#!/{}\n", "x".repeat(257)); - let too_long = parse_javascript_shebang("/workspace/long", overlong.as_bytes(), &[]) - .expect_err("overlong shebang must fail"); - assert!(too_long.to_string().contains("ENOEXEC")); + if std::env::var("AGENTOS_MODULE_READER_TRACE").is_ok() { + eprintln!( + "module-reader: entrypoint={:?} host_pairs={} package_tars={:?}", + resolved.env.get("AGENTOS_GUEST_ENTRYPOINT"), + pairs.len(), + package_tars + .iter() + .map(|(guest, _, _)| guest.as_str()) + .collect::>() + ); } + crate::plugins::host_dir::HostDirModuleReader::from_mounts_and_package_tars(pairs, package_tars) } -fn resolve_guest_command_entrypoint( - vm: &VmState, - guest_cwd: &str, - command: &str, - path_env: Option<&str>, -) -> Option { - if !is_path_like_specifier(command) { - if let Some(entrypoint) = vm.command_guest_paths.get(command) { - return Some(entrypoint.clone()); - } +fn host_node_modules_root(path: &Path) -> Option { + if let Some(root) = path + .ancestors() + .filter(|candidate| { + candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules") + }) + .last() + .map(Path::to_path_buf) + { + return Some(root); + } - for search_dir in guest_command_search_dirs(vm, guest_cwd, path_env) { - let candidate = normalize_path(&format!("{search_dir}/{command}")); - if let Some(entrypoint) = resolve_guest_command_path_candidate(vm, &candidate) { - return Some(entrypoint); - } - } + fs::canonicalize(path) + .ok()? + .ancestors() + .filter(|candidate| { + candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules") + }) + .last() + .map(Path::to_path_buf) +} - return None; - } +#[cfg(test)] +mod runtime_guest_path_mapping_tests { + use super::{host_node_modules_root, javascript_sync_rpc_option_bool}; + use serde_json::json; + use std::fs; + use std::time::{SystemTime, UNIX_EPOCH}; - let normalized = resolve_path_like_guest_specifier(guest_cwd, command); - resolve_guest_command_path_candidate(vm, &normalized).or_else(|| { - // Some guest shells materialize PATH lookups into absolute candidate paths. - // If that path points into a searched directory but does not exist, fall - // back to the command basename so the sidecar can remap VM command packages. - let parent_dir = Path::new(&normalized).parent()?.to_str()?; - if !guest_command_search_dirs(vm, guest_cwd, path_env) - .iter() - .any(|search_dir| normalize_path(search_dir) == normalize_path(parent_dir)) - { - return None; - } + #[test] + fn host_node_modules_root_prefers_workspace_root_over_pnpm_package_node_modules() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock should be monotonic") + .as_nanos(); + let temp = + std::env::temp_dir().join(format!("agentos-native-sidecar-node-modules-{unique}")); + let workspace_node_modules = temp.join("node_modules"); + let package_root = workspace_node_modules + .join(".pnpm") + .join("example@1.0.0") + .join("node_modules") + .join("@scope") + .join("pkg"); + fs::create_dir_all(&package_root).expect("package root should be created"); - let file_name = Path::new(&normalized).file_name()?.to_str()?; - vm.command_guest_paths.get(file_name).cloned() - }) -} + let resolved = + host_node_modules_root(&package_root).expect("node_modules root should resolve"); -fn guest_command_search_dirs(vm: &VmState, guest_cwd: &str, path_env: Option<&str>) -> Vec { - let mut search_dirs = Vec::new(); - let mut seen = BTreeSet::new(); + assert_eq!(resolved, workspace_node_modules); - if let Some(path) = path_env.or_else(|| vm.guest_env.get("PATH").map(String::as_str)) { - for segment in path.split(':') { - let trimmed = segment.trim(); - if trimmed.is_empty() { - continue; - } - let normalized = if trimmed.starts_with('/') { - normalize_path(trimmed) - } else { - normalize_path(&format!("{guest_cwd}/{trimmed}")) - }; - if seen.insert(normalized.clone()) { - search_dirs.push(normalized); - } - } + fs::remove_dir_all(&temp).expect("temp tree should be removed"); } - for fallback in ["/bin", "/usr/bin", "/usr/local/bin"] { - let normalized = String::from(fallback); - if seen.insert(normalized.clone()) { - search_dirs.push(normalized); - } - } + #[test] + fn host_node_modules_root_preserves_symlinked_workspace_node_modules_path() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock should be monotonic") + .as_nanos(); + let temp = std::env::temp_dir().join(format!( + "agentos-native-sidecar-node-modules-symlink-{unique}" + )); + let workspace_node_modules = temp.join("node_modules"); + let package_link = workspace_node_modules.join("@scope").join("pkg"); + let real_package = temp.join("registry").join("agent").join("pkg"); + fs::create_dir_all(package_link.parent().expect("package parent should exist")) + .expect("scoped parent should be created"); + fs::create_dir_all(&real_package).expect("real package root should be created"); + std::os::unix::fs::symlink(&real_package, &package_link) + .expect("package symlink should be created"); - search_dirs -} + let resolved = + host_node_modules_root(&package_link).expect("node_modules root should resolve"); -fn resolve_guest_command_path_candidate(vm: &VmState, candidate: &str) -> Option { - if candidate.starts_with(&format!("{}/", crate::package_projection::OPT_AGENTOS_BIN)) { - if let Ok(realpath) = vm.kernel.realpath(candidate) { - return Some(normalize_path(&realpath)); - } - } + assert_eq!(resolved, workspace_node_modules); - if candidate.starts_with("/bin/") - || candidate.starts_with("/usr/bin/") - || candidate.starts_with("/usr/local/bin/") - || candidate.starts_with(&format!("{}/", crate::package_projection::OPT_AGENTOS_BIN)) - || candidate.starts_with("/__secure_exec/commands/") - { - if let Some(file_name) = Path::new(candidate) - .file_name() - .and_then(|name| name.to_str()) - { - if let Some(guest_entrypoint) = vm.command_guest_paths.get(file_name) { - return Some(guest_entrypoint.clone()); - } - } + fs::remove_dir_all(&temp).expect("temp tree should be removed"); } - if vm - .kernel - .exists(candidate) - .ok() - .is_some_and(|exists| exists) - { - return Some(normalize_path(candidate)); + #[test] + fn javascript_sync_rpc_option_bool_accepts_boolean_recursive_argument() { + assert_eq!( + javascript_sync_rpc_option_bool(&[json!("/workspace"), json!(true)], 1, "recursive"), + Some(true) + ); + assert_eq!( + javascript_sync_rpc_option_bool( + &[json!("/workspace"), json!({ "recursive": false })], + 1, + "recursive" + ), + Some(false) + ); } - - resolve_vm_guest_path_to_host(vm, candidate) - .is_file() - .then(|| normalize_path(candidate)) } -fn resolve_host_entrypoint_within_vm_host_cwd( - vm: &VmState, - specifier: &str, -) -> Option<(String, String)> { - let candidate = Path::new(specifier); - if !candidate.is_absolute() { - return None; +#[cfg(test)] +mod spawn_host_net_resource_limit_tests { + use super::{ + add_live_host_net_transfer_descriptions, apply_spawn_host_net_file_actions, + check_spawn_host_net_resource_limit, host_net_open_description_options, + prepare_transferred_host_net_resource, register_host_net_transfer_description, + scm_rights_host_net_source, spawn_host_net_network_counts, spawn_host_net_source, + validate_host_net_metadata, ActiveExecution, ActiveProcess, HostNetOpenDescriptionOptions, + JavascriptPosixSpawnFileAction, JavascriptSpawnHostNetFd, SidecarKernel, + SpawnHostNetFdState, SpawnHostNetSource, ToolExecution, TransferredHostNetMetadata, + TransferredHostNetSocket, HOST_NET_AF_INET, HOST_NET_AF_INET6, HOST_NET_AF_UNIX, + HOST_NET_IPPROTO_TCP, HOST_NET_IPPROTO_UDP, HOST_NET_METADATA_MAX_STRING_BYTES, + HOST_NET_RECV_TIMEOUT_MAX_MS, HOST_NET_SOCK_CLOEXEC, HOST_NET_SOCK_DGRAM, + HOST_NET_SOCK_NONBLOCK, HOST_NET_SOCK_STREAM, + }; + use agentos_kernel::command_registry::CommandDriver; + use agentos_kernel::kernel::{KernelVmConfig, SpawnOptions}; + use agentos_kernel::mount_table::MountTable; + use agentos_kernel::permissions::Permissions; + use agentos_kernel::vfs::MemoryFileSystem; + use serde_json::{json, Value}; + use std::collections::BTreeMap; + use std::sync::{Arc, Mutex}; + + fn fd_state(description: usize, close_on_exec: bool) -> SpawnHostNetFdState { + SpawnHostNetFdState { + description, + close_on_exec, + } } - let normalized_entrypoint = normalize_host_path(candidate); - let normalized_host_cwd = normalize_host_path(&vm.host_cwd); - if !path_is_within_root(&normalized_entrypoint, &normalized_host_cwd) { - return None; + fn close_action(guest_fd: i32) -> JavascriptPosixSpawnFileAction { + JavascriptPosixSpawnFileAction { + command: 1, + guest_fd: Some(guest_fd), + fd: guest_fd, + source_fd: 0, + guest_source_fd: None, + oflag: 0, + mode: 0, + path: String::new(), + close_from_guest_fds: Vec::new(), + } } - let relative = normalized_entrypoint - .strip_prefix(&normalized_host_cwd) - .ok()? - .to_string_lossy() - .replace('\\', "/"); - let guest_entrypoint = if relative.is_empty() { - String::from("/") - } else { - normalize_path(&format!("/{relative}")) - }; - Some(( - guest_entrypoint, - normalized_entrypoint.to_string_lossy().into_owned(), - )) -} + #[test] + fn inherited_aliases_count_one_open_description() { + let fd_states = BTreeMap::from([ + (3, fd_state(0, false)), + (4, fd_state(0, false)), + (5, fd_state(1, false)), + ]); -fn prepare_guest_runtime_env( - vm: &VmState, - env: &mut BTreeMap, - guest_cwd: &str, - host_cwd: &Path, - guest_entrypoint: Option, -) -> Result<(), SidecarError> { - let user = vm.kernel.user_profile(); - let path_mappings = runtime_guest_path_mappings(vm); - let read_paths = expand_host_access_paths( - std::iter::once(vm.cwd.clone()) - .chain( - path_mappings - .iter() - .map(|mapping| PathBuf::from(&mapping.host_path)), - ) - .chain(std::iter::once(host_cwd.to_path_buf())) - .collect::>() - .as_slice(), - ); - let write_paths = dedupe_host_paths( - std::iter::once(vm.cwd.clone()) - .chain(std::iter::once(host_cwd.to_path_buf())) - .chain(runtime_guest_writable_host_paths(vm)) - .collect::>() - .as_slice(), - ); - let allowed_node_builtins = configured_allowed_node_builtins(vm); - let loopback_exempt_ports = configured_loopback_exempt_ports(vm); + let counts = spawn_host_net_network_counts(&fd_states, &[true, false]); - env.insert( - String::from("AGENTOS_GUEST_PATH_MAPPINGS"), - serde_json::to_string(&path_mappings).map_err(|error| { - SidecarError::InvalidState(format!("failed to encode guest path mappings: {error}")) - })?, - ); - env.entry(String::from(EXECUTION_SANDBOX_ROOT_ENV)) - .or_insert_with(|| normalize_host_path(&vm.cwd).to_string_lossy().into_owned()); - env.insert( - String::from("AGENTOS_EXTRA_FS_READ_PATHS"), - serde_json::to_string( - &read_paths - .iter() - .map(|path| path.to_string_lossy().into_owned()) - .collect::>(), - ) - .map_err(|error| { - SidecarError::InvalidState(format!("failed to encode read paths: {error}")) - })?, - ); - env.insert( - String::from("AGENTOS_EXTRA_FS_WRITE_PATHS"), - serde_json::to_string( - &write_paths - .iter() - .map(|path| path.to_string_lossy().into_owned()) - .collect::>(), - ) - .map_err(|error| { - SidecarError::InvalidState(format!("failed to encode write paths: {error}")) - })?, - ); - env.insert( - String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), - serde_json::to_string(&allowed_node_builtins).map_err(|error| { - SidecarError::InvalidState(format!("failed to encode allowed builtins: {error}")) - })?, - ); - // The guest JS host platform drives subtractive global scrubbing in the - // per-execution runtime shim (see prepend_v8_runtime_shim). - env.insert( - String::from("AGENTOS_JS_PLATFORM"), - js_runtime_platform_env(vm).to_owned(), - ); - // Module-resolution mode (omitted when full Node resolution / the default). - if let Some(resolution) = js_runtime_module_resolution_env(vm) { - env.insert( - String::from("AGENTOS_JS_MODULE_RESOLUTION"), - resolution.to_owned(), - ); - } - // Builtin allow-list gate for the live resolver. Present only when builtins - // should be restricted (non-node platform => deny all; node + explicit - // allow-list => exactly those). Absent => unrestricted (node default). - if let Some(allowlist) = js_runtime_enforced_builtins(vm) { - env.insert( - String::from("AGENTOS_JS_BUILTIN_ALLOWLIST"), - serde_json::to_string(&allowlist).map_err(|error| { - SidecarError::InvalidState(format!( - "failed to encode jsRuntime builtin allow-list: {error}" - )) - })?, - ); - } - // Virtual OS identity (os.cpus/totalmem/freemem/homedir/userInfo/...) now - // rides the typed `guest_runtime` (see `guest_runtime_identity`), exposed to - // the guest as the `__agentOSVirtualOs` structured global by the runtime - // shim — no longer the `AGENTOS_VIRTUAL_OS_*` env vars. - // Virtual process uid/gid now ride the typed `guest_runtime` identity - // (see `guest_runtime_identity`), not the `AGENTOS_VIRTUAL_PROCESS_*` env. - env.entry(String::from("HOME")) - .or_insert_with(|| user.homedir.clone()); - env.entry(String::from("USER")) - .or_insert_with(|| user.username.clone()); - env.entry(String::from("LOGNAME")) - .or_insert_with(|| user.username.clone()); - env.entry(String::from("SHELL")) - .or_insert_with(|| user.shell.clone()); - env.entry(String::from("PATH")).or_insert_with(|| { - vm.guest_env - .get("PATH") - .cloned() - .unwrap_or_else(|| crate::vm::DEFAULT_GUEST_PATH_ENV.to_owned()) - }); - env.entry(String::from("TMPDIR")) - .or_insert_with(|| String::from("/tmp")); - env.insert(String::from("PWD"), guest_cwd.to_owned()); - if !loopback_exempt_ports.is_empty() { - env.insert( - String::from(LOOPBACK_EXEMPT_PORTS_ENV), - serde_json::to_string(&loopback_exempt_ports).map_err(|error| { - SidecarError::InvalidState(format!("failed to encode loopback exemptions: {error}")) - })?, - ); - } - if let Some(guest_entrypoint) = guest_entrypoint { - env.insert(String::from("AGENTOS_GUEST_ENTRYPOINT"), guest_entrypoint); + assert_eq!(counts.sockets, 2); + assert_eq!(counts.connections, 1); } - Ok(()) -} -/// Build the typed per-execution JavaScript limits from the per-VM `VmLimits` -/// (sourced from `CreateVmConfig` on the BARE wire). These ride the execution -/// request, not `AGENTOS_*` env vars — see the env-vs-wire rule in -/// `crates/sidecar/CLAUDE.md`. -fn javascript_execution_limits(vm: &VmState) -> JavascriptExecutionLimits { - JavascriptExecutionLimits { - v8_heap_limit_mb: vm.limits.js_runtime.v8_heap_limit_mb, - sync_rpc_wait_timeout_ms: vm.limits.js_runtime.sync_rpc_wait_timeout_ms, - cpu_time_limit_ms: Some(vm.limits.js_runtime.cpu_time_limit_ms), - wall_clock_limit_ms: Some(vm.limits.js_runtime.wall_clock_limit_ms), - import_cache_materialize_timeout_ms: Some( - vm.limits.js_runtime.import_cache_materialize_timeout_ms, - ), + #[test] + fn close_actions_and_cloexec_elide_inherited_descriptions() { + let mut fd_states = BTreeMap::from([ + (3, fd_state(0, true)), + (4, fd_state(1, false)), + (5, fd_state(1, false)), + ]); + + apply_spawn_host_net_file_actions(&mut fd_states, &[close_action(4), close_action(5)]) + .expect("close actions should apply"); + fd_states.retain(|_, state| !state.close_on_exec); + let counts = spawn_host_net_network_counts(&fd_states, &[true, true]); + + assert_eq!(counts.sockets, 0); + assert_eq!(counts.connections, 0); } -} -/// Build the typed per-execution guest-runtime identity (virtual `process.*`) -/// from kernel state. Replaces the `AGENTOS_VIRTUAL_PROCESS_{UID,GID,PID,PPID}` -/// env round-trip: the runtime shim reads these from `guest_runtime`, not env. -/// `uid`/`gid` come from the VM user profile (applied to every guest); -/// `pid`/`ppid` are per-process and only set for paths that assigned them. -fn guest_runtime_identity( - vm: &VmState, - virtual_pid: Option, - virtual_ppid: Option, -) -> GuestRuntimeConfig { - let user = vm.kernel.user_profile(); - let resource_limits = vm.kernel.resource_limits(); - let identity = shared_guest_runtime_identity(&user, resource_limits, virtual_pid, virtual_ppid); - GuestRuntimeConfig { - virtual_uid: Some(identity.virtual_uid), - virtual_gid: Some(identity.virtual_gid), - virtual_pid: identity.virtual_pid, - virtual_ppid: identity.virtual_ppid, - virtual_exec_path: None, - os_cpu_count: Some(identity.os_cpu_count), - os_totalmem: Some(identity.os_totalmem), - os_freemem: Some(identity.os_freemem), - os_homedir: Some(identity.os_homedir), - os_hostname: Some(identity.os_hostname), - os_tmpdir: Some(identity.os_tmpdir), - os_type: Some(identity.os_type), - os_release: Some(identity.os_release), - os_version: Some(identity.os_version), - os_machine: Some(identity.os_machine), - os_shell: Some(identity.os_shell), - os_user: Some(identity.os_user), - high_resolution_time: vm - .configuration - .js_runtime - .as_ref() - .is_some_and(|cfg| cfg.high_resolution_time.unwrap_or(false)), - // Userland bundle to bake into the per-sidecar snapshot. The sidecar - // derives this from configured agent packages with `agent.snapshot`. - snapshot_userland_code: vm.configuration.snapshot_userland_code.clone(), + #[test] + fn recursive_vm_usage_rejects_nested_inheritance_over_either_limit() { + let socket_error = check_spawn_host_net_resource_limit( + Some(2), + 2, + 1, + "EMFILE", + "socket descriptions", + "maxSockets", + ) + .expect_err("nested inheritance must not exceed maxSockets") + .to_string(); + assert!(socket_error.contains("recursive VM usage from 2 to 3")); + assert!(socket_error.contains("limits.resources.maxSockets (2)")); + + let connection_error = check_spawn_host_net_resource_limit( + Some(4), + 4, + 1, + "EAGAIN", + "connected socket descriptions", + "maxConnections", + ) + .expect_err("nested inheritance must not exceed maxConnections") + .to_string(); + assert!(connection_error.contains("recursive VM usage from 4 to 5")); + assert!(connection_error.contains("limits.resources.maxConnections (4)")); + } + + fn canonical_tcp_metadata(nonblocking: bool) -> TransferredHostNetMetadata { + TransferredHostNetMetadata { + domain: HOST_NET_AF_INET, + socket_type: HOST_NET_SOCK_STREAM, + protocol: HOST_NET_IPPROTO_TCP, + nonblocking, + recv_timeout_ms: Some(250), + bind_options: None, + local_info: Some(json!({ "address": "127.0.0.1", "port": 41000 })), + local_unix_address: None, + local_reservation: None, + remote_info: Some(json!({ "address": "127.0.0.1", "port": 8080 })), + remote_unix_address: None, + listening: false, + } + } + + fn assert_metadata_mismatch(mut value: Value, field: &str, replacement: Value) { + value + .as_object_mut() + .expect("metadata object") + .insert(field.to_owned(), replacement); + let error = validate_host_net_metadata( + &value, + &canonical_tcp_metadata(false), + "tcp", + "spawn host-network", + ) + .expect_err("tampered metadata must be rejected") + .to_string(); + assert!(error.contains(field), "unexpected error: {error}"); } -} -/// The guest's virtual home directory, sourced from the VM user profile (the -/// same value carried to the guest as `os.homedir()` via `guest_runtime`). Used -/// by sidecar-internal `~`-path resolution; falls back to `/root` for a -/// non-absolute profile value. -fn guest_virtual_home(vm: &VmState) -> String { - let homedir = vm.kernel.user_profile().homedir; - if homedir.starts_with('/') { - homedir - } else { - String::from("/root") + #[test] + fn spawn_rejects_forged_socket_identity_lifecycle_and_addresses() { + let canonical = canonical_tcp_metadata(false).as_value(); + for (field, replacement) in [ + ("domain", json!(HOST_NET_AF_INET6)), + ("socketType", json!(HOST_NET_SOCK_DGRAM)), + ("protocol", json!(HOST_NET_IPPROTO_UDP)), + ("listening", json!(true)), + ("localInfo", json!({ "address": "10.0.0.1", "port": 41000 })), + ( + "remoteInfo", + json!({ "address": "169.254.169.254", "port": 80 }), + ), + ("localUnixAddress", json!("unix:/forged")), + ("remoteUnixAddress", json!("unix:/forged-peer")), + ] { + assert_metadata_mismatch(canonical.clone(), field, replacement); + } + + let mut wrong_class = canonical.clone(); + wrong_class["class"] = json!("udp"); + let error = validate_host_net_metadata( + &wrong_class, + &canonical_tcp_metadata(false), + "tcp", + "spawn host-network", + ) + .expect_err("guest class must not override the resolved resource") + .to_string(); + assert!(error.contains("class")); } -} -/// Build the typed per-execution Python limits from the per-VM `VmLimits`. -fn python_execution_limits(vm: &VmState) -> PythonExecutionLimits { - PythonExecutionLimits { - output_buffer_max_bytes: Some(vm.limits.python.output_buffer_max_bytes), - execution_timeout_ms: Some(vm.limits.python.execution_timeout_ms), - max_old_space_mb: Some(vm.limits.python.max_old_space_mb), - vfs_rpc_timeout_ms: Some(vm.limits.python.vfs_rpc_timeout_ms), + #[test] + fn spawn_rejects_ambiguous_or_unbounded_resource_ids_and_resolves_before_metadata() { + let fd = JavascriptSpawnHostNetFd { + guest_fd: 3, + close_on_exec: false, + socket_id: Some(String::from("socket-1")), + server_id: Some(String::from("listener-1")), + udp_socket_id: None, + metadata: canonical_tcp_metadata(false).as_value(), + }; + assert!(spawn_host_net_source(&fd) + .expect_err("multiple ids must be rejected") + .to_string() + .contains("exactly one resource id")); + + let fd = JavascriptSpawnHostNetFd { + socket_id: Some("x".repeat(257)), + server_id: None, + ..fd + }; + assert!(spawn_host_net_source(&fd) + .expect_err("oversized id must be rejected") + .to_string() + .contains("ENAMETOOLONG")); + + let mut config = KernelVmConfig::new("vm-host-net-metadata-resolution-order"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new( + super::EXECUTION_DRIVER_NAME, + [super::WASM_COMMAND], + )) + .expect("register execution driver"); + let kernel_handle = kernel + .spawn_process( + super::WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(super::EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn process"); + let mut process = ActiveProcess::new( + kernel_handle.pid(), + kernel_handle, + super::GuestRuntimeKind::WebAssembly, + ActiveExecution::Tool(ToolExecution::default()), + ); + let error = prepare_transferred_host_net_resource( + &mut kernel, + &mut process, + &SpawnHostNetSource::Tcp(String::from("forged-resource")), + &json!("also-malformed-metadata"), + "spawn host-network", + ) + .expect_err("unknown sidecar resource must be rejected first") + .to_string(); + assert!(error.contains("EBADF: unknown transferable socket forged-resource")); } -} -/// Build the typed per-execution WebAssembly limits from the per-VM kernel -/// `ResourceLimits`. Replaces the old `apply_wasm_limit_env` env round-trip; -/// notably this is the path that finally enforces the stack cap that the -/// `AGENTOS_WASM_MAX_STACK_BYTES` env knob set but no reader consumed. -fn wasm_execution_limits(vm: &VmState) -> WasmExecutionLimits { - let resource_limits = vm.kernel.resource_limits(); - WasmExecutionLimits { - max_fuel: resource_limits.max_wasm_fuel, - max_memory_bytes: resource_limits.max_wasm_memory_bytes, - max_stack_bytes: resource_limits - .max_wasm_stack_bytes - .map(|value| value as u64), - prewarm_timeout_ms: Some(vm.limits.wasm.prewarm_timeout_ms), - runner_heap_limit_mb: Some(vm.limits.wasm.runner_heap_limit_mb), - } -} + #[test] + fn valid_alias_metadata_is_canonicalized_and_creation_flags_do_not_override_ofd_state() { + let expected = canonical_tcp_metadata(false); + let canonical = expected.as_value(); + validate_host_net_metadata(&canonical, &expected, "tcp", "spawn host-network") + .expect("canonical alias metadata"); -/// The guest JavaScript host platform configured for this VM, defaulting to -/// full Node.js emulation when no `jsRuntime` config was supplied at create. -fn js_runtime_platform(vm: &VmState) -> vm_config::JsRuntimePlatform { - vm.configuration - .js_runtime - .as_ref() - .map(|cfg| cfg.platform) - .unwrap_or(vm_config::JsRuntimePlatform::Node) -} + let mut alias = canonical.clone(); + alias["protocol"] = json!(0); + alias["socketType"] = + json!(HOST_NET_SOCK_STREAM | HOST_NET_SOCK_NONBLOCK | HOST_NET_SOCK_CLOEXEC); + validate_host_net_metadata(&alias, &expected, "tcp", "spawn host-network") + .expect("Linux socket creation flags may differ from current OFD/fd state"); -/// Lowercase wire name for the configured platform, mirroring the serde -/// representation of `vm_config::JsRuntimePlatform`. -fn js_runtime_platform_env(vm: &VmState) -> &'static str { - match js_runtime_platform(vm) { - vm_config::JsRuntimePlatform::Node => "node", - vm_config::JsRuntimePlatform::Browser => "browser", - vm_config::JsRuntimePlatform::Neutral => "neutral", - vm_config::JsRuntimePlatform::Bare => "bare", + let emitted = expected.as_value(); + assert_eq!(emitted["socketType"], json!(HOST_NET_SOCK_STREAM)); + assert_eq!(emitted["nonblocking"], json!(false)); + assert!(emitted.get("closeOnExec").is_none()); } -} -/// Wire name for the configured module-resolution mode, or `None` when it is the -/// full-Node default (which the live resolver also assumes when the env is unset). -fn js_runtime_module_resolution_env(vm: &VmState) -> Option<&'static str> { - let resolution = vm - .configuration - .js_runtime - .as_ref() - .map(|cfg| cfg.module_resolution) - .unwrap_or(vm_config::JsModuleResolution::Node); - match resolution { - vm_config::JsModuleResolution::Node => None, - vm_config::JsModuleResolution::Relative => Some("relative"), - vm_config::JsModuleResolution::None => Some("none"), - } -} + #[test] + fn scm_rights_rejects_forged_ids_metadata_and_unbounded_open_state() { + let canonical = canonical_tcp_metadata(false).as_value(); + let mut ambiguous = canonical.clone(); + ambiguous["socketId"] = json!("socket-1"); + ambiguous["serverId"] = json!("listener-1"); + assert!(scm_rights_host_net_source(&ambiguous) + .expect_err("SCM_RIGHTS class ids must be unambiguous") + .to_string() + .contains("at most one resource id")); + + let mut wrong_class = canonical.clone(); + wrong_class["class"] = json!("listener"); + assert!(validate_host_net_metadata( + &wrong_class, + &canonical_tcp_metadata(false), + "tcp", + "SCM_RIGHTS host-network", + ) + .expect_err("SCM class forgery") + .to_string() + .contains("class")); + + let mut wrong_address = canonical.clone(); + wrong_address["remoteInfo"] = json!({ "address": "203.0.113.8", "port": 22 }); + assert!(validate_host_net_metadata( + &wrong_address, + &canonical_tcp_metadata(false), + "tcp", + "SCM_RIGHTS host-network", + ) + .expect_err("SCM address forgery") + .to_string() + .contains("remoteInfo")); -/// The builtin allow-list the live resolver should enforce, or `None` to leave -/// builtins unrestricted (full Node default — preserving today's behavior). -/// Non-node platforms enforce an empty list (deny all builtins). -fn js_runtime_enforced_builtins(vm: &VmState) -> Option> { - if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node { - return Some(Vec::new()); - } - vm.configuration - .js_runtime - .as_ref() - .and_then(|cfg| cfg.allowed_builtins.clone()) -} + let mut bad_nonblocking = canonical.clone(); + bad_nonblocking["nonblocking"] = json!("yes"); + assert!( + host_net_open_description_options(&bad_nonblocking, "SCM_RIGHTS host-network") + .expect_err("nonblocking must be strict boolean") + .to_string() + .contains("must be boolean") + ); -fn configured_allowed_node_builtins(vm: &VmState) -> Vec { - // Non-node platforms expose no Node builtin modules at all. - if js_runtime_platform(vm) != vm_config::JsRuntimePlatform::Node { - return Vec::new(); - } - // Under the node platform an explicit allow-list wins — including an explicit - // empty list, which means deny all. Absence falls back to the engine default. - let configured = match vm - .configuration - .js_runtime - .as_ref() - .and_then(|cfg| cfg.allowed_builtins.as_ref()) - { - Some(list) => list.clone(), - None => DEFAULT_ALLOWED_NODE_BUILTINS - .iter() - .map(|value| (*value).to_owned()) - .collect::>(), - }; - dedupe_strings(&configured) -} + let mut bad_timeout = canonical.clone(); + bad_timeout["recvTimeoutMs"] = json!(HOST_NET_RECV_TIMEOUT_MAX_MS + 1); + assert!( + host_net_open_description_options(&bad_timeout, "SCM_RIGHTS host-network") + .expect_err("timeout must be range bounded") + .to_string() + .contains("exceeds") + ); -fn configured_loopback_exempt_ports(vm: &VmState) -> Vec { - if !vm.configuration.loopback_exempt_ports.is_empty() { - return vm - .configuration - .loopback_exempt_ports - .iter() - .map(ToString::to_string) - .collect(); + let mut oversized = canonical; + oversized["extra"] = json!("x".repeat(HOST_NET_METADATA_MAX_STRING_BYTES + 1)); + assert!( + host_net_open_description_options(&oversized, "SCM_RIGHTS host-network") + .expect_err("metadata strings must be bounded") + .to_string() + .contains("ENAMETOOLONG") + ); } - vm.create_loopback_exempt_ports - .iter() - .map(ToString::to_string) - .collect() -} + #[test] + fn scm_pending_accepts_only_canonical_unconnected_socket_tuples() { + let pending = json!({ + "kind": "hostNet", + "domain": HOST_NET_AF_UNIX, + "socketType": HOST_NET_SOCK_STREAM, + "protocol": 0, + "nonblocking": true, + "recvTimeoutMs": null, + "bindOptions": null, + "localInfo": null, + "localUnixAddress": "unix-unnamed", + "localReservation": null, + "remoteInfo": null, + "remoteUnixAddress": null, + "listening": false, + }); + let options = host_net_open_description_options(&pending, "SCM_RIGHTS pending socket") + .expect("pending options"); + let canonical = + TransferredHostNetMetadata::pending(&pending, options, "SCM_RIGHTS pending socket") + .expect("valid unconnected Unix stream"); + assert_eq!( + canonical.as_value()["localUnixAddress"], + json!("unix-unnamed") + ); -/// Extract the `hostPath` string from a mount plugin's JSON-encoded config. -fn mount_config_host_path(config: &str) -> Option { - serde_json::from_str::(config) - .ok()? - .get("hostPath") - .and_then(Value::as_str) - .map(str::to_owned) -} + for (field, replacement) in [ + ("listening", json!(true)), + ("bindOptions", json!({ "path": "/forged" })), + ("remoteUnixAddress", json!("unix:/forged-peer")), + ] { + let mut forged = pending.clone(); + forged[field] = replacement; + let options = host_net_open_description_options(&forged, "SCM_RIGHTS pending socket") + .expect("open state remains syntactically valid"); + assert!(TransferredHostNetMetadata::pending( + &forged, + options, + "SCM_RIGHTS pending socket", + ) + .is_err()); + } -/// Host path backing a mount for HOST-SIDE resolution (entrypoint launch, import -/// cache location). `agentos_packages` is deliberately excluded by callers: -/// package tar mounts are guest-native and resolve through the kernel VFS. -fn mount_config_host_backing_path(config: &str) -> Option { - let value = serde_json::from_str::(config).ok()?; - value - .get("hostPath") - .and_then(Value::as_str) - .map(str::to_owned) -} + let unsupported = json!({ + "domain": HOST_NET_AF_UNIX, + "socketType": HOST_NET_SOCK_DGRAM, + "protocol": 0, + "listening": false, + }); + let options = HostNetOpenDescriptionOptions { + nonblocking: false, + recv_timeout_ms: None, + }; + assert!(TransferredHostNetMetadata::pending( + &unsupported, + options, + "SCM_RIGHTS pending socket", + ) + .expect_err("unsupported tuple") + .to_string() + .contains("EPROTONOSUPPORT")); + } -fn runtime_guest_writable_host_paths(vm: &VmState) -> Vec { - vm.configuration - .mounts - .iter() - .filter(|mount| !mount.read_only) - .filter_map(|mount| { - ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access")) - .then(|| mount_config_host_path(&mount.plugin.config)) - .flatten() - .map(PathBuf::from) - }) - .collect() -} + #[test] + fn duplicate_rights_share_one_description_and_queue_lifecycle_is_counted() { + let registry = Arc::new(Mutex::new(BTreeMap::new())); + let resource = TransferredHostNetSocket::Pending { + metadata: canonical_tcp_metadata(false), + description_handles: Arc::new(()), + }; + let duplicate = resource + .clone_for_fd_transfer() + .expect("duplicate one open-file description"); + register_host_net_transfer_description(®istry, &resource); + register_host_net_transfer_description(®istry, &duplicate); + + let mut queued = BTreeMap::new(); + add_live_host_net_transfer_descriptions(®istry, &mut queued); + assert_eq!(queued.len(), 1, "duplicate rights are one open description"); + check_spawn_host_net_resource_limit( + Some(1), + 1, + 0, + "EMFILE", + "SCM_RIGHTS socket descriptions", + "maxSockets", + ) + .expect("transferring an existing description at the maximum is allowed"); -fn runtime_guest_path_mappings(vm: &VmState) -> Vec { - let mut mappings = vm - .configuration - .mounts - .iter() - .filter_map(|mount| { - ((mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access")) - .then(|| { - mount_config_host_path(&mount.plugin.config).map(|host_path| { - RuntimeGuestPathMapping { - guest_path: normalize_path(&mount.guest_path), - host_path, - read_only: mount.read_only, - } - }) - }) - .flatten() - }) - .collect::>(); - let mut command_root_mappings = vm - .command_guest_paths - .values() - .filter_map(|guest_path| { - Path::new(guest_path) - .parent() - .and_then(|parent| parent.to_str()) - .map(normalize_path) - }) - .collect::>() - .into_iter() - .map(|guest_path| RuntimeGuestPathMapping { - host_path: resolve_vm_guest_path_to_host(vm, &guest_path) - .to_string_lossy() - .into_owned(), - guest_path, - read_only: false, - }) - .collect::>(); - mappings.append(&mut command_root_mappings); - let mut extra_node_modules_roots = mappings - .iter() - .filter(|mapping| mapping.guest_path.starts_with("/root/node_modules/")) - .filter_map(|mapping| { - host_node_modules_root(Path::new(&mapping.host_path)).map(|host_root| { - RuntimeGuestPathMapping { - guest_path: String::from("/root/node_modules"), - host_path: host_root.to_string_lossy().into_owned(), - read_only: mapping.read_only, - } - }) - }) - .collect::>(); - mappings.append(&mut extra_node_modules_roots); - mappings.push(RuntimeGuestPathMapping { - guest_path: String::from("/"), - host_path: vm.cwd.to_string_lossy().into_owned(), - read_only: false, - }); - mappings.sort_by_key(|mapping| std::cmp::Reverse(mapping.guest_path.len())); - mappings.dedup_by(|left, right| { - left.guest_path == right.guest_path && left.host_path == right.host_path - }); - mappings + drop(resource); + queued.clear(); + add_live_host_net_transfer_descriptions(®istry, &mut queued); + assert_eq!( + queued.len(), + 1, + "queued/received alias keeps the description live" + ); + + drop(duplicate); + queued.clear(); + add_live_host_net_transfer_descriptions(®istry, &mut queued); + assert!( + queued.is_empty(), + "dropping the final right releases the queue lease" + ); + + assert!(check_spawn_host_net_resource_limit( + Some(1), + 1, + 1, + "EMFILE", + "SCM_RIGHTS pending socket descriptions", + "maxSockets", + ) + .is_err()); + } } -/// Build a `Send`-able, read-only VFS module reader over the VM's read-only -/// `host_dir`/`module_access` mounts (and the derived `/root/node_modules` root -/// for nested mounts). When present, the V8 bridge thread resolves modules -/// inline against this reader — concurrently with the service loop — so a large -/// cold-start module graph never serializes behind / starves an in-flight ACP -/// `session/new` bootstrap on the single service-loop thread. The reader reads -/// the same mounted tree the guest sees (anchored resolve-beneath, escaping-symlink -/// refusal), never the host-direct path translator. Returns `None` when the VM -/// has no usable read-only mount, so resolution falls back to the service-loop -/// kernel reader. -fn build_module_reader( - vm: &VmState, - resolved: &ResolvedChildProcessExecution, -) -> Option { - let mut pairs: Vec<(String, PathBuf)> = vm - .configuration - .mounts - .iter() - .filter(|mount| mount.read_only) - .filter(|mount| (mount.plugin.id == "host_dir") || (mount.plugin.id == "module_access")) - .filter_map(|mount| { - mount_config_host_path(&mount.plugin.config) - .map(|host_path| (normalize_path(&mount.guest_path), PathBuf::from(host_path))) - }) - .collect(); +#[cfg(test)] +mod kernel_poll_sync_rpc_tests { + use super::{ + apply_spawn_session_or_rollback, install_preapplied_posix_spawn_file_actions, + javascript_spawn_attributes, missing_javascript_child_kill_result, + preapply_posix_spawn_file_actions, rollback_unregistered_spawn_child, + service_javascript_kernel_poll_sync_rpc, ActiveExecution, ActiveProcess, + JavascriptChildProcessSpawnOptions, JavascriptPosixSpawnFileAction, + JavascriptSyncRpcRequest, KernelPollFdResponse, SidecarKernel, ToolExecution, + EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND, WASM_COMMAND, + }; + use agentos_kernel::command_registry::CommandDriver; + use agentos_kernel::fd_table::{FD_CLOEXEC, F_GETFD, F_SETFD}; + use agentos_kernel::kernel::{KernelVmConfig, SpawnOptions}; + use agentos_kernel::mount_table::{MountOptions, MountTable}; + use agentos_kernel::permissions::Permissions; + use agentos_kernel::poll::{POLLHUP, POLLIN}; + use agentos_kernel::vfs::{ + MemoryFileSystem, VfsResult, VirtualDirEntry, VirtualFileSystem, VirtualStat, + }; + use serde_json::{json, Value}; + use std::collections::HashMap; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::Arc; - // Packed package-version leaves: module resolution reads packed - // `node_modules` content straight from the `.aospkg` mount index (shared - // mmap cache; no kernel access), mirroring what the guest sees through the - // kernel tar mount. `(guest_path, aospkg_path, tar_root)` triples. - let mut package_tars: Vec<(String, String, String)> = vm - .configuration - .mounts - .iter() - .filter(|mount| mount.plugin.id == "agentos_packages") - .filter_map(|mount| { - let config = serde_json::from_str::(&mount.plugin.config).ok()?; - if config.get("kind").and_then(Value::as_str) != Some("tar") { - return None; + #[derive(Default)] + struct SpawnOpenCounters { + exclusive_creates: AtomicUsize, + truncates: AtomicUsize, + } + + struct CountingSpawnMount { + inner: MemoryFileSystem, + counters: Arc, + } + + impl CountingSpawnMount { + fn new(counters: Arc) -> Self { + Self { + inner: MemoryFileSystem::new(), + counters, } - let tar_path = config.get("tarPath").and_then(Value::as_str)?.to_owned(); - let root = config - .get("root") - .and_then(Value::as_str) - .unwrap_or("/") - .to_owned(); - Some((normalize_path(&mount.guest_path), tar_path, root)) - }) - .collect(); - // `/current -> ` symlink leaves: alias the current prefix to - // the same tar so modules that self-locate through `current` (rather than - // the realpathed version dir) still resolve. - let current_aliases: Vec<(String, String, String)> = vm - .configuration - .mounts - .iter() - .filter(|mount| mount.plugin.id == "agentos_packages") - .filter_map(|mount| { - let config = serde_json::from_str::(&mount.plugin.config).ok()?; - if config.get("kind").and_then(Value::as_str) != Some("singleSymlink") { - return None; + } + } + + impl VirtualFileSystem for CountingSpawnMount { + fn read_file(&mut self, path: &str) -> VfsResult> { + self.inner.read_file(path) + } + + fn read_dir(&mut self, path: &str) -> VfsResult> { + self.inner.read_dir(path) + } + + fn read_dir_limited(&mut self, path: &str, max_entries: usize) -> VfsResult> { + self.inner.read_dir_limited(path, max_entries) + } + + fn read_dir_with_types(&mut self, path: &str) -> VfsResult> { + self.inner.read_dir_with_types(path) + } + + fn write_file(&mut self, path: &str, content: impl Into>) -> VfsResult<()> { + // Some mounted VFS wrappers implement exclusive creation in terms + // of the plugin's primitive write operation. Count the mutation + // that reaches this externally mutable mount, regardless of which + // wrapper supplied the exclusivity check. + if matches!(path, "/exclusive" | "/failed-exec-side-effect") { + self.counters + .exclusive_creates + .fetch_add(1, Ordering::SeqCst); } - let link_path = normalize_path(&mount.guest_path); - let target = config.get("target").and_then(Value::as_str)?; - let resolved_target = if target.starts_with('/') { - normalize_path(target) - } else { - let parent = Path::new(&link_path).parent()?.to_str()?; - normalize_path(&format!("{parent}/{target}")) - }; - package_tars + self.inner.write_file(path, content) + } + + fn write_file_with_mode( + &mut self, + path: &str, + content: impl Into>, + mode: Option, + ) -> VfsResult<()> { + self.inner.write_file_with_mode(path, content, mode) + } + + fn create_file_exclusive( + &mut self, + path: &str, + content: impl Into>, + ) -> VfsResult<()> { + self.counters + .exclusive_creates + .fetch_add(1, Ordering::SeqCst); + self.inner.create_file_exclusive(path, content) + } + + fn create_file_exclusive_with_mode( + &mut self, + path: &str, + content: impl Into>, + mode: Option, + ) -> VfsResult<()> { + self.counters + .exclusive_creates + .fetch_add(1, Ordering::SeqCst); + self.inner + .create_file_exclusive_with_mode(path, content, mode) + } + + fn append_file(&mut self, path: &str, content: impl Into>) -> VfsResult { + self.inner.append_file(path, content) + } + + fn create_dir(&mut self, path: &str) -> VfsResult<()> { + self.inner.create_dir(path) + } + + fn mkdir(&mut self, path: &str, recursive: bool) -> VfsResult<()> { + self.inner.mkdir(path, recursive) + } + + fn exists(&self, path: &str) -> bool { + self.inner.exists(path) + } + + fn stat(&mut self, path: &str) -> VfsResult { + self.inner.stat(path) + } + + fn remove_file(&mut self, path: &str) -> VfsResult<()> { + self.inner.remove_file(path) + } + + fn remove_dir(&mut self, path: &str) -> VfsResult<()> { + self.inner.remove_dir(path) + } + + fn rename(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> { + self.inner.rename(old_path, new_path) + } + + fn realpath(&self, path: &str) -> VfsResult { + self.inner.realpath(path) + } + + fn symlink(&mut self, target: &str, link_path: &str) -> VfsResult<()> { + self.inner.symlink(target, link_path) + } + + fn read_link(&self, path: &str) -> VfsResult { + self.inner.read_link(path) + } + + fn lstat(&self, path: &str) -> VfsResult { + self.inner.lstat(path) + } + + fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()> { + self.inner.link(old_path, new_path) + } + + fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()> { + self.inner.chmod(path, mode) + } + + fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> { + self.inner.chown(path, uid, gid) + } + + fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> { + self.inner.utimes(path, atime_ms, mtime_ms) + } + + fn truncate(&mut self, path: &str, length: u64) -> VfsResult<()> { + self.counters.truncates.fetch_add(1, Ordering::SeqCst); + self.inner.truncate(path, length) + } + + fn pread(&mut self, path: &str, offset: u64, length: usize) -> VfsResult> { + self.inner.pread(path, offset, length) + } + } + + #[test] + fn cleanup_kill_ignores_reaped_child_but_rejects_unknown_id() { + missing_javascript_child_kill_result(1, "child-1") + .expect("a previously allocated child is confirmed gone"); + + let unknown = missing_javascript_child_kill_result(1, "child-2") + .expect_err("a never-allocated child must remain an error"); + assert!(unknown + .to_string() + .contains("unknown child process child-2")); + + let malformed = missing_javascript_child_kill_result(1, "child-01") + .expect_err("a non-canonical child id must remain an error"); + assert!(malformed + .to_string() + .contains("unknown child process child-01")); + } + + #[test] + fn posix_spawn_dup2_actions_use_cloexec_sources_before_exec() { + let mut config = KernelVmConfig::new("vm-posix-spawn-cloexec-actions"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .unwrap(); + let parent = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .unwrap(); + let (source_a, ordinary_cloexec) = kernel + .open_pipe(EXECUTION_DRIVER_NAME, parent.pid()) + .unwrap(); + let (source_b, inherited_non_cloexec) = kernel + .open_pipe(EXECUTION_DRIVER_NAME, parent.pid()) + .unwrap(); + let (same_fd_source, _same_fd_peer) = kernel + .open_pipe(EXECUTION_DRIVER_NAME, parent.pid()) + .unwrap(); + for fd in [source_a, source_b, ordinary_cloexec, same_fd_source] { + kernel + .fd_fcntl(EXECUTION_DRIVER_NAME, parent.pid(), fd, F_SETFD, FD_CLOEXEC) + .unwrap(); + } + let source_description = kernel + .fd_transfer(EXECUTION_DRIVER_NAME, parent.pid(), source_a) + .unwrap() + .description_id(); + let same_fd_description = kernel + .fd_transfer(EXECUTION_DRIVER_NAME, parent.pid(), same_fd_source) + .unwrap() + .description_id(); + let dup2_action = |guest_source_fd: i32, source_fd: u32, guest_fd: i32, fd: i32| { + JavascriptPosixSpawnFileAction { + command: 2, + guest_fd: Some(guest_fd), + fd, + source_fd: source_fd as i32, + guest_source_fd: Some(guest_source_fd), + oflag: 0, + mode: 0, + path: String::new(), + close_from_guest_fds: Vec::new(), + } + }; + + // The first dup2 overwrites guest fd 8, so the following 8 -> 9 must + // observe source A. Both targets clear FD_CLOEXEC like Linux dup2. + let prepared = preapply_posix_spawn_file_actions( + &mut kernel, + parent.pid(), + "/", + None, + &[[7, source_a], [8, source_b], [10, same_fd_source]], + &[ + dup2_action(10, same_fd_source, 10, same_fd_source as i32), + dup2_action(7, source_a, 8, source_b as i32), + dup2_action(8, source_b, 9, 9), + ], + ) + .unwrap(); + + let prepared_fd = |guest_fd: u32| { + prepared + .applied + .fd_mappings .iter() - .find(|(guest, _, _)| *guest == resolved_target) - .map(|(_, tar_path, root)| (link_path, tar_path.clone(), root.clone())) - }) - .collect(); - package_tars.extend(current_aliases); + .find_map(|mapping| (mapping[0] == guest_fd).then_some(mapping[1])) + .expect("guest mapping must survive exec") + }; + let guest_8_fd = prepared_fd(8); + let guest_9_fd = prepared_fd(9); + let guest_10_fd = prepared_fd(10); + assert_eq!(guest_8_fd, source_b); + for target_fd in [guest_8_fd, guest_9_fd] { + let target = prepared + .fds + .iter() + .find(|entry| entry.fd == target_fd) + .expect("dup2 target must be prepared for the runtime child"); + assert_eq!(target.transfer.description_id(), source_description); + assert_eq!(target.fd_flags & FD_CLOEXEC, 0); + } + let guest_10 = prepared + .fds + .iter() + .find(|entry| entry.fd == guest_10_fd) + .expect("same-fd dup2 target must be prepared for the runtime child"); + assert_eq!(guest_10.transfer.description_id(), same_fd_description); + assert_eq!(guest_10.fd_flags & FD_CLOEXEC, 0); + assert_eq!( + guest_10_fd, same_fd_source, + "same-fd dup2 must retain the original open description" + ); + assert!( + !prepared + .applied + .fd_mappings + .iter() + .any(|mapping| mapping[0] == 7), + "the original CLOEXEC source must close at exec" + ); + assert!( + !prepared + .fds + .iter() + .any(|entry| entry.fd == ordinary_cloexec), + "an unmapped CLOEXEC descriptor must close at exec" + ); + assert!( + prepared + .fds + .iter() + .any(|entry| entry.fd == inherited_non_cloexec), + "an ordinary non-CLOEXEC descriptor must survive" + ); + assert_eq!( + kernel + .fd_fcntl(EXECUTION_DRIVER_NAME, parent.pid(), source_a, F_GETFD, 0,) + .unwrap(), + FD_CLOEXEC, + "staging must not mutate the parent descriptor table" + ); - let guest_entrypoint = resolved - .env - .get("AGENTOS_GUEST_ENTRYPOINT") - .map(|path| normalize_path(path)); - if let Some(guest_entrypoint) = guest_entrypoint.as_deref() { - // Package entrypoints may still carry their pre-realpath launch path - // (`/opt/agentos/bin/` or `/current/...` symlink leaves), so - // gate on EVERY agentos_packages mount prefix, not just the tar leaves. - let package_mount_prefixes: Vec = vm - .configuration - .mounts + // Installing the staged descriptions into the actual runtime child + // must keep the parent writer alive across multiple request/response + // turns. EOF begins only after the parent's last writer is closed. + let child = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(parent.pid()), + ..SpawnOptions::default() + }, + ) + .unwrap(); + let applied = + install_preapplied_posix_spawn_file_actions(&mut kernel, &child, prepared).unwrap(); + let child_read_fd = applied + .fd_mappings .iter() - .filter(|mount| mount.plugin.id == "agentos_packages") - .map(|mount| normalize_path(&mount.guest_path)) - .collect(); - let entrypoint_in_read_only_mount = pairs + .find_map(|mapping| (mapping[0] == 8).then_some(mapping[1])) + .expect("installed child read mapping"); + kernel + .fd_write( + EXECUTION_DRIVER_NAME, + parent.pid(), + ordinary_cloexec, + b"first", + ) + .unwrap(); + assert_eq!( + kernel + .fd_read(EXECUTION_DRIVER_NAME, child.pid(), child_read_fd, 16) + .unwrap(), + b"first" + ); + kernel + .fd_write( + EXECUTION_DRIVER_NAME, + parent.pid(), + ordinary_cloexec, + b"second", + ) + .unwrap(); + assert_eq!( + kernel + .fd_read(EXECUTION_DRIVER_NAME, child.pid(), child_read_fd, 16) + .unwrap(), + b"second" + ); + kernel + .fd_close(EXECUTION_DRIVER_NAME, parent.pid(), ordinary_cloexec) + .unwrap(); + assert!( + kernel + .fd_read(EXECUTION_DRIVER_NAME, child.pid(), child_read_fd, 1) + .unwrap() + .is_empty(), + "the child must observe EOF after the last writer closes" + ); + + child.finish(0); + kernel.waitpid(child.pid()).unwrap(); + parent.finish(0); + kernel.waitpid(parent.pid()).unwrap(); + } + + #[test] + fn posix_spawn_closefrom_actions_follow_guest_fd_ordering() { + let mut config = KernelVmConfig::new("vm-posix-spawn-closefrom-actions"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .unwrap(); + kernel.write_file("/before-closefrom", b"before").unwrap(); + kernel.write_file("/after-closefrom", b"after").unwrap(); + let parent = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .unwrap(); + + // Push the preserved source above the guest cutoff in the kernel + // namespace. closefrom(5) must still retain it because it is guest fd + // 4; the cutoff is never a raw kernel-fd comparison. + let mut dummy_fds = Vec::new(); + while dummy_fds.last().copied().unwrap_or(0) < 8 { + let (read_fd, write_fd) = kernel + .open_pipe(EXECUTION_DRIVER_NAME, parent.pid()) + .unwrap(); + dummy_fds.extend([read_fd, write_fd]); + } + let keep_kernel_fd = *dummy_fds.last().unwrap(); + let close_kernel_fd = dummy_fds[dummy_fds.len() - 2]; + + let action = + |command, guest_fd, source_guest_fd, path: &str| JavascriptPosixSpawnFileAction { + command, + guest_fd: Some(guest_fd), + fd: guest_fd, + source_fd: source_guest_fd, + guest_source_fd: Some(source_guest_fd), + oflag: 0, + mode: 0o600, + path: path.to_owned(), + close_from_guest_fds: Vec::new(), + }; + let actions = [ + action(3, 9, -1, "/before-closefrom"), + action(2, 10, 4, ""), + action(6, 5, 0, ""), + action(2, 11, 4, ""), + action(3, 12, -1, "/after-closefrom"), + ]; + let prepared = preapply_posix_spawn_file_actions( + &mut kernel, + parent.pid(), + "/", + None, + &[[4, keep_kernel_fd], [8, close_kernel_fd]], + &actions, + ) + .unwrap(); + + let surviving_guest_fds = prepared + .applied + .fd_mappings .iter() - .map(|(guest_path, _)| guest_path) - .chain(package_mount_prefixes.iter()) - .any(|guest_path| { - guest_entrypoint == guest_path - || guest_entrypoint.starts_with(&format!("{guest_path}/")) - }); - if !entrypoint_in_read_only_mount { - return None; + .map(|mapping| mapping[0]) + .collect::>(); + for fd in [4, 11, 12] { + assert!( + surviving_guest_fds.contains(&fd), + "action after closefrom must leave guest fd {fd} open" + ); + } + for fd in [8, 9, 10] { + assert!( + !surviving_guest_fds.contains(&fd), + "action before closefrom must close guest fd {fd}" + ); + assert!(prepared.applied.closed_guest_fds.contains(&fd)); + } + assert!( + prepared.fds.iter().any(|entry| entry.fd == keep_kernel_fd), + "guest fd below the cutoff must preserve a higher kernel fd" + ); + + parent.finish(0); + kernel.waitpid(parent.pid()).unwrap(); + } + + #[test] + fn preapplied_spawn_pipe_writer_lifetime_stress() { + let mut config = KernelVmConfig::new("vm-posix-spawn-pipe-lifetime-stress"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .unwrap(); + let parent = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .unwrap(); + + for iteration in 0..128u32 { + let (read_fd, write_fd) = kernel + .open_pipe(EXECUTION_DRIVER_NAME, parent.pid()) + .unwrap(); + let actions = [ + JavascriptPosixSpawnFileAction { + command: 2, + guest_fd: Some(0), + fd: 0, + source_fd: read_fd as i32, + guest_source_fd: Some(7), + oflag: 0, + mode: 0, + path: String::new(), + close_from_guest_fds: Vec::new(), + }, + JavascriptPosixSpawnFileAction { + command: 1, + guest_fd: Some(7), + fd: read_fd as i32, + source_fd: 0, + guest_source_fd: None, + oflag: 0, + mode: 0, + path: String::new(), + close_from_guest_fds: Vec::new(), + }, + JavascriptPosixSpawnFileAction { + command: 1, + guest_fd: Some(8), + fd: write_fd as i32, + source_fd: 0, + guest_source_fd: None, + oflag: 0, + mode: 0, + path: String::new(), + close_from_guest_fds: Vec::new(), + }, + ]; + let prepared = preapply_posix_spawn_file_actions( + &mut kernel, + parent.pid(), + "/", + None, + &[[7, read_fd], [8, write_fd]], + &actions, + ) + .unwrap(); + let child = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(parent.pid()), + ..SpawnOptions::default() + }, + ) + .unwrap(); + let applied = + install_preapplied_posix_spawn_file_actions(&mut kernel, &child, prepared).unwrap(); + let child_read_fd = applied + .fd_mappings + .iter() + .find_map(|mapping| (mapping[0] == 0).then_some(mapping[1])) + .unwrap(); + + for payload in [b"first".as_slice(), b"second".as_slice()] { + kernel + .fd_write(EXECUTION_DRIVER_NAME, parent.pid(), write_fd, payload) + .unwrap_or_else(|error| panic!("iteration {iteration} write failed: {error}")); + assert_eq!( + kernel + .fd_read( + EXECUTION_DRIVER_NAME, + child.pid(), + child_read_fd, + payload.len(), + ) + .unwrap_or_else(|error| { + panic!("iteration {iteration} read failed: {error}") + }), + payload, + "iteration {iteration} must preserve the parent writer" + ); + } + kernel + .fd_close(EXECUTION_DRIVER_NAME, parent.pid(), write_fd) + .unwrap(); + assert!( + kernel + .fd_read(EXECUTION_DRIVER_NAME, child.pid(), child_read_fd, 1) + .unwrap() + .is_empty(), + "iteration {iteration} must expose EOF after the last writer closes" + ); + kernel + .fd_close(EXECUTION_DRIVER_NAME, parent.pid(), read_fd) + .unwrap(); + child.finish(0); + kernel.waitpid(child.pid()).unwrap(); } - } - // Mirror runtime_guest_path_mappings: a mount nested under - // `/root/node_modules/` implies a `/root/node_modules` root the resolver - // walks, so expose that root too (e.g. software-package mounts). - let extra_roots: Vec<(String, PathBuf)> = pairs - .iter() - .filter(|(guest_path, _)| guest_path.starts_with("/root/node_modules/")) - .filter_map(|(_, host_path)| { - host_node_modules_root(host_path).map(|root| (String::from("/root/node_modules"), root)) - }) - .collect(); - pairs.extend(extra_roots); + parent.finish(0); + kernel.waitpid(parent.pid()).unwrap(); + } - if std::env::var("AGENTOS_MODULE_READER_TRACE").is_ok() { - eprintln!( - "module-reader: entrypoint={:?} host_pairs={} package_tars={:?}", - resolved.env.get("AGENTOS_GUEST_ENTRYPOINT"), - pairs.len(), - package_tars + #[test] + fn spawn_open_actions_transfer_exact_descriptions_and_mutate_plugin_once() { + let counters = Arc::new(SpawnOpenCounters::default()); + let mut mounts = MountTable::new(MemoryFileSystem::new()); + mounts + .mount( + "/plugin", + CountingSpawnMount::new(Arc::clone(&counters)), + MountOptions::new("counting-spawn-open"), + ) + .unwrap(); + let mut config = KernelVmConfig::new("vm-posix-spawn-open-transfer"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(mounts, config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .unwrap(); + let parent = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .unwrap(); + let open_action = |guest_fd, path: &str, oflag| JavascriptPosixSpawnFileAction { + command: 3, + guest_fd: Some(guest_fd), + fd: guest_fd, + source_fd: -1, + guest_source_fd: None, + oflag, + mode: 0o600, + path: path.to_owned(), + close_from_guest_fds: Vec::new(), + }; + let spawn_target = |kernel: &mut SidecarKernel| { + kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(parent.pid()), + ..SpawnOptions::default() + }, + ) + .unwrap() + }; + let prepared_description = |prepared: &super::PreparedPosixSpawnFileActions, + guest_fd: u32| { + let kernel_fd = prepared + .applied + .fd_mappings .iter() - .map(|(guest, _, _)| guest.as_str()) - .collect::>() + .find_map(|mapping| (mapping[0] == guest_fd).then_some(mapping[1])) + .expect("prepared guest fd mapping"); + let description_id = prepared + .fds + .iter() + .find_map(|entry| { + (entry.fd == kernel_fd).then_some(entry.transfer.description_id()) + }) + .expect("prepared transferred description"); + (kernel_fd, description_id) + }; + + let exclusive = preapply_posix_spawn_file_actions( + &mut kernel, + parent.pid(), + "/", + None, + &[], + &[open_action( + 40, + "/plugin/exclusive", + 0x1000_0000 | (1 << 12) | (4 << 12), + )], + ) + .unwrap(); + let (exclusive_fd, exclusive_description) = prepared_description(&exclusive, 40); + assert_eq!(counters.exclusive_creates.load(Ordering::SeqCst), 1); + let exclusive_child = spawn_target(&mut kernel); + install_preapplied_posix_spawn_file_actions(&mut kernel, &exclusive_child, exclusive) + .unwrap(); + assert_eq!(counters.exclusive_creates.load(Ordering::SeqCst), 1); + assert_eq!( + kernel + .fd_transfer(EXECUTION_DRIVER_NAME, exclusive_child.pid(), exclusive_fd) + .unwrap() + .description_id(), + exclusive_description ); - } - crate::plugins::host_dir::HostDirModuleReader::from_mounts_and_package_tars(pairs, package_tars) -} -fn host_node_modules_root(path: &Path) -> Option { - if let Some(root) = path - .ancestors() - .filter(|candidate| { - candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules") - }) - .last() - .map(Path::to_path_buf) - { - return Some(root); + kernel + .write_file("/plugin/truncated", b"must disappear") + .unwrap(); + let truncated = preapply_posix_spawn_file_actions( + &mut kernel, + parent.pid(), + "/", + None, + &[], + &[open_action( + 41, + "/plugin/truncated", + 0x1000_0000 | (8 << 12), + )], + ) + .unwrap(); + let (truncated_fd, truncated_description) = prepared_description(&truncated, 41); + assert_eq!(counters.truncates.load(Ordering::SeqCst), 1); + let truncated_child = spawn_target(&mut kernel); + install_preapplied_posix_spawn_file_actions(&mut kernel, &truncated_child, truncated) + .unwrap(); + assert_eq!(counters.truncates.load(Ordering::SeqCst), 1); + assert_eq!(kernel.read_file("/plugin/truncated").unwrap(), b""); + assert_eq!( + kernel + .fd_transfer(EXECUTION_DRIVER_NAME, truncated_child.pid(), truncated_fd) + .unwrap() + .description_id(), + truncated_description + ); + + // Command resolution happens after file actions. If exec returns + // ENOENT, the O_EXCL creation must remain, but no second open may occur. + let failed_exec = preapply_posix_spawn_file_actions( + &mut kernel, + parent.pid(), + "/", + None, + &[], + &[open_action( + 42, + "/plugin/failed-exec-side-effect", + 0x1000_0000 | (1 << 12) | (4 << 12), + )], + ) + .unwrap(); + let exec_error = kernel + .validate_executable_path("/plugin/missing-executable", "/") + .expect_err("the staged child image must fail exec resolution"); + assert_eq!(exec_error.code(), "ENOENT"); + drop(failed_exec); + assert_eq!(counters.exclusive_creates.load(Ordering::SeqCst), 2); + assert!(kernel.read_file("/plugin/failed-exec-side-effect").is_ok()); + + exclusive_child.finish(0); + truncated_child.finish(0); + parent.finish(0); + kernel.waitpid(exclusive_child.pid()).unwrap(); + kernel.waitpid(truncated_child.pid()).unwrap(); + kernel.waitpid(parent.pid()).unwrap(); } - fs::canonicalize(path) - .ok()? - .ancestors() - .filter(|candidate| { - candidate.file_name().and_then(|name| name.to_str()) == Some("node_modules") - }) - .last() - .map(Path::to_path_buf) -} + #[test] + fn posix_spawn_actions_preserve_ordered_side_effects_and_validate_cwd_like_linux() { + let mut config = KernelVmConfig::new("vm-posix-spawn-action-order"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .expect("register execution driver"); + let parent = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn parent"); + let open_action = |guest_fd, path: &str, oflag| JavascriptPosixSpawnFileAction { + command: 3, + guest_fd: Some(guest_fd), + fd: guest_fd, + source_fd: -1, + guest_source_fd: None, + oflag, + mode: 0o600, + path: path.to_owned(), + close_from_guest_fds: Vec::new(), + }; + let chdir_action = |path: &str| JavascriptPosixSpawnFileAction { + command: 4, + guest_fd: None, + fd: -1, + source_fd: -1, + guest_source_fd: None, + oflag: 0, + mode: 0, + path: path.to_owned(), + close_from_guest_fds: Vec::new(), + }; + let fchdir_action = |guest_fd| JavascriptPosixSpawnFileAction { + command: 5, + guest_fd: Some(guest_fd), + fd: guest_fd, + source_fd: -1, + guest_source_fd: None, + oflag: 0, + mode: 0, + path: String::new(), + close_from_guest_fds: Vec::new(), + }; -#[cfg(test)] -mod runtime_guest_path_mapping_tests { - use super::{host_node_modules_root, javascript_sync_rpc_option_bool}; - use serde_json::json; - use std::fs; - use std::time::{SystemTime, UNIX_EPOCH}; + let error = match preapply_posix_spawn_file_actions( + &mut kernel, + parent.pid(), + "/", + None, + &[], + &[ + open_action( + 40, + "/created-before-failed-chdir", + 0x1000_0000 | (1 << 12) | (4 << 12), + ), + chdir_action("/missing-directory"), + ], + ) { + Ok(_) => panic!("a missing chdir target must fail the spawn"), + Err(error) => error, + }; + assert!(error.to_string().contains("ENOENT"), "{error}"); + assert!( + kernel + .exists("/created-before-failed-chdir") + .expect("query earlier O_CREAT side effect"), + "Linux preserves successful earlier file-action side effects" + ); - #[test] - fn host_node_modules_root_prefers_workspace_root_over_pnpm_package_node_modules() { - let unique = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("clock should be monotonic") - .as_nanos(); - let temp = - std::env::temp_dir().join(format!("agentos-native-sidecar-node-modules-{unique}")); - let workspace_node_modules = temp.join("node_modules"); - let package_root = workspace_node_modules - .join(".pnpm") - .join("example@1.0.0") - .join("node_modules") - .join("@scope") - .join("pkg"); - fs::create_dir_all(&package_root).expect("package root should be created"); + kernel + .write_file("/regular-file", b"not a directory") + .expect("write regular file"); + let error = match preapply_posix_spawn_file_actions( + &mut kernel, + parent.pid(), + "/", + None, + &[], + &[ + open_action(41, "/regular-file", 0), + JavascriptPosixSpawnFileAction { + command: 5, + guest_fd: Some(41), + fd: 41, + source_fd: -1, + guest_source_fd: None, + oflag: 0, + mode: 0, + path: String::new(), + close_from_guest_fds: Vec::new(), + }, + ], + ) { + Ok(_) => panic!("fchdir on a regular file must fail"), + Err(error) => error, + }; + assert!(error.to_string().contains("ENOTDIR"), "{error}"); - let resolved = - host_node_modules_root(&package_root).expect("node_modules root should resolve"); + kernel.mkdir("/work", true).expect("create work directory"); + let prepared = preapply_posix_spawn_file_actions( + &mut kernel, + parent.pid(), + "/", + None, + &[], + &[chdir_action("/work")], + ) + .expect("chdir action succeeds"); + assert_eq!(prepared.cwd, "/work"); - assert_eq!(resolved, workspace_node_modules); + kernel + .mkdir("/real/chdir/subdirectory", true) + .expect("create canonical chdir target"); + kernel + .write_file("/real/chdir/target", b"canonical-chdir-target") + .expect("write canonical chdir-relative target"); + kernel + .write_file("/target", b"must-survive") + .expect("write non-target at the textual symlink parent"); + kernel + .symlink("/real/chdir/subdirectory", "/chdir-alias") + .expect("create chdir symlink"); + let chdir_relative = preapply_posix_spawn_file_actions( + &mut kernel, + parent.pid(), + "/", + None, + &[], + &[ + chdir_action("chdir-alias"), + open_action(42, "../target", 0x1000_0000 | (8 << 12)), + ], + ) + .expect("resolve open relative to canonical chdir target"); + assert_eq!(chdir_relative.cwd, "/real/chdir/subdirectory"); + assert_eq!( + kernel + .read_file("/real/chdir/target") + .expect("read canonical chdir-relative target"), + b"" + ); + assert_eq!( + kernel + .read_file("/target") + .expect("read textual-symlink-parent non-target"), + b"must-survive" + ); - fs::remove_dir_all(&temp).expect("temp tree should be removed"); + kernel + .mkdir("/real/fchdir/subdirectory", true) + .expect("create canonical fchdir target"); + kernel + .write_file("/real/fchdir/target", b"canonical-fchdir-target") + .expect("write canonical fchdir-relative target"); + kernel + .symlink("/real/fchdir/subdirectory", "/fchdir-alias") + .expect("create fchdir symlink"); + let fchdir_relative = preapply_posix_spawn_file_actions( + &mut kernel, + parent.pid(), + "/", + None, + &[], + &[ + open_action(43, "/fchdir-alias", 2 << 12), + fchdir_action(43), + open_action(44, "../target", 0x1000_0000 | (8 << 12)), + ], + ) + .expect("resolve open relative to canonical fchdir target"); + assert_eq!(fchdir_relative.cwd, "/real/fchdir/subdirectory"); + assert_eq!( + kernel + .read_file("/real/fchdir/target") + .expect("read canonical fchdir-relative target"), + b"" + ); + assert_eq!( + kernel + .read_file("/target") + .expect("read non-target after fchdir-relative open"), + b"must-survive" + ); + + parent.finish(0); + kernel.waitpid(parent.pid()).expect("reap parent"); } #[test] - fn host_node_modules_root_preserves_symlinked_workspace_node_modules_path() { - let unique = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("clock should be monotonic") - .as_nanos(); - let temp = std::env::temp_dir().join(format!( - "agentos-native-sidecar-node-modules-symlink-{unique}" - )); - let workspace_node_modules = temp.join("node_modules"); - let package_link = workspace_node_modules.join("@scope").join("pkg"); - let real_package = temp.join("registry").join("agent").join("pkg"); - fs::create_dir_all(package_link.parent().expect("package parent should exist")) - .expect("scoped parent should be created"); - fs::create_dir_all(&real_package).expect("real package root should be created"); - std::os::unix::fs::symlink(&real_package, &package_link) - .expect("package symlink should be created"); - - let resolved = - host_node_modules_root(&package_link).expect("node_modules root should resolve"); + fn rejected_unregistered_spawn_children_restore_process_and_fd_baselines() { + let mut config = KernelVmConfig::new("vm-rejected-spawn-rollback"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .expect("register execution driver"); + let parent = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn parent"); + let baseline = kernel.resource_snapshot(); - assert_eq!(resolved, workspace_node_modules); + for iteration in 0..32 { + let child = kernel + .spawn_process( + WASM_COMMAND, + vec![String::from("malformed-runtime-image")], + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(parent.pid()), + ..SpawnOptions::default() + }, + ) + .unwrap_or_else(|error| panic!("iteration {iteration} spawn failed: {error}")); + kernel + .open_pipe(EXECUTION_DRIVER_NAME, child.pid()) + .unwrap_or_else(|error| panic!("iteration {iteration} pipe failed: {error}")); + let tool = ToolExecution::default(); + let cancelled = Arc::clone(&tool.cancelled); + let mut execution = ActiveExecution::Tool(tool); + rollback_unregistered_spawn_child( + &mut kernel, + &child, + Some(&mut execution), + "malformed WASM regression test", + ); + assert!(cancelled.load(Ordering::Relaxed)); + assert_eq!( + kernel.resource_snapshot(), + baseline, + "iteration {iteration} must release the child process, fd table, and pipe" + ); + } - fs::remove_dir_all(&temp).expect("temp tree should be removed"); + parent.finish(0); + kernel.waitpid(parent.pid()).expect("reap parent"); } #[test] - fn javascript_sync_rpc_option_bool_accepts_boolean_recursive_argument() { + fn posix_spawn_setsid_creates_a_session_and_rejects_setpgroup_conflict() { + let mut config = KernelVmConfig::new("vm-posix-spawn-setsid"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new(EXECUTION_DRIVER_NAME, [WASM_COMMAND])) + .expect("register execution driver"); + let parent = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn parent"); + let child = kernel + .spawn_process( + WASM_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(parent.pid()), + ..SpawnOptions::default() + }, + ) + .expect("spawn child"); + let attributes = javascript_spawn_attributes(&JavascriptChildProcessSpawnOptions { + spawn_attr_flags: super::POSIX_SPAWN_SETSID, + ..Default::default() + }) + .expect("validate SETSID attributes"); + assert!(attributes.new_session); + apply_spawn_session_or_rollback(&mut kernel, &child, attributes.new_session) + .expect("apply SETSID"); assert_eq!( - javascript_sync_rpc_option_bool(&[json!("/workspace"), json!(true)], 1, "recursive"), - Some(true) + kernel + .getsid(EXECUTION_DRIVER_NAME, child.pid()) + .expect("child session id"), + child.pid() ); assert_eq!( - javascript_sync_rpc_option_bool( - &[json!("/workspace"), json!({ "recursive": false })], - 1, - "recursive" - ), - Some(false) + kernel + .getpgid(EXECUTION_DRIVER_NAME, child.pid()) + .expect("child process group"), + child.pid() ); + + let conflict = javascript_spawn_attributes(&JavascriptChildProcessSpawnOptions { + spawn_attr_flags: super::POSIX_SPAWN_SETSID | super::POSIX_SPAWN_SETPGROUP, + ..Default::default() + }) + .expect_err("SETSID|SETPGROUP must fail"); + assert!(conflict.to_string().contains("EPERM")); + + child.finish(0); + parent.finish(0); + kernel.waitpid(child.pid()).expect("reap child"); + kernel.waitpid(parent.pid()).expect("reap parent"); } -} -#[cfg(test)] -mod kernel_poll_sync_rpc_tests { - use super::{ - service_javascript_kernel_poll_sync_rpc, ActiveExecution, ActiveProcess, - JavascriptSyncRpcRequest, KernelPollFdResponse, SidecarKernel, ToolExecution, - EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND, - }; - use agentos_kernel::command_registry::CommandDriver; - use agentos_kernel::kernel::{KernelVmConfig, SpawnOptions}; - use agentos_kernel::mount_table::MountTable; - use agentos_kernel::permissions::Permissions; - use agentos_kernel::poll::{POLLHUP, POLLIN}; - use agentos_kernel::vfs::MemoryFileSystem; - use serde_json::{json, Value}; - use std::collections::HashMap; #[test] fn javascript_kernel_poll_sync_rpc_reports_multiple_kernel_fds() { let mut config = KernelVmConfig::new("vm-js-kernel-poll"); @@ -12346,6 +20481,7 @@ fn parse_dns_record_type(rrtype: &str) -> Result { "SRV" => Ok(RecordType::SRV), "CNAME" => Ok(RecordType::CNAME), "PTR" => Ok(RecordType::PTR), + "SSHFP" => Ok(RecordType::SSHFP), "NS" => Ok(RecordType::NS), "SOA" => Ok(RecordType::SOA), "NAPTR" => Ok(RecordType::NAPTR), @@ -13076,11 +21212,6 @@ fn parse_socket_inode(target: &Path) -> Option { trimmed.parse().ok() } -fn unix_socket_path(addr: &UnixSocketAddr) -> Option { - addr.as_pathname() - .map(|path| path.to_string_lossy().into_owned()) -} - fn find_unix_socket_for_pid( pid: u32, inodes: &BTreeSet, @@ -13168,14 +21299,19 @@ pub(crate) fn vm_network_resource_counts(vm: &VmState) -> NetworkResourceCounts sockets: snapshot.sockets, connections: snapshot.socket_connections, }; + let mut descriptions = BTreeMap::new(); for process in vm.active_processes.values() { - let process_counts = process.sidecar_only_network_resource_counts(); - counts.sockets += process_counts.sockets; - counts.connections += process_counts.connections; + process.collect_network_resource_counts(true, &mut descriptions, &mut counts); } + add_live_host_net_transfer_descriptions(&vm.host_net_transfer_descriptions, &mut descriptions); + add_host_net_description_counts(&descriptions, &mut counts); counts } +fn vm_spawn_host_net_resource_counts(vm: &VmState) -> NetworkResourceCounts { + vm_network_resource_counts(vm) +} + #[allow(clippy::too_many_arguments)] fn collect_javascript_socket_port_state( kernel: &SidecarKernel, @@ -13303,6 +21439,12 @@ fn collect_javascript_socket_port_state( pub(crate) fn build_javascript_socket_path_context( vm: &VmState, ) -> Result { + let mut abstract_namespace_digest = Sha256::new(); + abstract_namespace_digest.update(b"agentos-vm-unix-abstract-v1\0"); + abstract_namespace_digest.update(vm.connection_id.as_bytes()); + abstract_namespace_digest.update(b"\0"); + abstract_namespace_digest.update(vm.session_id.as_bytes()); + let unix_abstract_namespace = abstract_namespace_digest.finalize().into(); let mut loopback_exempt_ports = vm.create_loopback_exempt_ports.clone(); loopback_exempt_ports.extend(vm.configuration.loopback_exempt_ports.iter().copied()); let mut tcp_loopback_guest_to_host_ports = BTreeMap::new(); @@ -13325,7 +21467,10 @@ pub(crate) fn build_javascript_socket_path_context( ); } Ok(JavascriptSocketPathContext { - sandbox_root: vm.cwd.clone(), + unix_abstract_namespace, + unix_socket_host_dir: vm.unix_socket_host_dir.clone(), + unix_bound_addresses: Arc::clone(&vm.unix_address_registry), + host_net_transfer_descriptions: Arc::clone(&vm.host_net_transfer_descriptions), mounts: vm.configuration.mounts.clone(), listen_policy: vm.listen_policy, loopback_exempt_ports, @@ -13712,7 +21857,7 @@ fn host_mount_path_for_guest_path(vm: &VmState, guest_path: &str) -> Option Option Option { + let normalized = normalize_path(guest_path); + mounts + .iter() + .filter(|mount| mount.plugin.id == "host_dir" || mount.plugin.id == "module_access") + .filter(|mount| { + normalized == mount.guest_path + || normalized.starts_with(&format!("{}/", mount.guest_path.trim_end_matches('/'))) + }) + .max_by_key(|mount| mount.guest_path.len()) + .map(|mount| mount.read_only) +} + +fn reject_host_mounted_unix_socket_path( + context: &JavascriptSocketPathContext, + guest_path: &str, +) -> Result<(), SidecarError> { + if let Some(read_only) = host_mount_read_only_for_guest_path(&context.mounts, guest_path) { + let errno = if read_only { + libc::EROFS + } else { + libc::ENOTSUP + }; + return Err(sidecar_net_error(std::io::Error::from_raw_os_error(errno))); + } + Ok(()) +} + #[cfg(test)] mod host_mount_path_for_guest_path_from_mounts_tests { use super::host_mount_path_for_guest_path_from_mounts; @@ -14016,32 +22192,20 @@ mod host_mount_path_for_guest_path_from_mounts_tests { } } -fn resolve_guest_socket_host_path( +fn allocate_guest_socket_host_path( context: &JavascriptSocketPathContext, + kernel_pid: u32, + listener_id: &str, guest_path: &str, ) -> PathBuf { - if let Some(path) = host_mount_path_for_guest_path_from_mounts(&context.mounts, guest_path) { - return path; - } - - let normalized = normalize_path(guest_path); - let mut host_path = context.sandbox_root.clone(); - let suffix = normalized.trim_start_matches('/'); - if !suffix.is_empty() { - host_path.push(suffix); - } - host_path -} - -fn ensure_kernel_parent_directories( - kernel: &mut SidecarKernel, - path: &str, -) -> Result<(), SidecarError> { - let parent = dirname(path); - if parent != "/" && !kernel.exists(&parent).map_err(kernel_error)? { - kernel.mkdir(&parent, true).map_err(kernel_error)?; - } - Ok(()) + let mut digest = Sha256::new(); + digest.update(b"agentos-unix-path-v1\0"); + digest.update(kernel_pid.to_le_bytes()); + digest.update(listener_id.as_bytes()); + digest.update(b"\0"); + digest.update(guest_path.as_bytes()); + let leaf = abstract_unix_name_hex(&digest.finalize()[..16]); + context.unix_socket_host_dir.join(leaf) } // JavascriptChildProcessSpawnOptions, JavascriptChildProcessSpawnRequest moved to crate::protocol @@ -14058,6 +22222,9 @@ pub(crate) fn sanitize_javascript_child_process_internal_bootstrap_env( "AGENTOS_VIRTUAL_PROCESS_UID", "AGENTOS_VIRTUAL_PROCESS_GID", "AGENTOS_VIRTUAL_PROCESS_VERSION", + "AGENTOS_WASM_INITIAL_SIGNAL_MASK", + "AGENTOS_WASM_INITIAL_SIGNAL_IGNORES", + "AGENTOS_WASM_INITIAL_PENDING_SIGNALS", ]; env.iter() @@ -14086,6 +22253,22 @@ pub(crate) fn format_dns_resource(hostname: &str) -> String { format!("dns://{hostname}") } +fn format_unix_socket_resource( + path: Option<&str>, + abstract_path_hex: Option<&str>, + autobind: bool, +) -> String { + if let Some(path) = path { + format!("unix:{path}") + } else if let Some(hex) = abstract_path_hex { + format!("unix:abstract:{hex}") + } else if autobind { + String::from("unix:autobind") + } else { + String::from("unix:unnamed") + } +} + // --- Guest Python socket bridge helpers ------------------------------------ /// Host-socket read timeout for one `recv`/`recvfrom` RPC. Kept short so the @@ -14475,12 +22658,21 @@ fn javascript_net_read_value( fn io_error_code(error: &std::io::Error) -> Option { match error.raw_os_error() { + Some(libc::EACCES) => Some(String::from("EACCES")), + Some(libc::EAGAIN) => Some(String::from("EAGAIN")), Some(libc::EADDRINUSE) => Some(String::from("EADDRINUSE")), Some(libc::EADDRNOTAVAIL) => Some(String::from("EADDRNOTAVAIL")), Some(libc::ECONNREFUSED) => Some(String::from("ECONNREFUSED")), Some(libc::ECONNRESET) => Some(String::from("ECONNRESET")), Some(libc::EINVAL) => Some(String::from("EINVAL")), + Some(libc::EISCONN) => Some(String::from("EISCONN")), + Some(libc::ENAMETOOLONG) => Some(String::from("ENAMETOOLONG")), + Some(libc::ENOENT) => Some(String::from("ENOENT")), + Some(libc::ENOTDIR) => Some(String::from("ENOTDIR")), + Some(libc::ENOTSUP) => Some(String::from("ENOTSUP")), + Some(libc::ENXIO) => Some(String::from("ENXIO")), Some(libc::EPIPE) => Some(String::from("EPIPE")), + Some(libc::EROFS) => Some(String::from("EROFS")), Some(libc::ETIMEDOUT) => Some(String::from("ETIMEDOUT")), Some(libc::EHOSTUNREACH) => Some(String::from("EHOSTUNREACH")), Some(libc::ENETUNREACH) => Some(String::from("ENETUNREACH")), @@ -14496,6 +22688,17 @@ fn sidecar_net_error(error: std::io::Error) -> SidecarError { SidecarError::Execution(message) } +fn nonblocking_unix_connect_error(error: std::io::Error) -> SidecarError { + let code = error.raw_os_error(); + if code == Some(libc::EINPROGRESS) + || code == Some(libc::EALREADY) + || code == Some(libc::EWOULDBLOCK) + { + return sidecar_net_error(std::io::Error::from_raw_os_error(libc::EAGAIN)); + } + sidecar_net_error(error) +} + fn tls_provider() -> Arc { Arc::new(aws_lc_rs::default_provider()) } @@ -14564,7 +22767,31 @@ fn tls_private_key_from_material( ))) } -fn tls_root_store(options: &JavascriptTlsBridgeOptions) -> Result { +fn vm_default_ca_bundle_for_tls_options( + kernel: &mut SidecarKernel, + options: &JavascriptTlsBridgeOptions, +) -> Result, SidecarError> { + if options.is_server || options.reject_unauthorized == Some(false) || options.ca.is_some() { + return Ok(Vec::new()); + } + + read_vm_default_ca_bundle(kernel) +} + +fn read_vm_default_ca_bundle(kernel: &mut SidecarKernel) -> Result, SidecarError> { + kernel + .read_file(CA_CERTIFICATES_GUEST_PATH) + .map_err(|error| { + SidecarError::Execution(format!( + "failed to read VM TLS trust store {CA_CERTIFICATES_GUEST_PATH}: {error}" + )) + }) +} + +fn tls_root_store( + options: &JavascriptTlsBridgeOptions, + default_ca_bundle: &[u8], +) -> Result { let mut roots = RootCertStore::empty(); if let Some(ca) = options.ca.as_ref() { for certificate in tls_certificates_from_material(ca)? { @@ -14575,10 +22802,19 @@ fn tls_root_store(options: &JavascriptTlsBridgeOptions) -> Result, _>>() + .map_err(sidecar_net_error)?; + if certificates.is_empty() { + return Err(SidecarError::InvalidState(format!( + "VM TLS trust store {CA_CERTIFICATES_GUEST_PATH} did not contain any certificates" + ))); + } + for certificate in certificates { roots.add(certificate).map_err(|error| { SidecarError::InvalidState(format!( - "failed to add native TLS certificate to root store: {error}" + "failed to add VM TLS certificate from {CA_CERTIFICATES_GUEST_PATH} to root store: {error}" )) })?; } @@ -14588,8 +22824,9 @@ fn tls_root_store(options: &JavascriptTlsBridgeOptions) -> Result Result, SidecarError> { - let config = build_client_tls_config(options)?; + let config = build_client_tls_config(options, default_ca_bundle)?; let server_name = options .servername .clone() @@ -14628,9 +22865,10 @@ fn build_client_tls_stream( fn build_client_loopback_tls_stream( transport: crate::state::LoopbackTlsEndpoint, options: &JavascriptTlsBridgeOptions, + default_ca_bundle: &[u8], ) -> Result, SidecarError> { - let config = build_client_tls_config(options)?; + let config = build_client_tls_config(options, default_ca_bundle)?; let server_name = options .servername .clone() @@ -14657,6 +22895,7 @@ fn build_client_loopback_tls_stream( fn build_client_tls_config( options: &JavascriptTlsBridgeOptions, + default_ca_bundle: &[u8], ) -> Result { let provider = tls_provider(); let builder = ClientConfig::builder_with_provider(provider.clone()) @@ -14677,7 +22916,7 @@ fn build_client_tls_config( .with_no_client_auth() } else { builder - .with_root_certificates(tls_root_store(options)?) + .with_root_certificates(tls_root_store(options, default_ca_bundle)?) .with_no_client_auth() }; @@ -15265,7 +23504,7 @@ fn spawn_tls_socket_reader( fn spawn_unix_socket_reader( stream: UnixStream, - sender: Sender, + read_state: Arc<(Mutex, Condvar)>, event_pusher: Arc>>, saw_local_shutdown: Arc, saw_remote_end: Arc, @@ -15273,43 +23512,89 @@ fn spawn_unix_socket_reader( ) { thread::spawn(move || { let mut stream = stream; - let mut buffer = vec![0_u8; 64 * 1024]; loop { + let available = { + let (state_lock, ready) = &*read_state; + let mut state = match state_lock.lock() { + Ok(state) => state, + Err(_) => return, + }; + while state.bytes.len() >= state.max_buffered_bytes && !state.closed { + state = match ready.wait(state) { + Ok(state) => state, + Err(_) => return, + }; + } + if state.closed { + return; + } + state.max_buffered_bytes - state.bytes.len() + }; + let mut buffer = vec![0_u8; available.min(64 * 1024)]; match stream.read(&mut buffer) { Ok(0) => { saw_remote_end.store(true, Ordering::SeqCst); - let _ = sender.send(JavascriptTcpSocketEvent::End); + let (state_lock, ready) = &*read_state; + let mut state = match state_lock.lock() { + Ok(state) => state, + Err(_) => return, + }; + state + .terminal_events + .push_back(JavascriptTcpSocketEvent::End); push_socket_event(&event_pusher, "end"); if saw_local_shutdown.load(Ordering::SeqCst) && !close_notified.swap(true, Ordering::SeqCst) { - let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: false }); + state + .terminal_events + .push_back(JavascriptTcpSocketEvent::Close { had_error: false }); push_socket_event(&event_pusher, "close"); } + ready.notify_all(); break; } Ok(bytes_read) => { - if sender - .send(JavascriptTcpSocketEvent::Data( - buffer[..bytes_read].to_vec(), - )) - .is_err() - { - break; + let (state_lock, ready) = &*read_state; + let mut state = match state_lock.lock() { + Ok(state) => state, + Err(_) => return, + }; + state.bytes.extend(&buffer[..bytes_read]); + let warning_bytes = state.max_buffered_bytes.saturating_mul(4) / 5; + if !state.warned_near_limit && state.bytes.len() >= warning_bytes.max(1) { + state.warned_near_limit = true; + eprintln!( + "[agentos] AF_UNIX receive buffer is nearing limits.resources.maxSocketBufferedBytes ({} of {} bytes)", + state.bytes.len(), + state.max_buffered_bytes + ); } + ready.notify_all(); + drop(state); push_socket_event(&event_pusher, "data"); } Err(error) => { let code = io_error_code(&error); - let _ = sender.send(JavascriptTcpSocketEvent::Error { - code, - message: error.to_string(), - }); + let (state_lock, ready) = &*read_state; + let mut state = match state_lock.lock() { + Ok(state) => state, + Err(_) => return, + }; + state + .terminal_events + .push_back(JavascriptTcpSocketEvent::Error { + code, + message: error.to_string(), + }); push_socket_event(&event_pusher, "error"); if !close_notified.swap(true, Ordering::SeqCst) { - let _ = sender.send(JavascriptTcpSocketEvent::Close { had_error: true }); + state + .terminal_events + .push_back(JavascriptTcpSocketEvent::Close { had_error: true }); push_socket_event(&event_pusher, "close"); } + ready.notify_all(); break; } } @@ -15317,6 +23602,184 @@ fn spawn_unix_socket_reader( }); } +/// Whether a successful sync RPC can transition a pipe/socket descriptor from +/// not-readable to readable (including EOF). WASM process RPCs carry the real +/// method name as argument zero, so inspect that remapping too. +pub(crate) fn javascript_sync_rpc_may_make_fd_readable(request: &JavascriptSyncRpcRequest) -> bool { + let method = if request.method == "process.wasm_sync_rpc" { + request + .args + .first() + .and_then(Value::as_str) + .unwrap_or_default() + } else { + request.method.as_str() + }; + matches!( + method, + "process.fd_write" + | "process.fd_close" + | "process.fd_socket_shutdown" + | "__kernel_stdio_write" + | "child_process.write_stdin" + | "child_process.close_stdin" + ) +} + +/// Normalize child wait RPCs before the descendant event pump dispatches them. +/// WASM host-process calls arrive through `process.wasm_sync_rpc` with the real +/// method in argument zero; failing to unwrap `process.fd_read` here sends it +/// through the generic blocking service path and deadlocks the parent that must +/// write or close the pipe. +fn deferred_child_kernel_wait_request( + request: &JavascriptSyncRpcRequest, +) -> Result, SidecarError> { + if matches!( + request.method.as_str(), + "__kernel_stdin_read" | "__kernel_poll" | "process.fd_read" + ) { + return Ok(Some(request.clone())); + } + if request.method != "process.wasm_sync_rpc" { + return Ok(None); + } + let Some(method) = request.args.first().and_then(Value::as_str) else { + return Err(SidecarError::InvalidState(String::from( + "WASM process sync RPC method must be a string", + ))); + }; + if method != "process.fd_read" { + return Ok(None); + } + Ok(Some(JavascriptSyncRpcRequest { + id: request.id, + method: method.to_owned(), + args: request.args[1..].to_vec(), + raw_bytes_args: request + .raw_bytes_args + .iter() + .filter_map(|(index, bytes)| { + (*index > 0 && *index != usize::MAX).then(|| (*index - 1, bytes.clone())) + }) + .collect(), + })) +} + +#[cfg(test)] +mod deferred_child_kernel_wait_tests { + use super::{deferred_child_kernel_wait_request, JavascriptSyncRpcRequest}; + use serde_json::json; + use std::collections::HashMap; + + #[test] + fn wrapped_wasm_fd_read_is_normalized_for_descendant_deferral() { + let request = JavascriptSyncRpcRequest { + id: 41, + method: String::from("process.wasm_sync_rpc"), + args: vec![json!("process.fd_read"), json!(7), json!(4096), json!(5000)], + raw_bytes_args: HashMap::new(), + }; + + let normalized = deferred_child_kernel_wait_request(&request) + .expect("normalize wrapped request") + .expect("wrapped fd_read must use descendant wait path"); + assert_eq!(normalized.id, request.id); + assert_eq!(normalized.method, "process.fd_read"); + assert_eq!(normalized.args, request.args[1..]); + } +} + +fn recheck_ready_deferred_fd_reads( + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, +) -> Result<(), SidecarError> { + let parked_request = process + .deferred_kernel_wait_rpc + .as_ref() + .map(|(request, _)| request.clone()) + .filter(|request| request.method == "process.fd_read"); + + if let Some(request) = parked_request { + let descriptor = (|| { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_read fd")?; + let length = usize::try_from(javascript_sync_rpc_arg_u64( + &request.args, + 1, + "fd_read length", + )?) + .map_err(|_| SidecarError::InvalidState("fd_read length is too large".into()))?; + Ok::<_, SidecarError>((fd, length)) + })(); + + match descriptor { + Ok((fd, length)) => { + // Claim before the authoritative read. Readiness is only a + // scheduling hint; after the token is claimed, the zero-time + // read can safely resolve the request with bytes, EOF, or a + // cooperative EAGAIN. The runner hides EAGAIN from a blocking + // guest unless its configured maxBlockingReadMs expires. + process.deferred_kernel_wait_rpc = None; + let claimed = process + .execution + .claim_javascript_sync_rpc_response(request.id)?; + if claimed { + let read_result = kernel.fd_read_with_timeout_result( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + length, + Some(Duration::ZERO), + ); + match read_result { + Ok(Some(bytes)) => process + .execution + .respond_claimed_javascript_sync_rpc_success( + request.id, + javascript_sync_rpc_bytes_value(&bytes), + )?, + Ok(None) => process + .execution + .respond_claimed_javascript_sync_rpc_success( + request.id, + javascript_sync_rpc_bytes_value(&[]), + )?, + Err(error) => { + let error = kernel_error(error); + process + .execution + .respond_claimed_javascript_sync_rpc_error( + request.id, + javascript_sync_rpc_error_code(&error), + error.to_string(), + )?; + } + } + } + } + Err(error) => { + process.deferred_kernel_wait_rpc = None; + if process + .execution + .claim_javascript_sync_rpc_response(request.id)? + { + process + .execution + .respond_claimed_javascript_sync_rpc_error( + request.id, + javascript_sync_rpc_error_code(&error), + error.to_string(), + )?; + } + } + } + } + + for child in process.child_processes.values_mut() { + recheck_ready_deferred_fd_reads(kernel, child)?; + } + Ok(()) +} + /// Unblock a guest thread parked in a deferred `__kernel_stdin_read` / /// `__kernel_poll` sync RPC. Isolate termination cannot interrupt the native /// bridge wait, so teardown must answer the parked RPC BEFORE dropping the @@ -15334,6 +23797,7 @@ fn terminate_child_process_tree( kernel: &mut SidecarKernel, process: &mut ActiveProcess, kernel_readiness: &KernelSocketReadinessRegistry, + unix_address_registry: &GuestUnixAddressRegistry, ) { flush_parked_kernel_wait_rpc(process); let sqlite_database_ids = process.sqlite_databases.keys().copied().collect::>(); @@ -15365,29 +23829,75 @@ fn terminate_child_process_tree( for listener_id in listener_ids { if let Some(listener) = process.tcp_listeners.remove(&listener_id) { unregister_kernel_readiness_target(kernel_readiness, listener.kernel_socket_id); - let _ = listener.close(kernel, process.kernel_pid); + if let Err(error) = listener.close(kernel, process.kernel_pid) { + eprintln!("failed to close TCP listener {listener_id}: {error}"); + } } } let sockets = process.tcp_sockets.keys().cloned().collect::>(); for socket_id in sockets { if let Some(socket) = process.tcp_sockets.remove(&socket_id) { + if !socket.is_final_description_handle() { + continue; + } unregister_kernel_readiness_target(kernel_readiness, socket.kernel_socket_id); - let _ = socket.close(kernel, process.kernel_pid); + if let Err(error) = socket.close(kernel, process.kernel_pid) { + eprintln!("failed to close TCP socket {socket_id}: {error}"); + } } } let unix_listener_ids = process.unix_listeners.keys().cloned().collect::>(); for listener_id in unix_listener_ids { if let Some(listener) = process.unix_listeners.remove(&listener_id) { - let _ = listener.close(); + if !listener.is_final_description_handle() { + continue; + } + if let Err(error) = close_pending_guest_unix_connections( + unix_address_registry, + &listener.registry_binding_id, + ) { + eprintln!("failed to close pending Unix connections {listener_id}: {error}"); + } + if let Err(error) = + release_guest_unix_binding(unix_address_registry, &listener.registry_binding_id) + { + eprintln!("failed to release Unix listener metadata {listener_id}: {error}"); + } + if let Err(error) = + purge_guest_unix_target(unix_address_registry, &listener.registry_binding_id) + { + eprintln!("failed to purge Unix listener metadata {listener_id}: {error}"); + } + if let Err(error) = listener.close() { + eprintln!("failed to close Unix listener {listener_id}: {error}"); + } } } let unix_sockets = process.unix_sockets.keys().cloned().collect::>(); for socket_id in unix_sockets { - if let Some(socket) = process.unix_sockets.remove(&socket_id) { - let _ = socket.close(); + if let Some(mut socket) = process.unix_sockets.remove(&socket_id) { + if !socket.is_final_description_handle() { + continue; + } + if socket.listener_id.is_some() { + if let Err(error) = socket.cache_remote_peer_metadata(unix_address_registry) { + eprintln!("failed to cache Unix peer metadata for {socket_id}: {error}"); + } + if let Some(state) = socket.connection_state.as_ref() { + state.accepted_peer_open.store(false, Ordering::SeqCst); + } + } + if let Some(binding_id) = socket.local_registry_binding_id.as_deref() { + if let Err(error) = release_guest_unix_binding(unix_address_registry, binding_id) { + eprintln!("failed to release Unix socket metadata {socket_id}: {error}"); + } + } + if let Err(error) = socket.close() { + eprintln!("failed to close Unix socket {socket_id}: {error}"); + } } } @@ -15395,7 +23905,9 @@ fn terminate_child_process_tree( for socket_id in udp_socket_ids { if let Some(mut socket) = process.udp_sockets.remove(&socket_id) { unregister_kernel_readiness_target(kernel_readiness, socket.kernel_socket_id); - socket.close(kernel, process.kernel_pid); + if let Err(error) = socket.close(kernel, process.kernel_pid) { + eprintln!("failed to close UDP socket {socket_id}: {error}"); + } } } @@ -15404,7 +23916,7 @@ fn terminate_child_process_tree( let Some(mut child) = process.child_processes.remove(&child_id) else { continue; }; - terminate_child_process_tree(kernel, &mut child, kernel_readiness); + terminate_child_process_tree(kernel, &mut child, kernel_readiness, unix_address_registry); let _ = kernel.kill_process(EXECUTION_DRIVER_NAME, child.kernel_pid, SIGTERM); let _ = signal_runtime_process(child.execution.child_pid(), SIGTERM); child.kernel_handle.finish(0); @@ -16406,6 +24918,71 @@ pub(crate) fn javascript_sync_rpc_arg_u64_optional( javascript_sync_rpc_arg_u64(args, index, label).map(Some) } +fn wasm_process_resolve_at_path( + kernel: &SidecarKernel, + pid: u32, + dir_fd: u32, + path: &str, +) -> Result { + if path.starts_with('/') { + return Ok(normalize_path(path)); + } + let stat = kernel + .fd_stat(EXECUTION_DRIVER_NAME, pid, dir_fd) + .map_err(kernel_error)?; + if stat.filetype != agentos_kernel::fd_table::FILETYPE_DIRECTORY { + return Err(SidecarError::InvalidState(format!( + "ENOTDIR: file descriptor {dir_fd} is not a directory" + ))); + } + let base = kernel + .fd_path(EXECUTION_DRIVER_NAME, pid, dir_fd) + .map_err(kernel_error)?; + Ok(normalize_path(&format!("{base}/{path}"))) +} + +fn wasm_process_path_stat_value(stat: agentos_kernel::vfs::VirtualStat) -> Value { + let filetype = if stat.is_directory { + agentos_kernel::fd_table::FILETYPE_DIRECTORY + } else if stat.is_symbolic_link { + agentos_kernel::fd_table::FILETYPE_SYMBOLIC_LINK + } else { + agentos_kernel::fd_table::FILETYPE_REGULAR_FILE + }; + json!({ + "dev": stat.dev, + "ino": stat.ino, + "filetype": filetype, + "nlink": stat.nlink, + "size": stat.size, + "atimeMs": stat.atime_ms, + "mtimeMs": stat.mtime_ms, + "ctimeMs": stat.ctime_ms, + }) +} + +fn wasm_process_utime_spec( + nanoseconds: &str, + explicit: bool, + now: bool, +) -> Result { + if now { + return Ok(VirtualUtimeSpec::Now); + } + if !explicit { + return Ok(VirtualUtimeSpec::Omit); + } + let nanoseconds = nanoseconds.parse::().map_err(|_| { + SidecarError::InvalidState("EINVAL: pathname timestamp must be u64 nanoseconds".into()) + })?; + let seconds = i64::try_from(nanoseconds / 1_000_000_000).map_err(|_| { + SidecarError::InvalidState("EINVAL: pathname timestamp exceeds i64 seconds".into()) + })?; + VirtualTimeSpec::new(seconds, (nanoseconds % 1_000_000_000) as u32) + .map(VirtualUtimeSpec::Set) + .map_err(|error| SidecarError::InvalidState(format!("EINVAL: {error}"))) +} + pub(crate) fn javascript_sync_rpc_bytes_arg( args: &[Value], index: usize, @@ -16765,8 +25342,92 @@ where B: NativeSidecarBridge + Send + 'static, BridgeError: fmt::Debug + Send + Sync + 'static, { + const ALLOWED_WASM_PROCESS_SYNC_RPCS: &[&str] = &[ + "process.umask", + "process.getpgid", + "process.setpgid", + "process.waitpid_transition", + "process.itimer_real", + "process.fd_pipe", + "process.fd_open", + "process.path_open_at", + "process.path_mkdir_at", + "process.path_stat_at", + "process.path_chown_at", + "process.path_utimes_at", + "process.path_link_at", + "process.path_readlink_at", + "process.path_remove_dir_at", + "process.path_rename_at", + "process.path_symlink_at", + "process.path_unlink_at", + "process.fd_snapshot", + "process.fd_read", + "process.fd_pread", + "process.fd_write", + "process.fd_pwrite", + "process.fd_sync", + "process.fd_datasync", + "process.fd_readdir", + "process.fd_close", + "process.fd_stat", + "process.fd_filestat", + "process.fd_chmod", + "process.fd_chown", + "process.fd_truncate", + "process.fd_set_flags", + "process.fd_getfd", + "process.fd_setfd", + "process.fd_record_lock", + "process.fd_record_lock_cancel", + "process.fd_dup", + "process.fd_dup2", + "process.fd_dup_min", + "process.fd_seek", + "process.fd_chdir_path", + "process.fd_socketpair", + "process.fd_sendmsg_rights", + "process.fd_recvmsg_rights", + "process.fd_socket_shutdown", + "dns.resolveRawRr", + ]; + let remapped_request = if request.sync_request.method == "process.wasm_sync_rpc" { + let method = javascript_sync_rpc_arg_str( + &request.sync_request.args, + 0, + "WASM process sync RPC method", + )?; + if !ALLOWED_WASM_PROCESS_SYNC_RPCS.contains(&method) { + return Err(SidecarError::InvalidState(format!( + "unsupported WASM process sync RPC method {method}" + ))); + } + let raw_bytes_args = request + .sync_request + .raw_bytes_args + .iter() + .filter_map(|(index, bytes)| { + (*index > 0 && *index != usize::MAX).then(|| (*index - 1, bytes.clone())) + }) + .collect(); + Some(JavascriptSyncRpcRequest { + id: request.sync_request.id, + method: method.to_owned(), + args: request.sync_request.args[1..].to_vec(), + raw_bytes_args, + }) + } else { + None + }; + let original_request = request.sync_request; if sync_rpc_trace_enabled() { - record_sync_rpc(request.sync_request.method.as_str()); + record_sync_rpc( + remapped_request + .as_ref() + .unwrap_or(original_request) + .method + .as_str(), + ); } let JavascriptSyncRpcServiceRequest { bridge, @@ -16776,10 +25437,11 @@ where kernel, kernel_readiness, process, - sync_request: request, + sync_request: _, resource_limits, network_counts, } = request; + let request = remapped_request.as_ref().unwrap_or(original_request); if request.raw_bytes_args.contains_key(&usize::MAX) && request.method == "fs.readSync" { let kernel_pid = process.kernel_pid; let bytes = service_javascript_fs_read_sync_rpc(kernel, process, kernel_pid, request)?; @@ -16866,7 +25528,7 @@ where | "crypto.diffieHellmanSessionCall" | "crypto.diffieHellmanSessionDestroy" | "crypto.subtle" => service_javascript_crypto_sync_rpc(process, request), - "dns.lookup" | "dns.resolve" | "dns.resolve4" | "dns.resolve6" => { + "dns.lookup" | "dns.resolve" | "dns.resolve4" | "dns.resolve6" | "dns.resolveRawRr" => { service_javascript_dns_sync_rpc(bridge, kernel, vm_id, dns, request) } "net.http_listen" | "net.http_close" | "net.http_wait" | "net.http_respond" => { @@ -16918,6 +25580,8 @@ where }) } "net.connect" + | "net.bind_unix" + | "net.bind_connected_unix" | "net.reserve_tcp_port" | "net.release_tcp_port" | "net.listen" @@ -16974,57 +25638,1028 @@ where network_counts, }) } - "sqlite.constants" - | "sqlite.open" - | "sqlite.close" - | "sqlite.exec" - | "sqlite.query" - | "sqlite.prepare" - | "sqlite.location" - | "sqlite.checkpoint" - | "sqlite.statement.run" - | "sqlite.statement.get" - | "sqlite.statement.all" - | "sqlite.statement.iterate" - | "sqlite.statement.columns" - | "sqlite.statement.setReturnArrays" - | "sqlite.statement.setReadBigInts" - | "sqlite.statement.setAllowBareNamedParameters" - | "sqlite.statement.setAllowUnknownNamedParameters" - | "sqlite.statement.finalize" => { - service_javascript_sqlite_sync_rpc(kernel, process, request) + "sqlite.constants" + | "sqlite.open" + | "sqlite.close" + | "sqlite.exec" + | "sqlite.query" + | "sqlite.prepare" + | "sqlite.location" + | "sqlite.checkpoint" + | "sqlite.statement.run" + | "sqlite.statement.get" + | "sqlite.statement.all" + | "sqlite.statement.iterate" + | "sqlite.statement.columns" + | "sqlite.statement.setReturnArrays" + | "sqlite.statement.setReadBigInts" + | "sqlite.statement.setAllowBareNamedParameters" + | "sqlite.statement.setAllowUnknownNamedParameters" + | "sqlite.statement.finalize" => { + service_javascript_sqlite_sync_rpc(kernel, process, request) + } + "process.kill" => { + let target_pid = + javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?; + let signal = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?; + let parsed_signal = parse_signal(signal)?; + if parsed_signal == 0 { + kernel + .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal) + .map_err(kernel_error)?; + return Ok(Value::Null.into()); + } + let process_pid = i32::try_from(process.kernel_pid) + .map_err(|_| SidecarError::InvalidState("process pid exceeds i32".into()))?; + if target_pid != process_pid { + return Err(SidecarError::InvalidState(format!( + "unknown process pid {target_pid}" + ))); + } + if !matches!( + canonical_signal_name(parsed_signal), + Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG") + ) { + apply_active_process_default_signal(kernel, process, parsed_signal)?; + } + Ok(json!({ + "self": true, + "action": "default", + })) + } + "process.take_signal" => { + let signal = if process.real_interval_timer.take_expiry() { + Some(libc::SIGALRM) + } else { + process.pending_wasm_signals.pop_first() + }; + process + .pending_wasm_signals_gauge + .observe_depth(process.pending_wasm_signals.len()); + Ok(signal.map(Value::from).unwrap_or(Value::Null).into()) + } + "process.itimer_real" => { + let operation = javascript_sync_rpc_arg_u32(&request.args, 0, "ITIMER_REAL operation")?; + let values = match operation { + 0 => process.real_interval_timer.get(), + 1 => { + let value_us = javascript_sync_rpc_arg_u64( + &request.args, + 1, + "ITIMER_REAL value microseconds", + )?; + let interval_us = javascript_sync_rpc_arg_u64( + &request.args, + 2, + "ITIMER_REAL interval microseconds", + )?; + process.real_interval_timer.set(value_us, interval_us) + } + other => { + return Err(SidecarError::InvalidState(format!( + "EINVAL: invalid ITIMER_REAL operation {other}" + ))) + } + }; + Ok(json!({ + "remainingUs": values.0, + "intervalUs": values.1, + })) + } + "process.waitpid_transition" => { + let selector = javascript_sync_rpc_arg_i32(&request.args, 0, "waitpid selector")?; + let options = javascript_sync_rpc_arg_u32(&request.args, 1, "waitpid options")?; + if options & !(1 | 2 | 8) != 0 { + return Err(SidecarError::InvalidState(format!( + "EINVAL: invalid waitpid option bits {:#x}", + options & !(1 | 2 | 8) + ))); + } + let mut flags = WaitPidFlags::WNOHANG; + if options & 2 != 0 { + flags |= WaitPidFlags::WUNTRACED; + } + if options & 8 != 0 { + flags |= WaitPidFlags::WCONTINUED; + } + let transition = kernel + .take_nonterminal_wait_event( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + selector, + flags, + ) + .map_err(kernel_error)?; + match transition { + Some(event) => { + let status = match event.event { + agentos_kernel::kernel::WaitPidEvent::Stopped => { + ((event.status as u32 & 0xff) << 8) | 0x7f + } + agentos_kernel::kernel::WaitPidEvent::Continued => 0xffff, + agentos_kernel::kernel::WaitPidEvent::Exited => { + return Err(SidecarError::InvalidState(String::from( + "terminal wait event escaped nonterminal query", + ))) + } + }; + Ok(json!({ + "pid": event.pid, + "status": status, + })) + } + None => Ok(Value::Null), + } + } + "process.fd_pipe" => kernel + .open_pipe(EXECUTION_DRIVER_NAME, process.kernel_pid) + .map(|(read_fd, write_fd)| json!({ "readFd": read_fd, "writeFd": write_fd })) + .map_err(kernel_error), + "process.fd_open" => { + let path = javascript_sync_rpc_arg_str(&request.args, 0, "fd_open path")?; + let flags = javascript_sync_rpc_arg_u32(&request.args, 1, "fd_open flags")?; + let mode = javascript_sync_rpc_arg_u32_optional(&request.args, 2, "fd_open mode")?; + kernel + .fd_open(EXECUTION_DRIVER_NAME, process.kernel_pid, path, flags, mode) + .map(Value::from) + .map_err(kernel_error) + } + "process.path_open_at" => { + let dir_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "path_open_at dir fd")?; + let path = javascript_sync_rpc_arg_str(&request.args, 1, "path_open_at path")?; + let flags = javascript_sync_rpc_arg_u32(&request.args, 2, "path_open_at flags")?; + let mode = javascript_sync_rpc_arg_u32_optional(&request.args, 3, "path_open_at mode")?; + let path = wasm_process_resolve_at_path(kernel, process.kernel_pid, dir_fd, path)?; + kernel + .fd_open( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + &path, + flags, + mode, + ) + .map(Value::from) + .map_err(kernel_error) + } + "process.path_mkdir_at" => { + let dir_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "path_mkdir_at dir fd")?; + let path = javascript_sync_rpc_arg_str(&request.args, 1, "path_mkdir_at path")?; + let path = wasm_process_resolve_at_path(kernel, process.kernel_pid, dir_fd, path)?; + kernel + .mkdir_for_process( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + &path, + false, + None, + ) + .map(|()| Value::Null) + .map_err(kernel_error) + } + "process.path_stat_at" => { + let dir_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "path_stat_at dir fd")?; + let path = javascript_sync_rpc_arg_str(&request.args, 1, "path_stat_at path")?; + let follow = javascript_sync_rpc_arg_bool(&request.args, 2, "path_stat_at follow")?; + let path = wasm_process_resolve_at_path(kernel, process.kernel_pid, dir_fd, path)?; + let stat = if follow { + kernel.stat_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, &path) + } else { + kernel.lstat_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, &path) + } + .map_err(kernel_error)?; + Ok(wasm_process_path_stat_value(stat)) + } + "process.path_chown_at" => { + let dir_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "path_chown_at dir fd")?; + let path = javascript_sync_rpc_arg_str(&request.args, 1, "path_chown_at path")?; + let uid = javascript_sync_rpc_arg_u32(&request.args, 2, "path_chown_at uid")?; + let gid = javascript_sync_rpc_arg_u32(&request.args, 3, "path_chown_at gid")?; + let follow = javascript_sync_rpc_arg_bool(&request.args, 4, "path_chown_at follow")?; + let path = wasm_process_resolve_at_path(kernel, process.kernel_pid, dir_fd, path)?; + kernel + .chown_for_process( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + &path, + uid, + gid, + follow, + ) + .map(|()| Value::Null) + .map_err(kernel_error) + } + "process.path_utimes_at" => { + let dir_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "path_utimes_at dir fd")?; + let path = javascript_sync_rpc_arg_str(&request.args, 1, "path_utimes_at path")?; + let follow = javascript_sync_rpc_arg_bool(&request.args, 2, "path_utimes_at follow")?; + let atime_ns = javascript_sync_rpc_arg_str(&request.args, 3, "path_utimes_at atime")?; + let mtime_ns = javascript_sync_rpc_arg_str(&request.args, 4, "path_utimes_at mtime")?; + let fst_flags = javascript_sync_rpc_arg_u32(&request.args, 5, "path_utimes_at flags")?; + let path = wasm_process_resolve_at_path(kernel, process.kernel_pid, dir_fd, path)?; + let atime = wasm_process_utime_spec(atime_ns, fst_flags & 1 != 0, fst_flags & 2 != 0)?; + let mtime = wasm_process_utime_spec(mtime_ns, fst_flags & 4 != 0, fst_flags & 8 != 0)?; + if follow { + kernel.utimes_spec(&path, atime, mtime) + } else { + kernel.lutimes(&path, atime, mtime) + } + .map(|()| Value::Null) + .map_err(kernel_error) + } + "process.path_link_at" => { + let old_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "path_link_at old fd")?; + let old_path = javascript_sync_rpc_arg_str(&request.args, 1, "path_link_at old path")?; + let new_fd = javascript_sync_rpc_arg_u32(&request.args, 2, "path_link_at new fd")?; + let new_path = javascript_sync_rpc_arg_str(&request.args, 3, "path_link_at new path")?; + let follow = javascript_sync_rpc_arg_bool(&request.args, 4, "path_link_at follow")?; + let mut old_path = + wasm_process_resolve_at_path(kernel, process.kernel_pid, old_fd, old_path)?; + let new_path = + wasm_process_resolve_at_path(kernel, process.kernel_pid, new_fd, new_path)?; + if follow { + old_path = kernel + .realpath_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, &old_path) + .map_err(kernel_error)?; + } + kernel + .link(&old_path, &new_path) + .map(|()| Value::Null) + .map_err(kernel_error) + } + "process.path_readlink_at" => { + let dir_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "path_readlink_at dir fd")?; + let path = javascript_sync_rpc_arg_str(&request.args, 1, "path_readlink_at path")?; + let path = wasm_process_resolve_at_path(kernel, process.kernel_pid, dir_fd, path)?; + kernel + .read_link_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, &path) + .map(Value::String) + .map_err(kernel_error) + } + "process.path_remove_dir_at" => { + let dir_fd = + javascript_sync_rpc_arg_u32(&request.args, 0, "path_remove_dir_at dir fd")?; + let path = javascript_sync_rpc_arg_str(&request.args, 1, "path_remove_dir_at path")?; + let path = wasm_process_resolve_at_path(kernel, process.kernel_pid, dir_fd, path)?; + kernel + .remove_dir(&path) + .map(|()| Value::Null) + .map_err(kernel_error) + } + "process.path_rename_at" => { + let old_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "path_rename_at old fd")?; + let old_path = + javascript_sync_rpc_arg_str(&request.args, 1, "path_rename_at old path")?; + let new_fd = javascript_sync_rpc_arg_u32(&request.args, 2, "path_rename_at new fd")?; + let new_path = + javascript_sync_rpc_arg_str(&request.args, 3, "path_rename_at new path")?; + let old_path = + wasm_process_resolve_at_path(kernel, process.kernel_pid, old_fd, old_path)?; + let new_path = + wasm_process_resolve_at_path(kernel, process.kernel_pid, new_fd, new_path)?; + kernel + .rename(&old_path, &new_path) + .map(|()| Value::Null) + .map_err(kernel_error) + } + "process.path_symlink_at" => { + let target = javascript_sync_rpc_arg_str(&request.args, 0, "path_symlink_at target")?; + let dir_fd = javascript_sync_rpc_arg_u32(&request.args, 1, "path_symlink_at dir fd")?; + let path = javascript_sync_rpc_arg_str(&request.args, 2, "path_symlink_at path")?; + let path = wasm_process_resolve_at_path(kernel, process.kernel_pid, dir_fd, path)?; + kernel + .symlink(target, &path) + .map(|()| Value::Null) + .map_err(kernel_error) + } + "process.path_unlink_at" => { + let dir_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "path_unlink_at dir fd")?; + let path = javascript_sync_rpc_arg_str(&request.args, 1, "path_unlink_at path")?; + let path = wasm_process_resolve_at_path(kernel, process.kernel_pid, dir_fd, path)?; + kernel + .remove_file(&path) + .map(|()| Value::Null) + .map_err(kernel_error) + } + "process.fd_snapshot" => kernel + .fd_snapshot(EXECUTION_DRIVER_NAME, process.kernel_pid) + .map(|entries| { + Value::Array( + entries + .into_iter() + .map(|entry| { + json!({ + "fd": entry.fd, + "fdFlags": entry.fd_flags, + "statusFlags": entry.status_flags, + "filetype": entry.filetype, + "kind": if entry.is_socket { + "socket" + } else if entry.is_pipe { + "pipe" + } else if entry.is_pty { + "pty" + } else { + "file" + }, + }) + }) + .collect(), + ) + }) + .map_err(kernel_error), + "process.fd_read" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_read fd")?; + let length = usize::try_from(javascript_sync_rpc_arg_u64( + &request.args, + 1, + "fd_read length", + )?) + .map_err(|_| SidecarError::InvalidState("fd_read length is too large".into()))?; + let timeout_ms = + javascript_sync_rpc_arg_u64_optional(&request.args, 2, "fd_read timeout ms")?; + match timeout_ms { + Some(timeout_ms) => kernel + .fd_read_with_timeout_result( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + length, + Some(Duration::from_millis(timeout_ms)), + ) + .map(Option::unwrap_or_default), + None => kernel.fd_read(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, length), + } + .map(|bytes| javascript_sync_rpc_bytes_value(&bytes)) + .map_err(kernel_error) + } + "process.fd_pread" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_pread fd")?; + let length = usize::try_from(javascript_sync_rpc_arg_u64( + &request.args, + 1, + "fd_pread length", + )?) + .map_err(|_| SidecarError::InvalidState("fd_pread length is too large".into()))?; + let offset = javascript_sync_rpc_arg_str(&request.args, 2, "fd_pread offset")? + .parse::() + .map_err(|_| SidecarError::InvalidState("fd_pread offset must be u64".into()))?; + kernel + .fd_pread( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + length, + offset, + ) + .map(|bytes| javascript_sync_rpc_bytes_value(&bytes)) + .map_err(kernel_error) + } + "process.fd_write" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_write fd")?; + let data = javascript_sync_rpc_bytes_arg(&request.args, 1, "fd_write data")?; + kernel + .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, &data) + .map(Value::from) + .map_err(kernel_error) + } + "process.fd_pwrite" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_pwrite fd")?; + let data = javascript_sync_rpc_bytes_arg(&request.args, 1, "fd_pwrite data")?; + let offset = javascript_sync_rpc_arg_str(&request.args, 2, "fd_pwrite offset")? + .parse::() + .map_err(|_| SidecarError::InvalidState("fd_pwrite offset must be u64".into()))?; + kernel + .fd_pwrite(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, &data, offset) + .map(Value::from) + .map_err(kernel_error) + } + "process.fd_sync" | "process.fd_datasync" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_sync fd")?; + kernel + .fd_sync(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map(|()| Value::Null) + .map_err(kernel_error) + } + "process.fd_readdir" => { + const MAX_READDIR_ENTRIES_PER_CALL: usize = 4096; + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_readdir fd")?; + let cookie = javascript_sync_rpc_arg_str(&request.args, 1, "fd_readdir cookie")? + .parse::() + .map_err(|_| { + SidecarError::InvalidState("fd_readdir cookie must be usize".into()) + })?; + let max_entries = usize::try_from(javascript_sync_rpc_arg_u64( + &request.args, + 2, + "fd_readdir max entries", + )?) + .unwrap_or(usize::MAX) + .min(MAX_READDIR_ENTRIES_PER_CALL); + kernel + .fd_read_dir_with_types(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map(|entries| { + Value::Array( + entries + .into_iter() + .enumerate() + .skip(cookie) + .take(max_entries) + .map(|(index, entry)| { + json!({ + "name": entry.name, + "ino": entry.ino.to_string(), + "filetype": if entry.is_directory { + agentos_kernel::fd_table::FILETYPE_DIRECTORY + } else if entry.is_symbolic_link { + agentos_kernel::fd_table::FILETYPE_SYMBOLIC_LINK + } else { + agentos_kernel::fd_table::FILETYPE_REGULAR_FILE + }, + "next": index.saturating_add(1).to_string(), + }) + }) + .collect(), + ) + }) + .map_err(kernel_error) + } + "process.fd_close" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_close fd")?; + kernel + .fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map(|()| Value::Null) + .map_err(kernel_error) + } + "process.fd_stat" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_stat fd")?; + kernel + .fd_stat(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map(|stat| { + json!({ + "filetype": stat.filetype, + "flags": stat.flags, + "rights": stat.rights, + }) + }) + .map_err(kernel_error) + } + "process.fd_filestat" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_filestat fd")?; + let fd_stat = kernel + .fd_stat(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map_err(kernel_error)?; + kernel + .dev_fd_stat(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map(|stat| { + json!({ + "dev": stat.dev, + "ino": stat.ino, + "filetype": fd_stat.filetype, + "nlink": stat.nlink, + "mode": stat.mode, + "size": stat.size, + "atimeMs": stat.atime_ms, + "mtimeMs": stat.mtime_ms, + "ctimeMs": stat.ctime_ms, + }) + }) + .map_err(kernel_error) + } + "process.fd_chown" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_chown fd")?; + let uid = javascript_sync_rpc_arg_u32(&request.args, 1, "fd_chown uid")?; + let gid = javascript_sync_rpc_arg_u32(&request.args, 2, "fd_chown gid")?; + kernel + .fd_chown_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, uid, gid) + .map(|()| Value::Null) + .map_err(kernel_error) + } + "process.fd_chmod" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_chmod fd")?; + let mode = javascript_sync_rpc_arg_u32(&request.args, 1, "fd_chmod mode")?; + kernel + .fd_chmod_for_process(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, mode) + .map(|()| Value::Null) + .map_err(kernel_error) + } + "process.fd_truncate" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_truncate fd")?; + let length = javascript_sync_rpc_arg_str(&request.args, 1, "fd_truncate length")? + .parse::() + .map_err(|_| SidecarError::InvalidState("fd_truncate length must be u64".into()))?; + kernel + .fd_truncate(EXECUTION_DRIVER_NAME, process.kernel_pid, fd, length) + .map(|()| Value::Null) + .map_err(kernel_error) + } + "process.fd_set_flags" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_set_flags fd")?; + let flags = javascript_sync_rpc_arg_u32(&request.args, 1, "fd_set_flags flags")?; + kernel + .fd_fcntl( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + agentos_kernel::fd_table::F_SETFL, + flags, + ) + .map(Value::from) + .map_err(kernel_error) + } + "process.fd_getfd" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_getfd fd")?; + kernel + .fd_fcntl( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + agentos_kernel::fd_table::F_GETFD, + 0, + ) + .map(Value::from) + .map_err(kernel_error) + } + "process.fd_setfd" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_setfd fd")?; + let flags = javascript_sync_rpc_arg_u32(&request.args, 1, "fd_setfd flags")?; + kernel + .fd_fcntl( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + agentos_kernel::fd_table::F_SETFD, + flags, + ) + .map(Value::from) + .map_err(kernel_error) + } + "process.fd_record_lock" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_record_lock fd")?; + let command = javascript_sync_rpc_arg_u32(&request.args, 1, "fd_record_lock command")?; + let raw_lock_type = + javascript_sync_rpc_arg_u32(&request.args, 2, "fd_record_lock type")?; + let start = javascript_sync_rpc_arg_str(&request.args, 3, "fd_record_lock start")? + .parse::() + .map_err(|_| { + SidecarError::InvalidState("EINVAL: fd_record_lock start must be u64".into()) + })?; + let length = javascript_sync_rpc_arg_str(&request.args, 4, "fd_record_lock length")? + .parse::() + .map_err(|_| { + SidecarError::InvalidState("EINVAL: fd_record_lock length must be u64".into()) + })?; + let lock_type = match raw_lock_type { + 0 => agentos_kernel::fd_table::RecordLockType::Read, + 1 => agentos_kernel::fd_table::RecordLockType::Write, + 2 => agentos_kernel::fd_table::RecordLockType::Unlock, + _ => { + return Err(SidecarError::InvalidState( + "EINVAL: fd_record_lock type must be F_RDLCK, F_WRLCK, or F_UNLCK".into(), + )) + } + }; + let conflict = match command { + 12 => kernel.fd_record_lock( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + lock_type, + start, + length, + true, + ), + 13 => kernel.fd_record_lock( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + lock_type, + start, + length, + false, + ), + 14 => kernel + .fd_record_lock_wait( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + lock_type, + start, + length, + ) + .map(|()| None), + _ => { + return Err(SidecarError::InvalidState(format!( + "EINVAL: unsupported fd_record_lock command {command}" + ))) + } + } + .map_err(kernel_error)?; + let response = conflict.map_or_else( + || json!({ "type": 2, "pid": 0, "start": start.to_string(), "length": length.to_string() }), + |lock| { + let lock_type = match lock.lock_type { + agentos_kernel::fd_table::RecordLockType::Read => 0, + agentos_kernel::fd_table::RecordLockType::Write => 1, + agentos_kernel::fd_table::RecordLockType::Unlock => 2, + }; + json!({ + "type": lock_type, + "pid": lock.pid, + "start": lock.start.to_string(), + "length": lock.length().to_string(), + }) + }, + ); + Ok(response) + } + "process.fd_record_lock_cancel" => kernel + .fd_record_lock_cancel(EXECUTION_DRIVER_NAME, process.kernel_pid) + .map(|()| Value::Null) + .map_err(kernel_error), + "process.fd_dup" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_dup fd")?; + kernel + .fd_dup(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map(Value::from) + .map_err(kernel_error) + } + "process.fd_dup2" => { + let old_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_dup2 old fd")?; + let new_fd = javascript_sync_rpc_arg_u32(&request.args, 1, "fd_dup2 new fd")?; + kernel + .fd_dup2(EXECUTION_DRIVER_NAME, process.kernel_pid, old_fd, new_fd) + .map(|()| Value::Null) + .map_err(kernel_error) + } + "process.fd_dup_min" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_dup_min fd")?; + let min_fd = javascript_sync_rpc_arg_u32(&request.args, 1, "fd_dup_min minimum")?; + kernel + .fd_fcntl( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + agentos_kernel::fd_table::F_DUPFD, + min_fd, + ) + .map(Value::from) + .map_err(kernel_error) } - "process.kill" => { - let target_pid = - javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?; - let signal = javascript_sync_rpc_arg_str(&request.args, 1, "process.kill signal")?; - let parsed_signal = parse_signal(signal)?; - if parsed_signal == 0 { - kernel - .signal_process(EXECUTION_DRIVER_NAME, target_pid, parsed_signal) - .map_err(kernel_error)?; - return Ok(Value::Null.into()); - } - let process_pid = i32::try_from(process.kernel_pid) - .map_err(|_| SidecarError::InvalidState("process pid exceeds i32".into()))?; - if target_pid != process_pid { + "process.fd_seek" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fd_seek fd")?; + let offset = javascript_sync_rpc_arg_str(&request.args, 1, "fd_seek offset")? + .parse::() + .map_err(|_| SidecarError::InvalidState("fd_seek offset must be i64".into()))?; + let whence = u8::try_from(javascript_sync_rpc_arg_u32( + &request.args, + 2, + "fd_seek whence", + )?) + .map_err(|_| SidecarError::InvalidState("fd_seek whence is invalid".into()))?; + kernel + .fd_seek( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + fd, + offset, + whence, + ) + .map(|next| Value::String(next.to_string())) + .map_err(kernel_error) + } + "process.fd_chdir_path" => { + let fd = javascript_sync_rpc_arg_u32(&request.args, 0, "fchdir fd")?; + let stat = kernel + .fd_stat(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map_err(kernel_error)?; + if stat.filetype != agentos_kernel::fd_table::FILETYPE_DIRECTORY { return Err(SidecarError::InvalidState(format!( - "unknown process pid {target_pid}" + "ENOTDIR: file descriptor {fd} is not a directory" ))); } - process.pending_self_signal_exit = None; - if parsed_signal != 0 - && !matches!( - canonical_signal_name(parsed_signal), - Some("SIGWINCH" | "SIGCHLD" | "SIGCONT" | "SIGURG") + kernel + .fd_path(EXECUTION_DRIVER_NAME, process.kernel_pid, fd) + .map(Value::String) + .map_err(kernel_error) + } + "process.fd_socketpair" => { + let socket_kind = javascript_sync_rpc_arg_u32(&request.args, 0, "socketpair kind")?; + let nonblocking = + javascript_sync_rpc_arg_bool(&request.args, 1, "socketpair nonblocking")?; + let close_on_exec = + javascript_sync_rpc_arg_bool(&request.args, 2, "socketpair close-on-exec")?; + let socket_type = match socket_kind { + 1 => SocketType::Stream, + 2 => SocketType::Datagram, + 3 => SocketType::SeqPacket, + _ => { + return Err(SidecarError::InvalidState(format!( + "unsupported socketpair kind {socket_kind}" + ))) + } + }; + kernel + .fd_socketpair( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + socket_type, + nonblocking, + close_on_exec, ) - { - process.pending_self_signal_exit = Some(parsed_signal); + .map(|(first_fd, second_fd)| json!({ "firstFd": first_fd, "secondFd": second_fd })) + .map_err(kernel_error) + } + "process.fd_sendmsg_rights" => { + let socket_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "sendmsg socket fd")?; + let data = javascript_sync_rpc_bytes_arg(&request.args, 1, "sendmsg data")?; + let raw_rights = request + .args + .get(2) + .and_then(Value::as_array) + .ok_or_else(|| { + SidecarError::InvalidState( + "sendmsg rights must be an array of file descriptors".into(), + ) + })?; + if raw_rights.len() > LINUX_SCM_MAX_FD { + return Err(SidecarError::InvalidState(format!( + "EINVAL: SCM_RIGHTS accepts at most {LINUX_SCM_MAX_FD} descriptors" + ))); } - Ok(json!({ - "self": true, - "action": "default", - })) + if let Some(limit) = resource_limits.max_open_fds { + if raw_rights.len() > limit { + return Err(SidecarError::InvalidState(format!( + "EMFILE: SCM_RIGHTS descriptor list has {} entries, exceeding limits.resources.maxOpenFds ({limit}); raise limits.resources.maxOpenFds", + raw_rights.len() + ))); + } + } + let mut rights = Vec::with_capacity(raw_rights.len()); + let mut pending_host_net_count = 0usize; + for value in raw_rights { + if let Some(fd) = value.as_u64().and_then(|fd| u32::try_from(fd).ok()) { + rights.push(FdTransferRequest::Fd(fd)); + continue; + } + if value.get("kind").and_then(Value::as_str) != Some("hostNet") { + return Err(SidecarError::InvalidState( + "sendmsg rights entries must be kernel fds or hostNet descriptions".into(), + )); + } + let source = scm_rights_host_net_source(value)?; + let transferred = if let Some(source) = source { + let transferred = prepare_transferred_host_net_resource( + kernel, + process, + &source, + value, + "SCM_RIGHTS host-network", + )?; + transferred + } else { + let options = + host_net_open_description_options(value, "SCM_RIGHTS pending socket")?; + let metadata = TransferredHostNetMetadata::pending( + value, + options, + "SCM_RIGHTS pending socket", + )?; + pending_host_net_count = pending_host_net_count.saturating_add(1); + TransferredHostNetSocket::Pending { + metadata, + description_handles: Arc::new(()), + } + }; + register_host_net_transfer_description( + &socket_paths.host_net_transfer_descriptions, + &transferred, + ); + rights.push(FdTransferRequest::Opaque(Arc::new(transferred))); + } + check_spawn_host_net_resource_limit( + resource_limits.max_sockets, + network_counts.sockets, + pending_host_net_count, + "EMFILE", + "SCM_RIGHTS socket descriptions", + "maxSockets", + )?; + check_spawn_host_net_resource_limit( + resource_limits.max_connections, + network_counts.connections, + 0, + "EAGAIN", + "SCM_RIGHTS connected socket descriptions", + "maxConnections", + )?; + kernel + .fd_socket_sendmsg_transfers( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + socket_fd, + &data, + &rights, + ) + .map(Value::from) + .map_err(kernel_error) + } + "process.fd_recvmsg_rights" => { + let socket_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "recvmsg socket fd")?; + let max_bytes = usize::try_from(javascript_sync_rpc_arg_u64( + &request.args, + 1, + "recvmsg maximum bytes", + )?) + .map_err(|_| SidecarError::InvalidState("recvmsg byte limit is too large".into()))?; + let max_rights = usize::try_from(javascript_sync_rpc_arg_u64( + &request.args, + 2, + "recvmsg maximum rights", + )?) + .map_err(|_| SidecarError::InvalidState("recvmsg rights limit is too large".into()))?; + let close_on_exec = + javascript_sync_rpc_arg_bool(&request.args, 3, "recvmsg close-on-exec")?; + let peek = request + .args + .get(4) + .and_then(Value::as_bool) + .unwrap_or(false); + let dontwait = request + .args + .get(5) + .and_then(Value::as_bool) + .unwrap_or(false); + let waitall = request + .args + .get(6) + .and_then(Value::as_bool) + .unwrap_or(false); + let message = kernel + .fd_socket_recvmsg( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + socket_fd, + max_bytes, + max_rights, + close_on_exec, + peek, + dontwait, + waitall, + ) + .map_err(kernel_error)?; + Ok(if let Some(message) = message { + let mut rights = Vec::with_capacity(message.rights.len()); + for right in message.rights { + match right { + ReceivedFdRight::Fd(fd) => { + rights.push(json!({ "kind": "kernel", "fd": fd })); + } + ReceivedFdRight::Opaque(resource) => { + let transferred = Arc::downcast::(resource) + .map_err(|_| { + SidecarError::InvalidState( + "received unknown SCM_RIGHTS resource type".into(), + ) + })?; + let transferred = match Arc::try_unwrap(transferred) { + Ok(transferred) => transferred, + Err(shared) => shared.clone_for_fd_transfer()?, + }; + match transferred { + TransferredHostNetSocket::Tcp { + mut socket, + metadata, + } => { + let new_socket_id = process.allocate_tcp_socket_id(); + socket.listener_id = None; + socket.set_event_pusher( + process.execution.javascript_v8_session_handle(), + new_socket_id.clone(), + ); + register_kernel_readiness_target( + &kernel_readiness, + socket.kernel_socket_id, + process.execution.javascript_v8_session_handle(), + new_socket_id.clone(), + KernelSocketReadinessEvent::Data, + ); + let local = socket.guest_local_addr; + let remote = socket.guest_remote_addr; + process.tcp_sockets.insert(new_socket_id.clone(), socket); + rights.push(transferred_hostnet_value( + "tcp", + metadata, + Some(("socketId", new_socket_id)), + Some(local), + Some(remote), + )); + } + TransferredHostNetSocket::TcpListener { listener, metadata } => { + let listener_id = process.allocate_tcp_listener_id(); + let local = listener.guest_local_addr(); + register_kernel_readiness_target( + &kernel_readiness, + listener.kernel_socket_id, + process.execution.javascript_v8_session_handle(), + listener_id.clone(), + KernelSocketReadinessEvent::Accept, + ); + process.tcp_listeners.insert(listener_id.clone(), listener); + rights.push(transferred_hostnet_value( + "listener", + metadata, + Some(("serverId", listener_id)), + Some(local), + None, + )); + } + TransferredHostNetSocket::Udp { socket, metadata } => { + let socket_id = process.allocate_udp_socket_id(); + let local = socket.guest_local_addr; + register_kernel_readiness_target( + &kernel_readiness, + socket.kernel_socket_id, + process.execution.javascript_v8_session_handle(), + socket_id.clone(), + KernelSocketReadinessEvent::Datagram, + ); + process.udp_sockets.insert(socket_id.clone(), socket); + rights.push(transferred_hostnet_value( + "udp", + metadata, + Some(("udpSocketId", socket_id)), + local, + None, + )); + } + TransferredHostNetSocket::Unix { + mut socket, + metadata, + } => { + let socket_id = process.allocate_unix_socket_id(); + socket.listener_id = None; + process.unix_sockets.insert(socket_id.clone(), socket); + rights.push(transferred_hostnet_value( + "unix", + metadata, + Some(("socketId", socket_id)), + None, + None, + )); + } + TransferredHostNetSocket::UnixListener { listener, metadata } => { + let listener_id = process.allocate_unix_listener_id(); + process.unix_listeners.insert(listener_id.clone(), listener); + rights.push(transferred_hostnet_value( + "unix-listener", + metadata, + Some(("serverId", listener_id)), + None, + None, + )); + } + TransferredHostNetSocket::Pending { metadata, .. } => { + rights.push(transferred_hostnet_value( + "pending", metadata, None, None, None, + )); + } + } + } + } + } + json!({ + "data": javascript_sync_rpc_bytes_value(&message.payload), + "rights": rights, + "payloadTruncated": message.payload_truncated, + "controlTruncated": message.control_truncated, + "fullLength": message.full_length, + }) + } else { + json!({ + "data": javascript_sync_rpc_bytes_value(&[]), + "rights": [], + "payloadTruncated": false, + "controlTruncated": false, + "fullLength": 0, + }) + }) + } + "process.fd_socket_shutdown" => { + let socket_fd = javascript_sync_rpc_arg_u32(&request.args, 0, "shutdown socket fd")?; + let how = match javascript_sync_rpc_arg_u32(&request.args, 1, "shutdown mode")? { + 0 => KernelSocketShutdown::Read, + 1 => KernelSocketShutdown::Write, + 2 => KernelSocketShutdown::Both, + other => { + return Err(SidecarError::InvalidState(format!( + "invalid shutdown mode {other}" + ))) + } + }; + kernel + .fd_socket_shutdown(EXECUTION_DRIVER_NAME, process.kernel_pid, socket_fd, how) + .map(|()| Value::Null) + .map_err(kernel_error) } "process.umask" => { let new_mask = javascript_sync_rpc_arg_u32_optional(&request.args, 0, "process umask")?; @@ -17033,6 +26668,34 @@ where .map(|mask| json!(mask)) .map_err(kernel_error) } + "process.getpgid" => { + let requested_pid = + javascript_sync_rpc_arg_u32(&request.args, 0, "process getpgid pid")?; + let target_pid = if requested_pid == 0 { + process.kernel_pid + } else { + requested_pid + }; + kernel + .getpgid(EXECUTION_DRIVER_NAME, target_pid) + .map(|pgid| json!(pgid)) + .map_err(kernel_error) + } + "process.setpgid" => { + let requested_pid = + javascript_sync_rpc_arg_u32(&request.args, 0, "process setpgid pid")?; + let pgid = + javascript_sync_rpc_arg_u32(&request.args, 1, "process setpgid process group")?; + let target_pid = if requested_pid == 0 { + process.kernel_pid + } else { + requested_pid + }; + kernel + .setpgid(EXECUTION_DRIVER_NAME, target_pid, pgid) + .map(|()| Value::Null) + .map_err(kernel_error) + } "fs.chmodSync" | "fs.promises.chmod" => { let response = service_javascript_fs_sync_rpc(kernel, process, process.kernel_pid, request)?; @@ -19341,6 +29004,7 @@ fn service_javascript_kernel_stdin_sync_rpc( process: &mut ActiveProcess, request: &JavascriptSyncRpcRequest, ) -> Result { + flush_pending_kernel_stdin(kernel, process)?; let (max_bytes, timeout_ms) = parse_kernel_stdin_read_args(request)?; kernel_stdin_read_response( kernel, @@ -19652,7 +29316,10 @@ pub(crate) fn kernel_poll_response( })) } -fn install_kernel_stdin_pipe(kernel: &mut SidecarKernel, pid: u32) -> Result { +pub(crate) fn install_kernel_stdin_pipe( + kernel: &mut SidecarKernel, + pid: u32, +) -> Result { let (read_fd, write_fd) = kernel .open_pipe(EXECUTION_DRIVER_NAME, pid) .map_err(kernel_error)?; @@ -19662,6 +29329,36 @@ fn install_kernel_stdin_pipe(kernel: &mut SidecarKernel, pid: u32) -> Result git remote-https -> git-remote-https). + kernel + .fd_fcntl( + EXECUTION_DRIVER_NAME, + pid, + write_fd, + agentos_kernel::fd_table::F_SETFD, + agentos_kernel::fd_table::FD_CLOEXEC, + ) + .map_err(kernel_error)?; + // Non-blocking writes only: the sidecar dispatch thread feeds this pipe + // from `write_kernel_process_stdin` / `flush_pending_kernel_stdin`, and a + // blocking pipe write would deadlock — the child's pipe reads are parked + // sync RPCs serviced by this same thread. A full pipe surfaces EAGAIN and + // the remainder is queued in `ActiveProcess::pending_kernel_stdin`. + kernel + .fd_fcntl( + EXECUTION_DRIVER_NAME, + pid, + write_fd, + agentos_kernel::fd_table::F_SETFL, + agentos_kernel::fd_table::O_NONBLOCK, + ) + .map_err(kernel_error)?; Ok(write_fd) } @@ -19690,6 +29387,7 @@ pub(crate) fn write_kernel_process_stdin( kernel: &mut SidecarKernel, process: &mut ActiveProcess, chunk: &[u8], + pending_stdin_limit: usize, ) -> Result<(), SidecarError> { // Non-TTY JavaScript uses the in-process local stdin bridge, not a kernel // fd; a TTY JavaScript process (tty_master_fd set) DOES route through the @@ -19701,20 +29399,151 @@ pub(crate) fn write_kernel_process_stdin( let Some(writer_fd) = process.kernel_stdin_writer_fd else { return Ok(()); }; - kernel - .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, chunk) - .map_err(kernel_error)?; - // For a TTY process the master write above drives line-discipline echo into - // the master output buffer. Drain it now and surface it as the single - // ordered Stdout stream so typed-character echo reaches the host even while - // the guest is blocked in read() and not producing output of its own. - if let Some(echo) = drain_tty_master_output(kernel, process)? { - process.queue_pending_execution_event(ActiveExecutionEvent::Stdout(echo))?; - } - forward_tty_slave_input_to_javascript(kernel, process)?; + if process.tty_master_fd.is_some() { + kernel + .fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, chunk) + .map_err(kernel_error)?; + // For a TTY process the master write above drives line-discipline echo + // into the master output buffer. Drain it now and surface it as the + // single ordered Stdout stream so typed-character echo reaches the + // host even while the guest is blocked in read() and not producing + // output of its own. + if let Some(echo) = drain_tty_master_output(kernel, process)? { + process.queue_pending_execution_event(ActiveExecutionEvent::Stdout(echo))?; + } + forward_tty_slave_input_to_javascript(kernel, process)?; + return Ok(()); + } + // Pipe-backed stdin. The kernel pipe caps at MAX_PIPE_BUFFER_BYTES and + // fd_write reports POSIX partial writes, so anything the pipe cannot take + // right now is queued and flushed as the child drains (see + // `flush_pending_kernel_stdin`). Silently dropping the remainder is how + // multi-buffer stdin payloads (e.g. git's spooled pack piped into + // `index-pack --stdin`) used to truncate at 64 KiB. + let pending_total = process.pending_kernel_stdin.total; + if pending_total.saturating_add(chunk.len()) > pending_stdin_limit { + return Err(SidecarError::InvalidState(format!( + "child process stdin backlog limit exceeded: {pending_total} pending + {} new bytes \ + > {pending_stdin_limit} (limits.process.pendingStdinBytes); the child is not \ + draining stdin — write smaller chunks after the child consumes them or raise \ + limits.process.pendingStdinBytes", + chunk.len(), + ))); + } + if !process + .vm_pending_stdin_bytes_budget + .try_reserve(chunk.len()) + { + return Err(SidecarError::InvalidState(format!( + "VM child process stdin backlogs exceeded {} retained bytes \ + (limits.process.pendingStdinBytes); wait for a child to drain stdin or raise \ + limits.process.pendingStdinBytes", + process.vm_pending_stdin_bytes_budget.limit() + ))); + } + process.pending_kernel_stdin.push(chunk); + process + .pending_kernel_stdin_gauge + .observe_depth(process.pending_kernel_stdin.total); + flush_pending_kernel_stdin(kernel, process) +} + +/// Write as much queued stdin as the child's kernel pipe can take right now. +/// Called on every stdin write and from the child kernel-wait servicing path +/// (each parked `__kernel_stdin_read` / `__kernel_poll` re-check), so the +/// backlog drains in lockstep with the child's reads. Executes the deferred +/// stdin close once the backlog is empty. +pub(crate) fn flush_pending_kernel_stdin( + kernel: &mut SidecarKernel, + process: &mut ActiveProcess, +) -> Result<(), SidecarError> { + if process.tty_master_fd.is_some() { + return Ok(()); + } + let Some(writer_fd) = process.kernel_stdin_writer_fd else { + clear_pending_kernel_stdin(process); + process.pending_kernel_stdin_gauge.observe_depth(0); + process.pending_kernel_stdin.close_requested = false; + return Ok(()); + }; + while let Some(front) = process.pending_kernel_stdin.chunks.pop_front() { + let offset = process.pending_kernel_stdin.front_offset; + let slice = &front[offset..]; + match kernel.fd_write(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd, slice) { + Ok(written) if written >= slice.len() => { + process.pending_kernel_stdin.total = process + .pending_kernel_stdin + .total + .saturating_sub(slice.len()); + process.vm_pending_stdin_bytes_budget.release(slice.len()); + process.pending_kernel_stdin.front_offset = 0; + process + .pending_kernel_stdin_gauge + .observe_depth(process.pending_kernel_stdin.total); + } + Ok(written) => { + // Pipe is full mid-chunk; keep the remainder queued. + process.pending_kernel_stdin.total = + process.pending_kernel_stdin.total.saturating_sub(written); + process.vm_pending_stdin_bytes_budget.release(written); + process.pending_kernel_stdin.front_offset = offset + written; + process.pending_kernel_stdin.chunks.push_front(front); + process + .pending_kernel_stdin_gauge + .observe_depth(process.pending_kernel_stdin.total); + break; + } + Err(error) if error.code() == "EAGAIN" => { + process.pending_kernel_stdin.chunks.push_front(front); + break; + } + Err(error) if error.code() == "EPIPE" => { + // Reader side is gone (child exited or closed stdin). Release + // the writer, but preserve Linux write(2) behavior by returning + // EPIPE to the caller instead of reporting that discarded + // backlog bytes were accepted. + clear_pending_kernel_stdin(process); + process.pending_kernel_stdin_gauge.observe_depth(0); + process.pending_kernel_stdin.close_requested = false; + process.kernel_stdin_writer_fd = None; + if let Err(close_error) = + kernel.fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd) + { + tracing::warn!( + process_id = process.kernel_pid, + fd = writer_fd, + error = %close_error, + "failed to close child stdin after EPIPE" + ); + } + return Err(kernel_error(error)); + } + Err(error) => { + process.pending_kernel_stdin.chunks.push_front(front); + return Err(kernel_error(error)); + } + } + } + if process.pending_kernel_stdin.is_empty() && process.pending_kernel_stdin.close_requested { + process.pending_kernel_stdin.close_requested = false; + if let Some(writer_fd) = process.kernel_stdin_writer_fd.take() { + kernel + .fd_close(EXECUTION_DRIVER_NAME, process.kernel_pid, writer_fd) + .map_err(kernel_error)?; + } + } + process + .pending_kernel_stdin_gauge + .observe_depth(process.pending_kernel_stdin.total); Ok(()) } +fn clear_pending_kernel_stdin(process: &mut ActiveProcess) { + let pending_bytes = process.pending_kernel_stdin.total; + process.pending_kernel_stdin.clear(); + process.vm_pending_stdin_bytes_budget.release(pending_bytes); +} + /// For a TTY JavaScript guest, cooked input becomes readable on the PTY slave /// only after line discipline runs (on newline/VEOF in canonical mode; every /// byte in raw mode). The V8 isolate has no kernel-fd read loop of its own — @@ -19758,6 +29587,13 @@ pub(crate) fn close_kernel_process_stdin( kernel: &mut SidecarKernel, process: &mut ActiveProcess, ) -> Result<(), SidecarError> { + if !process.pending_kernel_stdin.is_empty() && process.kernel_stdin_writer_fd.is_some() { + // Queued stdin has not reached the pipe yet; closing now would hand + // the child a premature EOF. `flush_pending_kernel_stdin` performs the + // close once the backlog drains. + process.pending_kernel_stdin.close_requested = true; + return Ok(()); + } let Some(writer_fd) = process.kernel_stdin_writer_fd.take() else { return Ok(()); }; @@ -19977,7 +29813,11 @@ where match event { ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => { - let network_counts = process.network_resource_counts(); + let network_counts = process_network_resource_counts_with_transfers( + kernel, + process, + &socket_paths.host_net_transfer_descriptions, + ); let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest { bridge, vm_id, @@ -20334,6 +30174,7 @@ fn issue_outbound_http_request( options: &JavascriptHttpRequestOptions, headers: &HttpHeaderCollection, pinned_addresses: &[IpAddr], + default_ca_bundle: &[u8], ) -> Result { let method = options.method.as_deref().unwrap_or("GET"); // Pin the underlying resolver to the egress-vetted addresses. ureq performs @@ -20383,7 +30224,10 @@ fn issue_outbound_http_request( reject_unauthorized: options.reject_unauthorized, ..JavascriptTlsBridgeOptions::default() }; - agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config(&tls_options)?)); + agent_builder = agent_builder.tls_config(Arc::new(build_client_tls_config( + &tls_options, + default_ca_bundle, + )?)); } let agent = agent_builder.build(); let mut request = agent.request_url(method, url); @@ -20454,7 +30298,11 @@ where match event { ActiveExecutionEvent::JavascriptSyncRpcRequest(request) => { - let network_counts = process.network_resource_counts(); + let network_counts = process_network_resource_counts_with_transfers( + kernel, + process, + &socket_paths.host_net_transfer_descriptions, + ); let response = service_javascript_sync_rpc(JavascriptSyncRpcServiceRequest { bridge, vm_id, @@ -20621,7 +30469,7 @@ where .collect(), )) } - "dns.resolve" | "dns.resolve4" | "dns.resolve6" => { + "dns.resolve" | "dns.resolve4" | "dns.resolve6" | "dns.resolveRawRr" => { let payload = request .args .first() @@ -20646,16 +30494,91 @@ where .to_ascii_uppercase(), }; let record_type = parse_dns_record_type(&requested_type)?; - let resolution = resolve_dns_records( - bridge, - kernel, - vm_id, - dns, - &payload.hostname, - record_type, - DnsLookupPolicy::CheckPermissions, - )?; - dns_resolution_to_node_value(&resolution, &requested_type) + if request.method == "dns.resolveRawRr" { + if !matches!(requested_type.as_str(), "PTR" | "SSHFP") { + return Err(SidecarError::InvalidState(format!( + "EINVAL: raw DNS RR bridge does not support {requested_type}" + ))); + } + let resolution = match kernel.resolve_dns_records( + &payload.hostname, + record_type, + DnsLookupPolicy::CheckPermissions, + ) { + Ok(resolution) => { + emit_dns_record_resolution_event( + bridge, + vm_id, + &payload.hostname, + &resolution, + dns, + ); + resolution + } + Err(error) if matches!(error.code(), "ENOENT" | "ENODATA") => { + let status = if error.code() == "ENOENT" { + "nxdomain" + } else { + "nodata" + }; + return Ok(json!({ + "status": status, + "records": [], + })); + } + Err(error) => { + let sidecar_error = kernel_error(error.clone()); + if error.code() != "EACCES" { + emit_dns_resolution_failure_event( + bridge, + vm_id, + &payload.hostname, + dns, + &sidecar_error, + ); + } + return Err(sidecar_error); + } + }; + let records: Vec = resolution + .records() + .iter() + .filter_map(|record| { + let data = match record.data() { + RData::PTR(name) if requested_type == "PTR" => { + normalize_dns_name_for_node(&name.0).into_bytes() + } + RData::SSHFP(sshfp) if requested_type == "SSHFP" => { + let mut data = Vec::with_capacity(sshfp.fingerprint.len() + 2); + data.push(sshfp.algorithm.into()); + data.push(sshfp.fingerprint_type.into()); + data.extend_from_slice(&sshfp.fingerprint); + data + } + _ => return None, + }; + Some(json!({ + "data": base64::engine::general_purpose::STANDARD.encode(data), + "ttl": record.ttl(), + })) + }) + .collect(); + Ok(json!({ + "status": "ok", + "records": records, + })) + } else { + let resolution = resolve_dns_records( + bridge, + kernel, + vm_id, + dns, + &payload.hostname, + record_type, + DnsLookupPolicy::CheckPermissions, + )?; + dns_resolution_to_node_value(&resolution, &requested_type) + } } other => Err(SidecarError::InvalidState(format!( "unsupported JavaScript dns sync RPC method {other}" @@ -20833,7 +30756,7 @@ where SidecarError::InvalidState(format!("unknown UDP socket {socket_id}")) })?; unregister_kernel_readiness_target(&kernel_readiness, socket.kernel_socket_id); - socket.close(kernel, process.kernel_pid); + socket.close(kernel, process.kernel_pid)?; Ok(Value::Null) } "dgram.address" => { @@ -21348,6 +31271,7 @@ fn spawn_http2_client_session( session_id: u64, remote_addr: SocketAddr, tls: Option, + default_ca_bundle: Vec, snapshot: Arc>, mut command_rx: UnboundedReceiver, ) { @@ -21460,7 +31384,7 @@ fn spawn_http2_client_session( return; } }; - let connector = match build_client_tls_config(options) { + let connector = match build_client_tls_config(options, &default_ca_bundle) { Ok(config) => TlsConnector::from(Arc::new(config)), Err(error) => { push_http2_session_event( @@ -22592,10 +32516,22 @@ fn spawn_http2_server_accept_loop( ); } Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { - wait_fd_readable_until( + if let Err(wait_error) = wait_fd_readable_until( listener.as_fd(), Instant::now() + Duration::from_millis(100), - ); + ) { + push_http2_server_event( + &shared, + server_id, + Http2BridgeEvent { + kind: String::from("serverStreamError"), + id: server_id, + data: Some(http2_error_payload(wait_error.to_string())), + ..Http2BridgeEvent::default() + }, + ); + thread::sleep(HTTP2_POLL_DELAY); + } } Err(error) => { push_http2_server_event( @@ -22956,20 +32892,26 @@ where state.session_events.entry(session_id).or_default(); session_id }; + let tls = if secure { + Some(payload.tls.unwrap_or(JavascriptTlsBridgeOptions { + is_server: false, + servername: Some(host.to_string()), + alpn_protocols: Some(vec![String::from("h2")]), + ..JavascriptTlsBridgeOptions::default() + })) + } else { + None + }; + let default_ca_bundle = match tls.as_ref() { + Some(options) => vm_default_ca_bundle_for_tls_options(kernel, options)?, + None => Vec::new(), + }; spawn_http2_client_session( Arc::clone(&process.http2.shared), session_id, resolved.actual_addr, - if secure { - Some(payload.tls.unwrap_or(JavascriptTlsBridgeOptions { - is_server: false, - servername: Some(host.to_string()), - alpn_protocols: Some(vec![String::from("h2")]), - ..JavascriptTlsBridgeOptions::default() - })) - } else { - None - }, + tls, + default_ca_bundle, Arc::clone(&snapshot), command_rx, ); @@ -23268,6 +33210,23 @@ fn resolve_http2_file_response_guest_path(process: &ActiveProcess, path: &str) - } } +fn resolve_guest_unix_path( + process: &ActiveProcess, + path: &str, +) -> Result<(String, String), SidecarError> { + if path.as_bytes().len() > 108 { + return Err(sidecar_net_error(std::io::Error::from_raw_os_error( + libc::ENAMETOOLONG, + ))); + } + let resolved = if Path::new(path).is_absolute() { + normalize_path(path) + } else { + normalize_path(&format!("{}/{}", process.guest_cwd, path)) + }; + Ok((resolved, path.to_owned())) +} + pub(crate) fn clamp_javascript_net_poll_wait(wait_ms: u64) -> Duration { // WASM net.poll runs on the sidecar's sync-RPC main thread. Guest-controlled waits // must stay bounded so one VM cannot stall dispose/shutdown or unrelated VM work. @@ -23278,6 +33237,21 @@ pub(crate) fn clamp_javascript_net_poll_wait(wait_ms: u64) -> Duration { } } +fn javascript_unix_socket_buffer_limit(resource_limits: &ResourceLimits) -> usize { + resource_limits + .max_socket_buffered_bytes + .unwrap_or(DEFAULT_MAX_SOCKET_BUFFERED_BYTES) + .max(1) +} + +fn javascript_unix_socket_write_timeout(resource_limits: &ResourceLimits) -> Duration { + Duration::from_millis( + resource_limits + .max_blocking_read_ms + .unwrap_or(DEFAULT_BLOCKING_READ_TIMEOUT_MS), + ) +} + fn service_javascript_net_sync_rpc_response( request: JavascriptNetSyncRpcServiceRequest<'_, B>, ) -> Result @@ -23297,6 +33271,23 @@ where } = request; let trace_enabled = net_tcp_trace_enabled(&process.env); let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?; + let max_bytes = usize::try_from( + javascript_sync_rpc_arg_u64_optional(&request.args, 1, "net.socket_read maximum bytes")? + .unwrap_or(64 * 1024), + ) + .map_err(|_| SidecarError::InvalidState("net.socket_read byte limit is too large".into()))?; + let peek = request + .args + .get(2) + .and_then(Value::as_bool) + .unwrap_or(false); + let wait_ms = javascript_sync_rpc_arg_u64_optional( + &request.args, + 3, + "net.socket_read wait milliseconds", + )? + .unwrap_or_default(); + let wait = clamp_javascript_net_poll_wait(wait_ms); if trace_enabled { NET_TCP_TRACE_COUNTERS .socket_read_calls @@ -23307,13 +33298,20 @@ where } let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) { - socket.poll(kernel, process.kernel_pid, Duration::ZERO, trace_enabled)? + socket.poll_readable( + kernel, + process.kernel_pid, + wait, + max_bytes, + peek, + trace_enabled, + )? } else { let socket = process .unix_sockets .get_mut(socket_id) .ok_or_else(|| SidecarError::InvalidState(format!("unknown net socket {socket_id}")))?; - socket.poll(Duration::ZERO)? + socket.poll_readable(wait, max_bytes, peek)? }; match event { @@ -23445,6 +33443,425 @@ where *pending = Some(response_json.to_owned()); Ok(Value::Null) } + "net.bind_unix" => { + check_network_resource_limit( + resource_limits.max_sockets, + network_counts.sockets, + 1, + "socket", + )?; + let payload = request + .args + .first() + .cloned() + .ok_or_else(|| { + SidecarError::InvalidState(String::from( + "net.bind_unix requires a request payload", + )) + }) + .and_then(|value| { + serde_json::from_value::(value).map_err(|error| { + SidecarError::InvalidState(format!( + "invalid net.bind_unix payload: {error}" + )) + }) + })?; + let address_kinds = usize::from(payload.path.is_some()) + + usize::from(payload.abstract_path_hex.is_some()) + + usize::from(payload.autobind); + if address_kinds != 1 || payload.bound_server_id.is_some() { + return Err(SidecarError::InvalidState(String::from( + "net.bind_unix requires exactly one Unix address", + ))); + } + bridge.require_network_access( + vm_id, + NetworkOperation::Listen, + format_unix_socket_resource( + payload.path.as_deref(), + payload.abstract_path_hex.as_deref(), + payload.autobind, + ), + )?; + + let listener_id = process.allocate_unix_listener_id(); + let registry_binding_id = guest_unix_binding_id(process.kernel_pid, &listener_id); + let mut listener = if payload.autobind { + let mut bound = None; + for nonce in 0..4096 { + let guest_name = + guest_autobind_unix_name(process.kernel_pid, &listener_id, nonce); + let host_name = host_abstract_unix_name(socket_paths, &guest_name); + let host_address_key = abstract_unix_host_address_key(&host_name); + let local_path = abstract_unix_node_path(&guest_name); + let abstract_path_hex = abstract_unix_name_hex(&guest_name); + register_guest_unix_binding( + &socket_paths.unix_bound_addresses, + ®istry_binding_id, + &host_address_key, + GuestUnixAddress { + path: local_path, + abstract_path_hex: Some(abstract_path_hex), + }, + None, + None, + )?; + match ActiveUnixListener::bind_abstract_unlistened(&host_name, &guest_name) { + Ok(listener) => { + bound = Some(listener); + break; + } + Err(error) => { + rollback_guest_unix_binding( + &socket_paths.unix_bound_addresses, + ®istry_binding_id, + )?; + if guest_errno_code(&error.to_string()) != Some("EADDRINUSE") { + return Err(error); + } + } + } + } + bound.ok_or_else(|| { + SidecarError::Execution(String::from( + "EADDRINUSE: Linux AF_UNIX autobind namespace exhausted after 4096 attempts", + )) + })? + } else if let Some(hex) = payload.abstract_path_hex.as_deref() { + let guest_name = decode_abstract_unix_name(hex)?; + let host_name = host_abstract_unix_name(socket_paths, &guest_name); + let host_address_key = abstract_unix_host_address_key(&host_name); + register_guest_unix_binding( + &socket_paths.unix_bound_addresses, + ®istry_binding_id, + &host_address_key, + GuestUnixAddress { + path: abstract_unix_node_path(&guest_name), + abstract_path_hex: Some(abstract_unix_name_hex(&guest_name)), + }, + None, + None, + )?; + match ActiveUnixListener::bind_abstract_unlistened(&host_name, &guest_name) { + Ok(listener) => listener, + Err(error) => { + rollback_guest_unix_binding( + &socket_paths.unix_bound_addresses, + ®istry_binding_id, + )?; + return Err(error); + } + } + } else { + let (candidate_path, reported_path) = resolve_guest_unix_path( + process, + payload.path.as_deref().expect("validated path"), + )?; + reject_host_mounted_unix_socket_path(socket_paths, &candidate_path)?; + let canonical_candidate = kernel + .resolve_unix_socket_bind_target_for_process( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + &process.guest_cwd, + payload.path.as_deref().expect("validated path"), + ) + .map_err(kernel_error)?; + reject_host_mounted_unix_socket_path(socket_paths, &canonical_candidate)?; + let node = kernel + .bind_unix_socket_path_for_process( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + &process.guest_cwd, + payload.path.as_deref().expect("validated path"), + ) + .map_err(kernel_error)?; + let guest_path = node.canonical_path; + let host_path = allocate_guest_socket_host_path( + socket_paths, + process.kernel_pid, + &listener_id, + &guest_path, + ); + let host_address_key = pathname_unix_host_address_key(&host_path); + if let Err(error) = register_guest_unix_binding( + &socket_paths.unix_bound_addresses, + ®istry_binding_id, + &host_address_key, + GuestUnixAddress { + path: reported_path.clone(), + abstract_path_hex: None, + }, + Some((node.stat.dev, node.stat.ino)), + Some(host_path.clone()), + ) { + if let Err(rollback_error) = kernel.remove_file(&guest_path) { + return Err(SidecarError::Execution(format!( + "{error}; failed to roll back Unix socket node {guest_path}: {}", + kernel_error(rollback_error) + ))); + } + return Err(error); + } + let mut listener = + match ActiveUnixListener::bind_unlistened(&host_path, &reported_path) { + Ok(listener) => listener, + Err(error) => { + if let Err(rollback_error) = rollback_guest_unix_path_binding( + &socket_paths.unix_bound_addresses, + ®istry_binding_id, + kernel, + &guest_path, + &host_path, + ) { + return Err(SidecarError::Execution(format!( + "{error}; rollback failed: {rollback_error}" + ))); + } + return Err(error); + } + }; + listener.private_host_path = Some(host_path.clone()); + listener.guest_node_path = Some(guest_path); + listener + }; + listener.registry_binding_id = registry_binding_id; + let local_path = listener.path.clone(); + let abstract_path_hex = listener.abstract_path_hex.clone(); + process.unix_listeners.insert(listener_id.clone(), listener); + Ok(json!({ + "serverId": listener_id, + "localPath": local_path, + "localAbstractPathHex": abstract_path_hex, + })) + } + "net.bind_connected_unix" => { + let payload = request + .args + .first() + .cloned() + .ok_or_else(|| { + SidecarError::InvalidState(String::from( + "net.bind_connected_unix requires a request payload", + )) + }) + .and_then(|value| { + serde_json::from_value::(value).map_err( + |error| { + SidecarError::InvalidState(format!( + "invalid net.bind_connected_unix payload: {error}" + )) + }, + ) + })?; + if usize::from(payload.path.is_some()) + + usize::from(payload.abstract_path_hex.is_some()) + + usize::from(payload.autobind) + != 1 + { + return Err(SidecarError::InvalidState(String::from( + "net.bind_connected_unix requires exactly one Unix address", + ))); + } + bridge.require_network_access( + vm_id, + NetworkOperation::Listen, + format_unix_socket_resource( + payload.path.as_deref(), + payload.abstract_path_hex.as_deref(), + payload.autobind, + ), + )?; + let binding_id = guest_unix_binding_id( + process.kernel_pid, + &format!("connected:{}", payload.socket_id), + ); + let socket = process + .unix_sockets + .get(&payload.socket_id) + .ok_or_else(|| { + SidecarError::InvalidState(format!("unknown Unix socket {}", payload.socket_id)) + })?; + if socket.local_registry_binding_id.is_some() { + return Err(sidecar_net_error(std::io::Error::from_raw_os_error( + libc::EINVAL, + ))); + } + let remote_registry_binding_id = socket.remote_registry_binding_id.clone(); + let peer_can_observe_late_bind = + guest_unix_connection_peer_open(socket.connection_state.as_ref()); + + if payload.autobind || payload.abstract_path_hex.is_some() { + let explicit_name = payload + .abstract_path_hex + .as_deref() + .map(decode_abstract_unix_name) + .transpose()?; + let mut bound_name = None; + let attempts = if explicit_name.is_some() { 1 } else { 4096 }; + for nonce in 0..attempts { + let guest_name = explicit_name.clone().unwrap_or_else(|| { + guest_autobind_unix_name(process.kernel_pid, &binding_id, nonce).to_vec() + }); + let host_name = host_abstract_unix_name(socket_paths, &guest_name); + let host_address_key = abstract_unix_host_address_key(&host_name); + let local_path = abstract_unix_node_path(&guest_name); + let hex = abstract_unix_name_hex(&guest_name); + register_guest_unix_binding( + &socket_paths.unix_bound_addresses, + &binding_id, + &host_address_key, + GuestUnixAddress { + path: local_path, + abstract_path_hex: Some(hex), + }, + None, + None, + )?; + if peer_can_observe_late_bind { + let target_binding_id = remote_registry_binding_id + .as_deref() + .expect("tracked Unix connection has a target binding"); + if let Err(error) = queue_guest_unix_peer( + &socket_paths.unix_bound_addresses, + &binding_id, + target_binding_id, + ) { + rollback_guest_unix_binding( + &socket_paths.unix_bound_addresses, + &binding_id, + )?; + return Err(error); + } + } + let bind_result = process + .unix_sockets + .get_mut(&payload.socket_id) + .expect("validated Unix socket remains registered") + .bind_abstract(&host_name, &guest_name, &binding_id); + match bind_result { + Ok(()) => { + bound_name = Some(guest_name); + break; + } + Err(error) => { + rollback_guest_unix_binding( + &socket_paths.unix_bound_addresses, + &binding_id, + )?; + if explicit_name.is_some() + || guest_errno_code(&error.to_string()) != Some("EADDRINUSE") + { + return Err(error); + } + } + } + } + let guest_name = bound_name.ok_or_else(|| { + sidecar_net_error(std::io::Error::from_raw_os_error(libc::EADDRINUSE)) + })?; + let local_path = abstract_unix_node_path(&guest_name); + let hex = abstract_unix_name_hex(&guest_name); + Ok(json!({ + "localPath": local_path, + "localAbstractPathHex": hex, + })) + } else { + let (candidate_path, reported_path) = resolve_guest_unix_path( + process, + payload.path.as_deref().expect("validated path"), + )?; + reject_host_mounted_unix_socket_path(socket_paths, &candidate_path)?; + let canonical_candidate = kernel + .resolve_unix_socket_bind_target_for_process( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + &process.guest_cwd, + payload.path.as_deref().expect("validated path"), + ) + .map_err(kernel_error)?; + reject_host_mounted_unix_socket_path(socket_paths, &canonical_candidate)?; + let node = kernel + .bind_unix_socket_path_for_process( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + &process.guest_cwd, + payload.path.as_deref().expect("validated path"), + ) + .map_err(kernel_error)?; + let guest_path = node.canonical_path; + let host_path = allocate_guest_socket_host_path( + socket_paths, + process.kernel_pid, + &binding_id, + &guest_path, + ); + let host_address_key = pathname_unix_host_address_key(&host_path); + if let Err(error) = register_guest_unix_binding( + &socket_paths.unix_bound_addresses, + &binding_id, + &host_address_key, + GuestUnixAddress { + path: reported_path.clone(), + abstract_path_hex: None, + }, + Some((node.stat.dev, node.stat.ino)), + Some(host_path.clone()), + ) { + if let Err(rollback_error) = kernel.remove_file(&guest_path) { + return Err(SidecarError::Execution(format!( + "{error}; failed to roll back Unix socket node {guest_path}: {}", + kernel_error(rollback_error) + ))); + } + return Err(error); + } + if peer_can_observe_late_bind { + let target_binding_id = remote_registry_binding_id + .as_deref() + .expect("tracked Unix connection has a target binding"); + if let Err(error) = queue_guest_unix_peer( + &socket_paths.unix_bound_addresses, + &binding_id, + target_binding_id, + ) { + if let Err(rollback_error) = rollback_guest_unix_path_binding( + &socket_paths.unix_bound_addresses, + &binding_id, + kernel, + &guest_path, + &host_path, + ) { + return Err(SidecarError::Execution(format!( + "{error}; rollback failed: {rollback_error}" + ))); + } + return Err(error); + } + } + let bind_result = process + .unix_sockets + .get_mut(&payload.socket_id) + .expect("validated Unix socket remains registered") + .bind_path(&host_path, &reported_path, &binding_id); + if let Err(error) = bind_result { + if let Err(rollback_error) = rollback_guest_unix_path_binding( + &socket_paths.unix_bound_addresses, + &binding_id, + kernel, + &guest_path, + &host_path, + ) { + return Err(SidecarError::Execution(format!( + "{error}; rollback failed: {rollback_error}" + ))); + } + return Err(error); + } + Ok(json!({ + "localPath": reported_path, + })) + } + } "net.reserve_tcp_port" => { let payload = request .args @@ -23494,12 +33911,6 @@ where Ok(Value::Null) } "net.connect" => { - check_network_resource_limit( - resource_limits.max_sockets, - network_counts.sockets, - 1, - "socket", - )?; check_network_resource_limit( resource_limits.max_connections, network_counts.connections, @@ -23520,10 +33931,128 @@ where SidecarError::InvalidState(format!("invalid net.connect payload: {error}")) }) })?; - if let Some(path) = payload.path.as_deref() { - let guest_path = normalize_path(path); - let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path); - let socket = ActiveUnixSocket::connect(&host_path, &guest_path)?; + check_network_resource_limit( + resource_limits.max_sockets, + network_counts.sockets, + usize::from(payload.bound_server_id.is_none()), + "socket", + )?; + if payload.path.is_some() && payload.abstract_path_hex.is_some() { + return Err(SidecarError::InvalidState(String::from( + "net.connect accepts either path or abstractPathHex, not both", + ))); + } + if payload.path.is_some() || payload.abstract_path_hex.is_some() { + bridge.require_network_access( + vm_id, + NetworkOperation::Http, + format_unix_socket_resource( + payload.path.as_deref(), + payload.abstract_path_hex.as_deref(), + false, + ), + )?; + } + if let Some(hex) = payload.abstract_path_hex.as_deref() { + let guest_name = decode_abstract_unix_name(hex)?; + let host_name = host_abstract_unix_name(socket_paths, &guest_name); + let target_host_address_key = abstract_unix_host_address_key(&host_name); + let target_binding_id = guest_unix_binding_for_host_key( + &socket_paths.unix_bound_addresses, + &target_host_address_key, + )? + .map(|(binding_id, _)| binding_id) + .ok_or_else(|| { + sidecar_net_error(std::io::Error::from_raw_os_error(libc::ECONNREFUSED)) + })?; + let mut socket = if let Some(bound_id) = payload.bound_server_id.as_deref() { + let bound = process.unix_listeners.get_mut(bound_id).ok_or_else(|| { + SidecarError::InvalidState(format!("unknown bound Unix socket {bound_id}")) + })?; + let source_binding_id = bound.registry_binding_id.clone(); + let socket = bound.connect_bound_abstract( + &host_name, + &guest_name, + &source_binding_id, + javascript_unix_socket_buffer_limit(resource_limits), + )?; + queue_guest_unix_peer( + &socket_paths.unix_bound_addresses, + &source_binding_id, + &target_binding_id, + )?; + process.unix_listeners.remove(bound_id); + socket + } else { + ActiveUnixSocket::connect_abstract( + &host_name, + &guest_name, + javascript_unix_socket_buffer_limit(resource_limits), + )? + }; + socket.connection_state = Some(register_guest_unix_connection( + &socket_paths.unix_bound_addresses, + &target_binding_id, + )?); + socket.remote_registry_binding_id = Some(target_binding_id); + let socket_id = process.allocate_unix_socket_id(); + socket.set_event_pusher( + process.execution.javascript_v8_session_handle(), + socket_id.clone(), + ); + process.unix_sockets.insert(socket_id.clone(), socket); + Ok(json!({ + "socketId": socket_id, + "remotePath": abstract_unix_node_path(&guest_name), + "remoteAbstractPathHex": abstract_unix_name_hex(&guest_name), + })) + } else if let Some(path) = payload.path.as_deref() { + let (candidate_path, _requested_path) = resolve_guest_unix_path(process, path)?; + reject_host_mounted_unix_socket_path(socket_paths, &candidate_path)?; + let node = kernel + .resolve_unix_socket_connect_target_for_process( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + &process.guest_cwd, + path, + ) + .map_err(kernel_error)?; + reject_host_mounted_unix_socket_path(socket_paths, &node.canonical_path)?; + let (host_path, target_binding_id, target_address) = + guest_unix_path_target(socket_paths, (node.stat.dev, node.stat.ino))? + .ok_or_else(|| { + sidecar_net_error(std::io::Error::from_raw_os_error(libc::ECONNREFUSED)) + })?; + let mut socket = if let Some(bound_id) = payload.bound_server_id.as_deref() { + let bound = process.unix_listeners.get_mut(bound_id).ok_or_else(|| { + SidecarError::InvalidState(format!("unknown bound Unix socket {bound_id}")) + })?; + let source_binding_id = bound.registry_binding_id.clone(); + let socket = bound.connect_bound_path( + &host_path, + &target_address.path, + &source_binding_id, + javascript_unix_socket_buffer_limit(resource_limits), + )?; + queue_guest_unix_peer( + &socket_paths.unix_bound_addresses, + &source_binding_id, + &target_binding_id, + )?; + process.unix_listeners.remove(bound_id); + socket + } else { + ActiveUnixSocket::connect( + &host_path, + &target_address.path, + javascript_unix_socket_buffer_limit(resource_limits), + )? + }; + socket.connection_state = Some(register_guest_unix_connection( + &socket_paths.unix_bound_addresses, + &target_binding_id, + )?); + socket.remote_registry_binding_id = Some(target_binding_id); let socket_id = process.allocate_unix_socket_id(); socket.set_event_pusher( process.execution.javascript_v8_session_handle(), @@ -23532,9 +34061,14 @@ where process.unix_sockets.insert(socket_id.clone(), socket); Ok(json!({ "socketId": socket_id, - "remotePath": guest_path, + "remotePath": target_address.path, })) } else { + if payload.bound_server_id.is_some() { + return Err(SidecarError::InvalidState(String::from( + "net.connect boundServerId requires a Unix address", + ))); + } let port = payload.port.ok_or_else(|| { SidecarError::InvalidState(String::from( "net.connect requires either a path or port", @@ -23639,12 +34173,6 @@ where } } "net.listen" => { - check_network_resource_limit( - resource_limits.max_sockets, - network_counts.sockets, - 1, - "socket", - )?; let payload = request .args .first() @@ -23670,30 +34198,157 @@ where }, ), })?; - if let Some(path) = payload.path.as_deref() { - let guest_path = normalize_path(path); - if kernel.exists(&guest_path).map_err(kernel_error)? { - return Err(sidecar_net_error(std::io::Error::from_raw_os_error( - libc::EADDRINUSE, + check_network_resource_limit( + resource_limits.max_sockets, + network_counts.sockets, + usize::from(payload.bound_server_id.is_none()), + "socket", + )?; + if payload.path.is_some() && payload.abstract_path_hex.is_some() { + return Err(SidecarError::InvalidState(String::from( + "net.listen accepts either path or abstractPathHex, not both", + ))); + } + if let Some(listener_id) = payload.bound_server_id.as_deref() { + if payload.path.is_some() || payload.abstract_path_hex.is_some() || payload.autobind + { + return Err(SidecarError::InvalidState(String::from( + "net.listen boundServerId cannot be combined with an address", ))); } - - let host_path = resolve_guest_socket_host_path(socket_paths, &guest_path); - let on_host_mount = - host_mount_path_for_guest_path_from_mounts(&socket_paths.mounts, &guest_path) - .is_some(); - let listener = ActiveUnixListener::bind(&host_path, &guest_path, payload.backlog)?; - if !on_host_mount { - ensure_kernel_parent_directories(kernel, &guest_path)?; - kernel - .write_file(&guest_path, Vec::new()) - .map_err(kernel_error)?; - } + let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| { + SidecarError::InvalidState(format!("unknown bound Unix socket {listener_id}")) + })?; + listener.listen(payload.backlog)?; + Ok(json!({ + "serverId": listener_id, + "localPath": listener.path, + "localAbstractPathHex": listener.abstract_path_hex, + })) + } else if let Some(hex) = payload.abstract_path_hex.as_deref() { + bridge.require_network_access( + vm_id, + NetworkOperation::Listen, + format_unix_socket_resource(None, Some(hex), false), + )?; + let guest_name = decode_abstract_unix_name(hex)?; + let host_name = host_abstract_unix_name(socket_paths, &guest_name); + let listener_id = process.allocate_unix_listener_id(); + let registry_binding_id = guest_unix_binding_id(process.kernel_pid, &listener_id); + let host_address_key = abstract_unix_host_address_key(&host_name); + let local_path = abstract_unix_node_path(&guest_name); + let abstract_path_hex = abstract_unix_name_hex(&guest_name); + register_guest_unix_binding( + &socket_paths.unix_bound_addresses, + ®istry_binding_id, + &host_address_key, + GuestUnixAddress { + path: local_path.clone(), + abstract_path_hex: Some(abstract_path_hex.clone()), + }, + None, + None, + )?; + let mut listener = match ActiveUnixListener::bind_abstract( + &host_name, + &guest_name, + payload.backlog, + ) { + Ok(listener) => listener, + Err(error) => { + rollback_guest_unix_binding( + &socket_paths.unix_bound_addresses, + ®istry_binding_id, + )?; + return Err(error); + } + }; + listener.registry_binding_id = registry_binding_id; + process.unix_listeners.insert(listener_id.clone(), listener); + Ok(json!({ + "serverId": listener_id, + "localPath": local_path, + "localAbstractPathHex": Some(abstract_path_hex), + })) + } else if let Some(path) = payload.path.as_deref() { + bridge.require_network_access( + vm_id, + NetworkOperation::Listen, + format_unix_socket_resource(Some(path), None, false), + )?; + let (candidate_path, reported_path) = resolve_guest_unix_path(process, path)?; let listener_id = process.allocate_unix_listener_id(); + let registry_binding_id = guest_unix_binding_id(process.kernel_pid, &listener_id); + reject_host_mounted_unix_socket_path(socket_paths, &candidate_path)?; + let canonical_candidate = kernel + .resolve_unix_socket_bind_target_for_process( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + &process.guest_cwd, + path, + ) + .map_err(kernel_error)?; + reject_host_mounted_unix_socket_path(socket_paths, &canonical_candidate)?; + let node = kernel + .bind_unix_socket_path_for_process( + EXECUTION_DRIVER_NAME, + process.kernel_pid, + &process.guest_cwd, + path, + ) + .map_err(kernel_error)?; + let guest_path = node.canonical_path; + let host_path = allocate_guest_socket_host_path( + socket_paths, + process.kernel_pid, + &listener_id, + &guest_path, + ); + let host_address_key = pathname_unix_host_address_key(&host_path); + if let Err(error) = register_guest_unix_binding( + &socket_paths.unix_bound_addresses, + ®istry_binding_id, + &host_address_key, + GuestUnixAddress { + path: reported_path.clone(), + abstract_path_hex: None, + }, + Some((node.stat.dev, node.stat.ino)), + Some(host_path.clone()), + ) { + if let Err(rollback_error) = kernel.remove_file(&guest_path) { + return Err(SidecarError::Execution(format!( + "{error}; failed to roll back Unix socket node {guest_path}: {}", + kernel_error(rollback_error) + ))); + } + return Err(error); + } + let mut listener = + match ActiveUnixListener::bind(&host_path, &reported_path, payload.backlog) { + Ok(listener) => listener, + Err(error) => { + if let Err(rollback_error) = rollback_guest_unix_path_binding( + &socket_paths.unix_bound_addresses, + ®istry_binding_id, + kernel, + &guest_path, + &host_path, + ) { + return Err(SidecarError::Execution(format!( + "{error}; rollback failed: {rollback_error}" + ))); + } + return Err(error); + } + }; + listener.registry_binding_id = registry_binding_id; + listener.private_host_path = Some(host_path); + listener.guest_node_path = Some(guest_path); process.unix_listeners.insert(listener_id.clone(), listener); Ok(json!({ "serverId": listener_id, - "path": guest_path, + "localPath": reported_path, })) } else { let (family, bind_host, guest_host) = @@ -23766,9 +34421,9 @@ where .unwrap_or_default(); let wait = clamp_javascript_net_poll_wait(wait_ms); let event = if let Some(socket) = process.tcp_sockets.get_mut(socket_id) { - socket.poll(kernel, process.kernel_pid, wait, trace_enabled)? + socket.poll_readiness(kernel, process.kernel_pid, wait, trace_enabled)? } else if let Some(socket) = process.unix_sockets.get_mut(socket_id) { - socket.poll(wait)? + socket.poll_readiness(wait)? } else { return Err(SidecarError::InvalidState(format!( "unknown net socket {socket_id}" @@ -23798,7 +34453,7 @@ where &kernel_readiness, ); } else if let Some(socket) = process.unix_sockets.remove(socket_id) { - release_unix_socket_handle(process, socket_id, socket); + release_unix_socket_handle(process, socket_id, socket, socket_paths); } Ok(json!({ "type": "close", @@ -23814,15 +34469,41 @@ where if let Some(socket) = process.tcp_sockets.get(socket_id) { javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect") } else { - let socket = process.unix_sockets.get(socket_id).ok_or_else(|| { + let socket = process.unix_sockets.get_mut(socket_id).ok_or_else(|| { SidecarError::InvalidState(format!("unknown net socket {socket_id}")) })?; - javascript_net_json_string(socket.socket_info(), "net.socket_wait_connect") + javascript_net_json_string( + socket.socket_info(&socket_paths.unix_bound_addresses)?, + "net.socket_wait_connect", + ) } } "net.socket_read" => { let socket_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.socket_read socket id")?; + let max_bytes = usize::try_from( + javascript_sync_rpc_arg_u64_optional( + &request.args, + 1, + "net.socket_read maximum bytes", + )? + .unwrap_or(64 * 1024), + ) + .map_err(|_| { + SidecarError::InvalidState("net.socket_read byte limit is too large".into()) + })?; + let peek = request + .args + .get(2) + .and_then(Value::as_bool) + .unwrap_or(false); + let wait_ms = javascript_sync_rpc_arg_u64_optional( + &request.args, + 3, + "net.socket_read wait milliseconds", + )? + .unwrap_or_default(); + let wait = clamp_javascript_net_poll_wait(wait_ms); if trace_enabled { NET_TCP_TRACE_COUNTERS .socket_read_calls @@ -23832,17 +34513,19 @@ where .fetch_add(1, Ordering::Relaxed); } if let Some(socket) = process.tcp_sockets.get_mut(socket_id) { - javascript_net_read_value(socket.poll( + javascript_net_read_value(socket.poll_readable( kernel, process.kernel_pid, - Duration::ZERO, + wait, + max_bytes, + peek, trace_enabled, )?) } else { let socket = process.unix_sockets.get_mut(socket_id).ok_or_else(|| { SidecarError::InvalidState(format!("unknown net socket {socket_id}")) })?; - javascript_net_read_value(socket.poll(Duration::ZERO)?) + javascript_net_read_value(socket.poll_readable(wait, max_bytes, peek)?) } } "net.socket_set_no_delay" => { @@ -23974,7 +34657,11 @@ where ) }) { if let Some(stream) = stream { - let _ = stream.shutdown(Shutdown::Both); + if let Err(error) = stream.shutdown(Shutdown::Both) { + eprintln!( + "failed to reject excess TCP socket connection: {error}" + ); + } } return Ok(json!({ "type": "error", @@ -24041,11 +34728,16 @@ where let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| { SidecarError::InvalidState(format!("unknown net listener {listener_id}")) })?; - listener.poll(Duration::from_millis(wait_ms))? + let target_binding_id = listener.registry_binding_id.clone(); + listener.poll( + Duration::from_millis(wait_ms), + socket_paths, + &target_binding_id, + )? }; match event { - Some(JavascriptUnixListenerEvent::Connection(pending)) => { + Some(JavascriptUnixListenerEvent::Connection(mut pending)) => { if let Err(error) = check_network_resource_limit( resource_limits.max_sockets, network_counts.sockets, @@ -24060,19 +34752,35 @@ where "connection", ) }) { - let _ = pending.stream.shutdown(Shutdown::Both); + if let Err(error) = pending.stream.shutdown(Shutdown::Both) { + eprintln!("failed to reject excess Unix socket connection: {error}"); + } return Ok(json!({ "type": "error", "code": "EAGAIN", "message": error.to_string(), })); } - let socket = ActiveUnixSocket::from_stream( + let mut socket = ActiveUnixSocket::from_stream( pending.stream, Some(listener_id.to_string()), pending.local_path.clone(), pending.remote_path.clone(), + pending.local_abstract_path_hex.clone(), + pending.remote_abstract_path_hex.clone(), + None, + None, + javascript_unix_socket_buffer_limit(resource_limits), )?; + socket.connection_state = pending.connection_guard.state.take(); + socket.remote_registry_binding_id = Some( + process + .unix_listeners + .get(listener_id) + .expect("Unix listener remains registered during accept") + .registry_binding_id + .clone(), + ); let socket_id = process.allocate_unix_socket_id(); socket.set_event_pusher( process.execution.javascript_v8_session_handle(), @@ -24087,6 +34795,8 @@ where "socketId": socket_id, "localPath": pending.local_path, "remotePath": pending.remote_path, + "localAbstractPathHex": pending.local_abstract_path_hex, + "remoteAbstractPathHex": pending.remote_abstract_path_hex, })) } Some(JavascriptUnixListenerEvent::Error { code, message }) => Ok(json!({ @@ -24192,8 +34902,9 @@ where let listener = process.unix_listeners.get_mut(listener_id).ok_or_else(|| { SidecarError::InvalidState(format!("unknown net listener {listener_id}")) })?; - match listener.poll(Duration::ZERO)? { - Some(JavascriptUnixListenerEvent::Connection(pending)) => { + let target_binding_id = listener.registry_binding_id.clone(); + match listener.poll(Duration::ZERO, socket_paths, &target_binding_id)? { + Some(JavascriptUnixListenerEvent::Connection(mut pending)) => { check_network_resource_limit( resource_limits.max_sockets, network_counts.sockets, @@ -24209,13 +34920,22 @@ where let info = json!({ "localPath": pending.local_path.clone(), "remotePath": pending.remote_path.clone(), + "localAbstractPathHex": pending.local_abstract_path_hex.clone(), + "remoteAbstractPathHex": pending.remote_abstract_path_hex.clone(), }); - let socket = ActiveUnixSocket::from_stream( + let mut socket = ActiveUnixSocket::from_stream( pending.stream, Some(listener_id.to_string()), pending.local_path, pending.remote_path, + pending.local_abstract_path_hex, + pending.remote_abstract_path_hex, + None, + None, + javascript_unix_socket_buffer_limit(resource_limits), )?; + socket.connection_state = pending.connection_guard.state.take(); + socket.remote_registry_binding_id = Some(target_binding_id); let socket_id = process.allocate_unix_socket_id(); socket.set_event_pusher( process.execution.javascript_v8_session_handle(), @@ -24298,6 +35018,11 @@ where } else { javascript_sync_rpc_bytes_arg(&request.args, 1, "net.write chunk")? }; + let nonblocking = request + .args + .get(2) + .and_then(Value::as_bool) + .unwrap_or(false); if trace_enabled { NET_TCP_TRACE_COUNTERS .socket_write_calls @@ -24331,7 +35056,13 @@ where let socket = process.unix_sockets.get(socket_id).ok_or_else(|| { SidecarError::InvalidState(format!("unknown net socket {socket_id}")) })?; - socket.write_all(&chunk).map(|written| json!(written)) + socket + .write( + &chunk, + nonblocking, + javascript_unix_socket_write_timeout(resource_limits), + ) + .map(|written| json!(written)) } } "net.shutdown" => { @@ -24353,7 +35084,7 @@ where release_tcp_socket_handle(process, socket_id, socket, kernel, &kernel_readiness); Ok(Value::Null) } else if let Some(socket) = process.unix_sockets.remove(socket_id) { - release_unix_socket_handle(process, socket_id, socket); + release_unix_socket_handle(process, socket_id, socket, socket_paths); Ok(Value::Null) } else { Ok(Value::Null) @@ -24362,6 +35093,11 @@ where "net.server_close" => { let listener_id = javascript_sync_rpc_arg_str(&request.args, 0, "net.server_close listener id")?; + let unlink_node_path = request + .args + .get(1) + .and_then(Value::as_bool) + .unwrap_or(false); if let Some(listener) = process.tcp_listeners.remove(listener_id) { unregister_kernel_readiness_target(&kernel_readiness, listener.kernel_socket_id); listener.close(kernel, process.kernel_pid)?; @@ -24370,8 +35106,58 @@ where let listener = process.unix_listeners.remove(listener_id).ok_or_else(|| { SidecarError::InvalidState(format!("unknown net listener {listener_id}")) })?; - listener.close()?; - Ok(Value::Null) + if !listener.is_final_description_handle() { + return Ok(Value::Null); + } + let mut cleanup_errors = Vec::new(); + for socket in process + .unix_sockets + .values_mut() + .filter(|socket| socket.listener_id.as_deref() == Some(listener_id)) + { + if let Err(error) = + socket.cache_remote_peer_metadata(&socket_paths.unix_bound_addresses) + { + cleanup_errors.push(format!("cache accepted peer metadata: {error}")); + } + } + if let Err(error) = close_pending_guest_unix_connections( + &socket_paths.unix_bound_addresses, + &listener.registry_binding_id, + ) { + cleanup_errors.push(format!("close pending connections: {error}")); + } + if let Err(error) = release_guest_unix_binding( + &socket_paths.unix_bound_addresses, + &listener.registry_binding_id, + ) { + cleanup_errors.push(format!("release listener metadata: {error}")); + } + if let Err(error) = purge_guest_unix_target( + &socket_paths.unix_bound_addresses, + &listener.registry_binding_id, + ) { + cleanup_errors.push(format!("purge queued peer metadata: {error}")); + } + let guest_node_path = listener.guest_node_path.clone(); + if let Err(error) = listener.close() { + cleanup_errors.push(format!("close listener: {error}")); + } + if unlink_node_path { + if let Some(path) = guest_node_path.as_deref() { + match kernel.remove_file(path) { + Ok(()) => {} + Err(error) if error.code() == "ENOENT" => {} + Err(error) => cleanup_errors + .push(format!("remove Unix socket node: {}", kernel_error(error))), + } + } + } + if cleanup_errors.is_empty() { + Ok(Value::Null) + } else { + Err(SidecarError::Execution(cleanup_errors.join("; "))) + } } } "tls.get_ciphers" => javascript_net_json_string( @@ -24414,21 +35200,6 @@ pub(crate) fn canonical_signal_name(signal: i32) -> Option<&'static str> { agentos_native_sidecar_core::canonical_signal_name(signal) } -fn dispatch_v8_process_signal(process: &ActiveProcess, signal: i32) -> Result { - let Some(signal_name) = signal_name_for_stream_event(signal) else { - return Ok(false); - }; - process.execution.send_javascript_stream_event( - "signal", - json!({ - "signal": signal_name, - "number": signal, - "action": "default", - }), - )?; - Ok(true) -} - fn dispatch_v8_signal_to_tracked_processes( process: &ActiveProcess, target_pids: &BTreeSet, @@ -24450,6 +35221,28 @@ fn dispatch_v8_signal_to_tracked_processes( Ok(delivered) } +fn dispatch_v8_process_signal(process: &ActiveProcess, signal: i32) -> Result { + let Some(signal_name) = signal_name_for_stream_event(signal) else { + return Ok(false); + }; + if matches!(&process.execution, ActiveExecution::Wasm(execution) if execution.uses_shared_v8_runtime()) + { + if let Some(session) = process.execution.javascript_v8_session_handle() { + dispatch_v8_session_signal_async(session, signal); + return Ok(true); + } + } + process.execution.send_javascript_stream_event( + "signal", + json!({ + "signal": signal_name, + "number": signal, + "action": "default", + }), + )?; + Ok(true) +} + fn dispatch_v8_session_signal_async(session: V8SessionHandle, signal: i32) { let Some(signal_name) = signal_name_for_stream_event(signal).map(str::to_owned) else { return; @@ -24489,13 +35282,40 @@ pub(crate) fn parse_signal(signal: &str) -> Result { } pub(crate) fn runtime_child_is_alive(child_pid: u32) -> Result { - Ok(runtime_child_exit_status(child_pid)?.is_none()) + Ok(matches!( + runtime_child_exit_status(child_pid)?, + RuntimeChildStatusObservation::Running + )) +} + +#[derive(Debug, Clone, Copy)] +struct RuntimeChildExitStatus { + status: i32, + signal: Option, + core_dumped: bool, +} + +#[derive(Debug, Clone, Copy)] +enum RuntimeChildStatusObservation { + Running, + Exited(RuntimeChildExitStatus), + /// The pid is not a waitable child (or its status was already consumed). + /// This is not an exit status and must never be converted to exit(0). + NotWaitable, } #[cfg(not(target_os = "macos"))] -fn runtime_child_exit_status(child_pid: u32) -> Result, SidecarError> { +fn runtime_child_exit_status( + child_pid: u32, +) -> Result { if child_pid == 0 { - return Ok(Some(0)); + return Ok(RuntimeChildStatusObservation::Exited( + RuntimeChildExitStatus { + status: 0, + signal: None, + core_dumped: false, + }, + )); } let wait_flags = WaitPidFlag::WNOHANG @@ -24506,12 +35326,26 @@ fn runtime_child_exit_status(child_pid: u32) -> Result, SidecarError match wait_on_child(WaitId::Pid(Pid::from_raw(child_pid as i32)), wait_flags) { Ok(WaitStatus::StillAlive) | Ok(WaitStatus::Stopped(_, _)) - | Ok(WaitStatus::Continued(_)) => Ok(None), - Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)), - Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)), + | Ok(WaitStatus::Continued(_)) => Ok(RuntimeChildStatusObservation::Running), + Ok(WaitStatus::Exited(_, status)) => Ok(RuntimeChildStatusObservation::Exited( + RuntimeChildExitStatus { + status, + signal: None, + core_dumped: false, + }, + )), + Ok(WaitStatus::Signaled(_, signal, core_dumped)) => Ok( + RuntimeChildStatusObservation::Exited(RuntimeChildExitStatus { + status: 128 + signal as i32, + signal: Some(signal as i32), + core_dumped, + }), + ), #[cfg(any(target_os = "linux", target_os = "android"))] - Ok(WaitStatus::PtraceEvent(_, _, _) | WaitStatus::PtraceSyscall(_)) => Ok(None), - Err(nix::errno::Errno::ECHILD) => Ok(Some(0)), + Ok(WaitStatus::PtraceEvent(_, _, _) | WaitStatus::PtraceSyscall(_)) => { + Ok(RuntimeChildStatusObservation::Running) + } + Err(nix::errno::Errno::ECHILD) => Ok(RuntimeChildStatusObservation::NotWaitable), Err(error) => Err(SidecarError::Execution(format!( "failed to inspect guest runtime process {child_pid}: {error}" ))), @@ -24524,18 +35358,38 @@ fn runtime_child_exit_status(child_pid: u32) -> Result, SidecarError // reaping parent), but a second status query after exit returns ECHILD → treated // as "exited(0)" below. #[cfg(target_os = "macos")] -fn runtime_child_exit_status(child_pid: u32) -> Result, SidecarError> { +fn runtime_child_exit_status( + child_pid: u32, +) -> Result { if child_pid == 0 { - return Ok(Some(0)); + return Ok(RuntimeChildStatusObservation::Exited( + RuntimeChildExitStatus { + status: 0, + signal: None, + core_dumped: false, + }, + )); } match waitpid(Pid::from_raw(child_pid as i32), Some(WaitPidFlag::WNOHANG)) { Ok(WaitStatus::StillAlive) | Ok(WaitStatus::Stopped(_, _)) - | Ok(WaitStatus::Continued(_)) => Ok(None), - Ok(WaitStatus::Exited(_, status)) => Ok(Some(status)), - Ok(WaitStatus::Signaled(_, signal, _)) => Ok(Some(128 + signal as i32)), - Err(nix::errno::Errno::ECHILD) => Ok(Some(0)), + | Ok(WaitStatus::Continued(_)) => Ok(RuntimeChildStatusObservation::Running), + Ok(WaitStatus::Exited(_, status)) => Ok(RuntimeChildStatusObservation::Exited( + RuntimeChildExitStatus { + status, + signal: None, + core_dumped: false, + }, + )), + Ok(WaitStatus::Signaled(_, signal, core_dumped)) => Ok( + RuntimeChildStatusObservation::Exited(RuntimeChildExitStatus { + status: 128 + signal as i32, + signal: Some(signal as i32), + core_dumped, + }), + ), + Err(nix::errno::Errno::ECHILD) => Ok(RuntimeChildStatusObservation::NotWaitable), Err(error) => Err(SidecarError::Execution(format!( "failed to inspect guest runtime process {child_pid}: {error}" ))), @@ -24737,6 +35591,188 @@ mod error_code_tests { ); } } + +#[cfg(test)] +mod exec_signal_state_tests { + use super::{ + exact_exec_image_header_is_valid, reset_caught_signal_dispositions_after_exec, + GuestRuntimeKind, SignalDispositionAction, SignalHandlerRegistration, + }; + use std::collections::BTreeMap; + + #[test] + fn nested_exec_resets_caught_dispositions_under_descendant_key() { + let mut states = BTreeMap::from([ + ( + String::from("child-1"), + BTreeMap::from([ + ( + 10, + SignalHandlerRegistration { + action: SignalDispositionAction::User, + mask: Vec::new(), + flags: 0, + }, + ), + ( + 12, + SignalHandlerRegistration { + action: SignalDispositionAction::Ignore, + mask: Vec::new(), + flags: 0, + }, + ), + ]), + ), + ( + String::from("root/child-1"), + BTreeMap::from([( + 15, + SignalHandlerRegistration { + action: SignalDispositionAction::User, + mask: Vec::new(), + flags: 0, + }, + )]), + ), + ]); + + let key = reset_caught_signal_dispositions_after_exec( + &mut states, + "root", + &["parent", "child-1"], + ); + + assert_eq!(key, "child-1"); + let child = states.get("child-1").expect("descendant signal state"); + assert!(!child.contains_key(&10), "caught disposition must reset"); + assert_eq!( + child.get(&12).map(|registration| ®istration.action), + Some(&SignalDispositionAction::Ignore), + "ignored disposition must survive exec" + ); + assert!( + states + .get("root/child-1") + .expect("unrelated legacy/full-path state") + .contains_key(&15), + "exec must use the same leaf key as descendant signal registration" + ); + } + + #[test] + fn exact_exec_rejects_existing_files_with_invalid_runtime_format() { + assert!(exact_exec_image_header_is_valid( + &GuestRuntimeKind::WebAssembly, + b"\0asm" + )); + assert!(!exact_exec_image_header_is_valid( + &GuestRuntimeKind::WebAssembly, + b"data" + )); + assert!(exact_exec_image_header_is_valid( + &GuestRuntimeKind::JavaScript, + b"#!/u" + )); + assert!(!exact_exec_image_header_is_valid( + &GuestRuntimeKind::JavaScript, + b"cons" + )); + } +} + +#[cfg(test)] +mod tls_trust_tests { + use super::{ + tls_root_store, vm_default_ca_bundle_for_tls_options, JavascriptTlsBridgeOptions, + JavascriptTlsDataValue, JavascriptTlsMaterial, SidecarKernel, + }; + use agentos_kernel::kernel::KernelVmConfig; + use agentos_kernel::mount_table::MountTable; + use agentos_kernel::permissions::Permissions; + use agentos_kernel::vfs::MemoryFileSystem; + use agentos_native_sidecar_core::ca::{CA_CERTIFICATES_BUNDLE, CA_CERTIFICATES_GUEST_PATH}; + use base64::Engine; + use std::io::Cursor; + + fn first_ca_pem() -> Vec { + const END_MARKER: &[u8] = b"-----END CERTIFICATE-----"; + let end = CA_CERTIFICATES_BUNDLE + .windows(END_MARKER.len()) + .position(|window| window == END_MARKER) + .expect("embedded CA bundle contains a certificate") + + END_MARKER.len(); + let mut pem = CA_CERTIFICATES_BUNDLE[..end].to_vec(); + pem.push(b'\n'); + pem + } + + fn test_kernel(bundle: &[u8]) -> SidecarKernel { + let mut config = KernelVmConfig::new("vm-live-tls-trust"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .mkdir("/etc/ssl/certs", true) + .expect("create VM trust directory"); + kernel + .write_file(CA_CERTIFICATES_GUEST_PATH, bundle) + .expect("install VM trust bundle"); + kernel + } + + #[test] + fn default_tls_trust_is_read_live_from_the_vm_without_host_fallback() { + let first_pem = first_ca_pem(); + let options = JavascriptTlsBridgeOptions::default(); + let mut kernel = test_kernel(&first_pem); + + let first = vm_default_ca_bundle_for_tls_options(&mut kernel, &options) + .expect("read initial VM trust bundle"); + assert_eq!(first, first_pem); + assert_eq!( + tls_root_store(&options, &first) + .expect("build roots from the VM bundle") + .len(), + 1, + "the root store must contain only the one VM certificate, not host roots" + ); + + kernel + .write_file(CA_CERTIFICATES_GUEST_PATH, b"not a certificate") + .expect("replace the live VM trust bundle"); + let replacement = vm_default_ca_bundle_for_tls_options(&mut kernel, &options) + .expect("reread replacement VM trust bundle"); + assert_eq!(replacement, b"not a certificate"); + assert!( + tls_root_store(&options, &replacement).is_err(), + "an invalid VM bundle must fail instead of falling back to host trust" + ); + } + + #[test] + fn node_custom_ca_replaces_the_default_vm_bundle() { + let certificate = rustls_pemfile::certs(&mut Cursor::new(first_ca_pem())) + .next() + .expect("one certificate") + .expect("parse certificate"); + let options = JavascriptTlsBridgeOptions { + ca: Some(JavascriptTlsMaterial::Single( + JavascriptTlsDataValue::Buffer { + data: base64::engine::general_purpose::STANDARD.encode(certificate.as_ref()), + }, + )), + ..JavascriptTlsBridgeOptions::default() + }; + + assert_eq!( + tls_root_store(&options, b"invalid default bundle") + .expect("custom CA must replace, not append to, default roots") + .len(), + 1 + ); + } +} + #[cfg(test)] mod ssrf_egress_classifier_tests { // F-005/006/007 (sec-sidecar T1/T7/T11): the egress classifier must treat the @@ -24966,7 +36002,7 @@ mod dns_rebinding_pin_tests { let url = Url::parse(&format!("http://attacker.example:{port}/")).expect("url"); let pinned = vec![IpAddr::V4(Ipv4Addr::LOCALHOST)]; - let result = issue_outbound_http_request(&url, &options(), &empty_headers(), &pinned) + let result = issue_outbound_http_request(&url, &options(), &empty_headers(), &pinned, &[]) .expect("pinned request should reach the vetted loopback target"); let payload = result.as_str().expect("string payload"); assert!( @@ -24982,7 +36018,7 @@ mod dns_rebinding_pin_tests { #[test] fn outbound_http_refuses_when_no_vetted_address() { let url = Url::parse("https://attacker.example/").expect("url"); - let error = issue_outbound_http_request(&url, &options(), &empty_headers(), &[]) + let error = issue_outbound_http_request(&url, &options(), &empty_headers(), &[], &[]) .expect_err("empty pinned set must be refused"); let message = error.to_string(); assert!( diff --git a/crates/native-sidecar/src/filesystem.rs b/crates/native-sidecar/src/filesystem.rs index 989f5b8651..965e5f8059 100644 --- a/crates/native-sidecar/src/filesystem.rs +++ b/crates/native-sidecar/src/filesystem.rs @@ -16,8 +16,9 @@ use crate::service::{ log_stale_process_event, normalize_host_path, normalize_path, path_is_within_root, }; use crate::state::{ - ActiveExecutionEvent, ActiveProcess, BridgeError, SidecarKernel, VmState, - EXECUTION_DRIVER_NAME, EXECUTION_SANDBOX_ROOT_ENV, PYTHON_VFS_RPC_GUEST_ROOT, + ActiveExecutionEvent, ActiveProcess, BridgeError, ShadowNodeType, ShadowSyncInventoryEntry, + SidecarKernel, VmState, EXECUTION_DRIVER_NAME, EXECUTION_SANDBOX_ROOT_ENV, + PYTHON_VFS_RPC_GUEST_ROOT, }; use crate::{DispatchResult, NativeSidecar, NativeSidecarBridge, SidecarError}; @@ -515,21 +516,26 @@ fn mirror_guest_filesystem_shadow_after_call( ) .map_err(|error| SidecarError::InvalidState(error.to_string()))?; mirror_guest_file_write_to_shadow(vm, &payload.path, &bytes)?; + refresh_shadow_inventory_path(vm, &payload.path)?; } GuestFilesystemOperation::Pwrite => { // A positional write only carries the changed region; mirror the // full post-write file from the kernel so the shadow stays faithful. let bytes = vm.kernel.read_file(&payload.path).map_err(kernel_error)?; mirror_guest_file_write_to_shadow(vm, &payload.path, &bytes)?; + refresh_shadow_inventory_path(vm, &payload.path)?; } GuestFilesystemOperation::CreateDir | GuestFilesystemOperation::Mkdir => { mirror_guest_directory_write_to_shadow(vm, &payload.path)?; + refresh_shadow_inventory_path(vm, &payload.path)?; } GuestFilesystemOperation::RemoveFile | GuestFilesystemOperation::RemoveDir => { remove_guest_shadow_path(vm, &payload.path)?; + forget_shadow_inventory_path(vm, &payload.path); } GuestFilesystemOperation::Remove => { remove_guest_shadow_path(vm, &payload.path)?; + forget_shadow_inventory_path(vm, &payload.path); } GuestFilesystemOperation::Copy => { let destination = payload.destination_path.as_deref().ok_or_else(|| { @@ -539,6 +545,7 @@ fn mirror_guest_filesystem_shadow_after_call( })?; remove_guest_shadow_path(vm, destination)?; mirror_guest_subtree_to_shadow(vm, destination)?; + refresh_shadow_inventory_path(vm, destination)?; } GuestFilesystemOperation::Move => { let destination = payload.destination_path.as_deref().ok_or_else(|| { @@ -549,6 +556,8 @@ fn mirror_guest_filesystem_shadow_after_call( remove_guest_shadow_path(vm, &payload.path)?; remove_guest_shadow_path(vm, destination)?; mirror_guest_subtree_to_shadow(vm, destination)?; + forget_shadow_inventory_path(vm, &payload.path); + refresh_shadow_inventory_path(vm, destination)?; } GuestFilesystemOperation::Rename => { let destination = payload.destination_path.as_deref().ok_or_else(|| { @@ -557,6 +566,8 @@ fn mirror_guest_filesystem_shadow_after_call( )) })?; rename_guest_shadow_path(vm, &payload.path, destination)?; + forget_shadow_inventory_path(vm, &payload.path); + refresh_shadow_inventory_path(vm, destination)?; } GuestFilesystemOperation::Symlink => { let target = payload.target.as_deref().ok_or_else(|| { @@ -565,6 +576,7 @@ fn mirror_guest_filesystem_shadow_after_call( )) })?; mirror_guest_symlink_to_shadow(vm, &payload.path, target)?; + refresh_shadow_inventory_path(vm, &payload.path)?; } GuestFilesystemOperation::Link => { let destination = payload.destination_path.as_deref().ok_or_else(|| { @@ -573,12 +585,15 @@ fn mirror_guest_filesystem_shadow_after_call( )) })?; mirror_guest_link_to_shadow(vm, &payload.path, destination)?; + refresh_shadow_inventory_path(vm, &payload.path)?; + refresh_shadow_inventory_path(vm, destination)?; } GuestFilesystemOperation::Chmod => { let mode = payload.mode.ok_or_else(|| { SidecarError::InvalidState(String::from("guest filesystem chmod requires a mode")) })?; mirror_guest_chmod_to_shadow(vm, &payload.path, mode)?; + refresh_shadow_inventory_node(vm, &payload.path)?; } GuestFilesystemOperation::Utimes => { let atime_ms = payload.atime_ms.ok_or_else(|| { @@ -598,12 +613,14 @@ fn mirror_guest_filesystem_shadow_after_call( VirtualUtimeSpec::Set(VirtualTimeSpec::from_millis(mtime_ms)), true, )?; + refresh_shadow_inventory_node(vm, &payload.path)?; } GuestFilesystemOperation::Truncate => { let len = payload.len.ok_or_else(|| { SidecarError::InvalidState(String::from("guest filesystem truncate requires len")) })?; mirror_guest_truncate_to_shadow(vm, &payload.path, len)?; + refresh_shadow_inventory_node(vm, &payload.path)?; } GuestFilesystemOperation::ReadFile | GuestFilesystemOperation::Pread @@ -619,6 +636,120 @@ fn mirror_guest_filesystem_shadow_after_call( Ok(()) } +/// Keep the deletion/type inventory current when a wire filesystem mutation +/// mirrors kernel state into the host shadow. Without this write-side update, +/// a host runtime can delete a freshly-created shadow path before the next +/// read-side reconciliation and the kernel copy will be resurrected because +/// that pathname was never part of the previous inventory. +fn refresh_shadow_inventory_path(vm: &mut VmState, guest_path: &str) -> Result<(), SidecarError> { + let guest_path = normalize_path(guest_path); + let mut updates = collect_shadow_inventory_ancestors(vm, &guest_path)?; + let Some(node_type) = shadow_inventory_kernel_node_type(vm, &guest_path)? else { + forget_shadow_inventory_path(vm, &guest_path); + return Ok(()); + }; + updates.insert( + guest_path.clone(), + ShadowSyncInventoryEntry::present(node_type), + ); + if node_type == ShadowNodeType::Directory { + let entries = vm + .kernel + .read_dir_recursive(&guest_path, None) + .map_err(kernel_error)?; + for entry in entries { + let path = normalize_path(&entry.path); + if let Some(node_type) = shadow_inventory_kernel_node_type(vm, &path)? { + updates.insert(path, ShadowSyncInventoryEntry::present(node_type)); + } + } + } + + // Commit only after the complete replacement inventory has been built. + // A failed stat/read must leave the previous deletion baseline intact. + forget_shadow_inventory_path(vm, &guest_path); + vm.shadow_sync_inventory.extend(updates); + Ok(()) +} + +/// Refresh only a pathname's structural type. Metadata operations such as +/// chmod/utimes must not recursively read a directory after making it mode +/// 000: Linux reports the metadata operation as successful, and existing +/// descendant inventory remains valid even though readdir is now denied. +fn refresh_shadow_inventory_node(vm: &mut VmState, guest_path: &str) -> Result<(), SidecarError> { + let guest_path = normalize_path(guest_path); + let mut updates = collect_shadow_inventory_ancestors(vm, &guest_path)?; + if let Some(node_type) = shadow_inventory_kernel_node_type(vm, &guest_path)? { + updates.insert( + guest_path.clone(), + ShadowSyncInventoryEntry::present(node_type), + ); + } + vm.shadow_sync_inventory.extend(updates); + Ok(()) +} + +fn collect_shadow_inventory_ancestors( + vm: &mut VmState, + guest_path: &str, +) -> Result, SidecarError> { + // `create_dir_all` may have created ancestors as part of mirroring a leaf. + // Record those too so removing a newly-created empty parent directly from + // the shadow has the same unlink/rmdir effect in the kernel VFS. + let mut ancestors = Vec::new(); + let mut cursor = guest_path.to_owned(); + loop { + let Some(parent) = Path::new(&cursor).parent() else { + break; + }; + let parent = normalize_path(&parent.to_string_lossy()); + if parent == "/" { + break; + } + ancestors.push(parent.clone()); + cursor = parent; + } + let mut updates = BTreeMap::new(); + for ancestor in ancestors.into_iter().rev() { + if shadow_inventory_kernel_node_type(vm, &ancestor)? == Some(ShadowNodeType::Directory) { + updates.insert( + ancestor, + ShadowSyncInventoryEntry::present(ShadowNodeType::Directory), + ); + } + } + Ok(updates) +} + +fn shadow_inventory_kernel_node_type( + vm: &mut VmState, + guest_path: &str, +) -> Result, SidecarError> { + let stat = match vm.kernel.lstat(guest_path) { + Ok(stat) => stat, + Err(error) if error.code() == "ENOENT" => return Ok(None), + Err(error) => return Err(kernel_error(error)), + }; + let node_type = if stat.is_symbolic_link { + ShadowNodeType::Symlink + } else if stat.is_directory { + ShadowNodeType::Directory + } else { + ShadowNodeType::File + }; + Ok(Some(node_type)) +} + +fn forget_shadow_inventory_path(vm: &mut VmState, guest_path: &str) { + let guest_path = normalize_path(guest_path); + vm.shadow_sync_inventory + .retain(|path, _| !shadow_inventory_path_is_at_or_below(path, &guest_path)); +} + +fn shadow_inventory_path_is_at_or_below(path: &str, prefix: &str) -> bool { + path == prefix || (prefix != "/" && path.starts_with(&format!("{prefix}/"))) +} + pub(crate) fn handle_python_vfs_rpc_request( sidecar: &mut NativeSidecar, vm_id: &str, @@ -714,15 +845,19 @@ where // entry the guest just removed. PythonVfsRpcMethod::Unlink => { match vm.kernel.remove_file(&path).map_err(kernel_error) { - Ok(()) => remove_guest_shadow_path(vm, &path) - .map(|()| PythonVfsRpcResponsePayload::Empty), + Ok(()) => remove_guest_shadow_path(vm, &path).map(|()| { + forget_shadow_inventory_path(vm, &path); + PythonVfsRpcResponsePayload::Empty + }), Err(error) => Err(error), } } PythonVfsRpcMethod::Rmdir => { match vm.kernel.remove_dir(&path).map_err(kernel_error) { - Ok(()) => remove_guest_shadow_path(vm, &path) - .map(|()| PythonVfsRpcResponsePayload::Empty), + Ok(()) => remove_guest_shadow_path(vm, &path).map(|()| { + forget_shadow_inventory_path(vm, &path); + PythonVfsRpcResponsePayload::Empty + }), Err(error) => Err(error), } } @@ -735,8 +870,13 @@ where })?; let destination = normalize_python_vfs_rpc_path(destination)?; match vm.kernel.rename(&path, &destination).map_err(kernel_error) { - Ok(()) => rename_guest_shadow_path(vm, &path, &destination) - .map(|()| PythonVfsRpcResponsePayload::Empty), + Ok(()) => { + rename_guest_shadow_path(vm, &path, &destination).and_then(|()| { + forget_shadow_inventory_path(vm, &path); + refresh_shadow_inventory_path(vm, &destination)?; + Ok(PythonVfsRpcResponsePayload::Empty) + }) + } Err(error) => Err(error), } } @@ -1306,15 +1446,14 @@ pub(crate) fn service_javascript_fs_sync_rpc( phase_start, ); let phase_start = Instant::now(); - return open_mapped_host_fd(process, opened, Some(path.to_string())).inspect( - |_| { + return open_mapped_host_fd(kernel, process, opened, Some(path.to_string())) + .inspect(|_| { record_fs_sync_subphase( request.method.as_str(), "open_mapped_fd", phase_start, ); - }, - ); + }); } Some(MappedRuntimeHostAccess::ReadOnly(_)) => { return Err(read_only_mapped_runtime_host_path_error(path)); @@ -1555,11 +1694,19 @@ pub(crate) fn service_javascript_fs_sync_rpc( "EBADF: file descriptor {fd} is not open for writing" ))); } - let path = kernel - .fd_path(EXECUTION_DRIVER_NAME, kernel_pid, fd) + kernel + .fd_truncate(EXECUTION_DRIVER_NAME, kernel_pid, fd, length) .map_err(kernel_error)?; - kernel.truncate(&path, length).map_err(kernel_error)?; - mirror_kernel_path_to_process_shadow(kernel, process, kernel_pid, &path)?; + if kernel + .fd_path(EXECUTION_DRIVER_NAME, kernel_pid, fd) + .ok() + .is_some_and(|path| kernel.exists(&path).unwrap_or(false)) + { + let path = kernel + .fd_path(EXECUTION_DRIVER_NAME, kernel_pid, fd) + .map_err(kernel_error)?; + mirror_kernel_path_to_process_shadow(kernel, process, kernel_pid, &path)?; + } Ok(Value::Null) } "fs.readFileSync" | "fs.promises.readFile" => { @@ -3469,10 +3616,22 @@ fn materialize_mapped_host_path_from_kernel( /// [`std::fs::File`] — no path re-open, so there is no TOCTOU window and no /// `/proc/self/fd` dependency. fn open_mapped_host_fd( + kernel: &SidecarKernel, process: &mut ActiveProcess, opened: MappedRuntimeOpenedPath, guest_path: Option, ) -> Result { + if let Some(limit) = kernel.resource_limits().max_open_fds { + let observed = kernel + .resource_snapshot() + .open_fds + .saturating_add(process.mapped_host_fds.len()); + if observed >= limit { + return Err(SidecarError::InvalidState(format!( + "EMFILE: VM open file descriptor limit {limit} reached (limits.resources.maxOpenFds); raise the limit to open more mapped host files" + ))); + } + } let host_path = opened.host_path; let file = std::fs::File::from(opened.handle.into_owned_fd()); let fd = process.allocate_mapped_host_fd(crate::state::ActiveMappedHostFd { @@ -4254,9 +4413,18 @@ fn rename_guest_shadow_path( let from_shadow_path = shadow_host_path_for_guest(&vm.cwd, &from_path); let to_shadow_path = shadow_host_path_for_guest(&vm.cwd, &to_path); - if !from_shadow_path.exists() { - remove_shadow_path_if_exists(&to_shadow_path, &to_path)?; - return Ok(()); + match fs::symlink_metadata(&from_shadow_path) { + Ok(_) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + remove_shadow_path_if_exists(&to_shadow_path, &to_path)?; + return Ok(()); + } + Err(error) => { + return Err(SidecarError::Io(format!( + "failed to inspect shadow rename source {}: {error}", + from_shadow_path.display() + ))); + } } if let Some(parent) = to_shadow_path.parent() { diff --git a/crates/native-sidecar/src/limits.rs b/crates/native-sidecar/src/limits.rs index 183146e3c4..90ed6ccc14 100644 --- a/crates/native-sidecar/src/limits.rs +++ b/crates/native-sidecar/src/limits.rs @@ -1,18 +1,20 @@ //! Native compatibility exports for shared VM-scoped runtime limits. pub use agentos_native_sidecar_core::limits::{ - validate_vm_limits, AcpLimits, HttpLimits, JsRuntimeLimits, PluginLimits, PythonLimits, - ToolLimits, VmLimits, WasmLimits, DEFAULT_ACP_MAX_READ_LINE_BYTES, + validate_vm_limits, AcpLimits, HttpLimits, JsRuntimeLimits, PluginLimits, ProcessLimits, + PythonLimits, ToolLimits, VmLimits, WasmLimits, DEFAULT_ACP_MAX_READ_LINE_BYTES, DEFAULT_ACP_STDOUT_BUFFER_BYTE_LIMIT, DEFAULT_JS_CAPTURED_OUTPUT_LIMIT_BYTES, DEFAULT_JS_EVENT_PAYLOAD_LIMIT_BYTES, DEFAULT_JS_STDIN_BUFFER_LIMIT_BYTES, - DEFAULT_MAX_FETCH_RESPONSE_BYTES, DEFAULT_PYTHON_EXECUTION_TIMEOUT_MS, - DEFAULT_PYTHON_MAX_OLD_SPACE_MB, DEFAULT_PYTHON_OUTPUT_BUFFER_MAX_BYTES, - DEFAULT_PYTHON_VFS_RPC_TIMEOUT_MS, DEFAULT_TOOL_TIMEOUT_MS, DEFAULT_V8_HEAP_LIMIT_MB, - DEFAULT_V8_IPC_MAX_FRAME_BYTES, DEFAULT_WASM_CAPTURED_OUTPUT_LIMIT_BYTES, - DEFAULT_WASM_MAX_MODULE_FILE_BYTES, DEFAULT_WASM_SYNC_READ_LIMIT_BYTES, - MAX_PERSISTED_MANIFEST_BYTES, MAX_PERSISTED_MANIFEST_FILE_BYTES, MAX_REGISTERED_TOOLKITS, - MAX_REGISTERED_TOOLS_PER_VM, MAX_TOOLS_PER_TOOLKIT, MAX_TOOL_EXAMPLES_PER_TOOL, - MAX_TOOL_EXAMPLE_INPUT_BYTES, MAX_TOOL_SCHEMA_BYTES, MAX_TOOL_TIMEOUT_MS, + DEFAULT_MAX_FETCH_RESPONSE_BYTES, DEFAULT_PROCESS_PENDING_EVENT_BYTES, + DEFAULT_PROCESS_PENDING_EVENT_COUNT, DEFAULT_PROCESS_PENDING_STDIN_BYTES, + DEFAULT_PYTHON_EXECUTION_TIMEOUT_MS, DEFAULT_PYTHON_MAX_OLD_SPACE_MB, + DEFAULT_PYTHON_OUTPUT_BUFFER_MAX_BYTES, DEFAULT_PYTHON_VFS_RPC_TIMEOUT_MS, + DEFAULT_TOOL_TIMEOUT_MS, DEFAULT_V8_HEAP_LIMIT_MB, DEFAULT_V8_IPC_MAX_FRAME_BYTES, + DEFAULT_WASM_CAPTURED_OUTPUT_LIMIT_BYTES, DEFAULT_WASM_MAX_MODULE_FILE_BYTES, + DEFAULT_WASM_SYNC_READ_LIMIT_BYTES, MAX_PERSISTED_MANIFEST_BYTES, + MAX_PERSISTED_MANIFEST_FILE_BYTES, MAX_REGISTERED_TOOLKITS, MAX_REGISTERED_TOOLS_PER_VM, + MAX_TOOLS_PER_TOOLKIT, MAX_TOOL_EXAMPLES_PER_TOOL, MAX_TOOL_EXAMPLE_INPUT_BYTES, + MAX_TOOL_SCHEMA_BYTES, MAX_TOOL_TIMEOUT_MS, }; use agentos_vm_config::VmLimitsConfig; diff --git a/crates/native-sidecar/src/plugins/host_dir.rs b/crates/native-sidecar/src/plugins/host_dir.rs index ed3c2f7c07..3cafeb9c65 100644 --- a/crates/native-sidecar/src/plugins/host_dir.rs +++ b/crates/native-sidecar/src/plugins/host_dir.rs @@ -1874,7 +1874,7 @@ mod tar_module_reader_tests { #[test] fn tar_reader_resolves_packed_node_modules() { let aospkg = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../registry/agent/pi/dist/package.aospkg"); + .join("../../software/pi/dist/package.aospkg"); if !aospkg.is_file() { eprintln!("skip: pi aospkg not built"); return; diff --git a/crates/native-sidecar/src/plugins/js_bridge.rs b/crates/native-sidecar/src/plugins/js_bridge.rs index 1391b74b33..c73415dbbd 100644 --- a/crates/native-sidecar/src/plugins/js_bridge.rs +++ b/crates/native-sidecar/src/plugins/js_bridge.rs @@ -590,6 +590,16 @@ impl VirtualFileSystem for JsBridgeFilesystem { } fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> { + self.chown_spec(path, uid, gid, true) + } + + fn chown_spec( + &mut self, + path: &str, + uid: u32, + gid: u32, + follow_symlinks: bool, + ) -> VfsResult<()> { self.request_path( "chown", path, @@ -597,6 +607,7 @@ impl VirtualFileSystem for JsBridgeFilesystem { "path": path, "uid": uid, "gid": gid, + "followSymlinks": follow_symlinks, }), )?; Ok(()) diff --git a/crates/native-sidecar/src/service.rs b/crates/native-sidecar/src/service.rs index c15445f1dd..b3ed1553d1 100644 --- a/crates/native-sidecar/src/service.rs +++ b/crates/native-sidecar/src/service.rs @@ -1,11 +1,12 @@ use crate::bridge::{build_mount_plugin_registry, MountPluginContext}; pub(crate) use crate::execution::{ - build_javascript_socket_path_context, canonical_signal_name, dispatch_loopback_http_request, - error_code, format_tcp_resource, ignore_stale_javascript_sync_rpc_response, - javascript_sync_rpc_arg_i32, javascript_sync_rpc_arg_str, javascript_sync_rpc_arg_u32, - javascript_sync_rpc_arg_u32_optional, javascript_sync_rpc_arg_u64, - javascript_sync_rpc_arg_u64_optional, javascript_sync_rpc_bytes_arg, - javascript_sync_rpc_bytes_value, javascript_sync_rpc_encoding, javascript_sync_rpc_error_code, + apply_active_process_default_signal, build_javascript_socket_path_context, + canonical_signal_name, dispatch_loopback_http_request, error_code, flush_pending_kernel_stdin, + format_tcp_resource, ignore_stale_javascript_sync_rpc_response, javascript_sync_rpc_arg_i32, + javascript_sync_rpc_arg_str, javascript_sync_rpc_arg_u32, javascript_sync_rpc_arg_u32_optional, + javascript_sync_rpc_arg_u64, javascript_sync_rpc_arg_u64_optional, + javascript_sync_rpc_bytes_arg, javascript_sync_rpc_bytes_value, javascript_sync_rpc_encoding, + javascript_sync_rpc_error_code, javascript_sync_rpc_may_make_fd_readable, javascript_sync_rpc_option_bool, javascript_sync_rpc_option_u32, kernel_poll_response, kernel_stdin_read_response, mark_execute_exit_event_queued, parse_kernel_poll_args, parse_kernel_stdin_read_args, parse_signal, record_execute_exit_event_queue_wait, @@ -144,24 +145,15 @@ pub use crate::state::{NativeSidecarConfig, SidecarError}; #[derive(Debug, Default, Deserialize)] struct LegacyJavascriptChildProcessSpawnOptions { - #[serde(default)] - cwd: Option, - #[serde(default)] - env: BTreeMap, - #[serde(default)] - input: Option, - #[serde(default)] - shell: bool, - #[serde(default)] - detached: bool, - #[serde(default)] - stdio: Vec, + // The V8 sync host binding still carries command/argv/options as three + // strings. Flatten the canonical options object here so every newly added + // field crosses that compatibility bridge automatically; keeping a second + // hand-copied field list previously dropped POSIX spawn attributes and fd + // mappings without an error. + #[serde(flatten)] + options: JavascriptChildProcessSpawnOptions, #[serde(default, rename = "maxBuffer")] max_buffer: Option, - #[serde(default)] - timeout: Option, - #[serde(default, rename = "killSignal")] - kill_signal: Option, } #[derive(Debug, Deserialize)] @@ -195,37 +187,40 @@ pub(crate) fn parse_javascript_child_process_spawn_request( let parsed_args = serde_json::from_str::>(raw_args).map_err(|error| { SidecarError::InvalidState(format!("invalid child_process.spawn args payload: {error}")) })?; - let parsed_options = serde_json::from_str::( - raw_options, - ) - .map_err(|error| { - SidecarError::InvalidState(format!( - "invalid child_process.spawn options payload: {error}" - )) - })?; + let parsed_options = + parse_legacy_javascript_child_process_spawn_options(&vm.guest_env, raw_options)?; + let max_buffer = parsed_options.max_buffer; + let options = parsed_options.options; Ok(( JavascriptChildProcessSpawnRequest { command, args: parsed_args, - options: JavascriptChildProcessSpawnOptions { - cwd: parsed_options.cwd, - env: parsed_options.env, - internal_bootstrap_env: sanitize_javascript_child_process_internal_bootstrap_env( - &vm.guest_env, - ), - input: parsed_options.input, - shell: parsed_options.shell, - detached: parsed_options.detached, - stdio: parsed_options.stdio, - timeout: parsed_options.timeout, - kill_signal: parsed_options.kill_signal, - }, + options, }, - parsed_options.max_buffer, + max_buffer, )) } +fn parse_legacy_javascript_child_process_spawn_options( + vm_guest_env: &BTreeMap, + raw_options: &str, +) -> Result { + let mut parsed = serde_json::from_str::(raw_options) + .map_err(|error| { + SidecarError::InvalidState(format!( + "invalid child_process.spawn options payload: {error}" + )) + })?; + let mut internal_bootstrap_env = + sanitize_javascript_child_process_internal_bootstrap_env(vm_guest_env); + internal_bootstrap_env.extend(sanitize_javascript_child_process_internal_bootstrap_env( + &parsed.options.internal_bootstrap_env, + )); + parsed.options.internal_bootstrap_env = internal_bootstrap_env; + Ok(parsed) +} + impl SharedBridge { fn new(bridge: B) -> Self { Self { @@ -669,6 +664,7 @@ pub struct NativeSidecar { pub(crate) completed_sidecar_response_order: VecDeque, pub(crate) completed_sidecar_responses_gauge: Arc, pub(crate) pending_process_events_gauge: Arc, + pub(crate) pending_process_event_bytes_gauge: Arc, pub(crate) pending_sidecar_responses_gauge: Arc, pub(crate) outbound_sidecar_requests_gauge: Arc, pub(crate) sidecar_requests: SharedSidecarRequestClient, @@ -677,6 +673,8 @@ pub struct NativeSidecar { pub(crate) extension_sessions: BTreeMap<(String, String), ExtensionSessionResources>, pub(crate) extension_process_output_buffers: BTreeMap<(String, String), ExtensionBufferedProcessOutput>, + #[cfg(test)] + pub(crate) fail_next_exec_start_after_commit: bool, /// Session scopes (connection_id, session_id) disposed since the stdio /// transport last drained them. Lets the transport remove dead sessions from /// its active-session set instead of iterating them forever (M5). @@ -774,6 +772,10 @@ where TrackedLimit::PendingProcessEvents, MAX_PROCESS_EVENT_QUEUE, ), + pending_process_event_bytes_gauge: register_queue( + TrackedLimit::PendingProcessEventBytes, + agentos_native_sidecar_core::limits::DEFAULT_PROCESS_PENDING_EVENT_BYTES, + ), pending_sidecar_responses_gauge: register_queue( TrackedLimit::PendingSidecarResponses, MAX_PENDING_SIDECAR_RESPONSES, @@ -787,6 +789,8 @@ where extensions: BTreeMap::new(), extension_sessions: BTreeMap::new(), extension_process_output_buffers: BTreeMap::new(), + #[cfg(test)] + fail_next_exec_start_after_commit: false, disposed_sessions: Vec::new(), }) } @@ -1045,15 +1049,22 @@ where &mut self, envelope: ProcessEventEnvelope, ) -> Result<(), SidecarError> { - if self.pending_process_events.len() >= MAX_PROCESS_EVENT_QUEUE { - return Err(process_event_queue_overflow_error()); + self.try_queue_pending_process_event(envelope) + .map_err(|(error, _envelope)| error) + } + + pub(crate) fn try_queue_pending_process_event( + &mut self, + envelope: ProcessEventEnvelope, + ) -> Result<(), (SidecarError, ProcessEventEnvelope)> { + if let Err(error) = self.check_pending_process_event_capacity(&envelope) { + return Err((error, envelope)); } if matches!(&envelope.event, ActiveExecutionEvent::Exited(_)) { mark_execute_exit_event_queued(&envelope.vm_id, &envelope.process_id); } self.pending_process_events.push_back(envelope); - self.pending_process_events_gauge - .observe_depth(self.pending_process_events.len()); + self.observe_pending_process_event_depth(); Ok(()) } @@ -1061,15 +1072,12 @@ where &mut self, envelope: ProcessEventEnvelope, ) -> Result<(), SidecarError> { - if self.pending_process_events.len() >= MAX_PROCESS_EVENT_QUEUE { - return Err(process_event_queue_overflow_error()); - } + self.check_pending_process_event_capacity(&envelope)?; if matches!(&envelope.event, ActiveExecutionEvent::Exited(_)) { mark_execute_exit_event_queued(&envelope.vm_id, &envelope.process_id); } self.pending_process_events.push_front(envelope); - self.pending_process_events_gauge - .observe_depth(self.pending_process_events.len()); + self.observe_pending_process_event_depth(); Ok(()) } @@ -1077,6 +1085,57 @@ where MAX_PROCESS_EVENT_QUEUE.saturating_sub(self.pending_process_events.len()) } + pub(crate) fn check_pending_process_event_capacity( + &self, + envelope: &ProcessEventEnvelope, + ) -> Result<(), SidecarError> { + if self.pending_process_events.len() >= MAX_PROCESS_EVENT_QUEUE { + return Err(process_event_queue_overflow_error()); + } + let defaults = agentos_native_sidecar_core::limits::ProcessLimits::default(); + let limits = self + .vms + .get(&envelope.vm_id) + .map(|vm| &vm.limits.process) + .unwrap_or(&defaults); + let mut vm_count = 0usize; + let mut vm_bytes = 0usize; + for pending in self + .pending_process_events + .iter() + .filter(|pending| pending.vm_id == envelope.vm_id) + { + vm_count = vm_count.saturating_add(1); + vm_bytes = vm_bytes.saturating_add(pending.retained_bytes()); + } + if vm_count >= limits.pending_event_count { + return Err(SidecarError::InvalidState(format!( + "VM {} process event queue exceeded {} events (limits.process.pendingEventCount)", + envelope.vm_id, limits.pending_event_count + ))); + } + let next_bytes = vm_bytes.saturating_add(envelope.retained_bytes()); + if next_bytes > limits.pending_event_bytes { + return Err(SidecarError::InvalidState(format!( + "VM {} process event queue exceeded {} retained bytes (limits.process.pendingEventBytes)", + envelope.vm_id, limits.pending_event_bytes + ))); + } + Ok(()) + } + + pub(crate) fn observe_pending_process_event_depth(&self) { + self.pending_process_events_gauge + .observe_depth(self.pending_process_events.len()); + self.pending_process_event_bytes_gauge.observe_depth( + self.pending_process_events + .iter() + .fold(0usize, |bytes, event| { + bytes.saturating_add(event.retained_bytes()) + }), + ); + } + pub fn dispatch_blocking( &mut self, request: RequestFrame, @@ -1306,6 +1365,7 @@ where let Some(envelope) = self.pending_process_events.remove(index) else { continue; }; + self.observe_pending_process_event_depth(); if let Some(frame) = self.handle_process_event_envelope(envelope)? { return Ok(Some(frame)); } @@ -1400,21 +1460,8 @@ where } } self.pending_process_events = deferred; + self.observe_pending_process_event_depth(); record_execute_phase("process_exit_trailing_pending_scan", phase_start.elapsed()); - let drain_limit = self - .pending_process_event_capacity() - .saturating_sub(trailing.len().saturating_add(1)); - let phase_start = Instant::now(); - trailing.extend( - self.drain_process_events_blocking_with_limit(&vm_id, &process_id, drain_limit)? - .into_iter() - .filter(|event| !matches!(event, ActiveExecutionEvent::Exited(_))), - ); - record_execute_phase( - "process_exit_trailing_blocking_drain", - phase_start.elapsed(), - ); - if !trailing.is_empty() { if self.pending_process_event_capacity() < trailing.len() { return Err(process_event_queue_overflow_error()); @@ -1794,6 +1841,10 @@ where log_stale_process_event(&self.bridge, vm_id, process_id, "deferred kernel wait RPC"); return Ok(None); }; + // Reading from the pipe frees capacity. Top it off before every root + // process read/poll probe, matching the descendant-process path, and + // deliver a deferred close only after all accepted bytes are written. + flush_pending_kernel_stdin(&mut vm.kernel, process)?; let kernel_pid = process.kernel_pid; let deadline = match &process.deferred_kernel_wait_rpc { Some((parked, parked_deadline)) if parked.id == request.id => *parked_deadline, @@ -1973,6 +2024,42 @@ where )?; Ok(Value::Null.into()) } + "process.exec_fd_image_commit" => { + let Some(vm) = self.vms.get(vm_id) else { + log_stale_process_event( + &self.bridge, + vm_id, + process_id, + "javascript sync RPC process.exec_fd_image_commit", + ); + return Ok(()); + }; + let (payload, _) = + parse_javascript_child_process_spawn_request(vm, &request.args)?; + self.commit_wasm_fd_process_image(vm_id, process_id, &[], payload)?; + Ok(json!({ "committed": true }).into()) + } + "process.exec" => { + let Some(vm) = self.vms.get(vm_id) else { + log_stale_process_event( + &self.bridge, + vm_id, + process_id, + "javascript sync RPC process.exec", + ); + return Ok(()); + }; + let (payload, _) = + parse_javascript_child_process_spawn_request(vm, &request.args)?; + let local_replacement = payload.options.local_replacement; + match self.exec_javascript_process_image(vm_id, process_id, &[], payload) { + Ok(()) if local_replacement => Ok(json!({ "committed": true }).into()), + // Success destroys the blocked old image. Never reply: + // returning would resume instructions after execve. + Ok(()) => return Ok(()), + Err(error) => Err(error), + } + } "process.kill" => { let target_pid = javascript_sync_rpc_arg_i32(&request.args, 0, "process.kill target pid")?; @@ -2026,9 +2113,9 @@ where }; let pgid = target_pid.unsigned_abs(); match self.signal_vm_process_group(vm_id, caller_kernel_pid, pgid, signal) { - Ok(true) => Ok(self + Ok(true) => self .apply_self_process_kill(vm_id, process_id, parsed_signal) - .into()), + .map(Into::into), Ok(false) => Ok(Value::Null.into()), Err(error) => Err(error), } @@ -2086,9 +2173,9 @@ where } }; match target { - ProcessKillTarget::SelfProcess => Ok(self + ProcessKillTarget::SelfProcess => self .apply_self_process_kill(vm_id, process_id, parsed_signal) - .into()), + .map(Into::into), ProcessKillTarget::Child(child_process_id) => { self.kill_javascript_child_process( vm_id, @@ -2283,6 +2370,12 @@ where } }; + if response.is_ok() && javascript_sync_rpc_may_make_fd_readable(&request) { + if let Some(vm) = self.vms.get_mut(vm_id) { + Self::wake_ready_deferred_fd_reads(vm)?; + } + } + let Some(vm) = self.vms.get_mut(vm_id) else { log_stale_process_event( &self.bridge, @@ -2350,7 +2443,7 @@ where vm_id: &str, process_id: &str, parsed_signal: i32, - ) -> Value { + ) -> Result { let action = self .vms .get(vm_id) @@ -2367,18 +2460,18 @@ where { if let Some(vm) = self.vms.get_mut(vm_id) { if let Some(process) = vm.active_processes.get_mut(process_id) { - process.pending_self_signal_exit = Some(parsed_signal); + apply_active_process_default_signal(&mut vm.kernel, process, parsed_signal)?; } } } - json!({ + Ok(json!({ "self": true, "action": match action { SignalDispositionAction::Default => "default", SignalDispositionAction::Ignore => "ignore", SignalDispositionAction::User => "user", }, - }) + })) } pub(crate) fn vm_ids_for_scope( @@ -2508,7 +2601,9 @@ where .iter() .position(|event| event.vm_id == vm_id && event.process_id == process_id) { - return Ok(self.pending_process_events.remove(index)); + let envelope = self.pending_process_events.remove(index); + self.observe_pending_process_event_depth(); + return Ok(envelope); } let mut matching_envelope = None; @@ -3344,6 +3439,118 @@ fn symlinked_node_modules_hint(stderr: &str) -> Option<&'static str> { None } +#[cfg(test)] +mod legacy_child_spawn_options_tests { + use super::*; + + #[test] + fn legacy_v8_string_bridge_preserves_canonical_spawn_options() { + let vm_guest_env = BTreeMap::from([ + ( + String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), + String::from("node:path"), + ), + (String::from("AGENTOS_NOT_ALLOWED"), String::from("drop-me")), + ]); + let parsed = parse_legacy_javascript_child_process_spawn_options( + &vm_guest_env, + r#"{ + "argv0":"custom-zero", + "cloexecFds":[9,10], + "localReplacement":true, + "executableFd":11, + "cwd":"/work", + "env":{"VISIBLE":"yes"}, + "internalBootstrapEnv":{ + "AGENTOS_WASM_INITIAL_SIGNAL_MASK":"[10]", + "AGENTOS_NOT_ALLOWED":"drop-me-too" + }, + "spawnAttrFlags":70, + "spawnExactPath":false, + "spawnSearchPath":"/custom/bin:/bin", + "spawnSchedPolicy":0, + "spawnSchedPriority":0, + "spawnPgroup":42, + "spawnSignalDefaults":[13], + "spawnSignalMask":[10,12], + "spawnFileActions":[{ + "command":2, + "guestFd":41, + "fd":8, + "sourceFd":7, + "guestSourceFd":40, + "oflag":0, + "mode":420, + "path":"/tmp/unused" + }], + "spawnFdMappings":[[40,7],[50,8]], + "input":{"type":"Buffer","data":[97]}, + "shell":true, + "detached":true, + "stdio":["pipe","inherit","ignore"], + "maxBuffer":1234, + "timeout":5678, + "killSignal":"SIGUSR2" + }"#, + ) + .expect("parse V8 three-string options payload"); + + assert_eq!(parsed.max_buffer, Some(1234)); + let options = parsed.options; + assert_eq!(options.argv0.as_deref(), Some("custom-zero")); + assert_eq!(options.cloexec_fds, vec![9, 10]); + assert!(options.local_replacement); + assert_eq!(options.executable_fd, Some(11)); + assert_eq!(options.cwd.as_deref(), Some("/work")); + assert_eq!(options.env.get("VISIBLE").map(String::as_str), Some("yes")); + assert_eq!( + options + .internal_bootstrap_env + .get("AGENTOS_ALLOWED_NODE_BUILTINS") + .map(String::as_str), + Some("node:path") + ); + assert_eq!( + options + .internal_bootstrap_env + .get("AGENTOS_WASM_INITIAL_SIGNAL_MASK") + .map(String::as_str), + Some("[10]") + ); + assert!(!options + .internal_bootstrap_env + .contains_key("AGENTOS_NOT_ALLOWED")); + assert_eq!(options.spawn_attr_flags, 70); + assert!(!options.spawn_exact_path); + assert_eq!( + options.spawn_search_path.as_deref(), + Some("/custom/bin:/bin") + ); + assert_eq!(options.spawn_sched_policy, Some(0)); + assert_eq!(options.spawn_sched_priority, Some(0)); + assert_eq!(options.spawn_pgroup, Some(42)); + assert_eq!(options.spawn_signal_defaults, vec![13]); + assert_eq!(options.spawn_signal_mask, vec![10, 12]); + assert_eq!(options.spawn_fd_mappings, vec![[40, 7], [50, 8]]); + assert_eq!(options.spawn_file_actions.len(), 1); + let action = &options.spawn_file_actions[0]; + assert_eq!(action.command, 2); + assert_eq!(action.guest_fd, Some(41)); + assert_eq!(action.fd, 8); + assert_eq!(action.source_fd, 7); + assert_eq!(action.guest_source_fd, Some(40)); + assert_eq!(action.oflag, 0); + assert_eq!(action.mode, 420); + assert_eq!(action.path, "/tmp/unused"); + assert_eq!(options.input, Some(json!({"type":"Buffer","data":[97]}))); + assert!(options.shell); + assert!(options.detached); + assert_eq!(options.stdio, vec!["pipe", "inherit", "ignore"]); + assert_eq!(options.timeout, Some(5678)); + assert_eq!(options.kill_signal.as_deref(), Some("SIGUSR2")); + } +} + #[cfg(test)] mod symlinked_node_modules_hint_tests { use super::symlinked_node_modules_hint; diff --git a/crates/native-sidecar/src/state.rs b/crates/native-sidecar/src/state.rs index e0dd8be9db..2b142ec15a 100644 --- a/crates/native-sidecar/src/state.rs +++ b/crates/native-sidecar/src/state.rs @@ -9,11 +9,15 @@ use crate::protocol::{ SignalHandlerRegistration, SoftwareDescriptor, WasmPermissionTier, }; use crate::wire::DEFAULT_MAX_FRAME_BYTES; -use agentos_bridge::{BridgeTypes, FilesystemSnapshot}; +use agentos_bridge::{ + queue_tracker::{self, QueueGauge, TrackedLimit}, + BridgeTypes, FilesystemSnapshot, +}; use agentos_execution::{ v8_host::V8SessionHandle, JavascriptExecution, JavascriptSyncRpcRequest, PythonExecution, PythonVfsRpcRequest, WasmExecution, }; +use agentos_kernel::fd_table::TransferredFd; use agentos_kernel::kernel::{KernelProcessHandle, KernelVm}; use agentos_kernel::mount_table::MountTable; use agentos_kernel::root_fs::RootFilesystemMode; @@ -25,6 +29,7 @@ use rusqlite::Connection; use rustls::{ClientConnection, ServerConnection, StreamOwned}; use serde::{Deserialize, Serialize}; use serde_json::Value; +use socket2::Socket; use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::error::Error; use std::fmt; @@ -32,9 +37,9 @@ use std::fs::File; use std::net::{IpAddr, SocketAddr, TcpListener, TcpStream, UdpSocket}; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::PathBuf; -use std::sync::atomic::{AtomicBool, AtomicI64, Ordering}; +use std::sync::atomic::{AtomicBool, AtomicI64, AtomicUsize, Ordering}; use std::sync::mpsc::{Receiver, Sender}; -use std::sync::{Arc, Condvar, Mutex}; +use std::sync::{Arc, Condvar, Mutex, Weak}; use std::time::{Duration, Instant}; use tokio::sync::mpsc::UnboundedSender; @@ -46,6 +51,215 @@ pub(crate) type BridgeError = ::Error; pub(crate) type SidecarKernel = KernelVm; pub(crate) type KernelSocketReadinessRegistry = Arc>>; +pub(crate) type HostNetTransferDescriptionRegistry = + Arc>>; + +/// One VM-wide retained-byte envelope shared by every process queue of a +/// particular class. Per-process limits remain independently enforced; this +/// aggregate prevents `maxProcesses` from multiplying the VM's memory bound. +#[derive(Debug)] +pub(crate) struct VmPendingByteBudget { + used: AtomicUsize, + limit: usize, + gauge: Arc, +} + +impl VmPendingByteBudget { + pub(crate) fn new(limit: usize, tracked_limit: TrackedLimit) -> Arc { + Arc::new(Self { + used: AtomicUsize::new(0), + limit, + gauge: queue_tracker::register_queue(tracked_limit, limit), + }) + } + + pub(crate) fn try_reserve(&self, bytes: usize) -> bool { + if bytes == 0 { + return true; + } + let reserved = self + .used + .fetch_update(Ordering::AcqRel, Ordering::Acquire, |current| { + current + .checked_add(bytes) + .filter(|next| *next <= self.limit) + }); + match reserved { + Ok(previous) => { + self.gauge.observe_depth(previous.saturating_add(bytes)); + true + } + Err(current) => { + self.gauge.observe_depth(current); + false + } + } + } + + pub(crate) fn release(&self, bytes: usize) { + if bytes == 0 { + return; + } + let mut current = self.used.load(Ordering::Acquire); + loop { + let Some(next) = current.checked_sub(bytes) else { + tracing::error!( + released_bytes = bytes, + accounted_bytes = current, + limit = self.limit, + "pending-byte aggregate release exceeded accounted usage" + ); + self.gauge.observe_depth(current); + return; + }; + match self.used.compare_exchange_weak( + current, + next, + Ordering::AcqRel, + Ordering::Acquire, + ) { + Ok(_) => { + self.gauge.observe_depth(next); + return; + } + Err(actual) => current = actual, + } + } + } + + #[cfg(test)] + #[allow(dead_code)] + pub(crate) fn used(&self) -> usize { + self.used.load(Ordering::Acquire) + } + + pub(crate) fn limit(&self) -> usize { + self.limit + } +} + +#[derive(Debug)] +pub(crate) struct HostNetTransferDescription { + pub(crate) handles: Weak<()>, + pub(crate) connected: bool, +} + +#[derive(Debug)] +struct RealIntervalTimerState { + deadline: Option, + interval: Duration, + pending_expiry: bool, + shutdown: bool, +} + +/// Sidecar-clocked ITIMER_REAL state. One bounded worker is created for each +/// WASM process, and reconfiguration wakes that worker instead of spawning an +/// unbounded series of sleeping timer threads. +pub(crate) struct ActiveRealIntervalTimer { + state: Arc<(Mutex, Condvar)>, +} + +impl ActiveRealIntervalTimer { + pub(crate) fn new(start_worker: bool) -> Self { + let state = Arc::new(( + Mutex::new(RealIntervalTimerState { + deadline: None, + interval: Duration::ZERO, + pending_expiry: false, + shutdown: false, + }), + Condvar::new(), + )); + if start_worker { + let worker_state = Arc::clone(&state); + std::thread::spawn(move || { + let (lock, ready) = &*worker_state; + let mut timer = lock.lock().unwrap_or_else(|error| error.into_inner()); + loop { + if timer.shutdown { + break; + } + let Some(deadline) = timer.deadline else { + timer = ready.wait(timer).unwrap_or_else(|error| error.into_inner()); + continue; + }; + let now = Instant::now(); + if now < deadline { + let (next, _) = ready + .wait_timeout(timer, deadline.saturating_duration_since(now)) + .unwrap_or_else(|error| error.into_inner()); + timer = next; + continue; + } + + if timer.interval.is_zero() { + timer.deadline = None; + } else { + let interval = timer.interval; + let mut next = deadline; + while next <= now { + next = next.checked_add(interval).unwrap_or(now + interval); + } + timer.deadline = Some(next); + } + // Standard SIGALRM instances coalesce while pending. The + // runner drains this bit through process.take_signal at + // WASM syscall boundaries, including blocking sleeps. + timer.pending_expiry = true; + } + }); + } + Self { state } + } + + pub(crate) fn get(&self) -> (u64, u64) { + let timer = self + .state + .0 + .lock() + .unwrap_or_else(|error| error.into_inner()); + real_interval_timer_values(&timer, Instant::now()) + } + + pub(crate) fn set(&self, value_us: u64, interval_us: u64) -> (u64, u64) { + let (lock, ready) = &*self.state; + let mut timer = lock.lock().unwrap_or_else(|error| error.into_inner()); + let now = Instant::now(); + let previous = real_interval_timer_values(&timer, now); + timer.deadline = (value_us != 0).then(|| now + Duration::from_micros(value_us)); + timer.interval = Duration::from_micros(interval_us); + ready.notify_all(); + previous + } + + pub(crate) fn take_expiry(&self) -> bool { + let mut timer = self + .state + .0 + .lock() + .unwrap_or_else(|error| error.into_inner()); + std::mem::take(&mut timer.pending_expiry) + } +} + +fn real_interval_timer_values(timer: &RealIntervalTimerState, now: Instant) -> (u64, u64) { + let remaining = timer + .deadline + .map(|deadline| deadline.saturating_duration_since(now).as_micros()) + .unwrap_or_default() + .min(u128::from(u64::MAX)) as u64; + let interval = timer.interval.as_micros().min(u128::from(u64::MAX)) as u64; + (remaining, interval) +} + +impl Drop for ActiveRealIntervalTimer { + fn drop(&mut self) { + let (lock, ready) = &*self.state; + let mut timer = lock.lock().unwrap_or_else(|error| error.into_inner()); + timer.shutdown = true; + ready.notify_all(); + } +} // --------------------------------------------------------------------------- // Constants @@ -62,6 +276,7 @@ pub(crate) const WASM_COMMAND: &str = "wasm"; pub(crate) const PYTHON_VFS_RPC_GUEST_ROOT: &str = "/"; pub(crate) const EXECUTION_SANDBOX_ROOT_ENV: &str = "AGENTOS_SANDBOX_ROOT"; pub(crate) const WASM_STDIO_SYNC_RPC_ENV: &str = "AGENTOS_WASI_STDIO_SYNC_RPC"; +pub(crate) const WASM_EXEC_COMMIT_RPC_ENV: &str = "AGENTOS_WASM_EXEC_COMMIT_RPC"; #[cfg(test)] #[allow(dead_code)] pub(crate) const HOST_REALPATH_MAX_SYMLINK_DEPTH: usize = 40; @@ -326,6 +541,8 @@ pub(crate) struct VmState { /// Operator-tunable VM-scoped runtime limits. Immutable for the VM's lifetime; /// `ConfigureVm` does not mutate limits. pub(crate) limits: crate::limits::VmLimits, + pub(crate) pending_stdin_bytes_budget: Arc, + pub(crate) pending_event_bytes_budget: Arc, pub(crate) dns: VmDnsConfig, pub(crate) listen_policy: VmListenPolicy, pub(crate) create_loopback_exempt_ports: BTreeSet, @@ -337,6 +554,11 @@ pub(crate) struct VmState { pub(crate) host_cwd: PathBuf, pub(crate) kernel: SidecarKernel, pub(crate) kernel_socket_readiness: KernelSocketReadinessRegistry, + /// Sidecar-only host-network descriptions currently retained by an opaque + /// SCM_RIGHTS transfer. Weak entries make queue discard/receive lifecycle + /// automatic while allowing VM-wide limit accounting to see descriptions + /// that temporarily have no process-map entry. + pub(crate) host_net_transfer_descriptions: HostNetTransferDescriptionRegistry, pub(crate) loaded_snapshot: Option, pub(crate) configuration: VmConfiguration, pub(crate) layers: VmLayerStore, @@ -357,6 +579,17 @@ pub(crate) struct VmState { /// packages ship no `agentos-package.json`, so agent enumeration and /// resolution read this instead of the guest filesystem. pub(crate) projected_agent_launch: BTreeMap, + /// Guest paths that were present in the VM shadow root during the last + /// shadow->kernel sync walk. The next walk diffs the current shadow tree + /// against this set so guest deletions performed directly on the shadow + /// (host-side runtimes, WASI passthrough writes) propagate into the kernel + /// VFS instead of being resurrected by the otherwise additive sync. + /// Memory is bounded by the shadow tree itself, which is capped by the + /// kernel filesystem inode/byte resource limits that bound what the walk + /// can materialize. + pub(crate) shadow_sync_inventory: BTreeMap, + pub(crate) unix_address_registry: GuestUnixAddressRegistry, + pub(crate) unix_socket_host_dir: PathBuf, } /// Launch parameters for one projected agent package. @@ -373,6 +606,37 @@ pub(crate) struct ExitedProcessSnapshot { pub(crate) process: crate::protocol::ProcessSnapshotEntry, } +/// Filesystem object kind captured during a shadow-root inventory walk. +/// +/// Tracking the kind as well as the path is required for Linux replacement +/// semantics: a regular file replacing a symlink (or a directory replacing a +/// file) must replace the directory entry itself, never follow the stale +/// object that happened to occupy the same pathname in the kernel VFS. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum ShadowNodeType { + Directory, + File, + Symlink, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct ShadowSyncInventoryEntry { + pub(crate) node_type: ShadowNodeType, + /// The previous kernel entry could not be removed. Keeping the tombstone + /// makes the next walk retry instead of permanently forgetting a failed + /// reconciliation. + pub(crate) deletion_pending: bool, +} + +impl ShadowSyncInventoryEntry { + pub(crate) fn present(node_type: ShadowNodeType) -> Self { + Self { + node_type, + deletion_pending: false, + } + } +} + // --------------------------------------------------------------------------- // DNS configuration // --------------------------------------------------------------------------- @@ -385,7 +649,10 @@ pub(crate) struct VmDnsConfig { #[derive(Debug, Clone)] pub(crate) struct JavascriptSocketPathContext { - pub(crate) sandbox_root: PathBuf, + pub(crate) unix_abstract_namespace: [u8; 32], + pub(crate) unix_socket_host_dir: PathBuf, + pub(crate) unix_bound_addresses: GuestUnixAddressRegistry, + pub(crate) host_net_transfer_descriptions: HostNetTransferDescriptionRegistry, pub(crate) mounts: Vec, pub(crate) listen_policy: VmListenPolicy, pub(crate) loopback_exempt_ports: BTreeSet, @@ -398,6 +665,27 @@ pub(crate) struct JavascriptSocketPathContext { pub(crate) used_udp_guest_ports: BTreeMap>, } +#[derive(Debug, Clone)] +pub(crate) struct GuestUnixAddress { + pub(crate) path: String, + pub(crate) abstract_path_hex: Option, +} + +#[derive(Debug, Clone)] +pub(crate) struct GuestUnixAddressRegistryEntry { + pub(crate) host_address_key: String, + pub(crate) address: GuestUnixAddress, + pub(crate) guest_device_inode: Option<(u64, u64)>, + pub(crate) host_path: Option, + pub(crate) generation: u64, + pub(crate) active_bindings: usize, + pub(crate) queued_by_target: BTreeMap, + pub(crate) pending_connections: VecDeque>, +} + +pub(crate) type GuestUnixAddressRegistry = + Arc>>; + #[derive(Debug, Clone)] pub(crate) struct JavascriptHttpLoopbackTarget { pub(crate) process_id: String, @@ -449,11 +737,62 @@ impl Default for VmListenPolicy { // Active process state // --------------------------------------------------------------------------- +/// Stdin bytes accepted from a parent's `child_process.write_stdin` but not +/// yet written into the child's kernel stdin pipe. The kernel pipe holds at +/// most `MAX_PIPE_BUFFER_BYTES` (64 KiB) and `fd_write` reports partial +/// writes with POSIX pipe semantics, so multi-buffer stdin payloads (for +/// example git's spooled pack fed to `index-pack --stdin`) must be queued +/// host-side and flushed as the child drains its pipe. `close_requested` +/// defers the writer-fd close until the backlog fully drains so the child +/// never observes an early EOF. +#[derive(Default)] +pub(crate) struct PendingKernelStdin { + pub(crate) chunks: VecDeque>, + /// Bytes of the front chunk already written into the pipe. + pub(crate) front_offset: usize, + /// Total unwritten bytes across all queued chunks. + pub(crate) total: usize, + pub(crate) close_requested: bool, +} + +impl PendingKernelStdin { + const CHUNK_BYTES: usize = 64 * 1024; + + pub(crate) fn is_empty(&self) -> bool { + self.chunks.is_empty() + } + + pub(crate) fn push(&mut self, chunk: &[u8]) { + self.total += chunk.len(); + let mut remaining = chunk; + if let Some(back) = self.chunks.back_mut() { + let available = Self::CHUNK_BYTES.saturating_sub(back.len()); + let take = available.min(remaining.len()); + back.extend_from_slice(&remaining[..take]); + remaining = &remaining[take..]; + } + for part in remaining.chunks(Self::CHUNK_BYTES) { + self.chunks.push_back(part.to_vec()); + } + } + + pub(crate) fn clear(&mut self) { + self.chunks.clear(); + self.front_offset = 0; + self.total = 0; + } +} + #[allow(dead_code)] pub(crate) struct ActiveProcess { pub(crate) kernel_pid: u32, pub(crate) kernel_handle: KernelProcessHandle, pub(crate) kernel_stdin_writer_fd: Option, + /// Backlog for pipe-backed kernel stdin awaiting pipe capacity; see + /// [`PendingKernelStdin`]. + pub(crate) pending_kernel_stdin: PendingKernelStdin, + pub(crate) pending_kernel_stdin_gauge: Arc, + pub(crate) vm_pending_stdin_bytes_budget: Arc, /// For a TTY (PTY-backed) process, the master-end fd whose output buffer /// carries cooked-mode echo plus ONLCR-processed guest output. When set, /// this master output is the single ordered output stream surfaced to the @@ -469,7 +808,24 @@ pub(crate) struct ActiveProcess { pub(crate) mapped_host_fds: BTreeMap, pub(crate) next_mapped_host_fd: u32, pub(crate) pending_execution_events: VecDeque, + pub(crate) pending_execution_event_bytes: usize, + pub(crate) pending_execution_event_count_limit: usize, + pub(crate) pending_execution_event_bytes_limit: usize, + pub(crate) pending_execution_event_count_gauge: Arc, + pub(crate) pending_execution_event_bytes_gauge: Arc, + pub(crate) vm_pending_event_bytes_budget: Arc, pub(crate) pending_self_signal_exit: Option, + /// Actual terminating signal observed from the runtime process (or the + /// signal used for a shared-runtime synthetic exit). This is distinct from + /// a requested kill signal: handlers may catch one signal and later exit + /// for another reason. + pub(crate) exit_signal: Option, + pub(crate) exit_core_dumped: bool, + /// Pending standard signals use a set, matching Linux's coalescing rule: + /// multiple instances of the same standard signal occupy one pending bit. + pub(crate) pending_wasm_signals: BTreeSet, + pub(crate) pending_wasm_signals_gauge: Arc, + pub(crate) real_interval_timer: ActiveRealIntervalTimer, pub(crate) child_processes: BTreeMap, pub(crate) next_child_process_id: usize, pub(crate) http_servers: BTreeMap, @@ -791,7 +1147,7 @@ pub(crate) struct KernelSocketReadinessTarget { pub(crate) struct ActiveTcpSocket { pub(crate) stream: Option>>, pub(crate) pending_read_stream: Option>>>, - pub(crate) events: Option>, + pub(crate) events: Option>>>, pub(crate) event_sender: Option>, pub(crate) event_pusher: Arc>>, pub(crate) kernel_socket_id: Option, @@ -808,6 +1164,19 @@ pub(crate) struct ActiveTcpSocket { pub(crate) saw_local_shutdown: Arc, pub(crate) saw_remote_end: Arc, pub(crate) close_notified: Arc, + /// Bytes already read from the transport but not yet consumed by the + /// shared open socket description. Keeping this in the sidecar (rather + /// than per runner fd) preserves dup/SCM_RIGHTS read and MSG_PEEK + /// semantics across processes. + pub(crate) read_buffer: Arc>>, + /// One strong reference per guest-visible open socket description. This is + /// separate from transport/TLS worker Arcs so SCM_RIGHTS can decide when a + /// close is the final description close. + pub(crate) description_handles: Arc<()>, + /// Kernel open-description guard used after this socket first crosses + /// SCM_RIGHTS. It keeps owner-0 kernel sockets alive while queued or held + /// by another process and lets the kernel prune discarded transfers. + pub(crate) kernel_transfer_guard: Option, } #[derive(Debug)] @@ -944,6 +1313,10 @@ pub(crate) struct ActiveTcpListener { pub(crate) guest_local_addr: SocketAddr, pub(crate) backlog: usize, pub(crate) active_connection_ids: BTreeSet, + /// One strong reference per guest-visible listener description, including + /// descriptors queued in SCM_RIGHTS messages. + pub(crate) description_handles: Arc<()>, + pub(crate) kernel_transfer_guard: Option, } // --------------------------------------------------------------------------- @@ -964,28 +1337,66 @@ pub(crate) struct PendingUnixSocket { pub(crate) stream: UnixStream, pub(crate) local_path: Option, pub(crate) remote_path: Option, + pub(crate) local_abstract_path_hex: Option, + pub(crate) remote_abstract_path_hex: Option, + pub(crate) connection_guard: PendingUnixConnectionGuard, +} + +#[derive(Debug)] +pub(crate) struct GuestUnixConnectionState { + pub(crate) accepted_peer_open: AtomicBool, +} + +#[derive(Debug)] +pub(crate) struct PendingUnixConnectionGuard { + pub(crate) state: Option>, +} + +#[derive(Debug)] +pub(crate) struct UnixSocketReadState { + /// Bytes already removed from the host socket but not yet consumed by the + /// guest open socket description. The reader never grows this past + /// `max_buffered_bytes`, so an idle guest applies backpressure to the host + /// socket instead of growing an unbounded sidecar queue. + pub(crate) bytes: VecDeque, + pub(crate) terminal_events: VecDeque, + pub(crate) max_buffered_bytes: usize, + pub(crate) warned_near_limit: bool, + pub(crate) closed: bool, } #[derive(Debug)] pub(crate) struct ActiveUnixSocket { pub(crate) stream: Arc>, - pub(crate) events: Receiver, - pub(crate) event_sender: Sender, + pub(crate) read_state: Arc<(Mutex, Condvar)>, pub(crate) event_pusher: Arc>>, pub(crate) listener_id: Option, pub(crate) local_path: Option, pub(crate) remote_path: Option, + pub(crate) local_abstract_path_hex: Option, + pub(crate) remote_abstract_path_hex: Option, + pub(crate) local_registry_binding_id: Option, + pub(crate) remote_registry_binding_id: Option, + pub(crate) connection_state: Option>, + pub(crate) private_host_path: Option, pub(crate) saw_local_shutdown: Arc, pub(crate) saw_remote_end: Arc, pub(crate) close_notified: Arc, + pub(crate) description_handles: Arc<()>, } #[derive(Debug)] pub(crate) struct ActiveUnixListener { - pub(crate) listener: UnixListener, + pub(crate) listener: Option, + pub(crate) bound_socket: Option, pub(crate) path: String, + pub(crate) abstract_path_hex: Option, + pub(crate) registry_binding_id: String, + pub(crate) private_host_path: Option, + pub(crate) guest_node_path: Option, pub(crate) backlog: usize, pub(crate) active_connection_ids: BTreeSet, + pub(crate) description_handles: Arc<()>, } // --------------------------------------------------------------------------- @@ -1053,6 +1464,9 @@ pub(crate) struct ActiveUdpSocket { pub(crate) guest_local_addr: Option, pub(crate) recv_buffer_size: usize, pub(crate) send_buffer_size: usize, + /// One strong reference per guest-visible datagram socket description. + pub(crate) description_handles: Arc<()>, + pub(crate) kernel_transfer_guard: Option, } // --------------------------------------------------------------------------- @@ -1071,7 +1485,11 @@ pub(crate) enum ActiveExecution { pub(crate) struct ToolExecution { pub(crate) cancelled: Arc, pub(crate) pending_events: Arc>>, - pub(crate) events_overflowed: Arc, + pub(crate) event_overflow_reason: Arc>>, + pub(crate) pending_event_bytes: Arc, + pub(crate) pending_event_count_limit: Arc, + pub(crate) pending_event_bytes_limit: Arc, + pub(crate) vm_pending_event_bytes_budget: Arc, } impl Default for ToolExecution { @@ -1079,7 +1497,18 @@ impl Default for ToolExecution { Self { cancelled: Arc::new(AtomicBool::new(false)), pending_events: Arc::new(Mutex::new(VecDeque::new())), - events_overflowed: Arc::new(AtomicBool::new(false)), + event_overflow_reason: Arc::new(Mutex::new(None)), + pending_event_bytes: Arc::new(AtomicUsize::new(0)), + pending_event_count_limit: Arc::new(AtomicUsize::new( + agentos_native_sidecar_core::limits::DEFAULT_PROCESS_PENDING_EVENT_COUNT, + )), + pending_event_bytes_limit: Arc::new(AtomicUsize::new( + agentos_native_sidecar_core::limits::DEFAULT_PROCESS_PENDING_EVENT_BYTES, + )), + vm_pending_event_bytes_budget: VmPendingByteBudget::new( + agentos_native_sidecar_core::limits::DEFAULT_PROCESS_PENDING_EVENT_BYTES, + TrackedLimit::PendingExecutionEventBytes, + ), } } } diff --git a/crates/native-sidecar/src/stdio.rs b/crates/native-sidecar/src/stdio.rs index fb1bfe15ea..13d5b99dcb 100644 --- a/crates/native-sidecar/src/stdio.rs +++ b/crates/native-sidecar/src/stdio.rs @@ -64,6 +64,7 @@ const MAX_EVENT_READY_QUEUE: usize = 1; // frames from a busy turn should be buffered, so the writer only backpressures // when the host genuinely stops reading stdout rather than on every spike. const MAX_STDOUT_FRAME_QUEUE: usize = 4096; +const MAX_LIMIT_WARNING_QUEUE: usize = 128; #[cfg(test)] fn request_frame( @@ -163,12 +164,14 @@ async fn run_async(extensions: Vec>) -> Result<(), Box(); + channel::(MAX_LIMIT_WARNING_QUEUE); agentos_bridge::queue_tracker::set_limit_warning_handler(Box::new(move |warning| { - let _ = limit_warning_tx.send(warning.clone()); + if let Err(error) = limit_warning_tx.try_send(warning.clone()) { + eprintln!("failed to enqueue AgentOS limit warning: {error}"); + } })); let callback_transport = Arc::new(FrameSidecarRequestTransport::new(write_tx.clone())); sidecar.set_sidecar_request_transport(callback_transport.clone()); diff --git a/crates/native-sidecar/src/vm.rs b/crates/native-sidecar/src/vm.rs index be3afd17c3..3b7baff89c 100644 --- a/crates/native-sidecar/src/vm.rs +++ b/crates/native-sidecar/src/vm.rs @@ -22,9 +22,9 @@ use crate::service::{ }; use crate::state::{ BridgeError, KernelSocketReadinessEvent, KernelSocketReadinessRegistry, - KernelSocketReadinessTarget, VmConfiguration, VmDnsConfig, VmListenPolicy, VmState, - DISPOSE_VM_SIGKILL_GRACE, DISPOSE_VM_SIGTERM_GRACE, EXECUTION_DRIVER_NAME, JAVASCRIPT_COMMAND, - PYTHON_COMMAND, WASM_COMMAND, + KernelSocketReadinessTarget, VmConfiguration, VmDnsConfig, VmListenPolicy, VmPendingByteBudget, + VmState, DISPOSE_VM_SIGKILL_GRACE, DISPOSE_VM_SIGTERM_GRACE, EXECUTION_DRIVER_NAME, + JAVASCRIPT_COMMAND, PYTHON_COMMAND, WASM_COMMAND, }; use crate::{DispatchResult, NativeSidecar, NativeSidecarBridge, SidecarError}; @@ -43,6 +43,10 @@ use agentos_kernel::root_fs::{ RootFilesystemImportLimits, ROOT_FILESYSTEM_SNAPSHOT_FORMAT, }; use agentos_kernel::socket_table::{SocketReadiness, SocketReadinessKind}; +use agentos_native_sidecar_core::ca::{ + CA_CERTIFICATES_BUNDLE, CA_CERTIFICATES_GUEST_PATH, CA_CERTIFICATES_SYMLINK_PATH, + CA_CERTIFICATES_SYMLINK_TARGET, +}; use agentos_native_sidecar_core::permissions::{allow_all_policy, deny_all_policy}; use agentos_native_sidecar_core::{ layer_created_response, layer_sealed_response, overlay_created_response, @@ -53,11 +57,12 @@ use agentos_native_sidecar_core::{ }; use agentos_vm_config as vm_config; use base64::Engine; +use openssl::rand::rand_bytes; use std::collections::{BTreeMap, BTreeSet, VecDeque}; use std::fmt; use std::fs; use std::net::{IpAddr, SocketAddr}; -use std::os::unix::fs::PermissionsExt; +use std::os::unix::fs::{DirBuilderExt, PermissionsExt}; use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; @@ -109,6 +114,47 @@ const SHADOW_ROOT_BOOTSTRAP_DIRS: &[(&str, u32)] = &[ ("/workspace", 0o755), ]; +fn create_vm_unix_socket_host_dir() -> Result { + for _ in 0..32 { + let mut nonce = [0_u8; 16]; + rand_bytes(&mut nonce).map_err(|error| { + SidecarError::Io(format!("failed to generate Unix socket namespace: {error}")) + })?; + let suffix = nonce + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + let path = std::env::temp_dir().join(format!("agentos-uds-{suffix}")); + let mut builder = fs::DirBuilder::new(); + builder.mode(0o700); + match builder.create(&path) { + Ok(()) => { + if let Err(error) = fs::set_permissions(&path, fs::Permissions::from_mode(0o700)) { + let cleanup_error = fs::remove_dir(&path).err(); + return Err(SidecarError::Io(format!( + "failed to set private Unix socket namespace {} to mode 0700: {error}{}", + path.display(), + cleanup_error + .map(|cleanup| format!("; cleanup failed: {cleanup}")) + .unwrap_or_default() + ))); + } + return Ok(path); + } + Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => continue, + Err(error) => { + return Err(SidecarError::Io(format!( + "failed to create private Unix socket namespace {}: {error}", + path.display() + ))) + } + } + } + Err(SidecarError::Io(String::from( + "failed to allocate a unique private Unix socket namespace after 32 attempts", + ))) +} + fn send_kernel_socket_readiness_event( target: KernelSocketReadinessTarget, readiness: SocketReadiness, @@ -324,12 +370,27 @@ where .expect("owned session should exist") .vm_ids .insert(vm_id.clone()); + // Seed the baseline during VM creation. Otherwise a host-side deletion + // that happens before the first shadow sync has no prior inventory and + // the deleted kernel entry is resurrected/order-dependent. + let shadow_sync_inventory = crate::execution::initial_shadow_sync_inventory(&cwd)?; + let unix_socket_host_dir = create_vm_unix_socket_host_dir()?; + let pending_stdin_bytes_budget = VmPendingByteBudget::new( + limits.process.pending_stdin_bytes, + agentos_bridge::queue_tracker::TrackedLimit::PendingKernelStdinBytes, + ); + let pending_event_bytes_budget = VmPendingByteBudget::new( + limits.process.pending_event_bytes, + agentos_bridge::queue_tracker::TrackedLimit::PendingExecutionEventBytes, + ); self.vms.insert( vm_id.clone(), VmState { connection_id: connection_id.clone(), session_id: session_id.clone(), limits, + pending_stdin_bytes_budget, + pending_event_bytes_budget, dns, listen_policy, create_loopback_exempt_ports, @@ -341,6 +402,7 @@ where host_cwd, kernel, kernel_socket_readiness, + host_net_transfer_descriptions: Arc::new(Mutex::new(BTreeMap::new())), loaded_snapshot, configuration: VmConfiguration { permissions: permissions_policy, @@ -358,6 +420,9 @@ where signal_states: BTreeMap::new(), packages_staging_root: None, projected_agent_launch: BTreeMap::new(), + shadow_sync_inventory, + unix_address_registry: Arc::new(Mutex::new(BTreeMap::new())), + unix_socket_host_dir, }, ); @@ -908,6 +973,15 @@ where if let Some(staging_root) = vm.packages_staging_root.take() { let _ = fs::remove_dir_all(&staging_root); } + if let Err(error) = fs::remove_dir_all(&vm.unix_socket_host_dir) { + if error.kind() != std::io::ErrorKind::NotFound { + tracing::warn!( + path = %vm.unix_socket_host_dir.display(), + %error, + "failed to remove private Unix socket namespace during VM teardown" + ); + } + } // Surface the first failure only AFTER cleanup has completed. terminate_result?; @@ -1178,6 +1252,8 @@ fn bootstrap_native_root_filesystem( filesystem.chmod(guest_path, *mode).map_err(vfs_error)?; } + seed_native_ca_certificates_bundle(filesystem)?; + for entry in &descriptor.bootstrap_entries { apply_native_root_filesystem_entry(filesystem, entry)?; } @@ -1196,6 +1272,7 @@ fn apply_native_root_filesystem_entry( .next() .expect("root snapshot from one entry should contain one entry"); ensure_mounted_parent_directories(filesystem, &kernel_entry.path)?; + prepare_mounted_destination(filesystem, &kernel_entry.path, &kernel_entry.kind)?; match kernel_entry.kind { KernelFilesystemEntryKind::Directory => filesystem @@ -1229,6 +1306,76 @@ fn apply_native_root_filesystem_entry( Ok(()) } +fn seed_native_ca_certificates_bundle( + filesystem: &mut dyn MountedFileSystem, +) -> Result<(), SidecarError> { + if CA_CERTIFICATES_BUNDLE.is_empty() { + return Err(SidecarError::Io( + "embedded Mozilla CA certificate bundle is empty".to_string(), + )); + } + + if !mounted_entry_exists(filesystem, CA_CERTIFICATES_GUEST_PATH)? { + ensure_mounted_parent_directories(filesystem, CA_CERTIFICATES_GUEST_PATH)?; + filesystem + .write_file(CA_CERTIFICATES_GUEST_PATH, CA_CERTIFICATES_BUNDLE.to_vec()) + .map_err(vfs_error)?; + filesystem + .chmod(CA_CERTIFICATES_GUEST_PATH, 0o644) + .map_err(vfs_error)?; + filesystem + .chown(CA_CERTIFICATES_GUEST_PATH, 0, 0) + .map_err(vfs_error)?; + } + + if !mounted_entry_exists(filesystem, CA_CERTIFICATES_SYMLINK_PATH)? { + ensure_mounted_parent_directories(filesystem, CA_CERTIFICATES_SYMLINK_PATH)?; + filesystem + .symlink(CA_CERTIFICATES_SYMLINK_TARGET, CA_CERTIFICATES_SYMLINK_PATH) + .map_err(vfs_error)?; + } + + Ok(()) +} + +fn mounted_entry_exists( + filesystem: &dyn MountedFileSystem, + path: &str, +) -> Result { + match filesystem.lstat(path) { + Ok(_) => Ok(true), + Err(error) if error.code() == "ENOENT" => Ok(false), + Err(error) => Err(vfs_error(error)), + } +} + +fn prepare_mounted_destination( + filesystem: &mut dyn MountedFileSystem, + path: &str, + desired_kind: &KernelFilesystemEntryKind, +) -> Result<(), SidecarError> { + let existing = match filesystem.lstat(path) { + Ok(existing) => existing, + Err(error) if error.code() == "ENOENT" => return Ok(()), + Err(error) => return Err(vfs_error(error)), + }; + let already_compatible = match desired_kind { + KernelFilesystemEntryKind::Directory => existing.is_directory && !existing.is_symbolic_link, + KernelFilesystemEntryKind::File => !existing.is_directory && !existing.is_symbolic_link, + KernelFilesystemEntryKind::Symlink => false, + }; + if already_compatible { + return Ok(()); + } + + if existing.is_directory && !existing.is_symbolic_link { + filesystem.remove_dir(path).map_err(vfs_error)?; + } else { + filesystem.remove_file(path).map_err(vfs_error)?; + } + Ok(()) +} + fn ensure_mounted_parent_directories( filesystem: &mut dyn MountedFileSystem, path: &str, @@ -1722,6 +1869,11 @@ fn create_vm_shadow_root(vm_id: &str) -> Result { let root = std::env::temp_dir().join(format!("agentos-native-sidecar-shadow-{vm_id}-{nonce}")); fs::create_dir_all(&root) .map_err(|error| SidecarError::Io(format!("failed to create VM shadow root: {error}")))?; + initialize_vm_shadow_root(root) +} + +fn initialize_vm_shadow_root(root: PathBuf) -> Result { + let cleanup_root = root.clone(); // macOS: `std::env::temp_dir()` lives under `/var/folders/…`, but `/var` is a // symlink to `/private/var`, and macOS fd→path recovery (`fcntl(F_GETPATH)`) // reports the resolved `/private/var/…` form. Canonicalize the shadow root up @@ -1729,12 +1881,25 @@ fn create_vm_shadow_root(vm_id: &str) -> Result { // mapped-runtime confinement prefix checks (`strip_prefix(host_root)`) reject // every child and guest `readdir` of a populated dir returns empty. host_dir // mounts already canonicalize their root for the same reason. - #[cfg(target_os = "macos")] - let root = fs::canonicalize(&root).map_err(|error| { - SidecarError::Io(format!("failed to canonicalize VM shadow root: {error}")) - })?; - bootstrap_shadow_root(&root)?; - Ok(root) + let initialized = (|| { + #[cfg(target_os = "macos")] + let root = fs::canonicalize(&root).map_err(|error| { + SidecarError::Io(format!("failed to canonicalize VM shadow root: {error}")) + })?; + bootstrap_shadow_root(&root)?; + Ok(root) + })(); + + match initialized { + Ok(root) => Ok(root), + Err(error) => match fs::remove_dir_all(&cleanup_root) { + Ok(()) => Err(error), + Err(cleanup_error) => Err(SidecarError::Io(format!( + "{error}; additionally failed to clean shadow root {}: {cleanup_error}", + cleanup_root.display() + ))), + }, + } } fn bootstrap_shadow_root(root: &Path) -> Result<(), SidecarError> { @@ -1753,6 +1918,76 @@ fn bootstrap_shadow_root(root: &Path) -> Result<(), SidecarError> { )) })?; } + seed_ca_certificates_bundle(root)?; + Ok(()) +} + +/// Seed the Mozilla CA bundle into the shadow root at +/// `/etc/ssl/certs/ca-certificates.crt` (plus the conventional +/// `/etc/ssl/cert.pem` symlink) so guest TLS clients resolve trust the standard +/// Linux way. +fn seed_ca_certificates_bundle(root: &Path) -> Result<(), SidecarError> { + if CA_CERTIFICATES_BUNDLE.is_empty() { + return Err(SidecarError::Io( + "embedded Mozilla CA certificate bundle is empty".to_string(), + )); + } + + let bundle_path = shadow_path_for_guest(root, CA_CERTIFICATES_GUEST_PATH); + if let Some(parent) = bundle_path.parent() { + fs::create_dir_all(parent).map_err(|error| { + SidecarError::Io(format!( + "failed to create shadow CA certs directory {}: {error}", + parent.display() + )) + })?; + } + match fs::symlink_metadata(&bundle_path) { + Ok(_) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + fs::write(&bundle_path, CA_CERTIFICATES_BUNDLE).map_err(|error| { + SidecarError::Io(format!( + "failed to seed CA bundle {}: {error}", + bundle_path.display() + )) + })?; + fs::set_permissions(&bundle_path, fs::Permissions::from_mode(0o644)).map_err( + |error| { + SidecarError::Io(format!( + "failed to set CA bundle mode on {}: {error}", + bundle_path.display() + )) + }, + )?; + } + Err(error) => { + return Err(SidecarError::Io(format!( + "failed to inspect shadow CA bundle {}: {error}", + bundle_path.display() + ))); + } + } + + let symlink_path = shadow_path_for_guest(root, CA_CERTIFICATES_SYMLINK_PATH); + match fs::symlink_metadata(&symlink_path) { + Ok(_) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => { + std::os::unix::fs::symlink(CA_CERTIFICATES_SYMLINK_TARGET, &symlink_path).map_err( + |error| { + SidecarError::Io(format!( + "failed to seed CA bundle symlink {}: {error}", + symlink_path.display() + )) + }, + )?; + } + Err(error) => { + return Err(SidecarError::Io(format!( + "failed to inspect shadow CA bundle symlink {}: {error}", + symlink_path.display() + ))); + } + } Ok(()) } @@ -1771,7 +2006,9 @@ fn materialize_shadow_root_snapshot_entries( }) .transpose()? { - return materialize_shadow_entries(shadow_root, &root_snapshot_entries(&snapshot)); + materialize_shadow_entries(shadow_root, &root_snapshot_entries(&snapshot))?; + materialize_shadow_entries(shadow_root, &descriptor.bootstrap_entries)?; + return Ok(()); } validate_shadow_descriptor_import_limits(descriptor, &import_limits)?; @@ -1933,6 +2170,7 @@ fn materialize_shadow_entries( )) })?; } + prepare_shadow_destination(&shadow_path, &entry.kind, &entry.path)?; match entry.kind { crate::protocol::RootFilesystemEntryKind::Directory => { @@ -1953,8 +2191,6 @@ fn materialize_shadow_entries( })?; } crate::protocol::RootFilesystemEntryKind::Symlink => { - let _ = fs::remove_file(&shadow_path); - let _ = fs::remove_dir_all(&shadow_path); std::os::unix::fs::symlink( entry.target.as_deref().ok_or_else(|| { SidecarError::InvalidState(format!( @@ -1998,6 +2234,46 @@ fn materialize_shadow_entries( Ok(()) } +fn prepare_shadow_destination( + path: &Path, + desired_kind: &crate::protocol::RootFilesystemEntryKind, + guest_path: &str, +) -> Result<(), SidecarError> { + let existing = match fs::symlink_metadata(path) { + Ok(existing) => existing, + Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()), + Err(error) => { + return Err(SidecarError::Io(format!( + "failed to inspect shadow entry {guest_path}: {error}" + ))); + } + }; + let file_type = existing.file_type(); + let already_compatible = match desired_kind { + crate::protocol::RootFilesystemEntryKind::Directory => { + file_type.is_dir() && !file_type.is_symlink() + } + crate::protocol::RootFilesystemEntryKind::File => { + file_type.is_file() && !file_type.is_symlink() + } + crate::protocol::RootFilesystemEntryKind::Symlink => false, + }; + if already_compatible { + return Ok(()); + } + + let result = if file_type.is_dir() && !file_type.is_symlink() { + fs::remove_dir_all(path) + } else { + fs::remove_file(path) + }; + result.map_err(|error| { + SidecarError::Io(format!( + "failed to replace incompatible shadow entry {guest_path}: {error}" + )) + }) +} + fn decode_root_entry_content(entry: &RootFilesystemEntry) -> Result, SidecarError> { let content = entry.content.as_deref().unwrap_or_default(); match entry @@ -2148,9 +2424,11 @@ fn prune_kernel_command_stub( #[cfg(test)] mod tests { use super::{ - bootstrap_native_root_filesystem, bootstrap_shadow_root, - materialize_shadow_root_snapshot_entries, native_root_plugin_from_config, - prune_kernel_command_stub, shadow_path_for_guest, KERNEL_COMMAND_STUB, + bootstrap_native_root_filesystem, bootstrap_shadow_root, create_vm_unix_socket_host_dir, + initialize_vm_shadow_root, materialize_shadow_root_snapshot_entries, + native_root_plugin_from_config, prune_kernel_command_stub, shadow_path_for_guest, + CA_CERTIFICATES_BUNDLE, CA_CERTIFICATES_GUEST_PATH, CA_CERTIFICATES_SYMLINK_PATH, + CA_CERTIFICATES_SYMLINK_TARGET, KERNEL_COMMAND_STUB, }; use crate::plugins::chunked_local::ChunkedLocalMountPlugin; use crate::protocol::{ @@ -2167,8 +2445,32 @@ mod tests { use agentos_kernel::vfs::VirtualFileSystem; use std::fs; use std::os::unix::fs::PermissionsExt; + use std::path::Path; use std::time::{SystemTime, UNIX_EPOCH}; + #[test] + fn vm_unix_socket_host_directories_are_private_and_unique() { + let first = create_vm_unix_socket_host_dir() + .expect("first private Unix socket namespace should be created"); + let second = create_vm_unix_socket_host_dir() + .expect("second private Unix socket namespace should be created"); + + assert_ne!(first, second, "VMs must not share a Unix socket namespace"); + for path in [&first, &second] { + let mode = fs::metadata(path) + .expect("private Unix socket namespace metadata should be readable") + .permissions() + .mode() + & 0o7777; + assert_eq!(mode, 0o700, "private Unix socket namespace must be 0700"); + fs::remove_dir(path).expect("private Unix socket namespace should be removable"); + assert!( + !path.exists(), + "removed Unix socket namespace must stay absent" + ); + } + } + #[test] fn bootstrap_shadow_root_seeds_standard_directories() { let unique = SystemTime::now() @@ -2207,6 +2509,56 @@ mod tests { fs::remove_dir_all(&root).expect("temp shadow root should be removed"); } + #[test] + fn bootstrap_shadow_root_seeds_ca_bundle_when_present() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock should be monotonic") + .as_nanos(); + let root = std::env::temp_dir().join(format!("agentos-native-sidecar-ca-test-{unique}")); + fs::create_dir_all(&root).expect("temp shadow root should be created"); + + bootstrap_shadow_root(&root).expect("shadow bootstrap should succeed"); + + let bundle = shadow_path_for_guest(&root, CA_CERTIFICATES_GUEST_PATH); + let symlink = shadow_path_for_guest(&root, CA_CERTIFICATES_SYMLINK_PATH); + + assert!(!CA_CERTIFICATES_BUNDLE.is_empty()); + let seeded = fs::read(&bundle).expect("CA bundle should be seeded"); + assert_eq!( + seeded, CA_CERTIFICATES_BUNDLE, + "seeded CA bundle should match the embedded asset" + ); + let target = fs::read_link(&symlink).expect("cert.pem symlink should be seeded"); + assert_eq!( + target, + Path::new(CA_CERTIFICATES_SYMLINK_TARGET), + "cert.pem should point at certs/ca-certificates.crt" + ); + + fs::remove_dir_all(&root).expect("temp shadow root should be removed"); + } + + #[test] + fn failed_shadow_bootstrap_removes_temporary_root() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock should be monotonic") + .as_nanos(); + let root = + std::env::temp_dir().join(format!("agentos-native-sidecar-shadow-failure-{unique}")); + fs::create_dir_all(&root).expect("temp shadow root should be created"); + fs::write(root.join("dev"), b"blocks directory creation") + .expect("blocking file should be created"); + + initialize_vm_shadow_root(root.clone()) + .expect_err("invalid shadow scaffold should fail bootstrap"); + assert!( + !root.exists(), + "failed bootstrap must not leak its temporary shadow root" + ); + } + #[test] fn native_root_config_opens_chunked_local_as_persistent_root() { let unique = SystemTime::now() @@ -2245,12 +2597,20 @@ mod tests { bootstrap_native_root_filesystem( filesystem.as_mut(), &RootFilesystemDescriptor { - bootstrap_entries: vec![RootFilesystemEntry { - path: "/etc/agentos/boot.txt".to_string(), - kind: RootFilesystemEntryKind::File, - content: Some("booted".to_string()), - ..Default::default() - }], + bootstrap_entries: vec![ + RootFilesystemEntry { + path: "/etc/agentos/boot.txt".to_string(), + kind: RootFilesystemEntryKind::File, + content: Some("booted".to_string()), + ..Default::default() + }, + RootFilesystemEntry { + path: CA_CERTIFICATES_SYMLINK_PATH.to_string(), + kind: RootFilesystemEntryKind::File, + content: Some("custom native cert.pem\n".to_string()), + ..Default::default() + }, + ], ..Default::default() }, ) @@ -2267,6 +2627,24 @@ mod tests { .expect("bootstrap file should be readable"), b"booted".to_vec() ); + assert_eq!( + mount_table + .read_file(CA_CERTIFICATES_GUEST_PATH) + .expect("default CA bundle should be readable from native root"), + CA_CERTIFICATES_BUNDLE + ); + assert_eq!( + mount_table + .read_file(CA_CERTIFICATES_SYMLINK_PATH) + .expect("custom regular cert.pem should replace the default symlink"), + b"custom native cert.pem\n".to_vec() + ); + assert!( + !mount_table + .lstat(CA_CERTIFICATES_SYMLINK_PATH) + .expect("lstat custom native cert.pem") + .is_symbolic_link + ); mount_table .write_file("/home/agentos/persist.txt", b"persisted".to_vec()) .expect("write through sqlite root should succeed"); @@ -2305,6 +2683,75 @@ mod tests { let _ = fs::remove_dir_all(block_root); } + #[test] + fn custom_shadow_ca_files_replace_seeded_defaults_without_following_symlinks() { + let unique = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("clock should be monotonic") + .as_nanos(); + let root = + std::env::temp_dir().join(format!("agentos-native-sidecar-shadow-custom-ca-{unique}")); + fs::create_dir_all(&root).expect("temp shadow root should be created"); + bootstrap_shadow_root(&root).expect("shadow bootstrap should succeed"); + + let descriptor = RootFilesystemDescriptor { + bootstrap_entries: vec![ + RootFilesystemEntry { + path: "/custom/ca.pem".to_string(), + kind: RootFilesystemEntryKind::File, + content: Some("custom bundle\n".to_string()), + ..Default::default() + }, + RootFilesystemEntry { + path: CA_CERTIFICATES_GUEST_PATH.to_string(), + kind: RootFilesystemEntryKind::Symlink, + target: Some("../../../custom/ca.pem".to_string()), + ..Default::default() + }, + RootFilesystemEntry { + path: CA_CERTIFICATES_SYMLINK_PATH.to_string(), + kind: RootFilesystemEntryKind::File, + content: Some("custom cert.pem\n".to_string()), + ..Default::default() + }, + ], + ..RootFilesystemDescriptor::default() + }; + + materialize_shadow_root_snapshot_entries( + &root, + &descriptor, + None, + &ResourceLimits::default(), + ) + .expect("custom CA entries should materialize"); + + let bundle = shadow_path_for_guest(&root, CA_CERTIFICATES_GUEST_PATH); + let cert_pem = shadow_path_for_guest(&root, CA_CERTIFICATES_SYMLINK_PATH); + assert_eq!( + fs::read(&bundle).expect("read custom bundle through custom symlink"), + b"custom bundle\n" + ); + assert_eq!( + fs::read_link(&bundle).expect("read custom CA bundle symlink"), + Path::new("../../../custom/ca.pem"), + "a custom symlink must replace the seeded regular bundle" + ); + assert_eq!( + fs::read(&cert_pem).expect("read custom regular cert.pem"), + b"custom cert.pem\n" + ); + assert!( + !fs::symlink_metadata(cert_pem) + .expect("lstat custom cert.pem") + .file_type() + .is_symlink(), + "custom cert.pem must replace rather than follow the seeded symlink" + ); + + fs::remove_dir_all(&root).expect("temp shadow root should be removed"); + } + #[test] fn materialize_shadow_root_snapshot_entries_rejects_oversized_legacy_restored_snapshots() { let unique = SystemTime::now() diff --git a/crates/native-sidecar/tests/filesystem.rs b/crates/native-sidecar/tests/filesystem.rs index d0503985fe..c045191cdb 100644 --- a/crates/native-sidecar/tests/filesystem.rs +++ b/crates/native-sidecar/tests/filesystem.rs @@ -344,6 +344,8 @@ mod shadow_root { use serde_json::json; use std::collections::HashMap; use std::fs; + use std::os::unix::fs::PermissionsExt; + use std::path::{Path, PathBuf}; use std::time::Duration; use crate::support::{ @@ -395,10 +397,22 @@ mod shadow_root { }, }]; mounts.extend(extra_mounts); + configure_vm_mounts(sidecar, connection_id, session_id, &vm_id, mounts); + + vm_id + } + + fn configure_vm_mounts( + sidecar: &mut agentos_native_sidecar::NativeSidecar, + connection_id: &str, + session_id: &str, + vm_id: &str, + mounts: Vec, + ) { sidecar .dispatch_wire_blocking(wire_request( 4, - wire_vm(connection_id, session_id, &vm_id), + wire_vm(connection_id, session_id, vm_id), RequestPayload::ConfigureVmRequest(ConfigureVmRequest { mounts, software: Vec::new(), @@ -415,8 +429,6 @@ mod shadow_root { }), )) .expect("configure command mount"); - - vm_id } fn registry_command_root() -> Option { @@ -424,12 +436,12 @@ mod shadow_root { .join("../..") .canonicalize() .expect("canonicalize repo root"); - let copied = repo_root.join("registry/software/coreutils/wasm"); + let copied = repo_root.join("software/coreutils/wasm"); if copied.exists() { return Some(copied.to_string_lossy().into_owned()); } - let fallback = repo_root.join("registry/native/target/wasm32-wasip1/release/commands"); + let fallback = repo_root.join("toolchain/target/wasm32-wasip1/release/commands"); if fallback.exists() { return Some(fallback.to_string_lossy().into_owned()); } @@ -449,7 +461,7 @@ mod shadow_root { vm_id: &str, request_id: i64, payload: GuestFilesystemCallRequest, - ) { + ) -> agentos_native_sidecar::wire::GuestFilesystemResultResponse { let response = sidecar .dispatch_wire_blocking(wire_request( request_id, @@ -458,11 +470,927 @@ mod shadow_root { )) .expect("dispatch guest filesystem call"); match response.response.payload { - ResponsePayload::GuestFilesystemResultResponse(_) => {} + ResponsePayload::GuestFilesystemResultResponse(result) => result, other => panic!("expected guest_filesystem_result response, got {other:?}"), } } + fn base_guest_filesystem_request( + operation: GuestFilesystemOperation, + path: &str, + ) -> GuestFilesystemCallRequest { + GuestFilesystemCallRequest { + operation, + path: String::from(path), + destination_path: None, + target: None, + content: None, + encoding: None, + recursive: false, + max_depth: None, + mode: None, + uid: None, + gid: None, + atime_ms: None, + mtime_ms: None, + len: None, + offset: None, + } + } + + fn guest_path_exists( + sidecar: &mut agentos_native_sidecar::NativeSidecar, + connection_id: &str, + session_id: &str, + vm_id: &str, + request_id: i64, + path: &str, + ) -> bool { + let response = sidecar + .dispatch_wire_blocking(wire_request( + request_id, + wire_vm(connection_id, session_id, vm_id), + RequestPayload::GuestFilesystemCallRequest(base_guest_filesystem_request( + GuestFilesystemOperation::Exists, + path, + )), + )) + .expect("dispatch guest filesystem exists"); + match response.response.payload { + ResponsePayload::GuestFilesystemResultResponse(result) => { + result.exists.unwrap_or(false) + } + other => panic!("expected guest_filesystem_result response, got {other:?}"), + } + } + + fn guest_lstat( + sidecar: &mut agentos_native_sidecar::NativeSidecar, + connection_id: &str, + session_id: &str, + vm_id: &str, + request_id: i64, + path: &str, + ) -> agentos_native_sidecar::wire::GuestFilesystemStat { + guest_filesystem_call( + sidecar, + connection_id, + session_id, + vm_id, + request_id, + base_guest_filesystem_request(GuestFilesystemOperation::Lstat, path), + ) + .stat + .expect("guest lstat response should include stat") + } + + fn guest_read_text( + sidecar: &mut agentos_native_sidecar::NativeSidecar, + connection_id: &str, + session_id: &str, + vm_id: &str, + request_id: i64, + path: &str, + ) -> String { + let response = guest_filesystem_call( + sidecar, + connection_id, + session_id, + vm_id, + request_id, + base_guest_filesystem_request(GuestFilesystemOperation::ReadFile, path), + ); + assert_eq!( + response.encoding, + Some(RootFilesystemEntryEncoding::Utf8), + "test fixture should remain UTF-8" + ); + response + .content + .expect("read response should include content") + } + + fn locate_shadow_root(marker_guest_path: &str) -> PathBuf { + let marker_relative = marker_guest_path.trim_start_matches('/'); + fs::read_dir(std::env::temp_dir()) + .expect("list temp dir") + .filter_map(|entry| entry.ok().map(|entry| entry.path())) + .find(|candidate| { + candidate + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.starts_with("agentos-native-sidecar-shadow-")) + && fs::symlink_metadata(candidate.join(marker_relative)).is_ok() + }) + .expect("locate VM shadow root through unique marker") + } + + #[derive(Debug, Clone, Copy, PartialEq, Eq)] + enum TestNodeType { + File, + Symlink, + Directory, + } + + fn create_guest_test_node( + sidecar: &mut agentos_native_sidecar::NativeSidecar, + connection_id: &str, + session_id: &str, + vm_id: &str, + request_id: &mut i64, + path: &str, + node_type: TestNodeType, + ) { + let request = match node_type { + TestNodeType::File => { + let mut request = + base_guest_filesystem_request(GuestFilesystemOperation::WriteFile, path); + request.content = Some(String::from("replacement file\n")); + request.encoding = Some(RootFilesystemEntryEncoding::Utf8); + request + } + TestNodeType::Symlink => { + let mut request = + base_guest_filesystem_request(GuestFilesystemOperation::Symlink, path); + request.target = Some(String::from("target.txt")); + request + } + TestNodeType::Directory => { + let mut request = + base_guest_filesystem_request(GuestFilesystemOperation::Mkdir, path); + request.recursive = true; + request + } + }; + guest_filesystem_call( + sidecar, + connection_id, + session_id, + vm_id, + *request_id, + request, + ); + *request_id += 1; + } + + fn replace_shadow_test_node(path: &Path, node_type: TestNodeType) { + if let Ok(metadata) = fs::symlink_metadata(path) { + if metadata.is_dir() && !metadata.file_type().is_symlink() { + fs::remove_dir_all(path).expect("remove old shadow directory"); + } else { + fs::remove_file(path).expect("remove old shadow file or symlink"); + } + } + match node_type { + TestNodeType::File => { + fs::write(path, b"replacement file\n").expect("write replacement shadow file") + } + TestNodeType::Symlink => std::os::unix::fs::symlink("target.txt", path) + .expect("create replacement shadow symlink"), + TestNodeType::Directory => { + fs::create_dir(path).expect("create replacement shadow directory") + } + } + } + + /// Deleting a path directly from the VM shadow root (the way host-side + /// guest runtimes delete files, without a kernel-direct unlink) must + /// propagate into the kernel VFS on the next shadow sync walk instead of + /// being resurrected by the additive copy-in. + #[test] + fn shadow_direct_deletions_before_first_sync_propagate_into_kernel_vfs() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar); + // Guest filesystem calls need no command mounts; create the VM bare so + // this regression test runs even without built registry commands. + let cwd = temp_dir("filesystem-shadow-reconcile-cwd"); + let (vm_id, _) = create_vm_wire( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::JavaScript, + &cwd, + ); + + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock") + .as_nanos(); + let doomed_root = format!("/workspace/reconcile-{nonce}"); + let doomed_dir = format!("{doomed_root}/nested"); + let doomed_file = format!("{doomed_dir}/probe.txt"); + let survivor_file = format!("/workspace/reconcile-survivor-{nonce}.txt"); + + let mut mkdir = + base_guest_filesystem_request(GuestFilesystemOperation::CreateDir, &doomed_dir); + mkdir.recursive = true; + guest_filesystem_call(&mut sidecar, &connection_id, &session_id, &vm_id, 20, mkdir); + + let mut write = + base_guest_filesystem_request(GuestFilesystemOperation::WriteFile, &doomed_file); + write.content = Some(String::from("doomed\n")); + write.encoding = Some(RootFilesystemEntryEncoding::Utf8); + guest_filesystem_call(&mut sidecar, &connection_id, &session_id, &vm_id, 21, write); + + let mut survivor = + base_guest_filesystem_request(GuestFilesystemOperation::WriteFile, &survivor_file); + survivor.content = Some(String::from("survivor\n")); + survivor.encoding = Some(RootFilesystemEntryEncoding::Utf8); + guest_filesystem_call( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + 22, + survivor, + ); + + // Locate this VM's shadow root through the mirrored unique file, then + // delete the subtree before any read-side sync primes the inventory. + // This is exactly how a short-lived host-side runtime file can be + // created and deleted between reconciliation walks. + let marker_rel = format!("workspace/reconcile-{nonce}/nested/probe.txt"); + let shadow_root = std::fs::read_dir(std::env::temp_dir()) + .expect("list temp dir") + .filter_map(|entry| entry.ok().map(|entry| entry.path())) + .find(|candidate| { + candidate + .file_name() + .and_then(|name| name.to_str()) + .is_some_and(|name| name.starts_with("agentos-native-sidecar-shadow-")) + && candidate.join(&marker_rel).is_file() + }) + .expect("locate VM shadow root through mirrored probe file"); + fs::remove_dir_all(shadow_root.join(format!("workspace/reconcile-{nonce}"))) + .expect("delete subtree from the shadow root"); + + // The next host filesystem call re-walks the shadow; the kernel must + // drop the deleted subtree instead of resurrecting it forever. + assert!( + !guest_path_exists( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + 24, + &doomed_file, + ), + "kernel resurrected a file deleted from the shadow root" + ); + assert!( + !guest_path_exists( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + 25, + &doomed_root, + ), + "kernel resurrected a directory deleted from the shadow root" + ); + assert!( + guest_path_exists( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + 26, + &survivor_file, + ), + "deletion reconcile must not remove shadow-backed paths that still exist" + ); + + dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); + } + + #[test] + fn shadow_type_replacements_cover_file_symlink_directory_matrix() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar); + let cwd = temp_dir("filesystem-shadow-replacement-cwd"); + let (vm_id, _) = create_vm_wire( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::JavaScript, + &cwd, + ); + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock") + .as_nanos(); + let base = format!("/workspace/replacement-matrix-{nonce}"); + let target = format!("{base}/target.txt"); + let mut request_id = 20; + create_guest_test_node( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + &mut request_id, + &target, + TestNodeType::File, + ); + let shadow_root = locate_shadow_root(&target); + let transitions = [ + (TestNodeType::File, TestNodeType::Symlink), + (TestNodeType::File, TestNodeType::Directory), + (TestNodeType::Symlink, TestNodeType::File), + (TestNodeType::Symlink, TestNodeType::Directory), + (TestNodeType::Directory, TestNodeType::File), + (TestNodeType::Directory, TestNodeType::Symlink), + ]; + + for (index, (initial, replacement)) in transitions.into_iter().enumerate() { + let path = format!("{base}/node-{index}"); + create_guest_test_node( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + &mut request_id, + &path, + initial, + ); + // Prime this node's initial type so reconciliation must explicitly + // unlink the stale kernel node before copying the replacement. + guest_lstat( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + request_id, + &path, + ); + request_id += 1; + + replace_shadow_test_node(&shadow_root.join(path.trim_start_matches('/')), replacement); + let stat = guest_lstat( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + request_id, + &path, + ); + request_id += 1; + match replacement { + TestNodeType::File => { + assert!(!stat.is_directory && !stat.is_symbolic_link) + } + TestNodeType::Symlink => assert!(stat.is_symbolic_link), + TestNodeType::Directory => { + assert!(stat.is_directory && !stat.is_symbolic_link) + } + } + assert_eq!( + guest_read_text( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + request_id, + &target, + ), + "replacement file\n", + "replacing {initial:?} with {replacement:?} followed and overwrote the stale symlink target" + ); + request_id += 1; + } + + dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); + } + + #[test] + fn guest_rename_moves_a_broken_symlink_without_resurrecting_its_source() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar); + let cwd = temp_dir("filesystem-shadow-broken-symlink-rename-cwd"); + let (vm_id, _) = create_vm_wire( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::JavaScript, + &cwd, + ); + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock") + .as_nanos(); + let source = format!("/workspace/broken-link-{nonce}"); + let destination = format!("/workspace/renamed-broken-link-{nonce}"); + let target = format!("missing-target-{nonce}"); + let mut symlink_request = + base_guest_filesystem_request(GuestFilesystemOperation::Symlink, &source); + symlink_request.target = Some(target.clone()); + guest_filesystem_call( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + 20, + symlink_request, + ); + + let shadow_root = locate_shadow_root(&source); + let shadow_source = shadow_root.join(source.trim_start_matches('/')); + assert!(fs::symlink_metadata(&shadow_source) + .expect("lstat broken shadow symlink") + .file_type() + .is_symlink()); + assert!( + fs::metadata(&shadow_source).is_err(), + "test symlink must remain dangling" + ); + + let mut rename_request = + base_guest_filesystem_request(GuestFilesystemOperation::Rename, &source); + rename_request.destination_path = Some(destination.clone()); + guest_filesystem_call( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + 21, + rename_request, + ); + + assert!( + !guest_path_exists( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + 22, + &source, + ), + "reconciliation resurrected the renamed broken symlink at its source" + ); + assert!( + guest_lstat( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + 23, + &destination, + ) + .is_symbolic_link + ); + assert_eq!( + guest_filesystem_call( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + 24, + base_guest_filesystem_request(GuestFilesystemOperation::ReadLink, &destination,), + ) + .target + .as_deref(), + Some(target.as_str()) + ); + + dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); + } + + #[test] + fn shadow_mode_change_updates_an_existing_kernel_directory() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar); + let cwd = temp_dir("filesystem-shadow-existing-directory-mode-cwd"); + let (vm_id, _) = create_vm_wire( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::JavaScript, + &cwd, + ); + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock") + .as_nanos(); + let directory = format!("/workspace/mode-change-{nonce}"); + let mut request_id = 20; + create_guest_test_node( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + &mut request_id, + &directory, + TestNodeType::Directory, + ); + let shadow_root = locate_shadow_root(&directory); + let shadow_directory = shadow_root.join(directory.trim_start_matches('/')); + fs::set_permissions(&shadow_directory, fs::Permissions::from_mode(0o710)) + .expect("change existing shadow directory mode"); + + let stat = guest_lstat( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + request_id, + &directory, + ); + assert_eq!( + stat.mode & 0o7777, + 0o710, + "shadow reconciliation did not import the existing directory mode" + ); + + fs::set_permissions(&shadow_directory, fs::Permissions::from_mode(0o755)) + .expect("restore shadow directory mode"); + dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); + } + + #[test] + fn guest_chmod_zero_preserves_descendant_deletion_inventory() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar); + let cwd = temp_dir("filesystem-shadow-chmod-zero-cwd"); + let (vm_id, _) = create_vm_wire( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::JavaScript, + &cwd, + ); + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock") + .as_nanos(); + let directory = format!("/workspace/chmod-zero-{nonce}"); + let child = format!("{directory}/tracked-child.txt"); + let mut request_id = 20; + create_guest_test_node( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + &mut request_id, + &child, + TestNodeType::File, + ); + let shadow_root = locate_shadow_root(&child); + let shadow_directory = shadow_root.join(directory.trim_start_matches('/')); + let shadow_child = shadow_root.join(child.trim_start_matches('/')); + + let mut chmod_request = + base_guest_filesystem_request(GuestFilesystemOperation::Chmod, &directory); + chmod_request.mode = Some(0); + guest_filesystem_call( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + request_id, + chmod_request, + ); + request_id += 1; + + fs::set_permissions(&shadow_directory, fs::Permissions::from_mode(0o755)) + .expect("restore shadow directory access for direct deletion"); + fs::remove_file(&shadow_child).expect("delete tracked child from shadow"); + assert!( + !guest_path_exists( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + request_id, + &child, + ), + "chmod 000 discarded descendant inventory and resurrected a deleted child" + ); + + dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); + } + + #[test] + fn unreadable_shadow_subtree_does_not_delete_kernel_children() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar); + let cwd = temp_dir("filesystem-shadow-unreadable-cwd"); + let (vm_id, _) = create_vm_wire( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::JavaScript, + &cwd, + ); + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock") + .as_nanos(); + let directory = format!("/workspace/unreadable-{nonce}"); + let child = format!("{directory}/preserved.txt"); + let mut request_id = 20; + create_guest_test_node( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + &mut request_id, + &child, + TestNodeType::File, + ); + assert!(guest_path_exists( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + request_id, + &child, + )); + + let shadow_root = locate_shadow_root(&child); + let shadow_directory = shadow_root.join(directory.trim_start_matches('/')); + let original_mode = fs::metadata(&shadow_directory) + .expect("stat shadow directory") + .permissions() + .mode(); + fs::set_permissions(&shadow_directory, fs::Permissions::from_mode(0o000)) + .expect("make shadow directory unreadable"); + if fs::read_dir(&shadow_directory).is_ok() { + fs::set_permissions(&shadow_directory, fs::Permissions::from_mode(original_mode)) + .expect("restore readable shadow directory"); + dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); + return; + } + + let preserved = guest_path_exists( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + request_id + 1, + &child, + ); + fs::set_permissions(&shadow_directory, fs::Permissions::from_mode(original_mode)) + .expect("restore readable shadow directory"); + assert!( + preserved, + "an unreadable shadow subtree was mistaken for a deleted subtree" + ); + + dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); + } + + #[test] + fn shadow_sync_skips_every_live_normalized_mount_boundary() { + let host_mount = temp_dir("filesystem-shadow-mount-boundary-host"); + fs::write(host_mount.join("value.txt"), b"host plugin\n").expect("seed host mount file"); + + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar); + let cwd = temp_dir("filesystem-shadow-mount-boundary-cwd"); + let (vm_id, _) = create_vm_wire( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::JavaScript, + &cwd, + ); + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock") + .as_nanos(); + let locator = format!("/workspace/mount-boundary-locator-{nonce}.txt"); + let mut request_id = 20; + create_guest_test_node( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + &mut request_id, + &locator, + TestNodeType::File, + ); + let shadow_root = locate_shadow_root(&locator); + + configure_vm_mounts( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + vec![ + MountDescriptor { + guest_path: String::from("/mount-boundaries/./memory//"), + read_only: false, + plugin: MountPluginDescriptor { + id: String::from("memory"), + config: String::from("{}"), + }, + }, + MountDescriptor { + guest_path: String::from("/mount-boundaries/host/../host//"), + read_only: false, + plugin: MountPluginDescriptor { + id: String::from("host_dir"), + config: serde_json::to_string(&json!({ + "hostPath": host_mount.to_string_lossy().into_owned(), + "readOnly": false, + })) + .expect("serialize host mount config"), + }, + }, + ], + ); + + let memory_path = "/mount-boundaries/memory/value.txt"; + let mut write_memory = + base_guest_filesystem_request(GuestFilesystemOperation::WriteFile, memory_path); + write_memory.content = Some(String::from("memory plugin\n")); + write_memory.encoding = Some(RootFilesystemEntryEncoding::Utf8); + guest_filesystem_call( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + request_id, + write_memory, + ); + request_id += 1; + + let shadow_memory = shadow_root.join("mount-boundaries/memory/value.txt"); + let shadow_host = shadow_root.join("mount-boundaries/host/value.txt"); + fs::create_dir_all(shadow_host.parent().expect("shadow host parent")) + .expect("create stale host mount shadow"); + fs::write(&shadow_memory, b"stale shadow\n").expect("overwrite memory mount shadow"); + fs::write(&shadow_host, b"stale shadow\n").expect("write host mount shadow"); + + assert_eq!( + guest_read_text( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + request_id, + memory_path, + ), + "memory plugin\n" + ); + request_id += 1; + assert_eq!( + guest_read_text( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + request_id, + "/mount-boundaries/host/value.txt", + ), + "host plugin\n" + ); + request_id += 1; + + fs::remove_dir_all(shadow_root.join("mount-boundaries/memory")) + .expect("delete memory mount shadow subtree"); + fs::remove_dir_all(shadow_root.join("mount-boundaries/host")) + .expect("delete host mount shadow subtree"); + assert_eq!( + guest_read_text( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + request_id, + memory_path, + ), + "memory plugin\n", + "shadow deletion crossed the normalized memory mount boundary" + ); + request_id += 1; + assert_eq!( + guest_read_text( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + request_id, + "/mount-boundaries/host/value.txt", + ), + "host plugin\n", + "shadow deletion crossed the normalized host_dir mount boundary" + ); + + dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); + fs::remove_dir_all(host_mount).expect("remove host mount temp dir"); + } + + #[test] + fn failed_shadow_directory_deletion_retries_after_unmounted_mountpoint_is_removed() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = authenticate_and_open_session(&mut sidecar); + let cwd = temp_dir("filesystem-shadow-delete-retry-cwd"); + let (vm_id, _) = create_vm_wire( + &mut sidecar, + 3, + &connection_id, + &session_id, + GuestRuntimeKind::JavaScript, + &cwd, + ); + let nonce = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system clock") + .as_nanos(); + let directory = format!("/workspace/delete-retry-{nonce}"); + let mountpoint = format!("{directory}/mounted"); + let mut request_id = 20; + + create_guest_test_node( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + &mut request_id, + &directory, + TestNodeType::Directory, + ); + assert!(guest_path_exists( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + request_id, + &directory, + )); + request_id += 1; + + // Mounting creates the mountpoint in the kernel VFS but not in the host + // shadow. After unmounting, that kernel-only directory makes the first + // tracked parent removal fail ENOTEMPTY. + configure_vm_mounts( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + vec![MountDescriptor { + guest_path: mountpoint.clone(), + read_only: false, + plugin: MountPluginDescriptor { + id: String::from("memory"), + config: String::from("{}"), + }, + }], + ); + configure_vm_mounts( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + Vec::new(), + ); + let shadow_root = locate_shadow_root(&directory); + fs::remove_dir_all(shadow_root.join(directory.trim_start_matches('/'))) + .expect("remove retry directory from shadow"); + assert!( + guest_path_exists( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + request_id, + &directory, + ), + "first non-empty directory deletion should leave the kernel directory for retry" + ); + request_id += 1; + + guest_filesystem_call( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + request_id, + base_guest_filesystem_request(GuestFilesystemOperation::RemoveDir, &mountpoint), + ); + request_id += 1; + assert!( + !guest_path_exists( + &mut sidecar, + &connection_id, + &session_id, + &vm_id, + request_id, + &directory, + ), + "pending directory deletion was not retried after its blocker disappeared" + ); + + dispose_vm_and_close_session(&mut sidecar, &connection_id, &session_id, &vm_id); + } + #[allow(clippy::too_many_arguments)] fn execute_command( sidecar: &mut agentos_native_sidecar::NativeSidecar, diff --git a/crates/native-sidecar/tests/fixtures/limits-inventory.json b/crates/native-sidecar/tests/fixtures/limits-inventory.json index 65f5098b8a..08e26cf1a1 100644 --- a/crates/native-sidecar/tests/fixtures/limits-inventory.json +++ b/crates/native-sidecar/tests/fixtures/limits-inventory.json @@ -1114,5 +1114,11 @@ "path": "crates/execution/src/wasm.rs", "class": "invariant", "rationale": "Host-side LRU entry cap for cached wasm module bytes; process-wide cache sizing, not per-VM policy." + }, + { + "name": "MAX_PENDING_KERNEL_STDIN_BYTES", + "path": "crates/native-sidecar/src/execution.rs", + "class": "policy-deferred", + "rationale": "Host-side backlog cap for child stdin awaiting kernel pipe capacity; mirrors resource.max_fd_write_bytes, not yet wired to VmLimits." } ] diff --git a/crates/native-sidecar/tests/posix_path_repro.rs b/crates/native-sidecar/tests/posix_path_repro.rs index 3bec55fdc8..00ae319966 100644 --- a/crates/native-sidecar/tests/posix_path_repro.rs +++ b/crates/native-sidecar/tests/posix_path_repro.rs @@ -5,7 +5,6 @@ use agentos_native_sidecar::wire::{ }; use serde_json::{json, Value}; use std::collections::HashMap; -use std::fs; use std::path::{Path, PathBuf}; use std::process::Command; use std::time::Duration; @@ -30,19 +29,33 @@ const ALLOWED_NODE_BUILTINS: &[&str] = &[ "util", ]; +fn strip_benign_child_pid_warnings(stderr: &str) -> String { + stderr + .lines() + .filter(|line| !line.contains("WARN") || !line.contains("could not retrieve pid")) + .collect::>() + .join("\n") +} + fn registry_command_root() -> PathBuf { let repo_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../..") .canonicalize() .expect("canonicalize repo root"); - let commands = repo_root.join("registry/native/target/wasm32-wasip1/release/commands"); - if commands.exists() { - return commands; + let copied = repo_root.join("software/coreutils/wasm"); + if copied.exists() { + return copied; + } + + let fallback = repo_root.join("toolchain/target/wasm32-wasip1/release/commands"); + if fallback.exists() { + return fallback; } panic!( - "registry WASM commands are required for posix path repro tests: run `just registry-native`; expected {}", - commands.display() + "registry WASM commands are required for posix path repro tests: expected {} or {}", + copied.display(), + fallback.display() ); } @@ -343,9 +356,11 @@ console.log(JSON.stringify({ "guest shell should resolve relative paths inside the cwd: {guest}" ); assert_eq!( - guest["stderr"] - .as_str() - .expect("child stderr should be encoded as a string"), + strip_benign_child_pid_warnings( + guest["stderr"] + .as_str() + .expect("child stderr should be encoded as a string") + ), "", "guest shell should not emit unexpected stderr: {guest}" ); @@ -465,9 +480,11 @@ console.log(JSON.stringify({ "guest shell should still succeed with absolute paths: {guest}" ); assert_eq!( - guest["stderr"] - .as_str() - .expect("child stderr should be encoded as a string"), + strip_benign_child_pid_warnings( + guest["stderr"] + .as_str() + .expect("child stderr should be encoded as a string") + ), "", "guest shell should not emit unexpected stderr: {guest}" ); @@ -485,7 +502,6 @@ fn node_path_posix_edge_cases_match_host_node() { import path from "node:path"; console.log(JSON.stringify({ - posixIdentity: path.posix === path, resolve: path.resolve("/workspace/project/", "./src", "../tests", "spec.ts"), join: path.join("/workspace", "project", "..", "project", "note.txt"), normalize: path.normalize("/workspace//project/tests/../nested//file.txt"), @@ -498,113 +514,6 @@ console.log(JSON.stringify({ ); } -fn node_console_formatting_matches_host_node() { - assert_guest_matches_host( - "console-formatting", - r#" -const writes = []; -const originalWrite = process.stdout.write; -process.stdout.write = (chunk) => { - writes.push(String(chunk)); - return true; -}; -console.log("value:%s count:%d object:%o", "ok", 3, { nested: true }); -process.stdout.write = originalWrite; -originalWrite.call(process.stdout, JSON.stringify({ writes })); -"#, - ); -} - -fn javascript_child_process_executes_guest_shebang_scripts() { - assert_node_available(); - - let (cwd, entrypoint) = write_probe( - "child-shebang", - r#" -import childProcess from "node:child_process"; - -const result = childProcess.spawnSync( - "/workspace/hello.sh", - ["native"], - { encoding: "utf8" }, -); -const denied = childProcess.spawnSync( - "/workspace/not-executable.sh", - [], - { encoding: "utf8" }, -); -console.log(JSON.stringify({ - status: result.status, - signal: result.signal, - stdout: result.stdout, - stderr: result.stderr, - denied: { - status: denied.status, - signal: denied.signal, - errorCode: denied.error?.code ?? null, - stderr: denied.stderr, - }, -})); -"#, - ); - let script = cwd.join("hello.sh"); - write_fixture( - &script, - "#!/usr/bin/env -S sh\nprintf 'shebang:%s\\n' \"$1\"\n", - ); - let mut permissions = fs::metadata(&script) - .expect("read shebang fixture metadata") - .permissions(); - use std::os::unix::fs::PermissionsExt; - permissions.set_mode(0o755); - fs::set_permissions(&script, permissions).expect("make shebang fixture executable"); - write_fixture( - &cwd.join("not-executable.sh"), - "#!/bin/sh\nprintf 'must-not-run\\n'\n", - ); - - let guest = run_guest_probe( - "child-shebang", - &cwd, - &entrypoint, - true, - HashMap::new(), - vec![MountDescriptor { - guest_path: String::from("/workspace"), - read_only: false, - plugin: MountPluginDescriptor { - id: String::from("host_dir"), - config: serde_json::to_string(&json!({ - "hostPath": cwd, - "readOnly": false, - })) - .expect("serialize shebang workspace mount config"), - }, - }], - ); - - assert_eq!(guest["status"], json!(0), "shebang child failed: {guest}"); - assert_eq!( - guest["signal"], - Value::Null, - "shebang child signaled: {guest}" - ); - assert_eq!(guest["stdout"], "shebang:native\n"); - assert_eq!(guest["stderr"], ""); - assert_ne!( - guest["denied"]["status"], - json!(0), - "non-executable script unexpectedly ran: {guest}" - ); - assert!( - guest["denied"]["errorCode"] == "EACCES" - || guest["denied"]["stderr"] - .as_str() - .is_some_and(|stderr| stderr.contains("EACCES")), - "non-executable script did not surface EACCES: {guest}" - ); -} - fn filesystem_path_edge_cases_match_host_node() { assert_guest_matches_host( "filesystem-paths", @@ -647,7 +556,5 @@ fn posix_path_repro_suite() { filesystem_path_edge_cases_match_host_node(); guest_shell_absolute_paths_still_work_after_cd(); guest_shell_relative_paths_follow_cwd_after_cd(); - javascript_child_process_executes_guest_shebang_scripts(); - node_console_formatting_matches_host_node(); node_path_posix_edge_cases_match_host_node(); } diff --git a/crates/native-sidecar/tests/projection_bench.rs b/crates/native-sidecar/tests/projection_bench.rs index db6094cda5..034b4830f8 100644 --- a/crates/native-sidecar/tests/projection_bench.rs +++ b/crates/native-sidecar/tests/projection_bench.rs @@ -58,9 +58,9 @@ //! //! Then runs the repo's built registry tars (skipped with a note if a tar is //! absent, e.g. in a clean checkout that has not built `dist/`): -//! - coreutils: `registry/software/coreutils/dist/package.tar` (large, many commands) -//! - tar: `registry/software/tar/dist/package.tar` (single wasm binary) -//! - git: `registry/software/git/dist/package.tar` (the package +//! - coreutils: `software/coreutils/dist/package.tar` (large, many commands) +//! - tar: `software/tar/dist/package.tar` (single wasm binary) +//! - git: `software/git/dist/package.tar` (the package //! the marketing install comparison talks about) //! //! Override the source tars via env: @@ -73,9 +73,9 @@ //! ```text //! cargo test -p agentos-native-sidecar --release --test projection_bench -- --ignored --nocapture //! # or, pointing at built tars elsewhere: -//! PROJ_BENCH_COREUTILS_TAR=/abs/registry/software/coreutils/dist/package.tar \ -//! PROJ_BENCH_TAR_TAR=/abs/registry/software/tar/dist/package.tar \ -//! PROJ_BENCH_GIT_TAR=/abs/registry/software/git/dist/package.tar \ +//! PROJ_BENCH_COREUTILS_TAR=/abs/software/coreutils/dist/package.tar \ +//! PROJ_BENCH_TAR_TAR=/abs/software/tar/dist/package.tar \ +//! PROJ_BENCH_GIT_TAR=/abs/software/git/dist/package.tar \ //! cargo test -p agentos-native-sidecar --release --test projection_bench -- --ignored --nocapture //! ``` @@ -612,16 +612,10 @@ fn projection_bench() { let coreutils_tar = source_tar_path( "PROJ_BENCH_COREUTILS_TAR", - "registry/software/coreutils/dist/package.tar", - ); - let tar_tar = source_tar_path( - "PROJ_BENCH_TAR_TAR", - "registry/software/tar/dist/package.tar", - ); - let git_tar = source_tar_path( - "PROJ_BENCH_GIT_TAR", - "registry/software/git/dist/package.tar", + "software/coreutils/dist/package.tar", ); + let tar_tar = source_tar_path("PROJ_BENCH_TAR_TAR", "software/tar/dist/package.tar"); + let git_tar = source_tar_path("PROJ_BENCH_GIT_TAR", "software/git/dist/package.tar"); println!("\n# agentOS package load benchmark (.aospkg)"); println!( @@ -688,7 +682,7 @@ fn projection_bench() { fn coreutils_load_budget() { let coreutils_tar = source_tar_path( "PROJ_BENCH_COREUTILS_TAR", - "registry/software/coreutils/dist/package.tar", + "software/coreutils/dist/package.tar", ); if !coreutils_tar.is_file() { eprintln!( diff --git a/crates/native-sidecar/tests/service.rs b/crates/native-sidecar/tests/service.rs index 3387887e69..3299574f6a 100644 --- a/crates/native-sidecar/tests/service.rs +++ b/crates/native-sidecar/tests/service.rs @@ -111,7 +111,7 @@ mod service { ActiveCipherSession, ActiveDiffieHellmanSession, ActiveEcdhSession, ActiveExecution, ActiveExecutionEvent, ActiveProcess, ActiveSqliteDatabase, ActiveSqliteStatement, ActiveTcpListener, ActiveUdpSocket, ProcessEventEnvelope, SidecarKernel, ToolExecution, - VmListenPolicy, EXECUTION_SANDBOX_ROOT_ENV, JAVASCRIPT_COMMAND, + VmPendingByteBudget, EXECUTION_SANDBOX_ROOT_ENV, JAVASCRIPT_COMMAND, LOOPBACK_EXEMPT_PORTS_ENV, PYTHON_COMMAND, VM_DNS_SERVERS_METADATA_KEY, VM_LISTEN_ALLOW_PRIVILEGED_METADATA_KEY, VM_LISTEN_PORT_MAX_METADATA_KEY, VM_LISTEN_PORT_MIN_METADATA_KEY, WASM_COMMAND, WASM_STDIO_SYNC_RPC_ENV, @@ -297,12 +297,18 @@ ykAheWCsAteSEWVc0w==\n\ process_id: &str, ) { let kernel_handle = create_kernel_process_handle_for_tests(); + let vm = sidecar.vms.get(vm_id).expect("test vm"); + let process_limits = vm.limits.process.clone(); + let stdin_budget = Arc::clone(&vm.pending_stdin_bytes_budget); + let event_budget = Arc::clone(&vm.pending_event_bytes_budget); let process = ActiveProcess::new( kernel_handle.pid(), kernel_handle, GuestRuntimeKind::JavaScript, ActiveExecution::Tool(ToolExecution::default()), - ); + ) + .with_process_event_limits(&process_limits) + .with_vm_pending_byte_budgets(stdin_budget, event_budget); sidecar .vms .get_mut(vm_id) @@ -487,16 +493,27 @@ ykAheWCsAteSEWVc0w==\n\ fn tool_execution_event_overflow_is_reported() { let tool_execution = ToolExecution::default(); - for _ in 0..MAX_PROCESS_EVENT_QUEUE { - assert!(crate::execution::send_tool_process_event( - &tool_execution.pending_events, - &tool_execution.events_overflowed, - ActiveExecutionEvent::Stdout(Vec::new()), - )); - } + tool_execution + .pending_event_count_limit + .store(1, Ordering::Release); + assert!(crate::execution::send_tool_process_event( + &tool_execution.cancelled, + &tool_execution.pending_events, + &tool_execution.event_overflow_reason, + &tool_execution.pending_event_bytes, + &tool_execution.pending_event_count_limit, + &tool_execution.pending_event_bytes_limit, + &tool_execution.vm_pending_event_bytes_budget, + ActiveExecutionEvent::Stdout(Vec::new()), + )); assert!(!crate::execution::send_tool_process_event( + &tool_execution.cancelled, &tool_execution.pending_events, - &tool_execution.events_overflowed, + &tool_execution.event_overflow_reason, + &tool_execution.pending_event_bytes, + &tool_execution.pending_event_count_limit, + &tool_execution.pending_event_bytes_limit, + &tool_execution.vm_pending_event_bytes_budget, ActiveExecutionEvent::Exited(0), )); @@ -507,24 +524,334 @@ ykAheWCsAteSEWVc0w==\n\ let local = tokio::task::LocalSet::new(); runtime.block_on(local.run_until(async move { let mut execution = ActiveExecution::Tool(tool_execution); - for _ in 0..MAX_PROCESS_EVENT_QUEUE { - assert!(matches!( - execution - .poll_event(Duration::ZERO) - .await - .expect("poll queued tool event"), - Some(ActiveExecutionEvent::Stdout(_)) - )); - } + assert!(matches!( + execution + .poll_event(Duration::ZERO) + .await + .expect("poll queued tool event"), + Some(ActiveExecutionEvent::Stdout(_)) + )); let error = execution .poll_event(Duration::ZERO) .await .expect_err("tool event overflow should be reported"); assert!( - error.to_string().contains("process event queue exceeded"), + error + .to_string() + .contains("limits.process.pendingEventCount"), "unexpected overflow error: {error}" ); })); + + let tool_execution = ToolExecution::default(); + tool_execution + .pending_event_bytes_limit + .store(8, Ordering::Release); + assert!(!crate::execution::send_tool_process_event( + &tool_execution.cancelled, + &tool_execution.pending_events, + &tool_execution.event_overflow_reason, + &tool_execution.pending_event_bytes, + &tool_execution.pending_event_count_limit, + &tool_execution.pending_event_bytes_limit, + &tool_execution.vm_pending_event_bytes_budget, + ActiveExecutionEvent::Stdout(vec![0; 9]), + )); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("create tokio runtime"); + runtime.block_on(async move { + let mut execution = ActiveExecution::Tool(tool_execution); + let error = execution + .poll_event(Duration::ZERO) + .await + .expect_err("tool byte overflow should be reported"); + assert!( + error + .to_string() + .contains("limits.process.pendingEventBytes"), + "unexpected overflow error: {error}" + ); + }); + } + + fn vm_pending_event_bytes_are_shared_and_reclaimed() { + let event_envelope_bytes = ActiveExecutionEvent::Stdout(Vec::new()).retained_bytes(); + let aggregate_limit = event_envelope_bytes.saturating_mul(2).saturating_add(10); + let event_budget = VmPendingByteBudget::new( + aggregate_limit, + agentos_bridge::queue_tracker::TrackedLimit::PendingExecutionEventBytes, + ); + let stdin_budget = VmPendingByteBudget::new( + 64, + agentos_bridge::queue_tracker::TrackedLimit::PendingKernelStdinBytes, + ); + let mut limits = agentos_native_sidecar_core::limits::ProcessLimits::default(); + limits.pending_event_bytes = aggregate_limit; + + let mut child_one = { + let kernel_handle = create_kernel_process_handle_for_tests(); + ActiveProcess::new( + kernel_handle.pid(), + kernel_handle, + GuestRuntimeKind::WebAssembly, + ActiveExecution::Tool(ToolExecution::default()), + ) + .with_process_event_limits(&limits) + .with_vm_pending_byte_budgets(Arc::clone(&stdin_budget), Arc::clone(&event_budget)) + }; + let mut child_two = { + let kernel_handle = create_kernel_process_handle_for_tests(); + ActiveProcess::new( + kernel_handle.pid(), + kernel_handle, + GuestRuntimeKind::WebAssembly, + ActiveExecution::Tool(ToolExecution::default()), + ) + .with_process_event_limits(&limits) + .with_vm_pending_byte_budgets(Arc::clone(&stdin_budget), Arc::clone(&event_budget)) + }; + + child_one + .queue_pending_execution_event(ActiveExecutionEvent::Stdout(vec![1; 6])) + .expect("first child should reserve six VM event bytes"); + child_two + .queue_pending_execution_event(ActiveExecutionEvent::Stdout(vec![2; 4])) + .expect("second child should consume the remaining VM event bytes"); + let error = child_two + .queue_pending_execution_event(ActiveExecutionEvent::Stdout(vec![3])) + .expect_err("aggregate event bytes must reject a third enqueue"); + assert!( + error + .to_string() + .contains("limits.process.pendingEventBytes"), + "unexpected aggregate event error: {error}" + ); + assert_eq!(event_budget.used(), aggregate_limit); + + assert!(matches!( + child_one.pop_pending_execution_event(), + Some(ActiveExecutionEvent::Stdout(bytes)) if bytes.len() == 6 + )); + assert_eq!( + event_budget.used(), + event_envelope_bytes + 4, + "pop must release its bytes" + ); + child_two + .queue_pending_execution_event(ActiveExecutionEvent::Stdout(vec![4; 6])) + .expect("released event capacity should be reusable by a sibling"); + assert_eq!(event_budget.used(), aggregate_limit); + + drop(child_two); + assert_eq!( + event_budget.used(), + 0, + "process teardown must reclaim all of its pending events" + ); + + let tool_one = ToolExecution::default() + .with_vm_pending_event_bytes_budget(Arc::clone(&event_budget)); + let tool_two = ToolExecution::default() + .with_vm_pending_event_bytes_budget(Arc::clone(&event_budget)); + assert!(crate::execution::send_tool_process_event( + &tool_one.cancelled, + &tool_one.pending_events, + &tool_one.event_overflow_reason, + &tool_one.pending_event_bytes, + &tool_one.pending_event_count_limit, + &tool_one.pending_event_bytes_limit, + &tool_one.vm_pending_event_bytes_budget, + ActiveExecutionEvent::Stdout(vec![5; 6]), + )); + assert!(crate::execution::send_tool_process_event( + &tool_two.cancelled, + &tool_two.pending_events, + &tool_two.event_overflow_reason, + &tool_two.pending_event_bytes, + &tool_two.pending_event_count_limit, + &tool_two.pending_event_bytes_limit, + &tool_two.vm_pending_event_bytes_budget, + ActiveExecutionEvent::Stdout(vec![6; 4]), + )); + assert!(!crate::execution::send_tool_process_event( + &tool_two.cancelled, + &tool_two.pending_events, + &tool_two.event_overflow_reason, + &tool_two.pending_event_bytes, + &tool_two.pending_event_count_limit, + &tool_two.pending_event_bytes_limit, + &tool_two.vm_pending_event_bytes_budget, + ActiveExecutionEvent::Stdout(vec![7]), + )); + drop(tool_one); + assert_eq!(event_budget.used(), event_envelope_bytes + 4); + drop(tool_two); + assert_eq!( + event_budget.used(), + 0, + "tool teardown must reclaim background-produced events" + ); + } + + fn vm_pending_stdin_bytes_release_on_partial_flush_clear_and_teardown() { + let mut config = KernelVmConfig::new("vm-pending-stdin-budget"); + config.permissions = Permissions::allow_all(); + let mut kernel = SidecarKernel::new(MountTable::new(MemoryFileSystem::new()), config); + kernel + .register_driver(CommandDriver::new( + EXECUTION_DRIVER_NAME, + [JAVASCRIPT_COMMAND], + )) + .expect("register execution driver"); + let first_handle = kernel + .spawn_process( + JAVASCRIPT_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn first kernel child"); + let second_handle = kernel + .spawn_process( + JAVASCRIPT_COMMAND, + Vec::new(), + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + ..SpawnOptions::default() + }, + ) + .expect("spawn second kernel child"); + + let stdin_budget = VmPendingByteBudget::new( + 150_000, + agentos_bridge::queue_tracker::TrackedLimit::PendingKernelStdinBytes, + ); + let event_budget = VmPendingByteBudget::new( + 1024, + agentos_bridge::queue_tracker::TrackedLimit::PendingExecutionEventBytes, + ); + let mut limits = agentos_native_sidecar_core::limits::ProcessLimits::default(); + limits.pending_stdin_bytes = 200_000; + let first_pid = first_handle.pid(); + let second_pid = second_handle.pid(); + let mut child_one = ActiveProcess::new( + first_pid, + first_handle, + GuestRuntimeKind::WebAssembly, + ActiveExecution::Tool(ToolExecution::default()), + ) + .with_process_event_limits(&limits) + .with_vm_pending_byte_budgets(Arc::clone(&stdin_budget), Arc::clone(&event_budget)); + let mut child_two = ActiveProcess::new( + second_pid, + second_handle, + GuestRuntimeKind::WebAssembly, + ActiveExecution::Tool(ToolExecution::default()), + ) + .with_process_event_limits(&limits) + .with_vm_pending_byte_budgets(Arc::clone(&stdin_budget), Arc::clone(&event_budget)); + child_one.kernel_stdin_writer_fd = Some( + crate::execution::install_kernel_stdin_pipe(&mut kernel, first_pid) + .expect("install first stdin pipe"), + ); + child_two.kernel_stdin_writer_fd = Some( + crate::execution::install_kernel_stdin_pipe(&mut kernel, second_pid) + .expect("install second stdin pipe"), + ); + + let pipe_capacity = agentos_kernel::pipe_manager::MAX_PIPE_BUFFER_BYTES; + crate::execution::write_kernel_process_stdin( + &mut kernel, + &mut child_one, + &vec![1; pipe_capacity + 12_000], + limits.pending_stdin_bytes, + ) + .expect("first child should partially flush into its pipe"); + crate::execution::write_kernel_process_stdin( + &mut kernel, + &mut child_two, + &vec![2; pipe_capacity + 8_000], + limits.pending_stdin_bytes, + ) + .expect("second child should partially flush into its pipe"); + assert_eq!( + stdin_budget.used(), + 20_000, + "only the unflushed tails should remain charged" + ); + + let error = crate::execution::write_kernel_process_stdin( + &mut kernel, + &mut child_two, + &vec![3; 135_000], + limits.pending_stdin_bytes, + ) + .expect_err("combined child backlogs must obey the VM byte budget"); + assert!( + error + .to_string() + .contains("limits.process.pendingStdinBytes"), + "unexpected aggregate stdin error: {error}" + ); + assert_eq!(stdin_budget.used(), 20_000); + + let drained = kernel + .fd_read(EXECUTION_DRIVER_NAME, first_pid, 0, 6_000) + .expect("drain first child pipe"); + assert_eq!(drained.len(), 6_000); + crate::execution::flush_pending_kernel_stdin(&mut kernel, &mut child_one) + .expect("flush first child tail into released pipe capacity"); + assert_eq!( + stdin_budget.used(), + 14_000, + "partial flush must release exactly the written bytes" + ); + + crate::execution::write_kernel_process_stdin( + &mut kernel, + &mut child_two, + &vec![4; 135_000], + limits.pending_stdin_bytes, + ) + .expect("capacity released by one child should be reusable by another"); + assert_eq!(stdin_budget.used(), 149_000); + + drop(child_two); + assert_eq!( + stdin_budget.used(), + 6_000, + "child teardown must reclaim its entire stdin tail" + ); + child_one.kernel_stdin_writer_fd = None; + crate::execution::flush_pending_kernel_stdin(&mut kernel, &mut child_one) + .expect("missing writer should clear the remaining stdin backlog"); + assert_eq!(stdin_budget.used(), 0, "clear must release all bytes"); + } + + fn wasm_signal_queue_is_bounded() { + let kernel_handle = create_kernel_process_handle_for_tests(); + let mut process = ActiveProcess::new( + kernel_handle.pid(), + kernel_handle, + GuestRuntimeKind::WebAssembly, + ActiveExecution::Tool(ToolExecution::default()), + ); + for _ in 0..(MAX_PROCESS_EVENT_QUEUE * 2) { + process + .queue_pending_wasm_signal(nix::libc::SIGUSR1) + .expect("repeated standard signals should coalesce"); + } + assert_eq!(process.pending_wasm_signals.len(), 1); + for signal in 1..=64 { + process + .queue_pending_wasm_signal(signal) + .expect("distinct supported signals fit the finite signal set"); + } + assert!(process.pending_wasm_signals.len() <= 64); } fn descendant_transfer_overflow_preserves_global_queue() { @@ -597,6 +924,111 @@ ykAheWCsAteSEWVc0w==\n\ ); } + fn descendant_transfer_byte_overflow_restores_current_and_deferred_envelopes() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate sidecar"); + let vm_id = create_vm_with_metadata( + &mut sidecar, + &connection_id, + &session_id, + PermissionsPolicy::allow_all(), + BTreeMap::new(), + ) + .expect("create vm"); + insert_tool_process(&mut sidecar, &vm_id, "root-proc"); + + let existing = ActiveExecutionEvent::Stdout(Vec::new()); + let mut limits = agentos_native_sidecar_core::limits::ProcessLimits::default(); + limits.pending_event_bytes = existing.retained_bytes(); + let kernel_handle = create_kernel_process_handle_for_tests(); + let mut child = ActiveProcess::new( + kernel_handle.pid(), + kernel_handle, + GuestRuntimeKind::JavaScript, + ActiveExecution::Tool(ToolExecution::default()), + ) + .with_process_event_limits(&limits); + child + .queue_pending_execution_event(existing) + .expect("fill child retained-byte limit while leaving count capacity"); + sidecar + .vms + .get_mut(&vm_id) + .expect("test vm") + .active_processes + .get_mut("root-proc") + .expect("root process") + .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 expected = vec![ + (String::from("other-before"), 1u8), + (String::from("root-proc/child-1"), 2u8), + (String::from("other-after"), 3u8), + ]; + let signature = |sidecar: &NativeSidecar| { + sidecar + .pending_process_events + .iter() + .map(|envelope| { + let ActiveExecutionEvent::Stdout(bytes) = &envelope.event else { + panic!("expected stdout envelope"); + }; + (envelope.process_id.clone(), bytes[0]) + }) + .collect::>() + }; + + for queued in [ + envelope("other-before", 1), + envelope("root-proc/child-1", 2), + envelope("other-after", 3), + ] { + sidecar + .queue_pending_process_event(queued) + .expect("seed pending event order"); + } + let error = sidecar + .drain_queued_descendant_javascript_child_process_events( + &vm_id, + "root-proc", + &["child-1"], + ) + .expect_err("child byte limit should reject transfer"); + assert!(error.to_string().contains("pendingEventBytes")); + assert_eq!(signature(&sidecar), expected); + + sidecar.pending_process_events.clear(); + sidecar.observe_pending_process_event_depth(); + for queued in [ + envelope("other-before", 1), + envelope("root-proc/child-1", 2), + envelope("other-after", 3), + ] { + sidecar + .process_event_sender + .try_send(queued) + .expect("seed receiver event order"); + } + let error = sidecar + .drain_queued_descendant_javascript_child_process_events( + &vm_id, + "root-proc", + &["child-1"], + ) + .expect_err("receiver transfer should preserve a byte-limit failure"); + assert!(error.to_string().contains("pendingEventBytes")); + assert_eq!(signature(&sidecar), expected); + } + fn exit_trailing_requeue_preserves_exit_when_queue_is_full() { let mut sidecar = create_test_sidecar(); let (connection_id, session_id) = @@ -905,6 +1337,7 @@ ykAheWCsAteSEWVc0w==\n\ vm_id, context_id: context.context_id, argv: vec![String::from("./entry.mjs")], + argv0: None, env: BTreeMap::new(), cwd, limits: Default::default(), @@ -1339,17 +1772,17 @@ ykAheWCsAteSEWVc0w==\n\ .join("../..") .canonicalize() .expect("canonicalize repo root"); - let copied = repo_root.join("registry/software/coreutils/wasm"); + let copied = repo_root.join("software/coreutils/wasm"); if copied.exists() { return copied; } - let fallback = repo_root.join("registry/native/target/wasm32-wasip1/release/commands"); + let fallback = repo_root.join("toolchain/target/wasm32-wasip1/release/commands"); if fallback.exists() { return fallback; } - let vendored = repo_root.join("packages/core/commands"); + let vendored = repo_root.join("packages/runtime-core/commands"); if vendored.exists() { let staged = temp_dir("agentos-native-sidecar-vendored-commands"); for command in ["bash", "cat", "mkdir", "printf", "sh"] { @@ -1372,6 +1805,29 @@ ykAheWCsAteSEWVc0w==\n\ return staged; } + let legacy_vendored = repo_root.join("packages/core/commands"); + if legacy_vendored.exists() { + let staged = temp_dir("agentos-native-sidecar-vendored-commands"); + for command in ["bash", "cat", "mkdir", "printf", "sh"] { + let source = legacy_vendored.join(command); + let target = staged.join(command); + fs::copy(&source, &target).unwrap_or_else(|error| { + panic!( + "copy vendored command {} -> {}: {error}", + source.display(), + target.display() + ) + }); + let mut permissions = fs::metadata(&target) + .expect("stat staged vendored command") + .permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&target, permissions) + .expect("chmod staged vendored command"); + } + return staged; + } + panic!( "registry WASM commands are required for service fs regression tests: expected {}, {}, or {}", copied.display(), @@ -1846,6 +2302,7 @@ ykAheWCsAteSEWVc0w==\n\ vm_id: vm_id.to_owned(), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], + argv0: None, env: env.clone(), cwd: cwd.to_path_buf(), inline_code: None, @@ -2105,7 +2562,7 @@ ykAheWCsAteSEWVc0w==\n\ let next_event = { let 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.pending_execution_events.pop_front() { + if let Some(event) = process.pop_pending_execution_event() { Some(event) } else { process @@ -2174,7 +2631,7 @@ ykAheWCsAteSEWVc0w==\n\ let Some(process) = vm.active_processes.get_mut(&process_id) else { continue; }; - if let Some(event) = process.pending_execution_events.pop_front() { + if let Some(event) = process.pop_pending_execution_event() { Some(event) } else { process @@ -2225,7 +2682,7 @@ ykAheWCsAteSEWVc0w==\n\ let next_event = { let 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.pending_execution_events.pop_front() { + if let Some(event) = process.pop_pending_execution_event() { Some(event) } else { process @@ -2520,6 +2977,7 @@ ykAheWCsAteSEWVc0w==\n\ vm_id: vm_id.to_owned(), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], + argv0: None, env: BTreeMap::new(), cwd: cwd.to_path_buf(), inline_code: None, @@ -3293,6 +3751,8 @@ ykAheWCsAteSEWVc0w==\n\ guest_local_addr: SocketAddr::from(([127, 0, 0, 1], 49993)), backlog: listener.backlog, active_connection_ids: std::collections::BTreeSet::new(), + description_handles: std::sync::Arc::clone(&listener.description_handles), + kernel_transfer_guard: listener.kernel_transfer_guard.clone(), } }; process @@ -3311,6 +3771,8 @@ ykAheWCsAteSEWVc0w==\n\ guest_local_addr: Some(SocketAddr::from(([127, 0, 0, 1], 49994))), recv_buffer_size: socket.recv_buffer_size, send_buffer_size: socket.send_buffer_size, + description_handles: std::sync::Arc::clone(&socket.description_handles), + kernel_transfer_guard: socket.kernel_transfer_guard.clone(), } }; process @@ -5419,6 +5881,7 @@ ykAheWCsAteSEWVc0w==\n\ vm_id: vm_id.clone(), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], + argv0: None, env: BTreeMap::from([( String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), String::from( @@ -5589,6 +6052,7 @@ ykAheWCsAteSEWVc0w==\n\ vm_id: vm_id.clone(), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], + argv0: None, env: BTreeMap::new(), cwd: cwd.clone(), inline_code: None, @@ -6156,6 +6620,7 @@ ykAheWCsAteSEWVc0w==\n\ socket_id: None, command: None, args: Vec::new(), + argv0: None, cwd: None, env: BTreeMap::new(), shell: false, @@ -6201,6 +6666,7 @@ ykAheWCsAteSEWVc0w==\n\ socket_id: None, command: None, args: Vec::new(), + argv0: None, cwd: None, env: BTreeMap::new(), shell: false, @@ -9688,7 +10154,7 @@ ykAheWCsAteSEWVc0w==\n\ vm.active_processes .get_mut("proc-wasm-pty") .and_then(|process| { - if let Some(event) = process.pending_execution_events.pop_front() { + if let Some(event) = process.pop_pending_execution_event() { Some(event) } else { process @@ -9906,42 +10372,1397 @@ ykAheWCsAteSEWVc0w==\n\ &vm.host_cwd, &request, ) - .unwrap_or_else(|error| panic!("failed to resolve {command}: {error}")); + .unwrap_or_else(|error| panic!("failed to resolve {command}: {error}")); + assert_eq!( + resolved.runtime, + GuestRuntimeKind::WebAssembly, + "{command} should resolve as a WASM command" + ); + assert_eq!( + resolved.process_args, expected_process_args, + "{command} process args mismatch: {resolved:?}" + ); + assert!( + resolved.entrypoint.ends_with(&format!("/{command}")), + "{command} entrypoint should end with /{command}: {}", + resolved.entrypoint + ); + } + + 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 error = missing.expect_err("missing command should fail"); + assert!( + error + .to_string() + .contains("command not found: definitely-not-a-command"), + "missing command error should mention the command: {error}" + ); + + // 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, + &BTreeMap::new(), + &vm.guest_cwd, + &vm.host_cwd, + &crate::protocol::JavascriptChildProcessSpawnRequest { + command: String::from("/workspace/echo"), + args: Vec::new(), + options: crate::protocol::JavascriptChildProcessSpawnOptions::default(), + }, + true, + None, + ); + let error = exact_missing.expect_err("execve path must not fall back through PATH"); + assert!( + error + .to_string() + .contains("command not found: /workspace/echo"), + "exact-path error should retain the literal pathname: {error}" + ); + } + + fn local_wasm_exec_commit_uses_exact_guest_env_and_preserves_process_state() { + 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 kernel_handle = { + let 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"); + vm.kernel + .chmod("/replacement.wasm", 0o755) + .expect("mark replacement executable"); + vm.kernel + .write_file("/interpreter.wasm", b"\0asm\x01\0\0\0".to_vec()) + .expect("write script interpreter module"); + vm.kernel + .chmod("/interpreter.wasm", 0o755) + .expect("mark script interpreter executable"); + vm.kernel + .write_file( + "/script", + b"#!/interpreter.wasm one optional argument\n".to_vec(), + ) + .expect("write executable script"); + vm.kernel + .chmod("/script", 0o755) + .expect("mark script executable"); + vm.kernel + .spawn_process( + WASM_COMMAND, + vec![String::from("old-argv")], + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + env: BTreeMap::from([(String::from("STALE"), String::from("old"))]), + cwd: Some(String::from("/")), + ..SpawnOptions::default() + }, + ) + .expect("spawn kernel process") + }; + let kernel_pid = kernel_handle.pid(); + let mut process = ActiveProcess::new( + kernel_pid, + kernel_handle, + GuestRuntimeKind::WebAssembly, + ActiveExecution::Tool(ToolExecution::default()), + ) + .with_guest_cwd(String::from("/")) + .with_env(BTreeMap::from([( + String::from("STALE"), + String::from("old"), + )])); + process.host_write_dirty = true; + process.pending_self_signal_exit = Some(nix::libc::SIGTERM); + process + .queue_pending_wasm_signal(nix::libc::SIGUSR1) + .expect("queue pending signal"); + process + .queue_pending_execution_event(ActiveExecutionEvent::Stdout(b"before".to_vec())) + .expect("queue pre-exec output"); + process + .queue_pending_execution_event(ActiveExecutionEvent::Exited(9)) + .expect("queue stale old-image exit"); + sidecar + .vms + .get_mut(&vm_id) + .expect("created vm") + .active_processes + .insert(String::from("exec-process"), process); + + // The WASM runner precompiles the shebang interpreter and sends + // its Linux-rewritten argv with the original script pathname. + // Native commit must validate the interpreter chain rather than + // reject the script merely because its own header is not WASM. + sidecar + .exec_javascript_process_image( + &vm_id, + "exec-process", + &[], + crate::protocol::JavascriptChildProcessSpawnRequest { + command: String::from("/script"), + args: vec![ + String::from("one optional argument"), + String::from("/script"), + String::from("script-argument"), + ], + options: crate::protocol::JavascriptChildProcessSpawnOptions { + argv0: Some(String::from("/interpreter.wasm")), + env: BTreeMap::from([( + String::from("SCRIPT_ONLY"), + String::from("yes"), + )]), + local_replacement: true, + ..Default::default() + }, + }, + ) + .expect("commit local WASM shebang exec"); + { + let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); + assert_eq!( + vm.kernel + .read_file_for_process( + EXECUTION_DRIVER_NAME, + kernel_pid, + &format!("/proc/{kernel_pid}/cmdline"), + ) + .expect("read shebang-rewritten argv"), + b"/interpreter.wasm\0one optional argument\0/script\0script-argument\0" + .to_vec() + ); + } + + let replacement_env = BTreeMap::from([(String::from("ONLY"), String::from("new"))]); + sidecar + .exec_javascript_process_image( + &vm_id, + "exec-process", + &[], + crate::protocol::JavascriptChildProcessSpawnRequest { + command: String::from("replacement.wasm"), + args: vec![String::from("argument")], + options: crate::protocol::JavascriptChildProcessSpawnOptions { + argv0: Some(String::new()), + env: replacement_env.clone(), + local_replacement: true, + ..Default::default() + }, + }, + ) + .expect("commit local WASM exec"); + + { + let vm = sidecar.vms.get(&vm_id).expect("created vm"); + let process = vm + .active_processes + .get("exec-process") + .expect("exec process remains active"); + assert_eq!(process.kernel_pid, kernel_pid, "exec must retain PID"); + assert_eq!(process.env, replacement_env); + assert!(!process.env.contains_key("STALE")); + assert!( + process.env.keys().all(|key| !key.starts_with("AGENTOS_")), + "executor bootstrap env must not leak into guest envp: {:?}", + process.env + ); + assert!( + process.host_write_dirty, + "exec must preserve dirty VFS state" + ); + assert_eq!(process.pending_self_signal_exit, Some(nix::libc::SIGTERM)); + assert!(process.pending_wasm_signals.contains(&nix::libc::SIGUSR1)); + assert_eq!(process.pending_execution_events.len(), 1); + assert!(matches!( + process.pending_execution_events.front(), + Some(ActiveExecutionEvent::Stdout(bytes)) if bytes == b"before" + )); + } + + let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); + let kernel_process = vm + .kernel + .list_processes() + .get(&kernel_pid) + .cloned() + .expect("kernel process remains active"); + assert_eq!(kernel_process.command, ""); + assert_eq!( + vm.kernel + .read_file_for_process( + EXECUTION_DRIVER_NAME, + kernel_pid, + &format!("/proc/{kernel_pid}/environ"), + ) + .expect("read exact post-exec environment"), + b"ONLY=new\0".to_vec() + ); + assert_eq!( + vm.kernel + .read_file_for_process( + EXECUTION_DRIVER_NAME, + kernel_pid, + &format!("/proc/{kernel_pid}/cmdline"), + ) + .expect("read exact post-exec argv"), + b"\0argument\0".to_vec() + ); + + let fd_replacement_env = + BTreeMap::from([(String::from("FD_ONLY"), String::from("yes"))]); + sidecar + .commit_wasm_fd_process_image( + &vm_id, + "exec-process", + &[], + crate::protocol::JavascriptChildProcessSpawnRequest { + // This path deliberately does not exist in the VFS. + // The trusted runner owns and prevalidates the live FD + // image, so commit must never reopen this display path. + command: String::from("/proc/self/fd/1048576"), + args: vec![String::from("fd-argument")], + options: crate::protocol::JavascriptChildProcessSpawnOptions { + argv0: Some(String::from("fd-custom-argv0")), + executable_fd: Some(1_048_576), + env: fd_replacement_env.clone(), + local_replacement: true, + ..Default::default() + }, + }, + ) + .expect("commit prevalidated runner-owned fd image"); + let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); + assert_eq!( + vm.active_processes + .get("exec-process") + .expect("exec process remains active") + .env, + fd_replacement_env + ); + assert_eq!( + vm.kernel + .read_file_for_process( + EXECUTION_DRIVER_NAME, + kernel_pid, + &format!("/proc/{kernel_pid}/cmdline"), + ) + .expect("read fd-image argv"), + b"fd-custom-argv0\0fd-argument\0".to_vec() + ); + } + + fn prepare_cross_runtime_exec_fixture() -> (NativeSidecar, String, u32) { + assert_node_available(); + + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate and open session"); + let vm_id = create_vm( + &mut sidecar, + &connection_id, + &session_id, + PermissionsPolicy::allow_all(), + ) + .expect("create vm"); + sidecar.javascript_engine.set_import_cache_base_dir( + vm_id.clone(), + sidecar.cache_root.join("cross-runtime-exec-import-cache"), + ); + + let (kernel_handle, host_cwd) = { + let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); + vm.kernel.mkdir("/work", true).expect("create work dir"); + vm.kernel + .write_file( + "/work/replacement.js", + br#"#!/usr/bin/env node +const fs = require("node:fs"); +const snapshot = { + argv: process.argv, + argv0: process.argv0, + cwd: process.cwd(), + pid: process.pid, + only: process.env.ONLY ?? null, + stale: process.env.STALE ?? null, + fd: fs.readFileSync(`/proc/${process.pid}/fd/9`, "utf8"), + cmdline: fs.readFileSync(`/proc/${process.pid}/cmdline`).toString("base64"), + environ: fs.readFileSync(`/proc/${process.pid}/environ`).toString("base64"), +}; +process.stdout.write(`${JSON.stringify(snapshot)}\n`); +"# + .to_vec(), + ) + .expect("write replacement javascript"); + vm.kernel + .chmod("/work/replacement.js", 0o755) + .expect("mark replacement executable"); + vm.kernel + .write_file("/work/fd-data", b"preserved-fd".to_vec()) + .expect("write inherited fd fixture"); + let kernel_handle = vm + .kernel + .spawn_process( + WASM_COMMAND, + vec![String::from("old-image")], + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + env: BTreeMap::from([(String::from("STALE"), String::from("old"))]), + cwd: Some(String::from("/work")), + ..SpawnOptions::default() + }, + ) + .expect("spawn old kernel image"); + let kernel_pid = kernel_handle.pid(); + let opened_fd = vm + .kernel + .fd_open(EXECUTION_DRIVER_NAME, kernel_pid, "/work/fd-data", 0, None) + .expect("open inherited file"); + vm.kernel + .fd_dup2(EXECUTION_DRIVER_NAME, kernel_pid, opened_fd, 9) + .expect("install inherited fd 9"); + if opened_fd != 9 { + vm.kernel + .fd_close(EXECUTION_DRIVER_NAME, kernel_pid, opened_fd) + .expect("close extra inherited fd"); + } + (kernel_handle, vm.cwd.join("work")) + }; + let kernel_pid = kernel_handle.pid(); + let mut process = ActiveProcess::new( + kernel_pid, + kernel_handle, + GuestRuntimeKind::WebAssembly, + ActiveExecution::Tool(ToolExecution::default()), + ) + .with_guest_cwd(String::from("/work")) + .with_host_cwd(host_cwd) + .with_env(BTreeMap::from([( + String::from("STALE"), + String::from("old"), + )])); + process + .queue_pending_execution_event(ActiveExecutionEvent::Stdout( + b"old-image-output-before-exec\n".to_vec(), + )) + .expect("queue observable pre-exec output"); + process + .queue_pending_execution_event(ActiveExecutionEvent::Exited(91)) + .expect("queue stale old-image exit"); + sidecar + .vms + .get_mut(&vm_id) + .expect("created vm") + .active_processes + .insert(String::from("exec-process"), process); + + (sidecar, vm_id, kernel_pid) + } + + fn cross_runtime_exec_starts_after_committed_linux_process_state() { + let (mut sidecar, vm_id, kernel_pid) = prepare_cross_runtime_exec_fixture(); + let replacement_env = BTreeMap::from([(String::from("ONLY"), String::from("new"))]); + + sidecar + .exec_javascript_process_image( + &vm_id, + "exec-process", + &[], + crate::protocol::JavascriptChildProcessSpawnRequest { + command: String::from("replacement.js"), + args: vec![String::from("arg-one")], + options: crate::protocol::JavascriptChildProcessSpawnOptions { + argv0: Some(String::from("replacement-argv0")), + env: replacement_env.clone(), + ..Default::default() + }, + }, + ) + .expect("commit and start cross-runtime exec"); + + { + let vm = sidecar.vms.get(&vm_id).expect("created vm"); + let process = vm + .active_processes + .get("exec-process") + .expect("replacement process remains active"); + assert_eq!(process.kernel_pid, kernel_pid, "exec must preserve PID"); + assert_eq!(process.runtime, GuestRuntimeKind::JavaScript); + assert_eq!(process.guest_cwd, "/work"); + assert_eq!(process.env, replacement_env); + assert!( + process + .pending_execution_events + .iter() + .all(|event| !matches!( + event, + ActiveExecutionEvent::Exited(91) + | ActiveExecutionEvent::JavascriptSyncRpcRequest(_) + | ActiveExecutionEvent::PythonVfsRpcRequest(_) + | ActiveExecutionEvent::SignalState { .. } + )), + "old-image continuation events must not survive exec" + ); + } + + let (stdout, stderr, exit_code) = + drain_process_output(&mut sidecar, &vm_id, "exec-process"); + assert_eq!(exit_code, Some(0), "stderr: {stderr}"); + assert_eq!(stderr, ""); + let mut lines = stdout.lines(); + assert_eq!(lines.next(), Some("old-image-output-before-exec")); + let snapshot: Value = + serde_json::from_str(lines.next().expect("replacement state snapshot")) + .expect("parse replacement state snapshot"); + assert_eq!( + lines.next(), + None, + "unexpected replacement output: {stdout}" + ); + assert_eq!(snapshot["argv0"], json!("replacement-argv0")); + assert_eq!(snapshot["argv"][1], json!("/work/replacement.js")); + assert_eq!(snapshot["argv"][2], json!("arg-one")); + assert_eq!(snapshot["cwd"], json!("/work")); + assert_eq!(snapshot["pid"], json!(kernel_pid)); + assert_eq!(snapshot["only"], json!("new")); + assert_eq!(snapshot["stale"], Value::Null); + assert_eq!(snapshot["fd"], json!("preserved-fd")); + assert_eq!( + base64::engine::general_purpose::STANDARD + .decode(snapshot["cmdline"].as_str().expect("cmdline base64")) + .expect("decode cmdline"), + b"replacement-argv0\0arg-one\0" + ); + assert_eq!( + base64::engine::general_purpose::STANDARD + .decode(snapshot["environ"].as_str().expect("environ base64")) + .expect("decode environ"), + b"ONLY=new\0" + ); + } + + fn cross_runtime_exec_start_failure_is_fatal_after_kernel_commit() { + let (mut sidecar, vm_id, kernel_pid) = prepare_cross_runtime_exec_fixture(); + sidecar.fail_next_exec_start_after_commit = true; + + sidecar + .exec_javascript_process_image( + &vm_id, + "exec-process", + &[], + crate::protocol::JavascriptChildProcessSpawnRequest { + command: String::from("replacement.js"), + args: vec![String::from("fatal-argument")], + options: crate::protocol::JavascriptChildProcessSpawnOptions { + argv0: Some(String::from("fatal-argv0")), + env: BTreeMap::from([( + String::from("FATAL_ONLY"), + String::from("yes"), + )]), + ..Default::default() + }, + }, + ) + .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 kernel_process = vm + .kernel + .list_processes() + .get(&kernel_pid) + .cloned() + .expect("fatal replacement remains a waitable zombie"); + assert_eq!(kernel_process.command, "fatal-argv0"); + assert_eq!(kernel_process.exit_code, Some(127)); + let process = vm + .active_processes + .get_mut("exec-process") + .expect("fatal replacement remains queued for cleanup"); + assert_eq!(process.runtime, GuestRuntimeKind::JavaScript); + assert_eq!(process.guest_cwd, "/work"); + assert_eq!( + process.env, + BTreeMap::from([(String::from("FATAL_ONLY"), String::from("yes"))]) + ); + assert_eq!(process.kernel_pid, kernel_pid); + assert_eq!( + process.kernel_handle.wait(Duration::ZERO), + Some(127), + "a post-commit start failure must kill the replacement image" + ); + assert!(process.pending_execution_events.iter().any(|event| { + matches!(event, ActiveExecutionEvent::Stderr(message) + if String::from_utf8_lossy(message).contains("failed to start")) + })); + assert!(process + .pending_execution_events + .iter() + .any(|event| matches!(event, ActiveExecutionEvent::Exited(127)))); + assert!(!process + .pending_execution_events + .iter() + .any(|event| matches!(event, ActiveExecutionEvent::Exited(91)))); + } + + fn posix_spawn_file_actions_run_before_exact_exec_resolution() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate and open session"); + let vm_id = create_vm( + &mut sidecar, + &connection_id, + &session_id, + PermissionsPolicy::allow_all(), + ) + .expect("create vm"); + let cwd = temp_dir("agentos-native-sidecar-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"); + vm.kernel + .write_file("/truncated-script", b"#!/missing-interpreter\n".to_vec()) + .expect("write exact script fixture"); + vm.kernel + .chmod("/truncated-script", 0o755) + .expect("mark exact script executable"); + } + let open_action = + |guest_fd, path: &str, oflag| crate::protocol::JavascriptPosixSpawnFileAction { + command: 3, + guest_fd: Some(guest_fd), + fd: guest_fd, + source_fd: -1, + guest_source_fd: None, + oflag, + mode: 0o600, + path: path.to_owned(), + close_from_guest_fds: Vec::new(), + }; + + let error = sidecar + .spawn_javascript_child_process( + &vm_id, + "posix-spawn-parent", + crate::protocol::JavascriptChildProcessSpawnRequest { + command: String::from("/missing-executable"), + args: Vec::new(), + options: crate::protocol::JavascriptChildProcessSpawnOptions { + spawn_exact_path: true, + spawn_file_actions: vec![open_action( + 40, + "/created-before-enoent", + 0x1000_0000 | (1 << 12) | (4 << 12), + )], + ..Default::default() + }, + }, + ) + .expect_err("missing exact executable must fail"); + assert!(error.to_string().contains("ENOENT"), "{error}"); + assert!( + sidecar + .vms + .get(&vm_id) + .expect("created vm") + .kernel + .exists("/created-before-enoent") + .expect("query created file"), + "Linux keeps successful O_CREAT actions when the later exec fails" + ); + + let error = sidecar + .spawn_javascript_child_process( + &vm_id, + "posix-spawn-parent", + crate::protocol::JavascriptChildProcessSpawnRequest { + command: String::from("/truncated-script"), + args: Vec::new(), + options: crate::protocol::JavascriptChildProcessSpawnOptions { + spawn_exact_path: true, + spawn_file_actions: vec![open_action( + 41, + "/truncated-script", + 0x1000_0000 | (8 << 12), + )], + ..Default::default() + }, + }, + ) + .expect_err("truncating the target must affect exact image resolution"); + assert!(error.to_string().contains("ENOEXEC"), "{error}"); + assert_eq!( + sidecar + .vms + .get_mut(&vm_id) + .expect("created vm") + .kernel + .read_file("/truncated-script") + .expect("read truncated script"), + b"" + ); + } + + fn dirty_host_shadow_sync_precedes_top_level_and_nested_spawn_actions() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate and open session"); + let vm_id = create_vm( + &mut sidecar, + &connection_id, + &session_id, + PermissionsPolicy::allow_all(), + ) + .expect("create vm"); + let top_host_cwd = temp_dir("agentos-native-sidecar-posix-spawn-shadow-top"); + insert_fake_javascript_parent_process( + &mut sidecar, + &vm_id, + &top_host_cwd, + "posix-spawn-shadow-parent", + ); + write_fixture( + &top_host_cwd.join("truncate-before-failed-exec"), + b"host-dirty", + ); + sidecar + .vms + .get_mut(&vm_id) + .expect("created vm") + .active_processes + .get_mut("posix-spawn-shadow-parent") + .expect("top-level parent") + .host_write_dirty = true; + + let open_action = + |guest_fd, path: &str, oflag| crate::protocol::JavascriptPosixSpawnFileAction { + command: 3, + guest_fd: Some(guest_fd), + fd: guest_fd, + source_fd: -1, + guest_source_fd: None, + oflag, + mode: 0o600, + path: path.to_owned(), + close_from_guest_fds: Vec::new(), + }; + let exact_request = + |command: &str, action| crate::protocol::JavascriptChildProcessSpawnRequest { + command: command.to_owned(), + args: Vec::new(), + options: crate::protocol::JavascriptChildProcessSpawnOptions { + spawn_exact_path: true, + spawn_file_actions: vec![action], + ..Default::default() + }, + }; + + let error = sidecar + .spawn_javascript_child_process( + &vm_id, + "posix-spawn-shadow-parent", + exact_request( + "/missing-after-shadow-truncate", + open_action(50, "/truncate-before-failed-exec", 0x1000_0000 | (8 << 12)), + ), + ) + .expect_err("the later exact exec must fail"); + assert!(error.to_string().contains("ENOENT"), "{error}"); + assert_eq!( + sidecar + .vms + .get_mut(&vm_id) + .expect("created vm") + .kernel + .read_file("/truncate-before-failed-exec") + .expect("read file-action target after failed exec"), + b"", + "the pre-exec host sync must not run again after O_TRUNC" + ); + + let nested_host_cwd = temp_dir("agentos-native-sidecar-posix-spawn-shadow-nested"); + fs::create_dir(nested_host_cwd.join("host-only-directory")) + .expect("create host-only nested directory"); + let nested_module = nested_host_cwd.join("success.wasm"); + let nested_module_bytes = wat::parse_str(r#"(module (func (export "_start")))"#) + .expect("compile successful nested module"); + write_fixture(&nested_module, &nested_module_bytes); + let mut module_permissions = fs::metadata(&nested_module) + .expect("stat successful nested module") + .permissions(); + module_permissions.set_mode(0o755); + fs::set_permissions(&nested_module, module_permissions) + .expect("mark successful nested module executable"); + let staged_nested_module = sidecar + .vms + .get(&vm_id) + .expect("created vm") + .cwd + .join("success.wasm"); + write_fixture(&staged_nested_module, &nested_module_bytes); + let mut staged_module_permissions = fs::metadata(&staged_nested_module) + .expect("stat staged successful nested module") + .permissions(); + staged_module_permissions.set_mode(0o755); + fs::set_permissions(&staged_nested_module, staged_module_permissions) + .expect("mark staged successful nested module executable"); + + let (nested_handle, nested_env) = { + let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); + let root_pid = vm + .active_processes + .get("posix-spawn-shadow-parent") + .expect("root parent") + .kernel_pid; + let handle = vm + .kernel + .spawn_process( + JAVASCRIPT_COMMAND, + vec![String::from("nested-shadow-parent")], + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(root_pid), + cwd: Some(String::from("/")), + ..SpawnOptions::default() + }, + ) + .expect("spawn nested kernel parent"); + (handle, vm.guest_env.clone()) + }; + let nested_pid = nested_handle.pid(); + let mut nested_parent = ActiveProcess::new( + nested_pid, + nested_handle, + GuestRuntimeKind::JavaScript, + ActiveExecution::Tool(ToolExecution::default()), + ) + .with_guest_cwd(String::from("/")) + .with_env(nested_env) + .with_host_cwd(nested_host_cwd); + nested_parent.host_write_dirty = true; + sidecar + .vms + .get_mut(&vm_id) + .expect("created vm") + .active_processes + .get_mut("posix-spawn-shadow-parent") + .expect("root parent") + .child_processes + .insert(String::from("nested-shadow-parent"), nested_parent); + + sidecar + .spawn_descendant_javascript_child_process_for_test( + &vm_id, + "posix-spawn-shadow-parent", + &["nested-shadow-parent"], + exact_request( + "/success.wasm", + open_action( + 51, + "/host-only-directory/created-before-successful-exec", + 0x1000_0000 | (1 << 12) | (4 << 12), + ), + ), + ) + .expect("nested exact spawn succeeds after syncing its dirty host shadow"); + assert!( + sidecar + .vms + .get(&vm_id) + .expect("created vm") + .kernel + .exists("/host-only-directory/created-before-successful-exec") + .expect("query nested O_CREAT side effect"), + "the O_CREAT side effect must survive successful exec" + ); + } + + fn write_posix_spawnp_fixture( + sidecar: &mut NativeSidecar, + vm_id: &str, + guest_path: &str, + contents: impl AsRef<[u8]>, + mode: u32, + ) { + let contents = contents.as_ref(); + let host_path = { + let vm = sidecar.vms.get_mut(vm_id).expect("created vm"); + let parent = Path::new(guest_path) + .parent() + .and_then(Path::to_str) + .expect("fixture parent path"); + if parent != "/" { + vm.kernel + .mkdir(parent, true) + .expect("create fixture guest parent"); + } + vm.kernel + .write_file(guest_path, contents.to_vec()) + .expect("write guest fixture"); + vm.kernel + .chmod(guest_path, mode) + .expect("chmod guest fixture"); + vm.cwd.join(guest_path.trim_start_matches('/')) + }; + + fs::create_dir_all(host_path.parent().expect("fixture host parent")) + .expect("create fixture host parent"); + write_fixture(&host_path, contents); + let mut permissions = fs::metadata(&host_path) + .expect("stat host fixture") + .permissions(); + permissions.set_mode(mode); + fs::set_permissions(&host_path, permissions).expect("chmod host fixture"); + } + + fn posix_spawnp_request( + command: &str, + search_path: &str, + args: &[&str], + ) -> crate::protocol::JavascriptChildProcessSpawnRequest { + crate::protocol::JavascriptChildProcessSpawnRequest { + command: command.to_owned(), + args: args.iter().map(|arg| (*arg).to_owned()).collect(), + options: crate::protocol::JavascriptChildProcessSpawnOptions { + argv0: Some(String::from("caller-argv0")), + spawn_search_path: Some(search_path.to_owned()), + ..Default::default() + }, + } + } + + fn spawn_posix_spawnp_fixture( + sidecar: &mut NativeSidecar, + vm_id: &str, + nested: bool, + request: crate::protocol::JavascriptChildProcessSpawnRequest, + ) -> Result { + if nested { + sidecar.spawn_descendant_javascript_child_process_for_test( + vm_id, + "posix-spawnp-parent", + &["posix-spawnp-nested-parent"], + request, + ) + } else { + sidecar.spawn_javascript_child_process(vm_id, "posix-spawnp-parent", request) + } + } + + fn posix_spawnp_path_and_recursive_shebang_match_linux() { + 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 host_cwd = sidecar.vms.get(&vm_id).expect("created vm").cwd.clone(); + insert_fake_javascript_parent_process( + &mut sidecar, + &vm_id, + &host_cwd, + "posix-spawnp-parent", + ); + + let (nested_handle, nested_env) = { + let vm = sidecar.vms.get_mut(&vm_id).expect("created vm"); + let root_pid = vm + .active_processes + .get("posix-spawnp-parent") + .expect("root parent") + .kernel_pid; + let handle = vm + .kernel + .spawn_process( + JAVASCRIPT_COMMAND, + vec![String::from("posix-spawnp-nested-parent")], + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(root_pid), + cwd: Some(String::from("/")), + ..SpawnOptions::default() + }, + ) + .expect("spawn nested kernel parent"); + (handle, vm.guest_env.clone()) + }; + let nested_pid = nested_handle.pid(); + sidecar + .vms + .get_mut(&vm_id) + .expect("created vm") + .active_processes + .get_mut("posix-spawnp-parent") + .expect("root parent") + .child_processes + .insert( + String::from("posix-spawnp-nested-parent"), + ActiveProcess::new( + nested_pid, + nested_handle, + GuestRuntimeKind::JavaScript, + ActiveExecution::Tool(ToolExecution::default()), + ) + .with_guest_cwd(String::from("/")) + .with_env(nested_env) + .with_host_cwd(host_cwd), + ); + + let wasm = wat::parse_str(r#"(module (func (export "_start")))"#) + .expect("compile WASM fixture"); + for (path, contents, mode) in [ + ("/empty.wasm", wasm.as_slice(), 0o755), + ("/ spaced /space.wasm", wasm.as_slice(), 0o755), + ("/denied/tool", wasm.as_slice(), 0o644), + ("/allowed/tool", wasm.as_slice(), 0o755), + ("/denied2/tool", wasm.as_slice(), 0o644), + ("/interpreter.wasm", wasm.as_slice(), 0o755), + ] { + write_posix_spawnp_fixture(&mut sidecar, &vm_id, path, contents, mode); + } + for (path, contents) in [ + ("/scripts/simple", b"#!/interpreter.wasm\n".as_slice()), + ( + "/scripts/optional", + b"#!/interpreter.wasm --flag with space\n".as_slice(), + ), + ( + "/scripts/inner", + b"#!/interpreter.wasm --inner\n".as_slice(), + ), + ("/scripts/nested", b"#!/scripts/inner --outer\n".as_slice()), + ("/scripts/missing", b"#!/missing-interpreter\n".as_slice()), + ] { + write_posix_spawnp_fixture(&mut sidecar, &vm_id, path, contents, 0o755); + } + + sidecar + .vms + .get_mut(&vm_id) + .expect("created vm") + .command_guest_paths + .insert( + String::from("registered-only"), + String::from("/registered-only"), + ); + + for nested in [false, true] { + let scope = if nested { "nested" } else { "top-level" }; + + let error = spawn_posix_spawnp_fixture( + &mut sidecar, + &vm_id, + nested, + posix_spawnp_request("registered-only", "/excluded", &[]), + ) + .expect_err("an explicit PATH must exclude registered commands outside PATH"); + assert!( + error.to_string().contains("ENOENT"), + "{scope} excluded registered command returned {error}" + ); + + let empty_entry = spawn_posix_spawnp_fixture( + &mut sidecar, + &vm_id, + nested, + posix_spawnp_request("empty.wasm", "", &[]), + ) + .unwrap_or_else(|error| panic!("{scope} PATH=\"\" failed: {error}")); + assert_eq!(empty_entry["args"], json!(["caller-argv0"])); + + let empty_segment = spawn_posix_spawnp_fixture( + &mut sidecar, + &vm_id, + nested, + posix_spawnp_request("empty.wasm", ":/missing", &[]), + ) + .unwrap_or_else(|error| panic!("{scope} empty PATH segment failed: {error}")); + assert_eq!(empty_segment["args"], json!(["caller-argv0"])); + + let whitespace_entry = spawn_posix_spawnp_fixture( + &mut sidecar, + &vm_id, + nested, + posix_spawnp_request("space.wasm", "/ spaced ", &[]), + ) + .unwrap_or_else(|error| panic!("{scope} literal whitespace PATH failed: {error}")); + assert_eq!(whitespace_entry["args"], json!(["caller-argv0"])); + + let allowed_after_denied = spawn_posix_spawnp_fixture( + &mut sidecar, + &vm_id, + nested, + posix_spawnp_request("tool", "/denied:/allowed", &[]), + ) + .unwrap_or_else(|error| { + panic!("{scope} EACCES continuation to valid candidate failed: {error}") + }); + assert_eq!(allowed_after_denied["command"], json!("/allowed/tool")); + + let error = spawn_posix_spawnp_fixture( + &mut sidecar, + &vm_id, + nested, + posix_spawnp_request("tool", "/denied:/denied2", &[]), + ) + .expect_err("all denied PATH candidates must fail"); + assert!( + error.to_string().contains("EACCES"), + "{scope} all-denied PATH returned {error}" + ); + + let simple = spawn_posix_spawnp_fixture( + &mut sidecar, + &vm_id, + nested, + posix_spawnp_request("simple", "/scripts", &["tail"]), + ) + .unwrap_or_else(|error| panic!("{scope} PATH shebang failed: {error}")); + assert_eq!( + simple["args"], + json!(["/interpreter.wasm", "/scripts/simple", "tail"]) + ); + + let optional = spawn_posix_spawnp_fixture( + &mut sidecar, + &vm_id, + nested, + posix_spawnp_request("optional", "/scripts", &["tail"]), + ) + .unwrap_or_else(|error| panic!("{scope} optional shebang arg failed: {error}")); + assert_eq!( + optional["args"], + json!([ + "/interpreter.wasm", + "--flag with space", + "/scripts/optional", + "tail" + ]) + ); + + let recursive = spawn_posix_spawnp_fixture( + &mut sidecar, + &vm_id, + nested, + posix_spawnp_request("nested", "/scripts", &["tail"]), + ) + .unwrap_or_else(|error| panic!("{scope} nested shebang failed: {error}")); + assert_eq!( + recursive["args"], + json!([ + "/interpreter.wasm", + "--inner", + "/scripts/inner", + "--outer", + "/scripts/nested", + "tail" + ]) + ); + + let error = spawn_posix_spawnp_fixture( + &mut sidecar, + &vm_id, + nested, + posix_spawnp_request("missing", "/scripts", &[]), + ) + .expect_err("missing shebang interpreter must fail"); + assert!( + error.to_string().contains("ENOENT"), + "{scope} missing interpreter returned {error}" + ); + } + } + + fn repeated_malformed_wasm_spawns_restore_top_level_and_nested_baselines() { + let mut sidecar = create_test_sidecar(); + let (connection_id, session_id) = + authenticate_and_open_session(&mut sidecar).expect("authenticate and open session"); + let vm_id = create_vm( + &mut sidecar, + &connection_id, + &session_id, + PermissionsPolicy::allow_all(), + ) + .expect("create vm"); + let cwd = temp_dir("agentos-native-sidecar-malformed-wasm-spawn"); + insert_fake_javascript_parent_process( + &mut sidecar, + &vm_id, + &cwd, + "malformed-wasm-parent", + ); + let malformed_wasm = b"\0asm\x01\0\0\0\xff"; + 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"); + 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); + let mut successful_permissions = fs::metadata(&successful_host_path) + .expect("stat successful WASM fixture") + .permissions(); + successful_permissions.set_mode(0o755); + fs::set_permissions(&successful_host_path, successful_permissions) + .expect("mark host successful WASM fixture executable"); + vm.kernel + .write_file("/malformed.wasm", malformed_wasm.to_vec()) + .expect("write malformed WASM fixture"); + vm.kernel + .chmod("/malformed.wasm", 0o755) + .expect("mark malformed WASM executable"); + vm.kernel + .write_file("/success.wasm", successful_wasm) + .expect("write successful WASM fixture"); + vm.kernel + .chmod("/success.wasm", 0o755) + .expect("mark successful WASM executable"); + } + let malformed_request = || crate::protocol::JavascriptChildProcessSpawnRequest { + command: String::from("/malformed.wasm"), + args: Vec::new(), + options: crate::protocol::JavascriptChildProcessSpawnOptions { + spawn_exact_path: true, + ..Default::default() + }, + }; + + let top_level_baseline = sidecar + .vms + .get(&vm_id) + .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(), + ); + for iteration in 0..8 { + if sidecar + .spawn_javascript_child_process( + &vm_id, + "malformed-wasm-parent", + malformed_request(), + ) + .is_ok() + { + panic!("top-level malformed WASM spawn iteration {iteration} succeeded"); + } + let vm = sidecar.vms.get(&vm_id).expect("created vm"); assert_eq!( - resolved.runtime, - GuestRuntimeKind::WebAssembly, - "{command} should resolve as a WASM command" + vm.kernel.resource_snapshot(), + top_level_baseline, + "top-level iteration {iteration} leaked a process or fd" + ); + assert!( + vm.active_processes + .get("malformed-wasm-parent") + .expect("root parent") + .child_processes + .is_empty(), + "failed top-level spawn must not register a child" ); assert_eq!( - resolved.process_args, expected_process_args, - "{command} process args mismatch: {resolved:?}" + ( + 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(), + ), + 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 root_pid = vm + .active_processes + .get("malformed-wasm-parent") + .expect("root parent") + .kernel_pid; + let handle = vm + .kernel + .spawn_process( + JAVASCRIPT_COMMAND, + vec![String::from("nested-parent")], + SpawnOptions { + requester_driver: Some(String::from(EXECUTION_DRIVER_NAME)), + parent_pid: Some(root_pid), + cwd: Some(String::from("/")), + ..SpawnOptions::default() + }, + ) + .expect("spawn nested kernel parent"); + (handle, vm.guest_env.clone(), vm.cwd.clone()) + }; + let nested_pid = nested_handle.pid(); + sidecar + .vms + .get_mut(&vm_id) + .expect("created vm") + .active_processes + .get_mut("malformed-wasm-parent") + .expect("root parent") + .child_processes + .insert( + String::from("nested-parent"), + ActiveProcess::new( + nested_pid, + nested_handle, + GuestRuntimeKind::JavaScript, + ActiveExecution::Tool(ToolExecution::default()), + ) + .with_guest_cwd(String::from("/")) + .with_env(nested_env) + .with_host_cwd(nested_host_cwd), + ); + + let nested_baseline = sidecar + .vms + .get(&vm_id) + .expect("created vm") + .kernel + .resource_snapshot(); + for iteration in 0..8 { + if sidecar + .spawn_descendant_javascript_child_process_for_test( + &vm_id, + "malformed-wasm-parent", + &["nested-parent"], + malformed_request(), + ) + .is_ok() + { + panic!("nested malformed WASM spawn iteration {iteration} succeeded"); + } + let vm = sidecar.vms.get(&vm_id).expect("created vm"); + assert_eq!( + vm.kernel.resource_snapshot(), + nested_baseline, + "nested iteration {iteration} leaked a process or fd" ); assert!( - resolved.entrypoint.ends_with(&format!("/{command}")), - "{command} entrypoint should end with /{command}: {}", - resolved.entrypoint + vm.active_processes + .get("malformed-wasm-parent") + .expect("root parent") + .child_processes + .get("nested-parent") + .expect("nested parent") + .child_processes + .is_empty(), + "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(), + ), + context_baseline, + "nested iteration {iteration} leaked an execution context" ); } - 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 successful_request = || crate::protocol::JavascriptChildProcessSpawnRequest { + command: String::from("/success.wasm"), + args: Vec::new(), + options: crate::protocol::JavascriptChildProcessSpawnOptions { + spawn_exact_path: true, + ..Default::default() }, - ); - let error = missing.expect_err("missing command should fail"); - assert!( - error - .to_string() - .contains("command not found: definitely-not-a-command"), - "missing command error should mention the command: {error}" - ); + }; + for iteration in 0..4 { + let spawned = sidecar + .spawn_javascript_child_process( + &vm_id, + "malformed-wasm-parent", + successful_request(), + ) + .unwrap_or_else(|error| { + panic!("successful WASM spawn iteration {iteration} failed: {error}") + }); + let child_id = spawned["childId"] + .as_str() + .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(), + ), + context_baseline, + "successful spawn iteration {iteration} retained one-shot context metadata" + ); + + let mut reaped = false; + for _ in 0..64 { + let event = sidecar + .poll_javascript_child_process( + &vm_id, + "malformed-wasm-parent", + &child_id, + 250, + ) + .unwrap_or_else(|error| { + panic!("successful child poll iteration {iteration} failed: {error}") + }); + if event.get("type").and_then(Value::as_str) == Some("exit") { + assert_eq!(event["exitCode"], Value::from(0)); + reaped = true; + break; + } + } + assert!( + reaped, + "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(), + ), + context_baseline, + "successful spawn/reap iteration {iteration} leaked an execution context" + ); + } } + fn javascript_child_process_shell_mode_without_guest_sh_fails_loudly() { let mut sidecar = create_test_sidecar(); let (connection_id, session_id) = @@ -11368,6 +13189,7 @@ export async function loadPyodide() { socket_id: None, command: None, args: Vec::new(), + argv0: None, cwd: None, env: BTreeMap::new(), shell: false, @@ -11403,6 +13225,7 @@ export async function loadPyodide() { socket_id: None, command: None, args: Vec::new(), + argv0: None, cwd: None, env: BTreeMap::new(), shell: false, @@ -11482,6 +13305,7 @@ await new Promise(() => {}); vm_id: vm_id.clone(), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], + argv0: None, env: BTreeMap::from([( String::from("AGENTOS_NODE_SYNC_RPC_ENABLE"), String::from("1"), @@ -11986,6 +13810,7 @@ console.log( vm_id: vm_id.clone(), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], + argv0: None, env: BTreeMap::from([( String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), String::from( @@ -13177,6 +15002,7 @@ await new Promise(() => {}); vm_id: vm_id.clone(), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], + argv0: None, env: BTreeMap::from([( String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), String::from( @@ -15380,6 +17206,7 @@ console.log(JSON.stringify({ lookup, resolve4 })); vm_id: vm_id.clone(), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], + argv0: None, env: BTreeMap::from([( String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), String::from( @@ -15573,6 +17400,7 @@ process.exit(0); vm_id: vm_id.clone(), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], + argv0: None, env: BTreeMap::from([( String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), String::from( @@ -16275,6 +18103,7 @@ process.exit(0); vm_id: vm_id.clone(), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], + argv0: None, env: BTreeMap::from([( String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), String::from( @@ -19939,6 +21768,7 @@ console.log(`BODY:${{body}}`); vm_id: vm_id.clone(), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], + argv0: None, env: BTreeMap::from([( String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), String::from( @@ -19983,20 +21813,11 @@ console.log(`BODY:${{body}}`); let bridge = sidecar.bridge.clone(); let dns = sidecar.vms.get(&vm_id).expect("javascript vm").dns.clone(); let limits = ResourceLimits::default(); - let socket_paths = JavascriptSocketPathContext { - sandbox_root: cwd.clone(), - mounts: Vec::new(), - listen_policy: VmListenPolicy::default(), - loopback_exempt_ports: BTreeSet::new(), - tcp_loopback_guest_to_host_ports: BTreeMap::new(), - http_loopback_targets: BTreeMap::new(), - udp_loopback_guest_to_host_ports: BTreeMap::new(), - udp_loopback_host_to_guest_ports: BTreeMap::new(), - used_tcp_guest_ports: BTreeMap::new(), - used_udp_guest_ports: BTreeMap::new(), - }; + let socket_paths = build_javascript_socket_path_context( + sidecar.vms.get(&vm_id).expect("javascript vm"), + ) + .expect("build Unix socket path context"); let socket_path = "/tmp/secure-exec.sock"; - let host_socket_path = cwd.join("tmp/secure-exec.sock"); let listen = { let counts = sidecar @@ -20032,7 +21853,10 @@ console.log(`BODY:${{body}}`); .expect("listen on unix socket") }; let server_id = listen["serverId"].as_str().expect("server id").to_string(); - assert_eq!(listen["path"], Value::String(String::from(socket_path))); + assert_eq!( + listen["localPath"], + Value::String(String::from(socket_path)) + ); { let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); assert!( @@ -20042,7 +21866,23 @@ console.log(`BODY:${{body}}`); "kernel did not expose unix socket path" ); } + let host_socket_path = socket_paths + .unix_bound_addresses + .lock() + .expect("Unix address registry") + .values() + .next() + .and_then(|entry| entry.host_path.clone()) + .expect("pathname Unix host path"); assert!(host_socket_path.exists(), "host unix socket path missing"); + assert!( + host_socket_path.starts_with(&socket_paths.unix_socket_host_dir), + "host Unix socket escaped the per-VM private directory" + ); + assert!( + !host_socket_path.starts_with(&cwd), + "host Unix socket leaked into the JavaScript working directory" + ); let listener_lookup = sidecar .dispatch_blocking(request( @@ -20508,6 +22348,82 @@ console.log(`BODY:${{body}}`); .expect("close unix listener"); } + for (request_id, method, args, expected_resource) in [ + ( + 17_u64, + "net.listen", + vec![json!({ "path": "/tmp/denied.sock", "backlog": 1 })], + "unix:/tmp/denied.sock", + ), + ( + 18_u64, + "net.listen", + vec![json!({ "abstractPathHex": "64656e696564", "backlog": 1 })], + "unix:abstract:64656e696564", + ), + ( + 19_u64, + "net.connect", + vec![json!({ "path": socket_path })], + "unix:/tmp/secure-exec.sock", + ), + ( + 20_u64, + "net.connect", + vec![json!({ "abstractPathHex": "64656e696564" })], + "unix:abstract:64656e696564", + ), + ] { + bridge + .inspect(|bridge| { + bridge.push_permission_decision(agentos_bridge::PermissionDecision::deny( + "Unix sockets denied", + )); + }) + .expect("seed denied Unix socket permission"); + let counts = sidecar + .vms + .get(&vm_id) + .and_then(|vm| vm.active_processes.get("proc-js-unix")) + .expect("unix process") + .network_resource_counts(); + let vm = sidecar.vms.get_mut(&vm_id).expect("javascript vm"); + let process = vm + .active_processes + .get_mut("proc-js-unix") + .expect("unix process"); + let error = service_javascript_net_sync_rpc( + &bridge, + &vm_id, + &dns, + &socket_paths, + &mut vm.kernel, + process, + &JavascriptSyncRpcRequest { + raw_bytes_args: std::collections::HashMap::new(), + id: request_id, + method: String::from(method), + args, + }, + &limits, + counts, + ) + .expect_err("denied Unix socket operation must fail"); + assert!( + error.to_string().contains("EACCES"), + "denied Unix socket operation returned {error}" + ); + let expected_check = format!("net:{vm_id}:{expected_resource}"); + bridge + .inspect(|bridge| { + assert_eq!( + bridge.permission_checks.last().map(String::as_str), + Some(expected_check.as_str()), + ); + }) + .expect("inspect Unix socket permission request"); + } + sidecar .dispose_vm_internal_blocking( &connection_id, @@ -20516,6 +22432,10 @@ console.log(`BODY:${{body}}`); DisposeReason::Requested, ) .expect("dispose unix vm"); + assert!( + !socket_paths.unix_socket_host_dir.exists(), + "VM disposal must remove the private Unix socket namespace" + ); } fn javascript_child_process_rpc_spawns_nested_node_processes_inside_vm_kernel() { assert_node_available(); @@ -20611,6 +22531,7 @@ console.log(JSON.stringify({ vm_id: vm_id.clone(), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], + argv0: None, env: BTreeMap::from([( String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), String::from( @@ -20826,6 +22747,7 @@ console.log(JSON.stringify({ vm_id: vm_id.clone(), context_id: context.context_id, argv: vec![String::from("./entry.mjs")], + argv0: None, env: BTreeMap::from([( String::from("AGENTOS_ALLOWED_NODE_BUILTINS"), String::from( @@ -21217,7 +23139,9 @@ console.log(JSON.stringify({ pending_process_events_are_bounded(); process_event_receiver_overflow_preserves_queued_event(); tool_execution_event_overflow_is_reported(); + wasm_signal_queue_is_bounded(); descendant_transfer_overflow_preserves_global_queue(); + descendant_transfer_byte_overflow_restores_current_and_deferred_envelopes(); exit_trailing_requeue_preserves_exit_when_queue_is_full(); javascript_child_process_poll_reports_echild_when_child_disappears_after_drain(); javascript_child_process_internal_bootstrap_env_is_allowlisted(); @@ -21255,10 +23179,53 @@ console.log(JSON.stringify({ pending_process_events_are_bounded(); process_event_receiver_overflow_preserves_queued_event(); tool_execution_event_overflow_is_reported(); + wasm_signal_queue_is_bounded(); descendant_transfer_overflow_preserves_global_queue(); + descendant_transfer_byte_overflow_restores_current_and_deferred_envelopes(); exit_trailing_requeue_preserves_exit_when_queue_is_full(); } + #[test] + fn service_vm_pending_process_byte_budgets_are_aggregate() { + vm_pending_event_bytes_are_shared_and_reclaimed(); + vm_pending_stdin_bytes_release_on_partial_flush_clear_and_teardown(); + } + + #[test] + fn service_local_wasm_exec_matches_linux_process_state() { + local_wasm_exec_commit_uses_exact_guest_env_and_preserves_process_state(); + } + + #[test] + fn service_cross_runtime_exec_is_atomic_at_kernel_commit() { + cross_runtime_exec_starts_after_committed_linux_process_state(); + } + + #[test] + fn service_cross_runtime_exec_start_failure_is_post_commit_fatal() { + cross_runtime_exec_start_failure_is_fatal_after_kernel_commit(); + } + + #[test] + fn service_posix_spawn_actions_precede_exact_exec_resolution() { + posix_spawn_file_actions_run_before_exact_exec_resolution(); + } + + #[test] + fn service_dirty_host_shadow_sync_precedes_spawn_actions() { + dirty_host_shadow_sync_precedes_top_level_and_nested_spawn_actions(); + } + + #[test] + fn service_posix_spawnp_path_and_recursive_shebang_match_linux() { + posix_spawnp_path_and_recursive_shebang_match_linux(); + } + + #[test] + fn service_rejected_wasm_spawns_restore_kernel_resource_baselines() { + repeated_malformed_wasm_spawns_restore_top_level_and_nested_baselines(); + } + #[test] fn service_state_handle_tables_are_bounded() { sqlite_database_handles_are_bounded(); diff --git a/crates/sidecar-protocol/src/protocol.rs b/crates/sidecar-protocol/src/protocol.rs index a82bab77da..140394de68 100644 --- a/crates/sidecar-protocol/src/protocol.rs +++ b/crates/sidecar-protocol/src/protocol.rs @@ -2669,14 +2669,105 @@ fn validate_requirement( // JavaScript sync-RPC request types (deserialized from guest Node.js processes) // --------------------------------------------------------------------------- +#[derive(Debug, Clone, Deserialize)] +pub struct JavascriptPosixSpawnFileAction { + pub command: u32, + #[serde(rename = "guestFd", default)] + pub guest_fd: Option, + pub fd: i32, + #[serde(rename = "sourceFd")] + pub source_fd: i32, + #[serde(rename = "guestSourceFd", default)] + pub guest_source_fd: Option, + pub oflag: i32, + pub mode: u32, + pub path: String, + /// Runner-local public guest descriptors selected by closefrom. These do + /// not have kernel mappings, but the child bootstrap must still suppress + /// their untagged aliases while retaining private tagged preopens. + #[serde(rename = "closeFromGuestFds", default)] + pub close_from_guest_fds: Vec, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct JavascriptSpawnHostNetFd { + pub guest_fd: u32, + #[serde(default)] + pub close_on_exec: bool, + #[serde(default)] + pub socket_id: Option, + #[serde(default)] + pub server_id: Option, + #[serde(default)] + pub udp_socket_id: Option, + /// Runner-visible socket metadata needed to reconstruct the libc-facing + /// descriptor in the child. Resource ownership is never trusted from this + /// object: the sidecar resolves and clones the named parent resource. + #[serde(default)] + pub metadata: Value, +} + #[derive(Debug, Deserialize, Default)] pub struct JavascriptChildProcessSpawnOptions { + #[serde(default)] + pub argv0: Option, + /// Guest descriptors marked FD_CLOEXEC in libc's runner-local descriptor + /// table. Native exec applies these at the same commit point as the kernel + /// table; runner-local handles are closed by the in-place image swap. + #[serde(rename = "cloexecFds", default)] + pub cloexec_fds: Vec, + /// The sidecar-managed WASM runner has already validated the replacement + /// module and will swap images in place after the sidecar commits kernel + /// metadata. Other process.exec callers use the separate-execution path. + #[serde(rename = "localReplacement", default)] + pub local_replacement: bool, + /// Runner-private executable descriptor used only by the dedicated + /// process.exec_fd_image_commit route. Generic guest process.exec requests + /// cannot turn this into an FD-backed replacement. + #[serde(rename = "executableFd", default)] + pub executable_fd: Option, #[serde(default)] pub cwd: Option, #[serde(default)] pub env: BTreeMap, #[serde(rename = "internalBootstrapEnv", default)] pub internal_bootstrap_env: BTreeMap, + /// POSIX spawn attributes already validated by the WASM host-import + /// boundary. The sidecar validates them again before applying attributes + /// that belong to the kernel process table. + #[serde(rename = "spawnAttrFlags", default)] + pub spawn_attr_flags: u32, + /// Exact `posix_spawn` bypasses PATH lookup, including for bare names. + #[serde(rename = "spawnExactPath", default)] + pub spawn_exact_path: bool, + /// `posix_spawnp` uses the caller's PATH, which can differ from envp. + #[serde(rename = "spawnSearchPath", default)] + pub spawn_search_path: Option, + #[serde(rename = "spawnSchedPolicy", default)] + pub spawn_sched_policy: Option, + #[serde(rename = "spawnSchedPriority", default)] + pub spawn_sched_priority: Option, + #[serde(rename = "spawnPgroup", default)] + pub spawn_pgroup: Option, + #[serde(rename = "spawnSignalDefaults", default)] + pub spawn_signal_defaults: Vec, + #[serde(rename = "spawnSignalMask", default)] + pub spawn_signal_mask: Vec, + #[serde(rename = "spawnFileActions", default)] + pub spawn_file_actions: Vec, + /// Guest-to-kernel descriptor namespace captured by the WASM runner at + /// spawn time. WASI preopens occupy guest descriptors that do not exist in + /// the kernel table, so file actions cannot safely assume the two numeric + /// namespaces are identical. + #[serde(rename = "spawnFdMappings", default)] + pub spawn_fd_mappings: Vec<[u32; 2]>, + /// Host-network descriptors occupy the guest fd namespace but are backed + /// by sidecar-owned socket descriptions rather than kernel fd-table slots. + /// They must therefore be inherited explicitly instead of being coerced + /// into the private kernel descriptor namespace. + #[serde(rename = "spawnHostNetFds", default)] + pub spawn_host_net_fds: Vec, #[serde(default)] pub input: Option, #[serde(default)] @@ -2708,6 +2799,10 @@ pub struct JavascriptNetConnectRequest { pub port: Option, #[serde(default)] pub path: Option, + #[serde(rename = "abstractPathHex", default)] + pub abstract_path_hex: Option, + #[serde(rename = "boundServerId", default)] + pub bound_server_id: Option, #[serde(rename = "localAddress", default)] pub local_address: Option, #[serde(rename = "localPort", default)] @@ -2716,6 +2811,18 @@ pub struct JavascriptNetConnectRequest { pub local_reservation: Option, } +#[derive(Debug, Deserialize)] +pub struct JavascriptNetBindConnectedUnixRequest { + #[serde(rename = "socketId")] + pub socket_id: String, + #[serde(default)] + pub path: Option, + #[serde(rename = "abstractPathHex", default)] + pub abstract_path_hex: Option, + #[serde(default)] + pub autobind: bool, +} + #[derive(Debug, Deserialize)] pub struct JavascriptNetReserveTcpPortRequest { #[serde(default)] @@ -2732,6 +2839,12 @@ pub struct JavascriptNetListenRequest { pub port: Option, #[serde(default)] pub path: Option, + #[serde(rename = "abstractPathHex", default)] + pub abstract_path_hex: Option, + #[serde(rename = "boundServerId", default)] + pub bound_server_id: Option, + #[serde(default)] + pub autobind: bool, #[serde(default)] pub backlog: Option, #[serde(rename = "localReservation", default)] diff --git a/crates/sidecar-protocol/src/wire.rs b/crates/sidecar-protocol/src/wire.rs index a9822a1438..85536a9a44 100644 --- a/crates/sidecar-protocol/src/wire.rs +++ b/crates/sidecar-protocol/src/wire.rs @@ -519,6 +519,18 @@ fn legacy_limits_config( prewarm_timeout_ms: legacy_u64(metadata, "limits.wasm.prewarm_timeout_ms"), runner_heap_limit_mb: legacy_u64(metadata, "limits.wasm.runner_heap_limit_mb"), }; + let process = agentos_vm_config::ProcessLimitsConfig { + max_spawn_file_actions: legacy_u64(metadata, "limits.process.max_spawn_file_actions") + .or_else(|| legacy_u64(metadata, "limits.wasm.max_spawn_file_actions")), + max_spawn_file_action_bytes: legacy_u64( + metadata, + "limits.process.max_spawn_file_action_bytes", + ) + .or_else(|| legacy_u64(metadata, "limits.wasm.max_spawn_file_action_bytes")), + pending_stdin_bytes: legacy_u64(metadata, "limits.process.pending_stdin_bytes"), + pending_event_count: legacy_u64(metadata, "limits.process.pending_event_count"), + pending_event_bytes: legacy_u64(metadata, "limits.process.pending_event_bytes"), + }; let config = agentos_vm_config::VmLimitsConfig { resources: legacy_has_resource_limits(&resources).then_some(resources), @@ -529,6 +541,7 @@ fn legacy_limits_config( js_runtime: legacy_has_js_runtime_limits(&js_runtime).then_some(js_runtime), python: legacy_has_python_limits(&python).then_some(python), wasm: legacy_has_wasm_limits(&wasm).then_some(wasm), + process: legacy_has_process_limits(&process).then_some(process), }; if config.resources.is_none() @@ -539,6 +552,7 @@ fn legacy_limits_config( && config.js_runtime.is_none() && config.python.is_none() && config.wasm.is_none() + && config.process.is_none() { None } else { @@ -624,6 +638,14 @@ fn legacy_has_wasm_limits(config: &agentos_vm_config::WasmLimitsConfig) -> bool || config.runner_heap_limit_mb.is_some() } +fn legacy_has_process_limits(config: &agentos_vm_config::ProcessLimitsConfig) -> bool { + config.max_spawn_file_actions.is_some() + || config.max_spawn_file_action_bytes.is_some() + || config.pending_stdin_bytes.is_some() + || config.pending_event_count.is_some() + || config.pending_event_bytes.is_some() +} + // Ownership-scope constructor ergonomics. The generated BARE union exposes only the // tuple-wrapped variants (`ConnectionOwnership`/`SessionOwnership`/`VmOwnership`); restore // the hand-written `connection`/`session`/`vm` helpers the sidecar relies on. These live in diff --git a/crates/v8-runtime/src/embedded_runtime.rs b/crates/v8-runtime/src/embedded_runtime.rs index 4b8cb8e759..6c5cf6abbb 100644 --- a/crates/v8-runtime/src/embedded_runtime.rs +++ b/crates/v8-runtime/src/embedded_runtime.rs @@ -373,6 +373,18 @@ impl EmbeddedV8SessionHandle { }) } + pub fn pause(&self) -> io::Result<()> { + self.runtime.dispatch(RuntimeCommand::PauseSession { + session_id: self.session_id.clone(), + }) + } + + pub fn resume(&self) -> io::Result<()> { + self.runtime.dispatch(RuntimeCommand::ResumeSession { + session_id: self.session_id.clone(), + }) + } + pub fn destroy(&self) -> io::Result<()> { self.runtime.unregister_session(&self.session_id); self.runtime.dispatch(RuntimeCommand::DestroySession { @@ -635,6 +647,14 @@ fn dispatch_runtime_command( shutdown.finish(); Ok(()) } + RuntimeCommand::PauseSession { session_id } => { + let mgr = session_mgr.lock().expect("session manager lock poisoned"); + mgr.pause_session(&session_id).map_err(other_io_error) + } + RuntimeCommand::ResumeSession { session_id } => { + let mgr = session_mgr.lock().expect("session manager lock poisoned"); + mgr.resume_session(&session_id).map_err(other_io_error) + } RuntimeCommand::SendToSession { session_id, message, diff --git a/crates/v8-runtime/src/execution.rs b/crates/v8-runtime/src/execution.rs index 0e811ad3a2..4793943248 100644 --- a/crates/v8-runtime/src/execution.rs +++ b/crates/v8-runtime/src/execution.rs @@ -5215,7 +5215,7 @@ export const file = new File([], "empty.txt"); let scope = &mut v8::HandleScope::new(&mut iso); let local = v8::Local::new(scope, &ctx); let scope = &mut v8::ContextScope::new(scope, local); - crate::session::run_event_loop(scope, &rx, &pending, None, None) + crate::session::run_event_loop(scope, &rx, &pending, None, None, None) }; assert!( @@ -5294,7 +5294,7 @@ export const file = new File([], "empty.txt"); let scope = &mut v8::HandleScope::new(&mut iso); let local = v8::Local::new(scope, &ctx); let scope = &mut v8::ContextScope::new(scope, local); - crate::session::run_event_loop(scope, &rx, &pending, None, None) + crate::session::run_event_loop(scope, &rx, &pending, None, None, None) }; assert!(matches!( @@ -5345,7 +5345,7 @@ export const file = new File([], "empty.txt"); let scope = &mut v8::HandleScope::new(&mut iso); let local = v8::Local::new(scope, &ctx); let scope = &mut v8::ContextScope::new(scope, local); - crate::session::run_event_loop(scope, &rx, &pending, None, None) + crate::session::run_event_loop(scope, &rx, &pending, None, None, None) }; assert!( @@ -5395,7 +5395,7 @@ export const file = new File([], "empty.txt"); let scope = &mut v8::HandleScope::new(&mut iso); let local = v8::Local::new(scope, &ctx); let scope = &mut v8::ContextScope::new(scope, local); - crate::session::run_event_loop(scope, &rx, &pending, None, None) + crate::session::run_event_loop(scope, &rx, &pending, None, None, None) }; assert!( @@ -5417,7 +5417,7 @@ export const file = new File([], "empty.txt"); let scope = &mut v8::HandleScope::new(&mut iso); let local = v8::Local::new(scope, &ctx); let scope = &mut v8::ContextScope::new(scope, local); - crate::session::run_event_loop(scope, &rx, &pending, None, None) + crate::session::run_event_loop(scope, &rx, &pending, None, None, None) }; assert!(matches!( @@ -5496,7 +5496,7 @@ export const file = new File([], "empty.txt"); let scope = &mut v8::HandleScope::new(&mut iso); let local = v8::Local::new(scope, &ctx); let scope = &mut v8::ContextScope::new(scope, local); - crate::session::run_event_loop(scope, &rx, &pending, None, None) + crate::session::run_event_loop(scope, &rx, &pending, None, None, None) }; assert!(matches!( @@ -5568,7 +5568,7 @@ export const file = new File([], "empty.txt"); let scope = &mut v8::HandleScope::new(&mut iso); let local = v8::Local::new(scope, &ctx); let scope = &mut v8::ContextScope::new(scope, local); - crate::session::run_event_loop(scope, &rx, &pending, None, None); + crate::session::run_event_loop(scope, &rx, &pending, None, None, None); } // .then handler should have run (microtasks flushed) @@ -5655,7 +5655,7 @@ export const file = new File([], "empty.txt"); let scope = &mut v8::HandleScope::new(&mut iso); let local = v8::Local::new(scope, &ctx); let scope = &mut v8::ContextScope::new(scope, local); - crate::session::run_event_loop(scope, &rx, &pending, None, None) + crate::session::run_event_loop(scope, &rx, &pending, None, None, None) }; assert!(matches!( @@ -5738,7 +5738,7 @@ export const file = new File([], "empty.txt"); let scope = &mut v8::HandleScope::new(&mut iso); let local = v8::Local::new(scope, &ctx); let scope = &mut v8::ContextScope::new(scope, local); - crate::session::run_event_loop(scope, &rx, &pending, None, None) + crate::session::run_event_loop(scope, &rx, &pending, None, None, None) }; assert!(matches!( @@ -5817,7 +5817,7 @@ export const file = new File([], "empty.txt"); let scope = &mut v8::HandleScope::new(&mut iso); let local = v8::Local::new(scope, &ctx); let scope = &mut v8::ContextScope::new(scope, local); - crate::session::run_event_loop(scope, &rx, &pending, None, None) + crate::session::run_event_loop(scope, &rx, &pending, None, None, None) }; assert!(matches!( @@ -5889,7 +5889,7 @@ export const file = new File([], "empty.txt"); let scope = &mut v8::HandleScope::new(&mut iso); let local = v8::Local::new(scope, &ctx); let scope = &mut v8::ContextScope::new(scope, local); - crate::session::run_event_loop(scope, &rx, &pending, None, None) + crate::session::run_event_loop(scope, &rx, &pending, None, None, None) }; assert!(matches!( @@ -5965,7 +5965,7 @@ export const file = new File([], "empty.txt"); let scope = &mut v8::HandleScope::new(&mut iso); let local = v8::Local::new(scope, &ctx); let scope = &mut v8::ContextScope::new(scope, local); - crate::session::run_event_loop(scope, &rx, &pending, None, None); + crate::session::run_event_loop(scope, &rx, &pending, None, None, None); } // Microtask enqueued by the dispatch callback should have run @@ -6095,7 +6095,14 @@ export const file = new File([], "empty.txt"); let scope = &mut v8::HandleScope::new(&mut iso); let local = v8::Local::new(scope, &ctx); let scope = &mut v8::ContextScope::new(scope, local); - crate::session::run_event_loop(scope, &cmd_rx, &pending, Some(&abort_rx), None) + crate::session::run_event_loop( + scope, + &cmd_rx, + &pending, + Some(&abort_rx), + None, + None, + ) }; assert!( diff --git a/crates/v8-runtime/src/runtime_protocol.rs b/crates/v8-runtime/src/runtime_protocol.rs index 1291905e4b..331019cea7 100644 --- a/crates/v8-runtime/src/runtime_protocol.rs +++ b/crates/v8-runtime/src/runtime_protocol.rs @@ -21,6 +21,12 @@ pub enum RuntimeCommand { DestroySession { session_id: String, }, + PauseSession { + session_id: String, + }, + ResumeSession { + session_id: String, + }, WarmSnapshot { bridge_code: String, userland_code: String, diff --git a/crates/v8-runtime/src/session.rs b/crates/v8-runtime/src/session.rs index 498867918f..6b61a19c9b 100644 --- a/crates/v8-runtime/src/session.rs +++ b/crates/v8-runtime/src/session.rs @@ -41,6 +41,58 @@ pub enum SessionCommand { SetModuleReader(Box), } +#[derive(Default)] +struct SessionPauseState { + paused: bool, + shutdown: bool, +} + +#[derive(Default)] +pub(crate) struct SessionPauseControl { + state: Mutex, + resumed: Condvar, +} + +impl SessionPauseControl { + fn pause(&self) { + self.state + .lock() + .expect("session pause lock poisoned") + .paused = true; + } + + fn resume(&self) { + let mut state = self.state.lock().expect("session pause lock poisoned"); + state.paused = false; + self.resumed.notify_all(); + } + + fn shutdown(&self) { + let mut state = self.state.lock().expect("session pause lock poisoned"); + state.shutdown = true; + state.paused = false; + self.resumed.notify_all(); + } + + fn wait_while_paused(&self) { + let mut state = self.state.lock().expect("session pause lock poisoned"); + while state.paused && !state.shutdown { + state = self + .resumed + .wait(state) + .expect("session pause lock poisoned while waiting"); + } + } +} + +#[cfg(not(test))] +extern "C" fn pause_isolate_interrupt(_isolate: &mut v8::Isolate, data: *mut std::ffi::c_void) { + // SAFETY: pause_session passes one Arc strong reference with into_raw for + // this callback. V8 invokes each accepted interrupt exactly once. + let control = unsafe { Arc::from_raw(data.cast::()) }; + control.wait_while_paused(); +} + #[cfg(not(test))] type SharedIsolateHandle = Arc>>; #[cfg(test)] @@ -98,6 +150,7 @@ struct SessionAssignment { snapshot_cache: Arc, isolate_handle: SharedIsolateHandle, execution_abort: SharedExecutionAbort, + pause_control: Arc, session_id: String, output_generation: Option, } @@ -490,6 +543,7 @@ struct SessionEntry { isolate_handle: SharedIsolateHandle, /// Current execution abort handle used to wake sync bridge waits. execution_abort: SharedExecutionAbort, + pause_control: Arc, } /// Deferred shutdown work for a session that has already been removed from @@ -703,6 +757,7 @@ impl SessionManager { let (tx, rx) = crossbeam_channel::bounded(SESSION_COMMAND_CHANNEL_CAPACITY); let isolate_handle = Arc::new(Mutex::new(None)); let execution_abort = new_execution_abort(); + let pause_control = Arc::new(SessionPauseControl::default()); let assignment = SessionAssignment { heap_limit_mb, cpu_time_limit_ms, @@ -716,6 +771,7 @@ impl SessionManager { snapshot_cache: Arc::clone(&self.snapshot_cache), isolate_handle: Arc::clone(&isolate_handle), execution_abort: execution_abort.clone(), + pause_control: Arc::clone(&pause_control), session_id: session_id.clone(), output_generation, }; @@ -747,6 +803,7 @@ impl SessionManager { join_handle: Some(join_handle), isolate_handle, execution_abort, + pause_control, }, ); @@ -850,6 +907,7 @@ impl SessionManager { .sessions .get(session_id) .ok_or_else(|| format!("session {} does not exist", session_id))?; + entry.pause_control.shutdown(); #[cfg(not(test))] if let Some(handle) = entry @@ -891,6 +949,7 @@ impl SessionManager { .sessions .remove(session_id) .expect("checked session exists"); + entry.pause_control.shutdown(); #[cfg(not(test))] if let Some(handle) = entry @@ -999,6 +1058,41 @@ impl SessionManager { .ok_or_else(|| format!("session {} does not exist", session_id)) } + /// Pause a session at the V8 execution boundary. A running synchronous + /// script is interrupted on its isolate thread and remains parked with its + /// JavaScript stack intact until `resume_session` is called. + pub fn pause_session(&self, session_id: &str) -> Result<(), String> { + let entry = self + .sessions + .get(session_id) + .ok_or_else(|| format!("session {} does not exist", session_id))?; + entry.pause_control.pause(); + #[cfg(not(test))] + if let Some(handle) = entry + .isolate_handle + .lock() + .ok() + .and_then(|guard| guard.as_ref().cloned()) + { + let raw = Arc::into_raw(Arc::clone(&entry.pause_control)) as *mut std::ffi::c_void; + if !handle.request_interrupt(pause_isolate_interrupt, raw) { + // SAFETY: V8 rejected the interrupt, so it did not consume the + // strong reference transferred above. + unsafe { drop(Arc::from_raw(raw.cast::())) }; + } + } + Ok(()) + } + + pub fn resume_session(&self, session_id: &str) -> Result<(), String> { + let entry = self + .sessions + .get(session_id) + .ok_or_else(|| format!("session {} does not exist", session_id))?; + entry.pause_control.resume(); + Ok(()) + } + /// Send a message to a session. Blocks on the session command channel, so /// this must not be called while a shared lock on the manager is held. pub fn send_to_session(&self, session_id: &str, msg: SessionMessage) -> Result<(), String> { @@ -1217,6 +1311,7 @@ fn session_thread( snapshot_cache, isolate_handle, execution_abort, + pause_control, session_id, output_generation, } = assignment; @@ -1230,6 +1325,7 @@ fn session_thread( snapshot_cache, isolate_handle, execution_abort, + pause_control, ); // Acquire concurrency slot, but keep polling the session channel so a queued @@ -1346,6 +1442,7 @@ fn session_thread( rx.recv() }; + pause_control.wait_while_paused(); match next_command { Ok(SessionCommand::Shutdown) | Err(_) => break, Ok(SessionCommand::SetModuleReader(reader)) => { @@ -1579,6 +1676,7 @@ fn session_thread( rx.clone(), abort_rx.clone(), Arc::clone(&deferred_queue), + Arc::clone(&pause_control), ); let bridge_ctx = BridgeCallContext::with_receiver( Box::new(ChannelRuntimeEventSender::new( @@ -1867,6 +1965,7 @@ fn session_thread( &pending, Some(&abort_rx), Some(&deferred_queue), + Some(&pause_control), ) } else { EventLoopStatus::Completed @@ -1941,6 +2040,7 @@ fn session_thread( &pending, Some(&abort_rx), Some(&deferred_queue), + Some(&pause_control), ); if matches!(event_loop_status, EventLoopStatus::Terminated) { @@ -2199,12 +2299,13 @@ pub fn reset_pending_promises(pending: &mut crate::bridge::PendingPromises) { /// /// Returns true if execution completed normally, false if terminated. #[doc(hidden)] -pub fn run_event_loop( +pub(crate) fn run_event_loop( scope: &mut v8::HandleScope, rx: &Receiver, pending: &crate::bridge::PendingPromises, abort_rx: Option<&crossbeam_channel::Receiver<()>>, deferred: Option<&DeferredQueue>, + pause_control: Option<&SessionPauseControl>, ) -> EventLoopStatus { while !pending.is_empty() || execution::pending_module_evaluation_needs_wait(scope) @@ -2215,6 +2316,9 @@ pub fn run_event_loop( .map(|dq| !dq.lock().unwrap().is_empty()) .unwrap_or(false) { + if let Some(control) = pause_control { + control.wait_while_paused(); + } pump_v8_message_loop(scope); // Drain deferred messages queued by sync bridge calls before blocking @@ -2318,6 +2422,9 @@ pub fn run_event_loop( if let Some(cmd) = recv_result { break cmd; } + if let Some(control) = pause_control { + control.wait_while_paused(); + } // No command received — flush microtasks and check deferred queue scope.perform_microtask_checkpoint(); pump_v8_message_loop(scope); @@ -2549,6 +2656,7 @@ pub(crate) struct ChannelResponseReceiver { rx: Receiver, abort_rx: Option>, deferred: DeferredQueue, + pause_control: Option>, } impl ChannelResponseReceiver { @@ -2558,6 +2666,7 @@ impl ChannelResponseReceiver { rx, abort_rx: None, deferred, + pause_control: None, } } @@ -2565,11 +2674,13 @@ impl ChannelResponseReceiver { rx: Receiver, abort_rx: crossbeam_channel::Receiver<()>, deferred: DeferredQueue, + pause_control: Arc, ) -> Self { ChannelResponseReceiver { rx, abort_rx: Some(abort_rx), deferred, + pause_control: Some(pause_control), } } } @@ -2600,6 +2711,9 @@ impl crate::host_call::BridgeResponseReceiver for ChannelResponseReceiver { if let SessionMessage::BridgeResponse(response) = &frame { let call_id = response.call_id; if call_id == expected_call_id { + if let Some(control) = &self.pause_control { + control.wait_while_paused(); + } crate::host_call::record_sync_bridge_response_channel_received(call_id); return Ok(response.clone()); } @@ -3042,7 +3156,12 @@ mod tests { let deferred = new_deferred_queue(); let execution_abort = new_execution_abort(); let (_active_abort, abort_rx) = ActiveExecutionAbort::arm(&execution_abort); - let receiver = ChannelResponseReceiver::with_abort(rx, abort_rx, deferred); + let receiver = ChannelResponseReceiver::with_abort( + rx, + abort_rx, + deferred, + Arc::new(SessionPauseControl::default()), + ); let (result_tx, result_rx) = std::sync::mpsc::channel(); let join_handle = std::thread::spawn(move || { diff --git a/crates/v8-runtime/tests/embedded_runtime_session.rs b/crates/v8-runtime/tests/embedded_runtime_session.rs index dd2f6ef752..05fef4423e 100644 --- a/crates/v8-runtime/tests/embedded_runtime_session.rs +++ b/crates/v8-runtime/tests/embedded_runtime_session.rs @@ -531,6 +531,51 @@ fn assert_terminate_interrupts_sync_bridge_wait() -> io::Result<()> { Ok(()) } +fn assert_pause_preserves_synchronous_execution_stack() -> io::Result<()> { + let runtime = Arc::new(EmbeddedV8Runtime::new(Some(1))?); + let session_id = next_session_id(); + let receiver = register_and_create_session(&runtime, &session_id)?; + let handle = runtime.session_handle(session_id.clone()); + + dispatch_execute( + runtime.as_ref(), + &session_id, + 0, + "", + "_loadFileSync('/before-pause'); _loadFileSync('/after-resume');", + )?; + + let first_call = wait_for_bridge_call(&receiver, &session_id); + let first_call_id = match first_call { + RuntimeEvent::BridgeCall { call_id, .. } => call_id, + other => panic!("expected first bridge call, got {other:?}"), + }; + handle.pause()?; + handle.send_bridge_response(first_call_id, 0, Vec::new())?; + + let event_while_paused = receiver.recv_timeout(Duration::from_millis(100)); + handle.resume()?; + assert!( + event_while_paused.is_err(), + "paused synchronous execution must not advance to its next host call" + ); + + let second_call = wait_for_bridge_call(&receiver, &session_id); + let second_call_id = match second_call { + RuntimeEvent::BridgeCall { call_id, .. } => call_id, + other => panic!("expected second bridge call, got {other:?}"), + }; + handle.send_bridge_response(second_call_id, 0, Vec::new())?; + assert_execution_ok(&receiver, &session_id); + + handle.destroy()?; + runtime.unregister_session(&session_id); + wait_until("expected resumed session to drain cleanly", || { + runtime.session_count() == 0 && runtime.active_slot_count() == 0 + }); + Ok(()) +} + fn assert_cpu_terminated_session_can_execute_again() -> io::Result<()> { let runtime = Arc::new(EmbeddedV8Runtime::new(Some(1))?); let session_id = next_session_id(); @@ -626,6 +671,7 @@ fn embedded_runtime_session_consolidated_behaviors() -> io::Result<()> { assert_queued_work_waits_for_slot_release()?; assert_shared_runtime_handles_share_concurrency_quota()?; assert_terminate_interrupts_sync_bridge_wait()?; + assert_pause_preserves_synchronous_execution_stack()?; assert_cpu_terminated_session_can_execute_again()?; assert_isolate_churn_recreates_embedded_sessions_without_segv()?; Ok(()) diff --git a/crates/vfs/assets/base-filesystem.json b/crates/vfs/assets/base-filesystem.json index 64e03bfa53..e641efddd3 100644 --- a/crates/vfs/assets/base-filesystem.json +++ b/crates/vfs/assets/base-filesystem.json @@ -7,7 +7,8 @@ "transforms": [ "Normalize HOSTNAME to secure-exec", "Preserve the captured user-level environment and filesystem layout as the secure-exec base layer", - "Add the non-Alpine /workspace directory (default agent working directory) owned by the base user" + "Add the non-Alpine /workspace directory (default agent working directory) owned by the base user", + "Restore Alpine 3.22's /etc/services database and add the VM's Docker-style /etc/hosts entries for libc lookup parity" ] }, "environment": { @@ -93,6 +94,14 @@ "gid": 0, "content": "secure-exec\n" }, + { + "path": "/etc/hosts", + "type": "file", + "mode": "644", + "uid": 0, + "gid": 0, + "content": "127.0.0.1 localhost localhost.localdomain\n::1 localhost localhost.localdomain ip6-localhost ip6-loopback\nfe00:: ip6-localnet\nff00:: ip6-mcastprefix\nff02::1 ip6-allnodes\nff02::2 ip6-allrouters\n127.0.1.1 secure-exec\n" + }, { "path": "/etc/logrotate.d", "type": "directory", @@ -197,6 +206,14 @@ "gid": 0, "content": "# valid login shells\n/bin/sh\n/bin/ash\n" }, + { + "path": "/etc/services", + "type": "file", + "mode": "644", + "uid": 0, + "gid": 0, + "content": "# Network services, Internet style\n#\n# Updated from https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml .\n#\n# New ports will be added on request if they have been officially assigned\n# by IANA and used in the real-world or are needed by a debian package.\n# If you need a huge list of used numbers please install the nmap package.\n\ntcpmux\t\t1/tcp\t\t\t\t# TCP port service multiplexer\necho\t\t7/tcp\necho\t\t7/udp\ndiscard\t\t9/tcp\t\tsink null\ndiscard\t\t9/udp\t\tsink null\nsystat\t\t11/tcp\t\tusers\ndaytime\t\t13/tcp\ndaytime\t\t13/udp\nnetstat\t\t15/tcp\nqotd\t\t17/tcp\t\tquote\nchargen\t\t19/tcp\t\tttytst source\nchargen\t\t19/udp\t\tttytst source\nftp-data\t20/tcp\nftp\t\t21/tcp\nfsp\t\t21/udp\t\tfspd\nssh\t\t22/tcp\t\t\t\t# SSH Remote Login Protocol\ntelnet\t\t23/tcp\nsmtp\t\t25/tcp\t\tmail\ntime\t\t37/tcp\t\ttimserver\ntime\t\t37/udp\t\ttimserver\nwhois\t\t43/tcp\t\tnicname\ntacacs\t\t49/tcp\t\t\t\t# Login Host Protocol (TACACS)\ntacacs\t\t49/udp\ndomain\t\t53/tcp\t\t\t\t# Domain Name Server\ndomain\t\t53/udp\nbootps\t\t67/udp\nbootpc\t\t68/udp\ntftp\t\t69/udp\ngopher\t\t70/tcp\t\t\t\t# Internet Gopher\nfinger\t\t79/tcp\nhttp\t\t80/tcp\t\twww\t\t# WorldWideWeb HTTP\nkerberos\t88/tcp\t\tkerberos5 krb5 kerberos-sec\t# Kerberos v5\nkerberos\t88/udp\t\tkerberos5 krb5 kerberos-sec\t# Kerberos v5\niso-tsap\t102/tcp\t\ttsap\t\t# part of ISODE\nacr-nema\t104/tcp\t\tdicom\t\t# Digital Imag. & Comm. 300\npop3\t\t110/tcp\t\tpop-3\t\t# POP version 3\nsunrpc\t\t111/tcp\t\tportmapper\t# RPC 4.0 portmapper\nsunrpc\t\t111/udp\t\tportmapper\nauth\t\t113/tcp\t\tauthentication tap ident\nnntp\t\t119/tcp\t\treadnews untp\t# USENET News Transfer Protocol\nntp\t\t123/udp\t\t\t\t# Network Time Protocol\nepmap\t\t135/tcp\t\tloc-srv\t\t# DCE endpoint resolution\nnetbios-ns\t137/udp\t\t\t\t# NETBIOS Name Service\nnetbios-dgm\t138/udp\t\t\t\t# NETBIOS Datagram Service\nnetbios-ssn\t139/tcp\t\t\t\t# NETBIOS session service\nimap2\t\t143/tcp\t\timap\t\t# Interim Mail Access P 2 and 4\nsnmp\t\t161/tcp\t\t\t\t# Simple Net Mgmt Protocol\nsnmp\t\t161/udp\nsnmp-trap\t162/tcp\t\tsnmptrap\t# Traps for SNMP\nsnmp-trap\t162/udp\t\tsnmptrap\ncmip-man\t163/tcp\t\t\t\t# ISO mgmt over IP (CMOT)\ncmip-man\t163/udp\ncmip-agent\t164/tcp\ncmip-agent\t164/udp\nmailq\t\t174/tcp\t\t\t# Mailer transport queue for Zmailer\nxdmcp\t\t177/udp\t\t\t# X Display Manager Control Protocol\nbgp\t\t179/tcp\t\t\t\t# Border Gateway Protocol\nsmux\t\t199/tcp\t\t\t\t# SNMP Unix Multiplexer\nqmtp\t\t209/tcp\t\t\t\t# Quick Mail Transfer Protocol\nz3950\t\t210/tcp\t\twais\t\t# NISO Z39.50 database\nipx\t\t213/udp\t\t\t\t# IPX [RFC1234]\nptp-event\t319/udp\nptp-general\t320/udp\npawserv\t\t345/tcp\t\t\t\t# Perf Analysis Workbench\nzserv\t\t346/tcp\t\t\t\t# Zebra server\nrpc2portmap\t369/tcp\nrpc2portmap\t369/udp\t\t\t\t# Coda portmapper\ncodaauth2\t370/tcp\ncodaauth2\t370/udp\t\t\t\t# Coda authentication server\nclearcase\t371/udp\t\tClearcase\nldap\t\t389/tcp\t\t\t# Lightweight Directory Access Protocol\nldap\t\t389/udp\nsvrloc\t\t427/tcp\t\t\t\t# Server Location\nsvrloc\t\t427/udp\nhttps\t\t443/tcp\t\t\t\t# http protocol over TLS/SSL\nhttps\t\t443/udp\t\t\t\t# HTTP/3\nsnpp\t\t444/tcp\t\t\t\t# Simple Network Paging Protocol\nmicrosoft-ds\t445/tcp\t\t\t\t# Microsoft Naked CIFS\nkpasswd\t\t464/tcp\nkpasswd\t\t464/udp\nsubmissions\t465/tcp\t\tssmtp smtps urd # Submission over TLS [RFC8314]\nsaft\t\t487/tcp\t\t\t# Simple Asynchronous File Transfer\nisakmp\t\t500/udp\t\t\t\t# IPSEC key management\nrtsp\t\t554/tcp\t\t\t# Real Time Stream Control Protocol\nrtsp\t\t554/udp\nnqs\t\t607/tcp\t\t\t\t# Network Queuing system\nasf-rmcp\t623/udp\t\t# ASF Remote Management and Control Protocol\nqmqp\t\t628/tcp\nipp\t\t631/tcp\t\t\t\t# Internet Printing Protocol\nldp\t\t646/tcp\t\t\t\t# Label Distribution Protocol\nldp\t\t646/udp\n#\n# UNIX specific services\n#\nexec\t\t512/tcp\nbiff\t\t512/udp\t\tcomsat\nlogin\t\t513/tcp\nwho\t\t513/udp\t\twhod\nshell\t\t514/tcp\t\tcmd syslog\t# no passwords used\nsyslog\t\t514/udp\nprinter\t\t515/tcp\t\tspooler\t\t# line printer spooler\ntalk\t\t517/udp\nntalk\t\t518/udp\nroute\t\t520/udp\t\trouter routed\t# RIP\ngdomap\t\t538/tcp\t\t\t\t# GNUstep distributed objects\ngdomap\t\t538/udp\nuucp\t\t540/tcp\t\tuucpd\t\t# uucp daemon\nklogin\t\t543/tcp\t\t\t\t# Kerberized `rlogin' (v5)\nkshell\t\t544/tcp\t\tkrcmd\t\t# Kerberized `rsh' (v5)\ndhcpv6-client\t546/udp\ndhcpv6-server\t547/udp\nafpovertcp\t548/tcp\t\t\t\t# AFP over TCP\nnntps\t\t563/tcp\t\tsnntp\t\t# NNTP over SSL\nsubmission\t587/tcp\t\t\t\t# Submission [RFC4409]\nldaps\t\t636/tcp\t\t\t\t# LDAP over SSL\nldaps\t\t636/udp\ntinc\t\t655/tcp\t\t\t\t# tinc control port\ntinc\t\t655/udp\nsilc\t\t706/tcp\nkerberos-adm\t749/tcp\t\t\t\t# Kerberos `kadmin' (v5)\n#\ndomain-s\t853/tcp\t\t\t\t# DNS over TLS [RFC7858]\ndomain-s\t853/udp\t\t\t\t# DNS over DTLS [RFC8094]\nrsync\t\t873/tcp\nftps-data\t989/tcp\t\t\t\t# FTP over SSL (data)\nftps\t\t990/tcp\ntelnets\t\t992/tcp\t\t\t\t# Telnet over SSL\nimaps\t\t993/tcp\t\t\t\t# IMAP over SSL\npop3s\t\t995/tcp\t\t\t\t# POP-3 over SSL\n#\n# From ``Assigned Numbers'':\n#\n#> The Registered Ports are not controlled by the IANA and on most systems\n#> can be used by ordinary user processes or programs executed by ordinary\n#> users.\n#\n#> Ports are used in the TCP [45,106] to name the ends of logical\n#> connections which carry long term conversations. For the purpose of\n#> providing services to unknown callers, a service contact port is\n#> defined. This list specifies the port used by the server process as its\n#> contact port. While the IANA can not control uses of these ports it\n#> does register or list uses of these ports as a convienence to the\n#> community.\n#\nsocks\t\t1080/tcp\t\t\t# socks proxy server\nproofd\t\t1093/tcp\nrootd\t\t1094/tcp\nopenvpn\t\t1194/tcp\nopenvpn\t\t1194/udp\nrmiregistry\t1099/tcp\t\t\t# Java RMI Registry\nlotusnote\t1352/tcp\tlotusnotes\t# Lotus Note\nms-sql-s\t1433/tcp\t\t\t# Microsoft SQL Server\nms-sql-m\t1434/udp\t\t\t# Microsoft SQL Monitor\ningreslock\t1524/tcp\ndatametrics\t1645/tcp\told-radius\ndatametrics\t1645/udp\told-radius\nsa-msg-port\t1646/tcp\told-radacct\nsa-msg-port\t1646/udp\told-radacct\nkermit\t\t1649/tcp\ngroupwise\t1677/tcp\nl2f\t\t1701/udp\tl2tp\nradius\t\t1812/tcp\nradius\t\t1812/udp\nradius-acct\t1813/tcp\tradacct\t\t# Radius Accounting\nradius-acct\t1813/udp\tradacct\ncisco-sccp\t2000/tcp\t\t\t# Cisco SCCP\nnfs\t\t2049/tcp\t\t\t# Network File System\nnfs\t\t2049/udp\t\t\t# Network File System\ngnunet\t\t2086/tcp\ngnunet\t\t2086/udp\nrtcm-sc104\t2101/tcp\t\t\t# RTCM SC-104 IANA 1/29/99\nrtcm-sc104\t2101/udp\ngsigatekeeper\t2119/tcp\ngris\t\t2135/tcp\t\t# Grid Resource Information Server\ncvspserver\t2401/tcp\t\t\t# CVS client/server operations\nvenus\t\t2430/tcp\t\t\t# codacon port\nvenus\t\t2430/udp\t\t\t# Venus callback/wbc interface\nvenus-se\t2431/tcp\t\t\t# tcp side effects\nvenus-se\t2431/udp\t\t\t# udp sftp side effect\ncodasrv\t\t2432/tcp\t\t\t# not used\ncodasrv\t\t2432/udp\t\t\t# server port\ncodasrv-se\t2433/tcp\t\t\t# tcp side effects\ncodasrv-se\t2433/udp\t\t\t# udp sftp side effect\nmon\t\t2583/tcp\t\t\t# MON traps\nmon\t\t2583/udp\ndict\t\t2628/tcp\t\t\t# Dictionary server\nf5-globalsite\t2792/tcp\ngsiftp\t\t2811/tcp\ngpsd\t\t2947/tcp\ngds-db\t\t3050/tcp\tgds_db\t\t# InterBase server\nicpv2\t\t3130/udp\ticp\t\t# Internet Cache Protocol\nisns\t\t3205/tcp\t\t\t# iSNS Server Port\nisns\t\t3205/udp\t\t\t# iSNS Server Port\niscsi-target\t3260/tcp\nmysql\t\t3306/tcp\nms-wbt-server\t3389/tcp\nnut\t\t3493/tcp\t\t\t# Network UPS Tools\nnut\t\t3493/udp\ndistcc\t\t3632/tcp\t\t\t# distributed compiler\ndaap\t\t3689/tcp\t\t\t# Digital Audio Access Protocol\nsvn\t\t3690/tcp\tsubversion\t# Subversion protocol\nsuucp\t\t4031/tcp\t\t\t# UUCP over SSL\nsysrqd\t\t4094/tcp\t\t\t# sysrq daemon\nsieve\t\t4190/tcp\t\t\t# ManageSieve Protocol\nepmd\t\t4369/tcp\t\t\t# Erlang Port Mapper Daemon\nremctl\t\t4373/tcp\t\t# Remote Authenticated Command Service\nf5-iquery\t4353/tcp\t\t\t# F5 iQuery\nntske\t\t4460/tcp\t# Network Time Security Key Establishment\nipsec-nat-t\t4500/udp\t\t\t# IPsec NAT-Traversal [RFC3947]\niax\t\t4569/udp\t\t\t# Inter-Asterisk eXchange\nmtn\t\t4691/tcp\t\t\t# monotone Netsync Protocol\nradmin-port\t4899/tcp\t\t\t# RAdmin Port\nsip\t\t5060/tcp\t\t\t# Session Initiation Protocol\nsip\t\t5060/udp\nsip-tls\t\t5061/tcp\nsip-tls\t\t5061/udp\nxmpp-client\t5222/tcp\tjabber-client\t# Jabber Client Connection\nxmpp-server\t5269/tcp\tjabber-server\t# Jabber Server Connection\ncfengine\t5308/tcp\nmdns\t\t5353/udp\t\t\t# Multicast DNS\npostgresql\t5432/tcp\tpostgres\t# PostgreSQL Database\nfreeciv\t\t5556/tcp\trptp\t\t# Freeciv gameplay\namqps\t\t5671/tcp\t\t\t# AMQP protocol over TLS/SSL\namqp\t\t5672/tcp\namqp\t\t5672/sctp\nx11\t\t6000/tcp\tx11-0\t\t# X Window System\nx11-1\t\t6001/tcp\nx11-2\t\t6002/tcp\nx11-3\t\t6003/tcp\nx11-4\t\t6004/tcp\nx11-5\t\t6005/tcp\nx11-6\t\t6006/tcp\nx11-7\t\t6007/tcp\ngnutella-svc\t6346/tcp\t\t\t# gnutella\ngnutella-svc\t6346/udp\ngnutella-rtr\t6347/tcp\t\t\t# gnutella\ngnutella-rtr\t6347/udp\nredis\t\t6379/tcp\nsge-qmaster\t6444/tcp\tsge_qmaster\t# Grid Engine Qmaster Service\nsge-execd\t6445/tcp\tsge_execd\t# Grid Engine Execution Service\nmysql-proxy\t6446/tcp\t\t\t# MySQL Proxy\nbabel\t\t6696/udp\t\t\t# Babel Routing Protocol\nircs-u\t\t6697/tcp\t\t# Internet Relay Chat via TLS/SSL\nbbs\t\t7000/tcp\nafs3-fileserver 7000/udp\nafs3-callback\t7001/udp\t\t\t# callbacks to cache managers\nafs3-prserver\t7002/udp\t\t\t# users & groups database\nafs3-vlserver\t7003/udp\t\t\t# volume location database\nafs3-kaserver\t7004/udp\t\t\t# AFS/Kerberos authentication\nafs3-volser\t7005/udp\t\t\t# volume managment server\nafs3-bos\t7007/udp\t\t\t# basic overseer process\nafs3-update\t7008/udp\t\t\t# server-to-server updater\nafs3-rmtsys\t7009/udp\t\t\t# remote cache manager service\nfont-service\t7100/tcp\txfs\t\t# X Font Service\nhttp-alt\t8080/tcp\twebcache\t# WWW caching service\npuppet\t\t8140/tcp\t\t\t# The Puppet master service\nbacula-dir\t9101/tcp\t\t\t# Bacula Director\nbacula-fd\t9102/tcp\t\t\t# Bacula File Daemon\nbacula-sd\t9103/tcp\t\t\t# Bacula Storage Daemon\nxmms2\t\t9667/tcp\t# Cross-platform Music Multiplexing System\nnbd\t\t10809/tcp\t\t\t# Linux Network Block Device\nzabbix-agent\t10050/tcp\t\t\t# Zabbix Agent\nzabbix-trapper\t10051/tcp\t\t\t# Zabbix Trapper\namanda\t\t10080/tcp\t\t\t# amanda backup services\ndicom\t\t11112/tcp\nhkp\t\t11371/tcp\t\t\t# OpenPGP HTTP Keyserver\ndb-lsp\t\t17500/tcp\t\t\t# Dropbox LanSync Protocol\ndcap\t\t22125/tcp\t\t\t# dCache Access Protocol\ngsidcap\t\t22128/tcp\t\t\t# GSI dCache Access Protocol\nwnn6\t\t22273/tcp\t\t\t# wnn6\n\n#\n# Datagram Delivery Protocol services\n#\nrtmp\t\t1/ddp\t\t\t# Routing Table Maintenance Protocol\nnbp\t\t2/ddp\t\t\t# Name Binding Protocol\necho\t\t4/ddp\t\t\t# AppleTalk Echo Protocol\nzip\t\t6/ddp\t\t\t# Zone Information Protocol\n\n#=========================================================================\n# The remaining port numbers are not as allocated by IANA.\n#=========================================================================\n\n# Kerberos (Project Athena/MIT) services\nkerberos4\t750/udp\t\tkerberos-iv kdc\t# Kerberos (server)\nkerberos4\t750/tcp\t\tkerberos-iv kdc\nkerberos-master\t751/udp\t\tkerberos_master\t# Kerberos authentication\nkerberos-master\t751/tcp\npasswd-server\t752/udp\t\tpasswd_server\t# Kerberos passwd server\nkrb-prop\t754/tcp\t\tkrb_prop krb5_prop hprop # Kerberos slave propagation\nzephyr-srv\t2102/udp\t\t\t# Zephyr server\nzephyr-clt\t2103/udp\t\t\t# Zephyr serv-hm connection\nzephyr-hm\t2104/udp\t\t\t# Zephyr hostmanager\niprop\t\t2121/tcp\t\t\t# incremental propagation\nsupfilesrv\t871/tcp\t\t\t# Software Upgrade Protocol server\nsupfiledbg\t1127/tcp\t\t# Software Upgrade Protocol debugging\n\n#\n# Services added for the Debian GNU/Linux distribution\n#\npoppassd\t106/tcp\t\t\t\t# Eudora\nmoira-db\t775/tcp\t\tmoira_db\t# Moira database\nmoira-update\t777/tcp\t\tmoira_update\t# Moira update protocol\nmoira-ureg\t779/udp\t\tmoira_ureg\t# Moira user registration\nspamd\t\t783/tcp\t\t\t\t# spamassassin daemon\nskkserv\t\t1178/tcp\t\t\t# skk jisho server port\npredict\t\t1210/udp\t\t\t# predict -- satellite tracking\nrmtcfg\t\t1236/tcp\t\t\t# Gracilis Packeten remote config server\nxtel\t\t1313/tcp\t\t\t# french minitel\nxtelw\t\t1314/tcp\t\t\t# french minitel\nzebrasrv\t2600/tcp\t\t\t# zebra service\nzebra\t\t2601/tcp\t\t\t# zebra vty\nripd\t\t2602/tcp\t\t\t# ripd vty (zebra)\nripngd\t\t2603/tcp\t\t\t# ripngd vty (zebra)\nospfd\t\t2604/tcp\t\t\t# ospfd vty (zebra)\nbgpd\t\t2605/tcp\t\t\t# bgpd vty (zebra)\nospf6d\t\t2606/tcp\t\t\t# ospf6d vty (zebra)\nospfapi\t\t2607/tcp\t\t\t# OSPF-API\nisisd\t\t2608/tcp\t\t\t# ISISd vty (zebra)\nfax\t\t4557/tcp\t\t\t# FAX transmission service (old)\nhylafax\t\t4559/tcp\t\t\t# HylaFAX client-server protocol (new)\nmunin\t\t4949/tcp\tlrrd\t\t# Munin\nrplay\t\t5555/udp\t\t\t# RPlay audio service\nnrpe\t\t5666/tcp\t\t\t# Nagios Remote Plugin Executor\nnsca\t\t5667/tcp\t\t\t# Nagios Agent - NSCA\ncanna\t\t5680/tcp\t\t\t# cannaserver\nsyslog-tls\t6514/tcp\t\t\t# Syslog over TLS [RFC5425]\nsane-port\t6566/tcp\tsane saned\t# SANE network scanner daemon\nircd\t\t6667/tcp\t\t\t# Internet Relay Chat\nzope-ftp\t8021/tcp\t\t\t# zope management by ftp\ntproxy\t\t8081/tcp\t\t\t# Transparent Proxy\nomniorb\t\t8088/tcp\t\t\t# OmniORB\nclc-build-daemon 8990/tcp\t\t\t# Common lisp build daemon\nxinetd\t\t9098/tcp\ngit\t\t9418/tcp\t\t\t# Git Version Control System\nzope\t\t9673/tcp\t\t\t# zope server\nwebmin\t\t10000/tcp\nkamanda\t\t10081/tcp\t\t\t# amanda backup services (Kerberos)\namandaidx\t10082/tcp\t\t\t# amanda backup services\namidxtape\t10083/tcp\t\t\t# amanda backup services\nsgi-cmsd\t17001/udp\t\t# Cluster membership services daemon\nsgi-crsd\t17002/udp\nsgi-gcd\t\t17003/udp\t\t\t# SGI Group membership daemon\nsgi-cad\t\t17004/tcp\t\t\t# Cluster Admin daemon\nbinkp\t\t24554/tcp\t\t\t# binkp fidonet protocol\nasp\t\t27374/tcp\t\t\t# Address Search Protocol\nasp\t\t27374/udp\ncsync2\t\t30865/tcp\t\t\t# cluster synchronization tool\ndircproxy\t57000/tcp\t\t\t# Detachable IRC Proxy\ntfido\t\t60177/tcp\t\t\t# fidonet EMSI over telnet\nfido\t\t60179/tcp\t\t\t# fidonet EMSI over TCP\n\n# Local services\n" + }, { "path": "/etc/ssl", "type": "directory", diff --git a/crates/vfs/src/engine/engines/object.rs b/crates/vfs/src/engine/engines/object.rs index e03a98f608..dc01659b63 100644 --- a/crates/vfs/src/engine/engines/object.rs +++ b/crates/vfs/src/engine/engines/object.rs @@ -139,7 +139,7 @@ impl VirtualFileSystem for ObjectFs { } result.push(Dentry { name, - ino: 0, + ino: object_ino(&entry.name), kind: if entry.is_prefix { InodeType::Directory } else { @@ -200,13 +200,13 @@ impl VirtualFileSystem for ObjectFs { async fn stat(&self, path: &str) -> VfsResult { let key = self.key_for(path)?; if let Some(meta) = self.backend.head(&key).await? { - return Ok(object_stat(meta)); + return Ok(object_stat(meta, &key)); } let entries = self.backend.list(&self.dir_prefix_for(path)?).await?; if entries.is_empty() { return Err(VfsError::enoent(path)); } - Ok(object_stat(self.dir_meta())) + Ok(object_stat(self.dir_meta(), &key)) } async fn lstat(&self, path: &str) -> VfsResult { @@ -337,7 +337,20 @@ impl VirtualFileSystem for ObjectFs { } } -fn object_stat(meta: ObjectMeta) -> VirtualStat { +fn object_ino(key: &str) -> u64 { + let mut hash = 0xcbf2_9ce4_8422_2325u64; + for byte in key.trim_end_matches('/').as_bytes() { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(0x0000_0100_0000_01b3); + } + if hash == 0 { + 1 + } else { + hash + } +} + +fn object_stat(meta: ObjectMeta, key: &str) -> VirtualStat { let type_bits = match meta.kind { InodeType::File => crate::engine::types::S_IFREG, InodeType::Directory => crate::engine::types::S_IFDIR, @@ -353,7 +366,7 @@ fn object_stat(meta: ObjectMeta) -> VirtualStat { mtime: meta.mtime, ctime: meta.mtime, birthtime: meta.mtime, - ino: 0, + ino: object_ino(key), nlink: 1, uid: meta.uid, gid: meta.gid, diff --git a/crates/vfs/src/posix/mount_table.rs b/crates/vfs/src/posix/mount_table.rs index aac856e4cf..c3c0a0ee5a 100644 --- a/crates/vfs/src/posix/mount_table.rs +++ b/crates/vfs/src/posix/mount_table.rs @@ -86,6 +86,20 @@ pub trait MountedFileSystem: Any { fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()>; fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()>; fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()>; + fn chown_spec( + &mut self, + path: &str, + uid: u32, + gid: u32, + follow_symlinks: bool, + ) -> VfsResult<()> { + if !follow_symlinks { + return Err(VfsError::unsupported(format!( + "lchown is not supported for mount path '{path}'" + ))); + } + self.chown(path, uid, gid) + } fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()>; fn utimes_spec( &mut self, @@ -280,6 +294,16 @@ where VirtualFileSystem::chown(&mut self.inner, path, uid, gid) } + fn chown_spec( + &mut self, + path: &str, + uid: u32, + gid: u32, + follow_symlinks: bool, + ) -> VfsResult<()> { + VirtualFileSystem::chown_spec(&mut self.inner, path, uid, gid, follow_symlinks) + } + fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> { VirtualFileSystem::utimes(&mut self.inner, path, atime_ms, mtime_ms) } @@ -391,6 +415,16 @@ where (**self).chown(path, uid, gid) } + fn chown_spec( + &mut self, + path: &str, + uid: u32, + gid: u32, + follow_symlinks: bool, + ) -> VfsResult<()> { + (**self).chown_spec(path, uid, gid, follow_symlinks) + } + fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> { (**self).utimes(path, atime_ms, mtime_ms) } @@ -546,6 +580,19 @@ where )) } + fn chown_spec( + &mut self, + path: &str, + _uid: u32, + _gid: u32, + _follow_symlinks: bool, + ) -> VfsResult<()> { + Err(VfsError::new( + "EROFS", + format!("read-only filesystem: {path}"), + )) + } + fn utimes(&mut self, path: &str, _atime_ms: u64, _mtime_ms: u64) -> VfsResult<()> { Err(VfsError::new( "EROFS", @@ -1282,10 +1329,24 @@ impl VirtualFileSystem for MountTable { } fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> { - let (index, relative_path) = self.resolve_index(path)?; + self.chown_spec(path, uid, gid, true) + } + + fn chown_spec( + &mut self, + path: &str, + uid: u32, + gid: u32, + follow_symlinks: bool, + ) -> VfsResult<()> { + let (index, relative_path) = if follow_symlinks { + self.resolve_index(path)? + } else { + self.resolve_link_leaf_index(path)? + }; self.mounts[index] .filesystem - .chown(&relative_path, uid, gid) + .chown_spec(&relative_path, uid, gid, follow_symlinks) } fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> { diff --git a/crates/vfs/src/posix/overlay_fs.rs b/crates/vfs/src/posix/overlay_fs.rs index 25aa86b9e3..f2cead1a3f 100644 --- a/crates/vfs/src/posix/overlay_fs.rs +++ b/crates/vfs/src/posix/overlay_fs.rs @@ -1563,8 +1563,13 @@ impl VirtualFileSystem for OverlayFileSystem { if self.is_whited_out(path) { return Err(Self::entry_not_found(path)); } - let lower_exists = self.find_lower_by_exists(path).is_some(); - let upper_exists = self.exists_in_upper(path); + // POSIX unlink(2) removes the directory entry itself and never follows + // a symlink leaf, so existence must be checked with lstat semantics. + // `exists()` resolves symlinks, which made dangling symlinks (for + // example git's `.git/tXXXXXX` symlink-support probe) unremovable: + // the entry stayed listed by readdir while unlink reported ENOENT. + let lower_exists = self.find_lower_by_entry(path).is_some(); + let upper_exists = self.has_entry_in_upper(path); if !lower_exists && !upper_exists { return Err(Self::entry_not_found(path)); } @@ -1661,6 +1666,8 @@ impl VirtualFileSystem for OverlayFileSystem { let mut snapshot_entries = Vec::new(); self.collect_snapshot_entries(&old_normalized, &mut snapshot_entries)?; + self.clear_path_metadata(&resolved_new_normalized)?; + self.clear_subtree_metadata(&resolved_new_normalized)?; if let Ok(destination_stat) = self.merged_lstat(&resolved_new_normalized) { if destination_stat.is_directory && !destination_stat.is_symbolic_link @@ -1678,7 +1685,6 @@ impl VirtualFileSystem for OverlayFileSystem { .remove_file(&resolved_new_normalized)?; } } - self.clear_subtree_metadata(&resolved_new_normalized)?; } self.stage_snapshot_entries_in_upper(&snapshot_entries)?; @@ -1780,6 +1786,16 @@ impl VirtualFileSystem for OverlayFileSystem { } fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()> { + self.chown_spec(path, uid, gid, true) + } + + fn chown_spec( + &mut self, + path: &str, + uid: u32, + gid: u32, + follow_symlinks: bool, + ) -> VfsResult<()> { if self.touches_internal_metadata(path) { return Err(VfsError::permission_denied("chown", path)); } @@ -1789,7 +1805,8 @@ impl VirtualFileSystem for OverlayFileSystem { if !self.exists_in_upper(path) { self.copy_up_path(path)?; } - self.writable_upper(path)?.chown(path, uid, gid) + self.writable_upper(path)? + .chown_spec(path, uid, gid, follow_symlinks) } fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> { @@ -1985,6 +2002,84 @@ mod tests { assert_error_code(overlay.read_dir("/a"), "ENOENT"); } + #[test] + fn rename_clears_destination_whiteout() { + let lower = MemoryFileSystem::new(); + let mut overlay = OverlayFileSystem::new(vec![lower], OverlayMode::Ephemeral); + overlay + .write_file("/archive.zip", b"old".to_vec()) + .expect("create old archive"); + overlay + .remove_file("/archive.zip") + .expect("whiteout old archive"); + overlay + .write_file("/workspace-temp", b"new".to_vec()) + .expect("create replacement"); + + overlay + .rename("/workspace-temp", "/archive.zip") + .expect("rename replacement over whiteout"); + + assert_eq!( + overlay + .read_file("/archive.zip") + .expect("read renamed file"), + b"new" + ); + assert!(!overlay.exists("/workspace-temp")); + } + + #[test] + fn remove_file_unlinks_dangling_symlink() { + // git's symlink-support probe creates `.git/tXXXXXX -> testing` (a + // dangling symlink), lstats it, and unlinks it. unlink(2) must remove + // the link itself without resolving it. + let lower = MemoryFileSystem::new(); + let mut overlay = OverlayFileSystem::new(vec![lower], OverlayMode::Ephemeral); + overlay.mkdir("/repo", true).expect("create directory"); + overlay + .symlink("testing", "/repo/probe") + .expect("create dangling symlink"); + assert!( + overlay + .lstat("/repo/probe") + .expect("lstat dangling symlink") + .is_symbolic_link + ); + + overlay + .remove_file("/repo/probe") + .expect("unlink dangling symlink"); + + assert!(overlay.lstat("/repo/probe").is_err()); + assert_eq!( + overlay.read_dir("/repo").expect("read emptied directory"), + Vec::::new() + ); + overlay + .remove_dir("/repo") + .expect("rmdir emptied directory"); + } + + #[test] + fn remove_file_unlinks_dangling_symlink_from_lower_layer() { + let mut lower = MemoryFileSystem::new(); + lower.mkdir("/repo", true).expect("create lower directory"); + lower + .symlink("missing-target", "/repo/probe") + .expect("seed lower dangling symlink"); + + let mut overlay = OverlayFileSystem::new(vec![lower], OverlayMode::Ephemeral); + overlay + .remove_file("/repo/probe") + .expect("whiteout lower dangling symlink"); + + assert!(overlay.lstat("/repo/probe").is_err()); + overlay + .remove_dir("/repo") + .expect("rmdir merged-empty directory"); + } + #[test] fn remove_dir_still_rejects_visible_children() { let mut lower = MemoryFileSystem::new(); diff --git a/crates/vfs/src/posix/root_fs.rs b/crates/vfs/src/posix/root_fs.rs index bcfa06b318..f6bbdcb7b2 100644 --- a/crates/vfs/src/posix/root_fs.rs +++ b/crates/vfs/src/posix/root_fs.rs @@ -387,6 +387,16 @@ impl VirtualFileSystem for RootFileSystem { self.overlay.chown(path, uid, gid) } + fn chown_spec( + &mut self, + path: &str, + uid: u32, + gid: u32, + follow_symlinks: bool, + ) -> VfsResult<()> { + self.overlay.chown_spec(path, uid, gid, follow_symlinks) + } + fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> { self.overlay.utimes(path, atime_ms, mtime_ms) } diff --git a/crates/vfs/src/posix/vfs.rs b/crates/vfs/src/posix/vfs.rs index dbad5203c2..ecff2e2c24 100644 --- a/crates/vfs/src/posix/vfs.rs +++ b/crates/vfs/src/posix/vfs.rs @@ -291,6 +291,20 @@ pub trait VirtualFileSystem { fn link(&mut self, old_path: &str, new_path: &str) -> VfsResult<()>; fn chmod(&mut self, path: &str, mode: u32) -> VfsResult<()>; fn chown(&mut self, path: &str, uid: u32, gid: u32) -> VfsResult<()>; + fn chown_spec( + &mut self, + path: &str, + uid: u32, + gid: u32, + follow_symlinks: bool, + ) -> VfsResult<()> { + if !follow_symlinks { + return Err(VfsError::unsupported(format!( + "lchown is not supported for '{path}'" + ))); + } + self.chown(path, uid, gid) + } fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()>; fn utimes_spec( &mut self, @@ -1294,6 +1308,20 @@ impl VirtualFileSystem for MemoryFileSystem { Ok(()) } + fn chown_spec( + &mut self, + path: &str, + uid: u32, + gid: u32, + follow_symlinks: bool, + ) -> VfsResult<()> { + let inode = self.inode_mut_for_existing_path(path, "chown", follow_symlinks)?; + inode.metadata.uid = uid; + inode.metadata.gid = gid; + inode.metadata.ctime_ms = now_ms(); + Ok(()) + } + fn utimes(&mut self, path: &str, atime_ms: u64, mtime_ms: u64) -> VfsResult<()> { let inode = self.inode_mut_for_existing_path(path, "utimes", true)?; inode.metadata.atime_ms = atime_ms; @@ -1414,14 +1442,6 @@ pub fn validate_path(path: &str) -> VfsResult<()> { if path.as_bytes().contains(&0) { return Err(VfsError::invalid_input("path contains NUL byte")); } - if let Some(control) = path - .bytes() - .find(|byte| byte.is_ascii_control() && *byte != b'\0') - { - return Err(VfsError::invalid_input(format!( - "path contains control character byte 0x{control:02x}" - ))); - } let normalized = normalize_path(path); if normalized.len() > MAX_PATH_LENGTH { return Err(VfsError::path_too_long(path)); diff --git a/crates/vfs/tests/conformance.rs b/crates/vfs/tests/conformance.rs index abf95c12a5..3450bba16a 100644 --- a/crates/vfs/tests/conformance.rs +++ b/crates/vfs/tests/conformance.rs @@ -182,6 +182,28 @@ async fn object_fs_maps_files_to_native_objects() { assert_eq!(fs.read_file("/dir/file.txt").await.unwrap(), b"hello"); assert_eq!(fs.read_dir("/dir").await.unwrap(), vec!["file.txt"]); assert_eq!(fs.pread("/dir/file.txt", 1, 3).await.unwrap(), b"ell"); + + let file_entry = fs + .read_dir_with_types("/dir") + .await + .unwrap() + .into_iter() + .find(|entry| entry.name == "file.txt") + .unwrap(); + let file_stat = fs.stat("/dir/file.txt").await.unwrap(); + assert_ne!(file_entry.ino, 0); + assert_eq!(file_entry.ino, file_stat.ino); + + let directory_entry = fs + .read_dir_with_types("/") + .await + .unwrap() + .into_iter() + .find(|entry| entry.name == "dir") + .unwrap(); + let directory_stat = fs.stat("/dir").await.unwrap(); + assert_ne!(directory_entry.ino, 0); + assert_eq!(directory_entry.ino, directory_stat.ino); } #[tokio::test] diff --git a/crates/vfs/tests/posix_root_fs.rs b/crates/vfs/tests/posix_root_fs.rs index 1345002ab8..a0740047d4 100644 --- a/crates/vfs/tests/posix_root_fs.rs +++ b/crates/vfs/tests/posix_root_fs.rs @@ -293,6 +293,16 @@ fn root_filesystem_uses_bundled_base_and_round_trips_snapshots() { assert!(os_release.is_symbolic_link); assert_eq!(os_release.uid, 0); assert_eq!(os_release.gid, 0); + let services = root + .read_file("/etc/services") + .expect("read bundled Linux services database"); + assert!(services + .windows(b"smtp\t\t25/tcp\t\tmail".len()) + .any(|window| window == b"smtp\t\t25/tcp\t\tmail")); + assert_eq!( + root.read_file("/etc/hosts").expect("read bundled hosts file"), + b"127.0.0.1 localhost localhost.localdomain\n::1 localhost localhost.localdomain ip6-localhost ip6-loopback\nfe00:: ip6-localnet\nff00:: ip6-mcastprefix\nff02::1 ip6-allnodes\nff02::2 ip6-allrouters\n127.0.1.1 secure-exec\n" + ); root.mkdir("/workspace", true).expect("create workspace"); root.write_file("/workspace/run.sh", b"echo hi".to_vec()) diff --git a/crates/vfs/tests/posix_vfs.rs b/crates/vfs/tests/posix_vfs.rs index d5a4319513..d5c84546eb 100644 --- a/crates/vfs/tests/posix_vfs.rs +++ b/crates/vfs/tests/posix_vfs.rs @@ -27,14 +27,7 @@ fn generated_invalid_path(seed: u32) -> String { path.push('/'); } path.push(char::from(b'a' + ((seed + segment) % 26) as u8)); - let invalid_byte = if seed.is_multiple_of(2) { - 0 - } else if seed.is_multiple_of(5) { - 0x7f - } else { - 1 + ((seed + segment) % 31) as u8 - }; - path.push(char::from(invalid_byte)); + path.push('\0'); path.push(char::from(b'a' + (((seed / 3) + segment) % 26) as u8)); } path @@ -179,7 +172,7 @@ fn symlink_loops_fail_closed() { } #[test] -fn path_validation_rejects_nul_and_control_bytes_without_mutating_filesystem() { +fn path_validation_rejects_nul_without_mutating_filesystem() { let mut baseline = MemoryFileSystem::new(); baseline .write_file("/safe/file.txt", "safe contents") @@ -194,7 +187,7 @@ fn path_validation_rejects_nul_and_control_bytes_without_mutating_filesystem() { .create_dir("/safe/empty") .expect("seed removable dir"); - let invalid_paths = ["/bad\0path", "/bad\npath", "/bad\x7fpath"]; + let invalid_paths = ["/bad\0path"]; for invalid_path in invalid_paths { assert_invalid_path_keeps_snapshot(&baseline, invalid_path, |fs, path| fs.read_file(path)); @@ -260,16 +253,27 @@ fn path_validation_rejects_nul_and_control_bytes_without_mutating_filesystem() { fn validate_path_rejects_generated_invalid_inputs() { for seed in 0..1_000u32 { let invalid_path = generated_invalid_path(seed); - assert!( - invalid_path - .bytes() - .any(|byte| byte == 0 || byte.is_ascii_control()), - "generated path should contain at least one prohibited byte" - ); + assert!(invalid_path.contains('\0')); assert_error_code(validate_path(&invalid_path), "EINVAL"); } } +#[test] +fn path_validation_and_storage_accept_non_nul_ascii_control_bytes() { + let mut filesystem = MemoryFileSystem::new(); + + for path in ["/line\nbreak", "/unit\x1fseparator", "/delete\x7fbyte"] { + validate_path(path).expect("Linux permits non-NUL control bytes in pathnames"); + filesystem + .write_file(path, path.as_bytes()) + .expect("store control-byte pathname"); + assert_eq!( + filesystem.read_file(path).expect("read control-byte path"), + path.as_bytes() + ); + } +} + #[test] fn intermediate_symlink_components_are_resolved_for_reads_writes_and_stats() { let mut filesystem = MemoryFileSystem::new(); diff --git a/crates/vm-config/src/lib.rs b/crates/vm-config/src/lib.rs index 48246e71e7..283597d15a 100644 --- a/crates/vm-config/src/lib.rs +++ b/crates/vm-config/src/lib.rs @@ -546,6 +546,9 @@ pub struct VmLimitsConfig { #[serde(default, skip_serializing_if = "Option::is_none")] #[ts(optional)] pub wasm: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + #[ts(optional)] + pub process: Option, } impl VmLimitsConfig { @@ -672,6 +675,14 @@ limits_struct!(WasmLimitsConfig { runner_heap_limit_mb, }); +limits_struct!(ProcessLimitsConfig { + max_spawn_file_actions, + max_spawn_file_action_bytes, + pending_stdin_bytes, + pending_event_count, + pending_event_bytes, +}); + #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase", deny_unknown_fields)] #[ts(export, export_to = "../../../packages/core/src/generated/")] diff --git a/docs-internal/networking-parity-spec.md b/docs-internal/networking-parity-spec.md new file mode 100644 index 0000000000..de75e5ab99 --- /dev/null +++ b/docs-internal/networking-parity-spec.md @@ -0,0 +1,227 @@ +# Networking Parity Spec — curl / wget / git full TLS + network support + +Status: implemented · Owner: registry/runtime · Last updated: 2026-07-14 + +This document preserves the design baseline and decisions that drove the +networking stack. Sections describing missing TLS or future work refer to the +pre-implementation state. For the shipped architecture, see +[`TLS & SSL`](../website/src/content/docs/docs/architecture/tls-ssl.mdx). + +## Goal + +`curl`, `wget`, and `git` must have **full networking with real TLS/SSL, using +native Linux semantics** — not host-brokered shims. Concretely: + +- `curl https://…` and `wget https://…` verify certificates against a real CA + bundle, honor `--cacert`/`--ca-certificate`, fail with the **real exit codes** + (curl exit 60 = cert verify failure), and support `--compressed` (gzip, brotli, + zstd). +- `git clone/fetch/push` over **HTTPS smart-HTTP** works against GitHub/GitLab. +- Certificate trust comes from a **CA bundle inside the VM** (`/etc/ssl/certs/…`), + the way Debian's `ca-certificates` package + apt/curl/openssl/python all resolve + it — not from the host machine's trust store. + +## Baseline before implementation (verified) + +Sockets are **already real** and are not the problem. The patched wasi-libc +sysroot implements `socket()/connect()/getaddrinfo()/send()/recv()` over +`host_net` WASM imports (`toolchain/std-patches/wasi-libc/0008-sockets.patch`, +`0023-host-net-read-write-sockets.patch`; Rust mirror `toolchain/crates/wasi-ext`). +The runner forwards them to the sidecar socket table (`crates/execution/assets/ +runners/wasm-runner.mjs`). So curl/wget/git already do their own DNS, TCP and HTTP +byte-for-byte. **Only TLS and decompression are shimmed or missing.** + +| Tool | Networking today | TLS today | Gap | +|---|---|---|---| +| **curl** | real (8.11.1) | host-brokered `wasi_tls.c` → `net_tls_connect` → sidecar rustls + **host** cert store | no real verify, `--cacert`/exit-60/`-v` chain all wrong; no gzip/brotli/zstd | +| **wget** | real (1.24.5) | **none** (`--without-ssl`) — HTTP only | no HTTPS at all; no gzip | +| **git** | real, local-only (2.55.0, `NO_CURL`) | **none** | `git-remote-https` is a dead symlink→git binary; no HTTPS clone/fetch/push | + +The current TLS shim (`software/curl/native/c/overlay/lib/vtls/wasi_tls.c`) does +**no handshake and no verification in-guest**: it hands the TCP fd to the host +(`net_tls_connect(fd, host, flags)`), which terminates TLS with rustls + +`rustls_native_certs::load_native_certs()` — **the host machine's** trust store. +`--cacert`/`--capath`/`CURL_CA_BUNDLE`/`SSL_CERT_FILE` are silently ignored; any +TLS error returns curl exit 35 instead of 60; `curl -v` cannot print the chain. + +## The decision: in-guest TLS (mbedTLS) + a VM CA bundle + +Two directions were researched. **We choose in-guest TLS** because it is the only +one that delivers the acceptance criterion (native Linux semantics + a CA bundle +that mimics apt). + +### ✅ Chosen — in-guest mbedTLS 3.6 LTS + `/etc/ssl/certs/ca-certificates.crt` + +- **mbedTLS** is pure portable C99, zero platform deps, TLS 1.3, and curl has a + first-class maintained `USE_MBEDTLS` backend in 8.11.1. Entropy = `getentropy()` + (already proven in `wasi_tls.c`); clocks/time work; build single-threaded. +- Verification happens **in curl's own code path**: `--cacert` → + `mbedtls_x509_crt_parse_file` via the VFS, `-k` → generic `verifypeer`, verify + failure → `CURLE_PEER_FAILED_VERIFICATION` = **exit 60** with the real message, + `curl -v` prints version/cipher/cert. Exactly Linux curl-with-mbedTLS. +- The sidecar becomes a **dumb ciphertext pipe** — strictly better for the trust + model (the untrusted executor no longer asks the trusted host to authenticate + servers on its behalf), and hermetic (no dependence on the host's cert store). + +### ❌ Rejected for parity — host-brokered TLS (the current `net_tls_connect` path) + +Reusing the existing bridge is minimal work (wget = ~100-line shim, git = build +with the overlaid libcurl), but it **cannot reach native semantics**: it uses the +*host's* cert store (non-hermetic), ignores `--cacert`/`--capath`, returns the +wrong exit-code taxonomy, can't print the cert chain, and every future TLS flag +(client certs, `--pinnedpubkey`, TLS-version pinning) needs a new bridge hop — a +permanent shim treadmill. It also contradicts the "real tool, patch the sysroot" +philosophy the rest of the toolchain follows. Keep the host `net.socket_upgrade_tls` +path only for the **Node/JS runtime**, which is a separate surface. + +> Note: the current sidecar TLS path uses `rustls_native_certs` = the **host +> machine's** trust store (`crates/native-sidecar/src/execution.rs`). That is a +> latent hermeticity bug even for the JS runtime — it should read the VM's +> `/etc/ssl` bundle instead. Tracked here; fix alongside. + +## CA bundle — ship Debian-shaped trust inside the VM + +- Ship the **Mozilla CA bundle** (`curl.se/ca/cacert.pem`, i.e. the set Debian's + `ca-certificates` produces) at **`/etc/ssl/certs/ca-certificates.crt`**, with the + conventional `/etc/ssl/cert.pem` symlink. A `ca-certificates` registry package + owns the payload; VM bootstrap links it into the standard tree (the bootstrap + already seeds `/etc` in the shadow root — `crates/native-sidecar/src/vm.rs`). +- This one file at that one path is what makes the **whole class** of TLS tools + "just work": curl's compile-time `CURL_CA_BUNDLE` default, OpenSSL's `OPENSSLDIR` + (`/usr/lib/ssl` → `/etc/ssl/certs`), apt, python, wget — all resolve there on + Debian. `SSL_CERT_FILE`/`SSL_CERT_DIR`/`--cacert` env overrides then work for + free once the backend does real verification. + +## Per-tool implementation plan (completed) + +### curl +1. Vendor + build **mbedTLS 3.6** for `wasm32-wasip1` in `toolchain/c/Makefile` + (same pattern as the existing zlib target). +2. In `toolchain/c/scripts/build-curl-upstream.sh`: drop `--without-ssl` and the + `USE_WASI_TLS` injection; add `--with-mbedtls`, `--with-zlib`, `--with-brotli`, + `--with-zstd`, `--with-ca-bundle=/etc/ssl/certs/ca-certificates.crt`; retire the + `wasi_tls.c/h` + `vtls.c` overlay. +3. Tests: assert **exit 60** + real verify message on self-signed without `-k`; + `--cacert` acceptance of a test CA; `--compressed` gzip/br/zstd round-trips. + +### wget +GNU Wget has no mbedTLS backend, so give it a real TLS backend against the same +mbedTLS + CA bundle (its SSL abstraction is just 4 functions in `src/ssl.h`: +`ssl_init`, `ssl_cleanup`, `ssl_connect_wget`, `ssl_check_certificate`). +1. Add `wasi_ssl.c` implementing those 4 functions over **mbedTLS** (handshake + + `mbedtls_x509_crt_parse_file` on `/etc/ssl/certs/ca-certificates.crt`; + `--no-check-certificate`/`--ca-certificate`/`--ca-directory` map to real + mbedTLS verify config, matching Linux wget). Overlay dir mirrors curl's + (`software/wget/native/c/overlay/…`). +2. In `build-wget-upstream.sh`: keep `./configure --without-ssl` (so it doesn't + probe GnuTLS/OpenSSL), then post-configure patch `src/wget.h`'s `HAVE_SSL` + condition to add `|| defined HAVE_WASI_TLS`, pass `-DHAVE_WASI_TLS`, copy the + overlay, append a `wasi_ssl.o` compile/link rule to the generated `src/Makefile` + (the exact playbook the curl script uses). This lights up the `https` scheme, + all `--secure-protocol/--ca-certificate/--no-check-certificate` options, and + HSTS. +3. Enable **zlib** (`--with-zlib` via `ZLIB_CFLAGS/ZLIB_LIBS` at the existing + `build/zlib/libz.a`). Leave `--without-libpsl` (cosmetic — cookie public-suffix + only; wget's built-in heuristic covers single-URL HTTPS). +4. Tests: mirror curl's HTTPS cases (verify-fail on self-signed, + `--no-check-certificate` success, keep-alive), + gzip via `--compression`. + +### git — HTTPS smart-HTTP (clone/fetch/**push**) +Git's HTTP transport lives entirely in the **`git-remote-https` remote helper**, +which links **libcurl in-process** (git never shells out to a `curl` binary). The +current symlink `git-remote-https → git` is structurally broken (`git`'s +`cmd_main` dies "cannot handle remote-https as a builtin"). +1. **Produce a reusable libcurl** artifact (`make -C lib install` the overlaid, + mbedTLS-linked libcurl into a prefix under `toolchain/c/build/curl-upstream/ + install`). +2. In `toolchain/c/scripts/build-git-upstream.sh`: drop `NO_CURL=1`; add + `CURL_CFLAGS/CURL_LDFLAGS` pointing at that libcurl (defining them skips + `curl-config`); keep `NO_EXPAT=1`; build targets **`git git-remote-http`**; + wasm-opt both. (Expect a handful of small WASI compile fixes in + `http.c`/`remote-curl.c`, same class the curl script already makes; the + existing `git_compat.c` shims cover most.) +3. **Packaging:** install `git-remote-http` as a **real second command** (replace + the wrong symlink loop in `toolchain/c/Makefile`); in + `software/git/agentos-package.json` move `git-remote-http` into `commands` and + re-point `git-remote-https` → `git-remote-http` (keep `git-upload-pack`/ + `git-receive-pack`/`git-upload-archive` → `git`; those *are* builtins). +4. **Push** is the same libcurl POST plumbing as fetch (`remote-curl.c` spawns + `git send-pack` and streams the pack via chunked `Transfer-Encoding` + + `Expect: 100-continue` — all inside libcurl, no new host surface). HTTP Basic + auth (token/`user:pass` URLs, `GIT_ASKPASS`) works. (`credential-cache` + unavailable — `NO_UNIX_SOCKETS`; NTLM/Negotiate unsupported — irrelevant for + GitHub/GitLab.) +5. **Nothing new on the host side** — TLS/DNS/TCP/spawn/permission-tiers are all + in place. Spawn already resolves `git-remote-https` by name on the guest PATH + via the `proc_spawn` broker (proven: `git clone` already spawns + `git upload-pack` locally). Permission tiers already grant `git-remote-http(s)` + tier `full`. +6. **ssh transport (secondary):** `git@host:` spawns the separately delivered + in-guest OpenSSH client. SSH uses its own protocol crypto rather than mbedTLS. +7. Tests: flip `hasGitHttpHelper` in `software/git/test/git.test.ts` (the + smart-HTTP clone/fetch suite is already written but skipped); add a push case + (small + >1 MiB chunked POST). + +## Compression (curl) + +- **zlib 1.3.1 is already vendored** and built against this sysroot (git links it): + `--with-zlib` via `CPPFLAGS/LDFLAGS` at `build/zlib`. +- **brotli** — decoder (`common`+`dec`) is dependency-free portable C; add a + Makefile fetch/build target mirroring zlib; `--with-brotli` (decode only). +- **zstd** — already compiles under this sysroot inside the duckdb build + (`toolchain/c/build/duckdb-cmake/third_party/zstd/`); build libzstd + single-threaded; `--with-zstd`. +- Out of scope here: nghttp2 (HTTP/2), libpsl. + +## Refuted / dead-end theses (documented so we don't re-try them) + +- **Host-brokered TLS for parity** — works but never reaches native semantics + (host cert store, ignored `--cacert`, wrong exit codes, no `-v` chain, shim + treadmill). Rejected as the parity path; kept only for the JS runtime. +- **Full OpenSSL as the shared HTTPS backend** — unnecessary for curl, wget, and + git. A checksum-pinned OpenSSL 3.5.7 **libcrypto-only** build is proven and is + now used by OpenSSH to restore RSA/ECDSA/DH/AES algorithm coverage. It builds + without source patches by disabling threads, assembly, dynamic modules, + engines, applications, and libssl, and seeds through the owned `getrandom` + path. That scoped success does not justify replacing the smaller mbedTLS HTTPS + integrations with the full OpenSSL TLS/provider/config surface. +- **GnuTLS in-guest** (wget's default) — drags nettle → GMP (asm) → libtasn1 → + p11-kit (dlopen). Non-starter on WASI. +- **rustls + aws-lc-rs in guest** — aws-lc-rs is C+asm, does **not** build for + wasm32-wasip1. `ring` support is unofficial/spotty. Mixing a Rust staticlib into + the clang-LTO curl link is fiddly. No fidelity gain over mbedTLS. +- **rustls-native-certs (as the trust story)** — needs an OS cert store; the guest + has none, and host-side it depends on the host machine's store (non-hermetic). +- **BearSSL** — curl **removed** BearSSL support in 8.12.0 and it lacks TLS 1.3. + Dead end for any curl upgrade. +- **Reusing curl's `wasi_tls.c` for wget** — impossible; it's written against + libcurl-internal `Curl_cfilter` APIs. The shareable unit is the **host import + contract / the mbedTLS backend**, not the C file. +- **git: symlink `git-remote-https` → git binary** (current state) — git's + `cmd_main` hard-dies for a non-builtin `git-*` argv[0]. Structurally impossible. +- **git: spawn the `curl` CLI from a shell helper** — the remote helper is a + long-lived, stateful, bidirectional protocol-v2 program (auth retry, gzip, + full-duplex chunked POST). The CLI can't provide it. +- **git: gitoxide / libgit2 / reimplement smart protocol in-process** — not real + upstream git; diverges on CLI/config/hooks; would need its own WASI net port + anyway. Rejected. +- **git: dumb-HTTP only** — GitHub/GitLab/Gitea serve smart-HTTP only for fetch + and never support dumb push. Non-starter (dumb fetch rides along free). +- **Anything threaded** — the runner is single-threaded wasip1; all TLS/compression + libs build single-threaded (mbedTLS/zstd support it). + +## Work breakdown & sequencing + +1. **mbedTLS 3.6** target in `toolchain/c/Makefile` (shared by curl, wget, git's + libcurl). ← foundational, do first. +2. **CA bundle**: `ca-certificates` payload at `/etc/ssl/certs/ca-certificates.crt` + + VM bootstrap seeding + sidecar reads the VM bundle (not the host store). ← + foundational. +3. **curl**: mbedTLS + zlib/brotli/zstd + CA bundle; retire `wasi_tls.c`; tests. +4. **libcurl install artifact** (from step 3's build) for git to link. +5. **git**: build-with-curl, real `git-remote-http` helper, packaging, push; tests. +6. **wget**: `wasi_ssl.c` mbedTLS backend + zlib + CA bundle; tests. + +Steps 3/5/6 all sit on 1+2. One jj rev per tool. Real e2e tests against a TLS +server (verify-fail on self-signed, `--cacert` success, real clone/fetch/push) are +the deliverable — no host-store shortcuts. diff --git a/docs-internal/registry-flatten-colocation-spec.md b/docs-internal/registry-flatten-colocation-spec.md new file mode 100644 index 0000000000..f4298f4cc9 --- /dev/null +++ b/docs-internal/registry-flatten-colocation-spec.md @@ -0,0 +1,392 @@ +# Spec: Flatten `registry/` → `software/` + colocated `native/` + +Status: proposal · Owner: registry · Last updated: 2026-07-07 + +## Summary + +Eliminate the `registry/` wrapper. The package catalog becomes a top-level +`software/` directory where **every `@agentos-software/*` package — command +packages, agent adapters, and bundles — is one flat folder** that owns its +manifest, TS descriptor, e2e test, **and its own command source** under +`native/`. Shared WASM build infrastructure that no single package can own +(patched sysroot, shared runtime-shim crates, build orchestration, libc +conformance) moves to a top-level **`toolchain/`**. + +In the same effort, push the generic package-authoring capabilities that live +only in this repo's `Makefile`/`justfile` today — **compiling** a command to WASM +and **testing** it in a VM — into the published `agentos-toolchain` CLI, so the +`justfile` is left with only repo-specific orchestration. + +Result: `registry/` disappears; `software//` is self-describing; the +misleading "libc tests under registry" problem is gone; and authoring an agentOS +package is the same flow for us and for external users. + +## Motivation + +Today `registry/` mixes three unlike things: package manifests (`software/`, +`agent/`), the command **source + WASM toolchain** (`native/`), and libc/sysroot +**conformance tests** (`native/tests/`). "Registry" reads as "the catalog of +packages," so a C/Rust cross-compilation toolchain and libc conformance tests +living under it is misleading. Package source also lives far from the package +(`software/curl/` has the manifest; `curl.c` is in `registry/native/c/programs/`). + +## Target file structure + +``` +repo-root/ +├── software/ ← the entire @agentos-software/* catalog (flat, no registry/) +│ │ +│ ├── curl/ # C-based command package +│ │ ├── package.json # @agentos-software/curl +│ │ ├── agentos-package.json # manifest + registry block (title, description, category, kind) +│ │ ├── src/index.ts # TS descriptor +│ │ ├── test/curl.test.ts # e2e integration test +│ │ ├── native/ +│ │ │ └── c/ +│ │ │ ├── curl.c # command implementation +│ │ │ └── overlay/ # pkg-specific upstream overlay (was c/curl-upstream-overlay/) +│ │ └── bin/curl # staged build output (gitignored) +│ │ +│ ├── git/ # Rust-based command package +│ │ ├── package.json agentos-package.json src/ test/ bin/ +│ │ └── native/crates/ +│ │ ├── cmd-git/ # command crate (was native/crates/commands/git) +│ │ └── git/ # 1:1 lib crate (was native/crates/libs/git) +│ │ +│ ├── coreutils/ # multi-command package (sh + ~80 utils) +│ │ ├── package.json agentos-package.json src/ test/ bin/ +│ │ └── native/crates/ +│ │ ├── sh/ cat/ ls/ cp/ mv/ sort/ … # ~80 command crates +│ │ └── (du/ expr/ column/ rev/ strings/ — 1:1 libs that belong to coreutils) +│ │ +│ ├── sqlite3/ duckdb/ wget/ zip/ unzip/ # C-based, native/c/.c +│ ├── grep/ sed/ gawk/ jq/ yq/ fd/ ripgrep/ tree/ file/ tar/ gzip/ diffutils/ findutils/ # Rust, native/crates/ +│ │ +│ ├── claude/ codex/ opencode/ pi/ pi-cli/ # agent adapters — @agentos-software/*, JS only, no native/ +│ │ ├── package.json agentos-package.json (kind: "agent") src/adapter.ts test/ +│ │ +│ ├── vim/ # editor (C in native/c/) +│ └── browserbase/ build-essential/ common/ everything/ # meta/external — manifest only, no native/ +│ +├── toolchain/ ← shared WASM build infra (NOT a package; nothing here is @agentos-software/*) +│ ├── Cargo.toml # WASM cargo workspace root — members glob into software/*/native/crates/* +│ ├── Cargo.lock +│ ├── rust-toolchain.toml # nightly + wasm32-wasip1 + build-std +│ ├── Makefile # builds every software/*/native/ against the shared sysroot +│ ├── crates/ # SHARED rust crates used by ≥2 packages / infra +│ │ ├── shims/ builtins/ stubs/ wasi-ext/ +│ │ ├── wasi-http/ wasi-pty/ wasi-spawn/ +│ │ └── reqwest-shim/ portable-pty-wasi/ codex-network-proxy{,-wasi}/ codex-otel/ uucore/ ctrlc/ hostname/ +│ ├── sysroot/ # patched wasi-sdk build (was native/c/{include,patches,cmake,scripts} + wasi-sdk) +│ │ ├── wasi-sdk/ # fetched +│ │ ├── include/ patches/ cmake/ scripts/ +│ │ └── libs-cache/ # fetched upstream C sources (sqlite, zlib, curl, duckdb) +│ ├── std-patches/ # rust std patches 0001–0009 (was native/patches/) +│ ├── scripts/ # patch-std.sh patch-vendor.sh patch-wasi-libc.sh +│ ├── test-programs/ # C test-program fixtures (tcp_server, udp_echo, signal_handler, http_server, …) +│ │ # built here; runtime-core integration tests consume the built binaries +│ ├── conformance/ # libc/os-test/c-parity — tests the sysroot, not a package (was native/tests/) +│ │ ├── c-parity.test.ts libc-test-conformance.test.ts os-test-conformance.test.ts *-exclusions.json +│ └── target/ # shared cargo build output (gitignored) +│ +├── test-harness/ ← private workspace pkg; shared vitest helpers + WASM VM runtime + binary resolution +│ ├── package.json # @agentos/test-harness (private, not published) +│ └── src/{helpers,terminal-harness}.ts +│ +└── packages/ + └── runtime-core/ + └── tests/integration/ # VM integration tests (net, npm-e2e, wasi, signal, cross-runtime) +``` + +All native **compilation stays in `toolchain/`** — command source lives with its +package for editing, but nothing compiles WASM outside the toolchain build. C +test-program fixtures stay in `toolchain/test-programs/` (not scattered into +`packages/`) so there is exactly one place that invokes the C compiler + sysroot. + +## Boundary rules + +1. **`software//` owns**: `package.json`, `agentos-package.json`, `src/` + (TS descriptor), `test/` (e2e), `native/` (this package's command source), + `bin/` (staged output). +2. **`toolchain/` owns**: the patched sysroot, shared runtime-shim crates, std + patches, build orchestration, and libc/sysroot conformance tests. Nothing in + `toolchain/` is an `@agentos-software/*` package. +3. **Command source colocates; shared infra does not.** A crate used by exactly + one package moves into that package; a crate used by ≥2 packages (or that is + pure infra) stays in `toolchain/crates/`. This is the honest limit of + colocation — see [Shared vs per-package](#shared-vs-per-package-split). +4. **Agents are packages too.** `claude`/`codex`/`opencode`/`pi`/`pi-cli` live + under `software/` because they publish as `@agentos-software/*`. They carry + `kind: "agent"` in the manifest and have no `native/`. Command packages carry + `kind: "software"` (or omit; default). This `kind` field is what distinguishes + them — not folder location. +5. **`native/` holds the REAL upstream tool, patched — never a reimplementation.** + A command package's `native/` must build the genuine upstream program (GNU + coreutils, real `curl`/`git`/`jq`, GNU grep/sed/gawk/tar/gzip/diffutils, …) + fetched + pinned and patched for WASI — not a from-scratch Rust/C rewrite, a + stub, or a hand-rolled CLI over a library. The only exception is a tool whose + canonical upstream *is* the Rust project (`ripgrep`, `fd`). Several current + commands violate this (coreutils=uutils, grep, curl driver, and the + `secureexec-*` rewrites) — tracked in `registry-parity-worklist.md` + Cross-cutting #0; new packages must not add more. + +## What moves where + +| Current | Destination | +|---|---| +| `software//` | `software//` | +| `software//` | `software//` (+ `kind: "agent"`) | +| `registry/native/crates/commands/` | `software//native/crates/cmd-/` | +| `registry/native/crates/libs/` (1:1) | `software//native/crates//` | +| `registry/native/crates/libs/` (shared ≥2) | `toolchain/crates//` | +| `registry/native/crates/{wasi-ext,libs/{shims,builtins,stubs,wasi-http,wasi-pty,wasi-spawn}}` | `toolchain/crates/` | +| `registry/native/stubs/*` | `toolchain/crates/` | +| `registry/native/c/programs/.c` (a package command) | `software//native/c/.c` | +| `registry/native/c/programs/.c` (tcp_server, udp_echo, signal_handler, …) | `toolchain/test-programs/` (built by toolchain; consumed by runtime-core integration tests via the binary path) | +| `registry/native/c/{include,patches,cmake,scripts,vim overlay}` + wasi-sdk | `toolchain/sysroot/` | +| `registry/native/patches/` (std) | `toolchain/std-patches/` | +| `registry/native/scripts/` | `toolchain/scripts/` | +| `registry/native/tests/` (conformance) | `toolchain/conformance/` | +| `registry/native/{Cargo.toml,Cargo.lock,rust-toolchain.toml,Makefile}` | `toolchain/` | +| `registry/tests/` (empty leftover) | delete | + +The command→package owner is derived mechanically from each +`agentos-package.json` `commands` array — the migration script computes it, no +hand-mapping. + +## Shared vs per-package split + +Verified against the current tree: + +- **Shared → `toolchain/crates/`** (used by ≥2 commands or pure infra): + `shims` (7 users), `builtins` (3), `wasi-http` (4), `wasi-spawn` (2), + `stubs`, `wasi-ext`, `wasi-pty`, and all `stubs/*` shim crates + (`reqwest-shim`, `portable-pty-wasi`, `codex-network-proxy{,-wasi}`, + `codex-otel`, `uucore`, `ctrlc`, `hostname`). +- **Per-package → `software//native/crates/`** (1:1): `grep`→grep, + `awk`→gawk, `jq`→jq, `yq`→yq, `git`→git, `fd`→fd, `tar`→tar, `tree`→tree, + `gzip`→gzip, `diff`→diffutils, `find`→findutils, `file-cmd`→file, and + `du`/`expr`/`column`/`rev`/`strings-cmd`→coreutils. + +## Test execution & harness + +The old `agentos-registry` package and its `run-vitest.mjs` shim (which resolved +vitest out of the outer store) **dissolve**. Replacement: + +- A private workspace package **`test-harness/`** (`@agentos/test-harness`, not + published) owns the shared helpers, terminal harness, and `createWasmVmRuntime` + — the surface every test imports today from `registry/tests/helpers.ts`. +- Each `software/` gets a `test` script that runs **`agentos-toolchain test`** + (see [Toolchain CLI boundary](#toolchain-cli-vs-justfile-boundary)), which drives + the `test/` suite through the harness in a VM — the same runner external authors + use. `turbo test` discovers and runs them — this is what re-attaches the suites + to CI (fixing the current orphan + `@xterm/headless`-resolution bugs, since every + runner is now a workspace member). The `@agentos/test-harness` package supplies + `createWasmVmRuntime` + helpers that both the CLI runner and the raw `*.test.ts` + files import. +- Command binaries are resolved from `toolchain/target` (Rust) and the toolchain + C build dir via `AGENTOS_WASM_COMMANDS_DIR` / `AGENTOS_C_WASM_COMMANDS_DIR`, + set once in the harness. No relative-path coupling to the build tree. +- `runtime-core/tests/integration/` and `toolchain/conformance/` import the same + `@agentos/test-harness`. + +## Leftover `registry/` files + +When `registry/` is deleted, its non-package contents relocate: + +| Current | Destination | +|---|---| +| `software/package.json` (`agentos-registry`) + `registry/scripts/run-vitest.mjs` | removed — superseded by per-package `test` scripts + `test-harness/` | +| `registry/tsconfig.base.json` | `software/tsconfig.base.json` (shared by package `tsconfig`s) | +| `registry/CONTRIBUTING.md`, `registry/README.md` | `software/` (or `docs-internal/`), updated for the new layout | + +## Orphan commands (no owning package) + +`envsubst` ships a command + a passing test but **no `software/*` package +declares it**. Before executing, it must get a home — one of: (a) fold into an +existing package's `commands` (e.g. `coreutils`), (b) create `software/envsubst/`, +or (c) reclassify its `.c` + test as a `toolchain/test-programs` fixture. Same +check applies to any other command with a binary/test but no manifest owner — +the migration script lists them; each needs an explicit decision, none are +silently dropped. + +## Cargo workspace strategy (the critical detail) + +There are two Rust workspaces today: the **main** repo workspace (repo-root +`Cargo.toml`, explicit members, stable toolchain) and the **wasm** workspace +(`registry/native/Cargo.toml`, nightly + build-std). They must stay separate — +different toolchains. + +After the move, colocated crates live at `software/*/native/crates/*`, which is +inside the **main** workspace's directory tree. Cargo resolves a crate's +workspace by walking up to the nearest `[workspace]`, so those crates would be +captured by the repo-root workspace — wrong toolchain. Fix with two edits: + +1. **Repo-root `Cargo.toml`**: add `exclude = ["software"]` so the main + workspace never claims package `native/` crates. +2. **`toolchain/Cargo.toml`**: `[workspace]` with + `members = ["crates/*", "../software/*/native/crates/*"]`. Cargo permits + members outside the workspace root via relative path; this private, + never-published workspace is a valid use of it. + +All wasm builds run against the toolchain workspace explicitly +(`cargo build --manifest-path toolchain/Cargo.toml …`, driven by +`toolchain/Makefile`). Cargo's shared `target/` lives at `toolchain/target/`. + +**Fallback if cross-dir members prove fragile in practice:** colocate only the +**C sources + tests + manifest**, and keep the **Rust command crates** together +in `toolchain/crates/commands/`. Less pure, but sidesteps the workspace-spanning +entirely (C has no cargo-workspace concern). This is the one decision to lock +before executing — see [Risks](#risks--decision). + +## Reference updates (~40 sites, mechanical) + +- `software/*/package.json` build scripts: + `../../native/target/...` → `../../../toolchain/target/...` + (and the C build-output dir similarly). +- `justfile`: repoint **and rename** every `registry-*` recipe (the `registry` + prefix is dead once the folder is gone). Rename by the dir the recipe acts on — + `software-*` for package builds, `toolchain-*` for the WASM build/commands: + + | Current recipe | New recipe | Body change | + |---|---|---| + | `registry-build` | `software-build` | `pnpm --filter '@agentos-software/*' build` (unchanged filter) | + | `registry-native` | `toolchain-build` | `make -C registry/native commands` → `make -C toolchain commands` | + | `registry-native-cmd ` | `toolchain-cmd ` | `make -C registry/native cmd/` → `make -C toolchain cmd/` | + | `registry-native-preflight` | `toolchain-preflight` | `cd registry/native/c` → `cd toolchain/sysroot` | + | `registry-copy-commands` | `toolchain-copy-commands` | repoint copy-wasm-commands SRC to `toolchain/target/...` | + + Update any callers of the old recipe names (CI `just` invocations, other + recipes, docs) in the same pass. If you prefer a single flat prefix over the + dir-aligned split, use `software-*` for all — but `toolchain-*` reads truer for + the recipes that build the sysroot/commands rather than the packages. +- `packages/runtime-core/scripts/copy-wasm-commands.mjs`: SRC path + `registry/native/target/...` → `toolchain/target/...`. +- CI (`ci.yml`, `ci-nightly.yml`, `bench.yml`): `make -C registry/native`, + the rust-cache `workspaces:` mapping, and the + `hashFiles('registry/native/Cargo.lock')` cache key → `toolchain/`. +- Repo-root `Cargo.toml`: add `exclude = ["software"]`. +- `pnpm-workspace.yaml`: `software/*`, `software/*` → + `software/*` (single glob covers commands + agents) plus `test-harness`. +- `toolchain/Makefile`: replace the fixed `c/programs/*.c` source list with a + glob over `../software/*/native/c/*.c`, and cargo members over the workspace. + +## Toolchain CLI vs justfile boundary + +**Principle:** `agentos-toolchain` (the published CLI) owns everything a +third-party author of *any* `@scope/*` package needs — author → compile → test → +package → publish. The `justfile` owns only what is specific to building and +releasing *this* repo's registry. Nothing generic to package authoring should +live only in our `justfile`/`Makefile`. + +Today the CLI stops at packaging (`pack`, `pack-aospkg`, `stage`, `build`, +`publish`); `stage` consumes an *already-compiled* `--commands-dir`. The two +hardest capabilities — **compiling** a command to WASM and **testing** it in a VM +— live only in the repo Makefile/harness, so external authors can't do them. +This refactor closes that gap. + +### New published toolchain verbs + +1. **`agentos-toolchain compile []`** — compile the package's + `native/` (Rust crate(s) or C source) to a WASM command binary against the + **pinned agentOS sysroot**, emitting into the commands dir that `stage` + consumes. Wraps sysroot + build-std + wasm-opt. That pipeline is heavy, so the + CLI fetches a **pinned toolchain bundle** (the way it already fetches wasi-sdk) + or runs a container image; the pin is versioned for reproducible external + builds. This is the capability that today only `registry/native/Makefile` has. +2. **`agentos-toolchain test []`** — boot a throwaway VM, register the + package, run its `test/` suite (and/or a command smoke-run) via the public + `@rivet-dev/agentos-runtime-core/test-runtime`. This is what our + `software//test/` scripts *and* external authors call — one runner. +3. **`agentos-toolchain validate []`** — lint `agentos-package.json`: + declared `commands` map to staged binaries; `registry` block + `category` + + `kind` present; limits bounded. Turns our repo-only coverage/layout checks into + a per-package check every author gets. +4. **`agentos-toolchain init []`** — scaffold a package + (`agentos-package.json` + `src/index.ts` + `test/`) instead of copying an + existing package by hand. + +### Shim crates must be publishable + +The shared Rust crates in `toolchain/crates/` (`wasi-ext`, `wasi-http`, +`wasi-spawn`, `builtins`, `shims`) are **path-deps today, unpublished** — so an +external Rust command cannot build against them. For `compile` to work outside +this repo, either publish them (crates.io) or have `compile` vendor them. Add +them to the publish set (`scripts/publish` discovery). + +### justfile = thin loop over the CLI + +Repo recipes orchestrate; they must not reimplement what the CLI does: + +| Recipe | Body | +|---|---| +| `software-build` | for each `software/*`: `agentos-toolchain compile && stage && build` | +| `toolchain-build` | build/pin the shared sysroot bundle the CLI consumes | +| per-package `test` script + external authors | `agentos-toolchain test` | + +**Stays repo-specific** (not CLI, not overfit): `copy-wasm-commands` (vendor into +runtime-core), `verify-fixed-versions` (the 0.0.1 pin), `generate-secure-exec-mirror`, +registry-wide release orchestration, cross-repo dispatch, and the status +reporter / coverage gate scoped to *our* registry. + +## Migration phases + +1. **Scaffold `toolchain/`** — move `registry/native/{Cargo.*, rust-toolchain, + Makefile, patches→std-patches, scripts, tests→conformance}` and the shared + crates + sysroot. Repoint the workspace + Makefile + CI + copy-commands, and + **rename the `registry-*` justfile recipes** (`software-*` / `toolchain-*` per + the table above) plus their callers. Build green with crates still under + `toolchain/crates/commands/` (pre-split). +2. **Flatten catalog + harness** — `git mv software/* software/`, + `git mv software/* software/`; create `test-harness/` from the old + `registry/tests/helpers.ts`+`terminal-harness.ts`; give each package a `test` + script; update `pnpm-workspace.yaml`; add `kind` to agent manifests; relocate + leftover `registry/` files; delete empty `registry/`. +3. **Colocate command source** — move each `cmd-` + its 1:1 lib into + `software//native/`; move C command sources into + `software//native/c/`; C test-programs stay in `toolchain/test-programs/`; + apply the `Cargo.toml` exclude + cross-dir members; resolve orphan commands. +4. **Add `category`** to every `registry` manifest block (grouping metadata). +5. **Productize the toolchain** — add `compile` / `test` / `validate` / `init` + verbs to `agentos-toolchain`; make the shim crates publishable; rewrite the + `software-build` / `toolchain-build` recipes as thin loops over the CLI; point + each package `test` script at `agentos-toolchain test`. (Can land incrementally + after the move; the move doesn't block it, but the CLI is the long-term home.) +6. **Enforce** — structural test (below) + coverage gate; delete dead paths. + +## Enforcement (`scripts/check-layout.mjs`, CI) + +Fail the build if: +- any `*.test.ts` sits outside an allowed home (`software//test/`, + `toolchain/conformance/`, `packages/*/tests/**`); +- any `software/*` command package lacks `test/` (allowlist meta bundles + + external wrappers); +- `registry/` exists; +- a crate under `software/*/native/crates/` is claimed by the main workspace + (guards the Cargo split). + +## Risks & decision + +1. **Cargo cross-dir members (biggest).** `toolchain/Cargo.toml` owning + `../software/*/native/crates/*` plus repo-root `exclude = ["software"]` is + valid but unusual; some tooling assumes members live under the root. Two known + facets: `cargo metadata` on cross-dir members, and **rust-analyzer/`rust-toolchain` + resolution** — a colocated crate opened directly walks up to the repo-root + (stable) toolchain, not `toolchain/`'s nightly, so IDE builds may use the wrong + channel unless the harness always drives builds from `toolchain/`. **Decide: + full colocation (this spec) vs. the C-only fallback** (Rust crates stay in + `toolchain/crates/commands/`). Recommendation: attempt full colocation in a + throwaway branch first; fall back if `cargo metadata`/rust-analyzer misbehave. +2. **Makefile source discovery.** Building must glob C sources and cargo members + across `software/*` instead of a fixed list — a one-time Makefile rewrite. +3. **No build isolation.** Building one package still needs all of `toolchain/` + (sysroot + shared crates). Colocation improves navigation, not build scope. +4. **Shared crates aren't self-contained.** `software/coreutils/native/crates/sh` + still depends on `toolchain/crates/builtins`; a package folder is not fully + standalone. Accepted, documented in rule #3. +5. **Sysroot distribution for `agentos-toolchain compile`.** The compile verb + needs the patched sysroot + nightly + wasm-opt, which are large and + platform-specific. **Decide** how the published CLI ships them: a pinned + downloadable bundle (like wasi-sdk) vs. a container image. Until decided, + `compile` works in-repo only and external authors still can't build — so this + gates the "same flow for external users" goal, not the internal move. diff --git a/docs-internal/registry-parity-worklist.md b/docs-internal/registry-parity-worklist.md new file mode 100644 index 0000000000..ec74eeb464 --- /dev/null +++ b/docs-internal/registry-parity-worklist.md @@ -0,0 +1,888 @@ +# Registry Linux-Parity Worklist + +Status: worklist · Owner: registry · Last updated: 2026-07-08 + +## Goal (hand this to the driver agent) + +> Drive every item in this worklist to **clean Linux parity**: each command/ +> behavior must work end-to-end the way it does on real Linux, **proven by real +> e2e tests** — not by a WASI-specific port, a stub, or a shim that only satisfies +> the test. Example of the bar: `duckdb` must run real analytical SQL against real +> file-backed databases and pass real e2e tests — not a stripped "WASI duckdb" +> that only does `SELECT 1`. +> +> **Rules:** +> - **🚧 REAL TOOL, NOT A REIMPLEMENTATION (the load-bearing rule).** Every command +> must be the **real upstream tool** (GNU coreutils, GNU grep/sed/gawk, real +> `curl`, real `git`, real `jq`, GNU tar/gzip/diffutils, …) compiled to +> `wasm32-wasip1` and **patched as needed** for WASI. Do **NOT** ship a +> from-scratch Rust/C rewrite, a stub, or a hand-rolled CLI over a library. +> Reimplementations drift from Linux behavior in a thousand small ways and are +> exactly why several commands fail parity. Sole exception: when the upstream +> canonical tool *is itself* the Rust project (**ripgrep**, **fd**) — then the +> real project is correct. Prefer the genuine upstream tool (real git, real +> grep) over a rewrite; a *popular, established* reimplementation is an +> acceptable fallback only when the real tool genuinely won't build. +> - **"Not possible" is a valid outcome — but only after trying really hard.** The +> sysroot is **ours**: a patched Rust std + libc with custom host-syscall imports +> (see CLAUDE.md → Software Build (WASM Toolchain)). A missing libc/POSIX API +> (`getrlimit`/`RLIMIT_NOFILE`, `getgroups`, …) is **NOT** a WASI wall — it is a +> stub/patch we add one layer down, and the build should proceed as if targeting +> native POSIX. Only if a command *still* cannot be built as the real (or an +> established) tool do you mark it **`NOT POSSIBLE (WASI)`** in this doc, with a +> concrete explanation of the genuine, documented wall (never "WASI lacks a +> syscall we could implement") and what was tried. Exhaust real options first: +> patch the sysroot, patch the tool, stub the specific missing syscall — a +> genuine effort, not a quick bail. +> - **Commit clean revs — no stray artifacts.** Each rev must contain only the +> intended source + test changes. Never commit build outputs, vendored toolchain +> trees, `__pycache__`/`*.pyc`, generated binaries, or anything that belongs in +> `.gitignore`. Before `jj describe`, run `jj diff -r @ --summary` and confirm +> every path is intended — watch especially for `A` (added) paths under +> `toolchain/`, `**/target/`, `**/node_modules/`, `**/build/`, `**/__pycache__/`. +> Then **audit the entire stack up to main** (`jj diff -r 'main..@' --stat`, or +> per rev) and strip anything that slipped in with `jj restore --from +> `, adding the pattern to `.gitignore` so it cannot recur. +> - **One jj rev per item.** Concretely: **`jj new` before starting each item**, +> make that command's fix *and* its e2e test in that single change, `jj describe` +> it with a clear conventional-commit message, then `jj new` again for the next +> item. One command per rev — never batch two commands (or unrelated changes) +> into one rev. Verify the folder + branch first (`pwd`, `jj log -r @`) since the +> working copy is shared. +> - **Parity, not workarounds.** Fix the real cause (VFS syscall, shell semantics, +> link conflict, missing feature). If a WASI limitation forces a deviation from +> Linux, that is a finding to surface — not something to paper over in the test. +> - **Real tests are the deliverable.** A fix isn't done until an un-skipped e2e +> test exercises the real behavior in a VM and passes. No `describe.skip`, no +> assertions weakened to match broken output. +> - Work top-down by priority. Re-verify with the actual VM run, not just typecheck. + +## Priority tiers + +- **P0 — runtime/VM correctness**: bugs in the shared runtime that silently + corrupt data or break process control. Highest blast radius. +- **P1 — broken shipped commands**: packages that build but don't work like Linux. +- **P2 — build/compile failures**: packages that can't be produced at all. +- **P3 — disabled/absent coverage**: real behavior exists but no real test proves it. + +## Known CPU interval-timer boundary + +Disabled `ITIMER_VIRTUAL` and `ITIMER_PROF` timers now match Linux: `getitimer` +returns a zero timer and `setitimer` accepts a zero value. Arming either timer +still returns `ENOTSUP`. The concrete remaining wall is asynchronous delivery +during pure guest computation: V8 executes a WASM call synchronously, and the +runner has no per-instruction or safepoint callback through which it can deliver +`SIGVTALRM`/`SIGPROF`. The runtime has a per-thread CPU clock for its outer V8 +watchdog, but it can only terminate the isolate; it cannot enter the guest's +signal machinery at the exact CPU-time deadline. Polling at imported syscalls +would silently fail for a tight WASM loop, so it is not presented as parity. +Closing this gap requires a V8 interrupt/safepoint hook or instrumented WASM +fuel checkpoints that distinguish user CPU from user-plus-system CPU. + +--- + +## ⚠️ Cross-cutting #0 — Command provenance: replace reimplementations with real tools + +**This is the highest-leverage item and reshapes several below.** Audit revealed +that **most commands are NOT the real Linux tool** — they are custom Rust rewrites +(`secureexec-*` crates) or `uutils`, plus at least one hand-rolled C CLI (curl). +Per the load-bearing rule, each must become the **real upstream tool** compiled to +WASI and patched as needed. + +**Rule (your call):** an **established project** — whether it's the real upstream +tool *or* an established third-party package that does the real work (uutils, +jaq, etc.) — is **fine**. **Custom code we wrote ourselves** is **not** and must +be replaced with a real/established implementation. Audit of every command's +actual backing: + +### ✅ Established — keep (real upstream tool or established package doing the work) +| Command(s) | Backing | +|---|---| +| coreutils (`sh`+80) | **uutils** (`uucore`) — established Rust project | +| duckdb, vim | real upstream C source, patched for WASI | +| sqlite3 **engine** | real SQLite amalgamation (⚠️ but the *CLI* is ours — see below) | +| jq | **jaq** (`jaq-core/std/json`) — established Rust jq | +| yq | jaq + `serde_yaml`/`toml`/`quick-xml` — established parsers (thin glue is ours) | +| sed | `sed` crate (published) | +| gawk (`awk`) | `awk-rs` crate (published) | +| tar | `tar` crate (established) | +| gzip | `flate2` (established) | +| diffutils (`diff`) | `similar` crate (established) | +| file | `infer` crate (established magic-byte lib; note: not real libmagic `file`) | + +### ❌ CUSTOM WE BUILT — flag & replace with a real/established impl +| Command | Status | What it actually is | Replace with | +|---|---|---|---| +| **curl** | DONE | our custom driver over a libcurl fork | real `curl` CLI (upstream `src/tool_*.c`) | +| **wget** | DONE | our 174-line `wget.c` (dropped) | real GNU Wget vs our sysroot — stub `getrlimit`/`getgroups`, then build | +| **http-get** | DONE | our 95-line `http_get.c` | dropped; real curl covers HTTP fetches | +| **git** | DONE | our hand-rolled git from `sha1`+`flate2` | **real git** (upstream C), patched for WASI — **NOT gitoxide** | +| **fd** | DONE | our `secureexec-fd` on raw `regex` (not sharkdp/fd) | real **fd** (sharkdp) | +| **findutils** (`find`,`xargs`) | DONE | our hand-rolled on `regex`/shims | replaced with `uutils/findutils` | +| **tree** | DONE | our hand-rolled, zero deps | real `tree`, or an established one | +| **grep** | DONE | our `secureexec-grep` on raw `regex` (**not** an established grep pkg) | real **GNU grep** | +| **ripgrep** (`rg`) | DONE | our `secureexec-grep` recursive search shim, not real ripgrep | real upstream **ripgrep** | +| **zip** | DONE | our 203-line `zip.c` over zlib/minizip (not Info-ZIP) | real Info-ZIP, or an established lib's CLI | +| **unzip** | DONE | our 669-line `unzip.c` over zlib/minizip | real Info-ZIP unzip | +| **sqlite3 CLI** | DONE | our 558-line `sqlite3_cli.c` (engine is real SQLite; the shell is ours) | real SQLite `shell.c` (its official CLI) | +| **vix** | DONE | from-scratch source-less drop-zone binary | deleted; real `vim` covers the editor slot | + +Note: `codex`/`codex-exec` = the rivet fork of OpenAI's codex — established fork, +external build (tracked separately in #9). + +**Objective:** replace each ❌ with a real/established implementation built to +`wasm32-wasip1` and patched only where WASI forces it. The ✅ rows stay. + +**Networking — DONE.** Real curl and GNU Wget now terminate TLS in-guest with +mbedTLS against the VM CA bundle, and Git links the same libcurl through a real +`git-remote-http` helper. HTTPS certificate verification and trust overrides, +curl's native exit taxonomy and compression, Wget HTTPS/FTPS, and Git smart-HTTP +clone/fetch/push are covered by parity tests. OpenSSH separately provides direct +SSH and git-over-SSH transport. See **`docs-internal/networking-parity-spec.md`** +for the original design decisions and [TLS & SSL](../website/src/content/docs/docs/architecture/tls-ssl.mdx) +for the current architecture. + +**Approach:** one command at a time, one jj rev each: swap our custom code for the +established source (fetched + pinned like sqlite/duckdb), wire into the toolchain, +patch for WASI, prove parity with real e2e tests. + +**Interaction with other items:** subsumes several below — curl (#6) is "build the +real curl," and the `no-test` packages (#12) that are ❌ here should move to a real +impl *before* their tests are written, so the tests validate real behavior. + +**Decisions (settled):** git → **real git** (not gitoxide). grep → **real GNU +grep**, or a popular established grep if the real one won't build. + +**git — where the issues are (assessment):** +- **LICENSE — RESOLVED, not a blocker.** Each command ships as its **own published + npm package**, so a GPL-2.0 git binary in `@agentos-software/git` is **mere + aggregation** — it does not affect the Apache-2.0 licensing of agentOS or any + other package. Ship the git package under GPL-2.0 (offer its source) and we're + compliant. This **supersedes** the clean-room-reimpl rationale in + `toolchain/std-patches/git/README.md` ("cannot be vendored due to license + restrictions"): that premise no longer holds — go with **real git** (upstream C) + and update/remove that README. (gitoxide stays ruled out.) +- **Technical WASI issues if we do build real git** (from that README + git's own + build knobs), easiest → hardest: + - `mmap` (packfiles/index) → build `NO_MMAP=1` (malloc+read). Fine. + - signals (SIGPIPE/SIGCHLD) → build without; WASI has none. Fine. + - threads (index-pack/pack-objects) → `NO_PTHREADS`; single-threaded, slower. Fine. + - `fork()`+`exec()` in `run_command.c` (hooks, filters, remote helpers) → route to + `posix_spawn` via the `wasi-spawn` broker (spawn IS supported — same fix as wget). + - **Network transport (clone/fetch/push) — the hard part.** Smart-HTTP needs the + `git-remote-https` **helper subprocess** + libcurl; `git://` needs raw sockets; + ssh needs an `ssh` subprocess. Each helper must itself exist as a module. + - symlink checkout → `core.symlinks=false` fallback (WASI symlink support is + partial); local time → UTC like elsewhere. +- **Bottom line:** license is a non-issue (separate published package = mere + aggregation). **local** git (init/add/commit/log/diff/branch/merge/status/ + checkout) is very achievable; **remote** git (clone/fetch/push over + smart-HTTP/ssh) is the real effort. Proceed with real git. + +**Replacement findings — what each remaining ❌ tool will take (investigated):** + +The recurring wall is **never a syscall** — it's one of two known, already-solved +patterns: **(a) no threads** on `wasm32-wasip1` → serial patch like +`toolchain/std-patches/crates/uu_sort/0001-wasi-serial-sort.patch` (hits real fd & +ripgrep crates); **(b) gnulib `getrlimit`/`getgroups`** → sysroot stubs, already +documented for wget (item #8) (hits GNU grep/findutils). Subprocess spawn already +works (`wasi-spawn` broker), so `xargs` is not a blocker. + +- **git — DONE.** Replaced the custom Rust `sha1`+`flate2` implementation with real + upstream Git 2.55.0 built by the C toolchain and staged as `git` plus helper + command aliases. The WASI changes stayed below Git's behavior surface: + `run-command.c` uses `posix_spawn` so helper subprocesses go through the existing + wasi-spawn broker, the sysroot exposes Git's missing C compatibility surface, and + the runner now allocates synthetic fds in a high range so managed pipe/file fds + cannot collide with delegate-opened WASI fds. Smart HTTP remains intentionally + disabled in this build (`NO_CURL`), so HTTPS clone fails with the real Git helper + error instead of a custom transport. Proof: clean upstream Git rebuild passes in + `2026-07-08T11-28-00-0700-git-clean-rebuild-after-high-synthetic-fd.log`; + package build stages 6 commands in + `2026-07-08T11-33-00-0700-git-package-build-clean-binary-after-install.log`; + native sidecar rebuild passes in + `2026-07-08T11-34-00-0700-sidecar-rebuild-after-git-clean-package.log`; + full Git e2e passes 18/18 in + `2026-07-08T11-51-00-0700-git-full-e2e-high-synthetic-fd-clean-binary-after-test-fix.log`. + Rev: `tmvlxlvk` — `fix(git): build upstream git`. +- **sqlite3 CLI — DONE.** Engine was already the real amalgamation + (`libs/sqlite3/sqlite3.c`); the command now builds the official upstream + `shell.c` from the same fetched zip as `sqlite3`. The local 558-line + `sqlite3_cli.c` reimplementation is deleted, `toolchain/c/build/sqlite3` is the + primary C output, `sqlite3_cli` remains only as a compatibility alias, and the + tracked runtime-core fallback command is refreshed to the same official shell. + Proof: official shell build passes in + `2026-07-08T05-04-47-0700-sqlite3-official-shell-build-command-name.log`; + package-focused e2e passes 16/16, including real `.tables`, `.schema`, and + `.dump` CLI arguments, in + `2026-07-08T05-07-06-0700-sqlite3-official-shell-tests-final-focused.log`; + package build/check-types pass in + `2026-07-08T05-07-52-0700-sqlite3-package-build-official-shell-final.log` and + `2026-07-08T05-07-52-0700-sqlite3-check-types-official-shell-final.log`; + runtime-core fallback command path passes `.tables` in + `2026-07-08T05-09-12-0700-sqlite3-runtime-core-command-fallback-test.log`; + aggregate C `programs` builds 57 commands in + `2026-07-08T05-09-53-0700-sqlite3-make-programs-final.log`. + Rev: `typytnkk` — `fix(sqlite3): build official SQLite shell`. +- **http-get — DONE.** Dropped the 95-line raw-socket loopback client instead of + porting it: real curl now covers HTTP fetch behavior in DuckDB remote CSV tests, + runtime cross-network loopback tests, and the C conformance loopback row. + Removed the `@agentos-software/http-get` package, fallback command, shell/core + dependencies, registry listing, C command install entries, and lockfile package + edges. The low-level `http_get_test` fixture remains because it is a socket + diagnostic test program, not a shipped registry command. Proof: pnpm lockfile + refresh succeeds in + `2026-07-08T05-39-44-0700-http-get-pnpm-lock-refresh.log`; core and shell + type checks pass in + `2026-07-08T05-40-57-0700-http-get-core-check-types-after-install.log` and + `2026-07-08T05-41-34-0700-http-get-shell-check-types-final.log`; DuckDB test + file typecheck passes in + `2026-07-08T05-40-57-0700-http-get-duckdb-test-typecheck-after-install.log`; + aggregate C `programs` builds 57 commands, with `http_get` absent after stale + generated cleanup/install, in + `2026-07-08T05-41-34-0700-http-get-make-c-programs-without-command.log` and + `2026-07-08T05-43-24-0700-http-get-clear-stale-generated-and-install.log`; + cross-runtime network tests pass 11/11 with the WASM curl rows in + `2026-07-08T05-47-43-0700-http-get-runtime-cross-network-test-pass.log`. +- **tree — DONE.** Replaced the custom Rust `secureexec-tree`/`cmd-tree` crates + with upstream Steve Baker `tree` 2.3.2 from `OldManProgrammer/unix-tree`. + It builds as a C toolchain command from pinned source, stages into + `@agentos-software/tree`, and refreshes the tracked runtime-core fallback + command. Sysroot fixes live one layer down: install `` and provide + deterministic missing-group lookup stubs so upstream `-g` support links + without a tree-source WASI branch. Proof: upstream source inspection in + `2026-07-08T05-13-50-0700-tree-fetch-upstream-2.3.2-inspect.log`; sysroot + patch check passes in + `2026-07-08T05-18-16-0700-tree-wasi-libc-patch-check-group-lookup-fixed.log`; + Makefile build passes in + `2026-07-08T05-20-02-0700-tree-upstream-make-build.log`; package build and + check-types pass in + `2026-07-08T05-21-08-0700-tree-package-build-upstream-after-install.log` and + `2026-07-08T05-21-08-0700-tree-check-types-upstream-after-install.log`; e2e + tree tests pass 6/6 in + `2026-07-08T05-29-45-0700-tree-vitest-upstream-final.log`; aggregate C + `programs` builds 58 commands in + `2026-07-08T05-30-44-0700-tree-make-programs-final.log`; Cargo metadata no + longer includes the deleted Rust tree crates in + `2026-07-08T05-33-17-0700-tree-cargo-metadata-after-removing-empty-dirs.log`. + Rev: `kpmrwxln` — `fix(tree): build upstream tree`. +- **fd — DONE.** Replaced the custom Rust `secureexec-fd` regex walker with + upstream sharkdp `fd-find` 10.4.2. Because `fd-find` is a bin-only crate, + `cmd-fd` now acts as the workspace build trigger while the toolchain builds + the upstream `fd` binary directly. WASI compatibility stays in dependency + patches: `fd-find` gets narrow file-type/path/receiver adjustments, and + `ignore` uses a serial walker on `wasm32-wasip1` instead of host threads. Proof: + clean patch dry-runs pass in + `2026-07-08T06-19-50-0700-fd-find-patch-clean-dry-run.log` and + `2026-07-08T06-26-52-0700-fd-ignore-patch-final-dry-run.log`; + clean upstream WASM build compiles `fd-find v10.4.2` in + `2026-07-08T06-21-54-0700-fd-upstream-wasm-rebuild-after-ignore-fix.log`; + runtime/package command hashes match in + `2026-07-08T06-22-42-0700-fd-copy-built-binary-hashes.log`; package build and + check-types pass in + `2026-07-08T06-24-27-0700-fd-package-build.log` and + `2026-07-08T06-25-41-0700-fd-check-types-final.log`; e2e fd tests pass 9/9, + including `fd --version` reporting `fd 10.4.2`, in + `2026-07-08T06-25-00-0700-fd-vitest-upstream-after-dir-format.log`. + Rev: `mrskpomv` — `fix(fd): build upstream fd-find`. +- **grep — DONE.** Replaced the `@agentos-software/grep` package's custom + `secureexec-grep` command wrapper with upstream GNU grep 3.12 from the official + GNU release tarball. The real GNU `grep` binary builds through the C toolchain; + `egrep` and `fgrep` are separate tiny WASM launchers that preserve GNU's + upstream obsolescent scripts by spawning `grep -E` / `grep -F` through the + AgentOS process broker. Sysroot fix stayed one layer down: wasi-libc no longer + advertises/exports its nonstandard two-argument `opendirat`, which conflicted + with gnulib's helper. Proof: official GNU release listing captured in + `2026-07-08T06-29-11-0700-grep-gnu-ftp-latest.log`; configure options captured + in `2026-07-08T06-29-39-0700-grep-upstream-configure-help-probe.log`; + sysroot `opendirat` patch check and rebuild pass in + `2026-07-08T06-34-27-0700-grep-wasi-libc-opendirat-patch-check-definition.log` + and `2026-07-08T06-34-33-0700-grep-sysroot-rebuild-opendirat-definition.log`; + GNU grep build passes in + `2026-07-08T06-35-01-0700-grep-gnu-wasm-build-after-opendirat-symbol.log`; + `egrep`/`fgrep` launcher builds pass in + `2026-07-08T06-40-07-0700-grep-egrep-wrapper-build.log` and + `2026-07-08T06-40-42-0700-grep-fgrep-wrapper-build.log`; package build and + check-types pass in `2026-07-08T06-42-24-0700-grep-package-build-final.log` + and `2026-07-08T06-42-18-0700-grep-check-types-final.log`; e2e grep tests + pass 8/8 in `2026-07-08T06-43-59-0700-grep-vitest-after-wrapper-cache-repair.log`. + Rev: `uyukolvr` — `fix(grep): build upstream GNU grep`. +- **ripgrep — DONE.** Replaced the `@agentos-software/ripgrep` package's custom + `secureexec-grep` recursive search shim with upstream ripgrep 15.1.0 from the + canonical `BurntSushi/ripgrep` crate/release. The local `cmd-rg` crate is now + only a build trigger, and `toolchain/Makefile` builds ripgrep's own `rg` bin + directly. The old `secureexec-grep` library is gone. Proof: latest upstream + release captured in `2026-07-08T06-50-40-0700-ripgrep-github-latest-release.json`; + crate metadata captured in `2026-07-08T06-52-00-0700-ripgrep-cargo-info.log`; + upstream WASM build passes in + `2026-07-08T06-56-00-0700-ripgrep-upstream-wasm-build-after-grep-dir-remove-fixed.log`; + package build and check-types pass in + `2026-07-08T06-58-55-0700-ripgrep-package-build-after-install.log` and + `2026-07-08T06-58-56-0700-ripgrep-check-types-after-install.log`; e2e ripgrep + tests pass 8/8 in `2026-07-08T07-02-00-0700-ripgrep-vitest-after-git-fixture.log`. + Rev: `msypkqmo` — `fix(ripgrep): build upstream ripgrep`. +- **zip / unzip — DONE.** Replaced both custom minizip-based C wrappers with real + upstream Info-ZIP Zip 3.0 and UnZip 6.0 release tarballs, built through the C + toolchain. Runtime/sysroot fixes stayed one layer down: temp-file, ownership, + `system`/`popen`/`pclose` compatibility in patched wasi-libc; overlay rename + over destination whiteouts; and WASI host-passthrough read/write offset + tracking after `fd_seek`. Proof: wasi-libc patch check passes in + `2026-07-08T08-34-29-0700-wasi-libc-patch-check-final.log`; native sidecar + build passes in `2026-07-08T08-34-07-0700-sidecar-build-final-runner-format.log`; + VFS rename regression passes in + `2026-07-08T08-34-29-0700-vfs-core-rename-whiteout-final.log`; final Zip e2e + passes 2/2 in + `2026-07-08T08-36-45-0700-zip-test-final-after-current-sysroot-copy-after-install.log`; + final UnZip e2e passes 6/6 in + `2026-07-08T08-37-12-0700-unzip-test-final-after-current-sysroot-copy-after-install.log`. + Rev: `nppnuxpr` — `fix(infozip): build upstream zip and unzip`. +- **findutils — DONE.** Replaced the custom Rust `find` crate with upstream + `uutils/findutils` 0.9.0 entrypoints for both `find` and `xargs`. Kept the fix + in the toolchain/sysroot layer: Rust WASI metadata extensions and process + `ExitStatusExt::signal()` are exposed for crates that expect Unix-like std + surfaces, while `xargs` still uses normal `std::process::Command` spawning + through the existing VM broker. Proof: `find` wasm build passes in + `2026-07-08T12-42-00-0700-findutils-find-build-after-permissions-mode-patch.log`; + `xargs` wasm build passes in + `2026-07-08T12-45-00-0700-findutils-xargs-build-after-find-success.log`; + package build passes in + `2026-07-08T12-51-00-0700-findutils-package-build-after-install.log`; sidecar + validation build passes in + `2026-07-08T12-35-00-0700-native-sidecar-build-after-forced-pnpm.log`; final + package e2e passes 5/5, including `xargs -n 2 echo` spawn batching, in + `2026-07-08T12-44-00-0700-findutils-vitest-uutils-after-depth-test-fix.log`. + Rev: `msknmmps`. + +Ranked easiest→hardest: **sqlite3-CLI · http-get (drop) · tree · fd · grep · +ripgrep · zip · unzip · findutils.** + +## Status tracking (how the driver reports progress in this doc) + +Update this doc as you go — it is the single source of truth for status. For each +❌ command, set one status and keep it current: + +- **`TODO`** — not started. +- **`IN PROGRESS`** — being built; note the current blocker if any. +- **`DONE`** — the real/established tool builds and passes a real un-skipped e2e + test; link the jj rev. +- **`NOT POSSIBLE (WASI)`** — only after a genuine effort. Write a concrete + explanation: exactly what blocks it, what you tried (sysroot patch, tool patch, + syscall stub), and why it can't be made to work. This is a documented dead-end, + never a silent fallback to a custom rewrite. + +Mark each row's status inline in the table (or as a short line under the command) +so a reader sees the whole board at a glance. + +--- + +## P0 — Runtime / VM correctness + +### 1. brush-shell `>>` append truncates instead of appending — DONE +- **Broken:** `execSync` with `>>` onto a write-only file overwrites instead of + appends. `expected 'changed' to be 'originalchanged'`. (issue: rivet-dev/agentos#1657) +- **Objective:** `>>` opens `O_WRONLY|O_APPEND` against the kernel VFS and appends, + identical to bash on Linux. +- **Proof:** `bridge-child-process.test.ts` append redirection tests pass + un-skipped; direct kernel append and native sidecar append regressions pass. +- **rev:** `ouxrzutq` — `fix(runtime): honor >> append mode in guest shell VFS redirection` + +### 2. brush-shell `cat < file` stdin redirection fails (exit 1) — DONE +- **Broken:** `cat < stdin-input.txt` exits 1 — input redirection from a VFS path + isn't wired to the command's stdin. (issue: #1657) +- **Objective:** `< file` feeds the VFS file to stdin; command reads it and exits 0, + like Linux. +- **Proof:** the "stdin redirection feeds the kernel VFS file" test passes + un-skipped after the parent host-shadow pre-spawn sync fix in item 1. +- **rev:** `lonnzuqw` — `test(registry): mark stdin redirection parity proven` + +### 3. WasmVM signal/dispose — SIGKILL/SIGTERM don't terminate; dispose hangs — DONE +- **Broken:** SIGKILL/SIGTERM don't kill guest processes; `dispose` times out + (5 tests across `signal-forwarding.test.ts`, `dispose-behavior.test.ts`). +- **Objective:** signals delivered to guest processes terminate them promptly and + `dispose` tears down active WasmVM + Node processes, matching Linux signal + semantics. **Not yet filed — file a separate issue.** +- **Proof:** `signal-forwarding.test.ts` passes 5/5 in + `2026-07-07T23-11-36-0700-item3-signal-forwarding-final-pass-2.txt`; + `dispose-behavior.test.ts` passes 3/3 in + `2026-07-07T23-11-21-0700-item3-dispose-behavior-final-pass.txt`. +- **rev:** `zkywnwup` — `fix(runtime): unblock WasmVM signal waits and dispose` + +### 4. VFS missing `pwrite` — sqlite3 file-backed DBs don't persist — DONE +- **Broken:** `filesystem method pwrite is unavailable` — sqlite3 file-backed DB + can't persist across exec calls. +- **Objective:** the VFS implements positioned writes (`pwrite`/`pwritev`) so any + command doing positioned I/O (sqlite3, and others) behaves like Linux. +- **Proof:** sqlite3 "file-based DB persists across separate exec calls" passes + in `2026-07-07T23-18-45-0700-item4-sqlite3-file-db-pwrite-pass.txt`; direct + mounted JS VFS `pwrite` test passes in + `2026-07-07T23-18-45-0700-item4-runtime-core-custom-vfs-pwrite-pass.txt`. + Type/build checks pass in `2026-07-07T23-19-11-0700-item4-runtime-core-build.txt` + and `2026-07-07T23-19-11-0700-item4-sqlite3-check-types.txt`. +- **rev:** `klrzzkro` — `fix(vfs): expose positioned writes in test kernel` + +### 5. Socket-layer failures (net-server/udp/unix, signal_handler) — DONE +- **Broken:** in the audit run, `st.create is not a function` + a `LinkError` in + net tests; signal_handler didn't catch signals. May be partial-build artifacts. +- **Objective:** TCP/UDP/Unix socket + signal test programs run to completion in + the VM with real socket semantics. **First reconfirm on a full build** — if it + reproduces, fix the socket-table wiring / link error. +- **Proof:** net-server/net-udp/net-unix/signal-handler suites pass together in + `2026-07-08T00-23-43-0700-item5-four-suites-take-signal-bridge.txt`. + Runtime and native sidecar builds pass in + `2026-07-08T00-24-02-0700-item5-final-runtime-core-build.txt` and + `2026-07-08T00-24-02-0700-item5-final-native-sidecar-build.txt`; native + embedded signal coverage passes in + `2026-07-08T00-24-02-0700-item5-final-native-embedded-runtime-signal-suite.txt`. +- **rev:** `zvyxkkyv` — `fix(runtime): repair Wasm socket and signal integration` + +--- + +## P1 — Broken shipped commands + +### 6. curl — reimplemented CLI, exits 1 on every operation (incl. `--version`) — DONE +- **Broken:** the `curl` command is a **hand-rolled `curl.c` driver** over a + libcurl fork, not the real curl command-line tool — so 24/30 `curl.test.ts` fail + and every op returns exit 1, even `curl --version`. +- **Objective (per Cross-cutting #0):** **build the real curl command-line tool** + (upstream `src/tool_*.c`) to `wasm32-wasip1` against the patched sysroot, + patched only where WASI forces it — replacing the custom driver. All real curl + behavior (GET/POST, `-I`/`-D`, `-L`, `-u`, `-F`, `-o`/`-O`, `-w`, `-K`) then + works because it *is* curl, not a shim. +- **Proof:** `software/curl/test/` passes un-weakened: 25 passed, 5 skipped in + `2026-07-08T00-41-57-0700-item6-curl-test-after-tls-flags.txt`. Runtime runner + build/protocol checks pass in + `2026-07-08T00-41-51-0700-item6-runtime-core-build-tls-flags.txt`. +- **rev:** `oxoqrwvk` — `fix(curl): build the real curl CLI for WASI` +- **Note (how well it works):** it *is* the real curl CLI (`src/tool_main.c`) plus a + custom `vtls/wasi_tls.c` backend (`USE_WASI_TLS`) — HTTPS runs through the host + TLS bridge, not OpenSSL. Real HTTP(S) `GET/POST/-I/-D/-L/-u/-F/-o/-O/-w/-K` all + work because it's genuine curl. Known gaps from the trimmed `./configure`: + `--compressed`/gzip response decode (`--without-zlib`), brotli/zstd, `libpsl` + cookie-suffix checks, LDAP, and no CA bundle (cert trust is whatever `wasi_tls` + enforces). Those are the 5 skipped tests. Verdict: solid for real HTTP(S). + +### 7. zip / unzip — replace custom wrappers with real Info-ZIP — DONE +- **Broken:** the shipped commands were custom C wrappers over zlib/minizip, not + real Zip/UnZip. The old fallback parser also diverged from hardened Linux unzip + behavior on wrapping local offsets, empty normalized names, and hostile size + declarations. +- **Objective:** build real upstream Info-ZIP Zip and UnZip to `wasm32-wasip1`, + patching the AgentOS sysroot/runtime where needed, and prove real zip↔unzip + roundtrips plus malformed-archive rejection in VM e2e tests. +- **Resolved:** upstream Info-ZIP Zip 3.0 and UnZip 6.0 now build from release + tarballs through the C toolchain. The custom `software/zip/native/c/zip.c` and + `software/unzip/native/c/unzip.c` sources are deleted. Required compatibility + lives one layer down: patched wasi-libc temp-file, ownership, `system`, `popen`, + and `pclose` surfaces; VFS overlay rename-over-whiteout cleanup; and + host-passthrough `fd_seek` offset tracking in the WASI runner. +- **Proof:** wasi-libc patch check passes in + `2026-07-08T08-34-29-0700-wasi-libc-patch-check-final.log`; native sidecar + build passes in `2026-07-08T08-34-07-0700-sidecar-build-final-runner-format.log`; + VFS rename regression passes in + `2026-07-08T08-34-29-0700-vfs-core-rename-whiteout-final.log`; `software/zip` + e2e passes 2/2 in + `2026-07-08T08-36-45-0700-zip-test-final-after-current-sysroot-copy-after-install.log`; + `software/unzip` e2e passes 6/6 in + `2026-07-08T08-37-12-0700-unzip-test-final-after-current-sysroot-copy-after-install.log`. +- **rev:** `nppnuxpr` — `fix(infozip): build upstream zip and unzip` + +--- + +## P2 — Build / compile failures + +### 8. wget — DONE +- **Resolved:** the `wget` command is real upstream GNU Wget 1.24.5 built for + `wasm32-wasip1`, HTTP-only for now (`--without-ssl --without-zlib + --without-libpsl --disable-iri`) against the patched AgentOS C sysroot. The old + custom 174-line wrapper stays removed. +- **Sysroot/runtime fixes:** Wget builds without a Wget-source WASI fork by adding + the missing POSIX surface one layer down: process/terminal headers including + `spawn.h`, signal/process/timezone compatibility, overrideable `FD_SETSIZE`, + Wget-only `_POSIX_TIMERS` overlay, POSIX socket `read`/`write` routing through + `host_net`, low host-net fds, and `MSG_PEEK` queue preservation in the WASM + runner. Configure is seeded so gnulib trusts the sysroot `select` instead of + replacing it with a host-net-incompatible fallback. +- **Proof:** focused basename download passes in + `2026-07-08T04-33-31-0700-item8-wget-vitest-focused-clean-msg-peek.log`; full + Wget e2e suite passes 5/5 in + `2026-07-08T04-33-41-0700-item8-wget-vitest-full-clean-msg-peek.log`. Final + runner syntax and wasi-libc patch checks pass in + `2026-07-08T04-34-02-0700-item8-node-check-wasm-runner-final.log` and + `2026-07-08T04-34-02-0700-item8-wasi-libc-patch-check-final.log`. +- **rev:** `zuosnzmq` — `fix(wget): build real GNU Wget for WASI` + +### 9. codex-cli — DONE +- **Resolved:** the `codex`/`codex-exec` package now has an AgentOS-owned wrapper + for the external `codex-rs` fork build. `make -C toolchain codex-required` + requires `CODEX_REPO=/path/to/codex-rs/codex-rs`, uses this checkout's + `toolchain/c/vendor/wasi-sdk`, and installs the fork-built optimized wasm into + generated `software/codex/wasm/{codex-exec,codex}` for the package build. The + generated toolchain and wasm command directories are ignored and not committed. +- **Test fix:** the real `codex-exec --session-turn` e2e now uses a streaming + Responses mock (SSE) and disables Codex shell snapshots inside the VM config, + avoiding the optional pre-turn shell-snapshot subprocess deadlock while still + driving the real codex-core agent and shell tool path. +- **Proof:** `CODEX_REPO=/home/nathan/agent-e2e/codex-rs/codex-rs make -C + toolchain codex-required` builds and installs 29,924,651-byte command artifacts + in `2026-07-08T01-37-05-0700-item9-codex-build-rerun.txt`; `pnpm --dir + software/codex-cli build` stages 2 commands and assembles `package.aospkg` in + `2026-07-08T01-44-50-0700-item9-codex-cli-build.txt`; + `AGENTOS_E2E_FULL=1 pnpm --dir packages/core exec vitest run + tests/codex-fullturn.test.ts --reporter=verbose` passes 2 real VM tests in + `2026-07-08T01-53-55-0700-item9-core-codex-fullturn-pass.txt`. +- **rev:** `svksnzon` — `build(codex-cli): make the codex-rs fork build reproducible` + +### 10. vix — DONE (deleted) +- **Resolved:** `vix` was a from-scratch, source-less drop-zone binary — exactly + the kind of hand-rolled artifact this repo should not carry. **Removed entirely** + (package dir, shell import/dep, `EXTERNAL_COMMANDS` Makefile hack, README rows, + website registry entry) in rev + `chore(registry): remove vix package; document real-tool (no-reimplementation) principle`. + Real `vim` (#11) covers the editor slot. Preserved source (`vix.c`, `BUILD-vix.md`, + `vix.wasm`) remains in `~/progress/agent-os/2026-06-28-just-shell-fix/` if ever + needed. No further work. + +--- + +## P3 — Disabled / absent coverage (real tests to Linux parity) + +For each: replace `describe.skip` with `describeIf(binaryPresent)` **and** write +real e2e tests that prove Linux-parity behavior — not smoke tests. + +### 11. Disabled suites — git, duckdb, codex — DONE +- **Fixed:** the Git quickstart, DuckDB package, and Codex full-turn suites are no + longer excluded from the default core Vitest file set, so coverage cannot + disappear when the package artifacts are present. +- **Status:** + - **git — DONE.** The core quickstart e2e now exercises real upstream Git in a + VM for local origin creation, commit, branch, clone, checkout, `log`, and a + working-tree `diff`. It validates only the git package it uses instead of + eagerly requiring every registry package to be built. Proof: + `2026-07-08T13-13-00-0700-item11-git-quickstart-final.log`. + Rev: `svltqsmx`. + - **duckdb — DONE.** Rebuilt the upstream DuckDB package artifact from the + patched C sysroot and strengthened the package e2e so it validates only the + DuckDB/Curl packages it uses. The VM e2e now covers file-backed DML, real + analytical SQL, DuckDB CSV export, `read_csv_auto` re-import, and the + negative HTTP-URL path for DuckDB itself. Proof: + `2026-07-08T12-38-07-0700-item11-duckdb-package-build-after-install.log`; + `2026-07-08T12-46-18-0700-item11-duckdb-package-e2e-final-file-pass.log`; + `2026-07-08T12-46-11-0700-item11-core-tsc-duckdb-final-file-after-refresh.log`; + `2026-07-08T12-46-10-0700-item11-duckdb-biome-check-final-file-after-refresh.log`. + Rev: `qrwnvouk`. + - **codex — DONE.** Un-skipped the remaining real `codex-exec --session-turn` + full-turn coverage. The suite now proves the model turn, on-request shell + tool call, real subprocess filesystem side effect, and adapter-supplied + history replay in one VM-backed file with 4/4 passing. For this coverage + rev, the package was staged from existing real 29 MB `codex`/`codex-exec` + artifacts because rebuilding the external Codex fork in this checkout hit + dependency gating failures (`path-dedot`, then `tokio`/`rustls-native-certs`); + the failed rebuild logs are kept as proof. Proof: + `2026-07-08T12-52-54-0700-item11-codex-cli-package-build-from-prior-artifacts-after-install.log`; + `2026-07-08T12-54-08-0700-item11-codex-fullturn-final-unskipped.log`; + `2026-07-08T12-54-42-0700-item11-core-tsc-codex-final.log`; + `2026-07-08T12-54-42-0700-item11-codex-biome-check-final.log`; + rebuild blockers: + `2026-07-08T12-48-46-0700-item11-codex-required-build-fresh-cargo-home.log`, + `2026-07-08T12-50-38-0700-item11-codex-required-build-path-dedot-cfg.log`. + Rev: `ryqtvoqv`. +- **Final proof:** after removing the remaining core Vitest exclusions, the + explicit suites pass with real VM coverage: Git quickstart 1/1 in + `2026-07-08T14-35-00-0700-item11-status-git-quickstart-final.log`; DuckDB + package 4/4 in + `2026-07-08T14-36-00-0700-item11-status-duckdb-package-final.log`; Codex + full-turn 4/4 in + `2026-07-08T14-37-00-0700-item11-status-codex-fullturn-final.log`. +- **rev:** `test(core): include registry parity suites by default` + +### 12. No tests at all — 9 software + 5 agents — DONE +- **Broken:** zero e2e coverage: `gawk, sed, tar, gzip, jq, yq, diffutils, + file, vim`; agents `claude, codex, opencode, pi, pi-cli`. +- **Status:** + - **gawk — DONE.** Added package-local VM e2e coverage for the staged `awk` + command. The suite proves file-backed field extraction, explicit field + separators, numeric aggregation, `-f` script-file execution, and missing + input-file errors through the packaged WASM command. Proof: + `2026-07-08T13-16-03-0700-item12-gawk-package-e2e-after-install.log`; + `2026-07-08T13-16-03-0700-item12-gawk-check-types-after-install.log`. + Biome is not applicable for this package test path; it reported the file is + ignored by config in + `2026-07-08T13-16-16-0700-item12-gawk-biome-check.log`. Rev: `pzxkurol`. + - **sed — DONE.** Added package-local VM e2e coverage for the staged `sed` + command. The suite proves file-operand substitutions, addressed `-n` + printing, addressed deletion, multiple `-e` expressions, and missing + input-file errors through the packaged WASM command. Proof: + `2026-07-08T13-17-46-0700-item12-sed-package-e2e-initial.log`; + `2026-07-08T13-17-46-0700-item12-sed-check-types-initial.log`. + Biome is not applicable for this package test path; it reported the file is + ignored by config in + `2026-07-08T13-17-55-0700-item12-sed-biome-check.log`. Rev: `wvpklkqv`. + - **tar — DONE.** Added package-local VM e2e coverage for the staged `tar` + command and tightened the tar wrapper for Linux-like directory listing and + missing-input error context. The suite proves archive creation/listing, + extraction into `-C` directories, gzip auto-detection by extension, + `--strip-components`, and missing create-input errors through the packaged + WASM command. Proof: + `2026-07-08T13-21-48-0700-item12-tar-toolchain-cmd-build-clean-vendor.log`; + `2026-07-08T13-22-51-0700-item12-tar-package-build-after-wrapper-fix.log`; + `2026-07-08T13-23-02-0700-item12-tar-package-e2e-final.log`; + `2026-07-08T13-23-02-0700-item12-tar-check-types-final.log`; + `2026-07-08T13-23-02-0700-item12-tar-cargo-fmt-check-final.log`. + Biome is not applicable for this package test path; it reported the file is + ignored by config in + `2026-07-08T13-23-12-0700-item12-tar-biome-check.log`. Rev: `rszmulmk`. + - **gzip — DONE.** Added package-local VM e2e coverage for the staged + `gzip`/`gunzip`/`zcat` commands. The suite proves file compression with + `-k`, source removal without `-k`, `gunzip -fk`, `zcat` streaming, and + overwrite protection without `-f` through the packaged WASM commands. Proof: + `2026-07-08T13-25-01-0700-item12-gzip-package-e2e-initial.log`; + `2026-07-08T13-25-01-0700-item12-gzip-check-types-initial.log`. + Biome is not applicable for this package test path; it reported the file is + ignored by config in + `2026-07-08T13-25-12-0700-item12-gzip-biome-check.log`. Rev: `tlstlwvy`. + - **yq — DONE.** Added package-local VM e2e coverage for the staged `yq` + command and fixed the wrapper to accept file operands instead of only + stdin. The suite proves YAML filtering, YAML-to-JSON query output, + explicit JSON/TOML/XML input formats, and invalid YAML parse errors through + the packaged WASM command. Proof: + `2026-07-08T13-27-36-0700-item12-yq-toolchain-cmd-build.log`; + `2026-07-08T13-28-59-0700-item12-yq-package-build-after-file-operands.log`; + `2026-07-08T13-29-10-0700-item12-yq-package-e2e-final.log`; + `2026-07-08T13-29-10-0700-item12-yq-check-types-final.log`; + `2026-07-08T13-29-11-0700-item12-yq-cargo-fmt-check-final.log`. + Biome is not applicable for this package test path; it reported the file is + ignored by config in + `2026-07-08T13-29-23-0700-item12-yq-biome-check.log`. Rev: `znlmtymu`. + - **diffutils — DONE.** Added package-local VM e2e coverage for the staged + `diff` command and fixed recursive directory output to print the compared + file pair before hunk output, matching the Linux `diff -r` shape. The suite + proves identical-file exit 0, normal and unified diffs, brief output, + ignore-case/whitespace/blank-line flags, recursive directory comparisons, + and missing-input errors through the packaged WASM command. Proof: + `2026-07-08T13-32-10-0700-item12-diffutils-toolchain-cmd-build.log`; + `2026-07-08T13-34-45-0700-item12-diffutils-package-build-after-recursive-header.log`; + `2026-07-08T13-35-04-0700-item12-diffutils-package-e2e-final.log`; + `2026-07-08T13-35-04-0700-item12-diffutils-check-types-final.log`; + `2026-07-08T13-35-04-0700-item12-diffutils-cargo-fmt-check-final.log`. + Biome is not applicable for this package test path; it reported the file is + ignored by config in + `2026-07-08T13-35-04-0700-item12-diffutils-biome-check.log`. + - **file — DONE.** Added package-local VM e2e coverage for the staged `file` + command and fixed shebang script classification to run before generic magic + detection, producing Linux-like script descriptions instead of raw + `text/x-shellscript`. The suite proves text, JSON, script, PNG, empty-file, + directory, brief, MIME, stdin, and missing-input behavior through the + packaged WASM command. Proof: + `2026-07-08T13-39-36-0700-item12-file-toolchain-cmd-build.log`; + `2026-07-08T13-40-33-0700-item12-file-package-build-after-shebang-fix.log`; + `2026-07-08T13-41-05-0700-item12-file-package-e2e-final-after-install.log`; + `2026-07-08T13-41-05-0700-item12-file-check-types-final-after-install.log`; + `2026-07-08T13-40-47-0700-item12-file-cargo-fmt-check-final.log`. + Biome is not applicable for this package test path; it reported the file is + ignored by config in + `2026-07-08T13-41-05-0700-item12-file-biome-check-final-after-install.log`. + - **vim — DONE.** Built/staged the real upstream Vim command without app + source forks by keeping terminal/process compatibility in the patched C + sysroot, disabling host Wayland/dlfcn detection at configure time, and + trimming the Vim-local bridge down to package-specific gaps. Added + package-local VM e2e coverage that proves the packaged binary starts with + `-libcall` and edits/writes a file in Ex mode with the packaged runtime. + Proof: + `2026-07-08T14-03-20-0700-item12-vim-toolchain-cmd-build-final-ioctl.log`; + `2026-07-08T14-04-03-0700-item12-vim-package-build-final.log`; + `2026-07-08T14-04-03-0700-item12-vim-check-types-final.log`; + `2026-07-08T14-04-09-0700-item12-vim-package-e2e-final.log`. + Biome is not applicable for this package test path; it reported the file is + ignored by config in + `2026-07-08T14-04-51-0700-item12-vim-biome-check.log`. + - **jq — DONE.** Added package-local VM e2e coverage for the staged `jq` + command and fixed the jaq-backed CLI wrapper to accept Linux-style file + operands instead of only stdin. The suite now proves version output, + file-backed array filtering, aggregate JSON construction, slurped NDJSON, + and invalid JSON parse errors through the packaged WASM command. Proof: + `2026-07-08T13-10-55-0700-item12-jq-toolchain-cmd-build-isolated-target.log`; + `2026-07-08T13-12-18-0700-item12-jq-package-build-after-wrapper-fix.log`; + `2026-07-08T13-12-51-0700-item12-jq-package-e2e-final.log`; + `2026-07-08T13-12-51-0700-item12-jq-check-types-final.log`; + `2026-07-08T13-12-51-0700-item12-jq-cargo-fmt-check-final.log`. + Rev: `slnmvuqz`. + - **pi — DONE.** Enabled the existing real `createSession('pi')` headless + suite in default core Vitest coverage and unskipped the upstream Pi SDK bash + tool path. The suite proves initialization over the native sidecar + transport plus real ACP write-tool and bash-tool flows inside the VM. Proof: + `2026-07-08T14-37-00-0700-item12-cc-cache-restored-target-files.log`; + `2026-07-08T14-37-00-0700-item12-sidecar-build-after-manual-cc-restore.log`; + `2026-07-08T14-38-00-0700-item12-pi-headless-final-after-cc-restore.log`. + Rev: `mzuuypsm`. + - **pi-cli — DONE.** Enabled the existing real `createSession('pi-cli')` + headless suite in default core Vitest coverage and unskipped the unmodified + Pi CLI bash-tool flow. Pi and pi-cli now project registry command software + only when the local `.aospkg` artifacts are built, so write-tool coverage + still runs while bash-tool coverage is gated on real command availability. + Proof: + `2026-07-08T15-00-00-0700-item12-pi-cli-cc-cache-restored-target-files-final.log`; + `2026-07-08T15-00-00-0700-item12-pi-cli-sidecar-build-after-cc-restore-final.log`; + `2026-07-08T15-01-00-0700-item12-pi-cli-focused-final-after-cc-restore.log`. + Rev: `xqtkmsyn`. + - **claude — DONE.** Enabled the existing real `createSession('claude')` + session suite in default core Vitest coverage, projected the actual + `@agentos-software/claude-code` agent package, and replaced the missing + test-only `xu` binary dependency with a real PATH-backed `sh` command from + registry coreutils. The suite proves Claude ACP shell/tool flow, text-only + responses, nested Node `execSync`/`spawn`, session metadata/lifecycle, + modes, and raw ACP sends. The flat node_modules fixture cache now lives + outside root `node_modules` so VM mounts survive dependency refreshes. + Proof: + `2026-07-08T15-17-00-0700-item12-claude-cc-cache-restored-after-cache-move.log`; + `2026-07-08T15-17-00-0700-item12-claude-sidecar-build-after-cache-move.log`; + `2026-07-08T15-19-00-0700-item12-claude-session-after-cache-move.log`. + Rev: `rxmoulty`. + - **opencode — DONE.** Added default core Vitest coverage for real + `createSession('opencode')` initialization through the projected + `@agentos-software/opencode` agent package. The focused suite proves the + sidecar resolves the OpenCode ACP package, initializes the adapter, exposes + agent metadata/capabilities/modes, and registers the session through the + Agent OS session API. Proof: + `2026-07-08T15-36-00-0700-item12-opencode-cc-cache-restored-focused.log`; + `2026-07-08T15-36-00-0700-item12-opencode-sidecar-build-focused.log`; + `2026-07-08T15-37-00-0700-item12-opencode-real-session-final.log`. + Rev: `xtnuomsw`. + - **codex — DONE.** Codex does not currently expose a runnable + `createSession('codex')` ACP package: `@agentos-software/codex` is a + registry/package wrapper and `codex-session.test.ts` verifies the sidecar + rejects it as a session agent. The real Codex agent coverage is the + `codex-exec --session-turn` VM path from item 11, which drives the real + codex-core turn loop against a mock OpenAI Responses server and proves a + real shell subprocess side effect. Proof: + `2026-07-08T15-43-00-0700-item12-codex-cc-cache-restored-target-files.log`; + `2026-07-08T15-43-00-0700-item12-codex-sidecar-build-before-fullturn.log`; + `2026-07-08T15-44-00-0700-item12-codex-fullturn-current-stack.log`; + `2026-07-08T15-45-00-0700-item12-codex-session-negative-current-stack.log`. + Rev: `kyswsrtv`. +- **Objective:** write real e2e tests proving each behaves like its Linux + counterpart (jq processes real JSON, sed edits streams, tar round-trips archives, + gzip round-trips, etc.); agents exercise the real ACP + adapter against the upstream SDK. +- **Proof:** `software//test/` exists and passes for each; coverage gate green. +- **rev:** one per package, e.g. `test(jq): add real JSON-processing e2e` + +--- + +## Cross-cutting / misc + +### 13. `everything` meta-package has no `agentos-package.json` — DONE +- **Fixed:** added the missing meta-package manifest, aligned the bundle with all + current command packages (`duckdb`, `envsubst`, `git`, `sqlite3`, and `vim` + were missing), and refreshed the workspace lockfile edges. +- **Proof:** `software/everything/test/everything.test.ts` proves the manifest is + present and the default export resolves every command package descriptor once. + Package build/check-types pass in + `2026-07-08T14-21-00-0700-item13-everything-build-after-install.log` and + `2026-07-08T14-21-00-0700-item13-everything-check-types-after-install.log`; + the package test passes 2/2 in + `2026-07-08T14-21-00-0700-item13-everything-test-after-install.log`; + layout validation passes in + `2026-07-08T14-22-00-0700-item13-check-layout.log`. +- **rev:** `fix(everything): add valid package manifest` + +--- + +## Sequencing note + +P0 first — several P1/P3 items depend on it: curl (#6) needs sockets/HTTP; +sqlite3 file-DB tests (#11) need pwrite (#4). Fix the runtime layer, then the +commands that ride on it, then backfill coverage. One jj rev per item throughout. + +--- + +## Candidate software (future additions) + +Constraint: **C or Rust only** (no Go/Haskell/Python). Real upstream tool or an +established project, same rules as above. **Focus: tools agents invoke headless +and programmatically** — no TUI/visual tools, no dev/build toolchains, no +raw-socket tools. Feasibility: 🟢 easy (fs/compute), 🟡 needs a known pattern +(spawn→`wasi-spawn`, PTY, host TCP/DNS bridge). + +**Atuin-validated priority (from 348K real shell commands):** by actual usage the +clear wins are **jj** (18,929 — 3rd overall after sed/rg/grep) and **tmux** +(1,268), then **perl** (563). Modest but real: ssh/rsync (~23 each), psql (20), +dig (10), redis-cli (8), less (4), openssl (3). **Zero usage in this history:** +zstd, xz, gpg, ffmpeg, age, mlr, socat, nc, screen — generically useful but not +agent-triggered here, so lower priority. Big unserved *demand*: +**ps (1,364) + pkill (866) + pgrep (755) ≈ 3K** — see process management below. + +**Cross-source validation (Homebrew + Debian popcon + agent-sandbox base images + +SWE-agent/OpenHands/Terminal-Bench trajectories):** independent sources converge +tightly on the list below. **Every agent-sandbox image (OpenHands, devcontainers, +GH Actions) pre-installs:** curl, wget, git, jq, tmux, gnupg, xz, zip/unzip, rsync, +ssh, less, vim, tree, procps, psmisc, socat, netcat, ripgrep, bzip2, lz4, sqlite3, +patch, file — near-exact overlap with what we ship/plan. Real **agent +trajectories** are dominated by coreutils (ls/rm/find/mkdir/cat/mv/chmod) + python ++ curl/wget + grep/sed + openssl + ps — all shipped or listed. New adds surfaced: +**imagemagick** ⭐ (C, image ops — high popularity), **openssl** (confirmed +high-use), **aria2 / brotli / parallel / sshpass** (base-image staples). Method +signals worth knowing: (1) agents **edit files ~2:1 over running shell commands**, +and the dominant shell idiom is "write a python repro script, run it, `rm` it" — +so a solid coreutils + python + curl/grep/sed/openssl core matters more than tool +breadth; (2) `git` is **rare inside agent turns** (harnesses extract the diff +out-of-band) but stays essential; (3) a **long tail of project-specific CLIs** +(`dvc`, `sqlglot`, `sanic`, …) comes from pip/npm install, not the registry. + +**Requested (add):** ssh ✅, rsync 🟡, tmux/screen 🟡 (PTY — session persistence), +gpg 🟡, ffmpeg 🟡 (media transcode — heavy but headless), jj 🟢, dig 🟡, +nslookup 🟡, less ⭐🟡 (pager), openssl ⭐🟡 (TLS/certs/keys/hashing). +tail/head/cat are already in coreutils — confirm present. + +OpenSSH now carries a private, static OpenSSL 3.5.7 **libcrypto** dependency for +its standard software crypto algorithms. The `openssl` command remains a future +registry addition: libssl, providers/modules, and the CLI are not shipped, and +curl/wget/git continue to use mbedTLS. + +**Text / stream:** less ⭐🟡, **perl** ⭐🟡 (ubiquitous `-pe`/`-ne` text munging — +big C runtime but real; 563 uses in history), miller `mlr` 🟢 (CSV/JSON), +xmlstarlet 🟢, pcre2grep 🟢. (jq/yq/sed/awk/grep/head/tail already covered.) + +**Networking (host TCP/DNS bridge only):** openssl ⭐🟡, ssh ✅, nc/netcat 🟡 +(TCP/UDP), socat 🟡, whois 🟢, dig/nslookup 🟡, redis-cli / psql client 🟡, +aria2 🟡 (C++ downloader), sshpass 🟢 (ssh password helper). + +**VCS:** git (item above), jj 🟢. + +**Crypto:** gpg 🟡, openssl 🟡, age 🟢 (Rust), minisign 🟢. + +**Compression:** xz, zstd, bzip2, lz4, brotli, p7zip (7z) — all ⭐🟢, common + easy. + +**Media / image:** ffmpeg 🟡 (transcode), imagemagick ⭐🟡 (C, image ops — high +popularity; agents do image work). + +**Files / sync:** rsync 🟡, diff/patch 🟢, rename 🟢, fdupes 🟢 (find/fd tracked +above). + +**Session:** tmux/screen 🟡 (PTY). + +**Process management (add — real procps-ng + psmisc, C; ps/pkill/pgrep ≈ 3K uses):** +- **Need the `/proc` prerequisite (below):** ps, pgrep, pkill, pidof, pstree, + killall, uptime, free, vmstat, w, pwdx, pmap 🟡. +- **Signal-only — already work via the kernel (no /proc):** kill, killall-by-PID. + (kill, sleep, timeout, env, nohup, nproc, nice/renice are coreutils — confirm + they're shipped via uutils rather than re-adding.) + +**⚙️ Runtime prerequisite — implement `/proc` (process-table-backed):** procps +reads `/proc//{stat,cmdline,status,comm}` and enumerates `/proc//`. The +**kernel already owns the process table** (`crates/kernel/src/process_table.rs`), +so expose a read-only procfs view of it to the guest (per-PID stat/cmdline/status ++ directory enumeration). Scope it minimal — just the fields procps parses, backed +by the existing process table, not a full Linux procfs. Unlocks the whole +ps/pkill/pgrep family (and top/htop later if ever wanted). **This is a runtime/VFS +item, do it before the procps packages.** + +**Excluded — not worth it / not possible here:** +- **TUI / visual-only:** gitui, lazygit, eza, dust, ncdu, bat, delta, broot, k9s, + skim/fzf — a terminal UI has no agent value. +- **top / htop — excluded (TUIs).** (ps/pkill/pgrep and the rest of procps-ng + + psmisc are ADD items above, gated on the `/proc` runtime prerequisite.) +- **Raw sockets:** ping, traceroute, mtr, nmap (need raw/ICMP, not just TCP). +- **ptrace:** strace, ltrace, gdb, lldb, valgrind — genuinely impossible on WASI. +- **Dev / build toolchains:** make, cmake, clang/gcc, binutils, pkg-config, + ctags — out of scope. +- **Go-only:** rclone, gh, kubectl — no C/Rust equivalent. diff --git a/docs-internal/registry-test-standardization.md b/docs-internal/registry-test-standardization.md new file mode 100644 index 0000000000..8bffd5b217 --- /dev/null +++ b/docs-internal/registry-test-standardization.md @@ -0,0 +1,16 @@ +# Software test layout + +Status: superseded by `docs-internal/registry-flatten-colocation-spec.md`. + +The old registry-wide test plan assumed centralized package tests and a native +build tree under the removed wrapper directory. That layout has been replaced: + +- package e2e tests live under `software//test/`; +- shared VM test helpers live in the private `@agentos/test-harness` workspace + package; +- libc/sysroot conformance tests live under `toolchain/conformance/`; +- C VM test fixtures live under `toolchain/test-programs/`; +- native command source is colocated under `software//native/`, while + shared build infrastructure remains under `toolchain/`. + +Use `scripts/check-layout.mjs` for the current structural gate. diff --git a/docs/wasmvm/supported-commands.md b/docs/wasmvm/supported-commands.md index 3b40bc3ba2..eb036f31ed 100644 --- a/docs/wasmvm/supported-commands.md +++ b/docs/wasmvm/supported-commands.md @@ -213,7 +213,6 @@ | Command | just-bash | Status | Implementation | Target | |---------|-----------|--------|----------------|--------| | curl | yes | done | `curl_cli.c` (libcurl HTTP-only, C program via `host_net`) | — | -| wget | yes | done | `wget.c` (libcurl-based, C program via `host_net`) | — | ## Version Control diff --git a/examples/resource-limits/server.ts b/examples/resource-limits/server.ts index eba1a2af87..8037333fc3 100644 --- a/examples/resource-limits/server.ts +++ b/examples/resource-limits/server.ts @@ -13,6 +13,11 @@ const vm = agentOS({ maxWasmMemoryBytes: 128 * 1024 * 1024, // WASM linear memory maxWasmStackBytes: 4 * 1024 * 1024, // WASM call-stack ceiling }, + process: { + pendingStdinBytes: 64 * 1024 * 1024, // stdin waiting on a kernel pipe + pendingEventCount: 10_000, // queued process/runtime events per stage + pendingEventBytes: 64 * 1024 * 1024, // queued event payload per stage + }, jsRuntime: { v8HeapLimitMb: 128, // JS isolate heap cpuTimeLimitMs: 30_000, // active JS CPU time diff --git a/justfile b/justfile index 541c0f31e5..932e998c8c 100644 --- a/justfile +++ b/justfile @@ -8,33 +8,40 @@ release *args: release-preview REF: gh workflow run .github/workflows/publish.yaml --ref "{{ REF }}" -# --- @agentos-software/* registry packages (independent, PER-PACKAGE versions) --- -registry-native: - make -C registry/native commands +# --- @agentos-software/* software packages (independent, PER-PACKAGE versions) --- +toolchain-build: + make -C toolchain commands -registry-native-cmd name: - make -C registry/native cmd/{{ name }} +toolchain-cmd name: + make -C toolchain cmd/{{ name }} # Pre-flight for the publish "WASM Commands" job's fragile state: build the C # programs against the VANILLA wasi-sdk sysroot exactly like a fresh CI runner # (a locally-built patched sysroot is moved aside for the run). Catches # socket/netdb programs missing from PATCHED_PROGRAMS before CI does. -registry-native-preflight: +toolchain-preflight: #!/usr/bin/env bash set -euo pipefail - cd registry/native/c + cd toolchain/c if [ -e sysroot ]; then mv sysroot sysroot.preflight-stash; fi restore() { if [ -e sysroot.preflight-stash ]; then rm -rf sysroot; mv sysroot.preflight-stash sysroot; fi; } trap restore EXIT make wasi-sdk make programs -registry-copy-commands: +toolchain-copy-commands: node packages/runtime-core/scripts/copy-wasm-commands.mjs -registry-build: +software-build: pnpm --filter '@agentos-software/*' build +# Rebuild and stage the complete default WASM tool set from source. All outputs +# land in ignored build/bin/commands directories and must not be committed. +tools-rebuild: + just toolchain-build + just toolchain-copy-commands + just software-build + install-shell: #!/usr/bin/env bash set -euo pipefail diff --git a/package.json b/package.json index 4e38e9c42d..a17a126bfc 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "test:migration-parity": "pnpm --dir packages/core exec vitest run tests/migration-parity.test.ts --reporter=verbose", "test:post-python-parity": "pnpm --dir packages/core build && pnpm --dir packages/core exec vitest run tests/agentos-base-filesystem.test.ts", "test:watch": "pnpm exec turbo watch test", + "check-layout": "node scripts/check-layout.mjs", "check-types": "node scripts/verify-check-types.mjs && pnpm exec turbo check-types --concurrency=1", "lint": "pnpm biome check .", "fmt": "pnpm biome check --write --diagnostic-level=error .", @@ -22,14 +23,17 @@ "@biomejs/biome": "^2.3", "@copilotkit/llmock": "^1.6.0", "@mariozechner/pi-coding-agent": "^0.60.0", + "@agentos/test-harness": "workspace:*", "@rivet-dev/agentos-core": "workspace:*", "@agentos-software/claude-code": "workspace:*", "@agentos-software/codex": "workspace:*", "@agentos-software/common": "workspace:*", + "@rivet-dev/agentos-vm-test-harness": "workspace:*", "@rivet-dev/agentos-runtime-core": "workspace:*", "@agentos-software/pi": "workspace:*", "@types/node": "^22.19.15", "jszip": "^3.10.1", + "npm": "^11.18.0", "pdf-lib": "^1.17.1", "turbo": "^2.5.6", "typescript": "^5.9.2" diff --git a/packages/agentos-toolchain/README.md b/packages/agentos-toolchain/README.md index 72ce0f34ce..64180f005c 100644 --- a/packages/agentos-toolchain/README.md +++ b/packages/agentos-toolchain/README.md @@ -13,8 +13,8 @@ npx @rivet-dev/agentos-toolchain pack [options] packages lives here, next to the things it packages.** secure-exec owns the VM runtime that *runs* packages — the kernel, the VFS, the `/opt/agentos` mount, the `$PATH` command resolver, and the header/`binfmt` dispatch — and it owns the -package **definitions** themselves: the generic registry software (`registry/software/*`) -and the agent adapters (`registry/agent/*`). This toolchain is what builds those +package **definitions** themselves: the generic registry software (`software/*`) +and the agent adapters (`software/*`). This toolchain is what builds those definitions into the on-disk package format that the runtime resolves. Its `header.ts` is the same `binfmt` table the sidecar enforces (`crates/sidecar`), so keeping it in this repo keeps the producer and the consumer of the format in one diff --git a/packages/browser/tests/browser-wasm/pi-boot.spec.ts b/packages/browser/tests/browser-wasm/pi-boot.spec.ts index aeb5662d6d..a3e4b1b536 100644 --- a/packages/browser/tests/browser-wasm/pi-boot.spec.ts +++ b/packages/browser/tests/browser-wasm/pi-boot.spec.ts @@ -6,7 +6,7 @@ import { expect, test } from "@playwright/test"; // guest in the kernel-backed node-stdlib executor in real Chromium and speaks ACP. // // Requires the pi adapter bundle (built by scripts/build-wasm-test-assets when the -// registry/agent/pi dist + @mariozechner/pi-agent-core are present locally; skipped in +// software/pi dist + @mariozechner/pi-agent-core are present locally; skipped in // CI where they are absent). Runs the adapter in the executor's persistent-execution // mode (ExecOptions.persistent) so its async WHATWG-stream stdin pump can produce the // reply before the run completes. @@ -19,7 +19,7 @@ test("the real pi adapter boots in the converged executor and answers ACP initia const bundlePresent = await page.evaluate(() => fetch("/pi-adapter.bundle.cjs").then((r) => r.ok).catch(() => false), ); - test.skip(!bundlePresent, "pi adapter bundle not built (registry/agent/pi dist absent)"); + test.skip(!bundlePresent, "pi adapter bundle not built (software/pi dist absent)"); const result = await page.evaluate(() => window.__piBoot!.run()); diff --git a/packages/build-tools/bridge-src/builtins/child-process.ts b/packages/build-tools/bridge-src/builtins/child-process.ts index 7ad118c1d4..c2fc16016f 100644 --- a/packages/build-tools/bridge-src/builtins/child-process.ts +++ b/packages/build-tools/bridge-src/builtins/child-process.ts @@ -263,26 +263,28 @@ function routeChildProcessEvent(sessionId, type, data) { completeDetachedChildBootstrap(child); const wasConnected = child.connected; child.connected = false; - const signalCode = child._pendingSignalCode ?? (data && typeof data === "object" ? data.signal ?? null : null); + const signalCode = data && typeof data === "object" ? data.signal ?? null : null; const exitCode = data && typeof data === "object" ? data.code : data; child._pendingSignalCode = null; child.signalCode = signalCode; child.exitCode = signalCode == null ? exitCode : null; - child.stdout.emit("end"); - child.stderr.emit("end"); if (wasConnected) { child.emit("disconnect"); } - child.emit("close", child.exitCode, child.signalCode); child.emit("exit", child.exitCode, child.signalCode); - if (Array.isArray(child._inheritedFds)) { - for (const fd of child._inheritedFds) releaseChildInheritedFd(fd); - child._inheritedFds = []; - } - childProcessInstances.delete(sessionId); - if (typeof _unregisterHandle === "function") { - _unregisterHandle(`child:${sessionId}`); - } + child.stdout.emit("end"); + child.stderr.emit("end"); + queueMicrotask(() => { + child.emit("close", child.exitCode, child.signalCode); + if (Array.isArray(child._inheritedFds)) { + for (const fd of child._inheritedFds) releaseChildInheritedFd(fd); + child._inheritedFds = []; + } + childProcessInstances.delete(sessionId); + if (typeof _unregisterHandle === "function") { + _unregisterHandle(`child:${sessionId}`); + } + }); } } var childProcessDispatch = (eventTypeOrSessionId, payloadOrType, data) => { @@ -942,7 +944,7 @@ var ChildProcess = class { } } _complete(stdout, stderr, code) { - const signalCode = this._pendingSignalCode ?? this.signalCode; + const signalCode = this.signalCode; this._pendingSignalCode = null; this.signalCode = signalCode ?? null; this.exitCode = signalCode == null ? code : null; @@ -954,10 +956,10 @@ var ChildProcess = class { const buf = typeof Buffer !== "undefined" ? Buffer.from(stderr) : stderr; this.stderr.emit("data", buf); } + this.emit("exit", this.exitCode, this.signalCode); this.stdout.emit("end"); this.stderr.emit("end"); - this.emit("close", this.exitCode, this.signalCode); - this.emit("exit", this.exitCode, this.signalCode); + queueMicrotask(() => this.emit("close", this.exitCode, this.signalCode)); } }; function exec(command, options, callback) { @@ -1056,9 +1058,12 @@ function execSync(command, options) { JSON.stringify({ cwd: effectiveCwd, env: opts.env, + argv0: opts.argv0 == null ? void 0 : String(opts.argv0), input: opts.input == null ? null : encodeBridgeBytes(opts.input), maxBuffer, - shell: true + shell: true, + timeout: Number.isInteger(opts.timeout) && opts.timeout > 0 ? opts.timeout : null, + killSignal: normalizeChildProcessSignal(opts.killSignal).signalCode ?? "SIGTERM" }) ]); const result = typeof jsonResult === "string" ? JSON.parse(jsonResult) : jsonResult; @@ -1070,6 +1075,16 @@ function execSync(command, options) { result.stdout = typeof result.stdout === "string" ? "" : Buffer.from(""); } redirectSyncOutputToInheritedFd(execSyncStdio[2], result.stderr); + if (result.timedOut) { + const err = new Error(`spawnSync ${command} ETIMEDOUT`); + err.code = "ETIMEDOUT"; + err.status = result.signal == null && typeof result.code === "number" ? result.code : null; + err.signal = result.signal ?? null; + err.stdout = result.stdout; + err.stderr = result.stderr; + err.output = [null, result.stdout, result.stderr]; + throw err; + } if (result.maxBufferExceeded) { const err = new Error("stdout maxBuffer length exceeded"); err.code = "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"; @@ -1077,9 +1092,10 @@ function execSync(command, options) { err.stderr = result.stderr; throw err; } - if (result.code !== 0) { + if (result.code !== 0 || result.signal != null) { const err = new Error("Command failed: " + command); - err.status = result.code; + err.status = result.signal == null ? result.code : null; + err.signal = result.signal ?? null; err.stdout = result.stdout; err.stderr = result.stderr; err.output = [null, result.stdout, result.stderr]; @@ -1129,6 +1145,7 @@ function spawn(command, args, options) { JSON.stringify({ cwd: effectiveCwd, env: opts.env, + argv0: opts.argv0 == null ? void 0 : String(opts.argv0), shell: opts.shell === true || typeof opts.shell === "string", detached: opts.detached === true }) @@ -1264,6 +1281,7 @@ function spawnSync(command, args, options) { JSON.stringify({ cwd: effectiveCwd, env: opts.env, + argv0: opts.argv0 == null ? void 0 : String(opts.argv0), input: opts.input == null ? null : encodeBridgeBytes(opts.input), maxBuffer, shell: opts.shell === true || typeof opts.shell === "string", @@ -1291,8 +1309,8 @@ function spawnSync(command, args, options) { output: [null, stdoutValue, stderrValue], stdout: stdoutValue, stderr: stderrValue, - status: null, - signal: result.signal ?? killSignal, + status: typeof result.code === "number" && result.signal == null ? result.code : null, + signal: result.signal ?? null, error: err }; } @@ -1304,8 +1322,8 @@ function spawnSync(command, args, options) { output: [null, stdoutValue, stderrValue], stdout: stdoutValue, stderr: stderrValue, - status: result.code, - signal: null, + status: typeof result.code === "number" && result.signal == null ? result.code : null, + signal: result.signal ?? null, error: err }; } @@ -1314,8 +1332,8 @@ function spawnSync(command, args, options) { output: [null, stdoutValue, stderrValue], stdout: stdoutValue, stderr: stderrValue, - status: result.code, - signal: null, + status: typeof result.code === "number" && result.signal == null ? result.code : null, + signal: result.signal ?? null, error: void 0 }; } catch (err) { diff --git a/packages/build-tools/bridge-src/builtins/net.ts b/packages/build-tools/bridge-src/builtins/net.ts index baccdb55ac..35d82ed20c 100644 --- a/packages/build-tools/bridge-src/builtins/net.ts +++ b/packages/build-tools/bridge-src/builtins/net.ts @@ -220,8 +220,8 @@ function wakePeerBridgeReads(socket) { countNetBridgeMetric("peerWakeMiss"); return; } - const localPath = typeof socket.localPath === "string" ? socket.localPath : void 0; - const remotePath = typeof socket.remotePath === "string" ? socket.remotePath : void 0; + const localPath = typeof socket._localUnixPath === "string" ? socket._localUnixPath : void 0; + const remotePath = typeof socket._remoteUnixPath === "string" ? socket._remoteUnixPath : void 0; if (localPath === void 0 && remotePath === void 0) { countNetBridgeMetric("peerWakeInvalidTargets"); return; @@ -231,8 +231,8 @@ function wakePeerBridgeReads(socket) { if (peer === socket || peer.destroyed) { continue; } - const peerLocalPath = typeof peer.localPath === "string" ? peer.localPath : void 0; - const peerRemotePath = typeof peer.remotePath === "string" ? peer.remotePath : void 0; + const peerLocalPath = typeof peer._localUnixPath === "string" ? peer._localUnixPath : void 0; + const peerRemotePath = typeof peer._remoteUnixPath === "string" ? peer._remoteUnixPath : void 0; const fullPathMirror = localPath !== void 0 && remotePath !== void 0 && peerLocalPath !== void 0 && peerRemotePath !== void 0 && peerLocalPath === remotePath && peerRemotePath === localPath; const remoteToPeerLocal = remotePath !== void 0 && peerLocalPath !== void 0 && peerLocalPath === remotePath; const localToPeerRemote = localPath !== void 0 && peerRemotePath !== void 0 && peerRemotePath === localPath; @@ -313,7 +313,7 @@ function wakeNetServerAcceptForSocket(socket) { } return wakeNetServerAccept(server); } - const path = socket?.remotePath; + const path = socket?._remoteUnixPath; if (typeof path !== "string") { countNetBridgeMetric("acceptWakeSocketInvalidTargets"); return; @@ -515,6 +515,15 @@ function normalizeConnectArgs(portOrOptions, hostOrCallback, callback) { }; } +function unixSocketRequest(path) { + if (path.charCodeAt(0) === 0) { + return { + abstractPathHex: Buffer.from(path.slice(1), "utf8").toString("hex") + }; + } + return { path }; +} + function isValidIPv4Segment(segment) { if (!/^[0-9]{1,3}$/.test(segment)) { return false; @@ -1590,14 +1599,6 @@ var NetSocket = class _NetSocket { readyState = "open"; readableLength = 0; writableLength = 0; - remoteAddress; - remotePort; - remoteFamily; - localAddress = "0.0.0.0"; - localPort = 0; - localFamily = "IPv4"; - localPath; - remotePath; bytesRead = 0; bytesWritten = 0; bufferSize = 0; @@ -1618,6 +1619,8 @@ var NetSocket = class _NetSocket { _readableState = { endEmitted: false, ended: false }; _readQueue = []; _handle = null; + _localUnixPath; + _remoteUnixPath; constructor(options) { if (options?.allowHalfOpen) this.allowHalfOpen = true; if (options?.handle) this._handle = options.handle; @@ -1638,15 +1641,29 @@ var NetSocket = class _NetSocket { } = normalizeConnectArgs(portOrOptions, hostOrCallback, callback); if (cb) this.once("connect", cb); this.connecting = true; - this.remoteAddress = path ?? host; - this.remotePort = path ? void 0 : port; - this.remotePath = path; + if (path) { + delete this.remoteAddress; + delete this.remotePort; + delete this.remoteFamily; + delete this.localAddress; + delete this.localPort; + delete this.localFamily; + delete this.localPath; + delete this.remotePath; + } else { + this.remoteAddress = host; + this.remotePort = port; + this.localAddress ??= "0.0.0.0"; + this.localPort ??= 0; + this.localFamily ??= "IPv4"; + } + this._remoteUnixPath = path; this.pending = false; let handle; try { handle = normalizeNetSocketHandle(_netSocketConnectRaw.applySync( void 0, - [path ? { path } : { host, port, localAddress, localPort }] + [path ? unixSocketRequest(path) : { host, port, localAddress, localPort }] )); } catch (error) { this.connecting = false; @@ -1973,14 +1990,25 @@ var NetSocket = class _NetSocket { if (!info) { return; } + if (info.localPath !== undefined || info.remotePath !== undefined) { + this._localUnixPath = info.localPath; + this._remoteUnixPath = info.remotePath ?? this._remoteUnixPath; + delete this.localAddress; + delete this.localPort; + delete this.localFamily; + delete this.remoteAddress; + delete this.remotePort; + delete this.remoteFamily; + delete this.localPath; + delete this.remotePath; + return; + } this.localAddress = info.localAddress; this.localPort = info.localPort; this.localFamily = info.localFamily; - this.localPath = info.localPath; this.remoteAddress = info.remoteAddress ?? this.remoteAddress; this.remotePort = info.remotePort ?? this.remotePort; this.remoteFamily = info.remoteFamily ?? this.remoteFamily; - this.remotePath = info.remotePath ?? this.remotePath; } _applyAcceptedKeepAlive(initialDelay) { this._keepAliveState = true; @@ -2115,6 +2143,9 @@ var NetSocket = class _NetSocket { uncork() { } address() { + if (this._localUnixPath !== undefined || this._remoteUnixPath !== undefined) { + return {}; + } return { port: this.localPort, family: this.localFamily, address: this.localAddress }; } getCipher() { @@ -2845,8 +2876,8 @@ var NetServer = class { _socketId: clientHandle.socketId, localPort: clientHandle.info.localPort, remotePort: clientHandle.info.remotePort, - localPath: clientHandle.info.localPath, - remotePath: clientHandle.info.remotePath + _localUnixPath: clientHandle.info.localPath, + _remoteUnixPath: clientHandle.info.remotePath }); return; } @@ -2885,7 +2916,14 @@ var NetServer = class { try { const resultValue = _netServerListenRaw.applySyncPromise( void 0, - [{ port, host, path, backlog, readableAll, writableAll }] + [{ + port, + host, + ...(path ? unixSocketRequest(path) : {}), + backlog, + readableAll, + writableAll + }] ); const result = typeof resultValue === "string" ? JSON.parse(resultValue) : resultValue; const address = result.address ?? result; @@ -2936,9 +2974,13 @@ var NetServer = class { this._serverId = 0; void (async () => { try { - await _netServerCloseRaw.apply(void 0, [serverId], { + await _netServerCloseRaw.apply(void 0, [serverId, true], { result: { promise: true } }); + } catch (error) { + queueMicrotask(() => { + this._emit("error", error); + }); } finally { this._address = null; this._emit("close"); diff --git a/packages/build-tools/bridge-src/builtins/process.ts b/packages/build-tools/bridge-src/builtins/process.ts index f07df4e596..85958e794c 100644 --- a/packages/build-tools/bridge-src/builtins/process.ts +++ b/packages/build-tools/bridge-src/builtins/process.ts @@ -29,6 +29,9 @@ function readProcessConfig() { "node", "script.js" ], + argv0: typeof _processConfig !== "undefined" && typeof _processConfig.argv0 === "string" + ? _processConfig.argv0 + : "node", execPath: typeof _processConfig !== "undefined" && _processConfig.execPath || "/usr/bin/node", pid: typeof _processConfig !== "undefined" && _processConfig.pid || 1, ppid: typeof _processConfig !== "undefined" && _processConfig.ppid || 0, @@ -483,7 +486,7 @@ var process2 = { execPath: config2.execPath, execArgv: [], argv: config2.argv, - argv0: config2.argv[0] || "node", + argv0: config2.argv0, title: "node", env: config2.env, // Config stubs @@ -873,7 +876,7 @@ function applyProcessConfig(nextConfig) { process2.ppid = nextConfig.ppid; process2.execPath = nextConfig.execPath; process2.argv = nextConfig.argv; - process2.argv0 = nextConfig.argv[0] || "node"; + process2.argv0 = nextConfig.argv0; process2.env = nextConfig.env; process2.connected = nextConfig.env?.AGENTOS_NODE_IPC === "1"; process2._cwd = nextConfig.cwd; diff --git a/packages/build-tools/bridge-src/global-exposure.ts b/packages/build-tools/bridge-src/global-exposure.ts index a76be74fee..dfec0db7f5 100644 --- a/packages/build-tools/bridge-src/global-exposure.ts +++ b/packages/build-tools/bridge-src/global-exposure.ts @@ -35,11 +35,31 @@ var NODE_CUSTOM_GLOBAL_INVENTORY = [ classification: "hardened", rationale: "Host process signal bridge reference." }, + { + name: "_processExec", + classification: "hardened", + rationale: "Host process image replacement bridge reference." + }, + { + name: "_processExecFdImageCommit", + classification: "hardened", + rationale: "Host descriptor-backed process image commit bridge reference." + }, { name: "_processSignalState", classification: "hardened", rationale: "Host process signal-listener state bridge reference." }, + { + name: "_processTakeSignal", + classification: "hardened", + rationale: "Host process pending-signal drain bridge reference." + }, + { + name: "_processWasmSyncRpc", + classification: "hardened", + rationale: "Allowlisted WASM process syscall bridge reference." + }, { name: "_osConfig", classification: "hardened", @@ -860,6 +880,11 @@ var NODE_CUSTOM_GLOBAL_INVENTORY = [ classification: "hardened", rationale: "Host net socket connect-wait bridge reference." }, + { + name: "_netSocketWaitConnectSyncRaw", + classification: "hardened", + rationale: "Host synchronous net socket connect-wait bridge reference." + }, { name: "_netSocketReadRaw", classification: "hardened", @@ -925,6 +950,16 @@ var NODE_CUSTOM_GLOBAL_INVENTORY = [ classification: "hardened", rationale: "Host net server listen bridge reference." }, + { + name: "_netBindUnixRaw", + classification: "hardened", + rationale: "Host Unix-domain listener bind bridge reference." + }, + { + name: "_netBindConnectedUnixRaw", + classification: "hardened", + rationale: "Host connected Unix-domain socket bind bridge reference." + }, { name: "_netServerAcceptRaw", classification: "hardened", @@ -935,6 +970,11 @@ var NODE_CUSTOM_GLOBAL_INVENTORY = [ classification: "hardened", rationale: "Host net server close bridge reference." }, + { + name: "_netServerCloseSyncRaw", + classification: "hardened", + rationale: "Host synchronous net server close bridge reference." + }, { name: "_dgramSocketCreateRaw", classification: "hardened", diff --git a/packages/core/AGENTS.md b/packages/core/AGENTS.md deleted file mode 120000 index 681311eb9c..0000000000 --- a/packages/core/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -CLAUDE.md \ No newline at end of file diff --git a/packages/core/AGENTS.md b/packages/core/AGENTS.md new file mode 100644 index 0000000000..8540ca7f41 --- /dev/null +++ b/packages/core/AGENTS.md @@ -0,0 +1,172 @@ +# agentOS Core Package + +`@rivet-dev/agentos-core` -- contains VM ops, ACP client, session management. + +**⚠️ CRITICAL INVARIANT: ALL guest code MUST execute inside the kernel with ZERO host escapes.** The VM is a fully virtualized OS — every file read, network connection, and process spawn goes through the kernel. Guest code must never touch real host APIs. The Node.js execution engine is currently broken (spawns real host `node` processes instead of V8 isolates). See `crates/execution/CLAUDE.md`. + +## AgentOs Class + +- Wraps the kernel and proxies its API directly. +- **All public methods must accept and return JSON-serializable data.** No object references (Session, ManagedProcess, ShellHandle) in the public API. Reference resources by ID (session ID, PID, shell ID). +- Filesystem methods mirror the kernel API 1:1 (readFile, writeFile, mkdir, readdir, stat, exists, move, delete). +- Command execution mirrors the kernel API (exec, spawn). +- `fetch(port, request)` reaches services running inside the VM using the kernel network adapter pattern (`proc.network.fetch`). +- **Cron scheduling stays in the TypeScript layer.** The Rust sidecar has no concept of cron jobs. Cron expression parsing, timer management, overlap policies, and job execution dispatch all live in the TypeScript SDK. +- Keep cron schedule validation and `nextRun` computation on the shared helpers in `src/cron/parse-schedule.ts`; if `CronManager` and `TimerScheduleDriver` parse or reject schedules differently, `listCronJobs()` can advertise jobs the driver refuses (or immediately fires) and the API becomes self-contradictory. +- Native sidecar execution requests should stay unresolved on the TypeScript side. Forward `command`, `args`, `cwd`, and VM config through the wire payload, and let Rust own command lookup, guest-path to host-path mapping, shadow materialization, and `AGENT_OS_*` runtime env assembly. +- Native sidecar `exec()` should keep shell-sensitive commands on the `sh -c` wrapper path so cwd changes, pipelines, and other shell semantics stay truthful, but shell-free simple commands can use the direct spawn fast path regardless of driver. For Wasm commands in `src/sidecar/rpc-client.ts`, direct spawn preserves the real guest exit status for external-command failures like `cat /missing`, while the `sh -c` wrapper can swallow that non-zero status even when stderr is correct. +- In `src/sidecar/rpc-client.ts`, `&&` command chains must stay on a single guest `sh -c` execution. Splitting them into separate `exec()` calls loses shell state like `cd` and changes where relative redirects write. +- In `src/sidecar/rpc-client.ts`, shell syntax in `exec()` and shell-mode `spawn()` always routes to guest `sh -c`. The only fast path is the shell-free direct spawn; never parse redirects or any other shell grammar in the bridge. +- In `src/sidecar/rpc-client.ts`, keep the shell wrapper as `cd ... || exit` followed by the target command and trust the shell process exit code directly. Temp-file or assignment-based `$?` capture on the brush path is brittle: shell redirection can leave the file empty, inject `exit` parse errors into stderr, and silently turn failing guest commands green. +- In `src/sidecar/rpc-client.ts`, the simple-command parser must preserve backslashes for non-shell-special escapes inside double quotes. Commands like `printf "a\\nb\\n"` rely on the guest command seeing the literal `\n` bytes; only `\"`, `\\`, `\$`, ``\` ``, and line-continuation newlines should collapse on the native-sidecar fast path. +- In `src/sidecar/rpc-client.ts`, treat bare unquoted `!` as shell syntax, not as a direct-fast-path token. Commands like `test ! -f /tmp/file` rely on guest shell semantics, and bypassing the shell can flip the observed exit code even when the underlying file operation succeeded. +- If a file must be visible to both `vm.readFile()` and guest shell commands, it cannot live only in a local compat mount. Put it on a real sidecar-visible path or mount, and keep any read-only guarantees enforced below the TypeScript proxy layer. +- Host tool registration is split across the boundary: TypeScript converts Zod schemas to JSON Schema, generates prompt markdown, validates sidecar tool invocations, and runs the local `execute()` callbacks, while the sidecar owns CLI flag parsing and `agentos` command dispatch via `registerHostCallbacks` / `RegisterHostCallbacks`. +- Host-tool `inputSchema` conversion in `src/host-tools-zod.ts` is intentionally fail-closed. Support only the Zod subset that round-trips cleanly into the sidecar-facing JSON Schema contract; if a schema would degrade semantics or emit `$ref`/`$defs` (`discriminatedUnion`, `intersection`, `tuple`, `record`, `date`, `bigint`, custom refinements, metadata `id`, etc.), throw `HostToolSchemaConversionError` with the offending field path instead of coercing it to `{ type: "string" }`. +- The host-tool description limit is a cross-boundary contract: keep the 200-character maximum aligned between `src/host-tools.ts` and Rust `RegisterHostCallbacks` validation in `crates/sidecar/src/tools.rs`, with boundary tests on both sides when changing it. +- `src/sidecar/rpc-client.ts` is the consolidated home for framed sidecar I/O, compat proxy helpers, and sidecar descriptor serializers. Keep shared/explicit sidecar pool and VM lease bookkeeping in `src/agent-os.ts` rather than reintroducing another sidecar lifecycle layer. +- In `src/agent-os.ts`, shell teardown is two-phase: public `_shells` entries can disappear immediately on `closeShell()`, but `dispose()` must still await the separate pending shell-exit set before dropping the sidecar event listener, or late shell stdout/exit delivery can race into a closed bridge. +- The native sidecar framed stdio path now defaults to the BARE payload codec. Keep any JSON payload support behind explicit migration-only opts such as `payloadCodec: "json"`, and remember that BARE structs need every positional field serialized explicitly across the Rust/TypeScript boundary rather than relying on JSON-style `skip_serializing_if` omissions. +- In `src/sidecar/native-process-client.ts`, treat `child.on("exit")` and `child.on("error")` as the authoritative terminal-disconnect path for framed stdio clients. `stdout` can close before Node fills in `exitCode`/`signalCode`, so reject in-flight RPCs with a typed disconnect immediately and upgrade the stored terminal error once the concrete exit metadata arrives. +- In the native-sidecar event path, long-lived background loops should call `waitForEvent()` in abortable no-timeout mode instead of parking a multi-hour timeout sentinel. The abort signal is the cancellation mechanism; the timeout itself becomes the regression surface on idle VMs. +- For native-sidecar BARE ACP session bootstrap payloads, keep `SessionCreatedResponse` aligned with `crates/sidecar/protocol/agentos_native_sidecar_v1.bare`: `sessionId` is the first positional field on the wire, before optional `pid`, `modes`, `configOptions`, `agentCapabilities`, and `agentInfo`. If the TypeScript decoder reads `session_id` last, every `createSession()` response desynchronizes. +- Public SDK type exports now funnel through `src/types.ts`; keep legacy kernel/runtime implementation helpers behind `src/runtime-compat.ts` and avoid adding new public root exports directly from runtime internals. +- When adding a new public SDK option/result/helper type under `src/agent-os.ts`, `src/json-rpc.ts`, `src/host-dir-mount.ts`, or other root-facing modules, mirror it through `src/types.ts` and keep `tests/public-api-exports.test.ts` aligned so the package entrypoint stays truthful. + +## Agent Sessions (ACP) + +- Uses the **Agent Communication Protocol** (ACP) -- JSON-RPC 2.0 over stdio (newline-delimited) +- No HTTP adapter layer; communicate directly with agent ACP adapters over stdin/stdout +- Reference `~/sandbox-agent` for ACP integration patterns. Do not copy code from it. +- ACP docs: https://agentclientprotocol.com/get-started/introduction +- Session design is **agent-agnostic**: each agent type has a config specifying its ACP adapter package and main agent package name +- Currently configured agents: PI (`@agentos-software/pi`), PI CLI (`@agentos-software/pi-cli`), OpenCode (`@agentos-software/opencode`), Claude (`@agentos-software/claude-code`), and Codex (`@agentos-software/codex` + `@agentos-software/codex-cli`). +- **No host agent exceptions.** Host-native wrappers and host binary launch paths are not allowed. OpenCode support must use the real upstream OpenCode implementation rebuilt into the VM adapter package and executed inside the VM. +- `createSession("pi")` spawns the ACP adapter inside the VM, which calls the Pi SDK directly +- Agents are resolved BY THE SIDECAR, not the client. There is no `AGENT_CONFIGS` table; `AgentType` is just `string` (a manifest `name`, defined in `types.ts`). The client is npm-agnostic and parses NO manifests: `createSession(name)`/`resumeSession(name)` send only `agentType` on the wire (there is no `adapterEntrypoint` field), and the SIDECAR resolves the entrypoint/env/launchArgs from the projected `/opt/agentos//current/agentos-package.json`. `listAgents()` is a sidecar ACP RPC (`AcpListAgentsRequest`) — the sidecar enumerates the projected `/opt/agentos` packages. Adding/changing an agent = changing its package manifest (in the secure-exec registry), not this file; verify launch args/env with the mock-adapter session tests. The Rust client (`crates/client`) behaves IDENTICALLY (also sends only `agentType`) — see the root `CLAUDE.md` client-parity/npm-agnostic rule. +- In `createSession()`, treat `skipOsInstructions` as "skip the base `/etc/agentos/instructions.md` text" only. Still call agent `prepareInstructions(...)` when session-level `additionalInstructions` or tool-reference content exists, and forward `skipBase` through the options object instead of blanking out the caller's extra instructions. +- ACP agents that issue live `session/request_permission` calls during `session/prompt` cannot rely on queued session events alone. Route those permission round-trips through the sidecar callback channel (`SidecarRequestPayload`) so the host can answer them before the prompt request completes. +- Native-sidecar inbound ACP host callbacks are explicit sidecar-request payloads now. If Rust forwards an unknown ACP JSON-RPC request, answer it through `SidecarRequestPayload.type === "acp_request"` with an `acp_request_result` JSON-RPC response; otherwise the sidecar will only synthesize `-32601` after the callback transport is unavailable or times out. +- Host ACP callbacks in `src/agent-os.ts` are no longer a generic `-32601` stub: keep the dispatcher aligned with both the newer `fs/read` / `fs/write` / `fs/readDir` / `terminal/*` method names and the legacy aliases still exercised by native-sidecar tests such as `fs/read_text_file` and `fs/write_text_file`. +- On the native sidecar path, a top-level `session/cancel` request does not preempt an already running top-level `session/prompt` dispatch. If prompt callers must observe cancellation immediately, resolve the pending prompt request locally in `src/agent-os.ts` while still forwarding the real cancel RPC for eventual adapter/process cleanup. +- Native-sidecar ACP request timeouts should surface as JSON-RPC errors with `error.data.kind === "acp_timeout"` rather than string-only transport errors. Use `isAcpTimeoutErrorData()` from `src/json-rpc.ts` instead of parsing timeout messages. + +### Agent Adapter Approaches + +Each agent type can have two adapter approaches: +- **SDK adapter** (default) -- Embeds the agent SDK directly via library import (`createAgentSession()`). Lower memory footprint (~100MB less for Pi). Binary: `pi-sdk-acp`. Package: `@agentos-software/pi`. Agent ID: `pi`. +- **CLI adapter** -- Spawns the full agent CLI as a headless subprocess via its ACP adapter (`pi-acp` spawns `pi --mode rpc`). Higher memory overhead but provides full CLI feature set. Binary: `pi-acp`. Package: `@agentos-software/pi-cli`. Agent ID: `pi-cli`. + +### Agent Configs + +An agent's launch config lives entirely in its `/opt/agentos` package `agentos-package.json` manifest (`agent.acpEntrypoint`/`launchArgs`/`env`). Resolution is SIDECAR-SIDE: the client sends only the agent `name` (the `createSession(name)` id), and the sidecar reads the projected `/opt/agentos//current/agentos-package.json` to resolve the entrypoint (`/opt/agentos/bin/`), the manifest `env` (applied as defaults; caller env wins), and `launchArgs` (prepended before caller args), then spawns. The client parses no manifests and holds no per-agent config. There is no second hardcoded config surface to keep in sync — a default shell/env tweak is a manifest change. + +## Testing + +- **Framework**: vitest +- **Prefer scoped tests while iterating.** + - `pnpm --dir packages/core exec vitest run tests/path/to/file.test.ts` or `pnpm --dir packages/core exec vitest run -t "test name pattern"` + - Repo-root `pnpm test` is the RC sweep and exits cleanly; it is still too broad for normal iteration. + - `pnpm --dir packages/core test` intentionally uses Vitest's `verbose` reporter because `tests/wasm-commands.test.ts` and similar long-running VM suites otherwise sit silent for minutes and get misread as hangs during `US-088` sweeps. + - Use low timeouts for test commands (60000ms max). +- The vitest setup file at `tests/helpers/default-vm-permissions.ts` patches `AgentOs.create()` and disposes every cached shared sidecar via `__disposeAllSharedSidecarsForTesting()` in `afterAll`. Workers can hang on exit if the shared sidecar's piped stdio handles stay open, so any new test entrypoints that bypass this setup file must dispose their sidecars themselves. +- `NativeSidecarProcessClient.dispose()` enforces a graceful exit window then `SIGKILL`s the child if it ignores stdin EOF; `tests/native-sidecar-process.test.ts` covers the regression so future changes cannot reintroduce an unbounded teardown wait. +- In `packages/core` tests that capture `spawn()` stdout/stderr via callbacks and then call `waitProcess(pid)`, drain one macrotask (`await new Promise((resolve) => setTimeout(resolve, 0))`) before asserting on the buffered strings. Native-sidecar `process_output` events can arrive one turn after the exit notification, and tiny outputs like `curl -s` bodies are the first thing to get lost if you snapshot immediately. +- `NativeSidecarProcessClient.waitForEvent(...)` supports indexed `SidecarEventSelector` objects; prefer selectors over ad hoc lambdas on shared sidecar clients so buffered events stay O(1) to retrieve and `ownership` can pin a wait to one VM/session. +- The native sidecar client's unmatched event buffer is intentionally bounded and fail-closed. If a test or runtime path can leave `runEventPump` idle while output events stream, expect `SidecarEventBufferOverflow` rather than unbounded buffering, and set a larger `eventBufferCapacity` explicitly only for cases that truly need it. +- When Node/Vitest code needs to shell out to Cargo, resolve it through `src/sidecar/cargo.ts` instead of assuming a login shell already put `~/.cargo/bin` on `PATH`. +- For `tests/wasm-commands.test.ts`, broad `-t "grep"` or `-t "sed"` filters can pull in unrelated `rg`, `gzip`, or cross-package pipeline coverage via substring matches. When a story only gates the `grep`/`sed` blocks, use the explicit case names or a narrower `--testNamePattern` that only matches those block entries. +- For `tests/wasm-commands.test.ts` and similar long-running VM truth suites, prefer one shared VM per `describe(...)` block over one VM per individual test unless the case truly needs pristine bootstrap state. Per-test VM boots push the file into multi-minute runtimes and make the RC sweep look hung even when it is still progressing. +- Cross-workspace software package suites import `@rivet-dev/agentos-core` from `packages/core/dist`, not directly from `src/`. After changing exported test-runtime code such as `src/runtime-compat.ts`, rebuild `packages/core` before trusting software/package Vitest results. +- The `examples/quickstart` package also resolves `@rivet-dev/agentos-core` from `packages/core/dist`; after TypeScript changes in `packages/core/src`, rebuild `packages/core` before rerunning quickstart acceptance commands. +- The synthetic `openShell()` fallback in `src/sidecar/rpc-client.ts` needs PTY-style output semantics for xterm-based harnesses: normalize terminal-visible line endings to `\r\n`, and route command stderr through the main `onData` stream instead of treating it like a separate non-PTY stderr channel. +- **Always verify related tests pass before considering work done.** +- **All tests run inside the VM** -- network servers, file I/O, agent processes. +- For `vm.exec()` cwd/path tests, prefer setting up files from inside the guest shell when the assertion is about command resolution or relative paths. VM filesystem API writes becoming visible to host-backed runtimes is a separate shadow-sync surface and should be tested independently. +- For active agent-session/bash-tool filesystem regressions, cover the host read path in `tests/filesystem.test.ts` with a Claude llmock prompt. Long-lived session processes keep writing into the sidecar shadow root after a tool call returns, so `vm.readFile()`/`vm.stat()` need shadow reconciliation before the session itself exits. +- Session tests that need launch argv or OS-instruction assertions should inspect `getSessionAgentInfo(sessionId)` from sidecar state instead of spying on `kernel.spawn`; `createSession()` now launches through sidecar RPCs. +- `closeSession()` is intentionally fire-and-forget. Cleanup tests can await the internal `_sessionClosePromises` map when they need deterministic post-close assertions, but active-prompt cancellation cases should trigger the public close and then assert on resource release plus prompt error outcome separately, because the in-flight ACP request and the close request share the same sidecar connection. +- If you add or change a fire-and-forget session close path in `src/agent-os.ts`, attach a local `.catch(() => {})` to the dropped promise. The real close result is still exposed through `_sessionClosePromises`, and dropping the promise entirely turns shared-runtime close races into unhandled rejection noise in Vitest. +- Pi CLI session state currently reports the shared V8 host PID when multiple ACP sessions share one JavaScript runtime child. In cleanup tests, treat only host PIDs that are unique to a session as dedicated session roots; a shared PID is runtime-wide context, not three distinct leaked processes. +- For projected npm CLIs in package tests, prefer `node /root/node_modules//dist/.js` over `/root/node_modules/.bin/*`. pnpm's generated `.bin` wrappers embed host filesystem paths, which are not stable or guest-visible inside the VM. +- Browserbase VM tests should read credentials from host env as `BROWSER_BASE_API_KEY` / `BROWSER_BASE_PROJECT_ID`, alias them to `BROWSERBASE_API_KEY` / `BROWSERBASE_PROJECT_ID` in the guest env, and keep VM `network` permissions narrowed to `dns://*.browserbase.com` plus `tcp://*.browserbase.com:*` so remote Browserbase sessions work while direct guest egress stays denied. +- For Browserbase e2e flows inside the VM, prefer a small guest `fetch()` helper that creates/releases the Browserbase session plus `node /root/node_modules/@browserbasehq/browse-cli/dist/index.js --ws ...` over the browse daemon session socket path. The direct `--ws` mode avoids a guest-local Unix-socket control hop and keeps the test focused on Browserbase API plus CDP connectivity. +- For `tests/wasm-commands.test.ts` curl coverage, prefer a guest `net.createServer()` HTTP fixture over guest `http.createServer()` when the story is about the curl/WASM client path. The HTTP-server transport wrapper is a separate compatibility surface and can hide or conflate curl regressions. +- Layer lifecycle regressions should be covered in both `tests/layers.test.ts` for in-memory snapshot reuse/composition semantics and `crates/sidecar/tests/layer_management.rs` for VM-scoped layer RPC isolation; the package-level suite alone does not prove per-VM ownership boundaries. +- For guest-JavaScript startup diagnostics, isolate each suspect import or constructor in its own fresh VM. Once a V8-side probe wedges or times out, later `node` spawns in the same VM can degrade into generic broken-pipe noise instead of the original failure. +- Agent tests must be run sequentially in layers: + 1. PI headless mode (spawn pi directly, verify output) + 2. pi-acp manual spawn (JSON-RPC over stdio) + 3. Full `createSession()` API +- **API tokens**: All tests use `@copilotkit/llmock` with `ANTHROPIC_API_KEY='mock-key'`. No real API tokens needed. Do not load tokens from `~/misc/env.txt` or any external file. +- **Mock LLM testing**: Use `@copilotkit/llmock` to run a mock LLM server on the HOST (not inside the VM). Use `loopbackExemptPorts` in `AgentOs.create()` to exempt the mock port from SSRF checks. The kernel needs `permissions: allowAll` for network access. +- Compat-kernel loopback exemptions are sticky VM config. When `src/runtime-compat.ts` reconfigures a VM later to mount command directories, resend `loopbackExemptPorts` on every `configureVm()` call and seed the same port list into create-VM metadata so guest networking sees it before and after reconfiguration. +- Compat-kernel `createKernel()` bootstraps sidecar VMs under a temporary internal `allowAll` only when the caller provided explicit permissions, then reapplies the requested policy in `configureVm()` after local mounts and `/bin/*` command stubs are in place. Skipping that handoff makes default-deny VMs block their own runtime/bootstrap writes before the guest policy ever takes effect. +- In `src/runtime-compat.ts`, `rootView.exists("/bin/")` can return `true` from the kernel command registry before the sidecar shadow root has a real stub file. If a host-backed runtime needs the command visible on disk, materialize the stub unconditionally instead of skipping on `exists()`. +- In `src/runtime-compat.ts`, custom `createKernel({ filesystem })` snapshots need to be replayed through guest filesystem calls after `createVm()` when permissions allow it. Loading the root snapshot into the kernel alone is not enough for shell-launched WASM commands, because they read the sidecar shadow root and will miss pre-seeded files like `/hello.txt` unless those entries are mirrored there too. +- In `src/runtime-compat.ts`, `createWasmVmRuntime({ commandDirs })` is a stateful command-dir descriptor, not just a static command list: keep symlink-to-WASM alias discovery, basename-based `tryResolve()` for late-added binaries, and the descriptor’s internal command-path/module-cache bookkeeping aligned with the kernel mount path or the registry dynamic-module truth tests will drift out of sync. +- In `src/runtime-compat.ts`, `NativeKernel.processes` is not automatically shared with the native-sidecar proxy map. When `spawn()` wraps `proxy.spawn(...)`, mirror the proxy snapshot into `kernel.processes` immediately and after `wait()` so software integration tests that read `kernel.processes.get(pid)` see the same root-process status transitions as the public compat kernel. +- Declarative sidecar permission rules must use explicit `["*"]` wildcards for rule `operations` and `paths`/`patterns`; empty arrays are rejected by the native sidecar instead of being treated as implicit wildcards. +- **Pi SDK llmock setup**: Pi reads Anthropic endpoints from `~/.pi/agent/models.json`, not `ANTHROPIC_BASE_URL`. For `createSession("pi")` tests, write a provider override such as `{ "providers": { "anthropic": { "baseUrl": "", "apiKey": "mock-key" } } }` inside the VM before creating the session. +- Pi headless llmock tests should still pass `ANTHROPIC_BASE_URL` through the session env even with the `~/.pi/agent/models.json` override, because some Pi SDK request paths still consult the env-configured base URL during ACP-driven tool turns. +- `packages/core` agent-session tests execute secure-exec registry agent workspaces through their built `dist`/bin artifacts. After changing an adapter under `../secure-exec/software/*/src`, rebuild that workspace before trusting the core Vitest result. +- Keep Claude's default `CLAUDE_CODE_NODE_SHELL_WRAPPER` enabled (`"1"`) in both `src/agents.ts` and `../secure-exec/software/claude/src/index.ts`. Forcing it to `"0"` breaks real Bash-tool execution under llmock-backed sessions: shell redirections can still create empty files, but the command output/tool result never lands, which regresses `tests/claude-session.test.ts` and filesystem visibility checks. +- Registry/kernel suites that import `@rivet-dev/agentos-core/test/runtime` read `packages/core/dist/test/runtime.js`, not the TypeScript sources directly. After changing `src/runtime-compat.ts`, `src/sidecar/rpc-client.ts`, or other runtime-test surfaces, run `pnpm --dir packages/core build` before rerunning those registry Vitest files or they will keep exercising stale code. +- **Module access**: Pass `mounts: [nodeModulesMount("/node_modules")]` to `AgentOs.create()` to expose a host `node_modules` tree at `/root/node_modules`. The VM module resolver reads the mounted tree through the kernel VFS (no host-direct reads, no `moduleAccessCwd`). pnpm puts devDeps in `packages/core/node_modules/`, so tests use `nodeModulesMount(join(resolve(import.meta.dirname, ".."), "node_modules"))`. Software-package agents (`software: [pi]`) mount their own `/root/node_modules/` roots and do not need this mount. +- Quickstarts and integration tests that run full-tier registry commands (for example `@agentos-software/git`) should set both an explicit `/root/node_modules` mount (via `nodeModulesMount(...)`) and explicit `permissions` on `AgentOs.create()`. There is no `process.cwd()` default anymore: supply the exact `node_modules` tree (a flat install, not a pnpm workspace root whose symlinks escape the mount), and remember that omitting permissions defaults the native sidecar to deny-all. +- S3-backed core tests can use `tests/helpers/mock-s3.ts` as the explicit local harness instead of Docker/MinIO; when the endpoint resolves to `127.0.0.1` or `localhost`, set `AGENT_OS_ALLOW_LOCAL_S3_ENDPOINTS=1` before creating the VM so the sidecar accepts the local test endpoint. +- Sandbox toolkit quickstarts/tests that depend on external Docker should use an explicit `SKIP_DOCKER=1` gate instead of `skipIf`, and the truthful host-tool path is to read `AGENTOS_TOOLS_PORT` inside the VM and `POST` `{ toolkit, tool, input }` to `http://127.0.0.1:$AGENTOS_TOOLS_PORT/call` from a guest Node script. +- Shared Vitest helpers under `src/test/` should register optional capability coverage conditionally in code instead of with `describe.skipIf` / `test.skipIf`; `US-088` treats those markers as product-debt skips even when they only guard backend capability differences. +- Pi bash-tool E2E coverage depends on registry WASM commands being built locally. Gate those tests with `tests/helpers/registry-commands.ts` `hasRegistryCommands` and include the `@agentos-software/common` software package only when the command artifacts exist. +- Software package tests for C-built commands such as `duckdb` and `curl` should go through `tests/helpers/registry-commands.ts`: prefer copied `../secure-exec/software/*/wasm` artifacts, fall back to `../secure-exec/toolchain/c/build` when available, and let the helper build missing C-source artifacts on demand before declaring the command unavailable. When bootstrapping from secure-exec `toolchain/c`, build `make sysroot` first and then run a second `make` for the concrete `build/...` targets so `SYSROOT` resolves to the patched tree instead of the vanilla SDK sysroot chosen at parse time; in that second pass, treat `sysroot/lib/wasm32-wasi/libc.a` as already built so `make` does not loop back through the patch pipeline because of preserved sysroot timestamps. +- `tests/claude-session.test.ts` is the Claude SDK truth suite. It runs the real `@anthropic-ai/claude-agent-sdk` session path through llmock and covers PATH-backed `xu`, text-only replies, nested `node` `execSync` and `spawn`, metadata, lifecycle, and mode updates. Run it with `pnpm --dir packages/core exec vitest run tests/claude-session.test.ts --reporter=verbose` when verifying Claude regressions. +- **Kernel permissions are declarative pass-through config.** `AgentOsOptions.permissions` should stay JSON-serializable and be forwarded to the native sidecar without host-side probing or callback evaluation; Rust owns glob matching and policy decisions. +- ACP session events are live-only over `onSessionEvent()`. Do not reintroduce sequence numbers, local replay buffers, or event cursor recovery. +- ACP initialize intent belongs in `AgentOs.createSession()`: when the caller's ACP `protocolVersion` or `clientCapabilities` change, pass them through `src/sidecar/native-process-client.ts` instead of re-hardcoding initialize defaults in the Rust sidecar. +- **Sidecar permission path patterns preserve `*` vs `**`.** Use single-segment globs such as `/workspace/*` only for direct children; use `/workspace/**` when the VM should reach nested paths through the native sidecar permission policy. +- **Native-sidecar socket/process inspection is explicit now.** If a `Kernel` or `NativeSidecarProcessClient` caller needs `findListener()`, `findBoundUdp()`, or `getProcessSnapshot()`, grant `network.inspect` and/or `process.inspect` in the forwarded permissions; broad `network.listen` or `childProcess` access is not enough on its own. +- **Host tool invocation is its own permission surface.** Guest `agentos-*`/tools-RPC calls must grant `permissions.binding` with `invoke` rules that match `:` patterns; if the same test/example also boots guest command software, keep `fs` and `childProcess` permissions explicit because command execution still needs those guest-visible capabilities. +- `packages/core` Vitest now patches `AgentOs.create()` in `tests/helpers/default-vm-permissions.ts` to inject explicit allow-all permissions only when a suite omits them. Permission-focused tests must still pass their own `permissions` object so they exercise the real default-deny path instead of the generic test harness default. + +### Test Structure + +See `.agent/specs/test-structure.md` for the full restructuring plan. Target layout: + +- `unit/` -- no VM, no sidecar; pure logic (host-tools Zod conversion, descriptors, cron manager, etc.) +- `filesystem/` -- VFS CRUD, overlay, mount, layers, host-dir +- Shared filesystem conformance coverage in `src/test/file-system.ts` is fail-closed: backend-specific deviations must be modeled as explicit `capabilities` flags on the test descriptor, never with permissive `try/catch` branches that treat any thrown error as success. +- `process/` -- execution, signals, process tree, flat API wrappers +- `session/` -- ACP lifecycle, events, capabilities, MCP, cancellation +- `agents/{pi,claude,opencode,codex}/` -- per-agent adapter tests +- `wasm/` -- WASM command and permission tier tests +- `network/` -- connectivity and fetch behavior inside the VM +- `tests/migration-parity.test.ts` is the dedicated Rust/native migration gate. Keep it on the default `AgentOs.create()` sidecar path and make it cover filesystem, process, layer snapshot, tool dispatch, networking, and at least one real agent prompt/session flow together; the canonical invocation is `pnpm test:migration-parity` from the repo root. +- Host tool command-path coverage belongs with VM-backed sidecar tests such as `tests/sidecar-tool-dispatch.test.ts`, not a standalone TypeScript RPC server suite. +- Shell-backed host-tool dispatch coverage in `tests/sidecar-tool-dispatch.test.ts` needs the `@agentos-software/common` software package in the test VM so `/bin/sh` exists; otherwise the suite only proves direct spawn/RPC dispatch and misses the guest-shell path. +- `sidecar/` -- sidecar client, native process +- `cron/` -- cron integration + +### WASM Binaries and Quickstart Examples + +- **WASM command binaries are not checked into git.** The `../secure-exec/software/*/wasm/` directories are build artifacts. +- **Quickstart examples that use `exec()` or shell commands require WASM binaries.** Without them, these fail with "No shell available." +- **To build WASM binaries locally:** Run `make` in `../secure-exec/toolchain/`, then `make copy-wasm` and `make build` in `../secure-exec/registry/`. Requires Rust nightly + wasi-sdk. +- **Examples that work without WASM binaries:** `hello-world.ts`, `filesystem.ts`, `cron.ts` (schedule/cancel only). +- **When testing quickstart examples**, don't treat WASM-dependent failures as regressions unless the WASM binaries are present. + +### Known VM Limitations + +- `globalThis.fetch` is hardened (non-writable) in the VM -- can't be mocked in-process +- Kernel child_process.spawn can't resolve bare commands from PATH (e.g., `pi`). Use `PI_ACP_PI_COMMAND` env var to point to the `.js` entry directly. +- `allProcesses()` / `processTree()` on the native sidecar path should be derived from the VM's active-process snapshot rather than host `ps` output. Preserve the public `spawn()` PID for root processes by remapping the sidecar's kernel PID back through the root `process_id`, so nested guest `child_process.spawn()` children remain visible under the user-facing parent PID. +- Module resolution reads the mounted `/root/node_modules` through the kernel VFS. Host-side adapter/agent package.json reads (for bin resolution) still use `readFileSync` against the host dir behind the `/root/node_modules` mount (or the matching software root) +- Native ELF binaries cannot execute in the VM -- the kernel's command resolver only handles `.js`/`.mjs`/`.cjs` scripts and WASM commands. +- Projected native assets under `/root/node_modules` are readable through module access, but guest `child_process.spawn*()` still routes them through the VM command resolver; spawning a projected ELF currently fails during WASM warmup instead of executing host-native code. +- The native sidecar framed stdio client is bidirectional: host-originated `request`/`response` frames use positive `request_id` values, and sidecar-originated `sidecar_request`/`sidecar_response` frames use negative IDs. When adding host callbacks, register a sidecar request handler instead of assuming stdout only carries events plus responses. + +### Debugging Policy + +- **Never guess without concrete logs.** Every assertion about what's happening at runtime must be backed by log output. Add logs at every decision point and trace the full execution path before drawing conclusions. Never assume something is a timeout issue unless there are logs proving the system was actively busy for the entire duration. +- **Never use CJS transpilation as a workaround** for ESM module loading issues. Fix root causes in the ESM resolver, the `/root/node_modules` mount / kernel VFS, or V8 runtime. +- **Diagnosing stalls / backpressure / silent hangs:** secure-exec runs a central limit registry (`secure_exec_bridge::queue_tracker`) over the chain of bounded queues (V8→host event channel, per-session frame channel, sidecar stdout/stdin frame queues). A full queue applies backpressure (it blocks the producer), so a "hung" session is often a slow/stuck *consumer* upstream, not a deadlock. The registry emits a structured `WARN` ("bounded limit near capacity…") as any limit crosses ~80%, and resource/heap/CPU breaches surface as typed errors naming the limit. Set `SECURE_EXEC_LOG=warn` (the default) to see near-limit warnings, or `SECURE_EXEC_LOG=debug` for per-limit usage snapshots; secure-exec logs to **stderr** (stdout is the wire protocol). See the **Limits & Observability** architecture doc (`website/src/content/docs/docs/architecture/limits-and-observability.mdx`). +- **Maintain a friction log** at `.agent/notes/vm-friction.md` for anything that behaves differently from a standard POSIX/Node.js system. diff --git a/packages/core/CLAUDE.md b/packages/core/CLAUDE.md index 6baeb27ee8..8540ca7f41 100644 --- a/packages/core/CLAUDE.md +++ b/packages/core/CLAUDE.md @@ -77,7 +77,7 @@ An agent's launch config lives entirely in its `/opt/agentos` package `agentos-p - When Node/Vitest code needs to shell out to Cargo, resolve it through `src/sidecar/cargo.ts` instead of assuming a login shell already put `~/.cargo/bin` on `PATH`. - For `tests/wasm-commands.test.ts`, broad `-t "grep"` or `-t "sed"` filters can pull in unrelated `rg`, `gzip`, or cross-package pipeline coverage via substring matches. When a story only gates the `grep`/`sed` blocks, use the explicit case names or a narrower `--testNamePattern` that only matches those block entries. - For `tests/wasm-commands.test.ts` and similar long-running VM truth suites, prefer one shared VM per `describe(...)` block over one VM per individual test unless the case truly needs pristine bootstrap state. Per-test VM boots push the file into multi-minute runtimes and make the RC sweep look hung even when it is still progressing. -- Cross-workspace suites in `../secure-exec/registry/tests/*` import `@rivet-dev/agentos-core` from `packages/core/dist`, not directly from `src/`. After changing exported test-runtime code such as `src/runtime-compat.ts`, rebuild `packages/core` before trusting registry/package Vitest results. +- Cross-workspace software package suites import `@rivet-dev/agentos-core` from `packages/core/dist`, not directly from `src/`. After changing exported test-runtime code such as `src/runtime-compat.ts`, rebuild `packages/core` before trusting software/package Vitest results. - The `examples/quickstart` package also resolves `@rivet-dev/agentos-core` from `packages/core/dist`; after TypeScript changes in `packages/core/src`, rebuild `packages/core` before rerunning quickstart acceptance commands. - The synthetic `openShell()` fallback in `src/sidecar/rpc-client.ts` needs PTY-style output semantics for xterm-based harnesses: normalize terminal-visible line endings to `\r\n`, and route command stderr through the main `onData` stream instead of treating it like a separate non-PTY stderr channel. - **Always verify related tests pass before considering work done.** @@ -105,12 +105,12 @@ An agent's launch config lives entirely in its `/opt/agentos` package `agentos-p - In `src/runtime-compat.ts`, `rootView.exists("/bin/")` can return `true` from the kernel command registry before the sidecar shadow root has a real stub file. If a host-backed runtime needs the command visible on disk, materialize the stub unconditionally instead of skipping on `exists()`. - In `src/runtime-compat.ts`, custom `createKernel({ filesystem })` snapshots need to be replayed through guest filesystem calls after `createVm()` when permissions allow it. Loading the root snapshot into the kernel alone is not enough for shell-launched WASM commands, because they read the sidecar shadow root and will miss pre-seeded files like `/hello.txt` unless those entries are mirrored there too. - In `src/runtime-compat.ts`, `createWasmVmRuntime({ commandDirs })` is a stateful command-dir descriptor, not just a static command list: keep symlink-to-WASM alias discovery, basename-based `tryResolve()` for late-added binaries, and the descriptor’s internal command-path/module-cache bookkeeping aligned with the kernel mount path or the registry dynamic-module truth tests will drift out of sync. -- In `src/runtime-compat.ts`, `NativeKernel.processes` is not automatically shared with the native-sidecar proxy map. When `spawn()` wraps `proxy.spawn(...)`, mirror the proxy snapshot into `kernel.processes` immediately and after `wait()` so registry integration tests that read `kernel.processes.get(pid)` see the same root-process status transitions as the public compat kernel. +- In `src/runtime-compat.ts`, `NativeKernel.processes` is not automatically shared with the native-sidecar proxy map. When `spawn()` wraps `proxy.spawn(...)`, mirror the proxy snapshot into `kernel.processes` immediately and after `wait()` so software integration tests that read `kernel.processes.get(pid)` see the same root-process status transitions as the public compat kernel. - Declarative sidecar permission rules must use explicit `["*"]` wildcards for rule `operations` and `paths`/`patterns`; empty arrays are rejected by the native sidecar instead of being treated as implicit wildcards. - **Pi SDK llmock setup**: Pi reads Anthropic endpoints from `~/.pi/agent/models.json`, not `ANTHROPIC_BASE_URL`. For `createSession("pi")` tests, write a provider override such as `{ "providers": { "anthropic": { "baseUrl": "", "apiKey": "mock-key" } } }` inside the VM before creating the session. - Pi headless llmock tests should still pass `ANTHROPIC_BASE_URL` through the session env even with the `~/.pi/agent/models.json` override, because some Pi SDK request paths still consult the env-configured base URL during ACP-driven tool turns. -- `packages/core` agent-session tests execute secure-exec registry agent workspaces through their built `dist`/bin artifacts. After changing an adapter under `../secure-exec/registry/agent/*/src`, rebuild that workspace before trusting the core Vitest result. -- Keep Claude's default `CLAUDE_CODE_NODE_SHELL_WRAPPER` enabled (`"1"`) in both `src/agents.ts` and `../secure-exec/registry/agent/claude/src/index.ts`. Forcing it to `"0"` breaks real Bash-tool execution under llmock-backed sessions: shell redirections can still create empty files, but the command output/tool result never lands, which regresses `tests/claude-session.test.ts` and filesystem visibility checks. +- `packages/core` agent-session tests execute secure-exec registry agent workspaces through their built `dist`/bin artifacts. After changing an adapter under `../secure-exec/software/*/src`, rebuild that workspace before trusting the core Vitest result. +- Keep Claude's default `CLAUDE_CODE_NODE_SHELL_WRAPPER` enabled (`"1"`) in both `src/agents.ts` and `../secure-exec/software/claude/src/index.ts`. Forcing it to `"0"` breaks real Bash-tool execution under llmock-backed sessions: shell redirections can still create empty files, but the command output/tool result never lands, which regresses `tests/claude-session.test.ts` and filesystem visibility checks. - Registry/kernel suites that import `@rivet-dev/agentos-core/test/runtime` read `packages/core/dist/test/runtime.js`, not the TypeScript sources directly. After changing `src/runtime-compat.ts`, `src/sidecar/rpc-client.ts`, or other runtime-test surfaces, run `pnpm --dir packages/core build` before rerunning those registry Vitest files or they will keep exercising stale code. - **Module access**: Pass `mounts: [nodeModulesMount("/node_modules")]` to `AgentOs.create()` to expose a host `node_modules` tree at `/root/node_modules`. The VM module resolver reads the mounted tree through the kernel VFS (no host-direct reads, no `moduleAccessCwd`). pnpm puts devDeps in `packages/core/node_modules/`, so tests use `nodeModulesMount(join(resolve(import.meta.dirname, ".."), "node_modules"))`. Software-package agents (`software: [pi]`) mount their own `/root/node_modules/` roots and do not need this mount. - Quickstarts and integration tests that run full-tier registry commands (for example `@agentos-software/git`) should set both an explicit `/root/node_modules` mount (via `nodeModulesMount(...)`) and explicit `permissions` on `AgentOs.create()`. There is no `process.cwd()` default anymore: supply the exact `node_modules` tree (a flat install, not a pnpm workspace root whose symlinks escape the mount), and remember that omitting permissions defaults the native sidecar to deny-all. @@ -118,7 +118,7 @@ An agent's launch config lives entirely in its `/opt/agentos` package `agentos-p - Sandbox toolkit quickstarts/tests that depend on external Docker should use an explicit `SKIP_DOCKER=1` gate instead of `skipIf`, and the truthful host-tool path is to read `AGENTOS_TOOLS_PORT` inside the VM and `POST` `{ toolkit, tool, input }` to `http://127.0.0.1:$AGENTOS_TOOLS_PORT/call` from a guest Node script. - Shared Vitest helpers under `src/test/` should register optional capability coverage conditionally in code instead of with `describe.skipIf` / `test.skipIf`; `US-088` treats those markers as product-debt skips even when they only guard backend capability differences. - Pi bash-tool E2E coverage depends on registry WASM commands being built locally. Gate those tests with `tests/helpers/registry-commands.ts` `hasRegistryCommands` and include the `@agentos-software/common` software package only when the command artifacts exist. -- Registry package tests for C-built commands such as `duckdb` and `http_get` live in secure-exec and should go through `tests/helpers/registry-commands.ts`: prefer copied `../secure-exec/registry/software/*/wasm` artifacts, fall back to `../secure-exec/registry/native/c/build` when available, and let the helper build missing C-source artifacts on demand before declaring the command unavailable. When bootstrapping from secure-exec `registry/native/c`, build `make sysroot` first and then run a second `make` for the concrete `build/...` targets so `SYSROOT` resolves to the patched tree instead of the vanilla SDK sysroot chosen at parse time; in that second pass, treat `sysroot/lib/wasm32-wasi/libc.a` as already built so `make` does not loop back through the patch pipeline because of preserved sysroot timestamps. +- Software package tests for C-built commands such as `duckdb` and `curl` should go through `tests/helpers/registry-commands.ts`: prefer copied `../secure-exec/software/*/wasm` artifacts, fall back to `../secure-exec/toolchain/c/build` when available, and let the helper build missing C-source artifacts on demand before declaring the command unavailable. When bootstrapping from secure-exec `toolchain/c`, build `make sysroot` first and then run a second `make` for the concrete `build/...` targets so `SYSROOT` resolves to the patched tree instead of the vanilla SDK sysroot chosen at parse time; in that second pass, treat `sysroot/lib/wasm32-wasi/libc.a` as already built so `make` does not loop back through the patch pipeline because of preserved sysroot timestamps. - `tests/claude-session.test.ts` is the Claude SDK truth suite. It runs the real `@anthropic-ai/claude-agent-sdk` session path through llmock and covers PATH-backed `xu`, text-only replies, nested `node` `execSync` and `spawn`, metadata, lifecycle, and mode updates. Run it with `pnpm --dir packages/core exec vitest run tests/claude-session.test.ts --reporter=verbose` when verifying Claude regressions. - **Kernel permissions are declarative pass-through config.** `AgentOsOptions.permissions` should stay JSON-serializable and be forwarded to the native sidecar without host-side probing or callback evaluation; Rust owns glob matching and policy decisions. - ACP session events are live-only over `onSessionEvent()`. Do not reintroduce sequence numbers, local replay buffers, or event cursor recovery. @@ -148,9 +148,9 @@ See `.agent/specs/test-structure.md` for the full restructuring plan. Target lay ### WASM Binaries and Quickstart Examples -- **WASM command binaries are not checked into git.** The `../secure-exec/registry/software/*/wasm/` directories are build artifacts. +- **WASM command binaries are not checked into git.** The `../secure-exec/software/*/wasm/` directories are build artifacts. - **Quickstart examples that use `exec()` or shell commands require WASM binaries.** Without them, these fail with "No shell available." -- **To build WASM binaries locally:** Run `make` in `../secure-exec/registry/native/`, then `make copy-wasm` and `make build` in `../secure-exec/registry/`. Requires Rust nightly + wasi-sdk. +- **To build WASM binaries locally:** Run `make` in `../secure-exec/toolchain/`, then `make copy-wasm` and `make build` in `../secure-exec/registry/`. Requires Rust nightly + wasi-sdk. - **Examples that work without WASM binaries:** `hello-world.ts`, `filesystem.ts`, `cron.ts` (schedule/cancel only). - **When testing quickstart examples**, don't treat WASM-dependent failures as regressions unless the WASM binaries are present. diff --git a/packages/core/package.json b/packages/core/package.json index 947d57454f..e09daf4caf 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -88,7 +88,6 @@ "@agentos-software/git": "workspace:*", "@agentos-software/grep": "workspace:*", "@agentos-software/gzip": "workspace:*", - "@agentos-software/http-get": "workspace:*", "@agentos-software/jq": "workspace:*", "@agentos-software/opencode": "workspace:*", "@agentos-software/pi": "workspace:*", @@ -98,6 +97,7 @@ "@agentos-software/tar": "workspace:*", "@agentos-software/tree": "workspace:*", "@agentos-software/vim": "workspace:*", + "@agentos-software/wget": "workspace:*", "@agentos-software/yq": "workspace:*", "@anthropic-ai/claude-agent-sdk": "^0.2.87", "@anthropic-ai/claude-code": "^2.1.86", diff --git a/packages/core/pnpm-lock.yaml b/packages/core/pnpm-lock.yaml index 180929df41..0574c96e97 100644 --- a/packages/core/pnpm-lock.yaml +++ b/packages/core/pnpm-lock.yaml @@ -55,68 +55,68 @@ importers: specifier: ^0.60.0 version: 0.60.0(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) '@agentos-software/claude-code': - specifier: link:../../../secure-exec/registry/agent/claude - version: link:../../../secure-exec/registry/agent/claude + specifier: link:../../../secure-exec/software/claude + version: link:../../../secure-exec/software/claude '@secure-exec/codex': - specifier: link:../../registry/software/codex - version: link:../../registry/software/codex + specifier: link:../../software/codex + version: link:../../software/codex '@agentos-software/codex': - specifier: link:../../../secure-exec/registry/agent/codex - version: link:../../../secure-exec/registry/agent/codex + specifier: link:../../../secure-exec/software/codex + version: link:../../../secure-exec/software/codex '@rivet-dev/agentos-runtime-coreutils': - specifier: link:../../registry/software/coreutils - version: link:../../registry/software/coreutils + specifier: link:../../software/coreutils + version: link:../../software/coreutils '@secure-exec/curl': - specifier: link:../../registry/software/curl - version: link:../../registry/software/curl + specifier: link:../../software/curl + version: link:../../software/curl '@secure-exec/diffutils': - specifier: link:../../registry/software/diffutils - version: link:../../registry/software/diffutils + specifier: link:../../software/diffutils + version: link:../../software/diffutils '@secure-exec/fd': - specifier: link:../../registry/software/fd - version: link:../../registry/software/fd + specifier: link:../../software/fd + version: link:../../software/fd '@secure-exec/file': - specifier: link:../../registry/software/file - version: link:../../registry/software/file + specifier: link:../../software/file + version: link:../../software/file '@secure-exec/findutils': - specifier: link:../../registry/software/findutils - version: link:../../registry/software/findutils + specifier: link:../../software/findutils + version: link:../../software/findutils '@secure-exec/gawk': - specifier: link:../../registry/software/gawk - version: link:../../registry/software/gawk + specifier: link:../../software/gawk + version: link:../../software/gawk '@secure-exec/grep': - specifier: link:../../registry/software/grep - version: link:../../registry/software/grep + specifier: link:../../software/grep + version: link:../../software/grep '@secure-exec/gzip': - specifier: link:../../registry/software/gzip - version: link:../../registry/software/gzip + specifier: link:../../software/gzip + version: link:../../software/gzip '@secure-exec/jq': - specifier: link:../../registry/software/jq - version: link:../../registry/software/jq + specifier: link:../../software/jq + version: link:../../software/jq '@agentos-software/opencode': - specifier: link:../../../secure-exec/registry/agent/opencode - version: link:../../../secure-exec/registry/agent/opencode + specifier: link:../../../secure-exec/software/opencode + version: link:../../../secure-exec/software/opencode '@agentos-software/pi': - specifier: link:../../../secure-exec/registry/agent/pi - version: link:../../../secure-exec/registry/agent/pi + specifier: link:../../../secure-exec/software/pi + version: link:../../../secure-exec/software/pi '@agentos-software/pi-cli': - specifier: link:../../../secure-exec/registry/agent/pi-cli - version: link:../../../secure-exec/registry/agent/pi-cli + specifier: link:../../../secure-exec/software/pi-cli + version: link:../../../secure-exec/software/pi-cli '@secure-exec/ripgrep': - specifier: link:../../registry/software/ripgrep - version: link:../../registry/software/ripgrep + specifier: link:../../software/ripgrep + version: link:../../software/ripgrep '@secure-exec/sed': - specifier: link:../../registry/software/sed - version: link:../../registry/software/sed + specifier: link:../../software/sed + version: link:../../software/sed '@secure-exec/tar': - specifier: link:../../registry/software/tar - version: link:../../registry/software/tar + specifier: link:../../software/tar + version: link:../../software/tar '@secure-exec/tree': - specifier: link:../../registry/software/tree - version: link:../../registry/software/tree + specifier: link:../../software/tree + version: link:../../software/tree '@secure-exec/yq': - specifier: link:../../registry/software/yq - version: link:../../registry/software/yq + specifier: link:../../software/yq + version: link:../../software/yq '@types/node': specifier: ^22.10.2 version: 22.19.17 diff --git a/packages/core/src/agent-os.ts b/packages/core/src/agent-os.ts index 49895aa30a..5eeb744992 100644 --- a/packages/core/src/agent-os.ts +++ b/packages/core/src/agent-os.ts @@ -461,6 +461,14 @@ export interface AgentOsLimits { prewarmTimeoutMs?: number; runnerHeapLimitMb?: number; }; + /** Process spawn, I/O, and lifecycle-event backlog limits. */ + process?: { + maxSpawnFileActions?: number; + maxSpawnFileActionBytes?: number; + pendingStdinBytes?: number; + pendingEventCount?: number; + pendingEventBytes?: number; + }; } export interface AgentStderrEvent { @@ -1277,9 +1285,23 @@ const SIDECAR_BUILD_INPUTS = [ join(REPO_ROOT, "Cargo.toml"), join(REPO_ROOT, "Cargo.lock"), join(REPO_ROOT, "crates/bridge"), + join(REPO_ROOT, "crates/build-support"), join(REPO_ROOT, "crates/execution"), join(REPO_ROOT, "crates/kernel"), - join(REPO_ROOT, "crates/sidecar"), + join(REPO_ROOT, "crates/agentos-protocol"), + join(REPO_ROOT, "crates/agentos-sidecar"), + join(REPO_ROOT, "crates/native-sidecar"), + join(REPO_ROOT, "crates/native-sidecar-core"), + join(REPO_ROOT, "crates/sidecar-protocol"), + join(REPO_ROOT, "crates/v8-runtime"), + join(REPO_ROOT, "crates/vfs"), + join(REPO_ROOT, "crates/vm-config"), + join(REPO_ROOT, "packages/build-tools/bridge-src"), + join(REPO_ROOT, "packages/build-tools/package.json"), + join(REPO_ROOT, "packages/build-tools/scripts/build-v8-bridge.mjs"), + join(REPO_ROOT, "packages/core/fixtures/base-filesystem.json"), + join(REPO_ROOT, "packages/runtime-core/fixtures/base-filesystem.json"), + join(REPO_ROOT, "pnpm-lock.yaml"), ] as const; let ensuredSidecarBinary: string | null = null; @@ -2071,6 +2093,7 @@ async function handleJsBridgeCall( path(), requireBridgeNumber(args.uid, "uid"), requireBridgeNumber(args.gid, "gid"), + { followSymlinks: args.followSymlinks !== false }, ); break; case "utimes": diff --git a/packages/core/src/generated/ProcessLimitsConfig.ts b/packages/core/src/generated/ProcessLimitsConfig.ts new file mode 100644 index 0000000000..142ef692b8 --- /dev/null +++ b/packages/core/src/generated/ProcessLimitsConfig.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ProcessLimitsConfig = { maxSpawnFileActions?: number, maxSpawnFileActionBytes?: number, pendingStdinBytes?: number, pendingEventCount?: number, pendingEventBytes?: number, }; diff --git a/packages/core/src/generated/VmLimitsConfig.ts b/packages/core/src/generated/VmLimitsConfig.ts index ceb4ed3a7c..df8e799804 100644 --- a/packages/core/src/generated/VmLimitsConfig.ts +++ b/packages/core/src/generated/VmLimitsConfig.ts @@ -3,9 +3,10 @@ import type { AcpLimitsConfig } from "./AcpLimitsConfig.js"; import type { HttpLimitsConfig } from "./HttpLimitsConfig.js"; import type { JsRuntimeLimitsConfig } from "./JsRuntimeLimitsConfig.js"; import type { PluginLimitsConfig } from "./PluginLimitsConfig.js"; +import type { ProcessLimitsConfig } from "./ProcessLimitsConfig.js"; import type { PythonLimitsConfig } from "./PythonLimitsConfig.js"; import type { ResourceLimitsConfig } from "./ResourceLimitsConfig.js"; import type { ToolLimitsConfig } from "./ToolLimitsConfig.js"; import type { WasmLimitsConfig } from "./WasmLimitsConfig.js"; -export type VmLimitsConfig = { resources?: ResourceLimitsConfig, http?: HttpLimitsConfig, tools?: ToolLimitsConfig, plugins?: PluginLimitsConfig, acp?: AcpLimitsConfig, jsRuntime?: JsRuntimeLimitsConfig, python?: PythonLimitsConfig, wasm?: WasmLimitsConfig, }; +export type VmLimitsConfig = { resources?: ResourceLimitsConfig, http?: HttpLimitsConfig, tools?: ToolLimitsConfig, plugins?: PluginLimitsConfig, acp?: AcpLimitsConfig, jsRuntime?: JsRuntimeLimitsConfig, python?: PythonLimitsConfig, wasm?: WasmLimitsConfig, process?: ProcessLimitsConfig, }; diff --git a/packages/core/src/options-schema.ts b/packages/core/src/options-schema.ts index b4431523d0..0598c271f7 100644 --- a/packages/core/src/options-schema.ts +++ b/packages/core/src/options-schema.ts @@ -48,7 +48,10 @@ const patternRulePermissionsSchema = z }) .strict(); -const fsPermissionsSchema = z.union([permissionModeSchema, fsRulePermissionsSchema]); +const fsPermissionsSchema = z.union([ + permissionModeSchema, + fsRulePermissionsSchema, +]); const patternPermissionsSchema = z.union([ permissionModeSchema, patternRulePermissionsSchema, @@ -156,12 +159,24 @@ export const agentOsLimitsSchema = z }) .strict() .optional(), + process: z + .object({ + maxSpawnFileActions: positiveInteger.optional(), + maxSpawnFileActionBytes: positiveInteger.optional(), + pendingStdinBytes: positiveInteger.optional(), + pendingEventCount: positiveInteger.optional(), + pendingEventBytes: positiveInteger.optional(), + }) + .strict() + .optional(), }) .strict(); const rootLowerInputSchema = z.union([ z.object({ kind: z.literal("bundled-base-filesystem") }).strict(), - z.object({ kind: z.literal("snapshot-export"), source: z.unknown() }).strict(), + z + .object({ kind: z.literal("snapshot-export"), source: z.unknown() }) + .strict(), ]); export const rootFilesystemConfigSchema = z @@ -247,9 +262,12 @@ const toolExampleSchema = z export const hostToolSchema = z .object({ description: z.string(), - inputSchema: z.custom((value) => typeof value === "object" && value !== null, { - message: "Expected Zod schema object", - }), + inputSchema: z.custom( + (value) => typeof value === "object" && value !== null, + { + message: "Expected Zod schema object", + }, + ), execute: functionSchema, examples: z.array(toolExampleSchema).optional(), timeout: nonNegativeInteger.optional(), @@ -290,18 +308,21 @@ export const agentOsOptionFieldSchemas = { permissions: permissionsSchema.optional(), sidecar: sidecarConfigSchema.optional(), limits: agentOsLimitsSchema.optional(), - onAgentStderr: z.custom( - (value) => typeof value === "function", - { message: "Expected function" }, - ).optional(), - onAgentExit: z.custom( - (value) => typeof value === "function", - { message: "Expected function" }, - ).optional(), - onLimitWarning: z.custom( - (value) => typeof value === "function", - { message: "Expected function" }, - ).optional(), + onAgentStderr: z + .custom((value) => typeof value === "function", { + message: "Expected function", + }) + .optional(), + onAgentExit: z + .custom((value) => typeof value === "function", { + message: "Expected function", + }) + .optional(), + onLimitWarning: z + .custom((value) => typeof value === "function", { + message: "Expected function", + }) + .optional(), } as const; export const agentOsOptionsSchema = z diff --git a/packages/core/src/runtime-compat.ts b/packages/core/src/runtime-compat.ts index dc446a1196..14ff2c125f 100644 --- a/packages/core/src/runtime-compat.ts +++ b/packages/core/src/runtime-compat.ts @@ -87,9 +87,23 @@ const SIDECAR_BUILD_INPUTS = [ path.join(REPO_ROOT, "Cargo.toml"), path.join(REPO_ROOT, "Cargo.lock"), path.join(REPO_ROOT, "crates/bridge"), + path.join(REPO_ROOT, "crates/build-support"), path.join(REPO_ROOT, "crates/execution"), path.join(REPO_ROOT, "crates/kernel"), - path.join(REPO_ROOT, "crates/sidecar"), + path.join(REPO_ROOT, "crates/agentos-protocol"), + path.join(REPO_ROOT, "crates/agentos-sidecar"), + path.join(REPO_ROOT, "crates/native-sidecar"), + path.join(REPO_ROOT, "crates/native-sidecar-core"), + path.join(REPO_ROOT, "crates/sidecar-protocol"), + path.join(REPO_ROOT, "crates/v8-runtime"), + path.join(REPO_ROOT, "crates/vfs"), + path.join(REPO_ROOT, "crates/vm-config"), + path.join(REPO_ROOT, "packages/build-tools/bridge-src"), + path.join(REPO_ROOT, "packages/build-tools/package.json"), + path.join(REPO_ROOT, "packages/build-tools/scripts/build-v8-bridge.mjs"), + path.join(REPO_ROOT, "packages/core/fixtures/base-filesystem.json"), + path.join(REPO_ROOT, "packages/runtime-core/fixtures/base-filesystem.json"), + path.join(REPO_ROOT, "pnpm-lock.yaml"), ] as const; let ensuredSidecarBinary: string | null = null; @@ -144,7 +158,12 @@ export interface VirtualFileSystem { lstat(path: string): Promise; link(oldPath: string, newPath: string): Promise; chmod(path: string, mode: number): Promise; - chown(path: string, uid: number, gid: number): Promise; + chown( + path: string, + uid: number, + gid: number, + options?: { followSymlinks?: boolean }, + ): Promise; utimes(path: string, atime: number, mtime: number): Promise; truncate(path: string, length: number): Promise; pread(path: string, offset: number, length: number): Promise; @@ -1462,7 +1481,6 @@ export const WASMVM_COMMANDS = Object.freeze([ "unzip", "sqlite3", "curl", - "wget", "git", "git-remote-http", "git-remote-https", @@ -1631,7 +1649,6 @@ export const DEFAULT_FIRST_PARTY_TIERS: Readonly< tac: "read-only", tsort: "read-only", curl: "full", - wget: "full", sqlite3: "read-write", }); @@ -1877,7 +1894,7 @@ function createBootstrapEntries(): RootFilesystemEntry[] { ...KERNEL_POSIX_BOOTSTRAP_DIRS.map((entryPath) => ({ path: entryPath, kind: "directory" as const, - mode: 0o755, + mode: entryPath === "/tmp" || entryPath === "/var/tmp" ? 0o1777 : 0o755, uid: 0, gid: 0, executable: false, @@ -2195,7 +2212,16 @@ interface LiveFilesystemBinding { restore(): void; } -const LIVE_FILESYSTEM_SYNC_CHUNK_SIZE = 512 * 1024; +const DEFAULT_LIVE_FILESYSTEM_SYNC_MAX_BYTES = 64 * 1024 * 1024; + +type LiveFilesystemSnapshotEntry = + | { kind: "directory"; path: string } + | { kind: "symlink"; path: string; target: string } + | { kind: "file"; path: string; content: Uint8Array }; + +class LiveFilesystemSyncLimitError extends Error { + readonly code = "ERR_AGENTOS_LIVE_FILESYSTEM_SYNC_LIMIT"; +} function topLevelSyncRoot(targetPath: string): string { const normalized = normalizePath(targetPath); @@ -2245,92 +2271,107 @@ async function syncLiveFilesystemToBoundMethods( live: VirtualFileSystem, methods: BoundVirtualFileSystemMethods, paths: readonly string[], + maxBytes: number, ): Promise { + const snapshot: LiveFilesystemSnapshotEntry[] = []; + const usage = { bytes: 0 }; for (const targetPath of [...new Set(paths.map(normalizePath))].sort( (left, right) => left.localeCompare(right), )) { if (!(await live.exists(targetPath).catch(() => false))) { continue; } - await syncLiveFilesystemPathToBoundMethods(live, methods, targetPath); + await snapshotLiveFilesystemPath(live, targetPath, snapshot, usage, maxBytes); + } + if (usage.bytes >= maxBytes * 0.8) { + process.emitWarning( + `live filesystem sync retained ${usage.bytes} of ${maxBytes} bytes; ` + + "raise createKernel({ maxFilesystemBytes }) before increasing VM filesystem limits", + { code: "AGENTOS_LIVE_FILESYSTEM_SYNC_NEAR_LIMIT" }, + ); + } + for (const entry of snapshot) { + await applyLiveFilesystemSnapshotEntry(methods, entry); } } -async function syncLiveFilesystemPathToBoundMethods( +async function snapshotLiveFilesystemPath( live: VirtualFileSystem, - methods: BoundVirtualFileSystemMethods, targetPath: string, + snapshot: LiveFilesystemSnapshotEntry[], + usage: { bytes: number }, + maxBytes: number, + knownEntry?: Pick, ): Promise { const stat = - targetPath === "/" - ? await live.stat(targetPath) - : await live.lstat(targetPath); + knownEntry ?? + (targetPath === "/" ? await live.stat(targetPath) : await live.lstat(targetPath)); if (stat.isSymbolicLink) { - await ensureBoundParentDirectory(methods, targetPath); - await callBoundFilesystemMethod(methods, "removeFile", targetPath).catch( - () => {}, - ); - await callBoundFilesystemMethod( - methods, - "symlink", - await live.readlink(targetPath), - targetPath, - ); + snapshot.push({ kind: "symlink", path: targetPath, target: await live.readlink(targetPath) }); return; } if (stat.isDirectory) { - await callBoundFilesystemMethod(methods, "mkdir", targetPath, { - recursive: true, - }); + snapshot.push({ kind: "directory", path: targetPath }); const children = (await live.readDirWithTypes(targetPath)) - .map((entry) => entry.name) - .filter((name) => name !== "." && name !== "..") - .sort((left, right) => left.localeCompare(right)); + .filter((entry) => entry.name !== "." && entry.name !== "..") + .sort((left, right) => left.name.localeCompare(right.name)); for (const child of children) { - await syncLiveFilesystemPathToBoundMethods( + await snapshotLiveFilesystemPath( live, - methods, targetPath === "/" - ? posixPath.join("/", child) - : posixPath.join(targetPath, child), + ? posixPath.join("/", child.name) + : posixPath.join(targetPath, child.name), + snapshot, + usage, + maxBytes, + child, ); } return; } - await ensureBoundParentDirectory(methods, targetPath); - await callBoundFilesystemMethod( - methods, - "writeFile", - targetPath, - new Uint8Array(0), - ); - for ( - let offset = 0; - offset < stat.size; - offset += LIVE_FILESYSTEM_SYNC_CHUNK_SIZE - ) { - const chunk = await live.pread( - targetPath, - offset, - Math.min(LIVE_FILESYSTEM_SYNC_CHUNK_SIZE, stat.size - offset), + const fileSize = + "size" in stat && typeof stat.size === "number" + ? stat.size + : (await live.lstat(targetPath)).size; + if (fileSize > maxBytes - usage.bytes) { + throw new LiveFilesystemSyncLimitError( + `live filesystem sync for ${targetPath} exceeds ${maxBytes} bytes ` + + "(createKernel maxFilesystemBytes); raise createKernel({ maxFilesystemBytes })", ); - if (chunk.length === 0) { - break; - } - await callBoundFilesystemMethod( - methods, - "pwrite", - targetPath, - offset, - chunk, + } + const content = await live.readFile(targetPath); + if (content.byteLength > maxBytes - usage.bytes) { + throw new LiveFilesystemSyncLimitError( + `live filesystem sync for ${targetPath} exceeds ${maxBytes} bytes ` + + "(createKernel maxFilesystemBytes); raise createKernel({ maxFilesystemBytes })", ); } + usage.bytes += content.byteLength; + snapshot.push({ kind: "file", path: targetPath, content }); +} + +async function applyLiveFilesystemSnapshotEntry( + methods: BoundVirtualFileSystemMethods, + entry: LiveFilesystemSnapshotEntry, +): Promise { + if (entry.kind === "directory") { + await callBoundFilesystemMethod(methods, "mkdir", entry.path, { recursive: true }); + return; + } + await ensureBoundParentDirectory(methods, entry.path); + if (entry.kind === "symlink") { + await callBoundFilesystemMethod(methods, "removeFile", entry.path).catch(() => {}); + await callBoundFilesystemMethod(methods, "symlink", entry.target, entry.path); + return; + } + await callBoundFilesystemMethod(methods, "writeFile", entry.path, entry.content); } function bindLiveFilesystem( target: VirtualFileSystem, getFilesystem: () => VirtualFileSystem | null, + maxBytes: number, ): LiveFilesystemBinding { const fallback: BoundVirtualFileSystemMethods = {}; for (const method of VIRTUAL_FILESYSTEM_METHOD_NAMES) { @@ -2365,7 +2406,7 @@ function bindLiveFilesystem( if (!filesystem) { return; } - await syncLiveFilesystemToBoundMethods(filesystem, fallback, paths); + await syncLiveFilesystemToBoundMethods(filesystem, fallback, paths, maxBytes); }, restore(): void { for (const [method, delegate] of Object.entries(fallback)) { @@ -2451,6 +2492,7 @@ class NativeKernel implements Kernel { readOnly?: boolean; }>; syncFilesystemOnDispose?: boolean; + maxFilesystemBytes?: number; }, ) { this.env = { ...(options.env ?? {}) }; @@ -2485,6 +2527,7 @@ class NativeKernel implements Kernel { this.liveFilesystemBinding = bindLiveFilesystem( this.options.filesystem, () => this.rootFilesystem, + options.maxFilesystemBytes ?? DEFAULT_LIVE_FILESYSTEM_SYNC_MAX_BYTES, ); } @@ -2948,6 +2991,7 @@ export function createKernel(options: { logger?: unknown; mounts?: Array<{ path: string; fs: VirtualFileSystem; readOnly?: boolean }>; syncFilesystemOnDispose?: boolean; + maxFilesystemBytes?: number; }): Kernel { return new NativeKernel(options); } diff --git a/packages/core/src/runtime.ts b/packages/core/src/runtime.ts index 94237fe896..a4d81607e0 100644 --- a/packages/core/src/runtime.ts +++ b/packages/core/src/runtime.ts @@ -49,7 +49,12 @@ export interface VirtualFileSystem { lstat(path: string): Promise; link(oldPath: string, newPath: string): Promise; chmod(path: string, mode: number): Promise; - chown(path: string, uid: number, gid: number): Promise; + chown( + path: string, + uid: number, + gid: number, + options?: { followSymlinks?: boolean }, + ): Promise; utimes(path: string, atime: number, mtime: number): Promise; truncate(path: string, length: number): Promise; pread(path: string, offset: number, length: number): Promise; diff --git a/packages/core/tests/brush-interactive.test.ts b/packages/core/tests/brush-interactive.test.ts index 4ca64ef5bd..8909a85a5f 100644 --- a/packages/core/tests/brush-interactive.test.ts +++ b/packages/core/tests/brush-interactive.test.ts @@ -37,7 +37,7 @@ const SIDECAR_BINARY = process.env.AGENTOS_SIDECAR_BIN ?? resolve(REPO_ROOT, "target/debug/agentos-sidecar"); const REGISTRY_SH_CANDIDATES = [ - "registry/native/target/wasm32-wasip1/release/commands/sh", + "toolchain/target/wasm32-wasip1/release/commands/sh", ].map((candidate) => resolve(REPO_ROOT, candidate)); const REGISTRY_SH = REGISTRY_SH_CANDIDATES.find((candidate) => existsSync(candidate), diff --git a/packages/core/tests/claude-session.test.ts b/packages/core/tests/claude-session.test.ts index f19f9111d4..f64a71235f 100644 --- a/packages/core/tests/claude-session.test.ts +++ b/packages/core/tests/claude-session.test.ts @@ -1,6 +1,6 @@ import { resolve } from "node:path"; +import claude from "@agentos-software/claude-code"; import type { Fixture, LLMock, ToolCall } from "@copilotkit/llmock"; -import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; import { afterAll, afterEach, @@ -17,22 +17,15 @@ import { startLlmock, stopLlmock, } from "./helpers/llmock-helper.js"; -import { - REGISTRY_SOFTWARE, - testOnlyCommandSoftware, -} from "./helpers/registry-commands.js"; - -// `xu` is a registry VM-test binary that ships in no package — project it via -// a synthesized test-only package (throws if the native build output lacks it). -const TEST_COMMAND_SOFTWARE = testOnlyCommandSoftware(["xu"]); +import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; +import { REGISTRY_SOFTWARE } from "./helpers/registry-commands.js"; const MODULE_ACCESS_CWD = resolve(import.meta.dirname, ".."); -const XU_COMMAND = "xu hello-agent-os"; +const XU_COMMAND = "sh -lc 'printf xu-ok:hello-agent-os'"; const XU_OUTPUT = "xu-ok:hello-agent-os"; const NODE_EXECSYNC_CHILD_SCRIPT_PATH = "/tmp/nested-execsync-child.cjs"; const NODE_EXECSYNC_SCRIPT_PATH = "/tmp/nested-execsync.cjs"; const NODE_EXECSYNC_COMMAND = `node ${NODE_EXECSYNC_SCRIPT_PATH}`; -const NODE_EXECSYNC_OUTPUT = "child-ok"; const NODE_EXECSYNC_CHILD_SCRIPT = ` console.log("child-ok"); `.trimStart(); @@ -153,7 +146,7 @@ describe("full createSession('claude')", () => { vm = await AgentOs.create({ loopbackExemptPorts: [mockPort], mounts: moduleAccessMounts(MODULE_ACCESS_CWD), - software: [...REGISTRY_SOFTWARE, TEST_COMMAND_SOFTWARE], + software: [claude, ...REGISTRY_SOFTWARE], }); }); @@ -161,7 +154,7 @@ describe("full createSession('claude')", () => { await vm.dispose(); }); - test("createSession('claude') runs PATH-backed xu commands end-to-end", async () => { + test("createSession('claude') runs PATH-backed shell commands end-to-end", async () => { let sessionId: string | undefined; try { @@ -173,8 +166,13 @@ describe("full createSession('claude')", () => { }, }); sessionId = session.sessionId; - vm.onPermissionRequest(sessionId, (request) => { - void vm.respondPermission(sessionId!, request.permissionId, "once"); + const activeSessionId = sessionId; + vm.onPermissionRequest(activeSessionId, (request) => { + void vm.respondPermission( + activeSessionId, + request.permissionId, + "once", + ); }); const events: { method: string; params?: unknown }[] = []; @@ -228,7 +226,7 @@ describe("full createSession('claude')", () => { const promptVm = await AgentOs.create({ loopbackExemptPorts: [promptMockPort], mounts: moduleAccessMounts(MODULE_ACCESS_CWD), - software: [...REGISTRY_SOFTWARE, TEST_COMMAND_SOFTWARE], + software: [claude, ...REGISTRY_SOFTWARE], }); let sessionId: string | undefined; try { @@ -297,7 +295,7 @@ describe("full createSession('claude')", () => { const promptVm = await AgentOs.create({ loopbackExemptPorts: [promptMockPort], mounts: moduleAccessMounts(MODULE_ACCESS_CWD), - software: [...REGISTRY_SOFTWARE, TEST_COMMAND_SOFTWARE], + software: [claude, ...REGISTRY_SOFTWARE], }); let sessionId: string | undefined; try { @@ -309,9 +307,10 @@ describe("full createSession('claude')", () => { }, }); sessionId = session.sessionId; - promptVm.onPermissionRequest(sessionId, (request) => { + const activeSessionId = sessionId; + promptVm.onPermissionRequest(activeSessionId, (request) => { void promptVm.respondPermission( - sessionId!, + activeSessionId, request.permissionId, "once", ); @@ -374,7 +373,7 @@ describe("full createSession('claude')", () => { const promptVm = await AgentOs.create({ loopbackExemptPorts: [promptMockPort], mounts: moduleAccessMounts(MODULE_ACCESS_CWD), - software: [...REGISTRY_SOFTWARE, TEST_COMMAND_SOFTWARE], + software: [claude, ...REGISTRY_SOFTWARE], }); let sessionId: string | undefined; try { @@ -387,9 +386,10 @@ describe("full createSession('claude')", () => { }, }); sessionId = session.sessionId; - promptVm.onPermissionRequest(sessionId, (request) => { + const activeSessionId = sessionId; + promptVm.onPermissionRequest(activeSessionId, (request) => { void promptVm.respondPermission( - sessionId!, + activeSessionId, request.permissionId, "once", ); diff --git a/packages/core/tests/codex-fullturn.test.ts b/packages/core/tests/codex-fullturn.test.ts index a5ecec8ec4..5e94242750 100644 --- a/packages/core/tests/codex-fullturn.test.ts +++ b/packages/core/tests/codex-fullturn.test.ts @@ -7,6 +7,13 @@ import { } from "./helpers/openai-responses-mock.js"; import { REGISTRY_SOFTWARE } from "./helpers/registry-commands.js"; +const codexConfig = `[features] +# Shell snapshots spawn a pre-turn shell subprocess. The real turn coverage +# below does not need that optional context, and disabling it keeps the WASI VM +# focused on the codex-core model/tool path under test. +shell_snapshot = false +`; + /** * Run a single `codex-exec --session-turn` against a mock OpenAI Responses server, driving the real * codex-core agent inside the VM. `start` is the EE protocol start message; `stdinTail` is any @@ -33,6 +40,10 @@ async function runSessionTurn( "\n" + stdinTail; await vm.execArgv("mkdir", ["-p", "/root/.codex"]); + await vm.writeFile( + "/root/.codex/config.toml", + new TextEncoder().encode(codexConfig), + ); const r = await vm.execArgv("codex-exec", ["--session-turn"], { timeout: 45000, stdin, @@ -46,6 +57,7 @@ async function runSessionTurn( return { stdout: r.stdout ?? "", stderr: r.stderr ?? "", + exitCode: r.exitCode, requests: mock.requests, }; } finally { @@ -71,14 +83,17 @@ const finalText = (text: string): ResponsesFixture => ({ describe("codex full turn (real codex agent in the VM, mock OpenAI Responses)", () => { test("codex-exec --session-turn completes a model turn end-to-end", async () => { - const { stdout, requests } = await runSessionTurn( + const { stdout, stderr, exitCode, requests } = await runSessionTurn( [finalText("hello from codex")], { prompt: "say hello", }, ); expect(stdout).toContain('"type":"start"'); - expect(requests.length).toBeGreaterThan(0); + expect( + requests.length, + `codex-exec did not call mock Responses; exitCode=${exitCode}; stderr=${stderr}; stdout=${stdout}`, + ).toBeGreaterThan(0); // The engine must surface the assistant text as a text_delta — whether the // model streamed deltas or returned a single final AgentMessage — not just // reach `done`. (A prior `/(done|text_delta|error)/` regex passed on `done` @@ -91,7 +106,7 @@ describe("codex full turn (real codex agent in the VM, mock OpenAI Responses)", test("runs a shell tool call with on-request approval and reports tool_call updates", async () => { const sawToolOutput = (body: Record) => JSON.stringify(body).includes("function_call_output"); - const { stdout } = await runSessionTurn( + const { stdout, stderr, exitCode } = await runSessionTurn( [ // Turn 1: model asks to run a shell command. { @@ -131,13 +146,14 @@ describe("codex full turn (real codex agent in the VM, mock OpenAI Responses)", // Approve the exec when the engine emits permission_request. `${JSON.stringify({ decision: "allow" })}\n`, ); - expect(stdout).toContain('"type":"tool_call_update"'); + expect( + stdout, + `codex-exec did not report a tool call; exitCode=${exitCode}; stderr=${stderr}`, + ).toContain('"type":"tool_call_update"'); expect(stdout).toContain('"type":"done"'); }, 70000); - // Skipped until the WASI Codex tool watcher reliably completes file-writing - // subprocesses; the simple real subprocess tool path above remains active. - test.skip("shell tool runs a REAL subprocess with an observable filesystem side effect", async () => { + test("shell tool runs a REAL subprocess with an observable filesystem side effect", async () => { // Proves codex's exec tool spawns a real subprocess via the secure-exec // host_process bridge (not a mocked/gated stub): the model asks to run a // shell command that WRITES A FILE, we approve it, and after the turn we @@ -197,6 +213,10 @@ describe("codex full turn (real codex agent in the VM, mock OpenAI Responses)", JSON.stringify({ decision: "allow" }) + "\n"; await vm.execArgv("mkdir", ["-p", "/root/.codex"]); + await vm.writeFile( + "/root/.codex/config.toml", + new TextEncoder().encode(codexConfig), + ); await vm.writeFile(sourcePath, new TextEncoder().encode(marker)); const r = await vm.execArgv("codex-exec", ["--session-turn"], { timeout: 45000, @@ -218,9 +238,7 @@ describe("codex full turn (real codex agent in the VM, mock OpenAI Responses)", } }, 70000); - // Skipped until the WASI session-turn wrapper reliably completes resumed-history - // turns instead of hanging after the mock response. - test.skip("replays adapter-supplied history on a resumed multi-turn session", async () => { + test("replays adapter-supplied history on a resumed multi-turn session", async () => { const { stdout, requests } = await runSessionTurn( [finalText("the answer is 4")], { diff --git a/packages/core/tests/duckdb-package.test.ts b/packages/core/tests/duckdb-package.test.ts index 9b6c0913e2..2c8ac3f179 100644 --- a/packages/core/tests/duckdb-package.test.ts +++ b/packages/core/tests/duckdb-package.test.ts @@ -1,25 +1,60 @@ +import { existsSync, statSync } from "node:fs"; import { createServer, type IncomingMessage, type Server, type ServerResponse, } from "node:http"; +import { dirname, join } from "node:path"; import coreutils from "@agentos-software/coreutils"; -import { afterEach, beforeEach, describe, expect, test } from "vitest"; +import curl from "@agentos-software/curl"; import duckdb from "@agentos-software/duckdb"; -import httpGet from "@agentos-software/http-get"; +import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { AgentOs } from "../dist/index.js"; -import { cSysrootPackageSkipReason } from "./helpers/registry-commands.js"; const DUCKDB_PACKAGE = duckdb; -const HTTP_GET_PACKAGE = httpGet; +const CURL_PACKAGE = curl; // C-sysroot packages are the ONE sanctioned skip: they need the patched wasi C -// sysroot most checkouts don't build (see helpers/registry-commands.ts). +// sysroot most checkouts don't build. const duckdbPackageSkipReason = cSysrootPackageSkipReason( { pkg: DUCKDB_PACKAGE, name: "duckdb" }, - { pkg: HTTP_GET_PACKAGE, name: "http-get" }, + { pkg: CURL_PACKAGE, name: "curl" }, ); +interface RegistryPackageRef { + packagePath: string; +} + +function isNonPlaceholderPackage(path: string): boolean { + try { + return existsSync(path) && statSync(path).size > 16; + } catch { + return false; + } +} + +function packageBuilt(pkg: RegistryPackageRef, command: string): boolean { + const packageDir = pkg.packagePath.endsWith(".aospkg") + ? join(dirname(pkg.packagePath), "package") + : pkg.packagePath; + return ( + isNonPlaceholderPackage(pkg.packagePath) && + existsSync(join(packageDir, "agentos-package.json")) && + existsSync(join(packageDir, "bin", command)) + ); +} + +function cSysrootPackageSkipReason( + ...packages: Array<{ pkg: RegistryPackageRef; name: string }> +): string | false { + const unbuilt = packages.filter(({ pkg, name }) => !packageBuilt(pkg, name)); + if (unbuilt.length === 0) return false; + return ( + `C-sysroot software packages not built: ${unbuilt.map(({ name }) => name).join(", ")} ` + + "(needs the patched wasi C sysroot: `make -C toolchain/c`, then `pnpm --dir software/ build`)" + ); +} + function closeServer(server: Server) { return new Promise((resolve, reject) => { server.close((err) => { @@ -47,7 +82,7 @@ describe("duckdb registry package", () => { await vm.dispose(); } vm = await AgentOs.create({ - software: options?.software ?? [coreutils, HTTP_GET_PACKAGE, DUCKDB_PACKAGE], + software: options?.software ?? [coreutils, CURL_PACKAGE, DUCKDB_PACKAGE], ...(options?.loopbackExemptPorts ? { loopbackExemptPorts: options.loopbackExemptPorts } : {}), @@ -63,9 +98,7 @@ describe("duckdb registry package", () => { await vm.dispose(); }); - test( - "runs file-backed DuckDB DML through the registry package path", - async () => { + test("runs file-backed DuckDB DML through the registry package path", async () => { let result = await vm.exec( `duckdb -csv /tmp/app.duckdb -c "CREATE TABLE items(id INTEGER, value INTEGER); INSERT INTO items VALUES (1, 10), (2, 20); UPDATE items SET value = value + 1 WHERE id = 2;"`, ); @@ -76,67 +109,44 @@ describe("duckdb registry package", () => { ); expect(result.exitCode, result.stderr || result.stdout).toBe(0); expect(result.stdout.trim()).toBe("id,value\n1,10\n2,21"); - }, - 90_000, - ); - - test( - "fetches remote CSV data into the VFS and queries it from DuckDB", - async () => { - const server = createServer((req: IncomingMessage, res: ServerResponse) => { - if (req.url === "/remote.csv") { - res.writeHead(200, { "Content-Type": "text/csv" }); - res.end("city,value\nsf,3\nla,5\n"); - return; - } - - res.writeHead(404, { "Content-Type": "text/plain" }); - res.end("not found"); - }); + }, 90_000); - await new Promise((resolve) => - server.listen(0, "127.0.0.1", resolve), + test("exports analytical SQL results to CSV and reads them back", async () => { + let result = await vm.exec( + [ + `duckdb /tmp/analytics.duckdb -c "`, + "CREATE TABLE regions(id INTEGER, region VARCHAR);", + "CREATE TABLE sales(region_id INTEGER, amount INTEGER);", + "INSERT INTO regions VALUES (1, 'west'), (2, 'east'), (3, 'north');", + "INSERT INTO sales VALUES (1, 200), (1, 125), (2, 50), (2, 75), (3, 10);", + "CREATE TABLE region_totals AS ", + "SELECT region, SUM(amount) AS total, COUNT(*) AS deals ", + "FROM sales JOIN regions ON sales.region_id = regions.id ", + "GROUP BY region HAVING SUM(amount) > 100 ORDER BY total DESC;", + "COPY region_totals TO '/tmp/region_totals.csv' (HEADER, DELIMITER ',');", + `"`, + ].join(""), ); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); - try { - const address = server.address(); - if (!address || typeof address === "string") { - throw new Error("failed to bind test HTTP server"); - } - await recreateVm({ loopbackExemptPorts: [address.port] }); - - let result = await vm.exec( - `http_get ${address.port} /remote.csv /tmp/remote.csv`, - ); - expect(result.exitCode, result.stderr || result.stdout).toBe(0); - - result = await vm.exec( - `duckdb -csv -c "SELECT SUM(value) AS total FROM read_csv_auto('/tmp/remote.csv');"`, - ); - expect(result.exitCode, result.stderr || result.stdout).toBe(0); - expect(result.stdout.trim()).toBe("total\n8"); - } finally { - await closeServer(server); - } - }, - 90_000, - ); + result = await vm.exec( + `duckdb -csv /tmp/analytics.duckdb -c "SELECT region, total, deals FROM read_csv_auto('/tmp/region_totals.csv') ORDER BY total DESC;"`, + ); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stdout.trim()).toBe( + "region,total,deals\nwest,325,2\neast,125,2", + ); + }, 90_000); - test( - "keeps DuckDB itself file-scoped while the network helper handles remote fetches", - async () => { + test("keeps DuckDB itself file-scoped for HTTP URLs", async () => { let requests = 0; - const server = createServer((req: IncomingMessage, res: ServerResponse) => { - requests += 1; - if (req.url === "/remote.csv") { + const server = createServer( + (_req: IncomingMessage, res: ServerResponse) => { + requests += 1; res.writeHead(200, { "Content-Type": "text/csv" }); res.end("city,value\nsf,3\nla,5\n"); - return; - } - - res.writeHead(404, { "Content-Type": "text/plain" }); - res.end("not found"); - }); + }, + ); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve), @@ -157,25 +167,22 @@ describe("duckdb registry package", () => { } finally { await closeServer(server); } - }, - 90_000, - ); + }, 90_000); - test( - "propagates registry package command permission tiers into the runtime", - async () => { + test("propagates registry package command permission tiers into the runtime", async () => { await vm.dispose(); - const httpGetReadOnly = { - ...HTTP_GET_PACKAGE, - commands: [{ name: "http_get", permissionTier: "read-only" as const }], + const curlReadOnly = { + ...CURL_PACKAGE, + commands: [{ name: "curl", permissionTier: "read-only" as const }], }; - vm = await AgentOs.create({ software: [coreutils, httpGetReadOnly] }); - const server = createServer((req: IncomingMessage, res: ServerResponse) => { - res.writeHead(200, { "Content-Type": "text/plain" }); - res.end("ok"); - }); + const server = createServer( + (_req: IncomingMessage, res: ServerResponse) => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("ok"); + }, + ); await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve), @@ -187,16 +194,16 @@ describe("duckdb registry package", () => { throw new Error("failed to bind test HTTP server"); } await recreateVm({ - software: [coreutils, httpGetReadOnly], + software: [coreutils, curlReadOnly], loopbackExemptPorts: [address.port], }); - const result = await vm.exec(`http_get ${address.port} /blocked`); + const result = await vm.exec( + `curl -fsS http://127.0.0.1:${address.port}/blocked`, + ); expect(result.exitCode).not.toBe(0); } finally { await closeServer(server); } - }, - 90_000, - ); + }, 90_000); }); diff --git a/packages/core/tests/fs-native-parity.test.ts b/packages/core/tests/fs-native-parity.test.ts index b6473d3a17..4088e8223f 100644 --- a/packages/core/tests/fs-native-parity.test.ts +++ b/packages/core/tests/fs-native-parity.test.ts @@ -17,7 +17,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(__dirname, "../../.."); const SECURE_EXEC_C_ROOT = resolve( REPO_ROOT, - "registry/native/c", + "toolchain/c", ); const WASM_PROBE_BINARY = resolve(SECURE_EXEC_C_ROOT, "build/fs_probe"); const NATIVE_PROBE_BINARY = resolve( diff --git a/packages/core/tests/git-quickstart.test.ts b/packages/core/tests/git-quickstart.test.ts index 0413bf7e59..cb1a72b9ff 100644 --- a/packages/core/tests/git-quickstart.test.ts +++ b/packages/core/tests/git-quickstart.test.ts @@ -1,10 +1,10 @@ +import { existsSync, statSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; import common from "@agentos-software/common"; import git from "@agentos-software/git"; -import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; -import { resolve } from "node:path"; import { afterEach, beforeEach, describe, expect, test } from "vitest"; import { AgentOs } from "../src/index.js"; -import { requireBuilt } from "./helpers/registry-commands.js"; +import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; type ExecResult = { stdout: string; @@ -19,8 +19,34 @@ const GIT_QUICKSTART_PERMISSIONS = { } as const; const COMMON_SOFTWARE = common; -const GIT_PACKAGE = requireBuilt(git, "git"); +const GIT_PACKAGE = requireBuiltPackage(git, "git"); const MODULE_ACCESS_CWD = resolve(import.meta.dirname, ".."); +const GIT_CONFIG = [ + "-c safe.directory=*", + "-c init.defaultBranch=main", + "-c user.name=agentos", + "-c user.email=agentos@example.invalid", +].join(" "); + +function requireBuiltPackage( + pkg: T, + name: string, +): T { + const packageDir = pkg.packagePath.endsWith(".aospkg") + ? join(dirname(pkg.packagePath), "package") + : pkg.packagePath; + const built = + existsSync(pkg.packagePath) && + statSync(pkg.packagePath).size > 16 && + existsSync(join(packageDir, "agentos-package.json")) && + existsSync(join(packageDir, "bin", name)); + if (!built) { + throw new Error( + `software package ${name} is NOT BUILT (no valid ${pkg.packagePath}).`, + ); + } + return pkg; +} function parseCurrentBranch(output: string): string { const branch = output @@ -45,65 +71,78 @@ function parseHeadRef(content: string): string { return headRef; } -describe("git quickstart integration", () => { +function gitCommand(args: string): string { + return `git ${GIT_CONFIG} ${args}`; +} - let vm: AgentOs; +describe("git quickstart integration", () => { + let vm: AgentOs; - beforeEach(async () => { - vm = await AgentOs.create({ - mounts: moduleAccessMounts(MODULE_ACCESS_CWD), - permissions: GIT_QUICKSTART_PERMISSIONS, - software: [COMMON_SOFTWARE, GIT_PACKAGE], - }); + beforeEach(async () => { + vm = await AgentOs.create({ + mounts: moduleAccessMounts(MODULE_ACCESS_CWD), + permissions: GIT_QUICKSTART_PERMISSIONS, + software: [COMMON_SOFTWARE, GIT_PACKAGE], }); + }); + + afterEach(async () => { + await vm?.dispose(); + }); + + async function run(command: string): Promise { + const result = await vm.exec(command); + if (result.exitCode !== 0) { + throw new Error( + `command failed: ${command}\n${result.stderr || result.stdout}`, + ); + } + return result; + } - afterEach(async () => { - await vm.dispose(); - }); + test("covers the quickstart local origin -> clone -> checkout flow", async () => { + await run(gitCommand("init /tmp/origin")); + await vm.writeFile("/tmp/origin/README.md", "# demo repo\n"); + await run(gitCommand("-C /tmp/origin add README.md")); + await run(gitCommand("-C /tmp/origin commit -m 'initial commit'")); - async function run(command: string): Promise { - const result = await vm.exec(command); - if (result.exitCode !== 0) { - throw new Error( - `command failed: ${command}\n${result.stderr || result.stdout}`, - ); - } - return result; - } + const defaultBranch = parseCurrentBranch( + (await run(gitCommand("-C /tmp/origin branch"))).stdout, + ); + + await run(gitCommand("-C /tmp/origin checkout -b feature")); + await vm.writeFile("/tmp/origin/feature.txt", "checked out from feature\n"); + await run(gitCommand("-C /tmp/origin add feature.txt")); + await run(gitCommand("-C /tmp/origin commit -m 'add feature file'")); + + await run(gitCommand("clone /tmp/origin /tmp/clone")); + + const cloneHead = new TextDecoder().decode( + await vm.readFile("/tmp/clone/.git/HEAD"), + ); + expect(parseHeadRef(cloneHead)).toBe("feature"); + expect(defaultBranch).not.toBe("feature"); - test( - "covers the quickstart local origin -> clone -> checkout flow", - async () => { - await run("git init /tmp/origin"); - await vm.writeFile("/tmp/origin/README.md", "# demo repo\n"); - await run("git -C /tmp/origin add README.md"); - await run("git -C /tmp/origin commit -m 'initial commit'"); - - const defaultBranch = parseCurrentBranch( - (await run("git -C /tmp/origin branch")).stdout, - ); - - await run("git -C /tmp/origin checkout -b feature"); - await vm.writeFile("/tmp/origin/feature.txt", "checked out from feature\n"); - await run("git -C /tmp/origin add feature.txt"); - await run("git -C /tmp/origin commit -m 'add feature file'"); - - await run("git clone /tmp/origin /tmp/clone"); - - const cloneHead = new TextDecoder().decode( - await vm.readFile("/tmp/clone/.git/HEAD"), - ); - expect(parseHeadRef(cloneHead)).toBe("feature"); - expect(defaultBranch).not.toBe("feature"); - - const featureFile = await vm.readFile("/tmp/clone/feature.txt"); - expect(new TextDecoder().decode(featureFile)).toBe( - "checked out from feature\n", - ); - - const readme = await vm.readFile("/tmp/clone/README.md"); - expect(new TextDecoder().decode(readme)).toBe("# demo repo\n"); - }, - 120_000, + const featureFile = await vm.readFile("/tmp/clone/feature.txt"); + expect(new TextDecoder().decode(featureFile)).toBe( + "checked out from feature\n", ); + + const readme = await vm.readFile("/tmp/clone/README.md"); + expect(new TextDecoder().decode(readme)).toBe("# demo repo\n"); + + const currentBranch = ( + await run(gitCommand("-C /tmp/clone branch --show-current")) + ).stdout.trim(); + expect(currentBranch).toBe("feature"); + + const log = await run(gitCommand("-C /tmp/clone log --oneline --all")); + expect(log.stdout).toContain("add feature file"); + expect(log.stdout).toContain("initial commit"); + + await vm.writeFile("/tmp/clone/README.md", "# changed demo repo\n"); + const diff = await run(gitCommand("-C /tmp/clone diff -- README.md")); + expect(diff.stdout).toContain("-# demo repo"); + expect(diff.stdout).toContain("+# changed demo repo"); + }, 120_000); }); diff --git a/packages/core/tests/helpers/fixture-node-modules.ts b/packages/core/tests/helpers/fixture-node-modules.ts index 406ecdaa8a..f73ef24728 100644 --- a/packages/core/tests/helpers/fixture-node-modules.ts +++ b/packages/core/tests/helpers/fixture-node-modules.ts @@ -13,6 +13,7 @@ import { unlinkSync, writeFileSync, } from "node:fs"; +import { tmpdir } from "node:os"; import { dirname, join, resolve, sep } from "node:path"; /** @@ -93,7 +94,7 @@ function stripEscapingSymlinks(root: string): string[] { // A hoisted deploy has no `.pnpm` store-escape symlinks, so an // escaping link here is a workspace `link:` dep pnpm didn't copy // — e.g. an agent package that now lives in the sibling - // secure-exec repo (registry/agent/*). Materialize a dereferenced + // secure-exec repo (software/*). Materialize a dereferenced // copy so it's still present in the flat tree the VM mounts; a // published install would have it as a real dir. Dangling or // non-package escapes are dropped as before. @@ -129,12 +130,11 @@ export function ensureFlatNodeModules(cwd: string): string { const repoRoot = findRepoRoot(cwd); const safe = packageName.replace(/[^a-z0-9]+/gi, "_"); - const cacheRoot = join( - repoRoot, - "node_modules", - ".cache", - "agentos-flat-fixtures", - ); + const repoSafe = repoRoot.replace(/[^a-z0-9]+/gi, "_"); + const cacheBase = + process.env.AGENTOS_FLAT_FIXTURE_CACHE_ROOT ?? + join(tmpdir(), "agentos-flat-fixtures"); + const cacheRoot = join(cacheBase, repoSafe); mkdirSync(cacheRoot, { recursive: true }); const target = join(cacheRoot, safe); const readyMarker = join(target, ".ready"); diff --git a/packages/core/tests/helpers/openai-responses-mock.ts b/packages/core/tests/helpers/openai-responses-mock.ts index e771f886b1..dcbbff550c 100644 --- a/packages/core/tests/helpers/openai-responses-mock.ts +++ b/packages/core/tests/helpers/openai-responses-mock.ts @@ -43,6 +43,52 @@ function writeJson( res.end(payload); } +function writeSse( + res: ServerResponse, + events: Record[], +): void { + const payload = events + .map((event) => { + const type = String(event.type); + return `event: ${type}\ndata: ${JSON.stringify(event)}\n\n`; + }) + .join(""); + res.statusCode = 200; + res.setHeader("content-type", "text/event-stream"); + res.setHeader("cache-control", "no-cache"); + res.setHeader("content-length", Buffer.byteLength(payload)); + res.end(payload); +} + +function responseToSseEvents(response: Record) { + const id = typeof response.id === "string" ? response.id : "resp_mock"; + const events: Record[] = [ + { type: "response.created", response: { id } }, + ]; + const output = Array.isArray(response.output) ? response.output : []; + for (const item of output) { + if (!item || typeof item !== "object") continue; + events.push({ + type: "response.output_item.done", + item, + }); + } + events.push({ + type: "response.completed", + response: { + id, + usage: { + input_tokens: 0, + input_tokens_details: null, + output_tokens: 0, + output_tokens_details: null, + total_tokens: 0, + }, + }, + }); + return events; +} + export async function startResponsesMock( fixtures: ResponsesFixture[], ): Promise { @@ -69,7 +115,11 @@ export async function startResponsesMock( if (fixture.delayMs) { await new Promise((resolve) => setTimeout(resolve, fixture.delayMs)); } - writeJson(res, 200, fixture.response); + if (body.stream === true) { + writeSse(res, responseToSseEvents(fixture.response)); + } else { + writeJson(res, 200, fixture.response); + } } catch (error) { writeJson(res, 500, { error: "invalid_request", diff --git a/packages/core/tests/helpers/registry-command-availability.ts b/packages/core/tests/helpers/registry-command-availability.ts new file mode 100644 index 0000000000..309df23375 --- /dev/null +++ b/packages/core/tests/helpers/registry-command-availability.ts @@ -0,0 +1,58 @@ +import { + closeSync, + existsSync, + openSync, + readdirSync, + readSync, + statSync, +} from "node:fs"; +import { dirname, join } from "node:path"; + +interface RegistryPackageRef { + packagePath: string; +} + +const AOSPKG_MAGIC = Buffer.from([0x89, 0x41, 0x4f, 0x53]); + +function isPackedAospkg(path: string): boolean { + try { + if (statSync(path).size <= 16) return false; + const fd = openSync(path, "r"); + try { + const head = Buffer.alloc(4); + readSync(fd, head, 0, 4, 0); + return head.equals(AOSPKG_MAGIC); + } finally { + closeSync(fd); + } + } catch { + return false; + } +} + +export function hasBuiltRegistryCommands( + packages: readonly RegistryPackageRef[], +): boolean { + return packages.every((pkg) => { + const path = pkg.packagePath; + if (path.endsWith(".aospkg")) { + if (!isPackedAospkg(path)) return false; + return hasCompleteCommandDir(join(dirname(path), "package")); + } + return ( + existsSync(join(path, "agentos-package.json")) && + hasCompleteCommandDir(path) + ); + }); +} + +function hasCompleteCommandDir(path: string): boolean { + if (!existsSync(path)) return true; + const binDir = join(path, "bin"); + if (!existsSync(binDir)) return false; + const entries = readdirSync(binDir); + return ( + entries.length > 0 && + entries.every((entry) => existsSync(join(binDir, entry))) + ); +} diff --git a/packages/core/tests/helpers/registry-commands.ts b/packages/core/tests/helpers/registry-commands.ts index b2cc4e8da6..2a0b781c5e 100644 --- a/packages/core/tests/helpers/registry-commands.ts +++ b/packages/core/tests/helpers/registry-commands.ts @@ -12,8 +12,8 @@ * when it exists (local registry builds produce both), so a stale or empty * command set still fails loudly. * - * The only sanctioned exception is the C-sysroot package set (duckdb, - * http-get, sqlite3, wget, zip, unzip): those need the patched wasi C sysroot + * The only sanctioned exception is the C-sysroot package set (curl, duckdb, + * sqlite3, zip, unzip): those need the patched wasi C sysroot * that most checkouts don't have, so `cSysrootPackageSkipReason` reports a * skip reason instead of throwing. Everything else is load-or-throw. */ @@ -54,9 +54,9 @@ export interface RegistryPackageRef { const BUILD_INSTRUCTIONS = "Build the registry:\n" + - " just registry-native # native wasm binaries, once per checkout (slow)\n" + - " just registry-build # stage bin/ + pack every dist/package.aospkg\n" + - "See registry/README.md."; + " just toolchain-build # native wasm binaries, once per checkout (slow)\n" + + " just software-build # stage bin/ + pack every dist/package.aospkg\n" + + "See software/README.md."; /** `.aospkg` container magic (crates/vfs/package-format/v1.bare). */ const AOSPKG_MAGIC = Buffer.from([0x89, 0x41, 0x4f, 0x53]); @@ -131,7 +131,7 @@ function builtState(pkg: RegistryPackageRef): { } /** - * Assert a registry package is built (a real packed `.aospkg`, with a + * Assert a software package is built (a real packed `.aospkg`, with a * non-empty, fully-present command set when the staged transition dir is * available to inspect) and return it. Throws with build instructions * otherwise. @@ -143,26 +143,26 @@ export function requireBuilt( const { built, bin, missing } = builtState(pkg); if (!built) { throw new Error( - `registry package ${name} is NOT BUILT (no valid ${pkg.packagePath}).\n${BUILD_INSTRUCTIONS}`, + `software package ${name} is NOT BUILT (no valid ${pkg.packagePath}).\n${BUILD_INSTRUCTIONS}`, ); } if (bin !== null && Object.keys(bin).length === 0) { throw new Error( - `registry package ${name} is an EMPTY placeholder (no commands staged into bin/).\n${BUILD_INSTRUCTIONS}`, + `software package ${name} is an EMPTY placeholder (no commands staged into bin/).\n${BUILD_INSTRUCTIONS}`, ); } if (missing.length > 0) { throw new Error( - `registry package ${name} is missing built commands: ${missing.join(", ")}.\n${BUILD_INSTRUCTIONS}`, + `software package ${name} is missing built commands: ${missing.join(", ")}.\n${BUILD_INSTRUCTIONS}`, ); } return pkg; } /** - * Skip reason for the C-sysroot package set ONLY (duckdb, http-get, sqlite3, - * wget, zip, unzip). These need the patched wasi C sysroot - * (`make -C registry/native/c`), which most checkouts don't build — a missing + * Skip reason for the C-sysroot package set ONLY (curl, duckdb, sqlite3, + * zip, unzip). These need the patched wasi C sysroot + * (`make -C toolchain/c`), which most checkouts don't build — a missing * artifact is an environment limitation, not a forgotten build, so suites may * skip with this reason instead of throwing. */ @@ -179,8 +179,8 @@ export function cSysrootPackageSkipReason( }); if (unbuilt.length === 0) return false; return ( - `C-sysroot registry packages not built: ${unbuilt.map(({ name }) => name).join(", ")} ` + - "(needs the patched wasi C sysroot: `make -C registry/native/c`, then `just registry-build`)" + `C-sysroot software packages not built: ${unbuilt.map(({ name }) => name).join(", ")} ` + + "(needs the patched wasi C sysroot: `make -C toolchain/c`, then `just software-build`)" ); } @@ -205,7 +205,7 @@ export function packageCommandsDir(pkg: RegistryPackageRef): string { const dir = manifestDir(pkg); if (!dir) { throw new Error( - `registry package has no staged transition dir next to ${pkg.packagePath}.\n${BUILD_INSTRUCTIONS}`, + `software package has no staged transition dir next to ${pkg.packagePath}.\n${BUILD_INSTRUCTIONS}`, ); } return join(dir, "bin"); @@ -257,12 +257,12 @@ export const REGISTRY_SOFTWARE = ( export function testOnlyCommandSoftware( commands: string[] = ["xu"], ): RegistryPackageRef { - // registry/software//dist/package.aospkg -> registry/native/... — this + // software//dist/package.aospkg -> toolchain/... — this // follows whichever registry checkout the deps are linked to. const nativeCommandsDir = join( dirname(coreutils.packagePath), - "../../..", - "native/target/wasm32-wasip1/release/commands", + "../../../..", + "toolchain/target/wasm32-wasip1/release/commands", ); const dir = join(tmpdir(), `agentos-test-cmds-${process.pid}`); const binDir = join(dir, "bin"); diff --git a/packages/core/tests/opencode-real-session.test.ts b/packages/core/tests/opencode-real-session.test.ts new file mode 100644 index 0000000000..c47f4b2cd6 --- /dev/null +++ b/packages/core/tests/opencode-real-session.test.ts @@ -0,0 +1,76 @@ +import { resolve } from "node:path"; +import opencode from "@agentos-software/opencode"; +import { describe, expect, test } from "vitest"; +import type { AgentCapabilities, AgentInfo } from "../src/agent-os.js"; +import { AgentOs } from "../src/agent-os.js"; +import { + DEFAULT_TEXT_FIXTURE, + startLlmock, + stopLlmock, +} from "./helpers/llmock-helper.js"; +import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; +import { + createVmOpenCodeHome, + createVmWorkspace, +} from "./helpers/opencode-helper.js"; + +const MODULE_ACCESS_CWD = resolve(import.meta.dirname, ".."); + +async function createOpenCodeVm(mockUrl: string): Promise { + return AgentOs.create({ + loopbackExemptPorts: [Number(new URL(mockUrl).port)], + mounts: moduleAccessMounts(MODULE_ACCESS_CWD), + software: [opencode], + }); +} + +describe("real createSession('opencode')", () => { + test("initializes the projected OpenCode ACP package inside the VM", async () => { + const { mock, url } = await startLlmock([DEFAULT_TEXT_FIXTURE]); + const vm = await createOpenCodeVm(url); + + let sessionId: string | undefined; + try { + const homeDir = await createVmOpenCodeHome(vm, url); + const workspaceDir = await createVmWorkspace(vm); + sessionId = ( + await vm.createSession("opencode", { + cwd: workspaceDir, + env: { + HOME: homeDir, + ANTHROPIC_API_KEY: "mock-key", + }, + }) + ).sessionId; + + const agentInfo = vm.getSessionAgentInfo(sessionId) as AgentInfo; + expect(agentInfo.name).toBe("OpenCode"); + expect(agentInfo.version).toBeTruthy(); + + const capabilities = vm.getSessionCapabilities( + sessionId, + ) as AgentCapabilities; + expect(capabilities.promptCapabilities).toMatchObject({ + embeddedContext: true, + image: true, + }); + + const modes = vm.getSessionModes(sessionId); + expect(modes?.currentModeId).toBe("build"); + expect(modes?.availableModes.map((mode) => mode.id)).toEqual( + expect.arrayContaining(["build", "plan"]), + ); + + expect(vm.listSessions()).toContainEqual({ + sessionId, + agentType: "opencode", + }); + } finally { + if (sessionId) { + vm.closeSession(sessionId); + } + await vm.dispose(); + await stopLlmock(mock); + } + }, 120_000); +}); diff --git a/packages/core/tests/opencode-session.test.ts b/packages/core/tests/opencode-session.test.ts index 89084aedd8..62424a3f0d 100644 --- a/packages/core/tests/opencode-session.test.ts +++ b/packages/core/tests/opencode-session.test.ts @@ -27,11 +27,11 @@ const MODULE_ACCESS_CWD = resolve(import.meta.dirname, ".."); const REGISTRY_COMMAND_DIR_CANDIDATES = [ resolve( import.meta.dirname, - "../../../registry/native/target/wasm32-wasip1/release/commands", + "../../../toolchain/target/wasm32-wasip1/release/commands", ), resolve( import.meta.dirname, - "../../../../secure-exec/registry/native/target/wasm32-wasip1/release/commands", + "../../../../secure-exec/toolchain/target/wasm32-wasip1/release/commands", ), ]; diff --git a/packages/core/tests/pi-cli-headless.test.ts b/packages/core/tests/pi-cli-headless.test.ts index 5f639a3498..f48c45c19c 100644 --- a/packages/core/tests/pi-cli-headless.test.ts +++ b/packages/core/tests/pi-cli-headless.test.ts @@ -1,8 +1,7 @@ import { resolve } from "node:path"; -import type { Fixture, ToolCall } from "@copilotkit/llmock"; -import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; import common from "@agentos-software/common"; import piCli from "@agentos-software/pi-cli"; +import type { Fixture, ToolCall } from "@copilotkit/llmock"; import { describe, expect, test } from "vitest"; import { AgentOs } from "../src/agent-os.js"; import { @@ -10,8 +9,12 @@ import { startLlmock, stopLlmock, } from "./helpers/llmock-helper.js"; +import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; +import { hasBuiltRegistryCommands } from "./helpers/registry-command-availability.js"; const MODULE_ACCESS_CWD = resolve(import.meta.dirname, ".."); +const registryCommandsAvailable = hasBuiltRegistryCommands(common); +const registryCommandTest = registryCommandsAvailable ? test : test.skip; function getRequestBody(req: unknown): Record { const direct = req as Record; @@ -49,7 +52,7 @@ async function createPiCliVm(mockUrl: string): Promise { return AgentOs.create({ loopbackExemptPorts: [Number(new URL(mockUrl).port)], mounts: moduleAccessMounts(MODULE_ACCESS_CWD), - software: [common, piCli], + software: [...(registryCommandsAvailable ? common : []), piCli], }); } @@ -152,61 +155,58 @@ describe("full createSession('pi-cli') inside the VM", () => { } }, 120_000); - // Blocked on shell `>` redirect output being visible to `vm.readFile()`. - // This is the unmodified upstream Pi CLI bash path (`createLocalBashOperations` - // spawning the shell directly), with no Agent OS operations override, so the - // failure is a runtime gap independent of the SDK adapter: the redirect runs - // inside the guest shell but the written bytes do not reconcile to the host - // read path yet. Tracked in ~/.agents/todo/agentos-runtime-fixes.md - // (shell-exec redirect visibility). - test.skip("runs the unmodified Pi CLI ACP flow end-to-end for bash tool calls", async () => { - const workspacePath = "/home/agentos/workspace/bash-output.txt"; - const fixtures = createToolFixtures( - { - name: "bash", - arguments: JSON.stringify({ - command: `printf 'bash-ok' > ${workspacePath}`, - timeout: 10, - }), - }, - "bash-ok", - "bash-output.txt was written successfully.", - ); - const { mock, url } = await startLlmock(fixtures); - const vm = await createPiCliVm(url); + registryCommandTest( + "runs the unmodified Pi CLI ACP flow end-to-end for bash tool calls", + async () => { + const workspacePath = "/home/agentos/workspace/bash-output.txt"; + const fixtures = createToolFixtures( + { + name: "bash", + arguments: JSON.stringify({ + command: `printf 'bash-ok' > ${workspacePath}`, + timeout: 10, + }), + }, + "bash-ok", + "bash-output.txt was written successfully.", + ); + const { mock, url } = await startLlmock(fixtures); + const vm = await createPiCliVm(url); - let sessionId: string | undefined; - try { - const homeDir = await createVmPiHome(vm, url); - const workspaceDir = await createVmWorkspace(vm); - sessionId = ( - await vm.createSession("pi-cli", { - cwd: workspaceDir, - env: { - HOME: homeDir, - ANTHROPIC_API_KEY: "mock-key", - ANTHROPIC_BASE_URL: url, - }, - }) - ).sessionId; + let sessionId: string | undefined; + try { + const homeDir = await createVmPiHome(vm, url); + const workspaceDir = await createVmWorkspace(vm); + sessionId = ( + await vm.createSession("pi-cli", { + cwd: workspaceDir, + env: { + HOME: homeDir, + ANTHROPIC_API_KEY: "mock-key", + ANTHROPIC_BASE_URL: url, + }, + }) + ).sessionId; - const { response, text } = await vm.prompt( - sessionId, - `Use bash to write bash-ok into ${workspacePath}.`, - ); + const { response, text } = await vm.prompt( + sessionId, + `Use bash to write bash-ok into ${workspacePath}.`, + ); - expect(response.error).toBeUndefined(); - expect(text).toContain("bash-output.txt was written successfully."); - expect(new TextDecoder().decode(await vm.readFile(workspacePath))).toBe( - "bash-ok", - ); - expect(mock.getRequests().length).toBeGreaterThanOrEqual(2); - } finally { - if (sessionId) { - vm.closeSession(sessionId); + expect(response.error).toBeUndefined(); + expect(text).toContain("bash-output.txt was written successfully."); + expect(new TextDecoder().decode(await vm.readFile(workspacePath))).toBe( + "bash-ok", + ); + expect(mock.getRequests().length).toBeGreaterThanOrEqual(2); + } finally { + if (sessionId) { + vm.closeSession(sessionId); + } + await vm.dispose(); + await stopLlmock(mock); } - await vm.dispose(); - await stopLlmock(mock); - } - }, 120_000); + }, + 120_000, + ); }); diff --git a/packages/core/tests/pi-headless.test.ts b/packages/core/tests/pi-headless.test.ts index b1cd922223..294dffe7e1 100644 --- a/packages/core/tests/pi-headless.test.ts +++ b/packages/core/tests/pi-headless.test.ts @@ -1,8 +1,7 @@ import { resolve } from "node:path"; -import type { Fixture, ToolCall } from "@copilotkit/llmock"; -import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; import common from "@agentos-software/common"; import pi from "@agentos-software/pi"; +import type { Fixture, ToolCall } from "@copilotkit/llmock"; import { describe, expect, test } from "vitest"; import type { AgentCapabilities, AgentInfo } from "../src/agent-os.js"; import { AgentOs } from "../src/agent-os.js"; @@ -11,8 +10,12 @@ import { startLlmock, stopLlmock, } from "./helpers/llmock-helper.js"; +import { moduleAccessMounts } from "./helpers/node-modules-mount.js"; +import { hasBuiltRegistryCommands } from "./helpers/registry-command-availability.js"; const MODULE_ACCESS_CWD = resolve(import.meta.dirname, ".."); +const registryCommandsAvailable = hasBuiltRegistryCommands(common); +const registryCommandTest = registryCommandsAvailable ? test : test.skip; function getRequestBody(req: unknown): Record { const direct = req as Record; @@ -51,7 +54,7 @@ async function createPiVm(mockUrl: string): Promise { loopbackExemptPorts: [Number(new URL(mockUrl).port)], mounts: moduleAccessMounts(MODULE_ACCESS_CWD), // Default software ships no agents; pass the pi agent package explicitly. - software: [common, pi], + software: [...(registryCommandsAvailable ? common : []), pi], }); } @@ -206,63 +209,59 @@ describe("full createSession('pi') inside the VM", () => { } }, 120_000); - // Blocked on shell `>` redirect output being visible to `vm.readFile()`. - // The vanilla Pi SDK bash backend spawns the shell directly and the redirect - // runs inside the guest shell, but the written bytes do not reconcile to the - // host read path yet. Before the adapter dropped its custom bash operations - // override this case passed because the override routed the command through - // the rpc-client `sh -c` path that the host can observe; the vanilla backend - // surfaces the underlying runtime gap. Tracked in - // ~/.agents/todo/agentos-runtime-fixes.md (shell-exec redirect visibility). - test.skip("runs the real Pi SDK ACP flow end-to-end for bash tool calls", async () => { - const fixtures = createToolFixtures( - { - name: "bash", - arguments: JSON.stringify({ - command: "printf 'bash-ok' > bash-output.txt", - timeout: 10, - }), - }, - "bash-ok", - "bash-output.txt was written successfully.", - ); - const { mock, url } = await startLlmock(fixtures); - const vm = await createPiVm(url); + registryCommandTest( + "runs the real Pi SDK ACP flow end-to-end for bash tool calls", + async () => { + const fixtures = createToolFixtures( + { + name: "bash", + arguments: JSON.stringify({ + command: "printf 'bash-ok' > bash-output.txt", + timeout: 10, + }), + }, + "bash-ok", + "bash-output.txt was written successfully.", + ); + const { mock, url } = await startLlmock(fixtures); + const vm = await createPiVm(url); - let sessionId: string | undefined; - try { - const homeDir = await createVmPiHome(vm, url); - const workspaceDir = await createVmWorkspace(vm); - sessionId = ( - await vm.createSession("pi", { - cwd: workspaceDir, - env: { - HOME: homeDir, - ANTHROPIC_API_KEY: "mock-key", - ANTHROPIC_BASE_URL: url, - }, - }) - ).sessionId; + let sessionId: string | undefined; + try { + const homeDir = await createVmPiHome(vm, url); + const workspaceDir = await createVmWorkspace(vm); + sessionId = ( + await vm.createSession("pi", { + cwd: workspaceDir, + env: { + HOME: homeDir, + ANTHROPIC_API_KEY: "mock-key", + ANTHROPIC_BASE_URL: url, + }, + }) + ).sessionId; - const { response, text } = await vm.prompt( - sessionId, - "Use bash to write bash-ok into bash-output.txt.", - ); + const { response, text } = await vm.prompt( + sessionId, + "Use bash to write bash-ok into bash-output.txt.", + ); - expect(response.error).toBeUndefined(); - expect(text).toContain("bash-output.txt was written successfully."); - expect( - new TextDecoder().decode( - await vm.readFile(`${workspaceDir}/bash-output.txt`), - ), - ).toBe("bash-ok"); - expect(mock.getRequests().length).toBeGreaterThanOrEqual(2); - } finally { - if (sessionId) { - vm.closeSession(sessionId); + expect(response.error).toBeUndefined(); + expect(text).toContain("bash-output.txt was written successfully."); + expect( + new TextDecoder().decode( + await vm.readFile(`${workspaceDir}/bash-output.txt`), + ), + ).toBe("bash-ok"); + expect(mock.getRequests().length).toBeGreaterThanOrEqual(2); + } finally { + if (sessionId) { + vm.closeSession(sessionId); + } + await vm.dispose(); + await stopLlmock(mock); } - await vm.dispose(); - await stopLlmock(mock); - } - }, 120_000); + }, + 120_000, + ); }); diff --git a/packages/core/tests/pty-line-discipline.test.ts b/packages/core/tests/pty-line-discipline.test.ts index 037703e382..da18a89cd8 100644 --- a/packages/core/tests/pty-line-discipline.test.ts +++ b/packages/core/tests/pty-line-discipline.test.ts @@ -62,7 +62,7 @@ const NODE_PROBE_GUEST_PATH = "/pty_probe.mjs"; const WASI_SDK = resolve( REPO_ROOT, - "registry/native/c/vendor/wasi-sdk", + "toolchain/c/vendor/wasi-sdk", ); const SIDECAR_BINARY = resolve(REPO_ROOT, "target/debug/agentos-sidecar"); diff --git a/packages/core/tests/pty-protocol.test.ts b/packages/core/tests/pty-protocol.test.ts index 7a62712fa9..43bff98f96 100644 --- a/packages/core/tests/pty-protocol.test.ts +++ b/packages/core/tests/pty-protocol.test.ts @@ -17,7 +17,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(__dirname, "../../.."); const SECURE_EXEC_C_ROOT = resolve( REPO_ROOT, - "registry/native/c", + "toolchain/c", ); const SIDECAR_BINARY = resolve(REPO_ROOT, "target/debug/agentos-sidecar"); const PTY_PROBE_COMMAND_DIR = resolve(SECURE_EXEC_C_ROOT, "build"); diff --git a/packages/core/tests/public-api-exports.test.ts b/packages/core/tests/public-api-exports.test.ts index 0430ae1647..9488dc2534 100644 --- a/packages/core/tests/public-api-exports.test.ts +++ b/packages/core/tests/public-api-exports.test.ts @@ -61,9 +61,35 @@ describe("root public API exports", () => { expect(validateToolkits).toBeTypeOf("function"); expect(MAX_TOOL_DESCRIPTION_LENGTH).toBeGreaterThan(0); expect(agentOsLimitsSchema.safeParse({}).success).toBe(true); - expect(agentOsOptionsSchema.safeParse({ defaultSoftware: false }).success).toBe( - true, - ); + expect( + agentOsLimitsSchema.safeParse({ + process: { + maxSpawnFileActions: 4096, + maxSpawnFileActionBytes: 1024 * 1024, + pendingStdinBytes: 1024, + pendingEventCount: 16, + pendingEventBytes: 4096, + }, + }).success, + ).toBe(true); + expect( + agentOsLimitsSchema.safeParse({ + process: { pendingEventCount: 0 }, + }).success, + ).toBe(false); + expect( + agentOsLimitsSchema.safeParse({ + process: { maxSpawnFileActions: 0 }, + }).success, + ).toBe(false); + expect( + agentOsLimitsSchema.safeParse({ + process: { maxSpawnFileActionBytes: 0 }, + }).success, + ).toBe(false); + expect( + agentOsOptionsSchema.safeParse({ defaultSoftware: false }).success, + ).toBe(true); expect(hostToolSchema).toBeTypeOf("object"); expect(toolKitSchema).toBeTypeOf("object"); expect(mountConfigSchema).toBeTypeOf("object"); diff --git a/packages/core/tests/vim-interactive.test.ts b/packages/core/tests/vim-interactive.test.ts index edb48d8b1e..f6b6a91b8f 100644 --- a/packages/core/tests/vim-interactive.test.ts +++ b/packages/core/tests/vim-interactive.test.ts @@ -26,7 +26,7 @@ const REPO_ROOT = resolve(__dirname, "../../.."); // one-off fixture builds. const VIM_PACKAGE_DIR = resolve( REPO_ROOT, - "../secure-exec/registry/software/vim/dist/package", + "../secure-exec/software/vim/dist/package", ); const VIM_COMMAND_DIR = process.env.AGENTOS_VIM_FIXTURE_DIR ?? resolve(VIM_PACKAGE_DIR, "bin"); diff --git a/packages/core/tests/vim-native-parity.test.ts b/packages/core/tests/vim-native-parity.test.ts index 4fcdbe1b6a..a3b91147e9 100644 --- a/packages/core/tests/vim-native-parity.test.ts +++ b/packages/core/tests/vim-native-parity.test.ts @@ -10,7 +10,7 @@ const HERE = dirname(fileURLToPath(import.meta.url)); const REPO_ROOT = resolve(HERE, "../../.."); const VIM_PACKAGE_BIN = resolve( REPO_ROOT, - "../secure-exec/registry/software/vim/dist/package/bin/vim", + "../secure-exec/software/vim/dist/package/bin/vim", ); const NATIVE_VIM = "/usr/bin/vim"; const REF_SCRIPT = join(HERE, "helpers", "native-vim-ref.py"); diff --git a/packages/core/tests/vim-provides.test.ts b/packages/core/tests/vim-provides.test.ts index 1aa5879abb..835daa5a6e 100644 --- a/packages/core/tests/vim-provides.test.ts +++ b/packages/core/tests/vim-provides.test.ts @@ -27,7 +27,7 @@ const REPO_ROOT = resolve(__dirname, "../../.."); // one-off fixture builds. const VIM_PACKAGE_DIR = resolve( REPO_ROOT, - "../secure-exec/registry/software/vim/dist/package", + "../secure-exec/software/vim/dist/package", ); const VIM_COMMAND_DIR = process.env.AGENTOS_VIM_FIXTURE_DIR ?? resolve(VIM_PACKAGE_DIR, "bin"); diff --git a/packages/core/tests/vim-render.test.ts b/packages/core/tests/vim-render.test.ts index c1dcd65885..10a89edd70 100644 --- a/packages/core/tests/vim-render.test.ts +++ b/packages/core/tests/vim-render.test.ts @@ -20,7 +20,7 @@ const { Terminal } = xterm; const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../.."); const VIM_PACKAGE_BIN = resolve( REPO_ROOT, - "../secure-exec/registry/software/vim/dist/package/bin/vim", + "../secure-exec/software/vim/dist/package/bin/vim", ); const VIM_BINARY = process.env.AGENTOS_VIM_FIXTURE_BIN ?? VIM_PACKAGE_BIN; diff --git a/packages/core/vitest.config.ts b/packages/core/vitest.config.ts index 6e13417ef3..c5c20f45fc 100644 --- a/packages/core/vitest.config.ts +++ b/packages/core/vitest.config.ts @@ -10,18 +10,15 @@ import { configDefaults, defineConfig } from "vitest/config"; const SLOW_E2E_FILES = [ "tests/wasm-commands.test.ts", // ~24m "tests/session-cleanup.test.ts", // ~12m - "tests/claude-session.test.ts", // ~7.5m "tests/execute.test.ts", "tests/filesystem.test.ts", "tests/native-sidecar-process.test.ts", "tests/pi-vanilla-bash.test.ts", "tests/opencode-session.test.ts", - "tests/git-quickstart.test.ts", "tests/filesystem-move-delete.test.ts", "tests/batch-file-ops.test.ts", "tests/agentos-base-filesystem.test.ts", "tests/pi-sdk-boot-probe.test.ts", - "tests/pi-headless.test.ts", "tests/pi-tool-llmock.test.ts", "tests/native-sidecar-process-permissions.test.ts", "tests/pi-extensions.test.ts", @@ -29,7 +26,6 @@ const SLOW_E2E_FILES = [ "tests/child-process-detached.test.ts", "tests/readdir-recursive.test.ts", "tests/cron-integration.test.ts", - "tests/pi-cli-headless.test.ts", ]; // Pre-existing failures NOT caused by this branch (they were red before CI ever @@ -50,15 +46,8 @@ const KNOWN_FAILING_E2E_FILES = [ "tests/pi-acp-adapter.test.ts", "tests/process-lifecycle.test.ts", // Registry-artifact / shell-behavior failures (red in both CI and local): - // - duckdb-package: imports secure-exec registry/software/duckdb/dist (unbuilt WASM in CI). // - shell-flat-api: openShell/writeShell/onShellData yields empty output. - "tests/duckdb-package.test.ts", "tests/shell-flat-api.test.ts", - // codex-fullturn: the pinned @agentos-software/codex package intentionally - // stubs the turn ("codex-exec --session-turn is disabled until the real Codex - // agent package is wired"). Pre-existing unwired-feature state, not a - // regression — re-enable once the real Codex agent package is wired. - "tests/codex-fullturn.test.ts", ]; // Real-API, real-install matrix (agent × package manager). Hits a live LLM API diff --git a/packages/playground/agentos-worker.js b/packages/playground/agentos-worker.js index 9b0222587c..82d143a990 100644 --- a/packages/playground/agentos-worker.js +++ b/packages/playground/agentos-worker.js @@ -1,12 +1,12 @@ // Generated by packages/playground/scripts/build-worker.ts -var Qf=Object.create;var Qa=Object.defineProperty;var Zf=Object.getOwnPropertyDescriptor;var e1=Object.getOwnPropertyNames;var t1=Object.getPrototypeOf,n1=Object.prototype.hasOwnProperty;var Bn=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var r1=(e,t,r,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of e1(t))!n1.call(e,o)&&o!==r&&Qa(e,o,{get:()=>t[o],enumerable:!(s=Zf(t,o))||s.enumerable});return e};var Tr=(e,t,r)=>(r=e!=null?Qf(t1(e)):{},r1(t||!e||!e.__esModule?Qa(r,"default",{value:e,enumerable:!0}):r,e));var Uc=Bn((Si,Ei)=>{(function(e,t){typeof Si=="object"&&typeof Ei<"u"?Ei.exports=t():typeof define=="function"&&define.amd?define(t):(e=typeof globalThis<"u"?globalThis:e||self,e.resolveURI=t())})(Si,(function(){"use strict";let e=/^[\w+.-]+:\/\//,t=/^([\w+.-]+:)\/\/([^@/#?]*@)?([^:/#?]*)(:\d+)?(\/[^#?]*)?(\?[^#]*)?(#.*)?/,r=/^file:(?:\/\/((?![a-z]:)[^/#?]*)?)?(\/?[^#?]*)(\?[^#]*)?(#.*)?/i;function s(S){return e.test(S)}function o(S){return S.startsWith("//")}function a(S){return S.startsWith("/")}function c(S){return S.startsWith("file:")}function l(S){return/^[.?#]/.test(S)}function u(S){let P=t.exec(S);return m(P[1],P[2]||"",P[3],P[4]||"",P[5]||"/",P[6]||"",P[7]||"")}function p(S){let P=r.exec(S),B=P[2];return m("file:","",P[1]||"","",a(B)?B:"/"+B,P[3]||"",P[4]||"")}function m(S,P,B,C,j,H,k){return{scheme:S,user:P,host:B,port:C,path:j,query:H,hash:k,type:7}}function h(S){if(o(S)){let B=u("http:"+S);return B.scheme="",B.type=6,B}if(a(S)){let B=u("http://foo.com"+S);return B.scheme="",B.host="",B.type=5,B}if(c(S))return p(S);if(s(S))return u(S);let P=u("http://foo.com/"+S);return P.scheme="",P.host="",P.type=S?S.startsWith("?")?3:S.startsWith("#")?2:4:1,P}function b(S){if(S.endsWith("/.."))return S;let P=S.lastIndexOf("/");return S.slice(0,P+1)}function w(S,P){x(P,P.type),S.path==="/"?S.path=P.path:S.path=b(P.path)+S.path}function x(S,P){let B=P<=4,C=S.path.split("/"),j=1,H=0,k=!1;for(let g=1;gC&&(C=k)}x(B,C);let j=B.query+B.hash;switch(C){case 2:case 3:return j;case 4:{let H=B.path.slice(1);return H?l(P||S)&&!l(H)?"./"+H+j:H+j:j||"."}case 5:return B.path+j;default:return B.scheme+"//"+B.user+B.host+B.port+B.path+j}}return I}))});var ts=Bn(mt=>{"use strict";var Bp=mt&&mt.__extends||(function(){var e=function(t,r){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(s,o){s.__proto__=o}||function(s,o){for(var a in o)o.hasOwnProperty(a)&&(s[a]=o[a])},e(t,r)};return function(t,r){e(t,r);function s(){this.constructor=t}t.prototype=r===null?Object.create(r):(s.prototype=r.prototype,new s)}})();Object.defineProperty(mt,"__esModule",{value:!0});mt.DetailContext=mt.NoopContext=mt.VError=void 0;var Kc=(function(e){Bp(t,e);function t(r,s){var o=e.call(this,s)||this;return o.path=r,Object.setPrototypeOf(o,t.prototype),o}return t})(Error);mt.VError=Kc;var Rp=(function(){function e(){}return e.prototype.fail=function(t,r,s){return!1},e.prototype.unionResolver=function(){return this},e.prototype.createContext=function(){return this},e.prototype.resolveUnion=function(t){},e})();mt.NoopContext=Rp;var Vc=(function(){function e(){this._propNames=[""],this._messages=[null],this._score=0}return e.prototype.fail=function(t,r,s){return this._propNames.push(t),this._messages.push(r),this._score+=s,!1},e.prototype.unionResolver=function(){return new Lp},e.prototype.resolveUnion=function(t){for(var r,s,o=t,a=null,c=0,l=o.contexts;c=a._score)&&(a=u)}a&&a._score>0&&((r=this._propNames).push.apply(r,a._propNames),(s=this._messages).push.apply(s,a._messages))},e.prototype.getError=function(t){for(var r=[],s=this._propNames.length-1;s>=0;s--){var o=this._propNames[s];t+=typeof o=="number"?"["+o+"]":o?"."+o:"";var a=this._messages[s];a&&r.push(t+" "+a)}return new Kc(t,r.join("; "))},e.prototype.getErrorDetail=function(t){for(var r=[],s=this._propNames.length-1;s>=0;s--){var o=this._propNames[s];t+=typeof o=="number"?"["+o+"]":o?"."+o:"";var a=this._messages[s];a&&r.push({path:t,message:a})}for(var c=null,s=r.length-1;s>=0;s--)c&&(r[s].nested=[c]),c=r[s];return c},e})();mt.DetailContext=Vc;var Lp=(function(){function e(){this.contexts=[]}return e.prototype.createContext=function(){var t=new Vc;return this.contexts.push(t),t},e})()});var Di=Bn(M=>{"use strict";var Ve=M&&M.__extends||(function(){var e=function(t,r){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(s,o){s.__proto__=o}||function(s,o){for(var a in o)o.hasOwnProperty(a)&&(s[a]=o[a])},e(t,r)};return function(t,r){e(t,r);function s(){this.constructor=t}t.prototype=r===null?Object.create(r):(s.prototype=r.prototype,new s)}})();Object.defineProperty(M,"__esModule",{value:!0});M.basicTypes=M.BasicType=M.TParamList=M.TParam=M.param=M.TFunc=M.func=M.TProp=M.TOptional=M.opt=M.TIface=M.iface=M.TEnumLiteral=M.enumlit=M.TEnumType=M.enumtype=M.TIntersection=M.intersection=M.TUnion=M.union=M.TTuple=M.tuple=M.TArray=M.array=M.TLiteral=M.lit=M.TName=M.name=M.TType=void 0;var Xc=ts(),We=(function(){function e(){}return e})();M.TType=We;function Mt(e){return typeof e=="string"?Qc(e):e}function Bi(e,t){var r=e[t];if(!r)throw new Error("Unknown type "+t);return r}function Qc(e){return new Ri(e)}M.name=Qc;var Ri=(function(e){Ve(t,e);function t(r){var s=e.call(this)||this;return s.name=r,s._failMsg="is not a "+r,s}return t.prototype.getChecker=function(r,s,o){var a=this,c=Bi(r,this.name),l=c.getChecker(r,s,o);return c instanceof Ne||c instanceof t?l:function(u,p){return l(u,p)?!0:p.fail(null,a._failMsg,0)}},t})(We);M.TName=Ri;function Cp(e){return new Li(e)}M.lit=Cp;var Li=(function(e){Ve(t,e);function t(r){var s=e.call(this)||this;return s.value=r,s.name=JSON.stringify(r),s._failMsg="is not "+s.name,s}return t.prototype.getChecker=function(r,s){var o=this;return function(a,c){return a===o.value?!0:c.fail(null,o._failMsg,-1)}},t})(We);M.TLiteral=Li;function Np(e){return new Zc(Mt(e))}M.array=Np;var Zc=(function(e){Ve(t,e);function t(r){var s=e.call(this)||this;return s.ttype=r,s}return t.prototype.getChecker=function(r,s){var o=this.ttype.getChecker(r,s);return function(a,c){if(!Array.isArray(a))return c.fail(null,"is not an array",0);for(var l=0;l0&&o.push(a+" more"),s._failMsg="is none of "+o.join(", ")):s._failMsg="is none of "+a+" types",s}return t.prototype.getChecker=function(r,s){var o=this,a=this.ttypes.map(function(c){return c.getChecker(r,s)});return function(c,l){for(var u=l.unionResolver(),p=0;p{"use strict";var Jp=ne&&ne.__spreadArrays||function(){for(var e=0,t=0,r=arguments.length;t{"use strict";xr.__esModule=!0;xr.LinesAndColumns=void 0;var As=` -`,Hu="\r",Gu=(function(){function e(t){this.string=t;for(var r=[0],s=0;sthis.string.length)return null;for(var r=0,s=this.offsets;s[r+1]<=t;)r++;var o=t-s[r];return{line:r,column:o}},e.prototype.indexForLocation=function(t){var r=t.line,s=t.column;return r<0||r>=this.offsets.length||s<0||s>this.lengthOfLine(r)?null:this.offsets[r]+s},e.prototype.lengthOfLine=function(t){var r=this.offsets[t],s=t===this.offsets.length-1?this.string.length:this.offsets[t+1];return s-r},e})();xr.LinesAndColumns=Gu;xr.default=Gu});function s1(e){return e instanceof Uint8Array||ArrayBuffer.isView(e)&&e.constructor.name==="Uint8Array"&&"BYTES_PER_ELEMENT"in e&&e.BYTES_PER_ELEMENT===1}function $e(e,t=""){if(typeof e!="number"){let r=t&&`"${t}" `;throw new TypeError(`${r}expected number, got ${typeof e}`)}if(!Number.isSafeInteger(e)||e<0){let r=t&&`"${t}" `;throw new RangeError(`${r}expected integer >= 0, got ${e}`)}}function rn(e,t,r=""){let s=s1(e),o=e?.length,a=t!==void 0;if(!s||a&&o!==t){let c=r&&`"${r}" `,l=a?` of length ${t}`:"",u=s?`length=${o}`:`type=${typeof e}`,p=c+"expected Uint8Array"+l+", got "+u;throw s?new RangeError(p):new TypeError(p)}return e}function Ar(e){if(typeof e!="function"||typeof e.create!="function")throw new TypeError("Hash must wrapped by utils.createHasher");if($e(e.outputLen),$e(e.blockLen),e.outputLen<1)throw new Error('"outputLen" must be >= 1');if(e.blockLen<1)throw new Error('"blockLen" must be >= 1')}function sn(e,t=!0){if(e.destroyed)throw new Error("Hash instance has been destroyed");if(t&&e.finished)throw new Error("Hash#digest() has already been called")}function Pr(e,t){rn(e,void 0,"digestInto() output");let r=t.outputLen;if(e.length='+r)}function Ir(e){return new Uint32Array(e.buffer,e.byteOffset,Math.floor(e.byteLength/4))}function Ae(...e){for(let t=0;t>>t}function ee(e,t){return e<>>32-t>>>0}var o1=new Uint8Array(new Uint32Array([287454020]).buffer)[0]===68;function i1(e){return e<<24&4278190080|e<<8&16711680|e>>>8&65280|e>>>24&255}function a1(e){for(let t=0;te:a1;function c1(e){if(typeof e!="string")throw new TypeError("string expected");return new Uint8Array(new TextEncoder().encode(e))}function No(e,t=""){return typeof e=="string"?c1(e):rn(e,void 0,t)}function Or(e,t){if(t!==void 0&&{}.toString.call(t)!=="[object Object]")throw new TypeError("options must be object or undefined");return Object.assign(e,t)}function Bt(e,t={}){let r=(o,a)=>e(a).update(o).digest(),s=e(void 0);return r.outputLen=s.outputLen,r.blockLen=s.blockLen,r.canXOF=s.canXOF,r.create=o=>e(o),Object.assign(r,t),Object.freeze(r)}var Rn=e=>({oid:Uint8Array.from([6,9,96,134,72,1,101,3,4,2,e])});var Br=class{oHash;iHash;blockLen;outputLen;canXOF=!1;finished=!1;destroyed=!1;constructor(t,r){if(Ar(t),rn(r,void 0,"key"),this.iHash=t.create(),typeof this.iHash.update!="function")throw new Error("Expected instance of class which extends utils.Hash");this.blockLen=this.iHash.blockLen,this.outputLen=this.iHash.outputLen;let s=this.blockLen,o=new Uint8Array(s);o.set(r.length>s?t.create().update(r).digest():r);for(let a=0;a{let e=((t,r,s)=>new Br(t,r).update(s).digest());return e.create=(t,r)=>new Br(t,r),e})();function l1(e){return e instanceof Uint8Array||ArrayBuffer.isView(e)&&e.constructor.name==="Uint8Array"&&"BYTES_PER_ELEMENT"in e&&e.BYTES_PER_ELEMENT===1}function u1(e){if(typeof e!="boolean")throw new TypeError(`boolean expected, not ${e}`)}function Do(e){if(typeof e!="number")throw new TypeError("number expected, got "+typeof e);if(!Number.isSafeInteger(e)||e<0)throw new RangeError("positive integer expected, got "+e)}function we(e,t,r=""){let s=l1(e),o=e?.length,a=t!==void 0;if(!s||a&&o!==t){let c=r&&`"${r}" `,l=a?` of length ${t}`:"",u=s?`length=${o}`:`type=${typeof e}`,p=c+"expected Uint8Array"+l+", got "+u;throw s?new RangeError(p):new TypeError(p)}return e}function Fo(e,t=!0){if(e.destroyed)throw new Error("Hash instance has been destroyed");if(t&&e.finished)throw new Error("Hash#digest() has already been called")}function jo(e,t,r=!1){we(e,void 0,"output");let s=t.outputLen;if(e.lengthe<<24&4278190080|e<<8&16711680|e>>>8&65280|e>>>24&255,se=Rt?e=>e:e=>Uo(e)>>>0,f1=e=>{for(let t=0;te:f1;function Za(e,t){return!e.byteLength||!t.byteLength?!1:e.buffer===t.buffer&&e.byteOffset[]),a=(l,u)=>s(u,...o(l)).update(l).digest(),c=s(new Uint8Array(e),...o(new Uint8Array(0)));return a.outputLen=c.outputLen,a.blockLen=c.blockLen,a.create=(l,...u)=>s(l,...u),a}var Cr=(e,t)=>{function r(s,...o){if(we(s,void 0,"key"),e.nonceLength!==void 0){let m=o[0];we(m,e.varSizeNonce?void 0:e.nonceLength,"nonce")}let a=e.tagLength;a&&o[1]!==void 0&&we(o[1],void 0,"AAD");let c=t(s,...o),l=(m,h)=>{if(h!==void 0){if(m!==2)throw new Error("cipher output not supported");we(h,void 0,"output")}},u=!1;return{encrypt(m,h){if(u)throw new Error("cannot encrypt() twice with same key + nonce");return u=!0,we(m),l(c.encrypt.length,h),c.encrypt(m,h)},decrypt(m,h){if(we(m),a&&m.length{let o=s&1;return{s3:r<<31|s>>>1,s2:t<<31|r>>>1,s1:e<<31|t>>>1,s0:e>>>1^p1<<24&-(o&1)}},Nr=e=>(e>>>0&255)<<24|(e>>>8&255)<<16|(e>>>16&255)<<8|e>>>24&255|0;var h1=e=>e>64*1024?8:e>1024?4:2,qo=class{blockLen=cn;outputLen=cn;s0=0;s1=0;s2=0;s3=0;finished=!1;destroyed=!1;t;W;windowSize;constructor(t,r){we(t,16,"key"),t=Ge(t);let s=an(t),o=s.getUint32(0,!1),a=s.getUint32(4,!1),c=s.getUint32(8,!1),l=s.getUint32(12,!1),u=[];for(let x=0;x<128;x++)u.push({s0:Nr(o),s1:Nr(a),s2:Nr(c),s3:Nr(l)}),{s0:o,s1:a,s2:c,s3:l}=d1(o,a,c,l);let p=h1(r||1024);if(![1,2,4,8].includes(p))throw new Error("ghash: invalid window size, expected 2, 4 or 8");this.W=p;let h=128/p,b=this.windowSize=2**p,w=[];for(let x=0;x>>p-j-1&1))continue;let{s0:k,s1:O,s2:g,s3:E}=u[p*x+j];S^=k,P^=O,B^=g,C^=E}w.push({s0:S,s1:P,s2:B,s3:C})}this.t=w}_updateBlock(t,r,s,o){t^=this.s0,r^=this.s1,s^=this.s2,o^=this.s3;let{W:a,t:c,windowSize:l}=this,u=0,p=0,m=0,h=0,b=(1<>>8*I&255;for(let P=8/a-1;P>=0;P--){let B=S>>>a*P&b,{s0:C,s1:j,s2:H,s3:k}=c[w*l+B];u^=C,p^=j,m^=H,h^=k,w+=1}}this.s0=u,this.s1=p,this.s2=m,this.s3=h}update(t){Fo(this),we(t),t=Ge(t);let r=Ee(t),s=Math.floor(t.length/cn),o=t.length%cn;for(let a=0;anew qo(e,t),e=>[e.length]);var ze=16,rc=4,Dr=new Uint8Array(ze);var m1=283;function y1(e){if(![16,24,32].includes(e.length))throw new Error('"aes key" expected Uint8Array of length 16/24/32, got length='+e.length)}function zo(e){return e<<1^m1&-(e>>7)}function ln(e,t){let r=0;for(;t>0;t>>=1)r^=e&-(t&1),e=zo(e);return r}var g1=(e,t,r=1)=>{if(!Number.isSafeInteger(r)||r>4294967040)throw new Error("incBytes: wrong carry "+r);we(e);for(let s=0;s>>=8}},Go=(()=>{let e=new Uint8Array(256);for(let r=0,s=1;r<256;r++,s^=zo(s))e[r]=s;let t=new Uint8Array(256);t[0]=99;for(let r=0;r<255;r++){let s=e[255-r];s|=s<<8,t[e[r]]=(s^s>>4^s>>5^s>>6^s>>7^99)&255}return De(e),t})(),b1=Go.map((e,t)=>Go.indexOf(t)),_1=e=>e<<24|e>>>8,Ho=e=>e<<8|e>>>24;function sc(e,t){if(e.length!==256)throw new Error("Wrong sbox length");let r=new Uint32Array(256).map((p,m)=>t(e[m])),s=r.map(Ho),o=s.map(Ho),a=o.map(Ho),c=new Uint32Array(256*256),l=new Uint32Array(256*256),u=new Uint16Array(256*256);for(let p=0;p<256;p++)for(let m=0;m<256;m++){let h=p*256+m;c[h]=r[p]^s[m],l[h]=o[p]^a[m],u[h]=e[p]<<8|e[m]}return{sbox:e,sbox2:u,T0:r,T1:s,T2:o,T3:a,T01:c,T23:l}}var Ko=sc(Go,e=>ln(e,3)<<24|e<<16|e<<8|ln(e,2)),oc=sc(b1,e=>ln(e,11)<<24|ln(e,13)<<16|ln(e,9)<<8|ln(e,14)),w1=(()=>{let e=new Uint8Array(16);for(let t=0,r=1;t<16;t++,r=zo(r))e[t]=r;return e})();function jr(e){we(e);let t=e.length;y1(e);let{sbox2:r}=Ko,s=[];(!Rt||!et(e))&&s.push(e=Ge(e));let o=He(Ee(e)),a=o.length,c=u=>ft(r,u,u,u,u),l=new Uint32Array(t+28);l.set(o);for(let u=a;u6&&u%a===4&&(p=c(p)),l[u]=l[u-a]^p}return De(...s),l}function x1(e){let t=jr(e),r=t.slice(),s=t.length,{sbox2:o}=Ko,{T0:a,T1:c,T2:l,T3:u}=oc;for(let p=0;p>>8&255]^l[h>>>16&255]^u[h>>>24]}return r}function Lt(e,t,r,s,o,a){return e[r<<8&65280|s>>>8&255]^t[o>>>8&65280|a>>>24&255]}function ft(e,t,r,s,o){return e[t&255|r&65280]|e[s>>>16&255|o>>>16&65280]<<16}function un(e,t,r,s,o){let{sbox2:a,T01:c,T23:l}=Ko,u=0;t^=e[u++],r^=e[u++],s^=e[u++],o^=e[u++];let p=e.length/4-2;for(let x=0;x>>0,l.setUint32(m,b,t),{s0:w,s1:x,s2:I,s3:S}=un(e,se(c[0]),se(c[1]),se(c[2]),se(c[3]));let P=ze*Math.floor(u.length/rc);if(Ps(o,a),decrypt:(o,a)=>s(o,a)}});function S1(e){if(we(e),e.length%ze!==0)throw new Error("aes-(cbc/ecb).decrypt ciphertext should consist of blocks with size "+ze)}function E1(e,t,r){we(e);let s=e.length,o=s%ze;if(!t&&o!==0)throw new Error("aec/(cbc-ecb): unpadded plaintext with disabled padding");if(t){let l=ze-o;l||(l=ze),s=s+l}r=Ln(s,r),Lr(e,r),(!Rt||!et(e))&&(e=Ge(e));let a=Ee(e);He(a);let c=Ee(r);return{b:a,o:c,out:r}}function T1(e,t){if(!t)return e;let r=e.length;if(r===0)throw new Error("aes/pkcs7: empty ciphertext not allowed");let s=e[r-1],o=1;o&=s-1>>>31^1,o&=16-s>>>31^1;for(let a=0;a<16;a++){let c=a-s>>>31,l=(e[r-1-a]^s)===0?1:0;o&=l|c^1}if(!o)throw new Error("aes/pkcs7: wrong padding");return e.subarray(0,r-s)}function A1(e){let t=new Uint8Array(16),r=Ee(t);t.set(e);let s=ze-e.length;for(let o=ze-s;oo-c&&(this.process(s,0),c=0);for(let h=c;hm.length)throw new Error("_sha2: outputLen bigger than state");for(let h=0;hnew Xo),I1=Math.pow(2,32),O1=Array.from({length:64},(e,t)=>Math.floor(I1*Math.abs(Math.sin(t+1)))),Wr=pn.slice(0,4),Yo=new Uint32Array(16),Qo=class extends Ct{A=Wr[0]|0;B=Wr[1]|0;C=Wr[2]|0;D=Wr[3]|0;constructor(){super(64,16,8,!0)}get(){let{A:t,B:r,C:s,D:o}=this;return[t,r,s,o]}set(t,r,s,o){this.A=t|0,this.B=r|0,this.C=s|0,this.D=o|0}process(t,r){for(let l=0;l<16;l++,r+=4)Yo[l]=t.getUint32(r,!0);let{A:s,B:o,C:a,D:c}=this;for(let l=0;l<64;l++){let u,p,m;l<16?(u=fn(o,a,c),p=l,m=[7,12,17,22]):l<32?(u=fn(c,o,a),p=(5*l+1)%16,m=[5,9,14,20]):l<48?(u=o^a^c,p=(3*l+5)%16,m=[4,11,16,23]):(u=a^(o|~c),p=7*l%16,m=[6,10,15,21]),u=u+s+O1[l]+Yo[p],s=c,c=a,a=o,o=o+ee(u,m[l%4])}s=s+this.A|0,o=o+this.B|0,a=a+this.C|0,c=c+this.D|0,this.set(s,o,a,c)}roundClean(){Ae(Yo)}destroy(){this.destroyed=!0,this.set(0,0,0,0),Ae(this.buffer)}},ac=Bt(()=>new Qo);function R1(e,t,r,s){Ar(e);let o=Or({dkLen:32,asyncTick:10},s),{c:a,dkLen:c,asyncTick:l}=o;if($e(a,"c"),$e(c,"dkLen"),$e(l,"asyncTick"),a<1)throw new Error("iterations (c) must be >= 1");if(c<1)throw new Error('"dkLen" must be >= 1');if(c>(2**32-1)*e.outputLen)throw new Error("derived key too long");let u=No(t,"password"),p=No(r,"salt"),m=new Uint8Array(c),h=Rr.create(e,u),b=h._cloneInto().update(p);return{c:a,dkLen:c,asyncTick:l,DK:m,PRF:h,PRFSalt:b}}function L1(e,t,r,s,o){return e.destroy(),t.destroy(),s&&s.destroy(),Ae(o),r}function Nn(e,t,r,s){let{c:o,dkLen:a,DK:c,PRF:l,PRFSalt:u}=R1(e,t,r,s),p,m=new Uint8Array(4),h=on(m),b=new Uint8Array(l.outputLen);for(let w=1,x=0;x>cc&qr)}:{h:Number(e>>cc&qr)|0,l:Number(e&qr)|0}}function lc(e,t=!1){let r=e.length,s=new Uint32Array(r),o=new Uint32Array(r);for(let a=0;ae>>>r,ei=(e,t,r)=>e<<32-r|t>>>r,zt=(e,t,r)=>e>>>r|t<<32-r,Kt=(e,t,r)=>e<<32-r|t>>>r,Dn=(e,t,r)=>e<<64-r|t>>>r-32,Fn=(e,t,r)=>e>>>r-32|t<<64-r;function pt(e,t,r,s){let o=(t>>>0)+(s>>>0);return{h:e+r+(o/2**32|0)|0,l:o|0}}var uc=(e,t,r)=>(e>>>0)+(t>>>0)+(r>>>0),fc=(e,t,r,s)=>t+r+s+(e/2**32|0)|0,pc=(e,t,r,s)=>(e>>>0)+(t>>>0)+(r>>>0)+(s>>>0),dc=(e,t,r,s,o)=>t+r+s+o+(e/2**32|0)|0,hc=(e,t,r,s,o)=>(e>>>0)+(t>>>0)+(r>>>0)+(s>>>0)+(o>>>0),mc=(e,t,r,s,o,a)=>t+r+s+o+a+(e/2**32|0)|0;var D1=Uint32Array.from([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),Dt=new Uint32Array(64),$r=class extends Ct{constructor(t){super(64,t,8,!1)}get(){let{A:t,B:r,C:s,D:o,E:a,F:c,G:l,H:u}=this;return[t,r,s,o,a,c,l,u]}set(t,r,s,o,a,c,l,u){this.A=t|0,this.B=r|0,this.C=s|0,this.D=o|0,this.E=a|0,this.F=c|0,this.G=l|0,this.H=u|0}process(t,r){for(let h=0;h<16;h++,r+=4)Dt[h]=t.getUint32(r,!1);for(let h=16;h<64;h++){let b=Dt[h-15],w=Dt[h-2],x=it(b,7)^it(b,18)^b>>>3,I=it(w,17)^it(w,19)^w>>>10;Dt[h]=I+Dt[h-7]+x+Dt[h-16]|0}let{A:s,B:o,C:a,D:c,E:l,F:u,G:p,H:m}=this;for(let h=0;h<64;h++){let b=it(l,6)^it(l,11)^it(l,25),w=m+b+fn(l,u,p)+D1[h]+Dt[h]|0,I=(it(s,2)^it(s,13)^it(s,22))+Ur(s,o,a)|0;m=p,p=u,u=l,l=c+w|0,c=a,a=o,o=s,s=w+I|0}s=s+this.A|0,o=o+this.B|0,a=a+this.C|0,c=c+this.D|0,l=l+this.E|0,u=u+this.F|0,p=p+this.G|0,m=m+this.H|0,this.set(s,o,a,c,l,u,p,m)}roundClean(){Ae(Dt)}destroy(){this.destroyed=!0,this.set(0,0,0,0,0,0,0,0),Ae(this.buffer)}},ti=class extends $r{A=_t[0]|0;B=_t[1]|0;C=_t[2]|0;D=_t[3]|0;E=_t[4]|0;F=_t[5]|0;G=_t[6]|0;H=_t[7]|0;constructor(){super(32)}},ni=class extends $r{A=wt[0]|0;B=wt[1]|0;C=wt[2]|0;D=wt[3]|0;E=wt[4]|0;F=wt[5]|0;G=wt[6]|0;H=wt[7]|0;constructor(){super(28)}},yc=lc(["0x428a2f98d728ae22","0x7137449123ef65cd","0xb5c0fbcfec4d3b2f","0xe9b5dba58189dbbc","0x3956c25bf348b538","0x59f111f1b605d019","0x923f82a4af194f9b","0xab1c5ed5da6d8118","0xd807aa98a3030242","0x12835b0145706fbe","0x243185be4ee4b28c","0x550c7dc3d5ffb4e2","0x72be5d74f27b896f","0x80deb1fe3b1696b1","0x9bdc06a725c71235","0xc19bf174cf692694","0xe49b69c19ef14ad2","0xefbe4786384f25e3","0x0fc19dc68b8cd5b5","0x240ca1cc77ac9c65","0x2de92c6f592b0275","0x4a7484aa6ea6e483","0x5cb0a9dcbd41fbd4","0x76f988da831153b5","0x983e5152ee66dfab","0xa831c66d2db43210","0xb00327c898fb213f","0xbf597fc7beef0ee4","0xc6e00bf33da88fc2","0xd5a79147930aa725","0x06ca6351e003826f","0x142929670a0e6e70","0x27b70a8546d22ffc","0x2e1b21385c26c926","0x4d2c6dfc5ac42aed","0x53380d139d95b3df","0x650a73548baf63de","0x766a0abb3c77b2a8","0x81c2c92e47edaee6","0x92722c851482353b","0xa2bfe8a14cf10364","0xa81a664bbc423001","0xc24b8b70d0f89791","0xc76c51a30654be30","0xd192e819d6ef5218","0xd69906245565a910","0xf40e35855771202a","0x106aa07032bbd1b8","0x19a4c116b8d2d0c8","0x1e376c085141ab53","0x2748774cdf8eeb99","0x34b0bcb5e19b48a8","0x391c0cb3c5c95a63","0x4ed8aa4ae3418acb","0x5b9cca4f7763e373","0x682e6ff3d6b2b8a3","0x748f82ee5defb2fc","0x78a5636f43172f60","0x84c87814a1f0ab72","0x8cc702081a6439ec","0x90befffa23631e28","0xa4506cebde82bde9","0xbef9a3f7b2c67915","0xc67178f2e372532b","0xca273eceea26619c","0xd186b8c721c0c207","0xeada7dd6cde0eb1e","0xf57d4f7fee6ed178","0x06f067aa72176fba","0x0a637dc5a2c898a6","0x113f9804bef90dae","0x1b710b35131c471b","0x28db77f523047d84","0x32caab7b40c72493","0x3c9ebe0a15c9bebc","0x431d67c49c100d4c","0x4cc5d4becb3e42b6","0x597f299cfc657e2a","0x5fcb6fab3ad6faec","0x6c44198c4a475817"].map(e=>BigInt(e))),F1=yc[0],j1=yc[1],Ft=new Uint32Array(80),jt=new Uint32Array(80),Hr=class extends Ct{constructor(t){super(128,t,16,!1)}get(){let{Ah:t,Al:r,Bh:s,Bl:o,Ch:a,Cl:c,Dh:l,Dl:u,Eh:p,El:m,Fh:h,Fl:b,Gh:w,Gl:x,Hh:I,Hl:S}=this;return[t,r,s,o,a,c,l,u,p,m,h,b,w,x,I,S]}set(t,r,s,o,a,c,l,u,p,m,h,b,w,x,I,S){this.Ah=t|0,this.Al=r|0,this.Bh=s|0,this.Bl=o|0,this.Ch=a|0,this.Cl=c|0,this.Dh=l|0,this.Dl=u|0,this.Eh=p|0,this.El=m|0,this.Fh=h|0,this.Fl=b|0,this.Gh=w|0,this.Gl=x|0,this.Hh=I|0,this.Hl=S|0}process(t,r){for(let C=0;C<16;C++,r+=4)Ft[C]=t.getUint32(r),jt[C]=t.getUint32(r+=4);for(let C=16;C<80;C++){let j=Ft[C-15]|0,H=jt[C-15]|0,k=zt(j,H,1)^zt(j,H,8)^Zo(j,H,7),O=Kt(j,H,1)^Kt(j,H,8)^ei(j,H,7),g=Ft[C-2]|0,E=jt[C-2]|0,N=zt(g,E,19)^Dn(g,E,61)^Zo(g,E,6),W=Kt(g,E,19)^Fn(g,E,61)^ei(g,E,6),Y=pc(O,W,jt[C-7],jt[C-16]),re=dc(Y,k,N,Ft[C-7],Ft[C-16]);Ft[C]=re|0,jt[C]=Y|0}let{Ah:s,Al:o,Bh:a,Bl:c,Ch:l,Cl:u,Dh:p,Dl:m,Eh:h,El:b,Fh:w,Fl:x,Gh:I,Gl:S,Hh:P,Hl:B}=this;for(let C=0;C<80;C++){let j=zt(h,b,14)^zt(h,b,18)^Dn(h,b,41),H=Kt(h,b,14)^Kt(h,b,18)^Fn(h,b,41),k=h&w^~h&I,O=b&x^~b&S,g=hc(B,H,O,j1[C],jt[C]),E=mc(g,P,j,k,F1[C],Ft[C]),N=g|0,W=zt(s,o,28)^Dn(s,o,34)^Dn(s,o,39),Y=Kt(s,o,28)^Fn(s,o,34)^Fn(s,o,39),re=s&a^s&l^a&l,oe=o&c^o&u^c&u;P=I|0,B=S|0,I=w|0,S=x|0,w=h|0,x=b|0,{h,l:b}=pt(p|0,m|0,E|0,N|0),p=l|0,m=u|0,l=a|0,u=c|0,a=s|0,c=o|0;let Q=uc(N,Y,oe);s=fc(Q,E,W,re),o=Q|0}({h:s,l:o}=pt(this.Ah|0,this.Al|0,s|0,o|0)),{h:a,l:c}=pt(this.Bh|0,this.Bl|0,a|0,c|0),{h:l,l:u}=pt(this.Ch|0,this.Cl|0,l|0,u|0),{h:p,l:m}=pt(this.Dh|0,this.Dl|0,p|0,m|0),{h,l:b}=pt(this.Eh|0,this.El|0,h|0,b|0),{h:w,l:x}=pt(this.Fh|0,this.Fl|0,w|0,x|0),{h:I,l:S}=pt(this.Gh|0,this.Gl|0,I|0,S|0),{h:P,l:B}=pt(this.Hh|0,this.Hl|0,P|0,B|0),this.set(s,o,a,c,l,u,p,m,h,b,w,x,I,S,P,B)}roundClean(){Ae(Ft,jt)}destroy(){this.destroyed=!0,Ae(this.buffer),this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)}},ri=class extends Hr{Ah=Le[0]|0;Al=Le[1]|0;Bh=Le[2]|0;Bl=Le[3]|0;Ch=Le[4]|0;Cl=Le[5]|0;Dh=Le[6]|0;Dl=Le[7]|0;Eh=Le[8]|0;El=Le[9]|0;Fh=Le[10]|0;Fl=Le[11]|0;Gh=Le[12]|0;Gl=Le[13]|0;Hh=Le[14]|0;Hl=Le[15]|0;constructor(){super(64)}},si=class extends Hr{Ah=Re[0]|0;Al=Re[1]|0;Bh=Re[2]|0;Bl=Re[3]|0;Ch=Re[4]|0;Cl=Re[5]|0;Dh=Re[6]|0;Dl=Re[7]|0;Eh=Re[8]|0;El=Re[9]|0;Fh=Re[10]|0;Fl=Re[11]|0;Gh=Re[12]|0;Gl=Re[13]|0;Hh=Re[14]|0;Hl=Re[15]|0;constructor(){super(48)}};var jn=Bt(()=>new ti,Rn(1)),gc=Bt(()=>new ni,Rn(4)),bc=Bt(()=>new ri,Rn(3)),_c=Bt(()=>new si,Rn(2));function wc(e,t,r,s,o,a){let c=e[t++]^r[s++],l=e[t++]^r[s++],u=e[t++]^r[s++],p=e[t++]^r[s++],m=e[t++]^r[s++],h=e[t++]^r[s++],b=e[t++]^r[s++],w=e[t++]^r[s++],x=e[t++]^r[s++],I=e[t++]^r[s++],S=e[t++]^r[s++],P=e[t++]^r[s++],B=e[t++]^r[s++],C=e[t++]^r[s++],j=e[t++]^r[s++],H=e[t++]^r[s++],k=c,O=l,g=u,E=p,N=m,W=h,Y=b,re=w,oe=x,Q=I,de=S,he=P,Se=B,Oe=C,Te=j,Be=H;for(let Qe=0;Qe<8;Qe+=2)N^=ee(k+Se|0,7),oe^=ee(N+k|0,9),Se^=ee(oe+N|0,13),k^=ee(Se+oe|0,18),Q^=ee(W+O|0,7),Oe^=ee(Q+W|0,9),O^=ee(Oe+Q|0,13),W^=ee(O+Oe|0,18),Te^=ee(de+Y|0,7),g^=ee(Te+de|0,9),Y^=ee(g+Te|0,13),de^=ee(Y+g|0,18),E^=ee(Be+he|0,7),re^=ee(E+Be|0,9),he^=ee(re+E|0,13),Be^=ee(he+re|0,18),O^=ee(k+E|0,7),g^=ee(O+k|0,9),E^=ee(g+O|0,13),k^=ee(E+g|0,18),Y^=ee(W+N|0,7),re^=ee(Y+W|0,9),N^=ee(re+Y|0,13),W^=ee(N+re|0,18),he^=ee(de+Q|0,7),oe^=ee(he+de|0,9),Q^=ee(oe+he|0,13),de^=ee(Q+oe|0,18),Se^=ee(Be+Te|0,7),Oe^=ee(Se+Be|0,9),Te^=ee(Oe+Se|0,13),Be^=ee(Te+Oe|0,18);o[a++]=c+k|0,o[a++]=l+O|0,o[a++]=u+g|0,o[a++]=p+E|0,o[a++]=m+N|0,o[a++]=h+W|0,o[a++]=b+Y|0,o[a++]=w+re|0,o[a++]=x+oe|0,o[a++]=I+Q|0,o[a++]=S+de|0,o[a++]=P+he|0,o[a++]=B+Se|0,o[a++]=C+Oe|0,o[a++]=j+Te|0,o[a++]=H+Be|0}function oi(e,t,r,s,o){let a=s+0,c=s+16*o;for(let l=0;l<16;l++)r[c+l]=e[t+(2*o-1)*16+l];for(let l=0;l0&&(c+=16),wc(r,a,e,t+=16,r,c)}function M1(e,t,r){let s=Or({dkLen:32,asyncTick:10,maxmem:1073742848},r),{N:o,r:a,p:c,dkLen:l,asyncTick:u,maxmem:p,onProgress:m}=s;if($e(o,"N"),$e(a,"r"),$e(c,"p"),$e(l,"dkLen"),$e(u,"asyncTick"),$e(p,"maxmem"),m!==void 0&&typeof m!="function")throw new Error("progressCb must be a function");let h=128*a,b=h/4,w=Math.pow(2,32);if(o<=1||(o&o-1)!==0||o>w)throw new Error('"N" expected a power of 2, and 2^1 <= N <= 2^32');if(c<1||c>(w-1)*32/h)throw new Error('"p" expected integer 1..((2^32 - 1) * 32) / (128 * r)');if(l<1||l>(w-1)*32)throw new Error('"dkLen" expected integer 1..(2^32 - 1) * 32');let x=h*(o+c+1);if(x>p)throw new Error('"maxmem" limit was hit: memUsed(128*r*(N+p+1))='+x+", maxmem="+p);let I=Nn(jn,e,t,{c:1,dkLen:h*c}),S=Ir(I),P=Ir(new Uint8Array(h*o)),B=Ir(new Uint8Array(h)),C=()=>{};if(m){let j=2*o*c,H=Math.max(Math.floor(j/1e4),1),k=0;C=()=>{k++,m&&(!(k%H)||k===j)&&m(k/j)}}return{N:o,r:a,p:c,dkLen:l,blockSize32:b,V:P,B32:S,B:I,tmp:B,blockMixCb:C,asyncTick:u}}function U1(e,t,r,s,o){let a=Nn(jn,e,r,{c:1,dkLen:t});return Ae(r,s,o),a}function xc(e,t,r){let{N:s,r:o,p:a,dkLen:c,blockSize32:l,V:u,B32:p,B:m,tmp:h,blockMixCb:b}=M1(e,t,r);Co(p);for(let w=0;w>>0;for(let P=0;P";case n.template:return"template";case n.ellipsis:return"...";case n.backQuote:return"`";case n.dollarBraceL:return"${";case n.at:return"@";case n.hash:return"#";case n.eq:return"=";case n.assign:return"_=";case n.preIncDec:return"++/--";case n.postIncDec:return"++/--";case n.bang:return"!";case n.tilde:return"~";case n.pipeline:return"|>";case n.nullishCoalescing:return"??";case n.logicalOR:return"||";case n.logicalAND:return"&&";case n.bitwiseOR:return"|";case n.bitwiseXOR:return"^";case n.bitwiseAND:return"&";case n.equality:return"==/!=";case n.lessThan:return"<";case n.greaterThan:return">";case n.relationalOrEqual:return"<=/>=";case n.bitShiftL:return"<<";case n.bitShiftR:return">>/>>>";case n.plus:return"+";case n.minus:return"-";case n.modulo:return"%";case n.star:return"*";case n.slash:return"/";case n.exponent:return"**";case n.jsxName:return"jsxName";case n.jsxText:return"jsxText";case n.jsxEmptyText:return"jsxEmptyText";case n.jsxTagStart:return"jsxTagStart";case n.jsxTagEnd:return"jsxTagEnd";case n.typeParameterStart:return"typeParameterStart";case n.nonNullAssertion:return"nonNullAssertion";case n._break:return"break";case n._case:return"case";case n._catch:return"catch";case n._continue:return"continue";case n._debugger:return"debugger";case n._default:return"default";case n._do:return"do";case n._else:return"else";case n._finally:return"finally";case n._for:return"for";case n._function:return"function";case n._if:return"if";case n._return:return"return";case n._switch:return"switch";case n._throw:return"throw";case n._try:return"try";case n._var:return"var";case n._let:return"let";case n._const:return"const";case n._while:return"while";case n._with:return"with";case n._new:return"new";case n._this:return"this";case n._super:return"super";case n._class:return"class";case n._extends:return"extends";case n._export:return"export";case n._import:return"import";case n._yield:return"yield";case n._null:return"null";case n._true:return"true";case n._false:return"false";case n._in:return"in";case n._instanceof:return"instanceof";case n._typeof:return"typeof";case n._void:return"void";case n._delete:return"delete";case n._async:return"async";case n._get:return"get";case n._set:return"set";case n._declare:return"declare";case n._readonly:return"readonly";case n._abstract:return"abstract";case n._static:return"static";case n._public:return"public";case n._private:return"private";case n._protected:return"protected";case n._override:return"override";case n._as:return"as";case n._enum:return"enum";case n._type:return"type";case n._implements:return"implements";default:return""}}var Ue=class{constructor(t,r,s){this.startTokenIndex=t,this.endTokenIndex=r,this.isFunctionScope=s}},ai=class{constructor(t,r,s,o,a,c,l,u,p,m,h,b,w){this.potentialArrowAt=t,this.noAnonFunctionType=r,this.inDisallowConditionalTypesContext=s,this.tokensLength=o,this.scopesLength=a,this.pos=c,this.type=l,this.contextualKeyword=u,this.start=p,this.end=m,this.isType=h,this.scopeDepth=b,this.error=w}},Mn=class e{constructor(){e.prototype.__init.call(this),e.prototype.__init2.call(this),e.prototype.__init3.call(this),e.prototype.__init4.call(this),e.prototype.__init5.call(this),e.prototype.__init6.call(this),e.prototype.__init7.call(this),e.prototype.__init8.call(this),e.prototype.__init9.call(this),e.prototype.__init10.call(this),e.prototype.__init11.call(this),e.prototype.__init12.call(this),e.prototype.__init13.call(this)}__init(){this.potentialArrowAt=-1}__init2(){this.noAnonFunctionType=!1}__init3(){this.inDisallowConditionalTypesContext=!1}__init4(){this.tokens=[]}__init5(){this.scopes=[]}__init6(){this.pos=0}__init7(){this.type=n.eof}__init8(){this.contextualKeyword=d.NONE}__init9(){this.start=0}__init10(){this.end=0}__init11(){this.isType=!1}__init12(){this.scopeDepth=0}__init13(){this.error=null}snapshot(){return new ai(this.potentialArrowAt,this.noAnonFunctionType,this.inDisallowConditionalTypesContext,this.tokens.length,this.scopes.length,this.pos,this.type,this.contextualKeyword,this.start,this.end,this.isType,this.scopeDepth,this.error)}restoreFromSnapshot(t){this.potentialArrowAt=t.potentialArrowAt,this.noAnonFunctionType=t.noAnonFunctionType,this.inDisallowConditionalTypesContext=t.inDisallowConditionalTypesContext,this.tokens.length=t.tokensLength,this.scopes.length=t.scopesLength,this.pos=t.pos,this.type=t.type,this.contextualKeyword=t.contextualKeyword,this.start=t.start,this.end=t.end,this.isType=t.isType,this.scopeDepth=t.scopeDepth,this.error=t.error}};var y;(function(e){e[e.backSpace=8]="backSpace";let r=10;e[e.lineFeed=r]="lineFeed";let s=9;e[e.tab=s]="tab";let o=13;e[e.carriageReturn=o]="carriageReturn";let a=14;e[e.shiftOut=a]="shiftOut";let c=32;e[e.space=c]="space";let l=33;e[e.exclamationMark=l]="exclamationMark";let u=34;e[e.quotationMark=u]="quotationMark";let p=35;e[e.numberSign=p]="numberSign";let m=36;e[e.dollarSign=m]="dollarSign";let h=37;e[e.percentSign=h]="percentSign";let b=38;e[e.ampersand=b]="ampersand";let w=39;e[e.apostrophe=w]="apostrophe";let x=40;e[e.leftParenthesis=x]="leftParenthesis";let I=41;e[e.rightParenthesis=I]="rightParenthesis";let S=42;e[e.asterisk=S]="asterisk";let P=43;e[e.plusSign=P]="plusSign";let B=44;e[e.comma=B]="comma";let C=45;e[e.dash=C]="dash";let j=46;e[e.dot=j]="dot";let H=47;e[e.slash=H]="slash";let k=48;e[e.digit0=k]="digit0";let O=49;e[e.digit1=O]="digit1";let g=50;e[e.digit2=g]="digit2";let E=51;e[e.digit3=E]="digit3";let N=52;e[e.digit4=N]="digit4";let W=53;e[e.digit5=W]="digit5";let Y=54;e[e.digit6=Y]="digit6";let re=55;e[e.digit7=re]="digit7";let oe=56;e[e.digit8=oe]="digit8";let Q=57;e[e.digit9=Q]="digit9";let de=58;e[e.colon=de]="colon";let he=59;e[e.semicolon=he]="semicolon";let Se=60;e[e.lessThan=Se]="lessThan";let Oe=61;e[e.equalsTo=Oe]="equalsTo";let Te=62;e[e.greaterThan=Te]="greaterThan";let Be=63;e[e.questionMark=Be]="questionMark";let Qe=64;e[e.atSign=Qe]="atSign";let ut=65;e[e.uppercaseA=ut]="uppercaseA";let It=66;e[e.uppercaseB=It]="uppercaseB";let Ot=67;e[e.uppercaseC=Ot]="uppercaseC";let tn=68;e[e.uppercaseD=tn]="uppercaseD";let Ht=69;e[e.uppercaseE=Ht]="uppercaseE";let Gt=70;e[e.uppercaseF=Gt]="uppercaseF";let le=71;e[e.uppercaseG=le]="uppercaseG";let _=72;e[e.uppercaseH=_]="uppercaseH";let L=73;e[e.uppercaseI=L]="uppercaseI";let z=74;e[e.uppercaseJ=z]="uppercaseJ";let Z=75;e[e.uppercaseK=Z]="uppercaseK";let ie=76;e[e.uppercaseL=ie]="uppercaseL";let be=77;e[e.uppercaseM=be]="uppercaseM";let bt=78;e[e.uppercaseN=bt]="uppercaseN";let Ze=79;e[e.uppercaseO=Ze]="uppercaseO";let nn=80;e[e.uppercaseP=nn]="uppercaseP";let Fs=81;e[e.uppercaseQ=Fs]="uppercaseQ";let js=82;e[e.uppercaseR=js]="uppercaseR";let Ms=83;e[e.uppercaseS=Ms]="uppercaseS";let Us=84;e[e.uppercaseT=Us]="uppercaseT";let Ws=85;e[e.uppercaseU=Ws]="uppercaseU";let qs=86;e[e.uppercaseV=qs]="uppercaseV";let $s=87;e[e.uppercaseW=$s]="uppercaseW";let Hs=88;e[e.uppercaseX=Hs]="uppercaseX";let Gs=89;e[e.uppercaseY=Gs]="uppercaseY";let zs=90;e[e.uppercaseZ=zs]="uppercaseZ";let Ks=91;e[e.leftSquareBracket=Ks]="leftSquareBracket";let Vs=92;e[e.backslash=Vs]="backslash";let Js=93;e[e.rightSquareBracket=Js]="rightSquareBracket";let Ys=94;e[e.caret=Ys]="caret";let Xs=95;e[e.underscore=Xs]="underscore";let Qs=96;e[e.graveAccent=Qs]="graveAccent";let Zs=97;e[e.lowercaseA=Zs]="lowercaseA";let eo=98;e[e.lowercaseB=eo]="lowercaseB";let to=99;e[e.lowercaseC=to]="lowercaseC";let no=100;e[e.lowercaseD=no]="lowercaseD";let ro=101;e[e.lowercaseE=ro]="lowercaseE";let so=102;e[e.lowercaseF=so]="lowercaseF";let oo=103;e[e.lowercaseG=oo]="lowercaseG";let io=104;e[e.lowercaseH=io]="lowercaseH";let ao=105;e[e.lowercaseI=ao]="lowercaseI";let co=106;e[e.lowercaseJ=co]="lowercaseJ";let lo=107;e[e.lowercaseK=lo]="lowercaseK";let uo=108;e[e.lowercaseL=uo]="lowercaseL";let fo=109;e[e.lowercaseM=fo]="lowercaseM";let po=110;e[e.lowercaseN=po]="lowercaseN";let ho=111;e[e.lowercaseO=ho]="lowercaseO";let mo=112;e[e.lowercaseP=mo]="lowercaseP";let yo=113;e[e.lowercaseQ=yo]="lowercaseQ";let go=114;e[e.lowercaseR=go]="lowercaseR";let bo=115;e[e.lowercaseS=bo]="lowercaseS";let _o=116;e[e.lowercaseT=_o]="lowercaseT";let wo=117;e[e.lowercaseU=wo]="lowercaseU";let xo=118;e[e.lowercaseV=xo]="lowercaseV";let vo=119;e[e.lowercaseW=vo]="lowercaseW";let ko=120;e[e.lowercaseX=ko]="lowercaseX";let So=121;e[e.lowercaseY=So]="lowercaseY";let Eo=122;e[e.lowercaseZ=Eo]="lowercaseZ";let To=123;e[e.leftCurlyBrace=To]="leftCurlyBrace";let Ao=124;e[e.verticalBar=Ao]="verticalBar";let Po=125;e[e.rightCurlyBrace=Po]="rightCurlyBrace";let Io=126;e[e.tilde=Io]="tilde";let Oo=160;e[e.nonBreakingSpace=Oo]="nonBreakingSpace";let Bo=5760;e[e.oghamSpaceMark=Bo]="oghamSpaceMark";let Ro=8232;e[e.lineSeparator=Ro]="lineSeparator";let Lo=8233;e[e.paragraphSeparator=Lo]="paragraphSeparator"})(y||(y={}));var dn,X,te,i,R,vc;function Vt(){return vc++}function kc(e){if("pos"in e){let t=W1(e.pos);e.message+=` (${t.line}:${t.column})`,e.loc=t}return e}var ci=class{constructor(t,r){this.line=t,this.column=r}};function W1(e){let t=1,r=1;for(let s=0;sy.lowercaseZ));){let o=pi[e+(t-y.lowercaseA)+1];if(o===-1)break;e=o,r++}let s=pi[e];if(s>-1&&!Fe[t]){i.pos=r,s&1?K(s>>>1):K(n.name,s>>>1);return}for(;r=R.length){let e=i.tokens;e.length>=2&&e[e.length-1].start>=R.length&&e[e.length-2].start>=R.length&&G("Unexpectedly reached the end of input."),K(n.eof);return}$1(R.charCodeAt(i.pos))}function $1(e){vt[e]||e===y.backslash||e===y.atSign&&R.charCodeAt(i.pos+1)===y.atSign?di():xi(e)}function H1(){for(;R.charCodeAt(i.pos)!==y.asterisk||R.charCodeAt(i.pos+1)!==y.slash;)if(i.pos++,i.pos>R.length){G("Unterminated comment",i.pos-2);return}i.pos+=2}function _i(e){let t=R.charCodeAt(i.pos+=e);if(i.pos=y.digit0&&e<=y.digit9){Bc(!0);return}e===y.dot&&R.charCodeAt(i.pos+2)===y.dot?(i.pos+=3,K(n.ellipsis)):(++i.pos,K(n.dot))}function z1(){R.charCodeAt(i.pos+1)===y.equalsTo?ae(n.assign,2):ae(n.slash,1)}function K1(e){let t=e===y.asterisk?n.star:n.modulo,r=1,s=R.charCodeAt(i.pos+1);e===y.asterisk&&s===y.asterisk&&(r++,s=R.charCodeAt(i.pos+2),t=n.exponent),s===y.equalsTo&&R.charCodeAt(i.pos+2)!==y.greaterThan&&(r++,t=n.assign),ae(t,r)}function V1(e){let t=R.charCodeAt(i.pos+1);if(t===e){R.charCodeAt(i.pos+2)===y.equalsTo?ae(n.assign,3):ae(e===y.verticalBar?n.logicalOR:n.logicalAND,2);return}if(e===y.verticalBar){if(t===y.greaterThan){ae(n.pipeline,2);return}else if(t===y.rightCurlyBrace&&te){ae(n.braceBarR,2);return}}if(t===y.equalsTo){ae(n.assign,2);return}ae(e===y.verticalBar?n.bitwiseOR:n.bitwiseAND,1)}function J1(){R.charCodeAt(i.pos+1)===y.equalsTo?ae(n.assign,2):ae(n.bitwiseXOR,1)}function Y1(e){let t=R.charCodeAt(i.pos+1);if(t===e){ae(n.preIncDec,2);return}t===y.equalsTo?ae(n.assign,2):e===y.plusSign?ae(n.plus,1):ae(n.minus,1)}function X1(){let e=R.charCodeAt(i.pos+1);if(e===y.lessThan){if(R.charCodeAt(i.pos+2)===y.equalsTo){ae(n.assign,3);return}i.isType?ae(n.lessThan,1):ae(n.bitShiftL,2);return}e===y.equalsTo?ae(n.relationalOrEqual,2):ae(n.lessThan,1)}function Oc(){if(i.isType){ae(n.greaterThan,1);return}let e=R.charCodeAt(i.pos+1);if(e===y.greaterThan){let t=R.charCodeAt(i.pos+2)===y.greaterThan?3:2;if(R.charCodeAt(i.pos+t)===y.equalsTo){ae(n.assign,t+1);return}ae(n.bitShiftR,t);return}e===y.equalsTo?ae(n.relationalOrEqual,2):ae(n.greaterThan,1)}function Jr(){i.type===n.greaterThan&&(i.pos-=1,Oc())}function Q1(e){let t=R.charCodeAt(i.pos+1);if(t===y.equalsTo){ae(n.equality,R.charCodeAt(i.pos+2)===y.equalsTo?3:2);return}if(e===y.equalsTo&&t===y.greaterThan){i.pos+=2,K(n.arrow);return}ae(e===y.equalsTo?n.eq:n.bang,1)}function Z1(){let e=R.charCodeAt(i.pos+1),t=R.charCodeAt(i.pos+2);e===y.questionMark&&!(te&&i.isType)?t===y.equalsTo?ae(n.assign,3):ae(n.nullishCoalescing,2):e===y.dot&&!(t>=y.digit0&&t<=y.digit9)?(i.pos+=2,K(n.questionDot)):(++i.pos,K(n.question))}function xi(e){switch(e){case y.numberSign:++i.pos,K(n.hash);return;case y.dot:G1();return;case y.leftParenthesis:++i.pos,K(n.parenL);return;case y.rightParenthesis:++i.pos,K(n.parenR);return;case y.semicolon:++i.pos,K(n.semi);return;case y.comma:++i.pos,K(n.comma);return;case y.leftSquareBracket:++i.pos,K(n.bracketL);return;case y.rightSquareBracket:++i.pos,K(n.bracketR);return;case y.leftCurlyBrace:te&&R.charCodeAt(i.pos+1)===y.verticalBar?ae(n.braceBarL,2):(++i.pos,K(n.braceL));return;case y.rightCurlyBrace:++i.pos,K(n.braceR);return;case y.colon:R.charCodeAt(i.pos+1)===y.colon?ae(n.doubleColon,2):(++i.pos,K(n.colon));return;case y.questionMark:Z1();return;case y.atSign:++i.pos,K(n.at);return;case y.graveAccent:++i.pos,K(n.backQuote);return;case y.digit0:{let t=R.charCodeAt(i.pos+1);if(t===y.lowercaseX||t===y.uppercaseX||t===y.lowercaseO||t===y.uppercaseO||t===y.lowercaseB||t===y.uppercaseB){tp();return}}case y.digit1:case y.digit2:case y.digit3:case y.digit4:case y.digit5:case y.digit6:case y.digit7:case y.digit8:case y.digit9:Bc(!1);return;case y.quotationMark:case y.apostrophe:np(e);return;case y.slash:z1();return;case y.percentSign:case y.asterisk:K1(e);return;case y.verticalBar:case y.ampersand:V1(e);return;case y.caret:J1();return;case y.plusSign:case y.dash:Y1(e);return;case y.lessThan:X1();return;case y.greaterThan:Oc();return;case y.equalsTo:case y.exclamationMark:Q1(e);return;case y.tilde:ae(n.tilde,1);return;default:break}G(`Unexpected character '${String.fromCharCode(e)}'`,i.pos)}function ae(e,t){i.pos+=t,K(e)}function ep(){let e=i.pos,t=!1,r=!1;for(;;){if(i.pos>=R.length){G("Unterminated regular expression",e);return}let s=R.charCodeAt(i.pos);if(t)t=!1;else{if(s===y.leftSquareBracket)r=!0;else if(s===y.rightSquareBracket&&r)r=!1;else if(s===y.slash&&!r)break;t=s===y.backslash}++i.pos}++i.pos,sp(),K(n.regexp)}function hi(){for(;;){let e=R.charCodeAt(i.pos);if(e>=y.digit0&&e<=y.digit9||e===y.underscore)i.pos++;else break}}function tp(){for(i.pos+=2;;){let t=R.charCodeAt(i.pos);if(t>=y.digit0&&t<=y.digit9||t>=y.lowercaseA&&t<=y.lowercaseF||t>=y.uppercaseA&&t<=y.uppercaseF||t===y.underscore)i.pos++;else break}R.charCodeAt(i.pos)===y.lowercaseN?(++i.pos,K(n.bigint)):K(n.num)}function Bc(e){let t=!1,r=!1;e||hi();let s=R.charCodeAt(i.pos);if(s===y.dot&&(++i.pos,hi(),s=R.charCodeAt(i.pos)),(s===y.uppercaseE||s===y.lowercaseE)&&(s=R.charCodeAt(++i.pos),(s===y.plusSign||s===y.dash)&&++i.pos,hi(),s=R.charCodeAt(i.pos)),s===y.lowercaseN?(++i.pos,t=!0):s===y.lowercaseM&&(++i.pos,r=!0),t){K(n.bigint);return}if(r){K(n.decimal);return}K(n.num)}function np(e){for(i.pos++;;){if(i.pos>=R.length){G("Unterminated string constant");return}let t=R.charCodeAt(i.pos);if(t===y.backslash)i.pos++;else if(t===e)break;i.pos++}i.pos++,K(n.string)}function rp(){for(;;){if(i.pos>=R.length){G("Unterminated template");return}let e=R.charCodeAt(i.pos);if(e===y.graveAccent||e===y.dollarSign&&R.charCodeAt(i.pos+1)===y.leftCurlyBrace){if(i.pos===i.start&&f(n.template))if(e===y.dollarSign){i.pos+=2,K(n.dollarBraceL);return}else{++i.pos,K(n.backQuote);return}K(n.template);return}e===y.backslash&&i.pos++,i.pos++}}function sp(){for(;i.pos"],["nbsp","\xA0"],["iexcl","\xA1"],["cent","\xA2"],["pound","\xA3"],["curren","\xA4"],["yen","\xA5"],["brvbar","\xA6"],["sect","\xA7"],["uml","\xA8"],["copy","\xA9"],["ordf","\xAA"],["laquo","\xAB"],["not","\xAC"],["shy","\xAD"],["reg","\xAE"],["macr","\xAF"],["deg","\xB0"],["plusmn","\xB1"],["sup2","\xB2"],["sup3","\xB3"],["acute","\xB4"],["micro","\xB5"],["para","\xB6"],["middot","\xB7"],["cedil","\xB8"],["sup1","\xB9"],["ordm","\xBA"],["raquo","\xBB"],["frac14","\xBC"],["frac12","\xBD"],["frac34","\xBE"],["iquest","\xBF"],["Agrave","\xC0"],["Aacute","\xC1"],["Acirc","\xC2"],["Atilde","\xC3"],["Auml","\xC4"],["Aring","\xC5"],["AElig","\xC6"],["Ccedil","\xC7"],["Egrave","\xC8"],["Eacute","\xC9"],["Ecirc","\xCA"],["Euml","\xCB"],["Igrave","\xCC"],["Iacute","\xCD"],["Icirc","\xCE"],["Iuml","\xCF"],["ETH","\xD0"],["Ntilde","\xD1"],["Ograve","\xD2"],["Oacute","\xD3"],["Ocirc","\xD4"],["Otilde","\xD5"],["Ouml","\xD6"],["times","\xD7"],["Oslash","\xD8"],["Ugrave","\xD9"],["Uacute","\xDA"],["Ucirc","\xDB"],["Uuml","\xDC"],["Yacute","\xDD"],["THORN","\xDE"],["szlig","\xDF"],["agrave","\xE0"],["aacute","\xE1"],["acirc","\xE2"],["atilde","\xE3"],["auml","\xE4"],["aring","\xE5"],["aelig","\xE6"],["ccedil","\xE7"],["egrave","\xE8"],["eacute","\xE9"],["ecirc","\xEA"],["euml","\xEB"],["igrave","\xEC"],["iacute","\xED"],["icirc","\xEE"],["iuml","\xEF"],["eth","\xF0"],["ntilde","\xF1"],["ograve","\xF2"],["oacute","\xF3"],["ocirc","\xF4"],["otilde","\xF5"],["ouml","\xF6"],["divide","\xF7"],["oslash","\xF8"],["ugrave","\xF9"],["uacute","\xFA"],["ucirc","\xFB"],["uuml","\xFC"],["yacute","\xFD"],["thorn","\xFE"],["yuml","\xFF"],["OElig","\u0152"],["oelig","\u0153"],["Scaron","\u0160"],["scaron","\u0161"],["Yuml","\u0178"],["fnof","\u0192"],["circ","\u02C6"],["tilde","\u02DC"],["Alpha","\u0391"],["Beta","\u0392"],["Gamma","\u0393"],["Delta","\u0394"],["Epsilon","\u0395"],["Zeta","\u0396"],["Eta","\u0397"],["Theta","\u0398"],["Iota","\u0399"],["Kappa","\u039A"],["Lambda","\u039B"],["Mu","\u039C"],["Nu","\u039D"],["Xi","\u039E"],["Omicron","\u039F"],["Pi","\u03A0"],["Rho","\u03A1"],["Sigma","\u03A3"],["Tau","\u03A4"],["Upsilon","\u03A5"],["Phi","\u03A6"],["Chi","\u03A7"],["Psi","\u03A8"],["Omega","\u03A9"],["alpha","\u03B1"],["beta","\u03B2"],["gamma","\u03B3"],["delta","\u03B4"],["epsilon","\u03B5"],["zeta","\u03B6"],["eta","\u03B7"],["theta","\u03B8"],["iota","\u03B9"],["kappa","\u03BA"],["lambda","\u03BB"],["mu","\u03BC"],["nu","\u03BD"],["xi","\u03BE"],["omicron","\u03BF"],["pi","\u03C0"],["rho","\u03C1"],["sigmaf","\u03C2"],["sigma","\u03C3"],["tau","\u03C4"],["upsilon","\u03C5"],["phi","\u03C6"],["chi","\u03C7"],["psi","\u03C8"],["omega","\u03C9"],["thetasym","\u03D1"],["upsih","\u03D2"],["piv","\u03D6"],["ensp","\u2002"],["emsp","\u2003"],["thinsp","\u2009"],["zwnj","\u200C"],["zwj","\u200D"],["lrm","\u200E"],["rlm","\u200F"],["ndash","\u2013"],["mdash","\u2014"],["lsquo","\u2018"],["rsquo","\u2019"],["sbquo","\u201A"],["ldquo","\u201C"],["rdquo","\u201D"],["bdquo","\u201E"],["dagger","\u2020"],["Dagger","\u2021"],["bull","\u2022"],["hellip","\u2026"],["permil","\u2030"],["prime","\u2032"],["Prime","\u2033"],["lsaquo","\u2039"],["rsaquo","\u203A"],["oline","\u203E"],["frasl","\u2044"],["euro","\u20AC"],["image","\u2111"],["weierp","\u2118"],["real","\u211C"],["trade","\u2122"],["alefsym","\u2135"],["larr","\u2190"],["uarr","\u2191"],["rarr","\u2192"],["darr","\u2193"],["harr","\u2194"],["crarr","\u21B5"],["lArr","\u21D0"],["uArr","\u21D1"],["rArr","\u21D2"],["dArr","\u21D3"],["hArr","\u21D4"],["forall","\u2200"],["part","\u2202"],["exist","\u2203"],["empty","\u2205"],["nabla","\u2207"],["isin","\u2208"],["notin","\u2209"],["ni","\u220B"],["prod","\u220F"],["sum","\u2211"],["minus","\u2212"],["lowast","\u2217"],["radic","\u221A"],["prop","\u221D"],["infin","\u221E"],["ang","\u2220"],["and","\u2227"],["or","\u2228"],["cap","\u2229"],["cup","\u222A"],["int","\u222B"],["there4","\u2234"],["sim","\u223C"],["cong","\u2245"],["asymp","\u2248"],["ne","\u2260"],["equiv","\u2261"],["le","\u2264"],["ge","\u2265"],["sub","\u2282"],["sup","\u2283"],["nsub","\u2284"],["sube","\u2286"],["supe","\u2287"],["oplus","\u2295"],["otimes","\u2297"],["perp","\u22A5"],["sdot","\u22C5"],["lceil","\u2308"],["rceil","\u2309"],["lfloor","\u230A"],["rfloor","\u230B"],["lang","\u2329"],["rang","\u232A"],["loz","\u25CA"],["spades","\u2660"],["clubs","\u2663"],["hearts","\u2665"],["diams","\u2666"]]);function Wn(e){let[t,r]=Lc(e.jsxPragma||"React.createElement"),[s,o]=Lc(e.jsxFragmentPragma||"React.Fragment");return{base:t,suffix:r,fragmentBase:s,fragmentSuffix:o}}function Lc(e){let t=e.indexOf(".");return t===-1&&(t=e.length),[e.slice(0,t),e.slice(t)]}var ye=class{getPrefixCode(){return""}getHoistedCode(){return""}getSuffixCode(){return""}};var qn=class e extends ye{__init(){this.lastLineNumber=1}__init2(){this.lastIndex=0}__init3(){this.filenameVarName=null}__init4(){this.esmAutomaticImportNameResolutions={}}__init5(){this.cjsAutomaticModuleNameResolutions={}}constructor(t,r,s,o,a){super(),this.rootTransformer=t,this.tokens=r,this.importProcessor=s,this.nameManager=o,this.options=a,e.prototype.__init.call(this),e.prototype.__init2.call(this),e.prototype.__init3.call(this),e.prototype.__init4.call(this),e.prototype.__init5.call(this),this.jsxPragmaInfo=Wn(a),this.isAutomaticRuntime=a.jsxRuntime==="automatic",this.jsxImportSource=a.jsxImportSource||"react"}process(){return this.tokens.matches1(n.jsxTagStart)?(this.processJSXTag(),!0):!1}getPrefixCode(){let t="";if(this.filenameVarName&&(t+=`const ${this.filenameVarName} = ${JSON.stringify(this.options.filePath||"")};`),this.isAutomaticRuntime)if(this.importProcessor)for(let[r,s]of Object.entries(this.cjsAutomaticModuleNameResolutions))t+=`var ${s} = require("${r}");`;else{let{createElement:r,...s}=this.esmAutomaticImportNameResolutions;r&&(t+=`import {createElement as ${r}} from "${this.jsxImportSource}";`);let o=Object.entries(s).map(([a,c])=>`${a} as ${c}`).join(", ");if(o){let a=this.jsxImportSource+(this.options.production?"/jsx-runtime":"/jsx-dev-runtime");t+=`import {${o}} from "${a}";`}}return t}processJSXTag(){let{jsxRole:t,start:r}=this.tokens.currentToken(),s=this.options.production?null:this.getElementLocationCode(r);this.isAutomaticRuntime&&t!==Ke.KeyAfterPropSpread?this.transformTagToJSXFunc(s,t):this.transformTagToCreateElement(s)}getElementLocationCode(t){return`lineNumber: ${this.getLineNumberForIndex(t)}`}getLineNumberForIndex(t){let r=this.tokens.code;for(;this.lastIndex or > at the end of the tag.");o&&this.tokens.appendCode(`, ${o}`)}for(this.options.production||(o===null&&this.tokens.appendCode(", void 0"),this.tokens.appendCode(`, ${s}, ${this.getDevSource(t)}, this`)),this.tokens.removeInitialToken();!this.tokens.matches1(n.jsxTagEnd);)this.tokens.removeToken();this.tokens.replaceToken(")")}transformTagToCreateElement(t){if(this.tokens.replaceToken(this.getCreateElementInvocationCode()),this.tokens.matches1(n.jsxTagEnd))this.tokens.replaceToken(`${this.getFragmentCode()}, null`),this.processChildren(!0);else if(this.processTagIntro(),this.processPropsObjectWithDevInfo(t),!this.tokens.matches2(n.slash,n.jsxTagEnd))if(this.tokens.matches1(n.jsxTagEnd))this.tokens.removeToken(),this.processChildren(!0);else throw new Error("Expected either /> or > at the end of the tag.");for(this.tokens.removeInitialToken();!this.tokens.matches1(n.jsxTagEnd);)this.tokens.removeToken();this.tokens.replaceToken(")")}getJSXFuncInvocationCode(t){return this.options.production?t?this.claimAutoImportedFuncInvocation("jsxs","/jsx-runtime"):this.claimAutoImportedFuncInvocation("jsx","/jsx-runtime"):this.claimAutoImportedFuncInvocation("jsxDEV","/jsx-dev-runtime")}getCreateElementInvocationCode(){if(this.isAutomaticRuntime)return this.claimAutoImportedFuncInvocation("createElement","");{let{jsxPragmaInfo:t}=this;return`${this.importProcessor&&this.importProcessor.getIdentifierReplacement(t.base)||t.base}${t.suffix}(`}}getFragmentCode(){if(this.isAutomaticRuntime)return this.claimAutoImportedName("Fragment",this.options.production?"/jsx-runtime":"/jsx-dev-runtime");{let{jsxPragmaInfo:t}=this;return(this.importProcessor&&this.importProcessor.getIdentifierReplacement(t.fragmentBase)||t.fragmentBase)+t.fragmentSuffix}}claimAutoImportedFuncInvocation(t,r){let s=this.claimAutoImportedName(t,r);return this.importProcessor?`${s}.call(void 0, `:`${s}(`}claimAutoImportedName(t,r){if(this.importProcessor){let s=this.jsxImportSource+r;return this.cjsAutomaticModuleNameResolutions[s]||(this.cjsAutomaticModuleNameResolutions[s]=this.importProcessor.getFreeIdentifierForPath(s)),`${this.cjsAutomaticModuleNameResolutions[s]}.${t}`}else return this.esmAutomaticImportNameResolutions[t]||(this.esmAutomaticImportNameResolutions[t]=this.nameManager.claimFreeName(`_${t}`)),this.esmAutomaticImportNameResolutions[t]}processTagIntro(){let t=this.tokens.currentIndex()+1;for(;this.tokens.tokens[t].isType||!this.tokens.matches2AtIndex(t-1,n.jsxName,n.jsxName)&&!this.tokens.matches2AtIndex(t-1,n.greaterThan,n.jsxName)&&!this.tokens.matches1AtIndex(t,n.braceL)&&!this.tokens.matches1AtIndex(t,n.jsxTagEnd)&&!this.tokens.matches2AtIndex(t,n.slash,n.jsxTagEnd);)t++;if(t===this.tokens.currentIndex()+1){let r=this.tokens.identifierName();vi(r)&&this.tokens.replaceToken(`'${r}'`)}for(;this.tokens.currentIndex()=y.lowercaseA&&t<=y.lowercaseZ}function op(e){let t="",r="",s=!1,o=!1;for(let a=0;a()=>(t||e((t={exports:{}}).exports,t),t.exports);var sd=(e,t,r,s)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of td(t))!rd.call(e,o)&&o!==r&&Qa(e,o,{get:()=>t[o],enumerable:!(s=ed(t,o))||s.enumerable});return e};var Pr=(e,t,r)=>(r=e!=null?Zf(nd(e)):{},sd(t||!e||!e.__esModule?Qa(r,"default",{value:e,enumerable:!0}):r,e));var Wc=Bn((ki,Ei)=>{(function(e,t){typeof ki=="object"&&typeof Ei<"u"?Ei.exports=t():typeof define=="function"&&define.amd?define(t):(e=typeof globalThis<"u"?globalThis:e||self,e.resolveURI=t())})(ki,(function(){"use strict";let e=/^[\w+.-]+:\/\//,t=/^([\w+.-]+:)\/\/([^@/#?]*@)?([^:/#?]*)(:\d+)?(\/[^#?]*)?(\?[^#]*)?(#.*)?/,r=/^file:(?:\/\/((?![a-z]:)[^/#?]*)?)?(\/?[^#?]*)(\?[^#]*)?(#.*)?/i;function s(k){return e.test(k)}function o(k){return k.startsWith("//")}function a(k){return k.startsWith("/")}function c(k){return k.startsWith("file:")}function l(k){return/^[.?#]/.test(k)}function u(k){let T=t.exec(k);return m(T[1],T[2]||"",T[3],T[4]||"",T[5]||"/",T[6]||"",T[7]||"")}function d(k){let T=r.exec(k),B=T[2];return m("file:","",T[1]||"","",a(B)?B:"/"+B,T[3]||"",T[4]||"")}function m(k,T,B,R,M,$,S){return{scheme:k,user:T,host:B,port:R,path:M,query:$,hash:S,type:7}}function h(k){if(o(k)){let B=u("http:"+k);return B.scheme="",B.type=6,B}if(a(k)){let B=u("http://foo.com"+k);return B.scheme="",B.host="",B.type=5,B}if(c(k))return d(k);if(s(k))return u(k);let T=u("http://foo.com/"+k);return T.scheme="",T.host="",T.type=k?k.startsWith("?")?3:k.startsWith("#")?2:4:1,T}function b(k){if(k.endsWith("/.."))return k;let T=k.lastIndexOf("/");return k.slice(0,T+1)}function w(k,T){x(T,T.type),k.path==="/"?k.path=T.path:k.path=b(T.path)+k.path}function x(k,T){let B=T<=4,R=k.path.split("/"),M=1,$=0,S=!1;for(let y=1;yR&&(R=S)}x(B,R);let M=B.query+B.hash;switch(R){case 2:case 3:return M;case 4:{let $=B.path.slice(1);return $?l(T||k)&&!l($)?"./"+$+M:$+M:M||"."}case 5:return B.path+M;default:return B.scheme+"//"+B.user+B.host+B.port+B.path+M}}return O}))});var ts=Bn(mt=>{"use strict";var Cp=mt&&mt.__extends||(function(){var e=function(t,r){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(s,o){s.__proto__=o}||function(s,o){for(var a in o)o.hasOwnProperty(a)&&(s[a]=o[a])},e(t,r)};return function(t,r){e(t,r);function s(){this.constructor=t}t.prototype=r===null?Object.create(r):(s.prototype=r.prototype,new s)}})();Object.defineProperty(mt,"__esModule",{value:!0});mt.DetailContext=mt.NoopContext=mt.VError=void 0;var Vc=(function(e){Cp(t,e);function t(r,s){var o=e.call(this,s)||this;return o.path=r,Object.setPrototypeOf(o,t.prototype),o}return t})(Error);mt.VError=Vc;var Lp=(function(){function e(){}return e.prototype.fail=function(t,r,s){return!1},e.prototype.unionResolver=function(){return this},e.prototype.createContext=function(){return this},e.prototype.resolveUnion=function(t){},e})();mt.NoopContext=Lp;var Kc=(function(){function e(){this._propNames=[""],this._messages=[null],this._score=0}return e.prototype.fail=function(t,r,s){return this._propNames.push(t),this._messages.push(r),this._score+=s,!1},e.prototype.unionResolver=function(){return new Rp},e.prototype.resolveUnion=function(t){for(var r,s,o=t,a=null,c=0,l=o.contexts;c=a._score)&&(a=u)}a&&a._score>0&&((r=this._propNames).push.apply(r,a._propNames),(s=this._messages).push.apply(s,a._messages))},e.prototype.getError=function(t){for(var r=[],s=this._propNames.length-1;s>=0;s--){var o=this._propNames[s];t+=typeof o=="number"?"["+o+"]":o?"."+o:"";var a=this._messages[s];a&&r.push(t+" "+a)}return new Vc(t,r.join("; "))},e.prototype.getErrorDetail=function(t){for(var r=[],s=this._propNames.length-1;s>=0;s--){var o=this._propNames[s];t+=typeof o=="number"?"["+o+"]":o?"."+o:"";var a=this._messages[s];a&&r.push({path:t,message:a})}for(var c=null,s=r.length-1;s>=0;s--)c&&(r[s].nested=[c]),c=r[s];return c},e})();mt.DetailContext=Kc;var Rp=(function(){function e(){this.contexts=[]}return e.prototype.createContext=function(){var t=new Kc;return this.contexts.push(t),t},e})()});var Ni=Bn(j=>{"use strict";var Ke=j&&j.__extends||(function(){var e=function(t,r){return e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(s,o){s.__proto__=o}||function(s,o){for(var a in o)o.hasOwnProperty(a)&&(s[a]=o[a])},e(t,r)};return function(t,r){e(t,r);function s(){this.constructor=t}t.prototype=r===null?Object.create(r):(s.prototype=r.prototype,new s)}})();Object.defineProperty(j,"__esModule",{value:!0});j.basicTypes=j.BasicType=j.TParamList=j.TParam=j.param=j.TFunc=j.func=j.TProp=j.TOptional=j.opt=j.TIface=j.iface=j.TEnumLiteral=j.enumlit=j.TEnumType=j.enumtype=j.TIntersection=j.intersection=j.TUnion=j.union=j.TTuple=j.tuple=j.TArray=j.array=j.TLiteral=j.lit=j.TName=j.name=j.TType=void 0;var Yc=ts(),Ue=(function(){function e(){}return e})();j.TType=Ue;function jt(e){return typeof e=="string"?Qc(e):e}function Bi(e,t){var r=e[t];if(!r)throw new Error("Unknown type "+t);return r}function Qc(e){return new Ci(e)}j.name=Qc;var Ci=(function(e){Ke(t,e);function t(r){var s=e.call(this)||this;return s.name=r,s._failMsg="is not a "+r,s}return t.prototype.getChecker=function(r,s,o){var a=this,c=Bi(r,this.name),l=c.getChecker(r,s,o);return c instanceof Fe||c instanceof t?l:function(u,d){return l(u,d)?!0:d.fail(null,a._failMsg,0)}},t})(Ue);j.TName=Ci;function Fp(e){return new Li(e)}j.lit=Fp;var Li=(function(e){Ke(t,e);function t(r){var s=e.call(this)||this;return s.value=r,s.name=JSON.stringify(r),s._failMsg="is not "+s.name,s}return t.prototype.getChecker=function(r,s){var o=this;return function(a,c){return a===o.value?!0:c.fail(null,o._failMsg,-1)}},t})(Ue);j.TLiteral=Li;function Np(e){return new Zc(jt(e))}j.array=Np;var Zc=(function(e){Ke(t,e);function t(r){var s=e.call(this)||this;return s.ttype=r,s}return t.prototype.getChecker=function(r,s){var o=this.ttype.getChecker(r,s);return function(a,c){if(!Array.isArray(a))return c.fail(null,"is not an array",0);for(var l=0;l0&&o.push(a+" more"),s._failMsg="is none of "+o.join(", ")):s._failMsg="is none of "+a+" types",s}return t.prototype.getChecker=function(r,s){var o=this,a=this.ttypes.map(function(c){return c.getChecker(r,s)});return function(c,l){for(var u=l.unionResolver(),d=0;d{"use strict";var Xp=ne&&ne.__spreadArrays||function(){for(var e=0,t=0,r=arguments.length;t{"use strict";xr.__esModule=!0;xr.LinesAndColumns=void 0;var As=` +`,$u="\r",zu=(function(){function e(t){this.string=t;for(var r=[0],s=0;sthis.string.length)return null;for(var r=0,s=this.offsets;s[r+1]<=t;)r++;var o=t-s[r];return{line:r,column:o}},e.prototype.indexForLocation=function(t){var r=t.line,s=t.column;return r<0||r>=this.offsets.length||s<0||s>this.lengthOfLine(r)?null:this.offsets[r]+s},e.prototype.lengthOfLine=function(t){var r=this.offsets[t],s=t===this.offsets.length-1?this.string.length:this.offsets[t+1];return s-r},e})();xr.LinesAndColumns=zu;xr.default=zu});function od(e){return e instanceof Uint8Array||ArrayBuffer.isView(e)&&e.constructor.name==="Uint8Array"&&"BYTES_PER_ELEMENT"in e&&e.BYTES_PER_ELEMENT===1}function id(e){if(typeof e!="boolean")throw new TypeError(`boolean expected, not ${e}`)}function Ro(e){if(typeof e!="number")throw new TypeError("number expected, got "+typeof e);if(!Number.isSafeInteger(e)||e<0)throw new RangeError("positive integer expected, got "+e)}function we(e,t,r=""){let s=od(e),o=e?.length,a=t!==void 0;if(!s||a&&o!==t){let c=r&&`"${r}" `,l=a?` of length ${t}`:"",u=s?`length=${o}`:`type=${typeof e}`,d=c+"expected Uint8Array"+l+", got "+u;throw s?new RangeError(d):new TypeError(d)}return e}function Fo(e,t=!0){if(e.destroyed)throw new Error("Hash instance has been destroyed");if(t&&e.finished)throw new Error("Hash#digest() has already been called")}function No(e,t,r=!1){we(e,void 0,"output");let s=t.outputLen;if(e.lengthe<<24&4278190080|e<<8&16711680|e>>>8&65280|e>>>24&255,se=Bt?e=>e:e=>Mo(e)>>>0,ad=e=>{for(let t=0;te:ad;function Za(e,t){return!e.byteLength||!t.byteLength?!1:e.buffer===t.buffer&&e.byteOffset[]),a=(l,u)=>s(u,...o(l)).update(l).digest(),c=s(new Uint8Array(e),...o(new Uint8Array(0)));return a.outputLen=c.outputLen,a.blockLen=c.blockLen,a.create=(l,...u)=>s(l,...u),a}var Tr=(e,t)=>{function r(s,...o){if(we(s,void 0,"key"),e.nonceLength!==void 0){let m=o[0];we(m,e.varSizeNonce?void 0:e.nonceLength,"nonce")}let a=e.tagLength;a&&o[1]!==void 0&&we(o[1],void 0,"AAD");let c=t(s,...o),l=(m,h)=>{if(h!==void 0){if(m!==2)throw new Error("cipher output not supported");we(h,void 0,"output")}},u=!1;return{encrypt(m,h){if(u)throw new Error("cannot encrypt() twice with same key + nonce");return u=!0,we(m),l(c.encrypt.length,h),c.encrypt(m,h)},decrypt(m,h){if(we(m),a&&m.length{let o=s&1;return{s3:r<<31|s>>>1,s2:t<<31|r>>>1,s1:e<<31|t>>>1,s0:e>>>1^cd<<24&-(o&1)}},Ir=e=>(e>>>0&255)<<24|(e>>>8&255)<<16|(e>>>16&255)<<8|e>>>24&255|0;var ud=e=>e>64*1024?8:e>1024?4:2,Wo=class{blockLen=sn;outputLen=sn;s0=0;s1=0;s2=0;s3=0;finished=!1;destroyed=!1;t;W;windowSize;constructor(t,r){we(t,16,"key"),t=$e(t);let s=rn(t),o=s.getUint32(0,!1),a=s.getUint32(4,!1),c=s.getUint32(8,!1),l=s.getUint32(12,!1),u=[];for(let x=0;x<128;x++)u.push({s0:Ir(o),s1:Ir(a),s2:Ir(c),s3:Ir(l)}),{s0:o,s1:a,s2:c,s3:l}=ld(o,a,c,l);let d=ud(r||1024);if(![1,2,4,8].includes(d))throw new Error("ghash: invalid window size, expected 2, 4 or 8");this.W=d;let h=128/d,b=this.windowSize=2**d,w=[];for(let x=0;x>>d-M-1&1))continue;let{s0:S,s1:I,s2:y,s3:E}=u[d*x+M];k^=S,T^=I,B^=y,R^=E}w.push({s0:k,s1:T,s2:B,s3:R})}this.t=w}_updateBlock(t,r,s,o){t^=this.s0,r^=this.s1,s^=this.s2,o^=this.s3;let{W:a,t:c,windowSize:l}=this,u=0,d=0,m=0,h=0,b=(1<>>8*O&255;for(let T=8/a-1;T>=0;T--){let B=k>>>a*T&b,{s0:R,s1:M,s2:$,s3:S}=c[w*l+B];u^=R,d^=M,m^=$,h^=S,w+=1}}this.s0=u,this.s1=d,this.s2=m,this.s3=h}update(t){Fo(this),we(t),t=$e(t);let r=Ee(t),s=Math.floor(t.length/sn),o=t.length%sn;for(let a=0;anew Wo(e,t),e=>[e.length]);var ze=16,rc=4,Or=new Uint8Array(ze);var fd=283;function dd(e){if(![16,24,32].includes(e.length))throw new Error('"aes key" expected Uint8Array of length 16/24/32, got length='+e.length)}function $o(e){return e<<1^fd&-(e>>7)}function on(e,t){let r=0;for(;t>0;t>>=1)r^=e&-(t&1),e=$o(e);return r}var pd=(e,t,r=1)=>{if(!Number.isSafeInteger(r)||r>4294967040)throw new Error("incBytes: wrong carry "+r);we(e);for(let s=0;s>>=8}},Ho=(()=>{let e=new Uint8Array(256);for(let r=0,s=1;r<256;r++,s^=$o(s))e[r]=s;let t=new Uint8Array(256);t[0]=99;for(let r=0;r<255;r++){let s=e[255-r];s|=s<<8,t[e[r]]=(s^s>>4^s>>5^s>>6^s>>7^99)&255}return Ne(e),t})(),hd=Ho.map((e,t)=>Ho.indexOf(t)),md=e=>e<<24|e>>>8,qo=e=>e<<8|e>>>24;function sc(e,t){if(e.length!==256)throw new Error("Wrong sbox length");let r=new Uint32Array(256).map((d,m)=>t(e[m])),s=r.map(qo),o=s.map(qo),a=o.map(qo),c=new Uint32Array(256*256),l=new Uint32Array(256*256),u=new Uint16Array(256*256);for(let d=0;d<256;d++)for(let m=0;m<256;m++){let h=d*256+m;c[h]=r[d]^s[m],l[h]=o[d]^a[m],u[h]=e[d]<<8|e[m]}return{sbox:e,sbox2:u,T0:r,T1:s,T2:o,T3:a,T01:c,T23:l}}var zo=sc(Ho,e=>on(e,3)<<24|e<<16|e<<8|on(e,2)),oc=sc(hd,e=>on(e,11)<<24|on(e,13)<<16|on(e,9)<<8|on(e,14)),gd=(()=>{let e=new Uint8Array(16);for(let t=0,r=1;t<16;t++,r=$o(r))e[t]=r;return e})();function Cr(e){we(e);let t=e.length;dd(e);let{sbox2:r}=zo,s=[];(!Bt||!et(e))&&s.push(e=$e(e));let o=He(Ee(e)),a=o.length,c=u=>ft(r,u,u,u,u),l=new Uint32Array(t+28);l.set(o);for(let u=a;u6&&u%a===4&&(d=c(d)),l[u]=l[u-a]^d}return Ne(...s),l}function yd(e){let t=Cr(e),r=t.slice(),s=t.length,{sbox2:o}=zo,{T0:a,T1:c,T2:l,T3:u}=oc;for(let d=0;d>>8&255]^l[h>>>16&255]^u[h>>>24]}return r}function Ct(e,t,r,s,o,a){return e[r<<8&65280|s>>>8&255]^t[o>>>8&65280|a>>>24&255]}function ft(e,t,r,s,o){return e[t&255|r&65280]|e[s>>>16&255|o>>>16&65280]<<16}function an(e,t,r,s,o){let{sbox2:a,T01:c,T23:l}=zo,u=0;t^=e[u++],r^=e[u++],s^=e[u++],o^=e[u++];let d=e.length/4-2;for(let x=0;x>>0,l.setUint32(m,b,t),{s0:w,s1:x,s2:O,s3:k}=an(e,se(c[0]),se(c[1]),se(c[2]),se(c[3]));let T=ze*Math.floor(u.length/rc);if(Ts(o,a),decrypt:(o,a)=>s(o,a)}});function wd(e){if(we(e),e.length%ze!==0)throw new Error("aes-(cbc/ecb).decrypt ciphertext should consist of blocks with size "+ze)}function xd(e,t,r){we(e);let s=e.length,o=s%ze;if(!t&&o!==0)throw new Error("aec/(cbc-ecb): unpadded plaintext with disabled padding");if(t){let l=ze-o;l||(l=ze),s=s+l}r=Cn(s,r),Ar(e,r),(!Bt||!et(e))&&(e=$e(e));let a=Ee(e);He(a);let c=Ee(r);return{b:a,o:c,out:r}}function vd(e,t){if(!t)return e;let r=e.length;if(r===0)throw new Error("aes/pkcs7: empty ciphertext not allowed");let s=e[r-1],o=1;o&=s-1>>>31^1,o&=16-s>>>31^1;for(let a=0;a<16;a++){let c=a-s>>>31,l=(e[r-1-a]^s)===0?1:0;o&=l|c^1}if(!o)throw new Error("aes/pkcs7: wrong padding");return e.subarray(0,r-s)}function Sd(e){let t=new Uint8Array(16),r=Ee(t);t.set(e);let s=ze-e.length;for(let o=ze-s;o= 0, got ${e}`)}}function cn(e,t,r=""){let s=Ed(e),o=e?.length,a=t!==void 0;if(!s||a&&o!==t){let c=r&&`"${r}" `,l=a?` of length ${t}`:"",u=s?`length=${o}`:`type=${typeof e}`,d=c+"expected Uint8Array"+l+", got "+u;throw s?new RangeError(d):new TypeError(d)}return e}function Rr(e){if(typeof e!="function"||typeof e.create!="function")throw new TypeError("Hash must wrapped by utils.createHasher");if(Ge(e.outputLen),Ge(e.blockLen),e.outputLen<1)throw new Error('"outputLen" must be >= 1');if(e.blockLen<1)throw new Error('"blockLen" must be >= 1')}function ln(e,t=!0){if(e.destroyed)throw new Error("Hash instance has been destroyed");if(t&&e.finished)throw new Error("Hash#digest() has already been called")}function Fr(e,t){cn(e,void 0,"digestInto() output");let r=t.outputLen;if(e.length='+r)}function Nr(e){return new Uint32Array(e.buffer,e.byteOffset,Math.floor(e.byteLength/4))}function Ae(...e){for(let t=0;t>>t}function ee(e,t){return e<>>32-t>>>0}var Pd=new Uint8Array(new Uint32Array([287454020]).buffer)[0]===68;function Ad(e){return e<<24&4278190080|e<<8&16711680|e>>>8&65280|e>>>24&255}function Td(e){for(let t=0;te:Td;function Id(e){if(typeof e!="string")throw new TypeError("string expected");return new Uint8Array(new TextEncoder().encode(e))}function Jo(e,t=""){return typeof e=="string"?Id(e):cn(e,void 0,t)}function Dr(e,t){if(t!==void 0&&{}.toString.call(t)!=="[object Object]")throw new TypeError("options must be object or undefined");return Object.assign(e,t)}function Lt(e,t={}){let r=(o,a)=>e(a).update(o).digest(),s=e(void 0);return r.outputLen=s.outputLen,r.blockLen=s.blockLen,r.canXOF=s.canXOF,r.create=o=>e(o),Object.assign(r,t),Object.freeze(r)}var Rn=e=>({oid:Uint8Array.from([6,9,96,134,72,1,101,3,4,2,e])});var Mr=class{oHash;iHash;blockLen;outputLen;canXOF=!1;finished=!1;destroyed=!1;constructor(t,r){if(Rr(t),cn(r,void 0,"key"),this.iHash=t.create(),typeof this.iHash.update!="function")throw new Error("Expected instance of class which extends utils.Hash");this.blockLen=this.iHash.blockLen,this.outputLen=this.iHash.outputLen;let s=this.blockLen,o=new Uint8Array(s);o.set(r.length>s?t.create().update(r).digest():r);for(let a=0;a{let e=((t,r,s)=>new Mr(t,r).update(s).digest());return e.create=(t,r)=>new Mr(t,r),e})();function fn(e,t,r){return e&t^~e&r}function Wr(e,t,r){return e&t^e&r^t&r}var Rt=class{blockLen;outputLen;canXOF=!1;padOffset;isLE;buffer;view;finished=!1;length=0;pos=0;destroyed=!1;constructor(t,r,s,o){this.blockLen=t,this.outputLen=r,this.padOffset=s,this.isLE=o,this.buffer=new Uint8Array(t),this.view=un(this.buffer)}update(t){ln(this),cn(t);let{view:r,buffer:s,blockLen:o}=this,a=t.length;for(let c=0;co-c&&(this.process(s,0),c=0);for(let h=c;hm.length)throw new Error("_sha2: outputLen bigger than state");for(let h=0;hnew Yo),Od=Math.pow(2,32),Bd=Array.from({length:64},(e,t)=>Math.floor(Od*Math.abs(Math.sin(t+1)))),Ur=dn.slice(0,4),Xo=new Uint32Array(16),Qo=class extends Rt{A=Ur[0]|0;B=Ur[1]|0;C=Ur[2]|0;D=Ur[3]|0;constructor(){super(64,16,8,!0)}get(){let{A:t,B:r,C:s,D:o}=this;return[t,r,s,o]}set(t,r,s,o){this.A=t|0,this.B=r|0,this.C=s|0,this.D=o|0}process(t,r){for(let l=0;l<16;l++,r+=4)Xo[l]=t.getUint32(r,!0);let{A:s,B:o,C:a,D:c}=this;for(let l=0;l<64;l++){let u,d,m;l<16?(u=fn(o,a,c),d=l,m=[7,12,17,22]):l<32?(u=fn(c,o,a),d=(5*l+1)%16,m=[5,9,14,20]):l<48?(u=o^a^c,d=(3*l+5)%16,m=[4,11,16,23]):(u=a^(o|~c),d=7*l%16,m=[6,10,15,21]),u=u+s+Bd[l]+Xo[d],s=c,c=a,a=o,o=o+ee(u,m[l%4])}s=s+this.A|0,o=o+this.B|0,a=a+this.C|0,c=c+this.D|0,this.set(s,o,a,c)}roundClean(){Ae(Xo)}destroy(){this.destroyed=!0,this.set(0,0,0,0),Ae(this.buffer)}},ac=Lt(()=>new Qo);function Ld(e,t,r,s){Rr(e);let o=Dr({dkLen:32,asyncTick:10},s),{c:a,dkLen:c,asyncTick:l}=o;if(Ge(a,"c"),Ge(c,"dkLen"),Ge(l,"asyncTick"),a<1)throw new Error("iterations (c) must be >= 1");if(c<1)throw new Error('"dkLen" must be >= 1');if(c>(2**32-1)*e.outputLen)throw new Error("derived key too long");let u=Jo(t,"password"),d=Jo(r,"salt"),m=new Uint8Array(c),h=jr.create(e,u),b=h._cloneInto().update(d);return{c:a,dkLen:c,asyncTick:l,DK:m,PRF:h,PRFSalt:b}}function Rd(e,t,r,s,o){return e.destroy(),t.destroy(),s&&s.destroy(),Ae(o),r}function Fn(e,t,r,s){let{c:o,dkLen:a,DK:c,PRF:l,PRFSalt:u}=Ld(e,t,r,s),d,m=new Uint8Array(4),h=un(m),b=new Uint8Array(l.outputLen);for(let w=1,x=0;x>cc&qr)}:{h:Number(e>>cc&qr)|0,l:Number(e&qr)|0}}function lc(e,t=!1){let r=e.length,s=new Uint32Array(r),o=new Uint32Array(r);for(let a=0;ae>>>r,ei=(e,t,r)=>e<<32-r|t>>>r,Gt=(e,t,r)=>e>>>r|t<<32-r,Vt=(e,t,r)=>e<<32-r|t>>>r,Nn=(e,t,r)=>e<<64-r|t>>>r-32,Dn=(e,t,r)=>e>>>r-32|t<<64-r;function dt(e,t,r,s){let o=(t>>>0)+(s>>>0);return{h:e+r+(o/2**32|0)|0,l:o|0}}var uc=(e,t,r)=>(e>>>0)+(t>>>0)+(r>>>0),fc=(e,t,r,s)=>t+r+s+(e/2**32|0)|0,dc=(e,t,r,s)=>(e>>>0)+(t>>>0)+(r>>>0)+(s>>>0),pc=(e,t,r,s,o)=>t+r+s+o+(e/2**32|0)|0,hc=(e,t,r,s,o)=>(e>>>0)+(t>>>0)+(r>>>0)+(s>>>0)+(o>>>0),mc=(e,t,r,s,o,a)=>t+r+s+o+a+(e/2**32|0)|0;var Dd=Uint32Array.from([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),Nt=new Uint32Array(64),Hr=class extends Rt{constructor(t){super(64,t,8,!1)}get(){let{A:t,B:r,C:s,D:o,E:a,F:c,G:l,H:u}=this;return[t,r,s,o,a,c,l,u]}set(t,r,s,o,a,c,l,u){this.A=t|0,this.B=r|0,this.C=s|0,this.D=o|0,this.E=a|0,this.F=c|0,this.G=l|0,this.H=u|0}process(t,r){for(let h=0;h<16;h++,r+=4)Nt[h]=t.getUint32(r,!1);for(let h=16;h<64;h++){let b=Nt[h-15],w=Nt[h-2],x=it(b,7)^it(b,18)^b>>>3,O=it(w,17)^it(w,19)^w>>>10;Nt[h]=O+Nt[h-7]+x+Nt[h-16]|0}let{A:s,B:o,C:a,D:c,E:l,F:u,G:d,H:m}=this;for(let h=0;h<64;h++){let b=it(l,6)^it(l,11)^it(l,25),w=m+b+fn(l,u,d)+Dd[h]+Nt[h]|0,O=(it(s,2)^it(s,13)^it(s,22))+Wr(s,o,a)|0;m=d,d=u,u=l,l=c+w|0,c=a,a=o,o=s,s=w+O|0}s=s+this.A|0,o=o+this.B|0,a=a+this.C|0,c=c+this.D|0,l=l+this.E|0,u=u+this.F|0,d=d+this.G|0,m=m+this.H|0,this.set(s,o,a,c,l,u,d,m)}roundClean(){Ae(Nt)}destroy(){this.destroyed=!0,this.set(0,0,0,0,0,0,0,0),Ae(this.buffer)}},ti=class extends Hr{A=_t[0]|0;B=_t[1]|0;C=_t[2]|0;D=_t[3]|0;E=_t[4]|0;F=_t[5]|0;G=_t[6]|0;H=_t[7]|0;constructor(){super(32)}},ni=class extends Hr{A=wt[0]|0;B=wt[1]|0;C=wt[2]|0;D=wt[3]|0;E=wt[4]|0;F=wt[5]|0;G=wt[6]|0;H=wt[7]|0;constructor(){super(28)}},gc=lc(["0x428a2f98d728ae22","0x7137449123ef65cd","0xb5c0fbcfec4d3b2f","0xe9b5dba58189dbbc","0x3956c25bf348b538","0x59f111f1b605d019","0x923f82a4af194f9b","0xab1c5ed5da6d8118","0xd807aa98a3030242","0x12835b0145706fbe","0x243185be4ee4b28c","0x550c7dc3d5ffb4e2","0x72be5d74f27b896f","0x80deb1fe3b1696b1","0x9bdc06a725c71235","0xc19bf174cf692694","0xe49b69c19ef14ad2","0xefbe4786384f25e3","0x0fc19dc68b8cd5b5","0x240ca1cc77ac9c65","0x2de92c6f592b0275","0x4a7484aa6ea6e483","0x5cb0a9dcbd41fbd4","0x76f988da831153b5","0x983e5152ee66dfab","0xa831c66d2db43210","0xb00327c898fb213f","0xbf597fc7beef0ee4","0xc6e00bf33da88fc2","0xd5a79147930aa725","0x06ca6351e003826f","0x142929670a0e6e70","0x27b70a8546d22ffc","0x2e1b21385c26c926","0x4d2c6dfc5ac42aed","0x53380d139d95b3df","0x650a73548baf63de","0x766a0abb3c77b2a8","0x81c2c92e47edaee6","0x92722c851482353b","0xa2bfe8a14cf10364","0xa81a664bbc423001","0xc24b8b70d0f89791","0xc76c51a30654be30","0xd192e819d6ef5218","0xd69906245565a910","0xf40e35855771202a","0x106aa07032bbd1b8","0x19a4c116b8d2d0c8","0x1e376c085141ab53","0x2748774cdf8eeb99","0x34b0bcb5e19b48a8","0x391c0cb3c5c95a63","0x4ed8aa4ae3418acb","0x5b9cca4f7763e373","0x682e6ff3d6b2b8a3","0x748f82ee5defb2fc","0x78a5636f43172f60","0x84c87814a1f0ab72","0x8cc702081a6439ec","0x90befffa23631e28","0xa4506cebde82bde9","0xbef9a3f7b2c67915","0xc67178f2e372532b","0xca273eceea26619c","0xd186b8c721c0c207","0xeada7dd6cde0eb1e","0xf57d4f7fee6ed178","0x06f067aa72176fba","0x0a637dc5a2c898a6","0x113f9804bef90dae","0x1b710b35131c471b","0x28db77f523047d84","0x32caab7b40c72493","0x3c9ebe0a15c9bebc","0x431d67c49c100d4c","0x4cc5d4becb3e42b6","0x597f299cfc657e2a","0x5fcb6fab3ad6faec","0x6c44198c4a475817"].map(e=>BigInt(e))),Md=gc[0],jd=gc[1],Dt=new Uint32Array(80),Mt=new Uint32Array(80),$r=class extends Rt{constructor(t){super(128,t,16,!1)}get(){let{Ah:t,Al:r,Bh:s,Bl:o,Ch:a,Cl:c,Dh:l,Dl:u,Eh:d,El:m,Fh:h,Fl:b,Gh:w,Gl:x,Hh:O,Hl:k}=this;return[t,r,s,o,a,c,l,u,d,m,h,b,w,x,O,k]}set(t,r,s,o,a,c,l,u,d,m,h,b,w,x,O,k){this.Ah=t|0,this.Al=r|0,this.Bh=s|0,this.Bl=o|0,this.Ch=a|0,this.Cl=c|0,this.Dh=l|0,this.Dl=u|0,this.Eh=d|0,this.El=m|0,this.Fh=h|0,this.Fl=b|0,this.Gh=w|0,this.Gl=x|0,this.Hh=O|0,this.Hl=k|0}process(t,r){for(let R=0;R<16;R++,r+=4)Dt[R]=t.getUint32(r),Mt[R]=t.getUint32(r+=4);for(let R=16;R<80;R++){let M=Dt[R-15]|0,$=Mt[R-15]|0,S=Gt(M,$,1)^Gt(M,$,8)^Zo(M,$,7),I=Vt(M,$,1)^Vt(M,$,8)^ei(M,$,7),y=Dt[R-2]|0,E=Mt[R-2]|0,F=Gt(y,E,19)^Nn(y,E,61)^Zo(y,E,6),U=Vt(y,E,19)^Dn(y,E,61)^ei(y,E,6),X=dc(I,U,Mt[R-7],Mt[R-16]),re=pc(X,S,F,Dt[R-7],Dt[R-16]);Dt[R]=re|0,Mt[R]=X|0}let{Ah:s,Al:o,Bh:a,Bl:c,Ch:l,Cl:u,Dh:d,Dl:m,Eh:h,El:b,Fh:w,Fl:x,Gh:O,Gl:k,Hh:T,Hl:B}=this;for(let R=0;R<80;R++){let M=Gt(h,b,14)^Gt(h,b,18)^Nn(h,b,41),$=Vt(h,b,14)^Vt(h,b,18)^Dn(h,b,41),S=h&w^~h&O,I=b&x^~b&k,y=hc(B,$,I,jd[R],Mt[R]),E=mc(y,T,M,S,Md[R],Dt[R]),F=y|0,U=Gt(s,o,28)^Nn(s,o,34)^Nn(s,o,39),X=Vt(s,o,28)^Dn(s,o,34)^Dn(s,o,39),re=s&a^s&l^a&l,oe=o&c^o&u^c&u;T=O|0,B=k|0,O=w|0,k=x|0,w=h|0,x=b|0,{h,l:b}=dt(d|0,m|0,E|0,F|0),d=l|0,m=u|0,l=a|0,u=c|0,a=s|0,c=o|0;let Q=uc(F,X,oe);s=fc(Q,E,U,re),o=Q|0}({h:s,l:o}=dt(this.Ah|0,this.Al|0,s|0,o|0)),{h:a,l:c}=dt(this.Bh|0,this.Bl|0,a|0,c|0),{h:l,l:u}=dt(this.Ch|0,this.Cl|0,l|0,u|0),{h:d,l:m}=dt(this.Dh|0,this.Dl|0,d|0,m|0),{h,l:b}=dt(this.Eh|0,this.El|0,h|0,b|0),{h:w,l:x}=dt(this.Fh|0,this.Fl|0,w|0,x|0),{h:O,l:k}=dt(this.Gh|0,this.Gl|0,O|0,k|0),{h:T,l:B}=dt(this.Hh|0,this.Hl|0,T|0,B|0),this.set(s,o,a,c,l,u,d,m,h,b,w,x,O,k,T,B)}roundClean(){Ae(Dt,Mt)}destroy(){this.destroyed=!0,Ae(this.buffer),this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)}},ri=class extends $r{Ah=Le[0]|0;Al=Le[1]|0;Bh=Le[2]|0;Bl=Le[3]|0;Ch=Le[4]|0;Cl=Le[5]|0;Dh=Le[6]|0;Dl=Le[7]|0;Eh=Le[8]|0;El=Le[9]|0;Fh=Le[10]|0;Fl=Le[11]|0;Gh=Le[12]|0;Gl=Le[13]|0;Hh=Le[14]|0;Hl=Le[15]|0;constructor(){super(64)}},si=class extends $r{Ah=Ce[0]|0;Al=Ce[1]|0;Bh=Ce[2]|0;Bl=Ce[3]|0;Ch=Ce[4]|0;Cl=Ce[5]|0;Dh=Ce[6]|0;Dl=Ce[7]|0;Eh=Ce[8]|0;El=Ce[9]|0;Fh=Ce[10]|0;Fl=Ce[11]|0;Gh=Ce[12]|0;Gl=Ce[13]|0;Hh=Ce[14]|0;Hl=Ce[15]|0;constructor(){super(48)}};var Mn=Lt(()=>new ti,Rn(1)),yc=Lt(()=>new ni,Rn(4)),bc=Lt(()=>new ri,Rn(3)),_c=Lt(()=>new si,Rn(2));function wc(e,t,r,s,o,a){let c=e[t++]^r[s++],l=e[t++]^r[s++],u=e[t++]^r[s++],d=e[t++]^r[s++],m=e[t++]^r[s++],h=e[t++]^r[s++],b=e[t++]^r[s++],w=e[t++]^r[s++],x=e[t++]^r[s++],O=e[t++]^r[s++],k=e[t++]^r[s++],T=e[t++]^r[s++],B=e[t++]^r[s++],R=e[t++]^r[s++],M=e[t++]^r[s++],$=e[t++]^r[s++],S=c,I=l,y=u,E=d,F=m,U=h,X=b,re=w,oe=x,Q=O,pe=k,he=T,ke=B,Oe=R,Pe=M,Be=$;for(let Qe=0;Qe<8;Qe+=2)F^=ee(S+ke|0,7),oe^=ee(F+S|0,9),ke^=ee(oe+F|0,13),S^=ee(ke+oe|0,18),Q^=ee(U+I|0,7),Oe^=ee(Q+U|0,9),I^=ee(Oe+Q|0,13),U^=ee(I+Oe|0,18),Pe^=ee(pe+X|0,7),y^=ee(Pe+pe|0,9),X^=ee(y+Pe|0,13),pe^=ee(X+y|0,18),E^=ee(Be+he|0,7),re^=ee(E+Be|0,9),he^=ee(re+E|0,13),Be^=ee(he+re|0,18),I^=ee(S+E|0,7),y^=ee(I+S|0,9),E^=ee(y+I|0,13),S^=ee(E+y|0,18),X^=ee(U+F|0,7),re^=ee(X+U|0,9),F^=ee(re+X|0,13),U^=ee(F+re|0,18),he^=ee(pe+Q|0,7),oe^=ee(he+pe|0,9),Q^=ee(oe+he|0,13),pe^=ee(Q+oe|0,18),ke^=ee(Be+Pe|0,7),Oe^=ee(ke+Be|0,9),Pe^=ee(Oe+ke|0,13),Be^=ee(Pe+Oe|0,18);o[a++]=c+S|0,o[a++]=l+I|0,o[a++]=u+y|0,o[a++]=d+E|0,o[a++]=m+F|0,o[a++]=h+U|0,o[a++]=b+X|0,o[a++]=w+re|0,o[a++]=x+oe|0,o[a++]=O+Q|0,o[a++]=k+pe|0,o[a++]=T+he|0,o[a++]=B+ke|0,o[a++]=R+Oe|0,o[a++]=M+Pe|0,o[a++]=$+Be|0}function oi(e,t,r,s,o){let a=s+0,c=s+16*o;for(let l=0;l<16;l++)r[c+l]=e[t+(2*o-1)*16+l];for(let l=0;l0&&(c+=16),wc(r,a,e,t+=16,r,c)}function Wd(e,t,r){let s=Dr({dkLen:32,asyncTick:10,maxmem:1073742848},r),{N:o,r:a,p:c,dkLen:l,asyncTick:u,maxmem:d,onProgress:m}=s;if(Ge(o,"N"),Ge(a,"r"),Ge(c,"p"),Ge(l,"dkLen"),Ge(u,"asyncTick"),Ge(d,"maxmem"),m!==void 0&&typeof m!="function")throw new Error("progressCb must be a function");let h=128*a,b=h/4,w=Math.pow(2,32);if(o<=1||(o&o-1)!==0||o>w)throw new Error('"N" expected a power of 2, and 2^1 <= N <= 2^32');if(c<1||c>(w-1)*32/h)throw new Error('"p" expected integer 1..((2^32 - 1) * 32) / (128 * r)');if(l<1||l>(w-1)*32)throw new Error('"dkLen" expected integer 1..(2^32 - 1) * 32');let x=h*(o+c+1);if(x>d)throw new Error('"maxmem" limit was hit: memUsed(128*r*(N+p+1))='+x+", maxmem="+d);let O=Fn(Mn,e,t,{c:1,dkLen:h*c}),k=Nr(O),T=Nr(new Uint8Array(h*o)),B=Nr(new Uint8Array(h)),R=()=>{};if(m){let M=2*o*c,$=Math.max(Math.floor(M/1e4),1),S=0;R=()=>{S++,m&&(!(S%$)||S===M)&&m(S/M)}}return{N:o,r:a,p:c,dkLen:l,blockSize32:b,V:T,B32:k,B:O,tmp:B,blockMixCb:R,asyncTick:u}}function Ud(e,t,r,s,o){let a=Fn(Mn,e,r,{c:1,dkLen:t});return Ae(r,s,o),a}function xc(e,t,r){let{N:s,r:o,p:a,dkLen:c,blockSize32:l,V:u,B32:d,B:m,tmp:h,blockMixCb:b}=Wd(e,t,r);Ko(d);for(let w=0;w>>0;for(let T=0;T";case n.template:return"template";case n.ellipsis:return"...";case n.backQuote:return"`";case n.dollarBraceL:return"${";case n.at:return"@";case n.hash:return"#";case n.eq:return"=";case n.assign:return"_=";case n.preIncDec:return"++/--";case n.postIncDec:return"++/--";case n.bang:return"!";case n.tilde:return"~";case n.pipeline:return"|>";case n.nullishCoalescing:return"??";case n.logicalOR:return"||";case n.logicalAND:return"&&";case n.bitwiseOR:return"|";case n.bitwiseXOR:return"^";case n.bitwiseAND:return"&";case n.equality:return"==/!=";case n.lessThan:return"<";case n.greaterThan:return">";case n.relationalOrEqual:return"<=/>=";case n.bitShiftL:return"<<";case n.bitShiftR:return">>/>>>";case n.plus:return"+";case n.minus:return"-";case n.modulo:return"%";case n.star:return"*";case n.slash:return"/";case n.exponent:return"**";case n.jsxName:return"jsxName";case n.jsxText:return"jsxText";case n.jsxEmptyText:return"jsxEmptyText";case n.jsxTagStart:return"jsxTagStart";case n.jsxTagEnd:return"jsxTagEnd";case n.typeParameterStart:return"typeParameterStart";case n.nonNullAssertion:return"nonNullAssertion";case n._break:return"break";case n._case:return"case";case n._catch:return"catch";case n._continue:return"continue";case n._debugger:return"debugger";case n._default:return"default";case n._do:return"do";case n._else:return"else";case n._finally:return"finally";case n._for:return"for";case n._function:return"function";case n._if:return"if";case n._return:return"return";case n._switch:return"switch";case n._throw:return"throw";case n._try:return"try";case n._var:return"var";case n._let:return"let";case n._const:return"const";case n._while:return"while";case n._with:return"with";case n._new:return"new";case n._this:return"this";case n._super:return"super";case n._class:return"class";case n._extends:return"extends";case n._export:return"export";case n._import:return"import";case n._yield:return"yield";case n._null:return"null";case n._true:return"true";case n._false:return"false";case n._in:return"in";case n._instanceof:return"instanceof";case n._typeof:return"typeof";case n._void:return"void";case n._delete:return"delete";case n._async:return"async";case n._get:return"get";case n._set:return"set";case n._declare:return"declare";case n._readonly:return"readonly";case n._abstract:return"abstract";case n._static:return"static";case n._public:return"public";case n._private:return"private";case n._protected:return"protected";case n._override:return"override";case n._as:return"as";case n._enum:return"enum";case n._type:return"type";case n._implements:return"implements";default:return""}}var We=class{constructor(t,r,s){this.startTokenIndex=t,this.endTokenIndex=r,this.isFunctionScope=s}},ai=class{constructor(t,r,s,o,a,c,l,u,d,m,h,b,w){this.potentialArrowAt=t,this.noAnonFunctionType=r,this.inDisallowConditionalTypesContext=s,this.tokensLength=o,this.scopesLength=a,this.pos=c,this.type=l,this.contextualKeyword=u,this.start=d,this.end=m,this.isType=h,this.scopeDepth=b,this.error=w}},jn=class e{constructor(){e.prototype.__init.call(this),e.prototype.__init2.call(this),e.prototype.__init3.call(this),e.prototype.__init4.call(this),e.prototype.__init5.call(this),e.prototype.__init6.call(this),e.prototype.__init7.call(this),e.prototype.__init8.call(this),e.prototype.__init9.call(this),e.prototype.__init10.call(this),e.prototype.__init11.call(this),e.prototype.__init12.call(this),e.prototype.__init13.call(this)}__init(){this.potentialArrowAt=-1}__init2(){this.noAnonFunctionType=!1}__init3(){this.inDisallowConditionalTypesContext=!1}__init4(){this.tokens=[]}__init5(){this.scopes=[]}__init6(){this.pos=0}__init7(){this.type=n.eof}__init8(){this.contextualKeyword=p.NONE}__init9(){this.start=0}__init10(){this.end=0}__init11(){this.isType=!1}__init12(){this.scopeDepth=0}__init13(){this.error=null}snapshot(){return new ai(this.potentialArrowAt,this.noAnonFunctionType,this.inDisallowConditionalTypesContext,this.tokens.length,this.scopes.length,this.pos,this.type,this.contextualKeyword,this.start,this.end,this.isType,this.scopeDepth,this.error)}restoreFromSnapshot(t){this.potentialArrowAt=t.potentialArrowAt,this.noAnonFunctionType=t.noAnonFunctionType,this.inDisallowConditionalTypesContext=t.inDisallowConditionalTypesContext,this.tokens.length=t.tokensLength,this.scopes.length=t.scopesLength,this.pos=t.pos,this.type=t.type,this.contextualKeyword=t.contextualKeyword,this.start=t.start,this.end=t.end,this.isType=t.isType,this.scopeDepth=t.scopeDepth,this.error=t.error}};var g;(function(e){e[e.backSpace=8]="backSpace";let r=10;e[e.lineFeed=r]="lineFeed";let s=9;e[e.tab=s]="tab";let o=13;e[e.carriageReturn=o]="carriageReturn";let a=14;e[e.shiftOut=a]="shiftOut";let c=32;e[e.space=c]="space";let l=33;e[e.exclamationMark=l]="exclamationMark";let u=34;e[e.quotationMark=u]="quotationMark";let d=35;e[e.numberSign=d]="numberSign";let m=36;e[e.dollarSign=m]="dollarSign";let h=37;e[e.percentSign=h]="percentSign";let b=38;e[e.ampersand=b]="ampersand";let w=39;e[e.apostrophe=w]="apostrophe";let x=40;e[e.leftParenthesis=x]="leftParenthesis";let O=41;e[e.rightParenthesis=O]="rightParenthesis";let k=42;e[e.asterisk=k]="asterisk";let T=43;e[e.plusSign=T]="plusSign";let B=44;e[e.comma=B]="comma";let R=45;e[e.dash=R]="dash";let M=46;e[e.dot=M]="dot";let $=47;e[e.slash=$]="slash";let S=48;e[e.digit0=S]="digit0";let I=49;e[e.digit1=I]="digit1";let y=50;e[e.digit2=y]="digit2";let E=51;e[e.digit3=E]="digit3";let F=52;e[e.digit4=F]="digit4";let U=53;e[e.digit5=U]="digit5";let X=54;e[e.digit6=X]="digit6";let re=55;e[e.digit7=re]="digit7";let oe=56;e[e.digit8=oe]="digit8";let Q=57;e[e.digit9=Q]="digit9";let pe=58;e[e.colon=pe]="colon";let he=59;e[e.semicolon=he]="semicolon";let ke=60;e[e.lessThan=ke]="lessThan";let Oe=61;e[e.equalsTo=Oe]="equalsTo";let Pe=62;e[e.greaterThan=Pe]="greaterThan";let Be=63;e[e.questionMark=Be]="questionMark";let Qe=64;e[e.atSign=Qe]="atSign";let ut=65;e[e.uppercaseA=ut]="uppercaseA";let It=66;e[e.uppercaseB=It]="uppercaseB";let Ot=67;e[e.uppercaseC=Ot]="uppercaseC";let tn=68;e[e.uppercaseD=tn]="uppercaseD";let $t=69;e[e.uppercaseE=$t]="uppercaseE";let zt=70;e[e.uppercaseF=zt]="uppercaseF";let le=71;e[e.uppercaseG=le]="uppercaseG";let _=72;e[e.uppercaseH=_]="uppercaseH";let L=73;e[e.uppercaseI=L]="uppercaseI";let G=74;e[e.uppercaseJ=G]="uppercaseJ";let Z=75;e[e.uppercaseK=Z]="uppercaseK";let ie=76;e[e.uppercaseL=ie]="uppercaseL";let be=77;e[e.uppercaseM=be]="uppercaseM";let bt=78;e[e.uppercaseN=bt]="uppercaseN";let Ze=79;e[e.uppercaseO=Ze]="uppercaseO";let nn=80;e[e.uppercaseP=nn]="uppercaseP";let Ds=81;e[e.uppercaseQ=Ds]="uppercaseQ";let Ms=82;e[e.uppercaseR=Ms]="uppercaseR";let js=83;e[e.uppercaseS=js]="uppercaseS";let Ws=84;e[e.uppercaseT=Ws]="uppercaseT";let Us=85;e[e.uppercaseU=Us]="uppercaseU";let qs=86;e[e.uppercaseV=qs]="uppercaseV";let Hs=87;e[e.uppercaseW=Hs]="uppercaseW";let $s=88;e[e.uppercaseX=$s]="uppercaseX";let zs=89;e[e.uppercaseY=zs]="uppercaseY";let Gs=90;e[e.uppercaseZ=Gs]="uppercaseZ";let Vs=91;e[e.leftSquareBracket=Vs]="leftSquareBracket";let Ks=92;e[e.backslash=Ks]="backslash";let Js=93;e[e.rightSquareBracket=Js]="rightSquareBracket";let Xs=94;e[e.caret=Xs]="caret";let Ys=95;e[e.underscore=Ys]="underscore";let Qs=96;e[e.graveAccent=Qs]="graveAccent";let Zs=97;e[e.lowercaseA=Zs]="lowercaseA";let eo=98;e[e.lowercaseB=eo]="lowercaseB";let to=99;e[e.lowercaseC=to]="lowercaseC";let no=100;e[e.lowercaseD=no]="lowercaseD";let ro=101;e[e.lowercaseE=ro]="lowercaseE";let so=102;e[e.lowercaseF=so]="lowercaseF";let oo=103;e[e.lowercaseG=oo]="lowercaseG";let io=104;e[e.lowercaseH=io]="lowercaseH";let ao=105;e[e.lowercaseI=ao]="lowercaseI";let co=106;e[e.lowercaseJ=co]="lowercaseJ";let lo=107;e[e.lowercaseK=lo]="lowercaseK";let uo=108;e[e.lowercaseL=uo]="lowercaseL";let fo=109;e[e.lowercaseM=fo]="lowercaseM";let po=110;e[e.lowercaseN=po]="lowercaseN";let ho=111;e[e.lowercaseO=ho]="lowercaseO";let mo=112;e[e.lowercaseP=mo]="lowercaseP";let go=113;e[e.lowercaseQ=go]="lowercaseQ";let yo=114;e[e.lowercaseR=yo]="lowercaseR";let bo=115;e[e.lowercaseS=bo]="lowercaseS";let _o=116;e[e.lowercaseT=_o]="lowercaseT";let wo=117;e[e.lowercaseU=wo]="lowercaseU";let xo=118;e[e.lowercaseV=xo]="lowercaseV";let vo=119;e[e.lowercaseW=vo]="lowercaseW";let So=120;e[e.lowercaseX=So]="lowercaseX";let ko=121;e[e.lowercaseY=ko]="lowercaseY";let Eo=122;e[e.lowercaseZ=Eo]="lowercaseZ";let Po=123;e[e.leftCurlyBrace=Po]="leftCurlyBrace";let Ao=124;e[e.verticalBar=Ao]="verticalBar";let To=125;e[e.rightCurlyBrace=To]="rightCurlyBrace";let Io=126;e[e.tilde=Io]="tilde";let Oo=160;e[e.nonBreakingSpace=Oo]="nonBreakingSpace";let Bo=5760;e[e.oghamSpaceMark=Bo]="oghamSpaceMark";let Co=8232;e[e.lineSeparator=Co]="lineSeparator";let Lo=8233;e[e.paragraphSeparator=Lo]="paragraphSeparator"})(g||(g={}));var pn,Y,te,i,C,vc;function Kt(){return vc++}function Sc(e){if("pos"in e){let t=qd(e.pos);e.message+=` (${t.line}:${t.column})`,e.loc=t}return e}var ci=class{constructor(t,r){this.line=t,this.column=r}};function qd(e){let t=1,r=1;for(let s=0;sg.lowercaseZ));){let o=di[e+(t-g.lowercaseA)+1];if(o===-1)break;e=o,r++}let s=di[e];if(s>-1&&!De[t]){i.pos=r,s&1?V(s>>>1):V(n.name,s>>>1);return}for(;r=C.length){let e=i.tokens;e.length>=2&&e[e.length-1].start>=C.length&&e[e.length-2].start>=C.length&&z("Unexpectedly reached the end of input."),V(n.eof);return}$d(C.charCodeAt(i.pos))}function $d(e){vt[e]||e===g.backslash||e===g.atSign&&C.charCodeAt(i.pos+1)===g.atSign?pi():xi(e)}function zd(){for(;C.charCodeAt(i.pos)!==g.asterisk||C.charCodeAt(i.pos+1)!==g.slash;)if(i.pos++,i.pos>C.length){z("Unterminated comment",i.pos-2);return}i.pos+=2}function _i(e){let t=C.charCodeAt(i.pos+=e);if(i.pos=g.digit0&&e<=g.digit9){Bc(!0);return}e===g.dot&&C.charCodeAt(i.pos+2)===g.dot?(i.pos+=3,V(n.ellipsis)):(++i.pos,V(n.dot))}function Vd(){C.charCodeAt(i.pos+1)===g.equalsTo?ae(n.assign,2):ae(n.slash,1)}function Kd(e){let t=e===g.asterisk?n.star:n.modulo,r=1,s=C.charCodeAt(i.pos+1);e===g.asterisk&&s===g.asterisk&&(r++,s=C.charCodeAt(i.pos+2),t=n.exponent),s===g.equalsTo&&C.charCodeAt(i.pos+2)!==g.greaterThan&&(r++,t=n.assign),ae(t,r)}function Jd(e){let t=C.charCodeAt(i.pos+1);if(t===e){C.charCodeAt(i.pos+2)===g.equalsTo?ae(n.assign,3):ae(e===g.verticalBar?n.logicalOR:n.logicalAND,2);return}if(e===g.verticalBar){if(t===g.greaterThan){ae(n.pipeline,2);return}else if(t===g.rightCurlyBrace&&te){ae(n.braceBarR,2);return}}if(t===g.equalsTo){ae(n.assign,2);return}ae(e===g.verticalBar?n.bitwiseOR:n.bitwiseAND,1)}function Xd(){C.charCodeAt(i.pos+1)===g.equalsTo?ae(n.assign,2):ae(n.bitwiseXOR,1)}function Yd(e){let t=C.charCodeAt(i.pos+1);if(t===e){ae(n.preIncDec,2);return}t===g.equalsTo?ae(n.assign,2):e===g.plusSign?ae(n.plus,1):ae(n.minus,1)}function Qd(){let e=C.charCodeAt(i.pos+1);if(e===g.lessThan){if(C.charCodeAt(i.pos+2)===g.equalsTo){ae(n.assign,3);return}i.isType?ae(n.lessThan,1):ae(n.bitShiftL,2);return}e===g.equalsTo?ae(n.relationalOrEqual,2):ae(n.lessThan,1)}function Oc(){if(i.isType){ae(n.greaterThan,1);return}let e=C.charCodeAt(i.pos+1);if(e===g.greaterThan){let t=C.charCodeAt(i.pos+2)===g.greaterThan?3:2;if(C.charCodeAt(i.pos+t)===g.equalsTo){ae(n.assign,t+1);return}ae(n.bitShiftR,t);return}e===g.equalsTo?ae(n.relationalOrEqual,2):ae(n.greaterThan,1)}function Jr(){i.type===n.greaterThan&&(i.pos-=1,Oc())}function Zd(e){let t=C.charCodeAt(i.pos+1);if(t===g.equalsTo){ae(n.equality,C.charCodeAt(i.pos+2)===g.equalsTo?3:2);return}if(e===g.equalsTo&&t===g.greaterThan){i.pos+=2,V(n.arrow);return}ae(e===g.equalsTo?n.eq:n.bang,1)}function ep(){let e=C.charCodeAt(i.pos+1),t=C.charCodeAt(i.pos+2);e===g.questionMark&&!(te&&i.isType)?t===g.equalsTo?ae(n.assign,3):ae(n.nullishCoalescing,2):e===g.dot&&!(t>=g.digit0&&t<=g.digit9)?(i.pos+=2,V(n.questionDot)):(++i.pos,V(n.question))}function xi(e){switch(e){case g.numberSign:++i.pos,V(n.hash);return;case g.dot:Gd();return;case g.leftParenthesis:++i.pos,V(n.parenL);return;case g.rightParenthesis:++i.pos,V(n.parenR);return;case g.semicolon:++i.pos,V(n.semi);return;case g.comma:++i.pos,V(n.comma);return;case g.leftSquareBracket:++i.pos,V(n.bracketL);return;case g.rightSquareBracket:++i.pos,V(n.bracketR);return;case g.leftCurlyBrace:te&&C.charCodeAt(i.pos+1)===g.verticalBar?ae(n.braceBarL,2):(++i.pos,V(n.braceL));return;case g.rightCurlyBrace:++i.pos,V(n.braceR);return;case g.colon:C.charCodeAt(i.pos+1)===g.colon?ae(n.doubleColon,2):(++i.pos,V(n.colon));return;case g.questionMark:ep();return;case g.atSign:++i.pos,V(n.at);return;case g.graveAccent:++i.pos,V(n.backQuote);return;case g.digit0:{let t=C.charCodeAt(i.pos+1);if(t===g.lowercaseX||t===g.uppercaseX||t===g.lowercaseO||t===g.uppercaseO||t===g.lowercaseB||t===g.uppercaseB){np();return}}case g.digit1:case g.digit2:case g.digit3:case g.digit4:case g.digit5:case g.digit6:case g.digit7:case g.digit8:case g.digit9:Bc(!1);return;case g.quotationMark:case g.apostrophe:rp(e);return;case g.slash:Vd();return;case g.percentSign:case g.asterisk:Kd(e);return;case g.verticalBar:case g.ampersand:Jd(e);return;case g.caret:Xd();return;case g.plusSign:case g.dash:Yd(e);return;case g.lessThan:Qd();return;case g.greaterThan:Oc();return;case g.equalsTo:case g.exclamationMark:Zd(e);return;case g.tilde:ae(n.tilde,1);return;default:break}z(`Unexpected character '${String.fromCharCode(e)}'`,i.pos)}function ae(e,t){i.pos+=t,V(e)}function tp(){let e=i.pos,t=!1,r=!1;for(;;){if(i.pos>=C.length){z("Unterminated regular expression",e);return}let s=C.charCodeAt(i.pos);if(t)t=!1;else{if(s===g.leftSquareBracket)r=!0;else if(s===g.rightSquareBracket&&r)r=!1;else if(s===g.slash&&!r)break;t=s===g.backslash}++i.pos}++i.pos,op(),V(n.regexp)}function hi(){for(;;){let e=C.charCodeAt(i.pos);if(e>=g.digit0&&e<=g.digit9||e===g.underscore)i.pos++;else break}}function np(){for(i.pos+=2;;){let t=C.charCodeAt(i.pos);if(t>=g.digit0&&t<=g.digit9||t>=g.lowercaseA&&t<=g.lowercaseF||t>=g.uppercaseA&&t<=g.uppercaseF||t===g.underscore)i.pos++;else break}C.charCodeAt(i.pos)===g.lowercaseN?(++i.pos,V(n.bigint)):V(n.num)}function Bc(e){let t=!1,r=!1;e||hi();let s=C.charCodeAt(i.pos);if(s===g.dot&&(++i.pos,hi(),s=C.charCodeAt(i.pos)),(s===g.uppercaseE||s===g.lowercaseE)&&(s=C.charCodeAt(++i.pos),(s===g.plusSign||s===g.dash)&&++i.pos,hi(),s=C.charCodeAt(i.pos)),s===g.lowercaseN?(++i.pos,t=!0):s===g.lowercaseM&&(++i.pos,r=!0),t){V(n.bigint);return}if(r){V(n.decimal);return}V(n.num)}function rp(e){for(i.pos++;;){if(i.pos>=C.length){z("Unterminated string constant");return}let t=C.charCodeAt(i.pos);if(t===g.backslash)i.pos++;else if(t===e)break;i.pos++}i.pos++,V(n.string)}function sp(){for(;;){if(i.pos>=C.length){z("Unterminated template");return}let e=C.charCodeAt(i.pos);if(e===g.graveAccent||e===g.dollarSign&&C.charCodeAt(i.pos+1)===g.leftCurlyBrace){if(i.pos===i.start&&f(n.template))if(e===g.dollarSign){i.pos+=2,V(n.dollarBraceL);return}else{++i.pos,V(n.backQuote);return}V(n.template);return}e===g.backslash&&i.pos++,i.pos++}}function op(){for(;i.pos"],["nbsp","\xA0"],["iexcl","\xA1"],["cent","\xA2"],["pound","\xA3"],["curren","\xA4"],["yen","\xA5"],["brvbar","\xA6"],["sect","\xA7"],["uml","\xA8"],["copy","\xA9"],["ordf","\xAA"],["laquo","\xAB"],["not","\xAC"],["shy","\xAD"],["reg","\xAE"],["macr","\xAF"],["deg","\xB0"],["plusmn","\xB1"],["sup2","\xB2"],["sup3","\xB3"],["acute","\xB4"],["micro","\xB5"],["para","\xB6"],["middot","\xB7"],["cedil","\xB8"],["sup1","\xB9"],["ordm","\xBA"],["raquo","\xBB"],["frac14","\xBC"],["frac12","\xBD"],["frac34","\xBE"],["iquest","\xBF"],["Agrave","\xC0"],["Aacute","\xC1"],["Acirc","\xC2"],["Atilde","\xC3"],["Auml","\xC4"],["Aring","\xC5"],["AElig","\xC6"],["Ccedil","\xC7"],["Egrave","\xC8"],["Eacute","\xC9"],["Ecirc","\xCA"],["Euml","\xCB"],["Igrave","\xCC"],["Iacute","\xCD"],["Icirc","\xCE"],["Iuml","\xCF"],["ETH","\xD0"],["Ntilde","\xD1"],["Ograve","\xD2"],["Oacute","\xD3"],["Ocirc","\xD4"],["Otilde","\xD5"],["Ouml","\xD6"],["times","\xD7"],["Oslash","\xD8"],["Ugrave","\xD9"],["Uacute","\xDA"],["Ucirc","\xDB"],["Uuml","\xDC"],["Yacute","\xDD"],["THORN","\xDE"],["szlig","\xDF"],["agrave","\xE0"],["aacute","\xE1"],["acirc","\xE2"],["atilde","\xE3"],["auml","\xE4"],["aring","\xE5"],["aelig","\xE6"],["ccedil","\xE7"],["egrave","\xE8"],["eacute","\xE9"],["ecirc","\xEA"],["euml","\xEB"],["igrave","\xEC"],["iacute","\xED"],["icirc","\xEE"],["iuml","\xEF"],["eth","\xF0"],["ntilde","\xF1"],["ograve","\xF2"],["oacute","\xF3"],["ocirc","\xF4"],["otilde","\xF5"],["ouml","\xF6"],["divide","\xF7"],["oslash","\xF8"],["ugrave","\xF9"],["uacute","\xFA"],["ucirc","\xFB"],["uuml","\xFC"],["yacute","\xFD"],["thorn","\xFE"],["yuml","\xFF"],["OElig","\u0152"],["oelig","\u0153"],["Scaron","\u0160"],["scaron","\u0161"],["Yuml","\u0178"],["fnof","\u0192"],["circ","\u02C6"],["tilde","\u02DC"],["Alpha","\u0391"],["Beta","\u0392"],["Gamma","\u0393"],["Delta","\u0394"],["Epsilon","\u0395"],["Zeta","\u0396"],["Eta","\u0397"],["Theta","\u0398"],["Iota","\u0399"],["Kappa","\u039A"],["Lambda","\u039B"],["Mu","\u039C"],["Nu","\u039D"],["Xi","\u039E"],["Omicron","\u039F"],["Pi","\u03A0"],["Rho","\u03A1"],["Sigma","\u03A3"],["Tau","\u03A4"],["Upsilon","\u03A5"],["Phi","\u03A6"],["Chi","\u03A7"],["Psi","\u03A8"],["Omega","\u03A9"],["alpha","\u03B1"],["beta","\u03B2"],["gamma","\u03B3"],["delta","\u03B4"],["epsilon","\u03B5"],["zeta","\u03B6"],["eta","\u03B7"],["theta","\u03B8"],["iota","\u03B9"],["kappa","\u03BA"],["lambda","\u03BB"],["mu","\u03BC"],["nu","\u03BD"],["xi","\u03BE"],["omicron","\u03BF"],["pi","\u03C0"],["rho","\u03C1"],["sigmaf","\u03C2"],["sigma","\u03C3"],["tau","\u03C4"],["upsilon","\u03C5"],["phi","\u03C6"],["chi","\u03C7"],["psi","\u03C8"],["omega","\u03C9"],["thetasym","\u03D1"],["upsih","\u03D2"],["piv","\u03D6"],["ensp","\u2002"],["emsp","\u2003"],["thinsp","\u2009"],["zwnj","\u200C"],["zwj","\u200D"],["lrm","\u200E"],["rlm","\u200F"],["ndash","\u2013"],["mdash","\u2014"],["lsquo","\u2018"],["rsquo","\u2019"],["sbquo","\u201A"],["ldquo","\u201C"],["rdquo","\u201D"],["bdquo","\u201E"],["dagger","\u2020"],["Dagger","\u2021"],["bull","\u2022"],["hellip","\u2026"],["permil","\u2030"],["prime","\u2032"],["Prime","\u2033"],["lsaquo","\u2039"],["rsaquo","\u203A"],["oline","\u203E"],["frasl","\u2044"],["euro","\u20AC"],["image","\u2111"],["weierp","\u2118"],["real","\u211C"],["trade","\u2122"],["alefsym","\u2135"],["larr","\u2190"],["uarr","\u2191"],["rarr","\u2192"],["darr","\u2193"],["harr","\u2194"],["crarr","\u21B5"],["lArr","\u21D0"],["uArr","\u21D1"],["rArr","\u21D2"],["dArr","\u21D3"],["hArr","\u21D4"],["forall","\u2200"],["part","\u2202"],["exist","\u2203"],["empty","\u2205"],["nabla","\u2207"],["isin","\u2208"],["notin","\u2209"],["ni","\u220B"],["prod","\u220F"],["sum","\u2211"],["minus","\u2212"],["lowast","\u2217"],["radic","\u221A"],["prop","\u221D"],["infin","\u221E"],["ang","\u2220"],["and","\u2227"],["or","\u2228"],["cap","\u2229"],["cup","\u222A"],["int","\u222B"],["there4","\u2234"],["sim","\u223C"],["cong","\u2245"],["asymp","\u2248"],["ne","\u2260"],["equiv","\u2261"],["le","\u2264"],["ge","\u2265"],["sub","\u2282"],["sup","\u2283"],["nsub","\u2284"],["sube","\u2286"],["supe","\u2287"],["oplus","\u2295"],["otimes","\u2297"],["perp","\u22A5"],["sdot","\u22C5"],["lceil","\u2308"],["rceil","\u2309"],["lfloor","\u230A"],["rfloor","\u230B"],["lang","\u2329"],["rang","\u232A"],["loz","\u25CA"],["spades","\u2660"],["clubs","\u2663"],["hearts","\u2665"],["diams","\u2666"]]);function Un(e){let[t,r]=Lc(e.jsxPragma||"React.createElement"),[s,o]=Lc(e.jsxFragmentPragma||"React.Fragment");return{base:t,suffix:r,fragmentBase:s,fragmentSuffix:o}}function Lc(e){let t=e.indexOf(".");return t===-1&&(t=e.length),[e.slice(0,t),e.slice(t)]}var ge=class{getPrefixCode(){return""}getHoistedCode(){return""}getSuffixCode(){return""}};var qn=class e extends ge{__init(){this.lastLineNumber=1}__init2(){this.lastIndex=0}__init3(){this.filenameVarName=null}__init4(){this.esmAutomaticImportNameResolutions={}}__init5(){this.cjsAutomaticModuleNameResolutions={}}constructor(t,r,s,o,a){super(),this.rootTransformer=t,this.tokens=r,this.importProcessor=s,this.nameManager=o,this.options=a,e.prototype.__init.call(this),e.prototype.__init2.call(this),e.prototype.__init3.call(this),e.prototype.__init4.call(this),e.prototype.__init5.call(this),this.jsxPragmaInfo=Un(a),this.isAutomaticRuntime=a.jsxRuntime==="automatic",this.jsxImportSource=a.jsxImportSource||"react"}process(){return this.tokens.matches1(n.jsxTagStart)?(this.processJSXTag(),!0):!1}getPrefixCode(){let t="";if(this.filenameVarName&&(t+=`const ${this.filenameVarName} = ${JSON.stringify(this.options.filePath||"")};`),this.isAutomaticRuntime)if(this.importProcessor)for(let[r,s]of Object.entries(this.cjsAutomaticModuleNameResolutions))t+=`var ${s} = require("${r}");`;else{let{createElement:r,...s}=this.esmAutomaticImportNameResolutions;r&&(t+=`import {createElement as ${r}} from "${this.jsxImportSource}";`);let o=Object.entries(s).map(([a,c])=>`${a} as ${c}`).join(", ");if(o){let a=this.jsxImportSource+(this.options.production?"/jsx-runtime":"/jsx-dev-runtime");t+=`import {${o}} from "${a}";`}}return t}processJSXTag(){let{jsxRole:t,start:r}=this.tokens.currentToken(),s=this.options.production?null:this.getElementLocationCode(r);this.isAutomaticRuntime&&t!==Ve.KeyAfterPropSpread?this.transformTagToJSXFunc(s,t):this.transformTagToCreateElement(s)}getElementLocationCode(t){return`lineNumber: ${this.getLineNumberForIndex(t)}`}getLineNumberForIndex(t){let r=this.tokens.code;for(;this.lastIndex or > at the end of the tag.");o&&this.tokens.appendCode(`, ${o}`)}for(this.options.production||(o===null&&this.tokens.appendCode(", void 0"),this.tokens.appendCode(`, ${s}, ${this.getDevSource(t)}, this`)),this.tokens.removeInitialToken();!this.tokens.matches1(n.jsxTagEnd);)this.tokens.removeToken();this.tokens.replaceToken(")")}transformTagToCreateElement(t){if(this.tokens.replaceToken(this.getCreateElementInvocationCode()),this.tokens.matches1(n.jsxTagEnd))this.tokens.replaceToken(`${this.getFragmentCode()}, null`),this.processChildren(!0);else if(this.processTagIntro(),this.processPropsObjectWithDevInfo(t),!this.tokens.matches2(n.slash,n.jsxTagEnd))if(this.tokens.matches1(n.jsxTagEnd))this.tokens.removeToken(),this.processChildren(!0);else throw new Error("Expected either /> or > at the end of the tag.");for(this.tokens.removeInitialToken();!this.tokens.matches1(n.jsxTagEnd);)this.tokens.removeToken();this.tokens.replaceToken(")")}getJSXFuncInvocationCode(t){return this.options.production?t?this.claimAutoImportedFuncInvocation("jsxs","/jsx-runtime"):this.claimAutoImportedFuncInvocation("jsx","/jsx-runtime"):this.claimAutoImportedFuncInvocation("jsxDEV","/jsx-dev-runtime")}getCreateElementInvocationCode(){if(this.isAutomaticRuntime)return this.claimAutoImportedFuncInvocation("createElement","");{let{jsxPragmaInfo:t}=this;return`${this.importProcessor&&this.importProcessor.getIdentifierReplacement(t.base)||t.base}${t.suffix}(`}}getFragmentCode(){if(this.isAutomaticRuntime)return this.claimAutoImportedName("Fragment",this.options.production?"/jsx-runtime":"/jsx-dev-runtime");{let{jsxPragmaInfo:t}=this;return(this.importProcessor&&this.importProcessor.getIdentifierReplacement(t.fragmentBase)||t.fragmentBase)+t.fragmentSuffix}}claimAutoImportedFuncInvocation(t,r){let s=this.claimAutoImportedName(t,r);return this.importProcessor?`${s}.call(void 0, `:`${s}(`}claimAutoImportedName(t,r){if(this.importProcessor){let s=this.jsxImportSource+r;return this.cjsAutomaticModuleNameResolutions[s]||(this.cjsAutomaticModuleNameResolutions[s]=this.importProcessor.getFreeIdentifierForPath(s)),`${this.cjsAutomaticModuleNameResolutions[s]}.${t}`}else return this.esmAutomaticImportNameResolutions[t]||(this.esmAutomaticImportNameResolutions[t]=this.nameManager.claimFreeName(`_${t}`)),this.esmAutomaticImportNameResolutions[t]}processTagIntro(){let t=this.tokens.currentIndex()+1;for(;this.tokens.tokens[t].isType||!this.tokens.matches2AtIndex(t-1,n.jsxName,n.jsxName)&&!this.tokens.matches2AtIndex(t-1,n.greaterThan,n.jsxName)&&!this.tokens.matches1AtIndex(t,n.braceL)&&!this.tokens.matches1AtIndex(t,n.jsxTagEnd)&&!this.tokens.matches2AtIndex(t,n.slash,n.jsxTagEnd);)t++;if(t===this.tokens.currentIndex()+1){let r=this.tokens.identifierName();vi(r)&&this.tokens.replaceToken(`'${r}'`)}for(;this.tokens.currentIndex()=g.lowercaseA&&t<=g.lowercaseZ}function ip(e){let t="",r="",s=!1,o=!1;for(let a=0;a=y.digit0&&e<=y.digit9}function cp(e){return e>=y.digit0&&e<=y.digit9||e>=y.lowercaseA&&e<=y.lowercaseF||e>=y.uppercaseA&&e<=y.uppercaseF}function Xr(e,t){let r=Wn(t),s=new Set;for(let o=0;o0||r.namedExports.length>0)continue;[...r.defaultNames,...r.wildcardNames,...r.namedImports.map(({localName:o})=>o)].every(o=>this.shouldAutomaticallyElideImportedName(o))&&this.importsToReplace.set(t,"")}}shouldAutomaticallyElideImportedName(t){return this.isTypeScriptTransformEnabled&&!this.keepUnusedImports&&!this.nonTypeIdentifiers.has(t)}generateImportReplacements(){for(let[t,r]of this.importInfoByPath.entries()){let{defaultNames:s,wildcardNames:o,namedImports:a,namedExports:c,exportStarNames:l,hasStarExport:u}=r;if(s.length===0&&o.length===0&&a.length===0&&c.length===0&&l.length===0&&!u){this.importsToReplace.set(t,`require('${t}');`);continue}let p=this.getFreeIdentifierForPath(t),m;this.enableLegacyTypeScriptModuleInterop?m=p:m=o.length>0?o[0]:this.getFreeIdentifierForPath(t);let h=`var ${p} = require('${t}');`;if(o.length>0)for(let b of o){let w=this.enableLegacyTypeScriptModuleInterop?p:`${this.helperManager.getHelperName("interopRequireWildcard")}(${p})`;h+=` var ${b} = ${w};`}else l.length>0&&m!==p?h+=` var ${m} = ${this.helperManager.getHelperName("interopRequireWildcard")}(${p});`:s.length>0&&m!==p&&(h+=` var ${m} = ${this.helperManager.getHelperName("interopRequireDefault")}(${p});`);for(let{importedName:b,localName:w}of c)h+=` ${this.helperManager.getHelperName("createNamedExportFrom")}(${p}, '${w}', '${b}');`;for(let b of l)h+=` exports.${b} = ${m};`;u&&(h+=` ${this.helperManager.getHelperName("createStarExport")}(${p});`),this.importsToReplace.set(t,h);for(let b of s)this.identifierReplacements.set(b,`${m}.default`);for(let{importedName:b,localName:w}of a)this.identifierReplacements.set(w,`${p}.${b}`)}}getFreeIdentifierForPath(t){let r=t.split("/"),o=r[r.length-1].replace(/\W/g,"");return this.nameManager.claimFreeName(`_${o}`)}preprocessImportAtIndex(t){let r=[],s=[],o=[];if(t++,(this.tokens.matchesContextualAtIndex(t,d._type)||this.tokens.matches1AtIndex(t,n._typeof))&&!this.tokens.matches1AtIndex(t+1,n.comma)&&!this.tokens.matchesContextualAtIndex(t+1,d._from)||this.tokens.matches1AtIndex(t,n.parenL))return;if(this.tokens.matches1AtIndex(t,n.name)&&(r.push(this.tokens.identifierNameAtIndex(t)),t++,this.tokens.matches1AtIndex(t,n.comma)&&t++),this.tokens.matches1AtIndex(t,n.star)&&(t+=2,s.push(this.tokens.identifierNameAtIndex(t)),t++),this.tokens.matches1AtIndex(t,n.braceL)){let l=this.getNamedImports(t+1);t=l.newIndex;for(let u of l.namedImports)u.importedName==="default"?r.push(u.localName):o.push(u)}if(this.tokens.matchesContextualAtIndex(t,d._from)&&t++,!this.tokens.matches1AtIndex(t,n.string))throw new Error("Expected string token at the end of import statement.");let a=this.tokens.stringValueAtIndex(t),c=this.getImportInfo(a);c.defaultNames.push(...r),c.wildcardNames.push(...s),c.namedImports.push(...o),r.length===0&&s.length===0&&o.length===0&&(c.hasBareImport=!0)}preprocessExportAtIndex(t){if(this.tokens.matches2AtIndex(t,n._export,n._var)||this.tokens.matches2AtIndex(t,n._export,n._let)||this.tokens.matches2AtIndex(t,n._export,n._const))this.preprocessVarExportAtIndex(t);else if(this.tokens.matches2AtIndex(t,n._export,n._function)||this.tokens.matches2AtIndex(t,n._export,n._class)){let r=this.tokens.identifierNameAtIndex(t+2);this.addExportBinding(r,r)}else if(this.tokens.matches3AtIndex(t,n._export,n.name,n._function)){let r=this.tokens.identifierNameAtIndex(t+3);this.addExportBinding(r,r)}else this.tokens.matches2AtIndex(t,n._export,n.braceL)?this.preprocessNamedExportAtIndex(t):this.tokens.matches2AtIndex(t,n._export,n.star)&&this.preprocessExportStarAtIndex(t)}preprocessVarExportAtIndex(t){let r=0;for(let s=t+2;;s++)if(this.tokens.matches1AtIndex(s,n.braceL)||this.tokens.matches1AtIndex(s,n.dollarBraceL)||this.tokens.matches1AtIndex(s,n.bracketL))r++;else if(this.tokens.matches1AtIndex(s,n.braceR)||this.tokens.matches1AtIndex(s,n.bracketR))r--;else{if(r===0&&!this.tokens.matches1AtIndex(s,n.name))break;if(this.tokens.matches1AtIndex(1,n.eq)){let o=this.tokens.currentToken().rhsEndIndex;if(o==null)throw new Error("Expected = token with an end index.");s=o-1}else{let o=this.tokens.tokens[s];if(zr(o)){let a=this.tokens.identifierNameAtIndex(s);this.identifierReplacements.set(a,`exports.${a}`)}}}}preprocessNamedExportAtIndex(t){t+=2;let{newIndex:r,namedImports:s}=this.getNamedImports(t);if(t=r,this.tokens.matchesContextualAtIndex(t,d._from))t++;else{for(let{importedName:c,localName:l}of s)this.addExportBinding(c,l);return}if(!this.tokens.matches1AtIndex(t,n.string))throw new Error("Expected string token at the end of import statement.");let o=this.tokens.stringValueAtIndex(t);this.getImportInfo(o).namedExports.push(...s)}preprocessExportStarAtIndex(t){let r=null;if(this.tokens.matches3AtIndex(t,n._export,n.star,n._as)?(t+=3,r=this.tokens.identifierNameAtIndex(t),t+=2):t+=3,!this.tokens.matches1AtIndex(t,n.string))throw new Error("Expected string token at the end of star export statement.");let s=this.tokens.stringValueAtIndex(t),o=this.getImportInfo(s);r!==null?o.exportStarNames.push(r):o.hasStarExport=!0}getNamedImports(t){let r=[];for(;;){if(this.tokens.matches1AtIndex(t,n.braceR)){t++;break}let s=ht(this.tokens,t);if(t=s.endIndex,s.isType||r.push({importedName:s.leftName,localName:s.rightName}),this.tokens.matches2AtIndex(t,n.comma,n.braceR)){t+=2;break}else if(this.tokens.matches1AtIndex(t,n.braceR)){t++;break}else if(this.tokens.matches1AtIndex(t,n.comma))t++;else throw new Error(`Unexpected token: ${JSON.stringify(this.tokens.tokens[t])}`)}return{newIndex:t,namedImports:r}}getImportInfo(t){let r=this.importInfoByPath.get(t);if(r)return r;let s={defaultNames:[],wildcardNames:[],namedImports:[],namedExports:[],hasBareImport:!1,exportStarNames:[],hasStarExport:!1};return this.importInfoByPath.set(t,s),s}addExportBinding(t,r){this.exportBindingsByLocalName.has(t)||this.exportBindingsByLocalName.set(t,[]),this.exportBindingsByLocalName.get(t).push(r)}claimImportCode(t){let r=this.importsToReplace.get(t);return this.importsToReplace.set(t,""),r||""}getIdentifierReplacement(t){return this.identifierReplacements.get(t)||null}resolveExportBinding(t){let r=this.exportBindingsByLocalName.get(t);return!r||r.length===0?null:r.map(s=>`exports.${s}`).join(" = ")}getGlobalNames(){return new Set([...this.identifierReplacements.keys(),...this.exportBindingsByLocalName.keys()])}};var lp=44,up=59,Dc="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",Mc=new Uint8Array(64),fp=new Uint8Array(128);for(let e=0;e>>=5,s>0&&(o|=32),e.write(Mc[o])}while(s>0);return t}var Fc=1024*16,jc=typeof TextDecoder<"u"?new TextDecoder:typeof Buffer<"u"?{decode(e){return Buffer.from(e.buffer,e.byteOffset,e.byteLength).toString()}}:{decode(e){let t="";for(let r=0;r0?t+jc.decode(e.subarray(0,r)):t}};function ki(e){let t=new pp,r=0,s=0,o=0,a=0;for(let c=0;c0&&t.write(up),l.length===0)continue;let u=0;for(let p=0;p0&&t.write(lp),u=Hn(t,m[0],u),m.length!==1&&(r=Hn(t,m[1],r),s=Hn(t,m[2],s),o=Hn(t,m[3],o),m.length!==4&&(a=Hn(t,m[4],a)))}}return t.flush()}var dp=Tr(Uc(),1);var Ti=class{constructor(){this._indexes={__proto__:null},this.array=[]}};function hp(e,t){return e._indexes[t]}function Wc(e,t){let r=hp(e,t);if(r!==void 0)return r;let{array:s,_indexes:o}=e,a=s.push(t);return o[t]=a-1}var mp=0,yp=1,gp=2,bp=3,_p=4,$c=-1,Hc=class{constructor({file:e,sourceRoot:t}={}){this._names=new Ti,this._sources=new Ti,this._sourcesContent=[],this._mappings=[],this.file=e,this.sourceRoot=t,this._ignoreList=new Ti}};var Qr=(e,t,r,s,o,a,c,l)=>xp(!0,e,t,r,s,o,a,c,l);function wp(e){let{_mappings:t,_sources:r,_sourcesContent:s,_names:o,_ignoreList:a}=e;return Sp(t),{version:3,file:e.file||void 0,names:o.array,sourceRoot:e.sourceRoot||void 0,sources:r.array,sourcesContent:s,mappings:t,ignoreList:a.array}}function Gc(e){let t=wp(e);return Object.assign({},t,{mappings:ki(t.mappings)})}function xp(e,t,r,s,o,a,c,l,u){let{_mappings:p,_sources:m,_sourcesContent:h,_names:b}=t,w=vp(p,r),x=kp(w,s);if(!o)return e&&Ep(w,x)?void 0:qc(w,x,[s]);let I=Wc(m,o),S=l?Wc(b,l):$c;if(I===h.length&&(h[I]=u??null),!(e&&Tp(w,x,I,a,c,S)))return qc(w,x,l?[s,I,a,c,S]:[s,I,a,c])}function vp(e,t){for(let r=e.length;r<=t;r++)e[r]=[];return e[t]}function kp(e,t){let r=e.length;for(let s=r-1;s>=0;r=s--){let o=e[s];if(t>=o[mp])break}return r}function qc(e,t,r){for(let s=e.length;s>t;s--)e[s]=e[s-1];e[t]=r}function Sp(e){let{length:t}=e,r=t;for(let s=r-1;s>=0&&!(e[s].length>0);r=s,s--);r=g.digit0&&e<=g.digit9}function lp(e){return e>=g.digit0&&e<=g.digit9||e>=g.lowercaseA&&e<=g.lowercaseF||e>=g.uppercaseA&&e<=g.uppercaseF}function Yr(e,t){let r=Un(t),s=new Set;for(let o=0;o0||r.namedExports.length>0)continue;[...r.defaultNames,...r.wildcardNames,...r.namedImports.map(({localName:o})=>o)].every(o=>this.shouldAutomaticallyElideImportedName(o))&&this.importsToReplace.set(t,"")}}shouldAutomaticallyElideImportedName(t){return this.isTypeScriptTransformEnabled&&!this.keepUnusedImports&&!this.nonTypeIdentifiers.has(t)}generateImportReplacements(){for(let[t,r]of this.importInfoByPath.entries()){let{defaultNames:s,wildcardNames:o,namedImports:a,namedExports:c,exportStarNames:l,hasStarExport:u}=r;if(s.length===0&&o.length===0&&a.length===0&&c.length===0&&l.length===0&&!u){this.importsToReplace.set(t,`require('${t}');`);continue}let d=this.getFreeIdentifierForPath(t),m;this.enableLegacyTypeScriptModuleInterop?m=d:m=o.length>0?o[0]:this.getFreeIdentifierForPath(t);let h=`var ${d} = require('${t}');`;if(o.length>0)for(let b of o){let w=this.enableLegacyTypeScriptModuleInterop?d:`${this.helperManager.getHelperName("interopRequireWildcard")}(${d})`;h+=` var ${b} = ${w};`}else l.length>0&&m!==d?h+=` var ${m} = ${this.helperManager.getHelperName("interopRequireWildcard")}(${d});`:s.length>0&&m!==d&&(h+=` var ${m} = ${this.helperManager.getHelperName("interopRequireDefault")}(${d});`);for(let{importedName:b,localName:w}of c)h+=` ${this.helperManager.getHelperName("createNamedExportFrom")}(${d}, '${w}', '${b}');`;for(let b of l)h+=` exports.${b} = ${m};`;u&&(h+=` ${this.helperManager.getHelperName("createStarExport")}(${d});`),this.importsToReplace.set(t,h);for(let b of s)this.identifierReplacements.set(b,`${m}.default`);for(let{importedName:b,localName:w}of a)this.identifierReplacements.set(w,`${d}.${b}`)}}getFreeIdentifierForPath(t){let r=t.split("/"),o=r[r.length-1].replace(/\W/g,"");return this.nameManager.claimFreeName(`_${o}`)}preprocessImportAtIndex(t){let r=[],s=[],o=[];if(t++,(this.tokens.matchesContextualAtIndex(t,p._type)||this.tokens.matches1AtIndex(t,n._typeof))&&!this.tokens.matches1AtIndex(t+1,n.comma)&&!this.tokens.matchesContextualAtIndex(t+1,p._from)||this.tokens.matches1AtIndex(t,n.parenL))return;if(this.tokens.matches1AtIndex(t,n.name)&&(r.push(this.tokens.identifierNameAtIndex(t)),t++,this.tokens.matches1AtIndex(t,n.comma)&&t++),this.tokens.matches1AtIndex(t,n.star)&&(t+=2,s.push(this.tokens.identifierNameAtIndex(t)),t++),this.tokens.matches1AtIndex(t,n.braceL)){let l=this.getNamedImports(t+1);t=l.newIndex;for(let u of l.namedImports)u.importedName==="default"?r.push(u.localName):o.push(u)}if(this.tokens.matchesContextualAtIndex(t,p._from)&&t++,!this.tokens.matches1AtIndex(t,n.string))throw new Error("Expected string token at the end of import statement.");let a=this.tokens.stringValueAtIndex(t),c=this.getImportInfo(a);c.defaultNames.push(...r),c.wildcardNames.push(...s),c.namedImports.push(...o),r.length===0&&s.length===0&&o.length===0&&(c.hasBareImport=!0)}preprocessExportAtIndex(t){if(this.tokens.matches2AtIndex(t,n._export,n._var)||this.tokens.matches2AtIndex(t,n._export,n._let)||this.tokens.matches2AtIndex(t,n._export,n._const))this.preprocessVarExportAtIndex(t);else if(this.tokens.matches2AtIndex(t,n._export,n._function)||this.tokens.matches2AtIndex(t,n._export,n._class)){let r=this.tokens.identifierNameAtIndex(t+2);this.addExportBinding(r,r)}else if(this.tokens.matches3AtIndex(t,n._export,n.name,n._function)){let r=this.tokens.identifierNameAtIndex(t+3);this.addExportBinding(r,r)}else this.tokens.matches2AtIndex(t,n._export,n.braceL)?this.preprocessNamedExportAtIndex(t):this.tokens.matches2AtIndex(t,n._export,n.star)&&this.preprocessExportStarAtIndex(t)}preprocessVarExportAtIndex(t){let r=0;for(let s=t+2;;s++)if(this.tokens.matches1AtIndex(s,n.braceL)||this.tokens.matches1AtIndex(s,n.dollarBraceL)||this.tokens.matches1AtIndex(s,n.bracketL))r++;else if(this.tokens.matches1AtIndex(s,n.braceR)||this.tokens.matches1AtIndex(s,n.bracketR))r--;else{if(r===0&&!this.tokens.matches1AtIndex(s,n.name))break;if(this.tokens.matches1AtIndex(1,n.eq)){let o=this.tokens.currentToken().rhsEndIndex;if(o==null)throw new Error("Expected = token with an end index.");s=o-1}else{let o=this.tokens.tokens[s];if(Gr(o)){let a=this.tokens.identifierNameAtIndex(s);this.identifierReplacements.set(a,`exports.${a}`)}}}}preprocessNamedExportAtIndex(t){t+=2;let{newIndex:r,namedImports:s}=this.getNamedImports(t);if(t=r,this.tokens.matchesContextualAtIndex(t,p._from))t++;else{for(let{importedName:c,localName:l}of s)this.addExportBinding(c,l);return}if(!this.tokens.matches1AtIndex(t,n.string))throw new Error("Expected string token at the end of import statement.");let o=this.tokens.stringValueAtIndex(t);this.getImportInfo(o).namedExports.push(...s)}preprocessExportStarAtIndex(t){let r=null;if(this.tokens.matches3AtIndex(t,n._export,n.star,n._as)?(t+=3,r=this.tokens.identifierNameAtIndex(t),t+=2):t+=3,!this.tokens.matches1AtIndex(t,n.string))throw new Error("Expected string token at the end of star export statement.");let s=this.tokens.stringValueAtIndex(t),o=this.getImportInfo(s);r!==null?o.exportStarNames.push(r):o.hasStarExport=!0}getNamedImports(t){let r=[];for(;;){if(this.tokens.matches1AtIndex(t,n.braceR)){t++;break}let s=ht(this.tokens,t);if(t=s.endIndex,s.isType||r.push({importedName:s.leftName,localName:s.rightName}),this.tokens.matches2AtIndex(t,n.comma,n.braceR)){t+=2;break}else if(this.tokens.matches1AtIndex(t,n.braceR)){t++;break}else if(this.tokens.matches1AtIndex(t,n.comma))t++;else throw new Error(`Unexpected token: ${JSON.stringify(this.tokens.tokens[t])}`)}return{newIndex:t,namedImports:r}}getImportInfo(t){let r=this.importInfoByPath.get(t);if(r)return r;let s={defaultNames:[],wildcardNames:[],namedImports:[],namedExports:[],hasBareImport:!1,exportStarNames:[],hasStarExport:!1};return this.importInfoByPath.set(t,s),s}addExportBinding(t,r){this.exportBindingsByLocalName.has(t)||this.exportBindingsByLocalName.set(t,[]),this.exportBindingsByLocalName.get(t).push(r)}claimImportCode(t){let r=this.importsToReplace.get(t);return this.importsToReplace.set(t,""),r||""}getIdentifierReplacement(t){return this.identifierReplacements.get(t)||null}resolveExportBinding(t){let r=this.exportBindingsByLocalName.get(t);return!r||r.length===0?null:r.map(s=>`exports.${s}`).join(" = ")}getGlobalNames(){return new Set([...this.identifierReplacements.keys(),...this.exportBindingsByLocalName.keys()])}};var up=44,fp=59,Nc="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",jc=new Uint8Array(64),dp=new Uint8Array(128);for(let e=0;e>>=5,s>0&&(o|=32),e.write(jc[o])}while(s>0);return t}var Dc=1024*16,Mc=typeof TextDecoder<"u"?new TextDecoder:typeof Buffer<"u"?{decode(e){return Buffer.from(e.buffer,e.byteOffset,e.byteLength).toString()}}:{decode(e){let t="";for(let r=0;r0?t+Mc.decode(e.subarray(0,r)):t}};function Si(e){let t=new pp,r=0,s=0,o=0,a=0;for(let c=0;c0&&t.write(fp),l.length===0)continue;let u=0;for(let d=0;d0&&t.write(up),u=$n(t,m[0],u),m.length!==1&&(r=$n(t,m[1],r),s=$n(t,m[2],s),o=$n(t,m[3],o),m.length!==4&&(a=$n(t,m[4],a)))}}return t.flush()}var hp=Pr(Wc(),1);var Pi=class{constructor(){this._indexes={__proto__:null},this.array=[]}};function mp(e,t){return e._indexes[t]}function Uc(e,t){let r=mp(e,t);if(r!==void 0)return r;let{array:s,_indexes:o}=e,a=s.push(t);return o[t]=a-1}var gp=0,yp=1,bp=2,_p=3,wp=4,Hc=-1,$c=class{constructor({file:e,sourceRoot:t}={}){this._names=new Pi,this._sources=new Pi,this._sourcesContent=[],this._mappings=[],this.file=e,this.sourceRoot=t,this._ignoreList=new Pi}};var Qr=(e,t,r,s,o,a,c,l)=>vp(!0,e,t,r,s,o,a,c,l);function xp(e){let{_mappings:t,_sources:r,_sourcesContent:s,_names:o,_ignoreList:a}=e;return Ep(t),{version:3,file:e.file||void 0,names:o.array,sourceRoot:e.sourceRoot||void 0,sources:r.array,sourcesContent:s,mappings:t,ignoreList:a.array}}function zc(e){let t=xp(e);return Object.assign({},t,{mappings:Si(t.mappings)})}function vp(e,t,r,s,o,a,c,l,u){let{_mappings:d,_sources:m,_sourcesContent:h,_names:b}=t,w=Sp(d,r),x=kp(w,s);if(!o)return e&&Pp(w,x)?void 0:qc(w,x,[s]);let O=Uc(m,o),k=l?Uc(b,l):Hc;if(O===h.length&&(h[O]=u??null),!(e&&Ap(w,x,O,a,c,k)))return qc(w,x,l?[s,O,a,c,k]:[s,O,a,c])}function Sp(e,t){for(let r=e.length;r<=t;r++)e[r]=[];return e[t]}function kp(e,t){let r=e.length;for(let s=r-1;s>=0;r=s--){let o=e[s];if(t>=o[gp])break}return r}function qc(e,t,r){for(let s=e.length;s>t;s--)e[s]=e[s-1];e[t]=r}function Ep(e){let{length:t}=e,r=t;for(let s=r-1;s>=0&&!(e[s].length>0);r=s,s--);r0&&s[s.length-1].startTokenIndex===a+1;)s.pop();for(;o>=0&&t[o].endTokenIndex===a+1;)s.push(t[o]),o--;if(a<0)break;let c=e.tokens[a],l=e.identifierNameForToken(c);if(s.length>1&&!c.isType&&c.type===n.name&&r.has(l)){if(Tc(c))zc(s[s.length-1],e,l);else if(Ac(c)){let u=s.length-1;for(;u>0&&!s[u].isFunctionScope;)u--;if(u<0)throw new Error("Did not find parent function scope.");zc(s[u],e,l)}}}if(s.length>0)throw new Error("Expected empty scope stack after processing file.")}function zc(e,t,r){for(let s=e.startTokenIndex;s0&&!i.error;)f(n.braceL)||f(n.bracketL)?e++:(f(n.braceR)||f(n.bracketR))&&e--,A();return!0}return!1}function Pd(){let e=i.snapshot(),t=Id();return i.restoreFromSnapshot(e),t}function Id(){return A(),!!(f(n.parenR)||f(n.ellipsis)||Ad()&&(f(n.colon)||f(n.comma)||f(n.question)||f(n.eq)||f(n.parenR)&&(A(),f(n.arrow))))}function Qn(e){let t=J(0);T(e),Rd()||ge(),V(t)}function Od(){f(n.colon)&&Qn(n.colon)}function Yt(){f(n.colon)&&bn()}function Bd(){v(n.colon)&&ge()}function Rd(){let e=i.snapshot();return D(d._asserts)?(A(),_e(d._is)?(ge(),!0):Wi()||f(n._this)?(A(),_e(d._is)&&ge(),!0):(i.restoreFromSnapshot(e),!1)):Wi()||f(n._this)?(A(),D(d._is)&&!Pe()?(A(),ge(),!0):(i.restoreFromSnapshot(e),!1)):!1}function bn(){let e=J(0);T(n.colon),ge(),V(e)}function ge(){if(bl(),i.inDisallowConditionalTypesContext||Pe()||!v(n._extends))return;let e=i.inDisallowConditionalTypesContext;i.inDisallowConditionalTypesContext=!0,bl(),i.inDisallowConditionalTypesContext=e,T(n.question),ge(),T(n.colon),ge()}function Ld(){return D(d._abstract)&&fe()===n._new}function bl(){if(Td()){Ui(Ut.TSFunctionType);return}if(f(n._new)){Ui(Ut.TSConstructorType);return}else if(Ld()){Ui(Ut.TSAbstractConstructorType);return}Ed()}function El(){let e=J(1);ge(),T(n.greaterThan),V(e),wn()}function Tl(){if(v(n.jsxTagStart)){i.tokens[i.tokens.length-1].type=n.typeParameterStart;let e=J(1);for(;!f(n.greaterThan)&&!i.error;)ge(),v(n.comma);Je(),V(e)}}function Al(){for(;!f(n.braceL)&&!i.error;)Cd(),v(n.comma)}function Cd(){Zn(),f(n.lessThan)&&_n()}function Nd(){at(!1),qt(),v(n._extends)&&Al(),Sl()}function Dd(){at(!1),qt(),T(n.eq),ge(),ue()}function Fd(){if(f(n.string)?Wt():$(),v(n.eq)){let e=i.tokens.length-1;xe(),i.tokens[e].rhsEndIndex=i.tokens.length}}function Gi(){for(at(!1),T(n.braceL);!v(n.braceR)&&!i.error;)Fd(),v(n.comma)}function zi(){T(n.braceL),xn(n.braceR)}function $i(){at(!1),v(n.dot)?$i():zi()}function Pl(){D(d._global)?$():f(n.string)?rt():G(),f(n.braceL)?zi():ue()}function is(){yn(),T(n.eq),Md(),ue()}function jd(){return D(d._require)&&fe()===n.parenL}function Md(){jd()?Ud():Zn()}function Ud(){me(d._require),T(n.parenL),f(n.string)||G(),Wt(),T(n.parenR)}function Wd(){if(tt())return!1;switch(i.type){case n._function:{let e=J(1);A();let t=i.start;return kt(t,!0),V(e),!0}case n._class:{let e=J(1);return Et(!0,!1),V(e),!0}case n._const:if(f(n._const)&&hn(d._enum)){let e=J(1);return T(n._const),me(d._enum),i.tokens[i.tokens.length-1].type=n._enum,Gi(),V(e),!0}case n._var:case n._let:{let e=J(1);return tr(i.type!==n._var),V(e),!0}case n.name:{let e=J(1),t=i.contextualKeyword,r=!1;return t===d._global?(Pl(),r=!0):r=as(t,!0),V(e),r}default:return!1}}function _l(){return as(i.contextualKeyword,!0)}function qd(e){switch(e){case d._declare:{let t=i.tokens.length-1;if(Wd())return i.tokens[t].type=n._declare,!0;break}case d._global:if(f(n.braceL))return zi(),!0;break;default:return as(e,!1)}return!1}function as(e,t){switch(e){case d._abstract:if(gn(t)&&f(n._class))return i.tokens[i.tokens.length-1].type=n._abstract,Et(!0,!1),!0;break;case d._enum:if(gn(t)&&f(n.name))return i.tokens[i.tokens.length-1].type=n._enum,Gi(),!0;break;case d._interface:if(gn(t)&&f(n.name)){let r=J(t?2:1);return Nd(),V(r),!0}break;case d._module:if(gn(t)){if(f(n.string)){let r=J(t?2:1);return Pl(),V(r),!0}else if(f(n.name)){let r=J(t?2:1);return $i(),V(r),!0}}break;case d._namespace:if(gn(t)&&f(n.name)){let r=J(t?2:1);return $i(),V(r),!0}break;case d._type:if(gn(t)&&f(n.name)){let r=J(t?2:1);return Dd(),V(r),!0}break;default:break}return!1}function gn(e){return e?(A(),!0):!tt()}function $d(){let e=i.snapshot();return os(),St(),Od(),T(n.arrow),i.error?(i.restoreFromSnapshot(e),!1):(Qt(!0),!0)}function Ki(){i.type===n.bitShiftL&&(i.pos-=1,K(n.lessThan)),_n()}function _n(){let e=J(0);for(T(n.lessThan);!f(n.greaterThan)&&!i.error;)ge(),v(n.comma);e?(T(n.greaterThan),V(e)):(V(e),Jr(),T(n.greaterThan),i.tokens[i.tokens.length-1].isType=!0)}function Vi(){if(f(n.name))switch(i.contextualKeyword){case d._abstract:case d._declare:case d._enum:case d._interface:case d._module:case d._namespace:case d._type:return!0;default:break}return!1}function Il(e,t){if(f(n.colon)&&Qn(n.colon),!f(n.braceL)&&tt()){let r=i.tokens.length-1;for(;r>=0&&(i.tokens[r].start>=e||i.tokens[r].type===n._default||i.tokens[r].type===n._export);)i.tokens[r].isType=!0,r--;return}Qt(!1,t)}function Ol(e,t,r){if(!Pe()&&v(n.bang)){i.tokens[i.tokens.length-1].type=n.nonNullAssertion;return}if(f(n.lessThan)||f(n.bitShiftL)){let s=i.snapshot();if(!t&&Ji()&&$d())return;if(Ki(),!t&&v(n.parenL)?(i.tokens[i.tokens.length-1].subscriptStartIndex=e,ct()):f(n.backQuote)?cs():(i.type===n.greaterThan||i.type!==n.parenL&&i.type&n.IS_EXPRESSION_START&&!Pe())&&G(),i.error)i.restoreFromSnapshot(s);else return}else!t&&f(n.questionDot)&&fe()===n.lessThan&&(A(),i.tokens[e].isOptionalChainStart=!0,i.tokens[i.tokens.length-1].subscriptStartIndex=e,_n(),T(n.parenL),ct());er(e,t,r)}function Bl(){if(v(n._import))return D(d._type)&&fe()!==n.eq&&me(d._type),is(),!0;if(v(n.eq))return ve(),ue(),!0;if(_e(d._as))return me(d._namespace),$(),ue(),!0;if(D(d._type)){let e=fe();(e===n.braceL||e===n.star)&&A()}return!1}function Rl(){if($(),f(n.comma)||f(n.braceR)){i.tokens[i.tokens.length-1].identifierRole=F.ImportDeclaration;return}if($(),f(n.comma)||f(n.braceR)){i.tokens[i.tokens.length-1].identifierRole=F.ImportDeclaration,i.tokens[i.tokens.length-2].isType=!0,i.tokens[i.tokens.length-1].isType=!0;return}if($(),f(n.comma)||f(n.braceR)){i.tokens[i.tokens.length-3].identifierRole=F.ImportAccess,i.tokens[i.tokens.length-1].identifierRole=F.ImportDeclaration;return}$(),i.tokens[i.tokens.length-3].identifierRole=F.ImportAccess,i.tokens[i.tokens.length-1].identifierRole=F.ImportDeclaration,i.tokens[i.tokens.length-4].isType=!0,i.tokens[i.tokens.length-3].isType=!0,i.tokens[i.tokens.length-2].isType=!0,i.tokens[i.tokens.length-1].isType=!0}function Ll(){if($(),f(n.comma)||f(n.braceR)){i.tokens[i.tokens.length-1].identifierRole=F.ExportAccess;return}if($(),f(n.comma)||f(n.braceR)){i.tokens[i.tokens.length-1].identifierRole=F.ExportAccess,i.tokens[i.tokens.length-2].isType=!0,i.tokens[i.tokens.length-1].isType=!0;return}if($(),f(n.comma)||f(n.braceR)){i.tokens[i.tokens.length-3].identifierRole=F.ExportAccess;return}$(),i.tokens[i.tokens.length-3].identifierRole=F.ExportAccess,i.tokens[i.tokens.length-4].isType=!0,i.tokens[i.tokens.length-3].isType=!0,i.tokens[i.tokens.length-2].isType=!0,i.tokens[i.tokens.length-1].isType=!0}function Cl(){if(D(d._abstract)&&fe()===n._class)return i.type=n._abstract,A(),Et(!0,!0),!0;if(D(d._interface)){let e=J(2);return as(d._interface,!0),V(e),!0}return!1}function Nl(){if(i.type===n._const){let e=xt();if(e.type===n.name&&e.contextualKeyword===d._enum)return T(n._const),me(d._enum),i.tokens[i.tokens.length-1].type=n._enum,Gi(),!0}return!1}function Dl(e){let t=i.tokens.length;Yn([d._abstract,d._readonly,d._declare,d._static,d._override]);let r=i.tokens.length;if(kl()){let o=e?t-1:t;for(let a=o;a=R.length){G("Unterminated JSX contents");return}let r=R.charCodeAt(i.pos);if(r===y.lessThan||r===y.leftCurlyBrace){if(i.pos===i.start){if(r===y.lessThan){i.pos++,K(n.jsxTagStart);return}xi(r);return}e&&!t?K(n.jsxEmptyText):K(n.jsxText);return}r===y.lineFeed?e=!0:r!==y.space&&r!==y.carriageReturn&&r!==y.tab&&(t=!0),i.pos++}}function Kd(e){for(i.pos++;;){if(i.pos>=R.length){G("Unterminated string constant");return}if(R.charCodeAt(i.pos)===e){i.pos++;break}i.pos++}K(n.string)}function Vd(){let e;do{if(i.pos>R.length){G("Unexpectedly reached the end of input.");return}e=R.charCodeAt(++i.pos)}while(Fe[e]||e===y.dash);K(n.jsxName)}function Xi(){Je()}function Kl(e){if(Xi(),!v(n.colon)){i.tokens[i.tokens.length-1].identifierRole=e;return}Xi()}function Vl(){let e=i.tokens.length;Kl(F.Access);let t=!1;for(;f(n.dot);)t=!0,Je(),Xi();if(!t){let r=i.tokens[e],s=R.charCodeAt(r.start);s>=y.lowercaseA&&s<=y.lowercaseZ&&(r.identifierRole=null)}}function Jd(){switch(i.type){case n.braceL:A(),ve(),Je();return;case n.jsxTagStart:Qi(),Je();return;case n.string:Je();return;default:G("JSX value should be either an expression or a quoted JSX text")}}function Yd(){T(n.ellipsis),ve()}function Xd(e){if(f(n.jsxTagEnd))return!1;Vl(),X&&Tl();let t=!1;for(;!f(n.slash)&&!f(n.jsxTagEnd)&&!i.error;){if(v(n.braceL)){t=!0,T(n.ellipsis),xe(),Je();continue}t&&i.end-i.start===3&&R.charCodeAt(i.start)===y.lowercaseK&&R.charCodeAt(i.start+1)===y.lowercaseE&&R.charCodeAt(i.start+2)===y.lowercaseY&&(i.tokens[e].jsxRole=Ke.KeyAfterPropSpread),Kl(F.ObjectKey),f(n.eq)&&(Je(),Jd())}let r=f(n.slash);return r&&Je(),r}function Qd(){f(n.jsxTagEnd)||Vl()}function Jl(){let e=i.tokens.length-1;i.tokens[e].jsxRole=Ke.NoChildren;let t=0;if(!Xd(e))for(vn();;)switch(i.type){case n.jsxTagStart:if(Je(),f(n.slash)){Je(),Qd(),i.tokens[e].jsxRole!==Ke.KeyAfterPropSpread&&(t===1?i.tokens[e].jsxRole=Ke.OneChild:t>1&&(i.tokens[e].jsxRole=Ke.StaticChildren));return}t++,Jl(),vn();break;case n.jsxText:t++,vn();break;case n.jsxEmptyText:vn();break;case n.braceL:A(),f(n.ellipsis)?(Yd(),vn(),t+=2):(f(n.braceR)||(t++,ve()),vn());break;default:G();return}}function Qi(){Je(),Jl()}function Je(){i.tokens.push(new Jt),wi(),i.start=i.pos;let e=R.charCodeAt(i.pos);if(vt[e])Vd();else if(e===y.quotationMark||e===y.apostrophe)Kd(e);else switch(++i.pos,e){case y.greaterThan:K(n.jsxTagEnd);break;case y.lessThan:K(n.jsxTagStart);break;case y.slash:K(n.slash);break;case y.equalsTo:K(n.eq);break;case y.leftCurlyBrace:K(n.braceL);break;case y.dot:K(n.dot);break;case y.colon:K(n.colon);break;default:G()}}function vn(){i.tokens.push(new Jt),i.start=i.pos,zd()}function Yl(e){if(f(n.question)){let t=fe();if(t===n.colon||t===n.comma||t===n.parenR)return}Zi(e)}function Xl(){Vr(n.question),f(n.colon)&&(X?bn():te&&Tt())}var ea=class{constructor(t){this.stop=t}};function ve(e=!1){if(xe(e),f(n.comma))for(;v(n.comma);)xe(e)}function xe(e=!1,t=!1){return X?Hl(e,t):te?iu(e,t):nt(e,t)}function nt(e,t){if(f(n._yield))return hh(),!1;(f(n.parenL)||f(n.name)||f(n._yield))&&(i.potentialArrowAt=i.start);let r=Zd(e);return t&&oa(),i.type&n.IS_ASSIGN?(A(),xe(e),!1):r}function Zd(e){return th(e)?!0:(eh(e),!1)}function eh(e){X||te?Yl(e):Zi(e)}function Zi(e){v(n.question)&&(xe(),T(n.colon),xe(e))}function th(e){let t=i.tokens.length;return wn()?!0:(ls(t,-1,e),!1)}function ls(e,t,r){if(X&&(n._in&n.PRECEDENCE_MASK)>t&&!Pe()&&(_e(d._as)||_e(d._satisfies))){let o=J(1);ge(),V(o),Jr(),ls(e,t,r);return}let s=i.type&n.PRECEDENCE_MASK;if(s>0&&(!r||!f(n._in))&&s>t){let o=i.type;A(),o===n.nullishCoalescing&&(i.tokens[i.tokens.length-1].nullishStartIndex=e);let a=i.tokens.length;wn(),ls(a,o&n.IS_RIGHT_ASSOCIATIVE?s-1:s,r),o===n.nullishCoalescing&&(i.tokens[e].numNullishCoalesceStarts++,i.tokens[i.tokens.length-1].numNullishCoalesceEnds++),ls(e,t,r)}}function wn(){if(X&&!dn&&v(n.lessThan))return El(),!1;if(D(d._module)&&gi()===y.leftCurlyBrace&&!Gr())return mh(),!1;if(i.type&n.IS_PREFIX)return A(),wn(),!1;if(ta())return!0;for(;i.type&n.IS_POSTFIX&&!Ce();)i.type===n.preIncDec&&(i.type=n.postIncDec),A();return!1}function ta(){let e=i.tokens.length;return rt()?!0:(na(e),i.tokens.length>e&&i.tokens[e].isOptionalChainStart&&(i.tokens[i.tokens.length-1].isOptionalChainEnd=!0),!1)}function na(e,t=!1){te?cu(e,t):ra(e,t)}function ra(e,t=!1){let r=new ea(!1);do nh(e,t,r);while(!r.stop&&!i.error)}function nh(e,t,r){X?Ol(e,t,r):te?nu(e,t,r):er(e,t,r)}function er(e,t,r){if(!t&&v(n.doubleColon))sa(),r.stop=!0,na(e,t);else if(f(n.questionDot)){if(i.tokens[e].isOptionalChainStart=!0,t&&fe()===n.parenL){r.stop=!0;return}A(),i.tokens[i.tokens.length-1].subscriptStartIndex=e,v(n.bracketL)?(ve(),T(n.bracketR)):v(n.parenL)?ct():us()}else if(v(n.dot))i.tokens[i.tokens.length-1].subscriptStartIndex=e,us();else if(v(n.bracketL))i.tokens[i.tokens.length-1].subscriptStartIndex=e,ve(),T(n.bracketR);else if(!t&&f(n.parenL))if(Ji()){let s=i.snapshot(),o=i.tokens.length;A(),i.tokens[i.tokens.length-1].subscriptStartIndex=e;let a=Vt();i.tokens[i.tokens.length-1].contextId=a,ct(),i.tokens[i.tokens.length-1].contextId=a,rh()&&(i.restoreFromSnapshot(s),r.stop=!0,i.scopeDepth++,St(),sh(o))}else{A(),i.tokens[i.tokens.length-1].subscriptStartIndex=e;let s=Vt();i.tokens[i.tokens.length-1].contextId=s,ct(),i.tokens[i.tokens.length-1].contextId=s}else f(n.backQuote)?cs():r.stop=!0}function Ji(){return i.tokens[i.tokens.length-1].contextualKeyword===d._async&&!Ce()}function ct(){let e=!0;for(;!v(n.parenR)&&!i.error;){if(e)e=!1;else if(T(n.comma),v(n.parenR))break;eu(!1)}}function rh(){return f(n.colon)||f(n.arrow)}function sh(e){X?$l():te&&ou(),T(n.arrow),kn(e)}function sa(){let e=i.tokens.length;rt(),na(e,!0)}function rt(){if(v(n.modulo))return $(),!1;if(f(n.jsxText)||f(n.jsxEmptyText))return Wt(),!1;if(f(n.lessThan)&&dn)return i.type=n.jsxTagStart,Qi(),A(),!1;let e=i.potentialArrowAt===i.start;switch(i.type){case n.slash:case n.assign:Ic();case n._super:case n._this:case n.regexp:case n.num:case n.bigint:case n.decimal:case n.string:case n._null:case n._true:case n._false:return A(),!1;case n._import:return A(),f(n.dot)&&(i.tokens[i.tokens.length-1].type=n.name,A(),$()),!1;case n.name:{let t=i.tokens.length,r=i.start,s=i.contextualKeyword;return $(),s===d._await?(dh(),!1):s===d._async&&f(n._function)&&!Ce()?(A(),kt(r,!1),!1):e&&s===d._async&&!Ce()&&f(n.name)?(i.scopeDepth++,at(!1),T(n.arrow),kn(t),!0):f(n._do)&&!Ce()?(A(),At(),!1):e&&!Ce()&&f(n.arrow)?(i.scopeDepth++,rs(!1),T(n.arrow),kn(t),!0):(i.tokens[i.tokens.length-1].identifierRole=F.Access,!1)}case n._do:return A(),At(),!1;case n.parenL:return Ql(e);case n.bracketL:return A(),Zl(n.bracketR,!0),!1;case n.braceL:return Xn(!1,!1),!1;case n._function:return oh(),!1;case n.at:hs();case n._class:return Et(!1),!1;case n._new:return ah(),!1;case n.backQuote:return cs(),!1;case n.doubleColon:return A(),sa(),!1;case n.hash:{let t=gi();return vt[t]||t===y.backslash?us():A(),!1}default:return G(),!1}}function us(){v(n.hash),$()}function oh(){let e=i.start;$(),v(n.dot)&&$(),kt(e,!1)}function Wt(){A()}function nr(){T(n.parenL),ve(),T(n.parenR)}function Ql(e){let t=i.snapshot(),r=i.tokens.length;T(n.parenL);let s=!0;for(;!f(n.parenR)&&!i.error;){if(s)s=!1;else if(T(n.comma),f(n.parenR))break;if(f(n.ellipsis)){Mi(!1),oa();break}else xe(!1,!0)}return T(n.parenR),e&&ih()&&fs()?(i.restoreFromSnapshot(t),i.scopeDepth++,St(),fs(),kn(r),i.error?(i.restoreFromSnapshot(t),Ql(!1),!1):!0):!1}function ih(){return f(n.colon)||!Ce()}function fs(){return X?Gl():te?au():v(n.arrow)}function oa(){(X||te)&&Xl()}function ah(){if(T(n._new),v(n.dot)){$();return}ch(),te&&ru(),v(n.parenL)&&Zl(n.parenR)}function ch(){sa(),v(n.questionDot)}function cs(){for(dt(),dt();!f(n.backQuote)&&!i.error;)T(n.dollarBraceL),ve(),dt(),dt();A()}function Xn(e,t){let r=Vt(),s=!0;for(A(),i.tokens[i.tokens.length-1].contextId=r;!v(n.braceR)&&!i.error;){if(s)s=!1;else if(T(n.comma),v(n.braceR))break;let o=!1;if(f(n.ellipsis)){let a=i.tokens.length;if(ji(),e&&(i.tokens.length===a+2&&rs(t),v(n.braceR)))break;continue}e||(o=v(n.star)),!e&&D(d._async)?(o&&G(),$(),f(n.colon)||f(n.parenL)||f(n.braceR)||f(n.eq)||f(n.comma)||(f(n.star)&&(A(),o=!0),Xt(r))):Xt(r),ph(e,t,r)}i.tokens[i.tokens.length-1].contextId=r}function lh(e){return!e&&(f(n.string)||f(n.num)||f(n.bracketL)||f(n.name)||!!(i.type&n.IS_KEYWORD))}function uh(e,t){let r=i.start;return f(n.parenL)?(e&&G(),ps(r,!1),!0):lh(e)?(Xt(t),ps(r,!1),!0):!1}function fh(e,t){if(v(n.colon)){e?Kn(t):xe(!1);return}let r;e?i.scopeDepth===0?r=F.ObjectShorthandTopLevelDeclaration:t?r=F.ObjectShorthandBlockScopedDeclaration:r=F.ObjectShorthandFunctionScopedDeclaration:r=F.ObjectShorthand,i.tokens[i.tokens.length-1].identifierRole=r,Kn(t,!0)}function ph(e,t,r){X?Ul():te&&su(),uh(e,r)||fh(e,t)}function Xt(e){te&&ds(),v(n.bracketL)?(i.tokens[i.tokens.length-1].contextId=e,xe(),T(n.bracketR),i.tokens[i.tokens.length-1].contextId=e):(f(n.num)||f(n.string)||f(n.bigint)||f(n.decimal)?rt():us(),i.tokens[i.tokens.length-1].identifierRole=F.ObjectKey,i.tokens[i.tokens.length-1].contextId=e)}function ps(e,t){let r=Vt();i.scopeDepth++;let s=i.tokens.length;St(t,r),ia(e,r);let a=i.tokens.length;i.scopes.push(new Ue(s,a,!0)),i.scopeDepth--}function kn(e){Qt(!0);let t=i.tokens.length;i.scopes.push(new Ue(e,t,!0)),i.scopeDepth--}function ia(e,t=0){X?Il(e,t):te?tu(t):Qt(!1,t)}function Qt(e,t=0){e&&!f(n.braceL)?xe():At(!0,t)}function Zl(e,t=!1){let r=!0;for(;!v(e)&&!i.error;){if(r)r=!1;else if(T(n.comma),v(e))break;eu(t)}}function eu(e){e&&f(n.comma)||(f(n.ellipsis)?(ji(),oa()):f(n.question)?A():xe(!1,!0))}function $(){A(),i.tokens[i.tokens.length-1].type=n.name}function dh(){wn()}function hh(){A(),!f(n.semi)&&!Ce()&&(v(n.star),xe())}function mh(){me(d._module),T(n.braceL),xn(n.braceR)}function yh(e){return(e.type===n.name||!!(e.type&n.IS_KEYWORD))&&e.contextualKeyword!==d._from}function yt(e){let t=J(0);T(e||n.colon),qe(),V(t)}function lu(){T(n.modulo),me(d._checks),v(n.parenL)&&(ve(),T(n.parenR))}function la(){let e=J(0);T(n.colon),f(n.modulo)?lu():(qe(),f(n.modulo)&&lu()),V(e)}function gh(){A(),ua(!0)}function bh(){A(),$(),f(n.lessThan)&&st(),T(n.parenL),ca(),T(n.parenR),la(),ue()}function aa(){f(n._class)?gh():f(n._function)?bh():f(n._var)?_h():_e(d._module)?v(n.dot)?vh():wh():D(d._type)?kh():D(d._opaque)?Sh():D(d._interface)?Eh():f(n._export)?xh():G()}function _h(){A(),mu(),ue()}function wh(){for(f(n.string)?rt():$(),T(n.braceL);!f(n.braceR)&&!i.error;)f(n._import)?(A(),ga()):G();T(n.braceR)}function xh(){T(n._export),v(n._default)?f(n._function)||f(n._class)?aa():(qe(),ue()):f(n._var)||f(n._function)||f(n._class)||D(d._opaque)?aa():f(n.star)||f(n.braceL)||D(d._interface)||D(d._type)||D(d._opaque)?ya():G()}function vh(){me(d._exports),Tt(),ue()}function kh(){A(),pa()}function Sh(){A(),da(!0)}function Eh(){A(),ua()}function ua(e=!1){if(_s(),f(n.lessThan)&&st(),v(n._extends))do ms();while(!e&&v(n.comma));if(D(d._mixins)){A();do ms();while(v(n.comma))}if(D(d._implements)){A();do ms();while(v(n.comma))}ys(e,!1,e)}function ms(){pu(!1),f(n.lessThan)&&Zt()}function fa(){ua()}function _s(){$()}function pa(){_s(),f(n.lessThan)&&st(),yt(n.eq),ue()}function da(e){me(d._type),_s(),f(n.lessThan)&&st(),f(n.colon)&&yt(n.colon),e||yt(n.eq),ue()}function Th(){ds(),mu(),v(n.eq)&&qe()}function st(){let e=J(0);f(n.lessThan)||f(n.typeParameterStart)?A():G();do Th(),f(n.greaterThan)||T(n.comma);while(!f(n.greaterThan)&&!i.error);T(n.greaterThan),V(e)}function Zt(){let e=J(0);for(T(n.lessThan);!f(n.greaterThan)&&!i.error;)qe(),f(n.greaterThan)||T(n.comma);T(n.greaterThan),V(e)}function Ah(){if(me(d._interface),v(n._extends))do ms();while(v(n.comma));ys(!1,!1,!1)}function ha(){f(n.num)||f(n.string)?rt():$()}function Ph(){fe()===n.colon?(ha(),yt()):qe(),T(n.bracketR),yt()}function Ih(){ha(),T(n.bracketR),T(n.bracketR),f(n.lessThan)||f(n.parenL)?ma():(v(n.question),yt())}function ma(){for(f(n.lessThan)&&st(),T(n.parenL);!f(n.parenR)&&!f(n.ellipsis)&&!i.error;)gs(),f(n.parenR)||T(n.comma);v(n.ellipsis)&&gs(),T(n.parenR),yt()}function Oh(){ma()}function ys(e,t,r){let s;for(t&&f(n.braceBarL)?(T(n.braceBarL),s=n.braceBarR):(T(n.braceL),s=n.braceR);!f(s)&&!i.error;){if(r&&D(d._proto)){let o=fe();o!==n.colon&&o!==n.question&&(A(),e=!1)}if(e&&D(d._static)){let o=fe();o!==n.colon&&o!==n.question&&A()}if(ds(),v(n.bracketL))v(n.bracketL)?Ih():Ph();else if(f(n.parenL)||f(n.lessThan))Oh();else{if(D(d._get)||D(d._set)){let o=fe();(o===n.name||o===n.string||o===n.num)&&A()}Bh()}Rh()}T(s)}function Bh(){if(f(n.ellipsis)){if(T(n.ellipsis),v(n.comma)||v(n.semi),f(n.braceR))return;qe()}else ha(),f(n.lessThan)||f(n.parenL)?ma():(v(n.question),yt())}function Rh(){!v(n.semi)&&!v(n.comma)&&!f(n.braceR)&&!f(n.braceBarR)&&G()}function pu(e){for(e||$();v(n.dot);)$()}function Lh(){pu(!0),f(n.lessThan)&&Zt()}function Ch(){T(n._typeof),du()}function Nh(){for(T(n.bracketL);i.pos0&&t0?this.tokens[this.tokenIndex-1].end:0,this.tokenIndex0&&this.tokenAtRelativeIndex(-1).type===n._delete?t.isAsyncOperation?this.resultCode+=this.helperManager.getHelperName("asyncOptionalChainDelete"):this.resultCode+=this.helperManager.getHelperName("optionalChainDelete"):t.isAsyncOperation?this.resultCode+=this.helperManager.getHelperName("asyncOptionalChain"):this.resultCode+=this.helperManager.getHelperName("optionalChain"),this.resultCode+="([")}}appendTokenSuffix(){let t=this.currentToken();if(t.isOptionalChainEnd&&!this.disableESTransforms&&(this.resultCode+="])"),t.numNullishCoalesceEnds&&!this.disableESTransforms)for(let r=0;r0&&s[s.length-1].startTokenIndex===a+1;)s.pop();for(;o>=0&&t[o].endTokenIndex===a+1;)s.push(t[o]),o--;if(a<0)break;let c=e.tokens[a],l=e.identifierNameForToken(c);if(s.length>1&&!c.isType&&c.type===n.name&&r.has(l)){if(Pc(c))Gc(s[s.length-1],e,l);else if(Ac(c)){let u=s.length-1;for(;u>0&&!s[u].isFunctionScope;)u--;if(u<0)throw new Error("Did not find parent function scope.");Gc(s[u],e,l)}}}if(s.length>0)throw new Error("Expected empty scope stack after processing file.")}function Gc(e,t,r){for(let s=e.startTokenIndex;s0&&!i.error;)f(n.braceL)||f(n.bracketL)?e++:(f(n.braceR)||f(n.bracketR))&&e--,A();return!0}return!1}function I1(){let e=i.snapshot(),t=O1();return i.restoreFromSnapshot(e),t}function O1(){return A(),!!(f(n.parenR)||f(n.ellipsis)||T1()&&(f(n.colon)||f(n.comma)||f(n.question)||f(n.eq)||f(n.parenR)&&(A(),f(n.arrow))))}function Qn(e){let t=J(0);P(e),L1()||ye(),K(t)}function B1(){f(n.colon)&&Qn(n.colon)}function Xt(){f(n.colon)&&bn()}function C1(){v(n.colon)&&ye()}function L1(){let e=i.snapshot();return N(p._asserts)?(A(),_e(p._is)?(ye(),!0):Ui()||f(n._this)?(A(),_e(p._is)&&ye(),!0):(i.restoreFromSnapshot(e),!1)):Ui()||f(n._this)?(A(),N(p._is)&&!Te()?(A(),ye(),!0):(i.restoreFromSnapshot(e),!1)):!1}function bn(){let e=J(0);P(n.colon),ye(),K(e)}function ye(){if(bl(),i.inDisallowConditionalTypesContext||Te()||!v(n._extends))return;let e=i.inDisallowConditionalTypesContext;i.inDisallowConditionalTypesContext=!0,bl(),i.inDisallowConditionalTypesContext=e,P(n.question),ye(),P(n.colon),ye()}function R1(){return N(p._abstract)&&fe()===n._new}function bl(){if(A1()){Wi(Wt.TSFunctionType);return}if(f(n._new)){Wi(Wt.TSConstructorType);return}else if(R1()){Wi(Wt.TSAbstractConstructorType);return}P1()}function El(){let e=J(1);ye(),P(n.greaterThan),K(e),wn()}function Pl(){if(v(n.jsxTagStart)){i.tokens[i.tokens.length-1].type=n.typeParameterStart;let e=J(1);for(;!f(n.greaterThan)&&!i.error;)ye(),v(n.comma);Je(),K(e)}}function Al(){for(;!f(n.braceL)&&!i.error;)F1(),v(n.comma)}function F1(){Zn(),f(n.lessThan)&&_n()}function N1(){at(!1),qt(),v(n._extends)&&Al(),kl()}function D1(){at(!1),qt(),P(n.eq),ye(),ue()}function M1(){if(f(n.string)?Ut():H(),v(n.eq)){let e=i.tokens.length-1;xe(),i.tokens[e].rhsEndIndex=i.tokens.length}}function zi(){for(at(!1),P(n.braceL);!v(n.braceR)&&!i.error;)M1(),v(n.comma)}function Gi(){P(n.braceL),xn(n.braceR)}function Hi(){at(!1),v(n.dot)?Hi():Gi()}function Tl(){N(p._global)?H():f(n.string)?rt():z(),f(n.braceL)?Gi():ue()}function is(){gn(),P(n.eq),W1(),ue()}function j1(){return N(p._require)&&fe()===n.parenL}function W1(){j1()?U1():Zn()}function U1(){me(p._require),P(n.parenL),f(n.string)||z(),Ut(),P(n.parenR)}function q1(){if(tt())return!1;switch(i.type){case n._function:{let e=J(1);A();let t=i.start;return St(t,!0),K(e),!0}case n._class:{let e=J(1);return Et(!0,!1),K(e),!0}case n._const:if(f(n._const)&&hn(p._enum)){let e=J(1);return P(n._const),me(p._enum),i.tokens[i.tokens.length-1].type=n._enum,zi(),K(e),!0}case n._var:case n._let:{let e=J(1);return tr(i.type!==n._var),K(e),!0}case n.name:{let e=J(1),t=i.contextualKeyword,r=!1;return t===p._global?(Tl(),r=!0):r=as(t,!0),K(e),r}default:return!1}}function _l(){return as(i.contextualKeyword,!0)}function H1(e){switch(e){case p._declare:{let t=i.tokens.length-1;if(q1())return i.tokens[t].type=n._declare,!0;break}case p._global:if(f(n.braceL))return Gi(),!0;break;default:return as(e,!1)}return!1}function as(e,t){switch(e){case p._abstract:if(yn(t)&&f(n._class))return i.tokens[i.tokens.length-1].type=n._abstract,Et(!0,!1),!0;break;case p._enum:if(yn(t)&&f(n.name))return i.tokens[i.tokens.length-1].type=n._enum,zi(),!0;break;case p._interface:if(yn(t)&&f(n.name)){let r=J(t?2:1);return N1(),K(r),!0}break;case p._module:if(yn(t)){if(f(n.string)){let r=J(t?2:1);return Tl(),K(r),!0}else if(f(n.name)){let r=J(t?2:1);return Hi(),K(r),!0}}break;case p._namespace:if(yn(t)&&f(n.name)){let r=J(t?2:1);return Hi(),K(r),!0}break;case p._type:if(yn(t)&&f(n.name)){let r=J(t?2:1);return D1(),K(r),!0}break;default:break}return!1}function yn(e){return e?(A(),!0):!tt()}function $1(){let e=i.snapshot();return os(),kt(),B1(),P(n.arrow),i.error?(i.restoreFromSnapshot(e),!1):(Qt(!0),!0)}function Vi(){i.type===n.bitShiftL&&(i.pos-=1,V(n.lessThan)),_n()}function _n(){let e=J(0);for(P(n.lessThan);!f(n.greaterThan)&&!i.error;)ye(),v(n.comma);e?(P(n.greaterThan),K(e)):(K(e),Jr(),P(n.greaterThan),i.tokens[i.tokens.length-1].isType=!0)}function Ki(){if(f(n.name))switch(i.contextualKeyword){case p._abstract:case p._declare:case p._enum:case p._interface:case p._module:case p._namespace:case p._type:return!0;default:break}return!1}function Il(e,t){if(f(n.colon)&&Qn(n.colon),!f(n.braceL)&&tt()){let r=i.tokens.length-1;for(;r>=0&&(i.tokens[r].start>=e||i.tokens[r].type===n._default||i.tokens[r].type===n._export);)i.tokens[r].isType=!0,r--;return}Qt(!1,t)}function Ol(e,t,r){if(!Te()&&v(n.bang)){i.tokens[i.tokens.length-1].type=n.nonNullAssertion;return}if(f(n.lessThan)||f(n.bitShiftL)){let s=i.snapshot();if(!t&&Ji()&&$1())return;if(Vi(),!t&&v(n.parenL)?(i.tokens[i.tokens.length-1].subscriptStartIndex=e,ct()):f(n.backQuote)?cs():(i.type===n.greaterThan||i.type!==n.parenL&&i.type&n.IS_EXPRESSION_START&&!Te())&&z(),i.error)i.restoreFromSnapshot(s);else return}else!t&&f(n.questionDot)&&fe()===n.lessThan&&(A(),i.tokens[e].isOptionalChainStart=!0,i.tokens[i.tokens.length-1].subscriptStartIndex=e,_n(),P(n.parenL),ct());er(e,t,r)}function Bl(){if(v(n._import))return N(p._type)&&fe()!==n.eq&&me(p._type),is(),!0;if(v(n.eq))return ve(),ue(),!0;if(_e(p._as))return me(p._namespace),H(),ue(),!0;if(N(p._type)){let e=fe();(e===n.braceL||e===n.star)&&A()}return!1}function Cl(){if(H(),f(n.comma)||f(n.braceR)){i.tokens[i.tokens.length-1].identifierRole=D.ImportDeclaration;return}if(H(),f(n.comma)||f(n.braceR)){i.tokens[i.tokens.length-1].identifierRole=D.ImportDeclaration,i.tokens[i.tokens.length-2].isType=!0,i.tokens[i.tokens.length-1].isType=!0;return}if(H(),f(n.comma)||f(n.braceR)){i.tokens[i.tokens.length-3].identifierRole=D.ImportAccess,i.tokens[i.tokens.length-1].identifierRole=D.ImportDeclaration;return}H(),i.tokens[i.tokens.length-3].identifierRole=D.ImportAccess,i.tokens[i.tokens.length-1].identifierRole=D.ImportDeclaration,i.tokens[i.tokens.length-4].isType=!0,i.tokens[i.tokens.length-3].isType=!0,i.tokens[i.tokens.length-2].isType=!0,i.tokens[i.tokens.length-1].isType=!0}function Ll(){if(H(),f(n.comma)||f(n.braceR)){i.tokens[i.tokens.length-1].identifierRole=D.ExportAccess;return}if(H(),f(n.comma)||f(n.braceR)){i.tokens[i.tokens.length-1].identifierRole=D.ExportAccess,i.tokens[i.tokens.length-2].isType=!0,i.tokens[i.tokens.length-1].isType=!0;return}if(H(),f(n.comma)||f(n.braceR)){i.tokens[i.tokens.length-3].identifierRole=D.ExportAccess;return}H(),i.tokens[i.tokens.length-3].identifierRole=D.ExportAccess,i.tokens[i.tokens.length-4].isType=!0,i.tokens[i.tokens.length-3].isType=!0,i.tokens[i.tokens.length-2].isType=!0,i.tokens[i.tokens.length-1].isType=!0}function Rl(){if(N(p._abstract)&&fe()===n._class)return i.type=n._abstract,A(),Et(!0,!0),!0;if(N(p._interface)){let e=J(2);return as(p._interface,!0),K(e),!0}return!1}function Fl(){if(i.type===n._const){let e=xt();if(e.type===n.name&&e.contextualKeyword===p._enum)return P(n._const),me(p._enum),i.tokens[i.tokens.length-1].type=n._enum,zi(),!0}return!1}function Nl(e){let t=i.tokens.length;Xn([p._abstract,p._readonly,p._declare,p._static,p._override]);let r=i.tokens.length;if(Sl()){let o=e?t-1:t;for(let a=o;a=C.length){z("Unterminated JSX contents");return}let r=C.charCodeAt(i.pos);if(r===g.lessThan||r===g.leftCurlyBrace){if(i.pos===i.start){if(r===g.lessThan){i.pos++,V(n.jsxTagStart);return}xi(r);return}e&&!t?V(n.jsxEmptyText):V(n.jsxText);return}r===g.lineFeed?e=!0:r!==g.space&&r!==g.carriageReturn&&r!==g.tab&&(t=!0),i.pos++}}function K1(e){for(i.pos++;;){if(i.pos>=C.length){z("Unterminated string constant");return}if(C.charCodeAt(i.pos)===e){i.pos++;break}i.pos++}V(n.string)}function J1(){let e;do{if(i.pos>C.length){z("Unexpectedly reached the end of input.");return}e=C.charCodeAt(++i.pos)}while(De[e]||e===g.dash);V(n.jsxName)}function Yi(){Je()}function Vl(e){if(Yi(),!v(n.colon)){i.tokens[i.tokens.length-1].identifierRole=e;return}Yi()}function Kl(){let e=i.tokens.length;Vl(D.Access);let t=!1;for(;f(n.dot);)t=!0,Je(),Yi();if(!t){let r=i.tokens[e],s=C.charCodeAt(r.start);s>=g.lowercaseA&&s<=g.lowercaseZ&&(r.identifierRole=null)}}function X1(){switch(i.type){case n.braceL:A(),ve(),Je();return;case n.jsxTagStart:Qi(),Je();return;case n.string:Je();return;default:z("JSX value should be either an expression or a quoted JSX text")}}function Y1(){P(n.ellipsis),ve()}function Q1(e){if(f(n.jsxTagEnd))return!1;Kl(),Y&&Pl();let t=!1;for(;!f(n.slash)&&!f(n.jsxTagEnd)&&!i.error;){if(v(n.braceL)){t=!0,P(n.ellipsis),xe(),Je();continue}t&&i.end-i.start===3&&C.charCodeAt(i.start)===g.lowercaseK&&C.charCodeAt(i.start+1)===g.lowercaseE&&C.charCodeAt(i.start+2)===g.lowercaseY&&(i.tokens[e].jsxRole=Ve.KeyAfterPropSpread),Vl(D.ObjectKey),f(n.eq)&&(Je(),X1())}let r=f(n.slash);return r&&Je(),r}function Z1(){f(n.jsxTagEnd)||Kl()}function Jl(){let e=i.tokens.length-1;i.tokens[e].jsxRole=Ve.NoChildren;let t=0;if(!Q1(e))for(vn();;)switch(i.type){case n.jsxTagStart:if(Je(),f(n.slash)){Je(),Z1(),i.tokens[e].jsxRole!==Ve.KeyAfterPropSpread&&(t===1?i.tokens[e].jsxRole=Ve.OneChild:t>1&&(i.tokens[e].jsxRole=Ve.StaticChildren));return}t++,Jl(),vn();break;case n.jsxText:t++,vn();break;case n.jsxEmptyText:vn();break;case n.braceL:A(),f(n.ellipsis)?(Y1(),vn(),t+=2):(f(n.braceR)||(t++,ve()),vn());break;default:z();return}}function Qi(){Je(),Jl()}function Je(){i.tokens.push(new Jt),wi(),i.start=i.pos;let e=C.charCodeAt(i.pos);if(vt[e])J1();else if(e===g.quotationMark||e===g.apostrophe)K1(e);else switch(++i.pos,e){case g.greaterThan:V(n.jsxTagEnd);break;case g.lessThan:V(n.jsxTagStart);break;case g.slash:V(n.slash);break;case g.equalsTo:V(n.eq);break;case g.leftCurlyBrace:V(n.braceL);break;case g.dot:V(n.dot);break;case g.colon:V(n.colon);break;default:z()}}function vn(){i.tokens.push(new Jt),i.start=i.pos,V1()}function Xl(e){if(f(n.question)){let t=fe();if(t===n.colon||t===n.comma||t===n.parenR)return}Zi(e)}function Yl(){Kr(n.question),f(n.colon)&&(Y?bn():te&&Pt())}var ea=class{constructor(t){this.stop=t}};function ve(e=!1){if(xe(e),f(n.comma))for(;v(n.comma);)xe(e)}function xe(e=!1,t=!1){return Y?$l(e,t):te?iu(e,t):nt(e,t)}function nt(e,t){if(f(n._yield))return mh(),!1;(f(n.parenL)||f(n.name)||f(n._yield))&&(i.potentialArrowAt=i.start);let r=eh(e);return t&&oa(),i.type&n.IS_ASSIGN?(A(),xe(e),!1):r}function eh(e){return nh(e)?!0:(th(e),!1)}function th(e){Y||te?Xl(e):Zi(e)}function Zi(e){v(n.question)&&(xe(),P(n.colon),xe(e))}function nh(e){let t=i.tokens.length;return wn()?!0:(ls(t,-1,e),!1)}function ls(e,t,r){if(Y&&(n._in&n.PRECEDENCE_MASK)>t&&!Te()&&(_e(p._as)||_e(p._satisfies))){let o=J(1);ye(),K(o),Jr(),ls(e,t,r);return}let s=i.type&n.PRECEDENCE_MASK;if(s>0&&(!r||!f(n._in))&&s>t){let o=i.type;A(),o===n.nullishCoalescing&&(i.tokens[i.tokens.length-1].nullishStartIndex=e);let a=i.tokens.length;wn(),ls(a,o&n.IS_RIGHT_ASSOCIATIVE?s-1:s,r),o===n.nullishCoalescing&&(i.tokens[e].numNullishCoalesceStarts++,i.tokens[i.tokens.length-1].numNullishCoalesceEnds++),ls(e,t,r)}}function wn(){if(Y&&!pn&&v(n.lessThan))return El(),!1;if(N(p._module)&&yi()===g.leftCurlyBrace&&!zr())return gh(),!1;if(i.type&n.IS_PREFIX)return A(),wn(),!1;if(ta())return!0;for(;i.type&n.IS_POSTFIX&&!Re();)i.type===n.preIncDec&&(i.type=n.postIncDec),A();return!1}function ta(){let e=i.tokens.length;return rt()?!0:(na(e),i.tokens.length>e&&i.tokens[e].isOptionalChainStart&&(i.tokens[i.tokens.length-1].isOptionalChainEnd=!0),!1)}function na(e,t=!1){te?cu(e,t):ra(e,t)}function ra(e,t=!1){let r=new ea(!1);do rh(e,t,r);while(!r.stop&&!i.error)}function rh(e,t,r){Y?Ol(e,t,r):te?nu(e,t,r):er(e,t,r)}function er(e,t,r){if(!t&&v(n.doubleColon))sa(),r.stop=!0,na(e,t);else if(f(n.questionDot)){if(i.tokens[e].isOptionalChainStart=!0,t&&fe()===n.parenL){r.stop=!0;return}A(),i.tokens[i.tokens.length-1].subscriptStartIndex=e,v(n.bracketL)?(ve(),P(n.bracketR)):v(n.parenL)?ct():us()}else if(v(n.dot))i.tokens[i.tokens.length-1].subscriptStartIndex=e,us();else if(v(n.bracketL))i.tokens[i.tokens.length-1].subscriptStartIndex=e,ve(),P(n.bracketR);else if(!t&&f(n.parenL))if(Ji()){let s=i.snapshot(),o=i.tokens.length;A(),i.tokens[i.tokens.length-1].subscriptStartIndex=e;let a=Kt();i.tokens[i.tokens.length-1].contextId=a,ct(),i.tokens[i.tokens.length-1].contextId=a,sh()&&(i.restoreFromSnapshot(s),r.stop=!0,i.scopeDepth++,kt(),oh(o))}else{A(),i.tokens[i.tokens.length-1].subscriptStartIndex=e;let s=Kt();i.tokens[i.tokens.length-1].contextId=s,ct(),i.tokens[i.tokens.length-1].contextId=s}else f(n.backQuote)?cs():r.stop=!0}function Ji(){return i.tokens[i.tokens.length-1].contextualKeyword===p._async&&!Re()}function ct(){let e=!0;for(;!v(n.parenR)&&!i.error;){if(e)e=!1;else if(P(n.comma),v(n.parenR))break;eu(!1)}}function sh(){return f(n.colon)||f(n.arrow)}function oh(e){Y?Hl():te&&ou(),P(n.arrow),Sn(e)}function sa(){let e=i.tokens.length;rt(),na(e,!0)}function rt(){if(v(n.modulo))return H(),!1;if(f(n.jsxText)||f(n.jsxEmptyText))return Ut(),!1;if(f(n.lessThan)&&pn)return i.type=n.jsxTagStart,Qi(),A(),!1;let e=i.potentialArrowAt===i.start;switch(i.type){case n.slash:case n.assign:Ic();case n._super:case n._this:case n.regexp:case n.num:case n.bigint:case n.decimal:case n.string:case n._null:case n._true:case n._false:return A(),!1;case n._import:return A(),f(n.dot)&&(i.tokens[i.tokens.length-1].type=n.name,A(),H()),!1;case n.name:{let t=i.tokens.length,r=i.start,s=i.contextualKeyword;return H(),s===p._await?(hh(),!1):s===p._async&&f(n._function)&&!Re()?(A(),St(r,!1),!1):e&&s===p._async&&!Re()&&f(n.name)?(i.scopeDepth++,at(!1),P(n.arrow),Sn(t),!0):f(n._do)&&!Re()?(A(),At(),!1):e&&!Re()&&f(n.arrow)?(i.scopeDepth++,rs(!1),P(n.arrow),Sn(t),!0):(i.tokens[i.tokens.length-1].identifierRole=D.Access,!1)}case n._do:return A(),At(),!1;case n.parenL:return Ql(e);case n.bracketL:return A(),Zl(n.bracketR,!0),!1;case n.braceL:return Yn(!1,!1),!1;case n._function:return ih(),!1;case n.at:hs();case n._class:return Et(!1),!1;case n._new:return ch(),!1;case n.backQuote:return cs(),!1;case n.doubleColon:return A(),sa(),!1;case n.hash:{let t=yi();return vt[t]||t===g.backslash?us():A(),!1}default:return z(),!1}}function us(){v(n.hash),H()}function ih(){let e=i.start;H(),v(n.dot)&&H(),St(e,!1)}function Ut(){A()}function nr(){P(n.parenL),ve(),P(n.parenR)}function Ql(e){let t=i.snapshot(),r=i.tokens.length;P(n.parenL);let s=!0;for(;!f(n.parenR)&&!i.error;){if(s)s=!1;else if(P(n.comma),f(n.parenR))break;if(f(n.ellipsis)){ji(!1),oa();break}else xe(!1,!0)}return P(n.parenR),e&&ah()&&fs()?(i.restoreFromSnapshot(t),i.scopeDepth++,kt(),fs(),Sn(r),i.error?(i.restoreFromSnapshot(t),Ql(!1),!1):!0):!1}function ah(){return f(n.colon)||!Re()}function fs(){return Y?zl():te?au():v(n.arrow)}function oa(){(Y||te)&&Yl()}function ch(){if(P(n._new),v(n.dot)){H();return}lh(),te&&ru(),v(n.parenL)&&Zl(n.parenR)}function lh(){sa(),v(n.questionDot)}function cs(){for(pt(),pt();!f(n.backQuote)&&!i.error;)P(n.dollarBraceL),ve(),pt(),pt();A()}function Yn(e,t){let r=Kt(),s=!0;for(A(),i.tokens[i.tokens.length-1].contextId=r;!v(n.braceR)&&!i.error;){if(s)s=!1;else if(P(n.comma),v(n.braceR))break;let o=!1;if(f(n.ellipsis)){let a=i.tokens.length;if(Mi(),e&&(i.tokens.length===a+2&&rs(t),v(n.braceR)))break;continue}e||(o=v(n.star)),!e&&N(p._async)?(o&&z(),H(),f(n.colon)||f(n.parenL)||f(n.braceR)||f(n.eq)||f(n.comma)||(f(n.star)&&(A(),o=!0),Yt(r))):Yt(r),ph(e,t,r)}i.tokens[i.tokens.length-1].contextId=r}function uh(e){return!e&&(f(n.string)||f(n.num)||f(n.bracketL)||f(n.name)||!!(i.type&n.IS_KEYWORD))}function fh(e,t){let r=i.start;return f(n.parenL)?(e&&z(),ds(r,!1),!0):uh(e)?(Yt(t),ds(r,!1),!0):!1}function dh(e,t){if(v(n.colon)){e?Vn(t):xe(!1);return}let r;e?i.scopeDepth===0?r=D.ObjectShorthandTopLevelDeclaration:t?r=D.ObjectShorthandBlockScopedDeclaration:r=D.ObjectShorthandFunctionScopedDeclaration:r=D.ObjectShorthand,i.tokens[i.tokens.length-1].identifierRole=r,Vn(t,!0)}function ph(e,t,r){Y?Wl():te&&su(),fh(e,r)||dh(e,t)}function Yt(e){te&&ps(),v(n.bracketL)?(i.tokens[i.tokens.length-1].contextId=e,xe(),P(n.bracketR),i.tokens[i.tokens.length-1].contextId=e):(f(n.num)||f(n.string)||f(n.bigint)||f(n.decimal)?rt():us(),i.tokens[i.tokens.length-1].identifierRole=D.ObjectKey,i.tokens[i.tokens.length-1].contextId=e)}function ds(e,t){let r=Kt();i.scopeDepth++;let s=i.tokens.length;kt(t,r),ia(e,r);let a=i.tokens.length;i.scopes.push(new We(s,a,!0)),i.scopeDepth--}function Sn(e){Qt(!0);let t=i.tokens.length;i.scopes.push(new We(e,t,!0)),i.scopeDepth--}function ia(e,t=0){Y?Il(e,t):te?tu(t):Qt(!1,t)}function Qt(e,t=0){e&&!f(n.braceL)?xe():At(!0,t)}function Zl(e,t=!1){let r=!0;for(;!v(e)&&!i.error;){if(r)r=!1;else if(P(n.comma),v(e))break;eu(t)}}function eu(e){e&&f(n.comma)||(f(n.ellipsis)?(Mi(),oa()):f(n.question)?A():xe(!1,!0))}function H(){A(),i.tokens[i.tokens.length-1].type=n.name}function hh(){wn()}function mh(){A(),!f(n.semi)&&!Re()&&(v(n.star),xe())}function gh(){me(p._module),P(n.braceL),xn(n.braceR)}function yh(e){return(e.type===n.name||!!(e.type&n.IS_KEYWORD))&&e.contextualKeyword!==p._from}function gt(e){let t=J(0);P(e||n.colon),qe(),K(t)}function lu(){P(n.modulo),me(p._checks),v(n.parenL)&&(ve(),P(n.parenR))}function la(){let e=J(0);P(n.colon),f(n.modulo)?lu():(qe(),f(n.modulo)&&lu()),K(e)}function bh(){A(),ua(!0)}function _h(){A(),H(),f(n.lessThan)&&st(),P(n.parenL),ca(),P(n.parenR),la(),ue()}function aa(){f(n._class)?bh():f(n._function)?_h():f(n._var)?wh():_e(p._module)?v(n.dot)?Sh():xh():N(p._type)?kh():N(p._opaque)?Eh():N(p._interface)?Ph():f(n._export)?vh():z()}function wh(){A(),mu(),ue()}function xh(){for(f(n.string)?rt():H(),P(n.braceL);!f(n.braceR)&&!i.error;)f(n._import)?(A(),ya()):z();P(n.braceR)}function vh(){P(n._export),v(n._default)?f(n._function)||f(n._class)?aa():(qe(),ue()):f(n._var)||f(n._function)||f(n._class)||N(p._opaque)?aa():f(n.star)||f(n.braceL)||N(p._interface)||N(p._type)||N(p._opaque)?ga():z()}function Sh(){me(p._exports),Pt(),ue()}function kh(){A(),da()}function Eh(){A(),pa(!0)}function Ph(){A(),ua()}function ua(e=!1){if(_s(),f(n.lessThan)&&st(),v(n._extends))do ms();while(!e&&v(n.comma));if(N(p._mixins)){A();do ms();while(v(n.comma))}if(N(p._implements)){A();do ms();while(v(n.comma))}gs(e,!1,e)}function ms(){du(!1),f(n.lessThan)&&Zt()}function fa(){ua()}function _s(){H()}function da(){_s(),f(n.lessThan)&&st(),gt(n.eq),ue()}function pa(e){me(p._type),_s(),f(n.lessThan)&&st(),f(n.colon)&>(n.colon),e||gt(n.eq),ue()}function Ah(){ps(),mu(),v(n.eq)&&qe()}function st(){let e=J(0);f(n.lessThan)||f(n.typeParameterStart)?A():z();do Ah(),f(n.greaterThan)||P(n.comma);while(!f(n.greaterThan)&&!i.error);P(n.greaterThan),K(e)}function Zt(){let e=J(0);for(P(n.lessThan);!f(n.greaterThan)&&!i.error;)qe(),f(n.greaterThan)||P(n.comma);P(n.greaterThan),K(e)}function Th(){if(me(p._interface),v(n._extends))do ms();while(v(n.comma));gs(!1,!1,!1)}function ha(){f(n.num)||f(n.string)?rt():H()}function Ih(){fe()===n.colon?(ha(),gt()):qe(),P(n.bracketR),gt()}function Oh(){ha(),P(n.bracketR),P(n.bracketR),f(n.lessThan)||f(n.parenL)?ma():(v(n.question),gt())}function ma(){for(f(n.lessThan)&&st(),P(n.parenL);!f(n.parenR)&&!f(n.ellipsis)&&!i.error;)ys(),f(n.parenR)||P(n.comma);v(n.ellipsis)&&ys(),P(n.parenR),gt()}function Bh(){ma()}function gs(e,t,r){let s;for(t&&f(n.braceBarL)?(P(n.braceBarL),s=n.braceBarR):(P(n.braceL),s=n.braceR);!f(s)&&!i.error;){if(r&&N(p._proto)){let o=fe();o!==n.colon&&o!==n.question&&(A(),e=!1)}if(e&&N(p._static)){let o=fe();o!==n.colon&&o!==n.question&&A()}if(ps(),v(n.bracketL))v(n.bracketL)?Oh():Ih();else if(f(n.parenL)||f(n.lessThan))Bh();else{if(N(p._get)||N(p._set)){let o=fe();(o===n.name||o===n.string||o===n.num)&&A()}Ch()}Lh()}P(s)}function Ch(){if(f(n.ellipsis)){if(P(n.ellipsis),v(n.comma)||v(n.semi),f(n.braceR))return;qe()}else ha(),f(n.lessThan)||f(n.parenL)?ma():(v(n.question),gt())}function Lh(){!v(n.semi)&&!v(n.comma)&&!f(n.braceR)&&!f(n.braceBarR)&&z()}function du(e){for(e||H();v(n.dot);)H()}function Rh(){du(!0),f(n.lessThan)&&Zt()}function Fh(){P(n._typeof),pu()}function Nh(){for(P(n.bracketL);i.pos0&&t0?this.tokens[this.tokenIndex-1].end:0,this.tokenIndex0&&this.tokenAtRelativeIndex(-1).type===n._delete?t.isAsyncOperation?this.resultCode+=this.helperManager.getHelperName("asyncOptionalChainDelete"):this.resultCode+=this.helperManager.getHelperName("optionalChainDelete"):t.isAsyncOperation?this.resultCode+=this.helperManager.getHelperName("asyncOptionalChain"):this.resultCode+=this.helperManager.getHelperName("optionalChain"),this.resultCode+="([")}}appendTokenSuffix(){let t=this.currentToken();if(t.isOptionalChainEnd&&!this.disableESTransforms&&(this.resultCode+="])"),t.numNullishCoalesceEnds&&!this.disableESTransforms)for(let r=0;r ${r}require`);let s=this.tokens.currentToken().contextId;if(s==null)throw new Error("Expected context ID on dynamic import invocation.");for(this.tokens.copyToken();!this.tokens.matchesContextIdAndLabel(n.parenR,s);)this.rootTransformer.processToken();this.tokens.replaceToken(r?")))":"))");return}if(this.removeImportAndDetectIfShouldElide())this.tokens.removeToken();else{let r=this.tokens.stringValue();this.tokens.replaceTokenTrimmingLeftWhitespace(this.importProcessor.claimImportCode(r)),this.tokens.appendCode(this.importProcessor.claimImportCode(r))}Pt(this.tokens),this.tokens.matches1(n.semi)&&this.tokens.removeToken()}removeImportAndDetectIfShouldElide(){if(this.tokens.removeInitialToken(),this.tokens.matchesContextual(d._type)&&!this.tokens.matches1AtIndex(this.tokens.currentIndex()+1,n.comma)&&!this.tokens.matchesContextualAtIndex(this.tokens.currentIndex()+1,d._from))return this.removeRemainingImport(),!0;if(this.tokens.matches1(n.name)||this.tokens.matches1(n.star))return this.removeRemainingImport(),!1;if(this.tokens.matches1(n.string))return!1;let t=!1,r=!1;for(;!this.tokens.matches1(n.string);)(!t&&this.tokens.matches1(n.braceL)||this.tokens.matches1(n.comma))&&(this.tokens.removeToken(),this.tokens.matches1(n.braceR)||(r=!0),(this.tokens.matches2(n.name,n.comma)||this.tokens.matches2(n.name,n.braceR)||this.tokens.matches4(n.name,n.name,n.name,n.comma)||this.tokens.matches4(n.name,n.name,n.name,n.braceR))&&(t=!0)),this.tokens.removeToken();return this.keepUnusedImports?!1:this.isTypeScriptTransformEnabled?!t:this.isFlowTransformEnabled?r&&!t:!1}removeRemainingImport(){for(;!this.tokens.matches1(n.string);)this.tokens.removeToken()}processIdentifier(){let t=this.tokens.currentToken();if(t.shadowsGlobal)return!1;if(t.identifierRole===F.ObjectShorthand)return this.processObjectShorthand();if(t.identifierRole!==F.Access)return!1;let r=this.importProcessor.getIdentifierReplacement(this.tokens.identifierNameForToken(t));if(!r)return!1;let s=this.tokens.currentIndex()+1;for(;s=2&&this.tokens.matches1AtIndex(t-2,n.dot)||t>=2&&[n._var,n._let,n._const].includes(this.tokens.tokens[t-2].type))return!1;let s=this.importProcessor.resolveExportBinding(this.tokens.identifierNameForToken(r));return s?(this.tokens.copyToken(),this.tokens.appendCode(` ${s} =`),!0):!1}processComplexAssignment(){let t=this.tokens.currentIndex(),r=this.tokens.tokens[t-1];if(r.type!==n.name||r.shadowsGlobal||t>=2&&this.tokens.matches1AtIndex(t-2,n.dot))return!1;let s=this.importProcessor.resolveExportBinding(this.tokens.identifierNameForToken(r));return s?(this.tokens.appendCode(` = ${s}`),this.tokens.copyToken(),!0):!1}processPreIncDec(){let t=this.tokens.currentIndex(),r=this.tokens.tokens[t+1];if(r.type!==n.name||r.shadowsGlobal||t+2=1&&this.tokens.matches1AtIndex(t-1,n.dot))return!1;let o=this.tokens.identifierNameForToken(r),a=this.importProcessor.resolveExportBinding(o);if(!a)return!1;let c=this.tokens.rawCodeForToken(s),l=this.importProcessor.getIdentifierReplacement(o)||o;if(c==="++")this.tokens.replaceToken(`(${l} = ${a} = ${l} + 1, ${l} - 1)`);else if(c==="--")this.tokens.replaceToken(`(${l} = ${a} = ${l} - 1, ${l} + 1)`);else throw new Error(`Unexpected operator: ${c}`);return this.tokens.removeToken(),!0}processExportDefault(){let t=!0;if(this.tokens.matches4(n._export,n._default,n._function,n.name)||this.tokens.matches5(n._export,n._default,n.name,n._function,n.name)&&this.tokens.matchesContextualAtIndex(this.tokens.currentIndex()+2,d._async)){this.tokens.removeInitialToken(),this.tokens.removeToken();let r=this.processNamedFunction();this.tokens.appendCode(` exports.default = ${r};`)}else if(this.tokens.matches4(n._export,n._default,n._class,n.name)||this.tokens.matches5(n._export,n._default,n._abstract,n._class,n.name)||this.tokens.matches3(n._export,n._default,n.at)){this.tokens.removeInitialToken(),this.tokens.removeToken(),this.copyDecorators(),this.tokens.matches1(n._abstract)&&this.tokens.removeToken();let r=this.rootTransformer.processNamedClass();this.tokens.appendCode(` exports.default = ${r};`)}else if(lr(this.isTypeScriptTransformEnabled,this.keepUnusedImports,this.tokens,this.declarationInfo))t=!1,this.tokens.removeInitialToken(),this.tokens.removeToken(),this.tokens.removeToken();else if(this.reactHotLoaderTransformer){let r=this.nameManager.claimFreeName("_default");this.tokens.replaceToken(`let ${r}; exports.`),this.tokens.copyToken(),this.tokens.appendCode(` = ${r} =`),this.reactHotLoaderTransformer.setExtractedDefaultExportName(r)}else this.tokens.replaceToken("exports."),this.tokens.copyToken(),this.tokens.appendCode(" =");t&&(this.hadDefaultExport=!0)}copyDecorators(){for(;this.tokens.matches1(n.at);)if(this.tokens.copyToken(),this.tokens.matches1(n.parenL))this.tokens.copyExpectedToken(n.parenL),this.rootTransformer.processBalancedCode(),this.tokens.copyExpectedToken(n.parenR);else{for(this.tokens.copyExpectedToken(n.name);this.tokens.matches1(n.dot);)this.tokens.copyExpectedToken(n.dot),this.tokens.copyExpectedToken(n.name);this.tokens.matches1(n.parenL)&&(this.tokens.copyExpectedToken(n.parenL),this.rootTransformer.processBalancedCode(),this.tokens.copyExpectedToken(n.parenR))}}processExportVar(){this.isSimpleExportVar()?this.processSimpleExportVar():this.processComplexExportVar()}isSimpleExportVar(){let t=this.tokens.currentIndex();if(t++,t++,!this.tokens.matches1AtIndex(t,n.name))return!1;for(t++;tr.call(t,...c)),t=void 0)}return r}var Es="jest",Im=["mock","unmock","enableAutomock","disableAutomock"],dr=class e extends ye{__init(){this.hoistedFunctionNames=[]}constructor(t,r,s,o){super(),this.rootTransformer=t,this.tokens=r,this.nameManager=s,this.importProcessor=o,e.prototype.__init.call(this)}process(){return this.tokens.currentToken().scopeDepth===0&&this.tokens.matches4(n.name,n.dot,n.name,n.parenL)&&this.tokens.identifierName()===Es?Pm([this,"access",t=>t.importProcessor,"optionalAccess",t=>t.getGlobalNames,"call",t=>t(),"optionalAccess",t=>t.has,"call",t=>t(Es)])?!1:this.extractHoistedCalls():!1}getHoistedCode(){return this.hoistedFunctionNames.length>0?this.hoistedFunctionNames.map(t=>`${t}();`).join(""):""}extractHoistedCalls(){this.tokens.removeToken();let t=!1;for(;this.tokens.matches3(n.dot,n.name,n.parenL);){let r=this.tokens.identifierNameAtIndex(this.tokens.currentIndex()+1);if(Im.includes(r)){let o=this.nameManager.claimFreeName("__jestHoist");this.hoistedFunctionNames.push(o),this.tokens.replaceToken(`function ${o}(){${Es}.`),this.tokens.copyToken(),this.tokens.copyToken(),this.rootTransformer.processBalancedCode(),this.tokens.copyExpectedToken(n.parenR),this.tokens.appendCode(";}"),t=!1}else t?this.tokens.copyToken():this.tokens.replaceToken(`${Es}.`),this.tokens.copyToken(),this.tokens.copyToken(),this.rootTransformer.processBalancedCode(),this.tokens.copyExpectedToken(n.parenR),t=!0}return!0}};var hr=class extends ye{constructor(t){super(),this.tokens=t}process(){if(this.tokens.matches1(n.num)){let t=this.tokens.currentTokenCode();if(t.includes("_"))return this.tokens.replaceToken(t.replace(/_/g,"")),!0}return!1}};var mr=class extends ye{constructor(t,r){super(),this.tokens=t,this.nameManager=r}process(){return this.tokens.matches2(n._catch,n.braceL)?(this.tokens.copyToken(),this.tokens.appendCode(` (${this.nameManager.claimFreeName("e")})`),!0):!1}};var yr=class extends ye{constructor(t,r){super(),this.tokens=t,this.nameManager=r}process(){if(this.tokens.matches1(n.nullishCoalescing)){let s=this.tokens.currentToken();return this.tokens.tokens[s.nullishStartIndex].isAsyncOperation?this.tokens.replaceTokenTrimmingLeftWhitespace(", async () => ("):this.tokens.replaceTokenTrimmingLeftWhitespace(", () => ("),!0}if(this.tokens.matches1(n._delete)&&this.tokens.tokenAtRelativeIndex(1).isOptionalChainStart)return this.tokens.removeInitialToken(),!0;let r=this.tokens.currentToken().subscriptStartIndex;if(r!=null&&this.tokens.tokens[r].isOptionalChainStart&&this.tokens.tokenAtRelativeIndex(-1).type!==n._super){let s=this.nameManager.claimFreeName("_"),o;if(r>0&&this.tokens.matches1AtIndex(r-1,n._delete)&&this.isLastSubscriptInChain()?o=`${s} => delete ${s}`:o=`${s} => ${s}`,this.tokens.tokens[r].isAsyncOperation&&(o=`async ${o}`),this.tokens.matches2(n.questionDot,n.parenL)||this.tokens.matches2(n.questionDot,n.lessThan))this.justSkippedSuper()&&this.tokens.appendCode(".bind(this)"),this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalCall', ${o}`);else if(this.tokens.matches2(n.questionDot,n.bracketL))this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalAccess', ${o}`);else if(this.tokens.matches1(n.questionDot))this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalAccess', ${o}.`);else if(this.tokens.matches1(n.dot))this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'access', ${o}.`);else if(this.tokens.matches1(n.bracketL))this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'access', ${o}[`);else if(this.tokens.matches1(n.parenL))this.justSkippedSuper()&&this.tokens.appendCode(".bind(this)"),this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'call', ${o}(`);else throw new Error("Unexpected subscript operator in optional chain.");return!0}return!1}isLastSubscriptInChain(){let t=0;for(let r=this.tokens.currentIndex()+1;;r++){if(r>=this.tokens.tokens.length)throw new Error("Reached the end of the code while finding the end of the access chain.");if(this.tokens.tokens[r].isOptionalChainStart?t++:this.tokens.tokens[r].isOptionalChainEnd&&t--,t<0)return!0;if(t===0&&this.tokens.tokens[r].subscriptStartIndex!=null)return!1}}justSkippedSuper(){let t=0,r=this.tokens.currentIndex()-1;for(;;){if(r<0)throw new Error("Reached the start of the code while finding the start of the access chain.");if(this.tokens.tokens[r].isOptionalChainStart?t--:this.tokens.tokens[r].isOptionalChainEnd&&t++,t<0)return!1;if(t===0&&this.tokens.tokens[r].subscriptStartIndex!=null)return this.tokens.tokens[r-1].type===n._super;r--}}};var gr=class extends ye{constructor(t,r,s,o){super(),this.rootTransformer=t,this.tokens=r,this.importProcessor=s,this.options=o}process(){let t=this.tokens.currentIndex();if(this.tokens.identifierName()==="createReactClass"){let r=this.importProcessor&&this.importProcessor.getIdentifierReplacement("createReactClass");return r?this.tokens.replaceToken(`(0, ${r})`):this.tokens.copyToken(),this.tryProcessCreateClassCall(t),!0}if(this.tokens.matches3(n.name,n.dot,n.name)&&this.tokens.identifierName()==="React"&&this.tokens.identifierNameAtIndex(this.tokens.currentIndex()+2)==="createClass"){let r=this.importProcessor&&this.importProcessor.getIdentifierReplacement("React")||"React";return r?(this.tokens.replaceToken(r),this.tokens.copyToken(),this.tokens.copyToken()):(this.tokens.copyToken(),this.tokens.copyToken(),this.tokens.copyToken()),this.tryProcessCreateClassCall(t),!0}return!1}tryProcessCreateClassCall(t){let r=this.findDisplayName(t);r&&this.classNeedsDisplayName()&&(this.tokens.copyExpectedToken(n.parenL),this.tokens.copyExpectedToken(n.braceL),this.tokens.appendCode(`displayName: '${r}',`),this.rootTransformer.processBalancedCode(),this.tokens.copyExpectedToken(n.braceR),this.tokens.copyExpectedToken(n.parenR))}findDisplayName(t){return t<2?null:this.tokens.matches2AtIndex(t-2,n.name,n.eq)?this.tokens.identifierNameAtIndex(t-2):t>=2&&this.tokens.tokens[t-2].identifierRole===F.ObjectKey?this.tokens.identifierNameAtIndex(t-2):this.tokens.matches2AtIndex(t-2,n._export,n._default)?this.getDisplayNameFromFilename():null}getDisplayNameFromFilename(){let r=(this.options.filePath||"unknown").split("/"),s=r[r.length-1],o=s.lastIndexOf("."),a=o===-1?s:s.slice(0,o);return a==="index"&&r[r.length-2]?r[r.length-2]:a}classNeedsDisplayName(){let t=this.tokens.currentIndex();if(!this.tokens.matches2(n.parenL,n.braceL))return!1;let r=t+1,s=this.tokens.tokens[r].contextId;if(s==null)throw new Error("Expected non-null context ID on object open-brace.");for(;t ${r}require`);let s=this.tokens.currentToken().contextId;if(s==null)throw new Error("Expected context ID on dynamic import invocation.");for(this.tokens.copyToken();!this.tokens.matchesContextIdAndLabel(n.parenR,s);)this.rootTransformer.processToken();this.tokens.replaceToken(r?")))":"))");return}if(this.removeImportAndDetectIfShouldElide())this.tokens.removeToken();else{let r=this.tokens.stringValue();this.tokens.replaceTokenTrimmingLeftWhitespace(this.importProcessor.claimImportCode(r)),this.tokens.appendCode(this.importProcessor.claimImportCode(r))}Tt(this.tokens),this.tokens.matches1(n.semi)&&this.tokens.removeToken()}removeImportAndDetectIfShouldElide(){if(this.tokens.removeInitialToken(),this.tokens.matchesContextual(p._type)&&!this.tokens.matches1AtIndex(this.tokens.currentIndex()+1,n.comma)&&!this.tokens.matchesContextualAtIndex(this.tokens.currentIndex()+1,p._from))return this.removeRemainingImport(),!0;if(this.tokens.matches1(n.name)||this.tokens.matches1(n.star))return this.removeRemainingImport(),!1;if(this.tokens.matches1(n.string))return!1;let t=!1,r=!1;for(;!this.tokens.matches1(n.string);)(!t&&this.tokens.matches1(n.braceL)||this.tokens.matches1(n.comma))&&(this.tokens.removeToken(),this.tokens.matches1(n.braceR)||(r=!0),(this.tokens.matches2(n.name,n.comma)||this.tokens.matches2(n.name,n.braceR)||this.tokens.matches4(n.name,n.name,n.name,n.comma)||this.tokens.matches4(n.name,n.name,n.name,n.braceR))&&(t=!0)),this.tokens.removeToken();return this.keepUnusedImports?!1:this.isTypeScriptTransformEnabled?!t:this.isFlowTransformEnabled?r&&!t:!1}removeRemainingImport(){for(;!this.tokens.matches1(n.string);)this.tokens.removeToken()}processIdentifier(){let t=this.tokens.currentToken();if(t.shadowsGlobal)return!1;if(t.identifierRole===D.ObjectShorthand)return this.processObjectShorthand();if(t.identifierRole!==D.Access)return!1;let r=this.importProcessor.getIdentifierReplacement(this.tokens.identifierNameForToken(t));if(!r)return!1;let s=this.tokens.currentIndex()+1;for(;s=2&&this.tokens.matches1AtIndex(t-2,n.dot)||t>=2&&[n._var,n._let,n._const].includes(this.tokens.tokens[t-2].type))return!1;let s=this.importProcessor.resolveExportBinding(this.tokens.identifierNameForToken(r));return s?(this.tokens.copyToken(),this.tokens.appendCode(` ${s} =`),!0):!1}processComplexAssignment(){let t=this.tokens.currentIndex(),r=this.tokens.tokens[t-1];if(r.type!==n.name||r.shadowsGlobal||t>=2&&this.tokens.matches1AtIndex(t-2,n.dot))return!1;let s=this.importProcessor.resolveExportBinding(this.tokens.identifierNameForToken(r));return s?(this.tokens.appendCode(` = ${s}`),this.tokens.copyToken(),!0):!1}processPreIncDec(){let t=this.tokens.currentIndex(),r=this.tokens.tokens[t+1];if(r.type!==n.name||r.shadowsGlobal||t+2=1&&this.tokens.matches1AtIndex(t-1,n.dot))return!1;let o=this.tokens.identifierNameForToken(r),a=this.importProcessor.resolveExportBinding(o);if(!a)return!1;let c=this.tokens.rawCodeForToken(s),l=this.importProcessor.getIdentifierReplacement(o)||o;if(c==="++")this.tokens.replaceToken(`(${l} = ${a} = ${l} + 1, ${l} - 1)`);else if(c==="--")this.tokens.replaceToken(`(${l} = ${a} = ${l} - 1, ${l} + 1)`);else throw new Error(`Unexpected operator: ${c}`);return this.tokens.removeToken(),!0}processExportDefault(){let t=!0;if(this.tokens.matches4(n._export,n._default,n._function,n.name)||this.tokens.matches5(n._export,n._default,n.name,n._function,n.name)&&this.tokens.matchesContextualAtIndex(this.tokens.currentIndex()+2,p._async)){this.tokens.removeInitialToken(),this.tokens.removeToken();let r=this.processNamedFunction();this.tokens.appendCode(` exports.default = ${r};`)}else if(this.tokens.matches4(n._export,n._default,n._class,n.name)||this.tokens.matches5(n._export,n._default,n._abstract,n._class,n.name)||this.tokens.matches3(n._export,n._default,n.at)){this.tokens.removeInitialToken(),this.tokens.removeToken(),this.copyDecorators(),this.tokens.matches1(n._abstract)&&this.tokens.removeToken();let r=this.rootTransformer.processNamedClass();this.tokens.appendCode(` exports.default = ${r};`)}else if(lr(this.isTypeScriptTransformEnabled,this.keepUnusedImports,this.tokens,this.declarationInfo))t=!1,this.tokens.removeInitialToken(),this.tokens.removeToken(),this.tokens.removeToken();else if(this.reactHotLoaderTransformer){let r=this.nameManager.claimFreeName("_default");this.tokens.replaceToken(`let ${r}; exports.`),this.tokens.copyToken(),this.tokens.appendCode(` = ${r} =`),this.reactHotLoaderTransformer.setExtractedDefaultExportName(r)}else this.tokens.replaceToken("exports."),this.tokens.copyToken(),this.tokens.appendCode(" =");t&&(this.hadDefaultExport=!0)}copyDecorators(){for(;this.tokens.matches1(n.at);)if(this.tokens.copyToken(),this.tokens.matches1(n.parenL))this.tokens.copyExpectedToken(n.parenL),this.rootTransformer.processBalancedCode(),this.tokens.copyExpectedToken(n.parenR);else{for(this.tokens.copyExpectedToken(n.name);this.tokens.matches1(n.dot);)this.tokens.copyExpectedToken(n.dot),this.tokens.copyExpectedToken(n.name);this.tokens.matches1(n.parenL)&&(this.tokens.copyExpectedToken(n.parenL),this.rootTransformer.processBalancedCode(),this.tokens.copyExpectedToken(n.parenR))}}processExportVar(){this.isSimpleExportVar()?this.processSimpleExportVar():this.processComplexExportVar()}isSimpleExportVar(){let t=this.tokens.currentIndex();if(t++,t++,!this.tokens.matches1AtIndex(t,n.name))return!1;for(t++;tr.call(t,...c)),t=void 0)}return r}var Es="jest",Om=["mock","unmock","enableAutomock","disableAutomock"],pr=class e extends ge{__init(){this.hoistedFunctionNames=[]}constructor(t,r,s,o){super(),this.rootTransformer=t,this.tokens=r,this.nameManager=s,this.importProcessor=o,e.prototype.__init.call(this)}process(){return this.tokens.currentToken().scopeDepth===0&&this.tokens.matches4(n.name,n.dot,n.name,n.parenL)&&this.tokens.identifierName()===Es?Im([this,"access",t=>t.importProcessor,"optionalAccess",t=>t.getGlobalNames,"call",t=>t(),"optionalAccess",t=>t.has,"call",t=>t(Es)])?!1:this.extractHoistedCalls():!1}getHoistedCode(){return this.hoistedFunctionNames.length>0?this.hoistedFunctionNames.map(t=>`${t}();`).join(""):""}extractHoistedCalls(){this.tokens.removeToken();let t=!1;for(;this.tokens.matches3(n.dot,n.name,n.parenL);){let r=this.tokens.identifierNameAtIndex(this.tokens.currentIndex()+1);if(Om.includes(r)){let o=this.nameManager.claimFreeName("__jestHoist");this.hoistedFunctionNames.push(o),this.tokens.replaceToken(`function ${o}(){${Es}.`),this.tokens.copyToken(),this.tokens.copyToken(),this.rootTransformer.processBalancedCode(),this.tokens.copyExpectedToken(n.parenR),this.tokens.appendCode(";}"),t=!1}else t?this.tokens.copyToken():this.tokens.replaceToken(`${Es}.`),this.tokens.copyToken(),this.tokens.copyToken(),this.rootTransformer.processBalancedCode(),this.tokens.copyExpectedToken(n.parenR),t=!0}return!0}};var hr=class extends ge{constructor(t){super(),this.tokens=t}process(){if(this.tokens.matches1(n.num)){let t=this.tokens.currentTokenCode();if(t.includes("_"))return this.tokens.replaceToken(t.replace(/_/g,"")),!0}return!1}};var mr=class extends ge{constructor(t,r){super(),this.tokens=t,this.nameManager=r}process(){return this.tokens.matches2(n._catch,n.braceL)?(this.tokens.copyToken(),this.tokens.appendCode(` (${this.nameManager.claimFreeName("e")})`),!0):!1}};var gr=class extends ge{constructor(t,r){super(),this.tokens=t,this.nameManager=r}process(){if(this.tokens.matches1(n.nullishCoalescing)){let s=this.tokens.currentToken();return this.tokens.tokens[s.nullishStartIndex].isAsyncOperation?this.tokens.replaceTokenTrimmingLeftWhitespace(", async () => ("):this.tokens.replaceTokenTrimmingLeftWhitespace(", () => ("),!0}if(this.tokens.matches1(n._delete)&&this.tokens.tokenAtRelativeIndex(1).isOptionalChainStart)return this.tokens.removeInitialToken(),!0;let r=this.tokens.currentToken().subscriptStartIndex;if(r!=null&&this.tokens.tokens[r].isOptionalChainStart&&this.tokens.tokenAtRelativeIndex(-1).type!==n._super){let s=this.nameManager.claimFreeName("_"),o;if(r>0&&this.tokens.matches1AtIndex(r-1,n._delete)&&this.isLastSubscriptInChain()?o=`${s} => delete ${s}`:o=`${s} => ${s}`,this.tokens.tokens[r].isAsyncOperation&&(o=`async ${o}`),this.tokens.matches2(n.questionDot,n.parenL)||this.tokens.matches2(n.questionDot,n.lessThan))this.justSkippedSuper()&&this.tokens.appendCode(".bind(this)"),this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalCall', ${o}`);else if(this.tokens.matches2(n.questionDot,n.bracketL))this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalAccess', ${o}`);else if(this.tokens.matches1(n.questionDot))this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'optionalAccess', ${o}.`);else if(this.tokens.matches1(n.dot))this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'access', ${o}.`);else if(this.tokens.matches1(n.bracketL))this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'access', ${o}[`);else if(this.tokens.matches1(n.parenL))this.justSkippedSuper()&&this.tokens.appendCode(".bind(this)"),this.tokens.replaceTokenTrimmingLeftWhitespace(`, 'call', ${o}(`);else throw new Error("Unexpected subscript operator in optional chain.");return!0}return!1}isLastSubscriptInChain(){let t=0;for(let r=this.tokens.currentIndex()+1;;r++){if(r>=this.tokens.tokens.length)throw new Error("Reached the end of the code while finding the end of the access chain.");if(this.tokens.tokens[r].isOptionalChainStart?t++:this.tokens.tokens[r].isOptionalChainEnd&&t--,t<0)return!0;if(t===0&&this.tokens.tokens[r].subscriptStartIndex!=null)return!1}}justSkippedSuper(){let t=0,r=this.tokens.currentIndex()-1;for(;;){if(r<0)throw new Error("Reached the start of the code while finding the start of the access chain.");if(this.tokens.tokens[r].isOptionalChainStart?t--:this.tokens.tokens[r].isOptionalChainEnd&&t++,t<0)return!1;if(t===0&&this.tokens.tokens[r].subscriptStartIndex!=null)return this.tokens.tokens[r-1].type===n._super;r--}}};var yr=class extends ge{constructor(t,r,s,o){super(),this.rootTransformer=t,this.tokens=r,this.importProcessor=s,this.options=o}process(){let t=this.tokens.currentIndex();if(this.tokens.identifierName()==="createReactClass"){let r=this.importProcessor&&this.importProcessor.getIdentifierReplacement("createReactClass");return r?this.tokens.replaceToken(`(0, ${r})`):this.tokens.copyToken(),this.tryProcessCreateClassCall(t),!0}if(this.tokens.matches3(n.name,n.dot,n.name)&&this.tokens.identifierName()==="React"&&this.tokens.identifierNameAtIndex(this.tokens.currentIndex()+2)==="createClass"){let r=this.importProcessor&&this.importProcessor.getIdentifierReplacement("React")||"React";return r?(this.tokens.replaceToken(r),this.tokens.copyToken(),this.tokens.copyToken()):(this.tokens.copyToken(),this.tokens.copyToken(),this.tokens.copyToken()),this.tryProcessCreateClassCall(t),!0}return!1}tryProcessCreateClassCall(t){let r=this.findDisplayName(t);r&&this.classNeedsDisplayName()&&(this.tokens.copyExpectedToken(n.parenL),this.tokens.copyExpectedToken(n.braceL),this.tokens.appendCode(`displayName: '${r}',`),this.rootTransformer.processBalancedCode(),this.tokens.copyExpectedToken(n.braceR),this.tokens.copyExpectedToken(n.parenR))}findDisplayName(t){return t<2?null:this.tokens.matches2AtIndex(t-2,n.name,n.eq)?this.tokens.identifierNameAtIndex(t-2):t>=2&&this.tokens.tokens[t-2].identifierRole===D.ObjectKey?this.tokens.identifierNameAtIndex(t-2):this.tokens.matches2AtIndex(t-2,n._export,n._default)?this.getDisplayNameFromFilename():null}getDisplayNameFromFilename(){let r=(this.options.filePath||"unknown").split("/"),s=r[r.length-1],o=s.lastIndexOf("."),a=o===-1?s:s.slice(0,o);return a==="index"&&r[r.length-2]?r[r.length-2]:a}classNeedsDisplayName(){let t=this.tokens.currentIndex();if(!this.tokens.matches2(n.parenL,n.braceL))return!1;let r=t+1,s=this.tokens.tokens[r].contextId;if(s==null)throw new Error("Expected non-null context ID on object open-brace.");for(;t({variableName:s,uniqueLocalName:s}));return this.extractedDefaultExportName&&r.push({variableName:this.extractedDefaultExportName,uniqueLocalName:"default"}),` + })();`.replace(/\s+/g," ").trim()}getSuffixCode(){let t=new Set;for(let s of this.tokens.tokens)!s.isType&&Vr(s)&&s.identifierRole!==D.ImportDeclaration&&t.add(this.tokens.identifierNameForToken(s));let r=Array.from(t).map(s=>({variableName:s,uniqueLocalName:s}));return this.extractedDefaultExportName&&r.push({variableName:this.extractedDefaultExportName,uniqueLocalName:"default"}),` ;(function () { var reactHotLoader = require('react-hot-loader').default; var leaveModule = require('react-hot-loader').leaveModule; @@ -131,9 +131,9 @@ module.exports = exports.default; ${r.map(({variableName:s,uniqueLocalName:o})=>` reactHotLoader.register(${s}, "${o}", ${JSON.stringify(this.filePath||"")});`).join(` `)} leaveModule(module); -})();`}process(){return!1}};var Om=new Set(["break","case","catch","class","const","continue","debugger","default","delete","do","else","export","extends","finally","for","function","if","import","in","instanceof","new","return","super","switch","this","throw","try","typeof","var","void","while","with","yield","enum","implements","interface","let","package","private","protected","public","static","await","false","null","true"]);function Ts(e){if(e.length===0||!vt[e.charCodeAt(0)])return!1;for(let t=1;t` var ${c};`).join("");for(let c of this.transformers)r+=c.getHoistedCode();let s="";for(let c of this.transformers)s+=c.getSuffixCode();let o=this.tokens.finish(),{code:a}=o;if(a.startsWith("#!")){let c=a.indexOf(` +})();`}process(){return!1}};var Bm=new Set(["break","case","catch","class","const","continue","debugger","default","delete","do","else","export","extends","finally","for","function","if","import","in","instanceof","new","return","super","switch","this","throw","try","typeof","var","void","while","with","yield","enum","implements","interface","let","package","private","protected","public","static","await","false","null","true"]);function Ps(e){if(e.length===0||!vt[e.charCodeAt(0)])return!1;for(let t=1;t` var ${c};`).join("");for(let c of this.transformers)r+=c.getHoistedCode();let s="";for(let c of this.transformers)s+=c.getSuffixCode();let o=this.tokens.finish(),{code:a}=o;if(a.startsWith("#!")){let c=a.indexOf(` `);return c===-1&&(c=a.length,a+=` -`),{code:a.slice(0,c+1)+r+a.slice(c+1)+s,mappings:this.shiftMappings(o.mappings,r.length)}}else return{code:r+a+s,mappings:this.shiftMappings(o.mappings,r.length)}}processBalancedCode(){let t=0,r=0;for(;!this.tokens.isAtEnd();){if(this.tokens.matches1(n.braceL)||this.tokens.matches1(n.dollarBraceL))t++;else if(this.tokens.matches1(n.braceR)){if(t===0)return;t--}if(this.tokens.matches1(n.parenL))r++;else if(this.tokens.matches1(n.parenR)){if(r===0)return;r--}this.processToken()}}processToken(){if(this.tokens.matches1(n._class)){this.processClass();return}for(let t of this.transformers)if(t.process())return;this.tokens.copyToken()}processNamedClass(){if(!this.tokens.matches2(n._class,n.name))throw new Error("Expected identifier for exported class name.");let t=this.tokens.identifierNameAtIndex(this.tokens.currentIndex()+1);return this.processClass(),t}processClass(){let t=xa(this,this.tokens,this.nameManager,this.disableESTransforms),r=(t.headerInfo.isExpression||!t.headerInfo.className)&&t.staticInitializerNames.length+t.instanceInitializerNames.length>0,s=t.headerInfo.className;r&&(s=this.nameManager.claimFreeName("_class"),this.generatedVariables.push(s),this.tokens.appendCode(` (${s} =`));let a=this.tokens.currentToken().contextId;if(a==null)throw new Error("Expected class to have a context ID.");for(this.tokens.copyExpectedToken(n._class);!this.tokens.matchesContextIdAndLabel(n.braceL,a);)this.processToken();this.processClassBody(t,s);let c=t.staticInitializerNames.map(l=>`${s}.${l}()`);r?this.tokens.appendCode(`, ${c.map(l=>`${l}, `).join("")}${s})`):t.staticInitializerNames.length>0&&this.tokens.appendCode(` ${c.map(l=>`${l};`).join(" ")}`)}processClassBody(t,r){let{headerInfo:s,constructorInsertPos:o,constructorInitializerStatements:a,fields:c,instanceInitializerNames:l,rangesToRemove:u}=t,p=0,m=0,h=this.tokens.currentToken().contextId;if(h==null)throw new Error("Expected non-null context ID on class.");this.tokens.copyExpectedToken(n.braceL),this.isReactHotLoaderTransformEnabled&&this.tokens.appendCode("__reactstandin__regenerateByEval(key, code) {this[key] = eval(code);}");let b=a.length+l.length>0;if(o===null&&b){let w=this.makeConstructorInitCode(a,l,r);if(s.hasSuperclass){let x=this.nameManager.claimFreeName("args");this.tokens.appendCode(`constructor(...${x}) { super(...${x}); ${w}; }`)}else this.tokens.appendCode(`constructor() { ${w}; }`)}for(;!this.tokens.matchesContextIdAndLabel(n.braceR,h);)if(p=u[m].start){for(this.tokens.currentIndex()`${s}.prototype.${o}.call(this)`)].join(";")}processPossibleArrowParamEnd(){if(this.tokens.matches2(n.parenR,n.colon)&&this.tokens.tokenAtRelativeIndex(1).isType){let t=this.tokens.currentIndex()+1;for(;this.tokens.tokens[t].isType;)t++;if(this.tokens.matches1AtIndex(t,n.arrow)){for(this.tokens.removeInitialToken();this.tokens.currentIndex()"),!0}}return!1}processPossibleAsyncArrowWithTypeParams(){if(!this.tokens.matchesContextual(d._async)&&!this.tokens.matches1(n._async))return!1;let t=this.tokens.tokenAtRelativeIndex(1);if(t.type!==n.lessThan||!t.isType)return!1;let r=this.tokens.currentIndex()+1;for(;this.tokens.tokens[r].isType;)r++;if(this.tokens.matches1AtIndex(r,n.parenL)){for(this.tokens.replaceToken("async ("),this.tokens.removeInitialToken();this.tokens.currentIndex()0,s=t.headerInfo.className;r&&(s=this.nameManager.claimFreeName("_class"),this.generatedVariables.push(s),this.tokens.appendCode(` (${s} =`));let a=this.tokens.currentToken().contextId;if(a==null)throw new Error("Expected class to have a context ID.");for(this.tokens.copyExpectedToken(n._class);!this.tokens.matchesContextIdAndLabel(n.braceL,a);)this.processToken();this.processClassBody(t,s);let c=t.staticInitializerNames.map(l=>`${s}.${l}()`);r?this.tokens.appendCode(`, ${c.map(l=>`${l}, `).join("")}${s})`):t.staticInitializerNames.length>0&&this.tokens.appendCode(` ${c.map(l=>`${l};`).join(" ")}`)}processClassBody(t,r){let{headerInfo:s,constructorInsertPos:o,constructorInitializerStatements:a,fields:c,instanceInitializerNames:l,rangesToRemove:u}=t,d=0,m=0,h=this.tokens.currentToken().contextId;if(h==null)throw new Error("Expected non-null context ID on class.");this.tokens.copyExpectedToken(n.braceL),this.isReactHotLoaderTransformEnabled&&this.tokens.appendCode("__reactstandin__regenerateByEval(key, code) {this[key] = eval(code);}");let b=a.length+l.length>0;if(o===null&&b){let w=this.makeConstructorInitCode(a,l,r);if(s.hasSuperclass){let x=this.nameManager.claimFreeName("args");this.tokens.appendCode(`constructor(...${x}) { super(...${x}); ${w}; }`)}else this.tokens.appendCode(`constructor() { ${w}; }`)}for(;!this.tokens.matchesContextIdAndLabel(n.braceR,h);)if(d=u[m].start){for(this.tokens.currentIndex()`${s}.prototype.${o}.call(this)`)].join(";")}processPossibleArrowParamEnd(){if(this.tokens.matches2(n.parenR,n.colon)&&this.tokens.tokenAtRelativeIndex(1).isType){let t=this.tokens.currentIndex()+1;for(;this.tokens.tokens[t].isType;)t++;if(this.tokens.matches1AtIndex(t,n.arrow)){for(this.tokens.removeInitialToken();this.tokens.currentIndex()"),!0}}return!1}processPossibleAsyncArrowWithTypeParams(){if(!this.tokens.matchesContextual(p._async)&&!this.tokens.matches1(n._async))return!1;let t=this.tokens.tokenAtRelativeIndex(1);if(t.type!==n.lessThan||!t.isType)return!1;let r=this.tokens.currentIndex()+1;for(;this.tokens.tokens[r].isType;)r++;if(this.tokens.matches1AtIndex(r,n.parenL)){for(this.tokens.replaceToken("async ("),this.tokens.removeInitialToken();this.tokens.currentIndex() { @@ -196,7238 +196,7305 @@ ${r.map(({variableName:s,uniqueLocalName:o})=>` reactHotLoader.register(${s}, " }, }); } - `}var Ju=` - globalThis.__agentOSWasiHost = { - requireBuiltin: (name) => - globalThis.require(String(name).replace(/^node:/, "")), - syncReadLimitBytes: 16777216, - // Browser fs descriptors are a JS handle table, not real host OS fds with - // a kernel offset, so locally-opened files must use the offset-aware file - // branches (explicit position) rather than host-passthrough null reads. - disableLocalFdPassthrough: true, - // Guest stdin is delivered through the runtime process object, not a kernel - // fd, so read the queued bytes from process.stdin directly. - readStdin: (maxBytes) => - (globalThis.process && - globalThis.process.stdin && - typeof globalThis.process.stdin.read === "function" - ? globalThis.process.stdin.read(maxBytes) - : null), - // Queued stdin byte count for poll_oneoff readiness (does not consume). - stdinReadableBytes: () => - (globalThis.process && globalThis.process.stdin - ? Number(globalThis.process.stdin.readableLength || 0) - : 0), - }; - const Buffer = - (typeof globalThis !== "undefined" && globalThis.Buffer) || - (class __AgentOsWasiBuffer extends Uint8Array { - static alloc(size) { return new __AgentOsWasiBuffer(size >>> 0); } - static allocUnsafe(size) { return new __AgentOsWasiBuffer(size >>> 0); } - static isBuffer(value) { return value instanceof Uint8Array; } - static byteLength(value, encoding) { - if (value instanceof Uint8Array) return value.length; - if (encoding === "base64") return Math.floor((String(value).replace(/=+$/, "").length * 3) / 4); - if (encoding === "hex") return String(value).length >> 1; - return new TextEncoder().encode(String(value)).length; - } - static from(value, encodingOrOffset, length) { - if (typeof value === "string") { - const encoding = encodingOrOffset || "utf8"; - if (encoding === "base64") { - const binary = atob(value); - const out = new __AgentOsWasiBuffer(binary.length); - for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i) & 0xff; - return out; - } - if (encoding === "hex") { - const clean = String(value); - const out = new __AgentOsWasiBuffer(clean.length >> 1); - for (let i = 0; i < out.length; i += 1) out[i] = parseInt(clean.substr(i * 2, 2), 16); - return out; - } - const encoded = new TextEncoder().encode(value); - const out = new __AgentOsWasiBuffer(encoded.length); - out.set(encoded); - return out; - } - if (value instanceof ArrayBuffer) { - const offset = encodingOrOffset || 0; - const len = length === undefined ? value.byteLength - offset : length; - const view = new Uint8Array(value, offset, len); - const out = new __AgentOsWasiBuffer(view.length); - out.set(view); - return out; - } - if (ArrayBuffer.isView(value)) { - const view = new Uint8Array(value.buffer, value.byteOffset, value.byteLength); - const out = new __AgentOsWasiBuffer(view.length); - out.set(view); - return out; - } - const arr = Array.from(value || []); - const out = new __AgentOsWasiBuffer(arr.length); - for (let i = 0; i < arr.length; i += 1) out[i] = arr[i] & 0xff; - return out; - } - static concat(list, totalLength) { - const chunks = Array.from(list || []); - if (totalLength === undefined) { - totalLength = 0; - for (const chunk of chunks) totalLength += chunk.length; - } - const out = new __AgentOsWasiBuffer(totalLength >>> 0); - let offset = 0; - for (const chunk of chunks) { - if (offset >= out.length) break; - const slice = offset + chunk.length > out.length ? chunk.subarray(0, out.length - offset) : chunk; - out.set(slice, offset); - offset += slice.length; - } - return out; - } - toString(encoding, start, end) { - const view = this.subarray(start || 0, end === undefined ? this.length : end); - if (encoding === "base64") { - let binary = ""; - for (let i = 0; i < view.length; i += 1) binary += String.fromCharCode(view[i]); - return btoa(binary); - } - if (encoding === "hex") { - let hex = ""; - for (let i = 0; i < view.length; i += 1) hex += view[i].toString(16).padStart(2, "0"); - return hex; - } - return new TextDecoder().decode(view); - } - }); -if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule === "undefined") { - // Per-backend host seam (C / convergence): native populates it from its own - // host globals (the \`|| __agentOs*\` fallbacks below); a non-native backend - // (the browser converged worker) can pre-set \`globalThis.__agentOSWasiHost\` - // with browser-provided equivalents so this same preview1 runner is shared. - const __agentOSWasiHost = - (typeof globalThis.__agentOSWasiHost === "object" && - globalThis.__agentOSWasiHost) || - {}; - const __agentOSWasiRequireBuiltin = - __agentOSWasiHost.requireBuiltin || - (typeof __agentOSRequireBuiltin !== "undefined" - ? __agentOSRequireBuiltin - : (name) => globalThis.require(name)); - const __agentOSFs = () => __agentOSWasiRequireBuiltin("node:fs"); - const __agentOSPath = () => __agentOSWasiRequireBuiltin("node:path"); - const __agentOSCrypto = () => __agentOSWasiRequireBuiltin("node:crypto"); - // Stdio sync-RPC bridge + fd-handle lookup come from the host seam (a - // non-native backend supplies browser equivalents); native falls back to its - // own host globals so behavior is unchanged. - // Lazy resolvers: the native host globals are populated AFTER this module is - // defined (per-execution), so resolve at call time, not at module-load. - const __agentOSWasiSyncRpc = () => - __agentOSWasiHost.syncRpc || - (typeof globalThis.__agentOSSyncRpc !== "undefined" - ? globalThis.__agentOSSyncRpc - : undefined); - const __agentOSWasiLookupFdHandle = () => - __agentOSWasiHost.lookupFdHandle || - (typeof globalThis.lookupFdHandle === "function" - ? globalThis.lookupFdHandle - : undefined); - const __agentOSWasiErrnoSuccess = 0; - const __agentOSWasiErrnoAcces = 2; - const __agentOSWasiErrnoBadf = 8; - const __agentOSWasiErrnoExist = 20; - const __agentOSWasiErrnoFault = 21; - const __agentOSWasiErrnoInval = 28; - const __agentOSWasiErrnoIo = 29; - const __agentOSWasiErrnoNoent = 44; - const __agentOSWasiErrnoNosys = 52; - const __agentOSWasiErrnoNotdir = 54; - const __agentOSWasiErrnoPipe = 64; - const __agentOSWasiErrnoRofs = 69; - const __agentOSWasiErrnoNotcapable = 76; - const __agentOSWasiErrnoXdev = 18; - const __agentOSWasiFiletypeUnknown = 0; - const __agentOSWasiFiletypeCharacterDevice = 2; - const __agentOSWasiFiletypeDirectory = 3; - const __agentOSWasiFiletypeRegularFile = 4; - const __agentOSWasiFiletypeSymbolicLink = 7; - const __agentOSWasiLookupSymlinkFollow = 1; - const __agentOSWasiOpenCreate = 1; - const __agentOSWasiOpenDirectory = 2; - const __agentOSWasiOpenExclusive = 4; - const __agentOSWasiOpenTruncate = 8; - const __agentOSWasiRightFdRead = 1n << 1n; - const __agentOSWasiRightFdWrite = 1n << 6n; - const __agentOSWasiDefaultRightsBase = 0xffffffffffffffffn; - const __agentOSWasiDefaultRightsInheriting = 0xffffffffffffffffn; - const __agentOSWasiWhenceSet = 0; - const __agentOSWasiWhenceCur = 1; - const __agentOSWasiWhenceEnd = 2; - // Read cap: a non-native backend provides it via the seam; native uses its - // build-substituted constant. The ternary short-circuits so the native-only - // placeholder token is never evaluated when the seam supplies a number. - const __agentOSWasmSyncReadLimitBytes = - typeof __agentOSWasiHost.syncReadLimitBytes === "number" - ? __agentOSWasiHost.syncReadLimitBytes - : 16777216; - const __agentOSKernelStdioSyncRpcEnabled = () => - process?.env?.AGENTOS_WASI_STDIO_SYNC_RPC === "1"; - const __agentOSWasiDebugEnabled = () => process?.env?.AGENTOS_WASM_WASI_DEBUG === "1"; - const __agentOSWasiSyscallCountersEnabled = () => - process?.env?.AGENTOS_WASI_SYSCALL_COUNTERS === "1"; - const __agentOSWasiNow = () => - typeof performance?.now === "function" ? performance.now() : Date.now(); - const __agentOSWasiDebug = (message) => { - if (!__agentOSWasiDebugEnabled() || typeof process?.stderr?.write !== "function") { - return; - } - try { - process.stderr.write(\`[secure-exec-wasi] \${message}\\n\`); - } catch { - // Ignore debug logging failures. - } - }; - - class WASI { - constructor(options = {}) { - this.args = Array.isArray(options.args) ? options.args.map((value) => String(value)) : []; - this.env = - options.env && typeof options.env === "object" - ? Object.fromEntries( - Object.entries(options.env).map(([key, value]) => [String(key), String(value)]), - ) - : {}; - this.preopens = options.preopens && typeof options.preopens === "object" ? options.preopens : {}; - this.returnOnExit = options.returnOnExit === true; - this.instance = null; - this.nextFd = 3; - this.fsModule = null; - this.pathModule = null; - this.fdTable = new Map([ - [0, { kind: "stdin", fdFlags: 0 }], - [1, { kind: "stdout", fdFlags: 0 }], - [2, { kind: "stderr", fdFlags: 0 }], - ]); - this.statCache = new Map(); - this.syscallCountersEnabled = __agentOSWasiSyscallCountersEnabled(); - for (const [guestPath, spec] of Object.entries(this.preopens)) { - const normalized = this._normalizePreopenSpec(spec); - if (!normalized) { - continue; - } - this.fdTable.set(this.nextFd++, { - kind: "preopen", - guestPath: String(guestPath), - hostPath: normalized.hostPath, - readOnly: normalized.readOnly, - rightsBase: normalized.rightsBase, - rightsInheriting: normalized.rightsInheriting, - fdFlags: 0, - }); - } - this.wasiImport = { - args_get: (...args) => this._argsGet(...args), - args_sizes_get: (...args) => this._argsSizesGet(...args), - clock_time_get: (...args) => this._clockTimeGet(...args), - clock_res_get: (...args) => this._clockResGet(...args), - environ_get: (...args) => this._environGet(...args), - environ_sizes_get: (...args) => this._environSizesGet(...args), - fd_close: (...args) => this._fdClose(...args), - fd_fdstat_get: (...args) => this._fdFdstatGet(...args), - fd_fdstat_set_flags: (...args) => this._fdFdstatSetFlags(...args), - fd_filestat_get: (...args) => this._fdFilestatGet(...args), - fd_filestat_set_size: (...args) => this._fdFilestatSetSize(...args), - fd_prestat_dir_name: (...args) => this._fdPrestatDirName(...args), - fd_prestat_get: (...args) => this._fdPrestatGet(...args), - fd_pread: (...args) => this._fdPread(...args), - fd_pwrite: (...args) => this._fdPwrite(...args), - fd_readdir: (...args) => this._fdReaddir(...args), - fd_read: (...args) => this._fdRead(...args), - fd_seek: (...args) => this._fdSeek(...args), - fd_sync: (...args) => this._fdSync(...args), - fd_tell: (...args) => this._fdTell(...args), - fd_write: (...args) => this._fdWrite(...args), - path_create_directory: (...args) => this._pathCreateDirectory(...args), - path_filestat_get: (...args) => this._pathFilestatGet(...args), - path_link: (...args) => this._pathLink(...args), - path_open: (...args) => this._pathOpen(...args), - path_readlink: (...args) => this._pathReadlink(...args), - path_remove_directory: (...args) => this._pathRemoveDirectory(...args), - path_rename: (...args) => this._pathRename(...args), - path_symlink: (...args) => this._pathSymlink(...args), - path_unlink_file: (...args) => this._pathUnlinkFile(...args), - poll_oneoff: (...args) => this._pollOneoff(...args), - proc_exit: (...args) => this._procExit(...args), - random_get: (...args) => this._randomGet(...args), - sched_yield: (...args) => this._schedYield(...args), - }; - this._installSyscallCounterWrappers(); - } - - _fs() { - if (!this.fsModule) { - this.fsModule = __agentOSFs(); - } - return this.fsModule; - } - - _path() { - if (!this.pathModule) { - this.pathModule = __agentOSPath(); - } - return this.pathModule; - } - - _recordWasiSyscallMetric(name, startedAt, details = {}) { - if (!this.syscallCountersEnabled || typeof process?.stderr?.write !== "function") { - return; - } - try { - const elapsedMs = __agentOSWasiNow() - startedAt; - process.stderr.write( - \`__AGENTOS_WASI_SYSCALL_METRICS__:\${JSON.stringify({ - name, - elapsedMs, - ...details, - })}\\n\`, - ); - } catch { - // Ignore metrics failures. - } - } - - _measureWasiPhase(name, fn) { - if (!this.syscallCountersEnabled || !this._activeWasiMetric) { - return fn(); - } - const startedAt = __agentOSWasiNow(); - try { - return fn(); - } finally { - const phases = (this._activeWasiMetric.phases ??= {}); - phases[name] = (phases[name] ?? 0) + (__agentOSWasiNow() - startedAt); - } - } - - _installSyscallCounterWrappers() { - if (!this.syscallCountersEnabled) { - return; - } - for (const name of [ - "path_open", - "path_filestat_get", - "fd_filestat_get", - "fd_write", - ]) { - const original = this.wasiImport[name]; - if (typeof original !== "function") { - continue; - } - this.wasiImport[name] = (...args) => { - const startedAt = __agentOSWasiNow(); - const previousMetric = this._activeWasiMetric; - const activeMetric = { name, phases: {} }; - this._activeWasiMetric = activeMetric; - let result; - try { - result = original(...args); - } finally { - this._activeWasiMetric = previousMetric; - } - const details = { - result, - fd: Number(args[0]) >>> 0, - iovsLen: name === "fd_write" ? Number(args[2]) >>> 0 : undefined, - phases: activeMetric.phases, - }; - if (name === "path_filestat_get") { - try { - const target = this._readString(args[2], args[3]); - details.pathLen = target.length; - details.pathKind = target.startsWith("/") - ? "absolute" - : target.includes("/") - ? "relative-nested" - : "relative-child"; - details.pathSample = - target.length > 120 ? \`\${target.slice(0, 120)}...\` : target; - } catch { - // Leave path-shape fields absent if the guest pointer is invalid. - } - } - this._recordWasiSyscallMetric(name, startedAt, { - ...details, - }); - return result; - }; - } - } - - start(instance) { - this.instance = instance; - try { - if (typeof instance?.exports?._start === "function") { - instance.exports._start(); - } - return 0; - } catch (error) { - if (error && error.__agentOSWasiExit === true) { - return Number(error.code) >>> 0; - } - throw error; - } - } - - _memoryView() { - const memory = this.instance?.exports?.memory; - if (!(memory instanceof WebAssembly.Memory)) { - throw new Error("WASI memory export is unavailable"); - } - return new DataView(memory.buffer); - } - - _memoryBytes() { - const memory = this.instance?.exports?.memory; - if (!(memory instanceof WebAssembly.Memory)) { - throw new Error("WASI memory export is unavailable"); - } - return new Uint8Array(memory.buffer); - } - - _boundedIovLength(iovs, iovsLen) { - const view = this._memoryView(); - let length = 0; - for (let index = 0; index < (Number(iovsLen) >>> 0); index += 1) { - const entryOffset = (Number(iovs) >>> 0) + index * 8; - length += view.getUint32(entryOffset + 4, true); - if (length > __agentOSWasmSyncReadLimitBytes) { - throw new RangeError( - \`WASI read iov length \${length} exceeds \${__agentOSWasmSyncReadLimitBytes}\`, - ); - } - } - return length >>> 0; - } - - // Read-side iov capacity, clamped (not thrown) to the sync read cap. A guest - // may legitimately offer a huge read buffer (e.g. iov_len 0xffffffc0 = "read - // up to ~4GB"); the runner reads only what is available, bounded by the cap, - // so the read allocation/RPC stays bounded without rejecting the read. Writes - // keep using _boundedIovLength (throwing) because their iov length is real - // data that must not be silently truncated. - _boundedReadLength(iovs, iovsLen) { - const view = this._memoryView(); - let length = 0; - for (let index = 0; index < (Number(iovsLen) >>> 0); index += 1) { - const entryOffset = (Number(iovs) >>> 0) + index * 8; - length += view.getUint32(entryOffset + 4, true); - if (length >= __agentOSWasmSyncReadLimitBytes) { - return __agentOSWasmSyncReadLimitBytes; - } - } - return length >>> 0; - } - - _normalizeRights(value, fallback) { - try { - return BigInt.asUintN(64, BigInt(value)); - } catch { - return fallback; - } - } - - _normalizePreopenSpec(value) { - // Path-model seam (convergence item C): native maps guest paths to HOST - // paths (its preopen specs carry \`hostPath\`); a non-native backend with no - // host paths (the browser, whose \`require("fs")\` IS the kernel VFS) can - // supply \`__agentOSWasiHost.normalizePreopen\` to treat the guest/VFS path - // as the "hostPath" identity, so the same runner serves both. - if (typeof __agentOSWasiHost.normalizePreopen === "function") { - const seamNormalized = __agentOSWasiHost.normalizePreopen(value, { - defaultRightsBase: __agentOSWasiDefaultRightsBase, - defaultRightsInheriting: __agentOSWasiDefaultRightsInheriting, - normalizeRights: (rights, fallback) => - this._normalizeRights(rights, fallback), - }); - return seamNormalized ?? null; - } - if (typeof value === "string") { - return { - hostPath: String(value), - readOnly: false, - rightsBase: __agentOSWasiDefaultRightsBase, - rightsInheriting: __agentOSWasiDefaultRightsInheriting, - }; - } - if (!value || typeof value !== "object" || typeof value.hostPath !== "string") { - return null; - } - return { - hostPath: String(value.hostPath), - readOnly: value.readOnly === true, - rightsBase: this._normalizeRights( - value.rightsBase, - __agentOSWasiDefaultRightsBase, - ), - rightsInheriting: this._normalizeRights( - value.rightsInheriting, - __agentOSWasiDefaultRightsInheriting, - ), - }; - } - - _descriptorRightsBase(entry) { - return this._normalizeRights( - entry?.rightsBase, - __agentOSWasiDefaultRightsBase, - ); - } - - _descriptorRightsInheriting(entry) { - return this._normalizeRights( - entry?.rightsInheriting, - __agentOSWasiDefaultRightsInheriting, - ); - } - - _hasWriteRights(rights) { - try { - return (BigInt(rights) & __agentOSWasiRightFdWrite) !== 0n; - } catch { - return true; - } - } - - _writeUint32(ptr, value) { - try { - this._memoryView().setUint32(Number(ptr) >>> 0, Number(value) >>> 0, true); - return __agentOSWasiErrnoSuccess; - } catch { - __agentOSWasiDebug(\`writeUint32 failed ptr=\${Number(ptr)} value=\${Number(value)}\`); - return __agentOSWasiErrnoFault; - } - } - - _writeUint64(ptr, value) { - try { - this._memoryView().setBigUint64(Number(ptr) >>> 0, BigInt(value), true); - return __agentOSWasiErrnoSuccess; - } catch { - __agentOSWasiDebug(\`writeUint64 failed ptr=\${Number(ptr)} value=\${String(value)}\`); - return __agentOSWasiErrnoFault; - } - } - - _writeBytes(ptr, bytes) { - try { - this._memoryBytes().set(bytes, Number(ptr) >>> 0); - return __agentOSWasiErrnoSuccess; - } catch { - __agentOSWasiDebug(\`writeBytes failed ptr=\${Number(ptr)} len=\${bytes?.length ?? 0}\`); - return __agentOSWasiErrnoFault; - } - } - - _readBytes(ptr, len) { - const start = Number(ptr) >>> 0; - const end = start + (Number(len) >>> 0); - return Buffer.from(this._memoryBytes().slice(start, end)); - } - - _readString(ptr, len) { - return this._readBytes(ptr, len).toString("utf8"); - } - - _decodeSyncRpcBytes(value) { - if (value == null) { - return null; - } - if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) { - return value; - } - if (value instanceof Uint8Array) { - return Buffer.from(value); - } - if (ArrayBuffer.isView(value)) { - return Buffer.from(value.buffer, value.byteOffset, value.byteLength); - } - if (value instanceof ArrayBuffer) { - return Buffer.from(value); - } - if ( - value && - typeof value === "object" && - value.__agentOSType === "bytes" && - typeof value.base64 === "string" - ) { - return Buffer.from(value.base64, "base64"); - } - return null; - } - - _dequeuePipeBytes(pipe, maxBytes) { - if (!pipe || !Array.isArray(pipe.chunks) || pipe.chunks.length === 0) { - return Buffer.alloc(0); - } - - let remaining = Math.max(0, Number(maxBytes) >>> 0); - if (remaining === 0) { - return Buffer.alloc(0); - } - - const parts = []; - while (remaining > 0 && pipe.chunks.length > 0) { - const chunk = pipe.chunks[0]; - if (!chunk || chunk.length === 0) { - pipe.chunks.shift(); - continue; - } - - if (chunk.length <= remaining) { - parts.push(chunk); - pipe.chunks.shift(); - remaining -= chunk.length; - continue; - } - - parts.push(chunk.subarray(0, remaining)); - pipe.chunks[0] = chunk.subarray(remaining); - remaining = 0; - } - - return Buffer.concat(parts); - } - - _enqueuePipeBytes(pipe, bytes) { - if (!pipe || !Array.isArray(pipe.chunks)) { - return; - } - const chunk = Buffer.from(bytes ?? []); - if (chunk.length === 0) { - return; - } - pipe.chunks.push(chunk); - } - - _pipeHasReaders(pipe) { - return ( - (pipe?.readHandleCount ?? 0) > 0 || - (pipe?.consumers?.size ?? 0) > 0 - ); - } - - _flushPipeConsumers(pipe) { - if ( - !pipe || - typeof pipe.consumers?.entries !== "function" || - !Array.isArray(pipe.chunks) || - pipe.chunks.length === 0 || - typeof globalThis?.__agentOSSyncRpc?.callSync !== "function" - ) { - return false; - } - - let flushed = false; - while (pipe.chunks.length > 0) { - const chunk = pipe.chunks.shift(); - if (!chunk || chunk.length === 0) { - continue; - } - - for (const [consumerKey, consumer] of Array.from(pipe.consumers.entries())) { - if (!consumer || typeof consumer.childId !== "string") { - pipe.consumers.delete(consumerKey); - continue; - } - try { - __agentOSWasiSyncRpc().callSync("child_process.write_stdin", [ - consumer.childId, - chunk, - ]); - flushed = true; - } catch { - pipe.consumers.delete(consumerKey); - } - } - } - - return flushed; - } - - _closePipeConsumers(pipe) { - if ( - !pipe || - typeof pipe.consumers?.entries !== "function" || - typeof globalThis?.__agentOSSyncRpc?.callSync !== "function" - ) { - return false; - } - - let closed = false; - for (const [consumerKey, consumer] of Array.from(pipe.consumers.entries())) { - if (!consumer || typeof consumer.childId !== "string") { - pipe.consumers.delete(consumerKey); - continue; - } - try { - __agentOSWasiSyncRpc().callSync("child_process.close_stdin", [ - consumer.childId, - ]); - closed = true; - } catch { - // Ignore close errors during teardown. - } - pipe.consumers.delete(consumerKey); - } - - return closed; - } - - _pumpPipeProducers(pipe, waitMs) { - if ( - !pipe || - typeof pipe.producers?.entries !== "function" || - typeof globalThis?.__agentOSSyncRpc?.callSync !== "function" - ) { - return false; - } - - let processed = false; - for (const [producerKey, producer] of Array.from(pipe.producers.entries())) { - if (!producer || typeof producer.childId !== "string") { - pipe.producers.delete(producerKey); - continue; - } - - let event = null; - try { - event = __agentOSWasiSyncRpc().callSync("child_process.poll", [ - producer.childId, - Math.max(0, Number(waitMs) >>> 0), - ]); - } catch { - pipe.producers.delete(producerKey); - continue; - } - - if (!event) { - continue; - } - - processed = true; - const streamType = - producer.stream === "stderr" ? "stderr" : producer.stream === "stdout" ? "stdout" : null; - if ((event.type === "stdout" || event.type === "stderr") && event.type === streamType) { - const chunk = this._decodeSyncRpcBytes(event.data); - if (chunk && chunk.length > 0) { - pipe.chunks.push(Buffer.from(chunk)); - } - continue; - } - - if (event.type === "exit") { - pipe.producers.delete(producerKey); - if (pipe.producers.size === 0 && (pipe.writeHandleCount ?? 0) === 0) { - this._closePipeConsumers(pipe); - } - continue; - } - } - - return processed; - } + `}var Ju=`var process = globalThis.process || { + env: {}, + nextTick: (fn, ...args) => queueMicrotask(() => fn(...args)), +}; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; - _collectIovs(iovs, iovsLen) { - const totalLength = this._boundedIovLength(iovs, iovsLen); - const view = this._memoryView(); - const chunks = []; - for (let index = 0; index < (Number(iovsLen) >>> 0); index += 1) { - const entryOffset = (Number(iovs) >>> 0) + index * 8; - const ptr = view.getUint32(entryOffset, true); - const len = view.getUint32(entryOffset + 4, true); - chunks.push(this._readBytes(ptr, len)); - } - return Buffer.concat(chunks, totalLength); +// node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js +var require_base64_js = __commonJS({ + "node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js"(exports2) { + "use strict"; + exports2.byteLength = byteLength; + exports2.toByteArray = toByteArray; + exports2.fromByteArray = fromByteArray; + var lookup = []; + var revLookup = []; + var Arr = typeof Uint8Array !== "undefined" ? Uint8Array : Array; + var code = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for (i = 0, len = code.length; i < len; ++i) { + lookup[i] = code[i]; + revLookup[code.charCodeAt(i)] = i; } - - _writeToIovs(iovs, iovsLen, bytes) { - const view = this._memoryView(); - const memory = this._memoryBytes(); - let sourceOffset = 0; - for (let index = 0; index < (Number(iovsLen) >>> 0) && sourceOffset < bytes.length; index += 1) { - const entryOffset = (Number(iovs) >>> 0) + index * 8; - const ptr = view.getUint32(entryOffset, true); - const len = view.getUint32(entryOffset + 4, true); - const chunk = bytes.subarray(sourceOffset, sourceOffset + len); - memory.set(chunk, Number(ptr) >>> 0); - sourceOffset += chunk.length; + var i; + var len; + revLookup["-".charCodeAt(0)] = 62; + revLookup["_".charCodeAt(0)] = 63; + function getLens(b64) { + var len2 = b64.length; + if (len2 % 4 > 0) { + throw new Error("Invalid string. Length must be a multiple of 4"); } - return sourceOffset; - } - - _stringTable(values) { - return values.map((value) => Buffer.from(\`\${String(value)}\\0\`, "utf8")); + var validLen = b64.indexOf("="); + if (validLen === -1) validLen = len2; + var placeHoldersLen = validLen === len2 ? 0 : 4 - validLen % 4; + return [validLen, placeHoldersLen]; } - - _writeStringTable(values, offsetsPtr, bufferPtr) { - try { - const view = this._memoryView(); - const memory = this._memoryBytes(); - let cursor = Number(bufferPtr) >>> 0; - for (let index = 0; index < values.length; index += 1) { - const bytes = values[index]; - view.setUint32((Number(offsetsPtr) >>> 0) + index * 4, cursor, true); - memory.set(bytes, cursor); - cursor += bytes.length; - } - return __agentOSWasiErrnoSuccess; - } catch { - __agentOSWasiDebug( - \`writeStringTable failed offsetsPtr=\${Number(offsetsPtr)} bufferPtr=\${Number(bufferPtr)} count=\${values.length}\`, - ); - return __agentOSWasiErrnoFault; - } + function byteLength(b64) { + var lens = getLens(b64); + var validLen = lens[0]; + var placeHoldersLen = lens[1]; + return (validLen + placeHoldersLen) * 3 / 4 - placeHoldersLen; } - - _filetypeForStats(stats) { - if (!stats) { - return __agentOSWasiFiletypeUnknown; - } - const mode = Number(stats.mode); - if (Number.isFinite(mode)) { - switch (mode & 0o170000) { - case 0o040000: - return __agentOSWasiFiletypeDirectory; - case 0o100000: - return __agentOSWasiFiletypeRegularFile; - case 0o120000: - return __agentOSWasiFiletypeSymbolicLink; - case 0o020000: - return __agentOSWasiFiletypeCharacterDevice; - } - } - if (stats.isDirectory === true) { - return __agentOSWasiFiletypeDirectory; - } - if (stats.isSymbolicLink === true) { - return __agentOSWasiFiletypeSymbolicLink; - } - if (typeof stats.isDirectory === "function" && stats.isDirectory()) { - return __agentOSWasiFiletypeDirectory; - } - if (typeof stats.isFile === "function" && stats.isFile()) { - return __agentOSWasiFiletypeRegularFile; - } - if (typeof stats.isSymbolicLink === "function" && stats.isSymbolicLink()) { - return __agentOSWasiFiletypeSymbolicLink; - } - if (typeof stats.isCharacterDevice === "function" && stats.isCharacterDevice()) { - return __agentOSWasiFiletypeCharacterDevice; - } - if (stats.isDirectory === false && stats.isSymbolicLink === false) { - return __agentOSWasiFiletypeRegularFile; - } - return __agentOSWasiFiletypeUnknown; + function _byteLength(b64, validLen, placeHoldersLen) { + return (validLen + placeHoldersLen) * 3 / 4 - placeHoldersLen; } - - _filetypeForDirent(dirent) { - if (!dirent || typeof dirent !== "object") { - return __agentOSWasiFiletypeUnknown; - } - if (typeof dirent.isDirectory === "function" && dirent.isDirectory()) { - return __agentOSWasiFiletypeDirectory; - } - if (typeof dirent.isFile === "function" && dirent.isFile()) { - return __agentOSWasiFiletypeRegularFile; + function toByteArray(b64) { + var tmp; + var lens = getLens(b64); + var validLen = lens[0]; + var placeHoldersLen = lens[1]; + var arr = new Arr(_byteLength(b64, validLen, placeHoldersLen)); + var curByte = 0; + var len2 = placeHoldersLen > 0 ? validLen - 4 : validLen; + var i2; + for (i2 = 0; i2 < len2; i2 += 4) { + tmp = revLookup[b64.charCodeAt(i2)] << 18 | revLookup[b64.charCodeAt(i2 + 1)] << 12 | revLookup[b64.charCodeAt(i2 + 2)] << 6 | revLookup[b64.charCodeAt(i2 + 3)]; + arr[curByte++] = tmp >> 16 & 255; + arr[curByte++] = tmp >> 8 & 255; + arr[curByte++] = tmp & 255; } - if (typeof dirent.isSymbolicLink === "function" && dirent.isSymbolicLink()) { - return __agentOSWasiFiletypeSymbolicLink; + if (placeHoldersLen === 2) { + tmp = revLookup[b64.charCodeAt(i2)] << 2 | revLookup[b64.charCodeAt(i2 + 1)] >> 4; + arr[curByte++] = tmp & 255; } - if (typeof dirent.isCharacterDevice === "function" && dirent.isCharacterDevice()) { - return __agentOSWasiFiletypeCharacterDevice; + if (placeHoldersLen === 1) { + tmp = revLookup[b64.charCodeAt(i2)] << 10 | revLookup[b64.charCodeAt(i2 + 1)] << 4 | revLookup[b64.charCodeAt(i2 + 2)] >> 2; + arr[curByte++] = tmp >> 8 & 255; + arr[curByte++] = tmp & 255; } - return __agentOSWasiFiletypeUnknown; - } - - _clearStatCache() { - this.statCache?.clear?.(); - } - - _statCacheKey(resolved, follow) { - const path = typeof resolved?.guestPath === "string" - ? resolved.guestPath - : this._resolvedFsPath(resolved); - return typeof path === "string" ? \`\${follow ? "stat" : "lstat"}:\${path}\` : null; + return arr; } - - _fdFiletype(entry) { - if (!entry) { - return __agentOSWasiFiletypeUnknown; - } - if ( - entry.kind === "stdin" || - entry.kind === "stdout" || - entry.kind === "stderr" - ) { - return __agentOSWasiFiletypeCharacterDevice; - } - if (entry.kind === "preopen" || entry.kind === "directory") { - return __agentOSWasiFiletypeDirectory; - } - if (entry.kind === "symlink") { - return __agentOSWasiFiletypeSymbolicLink; - } - return __agentOSWasiFiletypeRegularFile; + function tripletToBase64(num) { + return lookup[num >> 18 & 63] + lookup[num >> 12 & 63] + lookup[num >> 6 & 63] + lookup[num & 63]; } - - _mapFsError(error) { - switch (error?.code) { - case "EACCES": - case "EPERM": - return __agentOSWasiErrnoAcces; - case "ENOENT": - return __agentOSWasiErrnoNoent; - case "ENOTDIR": - return __agentOSWasiErrnoNotdir; - case "EEXIST": - return __agentOSWasiErrnoExist; - case "EINVAL": - return __agentOSWasiErrnoInval; - case "EROFS": - return __agentOSWasiErrnoRofs; - case "EXDEV": - return __agentOSWasiErrnoXdev; - default: - return __agentOSWasiErrnoIo; + function encodeChunk(uint8, start, end) { + var tmp; + var output = []; + for (var i2 = start; i2 < end; i2 += 3) { + tmp = (uint8[i2] << 16 & 16711680) + (uint8[i2 + 1] << 8 & 65280) + (uint8[i2 + 2] & 255); + output.push(tripletToBase64(tmp)); } + return output.join(""); } - - _descriptorEntry(fd) { - return this.fdTable.get(Number(fd) >>> 0) ?? null; - } - - _localFdHandle(fd) { - // A non-native backend whose \`realFd\` values are not real host OS fds with - // their own kernel offset (the browser, whose fs descriptors are a JS - // handle table) disables local-fd passthrough so locally-opened files use - // the offset-aware file branches (fd_read/fd_write pass the tracked - // entry.offset as an explicit position) instead of host-passthrough reads - // that rely on a null position advancing a real fd. Native keeps passthrough - // so guest-opened fds can be shared with child processes. - if (__agentOSWasiHost.disableLocalFdPassthrough === true) { - return null; + function fromByteArray(uint8) { + var tmp; + var len2 = uint8.length; + var extraBytes = len2 % 3; + var parts = []; + var maxChunkLength = 16383; + for (var i2 = 0, len22 = len2 - extraBytes; i2 < len22; i2 += maxChunkLength) { + parts.push(encodeChunk(uint8, i2, i2 + maxChunkLength > len22 ? len22 : i2 + maxChunkLength)); } - const descriptor = Number(fd) >>> 0; - const entry = this._descriptorEntry(descriptor); - if (!entry || typeof entry.realFd !== "number") { - return null; + if (extraBytes === 1) { + tmp = uint8[len2 - 1]; + parts.push( + lookup[tmp >> 2] + lookup[tmp << 4 & 63] + "==" + ); + } else if (extraBytes === 2) { + tmp = (uint8[len2 - 2] << 8) + uint8[len2 - 1]; + parts.push( + lookup[tmp >> 10] + lookup[tmp >> 4 & 63] + lookup[tmp << 2 & 63] + "=" + ); } - return { - kind: "host-passthrough", - targetFd: entry.realFd, - displayFd: Number(fd) >>> 0, - refCount: 1, - open: true, - readOnly: entry.readOnly === true, - }; + return parts.join(""); } + } +}); - _externalFdHandle(fd) { - const descriptor = Number(fd) >>> 0; - const localHandle = this._localFdHandle(descriptor); - if (localHandle) { - return localHandle; +// node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js +var require_ieee754 = __commonJS({ + "node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js"(exports2) { + exports2.read = function(buffer2, offset, isLE, mLen, nBytes) { + var e, m; + var eLen = nBytes * 8 - mLen - 1; + var eMax = (1 << eLen) - 1; + var eBias = eMax >> 1; + var nBits = -7; + var i = isLE ? nBytes - 1 : 0; + var d = isLE ? -1 : 1; + var s = buffer2[offset + i]; + i += d; + e = s & (1 << -nBits) - 1; + s >>= -nBits; + nBits += eLen; + for (; nBits > 0; e = e * 256 + buffer2[offset + i], i += d, nBits -= 8) { } - try { - if (typeof lookupFdHandle === "function") { - return lookupFdHandle(descriptor) ?? null; - } - } catch { - // Fall through to other lookup paths. + m = e & (1 << -nBits) - 1; + e >>= -nBits; + nBits += mLen; + for (; nBits > 0; m = m * 256 + buffer2[offset + i], i += d, nBits -= 8) { } - try { - const __agentOSWasiFdHandleFn = __agentOSWasiLookupFdHandle(); - if (typeof __agentOSWasiFdHandleFn === "function") { - return __agentOSWasiFdHandleFn(descriptor) ?? null; + if (e === 0) { + e = 1 - eBias; + } else if (e === eMax) { + return m ? NaN : (s ? -1 : 1) * Infinity; + } else { + m = m + Math.pow(2, mLen); + e = e - eBias; + } + return (s ? -1 : 1) * m * Math.pow(2, e - mLen); + }; + exports2.write = function(buffer2, value, offset, isLE, mLen, nBytes) { + var e, m, c; + var eLen = nBytes * 8 - mLen - 1; + var eMax = (1 << eLen) - 1; + var eBias = eMax >> 1; + var rt = mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0; + var i = isLE ? 0 : nBytes - 1; + var d = isLE ? 1 : -1; + var s = value < 0 || value === 0 && 1 / value < 0 ? 1 : 0; + value = Math.abs(value); + if (isNaN(value) || value === Infinity) { + m = isNaN(value) ? 1 : 0; + e = eMax; + } else { + e = Math.floor(Math.log(value) / Math.LN2); + if (value * (c = Math.pow(2, -e)) < 1) { + e--; + c *= 2; + } + if (e + eBias >= 1) { + value += rt / c; + } else { + value += rt * Math.pow(2, 1 - eBias); + } + if (value * c >= 2) { + e++; + c /= 2; + } + if (e + eBias >= eMax) { + m = 0; + e = eMax; + } else if (e + eBias >= 1) { + m = (value * c - 1) * Math.pow(2, mLen); + e = e + eBias; + } else { + m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen); + e = 0; } - } catch { - // Ignore missing global bridge helpers. } - return null; - } - - _descriptorHostPath(entry) { - if (!entry) { - return null; + for (; mLen >= 8; buffer2[offset + i] = m & 255, i += d, m /= 256, mLen -= 8) { } - if (typeof entry.hostPath === "string") { - return entry.hostPath; + e = e << mLen | m; + eLen += mLen; + for (; eLen > 0; buffer2[offset + i] = e & 255, i += d, e /= 256, eLen -= 8) { } - if (typeof entry.realFd === "number") { - return __agentOSFs().readlinkSync(\`/proc/self/fd/\${entry.realFd}\`); + buffer2[offset + i - d] |= s * 128; + }; + } +}); + +// node_modules/.pnpm/buffer@5.7.1/node_modules/buffer/index.js +var require_buffer = __commonJS({ + "node_modules/.pnpm/buffer@5.7.1/node_modules/buffer/index.js"(exports2) { + "use strict"; + var base64 = require_base64_js(); + var ieee754 = require_ieee754(); + var customInspectSymbol = typeof Symbol === "function" && typeof Symbol["for"] === "function" ? Symbol["for"]("nodejs.util.inspect.custom") : null; + exports2.Buffer = Buffer2; + exports2.SlowBuffer = SlowBuffer; + exports2.INSPECT_MAX_BYTES = 50; + var K_MAX_LENGTH = 2147483647; + exports2.kMaxLength = K_MAX_LENGTH; + Buffer2.TYPED_ARRAY_SUPPORT = typedArraySupport(); + if (!Buffer2.TYPED_ARRAY_SUPPORT && typeof console !== "undefined" && typeof console.error === "function") { + console.error( + "This browser lacks typed array (Uint8Array) support which is required by \`buffer\` v5.x. Use \`buffer\` v4.x if you require old browser support." + ); + } + function typedArraySupport() { + try { + var arr = new Uint8Array(1); + var proto = { foo: function() { + return 42; + } }; + Object.setPrototypeOf(proto, Uint8Array.prototype); + Object.setPrototypeOf(arr, proto); + return arr.foo() === 42; + } catch (e) { + return false; } - return null; } - - _descriptorFsPath(entry) { - if (!entry) { - return null; + Object.defineProperty(Buffer2.prototype, "parent", { + enumerable: true, + get: function() { + if (!Buffer2.isBuffer(this)) return void 0; + return this.buffer; } - if (typeof entry.hostPath === "string" && entry.hostPath.length > 0) { - return entry.hostPath; + }); + Object.defineProperty(Buffer2.prototype, "offset", { + enumerable: true, + get: function() { + if (!Buffer2.isBuffer(this)) return void 0; + return this.byteOffset; } - if (typeof entry.guestPath === "string" && entry.guestPath.length > 0) { - return entry.guestPath; + }); + function createBuffer(length) { + if (length > K_MAX_LENGTH) { + throw new RangeError('The value "' + length + '" is invalid for option "size"'); } - return null; + var buf = new Uint8Array(length); + Object.setPrototypeOf(buf, Buffer2.prototype); + return buf; } - - _sidecarManagedProcess() { - if ( - typeof globalThis.__agentOSWasmInternalEnv?.AGENTOS_SANDBOX_ROOT === - "string" && - globalThis.__agentOSWasmInternalEnv.AGENTOS_SANDBOX_ROOT.length > 0 - ) { - return true; + function Buffer2(arg, encodingOrOffset, length) { + if (typeof arg === "number") { + if (typeof encodingOrOffset === "string") { + throw new TypeError( + 'The "string" argument must be of type string. Received type number' + ); + } + return allocUnsafe(arg); } - return ( - typeof process?.env?.AGENTOS_SANDBOX_ROOT === "string" && - process.env.AGENTOS_SANDBOX_ROOT.length > 0 - ); + return from(arg, encodingOrOffset, length); } - - _descriptorDirectoryFsPath(entry) { - if ( - (entry?.kind === "preopen" || entry?.kind === "directory") && - this._sidecarManagedProcess() - ) { - return this._descriptorGuestPath(entry); + Buffer2.poolSize = 8192; + function from(value, encodingOrOffset, length) { + if (typeof value === "string") { + return fromString(value, encodingOrOffset); } - return this._descriptorFsPath(entry); - } - - _descriptorGuestPath(entry) { - if (!entry) { - return null; + if (ArrayBuffer.isView(value)) { + return fromArrayView(value); + } + if (value == null) { + throw new TypeError( + "The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type " + typeof value + ); } - const guestPath = typeof entry.guestPath === "string" ? entry.guestPath : null; - if (guestPath === ".") { - return this._currentGuestCwd(); + if (isInstance(value, ArrayBuffer) || value && isInstance(value.buffer, ArrayBuffer)) { + return fromArrayBuffer(value, encodingOrOffset, length); } - if (typeof guestPath === "string" && guestPath.length > 0) { - return __agentOSPath().posix.normalize(guestPath); + if (typeof SharedArrayBuffer !== "undefined" && (isInstance(value, SharedArrayBuffer) || value && isInstance(value.buffer, SharedArrayBuffer))) { + return fromArrayBuffer(value, encodingOrOffset, length); } - return null; - } - - _descriptorPreopenName(entry) { - if (!entry) { - return null; + if (typeof value === "number") { + throw new TypeError( + 'The "value" argument must not be of type number. Received type number' + ); } - const guestPath = typeof entry.guestPath === "string" ? entry.guestPath : null; - if (guestPath === ".") { - return this._descriptorGuestPath(entry); + var valueOf = value.valueOf && value.valueOf(); + if (valueOf != null && valueOf !== value) { + return Buffer2.from(valueOf, encodingOrOffset, length); } - if (typeof guestPath === "string" && guestPath.length > 0) { - return __agentOSPath().posix.normalize(guestPath); + var b = fromObject(value); + if (b) return b; + if (typeof Symbol !== "undefined" && Symbol.toPrimitive != null && typeof value[Symbol.toPrimitive] === "function") { + return Buffer2.from( + value[Symbol.toPrimitive]("string"), + encodingOrOffset, + length + ); } - return null; + throw new TypeError( + "The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type " + typeof value + ); } - - _currentDirectoryPreopen() { - for (const entry of this.fdTable.values()) { - if (entry?.kind === "preopen" && entry.guestPath === ".") { - return entry; - } + Buffer2.from = function(value, encodingOrOffset, length) { + return from(value, encodingOrOffset, length); + }; + Object.setPrototypeOf(Buffer2.prototype, Uint8Array.prototype); + Object.setPrototypeOf(Buffer2, Uint8Array); + function assertSize(size) { + if (typeof size !== "number") { + throw new TypeError('"size" argument must be of type number'); + } else if (size < 0) { + throw new RangeError('The value "' + size + '" is invalid for option "size"'); } - return null; } - - _descriptorPathBase(entry, target) { - const baseGuestPath = this._descriptorGuestPath(entry); - if (typeof baseGuestPath !== "string") { - return null; + function alloc(size, fill, encoding) { + assertSize(size); + if (size <= 0) { + return createBuffer(size); } - return { - entry, - guestPath: baseGuestPath, - hostPath: typeof entry?.hostPath === "string" ? entry.hostPath : null, - }; - } - - _hostPathExists(hostPath) { - return this._measureWasiPhase("hostPathExists", () => { - try { - __agentOSFs().statSync(hostPath); - return true; - } catch { - return false; - } - }); - } - - _createParentExists(guestPath, hostPath) { - const target = - this._sidecarManagedProcess() && typeof guestPath === "string" - ? guestPath - : hostPath; - if (typeof target !== "string") { - return false; + if (fill !== void 0) { + return typeof encoding === "string" ? createBuffer(size).fill(fill, encoding) : createBuffer(size).fill(fill); } - return this._measureWasiPhase("createParentExists", () => { - try { - __agentOSFs().statSync(__agentOSPath().dirname(target)); - return true; - } catch { - return false; - } - }); - } - - _currentGuestCwd() { - const pwd = - typeof this.env?.PWD === "string" && this.env.PWD.startsWith("/") - ? this.env.PWD - : typeof this.env?.HOME === "string" && this.env.HOME.startsWith("/") - ? this.env.HOME - : "/"; - return __agentOSPath().posix.normalize(pwd); - } - - _resolveHostMappingForGuestPath(guestPath) { - return this._measureWasiPhase("resolveHostMapping", () => { - const normalized = __agentOSPath().posix.normalize(guestPath); - const mappings = []; - for (const entry of this.fdTable.values()) { - if (entry?.kind !== "preopen" || typeof entry.hostPath !== "string") { - continue; - } - const guestRoot = this._descriptorGuestPath(entry); - if (typeof guestRoot !== "string") { - continue; - } - mappings.push({ - guestRoot, - hostPath: entry.hostPath, - readOnly: entry.readOnly === true, - }); - } - mappings.sort((left, right) => right.guestRoot.length - left.guestRoot.length); - - for (const mapping of mappings) { - const matchesRoot = mapping.guestRoot === "/" && normalized.startsWith("/"); - const matchesNested = - normalized === mapping.guestRoot || - normalized.startsWith(\`\${mapping.guestRoot}/\`); - if (!matchesRoot && !matchesNested) { - continue; - } - const suffix = - normalized === mapping.guestRoot - ? "" - : mapping.guestRoot === "/" - ? normalized.slice(1) - : normalized.slice(mapping.guestRoot.length + 1); - return { - hostPath: suffix - ? __agentOSPath().join(mapping.hostPath, ...suffix.split("/")) - : mapping.hostPath, - readOnly: mapping.readOnly, - }; - } - - return null; - }); + return createBuffer(size); } - - _resolveHostPathForGuestPath(guestPath) { - return this._resolveHostMappingForGuestPath(guestPath)?.hostPath ?? null; + Buffer2.alloc = function(size, fill, encoding) { + return alloc(size, fill, encoding); + }; + function allocUnsafe(size) { + assertSize(size); + return createBuffer(size < 0 ? 0 : checked(size) | 0); } - - _rootRelativeTargetPrefersCwd(target) { - const normalizedTarget = __agentOSPath().posix.normalize(target || "."); - if (normalizedTarget !== ".") { - return false; + Buffer2.allocUnsafe = function(size) { + return allocUnsafe(size); + }; + Buffer2.allocUnsafeSlow = function(size) { + return allocUnsafe(size); + }; + function fromString(string, encoding) { + if (typeof encoding !== "string" || encoding === "") { + encoding = "utf8"; } - return !this._rootRelativeTargetMatchesAbsoluteArg(target); - } - - _rootRelativeTargetMatchesAbsoluteArg(target) { - const rootGuestPath = __agentOSPath().posix.resolve("/", target); - return this.args - .slice(1) - .some( - (arg) => - typeof arg === "string" && - arg.startsWith("/") && - __agentOSPath().posix.normalize(arg) === rootGuestPath, - ); - } - - _rootRelativeTargetIsWithinAbsoluteArg(target) { - const rootGuestPath = __agentOSPath().posix.resolve("/", target); - return this.args - .slice(1) - .some((arg) => { - if (typeof arg !== "string" || !arg.startsWith("/")) { - return false; - } - const normalizedArg = __agentOSPath().posix.normalize(arg); - return ( - rootGuestPath === normalizedArg || - rootGuestPath.startsWith(\`\${normalizedArg}/\`) - ); - }); + if (!Buffer2.isEncoding(encoding)) { + throw new TypeError("Unknown encoding: " + encoding); + } + var length = byteLength(string, encoding) | 0; + var buf = createBuffer(length); + var actual = buf.write(string, encoding); + if (actual !== length) { + buf = buf.slice(0, actual); + } + return buf; } - - _resolveRootRelativePath(target, preferCreateParent = false) { - const rootGuestPath = __agentOSPath().posix.resolve("/", target); - const rootMapping = this._resolveHostMappingForGuestPath(rootGuestPath); - const rootHostPath = rootMapping?.hostPath ?? null; - const cwdGuestPath = this._currentGuestCwd(); - if (cwdGuestPath !== "/") { - const cwdGuestTarget = __agentOSPath().posix.resolve(cwdGuestPath, target); - const cwdMapping = this._resolveHostMappingForGuestPath(cwdGuestTarget); - const cwdHostTarget = cwdMapping?.hostPath ?? null; - if ( - typeof cwdHostTarget === "string" && - ( - ( - preferCreateParent && - !this._rootRelativeTargetIsWithinAbsoluteArg(target) && - this._createParentExists(cwdGuestTarget, cwdHostTarget) - ) || - this._rootRelativeTargetPrefersCwd(target) || - ( - this._hostPathExists(cwdHostTarget) && - !(typeof rootHostPath === "string" && this._hostPathExists(rootHostPath)) - ) - ) - ) { - return { - guestPath: cwdGuestTarget, - hostPath: cwdHostTarget, - readOnly: cwdMapping?.readOnly === true, - }; - } + function fromArrayLike(array) { + var length = array.length < 0 ? 0 : checked(array.length) | 0; + var buf = createBuffer(length); + for (var i = 0; i < length; i += 1) { + buf[i] = array[i] & 255; } - return { - guestPath: rootGuestPath, - hostPath: rootHostPath, - readOnly: rootMapping?.readOnly === true, - }; + return buf; } - - _resolveAbsolutePath(target, preferCreateParent = false) { - const rootGuestPath = __agentOSPath().posix.normalize(target); - const rootMapping = this._resolveHostMappingForGuestPath(rootGuestPath); - const rootHostPath = rootMapping?.hostPath ?? null; - const cwdGuestPath = this._currentGuestCwd(); - if ( - cwdGuestPath !== "/" && - !this._rootRelativeTargetIsWithinAbsoluteArg(target) - ) { - const cwdGuestTarget = __agentOSPath().posix.resolve( - cwdGuestPath, - target.replace(/^\\/+/, ""), - ); - const cwdMapping = this._resolveHostMappingForGuestPath(cwdGuestTarget); - const cwdHostTarget = cwdMapping?.hostPath ?? null; - if ( - typeof cwdHostTarget === "string" && - ( - ( - preferCreateParent && - this._createParentExists(cwdGuestTarget, cwdHostTarget) - ) || - ( - this._hostPathExists(cwdHostTarget) && - !(typeof rootHostPath === "string" && this._hostPathExists(rootHostPath)) - ) - ) - ) { - return { - guestPath: cwdGuestTarget, - hostPath: cwdHostTarget, - readOnly: cwdMapping?.readOnly === true, - }; - } + function fromArrayView(arrayView) { + if (isInstance(arrayView, Uint8Array)) { + var copy = new Uint8Array(arrayView); + return fromArrayBuffer(copy.buffer, copy.byteOffset, copy.byteLength); } - return { - guestPath: rootGuestPath, - hostPath: rootHostPath, - readOnly: rootMapping?.readOnly === true, - }; + return fromArrayLike(arrayView); } - - _resolveDescriptorPath(fd, pathPtr, pathLen, options = {}) { - const entry = this._measureWasiPhase("descriptorEntry", () => this._descriptorEntry(fd)); - if (!entry) { - return { error: __agentOSWasiErrnoBadf }; + function fromArrayBuffer(array, byteOffset, length) { + if (byteOffset < 0 || array.byteLength < byteOffset) { + throw new RangeError('"offset" is outside of buffer bounds'); } - const target = this._measureWasiPhase("readString", () => this._readString(pathPtr, pathLen)); - const base = this._measureWasiPhase("descriptorPathBase", () => this._descriptorPathBase(entry, target)); - if (!base || typeof base.guestPath !== "string") { - return { error: __agentOSWasiErrnoBadf }; + if (array.byteLength < byteOffset + (length || 0)) { + throw new RangeError('"length" is outside of buffer bounds'); } - const guestPath = this._measureWasiPhase("guestPathResolve", () => - target.startsWith("/") - ? __agentOSPath().posix.normalize(target) - : __agentOSPath().posix.resolve(base.guestPath, target) - ); - const mapped = this._measureWasiPhase("descriptorPathMap", () => - target.startsWith("/") - ? this._resolveAbsolutePath( - target, - options.preferCreateParent === true, - ) - : base.guestPath === "/" - ? this._resolveRootRelativePath( - target, - options.preferCreateParent === true, - ) - : { - guestPath, - ...( - this._resolveHostMappingForGuestPath(guestPath) ?? - { hostPath: null, readOnly: false } - ), - } - ); - const hostPath = mapped.hostPath; - if (typeof hostPath !== "string") { - return { error: __agentOSWasiErrnoNoent }; + var buf; + if (byteOffset === void 0 && length === void 0) { + buf = new Uint8Array(array); + } else if (length === void 0) { + buf = new Uint8Array(array, byteOffset); + } else { + buf = new Uint8Array(array, byteOffset, length); } - return { - error: __agentOSWasiErrnoSuccess, - guestPath: mapped.guestPath, - hostPath, - readOnly: mapped.readOnly === true, - }; + Object.setPrototypeOf(buf, Buffer2.prototype); + return buf; } - - _resolvedFsPath(resolved) { - if (this._sidecarManagedProcess() && typeof resolved?.guestPath === "string") { - return resolved.guestPath; + function fromObject(obj) { + if (Buffer2.isBuffer(obj)) { + var len = checked(obj.length) | 0; + var buf = createBuffer(len); + if (buf.length === 0) { + return buf; + } + obj.copy(buf, 0, 0, len); + return buf; } - return resolved?.hostPath ?? null; - } - - _statResolvedPath(resolved, follow) { - if ( - typeof globalThis?.__agentOSSyncRpc?.callSync === "function" && - typeof resolved?.guestPath === "string" - ) { - return this._measureWasiPhase(follow ? "syncRpcStat" : "syncRpcLstat", () => - __agentOSWasiSyncRpc().callSync(follow ? "fs.statSync" : "fs.lstatSync", [ - resolved.guestPath, - ]) - ); + if (obj.length !== void 0) { + if (typeof obj.length !== "number" || numberIsNaN(obj.length)) { + return createBuffer(0); + } + return fromArrayLike(obj); } - const bridgeStat = follow ? globalThis?._fsStat : globalThis?._fsLstat; - if ( - typeof bridgeStat?.applySyncPromise === "function" && - typeof resolved?.guestPath === "string" - ) { - return this._measureWasiPhase(follow ? "bridgeStatSync" : "bridgeLstatSync", () => - bridgeStat.applySyncPromise(void 0, [resolved.guestPath]) - ); + if (obj.type === "Buffer" && Array.isArray(obj.data)) { + return fromArrayLike(obj.data); } - const fsPath = this._resolvedFsPath(resolved); - return this._measureWasiPhase(follow ? "fsModuleStatSync" : "fsModuleLstatSync", () => - follow ? this._fs().statSync(fsPath) : this._fs().lstatSync(fsPath) - ); } - - - _resolveDescriptorDirectStatPath(fd, target) { - if ( - typeof target !== "string" || - target.length === 0 || - target === "." || - target === ".." || - target.startsWith("/") - ) { - return null; + function checked(length) { + if (length >= K_MAX_LENGTH) { + throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x" + K_MAX_LENGTH.toString(16) + " bytes"); } - const entry = this._descriptorEntry(fd); - if ( - !entry || - (entry.kind !== "directory" && entry.kind !== "preopen") || - typeof entry.hostPath !== "string" || - entry.hostPath.length === 0 - ) { - return null; + return length | 0; + } + function SlowBuffer(length) { + if (+length != length) { + length = 0; } - const baseGuestPath = this._descriptorGuestPath(entry); - if (typeof baseGuestPath !== "string") { - return null; + return Buffer2.alloc(+length); + } + Buffer2.isBuffer = function isBuffer(b) { + return b != null && b._isBuffer === true && b !== Buffer2.prototype; + }; + Buffer2.compare = function compare(a, b) { + if (isInstance(a, Uint8Array)) a = Buffer2.from(a, a.offset, a.byteLength); + if (isInstance(b, Uint8Array)) b = Buffer2.from(b, b.offset, b.byteLength); + if (!Buffer2.isBuffer(a) || !Buffer2.isBuffer(b)) { + throw new TypeError( + 'The "buf1", "buf2" arguments must be one of type Buffer or Uint8Array' + ); } - if (entry.kind === "preopen" && baseGuestPath === "/") { - if (!this._rootRelativeTargetIsWithinAbsoluteArg(target)) { - return null; + if (a === b) return 0; + var x = a.length; + var y = b.length; + for (var i = 0, len = Math.min(x, y); i < len; ++i) { + if (a[i] !== b[i]) { + x = a[i]; + y = b[i]; + break; } - } else if (target.includes("/")) { - return null; } - return { - error: __agentOSWasiErrnoSuccess, - guestPath: this._path().posix.resolve(baseGuestPath, target), - hostPath: this._path().join(entry.hostPath, target), - readOnly: entry.readOnly === true, - }; - } - - _writeFilestat(statPtr, stats, fallbackType) { - try { - const view = this._memoryView(); - const offset = Number(statPtr) >>> 0; - const filetype = stats ? this._filetypeForStats(stats) : fallbackType; - const ino = - typeof stats?.inoExact === "bigint" ? stats.inoExact : BigInt(stats?.ino ?? 0); - const nlink = - typeof stats?.nlinkExact === "bigint" ? stats.nlinkExact : BigInt(stats?.nlink ?? 1); - const size = - typeof stats?.sizeExact === "bigint" ? stats.sizeExact : BigInt(stats?.size ?? 0); - view.setBigUint64(offset, 0n, true); - view.setBigUint64(offset + 8, ino, true); - view.setUint8(offset + 16, filetype); - view.setBigUint64(offset + 24, nlink, true); - view.setBigUint64(offset + 32, size, true); - view.setBigUint64(offset + 40, BigInt(Math.trunc((stats?.atimeMs ?? 0) * 1000000)), true); - view.setBigUint64(offset + 48, BigInt(Math.trunc((stats?.mtimeMs ?? 0) * 1000000)), true); - view.setBigUint64(offset + 56, BigInt(Math.trunc((stats?.ctimeMs ?? 0) * 1000000)), true); - return __agentOSWasiErrnoSuccess; - } catch (error) { - return this._mapFsError(error); + if (x < y) return -1; + if (y < x) return 1; + return 0; + }; + Buffer2.isEncoding = function isEncoding(encoding) { + switch (String(encoding).toLowerCase()) { + case "hex": + case "utf8": + case "utf-8": + case "ascii": + case "latin1": + case "binary": + case "base64": + case "ucs2": + case "ucs-2": + case "utf16le": + case "utf-16le": + return true; + default: + return false; } - } - - _argsSizesGet(argcPtr, argvBufSizePtr) { - const values = this._stringTable(this.args); - const total = values.reduce((sum, value) => sum + value.length, 0); - const argcStatus = this._writeUint32(argcPtr, values.length); - if (argcStatus !== __agentOSWasiErrnoSuccess) { - return argcStatus; + }; + Buffer2.concat = function concat(list, length) { + if (!Array.isArray(list)) { + throw new TypeError('"list" argument must be an Array of Buffers'); } - return this._writeUint32(argvBufSizePtr, total); - } - - _argsGet(argvPtr, argvBufPtr) { - return this._writeStringTable(this._stringTable(this.args), argvPtr, argvBufPtr); - } - - _environEntries() { - return Object.entries(this.env).map(([key, value]) => \`\${key}=\${value}\`); - } - - _environSizesGet(countPtr, bufSizePtr) { - const values = this._stringTable(this._environEntries()); - const total = values.reduce((sum, value) => sum + value.length, 0); - const countStatus = this._writeUint32(countPtr, values.length); - if (countStatus !== __agentOSWasiErrnoSuccess) { - return countStatus; + if (list.length === 0) { + return Buffer2.alloc(0); } - return this._writeUint32(bufSizePtr, total); - } - - _environGet(environPtr, environBufPtr) { - return this._writeStringTable( - this._stringTable(this._environEntries()), - environPtr, - environBufPtr, - ); - } - - _clockTimeGet(_clockId, _precision, resultPtr) { - return this._writeUint64(resultPtr, BigInt(Date.now()) * 1000000n); - } - - _clockResGet(_clockId, resultPtr) { - return this._writeUint64(resultPtr, 1000000n); - } - - _fdWrite(fd, iovs, iovsLen, nwrittenPtr) { - try { - const bytes = this._measureWasiPhase("collectIovs", () => this._collectIovs(iovs, iovsLen)); - const descriptor = Number(fd) >>> 0; - const handle = this._measureWasiPhase("externalFdHandle", () => this._externalFdHandle(descriptor)); - if (handle?.kind === "pipe-write" && handle.pipe) { - if (bytes.length > 0 && !this._pipeHasReaders(handle.pipe)) { - return __agentOSWasiErrnoPipe; - } - this._enqueuePipeBytes(handle.pipe, bytes); - this._flushPipeConsumers(handle.pipe); - return this._writeUint32(nwrittenPtr, bytes.length); - } - if ( - (handle?.kind === "passthrough" || handle?.kind === "host-passthrough") && - typeof handle.targetFd === "number" - ) { - if (handle.readOnly === true) { - return __agentOSWasiErrnoRofs; - } - if (descriptor === 1 || descriptor === 2) { - const sidecarManagedProcess = - typeof process?.env?.AGENTOS_SANDBOX_ROOT === "string" && - process.env.AGENTOS_SANDBOX_ROOT.length > 0; - const useKernelStdioSyncRpc = - sidecarManagedProcess || __agentOSKernelStdioSyncRpcEnabled(); - if (useKernelStdioSyncRpc) { - const written = this._measureWasiPhase("kernelStdioWrite", () => - Number( - __agentOSWasiSyncRpc().callSync("__kernel_stdio_write", [descriptor, bytes]), - ) >>> 0 - ); - return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); - } - } - const written = this._measureWasiPhase("writeSync", () => - __agentOSFs().writeSync( - handle.targetFd, - bytes, - 0, - bytes.length, - null, - ) - ); - return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); + var i; + if (length === void 0) { + length = 0; + for (i = 0; i < list.length; ++i) { + length += list[i].length; } - if (handle?.kind === "guest-file" && typeof handle.targetFd === "number") { - const position = handle.append ? null : (handle.position ?? 0); - const written = this._measureWasiPhase("writeSync", () => - __agentOSFs().writeSync( - handle.targetFd, - bytes, - 0, - bytes.length, - position, - ) - ); - if (handle.append) { - handle.position = this._measureWasiPhase("appendFstat", () => - Number(__agentOSFs().fstatSync(handle.targetFd).size ?? 0) - ); + } + var buffer2 = Buffer2.allocUnsafe(length); + var pos = 0; + for (i = 0; i < list.length; ++i) { + var buf = list[i]; + if (isInstance(buf, Uint8Array)) { + if (pos + buf.length > buffer2.length) { + Buffer2.from(buf).copy(buffer2, pos); } else { - handle.position = (handle.position ?? 0) + written; - } - return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); - } - if (handle?.kind === "stdio" && typeof handle.targetFd === "number") { - const targetFd = Number(handle.targetFd) >>> 0; - if (targetFd === 1 || targetFd === 2) { - const sidecarManagedProcess = - typeof process?.env?.AGENTOS_SANDBOX_ROOT === "string" && - process.env.AGENTOS_SANDBOX_ROOT.length > 0; - const useKernelStdioSyncRpc = - sidecarManagedProcess || __agentOSKernelStdioSyncRpcEnabled(); - const written = useKernelStdioSyncRpc - ? this._measureWasiPhase("kernelStdioWrite", () => - Number(__agentOSWasiSyncRpc().callSync("__kernel_stdio_write", [targetFd, bytes])) >>> 0 - ) - : this._measureWasiPhase("processStreamWrite", () => - (targetFd === 2 ? process.stderr.write(bytes) : process.stdout.write(bytes), bytes.length) - ); - return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); + Uint8Array.prototype.set.call( + buffer2, + buf, + pos + ); } - return __agentOSWasiErrnoBadf; - } - const entry = this.fdTable.get(descriptor); - if (!entry) { - return __agentOSWasiErrnoBadf; - } - if (entry.kind === "stdout") { - const sidecarManagedProcess = - typeof process?.env?.AGENTOS_SANDBOX_ROOT === "string" && - process.env.AGENTOS_SANDBOX_ROOT.length > 0; - const useKernelStdioSyncRpc = - sidecarManagedProcess || __agentOSKernelStdioSyncRpcEnabled(); - const written = useKernelStdioSyncRpc - ? this._measureWasiPhase("kernelStdioWrite", () => - Number(__agentOSWasiSyncRpc().callSync("__kernel_stdio_write", [1, bytes])) >>> 0 - ) - : this._measureWasiPhase("processStreamWrite", () => - (process.stdout.write(bytes), bytes.length) - ); - return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); - } - if (entry.kind === "stderr") { - const sidecarManagedProcess = - typeof process?.env?.AGENTOS_SANDBOX_ROOT === "string" && - process.env.AGENTOS_SANDBOX_ROOT.length > 0; - const useKernelStdioSyncRpc = - sidecarManagedProcess || __agentOSKernelStdioSyncRpcEnabled(); - const written = useKernelStdioSyncRpc - ? this._measureWasiPhase("kernelStdioWrite", () => - Number(__agentOSWasiSyncRpc().callSync("__kernel_stdio_write", [2, bytes])) >>> 0 - ) - : this._measureWasiPhase("processStreamWrite", () => - (process.stderr.write(bytes), bytes.length) - ); - return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); + } else if (!Buffer2.isBuffer(buf)) { + throw new TypeError('"list" argument must be an Array of Buffers'); + } else { + buf.copy(buffer2, pos); } - if (entry.readOnly === true) { - return __agentOSWasiErrnoRofs; + pos += buf.length; + } + return buffer2; + }; + function byteLength(string, encoding) { + if (Buffer2.isBuffer(string)) { + return string.length; + } + if (ArrayBuffer.isView(string) || isInstance(string, ArrayBuffer)) { + return string.byteLength; + } + if (typeof string !== "string") { + throw new TypeError( + 'The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type ' + typeof string + ); + } + var len = string.length; + var mustMatch = arguments.length > 2 && arguments[2] === true; + if (!mustMatch && len === 0) return 0; + var loweredCase = false; + for (; ; ) { + switch (encoding) { + case "ascii": + case "latin1": + case "binary": + return len; + case "utf8": + case "utf-8": + return utf8ToBytes(string).length; + case "ucs2": + case "ucs-2": + case "utf16le": + case "utf-16le": + return len * 2; + case "hex": + return len >>> 1; + case "base64": + return base64ToBytes(string).length; + default: + if (loweredCase) { + return mustMatch ? -1 : utf8ToBytes(string).length; + } + encoding = ("" + encoding).toLowerCase(); + loweredCase = true; } - if (entry.kind === "file") { - this._clearStatCache(); - const position = typeof entry.offset === "number" ? entry.offset : null; - const written = this._measureWasiPhase("writeSync", () => - __agentOSFs().writeSync( - entry.realFd, - bytes, - 0, - bytes.length, - position, - ) - ); - if (typeof entry.offset === "number") { - entry.offset += written; - } - return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); + } + } + Buffer2.byteLength = byteLength; + function slowToString(encoding, start, end) { + var loweredCase = false; + if (start === void 0 || start < 0) { + start = 0; + } + if (start > this.length) { + return ""; + } + if (end === void 0 || end > this.length) { + end = this.length; + } + if (end <= 0) { + return ""; + } + end >>>= 0; + start >>>= 0; + if (end <= start) { + return ""; + } + if (!encoding) encoding = "utf8"; + while (true) { + switch (encoding) { + case "hex": + return hexSlice(this, start, end); + case "utf8": + case "utf-8": + return utf8Slice(this, start, end); + case "ascii": + return asciiSlice(this, start, end); + case "latin1": + case "binary": + return latin1Slice(this, start, end); + case "base64": + return base64Slice(this, start, end); + case "ucs2": + case "ucs-2": + case "utf16le": + case "utf-16le": + return utf16leSlice(this, start, end); + default: + if (loweredCase) throw new TypeError("Unknown encoding: " + encoding); + encoding = (encoding + "").toLowerCase(); + loweredCase = true; } - return __agentOSWasiErrnoBadf; - } catch (error) { - return this._mapFsError(error); } } - - _positionedIoOffset(offset) { - const explicitOffset = Number(offset); - if (!Number.isFinite(explicitOffset) || explicitOffset < 0) { - return null; + Buffer2.prototype._isBuffer = true; + function swap(b, n, m) { + var i = b[n]; + b[n] = b[m]; + b[m] = i; + } + Buffer2.prototype.swap16 = function swap16() { + var len = this.length; + if (len % 2 !== 0) { + throw new RangeError("Buffer size must be a multiple of 16-bits"); } - return explicitOffset; + for (var i = 0; i < len; i += 2) { + swap(this, i, i + 1); + } + return this; + }; + Buffer2.prototype.swap32 = function swap32() { + var len = this.length; + if (len % 4 !== 0) { + throw new RangeError("Buffer size must be a multiple of 32-bits"); + } + for (var i = 0; i < len; i += 4) { + swap(this, i, i + 3); + swap(this, i + 1, i + 2); + } + return this; + }; + Buffer2.prototype.swap64 = function swap64() { + var len = this.length; + if (len % 8 !== 0) { + throw new RangeError("Buffer size must be a multiple of 64-bits"); + } + for (var i = 0; i < len; i += 8) { + swap(this, i, i + 7); + swap(this, i + 1, i + 6); + swap(this, i + 2, i + 5); + swap(this, i + 3, i + 4); + } + return this; + }; + Buffer2.prototype.toString = function toString() { + var length = this.length; + if (length === 0) return ""; + if (arguments.length === 0) return utf8Slice(this, 0, length); + return slowToString.apply(this, arguments); + }; + Buffer2.prototype.toLocaleString = Buffer2.prototype.toString; + Buffer2.prototype.equals = function equals(b) { + if (!Buffer2.isBuffer(b)) throw new TypeError("Argument must be a Buffer"); + if (this === b) return true; + return Buffer2.compare(this, b) === 0; + }; + Buffer2.prototype.inspect = function inspect() { + var str = ""; + var max = exports2.INSPECT_MAX_BYTES; + str = this.toString("hex", 0, max).replace(/(.{2})/g, "$1 ").trim(); + if (this.length > max) str += " ... "; + return ""; + }; + if (customInspectSymbol) { + Buffer2.prototype[customInspectSymbol] = Buffer2.prototype.inspect; } - - _fdPwrite(fd, iovs, iovsLen, offset, nwrittenPtr) { - try { - const bytes = this._collectIovs(iovs, iovsLen); - const descriptor = Number(fd) >>> 0; - const explicitOffset = this._positionedIoOffset(offset); - if (explicitOffset === null) { - return __agentOSWasiErrnoInval; - } - const handle = this._externalFdHandle(descriptor); - if ( - (handle?.kind === "passthrough" || handle?.kind === "host-passthrough") && - typeof handle.targetFd === "number" - ) { - if (handle.readOnly === true) { - return __agentOSWasiErrnoRofs; - } - const written = __agentOSFs().writeSync( - handle.targetFd, - bytes, - 0, - bytes.length, - explicitOffset, - ); - return this._writeUint32(nwrittenPtr, written); - } - const entry = this.fdTable.get(descriptor); - if (!entry || entry.kind !== "file") { - return __agentOSWasiErrnoBadf; - } - if (entry.readOnly === true) { - return __agentOSWasiErrnoRofs; - } - const written = __agentOSFs().writeSync( - entry.realFd, - bytes, - 0, - bytes.length, - explicitOffset, + Buffer2.prototype.compare = function compare(target, start, end, thisStart, thisEnd) { + if (isInstance(target, Uint8Array)) { + target = Buffer2.from(target, target.offset, target.byteLength); + } + if (!Buffer2.isBuffer(target)) { + throw new TypeError( + 'The "target" argument must be one of type Buffer or Uint8Array. Received type ' + typeof target ); - return this._writeUint32(nwrittenPtr, written); - } catch { - return __agentOSWasiErrnoFault; } - } - - _fdPread(fd, iovs, iovsLen, offset, nreadPtr) { - try { - const descriptor = Number(fd) >>> 0; - const explicitOffset = this._positionedIoOffset(offset); - if (explicitOffset === null) { - return __agentOSWasiErrnoInval; - } - const totalLength = this._boundedReadLength(iovs, iovsLen); - const buffer = Buffer.alloc(totalLength); - const handle = this._externalFdHandle(descriptor); - if ( - (handle?.kind === "passthrough" || handle?.kind === "host-passthrough") && - typeof handle.targetFd === "number" - ) { - const bytesRead = __agentOSFs().readSync( - handle.targetFd, - buffer, - 0, - totalLength, - explicitOffset, - ); - const written = this._writeToIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)); - return this._writeUint32(nreadPtr, written); + if (start === void 0) { + start = 0; + } + if (end === void 0) { + end = target ? target.length : 0; + } + if (thisStart === void 0) { + thisStart = 0; + } + if (thisEnd === void 0) { + thisEnd = this.length; + } + if (start < 0 || end > target.length || thisStart < 0 || thisEnd > this.length) { + throw new RangeError("out of range index"); + } + if (thisStart >= thisEnd && start >= end) { + return 0; + } + if (thisStart >= thisEnd) { + return -1; + } + if (start >= end) { + return 1; + } + start >>>= 0; + end >>>= 0; + thisStart >>>= 0; + thisEnd >>>= 0; + if (this === target) return 0; + var x = thisEnd - thisStart; + var y = end - start; + var len = Math.min(x, y); + var thisCopy = this.slice(thisStart, thisEnd); + var targetCopy = target.slice(start, end); + for (var i = 0; i < len; ++i) { + if (thisCopy[i] !== targetCopy[i]) { + x = thisCopy[i]; + y = targetCopy[i]; + break; } - const entry = this.fdTable.get(descriptor); - if (!entry || entry.kind !== "file") { - return __agentOSWasiErrnoBadf; + } + if (x < y) return -1; + if (y < x) return 1; + return 0; + }; + function bidirectionalIndexOf(buffer2, val, byteOffset, encoding, dir) { + if (buffer2.length === 0) return -1; + if (typeof byteOffset === "string") { + encoding = byteOffset; + byteOffset = 0; + } else if (byteOffset > 2147483647) { + byteOffset = 2147483647; + } else if (byteOffset < -2147483648) { + byteOffset = -2147483648; + } + byteOffset = +byteOffset; + if (numberIsNaN(byteOffset)) { + byteOffset = dir ? 0 : buffer2.length - 1; + } + if (byteOffset < 0) byteOffset = buffer2.length + byteOffset; + if (byteOffset >= buffer2.length) { + if (dir) return -1; + else byteOffset = buffer2.length - 1; + } else if (byteOffset < 0) { + if (dir) byteOffset = 0; + else return -1; + } + if (typeof val === "string") { + val = Buffer2.from(val, encoding); + } + if (Buffer2.isBuffer(val)) { + if (val.length === 0) { + return -1; } - const bytesRead = __agentOSFs().readSync( - entry.realFd, - buffer, - 0, - totalLength, - explicitOffset, - ); - const written = this._writeToIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)); - return this._writeUint32(nreadPtr, written); - } catch { - return __agentOSWasiErrnoFault; + return arrayIndexOf(buffer2, val, byteOffset, encoding, dir); + } else if (typeof val === "number") { + val = val & 255; + if (typeof Uint8Array.prototype.indexOf === "function") { + if (dir) { + return Uint8Array.prototype.indexOf.call(buffer2, val, byteOffset); + } else { + return Uint8Array.prototype.lastIndexOf.call(buffer2, val, byteOffset); + } + } + return arrayIndexOf(buffer2, [val], byteOffset, encoding, dir); } + throw new TypeError("val must be string, number or Buffer"); } - - _fdRead(fd, iovs, iovsLen, nreadPtr) { - try { - const descriptor = Number(fd) >>> 0; - const handle = this._externalFdHandle(descriptor); - if (handle?.kind === "pipe-read" && handle.pipe) { - const totalLength = this._boundedReadLength(iovs, iovsLen); - while (handle.pipe.chunks.length === 0) { - if (handle.pipe.writeHandleCount === 0 && handle.pipe.producers.size === 0) { - return this._writeUint32(nreadPtr, 0); - } - this._pumpPipeProducers(handle.pipe, 10); + function arrayIndexOf(arr, val, byteOffset, encoding, dir) { + var indexSize = 1; + var arrLength = arr.length; + var valLength = val.length; + if (encoding !== void 0) { + encoding = String(encoding).toLowerCase(); + if (encoding === "ucs2" || encoding === "ucs-2" || encoding === "utf16le" || encoding === "utf-16le") { + if (arr.length < 2 || val.length < 2) { + return -1; } - const chunk = this._dequeuePipeBytes(handle.pipe, totalLength); - const written = this._writeToIovs(iovs, iovsLen, chunk); - return this._writeUint32(nreadPtr, written); + indexSize = 2; + arrLength /= 2; + valLength /= 2; + byteOffset /= 2; } - if (handle?.kind === "stdio" && Number(handle.targetFd) === 0) { - const totalLength = this._boundedReadLength(iovs, iovsLen); - if (typeof __agentOSWasiHost.readStdin === "function") { - const value = __agentOSWasiHost.readStdin(totalLength); - if (value == null) { - return this._writeUint32(nreadPtr, 0); - } - const chunk = - typeof value === "string" - ? Buffer.from(value, "utf8") - : value instanceof Uint8Array - ? value - : Buffer.from(value); - if (chunk.length === 0) { - return this._writeUint32(nreadPtr, 0); + } + function read(buf, i2) { + if (indexSize === 1) { + return buf[i2]; + } else { + return buf.readUInt16BE(i2 * indexSize); + } + } + var i; + if (dir) { + var foundIndex = -1; + for (i = byteOffset; i < arrLength; i++) { + if (read(arr, i) === read(val, foundIndex === -1 ? 0 : i - foundIndex)) { + if (foundIndex === -1) foundIndex = i; + if (i - foundIndex + 1 === valLength) return foundIndex * indexSize; + } else { + if (foundIndex !== -1) i -= i - foundIndex; + foundIndex = -1; + } + } + } else { + if (byteOffset + valLength > arrLength) byteOffset = arrLength - valLength; + for (i = byteOffset; i >= 0; i--) { + var found = true; + for (var j = 0; j < valLength; j++) { + if (read(arr, i + j) !== read(val, j)) { + found = false; + break; } - const written = this._writeToIovs(iovs, iovsLen, chunk); - return this._writeUint32(nreadPtr, written); } - const buffer = Buffer.alloc(totalLength); - const bytesRead = __agentOSFs().readSync(0, buffer, 0, totalLength, null); - const written = this._writeToIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)); - return this._writeUint32(nreadPtr, written); + if (found) return i; } - const entry = this.fdTable.get(descriptor); - if (!entry) { - return __agentOSWasiErrnoBadf; + } + return -1; + } + Buffer2.prototype.includes = function includes(val, byteOffset, encoding) { + return this.indexOf(val, byteOffset, encoding) !== -1; + }; + Buffer2.prototype.indexOf = function indexOf(val, byteOffset, encoding) { + return bidirectionalIndexOf(this, val, byteOffset, encoding, true); + }; + Buffer2.prototype.lastIndexOf = function lastIndexOf(val, byteOffset, encoding) { + return bidirectionalIndexOf(this, val, byteOffset, encoding, false); + }; + function hexWrite(buf, string, offset, length) { + offset = Number(offset) || 0; + var remaining = buf.length - offset; + if (!length) { + length = remaining; + } else { + length = Number(length); + if (length > remaining) { + length = remaining; } - if (entry.kind === "stdin") { - const totalLength = this._boundedReadLength(iovs, iovsLen); - const syncRpc = - typeof globalThis?.__agentOSSyncRpc?.callSync === "function" - ? __agentOSWasiSyncRpc() - : null; - const sidecarManagedProcess = - typeof process?.env?.AGENTOS_SANDBOX_ROOT === "string" && - process.env.AGENTOS_SANDBOX_ROOT.length > 0; - if (syncRpc && (sidecarManagedProcess || __agentOSKernelStdioSyncRpcEnabled())) { - try { - let chunk = null; - while (true) { - const response = syncRpc.callSync("__kernel_stdin_read", [totalLength, 10]); - if ( - response && - typeof response === "object" && - typeof response.dataBase64 === "string" - ) { - chunk = Buffer.from(response.dataBase64, "base64"); - break; - } - if (response && typeof response === "object" && response.done === true) { - chunk = Buffer.alloc(0); - break; + } + var strLen = string.length; + if (length > strLen / 2) { + length = strLen / 2; + } + for (var i = 0; i < length; ++i) { + var parsed = parseInt(string.substr(i * 2, 2), 16); + if (numberIsNaN(parsed)) return i; + buf[offset + i] = parsed; + } + return i; + } + function utf8Write(buf, string, offset, length) { + return blitBuffer(utf8ToBytes(string, buf.length - offset), buf, offset, length); + } + function asciiWrite(buf, string, offset, length) { + return blitBuffer(asciiToBytes(string), buf, offset, length); + } + function base64Write(buf, string, offset, length) { + return blitBuffer(base64ToBytes(string), buf, offset, length); + } + function ucs2Write(buf, string, offset, length) { + return blitBuffer(utf16leToBytes(string, buf.length - offset), buf, offset, length); + } + Buffer2.prototype.write = function write(string, offset, length, encoding) { + if (offset === void 0) { + encoding = "utf8"; + length = this.length; + offset = 0; + } else if (length === void 0 && typeof offset === "string") { + encoding = offset; + length = this.length; + offset = 0; + } else if (isFinite(offset)) { + offset = offset >>> 0; + if (isFinite(length)) { + length = length >>> 0; + if (encoding === void 0) encoding = "utf8"; + } else { + encoding = length; + length = void 0; + } + } else { + throw new Error( + "Buffer.write(string, encoding, offset[, length]) is no longer supported" + ); + } + var remaining = this.length - offset; + if (length === void 0 || length > remaining) length = remaining; + if (string.length > 0 && (length < 0 || offset < 0) || offset > this.length) { + throw new RangeError("Attempt to write outside buffer bounds"); + } + if (!encoding) encoding = "utf8"; + var loweredCase = false; + for (; ; ) { + switch (encoding) { + case "hex": + return hexWrite(this, string, offset, length); + case "utf8": + case "utf-8": + return utf8Write(this, string, offset, length); + case "ascii": + case "latin1": + case "binary": + return asciiWrite(this, string, offset, length); + case "base64": + return base64Write(this, string, offset, length); + case "ucs2": + case "ucs-2": + case "utf16le": + case "utf-16le": + return ucs2Write(this, string, offset, length); + default: + if (loweredCase) throw new TypeError("Unknown encoding: " + encoding); + encoding = ("" + encoding).toLowerCase(); + loweredCase = true; + } + } + }; + Buffer2.prototype.toJSON = function toJSON() { + return { + type: "Buffer", + data: Array.prototype.slice.call(this._arr || this, 0) + }; + }; + function base64Slice(buf, start, end) { + if (start === 0 && end === buf.length) { + return base64.fromByteArray(buf); + } else { + return base64.fromByteArray(buf.slice(start, end)); + } + } + function utf8Slice(buf, start, end) { + end = Math.min(buf.length, end); + var res = []; + var i = start; + while (i < end) { + var firstByte = buf[i]; + var codePoint = null; + var bytesPerSequence = firstByte > 239 ? 4 : firstByte > 223 ? 3 : firstByte > 191 ? 2 : 1; + if (i + bytesPerSequence <= end) { + var secondByte, thirdByte, fourthByte, tempCodePoint; + switch (bytesPerSequence) { + case 1: + if (firstByte < 128) { + codePoint = firstByte; + } + break; + case 2: + secondByte = buf[i + 1]; + if ((secondByte & 192) === 128) { + tempCodePoint = (firstByte & 31) << 6 | secondByte & 63; + if (tempCodePoint > 127) { + codePoint = tempCodePoint; } - if ( - typeof Atomics?.wait === "function" && - typeof syntheticWaitArray !== "undefined" - ) { - Atomics.wait(syntheticWaitArray, 0, 0, 10); + } + break; + case 3: + secondByte = buf[i + 1]; + thirdByte = buf[i + 2]; + if ((secondByte & 192) === 128 && (thirdByte & 192) === 128) { + tempCodePoint = (firstByte & 15) << 12 | (secondByte & 63) << 6 | thirdByte & 63; + if (tempCodePoint > 2047 && (tempCodePoint < 55296 || tempCodePoint > 57343)) { + codePoint = tempCodePoint; } } - if (!chunk || chunk.length === 0) { - return this._writeUint32(nreadPtr, 0); + break; + case 4: + secondByte = buf[i + 1]; + thirdByte = buf[i + 2]; + fourthByte = buf[i + 3]; + if ((secondByte & 192) === 128 && (thirdByte & 192) === 128 && (fourthByte & 192) === 128) { + tempCodePoint = (firstByte & 15) << 18 | (secondByte & 63) << 12 | (thirdByte & 63) << 6 | fourthByte & 63; + if (tempCodePoint > 65535 && tempCodePoint < 1114112) { + codePoint = tempCodePoint; + } } - const written = this._writeToIovs(iovs, iovsLen, chunk); - return this._writeUint32(nreadPtr, written); - } catch { - // Fall back to direct stdin reads when the sync bridge is unavailable - // in the standalone runner bootstrap. - } - } - // Host-seam stdin (a non-native backend whose stdin is delivered through - // the runtime process object, not a kernel fd): read the queued bytes - // directly instead of fs.readSync on a descriptor the JS fs table does - // not own. - if (typeof __agentOSWasiHost.readStdin === "function") { - const value = __agentOSWasiHost.readStdin(totalLength); - if (value == null) { - return this._writeUint32(nreadPtr, 0); - } - const chunk = - typeof value === "string" - ? Buffer.from(value, "utf8") - : value instanceof Uint8Array - ? value - : Buffer.from(value); - if (chunk.length === 0) { - return this._writeUint32(nreadPtr, 0); - } - const written = this._writeToIovs(iovs, iovsLen, chunk); - return this._writeUint32(nreadPtr, written); } - const buffer = Buffer.alloc(totalLength); - const directStdinFd = - (handle?.kind === "passthrough" || handle?.kind === "host-passthrough") && - typeof handle.targetFd === "number" - ? handle.targetFd - : typeof process?.stdin?.fd === "number" - ? process.stdin.fd - : 0; - const bytesRead = __agentOSFs().readSync( - directStdinFd, - buffer, - 0, - totalLength, - null, - ); - const written = this._writeToIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)); - return this._writeUint32(nreadPtr, written); - } - if ( - (handle?.kind === "passthrough" || handle?.kind === "host-passthrough") && - typeof handle.targetFd === "number" - ) { - const totalLength = this._boundedReadLength(iovs, iovsLen); - const buffer = Buffer.alloc(totalLength); - const bytesRead = __agentOSFs().readSync( - handle.targetFd, - buffer, - 0, - totalLength, - null, - ); - const written = this._writeToIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)); - return this._writeUint32(nreadPtr, written); - } - if (entry.kind !== "file") { - return __agentOSWasiErrnoBadf; } - // WASI rights: a descriptor opened without FD_READ cannot be read. - if ( - typeof entry.rightsBase === "bigint" && - (entry.rightsBase & __agentOSWasiRightFdRead) === 0n - ) { - return __agentOSWasiErrnoNotcapable; + if (codePoint === null) { + codePoint = 65533; + bytesPerSequence = 1; + } else if (codePoint > 65535) { + codePoint -= 65536; + res.push(codePoint >>> 10 & 1023 | 55296); + codePoint = 56320 | codePoint & 1023; } - const totalLength = this._boundedReadLength(iovs, iovsLen); - const buffer = Buffer.alloc(totalLength); - const position = typeof entry.offset === "number" ? entry.offset : null; - const bytesRead = __agentOSFs().readSync( - entry.realFd, - buffer, - 0, - totalLength, - position, + res.push(codePoint); + i += bytesPerSequence; + } + return decodeCodePointsArray(res); + } + var MAX_ARGUMENTS_LENGTH = 4096; + function decodeCodePointsArray(codePoints) { + var len = codePoints.length; + if (len <= MAX_ARGUMENTS_LENGTH) { + return String.fromCharCode.apply(String, codePoints); + } + var res = ""; + var i = 0; + while (i < len) { + res += String.fromCharCode.apply( + String, + codePoints.slice(i, i += MAX_ARGUMENTS_LENGTH) ); - if (typeof entry.offset === "number") { - entry.offset += bytesRead; - } - const written = this._writeToIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)); - return this._writeUint32(nreadPtr, written); - } catch (error) { - return this._mapFsError(error); } - } - - _fdClose(fd) { - try { - const descriptor = Number(fd) >>> 0; - const handle = this._externalFdHandle(descriptor); - if (handle?.kind === "pipe-read" && handle.pipe) { - handle.open = false; - handle.pipe.readHandleCount = Math.max(0, (handle.pipe.readHandleCount ?? 0) - 1); - if (typeof handle.onClose === "function") { - handle.onClose(handle, descriptor); - } - return __agentOSWasiErrnoSuccess; - } - if (handle?.kind === "pipe-write" && handle.pipe) { - handle.open = false; - handle.pipe.writeHandleCount = Math.max(0, (handle.pipe.writeHandleCount ?? 0) - 1); - if (typeof handle.onClose === "function") { - handle.onClose(handle, descriptor); - } - return __agentOSWasiErrnoSuccess; - } - if (handle?.kind === "guest-file" || handle?.kind === "stdio") { - handle.open = false; - return __agentOSWasiErrnoSuccess; - } - const entry = this.fdTable.get(descriptor); - if (!entry) { - return __agentOSWasiErrnoBadf; - } - const retainedDelegateRefs = (() => { - try { - if (typeof globalThis.__agentOSWasiDelegateFdRefCount === "function") { - return Number(globalThis.__agentOSWasiDelegateFdRefCount(descriptor)) || 0; - } - } catch { - // Fall through to the default close path. - } - return 0; - })(); - if (entry.kind === "file" && retainedDelegateRefs <= 0) { - __agentOSFs().closeSync(entry.realFd); - } - if (descriptor > 2 && retainedDelegateRefs <= 0) { - this.fdTable.delete(descriptor); - } - return __agentOSWasiErrnoSuccess; - } catch { - return __agentOSWasiErrnoFault; + return res; + } + function asciiSlice(buf, start, end) { + var ret = ""; + end = Math.min(buf.length, end); + for (var i = start; i < end; ++i) { + ret += String.fromCharCode(buf[i] & 127); + } + return ret; + } + function latin1Slice(buf, start, end) { + var ret = ""; + end = Math.min(buf.length, end); + for (var i = start; i < end; ++i) { + ret += String.fromCharCode(buf[i]); + } + return ret; + } + function hexSlice(buf, start, end) { + var len = buf.length; + if (!start || start < 0) start = 0; + if (!end || end < 0 || end > len) end = len; + var out = ""; + for (var i = start; i < end; ++i) { + out += hexSliceLookupTable[buf[i]]; + } + return out; + } + function utf16leSlice(buf, start, end) { + var bytes = buf.slice(start, end); + var res = ""; + for (var i = 0; i < bytes.length - 1; i += 2) { + res += String.fromCharCode(bytes[i] + bytes[i + 1] * 256); + } + return res; + } + Buffer2.prototype.slice = function slice(start, end) { + var len = this.length; + start = ~~start; + end = end === void 0 ? len : ~~end; + if (start < 0) { + start += len; + if (start < 0) start = 0; + } else if (start > len) { + start = len; + } + if (end < 0) { + end += len; + if (end < 0) end = 0; + } else if (end > len) { + end = len; + } + if (end < start) end = start; + var newBuf = this.subarray(start, end); + Object.setPrototypeOf(newBuf, Buffer2.prototype); + return newBuf; + }; + function checkOffset(offset, ext, length) { + if (offset % 1 !== 0 || offset < 0) throw new RangeError("offset is not uint"); + if (offset + ext > length) throw new RangeError("Trying to access beyond buffer length"); + } + Buffer2.prototype.readUintLE = Buffer2.prototype.readUIntLE = function readUIntLE(offset, byteLength2, noAssert) { + offset = offset >>> 0; + byteLength2 = byteLength2 >>> 0; + if (!noAssert) checkOffset(offset, byteLength2, this.length); + var val = this[offset]; + var mul = 1; + var i = 0; + while (++i < byteLength2 && (mul *= 256)) { + val += this[offset + i] * mul; + } + return val; + }; + Buffer2.prototype.readUintBE = Buffer2.prototype.readUIntBE = function readUIntBE(offset, byteLength2, noAssert) { + offset = offset >>> 0; + byteLength2 = byteLength2 >>> 0; + if (!noAssert) { + checkOffset(offset, byteLength2, this.length); + } + var val = this[offset + --byteLength2]; + var mul = 1; + while (byteLength2 > 0 && (mul *= 256)) { + val += this[offset + --byteLength2] * mul; + } + return val; + }; + Buffer2.prototype.readUint8 = Buffer2.prototype.readUInt8 = function readUInt8(offset, noAssert) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 1, this.length); + return this[offset]; + }; + Buffer2.prototype.readUint16LE = Buffer2.prototype.readUInt16LE = function readUInt16LE(offset, noAssert) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 2, this.length); + return this[offset] | this[offset + 1] << 8; + }; + Buffer2.prototype.readUint16BE = Buffer2.prototype.readUInt16BE = function readUInt16BE(offset, noAssert) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 2, this.length); + return this[offset] << 8 | this[offset + 1]; + }; + Buffer2.prototype.readUint32LE = Buffer2.prototype.readUInt32LE = function readUInt32LE(offset, noAssert) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 4, this.length); + return (this[offset] | this[offset + 1] << 8 | this[offset + 2] << 16) + this[offset + 3] * 16777216; + }; + Buffer2.prototype.readUint32BE = Buffer2.prototype.readUInt32BE = function readUInt32BE(offset, noAssert) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 4, this.length); + return this[offset] * 16777216 + (this[offset + 1] << 16 | this[offset + 2] << 8 | this[offset + 3]); + }; + Buffer2.prototype.readIntLE = function readIntLE(offset, byteLength2, noAssert) { + offset = offset >>> 0; + byteLength2 = byteLength2 >>> 0; + if (!noAssert) checkOffset(offset, byteLength2, this.length); + var val = this[offset]; + var mul = 1; + var i = 0; + while (++i < byteLength2 && (mul *= 256)) { + val += this[offset + i] * mul; + } + mul *= 128; + if (val >= mul) val -= Math.pow(2, 8 * byteLength2); + return val; + }; + Buffer2.prototype.readIntBE = function readIntBE(offset, byteLength2, noAssert) { + offset = offset >>> 0; + byteLength2 = byteLength2 >>> 0; + if (!noAssert) checkOffset(offset, byteLength2, this.length); + var i = byteLength2; + var mul = 1; + var val = this[offset + --i]; + while (i > 0 && (mul *= 256)) { + val += this[offset + --i] * mul; } + mul *= 128; + if (val >= mul) val -= Math.pow(2, 8 * byteLength2); + return val; + }; + Buffer2.prototype.readInt8 = function readInt8(offset, noAssert) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 1, this.length); + if (!(this[offset] & 128)) return this[offset]; + return (255 - this[offset] + 1) * -1; + }; + Buffer2.prototype.readInt16LE = function readInt16LE(offset, noAssert) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 2, this.length); + var val = this[offset] | this[offset + 1] << 8; + return val & 32768 ? val | 4294901760 : val; + }; + Buffer2.prototype.readInt16BE = function readInt16BE(offset, noAssert) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 2, this.length); + var val = this[offset + 1] | this[offset] << 8; + return val & 32768 ? val | 4294901760 : val; + }; + Buffer2.prototype.readInt32LE = function readInt32LE(offset, noAssert) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 4, this.length); + return this[offset] | this[offset + 1] << 8 | this[offset + 2] << 16 | this[offset + 3] << 24; + }; + Buffer2.prototype.readInt32BE = function readInt32BE(offset, noAssert) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 4, this.length); + return this[offset] << 24 | this[offset + 1] << 16 | this[offset + 2] << 8 | this[offset + 3]; + }; + Buffer2.prototype.readFloatLE = function readFloatLE(offset, noAssert) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 4, this.length); + return ieee754.read(this, offset, true, 23, 4); + }; + Buffer2.prototype.readFloatBE = function readFloatBE(offset, noAssert) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 4, this.length); + return ieee754.read(this, offset, false, 23, 4); + }; + Buffer2.prototype.readDoubleLE = function readDoubleLE(offset, noAssert) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 8, this.length); + return ieee754.read(this, offset, true, 52, 8); + }; + Buffer2.prototype.readDoubleBE = function readDoubleBE(offset, noAssert) { + offset = offset >>> 0; + if (!noAssert) checkOffset(offset, 8, this.length); + return ieee754.read(this, offset, false, 52, 8); + }; + function checkInt(buf, value, offset, ext, max, min) { + if (!Buffer2.isBuffer(buf)) throw new TypeError('"buffer" argument must be a Buffer instance'); + if (value > max || value < min) throw new RangeError('"value" argument is out of bounds'); + if (offset + ext > buf.length) throw new RangeError("Index out of range"); } - - _fdSync(fd) { - try { - const descriptor = Number(fd) >>> 0; - const handle = this._externalFdHandle(descriptor); - if ( - (handle?.kind === "passthrough" || handle?.kind === "host-passthrough") && - typeof handle.targetFd === "number" - ) { - __agentOSFs().fsyncSync(handle.targetFd); - return __agentOSWasiErrnoSuccess; - } - const entry = this.fdTable.get(descriptor); - if (!entry) { - return __agentOSWasiErrnoBadf; - } - // fsync on a stdio stream (stdin/stdout/stderr) is a no-op success; only - // descriptors with a real backing fd are flushed. - if ( - entry.kind === "stdin" || - entry.kind === "stdout" || - entry.kind === "stderr" - ) { - return __agentOSWasiErrnoSuccess; - } - if (entry.kind !== "file" || typeof entry.realFd !== "number") { - return __agentOSWasiErrnoBadf; - } - __agentOSFs().fsyncSync(entry.realFd); - return __agentOSWasiErrnoSuccess; - } catch { - return __agentOSWasiErrnoFault; + Buffer2.prototype.writeUintLE = Buffer2.prototype.writeUIntLE = function writeUIntLE(value, offset, byteLength2, noAssert) { + value = +value; + offset = offset >>> 0; + byteLength2 = byteLength2 >>> 0; + if (!noAssert) { + var maxBytes = Math.pow(2, 8 * byteLength2) - 1; + checkInt(this, value, offset, byteLength2, maxBytes, 0); } - } - - _fdFdstatGet(fd, statPtr) { - try { - const entry = this._descriptorEntry(fd); - if (!entry) { - return __agentOSWasiErrnoBadf; - } - const view = this._memoryView(); - const offset = Number(statPtr) >>> 0; - view.setUint8(offset, this._fdFiletype(entry)); - view.setUint16(offset + 2, (Number(entry.fdFlags) >>> 0) & 0xffff, true); - view.setBigUint64(offset + 8, this._descriptorRightsBase(entry), true); - view.setBigUint64(offset + 16, this._descriptorRightsInheriting(entry), true); - return __agentOSWasiErrnoSuccess; - } catch { - return __agentOSWasiErrnoFault; + var mul = 1; + var i = 0; + this[offset] = value & 255; + while (++i < byteLength2 && (mul *= 256)) { + this[offset + i] = value / mul & 255; + } + return offset + byteLength2; + }; + Buffer2.prototype.writeUintBE = Buffer2.prototype.writeUIntBE = function writeUIntBE(value, offset, byteLength2, noAssert) { + value = +value; + offset = offset >>> 0; + byteLength2 = byteLength2 >>> 0; + if (!noAssert) { + var maxBytes = Math.pow(2, 8 * byteLength2) - 1; + checkInt(this, value, offset, byteLength2, maxBytes, 0); + } + var i = byteLength2 - 1; + var mul = 1; + this[offset + i] = value & 255; + while (--i >= 0 && (mul *= 256)) { + this[offset + i] = value / mul & 255; + } + return offset + byteLength2; + }; + Buffer2.prototype.writeUint8 = Buffer2.prototype.writeUInt8 = function writeUInt8(value, offset, noAssert) { + value = +value; + offset = offset >>> 0; + if (!noAssert) checkInt(this, value, offset, 1, 255, 0); + this[offset] = value & 255; + return offset + 1; + }; + Buffer2.prototype.writeUint16LE = Buffer2.prototype.writeUInt16LE = function writeUInt16LE(value, offset, noAssert) { + value = +value; + offset = offset >>> 0; + if (!noAssert) checkInt(this, value, offset, 2, 65535, 0); + this[offset] = value & 255; + this[offset + 1] = value >>> 8; + return offset + 2; + }; + Buffer2.prototype.writeUint16BE = Buffer2.prototype.writeUInt16BE = function writeUInt16BE(value, offset, noAssert) { + value = +value; + offset = offset >>> 0; + if (!noAssert) checkInt(this, value, offset, 2, 65535, 0); + this[offset] = value >>> 8; + this[offset + 1] = value & 255; + return offset + 2; + }; + Buffer2.prototype.writeUint32LE = Buffer2.prototype.writeUInt32LE = function writeUInt32LE(value, offset, noAssert) { + value = +value; + offset = offset >>> 0; + if (!noAssert) checkInt(this, value, offset, 4, 4294967295, 0); + this[offset + 3] = value >>> 24; + this[offset + 2] = value >>> 16; + this[offset + 1] = value >>> 8; + this[offset] = value & 255; + return offset + 4; + }; + Buffer2.prototype.writeUint32BE = Buffer2.prototype.writeUInt32BE = function writeUInt32BE(value, offset, noAssert) { + value = +value; + offset = offset >>> 0; + if (!noAssert) checkInt(this, value, offset, 4, 4294967295, 0); + this[offset] = value >>> 24; + this[offset + 1] = value >>> 16; + this[offset + 2] = value >>> 8; + this[offset + 3] = value & 255; + return offset + 4; + }; + Buffer2.prototype.writeIntLE = function writeIntLE(value, offset, byteLength2, noAssert) { + value = +value; + offset = offset >>> 0; + if (!noAssert) { + var limit = Math.pow(2, 8 * byteLength2 - 1); + checkInt(this, value, offset, byteLength2, limit - 1, -limit); } - } - - _fdFdstatSetFlags(fd, flags) { - try { - const entry = this._descriptorEntry(fd); - if (!entry) { - return __agentOSWasiErrnoBadf; + var i = 0; + var mul = 1; + var sub = 0; + this[offset] = value & 255; + while (++i < byteLength2 && (mul *= 256)) { + if (value < 0 && sub === 0 && this[offset + i - 1] !== 0) { + sub = 1; } - entry.fdFlags = (Number(flags) >>> 0) & 0xffff; - return __agentOSWasiErrnoSuccess; - } catch { - return __agentOSWasiErrnoFault; + this[offset + i] = (value / mul >> 0) - sub & 255; } - } - - _fdFilestatGet(fd, statPtr) { - try { - const entry = this._descriptorEntry(fd); - if (!entry) { - return __agentOSWasiErrnoBadf; - } - if ( - entry.kind === "stdin" || - entry.kind === "stdout" || - entry.kind === "stderr" - ) { - return this._writeFilestat(statPtr, null, __agentOSWasiFiletypeCharacterDevice); - } - if (entry.kind === "preopen") { - const stats = __agentOSFs().statSync(entry.guestPath); - return this._writeFilestat(statPtr, stats, __agentOSWasiFiletypeDirectory); - } - const stats = - typeof entry.realFd === "number" - ? __agentOSFs().fstatSync(entry.realFd) - : __agentOSFs().statSync(this._descriptorFsPath(entry)); - return this._writeFilestat(statPtr, stats, this._fdFiletype(entry)); - } catch (error) { - return this._mapFsError(error); + return offset + byteLength2; + }; + Buffer2.prototype.writeIntBE = function writeIntBE(value, offset, byteLength2, noAssert) { + value = +value; + offset = offset >>> 0; + if (!noAssert) { + var limit = Math.pow(2, 8 * byteLength2 - 1); + checkInt(this, value, offset, byteLength2, limit - 1, -limit); } - } - - _fdFilestatSetSize(fd, size) { - try { - const entry = this._descriptorEntry(fd); - if (!entry || entry.kind !== "file" || typeof entry.realFd !== "number") { - return __agentOSWasiErrnoBadf; - } - if (entry.readOnly === true) { - return __agentOSWasiErrnoRofs; + var i = byteLength2 - 1; + var mul = 1; + var sub = 0; + this[offset + i] = value & 255; + while (--i >= 0 && (mul *= 256)) { + if (value < 0 && sub === 0 && this[offset + i + 1] !== 0) { + sub = 1; } - __agentOSFs().ftruncateSync(entry.realFd, Number(size)); - return __agentOSWasiErrnoSuccess; - } catch (error) { - return this._mapFsError(error); + this[offset + i] = (value / mul >> 0) - sub & 255; } + return offset + byteLength2; + }; + Buffer2.prototype.writeInt8 = function writeInt8(value, offset, noAssert) { + value = +value; + offset = offset >>> 0; + if (!noAssert) checkInt(this, value, offset, 1, 127, -128); + if (value < 0) value = 255 + value + 1; + this[offset] = value & 255; + return offset + 1; + }; + Buffer2.prototype.writeInt16LE = function writeInt16LE(value, offset, noAssert) { + value = +value; + offset = offset >>> 0; + if (!noAssert) checkInt(this, value, offset, 2, 32767, -32768); + this[offset] = value & 255; + this[offset + 1] = value >>> 8; + return offset + 2; + }; + Buffer2.prototype.writeInt16BE = function writeInt16BE(value, offset, noAssert) { + value = +value; + offset = offset >>> 0; + if (!noAssert) checkInt(this, value, offset, 2, 32767, -32768); + this[offset] = value >>> 8; + this[offset + 1] = value & 255; + return offset + 2; + }; + Buffer2.prototype.writeInt32LE = function writeInt32LE(value, offset, noAssert) { + value = +value; + offset = offset >>> 0; + if (!noAssert) checkInt(this, value, offset, 4, 2147483647, -2147483648); + this[offset] = value & 255; + this[offset + 1] = value >>> 8; + this[offset + 2] = value >>> 16; + this[offset + 3] = value >>> 24; + return offset + 4; + }; + Buffer2.prototype.writeInt32BE = function writeInt32BE(value, offset, noAssert) { + value = +value; + offset = offset >>> 0; + if (!noAssert) checkInt(this, value, offset, 4, 2147483647, -2147483648); + if (value < 0) value = 4294967295 + value + 1; + this[offset] = value >>> 24; + this[offset + 1] = value >>> 16; + this[offset + 2] = value >>> 8; + this[offset + 3] = value & 255; + return offset + 4; + }; + function checkIEEE754(buf, value, offset, ext, max, min) { + if (offset + ext > buf.length) throw new RangeError("Index out of range"); + if (offset < 0) throw new RangeError("Index out of range"); } - - _fdSeek(fd, offset, whence, newOffsetPtr) { - try { - const entry = this._descriptorEntry(fd); - if (!entry || entry.kind !== "file" || typeof entry.realFd !== "number") { - return __agentOSWasiErrnoBadf; - } - const delta = Number(offset); - if (!Number.isFinite(delta)) { - return __agentOSWasiErrnoInval; - } - const currentOffset = typeof entry.offset === "number" ? entry.offset : 0; - let nextOffset = 0; - switch (Number(whence) >>> 0) { - case __agentOSWasiWhenceSet: - nextOffset = delta; - break; - case __agentOSWasiWhenceCur: - nextOffset = currentOffset + delta; - break; - case __agentOSWasiWhenceEnd: { - const stats = __agentOSFs().fstatSync(entry.realFd); - nextOffset = Number(stats?.size ?? 0) + delta; - break; - } - default: - return __agentOSWasiErrnoInval; - } - if (!Number.isFinite(nextOffset) || nextOffset < 0) { - return __agentOSWasiErrnoInval; - } - entry.offset = nextOffset; - return this._writeUint64(newOffsetPtr, BigInt(nextOffset)); - } catch (error) { - return this._mapFsError(error); + function writeFloat(buf, value, offset, littleEndian, noAssert) { + value = +value; + offset = offset >>> 0; + if (!noAssert) { + checkIEEE754(buf, value, offset, 4, 34028234663852886e22, -34028234663852886e22); } + ieee754.write(buf, value, offset, littleEndian, 23, 4); + return offset + 4; } - - _fdTell(fd, offsetPtr) { - try { - const entry = this._descriptorEntry(fd); - if (!entry || entry.kind !== "file") { - return __agentOSWasiErrnoBadf; - } - const offset = typeof entry.offset === "number" ? entry.offset : 0; - return this._writeUint64(offsetPtr, BigInt(offset)); - } catch (error) { - return this._mapFsError(error); + Buffer2.prototype.writeFloatLE = function writeFloatLE(value, offset, noAssert) { + return writeFloat(this, value, offset, true, noAssert); + }; + Buffer2.prototype.writeFloatBE = function writeFloatBE(value, offset, noAssert) { + return writeFloat(this, value, offset, false, noAssert); + }; + function writeDouble(buf, value, offset, littleEndian, noAssert) { + value = +value; + offset = offset >>> 0; + if (!noAssert) { + checkIEEE754(buf, value, offset, 8, 17976931348623157e292, -17976931348623157e292); } + ieee754.write(buf, value, offset, littleEndian, 52, 8); + return offset + 8; } - - _fdPrestatGet(fd, prestatPtr) { - try { - const entry = this._descriptorEntry(fd); - if (!entry || entry.kind !== "preopen") { - return __agentOSWasiErrnoBadf; + Buffer2.prototype.writeDoubleLE = function writeDoubleLE(value, offset, noAssert) { + return writeDouble(this, value, offset, true, noAssert); + }; + Buffer2.prototype.writeDoubleBE = function writeDoubleBE(value, offset, noAssert) { + return writeDouble(this, value, offset, false, noAssert); + }; + Buffer2.prototype.copy = function copy(target, targetStart, start, end) { + if (!Buffer2.isBuffer(target)) throw new TypeError("argument should be a Buffer"); + if (!start) start = 0; + if (!end && end !== 0) end = this.length; + if (targetStart >= target.length) targetStart = target.length; + if (!targetStart) targetStart = 0; + if (end > 0 && end < start) end = start; + if (end === start) return 0; + if (target.length === 0 || this.length === 0) return 0; + if (targetStart < 0) { + throw new RangeError("targetStart out of bounds"); + } + if (start < 0 || start >= this.length) throw new RangeError("Index out of range"); + if (end < 0) throw new RangeError("sourceEnd out of bounds"); + if (end > this.length) end = this.length; + if (target.length - targetStart < end - start) { + end = target.length - targetStart + start; + } + var len = end - start; + if (this === target && typeof Uint8Array.prototype.copyWithin === "function") { + this.copyWithin(targetStart, start, end); + } else { + Uint8Array.prototype.set.call( + target, + this.subarray(start, end), + targetStart + ); + } + return len; + }; + Buffer2.prototype.fill = function fill(val, start, end, encoding) { + if (typeof val === "string") { + if (typeof start === "string") { + encoding = start; + start = 0; + end = this.length; + } else if (typeof end === "string") { + encoding = end; + end = this.length; } - const guestPath = this._descriptorPreopenName(entry); - if (typeof guestPath !== "string") { - return __agentOSWasiErrnoBadf; + if (encoding !== void 0 && typeof encoding !== "string") { + throw new TypeError("encoding must be a string"); + } + if (typeof encoding === "string" && !Buffer2.isEncoding(encoding)) { + throw new TypeError("Unknown encoding: " + encoding); + } + if (val.length === 1) { + var code = val.charCodeAt(0); + if (encoding === "utf8" && code < 128 || encoding === "latin1") { + val = code; + } } - const view = this._memoryView(); - const offset = Number(prestatPtr) >>> 0; - view.setUint8(offset, 0); - view.setUint32(offset + 4, Buffer.byteLength(guestPath), true); - return __agentOSWasiErrnoSuccess; - } catch { - return __agentOSWasiErrnoFault; + } else if (typeof val === "number") { + val = val & 255; + } else if (typeof val === "boolean") { + val = Number(val); } - } - - _fdPrestatDirName(fd, pathPtr, pathLen) { - try { - const entry = this._descriptorEntry(fd); - if (!entry || entry.kind !== "preopen") { - return __agentOSWasiErrnoBadf; + if (start < 0 || this.length < start || this.length < end) { + throw new RangeError("Out of range index"); + } + if (end <= start) { + return this; + } + start = start >>> 0; + end = end === void 0 ? this.length : end >>> 0; + if (!val) val = 0; + var i; + if (typeof val === "number") { + for (i = start; i < end; ++i) { + this[i] = val; } - const guestPath = this._descriptorPreopenName(entry); - if (typeof guestPath !== "string") { - return __agentOSWasiErrnoBadf; + } else { + var bytes = Buffer2.isBuffer(val) ? val : Buffer2.from(val, encoding); + var len = bytes.length; + if (len === 0) { + throw new TypeError('The value "' + val + '" is invalid for argument "value"'); } - const bytes = Buffer.from(guestPath, "utf8"); - if ((Number(pathLen) >>> 0) < bytes.length) { - return __agentOSWasiErrnoFault; + for (i = 0; i < end - start; ++i) { + this[i + start] = bytes[i % len]; } - return this._writeBytes(pathPtr, bytes); - } catch { - return __agentOSWasiErrnoFault; } + return this; + }; + var INVALID_BASE64_RE = /[^+/0-9A-Za-z-_]/g; + function base64clean(str) { + str = str.split("=")[0]; + str = str.trim().replace(INVALID_BASE64_RE, ""); + if (str.length < 2) return ""; + while (str.length % 4 !== 0) { + str = str + "="; + } + return str; } - - _fdReaddir(fd, bufPtr, bufLen, cookie, bufUsedPtr) { - const startedAt = __agentOSWasiNow(); - const requestedCookie = Number(cookie) >>> 0; - const requestedBufLen = Number(bufLen) >>> 0; - try { - const entry = this._descriptorEntry(fd); - const fsPath = this._descriptorDirectoryFsPath(entry); - if ( - !entry || - (entry.kind !== "preopen" && entry.kind !== "directory") || - typeof fsPath !== "string" - ) { - return __agentOSWasiErrnoBadf; - } - let dirents = - requestedCookie > 0 && Array.isArray(entry.readdirCache) - ? entry.readdirCache - : null; - if (!dirents) { - dirents = __agentOSFs() - .readdirSync(fsPath, { withFileTypes: true }) - .map((entry) => - typeof entry === "string" - ? { name: entry, filetype: __agentOSWasiFiletypeUnknown } - : { - name: String(entry?.name ?? ""), - filetype: this._filetypeForDirent(entry), - } - ) - .sort((left, right) => left.name.localeCompare(right.name)); - entry.readdirCache = dirents; - } - const view = this._memoryView(); - const memory = this._memoryBytes(); - let offset = Number(bufPtr) >>> 0; - const limit = offset + requestedBufLen; - let used = 0; - let recordsReturned = 0; - let stoppedRecordTooLarge = false; - for (let index = requestedCookie; index < dirents.length; index += 1) { - const dirent = dirents[index]; - const name = typeof dirent === "string" ? dirent : String(dirent?.name ?? ""); - const filetype = - typeof dirent === "object" - ? Number(dirent?.filetype ?? __agentOSWasiFiletypeUnknown) >>> 0 - : __agentOSWasiFiletypeUnknown; - const nameBytes = Buffer.from(name, "utf8"); - const recordLen = 24 + nameBytes.length; - if (offset + recordLen > limit) { - const remaining = Math.max(0, limit - offset); - if (remaining > 0) { - const record = Buffer.alloc(recordLen); - const recordView = new DataView( - record.buffer, - record.byteOffset, - record.byteLength, - ); - recordView.setBigUint64(0, BigInt(index + 1), true); - recordView.setBigUint64(8, BigInt(index + 1), true); - recordView.setUint32(16, nameBytes.length, true); - recordView.setUint8( - 20, - filetype, - ); - record.set(nameBytes, 24); - memory.set(record.subarray(0, remaining), offset); - offset += remaining; - used += remaining; + function utf8ToBytes(string, units) { + units = units || Infinity; + var codePoint; + var length = string.length; + var leadSurrogate = null; + var bytes = []; + for (var i = 0; i < length; ++i) { + codePoint = string.charCodeAt(i); + if (codePoint > 55295 && codePoint < 57344) { + if (!leadSurrogate) { + if (codePoint > 56319) { + if ((units -= 3) > -1) bytes.push(239, 191, 189); + continue; + } else if (i + 1 === length) { + if ((units -= 3) > -1) bytes.push(239, 191, 189); + continue; } - stoppedRecordTooLarge = true; - break; + leadSurrogate = codePoint; + continue; } - view.setBigUint64(offset, BigInt(index + 1), true); - view.setBigUint64(offset + 8, BigInt(index + 1), true); - view.setUint32(offset + 16, nameBytes.length, true); - view.setUint8( - offset + 20, - filetype, + if (codePoint < 56320) { + if ((units -= 3) > -1) bytes.push(239, 191, 189); + leadSurrogate = codePoint; + continue; + } + codePoint = (leadSurrogate - 55296 << 10 | codePoint - 56320) + 65536; + } else if (leadSurrogate) { + if ((units -= 3) > -1) bytes.push(239, 191, 189); + } + leadSurrogate = null; + if (codePoint < 128) { + if ((units -= 1) < 0) break; + bytes.push(codePoint); + } else if (codePoint < 2048) { + if ((units -= 2) < 0) break; + bytes.push( + codePoint >> 6 | 192, + codePoint & 63 | 128 ); - memory.set(nameBytes, offset + 24); - offset += recordLen; - used += recordLen; - recordsReturned += 1; + } else if (codePoint < 65536) { + if ((units -= 3) < 0) break; + bytes.push( + codePoint >> 12 | 224, + codePoint >> 6 & 63 | 128, + codePoint & 63 | 128 + ); + } else if (codePoint < 1114112) { + if ((units -= 4) < 0) break; + bytes.push( + codePoint >> 18 | 240, + codePoint >> 12 & 63 | 128, + codePoint >> 6 & 63 | 128, + codePoint & 63 | 128 + ); + } else { + throw new Error("Invalid code point"); + } + } + return bytes; + } + function asciiToBytes(str) { + var byteArray = []; + for (var i = 0; i < str.length; ++i) { + byteArray.push(str.charCodeAt(i) & 255); + } + return byteArray; + } + function utf16leToBytes(str, units) { + var c, hi, lo; + var byteArray = []; + for (var i = 0; i < str.length; ++i) { + if ((units -= 2) < 0) break; + c = str.charCodeAt(i); + hi = c >> 8; + lo = c % 256; + byteArray.push(lo); + byteArray.push(hi); + } + return byteArray; + } + function base64ToBytes(str) { + return base64.toByteArray(base64clean(str)); + } + function blitBuffer(src, dst, offset, length) { + for (var i = 0; i < length; ++i) { + if (i + offset >= dst.length || i >= src.length) break; + dst[i + offset] = src[i]; + } + return i; + } + function isInstance(obj, type) { + return obj instanceof type || obj != null && obj.constructor != null && obj.constructor.name != null && obj.constructor.name === type.name; + } + function numberIsNaN(obj) { + return obj !== obj; + } + var hexSliceLookupTable = (function() { + var alphabet = "0123456789abcdef"; + var table = new Array(256); + for (var i = 0; i < 16; ++i) { + var i16 = i * 16; + for (var j = 0; j < 16; ++j) { + table[i16 + j] = alphabet[i] + alphabet[j]; } - const result = this._writeUint32(bufUsedPtr, used); - this._recordWasiSyscallMetric("fd_readdir", startedAt, { - result, - fd: Number(fd) >>> 0, - cookie: requestedCookie, - bufLen: requestedBufLen, - used, - recordsReturned, - totalDirentsRead: dirents.length, - stoppedRecordTooLarge, - }); - return result; - } catch (error) { - this._recordWasiSyscallMetric("fd_readdir", startedAt, { - result: "error", - fd: Number(fd) >>> 0, - cookie: requestedCookie, - bufLen: requestedBufLen, - }); - return this._mapFsError(error); } - } + return table; + })(); + } +}); - _pathCreateDirectory(fd, pathPtr, pathLen) { - try { - const resolved = this._resolveDescriptorPath(fd, pathPtr, pathLen, { - preferCreateParent: true, - }); - if (resolved.error !== __agentOSWasiErrnoSuccess) { - return resolved.error; - } - if (resolved.readOnly) { - return __agentOSWasiErrnoRofs; - } - this._clearStatCache(); - __agentOSFs().mkdirSync(this._resolvedFsPath(resolved)); - return __agentOSWasiErrnoSuccess; - } catch (error) { - return this._mapFsError(error); +// +var buffer = require_buffer(); +module.exports = buffer.default ?? buffer; +/*! Bundled license information: + +ieee754/index.js: + (*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh *) + +buffer/index.js: + (*! + * The buffer module from node.js, for the browser. + * + * @author Feross Aboukhadijeh + * @license MIT + *) +*/ + +if (module.exports && module.exports.default == null) module.exports.default = module.exports; +`;var Xu=`var process = globalThis.process || { + env: {}, + cwd: () => '/', +}; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; + +// node_modules/.pnpm/path-browserify@1.0.1/node_modules/path-browserify/index.js +var require_path_browserify = __commonJS({ + "node_modules/.pnpm/path-browserify@1.0.1/node_modules/path-browserify/index.js"(exports2, module2) { + "use strict"; + function assertPath(path2) { + if (typeof path2 !== "string") { + throw new TypeError("Path must be a string. Received " + JSON.stringify(path2)); } } - - _pathLink(oldFd, _oldFlags, oldPathPtr, oldPathLen, newFd, newPathPtr, newPathLen) { - try { - const source = this._resolveDescriptorPath(oldFd, oldPathPtr, oldPathLen); - if (source.error !== __agentOSWasiErrnoSuccess) { - return source.error; - } - const destination = this._resolveDescriptorPath(newFd, newPathPtr, newPathLen); - if (destination.error !== __agentOSWasiErrnoSuccess) { - return destination.error; - } - if (source.readOnly || destination.readOnly) { - return __agentOSWasiErrnoRofs; + function normalizeStringPosix(path2, allowAboveRoot) { + var res = ""; + var lastSegmentLength = 0; + var lastSlash = -1; + var dots = 0; + var code; + for (var i = 0; i <= path2.length; ++i) { + if (i < path2.length) + code = path2.charCodeAt(i); + else if (code === 47) + break; + else + code = 47; + if (code === 47) { + if (lastSlash === i - 1 || dots === 1) { + } else if (lastSlash !== i - 1 && dots === 2) { + if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) { + if (res.length > 2) { + var lastSlashIndex = res.lastIndexOf("/"); + if (lastSlashIndex !== res.length - 1) { + if (lastSlashIndex === -1) { + res = ""; + lastSegmentLength = 0; + } else { + res = res.slice(0, lastSlashIndex); + lastSegmentLength = res.length - 1 - res.lastIndexOf("/"); + } + lastSlash = i; + dots = 0; + continue; + } + } else if (res.length === 2 || res.length === 1) { + res = ""; + lastSegmentLength = 0; + lastSlash = i; + dots = 0; + continue; + } + } + if (allowAboveRoot) { + if (res.length > 0) + res += "/.."; + else + res = ".."; + lastSegmentLength = 2; + } + } else { + if (res.length > 0) + res += "/" + path2.slice(lastSlash + 1, i); + else + res = path2.slice(lastSlash + 1, i); + lastSegmentLength = i - lastSlash - 1; + } + lastSlash = i; + dots = 0; + } else if (code === 46 && dots !== -1) { + ++dots; + } else { + dots = -1; } - this._clearStatCache(); - __agentOSFs().linkSync(this._resolvedFsPath(source), this._resolvedFsPath(destination)); - return __agentOSWasiErrnoSuccess; - } catch (error) { - return this._mapFsError(error); } + return res; } - - _pathOpen(fd, _dirflags, pathPtr, pathLen, oflags, rightsBase, rightsInheriting, _fdflags, openedFdPtr) { - try { - const entry = this._measureWasiPhase("descriptorEntry", () => this._descriptorEntry(fd)); - if ( - !entry || - (entry.kind !== "preopen" && entry.kind !== "directory") || - typeof entry.hostPath !== "string" - ) { - return __agentOSWasiErrnoBadf; - } - const requestedFlags = Number(oflags) >>> 0; - const createOrTruncate = - (requestedFlags & __agentOSWasiOpenCreate) !== 0 || - (requestedFlags & __agentOSWasiOpenTruncate) !== 0; - const resolved = this._measureWasiPhase("resolveDescriptorPath", () => - this._resolveDescriptorPath(fd, pathPtr, pathLen, { - preferCreateParent: createOrTruncate, - }) - ); - if (resolved.error !== __agentOSWasiErrnoSuccess) { - return resolved.error; - } - const guestPath = resolved.guestPath; - const fsPath = this._resolvedFsPath(resolved); - const openDirectory = (requestedFlags & __agentOSWasiOpenDirectory) !== 0; - const allowedRightsBase = this._descriptorRightsBase(entry); - const allowedRightsInheriting = this._descriptorRightsInheriting(entry); - const requestedRightsBase = this._normalizeRights(rightsBase, allowedRightsInheriting); - const requestedRightsInheriting = this._normalizeRights( - rightsInheriting, - allowedRightsInheriting, - ); - if ( - (requestedRightsBase & ~allowedRightsInheriting) !== 0n || - (requestedRightsInheriting & ~allowedRightsInheriting) !== 0n - ) { - return __agentOSWasiErrnoAcces; - } - const requestedWriteAccess = - !openDirectory && - (createOrTruncate || this._hasWriteRights(requestedRightsBase)); - if ( - requestedWriteAccess && - !this._hasWriteRights(allowedRightsBase) - ) { - return __agentOSWasiErrnoAcces; - } - if (requestedWriteAccess && resolved.readOnly) { - return __agentOSWasiErrnoRofs; - } - if (createOrTruncate) { - this._clearStatCache(); - } - const fsConstants = __agentOSFs().constants ?? {}; - let openFlags = requestedWriteAccess - ? fsConstants.O_RDWR ?? 2 - : fsConstants.O_RDONLY ?? 0; - if ((requestedFlags & __agentOSWasiOpenCreate) !== 0) { - openFlags |= fsConstants.O_CREAT ?? 64; - } - if ((requestedFlags & __agentOSWasiOpenExclusive) !== 0) { - openFlags |= fsConstants.O_EXCL ?? 128; - } - if ((requestedFlags & __agentOSWasiOpenTruncate) !== 0) { - openFlags |= fsConstants.O_TRUNC ?? 512; - } - if (openDirectory) { - openFlags |= fsConstants.O_DIRECTORY ?? 0; - } - const realFd = this._measureWasiPhase("openSync", () => __agentOSFs().openSync(fsPath, openFlags)); - const openedKind = openDirectory || createOrTruncate - ? (openDirectory ? "directory" : "file") - : this._measureWasiPhase("postOpenStat", () => - __agentOSFs().statSync(fsPath).isDirectory() ? "directory" : "file" - ); - const openedFd = this.nextFd++; - this._measureWasiPhase("fdTableSet", () => { - this.fdTable.set(openedFd, { - kind: openedKind, - guestPath, - hostPath: fsPath, - readOnly: resolved.readOnly === true, - realFd, - offset: 0, - rightsBase: requestedRightsBase & allowedRightsInheriting, - rightsInheriting: requestedRightsInheriting & allowedRightsInheriting, - fdFlags: (Number(_fdflags) >>> 0) & 0xffff, - }); - }); - return this._measureWasiPhase("writeOpenedFd", () => this._writeUint32(openedFdPtr, openedFd)); - } catch (error) { - return this._mapFsError(error); + function _format(sep, pathObject) { + var dir = pathObject.dir || pathObject.root; + var base = pathObject.base || (pathObject.name || "") + (pathObject.ext || ""); + if (!dir) { + return base; + } + if (dir === pathObject.root) { + return dir + base; } + return dir + sep + base; } - - _pathSymlink(targetPtr, targetLen, fd, pathPtr, pathLen) { - try { - const resolved = this._resolveDescriptorPath(fd, pathPtr, pathLen); - if (resolved.error !== __agentOSWasiErrnoSuccess) { - return resolved.error; - } - if (resolved.readOnly) { - return __agentOSWasiErrnoRofs; + var posix2 = { + // path.resolve([from ...], to) + resolve: function resolve() { + var resolvedPath = ""; + var resolvedAbsolute = false; + var cwd; + for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { + var path2; + if (i >= 0) + path2 = arguments[i]; + else { + if (cwd === void 0) + cwd = process.cwd(); + path2 = cwd; + } + assertPath(path2); + if (path2.length === 0) { + continue; + } + resolvedPath = path2 + "/" + resolvedPath; + resolvedAbsolute = path2.charCodeAt(0) === 47; } - const target = this._readString(targetPtr, targetLen); - this._clearStatCache(); - __agentOSFs().symlinkSync(target, this._resolvedFsPath(resolved)); - return __agentOSWasiErrnoSuccess; - } catch (error) { - return this._mapFsError(error); - } - } - - _pathRemoveDirectory(fd, pathPtr, pathLen) { - try { - const resolved = this._resolveDescriptorPath(fd, pathPtr, pathLen); - if (resolved.error !== __agentOSWasiErrnoSuccess) { - return resolved.error; + resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute); + if (resolvedAbsolute) { + if (resolvedPath.length > 0) + return "/" + resolvedPath; + else + return "/"; + } else if (resolvedPath.length > 0) { + return resolvedPath; + } else { + return "."; } - if (resolved.readOnly) { - return __agentOSWasiErrnoRofs; + }, + normalize: function normalize(path2) { + assertPath(path2); + if (path2.length === 0) return "."; + var isAbsolute = path2.charCodeAt(0) === 47; + var trailingSeparator = path2.charCodeAt(path2.length - 1) === 47; + path2 = normalizeStringPosix(path2, !isAbsolute); + if (path2.length === 0 && !isAbsolute) path2 = "."; + if (path2.length > 0 && trailingSeparator) path2 += "/"; + if (isAbsolute) return "/" + path2; + return path2; + }, + isAbsolute: function isAbsolute(path2) { + assertPath(path2); + return path2.length > 0 && path2.charCodeAt(0) === 47; + }, + join: function join() { + if (arguments.length === 0) + return "."; + var joined; + for (var i = 0; i < arguments.length; ++i) { + var arg = arguments[i]; + assertPath(arg); + if (arg.length > 0) { + if (joined === void 0) + joined = arg; + else + joined += "/" + arg; + } } - this._clearStatCache(); - __agentOSFs().rmdirSync(this._resolvedFsPath(resolved)); - return __agentOSWasiErrnoSuccess; - } catch (error) { - return this._mapFsError(error); - } - } - - _pathRename(oldFd, oldPathPtr, oldPathLen, newFd, newPathPtr, newPathLen) { - try { - const source = this._resolveDescriptorPath(oldFd, oldPathPtr, oldPathLen); - if (source.error !== __agentOSWasiErrnoSuccess) { - return source.error; + if (joined === void 0) + return "."; + return posix2.normalize(joined); + }, + relative: function relative(from, to) { + assertPath(from); + assertPath(to); + if (from === to) return ""; + from = posix2.resolve(from); + to = posix2.resolve(to); + if (from === to) return ""; + var fromStart = 1; + for (; fromStart < from.length; ++fromStart) { + if (from.charCodeAt(fromStart) !== 47) + break; } - const destination = this._resolveDescriptorPath(newFd, newPathPtr, newPathLen); - if (destination.error !== __agentOSWasiErrnoSuccess) { - return destination.error; + var fromEnd = from.length; + var fromLen = fromEnd - fromStart; + var toStart = 1; + for (; toStart < to.length; ++toStart) { + if (to.charCodeAt(toStart) !== 47) + break; } - if (source.readOnly || destination.readOnly) { - return __agentOSWasiErrnoRofs; + var toEnd = to.length; + var toLen = toEnd - toStart; + var length = fromLen < toLen ? fromLen : toLen; + var lastCommonSep = -1; + var i = 0; + for (; i <= length; ++i) { + if (i === length) { + if (toLen > length) { + if (to.charCodeAt(toStart + i) === 47) { + return to.slice(toStart + i + 1); + } else if (i === 0) { + return to.slice(toStart + i); + } + } else if (fromLen > length) { + if (from.charCodeAt(fromStart + i) === 47) { + lastCommonSep = i; + } else if (i === 0) { + lastCommonSep = 0; + } + } + break; + } + var fromCode = from.charCodeAt(fromStart + i); + var toCode = to.charCodeAt(toStart + i); + if (fromCode !== toCode) + break; + else if (fromCode === 47) + lastCommonSep = i; } - this._clearStatCache(); - __agentOSFs().renameSync(this._resolvedFsPath(source), this._resolvedFsPath(destination)); - return __agentOSWasiErrnoSuccess; - } catch (error) { - return this._mapFsError(error); - } - } - - _pathUnlinkFile(fd, pathPtr, pathLen) { - try { - const resolved = this._resolveDescriptorPath(fd, pathPtr, pathLen); - if (resolved.error !== __agentOSWasiErrnoSuccess) { - return resolved.error; + var out = ""; + for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { + if (i === fromEnd || from.charCodeAt(i) === 47) { + if (out.length === 0) + out += ".."; + else + out += "/.."; + } } - if (resolved.readOnly) { - return __agentOSWasiErrnoRofs; + if (out.length > 0) + return out + to.slice(toStart + lastCommonSep); + else { + toStart += lastCommonSep; + if (to.charCodeAt(toStart) === 47) + ++toStart; + return to.slice(toStart); } - this._clearStatCache(); - __agentOSFs().unlinkSync(this._resolvedFsPath(resolved)); - return __agentOSWasiErrnoSuccess; - } catch (error) { - return this._mapFsError(error); - } - } - - _pathFilestatGet(fd, flags, pathPtr, pathLen, statPtr) { - try { - const target = this._measureWasiPhase("readString", () => this._readString(pathPtr, pathLen)); - const resolved = - this._measureWasiPhase("resolveDirectStatPath", () => this._resolveDescriptorDirectStatPath(fd, target)) ?? - this._measureWasiPhase("resolveDescriptorPath", () => this._resolveDescriptorPath(fd, pathPtr, pathLen)); - if (resolved.error !== __agentOSWasiErrnoSuccess) { - return resolved.error; + }, + _makeLong: function _makeLong(path2) { + return path2; + }, + dirname: function dirname(path2) { + assertPath(path2); + if (path2.length === 0) return "."; + var code = path2.charCodeAt(0); + var hasRoot = code === 47; + var end = -1; + var matchedSlash = true; + for (var i = path2.length - 1; i >= 1; --i) { + code = path2.charCodeAt(i); + if (code === 47) { + if (!matchedSlash) { + end = i; + break; + } + } else { + matchedSlash = false; + } } - const follow = (Number(flags) & __agentOSWasiLookupSymlinkFollow) !== 0; - const cacheKey = this._statCacheKey(resolved, follow); - let stats = cacheKey ? this.statCache.get(cacheKey) : undefined; - if (stats) { - this._measureWasiPhase("statCacheHit", () => undefined); + if (end === -1) return hasRoot ? "/" : "."; + if (hasRoot && end === 1) return "//"; + return path2.slice(0, end); + }, + basename: function basename(path2, ext) { + if (ext !== void 0 && typeof ext !== "string") throw new TypeError('"ext" argument must be a string'); + assertPath(path2); + var start = 0; + var end = -1; + var matchedSlash = true; + var i; + if (ext !== void 0 && ext.length > 0 && ext.length <= path2.length) { + if (ext.length === path2.length && ext === path2) return ""; + var extIdx = ext.length - 1; + var firstNonSlashEnd = -1; + for (i = path2.length - 1; i >= 0; --i) { + var code = path2.charCodeAt(i); + if (code === 47) { + if (!matchedSlash) { + start = i + 1; + break; + } + } else { + if (firstNonSlashEnd === -1) { + matchedSlash = false; + firstNonSlashEnd = i + 1; + } + if (extIdx >= 0) { + if (code === ext.charCodeAt(extIdx)) { + if (--extIdx === -1) { + end = i; + } + } else { + extIdx = -1; + end = firstNonSlashEnd; + } + } + } + } + if (start === end) end = firstNonSlashEnd; + else if (end === -1) end = path2.length; + return path2.slice(start, end); } else { - stats = this._measureWasiPhase(follow ? "statSync" : "lstatSync", () => - this._statResolvedPath(resolved, follow) - ); - if (cacheKey) { - this.statCache.set(cacheKey, stats); + for (i = path2.length - 1; i >= 0; --i) { + if (path2.charCodeAt(i) === 47) { + if (!matchedSlash) { + start = i + 1; + break; + } + } else if (end === -1) { + matchedSlash = false; + end = i + 1; + } } + if (end === -1) return ""; + return path2.slice(start, end); } - return this._measureWasiPhase("writeFilestat", () => this._writeFilestat(statPtr, stats, this._filetypeForStats(stats))); - } catch (error) { - return this._mapFsError(error); - } - } - - _pathReadlink(fd, pathPtr, pathLen, bufPtr, bufLen, bufUsedPtr) { - try { - const resolved = this._resolveDescriptorPath(fd, pathPtr, pathLen); - if (resolved.error !== __agentOSWasiErrnoSuccess) { - return resolved.error; + }, + extname: function extname(path2) { + assertPath(path2); + var startDot = -1; + var startPart = 0; + var end = -1; + var matchedSlash = true; + var preDotState = 0; + for (var i = path2.length - 1; i >= 0; --i) { + var code = path2.charCodeAt(i); + if (code === 47) { + if (!matchedSlash) { + startPart = i + 1; + break; + } + continue; + } + if (end === -1) { + matchedSlash = false; + end = i + 1; + } + if (code === 46) { + if (startDot === -1) + startDot = i; + else if (preDotState !== 1) + preDotState = 1; + } else if (startDot !== -1) { + preDotState = -1; + } } - const bytes = Buffer.from(__agentOSFs().readlinkSync(resolved.guestPath), "utf8"); - const length = Math.min(bytes.length, Number(bufLen) >>> 0); - const writeStatus = this._writeBytes(bufPtr, bytes.subarray(0, length)); - if (writeStatus !== __agentOSWasiErrnoSuccess) { - return writeStatus; + if (startDot === -1 || end === -1 || // We saw a non-dot character immediately before the dot + preDotState === 0 || // The (right-most) trimmed path component is exactly '..' + preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) { + return ""; } - return this._writeUint32(bufUsedPtr, length); - } catch (error) { - return this._mapFsError(error); - } - } - - _pollOneoff(inPtr, outPtr, nsubscriptions, neventsPtr) { - try { - const subscriptionCount = Number(nsubscriptions) >>> 0; - if (subscriptionCount === 0) { - return this._writeUint32(neventsPtr, 0); + return path2.slice(startDot, end); + }, + format: function format(pathObject) { + if (pathObject === null || typeof pathObject !== "object") { + throw new TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject); } - - const subscriptionSize = 48; - const eventSize = 32; - const kernelPollIn = 0x0001; - const kernelPollOut = 0x0004; - const kernelPollErr = 0x0008; - const kernelPollHup = 0x0010; - const view = this._memoryView(); - const memory = this._memoryBytes(); - const syncRpc = - typeof globalThis?.__agentOSSyncRpc?.callSync === "function" - ? __agentOSWasiSyncRpc() - : null; - const subscriptions = []; - let timeoutMs = null; - - for (let index = 0; index < subscriptionCount; index += 1) { - const base = (Number(inPtr) >>> 0) + index * subscriptionSize; - const tag = view.getUint8(base + 8); - const userdata = memory.slice(base, base + 8); - if (tag === 0) { - const timeoutNs = view.getBigUint64(base + 24, true); - const relativeTimeoutMs = Number(timeoutNs / 1000000n); - timeoutMs = - timeoutMs == null ? relativeTimeoutMs : Math.min(timeoutMs, relativeTimeoutMs); - subscriptions.push({ kind: "clock", userdata }); + return _format("/", pathObject); + }, + parse: function parse(path2) { + assertPath(path2); + var ret = { root: "", dir: "", base: "", ext: "", name: "" }; + if (path2.length === 0) return ret; + var code = path2.charCodeAt(0); + var isAbsolute = code === 47; + var start; + if (isAbsolute) { + ret.root = "/"; + start = 1; + } else { + start = 0; + } + var startDot = -1; + var startPart = 0; + var end = -1; + var matchedSlash = true; + var i = path2.length - 1; + var preDotState = 0; + for (; i >= start; --i) { + code = path2.charCodeAt(i); + if (code === 47) { + if (!matchedSlash) { + startPart = i + 1; + break; + } continue; } - - if (tag !== 1 && tag !== 2) { - subscriptions.push({ kind: "unsupported", userdata }); - continue; + if (end === -1) { + matchedSlash = false; + end = i + 1; } - - const fd = view.getUint32(base + 16, true); - const descriptor = Number(fd) >>> 0; - const handle = this._externalFdHandle(descriptor); - const entry = this._descriptorEntry(descriptor); - let targetFd = null; - if ( - (handle?.kind === "passthrough" || handle?.kind === "host-passthrough") && - typeof handle.targetFd === "number" - ) { - targetFd = Number(handle.targetFd) >>> 0; - } else if ( - entry?.kind === "stdin" || - entry?.kind === "stdout" || - entry?.kind === "stderr" - ) { - targetFd = descriptor; + if (code === 46) { + if (startDot === -1) startDot = i; + else if (preDotState !== 1) preDotState = 1; + } else if (startDot !== -1) { + preDotState = -1; + } + } + if (startDot === -1 || end === -1 || // We saw a non-dot character immediately before the dot + preDotState === 0 || // The (right-most) trimmed path component is exactly '..' + preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) { + if (end !== -1) { + if (startPart === 0 && isAbsolute) ret.base = ret.name = path2.slice(1, end); + else ret.base = ret.name = path2.slice(startPart, end); + } + } else { + if (startPart === 0 && isAbsolute) { + ret.name = path2.slice(1, startDot); + ret.base = path2.slice(1, end); + } else { + ret.name = path2.slice(startPart, startDot); + ret.base = path2.slice(startPart, end); } + ret.ext = path2.slice(startDot, end); + } + if (startPart > 0) ret.dir = path2.slice(0, startPart - 1); + else if (isAbsolute) ret.dir = "/"; + return ret; + }, + sep: "/", + delimiter: ":", + win32: null, + posix: null + }; + posix2.posix = posix2; + module2.exports = posix2; + } +}); - subscriptions.push({ - kind: tag === 1 ? "fd_read" : "fd_write", - fd: descriptor, - handle, - targetFd, - streamKind: entry?.kind, - userdata, - }); +// +var path = require_path_browserify(); +var resolved = path.default ?? path; +var posix = resolved.posix ?? resolved; +posix.posix = posix; +module.exports = posix; + +if (module.exports && module.exports.default == null) module.exports.default = module.exports; +`;var Yu=`var process = globalThis.process || { + env: {}, + nextTick: (fn, ...args) => queueMicrotask(() => fn(...args)), +}; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; + +// node_modules/.pnpm/has-symbols@1.1.0/node_modules/has-symbols/shams.js +var require_shams = __commonJS({ + "node_modules/.pnpm/has-symbols@1.1.0/node_modules/has-symbols/shams.js"(exports2, module2) { + "use strict"; + module2.exports = function hasSymbols() { + if (typeof Symbol !== "function" || typeof Object.getOwnPropertySymbols !== "function") { + return false; + } + if (typeof Symbol.iterator === "symbol") { + return true; + } + var obj = {}; + var sym = /* @__PURE__ */ Symbol("test"); + var symObj = Object(sym); + if (typeof sym === "string") { + return false; + } + if (Object.prototype.toString.call(sym) !== "[object Symbol]") { + return false; + } + if (Object.prototype.toString.call(symObj) !== "[object Symbol]") { + return false; + } + var symVal = 42; + obj[sym] = symVal; + for (var _ in obj) { + return false; + } + if (typeof Object.keys === "function" && Object.keys(obj).length !== 0) { + return false; + } + if (typeof Object.getOwnPropertyNames === "function" && Object.getOwnPropertyNames(obj).length !== 0) { + return false; + } + var syms = Object.getOwnPropertySymbols(obj); + if (syms.length !== 1 || syms[0] !== sym) { + return false; + } + if (!Object.prototype.propertyIsEnumerable.call(obj, sym)) { + return false; + } + if (typeof Object.getOwnPropertyDescriptor === "function") { + var descriptor = ( + /** @type {PropertyDescriptor} */ + Object.getOwnPropertyDescriptor(obj, sym) + ); + if (descriptor.value !== symVal || descriptor.enumerable !== true) { + return false; } + } + return true; + }; + } +}); - const deadline = timeoutMs == null ? null : Date.now() + Math.max(0, timeoutMs); - const readyEvents = []; - - while (readyEvents.length === 0) { - for (const subscription of subscriptions) { - // A clock subscription is ready once its deadline has elapsed; report - // it as a first-class event so it is returned alongside any ready fds - // (not only as a fallback when nothing else is ready). - if (subscription.kind === "clock") { - if (deadline != null && Date.now() >= deadline) { - readyEvents.push({ - userdata: subscription.userdata, - error: __agentOSWasiErrnoSuccess, - type: 0, - nbytes: 0, - flags: 0, - }); - } - continue; - } - if (subscription.kind === "fd_read" && subscription.handle?.kind === "pipe-read") { - const pipe = subscription.handle.pipe; - if ( - pipe && - (pipe.chunks.length > 0 || - (pipe.writeHandleCount === 0 && pipe.producers.size === 0)) - ) { - readyEvents.push({ - userdata: subscription.userdata, - error: __agentOSWasiErrnoSuccess, - type: 1, - nbytes: pipe.chunks[0]?.length ?? 0, - flags: 0, - }); - } - continue; - } - - // Without a kernel poll bridge, resolve stdin fd_read readiness from - // the host-seam queued byte count (the browser delivers stdin through - // the runtime process object). Reporting nbytes does not consume input. - if ( - !syncRpc && - subscription.kind === "fd_read" && - subscription.streamKind === "stdin" && - typeof __agentOSWasiHost.stdinReadableBytes === "function" - ) { - const available = Number(__agentOSWasiHost.stdinReadableBytes()) >>> 0; - if (available > 0) { - readyEvents.push({ - userdata: subscription.userdata, - error: __agentOSWasiErrnoSuccess, - type: 1, - nbytes: available, - flags: 0, - }); - } - continue; - } +// node_modules/.pnpm/has-tostringtag@1.0.2/node_modules/has-tostringtag/shams.js +var require_shams2 = __commonJS({ + "node_modules/.pnpm/has-tostringtag@1.0.2/node_modules/has-tostringtag/shams.js"(exports2, module2) { + "use strict"; + var hasSymbols = require_shams(); + module2.exports = function hasToStringTagShams() { + return hasSymbols() && !!Symbol.toStringTag; + }; + } +}); - if (subscription.kind === "fd_write" && subscription.handle?.kind === "pipe-write") { - readyEvents.push({ - userdata: subscription.userdata, - error: __agentOSWasiErrnoSuccess, - type: 2, - nbytes: 65536, - flags: 0, - }); - continue; - } +// node_modules/.pnpm/es-object-atoms@1.1.1/node_modules/es-object-atoms/index.js +var require_es_object_atoms = __commonJS({ + "node_modules/.pnpm/es-object-atoms@1.1.1/node_modules/es-object-atoms/index.js"(exports2, module2) { + "use strict"; + module2.exports = Object; + } +}); - // Without a kernel poll bridge (a non-native backend) stdout/stderr - // are always writable, so resolve their fd_write readiness directly - // instead of leaving it to the (absent) __kernel_poll round-trip. - if ( - !syncRpc && - subscription.kind === "fd_write" && - (subscription.streamKind === "stdout" || - subscription.streamKind === "stderr") - ) { - readyEvents.push({ - userdata: subscription.userdata, - error: __agentOSWasiErrnoSuccess, - type: 2, - nbytes: 65536, - flags: 0, - }); - } - } +// node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/index.js +var require_es_errors = __commonJS({ + "node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/index.js"(exports2, module2) { + "use strict"; + module2.exports = Error; + } +}); - if (readyEvents.length > 0) { - break; - } +// node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/eval.js +var require_eval = __commonJS({ + "node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/eval.js"(exports2, module2) { + "use strict"; + module2.exports = EvalError; + } +}); - // Without a kernel poll bridge, fd readiness is resolved synchronously - // above (stdio fast paths) or via pipes; if there is no clock to wait on - // and no pipe to pump, no further progress is possible, so stop instead - // of busy-waiting until the caller times out. - if ( - !syncRpc && - !subscriptions.some((subscription) => subscription.kind === "clock") && - !subscriptions.some( - (subscription) => - subscription.handle?.kind === "pipe-read" || - subscription.handle?.kind === "pipe-write", - ) - ) { - break; - } +// node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/range.js +var require_range = __commonJS({ + "node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/range.js"(exports2, module2) { + "use strict"; + module2.exports = RangeError; + } +}); - const pollTargets = subscriptions - .filter( - (subscription) => - (subscription.kind === "fd_read" || subscription.kind === "fd_write") && - typeof subscription.targetFd === "number", - ) - .map((subscription) => ({ - fd: subscription.targetFd, - events: subscription.kind === "fd_read" ? kernelPollIn : kernelPollOut, - })); - const waitMs = - deadline == null ? 10 : Math.max(0, Math.min(10, deadline - Date.now())); +// node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/ref.js +var require_ref = __commonJS({ + "node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/ref.js"(exports2, module2) { + "use strict"; + module2.exports = ReferenceError; + } +}); - if (syncRpc && pollTargets.length > 0) { - let response = null; - try { - response = syncRpc.callSync("__kernel_poll", [pollTargets, waitMs]); - } catch (error) { - __agentOSWasiDebug( - \`poll_oneoff __kernel_poll failed: \${ - error instanceof Error ? error.message : String(error) - }\`, - ); - } +// node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/syntax.js +var require_syntax = __commonJS({ + "node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/syntax.js"(exports2, module2) { + "use strict"; + module2.exports = SyntaxError; + } +}); - const responseEntries = Array.isArray(response?.fds) ? response.fds : []; - for (const subscription of subscriptions) { - if ( - (subscription.kind !== "fd_read" && subscription.kind !== "fd_write") || - typeof subscription.targetFd !== "number" - ) { - continue; - } +// node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/type.js +var require_type = __commonJS({ + "node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/type.js"(exports2, module2) { + "use strict"; + module2.exports = TypeError; + } +}); - const responseEntry = responseEntries.find( - (entry) => (Number(entry?.fd) >>> 0) === subscription.targetFd, - ); - const revents = Number(responseEntry?.revents) >>> 0; - const interested = - subscription.kind === "fd_read" - ? kernelPollIn | kernelPollErr | kernelPollHup - : kernelPollOut | kernelPollErr | kernelPollHup; - if ((revents & interested) === 0) { - continue; - } +// node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/uri.js +var require_uri = __commonJS({ + "node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/uri.js"(exports2, module2) { + "use strict"; + module2.exports = URIError; + } +}); - readyEvents.push({ - userdata: subscription.userdata, - error: __agentOSWasiErrnoSuccess, - type: subscription.kind === "fd_read" ? 1 : 2, - nbytes: subscription.kind === "fd_read" ? 1 : 65536, - flags: 0, - }); - } - } +// node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/abs.js +var require_abs = __commonJS({ + "node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/abs.js"(exports2, module2) { + "use strict"; + module2.exports = Math.abs; + } +}); - if (readyEvents.length > 0) { - break; - } +// node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/floor.js +var require_floor = __commonJS({ + "node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/floor.js"(exports2, module2) { + "use strict"; + module2.exports = Math.floor; + } +}); - let pumped = false; - for (const subscription of subscriptions) { - if (subscription.kind === "fd_read" && subscription.handle?.kind === "pipe-read") { - pumped = this._pumpPipeProducers(subscription.handle.pipe, 10) || pumped; - } - } +// node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/max.js +var require_max = __commonJS({ + "node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/max.js"(exports2, module2) { + "use strict"; + module2.exports = Math.max; + } +}); - if (pumped) { - continue; - } +// node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/min.js +var require_min = __commonJS({ + "node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/min.js"(exports2, module2) { + "use strict"; + module2.exports = Math.min; + } +}); - if (deadline != null && Date.now() >= deadline) { - break; - } +// node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/pow.js +var require_pow = __commonJS({ + "node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/pow.js"(exports2, module2) { + "use strict"; + module2.exports = Math.pow; + } +}); - if ( - pollTargets.length === 0 && - typeof Atomics?.wait !== "function" && - deadline == null - ) { - break; - } +// node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/round.js +var require_round = __commonJS({ + "node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/round.js"(exports2, module2) { + "use strict"; + module2.exports = Math.round; + } +}); - if ( - typeof Atomics?.wait === "function" && - typeof syntheticWaitArray !== "undefined" - ) { - Atomics.wait(syntheticWaitArray, 0, 0, waitMs); - } else if (!syncRpc && pollTargets.length === 0) { - break; - } - } +// node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/isNaN.js +var require_isNaN = __commonJS({ + "node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/isNaN.js"(exports2, module2) { + "use strict"; + module2.exports = Number.isNaN || function isNaN2(a) { + return a !== a; + }; + } +}); - if ( - readyEvents.length === 0 && - subscriptions.some((subscription) => subscription.kind === "clock") - ) { - const clockSubscription = subscriptions.find( - (subscription) => subscription.kind === "clock", - ); - readyEvents.push({ - userdata: clockSubscription.userdata, - error: __agentOSWasiErrnoSuccess, - type: 0, - nbytes: 0, - flags: 0, - }); - } +// node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/sign.js +var require_sign = __commonJS({ + "node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/sign.js"(exports2, module2) { + "use strict"; + var $isNaN = require_isNaN(); + module2.exports = function sign(number) { + if ($isNaN(number) || number === 0) { + return number; + } + return number < 0 ? -1 : 1; + }; + } +}); - for (let index = 0; index < readyEvents.length; index += 1) { - const base = (Number(outPtr) >>> 0) + index * eventSize; - const event = readyEvents[index]; - memory.set(event.userdata, base); - view.setUint16(base + 8, event.error, true); - view.setUint8(base + 10, event.type); - view.setBigUint64(base + 16, BigInt(event.nbytes), true); - view.setUint16(base + 24, event.flags, true); - } +// node_modules/.pnpm/gopd@1.2.0/node_modules/gopd/gOPD.js +var require_gOPD = __commonJS({ + "node_modules/.pnpm/gopd@1.2.0/node_modules/gopd/gOPD.js"(exports2, module2) { + "use strict"; + module2.exports = Object.getOwnPropertyDescriptor; + } +}); - return this._writeUint32(neventsPtr, readyEvents.length); - } catch (error) { - __agentOSWasiDebug( - \`poll_oneoff failed: \${error instanceof Error ? error.message : String(error)}\`, - ); - return __agentOSWasiErrnoFault; +// node_modules/.pnpm/gopd@1.2.0/node_modules/gopd/index.js +var require_gopd = __commonJS({ + "node_modules/.pnpm/gopd@1.2.0/node_modules/gopd/index.js"(exports2, module2) { + "use strict"; + var $gOPD = require_gOPD(); + if ($gOPD) { + try { + $gOPD([], "length"); + } catch (e) { + $gOPD = null; } } + module2.exports = $gOPD; + } +}); - _randomGet(bufPtr, bufLen) { +// node_modules/.pnpm/es-define-property@1.0.1/node_modules/es-define-property/index.js +var require_es_define_property = __commonJS({ + "node_modules/.pnpm/es-define-property@1.0.1/node_modules/es-define-property/index.js"(exports2, module2) { + "use strict"; + var $defineProperty = Object.defineProperty || false; + if ($defineProperty) { try { - const length = Number(bufLen) >>> 0; - const bytes = Buffer.allocUnsafe(length); - __agentOSCrypto().randomFillSync(bytes); - return this._writeBytes(bufPtr, bytes); - } catch { - return __agentOSWasiErrnoFault; + $defineProperty({}, "a", { value: 1 }); + } catch (e) { + $defineProperty = false; } } + module2.exports = $defineProperty; + } +}); - _schedYield() { - return __agentOSWasiErrnoSuccess; - } - - _procExit(code) { - if (this.returnOnExit) { - const error = new Error(\`wasi exit(\${Number(code) >>> 0})\`); - error.__agentOSWasiExit = true; - error.code = Number(code) >>> 0; - throw error; +// node_modules/.pnpm/has-symbols@1.1.0/node_modules/has-symbols/index.js +var require_has_symbols = __commonJS({ + "node_modules/.pnpm/has-symbols@1.1.0/node_modules/has-symbols/index.js"(exports2, module2) { + "use strict"; + var origSymbol = typeof Symbol !== "undefined" && Symbol; + var hasSymbolSham = require_shams(); + module2.exports = function hasNativeSymbols() { + if (typeof origSymbol !== "function") { + return false; } - process.exit(Number(code) >>> 0); - } + if (typeof Symbol !== "function") { + return false; + } + if (typeof origSymbol("foo") !== "symbol") { + return false; + } + if (typeof /* @__PURE__ */ Symbol("bar") !== "symbol") { + return false; + } + return hasSymbolSham(); + }; } +}); - Object.defineProperty(globalThis, "__agentOSWasiModule", { - configurable: true, - enumerable: false, - value: { WASI }, - writable: true, - }); -} +// node_modules/.pnpm/get-proto@1.0.1/node_modules/get-proto/Reflect.getPrototypeOf.js +var require_Reflect_getPrototypeOf = __commonJS({ + "node_modules/.pnpm/get-proto@1.0.1/node_modules/get-proto/Reflect.getPrototypeOf.js"(exports2, module2) { + "use strict"; + module2.exports = typeof Reflect !== "undefined" && Reflect.getPrototypeOf || null; + } +}); - // Re-export the shared runner WASI class as the browser wasi module. - module.exports = { WASI: globalThis.__agentOSWasiModule.WASI }; - module.exports.default = { WASI: globalThis.__agentOSWasiModule.WASI }; -`;var En={SIGHUP:1,SIGINT:2,SIGQUIT:3,SIGILL:4,SIGTRAP:5,SIGABRT:6,SIGIOT:6,SIGBUS:7,SIGFPE:8,SIGKILL:9,SIGUSR1:10,SIGSEGV:11,SIGUSR2:12,SIGPIPE:13,SIGALRM:14,SIGTERM:15,SIGSTKFLT:16,SIGCHLD:17,SIGCONT:18,SIGSTOP:19,SIGTSTP:20,SIGTTIN:21,SIGTTOU:22,SIGURG:23,SIGXCPU:24,SIGXFSZ:25,SIGVTALRM:26,SIGPROF:27,SIGWINCH:28,SIGIO:29,SIGPOLL:29,SIGPWR:30,SIGSYS:31},xx=new Set([0,...Object.values(En)]);function Yu(e){let t=e.trim().toUpperCase(),r=t.startsWith("SIG")?t:`SIG${t}`;return En[r]??null}function Xu(e){return e>0?128+e:null}var Qu=`var process = globalThis.process || { - env: {}, - nextTick: (fn, ...args) => queueMicrotask(() => fn(...args)), -}; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __commonJS = (cb, mod) => function __require() { - return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; -}; +// node_modules/.pnpm/get-proto@1.0.1/node_modules/get-proto/Object.getPrototypeOf.js +var require_Object_getPrototypeOf = __commonJS({ + "node_modules/.pnpm/get-proto@1.0.1/node_modules/get-proto/Object.getPrototypeOf.js"(exports2, module2) { + "use strict"; + var $Object = require_es_object_atoms(); + module2.exports = $Object.getPrototypeOf || null; + } +}); -// node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js -var require_base64_js = __commonJS({ - "node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js"(exports2) { +// node_modules/.pnpm/function-bind@1.1.2/node_modules/function-bind/implementation.js +var require_implementation = __commonJS({ + "node_modules/.pnpm/function-bind@1.1.2/node_modules/function-bind/implementation.js"(exports2, module2) { "use strict"; - exports2.byteLength = byteLength; - exports2.toByteArray = toByteArray; - exports2.fromByteArray = fromByteArray; - var lookup = []; - var revLookup = []; - var Arr = typeof Uint8Array !== "undefined" ? Uint8Array : Array; - var code = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - for (i = 0, len = code.length; i < len; ++i) { - lookup[i] = code[i]; - revLookup[code.charCodeAt(i)] = i; - } - var i; - var len; - revLookup["-".charCodeAt(0)] = 62; - revLookup["_".charCodeAt(0)] = 63; - function getLens(b64) { - var len2 = b64.length; - if (len2 % 4 > 0) { - throw new Error("Invalid string. Length must be a multiple of 4"); - } - var validLen = b64.indexOf("="); - if (validLen === -1) validLen = len2; - var placeHoldersLen = validLen === len2 ? 0 : 4 - validLen % 4; - return [validLen, placeHoldersLen]; - } - function byteLength(b64) { - var lens = getLens(b64); - var validLen = lens[0]; - var placeHoldersLen = lens[1]; - return (validLen + placeHoldersLen) * 3 / 4 - placeHoldersLen; - } - function _byteLength(b64, validLen, placeHoldersLen) { - return (validLen + placeHoldersLen) * 3 / 4 - placeHoldersLen; - } - function toByteArray(b64) { - var tmp; - var lens = getLens(b64); - var validLen = lens[0]; - var placeHoldersLen = lens[1]; - var arr = new Arr(_byteLength(b64, validLen, placeHoldersLen)); - var curByte = 0; - var len2 = placeHoldersLen > 0 ? validLen - 4 : validLen; - var i2; - for (i2 = 0; i2 < len2; i2 += 4) { - tmp = revLookup[b64.charCodeAt(i2)] << 18 | revLookup[b64.charCodeAt(i2 + 1)] << 12 | revLookup[b64.charCodeAt(i2 + 2)] << 6 | revLookup[b64.charCodeAt(i2 + 3)]; - arr[curByte++] = tmp >> 16 & 255; - arr[curByte++] = tmp >> 8 & 255; - arr[curByte++] = tmp & 255; + var ERROR_MESSAGE = "Function.prototype.bind called on incompatible "; + var toStr = Object.prototype.toString; + var max = Math.max; + var funcType = "[object Function]"; + var concatty = function concatty2(a, b) { + var arr = []; + for (var i = 0; i < a.length; i += 1) { + arr[i] = a[i]; } - if (placeHoldersLen === 2) { - tmp = revLookup[b64.charCodeAt(i2)] << 2 | revLookup[b64.charCodeAt(i2 + 1)] >> 4; - arr[curByte++] = tmp & 255; + for (var j = 0; j < b.length; j += 1) { + arr[j + a.length] = b[j]; } - if (placeHoldersLen === 1) { - tmp = revLookup[b64.charCodeAt(i2)] << 10 | revLookup[b64.charCodeAt(i2 + 1)] << 4 | revLookup[b64.charCodeAt(i2 + 2)] >> 2; - arr[curByte++] = tmp >> 8 & 255; - arr[curByte++] = tmp & 255; + return arr; + }; + var slicy = function slicy2(arrLike, offset) { + var arr = []; + for (var i = offset || 0, j = 0; i < arrLike.length; i += 1, j += 1) { + arr[j] = arrLike[i]; } return arr; - } - function tripletToBase64(num) { - return lookup[num >> 18 & 63] + lookup[num >> 12 & 63] + lookup[num >> 6 & 63] + lookup[num & 63]; - } - function encodeChunk(uint8, start, end) { - var tmp; - var output = []; - for (var i2 = start; i2 < end; i2 += 3) { - tmp = (uint8[i2] << 16 & 16711680) + (uint8[i2 + 1] << 8 & 65280) + (uint8[i2 + 2] & 255); - output.push(tripletToBase64(tmp)); + }; + var joiny = function(arr, joiner) { + var str = ""; + for (var i = 0; i < arr.length; i += 1) { + str += arr[i]; + if (i + 1 < arr.length) { + str += joiner; + } } - return output.join(""); - } - function fromByteArray(uint8) { - var tmp; - var len2 = uint8.length; - var extraBytes = len2 % 3; - var parts = []; - var maxChunkLength = 16383; - for (var i2 = 0, len22 = len2 - extraBytes; i2 < len22; i2 += maxChunkLength) { - parts.push(encodeChunk(uint8, i2, i2 + maxChunkLength > len22 ? len22 : i2 + maxChunkLength)); + return str; + }; + module2.exports = function bind(that) { + var target = this; + if (typeof target !== "function" || toStr.apply(target) !== funcType) { + throw new TypeError(ERROR_MESSAGE + target); } - if (extraBytes === 1) { - tmp = uint8[len2 - 1]; - parts.push( - lookup[tmp >> 2] + lookup[tmp << 4 & 63] + "==" - ); - } else if (extraBytes === 2) { - tmp = (uint8[len2 - 2] << 8) + uint8[len2 - 1]; - parts.push( - lookup[tmp >> 10] + lookup[tmp >> 4 & 63] + lookup[tmp << 2 & 63] + "=" + var args = slicy(arguments, 1); + var bound; + var binder = function() { + if (this instanceof bound) { + var result = target.apply( + this, + concatty(args, arguments) + ); + if (Object(result) === result) { + return result; + } + return this; + } + return target.apply( + that, + concatty(args, arguments) ); + }; + var boundLength = max(0, target.length - args.length); + var boundArgs = []; + for (var i = 0; i < boundLength; i++) { + boundArgs[i] = "$" + i; } - return parts.join(""); - } + bound = Function("binder", "return function (" + joiny(boundArgs, ",") + "){ return binder.apply(this,arguments); }")(binder); + if (target.prototype) { + var Empty = function Empty2() { + }; + Empty.prototype = target.prototype; + bound.prototype = new Empty(); + Empty.prototype = null; + } + return bound; + }; } }); -// node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js -var require_ieee754 = __commonJS({ - "node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js"(exports2) { - exports2.read = function(buffer2, offset, isLE, mLen, nBytes) { - var e, m; - var eLen = nBytes * 8 - mLen - 1; - var eMax = (1 << eLen) - 1; - var eBias = eMax >> 1; - var nBits = -7; - var i = isLE ? nBytes - 1 : 0; - var d = isLE ? -1 : 1; - var s = buffer2[offset + i]; - i += d; - e = s & (1 << -nBits) - 1; - s >>= -nBits; - nBits += eLen; - for (; nBits > 0; e = e * 256 + buffer2[offset + i], i += d, nBits -= 8) { - } - m = e & (1 << -nBits) - 1; - e >>= -nBits; - nBits += mLen; - for (; nBits > 0; m = m * 256 + buffer2[offset + i], i += d, nBits -= 8) { - } - if (e === 0) { - e = 1 - eBias; - } else if (e === eMax) { - return m ? NaN : (s ? -1 : 1) * Infinity; - } else { - m = m + Math.pow(2, mLen); - e = e - eBias; +// node_modules/.pnpm/function-bind@1.1.2/node_modules/function-bind/index.js +var require_function_bind = __commonJS({ + "node_modules/.pnpm/function-bind@1.1.2/node_modules/function-bind/index.js"(exports2, module2) { + "use strict"; + var implementation = require_implementation(); + module2.exports = Function.prototype.bind || implementation; + } +}); + +// node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/functionCall.js +var require_functionCall = __commonJS({ + "node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/functionCall.js"(exports2, module2) { + "use strict"; + module2.exports = Function.prototype.call; + } +}); + +// node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/functionApply.js +var require_functionApply = __commonJS({ + "node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/functionApply.js"(exports2, module2) { + "use strict"; + module2.exports = Function.prototype.apply; + } +}); + +// node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/reflectApply.js +var require_reflectApply = __commonJS({ + "node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/reflectApply.js"(exports2, module2) { + "use strict"; + module2.exports = typeof Reflect !== "undefined" && Reflect && Reflect.apply; + } +}); + +// node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/actualApply.js +var require_actualApply = __commonJS({ + "node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/actualApply.js"(exports2, module2) { + "use strict"; + var bind = require_function_bind(); + var $apply = require_functionApply(); + var $call = require_functionCall(); + var $reflectApply = require_reflectApply(); + module2.exports = $reflectApply || bind.call($call, $apply); + } +}); + +// node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/index.js +var require_call_bind_apply_helpers = __commonJS({ + "node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/index.js"(exports2, module2) { + "use strict"; + var bind = require_function_bind(); + var $TypeError = require_type(); + var $call = require_functionCall(); + var $actualApply = require_actualApply(); + module2.exports = function callBindBasic(args) { + if (args.length < 1 || typeof args[0] !== "function") { + throw new $TypeError("a function is required"); } - return (s ? -1 : 1) * m * Math.pow(2, e - mLen); + return $actualApply(bind, $call, args); }; - exports2.write = function(buffer2, value, offset, isLE, mLen, nBytes) { - var e, m, c; - var eLen = nBytes * 8 - mLen - 1; - var eMax = (1 << eLen) - 1; - var eBias = eMax >> 1; - var rt = mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0; - var i = isLE ? 0 : nBytes - 1; - var d = isLE ? 1 : -1; - var s = value < 0 || value === 0 && 1 / value < 0 ? 1 : 0; - value = Math.abs(value); - if (isNaN(value) || value === Infinity) { - m = isNaN(value) ? 1 : 0; - e = eMax; - } else { - e = Math.floor(Math.log(value) / Math.LN2); - if (value * (c = Math.pow(2, -e)) < 1) { - e--; - c *= 2; - } - if (e + eBias >= 1) { - value += rt / c; - } else { - value += rt * Math.pow(2, 1 - eBias); - } - if (value * c >= 2) { - e++; - c /= 2; - } - if (e + eBias >= eMax) { - m = 0; - e = eMax; - } else if (e + eBias >= 1) { - m = (value * c - 1) * Math.pow(2, mLen); - e = e + eBias; - } else { - m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen); - e = 0; - } + } +}); + +// node_modules/.pnpm/dunder-proto@1.0.1/node_modules/dunder-proto/get.js +var require_get = __commonJS({ + "node_modules/.pnpm/dunder-proto@1.0.1/node_modules/dunder-proto/get.js"(exports2, module2) { + "use strict"; + var callBind = require_call_bind_apply_helpers(); + var gOPD = require_gopd(); + var hasProtoAccessor; + try { + hasProtoAccessor = /** @type {{ __proto__?: typeof Array.prototype }} */ + [].__proto__ === Array.prototype; + } catch (e) { + if (!e || typeof e !== "object" || !("code" in e) || e.code !== "ERR_PROTO_ACCESS") { + throw e; } - for (; mLen >= 8; buffer2[offset + i] = m & 255, i += d, m /= 256, mLen -= 8) { + } + var desc = !!hasProtoAccessor && gOPD && gOPD( + Object.prototype, + /** @type {keyof typeof Object.prototype} */ + "__proto__" + ); + var $Object = Object; + var $getPrototypeOf = $Object.getPrototypeOf; + module2.exports = desc && typeof desc.get === "function" ? callBind([desc.get]) : typeof $getPrototypeOf === "function" ? ( + /** @type {import('./get')} */ + function getDunder(value) { + return $getPrototypeOf(value == null ? value : $Object(value)); } - e = e << mLen | m; - eLen += mLen; - for (; eLen > 0; buffer2[offset + i] = e & 255, i += d, e /= 256, eLen -= 8) { + ) : false; + } +}); + +// node_modules/.pnpm/get-proto@1.0.1/node_modules/get-proto/index.js +var require_get_proto = __commonJS({ + "node_modules/.pnpm/get-proto@1.0.1/node_modules/get-proto/index.js"(exports2, module2) { + "use strict"; + var reflectGetProto = require_Reflect_getPrototypeOf(); + var originalGetProto = require_Object_getPrototypeOf(); + var getDunderProto = require_get(); + module2.exports = reflectGetProto ? function getProto(O) { + return reflectGetProto(O); + } : originalGetProto ? function getProto(O) { + if (!O || typeof O !== "object" && typeof O !== "function") { + throw new TypeError("getProto: not an object"); } - buffer2[offset + i - d] |= s * 128; - }; + return originalGetProto(O); + } : getDunderProto ? function getProto(O) { + return getDunderProto(O); + } : null; } }); -// node_modules/.pnpm/buffer@5.7.1/node_modules/buffer/index.js -var require_buffer = __commonJS({ - "node_modules/.pnpm/buffer@5.7.1/node_modules/buffer/index.js"(exports2) { +// node_modules/.pnpm/hasown@2.0.2/node_modules/hasown/index.js +var require_hasown = __commonJS({ + "node_modules/.pnpm/hasown@2.0.2/node_modules/hasown/index.js"(exports2, module2) { "use strict"; - var base64 = require_base64_js(); - var ieee754 = require_ieee754(); - var customInspectSymbol = typeof Symbol === "function" && typeof Symbol["for"] === "function" ? Symbol["for"]("nodejs.util.inspect.custom") : null; - exports2.Buffer = Buffer2; - exports2.SlowBuffer = SlowBuffer; - exports2.INSPECT_MAX_BYTES = 50; - var K_MAX_LENGTH = 2147483647; - exports2.kMaxLength = K_MAX_LENGTH; - Buffer2.TYPED_ARRAY_SUPPORT = typedArraySupport(); - if (!Buffer2.TYPED_ARRAY_SUPPORT && typeof console !== "undefined" && typeof console.error === "function") { - console.error( - "This browser lacks typed array (Uint8Array) support which is required by \`buffer\` v5.x. Use \`buffer\` v4.x if you require old browser support." - ); - } - function typedArraySupport() { + var call = Function.prototype.call; + var $hasOwn = Object.prototype.hasOwnProperty; + var bind = require_function_bind(); + module2.exports = bind.call(call, $hasOwn); + } +}); + +// node_modules/.pnpm/get-intrinsic@1.3.0/node_modules/get-intrinsic/index.js +var require_get_intrinsic = __commonJS({ + "node_modules/.pnpm/get-intrinsic@1.3.0/node_modules/get-intrinsic/index.js"(exports2, module2) { + "use strict"; + var undefined2; + var $Object = require_es_object_atoms(); + var $Error = require_es_errors(); + var $EvalError = require_eval(); + var $RangeError = require_range(); + var $ReferenceError = require_ref(); + var $SyntaxError = require_syntax(); + var $TypeError = require_type(); + var $URIError = require_uri(); + var abs = require_abs(); + var floor = require_floor(); + var max = require_max(); + var min = require_min(); + var pow = require_pow(); + var round = require_round(); + var sign = require_sign(); + var $Function = Function; + var getEvalledConstructor = function(expressionSyntax) { try { - var arr = new Uint8Array(1); - var proto = { foo: function() { - return 42; - } }; - Object.setPrototypeOf(proto, Uint8Array.prototype); - Object.setPrototypeOf(arr, proto); - return arr.foo() === 42; + return $Function('"use strict"; return (' + expressionSyntax + ").constructor;")(); } catch (e) { - return false; - } - } - Object.defineProperty(Buffer2.prototype, "parent", { - enumerable: true, - get: function() { - if (!Buffer2.isBuffer(this)) return void 0; - return this.buffer; - } - }); - Object.defineProperty(Buffer2.prototype, "offset", { - enumerable: true, - get: function() { - if (!Buffer2.isBuffer(this)) return void 0; - return this.byteOffset; - } - }); - function createBuffer(length) { - if (length > K_MAX_LENGTH) { - throw new RangeError('The value "' + length + '" is invalid for option "size"'); } - var buf = new Uint8Array(length); - Object.setPrototypeOf(buf, Buffer2.prototype); - return buf; - } - function Buffer2(arg, encodingOrOffset, length) { - if (typeof arg === "number") { - if (typeof encodingOrOffset === "string") { - throw new TypeError( - 'The "string" argument must be of type string. Received type number' - ); + }; + var $gOPD = require_gopd(); + var $defineProperty = require_es_define_property(); + var throwTypeError = function() { + throw new $TypeError(); + }; + var ThrowTypeError = $gOPD ? (function() { + try { + arguments.callee; + return throwTypeError; + } catch (calleeThrows) { + try { + return $gOPD(arguments, "callee").get; + } catch (gOPDthrows) { + return throwTypeError; } - return allocUnsafe(arg); - } - return from(arg, encodingOrOffset, length); - } - Buffer2.poolSize = 8192; - function from(value, encodingOrOffset, length) { - if (typeof value === "string") { - return fromString(value, encodingOrOffset); - } - if (ArrayBuffer.isView(value)) { - return fromArrayView(value); - } - if (value == null) { - throw new TypeError( - "The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type " + typeof value - ); - } - if (isInstance(value, ArrayBuffer) || value && isInstance(value.buffer, ArrayBuffer)) { - return fromArrayBuffer(value, encodingOrOffset, length); - } - if (typeof SharedArrayBuffer !== "undefined" && (isInstance(value, SharedArrayBuffer) || value && isInstance(value.buffer, SharedArrayBuffer))) { - return fromArrayBuffer(value, encodingOrOffset, length); - } - if (typeof value === "number") { - throw new TypeError( - 'The "value" argument must not be of type number. Received type number' - ); - } - var valueOf = value.valueOf && value.valueOf(); - if (valueOf != null && valueOf !== value) { - return Buffer2.from(valueOf, encodingOrOffset, length); - } - var b = fromObject(value); - if (b) return b; - if (typeof Symbol !== "undefined" && Symbol.toPrimitive != null && typeof value[Symbol.toPrimitive] === "function") { - return Buffer2.from( - value[Symbol.toPrimitive]("string"), - encodingOrOffset, - length - ); } - throw new TypeError( - "The first argument must be one of type string, Buffer, ArrayBuffer, Array, or Array-like Object. Received type " + typeof value - ); - } - Buffer2.from = function(value, encodingOrOffset, length) { - return from(value, encodingOrOffset, length); + })() : throwTypeError; + var hasSymbols = require_has_symbols()(); + var getProto = require_get_proto(); + var $ObjectGPO = require_Object_getPrototypeOf(); + var $ReflectGPO = require_Reflect_getPrototypeOf(); + var $apply = require_functionApply(); + var $call = require_functionCall(); + var needsEval = {}; + var TypedArray = typeof Uint8Array === "undefined" || !getProto ? undefined2 : getProto(Uint8Array); + var INTRINSICS = { + __proto__: null, + "%AggregateError%": typeof AggregateError === "undefined" ? undefined2 : AggregateError, + "%Array%": Array, + "%ArrayBuffer%": typeof ArrayBuffer === "undefined" ? undefined2 : ArrayBuffer, + "%ArrayIteratorPrototype%": hasSymbols && getProto ? getProto([][Symbol.iterator]()) : undefined2, + "%AsyncFromSyncIteratorPrototype%": undefined2, + "%AsyncFunction%": needsEval, + "%AsyncGenerator%": needsEval, + "%AsyncGeneratorFunction%": needsEval, + "%AsyncIteratorPrototype%": needsEval, + "%Atomics%": typeof Atomics === "undefined" ? undefined2 : Atomics, + "%BigInt%": typeof BigInt === "undefined" ? undefined2 : BigInt, + "%BigInt64Array%": typeof BigInt64Array === "undefined" ? undefined2 : BigInt64Array, + "%BigUint64Array%": typeof BigUint64Array === "undefined" ? undefined2 : BigUint64Array, + "%Boolean%": Boolean, + "%DataView%": typeof DataView === "undefined" ? undefined2 : DataView, + "%Date%": Date, + "%decodeURI%": decodeURI, + "%decodeURIComponent%": decodeURIComponent, + "%encodeURI%": encodeURI, + "%encodeURIComponent%": encodeURIComponent, + "%Error%": $Error, + "%eval%": eval, + // eslint-disable-line no-eval + "%EvalError%": $EvalError, + "%Float16Array%": typeof Float16Array === "undefined" ? undefined2 : Float16Array, + "%Float32Array%": typeof Float32Array === "undefined" ? undefined2 : Float32Array, + "%Float64Array%": typeof Float64Array === "undefined" ? undefined2 : Float64Array, + "%FinalizationRegistry%": typeof FinalizationRegistry === "undefined" ? undefined2 : FinalizationRegistry, + "%Function%": $Function, + "%GeneratorFunction%": needsEval, + "%Int8Array%": typeof Int8Array === "undefined" ? undefined2 : Int8Array, + "%Int16Array%": typeof Int16Array === "undefined" ? undefined2 : Int16Array, + "%Int32Array%": typeof Int32Array === "undefined" ? undefined2 : Int32Array, + "%isFinite%": isFinite, + "%isNaN%": isNaN, + "%IteratorPrototype%": hasSymbols && getProto ? getProto(getProto([][Symbol.iterator]())) : undefined2, + "%JSON%": typeof JSON === "object" ? JSON : undefined2, + "%Map%": typeof Map === "undefined" ? undefined2 : Map, + "%MapIteratorPrototype%": typeof Map === "undefined" || !hasSymbols || !getProto ? undefined2 : getProto((/* @__PURE__ */ new Map())[Symbol.iterator]()), + "%Math%": Math, + "%Number%": Number, + "%Object%": $Object, + "%Object.getOwnPropertyDescriptor%": $gOPD, + "%parseFloat%": parseFloat, + "%parseInt%": parseInt, + "%Promise%": typeof Promise === "undefined" ? undefined2 : Promise, + "%Proxy%": typeof Proxy === "undefined" ? undefined2 : Proxy, + "%RangeError%": $RangeError, + "%ReferenceError%": $ReferenceError, + "%Reflect%": typeof Reflect === "undefined" ? undefined2 : Reflect, + "%RegExp%": RegExp, + "%Set%": typeof Set === "undefined" ? undefined2 : Set, + "%SetIteratorPrototype%": typeof Set === "undefined" || !hasSymbols || !getProto ? undefined2 : getProto((/* @__PURE__ */ new Set())[Symbol.iterator]()), + "%SharedArrayBuffer%": typeof SharedArrayBuffer === "undefined" ? undefined2 : SharedArrayBuffer, + "%String%": String, + "%StringIteratorPrototype%": hasSymbols && getProto ? getProto(""[Symbol.iterator]()) : undefined2, + "%Symbol%": hasSymbols ? Symbol : undefined2, + "%SyntaxError%": $SyntaxError, + "%ThrowTypeError%": ThrowTypeError, + "%TypedArray%": TypedArray, + "%TypeError%": $TypeError, + "%Uint8Array%": typeof Uint8Array === "undefined" ? undefined2 : Uint8Array, + "%Uint8ClampedArray%": typeof Uint8ClampedArray === "undefined" ? undefined2 : Uint8ClampedArray, + "%Uint16Array%": typeof Uint16Array === "undefined" ? undefined2 : Uint16Array, + "%Uint32Array%": typeof Uint32Array === "undefined" ? undefined2 : Uint32Array, + "%URIError%": $URIError, + "%WeakMap%": typeof WeakMap === "undefined" ? undefined2 : WeakMap, + "%WeakRef%": typeof WeakRef === "undefined" ? undefined2 : WeakRef, + "%WeakSet%": typeof WeakSet === "undefined" ? undefined2 : WeakSet, + "%Function.prototype.call%": $call, + "%Function.prototype.apply%": $apply, + "%Object.defineProperty%": $defineProperty, + "%Object.getPrototypeOf%": $ObjectGPO, + "%Math.abs%": abs, + "%Math.floor%": floor, + "%Math.max%": max, + "%Math.min%": min, + "%Math.pow%": pow, + "%Math.round%": round, + "%Math.sign%": sign, + "%Reflect.getPrototypeOf%": $ReflectGPO }; - Object.setPrototypeOf(Buffer2.prototype, Uint8Array.prototype); - Object.setPrototypeOf(Buffer2, Uint8Array); - function assertSize(size) { - if (typeof size !== "number") { - throw new TypeError('"size" argument must be of type number'); - } else if (size < 0) { - throw new RangeError('The value "' + size + '" is invalid for option "size"'); + if (getProto) { + try { + null.error; + } catch (e) { + errorProto = getProto(getProto(e)); + INTRINSICS["%Error.prototype%"] = errorProto; } } - function alloc(size, fill, encoding) { - assertSize(size); - if (size <= 0) { - return createBuffer(size); - } - if (fill !== void 0) { - return typeof encoding === "string" ? createBuffer(size).fill(fill, encoding) : createBuffer(size).fill(fill); + var errorProto; + var doEval = function doEval2(name) { + var value; + if (name === "%AsyncFunction%") { + value = getEvalledConstructor("async function () {}"); + } else if (name === "%GeneratorFunction%") { + value = getEvalledConstructor("function* () {}"); + } else if (name === "%AsyncGeneratorFunction%") { + value = getEvalledConstructor("async function* () {}"); + } else if (name === "%AsyncGenerator%") { + var fn = doEval2("%AsyncGeneratorFunction%"); + if (fn) { + value = fn.prototype; + } + } else if (name === "%AsyncIteratorPrototype%") { + var gen = doEval2("%AsyncGenerator%"); + if (gen && getProto) { + value = getProto(gen.prototype); + } } - return createBuffer(size); - } - Buffer2.alloc = function(size, fill, encoding) { - return alloc(size, fill, encoding); - }; - function allocUnsafe(size) { - assertSize(size); - return createBuffer(size < 0 ? 0 : checked(size) | 0); - } - Buffer2.allocUnsafe = function(size) { - return allocUnsafe(size); + INTRINSICS[name] = value; + return value; }; - Buffer2.allocUnsafeSlow = function(size) { - return allocUnsafe(size); + var LEGACY_ALIASES = { + __proto__: null, + "%ArrayBufferPrototype%": ["ArrayBuffer", "prototype"], + "%ArrayPrototype%": ["Array", "prototype"], + "%ArrayProto_entries%": ["Array", "prototype", "entries"], + "%ArrayProto_forEach%": ["Array", "prototype", "forEach"], + "%ArrayProto_keys%": ["Array", "prototype", "keys"], + "%ArrayProto_values%": ["Array", "prototype", "values"], + "%AsyncFunctionPrototype%": ["AsyncFunction", "prototype"], + "%AsyncGenerator%": ["AsyncGeneratorFunction", "prototype"], + "%AsyncGeneratorPrototype%": ["AsyncGeneratorFunction", "prototype", "prototype"], + "%BooleanPrototype%": ["Boolean", "prototype"], + "%DataViewPrototype%": ["DataView", "prototype"], + "%DatePrototype%": ["Date", "prototype"], + "%ErrorPrototype%": ["Error", "prototype"], + "%EvalErrorPrototype%": ["EvalError", "prototype"], + "%Float32ArrayPrototype%": ["Float32Array", "prototype"], + "%Float64ArrayPrototype%": ["Float64Array", "prototype"], + "%FunctionPrototype%": ["Function", "prototype"], + "%Generator%": ["GeneratorFunction", "prototype"], + "%GeneratorPrototype%": ["GeneratorFunction", "prototype", "prototype"], + "%Int8ArrayPrototype%": ["Int8Array", "prototype"], + "%Int16ArrayPrototype%": ["Int16Array", "prototype"], + "%Int32ArrayPrototype%": ["Int32Array", "prototype"], + "%JSONParse%": ["JSON", "parse"], + "%JSONStringify%": ["JSON", "stringify"], + "%MapPrototype%": ["Map", "prototype"], + "%NumberPrototype%": ["Number", "prototype"], + "%ObjectPrototype%": ["Object", "prototype"], + "%ObjProto_toString%": ["Object", "prototype", "toString"], + "%ObjProto_valueOf%": ["Object", "prototype", "valueOf"], + "%PromisePrototype%": ["Promise", "prototype"], + "%PromiseProto_then%": ["Promise", "prototype", "then"], + "%Promise_all%": ["Promise", "all"], + "%Promise_reject%": ["Promise", "reject"], + "%Promise_resolve%": ["Promise", "resolve"], + "%RangeErrorPrototype%": ["RangeError", "prototype"], + "%ReferenceErrorPrototype%": ["ReferenceError", "prototype"], + "%RegExpPrototype%": ["RegExp", "prototype"], + "%SetPrototype%": ["Set", "prototype"], + "%SharedArrayBufferPrototype%": ["SharedArrayBuffer", "prototype"], + "%StringPrototype%": ["String", "prototype"], + "%SymbolPrototype%": ["Symbol", "prototype"], + "%SyntaxErrorPrototype%": ["SyntaxError", "prototype"], + "%TypedArrayPrototype%": ["TypedArray", "prototype"], + "%TypeErrorPrototype%": ["TypeError", "prototype"], + "%Uint8ArrayPrototype%": ["Uint8Array", "prototype"], + "%Uint8ClampedArrayPrototype%": ["Uint8ClampedArray", "prototype"], + "%Uint16ArrayPrototype%": ["Uint16Array", "prototype"], + "%Uint32ArrayPrototype%": ["Uint32Array", "prototype"], + "%URIErrorPrototype%": ["URIError", "prototype"], + "%WeakMapPrototype%": ["WeakMap", "prototype"], + "%WeakSetPrototype%": ["WeakSet", "prototype"] }; - function fromString(string, encoding) { - if (typeof encoding !== "string" || encoding === "") { - encoding = "utf8"; - } - if (!Buffer2.isEncoding(encoding)) { - throw new TypeError("Unknown encoding: " + encoding); + var bind = require_function_bind(); + var hasOwn = require_hasown(); + var $concat = bind.call($call, Array.prototype.concat); + var $spliceApply = bind.call($apply, Array.prototype.splice); + var $replace = bind.call($call, String.prototype.replace); + var $strSlice = bind.call($call, String.prototype.slice); + var $exec = bind.call($call, RegExp.prototype.exec); + var rePropName = /[^%.[\\]]+|\\[(?:(-?\\d+(?:\\.\\d+)?)|(["'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2)\\]|(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|%$))/g; + var reEscapeChar = /\\\\(\\\\)?/g; + var stringToPath = function stringToPath2(string) { + var first = $strSlice(string, 0, 1); + var last = $strSlice(string, -1); + if (first === "%" && last !== "%") { + throw new $SyntaxError("invalid intrinsic syntax, expected closing \`%\`"); + } else if (last === "%" && first !== "%") { + throw new $SyntaxError("invalid intrinsic syntax, expected opening \`%\`"); } - var length = byteLength(string, encoding) | 0; - var buf = createBuffer(length); - var actual = buf.write(string, encoding); - if (actual !== length) { - buf = buf.slice(0, actual); + var result = []; + $replace(string, rePropName, function(match, number, quote, subString) { + result[result.length] = quote ? $replace(subString, reEscapeChar, "$1") : number || match; + }); + return result; + }; + var getBaseIntrinsic = function getBaseIntrinsic2(name, allowMissing) { + var intrinsicName = name; + var alias; + if (hasOwn(LEGACY_ALIASES, intrinsicName)) { + alias = LEGACY_ALIASES[intrinsicName]; + intrinsicName = "%" + alias[0] + "%"; } - return buf; - } - function fromArrayLike(array) { - var length = array.length < 0 ? 0 : checked(array.length) | 0; - var buf = createBuffer(length); - for (var i = 0; i < length; i += 1) { - buf[i] = array[i] & 255; + if (hasOwn(INTRINSICS, intrinsicName)) { + var value = INTRINSICS[intrinsicName]; + if (value === needsEval) { + value = doEval(intrinsicName); + } + if (typeof value === "undefined" && !allowMissing) { + throw new $TypeError("intrinsic " + name + " exists, but is not available. Please file an issue!"); + } + return { + alias, + name: intrinsicName, + value + }; } - return buf; - } - function fromArrayView(arrayView) { - if (isInstance(arrayView, Uint8Array)) { - var copy = new Uint8Array(arrayView); - return fromArrayBuffer(copy.buffer, copy.byteOffset, copy.byteLength); + throw new $SyntaxError("intrinsic " + name + " does not exist!"); + }; + module2.exports = function GetIntrinsic(name, allowMissing) { + if (typeof name !== "string" || name.length === 0) { + throw new $TypeError("intrinsic name must be a non-empty string"); } - return fromArrayLike(arrayView); - } - function fromArrayBuffer(array, byteOffset, length) { - if (byteOffset < 0 || array.byteLength < byteOffset) { - throw new RangeError('"offset" is outside of buffer bounds'); + if (arguments.length > 1 && typeof allowMissing !== "boolean") { + throw new $TypeError('"allowMissing" argument must be a boolean'); } - if (array.byteLength < byteOffset + (length || 0)) { - throw new RangeError('"length" is outside of buffer bounds'); + if ($exec(/^%?[^%]*%?$/, name) === null) { + throw new $SyntaxError("\`%\` may not be present anywhere but at the beginning and end of the intrinsic name"); } - var buf; - if (byteOffset === void 0 && length === void 0) { - buf = new Uint8Array(array); - } else if (length === void 0) { - buf = new Uint8Array(array, byteOffset); - } else { - buf = new Uint8Array(array, byteOffset, length); + var parts = stringToPath(name); + var intrinsicBaseName = parts.length > 0 ? parts[0] : ""; + var intrinsic = getBaseIntrinsic("%" + intrinsicBaseName + "%", allowMissing); + var intrinsicRealName = intrinsic.name; + var value = intrinsic.value; + var skipFurtherCaching = false; + var alias = intrinsic.alias; + if (alias) { + intrinsicBaseName = alias[0]; + $spliceApply(parts, $concat([0, 1], alias)); } - Object.setPrototypeOf(buf, Buffer2.prototype); - return buf; - } - function fromObject(obj) { - if (Buffer2.isBuffer(obj)) { - var len = checked(obj.length) | 0; - var buf = createBuffer(len); - if (buf.length === 0) { - return buf; + for (var i = 1, isOwn = true; i < parts.length; i += 1) { + var part = parts[i]; + var first = $strSlice(part, 0, 1); + var last = $strSlice(part, -1); + if ((first === '"' || first === "'" || first === "\`" || (last === '"' || last === "'" || last === "\`")) && first !== last) { + throw new $SyntaxError("property names with quotes must have matching quotes"); } - obj.copy(buf, 0, 0, len); - return buf; - } - if (obj.length !== void 0) { - if (typeof obj.length !== "number" || numberIsNaN(obj.length)) { - return createBuffer(0); + if (part === "constructor" || !isOwn) { + skipFurtherCaching = true; + } + intrinsicBaseName += "." + part; + intrinsicRealName = "%" + intrinsicBaseName + "%"; + if (hasOwn(INTRINSICS, intrinsicRealName)) { + value = INTRINSICS[intrinsicRealName]; + } else if (value != null) { + if (!(part in value)) { + if (!allowMissing) { + throw new $TypeError("base intrinsic for " + name + " exists, but the property is not available."); + } + return void undefined2; + } + if ($gOPD && i + 1 >= parts.length) { + var desc = $gOPD(value, part); + isOwn = !!desc; + if (isOwn && "get" in desc && !("originalValue" in desc.get)) { + value = desc.get; + } else { + value = value[part]; + } + } else { + isOwn = hasOwn(value, part); + value = value[part]; + } + if (isOwn && !skipFurtherCaching) { + INTRINSICS[intrinsicRealName] = value; + } } - return fromArrayLike(obj); } - if (obj.type === "Buffer" && Array.isArray(obj.data)) { - return fromArrayLike(obj.data); + return value; + }; + } +}); + +// node_modules/.pnpm/call-bound@1.0.4/node_modules/call-bound/index.js +var require_call_bound = __commonJS({ + "node_modules/.pnpm/call-bound@1.0.4/node_modules/call-bound/index.js"(exports2, module2) { + "use strict"; + var GetIntrinsic = require_get_intrinsic(); + var callBindBasic = require_call_bind_apply_helpers(); + var $indexOf = callBindBasic([GetIntrinsic("%String.prototype.indexOf%")]); + module2.exports = function callBoundIntrinsic(name, allowMissing) { + var intrinsic = ( + /** @type {(this: unknown, ...args: unknown[]) => unknown} */ + GetIntrinsic(name, !!allowMissing) + ); + if (typeof intrinsic === "function" && $indexOf(name, ".prototype.") > -1) { + return callBindBasic( + /** @type {const} */ + [intrinsic] + ); } - } - function checked(length) { - if (length >= K_MAX_LENGTH) { - throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x" + K_MAX_LENGTH.toString(16) + " bytes"); + return intrinsic; + }; + } +}); + +// node_modules/.pnpm/is-arguments@1.2.0/node_modules/is-arguments/index.js +var require_is_arguments = __commonJS({ + "node_modules/.pnpm/is-arguments@1.2.0/node_modules/is-arguments/index.js"(exports2, module2) { + "use strict"; + var hasToStringTag = require_shams2()(); + var callBound = require_call_bound(); + var $toString = callBound("Object.prototype.toString"); + var isStandardArguments = function isArguments(value) { + if (hasToStringTag && value && typeof value === "object" && Symbol.toStringTag in value) { + return false; } - return length | 0; - } - function SlowBuffer(length) { - if (+length != length) { - length = 0; + return $toString(value) === "[object Arguments]"; + }; + var isLegacyArguments = function isArguments(value) { + if (isStandardArguments(value)) { + return true; } - return Buffer2.alloc(+length); - } - Buffer2.isBuffer = function isBuffer(b) { - return b != null && b._isBuffer === true && b !== Buffer2.prototype; + return value !== null && typeof value === "object" && "length" in value && typeof value.length === "number" && value.length >= 0 && $toString(value) !== "[object Array]" && "callee" in value && $toString(value.callee) === "[object Function]"; }; - Buffer2.compare = function compare(a, b) { - if (isInstance(a, Uint8Array)) a = Buffer2.from(a, a.offset, a.byteLength); - if (isInstance(b, Uint8Array)) b = Buffer2.from(b, b.offset, b.byteLength); - if (!Buffer2.isBuffer(a) || !Buffer2.isBuffer(b)) { - throw new TypeError( - 'The "buf1", "buf2" arguments must be one of type Buffer or Uint8Array' - ); + var supportsStandardArguments = (function() { + return isStandardArguments(arguments); + })(); + isStandardArguments.isLegacyArguments = isLegacyArguments; + module2.exports = supportsStandardArguments ? isStandardArguments : isLegacyArguments; + } +}); + +// node_modules/.pnpm/is-regex@1.2.1/node_modules/is-regex/index.js +var require_is_regex = __commonJS({ + "node_modules/.pnpm/is-regex@1.2.1/node_modules/is-regex/index.js"(exports2, module2) { + "use strict"; + var callBound = require_call_bound(); + var hasToStringTag = require_shams2()(); + var hasOwn = require_hasown(); + var gOPD = require_gopd(); + var fn; + if (hasToStringTag) { + $exec = callBound("RegExp.prototype.exec"); + isRegexMarker = {}; + throwRegexMarker = function() { + throw isRegexMarker; + }; + badStringifier = { + toString: throwRegexMarker, + valueOf: throwRegexMarker + }; + if (typeof Symbol.toPrimitive === "symbol") { + badStringifier[Symbol.toPrimitive] = throwRegexMarker; } - if (a === b) return 0; - var x = a.length; - var y = b.length; - for (var i = 0, len = Math.min(x, y); i < len; ++i) { - if (a[i] !== b[i]) { - x = a[i]; - y = b[i]; - break; + fn = function isRegex(value) { + if (!value || typeof value !== "object") { + return false; } - } - if (x < y) return -1; - if (y < x) return 1; - return 0; - }; - Buffer2.isEncoding = function isEncoding(encoding) { - switch (String(encoding).toLowerCase()) { - case "hex": - case "utf8": - case "utf-8": - case "ascii": - case "latin1": - case "binary": - case "base64": - case "ucs2": - case "ucs-2": - case "utf16le": - case "utf-16le": - return true; - default: + var descriptor = ( + /** @type {NonNullable} */ + gOPD( + /** @type {{ lastIndex?: unknown }} */ + value, + "lastIndex" + ) + ); + var hasLastIndexDataProperty = descriptor && hasOwn(descriptor, "value"); + if (!hasLastIndexDataProperty) { + return false; + } + try { + $exec( + value, + /** @type {string} */ + /** @type {unknown} */ + badStringifier + ); + } catch (e) { + return e === isRegexMarker; + } + }; + } else { + $toString = callBound("Object.prototype.toString"); + regexClass = "[object RegExp]"; + fn = function isRegex(value) { + if (!value || typeof value !== "object" && typeof value !== "function") { return false; + } + return $toString(value) === regexClass; + }; + } + var $exec; + var isRegexMarker; + var throwRegexMarker; + var badStringifier; + var $toString; + var regexClass; + module2.exports = fn; + } +}); + +// node_modules/.pnpm/safe-regex-test@1.1.0/node_modules/safe-regex-test/index.js +var require_safe_regex_test = __commonJS({ + "node_modules/.pnpm/safe-regex-test@1.1.0/node_modules/safe-regex-test/index.js"(exports2, module2) { + "use strict"; + var callBound = require_call_bound(); + var isRegex = require_is_regex(); + var $exec = callBound("RegExp.prototype.exec"); + var $TypeError = require_type(); + module2.exports = function regexTester(regex) { + if (!isRegex(regex)) { + throw new $TypeError("\`regex\` must be a RegExp"); } + return function test(s) { + return $exec(regex, s) !== null; + }; }; - Buffer2.concat = function concat(list, length) { - if (!Array.isArray(list)) { - throw new TypeError('"list" argument must be an Array of Buffers'); + } +}); + +// node_modules/.pnpm/generator-function@2.0.1/node_modules/generator-function/index.js +var require_generator_function = __commonJS({ + "node_modules/.pnpm/generator-function@2.0.1/node_modules/generator-function/index.js"(exports2, module2) { + "use strict"; + var cached = ( + /** @type {GeneratorFunctionConstructor} */ + function* () { + }.constructor + ); + module2.exports = () => cached; + } +}); + +// node_modules/.pnpm/is-generator-function@1.1.2/node_modules/is-generator-function/index.js +var require_is_generator_function = __commonJS({ + "node_modules/.pnpm/is-generator-function@1.1.2/node_modules/is-generator-function/index.js"(exports2, module2) { + "use strict"; + var callBound = require_call_bound(); + var safeRegexTest = require_safe_regex_test(); + var isFnRegex = safeRegexTest(/^\\s*(?:function)?\\*/); + var hasToStringTag = require_shams2()(); + var getProto = require_get_proto(); + var toStr = callBound("Object.prototype.toString"); + var fnToStr = callBound("Function.prototype.toString"); + var getGeneratorFunction = require_generator_function(); + module2.exports = function isGeneratorFunction(fn) { + if (typeof fn !== "function") { + return false; } - if (list.length === 0) { - return Buffer2.alloc(0); + if (isFnRegex(fnToStr(fn))) { + return true; } - var i; - if (length === void 0) { - length = 0; - for (i = 0; i < list.length; ++i) { - length += list[i].length; - } + if (!hasToStringTag) { + var str = toStr(fn); + return str === "[object GeneratorFunction]"; } - var buffer2 = Buffer2.allocUnsafe(length); - var pos = 0; - for (i = 0; i < list.length; ++i) { - var buf = list[i]; - if (isInstance(buf, Uint8Array)) { - if (pos + buf.length > buffer2.length) { - Buffer2.from(buf).copy(buffer2, pos); - } else { - Uint8Array.prototype.set.call( - buffer2, - buf, - pos - ); - } - } else if (!Buffer2.isBuffer(buf)) { - throw new TypeError('"list" argument must be an Array of Buffers'); - } else { - buf.copy(buffer2, pos); - } - pos += buf.length; + if (!getProto) { + return false; } - return buffer2; + var GeneratorFunction = getGeneratorFunction(); + return GeneratorFunction && getProto(fn) === GeneratorFunction.prototype; }; - function byteLength(string, encoding) { - if (Buffer2.isBuffer(string)) { - return string.length; + } +}); + +// node_modules/.pnpm/is-callable@1.2.7/node_modules/is-callable/index.js +var require_is_callable = __commonJS({ + "node_modules/.pnpm/is-callable@1.2.7/node_modules/is-callable/index.js"(exports2, module2) { + "use strict"; + var fnToStr = Function.prototype.toString; + var reflectApply = typeof Reflect === "object" && Reflect !== null && Reflect.apply; + var badArrayLike; + var isCallableMarker; + if (typeof reflectApply === "function" && typeof Object.defineProperty === "function") { + try { + badArrayLike = Object.defineProperty({}, "length", { + get: function() { + throw isCallableMarker; + } + }); + isCallableMarker = {}; + reflectApply(function() { + throw 42; + }, null, badArrayLike); + } catch (_) { + if (_ !== isCallableMarker) { + reflectApply = null; + } } - if (ArrayBuffer.isView(string) || isInstance(string, ArrayBuffer)) { - return string.byteLength; + } else { + reflectApply = null; + } + var constructorRegex = /^\\s*class\\b/; + var isES6ClassFn = function isES6ClassFunction(value) { + try { + var fnStr = fnToStr.call(value); + return constructorRegex.test(fnStr); + } catch (e) { + return false; } - if (typeof string !== "string") { - throw new TypeError( - 'The "string" argument must be one of type string, Buffer, or ArrayBuffer. Received type ' + typeof string - ); + }; + var tryFunctionObject = function tryFunctionToStr(value) { + try { + if (isES6ClassFn(value)) { + return false; + } + fnToStr.call(value); + return true; + } catch (e) { + return false; } - var len = string.length; - var mustMatch = arguments.length > 2 && arguments[2] === true; - if (!mustMatch && len === 0) return 0; - var loweredCase = false; - for (; ; ) { - switch (encoding) { - case "ascii": - case "latin1": - case "binary": - return len; - case "utf8": - case "utf-8": - return utf8ToBytes(string).length; - case "ucs2": - case "ucs-2": - case "utf16le": - case "utf-16le": - return len * 2; - case "hex": - return len >>> 1; - case "base64": - return base64ToBytes(string).length; - default: - if (loweredCase) { - return mustMatch ? -1 : utf8ToBytes(string).length; + }; + var toStr = Object.prototype.toString; + var objectClass = "[object Object]"; + var fnClass = "[object Function]"; + var genClass = "[object GeneratorFunction]"; + var ddaClass = "[object HTMLAllCollection]"; + var ddaClass2 = "[object HTML document.all class]"; + var ddaClass3 = "[object HTMLCollection]"; + var hasToStringTag = typeof Symbol === "function" && !!Symbol.toStringTag; + var isIE68 = !(0 in [,]); + var isDDA = function isDocumentDotAll() { + return false; + }; + if (typeof document === "object") { + all = document.all; + if (toStr.call(all) === toStr.call(document.all)) { + isDDA = function isDocumentDotAll(value) { + if ((isIE68 || !value) && (typeof value === "undefined" || typeof value === "object")) { + try { + var str = toStr.call(value); + return (str === ddaClass || str === ddaClass2 || str === ddaClass3 || str === objectClass) && value("") == null; + } catch (e) { } - encoding = ("" + encoding).toLowerCase(); - loweredCase = true; - } + } + return false; + }; } } - Buffer2.byteLength = byteLength; - function slowToString(encoding, start, end) { - var loweredCase = false; - if (start === void 0 || start < 0) { - start = 0; - } - if (start > this.length) { - return ""; - } - if (end === void 0 || end > this.length) { - end = this.length; + var all; + module2.exports = reflectApply ? function isCallable(value) { + if (isDDA(value)) { + return true; } - if (end <= 0) { - return ""; + if (!value) { + return false; } - end >>>= 0; - start >>>= 0; - if (end <= start) { - return ""; + if (typeof value !== "function" && typeof value !== "object") { + return false; } - if (!encoding) encoding = "utf8"; - while (true) { - switch (encoding) { - case "hex": - return hexSlice(this, start, end); - case "utf8": - case "utf-8": - return utf8Slice(this, start, end); - case "ascii": - return asciiSlice(this, start, end); - case "latin1": - case "binary": - return latin1Slice(this, start, end); - case "base64": - return base64Slice(this, start, end); - case "ucs2": - case "ucs-2": - case "utf16le": - case "utf-16le": - return utf16leSlice(this, start, end); - default: - if (loweredCase) throw new TypeError("Unknown encoding: " + encoding); - encoding = (encoding + "").toLowerCase(); - loweredCase = true; + try { + reflectApply(value, null, badArrayLike); + } catch (e) { + if (e !== isCallableMarker) { + return false; } } - } - Buffer2.prototype._isBuffer = true; - function swap(b, n, m) { - var i = b[n]; - b[n] = b[m]; - b[m] = i; - } - Buffer2.prototype.swap16 = function swap16() { - var len = this.length; - if (len % 2 !== 0) { - throw new RangeError("Buffer size must be a multiple of 16-bits"); + return !isES6ClassFn(value) && tryFunctionObject(value); + } : function isCallable(value) { + if (isDDA(value)) { + return true; } - for (var i = 0; i < len; i += 2) { - swap(this, i, i + 1); + if (!value) { + return false; } - return this; - }; - Buffer2.prototype.swap32 = function swap32() { - var len = this.length; - if (len % 4 !== 0) { - throw new RangeError("Buffer size must be a multiple of 32-bits"); + if (typeof value !== "function" && typeof value !== "object") { + return false; } - for (var i = 0; i < len; i += 4) { - swap(this, i, i + 3); - swap(this, i + 1, i + 2); + if (hasToStringTag) { + return tryFunctionObject(value); } - return this; - }; - Buffer2.prototype.swap64 = function swap64() { - var len = this.length; - if (len % 8 !== 0) { - throw new RangeError("Buffer size must be a multiple of 64-bits"); + if (isES6ClassFn(value)) { + return false; } - for (var i = 0; i < len; i += 8) { - swap(this, i, i + 7); - swap(this, i + 1, i + 6); - swap(this, i + 2, i + 5); - swap(this, i + 3, i + 4); + var strClass = toStr.call(value); + if (strClass !== fnClass && strClass !== genClass && !/^\\[object HTML/.test(strClass)) { + return false; } - return this; - }; - Buffer2.prototype.toString = function toString() { - var length = this.length; - if (length === 0) return ""; - if (arguments.length === 0) return utf8Slice(this, 0, length); - return slowToString.apply(this, arguments); + return tryFunctionObject(value); }; - Buffer2.prototype.toLocaleString = Buffer2.prototype.toString; - Buffer2.prototype.equals = function equals(b) { - if (!Buffer2.isBuffer(b)) throw new TypeError("Argument must be a Buffer"); - if (this === b) return true; - return Buffer2.compare(this, b) === 0; + } +}); + +// node_modules/.pnpm/for-each@0.3.5/node_modules/for-each/index.js +var require_for_each = __commonJS({ + "node_modules/.pnpm/for-each@0.3.5/node_modules/for-each/index.js"(exports2, module2) { + "use strict"; + var isCallable = require_is_callable(); + var toStr = Object.prototype.toString; + var hasOwnProperty = Object.prototype.hasOwnProperty; + var forEachArray = function forEachArray2(array, iterator, receiver) { + for (var i = 0, len = array.length; i < len; i++) { + if (hasOwnProperty.call(array, i)) { + if (receiver == null) { + iterator(array[i], i, array); + } else { + iterator.call(receiver, array[i], i, array); + } + } + } }; - Buffer2.prototype.inspect = function inspect() { - var str = ""; - var max = exports2.INSPECT_MAX_BYTES; - str = this.toString("hex", 0, max).replace(/(.{2})/g, "$1 ").trim(); - if (this.length > max) str += " ... "; - return ""; + var forEachString = function forEachString2(string, iterator, receiver) { + for (var i = 0, len = string.length; i < len; i++) { + if (receiver == null) { + iterator(string.charAt(i), i, string); + } else { + iterator.call(receiver, string.charAt(i), i, string); + } + } }; - if (customInspectSymbol) { - Buffer2.prototype[customInspectSymbol] = Buffer2.prototype.inspect; + var forEachObject = function forEachObject2(object, iterator, receiver) { + for (var k in object) { + if (hasOwnProperty.call(object, k)) { + if (receiver == null) { + iterator(object[k], k, object); + } else { + iterator.call(receiver, object[k], k, object); + } + } + } + }; + function isArray(x) { + return toStr.call(x) === "[object Array]"; } - Buffer2.prototype.compare = function compare(target, start, end, thisStart, thisEnd) { - if (isInstance(target, Uint8Array)) { - target = Buffer2.from(target, target.offset, target.byteLength); + module2.exports = function forEach(list, iterator, thisArg) { + if (!isCallable(iterator)) { + throw new TypeError("iterator must be a function"); } - if (!Buffer2.isBuffer(target)) { - throw new TypeError( - 'The "target" argument must be one of type Buffer or Uint8Array. Received type ' + typeof target - ); + var receiver; + if (arguments.length >= 3) { + receiver = thisArg; } - if (start === void 0) { - start = 0; + if (isArray(list)) { + forEachArray(list, iterator, receiver); + } else if (typeof list === "string") { + forEachString(list, iterator, receiver); + } else { + forEachObject(list, iterator, receiver); } - if (end === void 0) { - end = target ? target.length : 0; + }; + } +}); + +// node_modules/.pnpm/possible-typed-array-names@1.1.0/node_modules/possible-typed-array-names/index.js +var require_possible_typed_array_names = __commonJS({ + "node_modules/.pnpm/possible-typed-array-names@1.1.0/node_modules/possible-typed-array-names/index.js"(exports2, module2) { + "use strict"; + module2.exports = [ + "Float16Array", + "Float32Array", + "Float64Array", + "Int8Array", + "Int16Array", + "Int32Array", + "Uint8Array", + "Uint8ClampedArray", + "Uint16Array", + "Uint32Array", + "BigInt64Array", + "BigUint64Array" + ]; + } +}); + +// node_modules/.pnpm/available-typed-arrays@1.0.7/node_modules/available-typed-arrays/index.js +var require_available_typed_arrays = __commonJS({ + "node_modules/.pnpm/available-typed-arrays@1.0.7/node_modules/available-typed-arrays/index.js"(exports2, module2) { + "use strict"; + var possibleNames = require_possible_typed_array_names(); + var g = typeof globalThis === "undefined" ? global : globalThis; + module2.exports = function availableTypedArrays() { + var out = []; + for (var i = 0; i < possibleNames.length; i++) { + if (typeof g[possibleNames[i]] === "function") { + out[out.length] = possibleNames[i]; + } } - if (thisStart === void 0) { - thisStart = 0; + return out; + }; + } +}); + +// node_modules/.pnpm/define-data-property@1.1.4/node_modules/define-data-property/index.js +var require_define_data_property = __commonJS({ + "node_modules/.pnpm/define-data-property@1.1.4/node_modules/define-data-property/index.js"(exports2, module2) { + "use strict"; + var $defineProperty = require_es_define_property(); + var $SyntaxError = require_syntax(); + var $TypeError = require_type(); + var gopd = require_gopd(); + module2.exports = function defineDataProperty(obj, property, value) { + if (!obj || typeof obj !== "object" && typeof obj !== "function") { + throw new $TypeError("\`obj\` must be an object or a function\`"); } - if (thisEnd === void 0) { - thisEnd = this.length; + if (typeof property !== "string" && typeof property !== "symbol") { + throw new $TypeError("\`property\` must be a string or a symbol\`"); } - if (start < 0 || end > target.length || thisStart < 0 || thisEnd > this.length) { - throw new RangeError("out of range index"); + if (arguments.length > 3 && typeof arguments[3] !== "boolean" && arguments[3] !== null) { + throw new $TypeError("\`nonEnumerable\`, if provided, must be a boolean or null"); } - if (thisStart >= thisEnd && start >= end) { - return 0; + if (arguments.length > 4 && typeof arguments[4] !== "boolean" && arguments[4] !== null) { + throw new $TypeError("\`nonWritable\`, if provided, must be a boolean or null"); } - if (thisStart >= thisEnd) { - return -1; + if (arguments.length > 5 && typeof arguments[5] !== "boolean" && arguments[5] !== null) { + throw new $TypeError("\`nonConfigurable\`, if provided, must be a boolean or null"); } - if (start >= end) { - return 1; + if (arguments.length > 6 && typeof arguments[6] !== "boolean") { + throw new $TypeError("\`loose\`, if provided, must be a boolean"); } - start >>>= 0; - end >>>= 0; - thisStart >>>= 0; - thisEnd >>>= 0; - if (this === target) return 0; - var x = thisEnd - thisStart; - var y = end - start; - var len = Math.min(x, y); - var thisCopy = this.slice(thisStart, thisEnd); - var targetCopy = target.slice(start, end); - for (var i = 0; i < len; ++i) { - if (thisCopy[i] !== targetCopy[i]) { - x = thisCopy[i]; - y = targetCopy[i]; - break; - } + var nonEnumerable = arguments.length > 3 ? arguments[3] : null; + var nonWritable = arguments.length > 4 ? arguments[4] : null; + var nonConfigurable = arguments.length > 5 ? arguments[5] : null; + var loose = arguments.length > 6 ? arguments[6] : false; + var desc = !!gopd && gopd(obj, property); + if ($defineProperty) { + $defineProperty(obj, property, { + configurable: nonConfigurable === null && desc ? desc.configurable : !nonConfigurable, + enumerable: nonEnumerable === null && desc ? desc.enumerable : !nonEnumerable, + value, + writable: nonWritable === null && desc ? desc.writable : !nonWritable + }); + } else if (loose || !nonEnumerable && !nonWritable && !nonConfigurable) { + obj[property] = value; + } else { + throw new $SyntaxError("This environment does not support defining a property as non-configurable, non-writable, or non-enumerable."); } - if (x < y) return -1; - if (y < x) return 1; - return 0; }; - function bidirectionalIndexOf(buffer2, val, byteOffset, encoding, dir) { - if (buffer2.length === 0) return -1; - if (typeof byteOffset === "string") { - encoding = byteOffset; - byteOffset = 0; - } else if (byteOffset > 2147483647) { - byteOffset = 2147483647; - } else if (byteOffset < -2147483648) { - byteOffset = -2147483648; - } - byteOffset = +byteOffset; - if (numberIsNaN(byteOffset)) { - byteOffset = dir ? 0 : buffer2.length - 1; + } +}); + +// node_modules/.pnpm/has-property-descriptors@1.0.2/node_modules/has-property-descriptors/index.js +var require_has_property_descriptors = __commonJS({ + "node_modules/.pnpm/has-property-descriptors@1.0.2/node_modules/has-property-descriptors/index.js"(exports2, module2) { + "use strict"; + var $defineProperty = require_es_define_property(); + var hasPropertyDescriptors = function hasPropertyDescriptors2() { + return !!$defineProperty; + }; + hasPropertyDescriptors.hasArrayLengthDefineBug = function hasArrayLengthDefineBug() { + if (!$defineProperty) { + return null; } - if (byteOffset < 0) byteOffset = buffer2.length + byteOffset; - if (byteOffset >= buffer2.length) { - if (dir) return -1; - else byteOffset = buffer2.length - 1; - } else if (byteOffset < 0) { - if (dir) byteOffset = 0; - else return -1; + try { + return $defineProperty([], "length", { value: 1 }).length !== 1; + } catch (e) { + return true; } - if (typeof val === "string") { - val = Buffer2.from(val, encoding); + }; + module2.exports = hasPropertyDescriptors; + } +}); + +// node_modules/.pnpm/set-function-length@1.2.2/node_modules/set-function-length/index.js +var require_set_function_length = __commonJS({ + "node_modules/.pnpm/set-function-length@1.2.2/node_modules/set-function-length/index.js"(exports2, module2) { + "use strict"; + var GetIntrinsic = require_get_intrinsic(); + var define = require_define_data_property(); + var hasDescriptors = require_has_property_descriptors()(); + var gOPD = require_gopd(); + var $TypeError = require_type(); + var $floor = GetIntrinsic("%Math.floor%"); + module2.exports = function setFunctionLength(fn, length) { + if (typeof fn !== "function") { + throw new $TypeError("\`fn\` is not a function"); } - if (Buffer2.isBuffer(val)) { - if (val.length === 0) { - return -1; - } - return arrayIndexOf(buffer2, val, byteOffset, encoding, dir); - } else if (typeof val === "number") { - val = val & 255; - if (typeof Uint8Array.prototype.indexOf === "function") { - if (dir) { - return Uint8Array.prototype.indexOf.call(buffer2, val, byteOffset); - } else { - return Uint8Array.prototype.lastIndexOf.call(buffer2, val, byteOffset); - } - } - return arrayIndexOf(buffer2, [val], byteOffset, encoding, dir); + if (typeof length !== "number" || length < 0 || length > 4294967295 || $floor(length) !== length) { + throw new $TypeError("\`length\` must be a positive 32-bit integer"); } - throw new TypeError("val must be string, number or Buffer"); - } - function arrayIndexOf(arr, val, byteOffset, encoding, dir) { - var indexSize = 1; - var arrLength = arr.length; - var valLength = val.length; - if (encoding !== void 0) { - encoding = String(encoding).toLowerCase(); - if (encoding === "ucs2" || encoding === "ucs-2" || encoding === "utf16le" || encoding === "utf-16le") { - if (arr.length < 2 || val.length < 2) { - return -1; - } - indexSize = 2; - arrLength /= 2; - valLength /= 2; - byteOffset /= 2; + var loose = arguments.length > 2 && !!arguments[2]; + var functionLengthIsConfigurable = true; + var functionLengthIsWritable = true; + if ("length" in fn && gOPD) { + var desc = gOPD(fn, "length"); + if (desc && !desc.configurable) { + functionLengthIsConfigurable = false; } - } - function read(buf, i2) { - if (indexSize === 1) { - return buf[i2]; - } else { - return buf.readUInt16BE(i2 * indexSize); + if (desc && !desc.writable) { + functionLengthIsWritable = false; } } - var i; - if (dir) { - var foundIndex = -1; - for (i = byteOffset; i < arrLength; i++) { - if (read(arr, i) === read(val, foundIndex === -1 ? 0 : i - foundIndex)) { - if (foundIndex === -1) foundIndex = i; - if (i - foundIndex + 1 === valLength) return foundIndex * indexSize; - } else { - if (foundIndex !== -1) i -= i - foundIndex; - foundIndex = -1; - } - } - } else { - if (byteOffset + valLength > arrLength) byteOffset = arrLength - valLength; - for (i = byteOffset; i >= 0; i--) { - var found = true; - for (var j = 0; j < valLength; j++) { - if (read(arr, i + j) !== read(val, j)) { - found = false; - break; - } - } - if (found) return i; + if (functionLengthIsConfigurable || functionLengthIsWritable || !loose) { + if (hasDescriptors) { + define( + /** @type {Parameters[0]} */ + fn, + "length", + length, + true, + true + ); + } else { + define( + /** @type {Parameters[0]} */ + fn, + "length", + length + ); } } - return -1; - } - Buffer2.prototype.includes = function includes(val, byteOffset, encoding) { - return this.indexOf(val, byteOffset, encoding) !== -1; + return fn; }; - Buffer2.prototype.indexOf = function indexOf(val, byteOffset, encoding) { - return bidirectionalIndexOf(this, val, byteOffset, encoding, true); + } +}); + +// node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/applyBind.js +var require_applyBind = __commonJS({ + "node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/applyBind.js"(exports2, module2) { + "use strict"; + var bind = require_function_bind(); + var $apply = require_functionApply(); + var actualApply = require_actualApply(); + module2.exports = function applyBind() { + return actualApply(bind, $apply, arguments); }; - Buffer2.prototype.lastIndexOf = function lastIndexOf(val, byteOffset, encoding) { - return bidirectionalIndexOf(this, val, byteOffset, encoding, false); + } +}); + +// node_modules/.pnpm/call-bind@1.0.8/node_modules/call-bind/index.js +var require_call_bind = __commonJS({ + "node_modules/.pnpm/call-bind@1.0.8/node_modules/call-bind/index.js"(exports2, module2) { + "use strict"; + var setFunctionLength = require_set_function_length(); + var $defineProperty = require_es_define_property(); + var callBindBasic = require_call_bind_apply_helpers(); + var applyBind = require_applyBind(); + module2.exports = function callBind(originalFunction) { + var func = callBindBasic(arguments); + var adjustedLength = originalFunction.length - (arguments.length - 1); + return setFunctionLength( + func, + 1 + (adjustedLength > 0 ? adjustedLength : 0), + true + ); }; - function hexWrite(buf, string, offset, length) { - offset = Number(offset) || 0; - var remaining = buf.length - offset; - if (!length) { - length = remaining; - } else { - length = Number(length); - if (length > remaining) { - length = remaining; + if ($defineProperty) { + $defineProperty(module2.exports, "apply", { value: applyBind }); + } else { + module2.exports.apply = applyBind; + } + } +}); + +// node_modules/.pnpm/which-typed-array@1.1.20/node_modules/which-typed-array/index.js +var require_which_typed_array = __commonJS({ + "node_modules/.pnpm/which-typed-array@1.1.20/node_modules/which-typed-array/index.js"(exports2, module2) { + "use strict"; + var forEach = require_for_each(); + var availableTypedArrays = require_available_typed_arrays(); + var callBind = require_call_bind(); + var callBound = require_call_bound(); + var gOPD = require_gopd(); + var getProto = require_get_proto(); + var $toString = callBound("Object.prototype.toString"); + var hasToStringTag = require_shams2()(); + var g = typeof globalThis === "undefined" ? global : globalThis; + var typedArrays = availableTypedArrays(); + var $slice = callBound("String.prototype.slice"); + var $indexOf = callBound("Array.prototype.indexOf", true) || function indexOf(array, value) { + for (var i = 0; i < array.length; i += 1) { + if (array[i] === value) { + return i; } } - var strLen = string.length; - if (length > strLen / 2) { - length = strLen / 2; - } - for (var i = 0; i < length; ++i) { - var parsed = parseInt(string.substr(i * 2, 2), 16); - if (numberIsNaN(parsed)) return i; - buf[offset + i] = parsed; - } - return i; - } - function utf8Write(buf, string, offset, length) { - return blitBuffer(utf8ToBytes(string, buf.length - offset), buf, offset, length); - } - function asciiWrite(buf, string, offset, length) { - return blitBuffer(asciiToBytes(string), buf, offset, length); - } - function base64Write(buf, string, offset, length) { - return blitBuffer(base64ToBytes(string), buf, offset, length); - } - function ucs2Write(buf, string, offset, length) { - return blitBuffer(utf16leToBytes(string, buf.length - offset), buf, offset, length); + return -1; + }; + var cache = { __proto__: null }; + if (hasToStringTag && gOPD && getProto) { + forEach(typedArrays, function(typedArray) { + var arr = new g[typedArray](); + if (Symbol.toStringTag in arr && getProto) { + var proto = getProto(arr); + var descriptor = gOPD(proto, Symbol.toStringTag); + if (!descriptor && proto) { + var superProto = getProto(proto); + descriptor = gOPD(superProto, Symbol.toStringTag); + } + if (descriptor && descriptor.get) { + var bound = callBind(descriptor.get); + cache[ + /** @type {\`$\${import('.').TypedArrayName}\`} */ + "$" + typedArray + ] = bound; + } + } + }); + } else { + forEach(typedArrays, function(typedArray) { + var arr = new g[typedArray](); + var fn = arr.slice || arr.set; + if (fn) { + var bound = ( + /** @type {import('./types').BoundSlice | import('./types').BoundSet} */ + // @ts-expect-error TODO FIXME + callBind(fn) + ); + cache[ + /** @type {\`$\${import('.').TypedArrayName}\`} */ + "$" + typedArray + ] = bound; + } + }); } - Buffer2.prototype.write = function write(string, offset, length, encoding) { - if (offset === void 0) { - encoding = "utf8"; - length = this.length; - offset = 0; - } else if (length === void 0 && typeof offset === "string") { - encoding = offset; - length = this.length; - offset = 0; - } else if (isFinite(offset)) { - offset = offset >>> 0; - if (isFinite(length)) { - length = length >>> 0; - if (encoding === void 0) encoding = "utf8"; - } else { - encoding = length; - length = void 0; + var tryTypedArrays = function tryAllTypedArrays(value) { + var found = false; + forEach( + /** @type {Record<\`\\$\${import('.').TypedArrayName}\`, Getter>} */ + cache, + /** @type {(getter: Getter, name: \`\\$\${import('.').TypedArrayName}\`) => void} */ + function(getter, typedArray) { + if (!found) { + try { + if ("$" + getter(value) === typedArray) { + found = /** @type {import('.').TypedArrayName} */ + $slice(typedArray, 1); + } + } catch (e) { + } + } } - } else { - throw new Error( - "Buffer.write(string, encoding, offset[, length]) is no longer supported" - ); - } - var remaining = this.length - offset; - if (length === void 0 || length > remaining) length = remaining; - if (string.length > 0 && (length < 0 || offset < 0) || offset > this.length) { - throw new RangeError("Attempt to write outside buffer bounds"); - } - if (!encoding) encoding = "utf8"; - var loweredCase = false; - for (; ; ) { - switch (encoding) { - case "hex": - return hexWrite(this, string, offset, length); - case "utf8": - case "utf-8": - return utf8Write(this, string, offset, length); - case "ascii": - case "latin1": - case "binary": - return asciiWrite(this, string, offset, length); - case "base64": - return base64Write(this, string, offset, length); - case "ucs2": - case "ucs-2": - case "utf16le": - case "utf-16le": - return ucs2Write(this, string, offset, length); - default: - if (loweredCase) throw new TypeError("Unknown encoding: " + encoding); - encoding = ("" + encoding).toLowerCase(); - loweredCase = true; + ); + return found; + }; + var trySlices = function tryAllSlices(value) { + var found = false; + forEach( + /** @type {Record<\`\\$\${import('.').TypedArrayName}\`, Getter>} */ + cache, + /** @type {(getter: Getter, name: \`\\$\${import('.').TypedArrayName}\`) => void} */ + function(getter, name) { + if (!found) { + try { + getter(value); + found = /** @type {import('.').TypedArrayName} */ + $slice(name, 1); + } catch (e) { + } + } } - } - }; - Buffer2.prototype.toJSON = function toJSON() { - return { - type: "Buffer", - data: Array.prototype.slice.call(this._arr || this, 0) - }; + ); + return found; }; - function base64Slice(buf, start, end) { - if (start === 0 && end === buf.length) { - return base64.fromByteArray(buf); - } else { - return base64.fromByteArray(buf.slice(start, end)); + module2.exports = function whichTypedArray(value) { + if (!value || typeof value !== "object") { + return false; } - } - function utf8Slice(buf, start, end) { - end = Math.min(buf.length, end); - var res = []; - var i = start; - while (i < end) { - var firstByte = buf[i]; - var codePoint = null; - var bytesPerSequence = firstByte > 239 ? 4 : firstByte > 223 ? 3 : firstByte > 191 ? 2 : 1; - if (i + bytesPerSequence <= end) { - var secondByte, thirdByte, fourthByte, tempCodePoint; - switch (bytesPerSequence) { - case 1: - if (firstByte < 128) { - codePoint = firstByte; - } - break; - case 2: - secondByte = buf[i + 1]; - if ((secondByte & 192) === 128) { - tempCodePoint = (firstByte & 31) << 6 | secondByte & 63; - if (tempCodePoint > 127) { - codePoint = tempCodePoint; - } - } - break; - case 3: - secondByte = buf[i + 1]; - thirdByte = buf[i + 2]; - if ((secondByte & 192) === 128 && (thirdByte & 192) === 128) { - tempCodePoint = (firstByte & 15) << 12 | (secondByte & 63) << 6 | thirdByte & 63; - if (tempCodePoint > 2047 && (tempCodePoint < 55296 || tempCodePoint > 57343)) { - codePoint = tempCodePoint; - } - } - break; - case 4: - secondByte = buf[i + 1]; - thirdByte = buf[i + 2]; - fourthByte = buf[i + 3]; - if ((secondByte & 192) === 128 && (thirdByte & 192) === 128 && (fourthByte & 192) === 128) { - tempCodePoint = (firstByte & 15) << 18 | (secondByte & 63) << 12 | (thirdByte & 63) << 6 | fourthByte & 63; - if (tempCodePoint > 65535 && tempCodePoint < 1114112) { - codePoint = tempCodePoint; - } - } - } + if (!hasToStringTag) { + var tag = $slice($toString(value), 8, -1); + if ($indexOf(typedArrays, tag) > -1) { + return tag; } - if (codePoint === null) { - codePoint = 65533; - bytesPerSequence = 1; - } else if (codePoint > 65535) { - codePoint -= 65536; - res.push(codePoint >>> 10 & 1023 | 55296); - codePoint = 56320 | codePoint & 1023; + if (tag !== "Object") { + return false; } - res.push(codePoint); - i += bytesPerSequence; + return trySlices(value); } - return decodeCodePointsArray(res); + if (!gOPD) { + return null; + } + return tryTypedArrays(value); + }; + } +}); + +// node_modules/.pnpm/is-typed-array@1.1.15/node_modules/is-typed-array/index.js +var require_is_typed_array = __commonJS({ + "node_modules/.pnpm/is-typed-array@1.1.15/node_modules/is-typed-array/index.js"(exports2, module2) { + "use strict"; + var whichTypedArray = require_which_typed_array(); + module2.exports = function isTypedArray(value) { + return !!whichTypedArray(value); + }; + } +}); + +// node_modules/.pnpm/util@0.12.5/node_modules/util/support/types.js +var require_types = __commonJS({ + "node_modules/.pnpm/util@0.12.5/node_modules/util/support/types.js"(exports2) { + "use strict"; + var isArgumentsObject = require_is_arguments(); + var isGeneratorFunction = require_is_generator_function(); + var whichTypedArray = require_which_typed_array(); + var isTypedArray = require_is_typed_array(); + function uncurryThis(f) { + return f.call.bind(f); } - var MAX_ARGUMENTS_LENGTH = 4096; - function decodeCodePointsArray(codePoints) { - var len = codePoints.length; - if (len <= MAX_ARGUMENTS_LENGTH) { - return String.fromCharCode.apply(String, codePoints); + var BigIntSupported = typeof BigInt !== "undefined"; + var SymbolSupported = typeof Symbol !== "undefined"; + var ObjectToString = uncurryThis(Object.prototype.toString); + var numberValue = uncurryThis(Number.prototype.valueOf); + var stringValue = uncurryThis(String.prototype.valueOf); + var booleanValue = uncurryThis(Boolean.prototype.valueOf); + if (BigIntSupported) { + bigIntValue = uncurryThis(BigInt.prototype.valueOf); + } + var bigIntValue; + if (SymbolSupported) { + symbolValue = uncurryThis(Symbol.prototype.valueOf); + } + var symbolValue; + function checkBoxedPrimitive(value, prototypeValueOf) { + if (typeof value !== "object") { + return false; } - var res = ""; - var i = 0; - while (i < len) { - res += String.fromCharCode.apply( - String, - codePoints.slice(i, i += MAX_ARGUMENTS_LENGTH) - ); + try { + prototypeValueOf(value); + return true; + } catch (e) { + return false; } - return res; } - function asciiSlice(buf, start, end) { - var ret = ""; - end = Math.min(buf.length, end); - for (var i = start; i < end; ++i) { - ret += String.fromCharCode(buf[i] & 127); - } - return ret; + exports2.isArgumentsObject = isArgumentsObject; + exports2.isGeneratorFunction = isGeneratorFunction; + exports2.isTypedArray = isTypedArray; + function isPromise(input) { + return typeof Promise !== "undefined" && input instanceof Promise || input !== null && typeof input === "object" && typeof input.then === "function" && typeof input.catch === "function"; } - function latin1Slice(buf, start, end) { - var ret = ""; - end = Math.min(buf.length, end); - for (var i = start; i < end; ++i) { - ret += String.fromCharCode(buf[i]); + exports2.isPromise = isPromise; + function isArrayBufferView(value) { + if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView) { + return ArrayBuffer.isView(value); } - return ret; + return isTypedArray(value) || isDataView(value); } - function hexSlice(buf, start, end) { - var len = buf.length; - if (!start || start < 0) start = 0; - if (!end || end < 0 || end > len) end = len; - var out = ""; - for (var i = start; i < end; ++i) { - out += hexSliceLookupTable[buf[i]]; - } - return out; + exports2.isArrayBufferView = isArrayBufferView; + function isUint8Array(value) { + return whichTypedArray(value) === "Uint8Array"; } - function utf16leSlice(buf, start, end) { - var bytes = buf.slice(start, end); - var res = ""; - for (var i = 0; i < bytes.length - 1; i += 2) { - res += String.fromCharCode(bytes[i] + bytes[i + 1] * 256); - } - return res; + exports2.isUint8Array = isUint8Array; + function isUint8ClampedArray(value) { + return whichTypedArray(value) === "Uint8ClampedArray"; } - Buffer2.prototype.slice = function slice(start, end) { - var len = this.length; - start = ~~start; - end = end === void 0 ? len : ~~end; - if (start < 0) { - start += len; - if (start < 0) start = 0; - } else if (start > len) { - start = len; - } - if (end < 0) { - end += len; - if (end < 0) end = 0; - } else if (end > len) { - end = len; - } - if (end < start) end = start; - var newBuf = this.subarray(start, end); - Object.setPrototypeOf(newBuf, Buffer2.prototype); - return newBuf; - }; - function checkOffset(offset, ext, length) { - if (offset % 1 !== 0 || offset < 0) throw new RangeError("offset is not uint"); - if (offset + ext > length) throw new RangeError("Trying to access beyond buffer length"); + exports2.isUint8ClampedArray = isUint8ClampedArray; + function isUint16Array(value) { + return whichTypedArray(value) === "Uint16Array"; + } + exports2.isUint16Array = isUint16Array; + function isUint32Array(value) { + return whichTypedArray(value) === "Uint32Array"; + } + exports2.isUint32Array = isUint32Array; + function isInt8Array(value) { + return whichTypedArray(value) === "Int8Array"; + } + exports2.isInt8Array = isInt8Array; + function isInt16Array(value) { + return whichTypedArray(value) === "Int16Array"; + } + exports2.isInt16Array = isInt16Array; + function isInt32Array(value) { + return whichTypedArray(value) === "Int32Array"; + } + exports2.isInt32Array = isInt32Array; + function isFloat32Array(value) { + return whichTypedArray(value) === "Float32Array"; + } + exports2.isFloat32Array = isFloat32Array; + function isFloat64Array(value) { + return whichTypedArray(value) === "Float64Array"; + } + exports2.isFloat64Array = isFloat64Array; + function isBigInt64Array(value) { + return whichTypedArray(value) === "BigInt64Array"; + } + exports2.isBigInt64Array = isBigInt64Array; + function isBigUint64Array(value) { + return whichTypedArray(value) === "BigUint64Array"; } - Buffer2.prototype.readUintLE = Buffer2.prototype.readUIntLE = function readUIntLE(offset, byteLength2, noAssert) { - offset = offset >>> 0; - byteLength2 = byteLength2 >>> 0; - if (!noAssert) checkOffset(offset, byteLength2, this.length); - var val = this[offset]; - var mul = 1; - var i = 0; - while (++i < byteLength2 && (mul *= 256)) { - val += this[offset + i] * mul; + exports2.isBigUint64Array = isBigUint64Array; + function isMapToString(value) { + return ObjectToString(value) === "[object Map]"; + } + isMapToString.working = typeof Map !== "undefined" && isMapToString(/* @__PURE__ */ new Map()); + function isMap(value) { + if (typeof Map === "undefined") { + return false; } - return val; - }; - Buffer2.prototype.readUintBE = Buffer2.prototype.readUIntBE = function readUIntBE(offset, byteLength2, noAssert) { - offset = offset >>> 0; - byteLength2 = byteLength2 >>> 0; - if (!noAssert) { - checkOffset(offset, byteLength2, this.length); + return isMapToString.working ? isMapToString(value) : value instanceof Map; + } + exports2.isMap = isMap; + function isSetToString(value) { + return ObjectToString(value) === "[object Set]"; + } + isSetToString.working = typeof Set !== "undefined" && isSetToString(/* @__PURE__ */ new Set()); + function isSet(value) { + if (typeof Set === "undefined") { + return false; } - var val = this[offset + --byteLength2]; - var mul = 1; - while (byteLength2 > 0 && (mul *= 256)) { - val += this[offset + --byteLength2] * mul; + return isSetToString.working ? isSetToString(value) : value instanceof Set; + } + exports2.isSet = isSet; + function isWeakMapToString(value) { + return ObjectToString(value) === "[object WeakMap]"; + } + isWeakMapToString.working = typeof WeakMap !== "undefined" && isWeakMapToString(/* @__PURE__ */ new WeakMap()); + function isWeakMap(value) { + if (typeof WeakMap === "undefined") { + return false; } - return val; - }; - Buffer2.prototype.readUint8 = Buffer2.prototype.readUInt8 = function readUInt8(offset, noAssert) { - offset = offset >>> 0; - if (!noAssert) checkOffset(offset, 1, this.length); - return this[offset]; - }; - Buffer2.prototype.readUint16LE = Buffer2.prototype.readUInt16LE = function readUInt16LE(offset, noAssert) { - offset = offset >>> 0; - if (!noAssert) checkOffset(offset, 2, this.length); - return this[offset] | this[offset + 1] << 8; - }; - Buffer2.prototype.readUint16BE = Buffer2.prototype.readUInt16BE = function readUInt16BE(offset, noAssert) { - offset = offset >>> 0; - if (!noAssert) checkOffset(offset, 2, this.length); - return this[offset] << 8 | this[offset + 1]; - }; - Buffer2.prototype.readUint32LE = Buffer2.prototype.readUInt32LE = function readUInt32LE(offset, noAssert) { - offset = offset >>> 0; - if (!noAssert) checkOffset(offset, 4, this.length); - return (this[offset] | this[offset + 1] << 8 | this[offset + 2] << 16) + this[offset + 3] * 16777216; - }; - Buffer2.prototype.readUint32BE = Buffer2.prototype.readUInt32BE = function readUInt32BE(offset, noAssert) { - offset = offset >>> 0; - if (!noAssert) checkOffset(offset, 4, this.length); - return this[offset] * 16777216 + (this[offset + 1] << 16 | this[offset + 2] << 8 | this[offset + 3]); - }; - Buffer2.prototype.readIntLE = function readIntLE(offset, byteLength2, noAssert) { - offset = offset >>> 0; - byteLength2 = byteLength2 >>> 0; - if (!noAssert) checkOffset(offset, byteLength2, this.length); - var val = this[offset]; - var mul = 1; - var i = 0; - while (++i < byteLength2 && (mul *= 256)) { - val += this[offset + i] * mul; + return isWeakMapToString.working ? isWeakMapToString(value) : value instanceof WeakMap; + } + exports2.isWeakMap = isWeakMap; + function isWeakSetToString(value) { + return ObjectToString(value) === "[object WeakSet]"; + } + isWeakSetToString.working = typeof WeakSet !== "undefined" && isWeakSetToString(/* @__PURE__ */ new WeakSet()); + function isWeakSet(value) { + return isWeakSetToString(value); + } + exports2.isWeakSet = isWeakSet; + function isArrayBufferToString(value) { + return ObjectToString(value) === "[object ArrayBuffer]"; + } + isArrayBufferToString.working = typeof ArrayBuffer !== "undefined" && isArrayBufferToString(new ArrayBuffer()); + function isArrayBuffer(value) { + if (typeof ArrayBuffer === "undefined") { + return false; } - mul *= 128; - if (val >= mul) val -= Math.pow(2, 8 * byteLength2); - return val; - }; - Buffer2.prototype.readIntBE = function readIntBE(offset, byteLength2, noAssert) { - offset = offset >>> 0; - byteLength2 = byteLength2 >>> 0; - if (!noAssert) checkOffset(offset, byteLength2, this.length); - var i = byteLength2; - var mul = 1; - var val = this[offset + --i]; - while (i > 0 && (mul *= 256)) { - val += this[offset + --i] * mul; + return isArrayBufferToString.working ? isArrayBufferToString(value) : value instanceof ArrayBuffer; + } + exports2.isArrayBuffer = isArrayBuffer; + function isDataViewToString(value) { + return ObjectToString(value) === "[object DataView]"; + } + isDataViewToString.working = typeof ArrayBuffer !== "undefined" && typeof DataView !== "undefined" && isDataViewToString(new DataView(new ArrayBuffer(1), 0, 1)); + function isDataView(value) { + if (typeof DataView === "undefined") { + return false; } - mul *= 128; - if (val >= mul) val -= Math.pow(2, 8 * byteLength2); - return val; - }; - Buffer2.prototype.readInt8 = function readInt8(offset, noAssert) { - offset = offset >>> 0; - if (!noAssert) checkOffset(offset, 1, this.length); - if (!(this[offset] & 128)) return this[offset]; - return (255 - this[offset] + 1) * -1; - }; - Buffer2.prototype.readInt16LE = function readInt16LE(offset, noAssert) { - offset = offset >>> 0; - if (!noAssert) checkOffset(offset, 2, this.length); - var val = this[offset] | this[offset + 1] << 8; - return val & 32768 ? val | 4294901760 : val; - }; - Buffer2.prototype.readInt16BE = function readInt16BE(offset, noAssert) { - offset = offset >>> 0; - if (!noAssert) checkOffset(offset, 2, this.length); - var val = this[offset + 1] | this[offset] << 8; - return val & 32768 ? val | 4294901760 : val; - }; - Buffer2.prototype.readInt32LE = function readInt32LE(offset, noAssert) { - offset = offset >>> 0; - if (!noAssert) checkOffset(offset, 4, this.length); - return this[offset] | this[offset + 1] << 8 | this[offset + 2] << 16 | this[offset + 3] << 24; - }; - Buffer2.prototype.readInt32BE = function readInt32BE(offset, noAssert) { - offset = offset >>> 0; - if (!noAssert) checkOffset(offset, 4, this.length); - return this[offset] << 24 | this[offset + 1] << 16 | this[offset + 2] << 8 | this[offset + 3]; - }; - Buffer2.prototype.readFloatLE = function readFloatLE(offset, noAssert) { - offset = offset >>> 0; - if (!noAssert) checkOffset(offset, 4, this.length); - return ieee754.read(this, offset, true, 23, 4); - }; - Buffer2.prototype.readFloatBE = function readFloatBE(offset, noAssert) { - offset = offset >>> 0; - if (!noAssert) checkOffset(offset, 4, this.length); - return ieee754.read(this, offset, false, 23, 4); - }; - Buffer2.prototype.readDoubleLE = function readDoubleLE(offset, noAssert) { - offset = offset >>> 0; - if (!noAssert) checkOffset(offset, 8, this.length); - return ieee754.read(this, offset, true, 52, 8); - }; - Buffer2.prototype.readDoubleBE = function readDoubleBE(offset, noAssert) { - offset = offset >>> 0; - if (!noAssert) checkOffset(offset, 8, this.length); - return ieee754.read(this, offset, false, 52, 8); - }; - function checkInt(buf, value, offset, ext, max, min) { - if (!Buffer2.isBuffer(buf)) throw new TypeError('"buffer" argument must be a Buffer instance'); - if (value > max || value < min) throw new RangeError('"value" argument is out of bounds'); - if (offset + ext > buf.length) throw new RangeError("Index out of range"); + return isDataViewToString.working ? isDataViewToString(value) : value instanceof DataView; } - Buffer2.prototype.writeUintLE = Buffer2.prototype.writeUIntLE = function writeUIntLE(value, offset, byteLength2, noAssert) { - value = +value; - offset = offset >>> 0; - byteLength2 = byteLength2 >>> 0; - if (!noAssert) { - var maxBytes = Math.pow(2, 8 * byteLength2) - 1; - checkInt(this, value, offset, byteLength2, maxBytes, 0); + exports2.isDataView = isDataView; + var SharedArrayBufferCopy = typeof SharedArrayBuffer !== "undefined" ? SharedArrayBuffer : void 0; + function isSharedArrayBufferToString(value) { + return ObjectToString(value) === "[object SharedArrayBuffer]"; + } + function isSharedArrayBuffer(value) { + if (typeof SharedArrayBufferCopy === "undefined") { + return false; } - var mul = 1; - var i = 0; - this[offset] = value & 255; - while (++i < byteLength2 && (mul *= 256)) { - this[offset + i] = value / mul & 255; + if (typeof isSharedArrayBufferToString.working === "undefined") { + isSharedArrayBufferToString.working = isSharedArrayBufferToString(new SharedArrayBufferCopy()); } - return offset + byteLength2; + return isSharedArrayBufferToString.working ? isSharedArrayBufferToString(value) : value instanceof SharedArrayBufferCopy; + } + exports2.isSharedArrayBuffer = isSharedArrayBuffer; + function isAsyncFunction(value) { + return ObjectToString(value) === "[object AsyncFunction]"; + } + exports2.isAsyncFunction = isAsyncFunction; + function isMapIterator(value) { + return ObjectToString(value) === "[object Map Iterator]"; + } + exports2.isMapIterator = isMapIterator; + function isSetIterator(value) { + return ObjectToString(value) === "[object Set Iterator]"; + } + exports2.isSetIterator = isSetIterator; + function isGeneratorObject(value) { + return ObjectToString(value) === "[object Generator]"; + } + exports2.isGeneratorObject = isGeneratorObject; + function isWebAssemblyCompiledModule(value) { + return ObjectToString(value) === "[object WebAssembly.Module]"; + } + exports2.isWebAssemblyCompiledModule = isWebAssemblyCompiledModule; + function isNumberObject(value) { + return checkBoxedPrimitive(value, numberValue); + } + exports2.isNumberObject = isNumberObject; + function isStringObject(value) { + return checkBoxedPrimitive(value, stringValue); + } + exports2.isStringObject = isStringObject; + function isBooleanObject(value) { + return checkBoxedPrimitive(value, booleanValue); + } + exports2.isBooleanObject = isBooleanObject; + function isBigIntObject(value) { + return BigIntSupported && checkBoxedPrimitive(value, bigIntValue); + } + exports2.isBigIntObject = isBigIntObject; + function isSymbolObject(value) { + return SymbolSupported && checkBoxedPrimitive(value, symbolValue); + } + exports2.isSymbolObject = isSymbolObject; + function isBoxedPrimitive(value) { + return isNumberObject(value) || isStringObject(value) || isBooleanObject(value) || isBigIntObject(value) || isSymbolObject(value); + } + exports2.isBoxedPrimitive = isBoxedPrimitive; + function isAnyArrayBuffer(value) { + return typeof Uint8Array !== "undefined" && (isArrayBuffer(value) || isSharedArrayBuffer(value)); + } + exports2.isAnyArrayBuffer = isAnyArrayBuffer; + ["isProxy", "isExternal", "isModuleNamespaceObject"].forEach(function(method) { + Object.defineProperty(exports2, method, { + enumerable: false, + value: function() { + throw new Error(method + " is not supported in userland"); + } + }); + }); + } +}); + +// node_modules/.pnpm/util@0.12.5/node_modules/util/support/isBufferBrowser.js +var require_isBufferBrowser = __commonJS({ + "node_modules/.pnpm/util@0.12.5/node_modules/util/support/isBufferBrowser.js"(exports2, module2) { + module2.exports = function isBuffer(arg) { + return arg && typeof arg === "object" && typeof arg.copy === "function" && typeof arg.fill === "function" && typeof arg.readUInt8 === "function"; }; - Buffer2.prototype.writeUintBE = Buffer2.prototype.writeUIntBE = function writeUIntBE(value, offset, byteLength2, noAssert) { - value = +value; - offset = offset >>> 0; - byteLength2 = byteLength2 >>> 0; - if (!noAssert) { - var maxBytes = Math.pow(2, 8 * byteLength2) - 1; - checkInt(this, value, offset, byteLength2, maxBytes, 0); - } - var i = byteLength2 - 1; - var mul = 1; - this[offset + i] = value & 255; - while (--i >= 0 && (mul *= 256)) { - this[offset + i] = value / mul & 255; + } +}); + +// node_modules/.pnpm/inherits@2.0.4/node_modules/inherits/inherits_browser.js +var require_inherits_browser = __commonJS({ + "node_modules/.pnpm/inherits@2.0.4/node_modules/inherits/inherits_browser.js"(exports2, module2) { + if (typeof Object.create === "function") { + module2.exports = function inherits(ctor, superCtor) { + if (superCtor) { + ctor.super_ = superCtor; + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); + } + }; + } else { + module2.exports = function inherits(ctor, superCtor) { + if (superCtor) { + ctor.super_ = superCtor; + var TempCtor = function() { + }; + TempCtor.prototype = superCtor.prototype; + ctor.prototype = new TempCtor(); + ctor.prototype.constructor = ctor; + } + }; + } + } +}); + +// node_modules/.pnpm/util@0.12.5/node_modules/util/util.js +var require_util = __commonJS({ + "node_modules/.pnpm/util@0.12.5/node_modules/util/util.js"(exports2) { + var getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors || function getOwnPropertyDescriptors2(obj) { + var keys = Object.keys(obj); + var descriptors = {}; + for (var i = 0; i < keys.length; i++) { + descriptors[keys[i]] = Object.getOwnPropertyDescriptor(obj, keys[i]); } - return offset + byteLength2; - }; - Buffer2.prototype.writeUint8 = Buffer2.prototype.writeUInt8 = function writeUInt8(value, offset, noAssert) { - value = +value; - offset = offset >>> 0; - if (!noAssert) checkInt(this, value, offset, 1, 255, 0); - this[offset] = value & 255; - return offset + 1; - }; - Buffer2.prototype.writeUint16LE = Buffer2.prototype.writeUInt16LE = function writeUInt16LE(value, offset, noAssert) { - value = +value; - offset = offset >>> 0; - if (!noAssert) checkInt(this, value, offset, 2, 65535, 0); - this[offset] = value & 255; - this[offset + 1] = value >>> 8; - return offset + 2; - }; - Buffer2.prototype.writeUint16BE = Buffer2.prototype.writeUInt16BE = function writeUInt16BE(value, offset, noAssert) { - value = +value; - offset = offset >>> 0; - if (!noAssert) checkInt(this, value, offset, 2, 65535, 0); - this[offset] = value >>> 8; - this[offset + 1] = value & 255; - return offset + 2; - }; - Buffer2.prototype.writeUint32LE = Buffer2.prototype.writeUInt32LE = function writeUInt32LE(value, offset, noAssert) { - value = +value; - offset = offset >>> 0; - if (!noAssert) checkInt(this, value, offset, 4, 4294967295, 0); - this[offset + 3] = value >>> 24; - this[offset + 2] = value >>> 16; - this[offset + 1] = value >>> 8; - this[offset] = value & 255; - return offset + 4; - }; - Buffer2.prototype.writeUint32BE = Buffer2.prototype.writeUInt32BE = function writeUInt32BE(value, offset, noAssert) { - value = +value; - offset = offset >>> 0; - if (!noAssert) checkInt(this, value, offset, 4, 4294967295, 0); - this[offset] = value >>> 24; - this[offset + 1] = value >>> 16; - this[offset + 2] = value >>> 8; - this[offset + 3] = value & 255; - return offset + 4; + return descriptors; }; - Buffer2.prototype.writeIntLE = function writeIntLE(value, offset, byteLength2, noAssert) { - value = +value; - offset = offset >>> 0; - if (!noAssert) { - var limit = Math.pow(2, 8 * byteLength2 - 1); - checkInt(this, value, offset, byteLength2, limit - 1, -limit); + var formatRegExp = /%[sdj%]/g; + exports2.format = function(f) { + if (!isString(f)) { + var objects = []; + for (var i = 0; i < arguments.length; i++) { + objects.push(inspect(arguments[i])); + } + return objects.join(" "); } - var i = 0; - var mul = 1; - var sub = 0; - this[offset] = value & 255; - while (++i < byteLength2 && (mul *= 256)) { - if (value < 0 && sub === 0 && this[offset + i - 1] !== 0) { - sub = 1; + var i = 1; + var args = arguments; + var len = args.length; + var str = String(f).replace(formatRegExp, function(x2) { + if (x2 === "%%") return "%"; + if (i >= len) return x2; + switch (x2) { + case "%s": + return String(args[i++]); + case "%d": + return Number(args[i++]); + case "%j": + try { + return JSON.stringify(args[i++]); + } catch (_) { + return "[Circular]"; + } + default: + return x2; + } + }); + for (var x = args[i]; i < len; x = args[++i]) { + if (isNull(x) || !isObject(x)) { + str += " " + x; + } else { + str += " " + inspect(x); } - this[offset + i] = (value / mul >> 0) - sub & 255; } - return offset + byteLength2; + return str; }; - Buffer2.prototype.writeIntBE = function writeIntBE(value, offset, byteLength2, noAssert) { - value = +value; - offset = offset >>> 0; - if (!noAssert) { - var limit = Math.pow(2, 8 * byteLength2 - 1); - checkInt(this, value, offset, byteLength2, limit - 1, -limit); + exports2.deprecate = function(fn, msg) { + if (typeof process !== "undefined" && process.noDeprecation === true) { + return fn; } - var i = byteLength2 - 1; - var mul = 1; - var sub = 0; - this[offset + i] = value & 255; - while (--i >= 0 && (mul *= 256)) { - if (value < 0 && sub === 0 && this[offset + i + 1] !== 0) { - sub = 1; - } - this[offset + i] = (value / mul >> 0) - sub & 255; + if (typeof process === "undefined") { + return function() { + return exports2.deprecate(fn, msg).apply(this, arguments); + }; } - return offset + byteLength2; - }; - Buffer2.prototype.writeInt8 = function writeInt8(value, offset, noAssert) { - value = +value; - offset = offset >>> 0; - if (!noAssert) checkInt(this, value, offset, 1, 127, -128); - if (value < 0) value = 255 + value + 1; - this[offset] = value & 255; - return offset + 1; - }; - Buffer2.prototype.writeInt16LE = function writeInt16LE(value, offset, noAssert) { - value = +value; - offset = offset >>> 0; - if (!noAssert) checkInt(this, value, offset, 2, 32767, -32768); - this[offset] = value & 255; - this[offset + 1] = value >>> 8; - return offset + 2; - }; - Buffer2.prototype.writeInt16BE = function writeInt16BE(value, offset, noAssert) { - value = +value; - offset = offset >>> 0; - if (!noAssert) checkInt(this, value, offset, 2, 32767, -32768); - this[offset] = value >>> 8; - this[offset + 1] = value & 255; - return offset + 2; - }; - Buffer2.prototype.writeInt32LE = function writeInt32LE(value, offset, noAssert) { - value = +value; - offset = offset >>> 0; - if (!noAssert) checkInt(this, value, offset, 4, 2147483647, -2147483648); - this[offset] = value & 255; - this[offset + 1] = value >>> 8; - this[offset + 2] = value >>> 16; - this[offset + 3] = value >>> 24; - return offset + 4; - }; - Buffer2.prototype.writeInt32BE = function writeInt32BE(value, offset, noAssert) { - value = +value; - offset = offset >>> 0; - if (!noAssert) checkInt(this, value, offset, 4, 2147483647, -2147483648); - if (value < 0) value = 4294967295 + value + 1; - this[offset] = value >>> 24; - this[offset + 1] = value >>> 16; - this[offset + 2] = value >>> 8; - this[offset + 3] = value & 255; - return offset + 4; - }; - function checkIEEE754(buf, value, offset, ext, max, min) { - if (offset + ext > buf.length) throw new RangeError("Index out of range"); - if (offset < 0) throw new RangeError("Index out of range"); - } - function writeFloat(buf, value, offset, littleEndian, noAssert) { - value = +value; - offset = offset >>> 0; - if (!noAssert) { - checkIEEE754(buf, value, offset, 4, 34028234663852886e22, -34028234663852886e22); + var warned = false; + function deprecated() { + if (!warned) { + if (process.throwDeprecation) { + throw new Error(msg); + } else if (process.traceDeprecation) { + console.trace(msg); + } else { + console.error(msg); + } + warned = true; + } + return fn.apply(this, arguments); } - ieee754.write(buf, value, offset, littleEndian, 23, 4); - return offset + 4; - } - Buffer2.prototype.writeFloatLE = function writeFloatLE(value, offset, noAssert) { - return writeFloat(this, value, offset, true, noAssert); + return deprecated; }; - Buffer2.prototype.writeFloatBE = function writeFloatBE(value, offset, noAssert) { - return writeFloat(this, value, offset, false, noAssert); + var debugs = {}; + var debugEnvRegex = /^$/; + if (process.env.NODE_DEBUG) { + debugEnv = process.env.NODE_DEBUG; + debugEnv = debugEnv.replace(/[|\\\\{}()[\\]^$+?.]/g, "\\\\$&").replace(/\\*/g, ".*").replace(/,/g, "$|^").toUpperCase(); + debugEnvRegex = new RegExp("^" + debugEnv + "$", "i"); + } + var debugEnv; + exports2.debuglog = function(set) { + set = set.toUpperCase(); + if (!debugs[set]) { + if (debugEnvRegex.test(set)) { + var pid = process.pid; + debugs[set] = function() { + var msg = exports2.format.apply(exports2, arguments); + console.error("%s %d: %s", set, pid, msg); + }; + } else { + debugs[set] = function() { + }; + } + } + return debugs[set]; }; - function writeDouble(buf, value, offset, littleEndian, noAssert) { - value = +value; - offset = offset >>> 0; - if (!noAssert) { - checkIEEE754(buf, value, offset, 8, 17976931348623157e292, -17976931348623157e292); + function inspect(obj, opts) { + var ctx = { + seen: [], + stylize: stylizeNoColor + }; + if (arguments.length >= 3) ctx.depth = arguments[2]; + if (arguments.length >= 4) ctx.colors = arguments[3]; + if (isBoolean(opts)) { + ctx.showHidden = opts; + } else if (opts) { + exports2._extend(ctx, opts); } - ieee754.write(buf, value, offset, littleEndian, 52, 8); - return offset + 8; + if (isUndefined(ctx.showHidden)) ctx.showHidden = false; + if (isUndefined(ctx.depth)) ctx.depth = 2; + if (isUndefined(ctx.colors)) ctx.colors = false; + if (isUndefined(ctx.customInspect)) ctx.customInspect = true; + if (ctx.colors) ctx.stylize = stylizeWithColor; + return formatValue(ctx, obj, ctx.depth); } - Buffer2.prototype.writeDoubleLE = function writeDoubleLE(value, offset, noAssert) { - return writeDouble(this, value, offset, true, noAssert); + exports2.inspect = inspect; + inspect.colors = { + "bold": [1, 22], + "italic": [3, 23], + "underline": [4, 24], + "inverse": [7, 27], + "white": [37, 39], + "grey": [90, 39], + "black": [30, 39], + "blue": [34, 39], + "cyan": [36, 39], + "green": [32, 39], + "magenta": [35, 39], + "red": [31, 39], + "yellow": [33, 39] }; - Buffer2.prototype.writeDoubleBE = function writeDoubleBE(value, offset, noAssert) { - return writeDouble(this, value, offset, false, noAssert); + inspect.styles = { + "special": "cyan", + "number": "yellow", + "boolean": "yellow", + "undefined": "grey", + "null": "bold", + "string": "green", + "date": "magenta", + // "name": intentionally not styling + "regexp": "red" }; - Buffer2.prototype.copy = function copy(target, targetStart, start, end) { - if (!Buffer2.isBuffer(target)) throw new TypeError("argument should be a Buffer"); - if (!start) start = 0; - if (!end && end !== 0) end = this.length; - if (targetStart >= target.length) targetStart = target.length; - if (!targetStart) targetStart = 0; - if (end > 0 && end < start) end = start; - if (end === start) return 0; - if (target.length === 0 || this.length === 0) return 0; - if (targetStart < 0) { - throw new RangeError("targetStart out of bounds"); + function stylizeWithColor(str, styleType) { + var style = inspect.styles[styleType]; + if (style) { + return "\\x1B[" + inspect.colors[style][0] + "m" + str + "\\x1B[" + inspect.colors[style][1] + "m"; + } else { + return str; } - if (start < 0 || start >= this.length) throw new RangeError("Index out of range"); - if (end < 0) throw new RangeError("sourceEnd out of bounds"); - if (end > this.length) end = this.length; - if (target.length - targetStart < end - start) { - end = target.length - targetStart + start; + } + function stylizeNoColor(str, styleType) { + return str; + } + function arrayToHash(array) { + var hash = {}; + array.forEach(function(val, idx) { + hash[val] = true; + }); + return hash; + } + function formatValue(ctx, value, recurseTimes) { + if (ctx.customInspect && value && isFunction(value.inspect) && // Filter out the util module, it's inspect function is special + value.inspect !== exports2.inspect && // Also filter out any prototype objects using the circular check. + !(value.constructor && value.constructor.prototype === value)) { + var ret = value.inspect(recurseTimes, ctx); + if (!isString(ret)) { + ret = formatValue(ctx, ret, recurseTimes); + } + return ret; } - var len = end - start; - if (this === target && typeof Uint8Array.prototype.copyWithin === "function") { - this.copyWithin(targetStart, start, end); + var primitive = formatPrimitive(ctx, value); + if (primitive) { + return primitive; + } + var keys = Object.keys(value); + var visibleKeys = arrayToHash(keys); + if (ctx.showHidden) { + keys = Object.getOwnPropertyNames(value); + } + if (isError(value) && (keys.indexOf("message") >= 0 || keys.indexOf("description") >= 0)) { + return formatError(value); + } + if (keys.length === 0) { + if (isFunction(value)) { + var name = value.name ? ": " + value.name : ""; + return ctx.stylize("[Function" + name + "]", "special"); + } + if (isRegExp(value)) { + return ctx.stylize(RegExp.prototype.toString.call(value), "regexp"); + } + if (isDate(value)) { + return ctx.stylize(Date.prototype.toString.call(value), "date"); + } + if (isError(value)) { + return formatError(value); + } + } + var base = "", array = false, braces = ["{", "}"]; + if (isArray(value)) { + array = true; + braces = ["[", "]"]; + } + if (isFunction(value)) { + var n = value.name ? ": " + value.name : ""; + base = " [Function" + n + "]"; + } + if (isRegExp(value)) { + base = " " + RegExp.prototype.toString.call(value); + } + if (isDate(value)) { + base = " " + Date.prototype.toUTCString.call(value); + } + if (isError(value)) { + base = " " + formatError(value); + } + if (keys.length === 0 && (!array || value.length == 0)) { + return braces[0] + base + braces[1]; + } + if (recurseTimes < 0) { + if (isRegExp(value)) { + return ctx.stylize(RegExp.prototype.toString.call(value), "regexp"); + } else { + return ctx.stylize("[Object]", "special"); + } + } + ctx.seen.push(value); + var output; + if (array) { + output = formatArray(ctx, value, recurseTimes, visibleKeys, keys); } else { - Uint8Array.prototype.set.call( - target, - this.subarray(start, end), - targetStart - ); + output = keys.map(function(key) { + return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array); + }); } - return len; - }; - Buffer2.prototype.fill = function fill(val, start, end, encoding) { - if (typeof val === "string") { - if (typeof start === "string") { - encoding = start; - start = 0; - end = this.length; - } else if (typeof end === "string") { - encoding = end; - end = this.length; + ctx.seen.pop(); + return reduceToSingleString(output, base, braces); + } + function formatPrimitive(ctx, value) { + if (isUndefined(value)) + return ctx.stylize("undefined", "undefined"); + if (isString(value)) { + var simple = "'" + JSON.stringify(value).replace(/^"|"$/g, "").replace(/'/g, "\\\\'").replace(/\\\\"/g, '"') + "'"; + return ctx.stylize(simple, "string"); + } + if (isNumber(value)) + return ctx.stylize("" + value, "number"); + if (isBoolean(value)) + return ctx.stylize("" + value, "boolean"); + if (isNull(value)) + return ctx.stylize("null", "null"); + } + function formatError(value) { + return "[" + Error.prototype.toString.call(value) + "]"; + } + function formatArray(ctx, value, recurseTimes, visibleKeys, keys) { + var output = []; + for (var i = 0, l = value.length; i < l; ++i) { + if (hasOwnProperty(value, String(i))) { + output.push(formatProperty( + ctx, + value, + recurseTimes, + visibleKeys, + String(i), + true + )); + } else { + output.push(""); } - if (encoding !== void 0 && typeof encoding !== "string") { - throw new TypeError("encoding must be a string"); + } + keys.forEach(function(key) { + if (!key.match(/^\\d+$/)) { + output.push(formatProperty( + ctx, + value, + recurseTimes, + visibleKeys, + key, + true + )); } - if (typeof encoding === "string" && !Buffer2.isEncoding(encoding)) { - throw new TypeError("Unknown encoding: " + encoding); + }); + return output; + } + function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) { + var name, str, desc; + desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] }; + if (desc.get) { + if (desc.set) { + str = ctx.stylize("[Getter/Setter]", "special"); + } else { + str = ctx.stylize("[Getter]", "special"); } - if (val.length === 1) { - var code = val.charCodeAt(0); - if (encoding === "utf8" && code < 128 || encoding === "latin1") { - val = code; + } else { + if (desc.set) { + str = ctx.stylize("[Setter]", "special"); + } + } + if (!hasOwnProperty(visibleKeys, key)) { + name = "[" + key + "]"; + } + if (!str) { + if (ctx.seen.indexOf(desc.value) < 0) { + if (isNull(recurseTimes)) { + str = formatValue(ctx, desc.value, null); + } else { + str = formatValue(ctx, desc.value, recurseTimes - 1); + } + if (str.indexOf("\\n") > -1) { + if (array) { + str = str.split("\\n").map(function(line) { + return " " + line; + }).join("\\n").slice(2); + } else { + str = "\\n" + str.split("\\n").map(function(line) { + return " " + line; + }).join("\\n"); + } } + } else { + str = ctx.stylize("[Circular]", "special"); } - } else if (typeof val === "number") { - val = val & 255; - } else if (typeof val === "boolean") { - val = Number(val); } - if (start < 0 || this.length < start || this.length < end) { - throw new RangeError("Out of range index"); + if (isUndefined(name)) { + if (array && key.match(/^\\d+$/)) { + return str; + } + name = JSON.stringify("" + key); + if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { + name = name.slice(1, -1); + name = ctx.stylize(name, "name"); + } else { + name = name.replace(/'/g, "\\\\'").replace(/\\\\"/g, '"').replace(/(^"|"$)/g, "'"); + name = ctx.stylize(name, "string"); + } } - if (end <= start) { - return this; + return name + ": " + str; + } + function reduceToSingleString(output, base, braces) { + var numLinesEst = 0; + var length = output.reduce(function(prev, cur) { + numLinesEst++; + if (cur.indexOf("\\n") >= 0) numLinesEst++; + return prev + cur.replace(/\\u001b\\[\\d\\d?m/g, "").length + 1; + }, 0); + if (length > 60) { + return braces[0] + (base === "" ? "" : base + "\\n ") + " " + output.join(",\\n ") + " " + braces[1]; } - start = start >>> 0; - end = end === void 0 ? this.length : end >>> 0; - if (!val) val = 0; - var i; - if (typeof val === "number") { - for (i = start; i < end; ++i) { - this[i] = val; + return braces[0] + base + " " + output.join(", ") + " " + braces[1]; + } + exports2.types = require_types(); + function isArray(ar) { + return Array.isArray(ar); + } + exports2.isArray = isArray; + function isBoolean(arg) { + return typeof arg === "boolean"; + } + exports2.isBoolean = isBoolean; + function isNull(arg) { + return arg === null; + } + exports2.isNull = isNull; + function isNullOrUndefined(arg) { + return arg == null; + } + exports2.isNullOrUndefined = isNullOrUndefined; + function isNumber(arg) { + return typeof arg === "number"; + } + exports2.isNumber = isNumber; + function isString(arg) { + return typeof arg === "string"; + } + exports2.isString = isString; + function isSymbol(arg) { + return typeof arg === "symbol"; + } + exports2.isSymbol = isSymbol; + function isUndefined(arg) { + return arg === void 0; + } + exports2.isUndefined = isUndefined; + function isRegExp(re) { + return isObject(re) && objectToString(re) === "[object RegExp]"; + } + exports2.isRegExp = isRegExp; + exports2.types.isRegExp = isRegExp; + function isObject(arg) { + return typeof arg === "object" && arg !== null; + } + exports2.isObject = isObject; + function isDate(d) { + return isObject(d) && objectToString(d) === "[object Date]"; + } + exports2.isDate = isDate; + exports2.types.isDate = isDate; + function isError(e) { + return isObject(e) && (objectToString(e) === "[object Error]" || e instanceof Error); + } + exports2.isError = isError; + exports2.types.isNativeError = isError; + function isFunction(arg) { + return typeof arg === "function"; + } + exports2.isFunction = isFunction; + function isPrimitive(arg) { + return arg === null || typeof arg === "boolean" || typeof arg === "number" || typeof arg === "string" || typeof arg === "symbol" || // ES6 symbol + typeof arg === "undefined"; + } + exports2.isPrimitive = isPrimitive; + exports2.isBuffer = require_isBufferBrowser(); + function objectToString(o) { + return Object.prototype.toString.call(o); + } + function pad(n) { + return n < 10 ? "0" + n.toString(10) : n.toString(10); + } + var months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec" + ]; + function timestamp() { + var d = /* @__PURE__ */ new Date(); + var time = [ + pad(d.getHours()), + pad(d.getMinutes()), + pad(d.getSeconds()) + ].join(":"); + return [d.getDate(), months[d.getMonth()], time].join(" "); + } + exports2.log = function() { + console.log("%s - %s", timestamp(), exports2.format.apply(exports2, arguments)); + }; + exports2.inherits = require_inherits_browser(); + exports2._extend = function(origin, add) { + if (!add || !isObject(add)) return origin; + var keys = Object.keys(add); + var i = keys.length; + while (i--) { + origin[keys[i]] = add[keys[i]]; + } + return origin; + }; + function hasOwnProperty(obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop); + } + var kCustomPromisifiedSymbol = typeof Symbol !== "undefined" ? /* @__PURE__ */ Symbol("util.promisify.custom") : void 0; + exports2.promisify = function promisify(original) { + if (typeof original !== "function") + throw new TypeError('The "original" argument must be of type Function'); + if (kCustomPromisifiedSymbol && original[kCustomPromisifiedSymbol]) { + var fn = original[kCustomPromisifiedSymbol]; + if (typeof fn !== "function") { + throw new TypeError('The "util.promisify.custom" argument must be of type Function'); } - } else { - var bytes = Buffer2.isBuffer(val) ? val : Buffer2.from(val, encoding); - var len = bytes.length; - if (len === 0) { - throw new TypeError('The value "' + val + '" is invalid for argument "value"'); + Object.defineProperty(fn, kCustomPromisifiedSymbol, { + value: fn, + enumerable: false, + writable: false, + configurable: true + }); + return fn; + } + function fn() { + var promiseResolve, promiseReject; + var promise = new Promise(function(resolve, reject) { + promiseResolve = resolve; + promiseReject = reject; + }); + var args = []; + for (var i = 0; i < arguments.length; i++) { + args.push(arguments[i]); } - for (i = 0; i < end - start; ++i) { - this[i + start] = bytes[i % len]; + args.push(function(err, value) { + if (err) { + promiseReject(err); + } else { + promiseResolve(value); + } + }); + try { + original.apply(this, args); + } catch (err) { + promiseReject(err); } + return promise; } - return this; + Object.setPrototypeOf(fn, Object.getPrototypeOf(original)); + if (kCustomPromisifiedSymbol) Object.defineProperty(fn, kCustomPromisifiedSymbol, { + value: fn, + enumerable: false, + writable: false, + configurable: true + }); + return Object.defineProperties( + fn, + getOwnPropertyDescriptors(original) + ); }; - var INVALID_BASE64_RE = /[^+/0-9A-Za-z-_]/g; - function base64clean(str) { - str = str.split("=")[0]; - str = str.trim().replace(INVALID_BASE64_RE, ""); - if (str.length < 2) return ""; - while (str.length % 4 !== 0) { - str = str + "="; + exports2.promisify.custom = kCustomPromisifiedSymbol; + function callbackifyOnRejected(reason, cb) { + if (!reason) { + var newReason = new Error("Promise was rejected with a falsy value"); + newReason.reason = reason; + reason = newReason; } - return str; + return cb(reason); } - function utf8ToBytes(string, units) { - units = units || Infinity; - var codePoint; - var length = string.length; - var leadSurrogate = null; - var bytes = []; - for (var i = 0; i < length; ++i) { - codePoint = string.charCodeAt(i); - if (codePoint > 55295 && codePoint < 57344) { - if (!leadSurrogate) { - if (codePoint > 56319) { - if ((units -= 3) > -1) bytes.push(239, 191, 189); - continue; - } else if (i + 1 === length) { - if ((units -= 3) > -1) bytes.push(239, 191, 189); - continue; - } - leadSurrogate = codePoint; - continue; - } - if (codePoint < 56320) { - if ((units -= 3) > -1) bytes.push(239, 191, 189); - leadSurrogate = codePoint; - continue; - } - codePoint = (leadSurrogate - 55296 << 10 | codePoint - 56320) + 65536; - } else if (leadSurrogate) { - if ((units -= 3) > -1) bytes.push(239, 191, 189); + function callbackify(original) { + if (typeof original !== "function") { + throw new TypeError('The "original" argument must be of type Function'); + } + function callbackified() { + var args = []; + for (var i = 0; i < arguments.length; i++) { + args.push(arguments[i]); } - leadSurrogate = null; - if (codePoint < 128) { - if ((units -= 1) < 0) break; - bytes.push(codePoint); - } else if (codePoint < 2048) { - if ((units -= 2) < 0) break; - bytes.push( - codePoint >> 6 | 192, - codePoint & 63 | 128 - ); - } else if (codePoint < 65536) { - if ((units -= 3) < 0) break; - bytes.push( - codePoint >> 12 | 224, - codePoint >> 6 & 63 | 128, - codePoint & 63 | 128 - ); - } else if (codePoint < 1114112) { - if ((units -= 4) < 0) break; - bytes.push( - codePoint >> 18 | 240, - codePoint >> 12 & 63 | 128, - codePoint >> 6 & 63 | 128, - codePoint & 63 | 128 - ); - } else { - throw new Error("Invalid code point"); + var maybeCb = args.pop(); + if (typeof maybeCb !== "function") { + throw new TypeError("The last argument must be of type Function"); } + var self = this; + var cb = function() { + return maybeCb.apply(self, arguments); + }; + original.apply(this, args).then( + function(ret) { + process.nextTick(cb.bind(null, null, ret)); + }, + function(rej) { + process.nextTick(callbackifyOnRejected.bind(null, rej, cb)); + } + ); } - return bytes; + Object.setPrototypeOf(callbackified, Object.getPrototypeOf(original)); + Object.defineProperties( + callbackified, + getOwnPropertyDescriptors(original) + ); + return callbackified; } - function asciiToBytes(str) { - var byteArray = []; - for (var i = 0; i < str.length; ++i) { - byteArray.push(str.charCodeAt(i) & 255); + exports2.callbackify = callbackify; + } +}); + +// +var util = require_util(); +module.exports = util.default ?? util; + +function installBuiltinUtilFormatWithOptions(builtinUtilModule) { + if (!builtinUtilModule || typeof builtinUtilModule.formatWithOptions === "function") { + return builtinUtilModule; + } + builtinUtilModule.formatWithOptions = function formatWithOptions(inspectOptions, format, ...args) { + const inspectValue = (value) => { + if (typeof builtinUtilModule.inspect === "function") { + return builtinUtilModule.inspect(value, inspectOptions); } - return byteArray; - } - function utf16leToBytes(str, units) { - var c, hi, lo; - var byteArray = []; - for (var i = 0; i < str.length; ++i) { - if ((units -= 2) < 0) break; - c = str.charCodeAt(i); - hi = c >> 8; - lo = c % 256; - byteArray.push(lo); - byteArray.push(hi); + try { + return JSON.stringify(value); + } catch { + return String(value); } - return byteArray; - } - function base64ToBytes(str) { - return base64.toByteArray(base64clean(str)); + }; + const formatValue = (value) => typeof value === "string" ? value : inspectValue(value); + if (typeof format !== "string") { + return [format, ...args].map(formatValue).join(" "); } - function blitBuffer(src, dst, offset, length) { - for (var i = 0; i < length; ++i) { - if (i + offset >= dst.length || i >= src.length) break; - dst[i + offset] = src[i]; + let index = 0; + const formatted = format.replace(/%[sdifjoO%]/g, (token) => { + if (token === "%%") { + return "%"; } - return i; + if (index >= args.length) { + return token; + } + const value = args[index++]; + switch (token) { + case "%s": + return String(value); + case "%d": + return Number(value).toString(); + case "%i": + return Number.parseInt(value, 10).toString(); + case "%f": + return Number.parseFloat(value).toString(); + case "%j": + try { + return JSON.stringify(value); + } catch { + return "[Circular]"; + } + case "%o": + case "%O": + return inspectValue(value); + default: + return token; + } + }); + if (index >= args.length) { + return formatted; } - function isInstance(obj, type) { - return obj instanceof type || obj != null && obj.constructor != null && obj.constructor.name != null && obj.constructor.name === type.name; + return [formatted, ...args.slice(index).map(formatValue)].join(" "); + }; + return builtinUtilModule; +} +module.exports = installBuiltinUtilFormatWithOptions(module.exports); +if (module.exports && module.exports.default == null) module.exports.default = module.exports; +`;var En={SIGHUP:1,SIGINT:2,SIGQUIT:3,SIGILL:4,SIGTRAP:5,SIGABRT:6,SIGIOT:6,SIGBUS:7,SIGFPE:8,SIGKILL:9,SIGUSR1:10,SIGSEGV:11,SIGUSR2:12,SIGPIPE:13,SIGALRM:14,SIGTERM:15,SIGSTKFLT:16,SIGCHLD:17,SIGCONT:18,SIGSTOP:19,SIGTSTP:20,SIGTTIN:21,SIGTTOU:22,SIGURG:23,SIGXCPU:24,SIGXFSZ:25,SIGVTALRM:26,SIGPROF:27,SIGWINCH:28,SIGIO:29,SIGPOLL:29,SIGPWR:30,SIGSYS:31},Ex=new Set([0,...Object.values(En)]);function Qu(e){let t=e.trim().toUpperCase(),r=t.startsWith("SIG")?t:`SIG${t}`;return En[r]??null}function Zu(e){return e>0?128+e:null}var ef=` + globalThis.__agentOSWasiHost = { + requireBuiltin: (name) => + globalThis.require(String(name).replace(/^node:/, "")), + syncReadLimitBytes: 16777216, + // Browser fs descriptors are a JS handle table, not real host OS fds with + // a kernel offset, so locally-opened files must use the offset-aware file + // branches (explicit position) rather than host-passthrough null reads. + disableLocalFdPassthrough: true, + // Guest stdin is delivered through the runtime process object, not a kernel + // fd, so read the queued bytes from process.stdin directly. + readStdin: (maxBytes) => + (globalThis.process && + globalThis.process.stdin && + typeof globalThis.process.stdin.read === "function" + ? globalThis.process.stdin.read(maxBytes) + : null), + // Queued stdin byte count for poll_oneoff readiness (does not consume). + stdinReadableBytes: () => + (globalThis.process && globalThis.process.stdin + ? Number(globalThis.process.stdin.readableLength || 0) + : 0), + }; + const Buffer = + (typeof globalThis !== "undefined" && globalThis.Buffer) || + (class __AgentOsWasiBuffer extends Uint8Array { + static alloc(size) { return new __AgentOsWasiBuffer(size >>> 0); } + static allocUnsafe(size) { return new __AgentOsWasiBuffer(size >>> 0); } + static isBuffer(value) { return value instanceof Uint8Array; } + static byteLength(value, encoding) { + if (value instanceof Uint8Array) return value.length; + if (encoding === "base64") return Math.floor((String(value).replace(/=+$/, "").length * 3) / 4); + if (encoding === "hex") return String(value).length >> 1; + return new TextEncoder().encode(String(value)).length; + } + static from(value, encodingOrOffset, length) { + if (typeof value === "string") { + const encoding = encodingOrOffset || "utf8"; + if (encoding === "base64") { + const binary = atob(value); + const out = new __AgentOsWasiBuffer(binary.length); + for (let i = 0; i < binary.length; i += 1) out[i] = binary.charCodeAt(i) & 0xff; + return out; + } + if (encoding === "hex") { + const clean = String(value); + const out = new __AgentOsWasiBuffer(clean.length >> 1); + for (let i = 0; i < out.length; i += 1) out[i] = parseInt(clean.substr(i * 2, 2), 16); + return out; + } + const encoded = new TextEncoder().encode(value); + const out = new __AgentOsWasiBuffer(encoded.length); + out.set(encoded); + return out; + } + if (value instanceof ArrayBuffer) { + const offset = encodingOrOffset || 0; + const len = length === undefined ? value.byteLength - offset : length; + const view = new Uint8Array(value, offset, len); + const out = new __AgentOsWasiBuffer(view.length); + out.set(view); + return out; + } + if (ArrayBuffer.isView(value)) { + const view = new Uint8Array(value.buffer, value.byteOffset, value.byteLength); + const out = new __AgentOsWasiBuffer(view.length); + out.set(view); + return out; + } + const arr = Array.from(value || []); + const out = new __AgentOsWasiBuffer(arr.length); + for (let i = 0; i < arr.length; i += 1) out[i] = arr[i] & 0xff; + return out; + } + static concat(list, totalLength) { + const chunks = Array.from(list || []); + if (totalLength === undefined) { + totalLength = 0; + for (const chunk of chunks) totalLength += chunk.length; + } + const out = new __AgentOsWasiBuffer(totalLength >>> 0); + let offset = 0; + for (const chunk of chunks) { + if (offset >= out.length) break; + const slice = offset + chunk.length > out.length ? chunk.subarray(0, out.length - offset) : chunk; + out.set(slice, offset); + offset += slice.length; + } + return out; + } + toString(encoding, start, end) { + const view = this.subarray(start || 0, end === undefined ? this.length : end); + if (encoding === "base64") { + let binary = ""; + for (let i = 0; i < view.length; i += 1) binary += String.fromCharCode(view[i]); + return btoa(binary); + } + if (encoding === "hex") { + let hex = ""; + for (let i = 0; i < view.length; i += 1) hex += view[i].toString(16).padStart(2, "0"); + return hex; + } + return new TextDecoder().decode(view); + } + }); +if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule === "undefined") { + // Per-backend host seam (C / convergence): native populates it from its own + // host globals (the \`|| __agentOs*\` fallbacks below); a non-native backend + // (the browser converged worker) can pre-set \`globalThis.__agentOSWasiHost\` + // with browser-provided equivalents so this same preview1 runner is shared. + const __agentOSWasiHost = + (typeof globalThis.__agentOSWasiHost === "object" && + globalThis.__agentOSWasiHost) || + {}; + const __agentOSWasiRequireBuiltin = + __agentOSWasiHost.requireBuiltin || + (typeof __agentOSRequireBuiltin !== "undefined" + ? __agentOSRequireBuiltin + : (name) => globalThis.require(name)); + const __agentOSFs = () => __agentOSWasiRequireBuiltin("node:fs"); + const __agentOSPath = () => __agentOSWasiRequireBuiltin("node:path"); + const __agentOSCrypto = () => __agentOSWasiRequireBuiltin("node:crypto"); + // Stdio sync-RPC bridge + fd-handle lookup come from the host seam (a + // non-native backend supplies browser equivalents); native falls back to its + // own host globals so behavior is unchanged. + // Lazy resolvers: the native host globals are populated AFTER this module is + // defined (per-execution), so resolve at call time, not at module-load. + const __agentOSWasiSyncRpc = () => + __agentOSWasiHost.syncRpc || + (typeof globalThis.__agentOSSyncRpc !== "undefined" + ? globalThis.__agentOSSyncRpc + : undefined); + const __agentOSWasiLookupFdHandle = () => + __agentOSWasiHost.lookupFdHandle || + (typeof globalThis.lookupFdHandle === "function" + ? globalThis.lookupFdHandle + : undefined); + const __agentOSWasiErrnoSuccess = 0; + const __agentOSWasiErrnoAcces = 2; + const __agentOSWasiErrnoBadf = 8; + const __agentOSWasiErrnoExist = 20; + const __agentOSWasiErrnoFault = 21; + const __agentOSWasiErrnoInval = 28; + const __agentOSWasiErrnoIo = 29; + const __agentOSWasiErrnoNoent = 44; + const __agentOSWasiErrnoNosys = 52; + const __agentOSWasiErrnoNotdir = 54; + const __agentOSWasiErrnoNotempty = 55; + const __agentOSWasiErrnoPipe = 64; + const __agentOSWasiErrnoRofs = 69; + const __agentOSWasiErrnoNotcapable = 76; + const __agentOSWasiErrnoXdev = 18; + const __agentOSWasiFiletypeUnknown = 0; + const __agentOSWasiFiletypeCharacterDevice = 2; + const __agentOSWasiFiletypeDirectory = 3; + const __agentOSWasiFiletypeRegularFile = 4; + const __agentOSWasiFiletypeSymbolicLink = 7; + const __agentOSWasiLookupSymlinkFollow = 1; + const __agentOSWasiOpenCreate = 1; + const __agentOSWasiOpenDirectory = 2; + const __agentOSWasiOpenExclusive = 4; + const __agentOSWasiOpenTruncate = 8; + const __agentOSWasiFdflagsAppend = 1; + const __agentOSWasiRightFdRead = 1n << 1n; + const __agentOSWasiRightFdWrite = 1n << 6n; + const __agentOSWasiDefaultRightsBase = 0xffffffffffffffffn; + const __agentOSWasiDefaultRightsInheriting = 0xffffffffffffffffn; + const __agentOSWasiWhenceSet = 0; + const __agentOSWasiWhenceCur = 1; + const __agentOSWasiWhenceEnd = 2; + // Read cap: a non-native backend provides it via the seam; native uses its + // build-substituted constant. The ternary short-circuits so the native-only + // placeholder token is never evaluated when the seam supplies a number. + const __agentOSWasmSyncReadLimitBytes = + typeof __agentOSWasiHost.syncReadLimitBytes === "number" + ? __agentOSWasiHost.syncReadLimitBytes + : 16777216; + const __agentOSKernelStdioSyncRpcEnabled = () => + process?.env?.AGENTOS_WASI_STDIO_SYNC_RPC === "1"; + const __agentOSWasiDebugEnabled = () => process?.env?.AGENTOS_WASM_WASI_DEBUG === "1"; + const __agentOSWasiSyscallCountersEnabled = () => + process?.env?.AGENTOS_WASI_SYSCALL_COUNTERS === "1"; + const __agentOSWasiNow = () => + typeof performance?.now === "function" ? performance.now() : Date.now(); + const __agentOSWasiDebug = (message) => { + if (!__agentOSWasiDebugEnabled() || typeof process?.stderr?.write !== "function") { + return; } - function numberIsNaN(obj) { - return obj !== obj; + try { + process.stderr.write(\`[secure-exec-wasi] \${message}\\n\`); + } catch { + // Ignore debug logging failures. } - var hexSliceLookupTable = (function() { - var alphabet = "0123456789abcdef"; - var table = new Array(256); - for (var i = 0; i < 16; ++i) { - var i16 = i * 16; - for (var j = 0; j < 16; ++j) { - table[i16 + j] = alphabet[i] + alphabet[j]; - } - } - return table; - })(); - } -}); - -// -var buffer = require_buffer(); -module.exports = buffer.default ?? buffer; -/*! Bundled license information: - -ieee754/index.js: - (*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh *) - -buffer/index.js: - (*! - * The buffer module from node.js, for the browser. - * - * @author Feross Aboukhadijeh - * @license MIT - *) -*/ - -if (module.exports && module.exports.default == null) module.exports.default = module.exports; -`;var Zu=`var process = globalThis.process || { - env: {}, - cwd: () => '/', -}; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __commonJS = (cb, mod) => function __require() { - return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; -}; + }; -// node_modules/.pnpm/path-browserify@1.0.1/node_modules/path-browserify/index.js -var require_path_browserify = __commonJS({ - "node_modules/.pnpm/path-browserify@1.0.1/node_modules/path-browserify/index.js"(exports2, module2) { - "use strict"; - function assertPath(path2) { - if (typeof path2 !== "string") { - throw new TypeError("Path must be a string. Received " + JSON.stringify(path2)); - } - } - function normalizeStringPosix(path2, allowAboveRoot) { - var res = ""; - var lastSegmentLength = 0; - var lastSlash = -1; - var dots = 0; - var code; - for (var i = 0; i <= path2.length; ++i) { - if (i < path2.length) - code = path2.charCodeAt(i); - else if (code === 47) - break; - else - code = 47; - if (code === 47) { - if (lastSlash === i - 1 || dots === 1) { - } else if (lastSlash !== i - 1 && dots === 2) { - if (res.length < 2 || lastSegmentLength !== 2 || res.charCodeAt(res.length - 1) !== 46 || res.charCodeAt(res.length - 2) !== 46) { - if (res.length > 2) { - var lastSlashIndex = res.lastIndexOf("/"); - if (lastSlashIndex !== res.length - 1) { - if (lastSlashIndex === -1) { - res = ""; - lastSegmentLength = 0; - } else { - res = res.slice(0, lastSlashIndex); - lastSegmentLength = res.length - 1 - res.lastIndexOf("/"); - } - lastSlash = i; - dots = 0; - continue; - } - } else if (res.length === 2 || res.length === 1) { - res = ""; - lastSegmentLength = 0; - lastSlash = i; - dots = 0; - continue; - } - } - if (allowAboveRoot) { - if (res.length > 0) - res += "/.."; - else - res = ".."; - lastSegmentLength = 2; - } - } else { - if (res.length > 0) - res += "/" + path2.slice(lastSlash + 1, i); - else - res = path2.slice(lastSlash + 1, i); - lastSegmentLength = i - lastSlash - 1; - } - lastSlash = i; - dots = 0; - } else if (code === 46 && dots !== -1) { - ++dots; - } else { - dots = -1; + class WASI { + constructor(options = {}) { + this.args = Array.isArray(options.args) ? options.args.map((value) => String(value)) : []; + this.env = + options.env && typeof options.env === "object" + ? Object.fromEntries( + Object.entries(options.env).map(([key, value]) => [String(key), String(value)]), + ) + : {}; + this.preopens = options.preopens && typeof options.preopens === "object" ? options.preopens : {}; + this.returnOnExit = options.returnOnExit === true; + this.instance = null; + this.nextFd = 3; + this.fsModule = null; + this.pathModule = null; + this.fdTable = new Map([ + [0, { kind: "stdin", fdFlags: 0 }], + [1, { kind: "stdout", fdFlags: 0 }], + [2, { kind: "stderr", fdFlags: 0 }], + ]); + this.statCache = new Map(); + this.syscallCountersEnabled = __agentOSWasiSyscallCountersEnabled(); + for (const [guestPath, spec] of Object.entries(this.preopens)) { + const normalized = this._normalizePreopenSpec(spec); + if (!normalized) { + continue; } + this.fdTable.set(this.nextFd++, { + kind: "preopen", + guestPath: String(guestPath), + hostPath: normalized.hostPath, + readOnly: normalized.readOnly, + rightsBase: normalized.rightsBase, + rightsInheriting: normalized.rightsInheriting, + fdFlags: 0, + }); } - return res; + this.wasiImport = { + args_get: (...args) => this._argsGet(...args), + args_sizes_get: (...args) => this._argsSizesGet(...args), + clock_time_get: (...args) => this._clockTimeGet(...args), + clock_res_get: (...args) => this._clockResGet(...args), + environ_get: (...args) => this._environGet(...args), + environ_sizes_get: (...args) => this._environSizesGet(...args), + fd_close: (...args) => this._fdClose(...args), + fd_fdstat_get: (...args) => this._fdFdstatGet(...args), + fd_fdstat_set_flags: (...args) => this._fdFdstatSetFlags(...args), + fd_filestat_get: (...args) => this._fdFilestatGet(...args), + fd_filestat_set_size: (...args) => this._fdFilestatSetSize(...args), + fd_prestat_dir_name: (...args) => this._fdPrestatDirName(...args), + fd_prestat_get: (...args) => this._fdPrestatGet(...args), + fd_pread: (...args) => this._fdPread(...args), + fd_pwrite: (...args) => this._fdPwrite(...args), + fd_readdir: (...args) => this._fdReaddir(...args), + fd_read: (...args) => this._fdRead(...args), + fd_seek: (...args) => this._fdSeek(...args), + fd_sync: (...args) => this._fdSync(...args), + fd_tell: (...args) => this._fdTell(...args), + fd_write: (...args) => this._fdWrite(...args), + path_create_directory: (...args) => this._pathCreateDirectory(...args), + path_filestat_get: (...args) => this._pathFilestatGet(...args), + path_link: (...args) => this._pathLink(...args), + path_open: (...args) => this._pathOpen(...args), + path_readlink: (...args) => this._pathReadlink(...args), + path_remove_directory: (...args) => this._pathRemoveDirectory(...args), + path_rename: (...args) => this._pathRename(...args), + path_symlink: (...args) => this._pathSymlink(...args), + path_unlink_file: (...args) => this._pathUnlinkFile(...args), + poll_oneoff: (...args) => this._pollOneoff(...args), + proc_exit: (...args) => this._procExit(...args), + random_get: (...args) => this._randomGet(...args), + sched_yield: (...args) => this._schedYield(...args), + }; + this._installSyscallCounterWrappers(); } - function _format(sep, pathObject) { - var dir = pathObject.dir || pathObject.root; - var base = pathObject.base || (pathObject.name || "") + (pathObject.ext || ""); - if (!dir) { - return base; + + _fs() { + if (!this.fsModule) { + this.fsModule = __agentOSFs(); } - if (dir === pathObject.root) { - return dir + base; + return this.fsModule; + } + + _path() { + if (!this.pathModule) { + this.pathModule = __agentOSPath(); } - return dir + sep + base; + return this.pathModule; } - var posix2 = { - // path.resolve([from ...], to) - resolve: function resolve() { - var resolvedPath = ""; - var resolvedAbsolute = false; - var cwd; - for (var i = arguments.length - 1; i >= -1 && !resolvedAbsolute; i--) { - var path2; - if (i >= 0) - path2 = arguments[i]; - else { - if (cwd === void 0) - cwd = process.cwd(); - path2 = cwd; - } - assertPath(path2); - if (path2.length === 0) { - continue; - } - resolvedPath = path2 + "/" + resolvedPath; - resolvedAbsolute = path2.charCodeAt(0) === 47; - } - resolvedPath = normalizeStringPosix(resolvedPath, !resolvedAbsolute); - if (resolvedAbsolute) { - if (resolvedPath.length > 0) - return "/" + resolvedPath; - else - return "/"; - } else if (resolvedPath.length > 0) { - return resolvedPath; - } else { - return "."; - } - }, - normalize: function normalize(path2) { - assertPath(path2); - if (path2.length === 0) return "."; - var isAbsolute = path2.charCodeAt(0) === 47; - var trailingSeparator = path2.charCodeAt(path2.length - 1) === 47; - path2 = normalizeStringPosix(path2, !isAbsolute); - if (path2.length === 0 && !isAbsolute) path2 = "."; - if (path2.length > 0 && trailingSeparator) path2 += "/"; - if (isAbsolute) return "/" + path2; - return path2; - }, - isAbsolute: function isAbsolute(path2) { - assertPath(path2); - return path2.length > 0 && path2.charCodeAt(0) === 47; - }, - join: function join() { - if (arguments.length === 0) - return "."; - var joined; - for (var i = 0; i < arguments.length; ++i) { - var arg = arguments[i]; - assertPath(arg); - if (arg.length > 0) { - if (joined === void 0) - joined = arg; - else - joined += "/" + arg; - } - } - if (joined === void 0) - return "."; - return posix2.normalize(joined); - }, - relative: function relative(from, to) { - assertPath(from); - assertPath(to); - if (from === to) return ""; - from = posix2.resolve(from); - to = posix2.resolve(to); - if (from === to) return ""; - var fromStart = 1; - for (; fromStart < from.length; ++fromStart) { - if (from.charCodeAt(fromStart) !== 47) - break; - } - var fromEnd = from.length; - var fromLen = fromEnd - fromStart; - var toStart = 1; - for (; toStart < to.length; ++toStart) { - if (to.charCodeAt(toStart) !== 47) - break; - } - var toEnd = to.length; - var toLen = toEnd - toStart; - var length = fromLen < toLen ? fromLen : toLen; - var lastCommonSep = -1; - var i = 0; - for (; i <= length; ++i) { - if (i === length) { - if (toLen > length) { - if (to.charCodeAt(toStart + i) === 47) { - return to.slice(toStart + i + 1); - } else if (i === 0) { - return to.slice(toStart + i); - } - } else if (fromLen > length) { - if (from.charCodeAt(fromStart + i) === 47) { - lastCommonSep = i; - } else if (i === 0) { - lastCommonSep = 0; - } - } - break; - } - var fromCode = from.charCodeAt(fromStart + i); - var toCode = to.charCodeAt(toStart + i); - if (fromCode !== toCode) - break; - else if (fromCode === 47) - lastCommonSep = i; - } - var out = ""; - for (i = fromStart + lastCommonSep + 1; i <= fromEnd; ++i) { - if (i === fromEnd || from.charCodeAt(i) === 47) { - if (out.length === 0) - out += ".."; - else - out += "/.."; - } - } - if (out.length > 0) - return out + to.slice(toStart + lastCommonSep); - else { - toStart += lastCommonSep; - if (to.charCodeAt(toStart) === 47) - ++toStart; - return to.slice(toStart); - } - }, - _makeLong: function _makeLong(path2) { - return path2; - }, - dirname: function dirname(path2) { - assertPath(path2); - if (path2.length === 0) return "."; - var code = path2.charCodeAt(0); - var hasRoot = code === 47; - var end = -1; - var matchedSlash = true; - for (var i = path2.length - 1; i >= 1; --i) { - code = path2.charCodeAt(i); - if (code === 47) { - if (!matchedSlash) { - end = i; - break; - } - } else { - matchedSlash = false; - } + + _recordWasiSyscallMetric(name, startedAt, details = {}) { + if (!this.syscallCountersEnabled || typeof process?.stderr?.write !== "function") { + return; + } + try { + const elapsedMs = __agentOSWasiNow() - startedAt; + process.stderr.write( + \`__AGENTOS_WASI_SYSCALL_METRICS__:\${JSON.stringify({ + name, + elapsedMs, + ...details, + })}\\n\`, + ); + } catch { + // Ignore metrics failures. + } + } + + _measureWasiPhase(name, fn) { + if (!this.syscallCountersEnabled || !this._activeWasiMetric) { + return fn(); + } + const startedAt = __agentOSWasiNow(); + try { + return fn(); + } finally { + const phases = (this._activeWasiMetric.phases ??= {}); + phases[name] = (phases[name] ?? 0) + (__agentOSWasiNow() - startedAt); + } + } + + _installSyscallCounterWrappers() { + if (!this.syscallCountersEnabled) { + return; + } + for (const name of [ + "path_open", + "path_filestat_get", + "fd_filestat_get", + "fd_write", + ]) { + const original = this.wasiImport[name]; + if (typeof original !== "function") { + continue; } - if (end === -1) return hasRoot ? "/" : "."; - if (hasRoot && end === 1) return "//"; - return path2.slice(0, end); - }, - basename: function basename(path2, ext) { - if (ext !== void 0 && typeof ext !== "string") throw new TypeError('"ext" argument must be a string'); - assertPath(path2); - var start = 0; - var end = -1; - var matchedSlash = true; - var i; - if (ext !== void 0 && ext.length > 0 && ext.length <= path2.length) { - if (ext.length === path2.length && ext === path2) return ""; - var extIdx = ext.length - 1; - var firstNonSlashEnd = -1; - for (i = path2.length - 1; i >= 0; --i) { - var code = path2.charCodeAt(i); - if (code === 47) { - if (!matchedSlash) { - start = i + 1; - break; - } - } else { - if (firstNonSlashEnd === -1) { - matchedSlash = false; - firstNonSlashEnd = i + 1; - } - if (extIdx >= 0) { - if (code === ext.charCodeAt(extIdx)) { - if (--extIdx === -1) { - end = i; - } - } else { - extIdx = -1; - end = firstNonSlashEnd; - } - } - } - } - if (start === end) end = firstNonSlashEnd; - else if (end === -1) end = path2.length; - return path2.slice(start, end); - } else { - for (i = path2.length - 1; i >= 0; --i) { - if (path2.charCodeAt(i) === 47) { - if (!matchedSlash) { - start = i + 1; - break; - } - } else if (end === -1) { - matchedSlash = false; - end = i + 1; - } + this.wasiImport[name] = (...args) => { + const startedAt = __agentOSWasiNow(); + const previousMetric = this._activeWasiMetric; + const activeMetric = { name, phases: {} }; + this._activeWasiMetric = activeMetric; + let result; + try { + result = original(...args); + } finally { + this._activeWasiMetric = previousMetric; } - if (end === -1) return ""; - return path2.slice(start, end); - } - }, - extname: function extname(path2) { - assertPath(path2); - var startDot = -1; - var startPart = 0; - var end = -1; - var matchedSlash = true; - var preDotState = 0; - for (var i = path2.length - 1; i >= 0; --i) { - var code = path2.charCodeAt(i); - if (code === 47) { - if (!matchedSlash) { - startPart = i + 1; - break; + const details = { + result, + fd: Number(args[0]) >>> 0, + iovsLen: name === "fd_write" ? Number(args[2]) >>> 0 : undefined, + phases: activeMetric.phases, + }; + if (name === "path_filestat_get") { + try { + const target = this._readString(args[2], args[3]); + details.pathLen = target.length; + details.pathKind = target.startsWith("/") + ? "absolute" + : target.includes("/") + ? "relative-nested" + : "relative-child"; + details.pathSample = + target.length > 120 ? \`\${target.slice(0, 120)}...\` : target; + } catch { + // Leave path-shape fields absent if the guest pointer is invalid. } - continue; - } - if (end === -1) { - matchedSlash = false; - end = i + 1; - } - if (code === 46) { - if (startDot === -1) - startDot = i; - else if (preDotState !== 1) - preDotState = 1; - } else if (startDot !== -1) { - preDotState = -1; } + this._recordWasiSyscallMetric(name, startedAt, { + ...details, + }); + return result; + }; + } + } + + start(instance) { + this.instance = instance; + try { + if (typeof instance?.exports?._start === "function") { + instance.exports._start(); } - if (startDot === -1 || end === -1 || // We saw a non-dot character immediately before the dot - preDotState === 0 || // The (right-most) trimmed path component is exactly '..' - preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) { - return ""; - } - return path2.slice(startDot, end); - }, - format: function format(pathObject) { - if (pathObject === null || typeof pathObject !== "object") { - throw new TypeError('The "pathObject" argument must be of type Object. Received type ' + typeof pathObject); - } - return _format("/", pathObject); - }, - parse: function parse(path2) { - assertPath(path2); - var ret = { root: "", dir: "", base: "", ext: "", name: "" }; - if (path2.length === 0) return ret; - var code = path2.charCodeAt(0); - var isAbsolute = code === 47; - var start; - if (isAbsolute) { - ret.root = "/"; - start = 1; - } else { - start = 0; + return 0; + } catch (error) { + if (error && error.__agentOSWasiExit === true) { + return Number(error.code) >>> 0; } - var startDot = -1; - var startPart = 0; - var end = -1; - var matchedSlash = true; - var i = path2.length - 1; - var preDotState = 0; - for (; i >= start; --i) { - code = path2.charCodeAt(i); - if (code === 47) { - if (!matchedSlash) { - startPart = i + 1; - break; - } - continue; - } - if (end === -1) { - matchedSlash = false; - end = i + 1; - } - if (code === 46) { - if (startDot === -1) startDot = i; - else if (preDotState !== 1) preDotState = 1; - } else if (startDot !== -1) { - preDotState = -1; - } + throw error; + } + } + + _memoryView() { + const memory = this.instance?.exports?.memory; + if (!(memory instanceof WebAssembly.Memory)) { + throw new Error("WASI memory export is unavailable"); + } + return new DataView(memory.buffer); + } + + _memoryBytes() { + const memory = this.instance?.exports?.memory; + if (!(memory instanceof WebAssembly.Memory)) { + throw new Error("WASI memory export is unavailable"); + } + return new Uint8Array(memory.buffer); + } + + _boundedIovLength(iovs, iovsLen) { + const view = this._memoryView(); + let length = 0; + for (let index = 0; index < (Number(iovsLen) >>> 0); index += 1) { + const entryOffset = (Number(iovs) >>> 0) + index * 8; + length += view.getUint32(entryOffset + 4, true); + if (length > __agentOSWasmSyncReadLimitBytes) { + throw new RangeError( + \`WASI read iov length \${length} exceeds \${__agentOSWasmSyncReadLimitBytes}\`, + ); } - if (startDot === -1 || end === -1 || // We saw a non-dot character immediately before the dot - preDotState === 0 || // The (right-most) trimmed path component is exactly '..' - preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) { - if (end !== -1) { - if (startPart === 0 && isAbsolute) ret.base = ret.name = path2.slice(1, end); - else ret.base = ret.name = path2.slice(startPart, end); - } - } else { - if (startPart === 0 && isAbsolute) { - ret.name = path2.slice(1, startDot); - ret.base = path2.slice(1, end); - } else { - ret.name = path2.slice(startPart, startDot); - ret.base = path2.slice(startPart, end); - } - ret.ext = path2.slice(startDot, end); + } + return length >>> 0; + } + + // Read-side iov capacity, clamped (not thrown) to the sync read cap. A guest + // may legitimately offer a huge read buffer (e.g. iov_len 0xffffffc0 = "read + // up to ~4GB"); the runner reads only what is available, bounded by the cap, + // so the read allocation/RPC stays bounded without rejecting the read. Writes + // keep using _boundedIovLength (throwing) because their iov length is real + // data that must not be silently truncated. + _boundedReadLength(iovs, iovsLen) { + const view = this._memoryView(); + let length = 0; + for (let index = 0; index < (Number(iovsLen) >>> 0); index += 1) { + const entryOffset = (Number(iovs) >>> 0) + index * 8; + length += view.getUint32(entryOffset + 4, true); + if (length >= __agentOSWasmSyncReadLimitBytes) { + return __agentOSWasmSyncReadLimitBytes; } - if (startPart > 0) ret.dir = path2.slice(0, startPart - 1); - else if (isAbsolute) ret.dir = "/"; - return ret; - }, - sep: "/", - delimiter: ":", - win32: null, - posix: null - }; - posix2.posix = posix2; - module2.exports = posix2; - } -}); + } + return length >>> 0; + } -// -var path = require_path_browserify(); -var resolved = path.default ?? path; -var posix = resolved.posix ?? resolved; -posix.posix = posix; -module.exports = posix; + _normalizeRights(value, fallback) { + try { + return BigInt.asUintN(64, BigInt(value)); + } catch { + return fallback; + } + } -if (module.exports && module.exports.default == null) module.exports.default = module.exports; -`;var ef=`var process = globalThis.process || { - env: {}, - nextTick: (fn, ...args) => queueMicrotask(() => fn(...args)), -}; -var __getOwnPropNames = Object.getOwnPropertyNames; -var __commonJS = (cb, mod) => function __require() { - return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; -}; + _normalizePreopenSpec(value) { + // Path-model seam (convergence item C): native maps guest paths to HOST + // paths (its preopen specs carry \`hostPath\`); a non-native backend with no + // host paths (the browser, whose \`require("fs")\` IS the kernel VFS) can + // supply \`__agentOSWasiHost.normalizePreopen\` to treat the guest/VFS path + // as the "hostPath" identity, so the same runner serves both. + if (typeof __agentOSWasiHost.normalizePreopen === "function") { + const seamNormalized = __agentOSWasiHost.normalizePreopen(value, { + defaultRightsBase: __agentOSWasiDefaultRightsBase, + defaultRightsInheriting: __agentOSWasiDefaultRightsInheriting, + normalizeRights: (rights, fallback) => + this._normalizeRights(rights, fallback), + }); + return seamNormalized ?? null; + } + if (typeof value === "string") { + return { + hostPath: String(value), + readOnly: false, + rightsBase: __agentOSWasiDefaultRightsBase, + rightsInheriting: __agentOSWasiDefaultRightsInheriting, + }; + } + if (!value || typeof value !== "object" || typeof value.hostPath !== "string") { + return null; + } + return { + hostPath: String(value.hostPath), + readOnly: value.readOnly === true, + rightsBase: this._normalizeRights( + value.rightsBase, + __agentOSWasiDefaultRightsBase, + ), + rightsInheriting: this._normalizeRights( + value.rightsInheriting, + __agentOSWasiDefaultRightsInheriting, + ), + }; + } -// node_modules/.pnpm/has-symbols@1.1.0/node_modules/has-symbols/shams.js -var require_shams = __commonJS({ - "node_modules/.pnpm/has-symbols@1.1.0/node_modules/has-symbols/shams.js"(exports2, module2) { - "use strict"; - module2.exports = function hasSymbols() { - if (typeof Symbol !== "function" || typeof Object.getOwnPropertySymbols !== "function") { - return false; + _descriptorRightsBase(entry) { + return this._normalizeRights( + entry?.rightsBase, + __agentOSWasiDefaultRightsBase, + ); + } + + _descriptorRightsInheriting(entry) { + return this._normalizeRights( + entry?.rightsInheriting, + __agentOSWasiDefaultRightsInheriting, + ); + } + + _hasWriteRights(rights) { + try { + return (BigInt(rights) & __agentOSWasiRightFdWrite) !== 0n; + } catch { + return true; } - if (typeof Symbol.iterator === "symbol") { + } + + _hasReadRights(rights) { + try { + return (BigInt(rights) & __agentOSWasiRightFdRead) !== 0n; + } catch { return true; } - var obj = {}; - var sym = /* @__PURE__ */ Symbol("test"); - var symObj = Object(sym); - if (typeof sym === "string") { - return false; + } + + _writeUint32(ptr, value) { + try { + this._memoryView().setUint32(Number(ptr) >>> 0, Number(value) >>> 0, true); + return __agentOSWasiErrnoSuccess; + } catch { + __agentOSWasiDebug(\`writeUint32 failed ptr=\${Number(ptr)} value=\${Number(value)}\`); + return __agentOSWasiErrnoFault; } - if (Object.prototype.toString.call(sym) !== "[object Symbol]") { - return false; + } + + _writeUint64(ptr, value) { + try { + this._memoryView().setBigUint64(Number(ptr) >>> 0, BigInt(value), true); + return __agentOSWasiErrnoSuccess; + } catch { + __agentOSWasiDebug(\`writeUint64 failed ptr=\${Number(ptr)} value=\${String(value)}\`); + return __agentOSWasiErrnoFault; } - if (Object.prototype.toString.call(symObj) !== "[object Symbol]") { - return false; + } + + _writeBytes(ptr, bytes) { + try { + this._memoryBytes().set(bytes, Number(ptr) >>> 0); + return __agentOSWasiErrnoSuccess; + } catch { + __agentOSWasiDebug(\`writeBytes failed ptr=\${Number(ptr)} len=\${bytes?.length ?? 0}\`); + return __agentOSWasiErrnoFault; } - var symVal = 42; - obj[sym] = symVal; - for (var _ in obj) { - return false; + } + + _readBytes(ptr, len) { + const start = Number(ptr) >>> 0; + const end = start + (Number(len) >>> 0); + return Buffer.from(this._memoryBytes().slice(start, end)); + } + + _readString(ptr, len) { + return this._readBytes(ptr, len).toString("utf8"); + } + + _decodeSyncRpcBytes(value) { + if (value == null) { + return null; } - if (typeof Object.keys === "function" && Object.keys(obj).length !== 0) { - return false; + if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) { + return value; } - if (typeof Object.getOwnPropertyNames === "function" && Object.getOwnPropertyNames(obj).length !== 0) { - return false; + if (value instanceof Uint8Array) { + return Buffer.from(value); } - var syms = Object.getOwnPropertySymbols(obj); - if (syms.length !== 1 || syms[0] !== sym) { - return false; + if (ArrayBuffer.isView(value)) { + return Buffer.from(value.buffer, value.byteOffset, value.byteLength); } - if (!Object.prototype.propertyIsEnumerable.call(obj, sym)) { - return false; + if (value instanceof ArrayBuffer) { + return Buffer.from(value); } - if (typeof Object.getOwnPropertyDescriptor === "function") { - var descriptor = ( - /** @type {PropertyDescriptor} */ - Object.getOwnPropertyDescriptor(obj, sym) - ); - if (descriptor.value !== symVal || descriptor.enumerable !== true) { - return false; - } + if ( + value && + typeof value === "object" && + value.__agentOSType === "bytes" && + typeof value.base64 === "string" + ) { + return Buffer.from(value.base64, "base64"); } - return true; - }; - } -}); + return null; + } -// node_modules/.pnpm/has-tostringtag@1.0.2/node_modules/has-tostringtag/shams.js -var require_shams2 = __commonJS({ - "node_modules/.pnpm/has-tostringtag@1.0.2/node_modules/has-tostringtag/shams.js"(exports2, module2) { - "use strict"; - var hasSymbols = require_shams(); - module2.exports = function hasToStringTagShams() { - return hasSymbols() && !!Symbol.toStringTag; - }; - } -}); + _dequeuePipeBytes(pipe, maxBytes) { + if (!pipe || !Array.isArray(pipe.chunks) || pipe.chunks.length === 0) { + return Buffer.alloc(0); + } -// node_modules/.pnpm/es-object-atoms@1.1.1/node_modules/es-object-atoms/index.js -var require_es_object_atoms = __commonJS({ - "node_modules/.pnpm/es-object-atoms@1.1.1/node_modules/es-object-atoms/index.js"(exports2, module2) { - "use strict"; - module2.exports = Object; - } -}); + let remaining = Math.max(0, Number(maxBytes) >>> 0); + if (remaining === 0) { + return Buffer.alloc(0); + } -// node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/index.js -var require_es_errors = __commonJS({ - "node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/index.js"(exports2, module2) { - "use strict"; - module2.exports = Error; - } -}); + const parts = []; + while (remaining > 0 && pipe.chunks.length > 0) { + const chunk = pipe.chunks[0]; + if (!chunk || chunk.length === 0) { + pipe.chunks.shift(); + continue; + } -// node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/eval.js -var require_eval = __commonJS({ - "node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/eval.js"(exports2, module2) { - "use strict"; - module2.exports = EvalError; - } -}); + if (chunk.length <= remaining) { + parts.push(chunk); + pipe.chunks.shift(); + remaining -= chunk.length; + continue; + } + + parts.push(chunk.subarray(0, remaining)); + pipe.chunks[0] = chunk.subarray(remaining); + remaining = 0; + } + + return Buffer.concat(parts); + } + + _enqueuePipeBytes(pipe, bytes) { + if (!pipe || !Array.isArray(pipe.chunks)) { + return; + } + const chunk = Buffer.from(bytes ?? []); + if (chunk.length === 0) { + return; + } + pipe.chunks.push(chunk); + } -// node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/range.js -var require_range = __commonJS({ - "node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/range.js"(exports2, module2) { - "use strict"; - module2.exports = RangeError; - } -}); + _pipeHasReaders(pipe) { + return ( + (pipe?.readHandleCount ?? 0) > 0 || + (pipe?.consumers?.size ?? 0) > 0 + ); + } -// node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/ref.js -var require_ref = __commonJS({ - "node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/ref.js"(exports2, module2) { - "use strict"; - module2.exports = ReferenceError; - } -}); + _flushPipeConsumers(pipe) { + if ( + !pipe || + typeof pipe.consumers?.entries !== "function" || + !Array.isArray(pipe.chunks) || + pipe.chunks.length === 0 || + typeof globalThis?.__agentOSSyncRpc?.callSync !== "function" + ) { + return false; + } -// node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/syntax.js -var require_syntax = __commonJS({ - "node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/syntax.js"(exports2, module2) { - "use strict"; - module2.exports = SyntaxError; - } -}); + let flushed = false; + while (pipe.chunks.length > 0) { + const chunk = pipe.chunks.shift(); + if (!chunk || chunk.length === 0) { + continue; + } -// node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/type.js -var require_type = __commonJS({ - "node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/type.js"(exports2, module2) { - "use strict"; - module2.exports = TypeError; - } -}); + for (const [consumerKey, consumer] of Array.from(pipe.consumers.entries())) { + if (!consumer || typeof consumer.childId !== "string") { + pipe.consumers.delete(consumerKey); + continue; + } + try { + __agentOSWasiSyncRpc().callSync("child_process.write_stdin", [ + consumer.childId, + chunk, + ]); + flushed = true; + } catch { + pipe.consumers.delete(consumerKey); + } + } + } -// node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/uri.js -var require_uri = __commonJS({ - "node_modules/.pnpm/es-errors@1.3.0/node_modules/es-errors/uri.js"(exports2, module2) { - "use strict"; - module2.exports = URIError; - } -}); + return flushed; + } -// node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/abs.js -var require_abs = __commonJS({ - "node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/abs.js"(exports2, module2) { - "use strict"; - module2.exports = Math.abs; - } -}); + _closePipeConsumers(pipe) { + if ( + !pipe || + typeof pipe.consumers?.entries !== "function" || + typeof globalThis?.__agentOSSyncRpc?.callSync !== "function" + ) { + return false; + } -// node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/floor.js -var require_floor = __commonJS({ - "node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/floor.js"(exports2, module2) { - "use strict"; - module2.exports = Math.floor; - } -}); + let closed = false; + for (const [consumerKey, consumer] of Array.from(pipe.consumers.entries())) { + if (!consumer || typeof consumer.childId !== "string") { + pipe.consumers.delete(consumerKey); + continue; + } + try { + __agentOSWasiSyncRpc().callSync("child_process.close_stdin", [ + consumer.childId, + ]); + closed = true; + } catch { + // Ignore close errors during teardown. + } + pipe.consumers.delete(consumerKey); + } -// node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/max.js -var require_max = __commonJS({ - "node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/max.js"(exports2, module2) { - "use strict"; - module2.exports = Math.max; - } -}); + return closed; + } -// node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/min.js -var require_min = __commonJS({ - "node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/min.js"(exports2, module2) { - "use strict"; - module2.exports = Math.min; - } -}); + _pumpPipeProducers(pipe, waitMs) { + if ( + !pipe || + typeof pipe.producers?.entries !== "function" || + typeof globalThis?.__agentOSSyncRpc?.callSync !== "function" + ) { + return false; + } -// node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/pow.js -var require_pow = __commonJS({ - "node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/pow.js"(exports2, module2) { - "use strict"; - module2.exports = Math.pow; - } -}); + let processed = false; + for (const [producerKey, producer] of Array.from(pipe.producers.entries())) { + if (!producer || typeof producer.childId !== "string") { + pipe.producers.delete(producerKey); + continue; + } -// node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/round.js -var require_round = __commonJS({ - "node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/round.js"(exports2, module2) { - "use strict"; - module2.exports = Math.round; - } -}); + let event = null; + try { + event = __agentOSWasiSyncRpc().callSync("child_process.poll", [ + producer.childId, + Math.max(0, Number(waitMs) >>> 0), + ]); + } catch { + pipe.producers.delete(producerKey); + continue; + } -// node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/isNaN.js -var require_isNaN = __commonJS({ - "node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/isNaN.js"(exports2, module2) { - "use strict"; - module2.exports = Number.isNaN || function isNaN2(a) { - return a !== a; - }; - } -}); + if (!event) { + continue; + } -// node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/sign.js -var require_sign = __commonJS({ - "node_modules/.pnpm/math-intrinsics@1.1.0/node_modules/math-intrinsics/sign.js"(exports2, module2) { - "use strict"; - var $isNaN = require_isNaN(); - module2.exports = function sign(number) { - if ($isNaN(number) || number === 0) { - return number; + processed = true; + const streamType = + producer.stream === "stderr" ? "stderr" : producer.stream === "stdout" ? "stdout" : null; + if ((event.type === "stdout" || event.type === "stderr") && event.type === streamType) { + const chunk = this._decodeSyncRpcBytes(event.data); + if (chunk && chunk.length > 0) { + pipe.chunks.push(Buffer.from(chunk)); + } + continue; + } + + if (event.type === "exit") { + pipe.producers.delete(producerKey); + if (pipe.producers.size === 0 && (pipe.writeHandleCount ?? 0) === 0) { + this._closePipeConsumers(pipe); + } + continue; + } } - return number < 0 ? -1 : 1; - }; - } -}); -// node_modules/.pnpm/gopd@1.2.0/node_modules/gopd/gOPD.js -var require_gOPD = __commonJS({ - "node_modules/.pnpm/gopd@1.2.0/node_modules/gopd/gOPD.js"(exports2, module2) { - "use strict"; - module2.exports = Object.getOwnPropertyDescriptor; - } -}); + return processed; + } -// node_modules/.pnpm/gopd@1.2.0/node_modules/gopd/index.js -var require_gopd = __commonJS({ - "node_modules/.pnpm/gopd@1.2.0/node_modules/gopd/index.js"(exports2, module2) { - "use strict"; - var $gOPD = require_gOPD(); - if ($gOPD) { - try { - $gOPD([], "length"); - } catch (e) { - $gOPD = null; + _collectIovs(iovs, iovsLen) { + const totalLength = this._boundedIovLength(iovs, iovsLen); + const view = this._memoryView(); + const chunks = []; + for (let index = 0; index < (Number(iovsLen) >>> 0); index += 1) { + const entryOffset = (Number(iovs) >>> 0) + index * 8; + const ptr = view.getUint32(entryOffset, true); + const len = view.getUint32(entryOffset + 4, true); + chunks.push(this._readBytes(ptr, len)); + } + return Buffer.concat(chunks, totalLength); + } + + _writeToIovs(iovs, iovsLen, bytes) { + const view = this._memoryView(); + const memory = this._memoryBytes(); + let sourceOffset = 0; + for (let index = 0; index < (Number(iovsLen) >>> 0) && sourceOffset < bytes.length; index += 1) { + const entryOffset = (Number(iovs) >>> 0) + index * 8; + const ptr = view.getUint32(entryOffset, true); + const len = view.getUint32(entryOffset + 4, true); + const chunk = bytes.subarray(sourceOffset, sourceOffset + len); + memory.set(chunk, Number(ptr) >>> 0); + sourceOffset += chunk.length; } + return sourceOffset; + } + + _stringTable(values) { + return values.map((value) => Buffer.from(\`\${String(value)}\\0\`, "utf8")); } - module2.exports = $gOPD; - } -}); -// node_modules/.pnpm/es-define-property@1.0.1/node_modules/es-define-property/index.js -var require_es_define_property = __commonJS({ - "node_modules/.pnpm/es-define-property@1.0.1/node_modules/es-define-property/index.js"(exports2, module2) { - "use strict"; - var $defineProperty = Object.defineProperty || false; - if ($defineProperty) { + _writeStringTable(values, offsetsPtr, bufferPtr) { try { - $defineProperty({}, "a", { value: 1 }); - } catch (e) { - $defineProperty = false; + const view = this._memoryView(); + const memory = this._memoryBytes(); + let cursor = Number(bufferPtr) >>> 0; + for (let index = 0; index < values.length; index += 1) { + const bytes = values[index]; + view.setUint32((Number(offsetsPtr) >>> 0) + index * 4, cursor, true); + memory.set(bytes, cursor); + cursor += bytes.length; + } + return __agentOSWasiErrnoSuccess; + } catch { + __agentOSWasiDebug( + \`writeStringTable failed offsetsPtr=\${Number(offsetsPtr)} bufferPtr=\${Number(bufferPtr)} count=\${values.length}\`, + ); + return __agentOSWasiErrnoFault; } } - module2.exports = $defineProperty; - } -}); -// node_modules/.pnpm/has-symbols@1.1.0/node_modules/has-symbols/index.js -var require_has_symbols = __commonJS({ - "node_modules/.pnpm/has-symbols@1.1.0/node_modules/has-symbols/index.js"(exports2, module2) { - "use strict"; - var origSymbol = typeof Symbol !== "undefined" && Symbol; - var hasSymbolSham = require_shams(); - module2.exports = function hasNativeSymbols() { - if (typeof origSymbol !== "function") { - return false; + _filetypeForStats(stats) { + if (!stats) { + return __agentOSWasiFiletypeUnknown; } - if (typeof Symbol !== "function") { - return false; + const mode = Number(stats.mode); + if (Number.isFinite(mode)) { + switch (mode & 0o170000) { + case 0o040000: + return __agentOSWasiFiletypeDirectory; + case 0o100000: + return __agentOSWasiFiletypeRegularFile; + case 0o120000: + return __agentOSWasiFiletypeSymbolicLink; + case 0o020000: + return __agentOSWasiFiletypeCharacterDevice; + } } - if (typeof origSymbol("foo") !== "symbol") { - return false; + if (stats.isDirectory === true) { + return __agentOSWasiFiletypeDirectory; } - if (typeof /* @__PURE__ */ Symbol("bar") !== "symbol") { - return false; + if (stats.isSymbolicLink === true) { + return __agentOSWasiFiletypeSymbolicLink; } - return hasSymbolSham(); - }; - } -}); - -// node_modules/.pnpm/get-proto@1.0.1/node_modules/get-proto/Reflect.getPrototypeOf.js -var require_Reflect_getPrototypeOf = __commonJS({ - "node_modules/.pnpm/get-proto@1.0.1/node_modules/get-proto/Reflect.getPrototypeOf.js"(exports2, module2) { - "use strict"; - module2.exports = typeof Reflect !== "undefined" && Reflect.getPrototypeOf || null; - } -}); - -// node_modules/.pnpm/get-proto@1.0.1/node_modules/get-proto/Object.getPrototypeOf.js -var require_Object_getPrototypeOf = __commonJS({ - "node_modules/.pnpm/get-proto@1.0.1/node_modules/get-proto/Object.getPrototypeOf.js"(exports2, module2) { - "use strict"; - var $Object = require_es_object_atoms(); - module2.exports = $Object.getPrototypeOf || null; - } -}); - -// node_modules/.pnpm/function-bind@1.1.2/node_modules/function-bind/implementation.js -var require_implementation = __commonJS({ - "node_modules/.pnpm/function-bind@1.1.2/node_modules/function-bind/implementation.js"(exports2, module2) { - "use strict"; - var ERROR_MESSAGE = "Function.prototype.bind called on incompatible "; - var toStr = Object.prototype.toString; - var max = Math.max; - var funcType = "[object Function]"; - var concatty = function concatty2(a, b) { - var arr = []; - for (var i = 0; i < a.length; i += 1) { - arr[i] = a[i]; + if (typeof stats.isDirectory === "function" && stats.isDirectory()) { + return __agentOSWasiFiletypeDirectory; } - for (var j = 0; j < b.length; j += 1) { - arr[j + a.length] = b[j]; + if (typeof stats.isFile === "function" && stats.isFile()) { + return __agentOSWasiFiletypeRegularFile; } - return arr; - }; - var slicy = function slicy2(arrLike, offset) { - var arr = []; - for (var i = offset || 0, j = 0; i < arrLike.length; i += 1, j += 1) { - arr[j] = arrLike[i]; + if (typeof stats.isSymbolicLink === "function" && stats.isSymbolicLink()) { + return __agentOSWasiFiletypeSymbolicLink; } - return arr; - }; - var joiny = function(arr, joiner) { - var str = ""; - for (var i = 0; i < arr.length; i += 1) { - str += arr[i]; - if (i + 1 < arr.length) { - str += joiner; - } + if (typeof stats.isCharacterDevice === "function" && stats.isCharacterDevice()) { + return __agentOSWasiFiletypeCharacterDevice; } - return str; - }; - module2.exports = function bind(that) { - var target = this; - if (typeof target !== "function" || toStr.apply(target) !== funcType) { - throw new TypeError(ERROR_MESSAGE + target); + if (stats.isDirectory === false && stats.isSymbolicLink === false) { + return __agentOSWasiFiletypeRegularFile; } - var args = slicy(arguments, 1); - var bound; - var binder = function() { - if (this instanceof bound) { - var result = target.apply( - this, - concatty(args, arguments) - ); - if (Object(result) === result) { - return result; - } - return this; - } - return target.apply( - that, - concatty(args, arguments) - ); - }; - var boundLength = max(0, target.length - args.length); - var boundArgs = []; - for (var i = 0; i < boundLength; i++) { - boundArgs[i] = "$" + i; + return __agentOSWasiFiletypeUnknown; + } + + _filetypeForDirent(dirent) { + if (!dirent || typeof dirent !== "object") { + return __agentOSWasiFiletypeUnknown; } - bound = Function("binder", "return function (" + joiny(boundArgs, ",") + "){ return binder.apply(this,arguments); }")(binder); - if (target.prototype) { - var Empty = function Empty2() { - }; - Empty.prototype = target.prototype; - bound.prototype = new Empty(); - Empty.prototype = null; + if (typeof dirent.isDirectory === "function" && dirent.isDirectory()) { + return __agentOSWasiFiletypeDirectory; } - return bound; - }; - } -}); - -// node_modules/.pnpm/function-bind@1.1.2/node_modules/function-bind/index.js -var require_function_bind = __commonJS({ - "node_modules/.pnpm/function-bind@1.1.2/node_modules/function-bind/index.js"(exports2, module2) { - "use strict"; - var implementation = require_implementation(); - module2.exports = Function.prototype.bind || implementation; - } -}); - -// node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/functionCall.js -var require_functionCall = __commonJS({ - "node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/functionCall.js"(exports2, module2) { - "use strict"; - module2.exports = Function.prototype.call; - } -}); - -// node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/functionApply.js -var require_functionApply = __commonJS({ - "node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/functionApply.js"(exports2, module2) { - "use strict"; - module2.exports = Function.prototype.apply; - } -}); - -// node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/reflectApply.js -var require_reflectApply = __commonJS({ - "node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/reflectApply.js"(exports2, module2) { - "use strict"; - module2.exports = typeof Reflect !== "undefined" && Reflect && Reflect.apply; - } -}); - -// node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/actualApply.js -var require_actualApply = __commonJS({ - "node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/actualApply.js"(exports2, module2) { - "use strict"; - var bind = require_function_bind(); - var $apply = require_functionApply(); - var $call = require_functionCall(); - var $reflectApply = require_reflectApply(); - module2.exports = $reflectApply || bind.call($call, $apply); - } -}); - -// node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/index.js -var require_call_bind_apply_helpers = __commonJS({ - "node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/index.js"(exports2, module2) { - "use strict"; - var bind = require_function_bind(); - var $TypeError = require_type(); - var $call = require_functionCall(); - var $actualApply = require_actualApply(); - module2.exports = function callBindBasic(args) { - if (args.length < 1 || typeof args[0] !== "function") { - throw new $TypeError("a function is required"); + if (typeof dirent.isFile === "function" && dirent.isFile()) { + return __agentOSWasiFiletypeRegularFile; } - return $actualApply(bind, $call, args); - }; - } -}); - -// node_modules/.pnpm/dunder-proto@1.0.1/node_modules/dunder-proto/get.js -var require_get = __commonJS({ - "node_modules/.pnpm/dunder-proto@1.0.1/node_modules/dunder-proto/get.js"(exports2, module2) { - "use strict"; - var callBind = require_call_bind_apply_helpers(); - var gOPD = require_gopd(); - var hasProtoAccessor; - try { - hasProtoAccessor = /** @type {{ __proto__?: typeof Array.prototype }} */ - [].__proto__ === Array.prototype; - } catch (e) { - if (!e || typeof e !== "object" || !("code" in e) || e.code !== "ERR_PROTO_ACCESS") { - throw e; + if (typeof dirent.isSymbolicLink === "function" && dirent.isSymbolicLink()) { + return __agentOSWasiFiletypeSymbolicLink; + } + if (typeof dirent.isCharacterDevice === "function" && dirent.isCharacterDevice()) { + return __agentOSWasiFiletypeCharacterDevice; } + return __agentOSWasiFiletypeUnknown; } - var desc = !!hasProtoAccessor && gOPD && gOPD( - Object.prototype, - /** @type {keyof typeof Object.prototype} */ - "__proto__" - ); - var $Object = Object; - var $getPrototypeOf = $Object.getPrototypeOf; - module2.exports = desc && typeof desc.get === "function" ? callBind([desc.get]) : typeof $getPrototypeOf === "function" ? ( - /** @type {import('./get')} */ - function getDunder(value) { - return $getPrototypeOf(value == null ? value : $Object(value)); + + _clearStatCache() { + this.statCache?.clear?.(); + } + + _statCacheKey(resolved, follow) { + const path = typeof resolved?.guestPath === "string" + ? resolved.guestPath + : this._resolvedFsPath(resolved); + return typeof path === "string" ? \`\${follow ? "stat" : "lstat"}:\${path}\` : null; + } + + _fdFiletype(entry) { + if (!entry) { + return __agentOSWasiFiletypeUnknown; } - ) : false; - } -}); + if ( + entry.kind === "stdin" || + entry.kind === "stdout" || + entry.kind === "stderr" + ) { + return __agentOSWasiFiletypeCharacterDevice; + } + if (entry.kind === "preopen" || entry.kind === "directory") { + return __agentOSWasiFiletypeDirectory; + } + if (entry.kind === "symlink") { + return __agentOSWasiFiletypeSymbolicLink; + } + return __agentOSWasiFiletypeRegularFile; + } -// node_modules/.pnpm/get-proto@1.0.1/node_modules/get-proto/index.js -var require_get_proto = __commonJS({ - "node_modules/.pnpm/get-proto@1.0.1/node_modules/get-proto/index.js"(exports2, module2) { - "use strict"; - var reflectGetProto = require_Reflect_getPrototypeOf(); - var originalGetProto = require_Object_getPrototypeOf(); - var getDunderProto = require_get(); - module2.exports = reflectGetProto ? function getProto(O) { - return reflectGetProto(O); - } : originalGetProto ? function getProto(O) { - if (!O || typeof O !== "object" && typeof O !== "function") { - throw new TypeError("getProto: not an object"); + _mapFsError(error) { + __agentOSWasiDebug( + \`fs error code=\${String(error?.code ?? "")} message=\${String(error?.message ?? error)}\`, + ); + switch (error?.code) { + case "EACCES": + case "EPERM": + return __agentOSWasiErrnoAcces; + case "ENOENT": + return __agentOSWasiErrnoNoent; + case "ENOTDIR": + return __agentOSWasiErrnoNotdir; + case "ENOTEMPTY": + return __agentOSWasiErrnoNotempty; + case "EEXIST": + return __agentOSWasiErrnoExist; + case "EINVAL": + return __agentOSWasiErrnoInval; + case "EROFS": + return __agentOSWasiErrnoRofs; + case "EXDEV": + return __agentOSWasiErrnoXdev; + default: + return __agentOSWasiErrnoIo; } - return originalGetProto(O); - } : getDunderProto ? function getProto(O) { - return getDunderProto(O); - } : null; - } -}); + } -// node_modules/.pnpm/hasown@2.0.2/node_modules/hasown/index.js -var require_hasown = __commonJS({ - "node_modules/.pnpm/hasown@2.0.2/node_modules/hasown/index.js"(exports2, module2) { - "use strict"; - var call = Function.prototype.call; - var $hasOwn = Object.prototype.hasOwnProperty; - var bind = require_function_bind(); - module2.exports = bind.call(call, $hasOwn); - } -}); + _descriptorEntry(fd) { + return this.fdTable.get(Number(fd) >>> 0) ?? null; + } -// node_modules/.pnpm/get-intrinsic@1.3.0/node_modules/get-intrinsic/index.js -var require_get_intrinsic = __commonJS({ - "node_modules/.pnpm/get-intrinsic@1.3.0/node_modules/get-intrinsic/index.js"(exports2, module2) { - "use strict"; - var undefined2; - var $Object = require_es_object_atoms(); - var $Error = require_es_errors(); - var $EvalError = require_eval(); - var $RangeError = require_range(); - var $ReferenceError = require_ref(); - var $SyntaxError = require_syntax(); - var $TypeError = require_type(); - var $URIError = require_uri(); - var abs = require_abs(); - var floor = require_floor(); - var max = require_max(); - var min = require_min(); - var pow = require_pow(); - var round = require_round(); - var sign = require_sign(); - var $Function = Function; - var getEvalledConstructor = function(expressionSyntax) { - try { - return $Function('"use strict"; return (' + expressionSyntax + ").constructor;")(); - } catch (e) { - } - }; - var $gOPD = require_gopd(); - var $defineProperty = require_es_define_property(); - var throwTypeError = function() { - throw new $TypeError(); - }; - var ThrowTypeError = $gOPD ? (function() { - try { - arguments.callee; - return throwTypeError; - } catch (calleeThrows) { - try { - return $gOPD(arguments, "callee").get; - } catch (gOPDthrows) { - return throwTypeError; - } + _localFdHandle(fd) { + // A non-native backend whose \`realFd\` values are not real host OS fds with + // their own kernel offset (the browser, whose fs descriptors are a JS + // handle table) disables local-fd passthrough so locally-opened files use + // the offset-aware file branches (fd_read/fd_write pass the tracked + // entry.offset as an explicit position) instead of host-passthrough reads + // that rely on a null position advancing a real fd. Native keeps passthrough + // so guest-opened fds can be shared with child processes. + if (__agentOSWasiHost.disableLocalFdPassthrough === true) { + return null; } - })() : throwTypeError; - var hasSymbols = require_has_symbols()(); - var getProto = require_get_proto(); - var $ObjectGPO = require_Object_getPrototypeOf(); - var $ReflectGPO = require_Reflect_getPrototypeOf(); - var $apply = require_functionApply(); - var $call = require_functionCall(); - var needsEval = {}; - var TypedArray = typeof Uint8Array === "undefined" || !getProto ? undefined2 : getProto(Uint8Array); - var INTRINSICS = { - __proto__: null, - "%AggregateError%": typeof AggregateError === "undefined" ? undefined2 : AggregateError, - "%Array%": Array, - "%ArrayBuffer%": typeof ArrayBuffer === "undefined" ? undefined2 : ArrayBuffer, - "%ArrayIteratorPrototype%": hasSymbols && getProto ? getProto([][Symbol.iterator]()) : undefined2, - "%AsyncFromSyncIteratorPrototype%": undefined2, - "%AsyncFunction%": needsEval, - "%AsyncGenerator%": needsEval, - "%AsyncGeneratorFunction%": needsEval, - "%AsyncIteratorPrototype%": needsEval, - "%Atomics%": typeof Atomics === "undefined" ? undefined2 : Atomics, - "%BigInt%": typeof BigInt === "undefined" ? undefined2 : BigInt, - "%BigInt64Array%": typeof BigInt64Array === "undefined" ? undefined2 : BigInt64Array, - "%BigUint64Array%": typeof BigUint64Array === "undefined" ? undefined2 : BigUint64Array, - "%Boolean%": Boolean, - "%DataView%": typeof DataView === "undefined" ? undefined2 : DataView, - "%Date%": Date, - "%decodeURI%": decodeURI, - "%decodeURIComponent%": decodeURIComponent, - "%encodeURI%": encodeURI, - "%encodeURIComponent%": encodeURIComponent, - "%Error%": $Error, - "%eval%": eval, - // eslint-disable-line no-eval - "%EvalError%": $EvalError, - "%Float16Array%": typeof Float16Array === "undefined" ? undefined2 : Float16Array, - "%Float32Array%": typeof Float32Array === "undefined" ? undefined2 : Float32Array, - "%Float64Array%": typeof Float64Array === "undefined" ? undefined2 : Float64Array, - "%FinalizationRegistry%": typeof FinalizationRegistry === "undefined" ? undefined2 : FinalizationRegistry, - "%Function%": $Function, - "%GeneratorFunction%": needsEval, - "%Int8Array%": typeof Int8Array === "undefined" ? undefined2 : Int8Array, - "%Int16Array%": typeof Int16Array === "undefined" ? undefined2 : Int16Array, - "%Int32Array%": typeof Int32Array === "undefined" ? undefined2 : Int32Array, - "%isFinite%": isFinite, - "%isNaN%": isNaN, - "%IteratorPrototype%": hasSymbols && getProto ? getProto(getProto([][Symbol.iterator]())) : undefined2, - "%JSON%": typeof JSON === "object" ? JSON : undefined2, - "%Map%": typeof Map === "undefined" ? undefined2 : Map, - "%MapIteratorPrototype%": typeof Map === "undefined" || !hasSymbols || !getProto ? undefined2 : getProto((/* @__PURE__ */ new Map())[Symbol.iterator]()), - "%Math%": Math, - "%Number%": Number, - "%Object%": $Object, - "%Object.getOwnPropertyDescriptor%": $gOPD, - "%parseFloat%": parseFloat, - "%parseInt%": parseInt, - "%Promise%": typeof Promise === "undefined" ? undefined2 : Promise, - "%Proxy%": typeof Proxy === "undefined" ? undefined2 : Proxy, - "%RangeError%": $RangeError, - "%ReferenceError%": $ReferenceError, - "%Reflect%": typeof Reflect === "undefined" ? undefined2 : Reflect, - "%RegExp%": RegExp, - "%Set%": typeof Set === "undefined" ? undefined2 : Set, - "%SetIteratorPrototype%": typeof Set === "undefined" || !hasSymbols || !getProto ? undefined2 : getProto((/* @__PURE__ */ new Set())[Symbol.iterator]()), - "%SharedArrayBuffer%": typeof SharedArrayBuffer === "undefined" ? undefined2 : SharedArrayBuffer, - "%String%": String, - "%StringIteratorPrototype%": hasSymbols && getProto ? getProto(""[Symbol.iterator]()) : undefined2, - "%Symbol%": hasSymbols ? Symbol : undefined2, - "%SyntaxError%": $SyntaxError, - "%ThrowTypeError%": ThrowTypeError, - "%TypedArray%": TypedArray, - "%TypeError%": $TypeError, - "%Uint8Array%": typeof Uint8Array === "undefined" ? undefined2 : Uint8Array, - "%Uint8ClampedArray%": typeof Uint8ClampedArray === "undefined" ? undefined2 : Uint8ClampedArray, - "%Uint16Array%": typeof Uint16Array === "undefined" ? undefined2 : Uint16Array, - "%Uint32Array%": typeof Uint32Array === "undefined" ? undefined2 : Uint32Array, - "%URIError%": $URIError, - "%WeakMap%": typeof WeakMap === "undefined" ? undefined2 : WeakMap, - "%WeakRef%": typeof WeakRef === "undefined" ? undefined2 : WeakRef, - "%WeakSet%": typeof WeakSet === "undefined" ? undefined2 : WeakSet, - "%Function.prototype.call%": $call, - "%Function.prototype.apply%": $apply, - "%Object.defineProperty%": $defineProperty, - "%Object.getPrototypeOf%": $ObjectGPO, - "%Math.abs%": abs, - "%Math.floor%": floor, - "%Math.max%": max, - "%Math.min%": min, - "%Math.pow%": pow, - "%Math.round%": round, - "%Math.sign%": sign, - "%Reflect.getPrototypeOf%": $ReflectGPO - }; - if (getProto) { - try { - null.error; - } catch (e) { - errorProto = getProto(getProto(e)); - INTRINSICS["%Error.prototype%"] = errorProto; + const descriptor = Number(fd) >>> 0; + const entry = this._descriptorEntry(descriptor); + if (!entry || typeof entry.realFd !== "number") { + return null; } + return { + kind: "host-passthrough", + targetFd: entry.realFd, + displayFd: Number(fd) >>> 0, + refCount: 1, + open: true, + readOnly: entry.readOnly === true, + append: entry.append === true, + }; } - var errorProto; - var doEval = function doEval2(name) { - var value; - if (name === "%AsyncFunction%") { - value = getEvalledConstructor("async function () {}"); - } else if (name === "%GeneratorFunction%") { - value = getEvalledConstructor("function* () {}"); - } else if (name === "%AsyncGeneratorFunction%") { - value = getEvalledConstructor("async function* () {}"); - } else if (name === "%AsyncGenerator%") { - var fn = doEval2("%AsyncGeneratorFunction%"); - if (fn) { - value = fn.prototype; + + _externalFdHandle(fd) { + const descriptor = Number(fd) >>> 0; + const localHandle = this._localFdHandle(descriptor); + if (localHandle) { + return localHandle; + } + try { + if (typeof lookupFdHandle === "function") { + return lookupFdHandle(descriptor) ?? null; } - } else if (name === "%AsyncIteratorPrototype%") { - var gen = doEval2("%AsyncGenerator%"); - if (gen && getProto) { - value = getProto(gen.prototype); + } catch { + // Fall through to other lookup paths. + } + try { + const __agentOSWasiFdHandleFn = __agentOSWasiLookupFdHandle(); + if (typeof __agentOSWasiFdHandleFn === "function") { + return __agentOSWasiFdHandleFn(descriptor) ?? null; } + } catch { + // Ignore missing global bridge helpers. } - INTRINSICS[name] = value; - return value; - }; - var LEGACY_ALIASES = { - __proto__: null, - "%ArrayBufferPrototype%": ["ArrayBuffer", "prototype"], - "%ArrayPrototype%": ["Array", "prototype"], - "%ArrayProto_entries%": ["Array", "prototype", "entries"], - "%ArrayProto_forEach%": ["Array", "prototype", "forEach"], - "%ArrayProto_keys%": ["Array", "prototype", "keys"], - "%ArrayProto_values%": ["Array", "prototype", "values"], - "%AsyncFunctionPrototype%": ["AsyncFunction", "prototype"], - "%AsyncGenerator%": ["AsyncGeneratorFunction", "prototype"], - "%AsyncGeneratorPrototype%": ["AsyncGeneratorFunction", "prototype", "prototype"], - "%BooleanPrototype%": ["Boolean", "prototype"], - "%DataViewPrototype%": ["DataView", "prototype"], - "%DatePrototype%": ["Date", "prototype"], - "%ErrorPrototype%": ["Error", "prototype"], - "%EvalErrorPrototype%": ["EvalError", "prototype"], - "%Float32ArrayPrototype%": ["Float32Array", "prototype"], - "%Float64ArrayPrototype%": ["Float64Array", "prototype"], - "%FunctionPrototype%": ["Function", "prototype"], - "%Generator%": ["GeneratorFunction", "prototype"], - "%GeneratorPrototype%": ["GeneratorFunction", "prototype", "prototype"], - "%Int8ArrayPrototype%": ["Int8Array", "prototype"], - "%Int16ArrayPrototype%": ["Int16Array", "prototype"], - "%Int32ArrayPrototype%": ["Int32Array", "prototype"], - "%JSONParse%": ["JSON", "parse"], - "%JSONStringify%": ["JSON", "stringify"], - "%MapPrototype%": ["Map", "prototype"], - "%NumberPrototype%": ["Number", "prototype"], - "%ObjectPrototype%": ["Object", "prototype"], - "%ObjProto_toString%": ["Object", "prototype", "toString"], - "%ObjProto_valueOf%": ["Object", "prototype", "valueOf"], - "%PromisePrototype%": ["Promise", "prototype"], - "%PromiseProto_then%": ["Promise", "prototype", "then"], - "%Promise_all%": ["Promise", "all"], - "%Promise_reject%": ["Promise", "reject"], - "%Promise_resolve%": ["Promise", "resolve"], - "%RangeErrorPrototype%": ["RangeError", "prototype"], - "%ReferenceErrorPrototype%": ["ReferenceError", "prototype"], - "%RegExpPrototype%": ["RegExp", "prototype"], - "%SetPrototype%": ["Set", "prototype"], - "%SharedArrayBufferPrototype%": ["SharedArrayBuffer", "prototype"], - "%StringPrototype%": ["String", "prototype"], - "%SymbolPrototype%": ["Symbol", "prototype"], - "%SyntaxErrorPrototype%": ["SyntaxError", "prototype"], - "%TypedArrayPrototype%": ["TypedArray", "prototype"], - "%TypeErrorPrototype%": ["TypeError", "prototype"], - "%Uint8ArrayPrototype%": ["Uint8Array", "prototype"], - "%Uint8ClampedArrayPrototype%": ["Uint8ClampedArray", "prototype"], - "%Uint16ArrayPrototype%": ["Uint16Array", "prototype"], - "%Uint32ArrayPrototype%": ["Uint32Array", "prototype"], - "%URIErrorPrototype%": ["URIError", "prototype"], - "%WeakMapPrototype%": ["WeakMap", "prototype"], - "%WeakSetPrototype%": ["WeakSet", "prototype"] - }; - var bind = require_function_bind(); - var hasOwn = require_hasown(); - var $concat = bind.call($call, Array.prototype.concat); - var $spliceApply = bind.call($apply, Array.prototype.splice); - var $replace = bind.call($call, String.prototype.replace); - var $strSlice = bind.call($call, String.prototype.slice); - var $exec = bind.call($call, RegExp.prototype.exec); - var rePropName = /[^%.[\\]]+|\\[(?:(-?\\d+(?:\\.\\d+)?)|(["'])((?:(?!\\2)[^\\\\]|\\\\.)*?)\\2)\\]|(?=(?:\\.|\\[\\])(?:\\.|\\[\\]|%$))/g; - var reEscapeChar = /\\\\(\\\\)?/g; - var stringToPath = function stringToPath2(string) { - var first = $strSlice(string, 0, 1); - var last = $strSlice(string, -1); - if (first === "%" && last !== "%") { - throw new $SyntaxError("invalid intrinsic syntax, expected closing \`%\`"); - } else if (last === "%" && first !== "%") { - throw new $SyntaxError("invalid intrinsic syntax, expected opening \`%\`"); + return null; + } + + _descriptorHostPath(entry) { + if (!entry) { + return null; } - var result = []; - $replace(string, rePropName, function(match, number, quote, subString) { - result[result.length] = quote ? $replace(subString, reEscapeChar, "$1") : number || match; - }); - return result; - }; - var getBaseIntrinsic = function getBaseIntrinsic2(name, allowMissing) { - var intrinsicName = name; - var alias; - if (hasOwn(LEGACY_ALIASES, intrinsicName)) { - alias = LEGACY_ALIASES[intrinsicName]; - intrinsicName = "%" + alias[0] + "%"; + if (typeof entry.hostPath === "string") { + return entry.hostPath; } - if (hasOwn(INTRINSICS, intrinsicName)) { - var value = INTRINSICS[intrinsicName]; - if (value === needsEval) { - value = doEval(intrinsicName); - } - if (typeof value === "undefined" && !allowMissing) { - throw new $TypeError("intrinsic " + name + " exists, but is not available. Please file an issue!"); - } - return { - alias, - name: intrinsicName, - value - }; + if (typeof entry.realFd === "number") { + return __agentOSFs().readlinkSync(\`/proc/self/fd/\${entry.realFd}\`); } - throw new $SyntaxError("intrinsic " + name + " does not exist!"); - }; - module2.exports = function GetIntrinsic(name, allowMissing) { - if (typeof name !== "string" || name.length === 0) { - throw new $TypeError("intrinsic name must be a non-empty string"); + return null; + } + + _descriptorFsPath(entry) { + if (!entry) { + return null; } - if (arguments.length > 1 && typeof allowMissing !== "boolean") { - throw new $TypeError('"allowMissing" argument must be a boolean'); + if (typeof entry.hostPath === "string" && entry.hostPath.length > 0) { + return entry.hostPath; } - if ($exec(/^%?[^%]*%?$/, name) === null) { - throw new $SyntaxError("\`%\` may not be present anywhere but at the beginning and end of the intrinsic name"); + if (typeof entry.guestPath === "string" && entry.guestPath.length > 0) { + return entry.guestPath; } - var parts = stringToPath(name); - var intrinsicBaseName = parts.length > 0 ? parts[0] : ""; - var intrinsic = getBaseIntrinsic("%" + intrinsicBaseName + "%", allowMissing); - var intrinsicRealName = intrinsic.name; - var value = intrinsic.value; - var skipFurtherCaching = false; - var alias = intrinsic.alias; - if (alias) { - intrinsicBaseName = alias[0]; - $spliceApply(parts, $concat([0, 1], alias)); + return null; + } + + _sidecarManagedProcess() { + if ( + typeof globalThis.__agentOSWasmInternalEnv?.AGENTOS_SANDBOX_ROOT === + "string" && + globalThis.__agentOSWasmInternalEnv.AGENTOS_SANDBOX_ROOT.length > 0 + ) { + return true; } - for (var i = 1, isOwn = true; i < parts.length; i += 1) { - var part = parts[i]; - var first = $strSlice(part, 0, 1); - var last = $strSlice(part, -1); - if ((first === '"' || first === "'" || first === "\`" || (last === '"' || last === "'" || last === "\`")) && first !== last) { - throw new $SyntaxError("property names with quotes must have matching quotes"); + return ( + typeof process?.env?.AGENTOS_SANDBOX_ROOT === "string" && + process.env.AGENTOS_SANDBOX_ROOT.length > 0 + ); + } + + _descriptorDirectoryFsPath(entry) { + if ( + (entry?.kind === "preopen" || entry?.kind === "directory") && + this._sidecarManagedProcess() + ) { + return this._descriptorGuestPath(entry); + } + return this._descriptorFsPath(entry); + } + + _descriptorGuestPath(entry) { + if (!entry) { + return null; + } + const guestPath = typeof entry.guestPath === "string" ? entry.guestPath : null; + if (guestPath === ".") { + return this._currentGuestCwd(); + } + if (typeof guestPath === "string" && guestPath.length > 0) { + return __agentOSPath().posix.normalize(guestPath); + } + return null; + } + + _descriptorPreopenName(entry) { + if (!entry) { + return null; + } + const guestPath = typeof entry.guestPath === "string" ? entry.guestPath : null; + if (guestPath === ".") { + return this._descriptorGuestPath(entry); + } + if (typeof guestPath === "string" && guestPath.length > 0) { + return __agentOSPath().posix.normalize(guestPath); + } + return null; + } + + _currentDirectoryPreopen() { + for (const entry of this.fdTable.values()) { + if (entry?.kind === "preopen" && entry.guestPath === ".") { + return entry; + } + } + return null; + } + + _descriptorPathBase(entry, target) { + const baseGuestPath = this._descriptorGuestPath(entry); + if (typeof baseGuestPath !== "string") { + return null; + } + return { + entry, + guestPath: baseGuestPath, + hostPath: typeof entry?.hostPath === "string" ? entry.hostPath : null, + }; + } + + _hostPathExists(hostPath) { + return this._measureWasiPhase("hostPathExists", () => { + try { + __agentOSFs().statSync(hostPath); + return true; + } catch { + return false; } - if (part === "constructor" || !isOwn) { - skipFurtherCaching = true; + }); + } + + _createParentExists(guestPath, hostPath) { + const target = + this._sidecarManagedProcess() && typeof guestPath === "string" + ? guestPath + : hostPath; + if (typeof target !== "string") { + return false; + } + return this._measureWasiPhase("createParentExists", () => { + try { + __agentOSFs().statSync(__agentOSPath().dirname(target)); + return true; + } catch { + return false; } - intrinsicBaseName += "." + part; - intrinsicRealName = "%" + intrinsicBaseName + "%"; - if (hasOwn(INTRINSICS, intrinsicRealName)) { - value = INTRINSICS[intrinsicRealName]; - } else if (value != null) { - if (!(part in value)) { - if (!allowMissing) { - throw new $TypeError("base intrinsic for " + name + " exists, but the property is not available."); - } - return void undefined2; + }); + } + + _currentGuestCwd() { + const pwd = + typeof this.env?.PWD === "string" && this.env.PWD.startsWith("/") + ? this.env.PWD + : typeof this.env?.HOME === "string" && this.env.HOME.startsWith("/") + ? this.env.HOME + : "/"; + return __agentOSPath().posix.normalize(pwd); + } + + _resolveHostMappingForGuestPath(guestPath) { + return this._measureWasiPhase("resolveHostMapping", () => { + const normalized = __agentOSPath().posix.normalize(guestPath); + const mappings = []; + for (const entry of this.fdTable.values()) { + if (entry?.kind !== "preopen" || typeof entry.hostPath !== "string") { + continue; } - if ($gOPD && i + 1 >= parts.length) { - var desc = $gOPD(value, part); - isOwn = !!desc; - if (isOwn && "get" in desc && !("originalValue" in desc.get)) { - value = desc.get; - } else { - value = value[part]; - } - } else { - isOwn = hasOwn(value, part); - value = value[part]; + const guestRoot = this._descriptorGuestPath(entry); + if (typeof guestRoot !== "string") { + continue; } - if (isOwn && !skipFurtherCaching) { - INTRINSICS[intrinsicRealName] = value; + mappings.push({ + guestRoot, + hostPath: entry.hostPath, + readOnly: entry.readOnly === true, + }); + } + mappings.sort((left, right) => right.guestRoot.length - left.guestRoot.length); + + for (const mapping of mappings) { + const matchesRoot = mapping.guestRoot === "/" && normalized.startsWith("/"); + const matchesNested = + normalized === mapping.guestRoot || + normalized.startsWith(\`\${mapping.guestRoot}/\`); + if (!matchesRoot && !matchesNested) { + continue; } + const suffix = + normalized === mapping.guestRoot + ? "" + : mapping.guestRoot === "/" + ? normalized.slice(1) + : normalized.slice(mapping.guestRoot.length + 1); + return { + hostPath: suffix + ? __agentOSPath().join(mapping.hostPath, ...suffix.split("/")) + : mapping.hostPath, + readOnly: mapping.readOnly, + }; } - } - return value; - }; - } -}); -// node_modules/.pnpm/call-bound@1.0.4/node_modules/call-bound/index.js -var require_call_bound = __commonJS({ - "node_modules/.pnpm/call-bound@1.0.4/node_modules/call-bound/index.js"(exports2, module2) { - "use strict"; - var GetIntrinsic = require_get_intrinsic(); - var callBindBasic = require_call_bind_apply_helpers(); - var $indexOf = callBindBasic([GetIntrinsic("%String.prototype.indexOf%")]); - module2.exports = function callBoundIntrinsic(name, allowMissing) { - var intrinsic = ( - /** @type {(this: unknown, ...args: unknown[]) => unknown} */ - GetIntrinsic(name, !!allowMissing) - ); - if (typeof intrinsic === "function" && $indexOf(name, ".prototype.") > -1) { - return callBindBasic( - /** @type {const} */ - [intrinsic] - ); - } - return intrinsic; - }; - } -}); + return null; + }); + } -// node_modules/.pnpm/is-arguments@1.2.0/node_modules/is-arguments/index.js -var require_is_arguments = __commonJS({ - "node_modules/.pnpm/is-arguments@1.2.0/node_modules/is-arguments/index.js"(exports2, module2) { - "use strict"; - var hasToStringTag = require_shams2()(); - var callBound = require_call_bound(); - var $toString = callBound("Object.prototype.toString"); - var isStandardArguments = function isArguments(value) { - if (hasToStringTag && value && typeof value === "object" && Symbol.toStringTag in value) { + _resolveHostPathForGuestPath(guestPath) { + return this._resolveHostMappingForGuestPath(guestPath)?.hostPath ?? null; + } + + _rootRelativeTargetPrefersCwd(target) { + const normalizedTarget = __agentOSPath().posix.normalize(target || "."); + if (normalizedTarget !== ".") { return false; } - return $toString(value) === "[object Arguments]"; - }; - var isLegacyArguments = function isArguments(value) { - if (isStandardArguments(value)) { - return true; - } - return value !== null && typeof value === "object" && "length" in value && typeof value.length === "number" && value.length >= 0 && $toString(value) !== "[object Array]" && "callee" in value && $toString(value.callee) === "[object Function]"; - }; - var supportsStandardArguments = (function() { - return isStandardArguments(arguments); - })(); - isStandardArguments.isLegacyArguments = isLegacyArguments; - module2.exports = supportsStandardArguments ? isStandardArguments : isLegacyArguments; - } -}); + return !this._rootRelativeTargetMatchesAbsoluteArg(target); + } -// node_modules/.pnpm/is-regex@1.2.1/node_modules/is-regex/index.js -var require_is_regex = __commonJS({ - "node_modules/.pnpm/is-regex@1.2.1/node_modules/is-regex/index.js"(exports2, module2) { - "use strict"; - var callBound = require_call_bound(); - var hasToStringTag = require_shams2()(); - var hasOwn = require_hasown(); - var gOPD = require_gopd(); - var fn; - if (hasToStringTag) { - $exec = callBound("RegExp.prototype.exec"); - isRegexMarker = {}; - throwRegexMarker = function() { - throw isRegexMarker; - }; - badStringifier = { - toString: throwRegexMarker, - valueOf: throwRegexMarker - }; - if (typeof Symbol.toPrimitive === "symbol") { - badStringifier[Symbol.toPrimitive] = throwRegexMarker; - } - fn = function isRegex(value) { - if (!value || typeof value !== "object") { - return false; - } - var descriptor = ( - /** @type {NonNullable} */ - gOPD( - /** @type {{ lastIndex?: unknown }} */ - value, - "lastIndex" - ) + _rootRelativeTargetMatchesAbsoluteArg(target) { + const rootGuestPath = __agentOSPath().posix.resolve("/", target); + return this.args + .slice(1) + .some( + (arg) => + typeof arg === "string" && + arg.startsWith("/") && + __agentOSPath().posix.normalize(arg) === rootGuestPath, ); - var hasLastIndexDataProperty = descriptor && hasOwn(descriptor, "value"); - if (!hasLastIndexDataProperty) { - return false; - } - try { - $exec( - value, - /** @type {string} */ - /** @type {unknown} */ - badStringifier + } + + _rootRelativeTargetIsWithinAbsoluteArg(target) { + const rootGuestPath = __agentOSPath().posix.resolve("/", target); + return this.args + .slice(1) + .some((arg) => { + if (typeof arg !== "string" || !arg.startsWith("/")) { + return false; + } + const normalizedArg = __agentOSPath().posix.normalize(arg); + return ( + rootGuestPath === normalizedArg || + rootGuestPath.startsWith(\`\${normalizedArg}/\`) ); - } catch (e) { - return e === isRegexMarker; - } - }; - } else { - $toString = callBound("Object.prototype.toString"); - regexClass = "[object RegExp]"; - fn = function isRegex(value) { - if (!value || typeof value !== "object" && typeof value !== "function") { - return false; + }); + } + + _resolveRootRelativePath(target, preferCreateParent = false) { + const rootGuestPath = __agentOSPath().posix.resolve("/", target); + const rootMapping = this._resolveHostMappingForGuestPath(rootGuestPath); + const rootHostPath = rootMapping?.hostPath ?? null; + const cwdGuestPath = this._currentGuestCwd(); + if (cwdGuestPath !== "/") { + const cwdGuestTarget = __agentOSPath().posix.resolve(cwdGuestPath, target); + const cwdMapping = this._resolveHostMappingForGuestPath(cwdGuestTarget); + const cwdHostTarget = cwdMapping?.hostPath ?? null; + if ( + typeof cwdHostTarget === "string" && + ( + ( + preferCreateParent && + !this._rootRelativeTargetIsWithinAbsoluteArg(target) && + this._createParentExists(cwdGuestTarget, cwdHostTarget) + ) || + this._rootRelativeTargetPrefersCwd(target) || + ( + this._hostPathExists(cwdHostTarget) && + !(typeof rootHostPath === "string" && this._hostPathExists(rootHostPath)) + ) + ) + ) { + return { + guestPath: cwdGuestTarget, + hostPath: cwdHostTarget, + readOnly: cwdMapping?.readOnly === true, + }; } - return $toString(value) === regexClass; + } + return { + guestPath: rootGuestPath, + hostPath: rootHostPath, + readOnly: rootMapping?.readOnly === true, }; } - var $exec; - var isRegexMarker; - var throwRegexMarker; - var badStringifier; - var $toString; - var regexClass; - module2.exports = fn; - } -}); -// node_modules/.pnpm/safe-regex-test@1.1.0/node_modules/safe-regex-test/index.js -var require_safe_regex_test = __commonJS({ - "node_modules/.pnpm/safe-regex-test@1.1.0/node_modules/safe-regex-test/index.js"(exports2, module2) { - "use strict"; - var callBound = require_call_bound(); - var isRegex = require_is_regex(); - var $exec = callBound("RegExp.prototype.exec"); - var $TypeError = require_type(); - module2.exports = function regexTester(regex) { - if (!isRegex(regex)) { - throw new $TypeError("\`regex\` must be a RegExp"); + _resolveAbsolutePath(target, preferCreateParent = false) { + const rootGuestPath = __agentOSPath().posix.normalize(target); + const rootMapping = this._resolveHostMappingForGuestPath(rootGuestPath); + const rootHostPath = rootMapping?.hostPath ?? null; + const cwdGuestPath = this._currentGuestCwd(); + if ( + cwdGuestPath !== "/" && + !this._rootRelativeTargetIsWithinAbsoluteArg(target) + ) { + const cwdGuestTarget = __agentOSPath().posix.resolve( + cwdGuestPath, + target.replace(/^\\/+/, ""), + ); + const cwdMapping = this._resolveHostMappingForGuestPath(cwdGuestTarget); + const cwdHostTarget = cwdMapping?.hostPath ?? null; + if ( + typeof cwdHostTarget === "string" && + ( + ( + preferCreateParent && + this._createParentExists(cwdGuestTarget, cwdHostTarget) + ) || + ( + this._hostPathExists(cwdHostTarget) && + !(typeof rootHostPath === "string" && this._hostPathExists(rootHostPath)) + ) + ) + ) { + return { + guestPath: cwdGuestTarget, + hostPath: cwdHostTarget, + readOnly: cwdMapping?.readOnly === true, + }; + } } - return function test(s) { - return $exec(regex, s) !== null; + return { + guestPath: rootGuestPath, + hostPath: rootHostPath, + readOnly: rootMapping?.readOnly === true, }; - }; - } -}); - -// node_modules/.pnpm/generator-function@2.0.1/node_modules/generator-function/index.js -var require_generator_function = __commonJS({ - "node_modules/.pnpm/generator-function@2.0.1/node_modules/generator-function/index.js"(exports2, module2) { - "use strict"; - var cached = ( - /** @type {GeneratorFunctionConstructor} */ - function* () { - }.constructor - ); - module2.exports = () => cached; - } -}); + } -// node_modules/.pnpm/is-generator-function@1.1.2/node_modules/is-generator-function/index.js -var require_is_generator_function = __commonJS({ - "node_modules/.pnpm/is-generator-function@1.1.2/node_modules/is-generator-function/index.js"(exports2, module2) { - "use strict"; - var callBound = require_call_bound(); - var safeRegexTest = require_safe_regex_test(); - var isFnRegex = safeRegexTest(/^\\s*(?:function)?\\*/); - var hasToStringTag = require_shams2()(); - var getProto = require_get_proto(); - var toStr = callBound("Object.prototype.toString"); - var fnToStr = callBound("Function.prototype.toString"); - var getGeneratorFunction = require_generator_function(); - module2.exports = function isGeneratorFunction(fn) { - if (typeof fn !== "function") { - return false; - } - if (isFnRegex(fnToStr(fn))) { - return true; + _resolveDescriptorPath(fd, pathPtr, pathLen, options = {}) { + const entry = this._measureWasiPhase("descriptorEntry", () => this._descriptorEntry(fd)); + if (!entry) { + return { error: __agentOSWasiErrnoBadf }; } - if (!hasToStringTag) { - var str = toStr(fn); - return str === "[object GeneratorFunction]"; + const target = this._measureWasiPhase("readString", () => this._readString(pathPtr, pathLen)); + const base = this._measureWasiPhase("descriptorPathBase", () => this._descriptorPathBase(entry, target)); + if (!base || typeof base.guestPath !== "string") { + return { error: __agentOSWasiErrnoBadf }; } - if (!getProto) { - return false; + const guestPath = this._measureWasiPhase("guestPathResolve", () => + target.startsWith("/") + ? __agentOSPath().posix.normalize(target) + : __agentOSPath().posix.resolve(base.guestPath, target) + ); + const mapped = this._measureWasiPhase("descriptorPathMap", () => + target.startsWith("/") + ? this._resolveAbsolutePath( + target, + options.preferCreateParent === true, + ) + : base.guestPath === "/" + ? this._resolveRootRelativePath( + target, + options.preferCreateParent === true, + ) + : { + guestPath, + ...( + this._resolveHostMappingForGuestPath(guestPath) ?? + { hostPath: null, readOnly: false } + ), + } + ); + const hostPath = mapped.hostPath; + if (typeof hostPath !== "string") { + return { error: __agentOSWasiErrnoNoent }; } - var GeneratorFunction = getGeneratorFunction(); - return GeneratorFunction && getProto(fn) === GeneratorFunction.prototype; - }; - } -}); + return { + error: __agentOSWasiErrnoSuccess, + guestPath: mapped.guestPath, + hostPath, + readOnly: mapped.readOnly === true, + }; + } -// node_modules/.pnpm/is-callable@1.2.7/node_modules/is-callable/index.js -var require_is_callable = __commonJS({ - "node_modules/.pnpm/is-callable@1.2.7/node_modules/is-callable/index.js"(exports2, module2) { - "use strict"; - var fnToStr = Function.prototype.toString; - var reflectApply = typeof Reflect === "object" && Reflect !== null && Reflect.apply; - var badArrayLike; - var isCallableMarker; - if (typeof reflectApply === "function" && typeof Object.defineProperty === "function") { - try { - badArrayLike = Object.defineProperty({}, "length", { - get: function() { - throw isCallableMarker; - } - }); - isCallableMarker = {}; - reflectApply(function() { - throw 42; - }, null, badArrayLike); - } catch (_) { - if (_ !== isCallableMarker) { - reflectApply = null; - } + _resolvedFsPath(resolved) { + if (this._sidecarManagedProcess() && typeof resolved?.guestPath === "string") { + return resolved.guestPath; } - } else { - reflectApply = null; + return resolved?.hostPath ?? null; } - var constructorRegex = /^\\s*class\\b/; - var isES6ClassFn = function isES6ClassFunction(value) { - try { - var fnStr = fnToStr.call(value); - return constructorRegex.test(fnStr); - } catch (e) { - return false; - } - }; - var tryFunctionObject = function tryFunctionToStr(value) { - try { - if (isES6ClassFn(value)) { - return false; - } - fnToStr.call(value); - return true; - } catch (e) { - return false; + + _statResolvedPath(resolved, follow) { + if ( + typeof globalThis?.__agentOSSyncRpc?.callSync === "function" && + typeof resolved?.guestPath === "string" + ) { + return this._measureWasiPhase(follow ? "syncRpcStat" : "syncRpcLstat", () => + __agentOSWasiSyncRpc().callSync(follow ? "fs.statSync" : "fs.lstatSync", [ + resolved.guestPath, + ]) + ); } - }; - var toStr = Object.prototype.toString; - var objectClass = "[object Object]"; - var fnClass = "[object Function]"; - var genClass = "[object GeneratorFunction]"; - var ddaClass = "[object HTMLAllCollection]"; - var ddaClass2 = "[object HTML document.all class]"; - var ddaClass3 = "[object HTMLCollection]"; - var hasToStringTag = typeof Symbol === "function" && !!Symbol.toStringTag; - var isIE68 = !(0 in [,]); - var isDDA = function isDocumentDotAll() { - return false; - }; - if (typeof document === "object") { - all = document.all; - if (toStr.call(all) === toStr.call(document.all)) { - isDDA = function isDocumentDotAll(value) { - if ((isIE68 || !value) && (typeof value === "undefined" || typeof value === "object")) { - try { - var str = toStr.call(value); - return (str === ddaClass || str === ddaClass2 || str === ddaClass3 || str === objectClass) && value("") == null; - } catch (e) { - } - } - return false; - }; + const bridgeStat = follow ? globalThis?._fsStat : globalThis?._fsLstat; + if ( + typeof bridgeStat?.applySyncPromise === "function" && + typeof resolved?.guestPath === "string" + ) { + return this._measureWasiPhase(follow ? "bridgeStatSync" : "bridgeLstatSync", () => + bridgeStat.applySyncPromise(void 0, [resolved.guestPath]) + ); } + const fsPath = this._resolvedFsPath(resolved); + return this._measureWasiPhase(follow ? "fsModuleStatSync" : "fsModuleLstatSync", () => + follow ? this._fs().statSync(fsPath) : this._fs().lstatSync(fsPath) + ); } - var all; - module2.exports = reflectApply ? function isCallable(value) { - if (isDDA(value)) { - return true; + + + _resolveDescriptorDirectStatPath(fd, target) { + if ( + typeof target !== "string" || + target.length === 0 || + target === "." || + target === ".." || + target.startsWith("/") + ) { + return null; } - if (!value) { - return false; + const entry = this._descriptorEntry(fd); + if ( + !entry || + (entry.kind !== "directory" && entry.kind !== "preopen") || + typeof entry.hostPath !== "string" || + entry.hostPath.length === 0 + ) { + return null; } - if (typeof value !== "function" && typeof value !== "object") { - return false; + const baseGuestPath = this._descriptorGuestPath(entry); + if (typeof baseGuestPath !== "string") { + return null; } - try { - reflectApply(value, null, badArrayLike); - } catch (e) { - if (e !== isCallableMarker) { - return false; + if (entry.kind === "preopen" && baseGuestPath === "/") { + if (!this._rootRelativeTargetIsWithinAbsoluteArg(target)) { + return null; } + } else if (target.includes("/")) { + return null; } - return !isES6ClassFn(value) && tryFunctionObject(value); - } : function isCallable(value) { - if (isDDA(value)) { - return true; - } - if (!value) { - return false; - } - if (typeof value !== "function" && typeof value !== "object") { - return false; - } - if (hasToStringTag) { - return tryFunctionObject(value); + return { + error: __agentOSWasiErrnoSuccess, + guestPath: this._path().posix.resolve(baseGuestPath, target), + hostPath: this._path().join(entry.hostPath, target), + readOnly: entry.readOnly === true, + }; + } + + _writeFilestat(statPtr, stats, fallbackType) { + try { + const view = this._memoryView(); + const offset = Number(statPtr) >>> 0; + const filetype = stats ? this._filetypeForStats(stats) : fallbackType; + const ino = + typeof stats?.inoExact === "bigint" ? stats.inoExact : BigInt(stats?.ino ?? 0); + const nlink = + typeof stats?.nlinkExact === "bigint" ? stats.nlinkExact : BigInt(stats?.nlink ?? 1); + const size = + typeof stats?.sizeExact === "bigint" ? stats.sizeExact : BigInt(stats?.size ?? 0); + view.setBigUint64(offset, 0n, true); + view.setBigUint64(offset + 8, ino, true); + view.setUint8(offset + 16, filetype); + view.setBigUint64(offset + 24, nlink, true); + view.setBigUint64(offset + 32, size, true); + view.setBigUint64(offset + 40, BigInt(Math.trunc((stats?.atimeMs ?? 0) * 1000000)), true); + view.setBigUint64(offset + 48, BigInt(Math.trunc((stats?.mtimeMs ?? 0) * 1000000)), true); + view.setBigUint64(offset + 56, BigInt(Math.trunc((stats?.ctimeMs ?? 0) * 1000000)), true); + return __agentOSWasiErrnoSuccess; + } catch (error) { + return this._mapFsError(error); } - if (isES6ClassFn(value)) { - return false; + } + + _argsSizesGet(argcPtr, argvBufSizePtr) { + const values = this._stringTable(this.args); + const total = values.reduce((sum, value) => sum + value.length, 0); + const argcStatus = this._writeUint32(argcPtr, values.length); + if (argcStatus !== __agentOSWasiErrnoSuccess) { + return argcStatus; } - var strClass = toStr.call(value); - if (strClass !== fnClass && strClass !== genClass && !/^\\[object HTML/.test(strClass)) { - return false; + return this._writeUint32(argvBufSizePtr, total); + } + + _argsGet(argvPtr, argvBufPtr) { + return this._writeStringTable(this._stringTable(this.args), argvPtr, argvBufPtr); + } + + _environEntries() { + return Object.entries(this.env).map(([key, value]) => \`\${key}=\${value}\`); + } + + _environSizesGet(countPtr, bufSizePtr) { + const values = this._stringTable(this._environEntries()); + const total = values.reduce((sum, value) => sum + value.length, 0); + const countStatus = this._writeUint32(countPtr, values.length); + if (countStatus !== __agentOSWasiErrnoSuccess) { + return countStatus; } - return tryFunctionObject(value); - }; - } -}); + return this._writeUint32(bufSizePtr, total); + } -// node_modules/.pnpm/for-each@0.3.5/node_modules/for-each/index.js -var require_for_each = __commonJS({ - "node_modules/.pnpm/for-each@0.3.5/node_modules/for-each/index.js"(exports2, module2) { - "use strict"; - var isCallable = require_is_callable(); - var toStr = Object.prototype.toString; - var hasOwnProperty = Object.prototype.hasOwnProperty; - var forEachArray = function forEachArray2(array, iterator, receiver) { - for (var i = 0, len = array.length; i < len; i++) { - if (hasOwnProperty.call(array, i)) { - if (receiver == null) { - iterator(array[i], i, array); + _environGet(environPtr, environBufPtr) { + return this._writeStringTable( + this._stringTable(this._environEntries()), + environPtr, + environBufPtr, + ); + } + + _clockTimeGet(_clockId, _precision, resultPtr) { + return this._writeUint64(resultPtr, BigInt(Date.now()) * 1000000n); + } + + _clockResGet(_clockId, resultPtr) { + return this._writeUint64(resultPtr, 1000000n); + } + + _fdWrite(fd, iovs, iovsLen, nwrittenPtr) { + try { + const bytes = this._measureWasiPhase("collectIovs", () => this._collectIovs(iovs, iovsLen)); + const descriptor = Number(fd) >>> 0; + const handle = this._measureWasiPhase("externalFdHandle", () => this._externalFdHandle(descriptor)); + if (handle?.kind === "pipe-write" && handle.pipe) { + if (bytes.length > 0 && !this._pipeHasReaders(handle.pipe)) { + return __agentOSWasiErrnoPipe; + } + this._enqueuePipeBytes(handle.pipe, bytes); + this._flushPipeConsumers(handle.pipe); + return this._writeUint32(nwrittenPtr, bytes.length); + } + if ( + (handle?.kind === "passthrough" || handle?.kind === "host-passthrough") && + typeof handle.targetFd === "number" + ) { + if (handle.readOnly === true) { + return __agentOSWasiErrnoRofs; + } + if (descriptor === 1 || descriptor === 2) { + const sidecarManagedProcess = + typeof process?.env?.AGENTOS_SANDBOX_ROOT === "string" && + process.env.AGENTOS_SANDBOX_ROOT.length > 0; + const useKernelStdioSyncRpc = + sidecarManagedProcess || __agentOSKernelStdioSyncRpcEnabled(); + if (useKernelStdioSyncRpc) { + const written = this._measureWasiPhase("kernelStdioWrite", () => + Number( + __agentOSWasiSyncRpc().callSync("__kernel_stdio_write", [descriptor, bytes]), + ) >>> 0 + ); + return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); + } + } + const entry = this._descriptorEntry(descriptor); + const localHostPassthrough = + handle.kind === "host-passthrough" && + entry?.kind === "file" && + entry.realFd === handle.targetFd; + const position = handle.append + ? this._measureWasiPhase("appendFstat", () => + Number(__agentOSFs().fstatSync(handle.targetFd).size ?? 0) + ) + : localHostPassthrough + ? (entry.offset ?? 0) + : null; + const written = this._measureWasiPhase("writeSync", () => + __agentOSFs().writeSync( + handle.targetFd, + bytes, + 0, + bytes.length, + position, + ) + ); + if (localHostPassthrough) { + if (handle.append) { + entry.offset = this._measureWasiPhase("appendFstat", () => + Number(__agentOSFs().fstatSync(handle.targetFd).size ?? 0) + ); + } else { + entry.offset = (entry.offset ?? 0) + written; + } + } + return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); + } + if (handle?.kind === "guest-file" && typeof handle.targetFd === "number") { + const position = handle.append + ? this._measureWasiPhase("appendFstat", () => + Number(__agentOSFs().fstatSync(handle.targetFd).size ?? 0) + ) + : (handle.position ?? 0); + const written = this._measureWasiPhase("writeSync", () => + __agentOSFs().writeSync( + handle.targetFd, + bytes, + 0, + bytes.length, + position, + ) + ); + if (handle.append) { + handle.position = this._measureWasiPhase("appendFstat", () => + Number(__agentOSFs().fstatSync(handle.targetFd).size ?? 0) + ); } else { - iterator.call(receiver, array[i], i, array); + handle.position = (handle.position ?? 0) + written; + } + return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); + } + if (handle?.kind === "stdio" && typeof handle.targetFd === "number") { + const targetFd = Number(handle.targetFd) >>> 0; + if (targetFd === 1 || targetFd === 2) { + const sidecarManagedProcess = + typeof process?.env?.AGENTOS_SANDBOX_ROOT === "string" && + process.env.AGENTOS_SANDBOX_ROOT.length > 0; + const useKernelStdioSyncRpc = + sidecarManagedProcess || __agentOSKernelStdioSyncRpcEnabled(); + const written = useKernelStdioSyncRpc + ? this._measureWasiPhase("kernelStdioWrite", () => + Number(__agentOSWasiSyncRpc().callSync("__kernel_stdio_write", [targetFd, bytes])) >>> 0 + ) + : this._measureWasiPhase("processStreamWrite", () => + (targetFd === 2 ? process.stderr.write(bytes) : process.stdout.write(bytes), bytes.length) + ); + return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); } + return __agentOSWasiErrnoBadf; + } + const entry = this.fdTable.get(descriptor); + if (!entry) { + return __agentOSWasiErrnoBadf; + } + if (entry.kind === "stdout") { + const sidecarManagedProcess = + typeof process?.env?.AGENTOS_SANDBOX_ROOT === "string" && + process.env.AGENTOS_SANDBOX_ROOT.length > 0; + const useKernelStdioSyncRpc = + sidecarManagedProcess || __agentOSKernelStdioSyncRpcEnabled(); + const written = useKernelStdioSyncRpc + ? this._measureWasiPhase("kernelStdioWrite", () => + Number(__agentOSWasiSyncRpc().callSync("__kernel_stdio_write", [1, bytes])) >>> 0 + ) + : this._measureWasiPhase("processStreamWrite", () => + (process.stdout.write(bytes), bytes.length) + ); + return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); } - } - }; - var forEachString = function forEachString2(string, iterator, receiver) { - for (var i = 0, len = string.length; i < len; i++) { - if (receiver == null) { - iterator(string.charAt(i), i, string); - } else { - iterator.call(receiver, string.charAt(i), i, string); + if (entry.kind === "stderr") { + const sidecarManagedProcess = + typeof process?.env?.AGENTOS_SANDBOX_ROOT === "string" && + process.env.AGENTOS_SANDBOX_ROOT.length > 0; + const useKernelStdioSyncRpc = + sidecarManagedProcess || __agentOSKernelStdioSyncRpcEnabled(); + const written = useKernelStdioSyncRpc + ? this._measureWasiPhase("kernelStdioWrite", () => + Number(__agentOSWasiSyncRpc().callSync("__kernel_stdio_write", [2, bytes])) >>> 0 + ) + : this._measureWasiPhase("processStreamWrite", () => + (process.stderr.write(bytes), bytes.length) + ); + return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); } - } - }; - var forEachObject = function forEachObject2(object, iterator, receiver) { - for (var k in object) { - if (hasOwnProperty.call(object, k)) { - if (receiver == null) { - iterator(object[k], k, object); - } else { - iterator.call(receiver, object[k], k, object); + if (entry.readOnly === true) { + return __agentOSWasiErrnoRofs; + } + if (entry.kind === "file") { + this._clearStatCache(); + const position = entry.append + ? this._measureWasiPhase("appendFstat", () => + Number(__agentOSFs().fstatSync(entry.realFd).size ?? 0) + ) + : (typeof entry.offset === "number" ? entry.offset : null); + const written = this._measureWasiPhase("writeSync", () => + __agentOSFs().writeSync( + entry.realFd, + bytes, + 0, + bytes.length, + position, + ) + ); + if (entry.append) { + entry.offset = this._measureWasiPhase("appendFstat", () => + Number(__agentOSFs().fstatSync(entry.realFd).size ?? 0) + ); + } else if (typeof entry.offset === "number") { + entry.offset += written; } + return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); } + return __agentOSWasiErrnoBadf; + } catch (error) { + return this._mapFsError(error); } - }; - function isArray(x) { - return toStr.call(x) === "[object Array]"; } - module2.exports = function forEach(list, iterator, thisArg) { - if (!isCallable(iterator)) { - throw new TypeError("iterator must be a function"); - } - var receiver; - if (arguments.length >= 3) { - receiver = thisArg; - } - if (isArray(list)) { - forEachArray(list, iterator, receiver); - } else if (typeof list === "string") { - forEachString(list, iterator, receiver); - } else { - forEachObject(list, iterator, receiver); - } - }; - } -}); - -// node_modules/.pnpm/possible-typed-array-names@1.1.0/node_modules/possible-typed-array-names/index.js -var require_possible_typed_array_names = __commonJS({ - "node_modules/.pnpm/possible-typed-array-names@1.1.0/node_modules/possible-typed-array-names/index.js"(exports2, module2) { - "use strict"; - module2.exports = [ - "Float16Array", - "Float32Array", - "Float64Array", - "Int8Array", - "Int16Array", - "Int32Array", - "Uint8Array", - "Uint8ClampedArray", - "Uint16Array", - "Uint32Array", - "BigInt64Array", - "BigUint64Array" - ]; - } -}); - -// node_modules/.pnpm/available-typed-arrays@1.0.7/node_modules/available-typed-arrays/index.js -var require_available_typed_arrays = __commonJS({ - "node_modules/.pnpm/available-typed-arrays@1.0.7/node_modules/available-typed-arrays/index.js"(exports2, module2) { - "use strict"; - var possibleNames = require_possible_typed_array_names(); - var g = typeof globalThis === "undefined" ? global : globalThis; - module2.exports = function availableTypedArrays() { - var out = []; - for (var i = 0; i < possibleNames.length; i++) { - if (typeof g[possibleNames[i]] === "function") { - out[out.length] = possibleNames[i]; - } - } - return out; - }; - } -}); - -// node_modules/.pnpm/define-data-property@1.1.4/node_modules/define-data-property/index.js -var require_define_data_property = __commonJS({ - "node_modules/.pnpm/define-data-property@1.1.4/node_modules/define-data-property/index.js"(exports2, module2) { - "use strict"; - var $defineProperty = require_es_define_property(); - var $SyntaxError = require_syntax(); - var $TypeError = require_type(); - var gopd = require_gopd(); - module2.exports = function defineDataProperty(obj, property, value) { - if (!obj || typeof obj !== "object" && typeof obj !== "function") { - throw new $TypeError("\`obj\` must be an object or a function\`"); - } - if (typeof property !== "string" && typeof property !== "symbol") { - throw new $TypeError("\`property\` must be a string or a symbol\`"); - } - if (arguments.length > 3 && typeof arguments[3] !== "boolean" && arguments[3] !== null) { - throw new $TypeError("\`nonEnumerable\`, if provided, must be a boolean or null"); - } - if (arguments.length > 4 && typeof arguments[4] !== "boolean" && arguments[4] !== null) { - throw new $TypeError("\`nonWritable\`, if provided, must be a boolean or null"); - } - if (arguments.length > 5 && typeof arguments[5] !== "boolean" && arguments[5] !== null) { - throw new $TypeError("\`nonConfigurable\`, if provided, must be a boolean or null"); - } - if (arguments.length > 6 && typeof arguments[6] !== "boolean") { - throw new $TypeError("\`loose\`, if provided, must be a boolean"); - } - var nonEnumerable = arguments.length > 3 ? arguments[3] : null; - var nonWritable = arguments.length > 4 ? arguments[4] : null; - var nonConfigurable = arguments.length > 5 ? arguments[5] : null; - var loose = arguments.length > 6 ? arguments[6] : false; - var desc = !!gopd && gopd(obj, property); - if ($defineProperty) { - $defineProperty(obj, property, { - configurable: nonConfigurable === null && desc ? desc.configurable : !nonConfigurable, - enumerable: nonEnumerable === null && desc ? desc.enumerable : !nonEnumerable, - value, - writable: nonWritable === null && desc ? desc.writable : !nonWritable - }); - } else if (loose || !nonEnumerable && !nonWritable && !nonConfigurable) { - obj[property] = value; - } else { - throw new $SyntaxError("This environment does not support defining a property as non-configurable, non-writable, or non-enumerable."); - } - }; - } -}); -// node_modules/.pnpm/has-property-descriptors@1.0.2/node_modules/has-property-descriptors/index.js -var require_has_property_descriptors = __commonJS({ - "node_modules/.pnpm/has-property-descriptors@1.0.2/node_modules/has-property-descriptors/index.js"(exports2, module2) { - "use strict"; - var $defineProperty = require_es_define_property(); - var hasPropertyDescriptors = function hasPropertyDescriptors2() { - return !!$defineProperty; - }; - hasPropertyDescriptors.hasArrayLengthDefineBug = function hasArrayLengthDefineBug() { - if (!$defineProperty) { + _positionedIoOffset(offset) { + const explicitOffset = Number(offset); + if (!Number.isFinite(explicitOffset) || explicitOffset < 0) { return null; } - try { - return $defineProperty([], "length", { value: 1 }).length !== 1; - } catch (e) { - return true; - } - }; - module2.exports = hasPropertyDescriptors; - } -}); + return explicitOffset; + } -// node_modules/.pnpm/set-function-length@1.2.2/node_modules/set-function-length/index.js -var require_set_function_length = __commonJS({ - "node_modules/.pnpm/set-function-length@1.2.2/node_modules/set-function-length/index.js"(exports2, module2) { - "use strict"; - var GetIntrinsic = require_get_intrinsic(); - var define = require_define_data_property(); - var hasDescriptors = require_has_property_descriptors()(); - var gOPD = require_gopd(); - var $TypeError = require_type(); - var $floor = GetIntrinsic("%Math.floor%"); - module2.exports = function setFunctionLength(fn, length) { - if (typeof fn !== "function") { - throw new $TypeError("\`fn\` is not a function"); - } - if (typeof length !== "number" || length < 0 || length > 4294967295 || $floor(length) !== length) { - throw new $TypeError("\`length\` must be a positive 32-bit integer"); - } - var loose = arguments.length > 2 && !!arguments[2]; - var functionLengthIsConfigurable = true; - var functionLengthIsWritable = true; - if ("length" in fn && gOPD) { - var desc = gOPD(fn, "length"); - if (desc && !desc.configurable) { - functionLengthIsConfigurable = false; - } - if (desc && !desc.writable) { - functionLengthIsWritable = false; + _fdPwrite(fd, iovs, iovsLen, offset, nwrittenPtr) { + try { + const bytes = this._collectIovs(iovs, iovsLen); + const descriptor = Number(fd) >>> 0; + const explicitOffset = this._positionedIoOffset(offset); + if (explicitOffset === null) { + return __agentOSWasiErrnoInval; } - } - if (functionLengthIsConfigurable || functionLengthIsWritable || !loose) { - if (hasDescriptors) { - define( - /** @type {Parameters[0]} */ - fn, - "length", - length, - true, - true - ); - } else { - define( - /** @type {Parameters[0]} */ - fn, - "length", - length + const handle = this._externalFdHandle(descriptor); + if ( + (handle?.kind === "passthrough" || handle?.kind === "host-passthrough") && + typeof handle.targetFd === "number" + ) { + if (handle.readOnly === true) { + return __agentOSWasiErrnoRofs; + } + const written = __agentOSFs().writeSync( + handle.targetFd, + bytes, + 0, + bytes.length, + explicitOffset, ); + return this._writeUint32(nwrittenPtr, written); + } + const entry = this.fdTable.get(descriptor); + if (!entry || entry.kind !== "file") { + return __agentOSWasiErrnoBadf; + } + if (entry.readOnly === true) { + return __agentOSWasiErrnoRofs; } + const written = __agentOSFs().writeSync( + entry.realFd, + bytes, + 0, + bytes.length, + explicitOffset, + ); + return this._writeUint32(nwrittenPtr, written); + } catch { + return __agentOSWasiErrnoFault; } - return fn; - }; - } -}); - -// node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/applyBind.js -var require_applyBind = __commonJS({ - "node_modules/.pnpm/call-bind-apply-helpers@1.0.2/node_modules/call-bind-apply-helpers/applyBind.js"(exports2, module2) { - "use strict"; - var bind = require_function_bind(); - var $apply = require_functionApply(); - var actualApply = require_actualApply(); - module2.exports = function applyBind() { - return actualApply(bind, $apply, arguments); - }; - } -}); - -// node_modules/.pnpm/call-bind@1.0.8/node_modules/call-bind/index.js -var require_call_bind = __commonJS({ - "node_modules/.pnpm/call-bind@1.0.8/node_modules/call-bind/index.js"(exports2, module2) { - "use strict"; - var setFunctionLength = require_set_function_length(); - var $defineProperty = require_es_define_property(); - var callBindBasic = require_call_bind_apply_helpers(); - var applyBind = require_applyBind(); - module2.exports = function callBind(originalFunction) { - var func = callBindBasic(arguments); - var adjustedLength = originalFunction.length - (arguments.length - 1); - return setFunctionLength( - func, - 1 + (adjustedLength > 0 ? adjustedLength : 0), - true - ); - }; - if ($defineProperty) { - $defineProperty(module2.exports, "apply", { value: applyBind }); - } else { - module2.exports.apply = applyBind; } - } -}); -// node_modules/.pnpm/which-typed-array@1.1.20/node_modules/which-typed-array/index.js -var require_which_typed_array = __commonJS({ - "node_modules/.pnpm/which-typed-array@1.1.20/node_modules/which-typed-array/index.js"(exports2, module2) { - "use strict"; - var forEach = require_for_each(); - var availableTypedArrays = require_available_typed_arrays(); - var callBind = require_call_bind(); - var callBound = require_call_bound(); - var gOPD = require_gopd(); - var getProto = require_get_proto(); - var $toString = callBound("Object.prototype.toString"); - var hasToStringTag = require_shams2()(); - var g = typeof globalThis === "undefined" ? global : globalThis; - var typedArrays = availableTypedArrays(); - var $slice = callBound("String.prototype.slice"); - var $indexOf = callBound("Array.prototype.indexOf", true) || function indexOf(array, value) { - for (var i = 0; i < array.length; i += 1) { - if (array[i] === value) { - return i; + _fdPread(fd, iovs, iovsLen, offset, nreadPtr) { + try { + const descriptor = Number(fd) >>> 0; + const explicitOffset = this._positionedIoOffset(offset); + if (explicitOffset === null) { + return __agentOSWasiErrnoInval; + } + const totalLength = this._boundedReadLength(iovs, iovsLen); + const buffer = Buffer.alloc(totalLength); + const handle = this._externalFdHandle(descriptor); + if ( + (handle?.kind === "passthrough" || handle?.kind === "host-passthrough") && + typeof handle.targetFd === "number" + ) { + const bytesRead = __agentOSFs().readSync( + handle.targetFd, + buffer, + 0, + totalLength, + explicitOffset, + ); + const written = this._writeToIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)); + return this._writeUint32(nreadPtr, written); + } + const entry = this.fdTable.get(descriptor); + if (!entry || entry.kind !== "file") { + return __agentOSWasiErrnoBadf; } + const bytesRead = __agentOSFs().readSync( + entry.realFd, + buffer, + 0, + totalLength, + explicitOffset, + ); + const written = this._writeToIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)); + return this._writeUint32(nreadPtr, written); + } catch { + return __agentOSWasiErrnoFault; } - return -1; - }; - var cache = { __proto__: null }; - if (hasToStringTag && gOPD && getProto) { - forEach(typedArrays, function(typedArray) { - var arr = new g[typedArray](); - if (Symbol.toStringTag in arr && getProto) { - var proto = getProto(arr); - var descriptor = gOPD(proto, Symbol.toStringTag); - if (!descriptor && proto) { - var superProto = getProto(proto); - descriptor = gOPD(superProto, Symbol.toStringTag); + } + + _fdRead(fd, iovs, iovsLen, nreadPtr) { + try { + const descriptor = Number(fd) >>> 0; + const handle = this._externalFdHandle(descriptor); + if (handle?.kind === "pipe-read" && handle.pipe) { + const totalLength = this._boundedReadLength(iovs, iovsLen); + while (handle.pipe.chunks.length === 0) { + if (handle.pipe.writeHandleCount === 0 && handle.pipe.producers.size === 0) { + return this._writeUint32(nreadPtr, 0); + } + this._pumpPipeProducers(handle.pipe, 10); } - if (descriptor && descriptor.get) { - var bound = callBind(descriptor.get); - cache[ - /** @type {\`$\${import('.').TypedArrayName}\`} */ - "$" + typedArray - ] = bound; + const chunk = this._dequeuePipeBytes(handle.pipe, totalLength); + const written = this._writeToIovs(iovs, iovsLen, chunk); + return this._writeUint32(nreadPtr, written); + } + if (handle?.kind === "stdio" && Number(handle.targetFd) === 0) { + const totalLength = this._boundedReadLength(iovs, iovsLen); + if (typeof __agentOSWasiHost.readStdin === "function") { + const value = __agentOSWasiHost.readStdin(totalLength); + if (value == null) { + return this._writeUint32(nreadPtr, 0); + } + const chunk = + typeof value === "string" + ? Buffer.from(value, "utf8") + : value instanceof Uint8Array + ? value + : Buffer.from(value); + if (chunk.length === 0) { + return this._writeUint32(nreadPtr, 0); + } + const written = this._writeToIovs(iovs, iovsLen, chunk); + return this._writeUint32(nreadPtr, written); } + const buffer = Buffer.alloc(totalLength); + const bytesRead = __agentOSFs().readSync(0, buffer, 0, totalLength, null); + const written = this._writeToIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)); + return this._writeUint32(nreadPtr, written); } - }); - } else { - forEach(typedArrays, function(typedArray) { - var arr = new g[typedArray](); - var fn = arr.slice || arr.set; - if (fn) { - var bound = ( - /** @type {import('./types').BoundSlice | import('./types').BoundSet} */ - // @ts-expect-error TODO FIXME - callBind(fn) - ); - cache[ - /** @type {\`$\${import('.').TypedArrayName}\`} */ - "$" + typedArray - ] = bound; + const entry = this.fdTable.get(descriptor); + if (!entry) { + return __agentOSWasiErrnoBadf; } - }); - } - var tryTypedArrays = function tryAllTypedArrays(value) { - var found = false; - forEach( - /** @type {Record<\`\\$\${import('.').TypedArrayName}\`, Getter>} */ - cache, - /** @type {(getter: Getter, name: \`\\$\${import('.').TypedArrayName}\`) => void} */ - function(getter, typedArray) { - if (!found) { + if (entry.kind === "stdin") { + const totalLength = this._boundedReadLength(iovs, iovsLen); + const syncRpc = + typeof globalThis?.__agentOSSyncRpc?.callSync === "function" + ? __agentOSWasiSyncRpc() + : null; + const sidecarManagedProcess = + typeof process?.env?.AGENTOS_SANDBOX_ROOT === "string" && + process.env.AGENTOS_SANDBOX_ROOT.length > 0; + if (syncRpc && (sidecarManagedProcess || __agentOSKernelStdioSyncRpcEnabled())) { try { - if ("$" + getter(value) === typedArray) { - found = /** @type {import('.').TypedArrayName} */ - $slice(typedArray, 1); + let chunk = null; + while (true) { + const response = syncRpc.callSync("__kernel_stdin_read", [totalLength, 10]); + if ( + response && + typeof response === "object" && + typeof response.dataBase64 === "string" + ) { + chunk = Buffer.from(response.dataBase64, "base64"); + break; + } + if (response && typeof response === "object" && response.done === true) { + chunk = Buffer.alloc(0); + break; + } + if ( + typeof Atomics?.wait === "function" && + typeof syntheticWaitArray !== "undefined" + ) { + Atomics.wait(syntheticWaitArray, 0, 0, 10); + } } - } catch (e) { + if (!chunk || chunk.length === 0) { + return this._writeUint32(nreadPtr, 0); + } + const written = this._writeToIovs(iovs, iovsLen, chunk); + return this._writeUint32(nreadPtr, written); + } catch { + // Fall back to direct stdin reads when the sync bridge is unavailable + // in the standalone runner bootstrap. + } + } + // Host-seam stdin (a non-native backend whose stdin is delivered through + // the runtime process object, not a kernel fd): read the queued bytes + // directly instead of fs.readSync on a descriptor the JS fs table does + // not own. + if (typeof __agentOSWasiHost.readStdin === "function") { + const value = __agentOSWasiHost.readStdin(totalLength); + if (value == null) { + return this._writeUint32(nreadPtr, 0); + } + const chunk = + typeof value === "string" + ? Buffer.from(value, "utf8") + : value instanceof Uint8Array + ? value + : Buffer.from(value); + if (chunk.length === 0) { + return this._writeUint32(nreadPtr, 0); } + const written = this._writeToIovs(iovs, iovsLen, chunk); + return this._writeUint32(nreadPtr, written); } + const buffer = Buffer.alloc(totalLength); + const directStdinFd = + (handle?.kind === "passthrough" || handle?.kind === "host-passthrough") && + typeof handle.targetFd === "number" + ? handle.targetFd + : typeof process?.stdin?.fd === "number" + ? process.stdin.fd + : 0; + const bytesRead = __agentOSFs().readSync( + directStdinFd, + buffer, + 0, + totalLength, + null, + ); + const written = this._writeToIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)); + return this._writeUint32(nreadPtr, written); } - ); - return found; - }; - var trySlices = function tryAllSlices(value) { - var found = false; - forEach( - /** @type {Record<\`\\$\${import('.').TypedArrayName}\`, Getter>} */ - cache, - /** @type {(getter: Getter, name: \`\\$\${import('.').TypedArrayName}\`) => void} */ - function(getter, name) { - if (!found) { - try { - getter(value); - found = /** @type {import('.').TypedArrayName} */ - $slice(name, 1); - } catch (e) { - } + if ( + (handle?.kind === "passthrough" || handle?.kind === "host-passthrough") && + typeof handle.targetFd === "number" + ) { + const localEntry = this._descriptorEntry(descriptor); + const localHostPassthrough = + handle.kind === "host-passthrough" && + localEntry?.kind === "file" && + localEntry.realFd === handle.targetFd; + const totalLength = this._boundedReadLength(iovs, iovsLen); + const buffer = Buffer.alloc(totalLength); + const bytesRead = __agentOSFs().readSync( + handle.targetFd, + buffer, + 0, + totalLength, + localHostPassthrough ? (localEntry.offset ?? 0) : null, + ); + if (localHostPassthrough) { + localEntry.offset = (localEntry.offset ?? 0) + bytesRead; } + const written = this._writeToIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)); + return this._writeUint32(nreadPtr, written); } - ); - return found; - }; - module2.exports = function whichTypedArray(value) { - if (!value || typeof value !== "object") { - return false; - } - if (!hasToStringTag) { - var tag = $slice($toString(value), 8, -1); - if ($indexOf(typedArrays, tag) > -1) { - return tag; + if (entry.kind !== "file") { + return __agentOSWasiErrnoBadf; } - if (tag !== "Object") { - return false; + // WASI rights: a descriptor opened without FD_READ cannot be read. + if ( + typeof entry.rightsBase === "bigint" && + (entry.rightsBase & __agentOSWasiRightFdRead) === 0n + ) { + return __agentOSWasiErrnoNotcapable; } - return trySlices(value); - } - if (!gOPD) { - return null; + const totalLength = this._boundedReadLength(iovs, iovsLen); + const buffer = Buffer.alloc(totalLength); + const position = typeof entry.offset === "number" ? entry.offset : null; + const bytesRead = __agentOSFs().readSync( + entry.realFd, + buffer, + 0, + totalLength, + position, + ); + if (typeof entry.offset === "number") { + entry.offset += bytesRead; + } + const written = this._writeToIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)); + return this._writeUint32(nreadPtr, written); + } catch (error) { + return this._mapFsError(error); } - return tryTypedArrays(value); - }; - } -}); - -// node_modules/.pnpm/is-typed-array@1.1.15/node_modules/is-typed-array/index.js -var require_is_typed_array = __commonJS({ - "node_modules/.pnpm/is-typed-array@1.1.15/node_modules/is-typed-array/index.js"(exports2, module2) { - "use strict"; - var whichTypedArray = require_which_typed_array(); - module2.exports = function isTypedArray(value) { - return !!whichTypedArray(value); - }; - } -}); - -// node_modules/.pnpm/util@0.12.5/node_modules/util/support/types.js -var require_types = __commonJS({ - "node_modules/.pnpm/util@0.12.5/node_modules/util/support/types.js"(exports2) { - "use strict"; - var isArgumentsObject = require_is_arguments(); - var isGeneratorFunction = require_is_generator_function(); - var whichTypedArray = require_which_typed_array(); - var isTypedArray = require_is_typed_array(); - function uncurryThis(f) { - return f.call.bind(f); - } - var BigIntSupported = typeof BigInt !== "undefined"; - var SymbolSupported = typeof Symbol !== "undefined"; - var ObjectToString = uncurryThis(Object.prototype.toString); - var numberValue = uncurryThis(Number.prototype.valueOf); - var stringValue = uncurryThis(String.prototype.valueOf); - var booleanValue = uncurryThis(Boolean.prototype.valueOf); - if (BigIntSupported) { - bigIntValue = uncurryThis(BigInt.prototype.valueOf); - } - var bigIntValue; - if (SymbolSupported) { - symbolValue = uncurryThis(Symbol.prototype.valueOf); } - var symbolValue; - function checkBoxedPrimitive(value, prototypeValueOf) { - if (typeof value !== "object") { - return false; - } + + _fdClose(fd) { try { - prototypeValueOf(value); - return true; - } catch (e) { - return false; - } - } - exports2.isArgumentsObject = isArgumentsObject; - exports2.isGeneratorFunction = isGeneratorFunction; - exports2.isTypedArray = isTypedArray; - function isPromise(input) { - return typeof Promise !== "undefined" && input instanceof Promise || input !== null && typeof input === "object" && typeof input.then === "function" && typeof input.catch === "function"; - } - exports2.isPromise = isPromise; - function isArrayBufferView(value) { - if (typeof ArrayBuffer !== "undefined" && ArrayBuffer.isView) { - return ArrayBuffer.isView(value); - } - return isTypedArray(value) || isDataView(value); - } - exports2.isArrayBufferView = isArrayBufferView; - function isUint8Array(value) { - return whichTypedArray(value) === "Uint8Array"; - } - exports2.isUint8Array = isUint8Array; - function isUint8ClampedArray(value) { - return whichTypedArray(value) === "Uint8ClampedArray"; - } - exports2.isUint8ClampedArray = isUint8ClampedArray; - function isUint16Array(value) { - return whichTypedArray(value) === "Uint16Array"; - } - exports2.isUint16Array = isUint16Array; - function isUint32Array(value) { - return whichTypedArray(value) === "Uint32Array"; - } - exports2.isUint32Array = isUint32Array; - function isInt8Array(value) { - return whichTypedArray(value) === "Int8Array"; - } - exports2.isInt8Array = isInt8Array; - function isInt16Array(value) { - return whichTypedArray(value) === "Int16Array"; - } - exports2.isInt16Array = isInt16Array; - function isInt32Array(value) { - return whichTypedArray(value) === "Int32Array"; - } - exports2.isInt32Array = isInt32Array; - function isFloat32Array(value) { - return whichTypedArray(value) === "Float32Array"; - } - exports2.isFloat32Array = isFloat32Array; - function isFloat64Array(value) { - return whichTypedArray(value) === "Float64Array"; - } - exports2.isFloat64Array = isFloat64Array; - function isBigInt64Array(value) { - return whichTypedArray(value) === "BigInt64Array"; - } - exports2.isBigInt64Array = isBigInt64Array; - function isBigUint64Array(value) { - return whichTypedArray(value) === "BigUint64Array"; - } - exports2.isBigUint64Array = isBigUint64Array; - function isMapToString(value) { - return ObjectToString(value) === "[object Map]"; - } - isMapToString.working = typeof Map !== "undefined" && isMapToString(/* @__PURE__ */ new Map()); - function isMap(value) { - if (typeof Map === "undefined") { - return false; + const descriptor = Number(fd) >>> 0; + const handle = this._externalFdHandle(descriptor); + if (handle?.kind === "pipe-read" && handle.pipe) { + handle.open = false; + handle.pipe.readHandleCount = Math.max(0, (handle.pipe.readHandleCount ?? 0) - 1); + if (typeof handle.onClose === "function") { + handle.onClose(handle, descriptor); + } + return __agentOSWasiErrnoSuccess; + } + if (handle?.kind === "pipe-write" && handle.pipe) { + handle.open = false; + handle.pipe.writeHandleCount = Math.max(0, (handle.pipe.writeHandleCount ?? 0) - 1); + if (typeof handle.onClose === "function") { + handle.onClose(handle, descriptor); + } + return __agentOSWasiErrnoSuccess; + } + if (handle?.kind === "guest-file" || handle?.kind === "stdio") { + handle.open = false; + return __agentOSWasiErrnoSuccess; + } + const entry = this.fdTable.get(descriptor); + if (!entry) { + return __agentOSWasiErrnoBadf; + } + const retainedDelegateRefs = (() => { + try { + if (typeof globalThis.__agentOSWasiDelegateFdRefCount === "function") { + return Number(globalThis.__agentOSWasiDelegateFdRefCount(descriptor)) || 0; + } + } catch { + // Fall through to the default close path. + } + return 0; + })(); + if (entry.kind === "file" && retainedDelegateRefs <= 0) { + __agentOSFs().closeSync(entry.realFd); + } + if (descriptor > 2 && retainedDelegateRefs <= 0) { + this.fdTable.delete(descriptor); + } + return __agentOSWasiErrnoSuccess; + } catch { + return __agentOSWasiErrnoFault; } - return isMapToString.working ? isMapToString(value) : value instanceof Map; - } - exports2.isMap = isMap; - function isSetToString(value) { - return ObjectToString(value) === "[object Set]"; } - isSetToString.working = typeof Set !== "undefined" && isSetToString(/* @__PURE__ */ new Set()); - function isSet(value) { - if (typeof Set === "undefined") { - return false; + + _fdSync(fd) { + try { + const descriptor = Number(fd) >>> 0; + const handle = this._externalFdHandle(descriptor); + if ( + (handle?.kind === "passthrough" || handle?.kind === "host-passthrough") && + typeof handle.targetFd === "number" + ) { + __agentOSFs().fsyncSync(handle.targetFd); + return __agentOSWasiErrnoSuccess; + } + const entry = this.fdTable.get(descriptor); + if (!entry) { + return __agentOSWasiErrnoBadf; + } + // fsync on a stdio stream (stdin/stdout/stderr) is a no-op success; only + // descriptors with a real backing fd are flushed. + if ( + entry.kind === "stdin" || + entry.kind === "stdout" || + entry.kind === "stderr" + ) { + return __agentOSWasiErrnoSuccess; + } + if (entry.kind !== "file" || typeof entry.realFd !== "number") { + return __agentOSWasiErrnoBadf; + } + __agentOSFs().fsyncSync(entry.realFd); + return __agentOSWasiErrnoSuccess; + } catch { + return __agentOSWasiErrnoFault; } - return isSetToString.working ? isSetToString(value) : value instanceof Set; - } - exports2.isSet = isSet; - function isWeakMapToString(value) { - return ObjectToString(value) === "[object WeakMap]"; } - isWeakMapToString.working = typeof WeakMap !== "undefined" && isWeakMapToString(/* @__PURE__ */ new WeakMap()); - function isWeakMap(value) { - if (typeof WeakMap === "undefined") { - return false; + + _fdFdstatGet(fd, statPtr) { + try { + const entry = this._descriptorEntry(fd); + if (!entry) { + return __agentOSWasiErrnoBadf; + } + const view = this._memoryView(); + const offset = Number(statPtr) >>> 0; + view.setUint8(offset, this._fdFiletype(entry)); + view.setUint16(offset + 2, (Number(entry.fdFlags) >>> 0) & 0xffff, true); + view.setBigUint64(offset + 8, this._descriptorRightsBase(entry), true); + view.setBigUint64(offset + 16, this._descriptorRightsInheriting(entry), true); + return __agentOSWasiErrnoSuccess; + } catch { + return __agentOSWasiErrnoFault; } - return isWeakMapToString.working ? isWeakMapToString(value) : value instanceof WeakMap; - } - exports2.isWeakMap = isWeakMap; - function isWeakSetToString(value) { - return ObjectToString(value) === "[object WeakSet]"; } - isWeakSetToString.working = typeof WeakSet !== "undefined" && isWeakSetToString(/* @__PURE__ */ new WeakSet()); - function isWeakSet(value) { - return isWeakSetToString(value); - } - exports2.isWeakSet = isWeakSet; - function isArrayBufferToString(value) { - return ObjectToString(value) === "[object ArrayBuffer]"; - } - isArrayBufferToString.working = typeof ArrayBuffer !== "undefined" && isArrayBufferToString(new ArrayBuffer()); - function isArrayBuffer(value) { - if (typeof ArrayBuffer === "undefined") { - return false; + + _fdFdstatSetFlags(fd, flags) { + try { + const entry = this._descriptorEntry(fd); + if (!entry) { + return __agentOSWasiErrnoBadf; + } + entry.fdFlags = (Number(flags) >>> 0) & 0xffff; + return __agentOSWasiErrnoSuccess; + } catch { + return __agentOSWasiErrnoFault; } - return isArrayBufferToString.working ? isArrayBufferToString(value) : value instanceof ArrayBuffer; } - exports2.isArrayBuffer = isArrayBuffer; - function isDataViewToString(value) { - return ObjectToString(value) === "[object DataView]"; - } - isDataViewToString.working = typeof ArrayBuffer !== "undefined" && typeof DataView !== "undefined" && isDataViewToString(new DataView(new ArrayBuffer(1), 0, 1)); - function isDataView(value) { - if (typeof DataView === "undefined") { - return false; + + _fdFilestatGet(fd, statPtr) { + try { + const entry = this._descriptorEntry(fd); + if (!entry) { + return __agentOSWasiErrnoBadf; + } + if ( + entry.kind === "stdin" || + entry.kind === "stdout" || + entry.kind === "stderr" + ) { + return this._writeFilestat(statPtr, null, __agentOSWasiFiletypeCharacterDevice); + } + if (entry.kind === "preopen") { + const stats = __agentOSFs().statSync(entry.guestPath); + return this._writeFilestat(statPtr, stats, __agentOSWasiFiletypeDirectory); + } + const stats = + typeof entry.realFd === "number" + ? __agentOSFs().fstatSync(entry.realFd) + : __agentOSFs().statSync(this._descriptorFsPath(entry)); + return this._writeFilestat(statPtr, stats, this._fdFiletype(entry)); + } catch (error) { + return this._mapFsError(error); } - return isDataViewToString.working ? isDataViewToString(value) : value instanceof DataView; - } - exports2.isDataView = isDataView; - var SharedArrayBufferCopy = typeof SharedArrayBuffer !== "undefined" ? SharedArrayBuffer : void 0; - function isSharedArrayBufferToString(value) { - return ObjectToString(value) === "[object SharedArrayBuffer]"; } - function isSharedArrayBuffer(value) { - if (typeof SharedArrayBufferCopy === "undefined") { - return false; - } - if (typeof isSharedArrayBufferToString.working === "undefined") { - isSharedArrayBufferToString.working = isSharedArrayBufferToString(new SharedArrayBufferCopy()); + + _fdFilestatSetSize(fd, size) { + try { + const entry = this._descriptorEntry(fd); + if (!entry || entry.kind !== "file" || typeof entry.realFd !== "number") { + return __agentOSWasiErrnoBadf; + } + if (entry.readOnly === true) { + return __agentOSWasiErrnoRofs; + } + __agentOSFs().ftruncateSync(entry.realFd, Number(size)); + return __agentOSWasiErrnoSuccess; + } catch (error) { + return this._mapFsError(error); } - return isSharedArrayBufferToString.working ? isSharedArrayBufferToString(value) : value instanceof SharedArrayBufferCopy; - } - exports2.isSharedArrayBuffer = isSharedArrayBuffer; - function isAsyncFunction(value) { - return ObjectToString(value) === "[object AsyncFunction]"; - } - exports2.isAsyncFunction = isAsyncFunction; - function isMapIterator(value) { - return ObjectToString(value) === "[object Map Iterator]"; - } - exports2.isMapIterator = isMapIterator; - function isSetIterator(value) { - return ObjectToString(value) === "[object Set Iterator]"; - } - exports2.isSetIterator = isSetIterator; - function isGeneratorObject(value) { - return ObjectToString(value) === "[object Generator]"; - } - exports2.isGeneratorObject = isGeneratorObject; - function isWebAssemblyCompiledModule(value) { - return ObjectToString(value) === "[object WebAssembly.Module]"; - } - exports2.isWebAssemblyCompiledModule = isWebAssemblyCompiledModule; - function isNumberObject(value) { - return checkBoxedPrimitive(value, numberValue); - } - exports2.isNumberObject = isNumberObject; - function isStringObject(value) { - return checkBoxedPrimitive(value, stringValue); } - exports2.isStringObject = isStringObject; - function isBooleanObject(value) { - return checkBoxedPrimitive(value, booleanValue); - } - exports2.isBooleanObject = isBooleanObject; - function isBigIntObject(value) { - return BigIntSupported && checkBoxedPrimitive(value, bigIntValue); - } - exports2.isBigIntObject = isBigIntObject; - function isSymbolObject(value) { - return SymbolSupported && checkBoxedPrimitive(value, symbolValue); - } - exports2.isSymbolObject = isSymbolObject; - function isBoxedPrimitive(value) { - return isNumberObject(value) || isStringObject(value) || isBooleanObject(value) || isBigIntObject(value) || isSymbolObject(value); - } - exports2.isBoxedPrimitive = isBoxedPrimitive; - function isAnyArrayBuffer(value) { - return typeof Uint8Array !== "undefined" && (isArrayBuffer(value) || isSharedArrayBuffer(value)); - } - exports2.isAnyArrayBuffer = isAnyArrayBuffer; - ["isProxy", "isExternal", "isModuleNamespaceObject"].forEach(function(method) { - Object.defineProperty(exports2, method, { - enumerable: false, - value: function() { - throw new Error(method + " is not supported in userland"); - } - }); - }); - } -}); - -// node_modules/.pnpm/util@0.12.5/node_modules/util/support/isBufferBrowser.js -var require_isBufferBrowser = __commonJS({ - "node_modules/.pnpm/util@0.12.5/node_modules/util/support/isBufferBrowser.js"(exports2, module2) { - module2.exports = function isBuffer(arg) { - return arg && typeof arg === "object" && typeof arg.copy === "function" && typeof arg.fill === "function" && typeof arg.readUInt8 === "function"; - }; - } -}); -// node_modules/.pnpm/inherits@2.0.4/node_modules/inherits/inherits_browser.js -var require_inherits_browser = __commonJS({ - "node_modules/.pnpm/inherits@2.0.4/node_modules/inherits/inherits_browser.js"(exports2, module2) { - if (typeof Object.create === "function") { - module2.exports = function inherits(ctor, superCtor) { - if (superCtor) { - ctor.super_ = superCtor; - ctor.prototype = Object.create(superCtor.prototype, { - constructor: { - value: ctor, - enumerable: false, - writable: true, - configurable: true - } - }); + _fdSeek(fd, offset, whence, newOffsetPtr) { + try { + const entry = this._descriptorEntry(fd); + if (!entry || entry.kind !== "file" || typeof entry.realFd !== "number") { + return __agentOSWasiErrnoBadf; + } + const delta = Number(offset); + if (!Number.isFinite(delta)) { + return __agentOSWasiErrnoInval; } - }; - } else { - module2.exports = function inherits(ctor, superCtor) { - if (superCtor) { - ctor.super_ = superCtor; - var TempCtor = function() { - }; - TempCtor.prototype = superCtor.prototype; - ctor.prototype = new TempCtor(); - ctor.prototype.constructor = ctor; + const currentOffset = typeof entry.offset === "number" ? entry.offset : 0; + let nextOffset = 0; + switch (Number(whence) >>> 0) { + case __agentOSWasiWhenceSet: + nextOffset = delta; + break; + case __agentOSWasiWhenceCur: + nextOffset = currentOffset + delta; + break; + case __agentOSWasiWhenceEnd: { + const stats = __agentOSFs().fstatSync(entry.realFd); + nextOffset = Number(stats?.size ?? 0) + delta; + break; + } + default: + return __agentOSWasiErrnoInval; } - }; + if (!Number.isFinite(nextOffset) || nextOffset < 0) { + return __agentOSWasiErrnoInval; + } + entry.offset = nextOffset; + return this._writeUint64(newOffsetPtr, BigInt(nextOffset)); + } catch (error) { + return this._mapFsError(error); + } } - } -}); -// node_modules/.pnpm/util@0.12.5/node_modules/util/util.js -var require_util = __commonJS({ - "node_modules/.pnpm/util@0.12.5/node_modules/util/util.js"(exports2) { - var getOwnPropertyDescriptors = Object.getOwnPropertyDescriptors || function getOwnPropertyDescriptors2(obj) { - var keys = Object.keys(obj); - var descriptors = {}; - for (var i = 0; i < keys.length; i++) { - descriptors[keys[i]] = Object.getOwnPropertyDescriptor(obj, keys[i]); - } - return descriptors; - }; - var formatRegExp = /%[sdj%]/g; - exports2.format = function(f) { - if (!isString(f)) { - var objects = []; - for (var i = 0; i < arguments.length; i++) { - objects.push(inspect(arguments[i])); + _fdTell(fd, offsetPtr) { + try { + const entry = this._descriptorEntry(fd); + if (!entry || entry.kind !== "file") { + return __agentOSWasiErrnoBadf; } - return objects.join(" "); + const offset = typeof entry.offset === "number" ? entry.offset : 0; + return this._writeUint64(offsetPtr, BigInt(offset)); + } catch (error) { + return this._mapFsError(error); } - var i = 1; - var args = arguments; - var len = args.length; - var str = String(f).replace(formatRegExp, function(x2) { - if (x2 === "%%") return "%"; - if (i >= len) return x2; - switch (x2) { - case "%s": - return String(args[i++]); - case "%d": - return Number(args[i++]); - case "%j": - try { - return JSON.stringify(args[i++]); - } catch (_) { - return "[Circular]"; - } - default: - return x2; + } + + _fdPrestatGet(fd, prestatPtr) { + try { + const entry = this._descriptorEntry(fd); + if (!entry || entry.kind !== "preopen") { + return __agentOSWasiErrnoBadf; } - }); - for (var x = args[i]; i < len; x = args[++i]) { - if (isNull(x) || !isObject(x)) { - str += " " + x; - } else { - str += " " + inspect(x); + const guestPath = this._descriptorPreopenName(entry); + if (typeof guestPath !== "string") { + return __agentOSWasiErrnoBadf; } + const view = this._memoryView(); + const offset = Number(prestatPtr) >>> 0; + view.setUint8(offset, 0); + view.setUint32(offset + 4, Buffer.byteLength(guestPath), true); + return __agentOSWasiErrnoSuccess; + } catch { + return __agentOSWasiErrnoFault; } - return str; - }; - exports2.deprecate = function(fn, msg) { - if (typeof process !== "undefined" && process.noDeprecation === true) { - return fn; - } - if (typeof process === "undefined") { - return function() { - return exports2.deprecate(fn, msg).apply(this, arguments); - }; + } + + _fdPrestatDirName(fd, pathPtr, pathLen) { + try { + const entry = this._descriptorEntry(fd); + if (!entry || entry.kind !== "preopen") { + return __agentOSWasiErrnoBadf; + } + const guestPath = this._descriptorPreopenName(entry); + if (typeof guestPath !== "string") { + return __agentOSWasiErrnoBadf; + } + const bytes = Buffer.from(guestPath, "utf8"); + if ((Number(pathLen) >>> 0) < bytes.length) { + return __agentOSWasiErrnoFault; + } + return this._writeBytes(pathPtr, bytes); + } catch { + return __agentOSWasiErrnoFault; } - var warned = false; - function deprecated() { - if (!warned) { - if (process.throwDeprecation) { - throw new Error(msg); - } else if (process.traceDeprecation) { - console.trace(msg); - } else { - console.error(msg); + } + + _fdReaddir(fd, bufPtr, bufLen, cookie, bufUsedPtr) { + const startedAt = __agentOSWasiNow(); + const requestedCookie = Number(cookie) >>> 0; + const requestedBufLen = Number(bufLen) >>> 0; + try { + const entry = this._descriptorEntry(fd); + const fsPath = this._descriptorDirectoryFsPath(entry); + if ( + !entry || + (entry.kind !== "preopen" && entry.kind !== "directory") || + typeof fsPath !== "string" + ) { + return __agentOSWasiErrnoBadf; + } + let dirents = + requestedCookie > 0 && Array.isArray(entry.readdirCache) + ? entry.readdirCache + : null; + if (!dirents) { + dirents = __agentOSFs() + .readdirSync(fsPath, { withFileTypes: true }) + .map((entry) => + typeof entry === "string" + ? { name: entry, filetype: __agentOSWasiFiletypeUnknown } + : { + name: String(entry?.name ?? ""), + filetype: this._filetypeForDirent(entry), + } + ) + .sort((left, right) => left.name.localeCompare(right.name)); + entry.readdirCache = dirents; + } + const view = this._memoryView(); + const memory = this._memoryBytes(); + let offset = Number(bufPtr) >>> 0; + const limit = offset + requestedBufLen; + let used = 0; + let recordsReturned = 0; + let stoppedRecordTooLarge = false; + for (let index = requestedCookie; index < dirents.length; index += 1) { + const dirent = dirents[index]; + const name = typeof dirent === "string" ? dirent : String(dirent?.name ?? ""); + const filetype = + typeof dirent === "object" + ? Number(dirent?.filetype ?? __agentOSWasiFiletypeUnknown) >>> 0 + : __agentOSWasiFiletypeUnknown; + const nameBytes = Buffer.from(name, "utf8"); + const recordLen = 24 + nameBytes.length; + if (offset + recordLen > limit) { + const remaining = Math.max(0, limit - offset); + if (remaining > 0) { + const record = Buffer.alloc(recordLen); + const recordView = new DataView( + record.buffer, + record.byteOffset, + record.byteLength, + ); + recordView.setBigUint64(0, BigInt(index + 1), true); + recordView.setBigUint64(8, BigInt(index + 1), true); + recordView.setUint32(16, nameBytes.length, true); + recordView.setUint8( + 20, + filetype, + ); + record.set(nameBytes, 24); + memory.set(record.subarray(0, remaining), offset); + offset += remaining; + used += remaining; + } + stoppedRecordTooLarge = true; + break; } - warned = true; + view.setBigUint64(offset, BigInt(index + 1), true); + view.setBigUint64(offset + 8, BigInt(index + 1), true); + view.setUint32(offset + 16, nameBytes.length, true); + view.setUint8( + offset + 20, + filetype, + ); + memory.set(nameBytes, offset + 24); + offset += recordLen; + used += recordLen; + recordsReturned += 1; } - return fn.apply(this, arguments); - } - return deprecated; - }; - var debugs = {}; - var debugEnvRegex = /^$/; - if (process.env.NODE_DEBUG) { - debugEnv = process.env.NODE_DEBUG; - debugEnv = debugEnv.replace(/[|\\\\{}()[\\]^$+?.]/g, "\\\\$&").replace(/\\*/g, ".*").replace(/,/g, "$|^").toUpperCase(); - debugEnvRegex = new RegExp("^" + debugEnv + "$", "i"); + const result = this._writeUint32(bufUsedPtr, used); + this._recordWasiSyscallMetric("fd_readdir", startedAt, { + result, + fd: Number(fd) >>> 0, + cookie: requestedCookie, + bufLen: requestedBufLen, + used, + recordsReturned, + totalDirentsRead: dirents.length, + stoppedRecordTooLarge, + }); + return result; + } catch (error) { + this._recordWasiSyscallMetric("fd_readdir", startedAt, { + result: "error", + fd: Number(fd) >>> 0, + cookie: requestedCookie, + bufLen: requestedBufLen, + }); + return this._mapFsError(error); + } } - var debugEnv; - exports2.debuglog = function(set) { - set = set.toUpperCase(); - if (!debugs[set]) { - if (debugEnvRegex.test(set)) { - var pid = process.pid; - debugs[set] = function() { - var msg = exports2.format.apply(exports2, arguments); - console.error("%s %d: %s", set, pid, msg); - }; - } else { - debugs[set] = function() { - }; + + _pathCreateDirectory(fd, pathPtr, pathLen) { + try { + const resolved = this._resolveDescriptorPath(fd, pathPtr, pathLen, { + preferCreateParent: true, + }); + if (resolved.error !== __agentOSWasiErrnoSuccess) { + return resolved.error; } + if (resolved.readOnly) { + return __agentOSWasiErrnoRofs; + } + this._clearStatCache(); + __agentOSFs().mkdirSync(this._resolvedFsPath(resolved)); + return __agentOSWasiErrnoSuccess; + } catch (error) { + return this._mapFsError(error); } - return debugs[set]; - }; - function inspect(obj, opts) { - var ctx = { - seen: [], - stylize: stylizeNoColor - }; - if (arguments.length >= 3) ctx.depth = arguments[2]; - if (arguments.length >= 4) ctx.colors = arguments[3]; - if (isBoolean(opts)) { - ctx.showHidden = opts; - } else if (opts) { - exports2._extend(ctx, opts); - } - if (isUndefined(ctx.showHidden)) ctx.showHidden = false; - if (isUndefined(ctx.depth)) ctx.depth = 2; - if (isUndefined(ctx.colors)) ctx.colors = false; - if (isUndefined(ctx.customInspect)) ctx.customInspect = true; - if (ctx.colors) ctx.stylize = stylizeWithColor; - return formatValue(ctx, obj, ctx.depth); } - exports2.inspect = inspect; - inspect.colors = { - "bold": [1, 22], - "italic": [3, 23], - "underline": [4, 24], - "inverse": [7, 27], - "white": [37, 39], - "grey": [90, 39], - "black": [30, 39], - "blue": [34, 39], - "cyan": [36, 39], - "green": [32, 39], - "magenta": [35, 39], - "red": [31, 39], - "yellow": [33, 39] - }; - inspect.styles = { - "special": "cyan", - "number": "yellow", - "boolean": "yellow", - "undefined": "grey", - "null": "bold", - "string": "green", - "date": "magenta", - // "name": intentionally not styling - "regexp": "red" - }; - function stylizeWithColor(str, styleType) { - var style = inspect.styles[styleType]; - if (style) { - return "\\x1B[" + inspect.colors[style][0] + "m" + str + "\\x1B[" + inspect.colors[style][1] + "m"; - } else { - return str; + + _pathLink(oldFd, _oldFlags, oldPathPtr, oldPathLen, newFd, newPathPtr, newPathLen) { + try { + const source = this._resolveDescriptorPath(oldFd, oldPathPtr, oldPathLen); + if (source.error !== __agentOSWasiErrnoSuccess) { + return source.error; + } + const destination = this._resolveDescriptorPath(newFd, newPathPtr, newPathLen); + if (destination.error !== __agentOSWasiErrnoSuccess) { + return destination.error; + } + if (source.readOnly || destination.readOnly) { + return __agentOSWasiErrnoRofs; + } + this._clearStatCache(); + __agentOSFs().linkSync(this._resolvedFsPath(source), this._resolvedFsPath(destination)); + return __agentOSWasiErrnoSuccess; + } catch (error) { + return this._mapFsError(error); } } - function stylizeNoColor(str, styleType) { - return str; - } - function arrayToHash(array) { - var hash = {}; - array.forEach(function(val, idx) { - hash[val] = true; - }); - return hash; - } - function formatValue(ctx, value, recurseTimes) { - if (ctx.customInspect && value && isFunction(value.inspect) && // Filter out the util module, it's inspect function is special - value.inspect !== exports2.inspect && // Also filter out any prototype objects using the circular check. - !(value.constructor && value.constructor.prototype === value)) { - var ret = value.inspect(recurseTimes, ctx); - if (!isString(ret)) { - ret = formatValue(ctx, ret, recurseTimes); + + _pathOpen(fd, _dirflags, pathPtr, pathLen, oflags, rightsBase, rightsInheriting, _fdflags, openedFdPtr) { + try { + const entry = this._measureWasiPhase("descriptorEntry", () => this._descriptorEntry(fd)); + if ( + !entry || + (entry.kind !== "preopen" && entry.kind !== "directory") || + typeof entry.hostPath !== "string" + ) { + return __agentOSWasiErrnoBadf; } - return ret; - } - var primitive = formatPrimitive(ctx, value); - if (primitive) { - return primitive; - } - var keys = Object.keys(value); - var visibleKeys = arrayToHash(keys); - if (ctx.showHidden) { - keys = Object.getOwnPropertyNames(value); - } - if (isError(value) && (keys.indexOf("message") >= 0 || keys.indexOf("description") >= 0)) { - return formatError(value); - } - if (keys.length === 0) { - if (isFunction(value)) { - var name = value.name ? ": " + value.name : ""; - return ctx.stylize("[Function" + name + "]", "special"); + const requestedFlags = Number(oflags) >>> 0; + const createOrTruncate = + (requestedFlags & __agentOSWasiOpenCreate) !== 0 || + (requestedFlags & __agentOSWasiOpenTruncate) !== 0; + const resolved = this._measureWasiPhase("resolveDescriptorPath", () => + this._resolveDescriptorPath(fd, pathPtr, pathLen, { + preferCreateParent: createOrTruncate, + }) + ); + if (resolved.error !== __agentOSWasiErrnoSuccess) { + return resolved.error; } - if (isRegExp(value)) { - return ctx.stylize(RegExp.prototype.toString.call(value), "regexp"); + const guestPath = resolved.guestPath; + const fsPath = this._resolvedFsPath(resolved); + const openDirectory = (requestedFlags & __agentOSWasiOpenDirectory) !== 0; + const allowedRightsBase = this._descriptorRightsBase(entry); + const allowedRightsInheriting = this._descriptorRightsInheriting(entry); + const requestedRightsBase = this._normalizeRights(rightsBase, allowedRightsInheriting); + const requestedRightsInheriting = this._normalizeRights( + rightsInheriting, + allowedRightsInheriting, + ); + if ( + (requestedRightsBase & ~allowedRightsInheriting) !== 0n || + (requestedRightsInheriting & ~allowedRightsInheriting) !== 0n + ) { + return __agentOSWasiErrnoAcces; } - if (isDate(value)) { - return ctx.stylize(Date.prototype.toString.call(value), "date"); + const requestedWriteAccess = + !openDirectory && + (createOrTruncate || this._hasWriteRights(requestedRightsBase)); + if ( + requestedWriteAccess && + !this._hasWriteRights(allowedRightsBase) + ) { + return __agentOSWasiErrnoAcces; } - if (isError(value)) { - return formatError(value); + if (requestedWriteAccess && resolved.readOnly) { + return __agentOSWasiErrnoRofs; } - } - var base = "", array = false, braces = ["{", "}"]; - if (isArray(value)) { - array = true; - braces = ["[", "]"]; - } - if (isFunction(value)) { - var n = value.name ? ": " + value.name : ""; - base = " [Function" + n + "]"; - } - if (isRegExp(value)) { - base = " " + RegExp.prototype.toString.call(value); - } - if (isDate(value)) { - base = " " + Date.prototype.toUTCString.call(value); - } - if (isError(value)) { - base = " " + formatError(value); - } - if (keys.length === 0 && (!array || value.length == 0)) { - return braces[0] + base + braces[1]; - } - if (recurseTimes < 0) { - if (isRegExp(value)) { - return ctx.stylize(RegExp.prototype.toString.call(value), "regexp"); - } else { - return ctx.stylize("[Object]", "special"); + if (createOrTruncate) { + this._clearStatCache(); } - } - ctx.seen.push(value); - var output; - if (array) { - output = formatArray(ctx, value, recurseTimes, visibleKeys, keys); - } else { - output = keys.map(function(key) { - return formatProperty(ctx, value, recurseTimes, visibleKeys, key, array); + const fsConstants = __agentOSFs().constants ?? {}; + const requestedFdFlags = Number(_fdflags) >>> 0; + const append = (requestedFdFlags & __agentOSWasiFdflagsAppend) !== 0; + let openFlags = requestedWriteAccess + ? (this._hasReadRights(requestedRightsBase) + ? fsConstants.O_RDWR ?? 2 + : fsConstants.O_WRONLY ?? 1) + : fsConstants.O_RDONLY ?? 0; + if ((requestedFlags & __agentOSWasiOpenCreate) !== 0) { + openFlags |= fsConstants.O_CREAT ?? 64; + } + if ((requestedFlags & __agentOSWasiOpenExclusive) !== 0) { + openFlags |= fsConstants.O_EXCL ?? 128; + } + if ((requestedFlags & __agentOSWasiOpenTruncate) !== 0) { + openFlags |= fsConstants.O_TRUNC ?? 512; + } + if (append) { + openFlags |= fsConstants.O_APPEND ?? 1024; + } + if (openDirectory) { + openFlags |= fsConstants.O_DIRECTORY ?? 0; + } + const realFd = this._measureWasiPhase("openSync", () => __agentOSFs().openSync(fsPath, openFlags)); + const openedKind = openDirectory || createOrTruncate + ? (openDirectory ? "directory" : "file") + : this._measureWasiPhase("postOpenStat", () => + __agentOSFs().statSync(fsPath).isDirectory() ? "directory" : "file" + ); + const openedFd = this.nextFd++; + this._measureWasiPhase("fdTableSet", () => { + this.fdTable.set(openedFd, { + kind: openedKind, + guestPath, + hostPath: fsPath, + readOnly: resolved.readOnly === true, + realFd, + offset: append + ? Number(__agentOSFs().fstatSync(realFd).size ?? 0) + : 0, + append, + rightsBase: requestedRightsBase & allowedRightsInheriting, + rightsInheriting: requestedRightsInheriting & allowedRightsInheriting, + fdFlags: requestedFdFlags & 0xffff, + }); }); + return this._measureWasiPhase("writeOpenedFd", () => this._writeUint32(openedFdPtr, openedFd)); + } catch (error) { + return this._mapFsError(error); } - ctx.seen.pop(); - return reduceToSingleString(output, base, braces); - } - function formatPrimitive(ctx, value) { - if (isUndefined(value)) - return ctx.stylize("undefined", "undefined"); - if (isString(value)) { - var simple = "'" + JSON.stringify(value).replace(/^"|"$/g, "").replace(/'/g, "\\\\'").replace(/\\\\"/g, '"') + "'"; - return ctx.stylize(simple, "string"); - } - if (isNumber(value)) - return ctx.stylize("" + value, "number"); - if (isBoolean(value)) - return ctx.stylize("" + value, "boolean"); - if (isNull(value)) - return ctx.stylize("null", "null"); - } - function formatError(value) { - return "[" + Error.prototype.toString.call(value) + "]"; } - function formatArray(ctx, value, recurseTimes, visibleKeys, keys) { - var output = []; - for (var i = 0, l = value.length; i < l; ++i) { - if (hasOwnProperty(value, String(i))) { - output.push(formatProperty( - ctx, - value, - recurseTimes, - visibleKeys, - String(i), - true - )); - } else { - output.push(""); + + _pathSymlink(targetPtr, targetLen, fd, pathPtr, pathLen) { + try { + const resolved = this._resolveDescriptorPath(fd, pathPtr, pathLen); + if (resolved.error !== __agentOSWasiErrnoSuccess) { + return resolved.error; } - } - keys.forEach(function(key) { - if (!key.match(/^\\d+$/)) { - output.push(formatProperty( - ctx, - value, - recurseTimes, - visibleKeys, - key, - true - )); + if (resolved.readOnly) { + return __agentOSWasiErrnoRofs; } - }); - return output; + const target = this._readString(targetPtr, targetLen); + this._clearStatCache(); + __agentOSFs().symlinkSync(target, this._resolvedFsPath(resolved)); + return __agentOSWasiErrnoSuccess; + } catch (error) { + return this._mapFsError(error); + } } - function formatProperty(ctx, value, recurseTimes, visibleKeys, key, array) { - var name, str, desc; - desc = Object.getOwnPropertyDescriptor(value, key) || { value: value[key] }; - if (desc.get) { - if (desc.set) { - str = ctx.stylize("[Getter/Setter]", "special"); - } else { - str = ctx.stylize("[Getter]", "special"); + + _pathRemoveDirectory(fd, pathPtr, pathLen) { + try { + const resolved = this._resolveDescriptorPath(fd, pathPtr, pathLen); + if (resolved.error !== __agentOSWasiErrnoSuccess) { + return resolved.error; } - } else { - if (desc.set) { - str = ctx.stylize("[Setter]", "special"); + if (resolved.readOnly) { + return __agentOSWasiErrnoRofs; } + this._clearStatCache(); + __agentOSFs().rmdirSync(this._resolvedFsPath(resolved)); + return __agentOSWasiErrnoSuccess; + } catch (error) { + return this._mapFsError(error); } - if (!hasOwnProperty(visibleKeys, key)) { - name = "[" + key + "]"; - } - if (!str) { - if (ctx.seen.indexOf(desc.value) < 0) { - if (isNull(recurseTimes)) { - str = formatValue(ctx, desc.value, null); - } else { - str = formatValue(ctx, desc.value, recurseTimes - 1); - } - if (str.indexOf("\\n") > -1) { - if (array) { - str = str.split("\\n").map(function(line) { - return " " + line; - }).join("\\n").slice(2); - } else { - str = "\\n" + str.split("\\n").map(function(line) { - return " " + line; - }).join("\\n"); - } - } - } else { - str = ctx.stylize("[Circular]", "special"); + } + + _pathRename(oldFd, oldPathPtr, oldPathLen, newFd, newPathPtr, newPathLen) { + try { + const source = this._resolveDescriptorPath(oldFd, oldPathPtr, oldPathLen); + if (source.error !== __agentOSWasiErrnoSuccess) { + return source.error; } - } - if (isUndefined(name)) { - if (array && key.match(/^\\d+$/)) { - return str; + const destination = this._resolveDescriptorPath(newFd, newPathPtr, newPathLen); + if (destination.error !== __agentOSWasiErrnoSuccess) { + return destination.error; } - name = JSON.stringify("" + key); - if (name.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)) { - name = name.slice(1, -1); - name = ctx.stylize(name, "name"); - } else { - name = name.replace(/'/g, "\\\\'").replace(/\\\\"/g, '"').replace(/(^"|"$)/g, "'"); - name = ctx.stylize(name, "string"); + if (source.readOnly || destination.readOnly) { + return __agentOSWasiErrnoRofs; } + this._clearStatCache(); + __agentOSFs().renameSync(this._resolvedFsPath(source), this._resolvedFsPath(destination)); + return __agentOSWasiErrnoSuccess; + } catch (error) { + return this._mapFsError(error); } - return name + ": " + str; - } - function reduceToSingleString(output, base, braces) { - var numLinesEst = 0; - var length = output.reduce(function(prev, cur) { - numLinesEst++; - if (cur.indexOf("\\n") >= 0) numLinesEst++; - return prev + cur.replace(/\\u001b\\[\\d\\d?m/g, "").length + 1; - }, 0); - if (length > 60) { - return braces[0] + (base === "" ? "" : base + "\\n ") + " " + output.join(",\\n ") + " " + braces[1]; - } - return braces[0] + base + " " + output.join(", ") + " " + braces[1]; - } - exports2.types = require_types(); - function isArray(ar) { - return Array.isArray(ar); - } - exports2.isArray = isArray; - function isBoolean(arg) { - return typeof arg === "boolean"; - } - exports2.isBoolean = isBoolean; - function isNull(arg) { - return arg === null; - } - exports2.isNull = isNull; - function isNullOrUndefined(arg) { - return arg == null; - } - exports2.isNullOrUndefined = isNullOrUndefined; - function isNumber(arg) { - return typeof arg === "number"; - } - exports2.isNumber = isNumber; - function isString(arg) { - return typeof arg === "string"; - } - exports2.isString = isString; - function isSymbol(arg) { - return typeof arg === "symbol"; - } - exports2.isSymbol = isSymbol; - function isUndefined(arg) { - return arg === void 0; - } - exports2.isUndefined = isUndefined; - function isRegExp(re) { - return isObject(re) && objectToString(re) === "[object RegExp]"; - } - exports2.isRegExp = isRegExp; - exports2.types.isRegExp = isRegExp; - function isObject(arg) { - return typeof arg === "object" && arg !== null; - } - exports2.isObject = isObject; - function isDate(d) { - return isObject(d) && objectToString(d) === "[object Date]"; - } - exports2.isDate = isDate; - exports2.types.isDate = isDate; - function isError(e) { - return isObject(e) && (objectToString(e) === "[object Error]" || e instanceof Error); - } - exports2.isError = isError; - exports2.types.isNativeError = isError; - function isFunction(arg) { - return typeof arg === "function"; - } - exports2.isFunction = isFunction; - function isPrimitive(arg) { - return arg === null || typeof arg === "boolean" || typeof arg === "number" || typeof arg === "string" || typeof arg === "symbol" || // ES6 symbol - typeof arg === "undefined"; - } - exports2.isPrimitive = isPrimitive; - exports2.isBuffer = require_isBufferBrowser(); - function objectToString(o) { - return Object.prototype.toString.call(o); - } - function pad(n) { - return n < 10 ? "0" + n.toString(10) : n.toString(10); } - var months = [ - "Jan", - "Feb", - "Mar", - "Apr", - "May", - "Jun", - "Jul", - "Aug", - "Sep", - "Oct", - "Nov", - "Dec" - ]; - function timestamp() { - var d = /* @__PURE__ */ new Date(); - var time = [ - pad(d.getHours()), - pad(d.getMinutes()), - pad(d.getSeconds()) - ].join(":"); - return [d.getDate(), months[d.getMonth()], time].join(" "); + + _pathUnlinkFile(fd, pathPtr, pathLen) { + try { + const resolved = this._resolveDescriptorPath(fd, pathPtr, pathLen); + if (resolved.error !== __agentOSWasiErrnoSuccess) { + return resolved.error; + } + if (resolved.readOnly) { + return __agentOSWasiErrnoRofs; + } + this._clearStatCache(); + __agentOSFs().unlinkSync(this._resolvedFsPath(resolved)); + return __agentOSWasiErrnoSuccess; + } catch (error) { + return this._mapFsError(error); + } } - exports2.log = function() { - console.log("%s - %s", timestamp(), exports2.format.apply(exports2, arguments)); - }; - exports2.inherits = require_inherits_browser(); - exports2._extend = function(origin, add) { - if (!add || !isObject(add)) return origin; - var keys = Object.keys(add); - var i = keys.length; - while (i--) { - origin[keys[i]] = add[keys[i]]; + + _pathFilestatGet(fd, flags, pathPtr, pathLen, statPtr) { + try { + const target = this._measureWasiPhase("readString", () => this._readString(pathPtr, pathLen)); + const resolved = + this._measureWasiPhase("resolveDirectStatPath", () => this._resolveDescriptorDirectStatPath(fd, target)) ?? + this._measureWasiPhase("resolveDescriptorPath", () => this._resolveDescriptorPath(fd, pathPtr, pathLen)); + if (resolved.error !== __agentOSWasiErrnoSuccess) { + return resolved.error; + } + const follow = (Number(flags) & __agentOSWasiLookupSymlinkFollow) !== 0; + const cacheKey = this._statCacheKey(resolved, follow); + let stats = cacheKey ? this.statCache.get(cacheKey) : undefined; + if (stats) { + this._measureWasiPhase("statCacheHit", () => undefined); + } else { + stats = this._measureWasiPhase(follow ? "statSync" : "lstatSync", () => + this._statResolvedPath(resolved, follow) + ); + if (cacheKey) { + this.statCache.set(cacheKey, stats); + } + } + return this._measureWasiPhase("writeFilestat", () => this._writeFilestat(statPtr, stats, this._filetypeForStats(stats))); + } catch (error) { + return this._mapFsError(error); } - return origin; - }; - function hasOwnProperty(obj, prop) { - return Object.prototype.hasOwnProperty.call(obj, prop); } - var kCustomPromisifiedSymbol = typeof Symbol !== "undefined" ? /* @__PURE__ */ Symbol("util.promisify.custom") : void 0; - exports2.promisify = function promisify(original) { - if (typeof original !== "function") - throw new TypeError('The "original" argument must be of type Function'); - if (kCustomPromisifiedSymbol && original[kCustomPromisifiedSymbol]) { - var fn = original[kCustomPromisifiedSymbol]; - if (typeof fn !== "function") { - throw new TypeError('The "util.promisify.custom" argument must be of type Function'); + + _pathReadlink(fd, pathPtr, pathLen, bufPtr, bufLen, bufUsedPtr) { + try { + const resolved = this._resolveDescriptorPath(fd, pathPtr, pathLen); + if (resolved.error !== __agentOSWasiErrnoSuccess) { + return resolved.error; } - Object.defineProperty(fn, kCustomPromisifiedSymbol, { - value: fn, - enumerable: false, - writable: false, - configurable: true - }); - return fn; + const bytes = Buffer.from(__agentOSFs().readlinkSync(resolved.guestPath), "utf8"); + const length = Math.min(bytes.length, Number(bufLen) >>> 0); + const writeStatus = this._writeBytes(bufPtr, bytes.subarray(0, length)); + if (writeStatus !== __agentOSWasiErrnoSuccess) { + return writeStatus; + } + return this._writeUint32(bufUsedPtr, length); + } catch (error) { + return this._mapFsError(error); } - function fn() { - var promiseResolve, promiseReject; - var promise = new Promise(function(resolve, reject) { - promiseResolve = resolve; - promiseReject = reject; - }); - var args = []; - for (var i = 0; i < arguments.length; i++) { - args.push(arguments[i]); + } + + _pollOneoff(inPtr, outPtr, nsubscriptions, neventsPtr) { + try { + const subscriptionCount = Number(nsubscriptions) >>> 0; + if (subscriptionCount === 0) { + return this._writeUint32(neventsPtr, 0); } - args.push(function(err, value) { - if (err) { - promiseReject(err); - } else { - promiseResolve(value); + + const subscriptionSize = 48; + const eventSize = 32; + const kernelPollIn = 0x0001; + const kernelPollOut = 0x0004; + const kernelPollErr = 0x0008; + const kernelPollHup = 0x0010; + const view = this._memoryView(); + const memory = this._memoryBytes(); + const syncRpc = + typeof globalThis?.__agentOSSyncRpc?.callSync === "function" + ? __agentOSWasiSyncRpc() + : null; + const subscriptions = []; + let timeoutMs = null; + + for (let index = 0; index < subscriptionCount; index += 1) { + const base = (Number(inPtr) >>> 0) + index * subscriptionSize; + const tag = view.getUint8(base + 8); + const userdata = memory.slice(base, base + 8); + if (tag === 0) { + const timeoutNs = view.getBigUint64(base + 24, true); + const relativeTimeoutMs = Number(timeoutNs / 1000000n); + timeoutMs = + timeoutMs == null ? relativeTimeoutMs : Math.min(timeoutMs, relativeTimeoutMs); + subscriptions.push({ kind: "clock", userdata }); + continue; + } + + if (tag !== 1 && tag !== 2) { + subscriptions.push({ kind: "unsupported", userdata }); + continue; + } + + const fd = view.getUint32(base + 16, true); + const descriptor = Number(fd) >>> 0; + const handle = this._externalFdHandle(descriptor); + const entry = this._descriptorEntry(descriptor); + let targetFd = null; + if ( + (handle?.kind === "passthrough" || handle?.kind === "host-passthrough") && + typeof handle.targetFd === "number" + ) { + targetFd = Number(handle.targetFd) >>> 0; + } else if ( + entry?.kind === "stdin" || + entry?.kind === "stdout" || + entry?.kind === "stderr" + ) { + targetFd = descriptor; + } + + subscriptions.push({ + kind: tag === 1 ? "fd_read" : "fd_write", + fd: descriptor, + handle, + targetFd, + streamKind: entry?.kind, + userdata, + }); + } + + const deadline = timeoutMs == null ? null : Date.now() + Math.max(0, timeoutMs); + const readyEvents = []; + + while (readyEvents.length === 0) { + for (const subscription of subscriptions) { + // A clock subscription is ready once its deadline has elapsed; report + // it as a first-class event so it is returned alongside any ready fds + // (not only as a fallback when nothing else is ready). + if (subscription.kind === "clock") { + if (deadline != null && Date.now() >= deadline) { + readyEvents.push({ + userdata: subscription.userdata, + error: __agentOSWasiErrnoSuccess, + type: 0, + nbytes: 0, + flags: 0, + }); + } + continue; + } + if (subscription.kind === "fd_read" && subscription.handle?.kind === "pipe-read") { + const pipe = subscription.handle.pipe; + if ( + pipe && + (pipe.chunks.length > 0 || + (pipe.writeHandleCount === 0 && pipe.producers.size === 0)) + ) { + readyEvents.push({ + userdata: subscription.userdata, + error: __agentOSWasiErrnoSuccess, + type: 1, + nbytes: pipe.chunks[0]?.length ?? 0, + flags: 0, + }); + } + continue; + } + + // Without a kernel poll bridge, resolve stdin fd_read readiness from + // the host-seam queued byte count (the browser delivers stdin through + // the runtime process object). Reporting nbytes does not consume input. + if ( + !syncRpc && + subscription.kind === "fd_read" && + subscription.streamKind === "stdin" && + typeof __agentOSWasiHost.stdinReadableBytes === "function" + ) { + const available = Number(__agentOSWasiHost.stdinReadableBytes()) >>> 0; + if (available > 0) { + readyEvents.push({ + userdata: subscription.userdata, + error: __agentOSWasiErrnoSuccess, + type: 1, + nbytes: available, + flags: 0, + }); + } + continue; + } + + if (subscription.kind === "fd_write" && subscription.handle?.kind === "pipe-write") { + readyEvents.push({ + userdata: subscription.userdata, + error: __agentOSWasiErrnoSuccess, + type: 2, + nbytes: 65536, + flags: 0, + }); + continue; + } + + // Without a kernel poll bridge (a non-native backend) stdout/stderr + // are always writable, so resolve their fd_write readiness directly + // instead of leaving it to the (absent) __kernel_poll round-trip. + if ( + !syncRpc && + subscription.kind === "fd_write" && + (subscription.streamKind === "stdout" || + subscription.streamKind === "stderr") + ) { + readyEvents.push({ + userdata: subscription.userdata, + error: __agentOSWasiErrnoSuccess, + type: 2, + nbytes: 65536, + flags: 0, + }); + } + } + + if (readyEvents.length > 0) { + break; + } + + // Without a kernel poll bridge, fd readiness is resolved synchronously + // above (stdio fast paths) or via pipes; if there is no clock to wait on + // and no pipe to pump, no further progress is possible, so stop instead + // of busy-waiting until the caller times out. + if ( + !syncRpc && + !subscriptions.some((subscription) => subscription.kind === "clock") && + !subscriptions.some( + (subscription) => + subscription.handle?.kind === "pipe-read" || + subscription.handle?.kind === "pipe-write", + ) + ) { + break; + } + + const pollTargets = subscriptions + .filter( + (subscription) => + (subscription.kind === "fd_read" || subscription.kind === "fd_write") && + typeof subscription.targetFd === "number", + ) + .map((subscription) => ({ + fd: subscription.targetFd, + events: subscription.kind === "fd_read" ? kernelPollIn : kernelPollOut, + })); + const waitMs = + deadline == null ? 10 : Math.max(0, Math.min(10, deadline - Date.now())); + + if (syncRpc && pollTargets.length > 0) { + let response = null; + try { + response = syncRpc.callSync("__kernel_poll", [pollTargets, waitMs]); + } catch (error) { + __agentOSWasiDebug( + \`poll_oneoff __kernel_poll failed: \${ + error instanceof Error ? error.message : String(error) + }\`, + ); + } + + const responseEntries = Array.isArray(response?.fds) ? response.fds : []; + for (const subscription of subscriptions) { + if ( + (subscription.kind !== "fd_read" && subscription.kind !== "fd_write") || + typeof subscription.targetFd !== "number" + ) { + continue; + } + + const responseEntry = responseEntries.find( + (entry) => (Number(entry?.fd) >>> 0) === subscription.targetFd, + ); + const revents = Number(responseEntry?.revents) >>> 0; + const interested = + subscription.kind === "fd_read" + ? kernelPollIn | kernelPollErr | kernelPollHup + : kernelPollOut | kernelPollErr | kernelPollHup; + if ((revents & interested) === 0) { + continue; + } + + readyEvents.push({ + userdata: subscription.userdata, + error: __agentOSWasiErrnoSuccess, + type: subscription.kind === "fd_read" ? 1 : 2, + nbytes: subscription.kind === "fd_read" ? 1 : 65536, + flags: 0, + }); + } + } + + if (readyEvents.length > 0) { + break; + } + + let pumped = false; + for (const subscription of subscriptions) { + if (subscription.kind === "fd_read" && subscription.handle?.kind === "pipe-read") { + pumped = this._pumpPipeProducers(subscription.handle.pipe, 10) || pumped; + } + } + + if (pumped) { + continue; + } + + if (deadline != null && Date.now() >= deadline) { + break; + } + + if ( + pollTargets.length === 0 && + typeof Atomics?.wait !== "function" && + deadline == null + ) { + break; + } + + if ( + typeof Atomics?.wait === "function" && + typeof syntheticWaitArray !== "undefined" + ) { + Atomics.wait(syntheticWaitArray, 0, 0, waitMs); + } else if (!syncRpc && pollTargets.length === 0) { + break; } - }); - try { - original.apply(this, args); - } catch (err) { - promiseReject(err); } - return promise; - } - Object.setPrototypeOf(fn, Object.getPrototypeOf(original)); - if (kCustomPromisifiedSymbol) Object.defineProperty(fn, kCustomPromisifiedSymbol, { - value: fn, - enumerable: false, - writable: false, - configurable: true - }); - return Object.defineProperties( - fn, - getOwnPropertyDescriptors(original) - ); - }; - exports2.promisify.custom = kCustomPromisifiedSymbol; - function callbackifyOnRejected(reason, cb) { - if (!reason) { - var newReason = new Error("Promise was rejected with a falsy value"); - newReason.reason = reason; - reason = newReason; - } - return cb(reason); - } - function callbackify(original) { - if (typeof original !== "function") { - throw new TypeError('The "original" argument must be of type Function'); - } - function callbackified() { - var args = []; - for (var i = 0; i < arguments.length; i++) { - args.push(arguments[i]); + + if ( + readyEvents.length === 0 && + subscriptions.some((subscription) => subscription.kind === "clock") + ) { + const clockSubscription = subscriptions.find( + (subscription) => subscription.kind === "clock", + ); + readyEvents.push({ + userdata: clockSubscription.userdata, + error: __agentOSWasiErrnoSuccess, + type: 0, + nbytes: 0, + flags: 0, + }); } - var maybeCb = args.pop(); - if (typeof maybeCb !== "function") { - throw new TypeError("The last argument must be of type Function"); + + for (let index = 0; index < readyEvents.length; index += 1) { + const base = (Number(outPtr) >>> 0) + index * eventSize; + const event = readyEvents[index]; + memory.set(event.userdata, base); + view.setUint16(base + 8, event.error, true); + view.setUint8(base + 10, event.type); + view.setBigUint64(base + 16, BigInt(event.nbytes), true); + view.setUint16(base + 24, event.flags, true); } - var self = this; - var cb = function() { - return maybeCb.apply(self, arguments); - }; - original.apply(this, args).then( - function(ret) { - process.nextTick(cb.bind(null, null, ret)); - }, - function(rej) { - process.nextTick(callbackifyOnRejected.bind(null, rej, cb)); - } + + return this._writeUint32(neventsPtr, readyEvents.length); + } catch (error) { + __agentOSWasiDebug( + \`poll_oneoff failed: \${error instanceof Error ? error.message : String(error)}\`, ); + return __agentOSWasiErrnoFault; } - Object.setPrototypeOf(callbackified, Object.getPrototypeOf(original)); - Object.defineProperties( - callbackified, - getOwnPropertyDescriptors(original) - ); - return callbackified; } - exports2.callbackify = callbackify; - } -}); - -// -var util = require_util(); -module.exports = util.default ?? util; -function installBuiltinUtilFormatWithOptions(builtinUtilModule) { - if (!builtinUtilModule || typeof builtinUtilModule.formatWithOptions === "function") { - return builtinUtilModule; - } - builtinUtilModule.formatWithOptions = function formatWithOptions(inspectOptions, format, ...args) { - const inspectValue = (value) => { - if (typeof builtinUtilModule.inspect === "function") { - return builtinUtilModule.inspect(value, inspectOptions); - } + _randomGet(bufPtr, bufLen) { try { - return JSON.stringify(value); + const length = Number(bufLen) >>> 0; + const bytes = Buffer.allocUnsafe(length); + __agentOSCrypto().randomFillSync(bytes); + return this._writeBytes(bufPtr, bytes); } catch { - return String(value); + return __agentOSWasiErrnoFault; } - }; - const formatValue = (value) => typeof value === "string" ? value : inspectValue(value); - if (typeof format !== "string") { - return [format, ...args].map(formatValue).join(" "); } - let index = 0; - const formatted = format.replace(/%[sdifjoO%]/g, (token) => { - if (token === "%%") { - return "%"; - } - if (index >= args.length) { - return token; - } - const value = args[index++]; - switch (token) { - case "%s": - return String(value); - case "%d": - return Number(value).toString(); - case "%i": - return Number.parseInt(value, 10).toString(); - case "%f": - return Number.parseFloat(value).toString(); - case "%j": - try { - return JSON.stringify(value); - } catch { - return "[Circular]"; - } - case "%o": - case "%O": - return inspectValue(value); - default: - return token; + + _schedYield() { + return __agentOSWasiErrnoSuccess; + } + + _procExit(code) { + if (this.returnOnExit) { + const error = new Error(\`wasi exit(\${Number(code) >>> 0})\`); + error.__agentOSWasiExit = true; + error.code = Number(code) >>> 0; + throw error; } - }); - if (index >= args.length) { - return formatted; + process.exit(Number(code) >>> 0); } - return [formatted, ...args.slice(index).map(formatValue)].join(" "); - }; - return builtinUtilModule; + } + + Object.defineProperty(globalThis, "__agentOSWasiModule", { + configurable: true, + enumerable: false, + value: { WASI }, + writable: true, + }); } -module.exports = installBuiltinUtilFormatWithOptions(module.exports); -if (module.exports && module.exports.default == null) module.exports.default = module.exports; -`;function Sa(e){let t=new Error(`ENOSYS: ${e} is not supported`);return t.code="ENOSYS",t}function Is(){return{async fetch(){throw Sa("network.fetch")},async dnsLookup(){return{error:"DNS not supported",code:"ENOSYS"}},async httpRequest(){throw Sa("network.httpRequest")}}}function Ea(e,t){return t?.endsWith(".mjs")?!0:t?.endsWith(".cjs")?!1:/\b(import|export)\b/.test(e)}function Ta(e){return e.replace(/(^|[^.\w$])import(\s*\()/g,"$1__dynamicImport$2")}var tf={fs:"module.exports = globalThis._fsModule;","node:fs":"module.exports = globalThis._fsModule;","fs/promises":"module.exports = globalThis._fsModule.promises || globalThis._fsModule;","node:fs/promises":"module.exports = globalThis._fsModule.promises || globalThis._fsModule;",util:ef,"node:util":"module.exports = require('util');","util/types":"module.exports = require('util').types;","node:util/types":"module.exports = require('util/types');",buffer:Qu,"node:buffer":"module.exports = require('buffer');",path:Zu,"node:path":"module.exports = require('path');",console:"module.exports = globalThis.console;","node:console":"module.exports = require('console');",process:"module.exports = globalThis.process;","node:process":"module.exports = globalThis.process;",module:` + + // Re-export the shared runner WASI class as the browser wasi module. + module.exports = { WASI: globalThis.__agentOSWasiModule.WASI }; + module.exports.default = { WASI: globalThis.__agentOSWasiModule.WASI }; +`;function ka(e){let t=new Error(`ENOSYS: ${e} is not supported`);return t.code="ENOSYS",t}function Is(){return{async fetch(){throw ka("network.fetch")},async dnsLookup(){return{error:"DNS not supported",code:"ENOSYS"}},async httpRequest(){throw ka("network.httpRequest")}}}function Ea(e,t){return t?.endsWith(".mjs")?!0:t?.endsWith(".cjs")?!1:/\b(import|export)\b/.test(e)}function Pa(e){return e.replace(/(^|[^.\w$])import(\s*\()/g,"$1__dynamicImport$2")}var Dm={fs:"module.exports = globalThis._fsModule;","node:fs":"module.exports = globalThis._fsModule;","fs/promises":"module.exports = globalThis._fsModule.promises || globalThis._fsModule;","node:fs/promises":"module.exports = globalThis._fsModule.promises || globalThis._fsModule;",util:Yu,"node:util":"module.exports = require('util');","util/types":"module.exports = require('util').types;","node:util/types":"module.exports = require('util/types');",buffer:Ju,"node:buffer":"module.exports = require('buffer');",path:Xu,"node:path":"module.exports = require('path');",console:"module.exports = globalThis.console;","node:console":"module.exports = require('console');",process:"module.exports = globalThis.process;","node:process":"module.exports = globalThis.process;",module:` const createRequire = () => globalThis.require; const Module = { createRequire }; module.exports = { createRequire, Module, builtinModules: [] }; @@ -7435,40 +7502,41 @@ if (module.exports && module.exports.default == null) module.exports.default = m `,"node:module":"module.exports = require('module');",stream:` class EventEmitterLike { constructor() { this._listeners = Object.create(null); } - on(event, fn) { (this._listeners[event] = this._listeners[event] || []).push(fn); return this; } + on(event, fn) { (this._listeners[event] = this._listeners[event] || []).push(fn); if (event === "data" && typeof this.resume === "function") this.resume(); return this; } addListener(event, fn) { return this.on(event, fn); } once(event, fn) { const w = (...a) => { this.off(event, w); fn(...a); }; w._origin = fn; return this.on(event, w); } off(event, fn) { if (this._listeners[event]) this._listeners[event] = this._listeners[event].filter((x) => x !== fn && x._origin !== fn); return this; } removeListener(event, fn) { return this.off(event, fn); } removeAllListeners(event) { if (event) delete this._listeners[event]; else this._listeners = Object.create(null); return this; } - emit(event, ...args) { const ls = (this._listeners[event] || []).slice(); for (const fn of ls) fn(...args); return ls.length > 0; } + emit(event, ...args) { const ls = (this._listeners[event] || []).slice(); if (ls.length === 0 && event === "error") throw (args[0] instanceof Error ? args[0] : new Error(String(args[0]))); for (const fn of ls) fn(...args); return ls.length > 0; } listenerCount(event) { return (this._listeners[event] || []).length; } } + const chunkLength = (chunk) => typeof chunk === "string" ? new TextEncoder().encode(chunk).length : Number(chunk && chunk.byteLength) || 1; + const streamError = (code, message) => { const error = new Error(message); error.code = code; return error; }; class Readable extends EventEmitterLike { - constructor(options) { super(); this.readable = true; this._readableOptions = options || {}; if (this._readableOptions.read) this._read = this._readableOptions.read; } - resume() { this.emit("resume"); return this; } - pause() { this.paused = true; return this; } + constructor(options) { super(); this.readable = true; this.readableEnded = false; this.destroyed = false; this.paused = true; this._buffer = []; this._bufferedBytes = 0; this._highWaterMark = Number(options && options.highWaterMark) || 16384; this._readableOptions = options || {}; this._readableAutoDestroy = this._readableOptions.autoDestroy !== false; if (this._readableOptions.read) this._read = this._readableOptions.read; } + _finishReadable() { if (this.readableEnded) return; this.readableEnded = true; this.readable = false; this.emit("end"); if (this._readableAutoDestroy && !this._closeScheduled) { this._closeScheduled = true; queueMicrotask(() => this.destroy()); } } + _flush() { while (!this.paused && this._buffer.length > 0) { const chunk = this._buffer.shift(); this._bufferedBytes -= chunkLength(chunk); this.emit("data", chunk); } if (!this.paused && this._ended && this._buffer.length === 0) this._finishReadable(); } + resume() { if (this.destroyed) return this; const changed = this.paused; this.paused = false; if (changed) this.emit("resume"); this._flush(); return this; } + pause() { this.paused = true; this.emit("pause"); return this; } setEncoding() { return this; } - read() { return null; } - push(chunk) { if (chunk == null) this.emit("end"); else this.emit("data", chunk); return true; } - pipe(dest) { this.on("data", (c) => dest.write && dest.write(c)); this.on("end", () => dest.end && dest.end()); return dest; } - destroy() { this.emit("close"); return this; } + read() { const chunk = this._buffer.shift() ?? null; if (chunk != null) this._bufferedBytes -= chunkLength(chunk); if (chunk == null && this._ended) this._finishReadable(); return chunk; } + push(chunk) { if (this.destroyed || this._ended) return false; if (chunk == null) { this._ended = true; this._flush(); return false; } this._buffer.push(chunk); this._bufferedBytes += chunkLength(chunk); this._flush(); return this._bufferedBytes < this._highWaterMark; } + pipe(dest) { this.on("data", (chunk) => { if (dest.write && dest.write(chunk) === false) { this.pause(); dest.once && dest.once("drain", () => this.resume()); } }); this.once("end", () => dest.end && dest.end()); this.once("error", (error) => dest.destroy ? dest.destroy(error) : dest.emit && dest.emit("error", error)); return dest; } + destroy(error) { if (this.destroyed) return this; this.destroyed = true; this.readable = false; this._buffer.length = 0; this._bufferedBytes = 0; if (error) this.emit("error", error); this.emit("close"); return this; } } - Readable.toWeb = (stream) => new ReadableStream({ start(controller) { - stream.on("data", (chunk) => controller.enqueue(chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk))); - stream.on("end", () => { try { controller.close(); } catch (e) {} }); - stream.on("error", (err) => controller.error(err)); - } }); + Readable.toWeb = (stream) => new ReadableStream({ start(controller) { stream.once("end", () => controller.close()); stream.once("error", (error) => controller.error(error)); stream.on("data", (chunk) => { controller.enqueue(chunk); if ((controller.desiredSize ?? 1) <= 0) stream.pause && stream.pause(); }); }, pull() { stream.resume && stream.resume(); }, cancel(reason) { stream.destroy && stream.destroy(reason instanceof Error ? reason : undefined); } }); class Writable extends EventEmitterLike { - constructor(options) { super(); this.writable = true; this._writableOptions = options || {}; if (this._writableOptions.write) this._writeImpl = this._writableOptions.write; } - write(chunk, encoding, cb) { if (typeof encoding === "function") { cb = encoding; encoding = undefined; } if (this._writeImpl) this._writeImpl(chunk, encoding, cb || (() => {})); else if (cb) cb(); this.emit("data", chunk); return true; } - end(chunk, encoding, cb) { const done = typeof chunk === "function" ? chunk : typeof encoding === "function" ? encoding : cb; if (chunk != null && typeof chunk !== "function") this.write(chunk); this.emit("finish"); this.emit("end"); if (done) done(); } - destroy() { this.emit("close"); return this; } + constructor(options) { super(); this.writable = true; this.writableEnded = false; this.writableFinished = false; this.destroyed = false; this._pendingWrites = 0; this._writableBufferedBytes = 0; this._writableNeedDrain = false; this._writableHighWaterMark = Number(options && options.highWaterMark) || 16384; this._writableOptions = options || {}; this._writableAutoDestroy = this._writableOptions.autoDestroy !== false; if (this._writableOptions.write) this._writeImpl = this._writableOptions.write; } + _finishIfReady() { if (this.writableEnded && this._pendingWrites === 0 && !this.writableFinished && !this.destroyed) { this.writableFinished = true; this.writable = false; this.emit("finish"); if (this._endCallback) { const callback = this._endCallback; this._endCallback = null; callback(); } if (this._writableAutoDestroy && !this._closeScheduled) { this._closeScheduled = true; queueMicrotask(() => this.destroy()); } } } + write(chunk, encoding, cb) { if (typeof encoding === "function") { cb = encoding; encoding = undefined; } if (this.destroyed || this.writableEnded) { const error = streamError("ERR_STREAM_WRITE_AFTER_END", "write after end"); queueMicrotask(() => { if (cb) cb(error); else this.emit("error", error); }); return false; } const size = chunkLength(chunk); this._pendingWrites += 1; this._writableBufferedBytes += size; const accepted = this._writableBufferedBytes < this._writableHighWaterMark; if (!accepted) this._writableNeedDrain = true; let completed = false; const done = (error) => { if (completed) return; completed = true; queueMicrotask(() => { this._pendingWrites -= 1; this._writableBufferedBytes = Math.max(0, this._writableBufferedBytes - size); if (error) { if (cb) cb(error); this.destroy(error); return; } if (cb) cb(); if (this._writableNeedDrain && this._writableBufferedBytes < this._writableHighWaterMark) { this._writableNeedDrain = false; this.emit("drain"); } this._finishIfReady(); }); }; try { if (this._writeImpl) this._writeImpl(chunk, encoding, done); else { this.emit("data", chunk); done(); } } catch (error) { done(error); } return accepted; } + end(chunk, encoding, cb) { const done = typeof chunk === "function" ? chunk : typeof encoding === "function" ? encoding : cb; if (this.writableEnded) { const error = streamError("ERR_STREAM_ALREADY_FINISHED", "end called after stream finished"); queueMicrotask(() => done ? done(error) : this.emit("error", error)); return this; } if (chunk != null && typeof chunk !== "function") this.write(chunk, typeof encoding === "string" ? encoding : undefined); this.writableEnded = true; this._endCallback = done || null; this._finishIfReady(); return this; } + destroy(error) { if (this.destroyed) return this; this.destroyed = true; this.writable = false; if (error) this.emit("error", error); this.emit("close"); return this; } } - Writable.toWeb = (stream) => new WritableStream({ write(chunk) { return new Promise((resolve) => stream.write(chunk, undefined, () => resolve())); }, close() { stream.end && stream.end(); } }); - class Duplex extends Readable { constructor(options) { super(options); this.writable = true; if (options && options.write) this._writeImpl = options.write; } write(chunk, encoding, cb) { if (typeof encoding === "function") { cb = encoding; } if (this._writeImpl) this._writeImpl(chunk, encoding, cb || (() => {})); else if (cb) cb(); return true; } end(chunk) { if (chunk != null) this.write(chunk); this.emit("finish"); this.emit("end"); } } + Writable.toWeb = (stream) => new WritableStream({ start(controller) { stream.on("error", (error) => controller.error(error)); }, write(chunk) { return new Promise((resolve, reject) => stream.write(chunk, undefined, (error) => error ? reject(error) : resolve())); }, close() { return new Promise((resolve, reject) => stream.end((error) => error ? reject(error) : resolve())); }, abort(reason) { stream.destroy && stream.destroy(reason instanceof Error ? reason : new Error(String(reason))); } }); + class Duplex extends Readable { constructor(options) { super(options); this.writable = true; this.writableEnded = false; this.writableFinished = false; this._pendingWrites = 0; this._writableBufferedBytes = 0; this._writableNeedDrain = false; this._writableHighWaterMark = Number(options && options.highWaterMark) || 16384; this._writableOptions = options || {}; this._writableAutoDestroy = this._writableOptions.autoDestroy !== false; this._writeImpl = options && options.write; } write(...args) { return Writable.prototype.write.apply(this, args); } end(...args) { return Writable.prototype.end.apply(this, args); } _finishIfReady() { return Writable.prototype._finishIfReady.call(this); } destroy(error) { if (this.destroyed) return this; this.writable = false; return Readable.prototype.destroy.call(this, error); } } class Transform extends Duplex {} - class PassThrough extends Transform { write(chunk, encoding, cb) { if (typeof encoding === "function") { cb = encoding; } this.emit("data", chunk); if (cb) cb(); return true; } end(chunk) { if (chunk != null) this.emit("data", chunk); this.emit("end"); this.emit("finish"); } } + class PassThrough extends Transform { constructor(options) { super(options); this._writeImpl = (chunk, _encoding, callback) => { this.push(chunk); callback(); }; } end(chunk, encoding, cb) { const done = typeof chunk === "function" ? chunk : typeof encoding === "function" ? encoding : cb; if (chunk != null && typeof chunk !== "function") this.write(chunk, typeof encoding === "string" ? encoding : undefined); this.push(null); return Writable.prototype.end.call(this, done); } } function finished(stream, optsOrCb, maybeCb) { const cb = typeof optsOrCb === "function" ? optsOrCb : maybeCb; if (stream && stream.on) { let done = false; const fire = (e) => { if (done) return; done = true; if (cb) cb(e || null); }; stream.on("end", () => fire()); stream.on("finish", () => fire()); stream.on("close", () => fire()); stream.on("error", (e) => fire(e)); } @@ -7692,10 +7760,42 @@ if (module.exports && module.exports.default == null) module.exports.default = m } emit(event, ...args) { const listeners = this._listeners.get(event) || []; + if (listeners.length === 0 && event === "error") { + throw (args[0] instanceof Error ? args[0] : new Error(String(args[0]))); + } for (const listener of [...listeners]) listener(...args); return listeners.length > 0; } } + const streamStateError = (code, message) => { + const error = new Error(message); + error.code = code; + return error; + }; + class ChildOutputStream extends Emitter { + constructor() { + super(); + this.readable = true; + this.readableEnded = false; + this.destroyed = false; + } + endReadable() { + if (this.readableEnded) return; + this.readableEnded = true; + this.readable = false; + this.emit("end"); + this.destroyed = true; + this.emit("close"); + } + destroy(error) { + if (this.destroyed) return this; + this.destroyed = true; + this.readable = false; + if (error) this.emit("error", error); + this.emit("close"); + return this; + } + } class ChildProcess extends Emitter { constructor(sessionId) { super(); @@ -7703,17 +7803,77 @@ if (module.exports && module.exports.default == null) module.exports.default = m this.exitCode = null; this.signalCode = null; this.killed = false; - this.stdout = new Emitter(); - this.stderr = new Emitter(); + this.stdout = new ChildOutputStream(); + this.stderr = new ChildOutputStream(); + this.spawnfile = ""; + this.spawnargs = []; + let stdinEnded = false; this.stdin = { - write: (data) => { - callSync(globalThis._childProcessStdinWrite, sessionId, typeof data === "string" ? new TextEncoder().encode(data) : data); - return true; + writable: true, + writableEnded: false, + destroyed: false, + write: (data, encoding, callback) => { + const done = typeof encoding === "function" ? encoding : callback; + if (stdinEnded) { + const error = streamStateError("ERR_STREAM_WRITE_AFTER_END", "write after end"); + queueMicrotask(() => done ? done(error) : this.stdin.emit("error", error)); + return false; + } + try { + callSync(globalThis._childProcessStdinWrite, sessionId, typeof data === "string" ? new TextEncoder().encode(data) : data); + if (done) queueMicrotask(() => done()); + return true; + } catch (error) { + queueMicrotask(() => done ? done(error) : this.stdin.emit("error", error)); + return false; + } }, - end: (data) => { - if (data != null) this.stdin.write(data); - callSync(globalThis._childProcessStdinClose, sessionId); + end: (data, encoding, callback) => { + const done = typeof data === "function" ? data : typeof encoding === "function" ? encoding : callback; + if (stdinEnded) { + if (done) queueMicrotask(() => done()); + return this.stdin; + } + if (data != null && typeof data !== "function") this.stdin.write(data, typeof encoding === "string" ? encoding : undefined); + stdinEnded = true; + this.stdin.writable = false; + this.stdin.writableEnded = true; + try { + callSync(globalThis._childProcessStdinClose, sessionId); + this.stdin.emit("finish"); + queueMicrotask(() => { + if (done) done(); + this.stdin.destroyed = true; + this.stdin.emit("close"); + }); + } catch (error) { + queueMicrotask(() => { + if (done) done(error); + else this.stdin.emit("error", error); + this.stdin.destroyed = true; + this.stdin.emit("close"); + }); + } + return this.stdin; + }, + destroy: (error) => { + if (this.stdin.destroyed) return this.stdin; + if (!stdinEnded) { + stdinEnded = true; + try { callSync(globalThis._childProcessStdinClose, sessionId); } catch (closeError) { if (!error) error = closeError; } + } + this.stdin.writable = false; + this.stdin.writableEnded = true; + this.stdin.destroyed = true; + if (error) this.stdin.emit("error", error); + this.stdin.emit("close"); + return this.stdin; }, + on: (...args) => { this.stdin._emitter.on(...args); return this.stdin; }, + once: (...args) => { this.stdin._emitter.once(...args); return this.stdin; }, + off: (...args) => { this.stdin._emitter.off(...args); return this.stdin; }, + emit: (...args) => this.stdin._emitter.emit(...args), + _emitter: new Emitter(), }; } } @@ -7741,6 +7901,11 @@ if (module.exports && module.exports.default == null) module.exports.default = m if (numeric !== undefined) return numeric; throw unknownSignalError(signal); }; + const signalNames = Object.entries(signalNumbers).reduce((names, [name, number]) => { + if (names[number] === undefined) names[number] = name; + return names; + }, {}); + const signalName = (signal) => signal == null ? null : typeof signal === "string" ? (signal.startsWith("SIG") ? signal : "SIG" + signal) : signalNames[Number(signal)] || null; const unknownSignalError = (signal) => { const error = new TypeError("Unknown signal: " + String(signal)); error.code = "ERR_UNKNOWN_SIGNAL"; @@ -7756,7 +7921,8 @@ if (module.exports && module.exports.default == null) module.exports.default = m command: String(command), args: args.map(String), options: { - cwd: options.cwd || (globalThis.process && globalThis.process.cwd ? globalThis.process.cwd() : "/"), + argv0: options.argv0 === undefined ? undefined : String(options.argv0), + cwd: options.cwd ?? (globalThis.process && globalThis.process.cwd ? globalThis.process.cwd() : "/"), env: options.env, }, }, @@ -7767,8 +7933,12 @@ if (module.exports && module.exports.default == null) module.exports.default = m return child; } const child = new ChildProcess(sessionId); + child.spawnfile = String(command); + child.spawnargs = [options.argv0 === undefined ? String(command) : String(options.argv0), ...args.map(String)]; child.kill = (signal) => { - callSync(globalThis._childProcessKill, sessionId, normalizeSignal(signal)); + if (child.exitCode != null || child.signalCode != null) return false; + const accepted = callSync(globalThis._childProcessKill, sessionId, normalizeSignal(signal)); + if (accepted === false) return false; child.killed = true; return true; }; @@ -7789,10 +7959,13 @@ if (module.exports && module.exports.default == null) module.exports.default = m return; } if (event.type === "exit") { - child.exitCode = event.exitCode; - child.signalCode = event.signal; - child.emit("exit", event.exitCode, event.signal); - child.emit("close", event.exitCode, event.signal); + const terminationSignal = signalName(event.signal); + child.exitCode = terminationSignal == null ? event.exitCode : null; + child.signalCode = terminationSignal; + child.emit("exit", child.exitCode, terminationSignal); + child.stdout.endReadable(); + child.stderr.endReadable(); + child.emit("close", child.exitCode, terminationSignal); } }; queueMicrotask(() => { @@ -7810,7 +7983,8 @@ if (module.exports && module.exports.default == null) module.exports.default = m command: String(command), args: args.map(String), options: { - cwd: options.cwd || (globalThis.process && globalThis.process.cwd ? globalThis.process.cwd() : "/"), + argv0: options.argv0 === undefined ? undefined : String(options.argv0), + cwd: options.cwd ?? (globalThis.process && globalThis.process.cwd ? globalThis.process.cwd() : "/"), env: options.env, input: encodeBytes(options.input), }, @@ -7824,8 +7998,8 @@ if (module.exports && module.exports.default == null) module.exports.default = m output: [null, stdout, stderr], stdout, stderr, - status: result.code, - signal: null, + status: result.signal == null ? result.code : null, + signal: signalName(result.signal), error: undefined, }; } catch (error) { @@ -9089,19 +9263,22 @@ if (module.exports && module.exports.default == null) module.exports.default = m verify: verifyOneShot, webcrypto: globalThis.crypto, }; - `,"node:crypto":"module.exports = require('crypto');",wasi:Ju,"node:wasi":"module.exports = require('wasi');","secure-exec:wasi-command-host":` + `,"node:crypto":"module.exports = require('crypto');",wasi:ef,"node:wasi":"module.exports = require('wasi');","secure-exec:wasi-command-host":` + const callSync = (ref, ...args) => { + if (typeof ref === "function") return ref(...args); + if (ref && typeof ref.applySync === "function") return ref.applySync(undefined, args); + if (ref && typeof ref.applySyncPromise === "function") return ref.applySyncPromise(undefined, args); + throw new Error("process exec bridge is not configured"); + }; function defaultDecode(bytes) { return new TextDecoder().decode(bytes); } function decodeNullSeparated(bytes) { - const out = []; - let start = 0; - for (let i = 0; i <= bytes.length; i += 1) { - if (i === bytes.length || bytes[i] === 0) { - if (i > start) out.push(defaultDecode(bytes.slice(start, i))); - start = i + 1; - } - } + if (!bytes || bytes.length === 0) return []; + const out = defaultDecode(bytes).split("\0"); + // libc/std encode a terminal NUL after the last element. Remove exactly + // that terminator while preserving every intentional empty element. + if (bytes[bytes.length - 1] === 0) out.pop(); return out; } function parseEnv(bytes) { @@ -9130,29 +9307,97 @@ if (module.exports && module.exports.default == null) module.exports.default = m const modules = new Map(); for (const [name, source] of Object.entries(commands || {})) { const value = await readCommandBytes(source); - modules.set(name, value instanceof WebAssembly.Module ? value : new WebAssembly.Module(value)); + const module = value instanceof WebAssembly.Module ? value : new WebAssembly.Module(value); + modules.set(name, module); } return modules; } + function unsupportedBrowserNetworkError(image, networkImports) { + const subject = image && (image.commandPath || image.argv?.[0]); + const error = new Error( + "browser WASI command " + (subject || "") + + " requires unsupported host_net imports: " + networkImports.join(", ") + + "; run this command in the native runtime", + ); + error.code = "ERR_AGENTOS_BROWSER_WASM_NETWORK_UNSUPPORTED"; + return error; + } + function assertBrowserNetworkSupported(image) { + const networkImports = WebAssembly.Module.imports(image.module) + .filter((entry) => entry.module === "host_net") + .map((entry) => entry.name); + if (networkImports.length > 0) { + throw unsupportedBrowserNetworkError(image, networkImports); + } + } async function createWasiCommandHost(options) { const WASI = options && options.WASI ? options.WASI : require("node:wasi").WASI; const commandModules = await loadCommandModules(options && options.commands); + const trustedMaxSpawnFileActions = /* @agentos-process-max-spawn-file-actions */ 4096; + const trustedMaxSpawnFileActionBytes = /* @agentos-process-max-spawn-file-action-bytes */ 1048576; + const requestedMaxSpawnFileActions = Number(options?.maxSpawnFileActions); + const requestedMaxSpawnFileActionBytes = Number(options?.maxSpawnFileActionBytes); + // Guest options may lower their own cap, but can never raise the trusted VM + // policy embedded by the worker when this builtin is loaded. + const maxSpawnFileActions = Number.isSafeInteger(requestedMaxSpawnFileActions) && requestedMaxSpawnFileActions > 0 + ? Math.min(requestedMaxSpawnFileActions, trustedMaxSpawnFileActions) + : trustedMaxSpawnFileActions; + const maxSpawnFileActionBytes = Number.isSafeInteger(requestedMaxSpawnFileActionBytes) && requestedMaxSpawnFileActionBytes > 0 + ? Math.min(requestedMaxSpawnFileActionBytes, trustedMaxSpawnFileActionBytes) + : trustedMaxSpawnFileActionBytes; + let warnedSpawnFileActions = false; + let warnedSpawnFileActionBytes = false; let memory = null; let nextPid = 100; + let activePid = 1; + let activePpid = 0; + const processGroups = new Map([[1, 1]]); const exitedChildren = new Map(); const deferredChildren = new Map(); const waitBuffer = new SharedArrayBuffer(4); const wait = new Int32Array(waitBuffer); const errnoSuccess = 0; + const errno2big = 1; + const errnoAcces = 2; const errnoBadf = 8; const errnoChild = 10; + const errnoFbig = 22; + const errnoInval = 28; + const errnoIo = 29; + const errnoLoop = 32; + const errnoNoent = 44; + const errnoNoexec = 45; const errnoNosys = 52; - let nextSyntheticFd = 1000; + const errnoNotsup = 58; + const errnoPerm = 63; + const errnoRofs = 69; + const errnoSrch = 71; + const errnoMfile = 33; + const linuxBinprmBufferSize = 256; + const linuxMaxInterpreterDepth = 4; + const configuredModuleFileBytes = Number(options?.maxModuleFileBytes); + const maxModuleFileBytes = Number.isSafeInteger(configuredModuleFileBytes) && configuredModuleFileBytes >= 0 + ? configuredModuleFileBytes + : 256 * 1024 * 1024; + const syntheticFdBase = 1 << 20; + const configuredSyntheticFdCount = Number(options?.maxSyntheticFds); + const maxSyntheticFdCount = Number.isSafeInteger(configuredSyntheticFdCount) && configuredSyntheticFdCount > 0 && configuredSyntheticFdCount <= 0xffffffff - syntheticFdBase + ? configuredSyntheticFdCount + : 4096; + const syntheticFdLimit = syntheticFdBase + maxSyntheticFdCount; + const syntheticFdWarnAt = Math.max(1, Math.floor(maxSyntheticFdCount * 0.9)); + let warnedAboutSyntheticFds = false; const syntheticFdEntries = new Map(); + let activeCloexecFds = new Set(); let activeFdOverrides = null; let activeChildCwd = null; + let activeWasi = null; let previousLookupFdHandle = null; let parentWasi = null; + const knownChildren = new Set(); + const runningChildren = new Set(); + let activeSpawnCallContext = null; + let activeBlockedSignals = new Set(); const getMemory = () => { if (!memory) throw new Error("WASI host command memory is not set"); return memory; @@ -9168,6 +9413,142 @@ if (module.exports && module.exports.default == null) module.exports.default = m }; const readBytes = (ptr, len) => bytes().slice(ptr >>> 0, (ptr >>> 0) + (len >>> 0)); const readString = (ptr, len) => defaultDecode(readBytes(ptr, len)); + const decodeSignalMask = (maskLo, maskHi) => { + const signals = []; + const lo = Number(maskLo) >>> 0; + const hi = Number(maskHi) >>> 0; + for (let bit = 0; bit < 32; bit += 1) { + if (((lo >>> bit) & 1) === 1) signals.push(bit + 1); + if (((hi >>> bit) & 1) === 1) signals.push(bit + 33); + } + return signals; + }; + const encodeSignalMask = (signals) => { + let lo = 0; + let hi = 0; + for (const signal of signals) { + if (signal >= 1 && signal <= 32) lo = (lo | (1 << (signal - 1))) >>> 0; + else if (signal >= 33 && signal <= 64) hi = (hi | (1 << (signal - 33))) >>> 0; + } + return { lo, hi }; + }; + const decodeSpawnActions = (actionsPtr, actionsLen, initialCwd) => { + const failLimit = (message) => { + if (process && process.stderr && typeof process.stderr.write === "function") { + process.stderr.write("[agentos] " + message + "\\n"); + } + const error = new Error(message); + error.code = "E2BIG"; + throw error; + }; + const warnNearLimit = (kind, current, limit) => { + const countLimit = kind === "actions"; + if ((countLimit ? warnedSpawnFileActions : warnedSpawnFileActionBytes) || + current < Math.ceil(limit * 0.9)) return; + if (countLimit) warnedSpawnFileActions = true; + else warnedSpawnFileActionBytes = true; + const setting = countLimit + ? "limits.process.maxSpawnFileActions" + : "limits.process.maxSpawnFileActionBytes"; + if (process && process.stderr && typeof process.stderr.write === "function") { + process.stderr.write("[agentos] posix_spawn file-action " + kind + " near " + + setting + " (" + current + "/" + limit + "); raise " + setting + " if needed\\n"); + } + }; + if ((actionsLen >>> 0) > maxSpawnFileActionBytes) { + failLimit("posix_spawn file-action payload exceeds limits.process.maxSpawnFileActionBytes (" + + maxSpawnFileActionBytes + "); raise limits.process.maxSpawnFileActionBytes if needed"); + } + warnNearLimit("bytes", actionsLen >>> 0, maxSpawnFileActionBytes); + const value = readBytes(actionsPtr, actionsLen); + const data = new DataView(value.buffer, value.byteOffset, value.byteLength); + const stdio = [0, 1, 2]; + const closed = new Set(); + const actions = []; + const actionFdPaths = new Map(); + let cwd = initialCwd; + let offset = 0; + let actionCount = 0; + const fail = (code, message) => { + const error = new Error(message); + error.code = code; + throw error; + }; + while (offset < value.byteLength) { + actionCount++; + if (actionCount > maxSpawnFileActions) { + failLimit("posix_spawn file actions exceed limits.process.maxSpawnFileActions (" + + maxSpawnFileActions + "); raise limits.process.maxSpawnFileActions if needed"); + } + warnNearLimit("actions", actionCount, maxSpawnFileActions); + if (value.byteLength - offset < 24) fail("EINVAL", "truncated posix_spawn action header"); + const command = data.getUint32(offset, true); + const fd = data.getInt32(offset + 4, true); + const sourceFd = data.getInt32(offset + 8, true); + const oflag = data.getInt32(offset + 12, true); + const mode = data.getUint32(offset + 16, true); + const pathLength = data.getUint32(offset + 20, true); + offset += 24; + const pathEnd = offset + pathLength; + if (pathEnd < offset || pathEnd > value.byteLength) fail("EINVAL", "truncated posix_spawn action path"); + const actionPath = defaultDecode(value.subarray(offset, pathEnd)); + offset = pathEnd; + if (command === 1) { + if (fd < 0) fail("EBADF", "posix_spawn close has an invalid fd"); + closed.add(fd); + actionFdPaths.delete(fd); + if (fd <= 2) stdio[fd] = 0xffffffff; + actions.push({ command, fd, sourceFd, oflag, mode, path: "" }); + } else if (command === 2) { + if (fd < 0 || sourceFd < 0 || closed.has(sourceFd)) fail("EBADF", "posix_spawn dup2 references a closed fd"); + const source = sourceFd <= 2 ? stdio[sourceFd] : sourceFd; + if (source === 0xffffffff) fail("EBADF", "posix_spawn dup2 references a closed fd"); + if (fd <= 2) stdio[fd] = source; + closed.delete(fd); + const sourcePath = actionFdPaths.get(sourceFd) || guestPathForBrowserFd(sourceFd); + if (sourcePath) actionFdPaths.set(fd, sourcePath); + else actionFdPaths.delete(fd); + actions.push({ command, fd, sourceFd, oflag, mode, path: "" }); + } else if (command === 3) { + if (fd < 0) fail("EBADF", "posix_spawn open has an invalid fd"); + if (!actionPath) fail("ENOENT", "posix_spawn open path is empty"); + const resolvedPath = path().posix.resolve(cwd || "/", actionPath); + closed.delete(fd); + actionFdPaths.set(fd, resolvedPath); + if (fd <= 2) stdio[fd] = fd; + actions.push({ command, fd, sourceFd, oflag, mode, path: resolvedPath }); + } else if (command === 4) { + if (!actionPath) fail("ENOENT", "posix_spawn chdir path is empty"); + cwd = path().posix.resolve(cwd || "/", actionPath); + actions.push({ command, fd, sourceFd, oflag, mode, path: cwd }); + } else if (command === 5) { + if (fd < 0 || closed.has(fd)) fail("EBADF", "posix_spawn fchdir has an invalid fd"); + const directoryPath = actionFdPaths.get(fd) || guestPathForBrowserFd(fd); + if (!directoryPath) fail("EBADF", "posix_spawn fchdir references an unknown fd"); + cwd = directoryPath; + actions.push({ command, fd, sourceFd, oflag, mode, path: cwd }); + } else if (command === 6) { + if (fd < 0) fail("EBADF", "posix_spawn closefrom has an invalid fd"); + for (const guestFd of new Set([ + ...syntheticFdEntries.keys(), + ...(activeFdOverrides ? activeFdOverrides.keys() : []), + ...actionFdPaths.keys(), + ])) { + if (guestFd < fd) continue; + closed.add(guestFd); + actionFdPaths.delete(guestFd); + } + for (let stdioFd = Math.max(fd, 0); stdioFd <= 2; stdioFd += 1) { + closed.add(stdioFd); + stdio[stdioFd] = 0xffffffff; + } + actions.push({ command, fd, sourceFd, oflag, mode, path: "" }); + } else { + fail("EINVAL", "unknown posix_spawn action opcode " + command); + } + } + return { stdio, cwd, actions }; + }; const fs = () => require("node:fs"); const path = () => require("node:path"); const userRecord = new TextEncoder().encode( @@ -9180,6 +9561,24 @@ if (module.exports && module.exports.default == null) module.exports.default = m if (stat && typeof stat.isSymbolicLink === "function" && stat.isSymbolicLink()) return 0o120777; return fallback >>> 0; }; + const hostFsErrno = (error) => { + switch (error && typeof error === "object" ? error.code : undefined) { + case "E2BIG": return errno2big; + case "EACCES": return errnoAcces; + case "EBADF": return errnoBadf; + case "EFBIG": return errnoFbig; + case "EMFILE": return errnoMfile; + case "EPERM": return errnoPerm; + case "ENOENT": return errnoNoent; + case "ENOEXEC": return errnoNoexec; + case "ELOOP": return errnoLoop; + case "EINVAL": return errnoInval; + case "ENOTSUP": return errnoNotsup; + case "EISDIR": return errnoInval; + case "EROFS": return errnoRofs; + default: return errnoIo; + } + }; const currentGuestCwd = () => { const cwd = typeof activeChildCwd === "string" && activeChildCwd.startsWith("/") ? activeChildCwd @@ -9194,27 +9593,214 @@ if (module.exports && module.exports.default == null) module.exports.default = m ? path().posix.normalize(value) : path().posix.resolve(currentGuestCwd(), value); }; + const pathEntries = (env) => { + const value = env && Object.prototype.hasOwnProperty.call(env, "PATH") + ? String(env.PATH) + : "/bin:/usr/bin"; + return value.split(":").map((entry) => entry || "."); + }; + const resolveCommandModule = (commandPath, env, cwd) => { + const raw = String(commandPath); + const hasSlash = raw.includes("/"); + const normalized = hasSlash + ? path().posix.resolve(cwd || "/", raw) + : raw; + if (hasSlash) return commandModules.get(normalized) || null; + for (const directory of pathEntries(env)) { + const candidate = path().posix.resolve(cwd || "/", directory, raw); + if (commandModules.has(candidate)) return commandModules.get(candidate); + } + return null; + }; + const resolveExecModule = (commandPath, cwd) => { + const raw = String(commandPath); + const normalized = path().posix.resolve(cwd || "/", raw); + return commandModules.get(normalized) || null; + }; + const executableError = (code, message) => { + const error = new Error(message); + error.code = code; + return error; + }; + const validateExecutableStat = (stat, subject) => { + if (!stat || typeof stat.isFile !== "function" || !stat.isFile()) { + throw executableError("EACCES", subject + " is not a regular executable file"); + } + if ((Number(stat.mode) & 0o111) === 0) { + throw executableError("EACCES", subject + " does not have an executable mode bit"); + } + const size = Number(stat.size); + if (!Number.isSafeInteger(size) || size < 0 || size > maxModuleFileBytes) { + throw executableError( + "EFBIG", + subject + " exceeds limits.wasm.maxModuleFileBytes (" + maxModuleFileBytes + "); raise limits.wasm.maxModuleFileBytes if needed", + ); + } + return size; + }; + const parseLinuxShebang = (value) => { + const executableBytes = value instanceof Uint8Array ? value : new Uint8Array(value); + if (executableBytes.length < 2 || executableBytes[0] !== 0x23 || executableBytes[1] !== 0x21) return null; + const header = executableBytes.slice(2, Math.min(executableBytes.length, linuxBinprmBufferSize)); + const newline = header.indexOf(0x0a); + let line = new TextDecoder().decode(newline >= 0 ? header.slice(0, newline) : header).replace(/[ ]+$/u, ""); + const first = line.search(/[^ ]/u); + if (first < 0) throw executableError("ENOEXEC", "shebang does not name an interpreter"); + line = line.slice(first); + const separator = line.search(/[ ]/u); + if (newline < 0 && executableBytes.length >= linuxBinprmBufferSize && separator < 0) { + throw executableError("ENOEXEC", "shebang interpreter path exceeds the Linux header limit"); + } + if (separator < 0) return { interpreter: line, optionalArgument: null }; + const optionalArgument = line.slice(separator).replace(/^[ ]+|[ ]+$/gu, ""); + return { + interpreter: line.slice(0, separator), + optionalArgument: optionalArgument || null, + }; + }; + const readExecutableFd = (targetFd, subject) => { + const stat = fs().fstatSync(targetFd); + const size = validateExecutableStat(stat, subject); + const value = new Uint8Array(size); + let offset = 0; + while (offset < size) { + const count = fs().readSync(targetFd, value, offset, size - offset, offset); + if (!Number.isInteger(count) || count <= 0) throw executableError("EIO", subject + " changed while being read"); + offset += count; + } + return value; + }; + const compileBrowserExecImage = (value, subject, argv, depth = 0) => { + const shebang = parseLinuxShebang(value); + if (shebang) { + if (depth >= linuxMaxInterpreterDepth) throw executableError("ELOOP", "interpreter recursion exceeds the Linux limit"); + return loadBrowserExecImage( + shebang.interpreter, + [ + shebang.interpreter, + ...(shebang.optionalArgument === null ? [] : [shebang.optionalArgument]), + String(subject), + ...argv.slice(1), + ], + depth + 1, + ); + } + try { + return { module: new WebAssembly.Module(value), argv }; + } catch (error) { + if (error instanceof WebAssembly.CompileError) throw executableError("ENOEXEC", subject + " is not a supported WebAssembly executable image"); + throw error; + } + }; + const loadBrowserExecImage = (commandPath, argv, depth = 0) => { + const normalized = path().posix.resolve(currentGuestCwd(), String(commandPath)); + const registered = commandModules.get(normalized); + if (registered) return { module: registered, argv }; + const stat = fs().statSync(normalized); + validateExecutableStat(stat, normalized); + const value = fs().readFileSync(normalized); + if (Number(value.byteLength) > maxModuleFileBytes) throw executableError("EFBIG", normalized + " exceeds limits.wasm.maxModuleFileBytes"); + return compileBrowserExecImage(value, String(commandPath), argv, depth); + }; + const releaseGuestFileDescription = (description) => { + description.refCount = Math.max(0, (description.refCount || 1) - 1); + if (description.refCount === 0 && description.closed !== true) { + description.closed = true; + fs().closeSync(description.targetFd); + } + }; + const createGuestFileHandle = (description, onClose) => { + let open = true; + const handle = { kind: "guest-file", description, onClose }; + Object.defineProperties(handle, { + open: { + get: () => open, + set: (value) => { + if (value !== false || !open) return; + open = false; + if (typeof handle.onClose === "function") handle.onClose(handle); + releaseGuestFileDescription(description); + }, + }, + targetFd: { get: () => description.targetFd }, + position: { + get: () => description.position, + set: (value) => { + description.position = value; + if (description.ownerEntry) description.ownerEntry.offset = value; + }, + }, + readOnly: { get: () => description.readOnly }, + append: { get: () => description.append }, + }); + return handle; + }; const lookupSyntheticFd = (fd) => { const descriptor = fd >>> 0; const override = activeFdOverrides && activeFdOverrides.get(descriptor); if (override && override.open !== false) return override; const handle = syntheticFdEntries.get(descriptor); if (handle && handle.open !== false) return handle; - if (typeof previousLookupFdHandle === "function") return previousLookupFdHandle(descriptor); - const parentEntry = parentWasi && parentWasi.fdTable && parentWasi.fdTable.get(descriptor); - if (parentEntry && parentEntry.kind === "file" && typeof parentEntry.realFd === "number") { - return { - kind: "guest-file", - targetFd: parentEntry.realFd, - position: typeof parentEntry.offset === "number" ? parentEntry.offset : 0, - readOnly: parentEntry.readOnly === true, - open: true, - }; + if (typeof previousLookupFdHandle === "function") { + const previous = previousLookupFdHandle(descriptor); + if (previous) return previous; + } + const wasi = activeWasi || parentWasi; + const parentEntry = wasi && wasi.fdTable && wasi.fdTable.get(descriptor); + if (parentEntry && (parentEntry.kind === "file" || parentEntry.kind === "directory") && typeof parentEntry.realFd === "number") { + if (!parentEntry.__agentOSOpenFileDescription) { + const rights = typeof parentEntry.rightsBase === "bigint" ? parentEntry.rightsBase : null; + const initialPosition = typeof parentEntry.offset === "number" ? parentEntry.offset : 0; + const description = { + targetFd: parentEntry.realFd, + storedPosition: initialPosition, + readOnly: parentEntry.readOnly === true, + canRead: rights == null || (rights & (1n << 1n)) !== 0n, + canWrite: parentEntry.readOnly !== true && (rights == null || (rights & (1n << 6n)) !== 0n), + append: parentEntry.append === true, + isDirectory: parentEntry.kind === "directory", + refCount: 1, + ownerEntry: parentEntry, + }; + Object.defineProperty(description, "position", { + get: () => typeof description.ownerEntry?.offset === "number" + ? description.ownerEntry.offset + : description.storedPosition, + set: (value) => { + description.storedPosition = value; + if (description.ownerEntry) description.ownerEntry.offset = value; + }, + }); + parentEntry.__agentOSOpenFileDescription = description; + } + return createGuestFileHandle( + parentEntry.__agentOSOpenFileDescription, + () => { + if (wasi.fdTable.get(descriptor) === parentEntry) wasi.fdTable.delete(descriptor); + }, + ); + } + return null; + }; + const guestPathForBrowserFd = (fd) => { + const handle = lookupSyntheticFd(fd); + const description = handle?.kind === "guest-file" ? handle.description : null; + if (typeof description?.guestPath === "string") return description.guestPath; + const wasi = activeWasi || parentWasi; + const entry = wasi?.fdTable?.get(Number(fd) >>> 0); + for (const candidate of [entry?.guestPath, entry?.path, entry?.preopenPath]) { + if (typeof candidate === "string" && candidate.startsWith("/")) { + return path().posix.normalize(candidate); + } } return null; }; const closeSyntheticHandle = (handle) => { if (!handle || handle.open === false) return; + if (handle.kind === "guest-file") { + handle.open = false; + return; + } handle.open = false; if (handle.kind === "pipe-read" && handle.pipe) { handle.pipe.readHandleCount = Math.max(0, (handle.pipe.readHandleCount || 0) - 1); @@ -9229,68 +9815,449 @@ if (module.exports && module.exports.default == null) module.exports.default = m return { kind: "stdio", targetFd: handle.targetFd, open: true }; } if (handle.kind === "guest-file") { - return { ...handle, open: true }; + if (!handle.description) return null; + handle.description.refCount = (handle.description.refCount || 1) + 1; + return createGuestFileHandle(handle.description); } if (!handle.pipe) return null; if (handle.kind === "pipe-read") { handle.pipe.readHandleCount = (handle.pipe.readHandleCount || 0) + 1; - return { kind: "pipe-read", pipe: handle.pipe, open: true, onClose: handle.onClose }; + const onClose = handle.baseOnClose || handle.onClose; + return { kind: "pipe-read", pipe: handle.pipe, open: true, baseOnClose: onClose, onClose }; } if (handle.kind === "pipe-write") { handle.pipe.writeHandleCount = (handle.pipe.writeHandleCount || 0) + 1; - return { kind: "pipe-write", pipe: handle.pipe, open: true, onClose: handle.onClose }; + const onClose = handle.baseOnClose || handle.onClose; + return { kind: "pipe-write", pipe: handle.pipe, open: true, baseOnClose: onClose, onClose }; } return null; }; + const cloneOpenDescriptorsForSpawn = () => { + const overrides = new Map(); + const wasi = activeWasi || parentWasi; + const openFds = new Set([ + ...(activeFdOverrides ? activeFdOverrides.keys() : []), + ...syntheticFdEntries.keys(), + ...(wasi?.fdTable instanceof Map ? wasi.fdTable.keys() : []), + ]); + for (const fd of [...openFds].sort((left, right) => left - right)) { + const handle = lookupSyntheticFd(fd); + const cloned = cloneSyntheticHandle(handle); + if (cloned) overrides.set(fd, cloned); + } + return overrides; + }; + const browserOpenFlagsFromSpawnAction = (oflag) => { + const value = Number(oflag) >>> 0; + let flags = (value & 0x10000000) !== 0 + ? ((value & 0x04000000) !== 0 ? 0o2 : 0o1) + : 0; + if ((value & 0x00000001) !== 0) flags |= 0o2000; + if ((value & 0x00000004) !== 0) flags |= 0o4000; + if ((value & (1 << 12)) !== 0) flags |= 0o100; + if ((value & (4 << 12)) !== 0) flags |= 0o200; + if ((value & (8 << 12)) !== 0) flags |= 0o1000; + return flags; + }; + const applyBrowserSpawnFileActions = (overrides, actions, cloexecFds) => { + for (const action of actions || []) { + const fd = Number(action.fd); + if (!Number.isInteger(fd) || fd < 0) { + const error = new Error("posix_spawn action has an invalid fd"); + error.code = "EBADF"; + throw error; + } + if (action.command === 1) { + const handle = overrides.get(fd); + if (!handle) { + const error = new Error("posix_spawn close references a closed fd"); + error.code = "EBADF"; + throw error; + } + closeSyntheticHandle(handle); + overrides.delete(fd); + cloexecFds.delete(fd); + continue; + } + if (action.command === 2) { + const sourceFd = Number(action.sourceFd); + const source = overrides.get(sourceFd); + if (!source) { + const error = new Error("posix_spawn dup2 references a closed fd"); + error.code = "EBADF"; + throw error; + } + if (sourceFd === fd) { + cloexecFds.delete(fd); + continue; + } + const cloned = cloneSyntheticHandle(source); + if (!cloned) { + const error = new Error("posix_spawn dup2 cannot duplicate this fd"); + error.code = "EBADF"; + throw error; + } + const replaced = overrides.get(fd); + if (replaced) closeSyntheticHandle(replaced); + overrides.set(fd, cloned); + cloexecFds.delete(fd); + continue; + } + if (action.command === 3) { + const replaced = overrides.get(fd); + if (replaced) { + closeSyntheticHandle(replaced); + overrides.delete(fd); + } + const targetFd = fs().openSync( + action.path, + browserOpenFlagsFromSpawnAction(action.oflag), + Number(action.mode) >>> 0, + ); + const stat = fs().fstatSync(targetFd); + if ((Number(action.oflag) & (2 << 12)) !== 0 && !stat.isDirectory()) { + fs().closeSync(targetFd); + const error = new Error("posix_spawn open target is not a directory"); + error.code = "ENOTDIR"; + throw error; + } + const rawFlags = Number(action.oflag) >>> 0; + const canWrite = (rawFlags & 0x10000000) !== 0; + const canRead = !canWrite || (rawFlags & 0x04000000) !== 0; + const description = { + targetFd, + position: 0, + readOnly: !canWrite, + canRead, + canWrite, + append: (rawFlags & 1) !== 0, + isDirectory: stat.isDirectory(), + guestPath: action.path, + refCount: 1, + }; + overrides.set(fd, createGuestFileHandle(description)); + cloexecFds.delete(fd); + continue; + } + if (action.command === 6) { + for (const [openFd, handle] of Array.from(overrides.entries())) { + if (openFd < fd) continue; + closeSyntheticHandle(handle); + overrides.delete(openFd); + cloexecFds.delete(openFd); + } + } + } + return overrides; + }; const handleMatchesStdio = (handle, expectedKind) => { if (!handle || handle.open === false) return false; if (handle.kind === "stdio") { if (expectedKind === "read") return handle.targetFd === 0; if (expectedKind === "write") return handle.targetFd === 1 || handle.targetFd === 2; } - if (expectedKind === "read") return handle.kind === "pipe-read" || handle.kind === "guest-file"; - if (expectedKind === "write") return handle.kind === "pipe-write" || handle.kind === "guest-file"; + if (expectedKind === "read") return handle.kind === "pipe-read" || (handle.kind === "guest-file" && !handle.description?.isDirectory); + if (expectedKind === "write") return handle.kind === "pipe-write" || (handle.kind === "guest-file" && !handle.description?.isDirectory && handle.description?.canWrite !== false); return handle.kind === expectedKind; }; - const allocateSyntheticFd = (handle) => { - const fd = nextSyntheticFd++; - syntheticFdEntries.set(fd, handle); - return fd; + const guestFileDescription = (handle) => { + if (!handle || handle.kind !== "guest-file" || handle.open === false) return null; + if (handle.description) return handle.description; + if (typeof handle.targetFd !== "number") return null; + return handle; + }; + const registerSyntheticGuestFile = (fd, handle, targetWasi = activeWasi || parentWasi) => { + if (!handle || handle.kind !== "guest-file" || !handle.description) return; + const wasi = targetWasi; + if (!wasi?.fdTable) return; + const description = handle.description; + const mirror = { + kind: description.isDirectory ? "directory" : "file", + realFd: description.targetFd, + readOnly: description.readOnly === true, + append: description.append === true, + rightsBase: + (description.canRead === false ? 0n : 1n << 1n) | + (description.canWrite === false ? 0n : 1n << 6n), + __agentOSSyntheticMirror: true, + }; + Object.defineProperty(mirror, "offset", { + get: () => description.position, + set: (value) => { description.position = value; }, + }); + wasi.fdTable.set(fd, mirror); + handle.onClose = () => { + if (wasi.fdTable.get(fd) === mirror) wasi.fdTable.delete(fd); + }; + }; + const registerSyntheticHandle = ( + fd, + handle, + entries = activeFdOverrides || syntheticFdEntries, + targetWasi = activeWasi || parentWasi, + ) => { + registerSyntheticGuestFile(fd, handle, targetWasi); + if (!handle) return; + const onClose = handle.baseOnClose || handle.onClose; + handle.baseOnClose = onClose; + handle.onClose = (...args) => { + if (entries.get(fd) === handle) entries.delete(fd); + if (typeof onClose === "function") onClose(...args); + }; + }; + const allocateSyntheticFd = (handle, minimumFd = syntheticFdBase) => { + const entries = activeFdOverrides || syntheticFdEntries; + if (!warnedAboutSyntheticFds && entries.size >= syntheticFdWarnAt) { + warnedAboutSyntheticFds = true; + console.warn( + "[agentos] WASI synthetic fd usage is near the " + maxSyntheticFdCount + + " descriptor limit; raise createWasiCommandHost({ maxSyntheticFds }) if needed", + ); + } + if (entries.size >= maxSyntheticFdCount) return null; + const firstFd = Math.max(syntheticFdBase, Number(minimumFd)); + if (!Number.isSafeInteger(firstFd) || firstFd < 0 || firstFd >= syntheticFdLimit) return null; + for (let fd = firstFd; fd < syntheticFdLimit; fd += 1) { + if (entries.has(fd) || (activeWasi || parentWasi)?.fdTable?.has(fd)) continue; + entries.set(fd, handle); + registerSyntheticHandle(fd, handle, entries); + return fd; + } + return null; }; const replaceSyntheticFd = (fd, handle) => { const descriptor = fd >>> 0; - closeSyntheticHandle(syntheticFdEntries.get(descriptor)); - syntheticFdEntries.set(descriptor, handle); + const entries = activeFdOverrides || syntheticFdEntries; + if (!entries.has(descriptor) && entries.size >= maxSyntheticFdCount) return false; + const existingSynthetic = entries.get(descriptor); + if (existingSynthetic) { + closeSyntheticHandle(existingSynthetic); + } else { + const wasi = activeWasi || parentWasi; + const existingEntry = wasi?.fdTable?.get(descriptor); + if (existingEntry) { + const existingHandle = lookupSyntheticFd(descriptor); + if (existingHandle?.kind === "guest-file") { + closeSyntheticHandle(existingHandle); + } else if (typeof existingEntry.realFd === "number") { + fs().closeSync(existingEntry.realFd); + wasi.fdTable.delete(descriptor); + } + } + } + entries.set(descriptor, handle); + registerSyntheticHandle(descriptor, handle, entries); + return true; }; const pipeHasOpenWriters = (handle) => handle && handle.kind === "pipe-read" && handle.pipe && (handle.pipe.writeHandleCount || 0) > 0; + const execReplacementMarker = Symbol("agentos.browser.exec-replacement"); + const wrappedParentWasis = new WeakSet(); + const procFdAliasSets = new WeakMap(); + const isExecReplacement = (error) => + error && typeof error === "object" && error.marker === execReplacementMarker; + const readExecCloseFds = (cloexecFdsPtr, cloexecFdsLen) => { + const closeCount = Number(cloexecFdsLen) >>> 0; + if (closeCount > maxSyntheticFdCount) throw executableError("EINVAL", "exec CLOEXEC fd list exceeds the configured descriptor limit"); + const closePtr = Number(cloexecFdsPtr) >>> 0; + const closeBytes = closeCount * 4; + if (closePtr > getMemory().buffer.byteLength - closeBytes) throw executableError("EINVAL", "exec CLOEXEC fd list is outside guest memory"); + const closeFds = []; + for (let index = 0; index < closeCount; index += 1) { + closeFds.push(view().getUint32(closePtr + index * 4, true)); + } + return closeFds; + }; + const inheritWasiFdTable = (sourceWasi, targetWasi) => { + if (!(sourceWasi?.fdTable instanceof Map) || !(targetWasi?.fdTable instanceof Map)) return; + targetWasi.fdTable.clear(); + for (const [fd, entry] of sourceWasi.fdTable) targetWasi.fdTable.set(fd, entry); + const inheritedNextFd = Number(sourceWasi.nextFd); + if (Number.isSafeInteger(inheritedNextFd) && inheritedNextFd >= 3) { + targetWasi.nextFd = inheritedNextFd; + } else { + const ordinaryFds = Array.from(sourceWasi.fdTable.keys()) + .filter((fd) => Number.isSafeInteger(fd) && fd >= 3 && fd < syntheticFdBase); + targetWasi.nextFd = ordinaryFds.length > 0 ? Math.max(...ordinaryFds) + 1 : 3; + } + }; + const installProcFdAliases = (wasi) => { + if (!wasi?.wasiImport || procFdAliasSets.has(wasi)) return; + const aliases = new Set(); + procFdAliasSets.set(wasi, aliases); + const delegatePathOpen = typeof wasi.wasiImport.path_open === "function" + ? wasi.wasiImport.path_open.bind(wasi.wasiImport) + : null; + const delegateFdClose = typeof wasi.wasiImport.fd_close === "function" + ? wasi.wasiImport.fd_close.bind(wasi.wasiImport) + : null; + if (delegatePathOpen) { + wasi.wasiImport.path_open = (fd, dirflags, pathPtr, pathLen, oflags, rightsBase, rightsInheriting, fdflags, openedFdPtr) => { + const target = readString(pathPtr, pathLen); + const normalizedTarget = target.startsWith("/") ? target.slice(1) : target; + const procFdPrefix = "proc/self/fd/"; + const sourceText = normalizedTarget.startsWith(procFdPrefix) + ? normalizedTarget.slice(procFdPrefix.length) + : ""; + if (!sourceText || !Array.from(sourceText).every((char) => char >= "0" && char <= "9")) { + return delegatePathOpen(fd, dirflags, pathPtr, pathLen, oflags, rightsBase, rightsInheriting, fdflags, openedFdPtr); + } + if ((Number(oflags) & 0x0f) !== 0 || (BigInt(rightsBase) & (1n << 6n)) !== 0n) return errnoAcces; + const sourceFd = Number(sourceText); + const source = wasi.fdTable?.get(sourceFd); + if (!source || source.kind !== "file" || typeof source.realFd !== "number") return errnoNoent; + let openedFd = Number(wasi.nextFd); + if (!Number.isSafeInteger(openedFd) || openedFd < 3) openedFd = 3; + while (wasi.fdTable.has(openedFd)) openedFd += 1; + const alias = { ...source, offset: 0, __agentOSProcFdAlias: true }; + delete alias.__agentOSOpenFileDescription; + wasi.fdTable.set(openedFd, alias); + wasi.nextFd = openedFd + 1; + aliases.add(openedFd); + return writeU32(openedFdPtr, openedFd); + }; + } + if (delegateFdClose) { + wasi.wasiImport.fd_close = (fd) => { + const descriptor = Number(fd) >>> 0; + if (aliases.delete(descriptor)) { + wasi.fdTable?.delete(descriptor); + return errnoSuccess; + } + return delegateFdClose(descriptor); + }; + } + }; + const installDescriptorOverrides = (wasi, overrides) => { + if (!(overrides instanceof Map) || !wasi?.wasiImport) return; + for (const [fd, handle] of overrides) { + registerSyntheticGuestFile(fd, handle, wasi); + } + const delegateFdClose = typeof wasi.wasiImport.fd_close === "function" + ? wasi.wasiImport.fd_close.bind(wasi.wasiImport) + : null; + wasi.wasiImport.fd_close = (fd) => { + const descriptor = Number(fd) >>> 0; + const handle = overrides.get(descriptor); + if (!handle) return delegateFdClose ? delegateFdClose(descriptor) : errnoBadf; + closeSyntheticHandle(handle); + overrides.delete(descriptor); + wasi.fdTable?.delete(descriptor); + activeCloexecFds.delete(descriptor); + return errnoSuccess; + }; + }; + const instantiateProcessImage = (image, inheritedWasi, descriptorOverrides = null) => { + // Registered commands are inert data until selected by spawn/exec. + // Validate the selected image here so unused native-only registry + // commands do not make browser VM construction fail. + assertBrowserNetworkSupported(image); + const wasi = new WASI({ + returnOnExit: true, + args: image.argv, + env: image.env, + preopens: inheritedWasi ? {} : { "/": image.cwd || "/" }, + }); + if (inheritedWasi) inheritWasiFdTable(inheritedWasi, wasi); + installDescriptorOverrides(wasi, descriptorOverrides); + installProcFdAliases(wasi); + const imports = { + wasi_snapshot_preview1: wasi.wasiImport, + ...host.imports, + }; + return { + wasi, + instance: new WebAssembly.Instance(image.module, imports), + }; + }; + const closeExecFds = (wasi, descriptors) => { + let warned = false; + const warnClose = (fd, detail) => { + if (warned) return; + warned = true; + console.warn("[agentos] exec committed but closing FD_CLOEXEC descriptor " + fd + " failed (" + detail + "); further close failures are suppressed"); + }; + for (const value of new Set(descriptors || [])) { + const fd = Number(value) >>> 0; + activeCloexecFds.delete(fd); + const override = activeFdOverrides?.get(fd); + if (override) { + closeSyntheticHandle(override); + activeFdOverrides.delete(fd); + } + const synthetic = activeFdOverrides ? null : syntheticFdEntries.get(fd); + if (synthetic) { + closeSyntheticHandle(synthetic); + syntheticFdEntries.delete(fd); + } + if (wasi?.fdTable?.has(fd)) { + try { + const result = wasi.wasiImport?.fd_close?.(fd); + if (typeof result === "number" && result !== errnoSuccess) warnClose(fd, "WASI errno " + result); + } catch (error) { + // Linux commits exec even when closing a descriptor reports an error. + warnClose(fd, error instanceof Error ? error.message : String(error)); + } + wasi.fdTable.delete(fd); + } + } + }; const runChild = (child) => { const parentMemory = memory; + const previousActivePid = activePid; + const previousActivePpid = activePpid; const previousActiveFdOverrides = activeFdOverrides; const previousActiveChildCwd = activeChildCwd; + const previousActiveWasi = activeWasi; + const previousBlockedSignals = activeBlockedSignals; + const previousCloexecFds = activeCloexecFds; try { - const childWasi = new WASI({ - returnOnExit: true, - args: [child.commandPath, ...child.argv.slice(1)], - env: child.env, - preopens: { "/": child.cwd || "/" }, - }); - const childImports = { - wasi_snapshot_preview1: childWasi.wasiImport, - ...host.imports, - }; - const childInstance = new WebAssembly.Instance(child.module, childImports); - memory = childInstance.exports.memory; + activePid = child.pid; + activePpid = child.ppid; activeFdOverrides = child.overrides; - activeChildCwd = child.cwd || "/"; - const exitCode = childWasi.start(childInstance); - exitedChildren.set(child.pid, exitCode << 8); - } catch { - exitedChildren.set(child.pid, 127 << 8); + activeBlockedSignals = child.signalMask || new Set(); + activeCloexecFds = child.cloexecFds || new Set(); + runningChildren.add(child.pid); + let image = child; + let inheritedWasi = null; + for (;;) { + const started = instantiateProcessImage(image, inheritedWasi, child.overrides); + memory = started.instance.exports.memory; + activeChildCwd = image.cwd || "/"; + activeWasi = started.wasi; + const mask = encodeSignalMask(activeBlockedSignals); + if (mask.lo !== 0 || mask.hi !== 0) { + const setter = started.instance.exports.__agentos_set_initial_sigmask; + if (typeof setter !== "function") throw new Error("spawned WASM image cannot initialize its inherited signal mask"); + setter(mask.lo, mask.hi); + } + try { + const exitCode = started.wasi.start(started.instance); + exitedChildren.set(child.pid, { exitCode: Number(exitCode) || 0, signal: 0 }); + break; + } catch (error) { + if (!isExecReplacement(error)) throw error; + closeExecFds(started.wasi, error.image.closeFds); + inheritedWasi = started.wasi; + image = error.image; + } + } + } catch (error) { + if (error?.code === "ERR_AGENTOS_BROWSER_WASM_NETWORK_UNSUPPORTED") { + throw error; + } + exitedChildren.set(child.pid, { exitCode: 127, signal: 0 }); } finally { - for (const handle of child.childOverrideHandles) closeSyntheticHandle(handle); + runningChildren.delete(child.pid); + for (const handle of new Set(child.overrides.values())) closeSyntheticHandle(handle); + child.overrides.clear(); activeFdOverrides = previousActiveFdOverrides; activeChildCwd = previousActiveChildCwd; + activeWasi = previousActiveWasi; + activeBlockedSignals = previousBlockedSignals; + activeCloexecFds = previousCloexecFds; + activePid = previousActivePid; + activePpid = previousActivePpid; memory = parentMemory; } }; @@ -9318,6 +10285,34 @@ if (module.exports && module.exports.default == null) module.exports.default = m }, setParentWasi(wasi) { parentWasi = wasi || null; + if (!activeWasi) activeWasi = parentWasi; + if (wasi && typeof wasi.start === "function" && !wrappedParentWasis.has(wasi)) { + wrappedParentWasis.add(wasi); + const initialStart = wasi.start.bind(wasi); + wasi.start = (instance) => { + let currentWasi = wasi; + let currentInstance = instance; + let currentStart = initialStart; + let currentCwd = currentGuestCwd(); + for (;;) { + memory = currentInstance.exports.memory; + activeChildCwd = currentCwd; + activeWasi = currentWasi; + parentWasi = currentWasi; + try { + return currentStart(currentInstance); + } catch (error) { + if (!isExecReplacement(error)) throw error; + closeExecFds(currentWasi, error.image.closeFds); + const started = instantiateProcessImage(error.image, currentWasi); + currentWasi = started.wasi; + currentInstance = started.instance; + currentStart = currentWasi.start.bind(currentWasi); + currentCwd = error.image.cwd || "/"; + } + } + }; + } return host; }, installBlockingStdin(processLike) { @@ -9392,7 +10387,7 @@ if (module.exports && module.exports.default == null) module.exports.default = m }, // Toggle terminal raw mode on the guest's PTY. crossterm calls this instead // of tcsetattr; route it to the kernel via process.stdin.setRawMode (which - // drives __pty_set_raw_mode), so reedline gets raw \r keystrokes and submits + // drives __pty_set_raw_mode), so reedline gets raw \\r keystrokes and submits // commands. Returns errno 0. set_raw_mode(_enabled) { return 0; @@ -9419,9 +10414,9 @@ if (module.exports && module.exports.default == null) module.exports.default = m if (descriptor <= 2) return 0o020666; const handle = lookupSyntheticFd(descriptor); if (handle && (handle.kind === "pipe-read" || handle.kind === "pipe-write")) return 0o010600; - if (handle && handle.kind === "guest-file" && typeof handle.targetFd === "number") { + if (handle && handle.kind === "guest-file" && typeof guestFileDescription(handle)?.targetFd === "number") { try { - return modeFromStat(fs().fstatSync(handle.targetFd), 0o100644); + return modeFromStat(fs().fstatSync(guestFileDescription(handle).targetFd), 0o100644); } catch { return 0o100644; } @@ -9451,15 +10446,14 @@ if (module.exports && module.exports.default == null) module.exports.default = m return 0; } }, - // Matches node runner host_fs.chmod(fd, pathPtr, pathLen, mode): - // 0 on success, 1 on failure. + // Match node's host_fs chmod errno contract exactly. chmod(_fd, pathPtr, pathLen, mode) { try { const guestPath = resolveGuestPath(readString(pathPtr, pathLen)); fs().chmodSync(guestPath, Number(mode) >>> 0); - return 0; - } catch { - return 1; + return errnoSuccess; + } catch (error) { + return hostFsErrno(error); } }, // The node runner exports 7 host_fs symbols; mirror the full @@ -9474,9 +10468,9 @@ if (module.exports && module.exports.default == null) module.exports.default = m if ( handle && handle.kind === "guest-file" && - typeof handle.targetFd === "number" + typeof guestFileDescription(handle)?.targetFd === "number" ) { - return BigInt(fs().fstatSync(handle.targetFd).size ?? -1); + return BigInt(fs().fstatSync(guestFileDescription(handle).targetFd).size ?? -1); } const parentEntry = parentWasi && parentWasi.fdTable && parentWasi.fdTable.get(descriptor); @@ -9503,116 +10497,506 @@ if (module.exports && module.exports.default == null) module.exports.default = m return (1n << 64n) - 1n; } }, - // Browser fs() has no fd-based fchmod/ftruncate; provide the - // symbols (best-effort failure) so imports resolve. Rust guest - // binaries bypass wasi-libc stat/chmod/truncate, so these paths - // are unreached in practice. - fchmod(_fd, _mode) { - return 1; + fchmod(fd, mode) { + const descriptor = fd >>> 0; + const handle = lookupSyntheticFd(descriptor) || (descriptor <= 2 + ? { kind: "stdio", targetFd: descriptor, open: true } + : null); + if (!handle) return errnoBadf; + const description = guestFileDescription(handle); + if (!description) return errnoInval; + try { + fs().fchmodSync(description.targetFd, Number(mode) & 0o7777); + return errnoSuccess; + } catch (error) { + return hostFsErrno(error); + } }, - ftruncate(_fd, _length) { - return 1; + ftruncate(fd, length) { + const descriptor = fd >>> 0; + const handle = lookupSyntheticFd(descriptor) || (descriptor <= 2 + ? { kind: "stdio", targetFd: descriptor, open: true } + : null); + if (!handle) return errnoBadf; + const description = guestFileDescription(handle); + if (!description) return errnoInval; + const size = Number(length); + if (!Number.isSafeInteger(size) || size < 0 || description.isDirectory) return errnoInval; + if (description.readOnly === true || description.canWrite === false) return errnoInval; + try { + fs().ftruncateSync(description.targetFd, size); + return errnoSuccess; + } catch (error) { + return hostFsErrno(error); + } }, }, - host_process: { + host_process: { proc_spawn(argvPtr, argvLen, envpPtr, envpLen, stdinFd, stdoutFd, stderrFd, cwdPtr, cwdLen, retPid) { try { - const argv = decodeNullSeparated(readBytes(argvPtr, argvLen)); - if (argv.length === 0) return errnoNosys; - const commandPath = argv[0]; - const commandName = commandPath.split("/").filter(Boolean).at(-1) || commandPath; - const module = commandModules.get(commandName); - if (!module) return errnoNosys; - const env = { - ...(options && options.env ? options.env : {}), - ...parseEnv(readBytes(envpPtr, envpLen)), - PATH: (options && options.path) || "/bin:/usr/bin", + const argvBytes = readBytes(argvPtr, argvLen); + const commandLength = argvBytes.indexOf(0); + if (commandLength <= 0) return errnoNoent; + return this.proc_spawn_v2( + argvPtr, + commandLength, + argvPtr, + argvLen, + envpPtr, + envpLen, + stdinFd, + stdoutFd, + stderrFd, + cwdPtr, + cwdLen, + retPid, + ); + } catch (error) { + return hostFsErrno(error); + } + }, + proc_spawn_v3(execPathPtr, execPathLen, argvPtr, argvLen, envpPtr, envpLen, actionsPtr, actionsLen, cwdPtr, cwdLen, attrFlags, sigDefaultLo, sigDefaultHi, sigMaskLo, sigMaskHi, pgroup, retPid) { + const flags = attrFlags >>> 0; + const supportedFlags = 1 | 2 | 4 | 8 | 64; + if ((flags & ~supportedFlags) !== 0) return errnoNotsup; + if ((pgroup | 0) < 0) return errnoInval; + if (activeSpawnCallContext) return errnoIo; + try { + const initialCwd = cwdLen ? readString(cwdPtr, cwdLen) : ((options && options.cwd) || "/"); + const actions = decodeSpawnActions(actionsPtr, actionsLen, initialCwd); + const signalMask = decodeSignalMask(sigMaskLo, sigMaskHi).filter((signal) => signal !== 9 && signal !== 19); + activeSpawnCallContext = { + attrFlags: flags, + pgroup: pgroup | 0, + signalMask, + signalDefaults: (flags & 4) !== 0 ? decodeSignalMask(sigDefaultLo, sigDefaultHi) : [], + fileActions: actions.actions, }; - const cwd = cwdLen ? readString(cwdPtr, cwdLen) : ((options && options.cwd) || "/"); - const childOverrideHandles = []; - const overrides = new Map(); + return this.proc_spawn_v2( + execPathPtr, + execPathLen, + argvPtr, + argvLen, + envpPtr, + envpLen, + actions.stdio[0], + actions.stdio[1], + actions.stdio[2], + cwdPtr, + 0, + retPid, + actions.cwd, + ); + } catch (error) { + return hostFsErrno(error); + } finally { + activeSpawnCallContext = null; + } + }, + proc_spawn_v2(execPathPtr, execPathLen, argvPtr, argvLen, envpPtr, envpLen, stdinFd, stdoutFd, stderrFd, cwdPtr, cwdLen, retPid, resolvedCwdOverride) { + const childOverrideHandles = []; + let overrides = null; + try { + const commandPath = readString(execPathPtr, execPathLen); + if (!commandPath) return errnoNoent; + const argv = decodeNullSeparated(readBytes(argvPtr, argvLen)); + const env = parseEnv(readBytes(envpPtr, envpLen)); + const cwd = path().posix.resolve(typeof resolvedCwdOverride === "string" ? resolvedCwdOverride : (cwdLen ? readString(cwdPtr, cwdLen) : ((options && options.cwd) || "/"))); + const module = resolveCommandModule(commandPath, env, cwd); + if (!module) return errnoNoent; + overrides = cloneOpenDescriptorsForSpawn(); + const childCloexecFds = new Set( + [...activeCloexecFds].filter((fd) => overrides.has(fd)), + ); + applyBrowserSpawnFileActions( + overrides, + activeSpawnCallContext?.fileActions || [], + childCloexecFds, + ); + for (const fd of childCloexecFds) { + const handle = overrides.get(fd); + if (handle) closeSyntheticHandle(handle); + overrides.delete(fd); + } + const actionStdioFds = new Set( + (activeSpawnCallContext?.fileActions || []) + .filter((action) => action.command >= 1 && action.command <= 3 && action.fd >= 0 && action.fd <= 2) + .map((action) => action.fd), + ); for (const [childFd, parentFd, expectedKind] of [ [0, stdinFd >>> 0, "read"], [1, stdoutFd >>> 0, "write"], [2, stderrFd >>> 0, "write"], ]) { - const parentHandle = lookupSyntheticFd(parentFd); + if (actionStdioFds.has(childFd)) continue; + if (activeSpawnCallContext && parentFd === 0xffffffff) continue; + const parentHandle = overrides.get(parentFd) || lookupSyntheticFd(parentFd); if (parentFd <= 2 && !parentHandle) continue; - if (!handleMatchesStdio(parentHandle, expectedKind)) return errnoBadf; + if (!handleMatchesStdio(parentHandle, expectedKind)) { + const error = new Error("spawn stdio references an incompatible fd"); + error.code = "EBADF"; + throw error; + } const childHandle = cloneSyntheticHandle(parentHandle); - if (!childHandle) return errnoBadf; + if (!childHandle) { + const error = new Error("spawn stdio fd cannot be duplicated"); + error.code = "EBADF"; + throw error; + } + const replaced = overrides.get(childFd); + if (replaced) closeSyntheticHandle(replaced); overrides.set(childFd, childHandle); - childOverrideHandles.push(childHandle); } + childOverrideHandles.push(...new Set(overrides.values())); const pid = nextPid++; - const child = { pid, module, commandPath, argv, env, cwd, overrides, childOverrideHandles }; + const parentPid = activePid; + const inheritedPgid = processGroups.get(parentPid) || parentPid; + const requestedPgid = activeSpawnCallContext?.attrFlags & 2 + ? (activeSpawnCallContext.pgroup === 0 ? pid : activeSpawnCallContext.pgroup) + : inheritedPgid; + if (requestedPgid !== pid && !Array.from(processGroups.values()).includes(requestedPgid)) { + const error = new Error("spawn requested an unknown process group"); + error.code = "EPERM"; + throw error; + } + processGroups.set(pid, requestedPgid); + const child = { + pid, + ppid: parentPid, + module, + commandPath, + argv, + env, + cwd, + overrides, + childOverrideHandles, + signalMask: new Set(activeSpawnCallContext?.signalMask || []), + cloexecFds: new Set(), + }; + knownChildren.add(pid); if (pipeHasOpenWriters(overrides.get(0))) { deferredChildren.set(pid, child); } else { runChild(child); } return writeU32(retPid, pid); - } catch { - return errnoNosys; - } - }, - proc_waitpid(pid, _options, retStatus, retPid) { - const requested = pid >>> 0; - runReadyDeferredChildren(requested === 0xffffffff ? undefined : requested); - const childPid = requested === 0xffffffff - ? exitedChildren.keys().next().value - : requested; - if (!childPid || !exitedChildren.has(childPid)) { - writeU32(retPid, 0); - return errnoChild; + } catch (error) { + for (const handle of new Set(overrides?.values() || childOverrideHandles)) closeSyntheticHandle(handle); + overrides?.clear(); + if (error?.code === "ERR_AGENTOS_BROWSER_WASM_NETWORK_UNSUPPORTED") { + throw error; + } + return hostFsErrno(error); } - writeU32(retStatus, exitedChildren.get(childPid) || 0); - writeU32(retPid, childPid); - exitedChildren.delete(childPid); - return errnoSuccess; - }, - fd_dup(fd, retNewFd) { - const descriptor = fd >>> 0; - const handle = lookupSyntheticFd(descriptor) || (descriptor <= 2 - ? { kind: "stdio", targetFd: descriptor, open: true } - : null); - if (!handle) return writeU32(retNewFd, fd); - const cloned = cloneSyntheticHandle(handle); - if (!cloned) return errnoBadf; - return writeU32(retNewFd, allocateSyntheticFd(cloned)); - }, - fd_dup2(oldFd, newFd) { - if (oldFd === newFd) return errnoSuccess; - const handle = lookupSyntheticFd(oldFd >>> 0); - if (!handle) return oldFd <= 2 && newFd <= 2 ? errnoSuccess : errnoBadf; - const cloned = cloneSyntheticHandle(handle); - if (!cloned) return errnoBadf; - replaceSyntheticFd(newFd >>> 0, cloned); - return errnoSuccess; - }, - fd_pipe(retReadFd, retWriteFd) { - const pipe = { - chunks: [], - consumers: new Map(), - producers: new Map(), - readHandleCount: 1, - writeHandleCount: 1, - }; - const readFd = allocateSyntheticFd({ kind: "pipe-read", pipe, open: true, onClose: onPipeHandleClose }); - const writeFd = allocateSyntheticFd({ kind: "pipe-write", pipe, open: true, onClose: onPipeHandleClose }); - writeU32(retReadFd, readFd); - writeU32(retWriteFd, writeFd); - return errnoSuccess; }, - proc_getpid(retPid) { return writeU32(retPid, 1); }, - proc_getppid(retPid) { return writeU32(retPid, 0); }, - proc_kill() { return errnoNosys; }, - sleep_ms(milliseconds) { - Atomics.wait(wait, 0, 0, milliseconds >>> 0); - return errnoSuccess; - }, - pty_open() { return errnoNosys; }, - proc_sigaction() { return errnoSuccess; }, + proc_exec(execPathPtr, execPathLen, argvPtr, argvLen, envpPtr, envpLen, cloexecFdsPtr, cloexecFdsLen) { + try { + const command = readString(execPathPtr, execPathLen); + if (!command) return errnoNoent; + const argv = decodeNullSeparated(readBytes(argvPtr, argvLen)); + const env = parseEnv(readBytes(envpPtr, envpLen)); + const cwd = currentGuestCwd(); + const image = loadBrowserExecImage(command, argv); + const closeFds = readExecCloseFds(cloexecFdsPtr, cloexecFdsLen); + throw { + marker: execReplacementMarker, + image: { module: image.module, commandPath: command, argv: image.argv, env, cwd, closeFds }, + }; + } catch (error) { + if (isExecReplacement(error)) throw error; + return hostFsErrno(error); + } + }, + proc_fexec(execFd, argvPtr, argvLen, envpPtr, envpLen, cloexecFdsPtr, cloexecFdsLen) { + try { + const descriptor = execFd >>> 0; + const handle = lookupSyntheticFd(descriptor); + const description = guestFileDescription(handle); + if (!description || description.isDirectory || typeof description.targetFd !== "number") return errnoBadf; + const argv = decodeNullSeparated(readBytes(argvPtr, argvLen)); + const env = parseEnv(readBytes(envpPtr, envpLen)); + const closeFds = readExecCloseFds(cloexecFdsPtr, cloexecFdsLen); + const scriptRef = "/proc/self/fd/" + descriptor; + const value = readExecutableFd(description.targetFd, scriptRef); + if (parseLinuxShebang(value) && closeFds.includes(descriptor)) return errnoNoent; + const image = compileBrowserExecImage(value, scriptRef, argv); + throw { + marker: execReplacementMarker, + image: { + module: image.module, + commandPath: scriptRef, + argv: image.argv, + env, + cwd: currentGuestCwd(), + closeFds, + }, + }; + } catch (error) { + if (isExecReplacement(error)) throw error; + return hostFsErrno(error); + } + }, + proc_waitpid(pid, optionsValue, retStatus, retPid) { + const requested = pid >>> 0; + const waitOptions = optionsValue >>> 0; + const waitNoHang = 1; + if ((waitOptions & ~waitNoHang) !== 0) return errnoInval; + const anyChild = requested === 0xffffffff; + if (!anyChild && !knownChildren.has(requested)) return errnoChild; + if (anyChild && knownChildren.size === 0) return errnoChild; + const findExited = () => anyChild ? exitedChildren.keys().next().value : (exitedChildren.has(requested) ? requested : undefined); + let childPid; + for (;;) { + runReadyDeferredChildren(anyChild ? undefined : requested); + childPid = findExited(); + if (childPid !== undefined) break; + if ((waitOptions & waitNoHang) !== 0) { + writeU32(retStatus, 0); + writeU32(retPid, 0); + return errnoSuccess; + } + Atomics.wait(wait, 0, 0, 1); + } + const status = exitedChildren.get(childPid); + const signal = status?.signal ?? 0; + writeU32(retStatus, signal === 0 ? (status?.exitCode ?? 0) : 128 + signal); + writeU32(retPid, childPid); + exitedChildren.delete(childPid); + knownChildren.delete(childPid); + processGroups.delete(childPid); + return errnoSuccess; + }, + proc_waitpid_v2(pid, optionsValue, retExitCode, retSignal, retPid, retCoreDumped) { + const requested = pid >>> 0; + const waitOptions = optionsValue >>> 0; + const waitNoHang = 1; + if ((waitOptions & ~waitNoHang) !== 0) return errnoInval; + const anyChild = requested === 0xffffffff; + if (!anyChild && !knownChildren.has(requested)) return errnoChild; + if (anyChild && knownChildren.size === 0) return errnoChild; + const findExited = () => anyChild ? exitedChildren.keys().next().value : (exitedChildren.has(requested) ? requested : undefined); + let childPid; + for (;;) { + runReadyDeferredChildren(anyChild ? undefined : requested); + childPid = findExited(); + if (childPid !== undefined) break; + if ((waitOptions & waitNoHang) !== 0) { + writeU32(retExitCode, 0); + writeU32(retSignal, 0); + writeU32(retPid, 0); + if (retCoreDumped !== undefined) writeU32(retCoreDumped, 0); + return errnoSuccess; + } + Atomics.wait(wait, 0, 0, 1); + } + const status = exitedChildren.get(childPid); + writeU32(retExitCode, status?.exitCode ?? 0); + writeU32(retSignal, status?.signal ?? 0); + writeU32(retPid, childPid); + if (retCoreDumped !== undefined) writeU32(retCoreDumped, 0); + exitedChildren.delete(childPid); + knownChildren.delete(childPid); + processGroups.delete(childPid); + return errnoSuccess; + }, + fd_dup(fd, retNewFd) { + const descriptor = fd >>> 0; + const handle = lookupSyntheticFd(descriptor) || (descriptor <= 2 + ? { kind: "stdio", targetFd: descriptor, open: true } + : null); + if (!handle) return errnoBadf; + const cloned = cloneSyntheticHandle(handle); + if (!cloned) return errnoBadf; + const allocated = allocateSyntheticFd(cloned); + if (allocated == null) { + closeSyntheticHandle(cloned); + return errnoMfile; + } + activeCloexecFds.delete(allocated); + return writeU32(retNewFd, allocated); + }, + fd_getfd(fd, retFlags) { + const descriptor = Number(fd); + if (!Number.isInteger(descriptor) || descriptor < 0) return errnoBadf; + const handle = lookupSyntheticFd(descriptor) || (descriptor <= 2 + ? { kind: "stdio", targetFd: descriptor, open: true } + : null); + if (!handle) return errnoBadf; + return writeU32(retFlags, activeCloexecFds.has(descriptor) ? 1 : 0); + }, + fd_setfd(fd, flags) { + const descriptor = Number(fd); + const value = Number(flags) >>> 0; + if (!Number.isInteger(descriptor) || descriptor < 0) return errnoBadf; + if ((value & ~1) !== 0) return errnoInval; + const handle = lookupSyntheticFd(descriptor) || (descriptor <= 2 + ? { kind: "stdio", targetFd: descriptor, open: true } + : null); + if (!handle) return errnoBadf; + if ((value & 1) !== 0) activeCloexecFds.add(descriptor); + else activeCloexecFds.delete(descriptor); + return errnoSuccess; + }, + fd_dup_min(fd, minFd, retNewFd) { + const source = Number(fd); + const minimum = Number(minFd); + if (!Number.isInteger(source) || source < 0) return errnoBadf; + if (!Number.isInteger(minimum) || minimum < 0) return errnoInval; + const handle = lookupSyntheticFd(source) || (source <= 2 + ? { kind: "stdio", targetFd: source, open: true } + : null); + if (!handle) return errnoBadf; + const cloned = cloneSyntheticHandle(handle); + if (!cloned) return errnoBadf; + const allocated = allocateSyntheticFd(cloned, minimum); + if (allocated == null) { + closeSyntheticHandle(cloned); + return errnoMfile; + } + activeCloexecFds.delete(allocated); + return writeU32(retNewFd, allocated); + }, + fd_dup2(oldFd, newFd) { + const source = oldFd >>> 0; + const destination = newFd >>> 0; + const handle = lookupSyntheticFd(source) || (source <= 2 ? { kind: "stdio", targetFd: source, open: true } : null); + if (!handle) return errnoBadf; + if (source === destination) return errnoSuccess; + const cloned = cloneSyntheticHandle(handle); + if (!cloned) return errnoBadf; + try { + if (!replaceSyntheticFd(destination, cloned)) { + closeSyntheticHandle(cloned); + return errnoMfile; + } + } catch (error) { + closeSyntheticHandle(cloned); + return hostFsErrno(error); + } + activeCloexecFds.delete(destination); + return errnoSuccess; + }, + proc_closefrom(lowFd) { + const minimumFd = lowFd >>> 0; + const entries = activeFdOverrides || syntheticFdEntries; + for (const [fd, handle] of Array.from(entries.entries())) { + if (fd < minimumFd) continue; + closeSyntheticHandle(handle); + entries.delete(fd); + activeCloexecFds.delete(fd); + } + const wasi = activeWasi || parentWasi; + if (wasi && wasi.fdTable) { + for (const [fd, entry] of Array.from(wasi.fdTable.entries())) { + if (fd < minimumFd) continue; + const guestHandle = activeFdOverrides + ? activeFdOverrides.get(fd) + : lookupSyntheticFd(fd); + if (guestHandle?.kind === "guest-file") { + try { + closeSyntheticHandle(guestHandle); + } catch (error) { + return hostFsErrno(error); + } + } else if (typeof entry?.realFd === "number") { + try { + fs().closeSync(entry.realFd); + } catch (error) { + const errno = hostFsErrno(error); + if (errno !== errnoBadf) return errno; + } + } else if (typeof wasi.wasiImport?.fd_close === "function") { + const errno = wasi.wasiImport.fd_close(fd); + if (errno !== undefined && errno !== errnoSuccess && errno !== errnoBadf) return errno; + } + wasi.fdTable.delete(fd); + activeCloexecFds.delete(fd); + } + } + return errnoSuccess; + }, + fd_pipe(retReadFd, retWriteFd) { + const pipe = { + chunks: [], + consumers: new Map(), + producers: new Map(), + readHandleCount: 1, + writeHandleCount: 1, + }; + const readHandle = { kind: "pipe-read", pipe, open: true, baseOnClose: onPipeHandleClose, onClose: onPipeHandleClose }; + const readFd = allocateSyntheticFd(readHandle); + if (readFd == null) return errnoMfile; + const writeHandle = { kind: "pipe-write", pipe, open: true, baseOnClose: onPipeHandleClose, onClose: onPipeHandleClose }; + const writeFd = allocateSyntheticFd(writeHandle); + if (writeFd == null) { + closeSyntheticHandle(readHandle); + return errnoMfile; + } + activeCloexecFds.delete(readFd); + activeCloexecFds.delete(writeFd); + writeU32(retReadFd, readFd); + writeU32(retWriteFd, writeFd); + return errnoSuccess; + }, + proc_getpid(retPid) { return writeU32(retPid, activePid); }, + proc_getppid(retPid) { return writeU32(retPid, activePpid); }, + proc_getpgid(pid, retPgid) { + const targetPid = (pid >>> 0) || activePid; + const pgid = processGroups.get(targetPid); + return pgid === undefined ? errnoSrch : writeU32(retPgid, pgid); + }, + proc_setpgid(pid, pgid) { + const targetPid = (pid >>> 0) || activePid; + const targetPgid = (pgid >>> 0) || targetPid; + if (!processGroups.has(targetPid)) return errnoSrch; + if (targetPgid !== targetPid && !Array.from(processGroups.values()).includes(targetPgid)) return errnoPerm; + processGroups.set(targetPid, targetPgid); + return errnoSuccess; + }, + proc_kill(pid, signal) { + const targetPid = pid >>> 0; + const signalNumber = signal >>> 0; + if (signalNumber > 31) return errnoInval; + if (!knownChildren.has(targetPid) || exitedChildren.has(targetPid)) return errnoSrch; + if (signalNumber === 0) return errnoSuccess; + if (![1, 2, 9, 15].includes(signalNumber)) return errnoNosys; + if (runningChildren.has(targetPid)) return errnoNosys; + const child = deferredChildren.get(targetPid); + if (!child) return errnoSrch; + deferredChildren.delete(targetPid); + for (const handle of child.childOverrideHandles) closeSyntheticHandle(handle); + exitedChildren.set(targetPid, { exitCode: 0, signal: signalNumber }); + Atomics.notify(wait, 0); + return errnoSuccess; + }, + sleep_ms(milliseconds) { + const deadline = Date.now() + (milliseconds >>> 0); + while (Date.now() < deadline) { + // Keep guest sleeps interruptible by V8 termination during kill/dispose. + Atomics.wait(wait, 0, 0, Math.max(1, Math.min(10, deadline - Date.now()))); + } + return errnoSuccess; + }, + pty_open() { return errnoNosys; }, + proc_sigaction() { return errnoNosys; }, + proc_signal_mask_v2(how, setLo, setHi, retOldLo, retOldHi) { + const previous = encodeSignalMask(activeBlockedSignals); + writeU32(retOldLo, previous.lo); + writeU32(retOldHi, previous.hi); + const operation = how >>> 0; + if (operation === 3) return errnoSuccess; + if (operation > 2) return errnoInval; + const requested = decodeSignalMask(setLo, setHi).filter((signal) => signal !== 9 && signal !== 19); + if (operation === 0) { + for (const signal of requested) activeBlockedSignals.add(signal); + } else if (operation === 1) { + for (const signal of requested) activeBlockedSignals.delete(signal); + } else { + activeBlockedSignals.clear(); + for (const signal of requested) activeBlockedSignals.add(signal); + } + return errnoSuccess; + }, }, }, }; @@ -9668,13 +11052,13 @@ if (module.exports && module.exports.default == null) module.exports.default = m userInfo: () => ({ username, uid, gid, shell, homedir }), version: () => stringValue(virtualOs.version, "#1 SMP PREEMPT_DYNAMIC secure-exec"), }; - `,"node:os":"module.exports = require('os');"};function U(e,t){globalThis[e]=t}function Me(e,t){globalThis[e]=t}function nf(e){return e==="overrideProcessCwd"?` + `,"node:os":"module.exports = require('os');"};function tf(e,t){return Number.isSafeInteger(e)&&Number(e)>0?Number(e):t}function nf(e,t){let r=e.replace(/^node:/,""),s=Dm[r];if(!s||r!=="secure-exec:wasi-command-host")return s??null;let o=tf(t?.maxSpawnFileActions,4096),a=tf(t?.maxSpawnFileActionBytes,1024*1024);return s.replace("/* @agentos-process-max-spawn-file-actions */ 4096",`/* @agentos-process-max-spawn-file-actions */ ${o}`).replace("/* @agentos-process-max-spawn-file-action-bytes */ 1048576",`/* @agentos-process-max-spawn-file-action-bytes */ ${a}`)}function W(e,t){globalThis[e]=t}function je(e,t){globalThis[e]=t}function rf(e){return e==="overrideProcessCwd"?` if (globalThis.process && globalThis.__runtimeProcessCwdOverride) { globalThis.process.cwd = () => String(globalThis.__runtimeProcessCwdOverride); } - `:""}function rf(){return` + `:""}function sf(){return` (function () { - ${Vu()} + ${Ku()} const callSyncBridge = (ref, ...args) => { if (typeof ref === "function") { @@ -9823,5 +11207,5 @@ if (module.exports && module.exports.default == null) module.exports.default = m }; globalThis.console = consoleObject; })(); - `}var Dm=new Set(["localhost","ip4-localhost","ip4-loopback"]),Fm=new Set(["ip6-localhost","ip6-loopback"]);function jm(e){let t=e.split(".");return t.length===4&&t.every(r=>{if(!/^\d+$/.test(r))return!1;let s=Number(r);return s>=0&&s<=255&&String(s)===r})}function Mm(e){return e.includes(":")&&/^[0-9a-fA-F:.]+$/.test(e)}function Um(e){let t=e.trim().toLowerCase();return Dm.has(t)?{address:"127.0.0.1",family:4}:Fm.has(t)?{address:"::1",family:6}:jm(t)?{address:t,family:4}:Mm(t)?{address:t,family:6}:{error:"DNS not supported in browser",code:"ENOSYS"}}var Wm=typeof globalThis<"u"&&typeof globalThis.fetch=="function"?globalThis.fetch.bind(globalThis):void 0;function sf(){let e=Wm??fetch;return{async fetch(t,r){let s=await e(t,{method:r?.method||"GET",headers:r?.headers,body:r?.body}),o={};s.headers.forEach((u,p)=>{o[p]=u});let a=s.headers.get("content-type")||"",c=a.includes("octet-stream")||a.includes("gzip")||t.endsWith(".tgz"),l;if(c){let u=await s.arrayBuffer();l=Ku(new Uint8Array(u)),o["x-body-encoding"]="base64"}else l=await s.text();return{ok:s.ok,status:s.status,statusText:s.statusText,headers:o,body:l,url:s.url,redirected:s.redirected}},async dnsLookup(t){return Um(t)},async httpRequest(t,r){let s=await e(t,{method:r?.method||"GET",headers:r?.headers,body:r?.body}),o={};s.headers.forEach((c,l)=>{o[l]=c});let a=await s.text();return{status:s.status,statusText:s.statusText,headers:o,body:a,url:s.url}}}}var of={EPERM:1,ENOENT:2,ESRCH:3,EINTR:4,EIO:5,ENXIO:6,E2BIG:7,ENOEXEC:8,EBADF:9,ECHILD:10,EAGAIN:11,EWOULDBLOCK:11,ENOMEM:12,EACCES:13,EFAULT:14,ENOTBLK:15,EBUSY:16,EEXIST:17,EXDEV:18,ENODEV:19,ENOTDIR:20,EISDIR:21,EINVAL:22,ENFILE:23,EMFILE:24,ENOTTY:25,ETXTBSY:26,EFBIG:27,ENOSPC:28,ESPIPE:29,EROFS:30,EMLINK:31,EPIPE:32,EDOM:33,ERANGE:34,ENAMETOOLONG:36,ENOSYS:38,ENOTEMPTY:39,ELOOP:40,EOVERFLOW:75,ENOTSOCK:88,EDESTADDRREQ:89,EMSGSIZE:90,EPROTOTYPE:91,ENOPROTOOPT:92,EPROTONOSUPPORT:93,ENOTSUP:95,EOPNOTSUPP:95,EAFNOSUPPORT:97,EADDRINUSE:98,EADDRNOTAVAIL:99,ENETDOWN:100,ENETUNREACH:101,ECONNABORTED:103,ECONNRESET:104,ENOBUFS:105,EISCONN:106,ENOTCONN:107,ETIMEDOUT:110,ECONNREFUSED:111,EHOSTUNREACH:113,EALREADY:114,EINPROGRESS:115};function Aa(e){if(e&&Object.prototype.hasOwnProperty.call(of,e))return-of[e]}var $x=4*Int32Array.BYTES_PER_ELEMENT;var Hx=16*1024*1024,Gx=64*1024;var qm=["fs.readFile","fs.writeFile","fs.readFileBinary","fs.writeFileBinary","fs.pread","fs.pwrite","fs.readDir","fs.createDir","fs.mkdir","fs.rmdir","fs.exists","fs.stat","fs.lstat","fs.unlink","fs.rename","fs.realpath","fs.readlink","fs.symlink","fs.link","fs.chmod","fs.truncate","module.resolve","module.loadFile","module.format","module.batchResolve","child_process.spawn","child_process.poll","child_process.write_stdin","child_process.close_stdin","child_process.kill","child_process.spawn_sync","process.signal_state","network.fetch","dgram.create","dgram.bind","dgram.recv","dgram.send","dgram.close","dgram.address","dgram.setBufferSize","dgram.getBufferSize","pty.open","pty.read","pty.write","pty.close","pty.resize","pty.setForegroundPgid","pty.tcgetattr","pty.tcsetattr"],zx=new Set(qm);function af(){if(typeof SharedArrayBuffer>"u")throw new Error("Browser runtime requires SharedArrayBuffer for sync filesystem and module loading parity");if(typeof Atomics>"u"||typeof Atomics.wait!="function")throw new Error("Browser runtime requires Atomics.wait for sync filesystem and module loading parity")}var Oa=null,Fa=!1,en=null,Bs="freeze",Xe=null,lt=null,ot=null,Rs=!1,ja=null,Ma=new Map,zm=new Map,Tn=null,Ua=!1,$a=null,Ha=null,Km=12e4,df=8192,hf=8192;function Vm(e){return Object.entries(En).find(([,t])=>t===e)?.[0]??`SIG${e}`}var xf=16*1024*1024,vf=4*1024*1024,mf="ERR_SANDBOX_PAYLOAD_TOO_LARGE",Jm=16384,Ym=8,Xm=1,Ba=xf,Os=vf,Ga=new TextEncoder,$t=new TextDecoder,za=eval,Ka=["byteLength","slice","grow","maxByteLength","growable"],Ie={captured:!1,sharedArrayBufferPrototypeDescriptors:new Map};function Qm(e){return Ga.encode(e).byteLength}function Ns(){if(!en)throw new Error("Browser runtime worker control channel is not initialized");return en}function Va(){if(Ie.captured)return;Ie.captured=!0,Ie.dateDescriptor=Object.getOwnPropertyDescriptor(globalThis,"Date"),Ie.dateValue=globalThis.Date,Ie.performanceDescriptor=Object.getOwnPropertyDescriptor(globalThis,"performance"),Ie.performanceValue=globalThis.performance,Ie.sharedArrayBufferDescriptor=Object.getOwnPropertyDescriptor(globalThis,"SharedArrayBuffer"),Ie.sharedArrayBufferValue=globalThis.SharedArrayBuffer;let e=globalThis.SharedArrayBuffer;if(typeof e!="function")return;let t=e.prototype;for(let r of Ka)Ie.sharedArrayBufferPrototypeDescriptors.set(r,Object.getOwnPropertyDescriptor(t,r))}function Ra(e,t){if(t)try{Object.defineProperty(globalThis,e,t);return}catch{if("value"in t){globalThis[e]=t.value;return}}Reflect.deleteProperty(globalThis,e)}function Zm(){let e=Ie.sharedArrayBufferValue;if(typeof e!="function")return;let t=e.prototype;for(let r of Ka){let s=Ie.sharedArrayBufferPrototypeDescriptors.get(r);try{s?Object.defineProperty(t,r,s):delete t[r]}catch{}}}function ey(){Va(),Ra("Date",Ie.dateDescriptor),Ra("performance",Ie.performanceDescriptor),Zm(),Ra("SharedArrayBuffer",Ie.sharedArrayBufferDescriptor),(typeof globalThis.performance>"u"||globalThis.performance===null)&&Object.defineProperty(globalThis,"performance",{value:{now:()=>Date.now()},configurable:!0,writable:!0})}function ty(e,t){if(Va(),ey(),e!=="freeze")return;let r=typeof t=="number"&&Number.isFinite(t)?Math.trunc(t):Date.now(),s=Ie.dateValue??Ie.dateDescriptor?.value??Date,o=()=>r,a=function(...p){return new.target?p.length===0?new s(r):new s(...p):s()};Object.defineProperty(a,"prototype",{value:s.prototype,writable:!1,configurable:!1}),Object.defineProperty(a,"now",{value:o,configurable:!0,writable:!1}),a.parse=s.parse,a.UTC=s.UTC;try{Object.defineProperty(globalThis,"Date",{value:a,configurable:!0,writable:!1})}catch{globalThis.Date=a}let c=Object.create(null),l=Ie.performanceValue;if(typeof l<"u"&&l!==null){let p=l;for(let m of Object.getOwnPropertyNames(Object.getPrototypeOf(l)??l))if(m!=="now")try{let h=p[m];c[m]=typeof h=="function"?h.bind(l):h}catch{}}Object.defineProperty(c,"now",{value:()=>0,configurable:!0,writable:!1}),Object.freeze(c);try{Object.defineProperty(globalThis,"performance",{value:c,configurable:!0,writable:!1})}catch{globalThis.performance=c}let u=Ie.sharedArrayBufferValue;if(typeof u=="function"){let p=u.prototype;for(let m of Ka)try{Object.defineProperty(p,m,{get(){throw new TypeError("SharedArrayBuffer is not available in sandbox")},configurable:!0})}catch{}}try{Object.defineProperty(globalThis,"SharedArrayBuffer",{value:void 0,configurable:!0,writable:!1,enumerable:!1})}catch{Reflect.deleteProperty(globalThis,"SharedArrayBuffer")}return r}function Wa(e,t,r){if(t<=r)return;let s=new Error(`[${mf}] ${e}: payload is ${t} bytes, limit is ${r} bytes`);throw s.code=mf,s}function La(e,t,r){Wa(e,Qm(t),r)}function ny(e){if(e==null)return{};if(typeof e=="string"){let t=JSON.parse(e);return typeof t=="object"&&t!==null?t:{}}return typeof e=="object"?e:{}}function Ca(e,t,r){let s=e==null?t:Number(e);if(!Number.isInteger(s)||s<=0)throw new Error(`crypto.scrypt ${r} must be a positive integer`);return s}function ry(e,t){let r=ny(e),s=Number(t);if(!Number.isInteger(s)||s<0)throw new Error("crypto.scrypt key length must be a non-negative integer");let o=Ca(r.cost??r.N,Jm,"cost");if((o&o-1)!==0)throw new Error("crypto.scrypt cost must be a positive power of two");return{N:o,r:Ca(r.blockSize??r.r,Ym,"block size"),p:Ca(r.parallelization??r.p,Xm,"parallelization"),dkLen:s}}var kf={md5:ac,sha1:ic,sha224:gc,sha256:jn,sha384:_c,sha512:bc};function Ja(e){let t=String(e).trim().toLowerCase().replace(/[-_]/g,"");if(Object.hasOwn(kf,t))return t;throw new Error(`Unsupported browser crypto digest algorithm: ${e}`)}var sy={md5:"3020300c06082a864886f70d020505000410",sha1:"3021300906052b0e03021a05000414",sha224:"302d300d06096086480165030402040500041c",sha256:"3031300d060960864801650304020105000420",sha384:"3041300d060960864801650304020205000430",sha512:"3051300d060960864801650304020305000440"};function Sr(e){return kf[Ja(e)]}function Ya(e,t){return Sr(e)(ke(t))}function oy(e,t,r){return Rr(Sr(e),ke(t),ke(r))}function iy(e){let t=String(e).trim().toLowerCase().replace(/^rsa[-_]/,"");return Ja(t)}function ay(e){return Array.from(e,t=>t.toString(16).padStart(2,"0")).join("")}function Sf(e){let t=new Uint8Array(e.length/2);for(let r=0;ro+a.byteLength,0),r=new Uint8Array(t),s=0;for(let o of e)r.set(o,s),s+=o.byteLength;return r}function Xa(e){let t;if(typeof e=="string")try{let s=JSON.parse(e);t=typeof s=="string"?s:e}catch{t=e}else{if(e&&typeof e=="object"&&"key"in e)return Xa(e.key);throw new Error("Browser crypto RSA key must be a PEM string")}let r=t.replace(/-----BEGIN [^-]+-----|-----END [^-]+-----|\s+/g,"");return Ps(r)}var Pn=class e{bytes;offset=0;constructor(t){this.bytes=t}get position(){return this.offset}set position(t){this.offset=t}readTag(t){let r=this.bytes[this.offset++];if(r!==t)throw new Error(`Invalid RSA key DER tag: expected ${t}, got ${r}`);let s=this.readLength(),o=this.offset+s;if(o>this.bytes.byteLength)throw new Error("Invalid RSA key DER length");let a=this.bytes.subarray(this.offset,o);return this.offset=o,a}readSequence(){return new e(this.readTag(48))}readInteger(){let t=this.readTag(2);for(;t.byteLength>1&&t[0]===0;)t=t.subarray(1);return t}readOctetString(){return this.readTag(4)}readBitString(){let t=this.readTag(3);if(t[0]!==0)throw new Error("Unsupported RSA key bit string padding");return t.subarray(1)}skipAny(){if(this.bytes[this.offset++]==null)throw new Error("Invalid RSA key DER");let r=this.readLength();if(this.offset+=r,this.offset>this.bytes.byteLength)throw new Error("Invalid RSA key DER length")}readLength(){let t=this.bytes[this.offset++];if(t==null)throw new Error("Invalid RSA key DER length");if((t&128)===0)return t;let r=t&127;if(r===0||r>4)throw new Error("Unsupported RSA key DER length");let s=0;for(let o=0;ot)throw new Error("RSA value exceeds modulus length");let o=new Uint8Array(t);return o.set(s,t-s.byteLength),o}function On(e,t,r){let s=1n,o=e%r,a=t;for(;a>0n;)(a&1n)===1n&&(s=s*o%r),o=o*o%r,a>>=1n;return s}function cy(e){let t=e.readInteger(),r=e.readInteger();return{modulus:gt(t),exponent:gt(r),modulusLength:t.byteLength}}function Ef(e){let t=Xa(e),r=new Pn(t).readSequence();r.skipAny();let s=new Pn(r.readBitString()).readSequence();return cy(s)}function Tf(e){let t=Xa(e),r=new Pn(t).readSequence();r.skipAny(),r.skipAny();let s=new Pn(r.readOctetString()).readSequence();s.skipAny();let o=s.readInteger();s.skipAny();let a=s.readInteger();return{modulus:gt(o),exponent:gt(a),modulusLength:o.byteLength}}function Af(e,t){let r=iy(e),s=Ya(r,t);return{hashName:r,encoded:An([Sf(sy[r]),s])}}function Pf(e,t,r){let s=t.byteLength+11;if(r0;){let c=new Uint8Array([o>>>24&255,o>>>16&255,o>>>8&255,o&255]),l=Ya(r,An([e,c]));s.push(l.subarray(0,Math.min(a,l.byteLength))),a-=l.byteLength,o++}return An(s).subarray(0,t)}function If(e){return e==null?"sha1":Ja(e)}function dy(e,t){let r=e.modulusLength-t.byteLength-3;if(r<8)throw new Error("RSA_PKCS1_PADDING input is too large for key size");let s=An([new Uint8Array([0,2]),py(r),new Uint8Array([0]),t]),o=On(gt(s),e.exponent,e.modulus);return In(o,e.modulusLength)}function hy(e,t){if(t.byteLength!==e.modulusLength)throw new Error("RSA_PKCS1_PADDING encrypted input has invalid length");let r=In(On(gt(t),e.exponent,e.modulus),e.modulusLength);if(r[0]!==0||r[1]!==2)throw new Error("RSA_PKCS1_PADDING block is invalid");let s=-1;for(let o=2;ol)throw new Error("RSA_PKCS1_OAEP_PADDING input is too large for key size");let u=a(o),p=new Uint8Array(l-t.byteLength),m=An([u,p,new Uint8Array([1]),t]),h=new Uint8Array(c);if(!globalThis.crypto?.getRandomValues)throw new Error("Browser runtime crypto requires getRandomValues support");globalThis.crypto.getRandomValues(h);let b=Cs(h,e.modulusLength-c-1,s),w=Ls(m,b),x=Cs(w,c,s),I=Ls(h,x),S=An([new Uint8Array([0]),I,w]),P=On(gt(S),e.exponent,e.modulus);return In(P,e.modulusLength)}function yy(e,t,r){if(t.byteLength!==e.modulusLength)throw new Error("RSA_PKCS1_OAEP_PADDING encrypted input has invalid length");let s=If(r.oaepHash),o=kr(r.oaepLabel),a=Sr(s),c=a.outputLen;if(e.modulusLength<2*c+2)throw new Error("RSA key is too small for OAEP");let l=In(On(gt(t),e.exponent,e.modulus),e.modulusLength),u=l.subarray(1,1+c),p=l.subarray(1+c),m=Cs(p,c,s),h=Ls(u,m),b=Cs(h,e.modulusLength-c-1,s),w=Ls(p,b),x=a(o),I=l[0];for(let P=0;Pe(...s);return{applySync:t,applySyncPromise:t}}function gf(e){return{apply(t,r){return e(...r)}}}function ky(e){if(typeof e=="string")return e;if(e&&typeof e=="object"&&"encoding"in e){let t=e.encoding;return typeof t=="string"?t:null}return null}function Sy(e){return typeof Buffer=="function"?Buffer.from(e):e}function bf(e){return{...e,isFile:()=>!e.isDirectory&&!e.isSymbolicLink,isDirectory:()=>e.isDirectory,isSymbolicLink:()=>e.isSymbolicLink}}function Ey(e){return{name:e.name,isFile:()=>!e.isDirectory&&!e.isSymbolicLink,isDirectory:()=>e.isDirectory,isSymbolicLink:()=>!!e.isSymbolicLink}}function Ty(e){let t=new Map,r=1e3,s=(k,O,g)=>{let E=new Error(`${k}: ${O} '${g}'`);E.code=k;let N=Aa(k);return N!==void 0&&(E.errno=N),E.syscall=O,E},o=(k,O)=>{let g=t.get(Number(k));if(!g)throw s("EBADF",O,String(k));return g},a=0,c=1,l=2,u=64,p=128,m=512,h=1024,b={r:a,rs:a,sr:a,"r+":l,"rs+":l,"sr+":l,w:c|u|m,wx:c|u|m|p,xw:c|u|m|p,"w+":l|u|m,"wx+":l|u|m|p,"xw+":l|u|m|p,a:c|u|h,ax:c|u|h|p,xa:c|u|h|p,as:c|u|h,sa:c|u|h,"a+":l|u|h,"ax+":l|u|h|p,"xa+":l|u|h|p,"as+":l|u|h,"sa+":l|u|h},w=k=>{if(typeof k=="number")return k;let O=b[k];if(O===void 0){let g=new Error(`Unknown file open flag: ${k}`);throw g.code="ERR_INVALID_ARG_VALUE",g}return O},x=(k,O,g)=>g<=0?new Uint8Array(0):ke(e.requestBinary("fs.pread",[k,O,g])),I=(k,O)=>ky(O)?e.requestText("fs.readFile",[k]):Sy(e.requestBinary("fs.readFileBinary",[k])),S=(k,O)=>{if(typeof O=="string"){e.requestVoid("fs.writeFile",[k,O]);return}e.requestVoid("fs.writeFileBinary",[k,ke(O)])},P=(k,O)=>{if(typeof O=="boolean"?O:O?.recursive??!0){e.requestVoid("fs.mkdir",[k]);return}e.requestVoid("fs.createDir",[k])},B=(k,O)=>{let g=e.requestJson("fs.readDir",[k]);return O?.withFileTypes?g.map(E=>Ey(E)):g.map(E=>E.name)},C=k=>bf(e.requestJson("fs.stat",[k])),j=k=>bf(e.requestJson("fs.lstat",[k]));return{readFileSync:I,writeFileSync:S,mkdirSync:P,readdirSync:B,existsSync(k){return e.requestJson("fs.exists",[k])},statSync:C,lstatSync:j,unlinkSync(k){e.requestVoid("fs.unlink",[k])},rmdirSync(k){e.requestVoid("fs.rmdir",[k])},rmSync(k){e.requestVoid("fs.unlink",[k])},renameSync(k,O){e.requestVoid("fs.rename",[k,O])},realpathSync(k){return e.requestText("fs.realpath",[k])},readlinkSync(k){return e.requestText("fs.readlink",[k])},symlinkSync(k,O){e.requestVoid("fs.symlink",[k,O])},linkSync(k,O){e.requestVoid("fs.link",[k,O])},chmodSync(k,O){e.requestVoid("fs.chmod",[k,O])},truncateSync(k,O=0){e.requestVoid("fs.truncate",[k,O])},constants:{O_RDONLY:0,O_WRONLY:1,O_RDWR:2,O_CREAT:64,O_EXCL:128,O_TRUNC:512,O_APPEND:1024,O_DIRECTORY:65536},openSync(k,O=0){let g=w(O);if((g&u)!==0){let Y=e.requestJson("fs.exists",[k]);if(Y&&(g&p)!==0)throw s("EEXIST","open",k);Y||e.requestVoid("fs.writeFile",[k,""])}(g&m)!==0&&e.requestVoid("fs.truncate",[k,0]);let E=g&3;if((E===0||E===2)&&(g&(u|m))===0)try{x(k,0,1)}catch(Y){let re=Y?.code;if(re==="EACCES"||re==="EPERM")throw Y}let W=r++;return t.set(W,{path:k,flags:g}),W},readSync(k,O,g=0,E,N){let W=o(k,"read"),Y=typeof E=="number"?E:O.length-g,re=typeof N=="number"&&N>=0?N:0,oe=x(W.path,re,Y),Q=Math.min(oe.length,Y);for(let de=0;de=0?Q=N:(W.flags&h)!==0?Q=C(W.path).size:Q=0,e.requestVoid("fs.pwrite",[W.path,Q,oe]),re},closeSync(k){t.delete(Number(k))},fstatSync(k){return C(o(k,"fstat").path)},ftruncateSync(k,O=0){e.requestVoid("fs.truncate",[o(k,"ftruncate").path,Number(O)||0])},fsyncSync(){},fdatasyncSync(){},promises:{readFile(k,O){return Promise.resolve(I(k,O))},writeFile(k,O){return S(k,O),Promise.resolve()},mkdir(k,O){return P(k,O),Promise.resolve()},readdir(k,O){return Promise.resolve(B(k,O))},stat(k){return Promise.resolve(C(k))},lstat(k){return Promise.resolve(j(k))},unlink(k){return e.requestVoid("fs.unlink",[k]),Promise.resolve()},rmdir(k){return e.requestVoid("fs.rmdir",[k]),Promise.resolve()},rm(k){return e.requestVoid("fs.unlink",[k]),Promise.resolve()},rename(k,O){return e.requestVoid("fs.rename",[k,O]),Promise.resolve()},realpath(k){return Promise.resolve(e.requestText("fs.realpath",[k]))},readlink(k){return Promise.resolve(e.requestText("fs.readlink",[k]))},symlink(k,O){return e.requestVoid("fs.symlink",[k,O]),Promise.resolve()},link(k,O){return e.requestVoid("fs.link",[k,O]),Promise.resolve()},chmod(k,O){return e.requestVoid("fs.chmod",[k,O]),Promise.resolve()},truncate(k,O=0){return e.requestVoid("fs.truncate",[k,O]),Promise.resolve()}}}}var Ds=self.postMessage.bind(self);function vr(e){Ds({controlToken:Ns(),...e})}function _f(e,t){t.then(r=>{vr({type:"response",id:e,ok:!0,result:r})},r=>{let s=r;vr({type:"response",id:e,ok:!1,error:{message:s?.message??String(r),stack:s?.stack,code:s?.code}})})}function Ay(e){Ds({controlToken:Ns(),...e})}function Py(e,t,r,s){let o={controlToken:Ns(),type:"stdio",executionId:e,requestId:t,channel:r,message:s};Ds(o)}function Iy(e,t,r){let s={controlToken:Ns(),type:"pty-opened",executionId:e,requestId:t,...r};Ds(s)}function Lf(e,t,r,s){Py(e,t,r,vy(s))}function wf(e,t){if(!Rs||lt===null||ot===null)return;let r=t.map(s=>Cf(s)).join(" ");Lf(ot,lt,e,r)}function Oy(e){let t=new Int32Array(e.signalBuffer),r=new Uint8Array(e.dataBuffer),s=1,o=e.timeoutMs??3e4;function a(l){return l<=0?new Uint8Array(0):r.slice(0,l)}function c(l,u){if(!ot||lt===null)throw new Error(`Browser runtime sync bridge ${l} called outside an active execution`);for(Atomics.store(t,0,0),Atomics.store(t,1,0),Atomics.store(t,2,0),Atomics.store(t,3,0),Ay({type:"sync-request",requestId:s++,executionId:ot,processRequestId:lt,operation:l,args:u});Atomics.wait(t,0,0,o)==="timed-out";)throw new Error(`Browser runtime sync bridge timed out while handling ${l}`);let p=Atomics.load(t,1),m=Atomics.load(t,2),h=Atomics.load(t,3),b=a(h);if(Atomics.store(t,0,0),p===1){let w=JSON.parse($t.decode(b)),x=new Error(w.message);if(w.code){x.code=w.code;let I=Aa(w.code);I!==void 0&&(x.errno=I)}throw x}return{kind:m,bytes:b}}return{requestVoid(l,u){c(l,u)},requestText(l,u){let p=c(l,u);if(p.kind!==1)throw new Error(`Expected text response from ${l}, received kind ${p.kind}`);return $t.decode(p.bytes)},requestNullableText(l,u){let p=c(l,u);if(p.kind===0)return null;if(p.kind!==1)throw new Error(`Expected text response from ${l}, received kind ${p.kind}`);return $t.decode(p.bytes)},requestBinary(l,u){let p=c(l,u);if(p.kind!==2)throw new Error(`Expected binary response from ${l}, received kind ${p.kind}`);return p.bytes},requestJson(l,u){let p=c(l,u);if(p.kind!==3)throw new Error(`Expected JSON response from ${l}, received kind ${p.kind}`);return JSON.parse($t.decode(p.bytes))}}}async function By(e){if(Fa)return;if(af(),Va(),!e.syncBridge)throw new Error("Browser runtime sync bridge is required for filesystem and module loading parity");let t=Oy(e.syncBridge);ja=t,Ba=e.payloadLimits?.base64TransferBytes??xf,Os=e.payloadLimits?.jsonPayloadBytes??vf,e.networkEnabled?Oa=sf():Oa=Is();let r=e.processConfig??{};Xe=r,Bs=e.timingMitigation??r.timingMitigation??Bs,r.timingMitigation=Bs,delete r.frozenTimeMs,U("_processConfig",r);let s=e.osConfig??{};U("_osConfig",s),U("__agentOSVirtualOs",s),U("_log",q((...g)=>{wf("stdout",g)})),U("_error",q((...g)=>{wf("stderr",g)}));let o=q(g=>{let E=t.requestText("fs.readFile",[g]);return La(`fs.readFile ${g}`,E,Os),E}),a=q((g,E)=>{La(`fs.writeFile ${g}`,E,Os),t.requestVoid("fs.writeFile",[g,E])}),c=q(g=>{let E=t.requestBinary("fs.readFileBinary",[g]);return Wa(`fs.readFileBinary ${g}`,E.byteLength,Ba),E}),l=q((g,E)=>{Wa(`fs.writeFileBinary ${g}`,E.byteLength,Ba),t.requestVoid("fs.writeFileBinary",[g,E])}),u=q(g=>{let E=JSON.stringify(t.requestJson("fs.readDir",[g]));return La(`fs.readDir ${g}`,E,Os),E}),p=q(g=>{t.requestVoid("fs.mkdir",[g])}),m=q(g=>{t.requestVoid("fs.rmdir",[g])}),h=q(g=>t.requestJson("fs.exists",[g])),b=q(g=>JSON.stringify(t.requestJson("fs.stat",[g]))),w=q(g=>{t.requestVoid("fs.unlink",[g])}),x=q((g,E)=>{t.requestVoid("fs.rename",[g,E])});U("_fs",{readFile:o,writeFile:a,readFileBinary:c,writeFileBinary:l,readDir:u,mkdir:p,rmdir:m,exists:h,stat:b,unlink:w,rename:x}),U("_loadPolyfill",q(g=>{let E=g.replace(/^node:/,"");return tf[E]??null}));let I=(g,E,N)=>t.requestNullableText("module.resolve",[g,E,N??"require"]),S=(g,E)=>{let N=t.requestNullableText("module.loadFile",[g]);if(N===null)return null;let W=N;return Ea(N,g)&&(W=ka(W,{transforms:["imports"]}).code),Ta(W)},P=g=>t.requestNullableText("module.format",[g]),B=g=>t.requestJson("module.batchResolve",[g]);U("_resolveModuleSync",q(I)),U("_loadFileSync",q(S)),U("_resolveModule",q(I)),U("_loadFile",q(S)),U("_moduleFormat",q(P)),U("_batchResolveModules",q(B));let C=g=>{if(!Number.isInteger(g)||g<0)throw new Error("crypto random byte length must be a non-negative integer");let E=new Uint8Array(g),N=globalThis.crypto;if(!N?.getRandomValues)throw new Error("Browser runtime crypto requires getRandomValues support");for(let W=0;WC(Number(g)))),U("_cryptoRandomUUID",q(()=>{if(typeof globalThis.crypto?.randomUUID!="function")throw new Error("Browser runtime crypto requires randomUUID support");return globalThis.crypto.randomUUID()})),U("_cryptoHashDigest",q((g,E)=>Ya(g,E))),U("_cryptoHmacDigest",q((g,E,N)=>oy(g,E,N))),U("_cryptoPbkdf2",q((g,E,N,W,Y)=>by(g,E,N,W,Y))),U("_cryptoScrypt",q((g,E,N,W)=>xc(ke(g),ke(E),ry(W,N)))),U("_cryptoCipheriv",q((g,E,N,W,Y)=>_y(g,E,N,W,Y))),U("_cryptoDecipheriv",q((g,E,N,W,Y)=>wy(g,E,N,W,Y))),U("_cryptoCipherivCreate",q(()=>Ye("_cryptoCipherivCreate"))),U("_cryptoCipherivUpdate",q(()=>Ye("_cryptoCipherivUpdate"))),U("_cryptoCipherivFinal",q(()=>Ye("_cryptoCipherivFinal"))),U("_cryptoSign",q((g,E,N)=>ly(g,E,N))),U("_cryptoVerify",q((g,E,N,W)=>uy(g,E,N,W))),U("_cryptoAsymmetricOp",q((g,E,N,W)=>gy(g,E,N,W))),U("_cryptoCreateKeyObject",q(()=>Ye("_cryptoCreateKeyObject"))),U("_cryptoGenerateKeyPairSync",q(()=>Ye("_cryptoGenerateKeyPairSync"))),U("_cryptoGenerateKeySync",q(()=>Ye("_cryptoGenerateKeySync"))),U("_cryptoGeneratePrimeSync",q(()=>Ye("_cryptoGeneratePrimeSync"))),U("_cryptoDiffieHellman",q(()=>Ye("_cryptoDiffieHellman"))),U("_cryptoDiffieHellmanGroup",q(()=>Ye("_cryptoDiffieHellmanGroup"))),U("_cryptoDiffieHellmanSessionCreate",q(()=>Ye("_cryptoDiffieHellmanSessionCreate"))),U("_cryptoDiffieHellmanSessionCall",q(()=>Ye("_cryptoDiffieHellmanSessionCall"))),U("_cryptoDiffieHellmanSessionDestroy",q(()=>Ye("_cryptoDiffieHellmanSessionDestroy"))),U("_cryptoSubtle",q(()=>Ye("_cryptoSubtle"))),U("_scheduleTimer",{apply(g,E){return new Promise(N=>{setTimeout(N,E[0])})}});let j=Oa??Is(),H=(g,E)=>t.requestJson("network.fetch",[g,E]);U("_networkFetchRaw",gf(async(g,E)=>{let N=JSON.parse(E),W=H(g,N);return JSON.stringify(W)})),U("_networkDnsLookupRaw",gf(async g=>{let E=typeof g=="string"?g:String(g.hostname??""),N=await j.dnsLookup(E);if(N.error){let W=new Error(N.error);throw W.code=N.code,W}return JSON.stringify(N)})),U("fetch",async(g,E)=>{let N=g??{},W=typeof g=="string"?g:String(N.url??g),Y=String(E?.method??N.method??"GET"),re={},oe=E?.headers??N.headers;if(oe)if(Array.isArray(oe))for(let he of oe)re[he[0]]=he[1];else if(typeof oe.forEach=="function")oe.forEach((he,Se)=>{re[Se]=he});else for(let he of Object.keys(oe))re[he]=String(oe[he]);let Q=E?.body??N.body;Q!=null&&typeof Q!="string"&&(Q=Q instanceof Uint8Array?new TextDecoder().decode(Q):String(Q));let de=H(W,{method:Y,headers:re,body:Q??null});return new Response(de.body??"",{status:de.status??200,statusText:de.statusText||"",headers:de.headers??{}})}),U("global",globalThis),typeof globalThis.setImmediate!="function"&&(U("setImmediate",(g,...E)=>setTimeout(()=>g(...E),0)),U("clearImmediate",g=>clearTimeout(g))),U("_childProcessSpawnStart",q(g=>t.requestJson("child_process.spawn",[g]))),U("_childProcessPoll",q((g,E)=>t.requestJson("child_process.poll",[g,E??0]))),U("_childProcessStdinWrite",q((g,E)=>{t.requestVoid("child_process.write_stdin",[g,E])})),U("_childProcessStdinClose",q(g=>{t.requestVoid("child_process.close_stdin",[g])})),U("_childProcessKill",q((g,E)=>{t.requestVoid("child_process.kill",[g,E])})),U("_childProcessSpawnSync",q(g=>t.requestText("child_process.spawn_sync",[g]))),U("_processSignalState",q((g,E,N,W)=>{t.requestVoid("process.signal_state",[g,E,N,W])})),U("_dgramSocketCreateRaw",q(g=>t.requestJson("dgram.create",[g]))),U("_dgramSocketBindRaw",q((g,E)=>t.requestJson("dgram.bind",[g,E]))),U("_dgramSocketRecvRaw",q((g,E)=>t.requestJson("dgram.recv",[g,E??0]))),U("_dgramSocketSendRaw",q((g,E,N)=>t.requestJson("dgram.send",[g,E,N]))),U("_dgramSocketCloseRaw",q(g=>t.requestJson("dgram.close",[g]))),U("_dgramSocketAddressRaw",q(g=>t.requestJson("dgram.address",[g]))),U("_dgramSocketSetBufferSizeRaw",q((g,E,N)=>t.requestJson("dgram.setBufferSize",[g,E,N]))),U("_dgramSocketGetBufferSizeRaw",q((g,E)=>t.requestJson("dgram.getBufferSize",[g,E]))),U("_fsModule",Ty(t)),Me("_moduleCache",{}),Me("_pendingModules",{}),Me("_currentModule",{dirname:"/"}),za(rf()),Fy();let k=["XMLHttpRequest","WebSocket","importScripts","indexedDB","caches","BroadcastChannel"];for(let g of k){try{delete self[g]}catch{}Object.defineProperty(self,g,{get(){throw new ReferenceError(`${g} is not available in sandbox`)},configurable:!1})}let O=self.onmessage;Object.defineProperty(self,"onmessage",{value:O,writable:!1,configurable:!1}),Object.defineProperty(self,"postMessage",{get(){throw new TypeError("postMessage is not available in sandbox")},configurable:!1}),Fa=!0}function Ry(e){Me("_moduleCache",{}),Me("_pendingModules",{}),Me("_currentModule",{dirname:e})}function Ly(){Me("__dynamicImport",e=>{let t=zm.get(e);if(t)return Promise.resolve(t);try{let r=globalThis.require;if(typeof r!="function")throw new Error("require is not available in browser runtime");let s=r(e);return Promise.resolve({default:s,...s})}catch(r){return Promise.reject(new Error(`Cannot dynamically import '${e}': ${String(r)}`))}})}function Da(e,t){return t?e:Ga.encode(e)}function Cf(e){return typeof e=="string"?e:e instanceof Uint8Array?$t.decode(e):ArrayBuffer.isView(e)?$t.decode(new Uint8Array(e.buffer,e.byteOffset,e.byteLength)):e instanceof ArrayBuffer?$t.decode(new Uint8Array(e)):String(e)}function Cy(e,t){return lt===null||ot===null||Lf(ot,lt,e,Cf(t)),!0}function Ny(){let e="/",t="",r=0,s=!1,o=!1,a=null,c=0,l=!1,u=Object.create(null),p=Object.create(null),m={isatty(_){return a!==null&&(typeof _=="number"||typeof _=="string")&&(_===0||_===1||_===2||_==="0"||_==="1"||_==="2")},columns(){return a?.columns??80},rows(){return a?.rows??24}};globalThis.__agentOSTtyState=m;let h=_=>{if(!_||typeof _!="object")return null;let L=_,z,Z=L.slaveFd,ie=Number.isInteger(L.columns)?L.columns:80,be=Number.isInteger(L.rows)?L.rows:24,bt;if(L.open===!0){let Ze=x().requestJson("pty.open",[{}]);if(!Number.isInteger(Ze.masterFd)||!Number.isInteger(Ze.slaveFd))throw new Error("pty.open returned invalid fd pair");z=Ze.masterFd,Z=Ze.slaveFd,bt=typeof Ze.path=="string"?Ze.path:void 0,x().requestVoid("pty.resize",[{fd:Z,cols:Math.max(1,ie),rows:Math.max(1,be)}]),ot&<!==null&&Iy(ot,lt,{masterFd:z,slaveFd:Z,path:bt,columns:Math.max(1,ie),rows:Math.max(1,be)})}return!Number.isInteger(Z)||Z<0?null:{masterFd:z,slaveFd:Z,path:bt,columns:Math.max(1,ie),rows:Math.max(1,be)}},b=(_,L)=>{let z=[...u[_]??[],...p[_]??[]];p[_]=[];for(let Z of z)Z(L);return z.length>0},w=()=>{for(let _ of Object.keys(u))u[_]=[];for(let _ of Object.keys(p))p[_]=[]},x=()=>{let _=ja;if(!_)throw new Error("PTY stdio requires an active sync bridge");return _},I=_=>O.encoding?$t.decode(_):_,S=()=>{if(!a||s||O.paused)return!1;let _=x().requestJson("pty.read",[{fd:a.slaveFd,maxBytes:4096,timeoutMs:0}]);return typeof _.data!="string"?!1:(b("data",I(Ps(_.data))),!0)},P=()=>{if(!a||s||O.paused||l)return;let _=c;l=!0,setTimeout(()=>{if(l=!1,!(_!==c||!a||s||O.paused))try{S()}finally{P()}},0)},B=(_,L)=>{if(!a)return!1;let z=ke(_);return x().requestJson("pty.write",[{fd:a.slaveFd,data:z}]),!0},C=_=>{a&&x().requestVoid("pty.tcsetattr",[_?{fd:a.slaveFd,icrnl:!1,opost:!1,icanon:!1,echo:!1,isig:!1}:{fd:a.slaveFd,icrnl:!0,opost:!0,icanon:!0,echo:!0,isig:!0}])},j=()=>{c+=1,l=!1},H=()=>{if(o=!1,!(O.paused||s)){if(a){S(),P();return}if(r{s||a||(O.paused&&(O.paused=!1),b("data",Da(_,O.encoding)))},Ha=()=>{s||a||(s=!0,b("end"),b("close"))};let k=()=>{o||(o=!0,queueMicrotask(H))},O={readable:!0,paused:!0,encoding:null,isRaw:!1,read(_){if(a){let z=x().requestJson("pty.read",[{fd:a.slaveFd,maxBytes:_??4096,timeoutMs:0}]);return typeof z.data=="string"?I(Ps(z.data)):null}if(r>=t.length)return null;let L=_?t.slice(r,r+_):t.slice(r);return r+=L.length,Da(L,O.encoding)},on(_,L){return u[_]||(u[_]=[]),u[_].push(L),_==="data"&&O.paused&&O.resume(),O},once(_,L){return p[_]||(p[_]=[]),p[_].push(L),_==="data"&&O.paused&&O.resume(),O},off(_,L){return u[_]&&(u[_]=u[_].filter(z=>z!==L)),O},removeListener(_,L){return O.off(_,L)},emit(_,L){return b(_,L)},pause(){return O.paused=!0,O},resume(){return O.paused=!1,a?P():k(),O},setEncoding(_){return O.encoding=_,O},setRawMode(_){return C(_),O.isRaw=_,O},get readableLength(){return a?0:Ga.encode(t.slice(r)).byteLength},get isTTY(){return a!==null},async*[Symbol.asyncIterator](){let _=t.slice(r);for(let L of _.split(` -`))L.length>0&&(yield L)}},g=Object.create(null),E=Object.create(null),N=Object.create(null),W=Object.create(null),Y=Object.create(null),re=Object.create(null),oe=_=>{if(typeof _!="function")throw new TypeError("process listener must be a function");return _},Q=_=>(g[_]?.length??0)+(E[_]?.length??0),de=(_,L)=>{let z=Yu(_);if(z===null)return;let Z=ja;Z&&Z.requestVoid("process.signal_state",[z,L,"[]",0])},he=(_,L,z)=>{L===0&&z>0?de(_,"user"):L>0&&z===0&&de(_,"default")},Se=(_,L,z)=>{oe(L);let Z=Q(_),ie=z?E:g;return ie[_]||(ie[_]=[]),ie[_].push(L),he(_,Z,Q(_)),le},Oe=(_,L)=>{oe(L);let z=Q(_);return g[_]&&(g[_]=g[_].filter(Z=>Z!==L)),E[_]&&(E[_]=E[_].filter(Z=>Z!==L)),he(_,z,Q(_)),le},Te=(_,L)=>{let z=Q(_),Z=[...g[_]??[],...E[_]??[]];E[_]=[];for(let ie of Z)ie(L);return he(_,z,Q(_)),Z.length>0},Be=_=>{if(typeof _!="function")throw new TypeError("stream listener must be a function");return _},Qe=(_,L,z,Z)=>(Be(z),_[L]||(_[L]=[]),_[L].push(z),Z),ut=(_,L,z,Z)=>(Be(z),_[L]&&(_[L]=_[L].filter(ie=>ie!==z)),Z),It=(_,L,z,Z)=>{let ie=[..._[z]??[],...L[z]??[]];L[z]=[];for(let be of ie)be(Z);return ie.length>0},Ot=(_,L)=>{for(let z of Object.keys(_))_[z]=[];for(let z of Object.keys(L))L[z]=[]},tn=(_,L,z)=>{let Z={get isTTY(){return a!==null},get columns(){return a?.columns??80},get rows(){return a?.rows??24},write(ie,be,bt){let Ze=a?B(ie,be):Cy(_,ie),nn=typeof be=="function"?be:bt;return typeof nn=="function"&&nn(),Ze},on(ie,be){return Qe(L,String(ie),be,Z)},once(ie,be){return Qe(z,String(ie),be,Z)},off(ie,be){return ut(L,String(ie),be,Z)},removeListener(ie,be){return ut(L,String(ie),be,Z),ut(z,String(ie),be,Z),Z},emit(ie,be){return It(L,z,String(ie),be)}};return Z},Ht=tn("stdout",N,W),Gt=tn("stderr",Y,re),le={browser:!0,env:{},argv:["node"],argv0:"node",pid:1,ppid:0,uid:1e3,gid:1e3,platform:"browser",arch:"x64",version:"v22.0.0",versions:{node:"22.0.0",openssl:"3.6.2"},stdin:O,stdout:Ht,stderr:Gt,exitCode:0,cwd:()=>e,chdir:_=>{e=String(_)},getuid:()=>le.uid,getgid:()=>le.gid,geteuid:()=>le.uid,getegid:()=>le.gid,getgroups:()=>[le.gid],nextTick:(_,...L)=>{queueMicrotask(()=>_(...L))},exit(_){let L=typeof _=="number"?_:le.exitCode??0;if(le.exitCode=L,Tn){let z=Tn;Tn=null,z(L);return}throw new Error(`process.exit(${L})`)},kill(_,L="SIGTERM"){if(_!==le.pid)throw new Error(`process.kill only supports the current browser process pid (${le.pid})`);let z=typeof L=="number"?Vm(L):String(L);return z==="SIGWINCH"&&(Ht.emit("resize"),Gt.emit("resize")),Te(z)},on(_,L){return Se(String(_),L,!1)},once(_,L){return Se(String(_),L,!0)},off(_,L){return Oe(String(_),L)},removeListener(_,L){return Oe(String(_),L)},emit(_,L){return Te(String(_),L)},__secureExecRefreshProcess(_){j(),w();for(let L of Object.keys(g))g[L]=[];for(let L of Object.keys(E))E[L]=[];Ot(N,W),Ot(Y,re),t=typeof _?.stdin=="string"?_.stdin:"",r=0,s=!1,o=!1,O.paused=!0,O.encoding=null,O.isRaw=!1,a=h(_?.stdioPty),le.exitCode=0,le.env=_?.env&&typeof _.env=="object"?{..._.env}:{},typeof _?.cwd=="string"&&(e=_.cwd),le.argv=Array.isArray(_?.argv)?_.argv.map(L=>String(L)):["node"],le.argv0=le.argv[0]??"node",typeof _?.platform=="string"&&(le.platform=_.platform),typeof _?.arch=="string"&&(le.arch=_.arch),typeof _?.version=="string"&&(le.version=_.version,le.versions.node=_.version.replace(/^v/,"")),typeof _?.pid=="number"&&(le.pid=_.pid),typeof _?.ppid=="number"&&(le.ppid=_.ppid),typeof _?.uid=="number"&&(le.uid=_.uid),typeof _?.gid=="number"&&(le.gid=_.gid)},__secureExecStopPtyStdio(){j(),a=null},__secureExecResizePty(_,L){a&&(a.columns=Math.max(1,Math.trunc(_)),a.rows=Math.max(1,Math.trunc(L)),Ht.emit("resize"),Gt.emit("resize"),Te("SIGWINCH"))}};return le}function Er(){let e=globalThis.process;if(!(!e||typeof e!="object"))return e}function qa(){let t=Er()?.__secureExecRefreshProcess;typeof t=="function"&&t(Xe)}function Dy(e,t,r){if(e!==ot)return;let o=Er()?.__secureExecResizePty;typeof o=="function"&&o(t,r)}function Fy(){if(Er()){qa();return}Me("process",Ny()),qa()}function jy(e,t,r){if(Xe&&(Xe.timingMitigation=t,r===void 0?delete Xe.frozenTimeMs:Xe.frozenTimeMs=r,Xe.stdin=e?.stdin??"",e?.stdioPty?Xe.stdioPty=e.stdioPty:delete Xe.stdioPty,e?.env)){let o=Xe.env&&typeof Xe.env=="object"?Xe.env:{};Xe.env={...o,...e.env}}qa();let s=Er();if(s&&(s.exitCode=0,s.timingMitigation=t,r===void 0?delete s.frozenTimeMs:s.frozenTimeMs=r,e?.cwd&&typeof s.chdir=="function")){Me("__runtimeProcessCwdOverride",e.cwd),za(nf("overrideProcessCwd"));try{s.chdir(e.cwd)}catch(o){if(!(o instanceof Error&&o.message.includes("process.chdir() is not supported in workers")))throw o}}}async function Nf(e,t,r,s,o=!1){Ry(s?.cwd??"/");let a=s?.timingMitigation??Bs,c=ty(a),l=lt,u=ot,p=Rs;lt=t,ot=e,Rs=o,Tn=null,Ua=!!s?.streamingStdin,jy(s,a,c),Ly();try{let m=(async()=>{let b=r;Ea(r,s?.filePath)&&(b=ka(b,{transforms:["imports"]}).code),b=Ta(b),Me("module",{exports:{}});let w=globalThis.module;if(Me("exports",w.exports),s?.filePath){let B=s.filePath.includes("/")&&s.filePath.substring(0,s.filePath.lastIndexOf("/"))||"/";Me("__filename",s.filePath),Me("__dirname",B),Me("_currentModule",{dirname:B,filename:s.filePath})}let x=s?.persistent?new Promise(B=>{Tn=B}):null,I=za(b);I&&typeof I=="object"&&typeof I.then=="function"&&await I,await Promise.resolve();let S=()=>globalThis.process?.exitCode??0;if(x){let B=await Promise.race([x,new Promise(C=>setTimeout(()=>{Tn=null,C(S())},Km))]);return await new Promise(C=>setTimeout(C,0)),await new Promise(C=>setTimeout(C,0)),{code:B}}let P=globalThis._waitForActiveHandles;return typeof P=="function"&&await P(),{code:S()}})(),h=new Promise(b=>{Ma.set(e,w=>{let x=Xu(w);b({code:x??0})})});return await Promise.race([m,h])}catch(m){let h=m instanceof Error?m.message:String(m),b=h.match(/process\.exit\((\d+)\)/);if(b)return{code:Number.parseInt(b[1],10)};let w=m instanceof Error&&m.stack?m.stack:h;return{code:1,errorMessage:xy(w)}}finally{Er()?.__secureExecStopPtyStdio?.(),Ma.delete(e),lt=l,ot=u,Rs=p,Ua=!1,$a=null,Ha=null}}async function My(e,t,r,s,o=!1){let a=await Nf(e,t,r,{filePath:s},o),c=globalThis.module;return{...a,exports:c?.exports}}self.onmessage=async e=>{let t=e.data;try{if(t.type==="init"){if(typeof t.controlToken!="string"||t.controlToken.length===0||en&&t.controlToken!==en)return;en=t.controlToken,await By(t.payload),vr({type:"response",id:t.id,ok:!0,result:!0});return}if(!en||t.controlToken!==en)return;if(!Fa)throw new Error("Sandbox worker not initialized");if(t.type==="exec"){_f(t.id,Nf(t.payload.executionId,t.id,t.payload.code,t.payload.options,t.payload.captureStdio));return}if(t.type==="write-stdin"){$a?.(t.data);return}if(t.type==="end-stdin"){Ha?.();return}if(t.type==="resize-pty"){Dy(t.executionId,t.columns,t.rows);return}if(t.type==="run"){_f(t.id,My(t.payload.executionId,t.id,t.payload.code,t.payload.filePath,t.payload.captureStdio));return}if(t.type==="signal"){let r=Number(t.payload.signal),s=Ma.get(t.payload.executionId);s&&Number.isInteger(r)&&s(r);return}if(t.type==="extension"){let r=new Error(`Browser worker extension dispatch is not implemented for namespace ${t.payload.namespace}`);throw r.code="ERR_SECURE_EXEC_BROWSER_EXTENSION_UNSUPPORTED",r}t.type==="dispose"&&(vr({type:"response",id:t.id,ok:!0,result:!0}),close())}catch(r){let s=r;vr({type:"response",id:t.id,ok:!1,error:{message:s?.message??String(r),stack:s?.stack,code:s?.code}})}}; + `}var Mm=new Set(["localhost","ip4-localhost","ip4-loopback"]),jm=new Set(["ip6-localhost","ip6-loopback"]);function Wm(e){let t=e.split(".");return t.length===4&&t.every(r=>{if(!/^\d+$/.test(r))return!1;let s=Number(r);return s>=0&&s<=255&&String(s)===r})}function Um(e){return e.includes(":")&&/^[0-9a-fA-F:.]+$/.test(e)}function qm(e){let t=e.trim().toLowerCase();return Mm.has(t)?{address:"127.0.0.1",family:4}:jm.has(t)?{address:"::1",family:6}:Wm(t)?{address:t,family:4}:Um(t)?{address:t,family:6}:{error:"DNS not supported in browser",code:"ENOSYS"}}var Hm=typeof globalThis<"u"&&typeof globalThis.fetch=="function"?globalThis.fetch.bind(globalThis):void 0;function of(){let e=Hm??fetch;return{async fetch(t,r){let s=await e(t,{method:r?.method||"GET",headers:r?.headers,body:r?.body}),o={};s.headers.forEach((u,d)=>{o[d]=u});let a=s.headers.get("content-type")||"",c=a.includes("octet-stream")||a.includes("gzip")||t.endsWith(".tgz"),l;if(c){let u=await s.arrayBuffer();l=Vu(new Uint8Array(u)),o["x-body-encoding"]="base64"}else l=await s.text();return{ok:s.ok,status:s.status,statusText:s.statusText,headers:o,body:l,url:s.url,redirected:s.redirected}},async dnsLookup(t){return qm(t)},async httpRequest(t,r){let s=await e(t,{method:r?.method||"GET",headers:r?.headers,body:r?.body}),o={};s.headers.forEach((c,l)=>{o[l]=c});let a=await s.text();return{status:s.status,statusText:s.statusText,headers:o,body:a,url:s.url}}}}var af={EPERM:1,ENOENT:2,ESRCH:3,EINTR:4,EIO:5,ENXIO:6,E2BIG:7,ENOEXEC:8,EBADF:9,ECHILD:10,EAGAIN:11,EWOULDBLOCK:11,ENOMEM:12,EACCES:13,EFAULT:14,ENOTBLK:15,EBUSY:16,EEXIST:17,EXDEV:18,ENODEV:19,ENOTDIR:20,EISDIR:21,EINVAL:22,ENFILE:23,EMFILE:24,ENOTTY:25,ETXTBSY:26,EFBIG:27,ENOSPC:28,ESPIPE:29,EROFS:30,EMLINK:31,EPIPE:32,EDOM:33,ERANGE:34,ENAMETOOLONG:36,ENOSYS:38,ENOTEMPTY:39,ELOOP:40,EOVERFLOW:75,ENOTSOCK:88,EDESTADDRREQ:89,EMSGSIZE:90,EPROTOTYPE:91,ENOPROTOOPT:92,EPROTONOSUPPORT:93,ENOTSUP:95,EOPNOTSUPP:95,EAFNOSUPPORT:97,EADDRINUSE:98,EADDRNOTAVAIL:99,ENETDOWN:100,ENETUNREACH:101,ECONNABORTED:103,ECONNRESET:104,ENOBUFS:105,EISCONN:106,ENOTCONN:107,ETIMEDOUT:110,ECONNREFUSED:111,EHOSTUNREACH:113,EALREADY:114,EINPROGRESS:115};function Aa(e){if(e&&Object.prototype.hasOwnProperty.call(af,e))return-af[e]}var zx=4*Int32Array.BYTES_PER_ELEMENT;var Gx=16*1024*1024,Vx=64*1024;var $m=["fs.readFile","fs.writeFile","fs.readFileBinary","fs.writeFileBinary","fs.pread","fs.pwrite","fs.readDir","fs.createDir","fs.mkdir","fs.rmdir","fs.exists","fs.stat","fs.lstat","fs.unlink","fs.rename","fs.realpath","fs.readlink","fs.symlink","fs.link","fs.chmod","fs.truncate","module.resolve","module.loadFile","module.format","module.batchResolve","child_process.spawn","child_process.poll","child_process.write_stdin","child_process.close_stdin","child_process.kill","child_process.spawn_sync","process.signal_state","network.fetch","dgram.create","dgram.bind","dgram.recv","dgram.send","dgram.close","dgram.address","dgram.setBufferSize","dgram.getBufferSize","pty.open","pty.read","pty.write","pty.close","pty.resize","pty.setForegroundPgid","pty.tcgetattr","pty.tcsetattr"],Kx=new Set($m);function cf(){if(typeof SharedArrayBuffer>"u")throw new Error("Browser runtime requires SharedArrayBuffer for sync filesystem and module loading parity");if(typeof Atomics>"u"||typeof Atomics.wait!="function")throw new Error("Browser runtime requires Atomics.wait for sync filesystem and module loading parity")}var Oa=null,Da=!1,en=null,Bs="freeze",Ye=null,lt=null,ot=null,Cs=!1,Ma=null,ja=new Map,Km=new Map,Pn=null,Wa=!1,Ha=null,$a=null,Jm=12e4,hf=8192,mf=8192;function Xm(e){return Object.entries(En).find(([,t])=>t===e)?.[0]??`SIG${e}`}var vf=16*1024*1024,Sf=4*1024*1024,gf="ERR_SANDBOX_PAYLOAD_TOO_LARGE",Ym=16384,Qm=8,Zm=1,Ba=vf,Os=Sf,za=new TextEncoder,Ht=new TextDecoder,Ga=eval,Va=["byteLength","slice","grow","maxByteLength","growable"],Ie={captured:!1,sharedArrayBufferPrototypeDescriptors:new Map};function eg(e){return za.encode(e).byteLength}function Fs(){if(!en)throw new Error("Browser runtime worker control channel is not initialized");return en}function Ka(){if(Ie.captured)return;Ie.captured=!0,Ie.dateDescriptor=Object.getOwnPropertyDescriptor(globalThis,"Date"),Ie.dateValue=globalThis.Date,Ie.performanceDescriptor=Object.getOwnPropertyDescriptor(globalThis,"performance"),Ie.performanceValue=globalThis.performance,Ie.sharedArrayBufferDescriptor=Object.getOwnPropertyDescriptor(globalThis,"SharedArrayBuffer"),Ie.sharedArrayBufferValue=globalThis.SharedArrayBuffer;let e=globalThis.SharedArrayBuffer;if(typeof e!="function")return;let t=e.prototype;for(let r of Va)Ie.sharedArrayBufferPrototypeDescriptors.set(r,Object.getOwnPropertyDescriptor(t,r))}function Ca(e,t){if(t)try{Object.defineProperty(globalThis,e,t);return}catch{if("value"in t){globalThis[e]=t.value;return}}Reflect.deleteProperty(globalThis,e)}function tg(){let e=Ie.sharedArrayBufferValue;if(typeof e!="function")return;let t=e.prototype;for(let r of Va){let s=Ie.sharedArrayBufferPrototypeDescriptors.get(r);try{s?Object.defineProperty(t,r,s):delete t[r]}catch{}}}function ng(){Ka(),Ca("Date",Ie.dateDescriptor),Ca("performance",Ie.performanceDescriptor),tg(),Ca("SharedArrayBuffer",Ie.sharedArrayBufferDescriptor),(typeof globalThis.performance>"u"||globalThis.performance===null)&&Object.defineProperty(globalThis,"performance",{value:{now:()=>Date.now()},configurable:!0,writable:!0})}function rg(e,t){if(Ka(),ng(),e!=="freeze")return;let r=typeof t=="number"&&Number.isFinite(t)?Math.trunc(t):Date.now(),s=Ie.dateValue??Ie.dateDescriptor?.value??Date,o=()=>r,a=function(...d){return new.target?d.length===0?new s(r):new s(...d):s()};Object.defineProperty(a,"prototype",{value:s.prototype,writable:!1,configurable:!1}),Object.defineProperty(a,"now",{value:o,configurable:!0,writable:!1}),a.parse=s.parse,a.UTC=s.UTC;try{Object.defineProperty(globalThis,"Date",{value:a,configurable:!0,writable:!1})}catch{globalThis.Date=a}let c=Object.create(null),l=Ie.performanceValue;if(typeof l<"u"&&l!==null){let d=l;for(let m of Object.getOwnPropertyNames(Object.getPrototypeOf(l)??l))if(m!=="now")try{let h=d[m];c[m]=typeof h=="function"?h.bind(l):h}catch{}}Object.defineProperty(c,"now",{value:()=>0,configurable:!0,writable:!1}),Object.freeze(c);try{Object.defineProperty(globalThis,"performance",{value:c,configurable:!0,writable:!1})}catch{globalThis.performance=c}let u=Ie.sharedArrayBufferValue;if(typeof u=="function"){let d=u.prototype;for(let m of Va)try{Object.defineProperty(d,m,{get(){throw new TypeError("SharedArrayBuffer is not available in sandbox")},configurable:!0})}catch{}}try{Object.defineProperty(globalThis,"SharedArrayBuffer",{value:void 0,configurable:!0,writable:!1,enumerable:!1})}catch{Reflect.deleteProperty(globalThis,"SharedArrayBuffer")}return r}function Ua(e,t,r){if(t<=r)return;let s=new Error(`[${gf}] ${e}: payload is ${t} bytes, limit is ${r} bytes`);throw s.code=gf,s}function La(e,t,r){Ua(e,eg(t),r)}function sg(e){if(e==null)return{};if(typeof e=="string"){let t=JSON.parse(e);return typeof t=="object"&&t!==null?t:{}}return typeof e=="object"?e:{}}function Ra(e,t,r){let s=e==null?t:Number(e);if(!Number.isInteger(s)||s<=0)throw new Error(`crypto.scrypt ${r} must be a positive integer`);return s}function og(e,t){let r=sg(e),s=Number(t);if(!Number.isInteger(s)||s<0)throw new Error("crypto.scrypt key length must be a non-negative integer");let o=Ra(r.cost??r.N,Ym,"cost");if((o&o-1)!==0)throw new Error("crypto.scrypt cost must be a positive power of two");return{N:o,r:Ra(r.blockSize??r.r,Qm,"block size"),p:Ra(r.parallelization??r.p,Zm,"parallelization"),dkLen:s}}var kf={md5:ac,sha1:ic,sha224:yc,sha256:Mn,sha384:_c,sha512:bc};function Ja(e){let t=String(e).trim().toLowerCase().replace(/[-_]/g,"");if(Object.hasOwn(kf,t))return t;throw new Error(`Unsupported browser crypto digest algorithm: ${e}`)}var ig={md5:"3020300c06082a864886f70d020505000410",sha1:"3021300906052b0e03021a05000414",sha224:"302d300d06096086480165030402040500041c",sha256:"3031300d060960864801650304020105000420",sha384:"3041300d060960864801650304020205000430",sha512:"3051300d060960864801650304020305000440"};function kr(e){return kf[Ja(e)]}function Xa(e,t){return kr(e)(Se(t))}function ag(e,t,r){return jr(kr(e),Se(t),Se(r))}function cg(e){let t=String(e).trim().toLowerCase().replace(/^rsa[-_]/,"");return Ja(t)}function lg(e){return Array.from(e,t=>t.toString(16).padStart(2,"0")).join("")}function Ef(e){let t=new Uint8Array(e.length/2);for(let r=0;ro+a.byteLength,0),r=new Uint8Array(t),s=0;for(let o of e)r.set(o,s),s+=o.byteLength;return r}function Ya(e){let t;if(typeof e=="string")try{let s=JSON.parse(e);t=typeof s=="string"?s:e}catch{t=e}else{if(e&&typeof e=="object"&&"key"in e)return Ya(e.key);throw new Error("Browser crypto RSA key must be a PEM string")}let r=t.replace(/-----BEGIN [^-]+-----|-----END [^-]+-----|\s+/g,"");return Ts(r)}var Tn=class e{bytes;offset=0;constructor(t){this.bytes=t}get position(){return this.offset}set position(t){this.offset=t}readTag(t){let r=this.bytes[this.offset++];if(r!==t)throw new Error(`Invalid RSA key DER tag: expected ${t}, got ${r}`);let s=this.readLength(),o=this.offset+s;if(o>this.bytes.byteLength)throw new Error("Invalid RSA key DER length");let a=this.bytes.subarray(this.offset,o);return this.offset=o,a}readSequence(){return new e(this.readTag(48))}readInteger(){let t=this.readTag(2);for(;t.byteLength>1&&t[0]===0;)t=t.subarray(1);return t}readOctetString(){return this.readTag(4)}readBitString(){let t=this.readTag(3);if(t[0]!==0)throw new Error("Unsupported RSA key bit string padding");return t.subarray(1)}skipAny(){if(this.bytes[this.offset++]==null)throw new Error("Invalid RSA key DER");let r=this.readLength();if(this.offset+=r,this.offset>this.bytes.byteLength)throw new Error("Invalid RSA key DER length")}readLength(){let t=this.bytes[this.offset++];if(t==null)throw new Error("Invalid RSA key DER length");if((t&128)===0)return t;let r=t&127;if(r===0||r>4)throw new Error("Unsupported RSA key DER length");let s=0;for(let o=0;ot)throw new Error("RSA value exceeds modulus length");let o=new Uint8Array(t);return o.set(s,t-s.byteLength),o}function On(e,t,r){let s=1n,o=e%r,a=t;for(;a>0n;)(a&1n)===1n&&(s=s*o%r),o=o*o%r,a>>=1n;return s}function ug(e){let t=e.readInteger(),r=e.readInteger();return{modulus:yt(t),exponent:yt(r),modulusLength:t.byteLength}}function Pf(e){let t=Ya(e),r=new Tn(t).readSequence();r.skipAny();let s=new Tn(r.readBitString()).readSequence();return ug(s)}function Af(e){let t=Ya(e),r=new Tn(t).readSequence();r.skipAny(),r.skipAny();let s=new Tn(r.readOctetString()).readSequence();s.skipAny();let o=s.readInteger();s.skipAny();let a=s.readInteger();return{modulus:yt(o),exponent:yt(a),modulusLength:o.byteLength}}function Tf(e,t){let r=cg(e),s=Xa(r,t);return{hashName:r,encoded:An([Ef(ig[r]),s])}}function If(e,t,r){let s=t.byteLength+11;if(r0;){let c=new Uint8Array([o>>>24&255,o>>>16&255,o>>>8&255,o&255]),l=Xa(r,An([e,c]));s.push(l.subarray(0,Math.min(a,l.byteLength))),a-=l.byteLength,o++}return An(s).subarray(0,t)}function Of(e){return e==null?"sha1":Ja(e)}function mg(e,t){let r=e.modulusLength-t.byteLength-3;if(r<8)throw new Error("RSA_PKCS1_PADDING input is too large for key size");let s=An([new Uint8Array([0,2]),hg(r),new Uint8Array([0]),t]),o=On(yt(s),e.exponent,e.modulus);return In(o,e.modulusLength)}function gg(e,t){if(t.byteLength!==e.modulusLength)throw new Error("RSA_PKCS1_PADDING encrypted input has invalid length");let r=In(On(yt(t),e.exponent,e.modulus),e.modulusLength);if(r[0]!==0||r[1]!==2)throw new Error("RSA_PKCS1_PADDING block is invalid");let s=-1;for(let o=2;ol)throw new Error("RSA_PKCS1_OAEP_PADDING input is too large for key size");let u=a(o),d=new Uint8Array(l-t.byteLength),m=An([u,d,new Uint8Array([1]),t]),h=new Uint8Array(c);if(!globalThis.crypto?.getRandomValues)throw new Error("Browser runtime crypto requires getRandomValues support");globalThis.crypto.getRandomValues(h);let b=Rs(h,e.modulusLength-c-1,s),w=Ls(m,b),x=Rs(w,c,s),O=Ls(h,x),k=An([new Uint8Array([0]),O,w]),T=On(yt(k),e.exponent,e.modulus);return In(T,e.modulusLength)}function bg(e,t,r){if(t.byteLength!==e.modulusLength)throw new Error("RSA_PKCS1_OAEP_PADDING encrypted input has invalid length");let s=Of(r.oaepHash),o=Sr(r.oaepLabel),a=kr(s),c=a.outputLen;if(e.modulusLength<2*c+2)throw new Error("RSA key is too small for OAEP");let l=In(On(yt(t),e.exponent,e.modulus),e.modulusLength),u=l.subarray(1,1+c),d=l.subarray(1+c),m=Rs(d,c,s),h=Ls(u,m),b=Rs(h,e.modulusLength-c-1,s),w=Ls(d,b),x=a(o),O=l[0];for(let T=0;Te(...s);return{applySync:t,applySyncPromise:t}}function bf(e){return{apply(t,r){return e(...r)}}}function Eg(e){if(typeof e=="string")return e;if(e&&typeof e=="object"&&"encoding"in e){let t=e.encoding;return typeof t=="string"?t:null}return null}function Pg(e){return typeof Buffer=="function"?Buffer.from(e):e}function _f(e){return{...e,isFile:()=>!e.isDirectory&&!e.isSymbolicLink,isDirectory:()=>e.isDirectory,isSymbolicLink:()=>e.isSymbolicLink}}function Ag(e){return{name:e.name,isFile:()=>!e.isDirectory&&!e.isSymbolicLink,isDirectory:()=>e.isDirectory,isSymbolicLink:()=>!!e.isSymbolicLink}}function Tg(e){let t=new Map,r=1e3,s=(S,I,y)=>{let E=new Error(`${S}: ${I} '${y}'`);E.code=S;let F=Aa(S);return F!==void 0&&(E.errno=F),E.syscall=I,E},o=(S,I)=>{let y=t.get(Number(S));if(!y)throw s("EBADF",I,String(S));return y},a=0,c=1,l=2,u=64,d=128,m=512,h=1024,b={r:a,rs:a,sr:a,"r+":l,"rs+":l,"sr+":l,w:c|u|m,wx:c|u|m|d,xw:c|u|m|d,"w+":l|u|m,"wx+":l|u|m|d,"xw+":l|u|m|d,a:c|u|h,ax:c|u|h|d,xa:c|u|h|d,as:c|u|h,sa:c|u|h,"a+":l|u|h,"ax+":l|u|h|d,"xa+":l|u|h|d,"as+":l|u|h,"sa+":l|u|h},w=S=>{if(typeof S=="number")return S;let I=b[S];if(I===void 0){let y=new Error(`Unknown file open flag: ${S}`);throw y.code="ERR_INVALID_ARG_VALUE",y}return I},x=(S,I,y)=>y<=0?new Uint8Array(0):Se(e.requestBinary("fs.pread",[S,I,y])),O=(S,I)=>Eg(I)?e.requestText("fs.readFile",[S]):Pg(e.requestBinary("fs.readFileBinary",[S])),k=(S,I)=>{if(typeof I=="string"){e.requestVoid("fs.writeFile",[S,I]);return}e.requestVoid("fs.writeFileBinary",[S,Se(I)])},T=(S,I)=>{if(typeof I=="boolean"?I:I?.recursive??!0){e.requestVoid("fs.mkdir",[S]);return}e.requestVoid("fs.createDir",[S])},B=(S,I)=>{let y=e.requestJson("fs.readDir",[S]);return I?.withFileTypes?y.map(E=>Ag(E)):y.map(E=>E.name)},R=S=>_f(e.requestJson("fs.stat",[S])),M=S=>_f(e.requestJson("fs.lstat",[S]));return{readFileSync:O,writeFileSync:k,mkdirSync:T,readdirSync:B,existsSync(S){return e.requestJson("fs.exists",[S])},statSync:R,lstatSync:M,unlinkSync(S){e.requestVoid("fs.unlink",[S])},rmdirSync(S){e.requestVoid("fs.rmdir",[S])},rmSync(S){e.requestVoid("fs.unlink",[S])},renameSync(S,I){e.requestVoid("fs.rename",[S,I])},realpathSync(S){return e.requestText("fs.realpath",[S])},readlinkSync(S){return e.requestText("fs.readlink",[S])},symlinkSync(S,I){e.requestVoid("fs.symlink",[S,I])},linkSync(S,I){e.requestVoid("fs.link",[S,I])},chmodSync(S,I){e.requestVoid("fs.chmod",[S,I])},truncateSync(S,I=0){e.requestVoid("fs.truncate",[S,I])},constants:{O_RDONLY:0,O_WRONLY:1,O_RDWR:2,O_CREAT:64,O_EXCL:128,O_TRUNC:512,O_APPEND:1024,O_DIRECTORY:65536},openSync(S,I=0){let y=w(I);if((y&u)!==0){let X=e.requestJson("fs.exists",[S]);if(X&&(y&d)!==0)throw s("EEXIST","open",S);X||e.requestVoid("fs.writeFile",[S,""])}(y&m)!==0&&e.requestVoid("fs.truncate",[S,0]);let E=y&3;if((E===0||E===2)&&(y&(u|m))===0)try{x(S,0,1)}catch(X){let re=X?.code;if(re==="EACCES"||re==="EPERM")throw X}let U=r++;return t.set(U,{path:S,flags:y}),U},readSync(S,I,y=0,E,F){let U=o(S,"read"),X=typeof E=="number"?E:I.length-y,re=typeof F=="number"&&F>=0?F:0,oe=x(U.path,re,X),Q=Math.min(oe.length,X);for(let pe=0;pe=0?Q=F:(U.flags&h)!==0?Q=R(U.path).size:Q=0,e.requestVoid("fs.pwrite",[U.path,Q,oe]),re},closeSync(S){t.delete(Number(S))},fstatSync(S){return R(o(S,"fstat").path)},fchmodSync(S,I){e.requestVoid("fs.chmod",[o(S,"fchmod").path,Number(I)||0])},ftruncateSync(S,I=0){e.requestVoid("fs.truncate",[o(S,"ftruncate").path,Number(I)||0])},fsyncSync(){},fdatasyncSync(){},promises:{readFile(S,I){return Promise.resolve(O(S,I))},writeFile(S,I){return k(S,I),Promise.resolve()},mkdir(S,I){return T(S,I),Promise.resolve()},readdir(S,I){return Promise.resolve(B(S,I))},stat(S){return Promise.resolve(R(S))},lstat(S){return Promise.resolve(M(S))},unlink(S){return e.requestVoid("fs.unlink",[S]),Promise.resolve()},rmdir(S){return e.requestVoid("fs.rmdir",[S]),Promise.resolve()},rm(S){return e.requestVoid("fs.unlink",[S]),Promise.resolve()},rename(S,I){return e.requestVoid("fs.rename",[S,I]),Promise.resolve()},realpath(S){return Promise.resolve(e.requestText("fs.realpath",[S]))},readlink(S){return Promise.resolve(e.requestText("fs.readlink",[S]))},symlink(S,I){return e.requestVoid("fs.symlink",[S,I]),Promise.resolve()},link(S,I){return e.requestVoid("fs.link",[S,I]),Promise.resolve()},chmod(S,I){return e.requestVoid("fs.chmod",[S,I]),Promise.resolve()},truncate(S,I=0){return e.requestVoid("fs.truncate",[S,I]),Promise.resolve()}}}}var Ns=self.postMessage.bind(self);function vr(e){Ns({controlToken:Fs(),...e})}function wf(e,t){t.then(r=>{vr({type:"response",id:e,ok:!0,result:r})},r=>{let s=r;vr({type:"response",id:e,ok:!1,error:{message:s?.message??String(r),stack:s?.stack,code:s?.code}})})}function Ig(e){Ns({controlToken:Fs(),...e})}function Og(e,t,r,s){let o={controlToken:Fs(),type:"stdio",executionId:e,requestId:t,channel:r,message:s};Ns(o)}function Bg(e,t,r){let s={controlToken:Fs(),type:"pty-opened",executionId:e,requestId:t,...r};Ns(s)}function Rf(e,t,r,s){Og(e,t,r,kg(s))}function xf(e,t){if(!Cs||lt===null||ot===null)return;let r=t.map(s=>Ff(s)).join(" ");Rf(ot,lt,e,r)}function Cg(e){let t=new Int32Array(e.signalBuffer),r=new Uint8Array(e.dataBuffer),s=1,o=e.timeoutMs??3e4;function a(l){return l<=0?new Uint8Array(0):r.slice(0,l)}function c(l,u){if(!ot||lt===null)throw new Error(`Browser runtime sync bridge ${l} called outside an active execution`);for(Atomics.store(t,0,0),Atomics.store(t,1,0),Atomics.store(t,2,0),Atomics.store(t,3,0),Ig({type:"sync-request",requestId:s++,executionId:ot,processRequestId:lt,operation:l,args:u});Atomics.wait(t,0,0,o)==="timed-out";)throw new Error(`Browser runtime sync bridge timed out while handling ${l}`);let d=Atomics.load(t,1),m=Atomics.load(t,2),h=Atomics.load(t,3),b=a(h);if(Atomics.store(t,0,0),d===1){let w=JSON.parse(Ht.decode(b)),x=new Error(w.message);if(w.code){x.code=w.code;let O=Aa(w.code);O!==void 0&&(x.errno=O)}throw x}return{kind:m,bytes:b}}return{requestVoid(l,u){c(l,u)},requestText(l,u){let d=c(l,u);if(d.kind!==1)throw new Error(`Expected text response from ${l}, received kind ${d.kind}`);return Ht.decode(d.bytes)},requestNullableText(l,u){let d=c(l,u);if(d.kind===0)return null;if(d.kind!==1)throw new Error(`Expected text response from ${l}, received kind ${d.kind}`);return Ht.decode(d.bytes)},requestBinary(l,u){let d=c(l,u);if(d.kind!==2)throw new Error(`Expected binary response from ${l}, received kind ${d.kind}`);return d.bytes},requestJson(l,u){let d=c(l,u);if(d.kind!==3)throw new Error(`Expected JSON response from ${l}, received kind ${d.kind}`);return JSON.parse(Ht.decode(d.bytes))}}}async function Lg(e){if(Da)return;if(cf(),Ka(),!e.syncBridge)throw new Error("Browser runtime sync bridge is required for filesystem and module loading parity");let t=Cg(e.syncBridge);Ma=t,Ba=e.payloadLimits?.base64TransferBytes??vf,Os=e.payloadLimits?.jsonPayloadBytes??Sf,e.networkEnabled?Oa=of():Oa=Is();let r=e.processConfig??{};Ye=r,Bs=e.timingMitigation??r.timingMitigation??Bs,r.timingMitigation=Bs,delete r.frozenTimeMs,W("_processConfig",r);let s=e.osConfig??{};W("_osConfig",s),W("__agentOSVirtualOs",s),W("_log",q((...y)=>{xf("stdout",y)})),W("_error",q((...y)=>{xf("stderr",y)}));let o=q(y=>{let E=t.requestText("fs.readFile",[y]);return La(`fs.readFile ${y}`,E,Os),E}),a=q((y,E)=>{La(`fs.writeFile ${y}`,E,Os),t.requestVoid("fs.writeFile",[y,E])}),c=q(y=>{let E=t.requestBinary("fs.readFileBinary",[y]);return Ua(`fs.readFileBinary ${y}`,E.byteLength,Ba),E}),l=q((y,E)=>{Ua(`fs.writeFileBinary ${y}`,E.byteLength,Ba),t.requestVoid("fs.writeFileBinary",[y,E])}),u=q(y=>{let E=JSON.stringify(t.requestJson("fs.readDir",[y]));return La(`fs.readDir ${y}`,E,Os),E}),d=q(y=>{t.requestVoid("fs.mkdir",[y])}),m=q(y=>{t.requestVoid("fs.rmdir",[y])}),h=q(y=>t.requestJson("fs.exists",[y])),b=q(y=>JSON.stringify(t.requestJson("fs.stat",[y]))),w=q(y=>{t.requestVoid("fs.unlink",[y])}),x=q((y,E)=>{t.requestVoid("fs.rename",[y,E])});W("_fs",{readFile:o,writeFile:a,readFileBinary:c,writeFileBinary:l,readDir:u,mkdir:d,rmdir:m,exists:h,stat:b,unlink:w,rename:x}),W("_loadPolyfill",q(y=>nf(y,e.processLimits)));let O=(y,E,F)=>t.requestNullableText("module.resolve",[y,E,F??"require"]),k=(y,E)=>{let F=t.requestNullableText("module.loadFile",[y]);if(F===null)return null;let U=F;return Ea(F,y)&&(U=Sa(U,{transforms:["imports"]}).code),Pa(U)},T=y=>t.requestNullableText("module.format",[y]),B=y=>t.requestJson("module.batchResolve",[y]);W("_resolveModuleSync",q(O)),W("_loadFileSync",q(k)),W("_resolveModule",q(O)),W("_loadFile",q(k)),W("_moduleFormat",q(T)),W("_batchResolveModules",q(B));let R=y=>{if(!Number.isInteger(y)||y<0)throw new Error("crypto random byte length must be a non-negative integer");let E=new Uint8Array(y),F=globalThis.crypto;if(!F?.getRandomValues)throw new Error("Browser runtime crypto requires getRandomValues support");for(let U=0;UR(Number(y)))),W("_cryptoRandomUUID",q(()=>{if(typeof globalThis.crypto?.randomUUID!="function")throw new Error("Browser runtime crypto requires randomUUID support");return globalThis.crypto.randomUUID()})),W("_cryptoHashDigest",q((y,E)=>Xa(y,E))),W("_cryptoHmacDigest",q((y,E,F)=>ag(y,E,F))),W("_cryptoPbkdf2",q((y,E,F,U,X)=>wg(y,E,F,U,X))),W("_cryptoScrypt",q((y,E,F,U)=>xc(Se(y),Se(E),og(U,F)))),W("_cryptoCipheriv",q((y,E,F,U,X)=>xg(y,E,F,U,X))),W("_cryptoDecipheriv",q((y,E,F,U,X)=>vg(y,E,F,U,X))),W("_cryptoCipherivCreate",q(()=>Xe("_cryptoCipherivCreate"))),W("_cryptoCipherivUpdate",q(()=>Xe("_cryptoCipherivUpdate"))),W("_cryptoCipherivFinal",q(()=>Xe("_cryptoCipherivFinal"))),W("_cryptoSign",q((y,E,F)=>fg(y,E,F))),W("_cryptoVerify",q((y,E,F,U)=>dg(y,E,F,U))),W("_cryptoAsymmetricOp",q((y,E,F,U)=>_g(y,E,F,U))),W("_cryptoCreateKeyObject",q(()=>Xe("_cryptoCreateKeyObject"))),W("_cryptoGenerateKeyPairSync",q(()=>Xe("_cryptoGenerateKeyPairSync"))),W("_cryptoGenerateKeySync",q(()=>Xe("_cryptoGenerateKeySync"))),W("_cryptoGeneratePrimeSync",q(()=>Xe("_cryptoGeneratePrimeSync"))),W("_cryptoDiffieHellman",q(()=>Xe("_cryptoDiffieHellman"))),W("_cryptoDiffieHellmanGroup",q(()=>Xe("_cryptoDiffieHellmanGroup"))),W("_cryptoDiffieHellmanSessionCreate",q(()=>Xe("_cryptoDiffieHellmanSessionCreate"))),W("_cryptoDiffieHellmanSessionCall",q(()=>Xe("_cryptoDiffieHellmanSessionCall"))),W("_cryptoDiffieHellmanSessionDestroy",q(()=>Xe("_cryptoDiffieHellmanSessionDestroy"))),W("_cryptoSubtle",q(()=>Xe("_cryptoSubtle"))),W("_scheduleTimer",{apply(y,E){return new Promise(F=>{setTimeout(F,E[0])})}});let M=Oa??Is(),$=(y,E)=>t.requestJson("network.fetch",[y,E]);W("_networkFetchRaw",bf(async(y,E)=>{let F=JSON.parse(E),U=$(y,F);return JSON.stringify(U)})),W("_networkDnsLookupRaw",bf(async y=>{let E=typeof y=="string"?y:String(y.hostname??""),F=await M.dnsLookup(E);if(F.error){let U=new Error(F.error);throw U.code=F.code,U}return JSON.stringify(F)})),W("fetch",async(y,E)=>{let F=y??{},U=typeof y=="string"?y:String(F.url??y),X=String(E?.method??F.method??"GET"),re={},oe=E?.headers??F.headers;if(oe)if(Array.isArray(oe))for(let he of oe)re[he[0]]=he[1];else if(typeof oe.forEach=="function")oe.forEach((he,ke)=>{re[ke]=he});else for(let he of Object.keys(oe))re[he]=String(oe[he]);let Q=E?.body??F.body;Q!=null&&typeof Q!="string"&&(Q=Q instanceof Uint8Array?new TextDecoder().decode(Q):String(Q));let pe=$(U,{method:X,headers:re,body:Q??null});return new Response(pe.body??"",{status:pe.status??200,statusText:pe.statusText||"",headers:pe.headers??{}})}),W("global",globalThis),typeof globalThis.setImmediate!="function"&&(W("setImmediate",(y,...E)=>setTimeout(()=>y(...E),0)),W("clearImmediate",y=>clearTimeout(y))),W("_childProcessSpawnStart",q(y=>t.requestJson("child_process.spawn",[y]))),W("_childProcessPoll",q((y,E)=>t.requestJson("child_process.poll",[y,E??0]))),W("_childProcessStdinWrite",q((y,E)=>{t.requestVoid("child_process.write_stdin",[y,E])})),W("_childProcessStdinClose",q(y=>{t.requestVoid("child_process.close_stdin",[y])})),W("_childProcessKill",q((y,E)=>t.requestJson("child_process.kill",[y,E]))),W("_childProcessSpawnSync",q(y=>t.requestText("child_process.spawn_sync",[y]))),W("_processSignalState",q((y,E,F,U)=>{t.requestVoid("process.signal_state",[y,E,F,U])})),W("_dgramSocketCreateRaw",q(y=>t.requestJson("dgram.create",[y]))),W("_dgramSocketBindRaw",q((y,E)=>t.requestJson("dgram.bind",[y,E]))),W("_dgramSocketRecvRaw",q((y,E)=>t.requestJson("dgram.recv",[y,E??0]))),W("_dgramSocketSendRaw",q((y,E,F)=>t.requestJson("dgram.send",[y,E,F]))),W("_dgramSocketCloseRaw",q(y=>t.requestJson("dgram.close",[y]))),W("_dgramSocketAddressRaw",q(y=>t.requestJson("dgram.address",[y]))),W("_dgramSocketSetBufferSizeRaw",q((y,E,F)=>t.requestJson("dgram.setBufferSize",[y,E,F]))),W("_dgramSocketGetBufferSizeRaw",q((y,E)=>t.requestJson("dgram.getBufferSize",[y,E]))),W("_fsModule",Tg(t)),je("_moduleCache",{}),je("_pendingModules",{}),je("_currentModule",{dirname:"/"}),Ga(sf()),jg();let S=["XMLHttpRequest","WebSocket","importScripts","indexedDB","caches","BroadcastChannel"];for(let y of S){try{delete self[y]}catch{}Object.defineProperty(self,y,{get(){throw new ReferenceError(`${y} is not available in sandbox`)},configurable:!1})}let I=self.onmessage;Object.defineProperty(self,"onmessage",{value:I,writable:!1,configurable:!1}),Object.defineProperty(self,"postMessage",{get(){throw new TypeError("postMessage is not available in sandbox")},configurable:!1}),Da=!0}function Rg(e){je("_moduleCache",{}),je("_pendingModules",{}),je("_currentModule",{dirname:e})}function Fg(){je("__dynamicImport",e=>{let t=Km.get(e);if(t)return Promise.resolve(t);try{let r=globalThis.require;if(typeof r!="function")throw new Error("require is not available in browser runtime");let s=r(e);return Promise.resolve({default:s,...s})}catch(r){return Promise.reject(new Error(`Cannot dynamically import '${e}': ${String(r)}`))}})}function Na(e,t){return t?e:za.encode(e)}function Ff(e){return typeof e=="string"?e:e instanceof Uint8Array?Ht.decode(e):ArrayBuffer.isView(e)?Ht.decode(new Uint8Array(e.buffer,e.byteOffset,e.byteLength)):e instanceof ArrayBuffer?Ht.decode(new Uint8Array(e)):String(e)}function Ng(e,t){return lt===null||ot===null||Rf(ot,lt,e,Ff(t)),!0}function Dg(){let e="/",t="",r=0,s=!1,o=!1,a=null,c=0,l=!1,u=Object.create(null),d=Object.create(null),m={isatty(_){return a!==null&&(typeof _=="number"||typeof _=="string")&&(_===0||_===1||_===2||_==="0"||_==="1"||_==="2")},columns(){return a?.columns??80},rows(){return a?.rows??24}};globalThis.__agentOSTtyState=m;let h=_=>{if(!_||typeof _!="object")return null;let L=_,G,Z=L.slaveFd,ie=Number.isInteger(L.columns)?L.columns:80,be=Number.isInteger(L.rows)?L.rows:24,bt;if(L.open===!0){let Ze=x().requestJson("pty.open",[{}]);if(!Number.isInteger(Ze.masterFd)||!Number.isInteger(Ze.slaveFd))throw new Error("pty.open returned invalid fd pair");G=Ze.masterFd,Z=Ze.slaveFd,bt=typeof Ze.path=="string"?Ze.path:void 0,x().requestVoid("pty.resize",[{fd:Z,cols:Math.max(1,ie),rows:Math.max(1,be)}]),ot&<!==null&&Bg(ot,lt,{masterFd:G,slaveFd:Z,path:bt,columns:Math.max(1,ie),rows:Math.max(1,be)})}return!Number.isInteger(Z)||Z<0?null:{masterFd:G,slaveFd:Z,path:bt,columns:Math.max(1,ie),rows:Math.max(1,be)}},b=(_,L)=>{let G=[...u[_]??[],...d[_]??[]];d[_]=[];for(let Z of G)Z(L);return G.length>0},w=()=>{for(let _ of Object.keys(u))u[_]=[];for(let _ of Object.keys(d))d[_]=[]},x=()=>{let _=Ma;if(!_)throw new Error("PTY stdio requires an active sync bridge");return _},O=_=>I.encoding?Ht.decode(_):_,k=()=>{if(!a||s||I.paused)return!1;let _=x().requestJson("pty.read",[{fd:a.slaveFd,maxBytes:4096,timeoutMs:0}]);return typeof _.data!="string"?!1:(b("data",O(Ts(_.data))),!0)},T=()=>{if(!a||s||I.paused||l)return;let _=c;l=!0,setTimeout(()=>{if(l=!1,!(_!==c||!a||s||I.paused))try{k()}finally{T()}},0)},B=(_,L)=>{if(!a)return!1;let G=Se(_);return x().requestJson("pty.write",[{fd:a.slaveFd,data:G}]),!0},R=_=>{a&&x().requestVoid("pty.tcsetattr",[_?{fd:a.slaveFd,icrnl:!1,opost:!1,icanon:!1,echo:!1,isig:!1}:{fd:a.slaveFd,icrnl:!0,opost:!0,icanon:!0,echo:!0,isig:!0}])},M=()=>{c+=1,l=!1},$=()=>{if(o=!1,!(I.paused||s)){if(a){k(),T();return}if(r{s||a||(I.paused&&(I.paused=!1),b("data",Na(_,I.encoding)))},$a=()=>{s||a||(s=!0,b("end"),b("close"))};let S=()=>{o||(o=!0,queueMicrotask($))},I={readable:!0,paused:!0,encoding:null,isRaw:!1,read(_){if(a){let G=x().requestJson("pty.read",[{fd:a.slaveFd,maxBytes:_??4096,timeoutMs:0}]);return typeof G.data=="string"?O(Ts(G.data)):null}if(r>=t.length)return null;let L=_?t.slice(r,r+_):t.slice(r);return r+=L.length,Na(L,I.encoding)},on(_,L){return u[_]||(u[_]=[]),u[_].push(L),_==="data"&&I.paused&&I.resume(),I},once(_,L){return d[_]||(d[_]=[]),d[_].push(L),_==="data"&&I.paused&&I.resume(),I},off(_,L){return u[_]&&(u[_]=u[_].filter(G=>G!==L)),I},removeListener(_,L){return I.off(_,L)},emit(_,L){return b(_,L)},pause(){return I.paused=!0,I},resume(){return I.paused=!1,a?T():S(),I},setEncoding(_){return I.encoding=_,I},setRawMode(_){return R(_),I.isRaw=_,I},get readableLength(){return a?0:za.encode(t.slice(r)).byteLength},get isTTY(){return a!==null},async*[Symbol.asyncIterator](){let _=t.slice(r);for(let L of _.split(` +`))L.length>0&&(yield L)}},y=Object.create(null),E=Object.create(null),F=Object.create(null),U=Object.create(null),X=Object.create(null),re=Object.create(null),oe=_=>{if(typeof _!="function")throw new TypeError("process listener must be a function");return _},Q=_=>(y[_]?.length??0)+(E[_]?.length??0),pe=(_,L)=>{let G=Qu(_);if(G===null)return;let Z=Ma;Z&&Z.requestVoid("process.signal_state",[G,L,"[]",0])},he=(_,L,G)=>{L===0&&G>0?pe(_,"user"):L>0&&G===0&&pe(_,"default")},ke=(_,L,G)=>{oe(L);let Z=Q(_),ie=G?E:y;return ie[_]||(ie[_]=[]),ie[_].push(L),he(_,Z,Q(_)),le},Oe=(_,L)=>{oe(L);let G=Q(_);return y[_]&&(y[_]=y[_].filter(Z=>Z!==L)),E[_]&&(E[_]=E[_].filter(Z=>Z!==L)),he(_,G,Q(_)),le},Pe=(_,L)=>{let G=Q(_),Z=[...y[_]??[],...E[_]??[]];E[_]=[];for(let ie of Z)ie(L);return he(_,G,Q(_)),Z.length>0},Be=_=>{if(typeof _!="function")throw new TypeError("stream listener must be a function");return _},Qe=(_,L,G,Z)=>(Be(G),_[L]||(_[L]=[]),_[L].push(G),Z),ut=(_,L,G,Z)=>(Be(G),_[L]&&(_[L]=_[L].filter(ie=>ie!==G)),Z),It=(_,L,G,Z)=>{let ie=[..._[G]??[],...L[G]??[]];L[G]=[];for(let be of ie)be(Z);return ie.length>0},Ot=(_,L)=>{for(let G of Object.keys(_))_[G]=[];for(let G of Object.keys(L))L[G]=[]},tn=(_,L,G)=>{let Z={get isTTY(){return a!==null},get columns(){return a?.columns??80},get rows(){return a?.rows??24},write(ie,be,bt){let Ze=a?B(ie,be):Ng(_,ie),nn=typeof be=="function"?be:bt;return typeof nn=="function"&&nn(),Ze},on(ie,be){return Qe(L,String(ie),be,Z)},once(ie,be){return Qe(G,String(ie),be,Z)},off(ie,be){return ut(L,String(ie),be,Z)},removeListener(ie,be){return ut(L,String(ie),be,Z),ut(G,String(ie),be,Z),Z},emit(ie,be){return It(L,G,String(ie),be)}};return Z},$t=tn("stdout",F,U),zt=tn("stderr",X,re),le={browser:!0,env:{},argv:["node"],argv0:"node",pid:1,ppid:0,uid:1e3,gid:1e3,platform:"browser",arch:"x64",version:"v22.0.0",versions:{node:"22.0.0",openssl:"3.6.2"},stdin:I,stdout:$t,stderr:zt,exitCode:0,cwd:()=>e,chdir:_=>{e=String(_)},getuid:()=>le.uid,getgid:()=>le.gid,geteuid:()=>le.uid,getegid:()=>le.gid,getgroups:()=>[le.gid],nextTick:(_,...L)=>{queueMicrotask(()=>_(...L))},exit(_){let L=typeof _=="number"?_:le.exitCode??0;if(le.exitCode=L,Pn){let G=Pn;Pn=null,G(L);return}throw new Error(`process.exit(${L})`)},kill(_,L="SIGTERM"){if(_!==le.pid)throw new Error(`process.kill only supports the current browser process pid (${le.pid})`);let G=typeof L=="number"?Xm(L):String(L);return G==="SIGWINCH"&&($t.emit("resize"),zt.emit("resize")),Pe(G)},on(_,L){return ke(String(_),L,!1)},once(_,L){return ke(String(_),L,!0)},off(_,L){return Oe(String(_),L)},removeListener(_,L){return Oe(String(_),L)},emit(_,L){return Pe(String(_),L)},__secureExecRefreshProcess(_){M(),w();for(let L of Object.keys(y))y[L]=[];for(let L of Object.keys(E))E[L]=[];Ot(F,U),Ot(X,re),t=typeof _?.stdin=="string"?_.stdin:"",r=0,s=!1,o=!1,I.paused=!0,I.encoding=null,I.isRaw=!1,a=h(_?.stdioPty),le.exitCode=0,le.env=_?.env&&typeof _.env=="object"?{..._.env}:{},typeof _?.cwd=="string"&&(e=_.cwd),le.argv=Array.isArray(_?.argv)?_.argv.map(L=>String(L)):["node"],le.argv0=le.argv[0]??"node",typeof _?.platform=="string"&&(le.platform=_.platform),typeof _?.arch=="string"&&(le.arch=_.arch),typeof _?.version=="string"&&(le.version=_.version,le.versions.node=_.version.replace(/^v/,"")),typeof _?.pid=="number"&&(le.pid=_.pid),typeof _?.ppid=="number"&&(le.ppid=_.ppid),typeof _?.uid=="number"&&(le.uid=_.uid),typeof _?.gid=="number"&&(le.gid=_.gid)},__secureExecStopPtyStdio(){M(),a=null},__secureExecResizePty(_,L){a&&(a.columns=Math.max(1,Math.trunc(_)),a.rows=Math.max(1,Math.trunc(L)),$t.emit("resize"),zt.emit("resize"),Pe("SIGWINCH"))}};return le}function Er(){let e=globalThis.process;if(!(!e||typeof e!="object"))return e}function qa(){let t=Er()?.__secureExecRefreshProcess;typeof t=="function"&&t(Ye)}function Mg(e,t,r){if(e!==ot)return;let o=Er()?.__secureExecResizePty;typeof o=="function"&&o(t,r)}function jg(){if(Er()){qa();return}je("process",Dg()),qa()}function Wg(e,t,r){if(Ye&&(Ye.timingMitigation=t,r===void 0?delete Ye.frozenTimeMs:Ye.frozenTimeMs=r,Ye.stdin=e?.stdin??"",e?.stdioPty?Ye.stdioPty=e.stdioPty:delete Ye.stdioPty,e?.env)){let o=Ye.env&&typeof Ye.env=="object"?Ye.env:{};Ye.env={...o,...e.env}}qa();let s=Er();if(s&&(s.exitCode=0,s.timingMitigation=t,r===void 0?delete s.frozenTimeMs:s.frozenTimeMs=r,e?.cwd&&typeof s.chdir=="function")){je("__runtimeProcessCwdOverride",e.cwd),Ga(rf("overrideProcessCwd"));try{s.chdir(e.cwd)}catch(o){if(!(o instanceof Error&&o.message.includes("process.chdir() is not supported in workers")))throw o}}}async function Nf(e,t,r,s,o=!1){Rg(s?.cwd??"/");let a=s?.timingMitigation??Bs,c=rg(a),l=lt,u=ot,d=Cs;lt=t,ot=e,Cs=o,Pn=null,Wa=!!s?.streamingStdin,Wg(s,a,c),Fg();try{let m=(async()=>{let b=r;Ea(r,s?.filePath)&&(b=Sa(b,{transforms:["imports"]}).code),b=Pa(b),je("module",{exports:{}});let w=globalThis.module;if(je("exports",w.exports),s?.filePath){let B=s.filePath.includes("/")&&s.filePath.substring(0,s.filePath.lastIndexOf("/"))||"/";je("__filename",s.filePath),je("__dirname",B),je("_currentModule",{dirname:B,filename:s.filePath})}let x=s?.persistent?new Promise(B=>{Pn=B}):null,O=Ga(b);O&&typeof O=="object"&&typeof O.then=="function"&&await O,await Promise.resolve();let k=()=>globalThis.process?.exitCode??0;if(x){let B=await Promise.race([x,new Promise(R=>setTimeout(()=>{Pn=null,R(k())},Jm))]);return await new Promise(R=>setTimeout(R,0)),await new Promise(R=>setTimeout(R,0)),{code:B}}let T=globalThis._waitForActiveHandles;return typeof T=="function"&&await T(),{code:k()}})(),h=new Promise(b=>{ja.set(e,w=>{let x=Zu(w);b({code:x??0})})});return await Promise.race([m,h])}catch(m){let h=m instanceof Error?m.message:String(m),b=h.match(/process\.exit\((\d+)\)/);if(b)return{code:Number.parseInt(b[1],10)};let w=m instanceof Error&&m.stack?m.stack:h;return{code:1,errorMessage:Sg(w)}}finally{Er()?.__secureExecStopPtyStdio?.(),ja.delete(e),lt=l,ot=u,Cs=d,Wa=!1,Ha=null,$a=null}}async function Ug(e,t,r,s,o=!1){let a=await Nf(e,t,r,{filePath:s},o),c=globalThis.module;return{...a,exports:c?.exports}}self.onmessage=async e=>{let t=e.data;try{if(t.type==="init"){if(typeof t.controlToken!="string"||t.controlToken.length===0||en&&t.controlToken!==en)return;en=t.controlToken,await Lg(t.payload),vr({type:"response",id:t.id,ok:!0,result:!0});return}if(!en||t.controlToken!==en)return;if(!Da)throw new Error("Sandbox worker not initialized");if(t.type==="exec"){wf(t.id,Nf(t.payload.executionId,t.id,t.payload.code,t.payload.options,t.payload.captureStdio));return}if(t.type==="write-stdin"){Ha?.(t.data);return}if(t.type==="end-stdin"){$a?.();return}if(t.type==="resize-pty"){Mg(t.executionId,t.columns,t.rows);return}if(t.type==="run"){wf(t.id,Ug(t.payload.executionId,t.id,t.payload.code,t.payload.filePath,t.payload.captureStdio));return}if(t.type==="signal"){let r=Number(t.payload.signal),s=ja.get(t.payload.executionId);s&&Number.isInteger(r)&&s(r);return}if(t.type==="extension"){let r=new Error(`Browser worker extension dispatch is not implemented for namespace ${t.payload.namespace}`);throw r.code="ERR_SECURE_EXEC_BROWSER_EXTENSION_UNSUPPORTED",r}t.type==="dispose"&&(vr({type:"response",id:t.id,ok:!0,result:!0}),close())}catch(r){let s=r;vr({type:"response",id:t.id,ok:!1,error:{message:s?.message??String(r),stack:s?.stack,code:s?.code}})}}; diff --git a/packages/runtime-benchmarks/README.md b/packages/runtime-benchmarks/README.md index f5fd1a1958..0926750381 100644 --- a/packages/runtime-benchmarks/README.md +++ b/packages/runtime-benchmarks/README.md @@ -165,7 +165,7 @@ Focused lanes live under `src/focused/` and preserve the legacy CLI flags, env v - **`wasi-ls-scaling`**: focused `ls` command scaling. Knobs: `BENCH_WASI_LS_ITERATIONS`, `BENCH_WASI_LS_WARMUP`, `BENCH_WASI_LS_SERIAL_RUNS`, `BENCH_WASI_LS_FILE_COUNTS`, `BENCH_WASI_LS_VARIANTS`, `BENCH_WASI_LS_WASM_WARMUP_DEBUG`, `BENCH_WASI_LS_SYSCALL_COUNTERS`. - **`wasi-ls-scaling-counters`**: `ls` scaling with syscall counters. -The shell/coreutils focused lanes use the local `NodeRuntime` command-dir resolution, which prefers `registry/native/target/wasm32-wasip1/release/commands` when `make -C registry/native wasm` has been run. +The shell/coreutils focused lanes use the local `NodeRuntime` command-dir resolution, which prefers `toolchain/target/wasm32-wasip1/release/commands` when `make -C toolchain wasm` has been run. ## Net Family diff --git a/packages/runtime-benchmarks/src/focused/wasi-ls-scaling.bench.ts b/packages/runtime-benchmarks/src/focused/wasi-ls-scaling.bench.ts index eb60b67c79..8de65f2092 100644 --- a/packages/runtime-benchmarks/src/focused/wasi-ls-scaling.bench.ts +++ b/packages/runtime-benchmarks/src/focused/wasi-ls-scaling.bench.ts @@ -273,7 +273,7 @@ function resolveAgentosRoot(): string | null { join(process.cwd(), "../agentos"), ].filter((path): path is string => Boolean(path)); for (const candidate of candidates) { - if (existsSync(join(candidate, "registry/software/coreutils/wasm/ls"))) { + if (existsSync(join(candidate, "software/coreutils/wasm/ls"))) { return candidate; } } @@ -282,7 +282,7 @@ function resolveAgentosRoot(): string | null { function lsModuleBytes(agentosRoot: string | null): number | null { if (!agentosRoot) return null; - const modulePath = join(agentosRoot, "registry/software/coreutils/wasm/ls"); + const modulePath = join(agentosRoot, "software/coreutils/wasm/ls"); return existsSync(modulePath) ? statSync(modulePath).size : null; } diff --git a/packages/runtime-benchmarks/src/focused/wasm-command-floor.bench.ts b/packages/runtime-benchmarks/src/focused/wasm-command-floor.bench.ts index f1ae9cfe36..b12de9f275 100644 --- a/packages/runtime-benchmarks/src/focused/wasm-command-floor.bench.ts +++ b/packages/runtime-benchmarks/src/focused/wasm-command-floor.bench.ts @@ -92,7 +92,7 @@ function resolveAgentosRoot(): string | null { ].filter((path): path is string => Boolean(path)); for (const candidate of candidates) { - if (existsSync(join(candidate, "registry/software/coreutils/wasm"))) { + if (existsSync(join(candidate, "software/coreutils/wasm"))) { return candidate; } } @@ -103,7 +103,7 @@ function commandModuleBytes(command: string, agentosRoot: string | null): number if (!agentosRoot) { return null; } - const modulePath = join(agentosRoot, "registry/software/coreutils/wasm", command); + const modulePath = join(agentosRoot, "software/coreutils/wasm", command); return existsSync(modulePath) ? statSync(modulePath).size : null; } diff --git a/packages/runtime-browser/AGENTS.md b/packages/runtime-browser/AGENTS.md deleted file mode 120000 index 681311eb9c..0000000000 --- a/packages/runtime-browser/AGENTS.md +++ /dev/null @@ -1 +0,0 @@ -CLAUDE.md \ No newline at end of file diff --git a/packages/runtime-browser/AGENTS.md b/packages/runtime-browser/AGENTS.md new file mode 100644 index 0000000000..22ea843adf --- /dev/null +++ b/packages/runtime-browser/AGENTS.md @@ -0,0 +1,5 @@ +# Browser Support + +- Browser support is untested after the secure-exec split; only build-level validation is required during migration. +- Provenance: moved from rivet-dev/agentos@87ed8e21e454. +- Keep this package generic to secure-exec. Agent OS browser glue belongs in Agent OS. diff --git a/packages/runtime-browser/README.md b/packages/runtime-browser/README.md index 8d1b60c68a..2a44d08159 100644 --- a/packages/runtime-browser/README.md +++ b/packages/runtime-browser/README.md @@ -4,3 +4,11 @@ Browser driver primitives for secure-exec. - Package: `@rivet-dev/agentos-runtime-browser` - Exports: `createBrowserDriver`, `createBrowserRuntimeDriverFactory`, `createOpfsFileSystem`, `BrowserWorkerAdapter` + +The browser WASI command host does not implement the native runtime's +`host_net` socket transport. Commands importing `host_net` (including the +registry SSH, Git, and curl artifacts) may be projected but are rejected when +that process image is actually spawned or executed, with +`ERR_AGENTOS_BROWSER_WASM_NETWORK_UNSUPPORTED`; run those commands in the +native runtime. Unused native-only commands do not prevent a browser VM from +starting. diff --git a/packages/runtime-browser/scripts/check-bridge-contract.mjs b/packages/runtime-browser/scripts/check-bridge-contract.mjs index c1524001a5..731a5b859d 100644 --- a/packages/runtime-browser/scripts/check-bridge-contract.mjs +++ b/packages/runtime-browser/scripts/check-bridge-contract.mjs @@ -132,10 +132,13 @@ const browserUnsupportedContractGlobals = new Set([ "_kernelStdinRead", "_kernelStdinReadRaw", "_kernelStdioWriteRaw", + "_netBindConnectedUnixRaw", + "_netBindUnixRaw", "_netReserveTcpPortRaw", "_netReleaseTcpPortRaw", "_netServerAcceptRaw", "_netServerCloseRaw", + "_netServerCloseSyncRaw", "_netServerListenRaw", "_netSocketConnectRaw", "_netSocketDestroyRaw", @@ -148,6 +151,7 @@ const browserUnsupportedContractGlobals = new Set([ "_netSocketTlsQueryRaw", "_netSocketUpgradeTlsRaw", "_netSocketWaitConnectRaw", + "_netSocketWaitConnectSyncRaw", "_netSocketWriteRaw", "_networkDnsLookupSyncRaw", "_networkDnsResolveRaw", @@ -179,6 +183,10 @@ const browserUnsupportedContractGlobals = new Set([ "_networkHttpServerRespondRaw", "_networkHttpServerWaitRaw", "_processKill", + "_processExec", + "_processExecFdImageCommit", + "_processTakeSignal", + "_processWasmSyncRpc", "_ptySetRawMode", "_pythonRpc", "_pythonStdinRead", diff --git a/packages/runtime-browser/src/child-process-bridge.ts b/packages/runtime-browser/src/child-process-bridge.ts index e92be3af6f..45618325a6 100644 --- a/packages/runtime-browser/src/child-process-bridge.ts +++ b/packages/runtime-browser/src/child-process-bridge.ts @@ -6,6 +6,7 @@ export type BrowserChildProcessBytes = { }; export type BrowserChildProcessSpawnOptions = { + argv0?: string; cwd?: string; env?: Record; input?: BrowserChildProcessBytes | string | Uint8Array; @@ -19,7 +20,11 @@ export type BrowserChildProcessSpawnRequest = { export type BrowserChildProcessPollEvent = | { type: "stdout" | "stderr"; data: BrowserChildProcessBytes } - | { type: "exit"; exitCode: number; signal: null }; + | { + type: "exit"; + exitCode: number | null; + signal: number | string | null; + }; export function encodeChildProcessBytes( data: Uint8Array, @@ -81,7 +86,12 @@ export function parseChildProcessSpawnRequest( command: record.command, args: record.args.map((entry) => String(entry)), options: { - cwd: typeof optionsRecord.cwd === "string" ? optionsRecord.cwd : undefined, + argv0: + typeof optionsRecord.argv0 === "string" + ? optionsRecord.argv0 + : undefined, + cwd: + typeof optionsRecord.cwd === "string" ? optionsRecord.cwd : undefined, env, input: optionsRecord.input as BrowserChildProcessSpawnOptions["input"], }, diff --git a/packages/runtime-browser/src/runtime-driver.ts b/packages/runtime-browser/src/runtime-driver.ts index 1b0246b159..21a3db4cd9 100644 --- a/packages/runtime-browser/src/runtime-driver.ts +++ b/packages/runtime-browser/src/runtime-driver.ts @@ -110,6 +110,19 @@ type BrowserChildProcessSession = { exited: boolean; }; +function normalizeCommandWaitResult( + result: Awaited>, +): Extract { + if (typeof result === "number") { + return { type: "exit", exitCode: result, signal: null }; + } + return { + type: "exit", + exitCode: result.exitCode, + signal: result.signal, + }; +} + type BrowserNetworkPermission = NonNullable< RuntimeDriverOptions["system"]["permissions"] >["network"]; @@ -310,6 +323,7 @@ async function handleSyncBridgeOperation( const sessionId = host.allocateChildProcessSessionId(); const events: BrowserChildProcessPollEvent[] = []; const process = host.commandExecutor.spawn(command, args, { + argv0: options.argv0, cwd: options.cwd, env: options.env, onStdout: (data) => { @@ -333,12 +347,18 @@ async function handleSyncBridgeOperation( }; void process .wait() - .then((code) => { + .then((result) => { session.exited = true; - session.events.push({ type: "exit", exitCode: code, signal: null }); + session.events.push(normalizeCommandWaitResult(result)); }) - .catch(() => { + .catch((error) => { session.exited = true; + const message = + error instanceof Error ? error.message : String(error); + session.events.push({ + type: "stderr", + data: encodeChildProcessBytes(new TextEncoder().encode(message)), + }); session.events.push({ type: "exit", exitCode: 1, signal: null }); }); host.childProcessSessions.set(sessionId, session); @@ -382,8 +402,11 @@ async function handleSyncBridgeOperation( if (!session || session.executionId !== message.executionId) { throw new Error(`unknown child_process session ${sessionId}`); } - session.process.kill(signal); - return { kind: SYNC_BRIDGE_KIND_NONE }; + const accepted = session.process.kill(signal); + return { + kind: SYNC_BRIDGE_KIND_JSON, + value: accepted !== false, + }; } case "child_process.spawn_sync": { const request = parseChildProcessSpawnRequest( @@ -395,6 +418,7 @@ async function handleSyncBridgeOperation( const stdoutChunks: Uint8Array[] = []; const stderrChunks: Uint8Array[] = []; const proc = host.commandExecutor.spawn(command, args, { + argv0: options.argv0, cwd: options.cwd, env: options.env, onStdout: (data) => stdoutChunks.push(data), @@ -405,14 +429,15 @@ async function handleSyncBridgeOperation( proc.writeStdin(input); } proc.closeStdin(); - const exitCode = await proc.wait(); + const waitResult = normalizeCommandWaitResult(await proc.wait()); const decoder = new TextDecoder(); return { kind: SYNC_BRIDGE_KIND_TEXT, value: JSON.stringify({ stdout: stdoutChunks.map((chunk) => decoder.decode(chunk)).join(""), stderr: stderrChunks.map((chunk) => decoder.decode(chunk)).join(""), - code: exitCode, + code: waitResult.exitCode, + signal: waitResult.signal, }), }; } @@ -511,6 +536,9 @@ export class BrowserRuntimeDriver implements NodeRuntimeDriver { options.runtime.process, options.system.permissions, ), + // The converged sidecar VM config is trusted host input and the source of + // truth for policy. Never derive these caps from guest bootstrap options. + processLimits: factoryOptions.convergedSidecar?.config.limits?.process, osConfig: options.runtime.os, filesystem: browserSystemOptions.filesystem, networkEnabled: browserSystemOptions.networkEnabled, @@ -980,7 +1008,9 @@ export class BrowserRuntimeDriver implements NodeRuntimeDriver { operation, args, async () => { - throw new Error(`legacy PTY fallback is not available for ${operation}`); + throw new Error( + `legacy PTY fallback is not available for ${operation}`, + ); }, ); } @@ -994,7 +1024,9 @@ export class BrowserRuntimeDriver implements NodeRuntimeDriver { { fd, data: toUint8Array(data) }, ]); if (response.kind !== SYNC_BRIDGE_KIND_JSON) { - throw new Error(`Expected JSON response from pty.write, received ${response.kind}`); + throw new Error( + `Expected JSON response from pty.write, received ${response.kind}`, + ); } return Number((response.value as { written?: unknown }).written ?? 0); } @@ -1012,7 +1044,9 @@ export class BrowserRuntimeDriver implements NodeRuntimeDriver { }, ]); if (response.kind !== SYNC_BRIDGE_KIND_JSON) { - throw new Error(`Expected JSON response from pty.read, received ${response.kind}`); + throw new Error( + `Expected JSON response from pty.read, received ${response.kind}`, + ); } const data = (response.value as { data?: unknown }).data; return typeof data === "string" ? base64ToBytes(data) : null; diff --git a/packages/runtime-browser/src/runtime.ts b/packages/runtime-browser/src/runtime.ts index 33738bd364..105bb733f9 100644 --- a/packages/runtime-browser/src/runtime.ts +++ b/packages/runtime-browser/src/runtime.ts @@ -1,13 +1,13 @@ +import { guestEncodingBootstrapCode } from "./encoding.js"; +import { BROWSER_BUFFER_POLYFILL_CODE } from "./generated/buffer-polyfill.js"; +import { BROWSER_PATH_POLYFILL_CODE } from "./generated/path-polyfill.js"; +import { BROWSER_UTIL_POLYFILL_CODE } from "./generated/util-polyfill.js"; import { createInMemoryFileSystem, InMemoryFileSystem, } from "./os-filesystem.js"; -import { guestEncodingBootstrapCode } from "./encoding.js"; -import { BROWSER_WASI_POLYFILL_CODE } from "./wasi-polyfill.js"; import { PROCESS_SIGNAL_NUMBERS } from "./signals.js"; -import { BROWSER_BUFFER_POLYFILL_CODE } from "./generated/buffer-polyfill.js"; -import { BROWSER_PATH_POLYFILL_CODE } from "./generated/path-polyfill.js"; -import { BROWSER_UTIL_POLYFILL_CODE } from "./generated/util-polyfill.js"; +import { BROWSER_WASI_POLYFILL_CODE } from "./wasi-polyfill.js"; export type StdioChannel = "stdout" | "stderr"; export type TimingMitigation = "off" | "freeze"; @@ -78,6 +78,7 @@ export interface Permissions { childProcess?: PermissionCheck<{ command: string; args: string[]; + argv0?: string; cwd?: string; env?: Record; }>; @@ -201,21 +202,29 @@ export interface ProcessConfig { export type StdioEvent = { channel: StdioChannel; message: string }; export type StdioHook = (event: StdioEvent) => void; +export type CommandWaitResult = + | number + | { + exitCode: number | null; + signal: number | string | null; + }; + export interface CommandExecutor { spawn( command: string, args: string[], options?: { + argv0?: string; cwd?: string; env?: Record; onStdout?: (data: Uint8Array) => void; onStderr?: (data: Uint8Array) => void; }, ): { - wait(): Promise; + wait(): Promise; writeStdin(data: Uint8Array | string): void; closeStdin(): void; - kill(signal?: number): void; + kill(signal?: number): boolean | void; }; } @@ -295,7 +304,11 @@ export interface NodeRuntimeDriver { /** End stdin for a running `streamingStdin` execution. */ endStdin?(executionId: string): void; /** Write bytes to a PTY fd owned by the execution. */ - writePty?(executionId: string, fd: number, data: string | Uint8Array): Promise; + writePty?( + executionId: string, + fd: number, + data: string | Uint8Array, + ): Promise; /** Read bytes from a PTY fd owned by the execution. */ readPty?( executionId: string, @@ -406,13 +419,18 @@ export function wrapCommandExecutor( const check = ( command: string, args: string[], - options?: { cwd?: string; env?: Record }, + options?: { + argv0?: string; + cwd?: string; + env?: Record; + }, ): void => { if ( !permissionAllowed( permissions.childProcess?.({ command, args, + argv0: options?.argv0, cwd: options?.cwd, env: options?.env, }), @@ -1165,40 +1183,41 @@ export const POLYFILL_CODE_MAP: Record = { stream: ` class EventEmitterLike { constructor() { this._listeners = Object.create(null); } - on(event, fn) { (this._listeners[event] = this._listeners[event] || []).push(fn); return this; } + on(event, fn) { (this._listeners[event] = this._listeners[event] || []).push(fn); if (event === "data" && typeof this.resume === "function") this.resume(); return this; } addListener(event, fn) { return this.on(event, fn); } once(event, fn) { const w = (...a) => { this.off(event, w); fn(...a); }; w._origin = fn; return this.on(event, w); } off(event, fn) { if (this._listeners[event]) this._listeners[event] = this._listeners[event].filter((x) => x !== fn && x._origin !== fn); return this; } removeListener(event, fn) { return this.off(event, fn); } removeAllListeners(event) { if (event) delete this._listeners[event]; else this._listeners = Object.create(null); return this; } - emit(event, ...args) { const ls = (this._listeners[event] || []).slice(); for (const fn of ls) fn(...args); return ls.length > 0; } + emit(event, ...args) { const ls = (this._listeners[event] || []).slice(); if (ls.length === 0 && event === "error") throw (args[0] instanceof Error ? args[0] : new Error(String(args[0]))); for (const fn of ls) fn(...args); return ls.length > 0; } listenerCount(event) { return (this._listeners[event] || []).length; } } + const chunkLength = (chunk) => typeof chunk === "string" ? new TextEncoder().encode(chunk).length : Number(chunk && chunk.byteLength) || 1; + const streamError = (code, message) => { const error = new Error(message); error.code = code; return error; }; class Readable extends EventEmitterLike { - constructor(options) { super(); this.readable = true; this._readableOptions = options || {}; if (this._readableOptions.read) this._read = this._readableOptions.read; } - resume() { this.emit("resume"); return this; } - pause() { this.paused = true; return this; } + constructor(options) { super(); this.readable = true; this.readableEnded = false; this.destroyed = false; this.paused = true; this._buffer = []; this._bufferedBytes = 0; this._highWaterMark = Number(options && options.highWaterMark) || 16384; this._readableOptions = options || {}; this._readableAutoDestroy = this._readableOptions.autoDestroy !== false; if (this._readableOptions.read) this._read = this._readableOptions.read; } + _finishReadable() { if (this.readableEnded) return; this.readableEnded = true; this.readable = false; this.emit("end"); if (this._readableAutoDestroy && !this._closeScheduled) { this._closeScheduled = true; queueMicrotask(() => this.destroy()); } } + _flush() { while (!this.paused && this._buffer.length > 0) { const chunk = this._buffer.shift(); this._bufferedBytes -= chunkLength(chunk); this.emit("data", chunk); } if (!this.paused && this._ended && this._buffer.length === 0) this._finishReadable(); } + resume() { if (this.destroyed) return this; const changed = this.paused; this.paused = false; if (changed) this.emit("resume"); this._flush(); return this; } + pause() { this.paused = true; this.emit("pause"); return this; } setEncoding() { return this; } - read() { return null; } - push(chunk) { if (chunk == null) this.emit("end"); else this.emit("data", chunk); return true; } - pipe(dest) { this.on("data", (c) => dest.write && dest.write(c)); this.on("end", () => dest.end && dest.end()); return dest; } - destroy() { this.emit("close"); return this; } - } - Readable.toWeb = (stream) => new ReadableStream({ start(controller) { - stream.on("data", (chunk) => controller.enqueue(chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk))); - stream.on("end", () => { try { controller.close(); } catch (e) {} }); - stream.on("error", (err) => controller.error(err)); - } }); + read() { const chunk = this._buffer.shift() ?? null; if (chunk != null) this._bufferedBytes -= chunkLength(chunk); if (chunk == null && this._ended) this._finishReadable(); return chunk; } + push(chunk) { if (this.destroyed || this._ended) return false; if (chunk == null) { this._ended = true; this._flush(); return false; } this._buffer.push(chunk); this._bufferedBytes += chunkLength(chunk); this._flush(); return this._bufferedBytes < this._highWaterMark; } + pipe(dest) { this.on("data", (chunk) => { if (dest.write && dest.write(chunk) === false) { this.pause(); dest.once && dest.once("drain", () => this.resume()); } }); this.once("end", () => dest.end && dest.end()); this.once("error", (error) => dest.destroy ? dest.destroy(error) : dest.emit && dest.emit("error", error)); return dest; } + destroy(error) { if (this.destroyed) return this; this.destroyed = true; this.readable = false; this._buffer.length = 0; this._bufferedBytes = 0; if (error) this.emit("error", error); this.emit("close"); return this; } + } + Readable.toWeb = (stream) => new ReadableStream({ start(controller) { stream.once("end", () => controller.close()); stream.once("error", (error) => controller.error(error)); stream.on("data", (chunk) => { controller.enqueue(chunk); if ((controller.desiredSize ?? 1) <= 0) stream.pause && stream.pause(); }); }, pull() { stream.resume && stream.resume(); }, cancel(reason) { stream.destroy && stream.destroy(reason instanceof Error ? reason : undefined); } }); class Writable extends EventEmitterLike { - constructor(options) { super(); this.writable = true; this._writableOptions = options || {}; if (this._writableOptions.write) this._writeImpl = this._writableOptions.write; } - write(chunk, encoding, cb) { if (typeof encoding === "function") { cb = encoding; encoding = undefined; } if (this._writeImpl) this._writeImpl(chunk, encoding, cb || (() => {})); else if (cb) cb(); this.emit("data", chunk); return true; } - end(chunk, encoding, cb) { const done = typeof chunk === "function" ? chunk : typeof encoding === "function" ? encoding : cb; if (chunk != null && typeof chunk !== "function") this.write(chunk); this.emit("finish"); this.emit("end"); if (done) done(); } - destroy() { this.emit("close"); return this; } + constructor(options) { super(); this.writable = true; this.writableEnded = false; this.writableFinished = false; this.destroyed = false; this._pendingWrites = 0; this._writableBufferedBytes = 0; this._writableNeedDrain = false; this._writableHighWaterMark = Number(options && options.highWaterMark) || 16384; this._writableOptions = options || {}; this._writableAutoDestroy = this._writableOptions.autoDestroy !== false; if (this._writableOptions.write) this._writeImpl = this._writableOptions.write; } + _finishIfReady() { if (this.writableEnded && this._pendingWrites === 0 && !this.writableFinished && !this.destroyed) { this.writableFinished = true; this.writable = false; this.emit("finish"); if (this._endCallback) { const callback = this._endCallback; this._endCallback = null; callback(); } if (this._writableAutoDestroy && !this._closeScheduled) { this._closeScheduled = true; queueMicrotask(() => this.destroy()); } } } + write(chunk, encoding, cb) { if (typeof encoding === "function") { cb = encoding; encoding = undefined; } if (this.destroyed || this.writableEnded) { const error = streamError("ERR_STREAM_WRITE_AFTER_END", "write after end"); queueMicrotask(() => { if (cb) cb(error); else this.emit("error", error); }); return false; } const size = chunkLength(chunk); this._pendingWrites += 1; this._writableBufferedBytes += size; const accepted = this._writableBufferedBytes < this._writableHighWaterMark; if (!accepted) this._writableNeedDrain = true; let completed = false; const done = (error) => { if (completed) return; completed = true; queueMicrotask(() => { this._pendingWrites -= 1; this._writableBufferedBytes = Math.max(0, this._writableBufferedBytes - size); if (error) { if (cb) cb(error); this.destroy(error); return; } if (cb) cb(); if (this._writableNeedDrain && this._writableBufferedBytes < this._writableHighWaterMark) { this._writableNeedDrain = false; this.emit("drain"); } this._finishIfReady(); }); }; try { if (this._writeImpl) this._writeImpl(chunk, encoding, done); else { this.emit("data", chunk); done(); } } catch (error) { done(error); } return accepted; } + end(chunk, encoding, cb) { const done = typeof chunk === "function" ? chunk : typeof encoding === "function" ? encoding : cb; if (this.writableEnded) { const error = streamError("ERR_STREAM_ALREADY_FINISHED", "end called after stream finished"); queueMicrotask(() => done ? done(error) : this.emit("error", error)); return this; } if (chunk != null && typeof chunk !== "function") this.write(chunk, typeof encoding === "string" ? encoding : undefined); this.writableEnded = true; this._endCallback = done || null; this._finishIfReady(); return this; } + destroy(error) { if (this.destroyed) return this; this.destroyed = true; this.writable = false; if (error) this.emit("error", error); this.emit("close"); return this; } } - Writable.toWeb = (stream) => new WritableStream({ write(chunk) { return new Promise((resolve) => stream.write(chunk, undefined, () => resolve())); }, close() { stream.end && stream.end(); } }); - class Duplex extends Readable { constructor(options) { super(options); this.writable = true; if (options && options.write) this._writeImpl = options.write; } write(chunk, encoding, cb) { if (typeof encoding === "function") { cb = encoding; } if (this._writeImpl) this._writeImpl(chunk, encoding, cb || (() => {})); else if (cb) cb(); return true; } end(chunk) { if (chunk != null) this.write(chunk); this.emit("finish"); this.emit("end"); } } + Writable.toWeb = (stream) => new WritableStream({ start(controller) { stream.on("error", (error) => controller.error(error)); }, write(chunk) { return new Promise((resolve, reject) => stream.write(chunk, undefined, (error) => error ? reject(error) : resolve())); }, close() { return new Promise((resolve, reject) => stream.end((error) => error ? reject(error) : resolve())); }, abort(reason) { stream.destroy && stream.destroy(reason instanceof Error ? reason : new Error(String(reason))); } }); + class Duplex extends Readable { constructor(options) { super(options); this.writable = true; this.writableEnded = false; this.writableFinished = false; this._pendingWrites = 0; this._writableBufferedBytes = 0; this._writableNeedDrain = false; this._writableHighWaterMark = Number(options && options.highWaterMark) || 16384; this._writableOptions = options || {}; this._writableAutoDestroy = this._writableOptions.autoDestroy !== false; this._writeImpl = options && options.write; } write(...args) { return Writable.prototype.write.apply(this, args); } end(...args) { return Writable.prototype.end.apply(this, args); } _finishIfReady() { return Writable.prototype._finishIfReady.call(this); } destroy(error) { if (this.destroyed) return this; this.writable = false; return Readable.prototype.destroy.call(this, error); } } class Transform extends Duplex {} - class PassThrough extends Transform { write(chunk, encoding, cb) { if (typeof encoding === "function") { cb = encoding; } this.emit("data", chunk); if (cb) cb(); return true; } end(chunk) { if (chunk != null) this.emit("data", chunk); this.emit("end"); this.emit("finish"); } } + class PassThrough extends Transform { constructor(options) { super(options); this._writeImpl = (chunk, _encoding, callback) => { this.push(chunk); callback(); }; } end(chunk, encoding, cb) { const done = typeof chunk === "function" ? chunk : typeof encoding === "function" ? encoding : cb; if (chunk != null && typeof chunk !== "function") this.write(chunk, typeof encoding === "string" ? encoding : undefined); this.push(null); return Writable.prototype.end.call(this, done); } } function finished(stream, optsOrCb, maybeCb) { const cb = typeof optsOrCb === "function" ? optsOrCb : maybeCb; if (stream && stream.on) { let done = false; const fire = (e) => { if (done) return; done = true; if (cb) cb(e || null); }; stream.on("end", () => fire()); stream.on("finish", () => fire()); stream.on("close", () => fire()); stream.on("error", (e) => fire(e)); } @@ -1495,10 +1514,42 @@ export const POLYFILL_CODE_MAP: Record = { } emit(event, ...args) { const listeners = this._listeners.get(event) || []; + if (listeners.length === 0 && event === "error") { + throw (args[0] instanceof Error ? args[0] : new Error(String(args[0]))); + } for (const listener of [...listeners]) listener(...args); return listeners.length > 0; } } + const streamStateError = (code, message) => { + const error = new Error(message); + error.code = code; + return error; + }; + class ChildOutputStream extends Emitter { + constructor() { + super(); + this.readable = true; + this.readableEnded = false; + this.destroyed = false; + } + endReadable() { + if (this.readableEnded) return; + this.readableEnded = true; + this.readable = false; + this.emit("end"); + this.destroyed = true; + this.emit("close"); + } + destroy(error) { + if (this.destroyed) return this; + this.destroyed = true; + this.readable = false; + if (error) this.emit("error", error); + this.emit("close"); + return this; + } + } class ChildProcess extends Emitter { constructor(sessionId) { super(); @@ -1506,17 +1557,77 @@ export const POLYFILL_CODE_MAP: Record = { this.exitCode = null; this.signalCode = null; this.killed = false; - this.stdout = new Emitter(); - this.stderr = new Emitter(); + this.stdout = new ChildOutputStream(); + this.stderr = new ChildOutputStream(); + this.spawnfile = ""; + this.spawnargs = []; + let stdinEnded = false; this.stdin = { - write: (data) => { - callSync(globalThis._childProcessStdinWrite, sessionId, typeof data === "string" ? new TextEncoder().encode(data) : data); - return true; + writable: true, + writableEnded: false, + destroyed: false, + write: (data, encoding, callback) => { + const done = typeof encoding === "function" ? encoding : callback; + if (stdinEnded) { + const error = streamStateError("ERR_STREAM_WRITE_AFTER_END", "write after end"); + queueMicrotask(() => done ? done(error) : this.stdin.emit("error", error)); + return false; + } + try { + callSync(globalThis._childProcessStdinWrite, sessionId, typeof data === "string" ? new TextEncoder().encode(data) : data); + if (done) queueMicrotask(() => done()); + return true; + } catch (error) { + queueMicrotask(() => done ? done(error) : this.stdin.emit("error", error)); + return false; + } + }, + end: (data, encoding, callback) => { + const done = typeof data === "function" ? data : typeof encoding === "function" ? encoding : callback; + if (stdinEnded) { + if (done) queueMicrotask(() => done()); + return this.stdin; + } + if (data != null && typeof data !== "function") this.stdin.write(data, typeof encoding === "string" ? encoding : undefined); + stdinEnded = true; + this.stdin.writable = false; + this.stdin.writableEnded = true; + try { + callSync(globalThis._childProcessStdinClose, sessionId); + this.stdin.emit("finish"); + queueMicrotask(() => { + if (done) done(); + this.stdin.destroyed = true; + this.stdin.emit("close"); + }); + } catch (error) { + queueMicrotask(() => { + if (done) done(error); + else this.stdin.emit("error", error); + this.stdin.destroyed = true; + this.stdin.emit("close"); + }); + } + return this.stdin; }, - end: (data) => { - if (data != null) this.stdin.write(data); - callSync(globalThis._childProcessStdinClose, sessionId); + destroy: (error) => { + if (this.stdin.destroyed) return this.stdin; + if (!stdinEnded) { + stdinEnded = true; + try { callSync(globalThis._childProcessStdinClose, sessionId); } catch (closeError) { if (!error) error = closeError; } + } + this.stdin.writable = false; + this.stdin.writableEnded = true; + this.stdin.destroyed = true; + if (error) this.stdin.emit("error", error); + this.stdin.emit("close"); + return this.stdin; }, + on: (...args) => { this.stdin._emitter.on(...args); return this.stdin; }, + once: (...args) => { this.stdin._emitter.once(...args); return this.stdin; }, + off: (...args) => { this.stdin._emitter.off(...args); return this.stdin; }, + emit: (...args) => this.stdin._emitter.emit(...args), + _emitter: new Emitter(), }; } } @@ -1544,6 +1655,11 @@ export const POLYFILL_CODE_MAP: Record = { if (numeric !== undefined) return numeric; throw unknownSignalError(signal); }; + const signalNames = Object.entries(signalNumbers).reduce((names, [name, number]) => { + if (names[number] === undefined) names[number] = name; + return names; + }, {}); + const signalName = (signal) => signal == null ? null : typeof signal === "string" ? (signal.startsWith("SIG") ? signal : "SIG" + signal) : signalNames[Number(signal)] || null; const unknownSignalError = (signal) => { const error = new TypeError("Unknown signal: " + String(signal)); error.code = "ERR_UNKNOWN_SIGNAL"; @@ -1559,7 +1675,8 @@ export const POLYFILL_CODE_MAP: Record = { command: String(command), args: args.map(String), options: { - cwd: options.cwd || (globalThis.process && globalThis.process.cwd ? globalThis.process.cwd() : "/"), + argv0: options.argv0 === undefined ? undefined : String(options.argv0), + cwd: options.cwd ?? (globalThis.process && globalThis.process.cwd ? globalThis.process.cwd() : "/"), env: options.env, }, }, @@ -1570,8 +1687,12 @@ export const POLYFILL_CODE_MAP: Record = { return child; } const child = new ChildProcess(sessionId); + child.spawnfile = String(command); + child.spawnargs = [options.argv0 === undefined ? String(command) : String(options.argv0), ...args.map(String)]; child.kill = (signal) => { - callSync(globalThis._childProcessKill, sessionId, normalizeSignal(signal)); + if (child.exitCode != null || child.signalCode != null) return false; + const accepted = callSync(globalThis._childProcessKill, sessionId, normalizeSignal(signal)); + if (accepted === false) return false; child.killed = true; return true; }; @@ -1592,10 +1713,13 @@ export const POLYFILL_CODE_MAP: Record = { return; } if (event.type === "exit") { - child.exitCode = event.exitCode; - child.signalCode = event.signal; - child.emit("exit", event.exitCode, event.signal); - child.emit("close", event.exitCode, event.signal); + const terminationSignal = signalName(event.signal); + child.exitCode = terminationSignal == null ? event.exitCode : null; + child.signalCode = terminationSignal; + child.emit("exit", child.exitCode, terminationSignal); + child.stdout.endReadable(); + child.stderr.endReadable(); + child.emit("close", child.exitCode, terminationSignal); } }; queueMicrotask(() => { @@ -1613,7 +1737,8 @@ export const POLYFILL_CODE_MAP: Record = { command: String(command), args: args.map(String), options: { - cwd: options.cwd || (globalThis.process && globalThis.process.cwd ? globalThis.process.cwd() : "/"), + argv0: options.argv0 === undefined ? undefined : String(options.argv0), + cwd: options.cwd ?? (globalThis.process && globalThis.process.cwd ? globalThis.process.cwd() : "/"), env: options.env, input: encodeBytes(options.input), }, @@ -1627,8 +1752,8 @@ export const POLYFILL_CODE_MAP: Record = { output: [null, stdout, stderr], stdout, stderr, - status: result.code, - signal: null, + status: result.signal == null ? result.code : null, + signal: signalName(result.signal), error: undefined, }; } catch (error) { @@ -2903,18 +3028,21 @@ export const POLYFILL_CODE_MAP: Record = { wasi: BROWSER_WASI_POLYFILL_CODE, "node:wasi": "module.exports = require('wasi');", "secure-exec:wasi-command-host": ` + const callSync = (ref, ...args) => { + if (typeof ref === "function") return ref(...args); + if (ref && typeof ref.applySync === "function") return ref.applySync(undefined, args); + if (ref && typeof ref.applySyncPromise === "function") return ref.applySyncPromise(undefined, args); + throw new Error("process exec bridge is not configured"); + }; function defaultDecode(bytes) { return new TextDecoder().decode(bytes); } function decodeNullSeparated(bytes) { - const out = []; - let start = 0; - for (let i = 0; i <= bytes.length; i += 1) { - if (i === bytes.length || bytes[i] === 0) { - if (i > start) out.push(defaultDecode(bytes.slice(start, i))); - start = i + 1; - } - } + if (!bytes || bytes.length === 0) return []; + const out = defaultDecode(bytes).split("\0"); + // libc/std encode a terminal NUL after the last element. Remove exactly + // that terminator while preserving every intentional empty element. + if (bytes[bytes.length - 1] === 0) out.pop(); return out; } function parseEnv(bytes) { @@ -2943,29 +3071,97 @@ export const POLYFILL_CODE_MAP: Record = { const modules = new Map(); for (const [name, source] of Object.entries(commands || {})) { const value = await readCommandBytes(source); - modules.set(name, value instanceof WebAssembly.Module ? value : new WebAssembly.Module(value)); + const module = value instanceof WebAssembly.Module ? value : new WebAssembly.Module(value); + modules.set(name, module); } return modules; } + function unsupportedBrowserNetworkError(image, networkImports) { + const subject = image && (image.commandPath || image.argv?.[0]); + const error = new Error( + "browser WASI command " + (subject || "") + + " requires unsupported host_net imports: " + networkImports.join(", ") + + "; run this command in the native runtime", + ); + error.code = "ERR_AGENTOS_BROWSER_WASM_NETWORK_UNSUPPORTED"; + return error; + } + function assertBrowserNetworkSupported(image) { + const networkImports = WebAssembly.Module.imports(image.module) + .filter((entry) => entry.module === "host_net") + .map((entry) => entry.name); + if (networkImports.length > 0) { + throw unsupportedBrowserNetworkError(image, networkImports); + } + } async function createWasiCommandHost(options) { const WASI = options && options.WASI ? options.WASI : require("node:wasi").WASI; const commandModules = await loadCommandModules(options && options.commands); + const trustedMaxSpawnFileActions = /* @agentos-process-max-spawn-file-actions */ 4096; + const trustedMaxSpawnFileActionBytes = /* @agentos-process-max-spawn-file-action-bytes */ 1048576; + const requestedMaxSpawnFileActions = Number(options?.maxSpawnFileActions); + const requestedMaxSpawnFileActionBytes = Number(options?.maxSpawnFileActionBytes); + // Guest options may lower their own cap, but can never raise the trusted VM + // policy embedded by the worker when this builtin is loaded. + const maxSpawnFileActions = Number.isSafeInteger(requestedMaxSpawnFileActions) && requestedMaxSpawnFileActions > 0 + ? Math.min(requestedMaxSpawnFileActions, trustedMaxSpawnFileActions) + : trustedMaxSpawnFileActions; + const maxSpawnFileActionBytes = Number.isSafeInteger(requestedMaxSpawnFileActionBytes) && requestedMaxSpawnFileActionBytes > 0 + ? Math.min(requestedMaxSpawnFileActionBytes, trustedMaxSpawnFileActionBytes) + : trustedMaxSpawnFileActionBytes; + let warnedSpawnFileActions = false; + let warnedSpawnFileActionBytes = false; let memory = null; let nextPid = 100; + let activePid = 1; + let activePpid = 0; + const processGroups = new Map([[1, 1]]); const exitedChildren = new Map(); const deferredChildren = new Map(); const waitBuffer = new SharedArrayBuffer(4); const wait = new Int32Array(waitBuffer); const errnoSuccess = 0; + const errno2big = 1; + const errnoAcces = 2; const errnoBadf = 8; const errnoChild = 10; + const errnoFbig = 22; + const errnoInval = 28; + const errnoIo = 29; + const errnoLoop = 32; + const errnoNoent = 44; + const errnoNoexec = 45; const errnoNosys = 52; - let nextSyntheticFd = 1000; + const errnoNotsup = 58; + const errnoPerm = 63; + const errnoRofs = 69; + const errnoSrch = 71; + const errnoMfile = 33; + const linuxBinprmBufferSize = 256; + const linuxMaxInterpreterDepth = 4; + const configuredModuleFileBytes = Number(options?.maxModuleFileBytes); + const maxModuleFileBytes = Number.isSafeInteger(configuredModuleFileBytes) && configuredModuleFileBytes >= 0 + ? configuredModuleFileBytes + : 256 * 1024 * 1024; + const syntheticFdBase = 1 << 20; + const configuredSyntheticFdCount = Number(options?.maxSyntheticFds); + const maxSyntheticFdCount = Number.isSafeInteger(configuredSyntheticFdCount) && configuredSyntheticFdCount > 0 && configuredSyntheticFdCount <= 0xffffffff - syntheticFdBase + ? configuredSyntheticFdCount + : 4096; + const syntheticFdLimit = syntheticFdBase + maxSyntheticFdCount; + const syntheticFdWarnAt = Math.max(1, Math.floor(maxSyntheticFdCount * 0.9)); + let warnedAboutSyntheticFds = false; const syntheticFdEntries = new Map(); + let activeCloexecFds = new Set(); let activeFdOverrides = null; let activeChildCwd = null; + let activeWasi = null; let previousLookupFdHandle = null; let parentWasi = null; + const knownChildren = new Set(); + const runningChildren = new Set(); + let activeSpawnCallContext = null; + let activeBlockedSignals = new Set(); const getMemory = () => { if (!memory) throw new Error("WASI host command memory is not set"); return memory; @@ -2981,6 +3177,142 @@ export const POLYFILL_CODE_MAP: Record = { }; const readBytes = (ptr, len) => bytes().slice(ptr >>> 0, (ptr >>> 0) + (len >>> 0)); const readString = (ptr, len) => defaultDecode(readBytes(ptr, len)); + const decodeSignalMask = (maskLo, maskHi) => { + const signals = []; + const lo = Number(maskLo) >>> 0; + const hi = Number(maskHi) >>> 0; + for (let bit = 0; bit < 32; bit += 1) { + if (((lo >>> bit) & 1) === 1) signals.push(bit + 1); + if (((hi >>> bit) & 1) === 1) signals.push(bit + 33); + } + return signals; + }; + const encodeSignalMask = (signals) => { + let lo = 0; + let hi = 0; + for (const signal of signals) { + if (signal >= 1 && signal <= 32) lo = (lo | (1 << (signal - 1))) >>> 0; + else if (signal >= 33 && signal <= 64) hi = (hi | (1 << (signal - 33))) >>> 0; + } + return { lo, hi }; + }; + const decodeSpawnActions = (actionsPtr, actionsLen, initialCwd) => { + const failLimit = (message) => { + if (process && process.stderr && typeof process.stderr.write === "function") { + process.stderr.write("[agentos] " + message + "\\n"); + } + const error = new Error(message); + error.code = "E2BIG"; + throw error; + }; + const warnNearLimit = (kind, current, limit) => { + const countLimit = kind === "actions"; + if ((countLimit ? warnedSpawnFileActions : warnedSpawnFileActionBytes) || + current < Math.ceil(limit * 0.9)) return; + if (countLimit) warnedSpawnFileActions = true; + else warnedSpawnFileActionBytes = true; + const setting = countLimit + ? "limits.process.maxSpawnFileActions" + : "limits.process.maxSpawnFileActionBytes"; + if (process && process.stderr && typeof process.stderr.write === "function") { + process.stderr.write("[agentos] posix_spawn file-action " + kind + " near " + + setting + " (" + current + "/" + limit + "); raise " + setting + " if needed\\n"); + } + }; + if ((actionsLen >>> 0) > maxSpawnFileActionBytes) { + failLimit("posix_spawn file-action payload exceeds limits.process.maxSpawnFileActionBytes (" + + maxSpawnFileActionBytes + "); raise limits.process.maxSpawnFileActionBytes if needed"); + } + warnNearLimit("bytes", actionsLen >>> 0, maxSpawnFileActionBytes); + const value = readBytes(actionsPtr, actionsLen); + const data = new DataView(value.buffer, value.byteOffset, value.byteLength); + const stdio = [0, 1, 2]; + const closed = new Set(); + const actions = []; + const actionFdPaths = new Map(); + let cwd = initialCwd; + let offset = 0; + let actionCount = 0; + const fail = (code, message) => { + const error = new Error(message); + error.code = code; + throw error; + }; + while (offset < value.byteLength) { + actionCount++; + if (actionCount > maxSpawnFileActions) { + failLimit("posix_spawn file actions exceed limits.process.maxSpawnFileActions (" + + maxSpawnFileActions + "); raise limits.process.maxSpawnFileActions if needed"); + } + warnNearLimit("actions", actionCount, maxSpawnFileActions); + if (value.byteLength - offset < 24) fail("EINVAL", "truncated posix_spawn action header"); + const command = data.getUint32(offset, true); + const fd = data.getInt32(offset + 4, true); + const sourceFd = data.getInt32(offset + 8, true); + const oflag = data.getInt32(offset + 12, true); + const mode = data.getUint32(offset + 16, true); + const pathLength = data.getUint32(offset + 20, true); + offset += 24; + const pathEnd = offset + pathLength; + if (pathEnd < offset || pathEnd > value.byteLength) fail("EINVAL", "truncated posix_spawn action path"); + const actionPath = defaultDecode(value.subarray(offset, pathEnd)); + offset = pathEnd; + if (command === 1) { + if (fd < 0) fail("EBADF", "posix_spawn close has an invalid fd"); + closed.add(fd); + actionFdPaths.delete(fd); + if (fd <= 2) stdio[fd] = 0xffffffff; + actions.push({ command, fd, sourceFd, oflag, mode, path: "" }); + } else if (command === 2) { + if (fd < 0 || sourceFd < 0 || closed.has(sourceFd)) fail("EBADF", "posix_spawn dup2 references a closed fd"); + const source = sourceFd <= 2 ? stdio[sourceFd] : sourceFd; + if (source === 0xffffffff) fail("EBADF", "posix_spawn dup2 references a closed fd"); + if (fd <= 2) stdio[fd] = source; + closed.delete(fd); + const sourcePath = actionFdPaths.get(sourceFd) || guestPathForBrowserFd(sourceFd); + if (sourcePath) actionFdPaths.set(fd, sourcePath); + else actionFdPaths.delete(fd); + actions.push({ command, fd, sourceFd, oflag, mode, path: "" }); + } else if (command === 3) { + if (fd < 0) fail("EBADF", "posix_spawn open has an invalid fd"); + if (!actionPath) fail("ENOENT", "posix_spawn open path is empty"); + const resolvedPath = path().posix.resolve(cwd || "/", actionPath); + closed.delete(fd); + actionFdPaths.set(fd, resolvedPath); + if (fd <= 2) stdio[fd] = fd; + actions.push({ command, fd, sourceFd, oflag, mode, path: resolvedPath }); + } else if (command === 4) { + if (!actionPath) fail("ENOENT", "posix_spawn chdir path is empty"); + cwd = path().posix.resolve(cwd || "/", actionPath); + actions.push({ command, fd, sourceFd, oflag, mode, path: cwd }); + } else if (command === 5) { + if (fd < 0 || closed.has(fd)) fail("EBADF", "posix_spawn fchdir has an invalid fd"); + const directoryPath = actionFdPaths.get(fd) || guestPathForBrowserFd(fd); + if (!directoryPath) fail("EBADF", "posix_spawn fchdir references an unknown fd"); + cwd = directoryPath; + actions.push({ command, fd, sourceFd, oflag, mode, path: cwd }); + } else if (command === 6) { + if (fd < 0) fail("EBADF", "posix_spawn closefrom has an invalid fd"); + for (const guestFd of new Set([ + ...syntheticFdEntries.keys(), + ...(activeFdOverrides ? activeFdOverrides.keys() : []), + ...actionFdPaths.keys(), + ])) { + if (guestFd < fd) continue; + closed.add(guestFd); + actionFdPaths.delete(guestFd); + } + for (let stdioFd = Math.max(fd, 0); stdioFd <= 2; stdioFd += 1) { + closed.add(stdioFd); + stdio[stdioFd] = 0xffffffff; + } + actions.push({ command, fd, sourceFd, oflag, mode, path: "" }); + } else { + fail("EINVAL", "unknown posix_spawn action opcode " + command); + } + } + return { stdio, cwd, actions }; + }; const fs = () => require("node:fs"); const path = () => require("node:path"); const userRecord = new TextEncoder().encode( @@ -2993,6 +3325,24 @@ export const POLYFILL_CODE_MAP: Record = { if (stat && typeof stat.isSymbolicLink === "function" && stat.isSymbolicLink()) return 0o120777; return fallback >>> 0; }; + const hostFsErrno = (error) => { + switch (error && typeof error === "object" ? error.code : undefined) { + case "E2BIG": return errno2big; + case "EACCES": return errnoAcces; + case "EBADF": return errnoBadf; + case "EFBIG": return errnoFbig; + case "EMFILE": return errnoMfile; + case "EPERM": return errnoPerm; + case "ENOENT": return errnoNoent; + case "ENOEXEC": return errnoNoexec; + case "ELOOP": return errnoLoop; + case "EINVAL": return errnoInval; + case "ENOTSUP": return errnoNotsup; + case "EISDIR": return errnoInval; + case "EROFS": return errnoRofs; + default: return errnoIo; + } + }; const currentGuestCwd = () => { const cwd = typeof activeChildCwd === "string" && activeChildCwd.startsWith("/") ? activeChildCwd @@ -3007,27 +3357,214 @@ export const POLYFILL_CODE_MAP: Record = { ? path().posix.normalize(value) : path().posix.resolve(currentGuestCwd(), value); }; + const pathEntries = (env) => { + const value = env && Object.prototype.hasOwnProperty.call(env, "PATH") + ? String(env.PATH) + : "/bin:/usr/bin"; + return value.split(":").map((entry) => entry || "."); + }; + const resolveCommandModule = (commandPath, env, cwd) => { + const raw = String(commandPath); + const hasSlash = raw.includes("/"); + const normalized = hasSlash + ? path().posix.resolve(cwd || "/", raw) + : raw; + if (hasSlash) return commandModules.get(normalized) || null; + for (const directory of pathEntries(env)) { + const candidate = path().posix.resolve(cwd || "/", directory, raw); + if (commandModules.has(candidate)) return commandModules.get(candidate); + } + return null; + }; + const resolveExecModule = (commandPath, cwd) => { + const raw = String(commandPath); + const normalized = path().posix.resolve(cwd || "/", raw); + return commandModules.get(normalized) || null; + }; + const executableError = (code, message) => { + const error = new Error(message); + error.code = code; + return error; + }; + const validateExecutableStat = (stat, subject) => { + if (!stat || typeof stat.isFile !== "function" || !stat.isFile()) { + throw executableError("EACCES", subject + " is not a regular executable file"); + } + if ((Number(stat.mode) & 0o111) === 0) { + throw executableError("EACCES", subject + " does not have an executable mode bit"); + } + const size = Number(stat.size); + if (!Number.isSafeInteger(size) || size < 0 || size > maxModuleFileBytes) { + throw executableError( + "EFBIG", + subject + " exceeds limits.wasm.maxModuleFileBytes (" + maxModuleFileBytes + "); raise limits.wasm.maxModuleFileBytes if needed", + ); + } + return size; + }; + const parseLinuxShebang = (value) => { + const executableBytes = value instanceof Uint8Array ? value : new Uint8Array(value); + if (executableBytes.length < 2 || executableBytes[0] !== 0x23 || executableBytes[1] !== 0x21) return null; + const header = executableBytes.slice(2, Math.min(executableBytes.length, linuxBinprmBufferSize)); + const newline = header.indexOf(0x0a); + let line = new TextDecoder().decode(newline >= 0 ? header.slice(0, newline) : header).replace(/[\t ]+$/u, ""); + const first = line.search(/[^\t ]/u); + if (first < 0) throw executableError("ENOEXEC", "shebang does not name an interpreter"); + line = line.slice(first); + const separator = line.search(/[\t ]/u); + if (newline < 0 && executableBytes.length >= linuxBinprmBufferSize && separator < 0) { + throw executableError("ENOEXEC", "shebang interpreter path exceeds the Linux header limit"); + } + if (separator < 0) return { interpreter: line, optionalArgument: null }; + const optionalArgument = line.slice(separator).replace(/^[\t ]+|[\t ]+$/gu, ""); + return { + interpreter: line.slice(0, separator), + optionalArgument: optionalArgument || null, + }; + }; + const readExecutableFd = (targetFd, subject) => { + const stat = fs().fstatSync(targetFd); + const size = validateExecutableStat(stat, subject); + const value = new Uint8Array(size); + let offset = 0; + while (offset < size) { + const count = fs().readSync(targetFd, value, offset, size - offset, offset); + if (!Number.isInteger(count) || count <= 0) throw executableError("EIO", subject + " changed while being read"); + offset += count; + } + return value; + }; + const compileBrowserExecImage = (value, subject, argv, depth = 0) => { + const shebang = parseLinuxShebang(value); + if (shebang) { + if (depth >= linuxMaxInterpreterDepth) throw executableError("ELOOP", "interpreter recursion exceeds the Linux limit"); + return loadBrowserExecImage( + shebang.interpreter, + [ + shebang.interpreter, + ...(shebang.optionalArgument === null ? [] : [shebang.optionalArgument]), + String(subject), + ...argv.slice(1), + ], + depth + 1, + ); + } + try { + return { module: new WebAssembly.Module(value), argv }; + } catch (error) { + if (error instanceof WebAssembly.CompileError) throw executableError("ENOEXEC", subject + " is not a supported WebAssembly executable image"); + throw error; + } + }; + const loadBrowserExecImage = (commandPath, argv, depth = 0) => { + const normalized = path().posix.resolve(currentGuestCwd(), String(commandPath)); + const registered = commandModules.get(normalized); + if (registered) return { module: registered, argv }; + const stat = fs().statSync(normalized); + validateExecutableStat(stat, normalized); + const value = fs().readFileSync(normalized); + if (Number(value.byteLength) > maxModuleFileBytes) throw executableError("EFBIG", normalized + " exceeds limits.wasm.maxModuleFileBytes"); + return compileBrowserExecImage(value, String(commandPath), argv, depth); + }; + const releaseGuestFileDescription = (description) => { + description.refCount = Math.max(0, (description.refCount || 1) - 1); + if (description.refCount === 0 && description.closed !== true) { + description.closed = true; + fs().closeSync(description.targetFd); + } + }; + const createGuestFileHandle = (description, onClose) => { + let open = true; + const handle = { kind: "guest-file", description, onClose }; + Object.defineProperties(handle, { + open: { + get: () => open, + set: (value) => { + if (value !== false || !open) return; + open = false; + if (typeof handle.onClose === "function") handle.onClose(handle); + releaseGuestFileDescription(description); + }, + }, + targetFd: { get: () => description.targetFd }, + position: { + get: () => description.position, + set: (value) => { + description.position = value; + if (description.ownerEntry) description.ownerEntry.offset = value; + }, + }, + readOnly: { get: () => description.readOnly }, + append: { get: () => description.append }, + }); + return handle; + }; const lookupSyntheticFd = (fd) => { const descriptor = fd >>> 0; const override = activeFdOverrides && activeFdOverrides.get(descriptor); if (override && override.open !== false) return override; const handle = syntheticFdEntries.get(descriptor); if (handle && handle.open !== false) return handle; - if (typeof previousLookupFdHandle === "function") return previousLookupFdHandle(descriptor); - const parentEntry = parentWasi && parentWasi.fdTable && parentWasi.fdTable.get(descriptor); - if (parentEntry && parentEntry.kind === "file" && typeof parentEntry.realFd === "number") { - return { - kind: "guest-file", - targetFd: parentEntry.realFd, - position: typeof parentEntry.offset === "number" ? parentEntry.offset : 0, - readOnly: parentEntry.readOnly === true, - open: true, - }; + if (typeof previousLookupFdHandle === "function") { + const previous = previousLookupFdHandle(descriptor); + if (previous) return previous; + } + const wasi = activeWasi || parentWasi; + const parentEntry = wasi && wasi.fdTable && wasi.fdTable.get(descriptor); + if (parentEntry && (parentEntry.kind === "file" || parentEntry.kind === "directory") && typeof parentEntry.realFd === "number") { + if (!parentEntry.__agentOSOpenFileDescription) { + const rights = typeof parentEntry.rightsBase === "bigint" ? parentEntry.rightsBase : null; + const initialPosition = typeof parentEntry.offset === "number" ? parentEntry.offset : 0; + const description = { + targetFd: parentEntry.realFd, + storedPosition: initialPosition, + readOnly: parentEntry.readOnly === true, + canRead: rights == null || (rights & (1n << 1n)) !== 0n, + canWrite: parentEntry.readOnly !== true && (rights == null || (rights & (1n << 6n)) !== 0n), + append: parentEntry.append === true, + isDirectory: parentEntry.kind === "directory", + refCount: 1, + ownerEntry: parentEntry, + }; + Object.defineProperty(description, "position", { + get: () => typeof description.ownerEntry?.offset === "number" + ? description.ownerEntry.offset + : description.storedPosition, + set: (value) => { + description.storedPosition = value; + if (description.ownerEntry) description.ownerEntry.offset = value; + }, + }); + parentEntry.__agentOSOpenFileDescription = description; + } + return createGuestFileHandle( + parentEntry.__agentOSOpenFileDescription, + () => { + if (wasi.fdTable.get(descriptor) === parentEntry) wasi.fdTable.delete(descriptor); + }, + ); + } + return null; + }; + const guestPathForBrowserFd = (fd) => { + const handle = lookupSyntheticFd(fd); + const description = handle?.kind === "guest-file" ? handle.description : null; + if (typeof description?.guestPath === "string") return description.guestPath; + const wasi = activeWasi || parentWasi; + const entry = wasi?.fdTable?.get(Number(fd) >>> 0); + for (const candidate of [entry?.guestPath, entry?.path, entry?.preopenPath]) { + if (typeof candidate === "string" && candidate.startsWith("/")) { + return path().posix.normalize(candidate); + } } return null; }; const closeSyntheticHandle = (handle) => { if (!handle || handle.open === false) return; + if (handle.kind === "guest-file") { + handle.open = false; + return; + } handle.open = false; if (handle.kind === "pipe-read" && handle.pipe) { handle.pipe.readHandleCount = Math.max(0, (handle.pipe.readHandleCount || 0) - 1); @@ -3042,68 +3579,449 @@ export const POLYFILL_CODE_MAP: Record = { return { kind: "stdio", targetFd: handle.targetFd, open: true }; } if (handle.kind === "guest-file") { - return { ...handle, open: true }; + if (!handle.description) return null; + handle.description.refCount = (handle.description.refCount || 1) + 1; + return createGuestFileHandle(handle.description); } if (!handle.pipe) return null; if (handle.kind === "pipe-read") { handle.pipe.readHandleCount = (handle.pipe.readHandleCount || 0) + 1; - return { kind: "pipe-read", pipe: handle.pipe, open: true, onClose: handle.onClose }; + const onClose = handle.baseOnClose || handle.onClose; + return { kind: "pipe-read", pipe: handle.pipe, open: true, baseOnClose: onClose, onClose }; } if (handle.kind === "pipe-write") { handle.pipe.writeHandleCount = (handle.pipe.writeHandleCount || 0) + 1; - return { kind: "pipe-write", pipe: handle.pipe, open: true, onClose: handle.onClose }; + const onClose = handle.baseOnClose || handle.onClose; + return { kind: "pipe-write", pipe: handle.pipe, open: true, baseOnClose: onClose, onClose }; } return null; }; + const cloneOpenDescriptorsForSpawn = () => { + const overrides = new Map(); + const wasi = activeWasi || parentWasi; + const openFds = new Set([ + ...(activeFdOverrides ? activeFdOverrides.keys() : []), + ...syntheticFdEntries.keys(), + ...(wasi?.fdTable instanceof Map ? wasi.fdTable.keys() : []), + ]); + for (const fd of [...openFds].sort((left, right) => left - right)) { + const handle = lookupSyntheticFd(fd); + const cloned = cloneSyntheticHandle(handle); + if (cloned) overrides.set(fd, cloned); + } + return overrides; + }; + const browserOpenFlagsFromSpawnAction = (oflag) => { + const value = Number(oflag) >>> 0; + let flags = (value & 0x10000000) !== 0 + ? ((value & 0x04000000) !== 0 ? 0o2 : 0o1) + : 0; + if ((value & 0x00000001) !== 0) flags |= 0o2000; + if ((value & 0x00000004) !== 0) flags |= 0o4000; + if ((value & (1 << 12)) !== 0) flags |= 0o100; + if ((value & (4 << 12)) !== 0) flags |= 0o200; + if ((value & (8 << 12)) !== 0) flags |= 0o1000; + return flags; + }; + const applyBrowserSpawnFileActions = (overrides, actions, cloexecFds) => { + for (const action of actions || []) { + const fd = Number(action.fd); + if (!Number.isInteger(fd) || fd < 0) { + const error = new Error("posix_spawn action has an invalid fd"); + error.code = "EBADF"; + throw error; + } + if (action.command === 1) { + const handle = overrides.get(fd); + if (!handle) { + const error = new Error("posix_spawn close references a closed fd"); + error.code = "EBADF"; + throw error; + } + closeSyntheticHandle(handle); + overrides.delete(fd); + cloexecFds.delete(fd); + continue; + } + if (action.command === 2) { + const sourceFd = Number(action.sourceFd); + const source = overrides.get(sourceFd); + if (!source) { + const error = new Error("posix_spawn dup2 references a closed fd"); + error.code = "EBADF"; + throw error; + } + if (sourceFd === fd) { + cloexecFds.delete(fd); + continue; + } + const cloned = cloneSyntheticHandle(source); + if (!cloned) { + const error = new Error("posix_spawn dup2 cannot duplicate this fd"); + error.code = "EBADF"; + throw error; + } + const replaced = overrides.get(fd); + if (replaced) closeSyntheticHandle(replaced); + overrides.set(fd, cloned); + cloexecFds.delete(fd); + continue; + } + if (action.command === 3) { + const replaced = overrides.get(fd); + if (replaced) { + closeSyntheticHandle(replaced); + overrides.delete(fd); + } + const targetFd = fs().openSync( + action.path, + browserOpenFlagsFromSpawnAction(action.oflag), + Number(action.mode) >>> 0, + ); + const stat = fs().fstatSync(targetFd); + if ((Number(action.oflag) & (2 << 12)) !== 0 && !stat.isDirectory()) { + fs().closeSync(targetFd); + const error = new Error("posix_spawn open target is not a directory"); + error.code = "ENOTDIR"; + throw error; + } + const rawFlags = Number(action.oflag) >>> 0; + const canWrite = (rawFlags & 0x10000000) !== 0; + const canRead = !canWrite || (rawFlags & 0x04000000) !== 0; + const description = { + targetFd, + position: 0, + readOnly: !canWrite, + canRead, + canWrite, + append: (rawFlags & 1) !== 0, + isDirectory: stat.isDirectory(), + guestPath: action.path, + refCount: 1, + }; + overrides.set(fd, createGuestFileHandle(description)); + cloexecFds.delete(fd); + continue; + } + if (action.command === 6) { + for (const [openFd, handle] of Array.from(overrides.entries())) { + if (openFd < fd) continue; + closeSyntheticHandle(handle); + overrides.delete(openFd); + cloexecFds.delete(openFd); + } + } + } + return overrides; + }; const handleMatchesStdio = (handle, expectedKind) => { if (!handle || handle.open === false) return false; if (handle.kind === "stdio") { if (expectedKind === "read") return handle.targetFd === 0; if (expectedKind === "write") return handle.targetFd === 1 || handle.targetFd === 2; } - if (expectedKind === "read") return handle.kind === "pipe-read" || handle.kind === "guest-file"; - if (expectedKind === "write") return handle.kind === "pipe-write" || handle.kind === "guest-file"; + if (expectedKind === "read") return handle.kind === "pipe-read" || (handle.kind === "guest-file" && !handle.description?.isDirectory); + if (expectedKind === "write") return handle.kind === "pipe-write" || (handle.kind === "guest-file" && !handle.description?.isDirectory && handle.description?.canWrite !== false); return handle.kind === expectedKind; }; - const allocateSyntheticFd = (handle) => { - const fd = nextSyntheticFd++; - syntheticFdEntries.set(fd, handle); - return fd; + const guestFileDescription = (handle) => { + if (!handle || handle.kind !== "guest-file" || handle.open === false) return null; + if (handle.description) return handle.description; + if (typeof handle.targetFd !== "number") return null; + return handle; + }; + const registerSyntheticGuestFile = (fd, handle, targetWasi = activeWasi || parentWasi) => { + if (!handle || handle.kind !== "guest-file" || !handle.description) return; + const wasi = targetWasi; + if (!wasi?.fdTable) return; + const description = handle.description; + const mirror = { + kind: description.isDirectory ? "directory" : "file", + realFd: description.targetFd, + readOnly: description.readOnly === true, + append: description.append === true, + rightsBase: + (description.canRead === false ? 0n : 1n << 1n) | + (description.canWrite === false ? 0n : 1n << 6n), + __agentOSSyntheticMirror: true, + }; + Object.defineProperty(mirror, "offset", { + get: () => description.position, + set: (value) => { description.position = value; }, + }); + wasi.fdTable.set(fd, mirror); + handle.onClose = () => { + if (wasi.fdTable.get(fd) === mirror) wasi.fdTable.delete(fd); + }; + }; + const registerSyntheticHandle = ( + fd, + handle, + entries = activeFdOverrides || syntheticFdEntries, + targetWasi = activeWasi || parentWasi, + ) => { + registerSyntheticGuestFile(fd, handle, targetWasi); + if (!handle) return; + const onClose = handle.baseOnClose || handle.onClose; + handle.baseOnClose = onClose; + handle.onClose = (...args) => { + if (entries.get(fd) === handle) entries.delete(fd); + if (typeof onClose === "function") onClose(...args); + }; + }; + const allocateSyntheticFd = (handle, minimumFd = syntheticFdBase) => { + const entries = activeFdOverrides || syntheticFdEntries; + if (!warnedAboutSyntheticFds && entries.size >= syntheticFdWarnAt) { + warnedAboutSyntheticFds = true; + console.warn( + "[agentos] WASI synthetic fd usage is near the " + maxSyntheticFdCount + + " descriptor limit; raise createWasiCommandHost({ maxSyntheticFds }) if needed", + ); + } + if (entries.size >= maxSyntheticFdCount) return null; + const firstFd = Math.max(syntheticFdBase, Number(minimumFd)); + if (!Number.isSafeInteger(firstFd) || firstFd < 0 || firstFd >= syntheticFdLimit) return null; + for (let fd = firstFd; fd < syntheticFdLimit; fd += 1) { + if (entries.has(fd) || (activeWasi || parentWasi)?.fdTable?.has(fd)) continue; + entries.set(fd, handle); + registerSyntheticHandle(fd, handle, entries); + return fd; + } + return null; }; const replaceSyntheticFd = (fd, handle) => { const descriptor = fd >>> 0; - closeSyntheticHandle(syntheticFdEntries.get(descriptor)); - syntheticFdEntries.set(descriptor, handle); + const entries = activeFdOverrides || syntheticFdEntries; + if (!entries.has(descriptor) && entries.size >= maxSyntheticFdCount) return false; + const existingSynthetic = entries.get(descriptor); + if (existingSynthetic) { + closeSyntheticHandle(existingSynthetic); + } else { + const wasi = activeWasi || parentWasi; + const existingEntry = wasi?.fdTable?.get(descriptor); + if (existingEntry) { + const existingHandle = lookupSyntheticFd(descriptor); + if (existingHandle?.kind === "guest-file") { + closeSyntheticHandle(existingHandle); + } else if (typeof existingEntry.realFd === "number") { + fs().closeSync(existingEntry.realFd); + wasi.fdTable.delete(descriptor); + } + } + } + entries.set(descriptor, handle); + registerSyntheticHandle(descriptor, handle, entries); + return true; }; const pipeHasOpenWriters = (handle) => handle && handle.kind === "pipe-read" && handle.pipe && (handle.pipe.writeHandleCount || 0) > 0; + const execReplacementMarker = Symbol("agentos.browser.exec-replacement"); + const wrappedParentWasis = new WeakSet(); + const procFdAliasSets = new WeakMap(); + const isExecReplacement = (error) => + error && typeof error === "object" && error.marker === execReplacementMarker; + const readExecCloseFds = (cloexecFdsPtr, cloexecFdsLen) => { + const closeCount = Number(cloexecFdsLen) >>> 0; + if (closeCount > maxSyntheticFdCount) throw executableError("EINVAL", "exec CLOEXEC fd list exceeds the configured descriptor limit"); + const closePtr = Number(cloexecFdsPtr) >>> 0; + const closeBytes = closeCount * 4; + if (closePtr > getMemory().buffer.byteLength - closeBytes) throw executableError("EINVAL", "exec CLOEXEC fd list is outside guest memory"); + const closeFds = []; + for (let index = 0; index < closeCount; index += 1) { + closeFds.push(view().getUint32(closePtr + index * 4, true)); + } + return closeFds; + }; + const inheritWasiFdTable = (sourceWasi, targetWasi) => { + if (!(sourceWasi?.fdTable instanceof Map) || !(targetWasi?.fdTable instanceof Map)) return; + targetWasi.fdTable.clear(); + for (const [fd, entry] of sourceWasi.fdTable) targetWasi.fdTable.set(fd, entry); + const inheritedNextFd = Number(sourceWasi.nextFd); + if (Number.isSafeInteger(inheritedNextFd) && inheritedNextFd >= 3) { + targetWasi.nextFd = inheritedNextFd; + } else { + const ordinaryFds = Array.from(sourceWasi.fdTable.keys()) + .filter((fd) => Number.isSafeInteger(fd) && fd >= 3 && fd < syntheticFdBase); + targetWasi.nextFd = ordinaryFds.length > 0 ? Math.max(...ordinaryFds) + 1 : 3; + } + }; + const installProcFdAliases = (wasi) => { + if (!wasi?.wasiImport || procFdAliasSets.has(wasi)) return; + const aliases = new Set(); + procFdAliasSets.set(wasi, aliases); + const delegatePathOpen = typeof wasi.wasiImport.path_open === "function" + ? wasi.wasiImport.path_open.bind(wasi.wasiImport) + : null; + const delegateFdClose = typeof wasi.wasiImport.fd_close === "function" + ? wasi.wasiImport.fd_close.bind(wasi.wasiImport) + : null; + if (delegatePathOpen) { + wasi.wasiImport.path_open = (fd, dirflags, pathPtr, pathLen, oflags, rightsBase, rightsInheriting, fdflags, openedFdPtr) => { + const target = readString(pathPtr, pathLen); + const normalizedTarget = target.startsWith("/") ? target.slice(1) : target; + const procFdPrefix = "proc/self/fd/"; + const sourceText = normalizedTarget.startsWith(procFdPrefix) + ? normalizedTarget.slice(procFdPrefix.length) + : ""; + if (!sourceText || !Array.from(sourceText).every((char) => char >= "0" && char <= "9")) { + return delegatePathOpen(fd, dirflags, pathPtr, pathLen, oflags, rightsBase, rightsInheriting, fdflags, openedFdPtr); + } + if ((Number(oflags) & 0x0f) !== 0 || (BigInt(rightsBase) & (1n << 6n)) !== 0n) return errnoAcces; + const sourceFd = Number(sourceText); + const source = wasi.fdTable?.get(sourceFd); + if (!source || source.kind !== "file" || typeof source.realFd !== "number") return errnoNoent; + let openedFd = Number(wasi.nextFd); + if (!Number.isSafeInteger(openedFd) || openedFd < 3) openedFd = 3; + while (wasi.fdTable.has(openedFd)) openedFd += 1; + const alias = { ...source, offset: 0, __agentOSProcFdAlias: true }; + delete alias.__agentOSOpenFileDescription; + wasi.fdTable.set(openedFd, alias); + wasi.nextFd = openedFd + 1; + aliases.add(openedFd); + return writeU32(openedFdPtr, openedFd); + }; + } + if (delegateFdClose) { + wasi.wasiImport.fd_close = (fd) => { + const descriptor = Number(fd) >>> 0; + if (aliases.delete(descriptor)) { + wasi.fdTable?.delete(descriptor); + return errnoSuccess; + } + return delegateFdClose(descriptor); + }; + } + }; + const installDescriptorOverrides = (wasi, overrides) => { + if (!(overrides instanceof Map) || !wasi?.wasiImport) return; + for (const [fd, handle] of overrides) { + registerSyntheticGuestFile(fd, handle, wasi); + } + const delegateFdClose = typeof wasi.wasiImport.fd_close === "function" + ? wasi.wasiImport.fd_close.bind(wasi.wasiImport) + : null; + wasi.wasiImport.fd_close = (fd) => { + const descriptor = Number(fd) >>> 0; + const handle = overrides.get(descriptor); + if (!handle) return delegateFdClose ? delegateFdClose(descriptor) : errnoBadf; + closeSyntheticHandle(handle); + overrides.delete(descriptor); + wasi.fdTable?.delete(descriptor); + activeCloexecFds.delete(descriptor); + return errnoSuccess; + }; + }; + const instantiateProcessImage = (image, inheritedWasi, descriptorOverrides = null) => { + // Registered commands are inert data until selected by spawn/exec. + // Validate the selected image here so unused native-only registry + // commands do not make browser VM construction fail. + assertBrowserNetworkSupported(image); + const wasi = new WASI({ + returnOnExit: true, + args: image.argv, + env: image.env, + preopens: inheritedWasi ? {} : { "/": image.cwd || "/" }, + }); + if (inheritedWasi) inheritWasiFdTable(inheritedWasi, wasi); + installDescriptorOverrides(wasi, descriptorOverrides); + installProcFdAliases(wasi); + const imports = { + wasi_snapshot_preview1: wasi.wasiImport, + ...host.imports, + }; + return { + wasi, + instance: new WebAssembly.Instance(image.module, imports), + }; + }; + const closeExecFds = (wasi, descriptors) => { + let warned = false; + const warnClose = (fd, detail) => { + if (warned) return; + warned = true; + console.warn("[agentos] exec committed but closing FD_CLOEXEC descriptor " + fd + " failed (" + detail + "); further close failures are suppressed"); + }; + for (const value of new Set(descriptors || [])) { + const fd = Number(value) >>> 0; + activeCloexecFds.delete(fd); + const override = activeFdOverrides?.get(fd); + if (override) { + closeSyntheticHandle(override); + activeFdOverrides.delete(fd); + } + const synthetic = activeFdOverrides ? null : syntheticFdEntries.get(fd); + if (synthetic) { + closeSyntheticHandle(synthetic); + syntheticFdEntries.delete(fd); + } + if (wasi?.fdTable?.has(fd)) { + try { + const result = wasi.wasiImport?.fd_close?.(fd); + if (typeof result === "number" && result !== errnoSuccess) warnClose(fd, "WASI errno " + result); + } catch (error) { + // Linux commits exec even when closing a descriptor reports an error. + warnClose(fd, error instanceof Error ? error.message : String(error)); + } + wasi.fdTable.delete(fd); + } + } + }; const runChild = (child) => { const parentMemory = memory; + const previousActivePid = activePid; + const previousActivePpid = activePpid; const previousActiveFdOverrides = activeFdOverrides; const previousActiveChildCwd = activeChildCwd; + const previousActiveWasi = activeWasi; + const previousBlockedSignals = activeBlockedSignals; + const previousCloexecFds = activeCloexecFds; try { - const childWasi = new WASI({ - returnOnExit: true, - args: [child.commandPath, ...child.argv.slice(1)], - env: child.env, - preopens: { "/": child.cwd || "/" }, - }); - const childImports = { - wasi_snapshot_preview1: childWasi.wasiImport, - ...host.imports, - }; - const childInstance = new WebAssembly.Instance(child.module, childImports); - memory = childInstance.exports.memory; + activePid = child.pid; + activePpid = child.ppid; activeFdOverrides = child.overrides; - activeChildCwd = child.cwd || "/"; - const exitCode = childWasi.start(childInstance); - exitedChildren.set(child.pid, exitCode << 8); - } catch { - exitedChildren.set(child.pid, 127 << 8); + activeBlockedSignals = child.signalMask || new Set(); + activeCloexecFds = child.cloexecFds || new Set(); + runningChildren.add(child.pid); + let image = child; + let inheritedWasi = null; + for (;;) { + const started = instantiateProcessImage(image, inheritedWasi, child.overrides); + memory = started.instance.exports.memory; + activeChildCwd = image.cwd || "/"; + activeWasi = started.wasi; + const mask = encodeSignalMask(activeBlockedSignals); + if (mask.lo !== 0 || mask.hi !== 0) { + const setter = started.instance.exports.__agentos_set_initial_sigmask; + if (typeof setter !== "function") throw new Error("spawned WASM image cannot initialize its inherited signal mask"); + setter(mask.lo, mask.hi); + } + try { + const exitCode = started.wasi.start(started.instance); + exitedChildren.set(child.pid, { exitCode: Number(exitCode) || 0, signal: 0 }); + break; + } catch (error) { + if (!isExecReplacement(error)) throw error; + closeExecFds(started.wasi, error.image.closeFds); + inheritedWasi = started.wasi; + image = error.image; + } + } + } catch (error) { + if (error?.code === "ERR_AGENTOS_BROWSER_WASM_NETWORK_UNSUPPORTED") { + throw error; + } + exitedChildren.set(child.pid, { exitCode: 127, signal: 0 }); } finally { - for (const handle of child.childOverrideHandles) closeSyntheticHandle(handle); + runningChildren.delete(child.pid); + for (const handle of new Set(child.overrides.values())) closeSyntheticHandle(handle); + child.overrides.clear(); activeFdOverrides = previousActiveFdOverrides; activeChildCwd = previousActiveChildCwd; + activeWasi = previousActiveWasi; + activeBlockedSignals = previousBlockedSignals; + activeCloexecFds = previousCloexecFds; + activePid = previousActivePid; + activePpid = previousActivePpid; memory = parentMemory; } }; @@ -3131,6 +4049,34 @@ export const POLYFILL_CODE_MAP: Record = { }, setParentWasi(wasi) { parentWasi = wasi || null; + if (!activeWasi) activeWasi = parentWasi; + if (wasi && typeof wasi.start === "function" && !wrappedParentWasis.has(wasi)) { + wrappedParentWasis.add(wasi); + const initialStart = wasi.start.bind(wasi); + wasi.start = (instance) => { + let currentWasi = wasi; + let currentInstance = instance; + let currentStart = initialStart; + let currentCwd = currentGuestCwd(); + for (;;) { + memory = currentInstance.exports.memory; + activeChildCwd = currentCwd; + activeWasi = currentWasi; + parentWasi = currentWasi; + try { + return currentStart(currentInstance); + } catch (error) { + if (!isExecReplacement(error)) throw error; + closeExecFds(currentWasi, error.image.closeFds); + const started = instantiateProcessImage(error.image, currentWasi); + currentWasi = started.wasi; + currentInstance = started.instance; + currentStart = currentWasi.start.bind(currentWasi); + currentCwd = error.image.cwd || "/"; + } + } + }; + } return host; }, installBlockingStdin(processLike) { @@ -3232,9 +4178,9 @@ export const POLYFILL_CODE_MAP: Record = { if (descriptor <= 2) return 0o020666; const handle = lookupSyntheticFd(descriptor); if (handle && (handle.kind === "pipe-read" || handle.kind === "pipe-write")) return 0o010600; - if (handle && handle.kind === "guest-file" && typeof handle.targetFd === "number") { + if (handle && handle.kind === "guest-file" && typeof guestFileDescription(handle)?.targetFd === "number") { try { - return modeFromStat(fs().fstatSync(handle.targetFd), 0o100644); + return modeFromStat(fs().fstatSync(guestFileDescription(handle).targetFd), 0o100644); } catch { return 0o100644; } @@ -3264,15 +4210,14 @@ export const POLYFILL_CODE_MAP: Record = { return 0; } }, - // Matches node runner host_fs.chmod(fd, pathPtr, pathLen, mode): - // 0 on success, 1 on failure. + // Match node's host_fs chmod errno contract exactly. chmod(_fd, pathPtr, pathLen, mode) { try { const guestPath = resolveGuestPath(readString(pathPtr, pathLen)); fs().chmodSync(guestPath, Number(mode) >>> 0); - return 0; - } catch { - return 1; + return errnoSuccess; + } catch (error) { + return hostFsErrno(error); } }, // The node runner exports 7 host_fs symbols; mirror the full @@ -3287,9 +4232,9 @@ export const POLYFILL_CODE_MAP: Record = { if ( handle && handle.kind === "guest-file" && - typeof handle.targetFd === "number" + typeof guestFileDescription(handle)?.targetFd === "number" ) { - return BigInt(fs().fstatSync(handle.targetFd).size ?? -1); + return BigInt(fs().fstatSync(guestFileDescription(handle).targetFd).size ?? -1); } const parentEntry = parentWasi && parentWasi.fdTable && parentWasi.fdTable.get(descriptor); @@ -3316,126 +4261,506 @@ export const POLYFILL_CODE_MAP: Record = { return (1n << 64n) - 1n; } }, - // Browser fs() has no fd-based fchmod/ftruncate; provide the - // symbols (best-effort failure) so imports resolve. Rust guest - // binaries bypass wasi-libc stat/chmod/truncate, so these paths - // are unreached in practice. - fchmod(_fd, _mode) { - return 1; + fchmod(fd, mode) { + const descriptor = fd >>> 0; + const handle = lookupSyntheticFd(descriptor) || (descriptor <= 2 + ? { kind: "stdio", targetFd: descriptor, open: true } + : null); + if (!handle) return errnoBadf; + const description = guestFileDescription(handle); + if (!description) return errnoInval; + try { + fs().fchmodSync(description.targetFd, Number(mode) & 0o7777); + return errnoSuccess; + } catch (error) { + return hostFsErrno(error); + } }, - ftruncate(_fd, _length) { - return 1; + ftruncate(fd, length) { + const descriptor = fd >>> 0; + const handle = lookupSyntheticFd(descriptor) || (descriptor <= 2 + ? { kind: "stdio", targetFd: descriptor, open: true } + : null); + if (!handle) return errnoBadf; + const description = guestFileDescription(handle); + if (!description) return errnoInval; + const size = Number(length); + if (!Number.isSafeInteger(size) || size < 0 || description.isDirectory) return errnoInval; + if (description.readOnly === true || description.canWrite === false) return errnoInval; + try { + fs().ftruncateSync(description.targetFd, size); + return errnoSuccess; + } catch (error) { + return hostFsErrno(error); + } }, }, - host_process: { - proc_spawn(argvPtr, argvLen, envpPtr, envpLen, stdinFd, stdoutFd, stderrFd, cwdPtr, cwdLen, retPid) { - try { - const argv = decodeNullSeparated(readBytes(argvPtr, argvLen)); - if (argv.length === 0) return errnoNosys; - const commandPath = argv[0]; - const commandName = commandPath.split("/").filter(Boolean).at(-1) || commandPath; - const module = commandModules.get(commandName); - if (!module) return errnoNosys; - const env = { - ...(options && options.env ? options.env : {}), - ...parseEnv(readBytes(envpPtr, envpLen)), - PATH: (options && options.path) || "/bin:/usr/bin", + host_process: { + proc_spawn(argvPtr, argvLen, envpPtr, envpLen, stdinFd, stdoutFd, stderrFd, cwdPtr, cwdLen, retPid) { + try { + const argvBytes = readBytes(argvPtr, argvLen); + const commandLength = argvBytes.indexOf(0); + if (commandLength <= 0) return errnoNoent; + return this.proc_spawn_v2( + argvPtr, + commandLength, + argvPtr, + argvLen, + envpPtr, + envpLen, + stdinFd, + stdoutFd, + stderrFd, + cwdPtr, + cwdLen, + retPid, + ); + } catch (error) { + return hostFsErrno(error); + } + }, + proc_spawn_v3(execPathPtr, execPathLen, argvPtr, argvLen, envpPtr, envpLen, actionsPtr, actionsLen, cwdPtr, cwdLen, attrFlags, sigDefaultLo, sigDefaultHi, sigMaskLo, sigMaskHi, pgroup, retPid) { + const flags = attrFlags >>> 0; + const supportedFlags = 1 | 2 | 4 | 8 | 64; + if ((flags & ~supportedFlags) !== 0) return errnoNotsup; + if ((pgroup | 0) < 0) return errnoInval; + if (activeSpawnCallContext) return errnoIo; + try { + const initialCwd = cwdLen ? readString(cwdPtr, cwdLen) : ((options && options.cwd) || "/"); + const actions = decodeSpawnActions(actionsPtr, actionsLen, initialCwd); + const signalMask = decodeSignalMask(sigMaskLo, sigMaskHi).filter((signal) => signal !== 9 && signal !== 19); + activeSpawnCallContext = { + attrFlags: flags, + pgroup: pgroup | 0, + signalMask, + signalDefaults: (flags & 4) !== 0 ? decodeSignalMask(sigDefaultLo, sigDefaultHi) : [], + fileActions: actions.actions, }; - const cwd = cwdLen ? readString(cwdPtr, cwdLen) : ((options && options.cwd) || "/"); - const childOverrideHandles = []; - const overrides = new Map(); + return this.proc_spawn_v2( + execPathPtr, + execPathLen, + argvPtr, + argvLen, + envpPtr, + envpLen, + actions.stdio[0], + actions.stdio[1], + actions.stdio[2], + cwdPtr, + 0, + retPid, + actions.cwd, + ); + } catch (error) { + return hostFsErrno(error); + } finally { + activeSpawnCallContext = null; + } + }, + proc_spawn_v2(execPathPtr, execPathLen, argvPtr, argvLen, envpPtr, envpLen, stdinFd, stdoutFd, stderrFd, cwdPtr, cwdLen, retPid, resolvedCwdOverride) { + const childOverrideHandles = []; + let overrides = null; + try { + const commandPath = readString(execPathPtr, execPathLen); + if (!commandPath) return errnoNoent; + const argv = decodeNullSeparated(readBytes(argvPtr, argvLen)); + const env = parseEnv(readBytes(envpPtr, envpLen)); + const cwd = path().posix.resolve(typeof resolvedCwdOverride === "string" ? resolvedCwdOverride : (cwdLen ? readString(cwdPtr, cwdLen) : ((options && options.cwd) || "/"))); + const module = resolveCommandModule(commandPath, env, cwd); + if (!module) return errnoNoent; + overrides = cloneOpenDescriptorsForSpawn(); + const childCloexecFds = new Set( + [...activeCloexecFds].filter((fd) => overrides.has(fd)), + ); + applyBrowserSpawnFileActions( + overrides, + activeSpawnCallContext?.fileActions || [], + childCloexecFds, + ); + for (const fd of childCloexecFds) { + const handle = overrides.get(fd); + if (handle) closeSyntheticHandle(handle); + overrides.delete(fd); + } + const actionStdioFds = new Set( + (activeSpawnCallContext?.fileActions || []) + .filter((action) => action.command >= 1 && action.command <= 3 && action.fd >= 0 && action.fd <= 2) + .map((action) => action.fd), + ); for (const [childFd, parentFd, expectedKind] of [ [0, stdinFd >>> 0, "read"], [1, stdoutFd >>> 0, "write"], [2, stderrFd >>> 0, "write"], ]) { - const parentHandle = lookupSyntheticFd(parentFd); + if (actionStdioFds.has(childFd)) continue; + if (activeSpawnCallContext && parentFd === 0xffffffff) continue; + const parentHandle = overrides.get(parentFd) || lookupSyntheticFd(parentFd); if (parentFd <= 2 && !parentHandle) continue; - if (!handleMatchesStdio(parentHandle, expectedKind)) return errnoBadf; + if (!handleMatchesStdio(parentHandle, expectedKind)) { + const error = new Error("spawn stdio references an incompatible fd"); + error.code = "EBADF"; + throw error; + } const childHandle = cloneSyntheticHandle(parentHandle); - if (!childHandle) return errnoBadf; - overrides.set(childFd, childHandle); - childOverrideHandles.push(childHandle); + if (!childHandle) { + const error = new Error("spawn stdio fd cannot be duplicated"); + error.code = "EBADF"; + throw error; } - const pid = nextPid++; - const child = { pid, module, commandPath, argv, env, cwd, overrides, childOverrideHandles }; - for (const parentFd of [stdinFd >>> 0, stdoutFd >>> 0, stderrFd >>> 0]) { - if (parentFd > 2) { - closeSyntheticHandle(syntheticFdEntries.get(parentFd)); - } + const replaced = overrides.get(childFd); + if (replaced) closeSyntheticHandle(replaced); + overrides.set(childFd, childHandle); } + childOverrideHandles.push(...new Set(overrides.values())); + const pid = nextPid++; + const parentPid = activePid; + const inheritedPgid = processGroups.get(parentPid) || parentPid; + const requestedPgid = activeSpawnCallContext?.attrFlags & 2 + ? (activeSpawnCallContext.pgroup === 0 ? pid : activeSpawnCallContext.pgroup) + : inheritedPgid; + if (requestedPgid !== pid && !Array.from(processGroups.values()).includes(requestedPgid)) { + const error = new Error("spawn requested an unknown process group"); + error.code = "EPERM"; + throw error; + } + processGroups.set(pid, requestedPgid); + const child = { + pid, + ppid: parentPid, + module, + commandPath, + argv, + env, + cwd, + overrides, + childOverrideHandles, + signalMask: new Set(activeSpawnCallContext?.signalMask || []), + cloexecFds: new Set(), + }; + knownChildren.add(pid); if (pipeHasOpenWriters(overrides.get(0))) { deferredChildren.set(pid, child); } else { runChild(child); } return writeU32(retPid, pid); - } catch { - return errnoNosys; + } catch (error) { + for (const handle of new Set(overrides?.values() || childOverrideHandles)) closeSyntheticHandle(handle); + overrides?.clear(); + if (error?.code === "ERR_AGENTOS_BROWSER_WASM_NETWORK_UNSUPPORTED") { + throw error; + } + return hostFsErrno(error); } }, - proc_waitpid(pid, _options, retStatus, retPid) { + proc_exec(execPathPtr, execPathLen, argvPtr, argvLen, envpPtr, envpLen, cloexecFdsPtr, cloexecFdsLen) { + try { + const command = readString(execPathPtr, execPathLen); + if (!command) return errnoNoent; + const argv = decodeNullSeparated(readBytes(argvPtr, argvLen)); + const env = parseEnv(readBytes(envpPtr, envpLen)); + const cwd = currentGuestCwd(); + const image = loadBrowserExecImage(command, argv); + const closeFds = readExecCloseFds(cloexecFdsPtr, cloexecFdsLen); + throw { + marker: execReplacementMarker, + image: { module: image.module, commandPath: command, argv: image.argv, env, cwd, closeFds }, + }; + } catch (error) { + if (isExecReplacement(error)) throw error; + return hostFsErrno(error); + } + }, + proc_fexec(execFd, argvPtr, argvLen, envpPtr, envpLen, cloexecFdsPtr, cloexecFdsLen) { + try { + const descriptor = execFd >>> 0; + const handle = lookupSyntheticFd(descriptor); + const description = guestFileDescription(handle); + if (!description || description.isDirectory || typeof description.targetFd !== "number") return errnoBadf; + const argv = decodeNullSeparated(readBytes(argvPtr, argvLen)); + const env = parseEnv(readBytes(envpPtr, envpLen)); + const closeFds = readExecCloseFds(cloexecFdsPtr, cloexecFdsLen); + const scriptRef = "/proc/self/fd/" + descriptor; + const value = readExecutableFd(description.targetFd, scriptRef); + if (parseLinuxShebang(value) && closeFds.includes(descriptor)) return errnoNoent; + const image = compileBrowserExecImage(value, scriptRef, argv); + throw { + marker: execReplacementMarker, + image: { + module: image.module, + commandPath: scriptRef, + argv: image.argv, + env, + cwd: currentGuestCwd(), + closeFds, + }, + }; + } catch (error) { + if (isExecReplacement(error)) throw error; + return hostFsErrno(error); + } + }, + proc_waitpid(pid, optionsValue, retStatus, retPid) { const requested = pid >>> 0; - runReadyDeferredChildren(requested === 0xffffffff ? undefined : requested); - const childPid = requested === 0xffffffff - ? exitedChildren.keys().next().value - : requested; - if (!childPid || !exitedChildren.has(childPid)) { - if ((_options >>> 0) !== 0) { + const waitOptions = optionsValue >>> 0; + const waitNoHang = 1; + if ((waitOptions & ~waitNoHang) !== 0) return errnoInval; + const anyChild = requested === 0xffffffff; + if (!anyChild && !knownChildren.has(requested)) return errnoChild; + if (anyChild && knownChildren.size === 0) return errnoChild; + const findExited = () => anyChild ? exitedChildren.keys().next().value : (exitedChildren.has(requested) ? requested : undefined); + let childPid; + for (;;) { + runReadyDeferredChildren(anyChild ? undefined : requested); + childPid = findExited(); + if (childPid !== undefined) break; + if ((waitOptions & waitNoHang) !== 0) { writeU32(retStatus, 0); writeU32(retPid, 0); return errnoSuccess; } - writeU32(retPid, 0); - return errnoChild; + Atomics.wait(wait, 0, 0, 1); } - writeU32(retStatus, exitedChildren.get(childPid) || 0); - writeU32(retPid, childPid); - exitedChildren.delete(childPid); - return errnoSuccess; - }, - fd_dup(fd, retNewFd) { - const descriptor = fd >>> 0; - const handle = lookupSyntheticFd(descriptor) || (descriptor <= 2 - ? { kind: "stdio", targetFd: descriptor, open: true } - : null); - if (!handle) return writeU32(retNewFd, fd); - const cloned = cloneSyntheticHandle(handle); - if (!cloned) return errnoBadf; - return writeU32(retNewFd, allocateSyntheticFd(cloned)); - }, - fd_dup2(oldFd, newFd) { - if (oldFd === newFd) return errnoSuccess; - const handle = lookupSyntheticFd(oldFd >>> 0); - if (!handle) return oldFd <= 2 && newFd <= 2 ? errnoSuccess : errnoBadf; - const cloned = cloneSyntheticHandle(handle); - if (!cloned) return errnoBadf; - replaceSyntheticFd(newFd >>> 0, cloned); - return errnoSuccess; - }, - fd_pipe(retReadFd, retWriteFd) { - const pipe = { - chunks: [], - consumers: new Map(), - producers: new Map(), - readHandleCount: 1, - writeHandleCount: 1, - }; - const readFd = allocateSyntheticFd({ kind: "pipe-read", pipe, open: true, onClose: onPipeHandleClose }); - const writeFd = allocateSyntheticFd({ kind: "pipe-write", pipe, open: true, onClose: onPipeHandleClose }); - writeU32(retReadFd, readFd); - writeU32(retWriteFd, writeFd); - return errnoSuccess; - }, - proc_getpid(retPid) { return writeU32(retPid, 1); }, - proc_getppid(retPid) { return writeU32(retPid, 0); }, - proc_kill() { return errnoNosys; }, - sleep_ms(milliseconds) { - Atomics.wait(wait, 0, 0, milliseconds >>> 0); - return errnoSuccess; - }, - pty_open() { return errnoNosys; }, - proc_sigaction() { return errnoSuccess; }, + const status = exitedChildren.get(childPid); + const signal = status?.signal ?? 0; + writeU32(retStatus, signal === 0 ? (status?.exitCode ?? 0) : 128 + signal); + writeU32(retPid, childPid); + exitedChildren.delete(childPid); + knownChildren.delete(childPid); + processGroups.delete(childPid); + return errnoSuccess; + }, + proc_waitpid_v2(pid, optionsValue, retExitCode, retSignal, retPid, retCoreDumped) { + const requested = pid >>> 0; + const waitOptions = optionsValue >>> 0; + const waitNoHang = 1; + if ((waitOptions & ~waitNoHang) !== 0) return errnoInval; + const anyChild = requested === 0xffffffff; + if (!anyChild && !knownChildren.has(requested)) return errnoChild; + if (anyChild && knownChildren.size === 0) return errnoChild; + const findExited = () => anyChild ? exitedChildren.keys().next().value : (exitedChildren.has(requested) ? requested : undefined); + let childPid; + for (;;) { + runReadyDeferredChildren(anyChild ? undefined : requested); + childPid = findExited(); + if (childPid !== undefined) break; + if ((waitOptions & waitNoHang) !== 0) { + writeU32(retExitCode, 0); + writeU32(retSignal, 0); + writeU32(retPid, 0); + if (retCoreDumped !== undefined) writeU32(retCoreDumped, 0); + return errnoSuccess; + } + Atomics.wait(wait, 0, 0, 1); + } + const status = exitedChildren.get(childPid); + writeU32(retExitCode, status?.exitCode ?? 0); + writeU32(retSignal, status?.signal ?? 0); + writeU32(retPid, childPid); + if (retCoreDumped !== undefined) writeU32(retCoreDumped, 0); + exitedChildren.delete(childPid); + knownChildren.delete(childPid); + processGroups.delete(childPid); + return errnoSuccess; + }, + fd_dup(fd, retNewFd) { + const descriptor = fd >>> 0; + const handle = lookupSyntheticFd(descriptor) || (descriptor <= 2 + ? { kind: "stdio", targetFd: descriptor, open: true } + : null); + if (!handle) return errnoBadf; + const cloned = cloneSyntheticHandle(handle); + if (!cloned) return errnoBadf; + const allocated = allocateSyntheticFd(cloned); + if (allocated == null) { + closeSyntheticHandle(cloned); + return errnoMfile; + } + activeCloexecFds.delete(allocated); + return writeU32(retNewFd, allocated); + }, + fd_getfd(fd, retFlags) { + const descriptor = Number(fd); + if (!Number.isInteger(descriptor) || descriptor < 0) return errnoBadf; + const handle = lookupSyntheticFd(descriptor) || (descriptor <= 2 + ? { kind: "stdio", targetFd: descriptor, open: true } + : null); + if (!handle) return errnoBadf; + return writeU32(retFlags, activeCloexecFds.has(descriptor) ? 1 : 0); + }, + fd_setfd(fd, flags) { + const descriptor = Number(fd); + const value = Number(flags) >>> 0; + if (!Number.isInteger(descriptor) || descriptor < 0) return errnoBadf; + if ((value & ~1) !== 0) return errnoInval; + const handle = lookupSyntheticFd(descriptor) || (descriptor <= 2 + ? { kind: "stdio", targetFd: descriptor, open: true } + : null); + if (!handle) return errnoBadf; + if ((value & 1) !== 0) activeCloexecFds.add(descriptor); + else activeCloexecFds.delete(descriptor); + return errnoSuccess; + }, + fd_dup_min(fd, minFd, retNewFd) { + const source = Number(fd); + const minimum = Number(minFd); + if (!Number.isInteger(source) || source < 0) return errnoBadf; + if (!Number.isInteger(minimum) || minimum < 0) return errnoInval; + const handle = lookupSyntheticFd(source) || (source <= 2 + ? { kind: "stdio", targetFd: source, open: true } + : null); + if (!handle) return errnoBadf; + const cloned = cloneSyntheticHandle(handle); + if (!cloned) return errnoBadf; + const allocated = allocateSyntheticFd(cloned, minimum); + if (allocated == null) { + closeSyntheticHandle(cloned); + return errnoMfile; + } + activeCloexecFds.delete(allocated); + return writeU32(retNewFd, allocated); + }, + fd_dup2(oldFd, newFd) { + const source = oldFd >>> 0; + const destination = newFd >>> 0; + const handle = lookupSyntheticFd(source) || (source <= 2 ? { kind: "stdio", targetFd: source, open: true } : null); + if (!handle) return errnoBadf; + if (source === destination) return errnoSuccess; + const cloned = cloneSyntheticHandle(handle); + if (!cloned) return errnoBadf; + try { + if (!replaceSyntheticFd(destination, cloned)) { + closeSyntheticHandle(cloned); + return errnoMfile; + } + } catch (error) { + closeSyntheticHandle(cloned); + return hostFsErrno(error); + } + activeCloexecFds.delete(destination); + return errnoSuccess; + }, + proc_closefrom(lowFd) { + const minimumFd = lowFd >>> 0; + const entries = activeFdOverrides || syntheticFdEntries; + for (const [fd, handle] of Array.from(entries.entries())) { + if (fd < minimumFd) continue; + closeSyntheticHandle(handle); + entries.delete(fd); + activeCloexecFds.delete(fd); + } + const wasi = activeWasi || parentWasi; + if (wasi && wasi.fdTable) { + for (const [fd, entry] of Array.from(wasi.fdTable.entries())) { + if (fd < minimumFd) continue; + const guestHandle = activeFdOverrides + ? activeFdOverrides.get(fd) + : lookupSyntheticFd(fd); + if (guestHandle?.kind === "guest-file") { + try { + closeSyntheticHandle(guestHandle); + } catch (error) { + return hostFsErrno(error); + } + } else if (typeof entry?.realFd === "number") { + try { + fs().closeSync(entry.realFd); + } catch (error) { + const errno = hostFsErrno(error); + if (errno !== errnoBadf) return errno; + } + } else if (typeof wasi.wasiImport?.fd_close === "function") { + const errno = wasi.wasiImport.fd_close(fd); + if (errno !== undefined && errno !== errnoSuccess && errno !== errnoBadf) return errno; + } + wasi.fdTable.delete(fd); + activeCloexecFds.delete(fd); + } + } + return errnoSuccess; + }, + fd_pipe(retReadFd, retWriteFd) { + const pipe = { + chunks: [], + consumers: new Map(), + producers: new Map(), + readHandleCount: 1, + writeHandleCount: 1, + }; + const readHandle = { kind: "pipe-read", pipe, open: true, baseOnClose: onPipeHandleClose, onClose: onPipeHandleClose }; + const readFd = allocateSyntheticFd(readHandle); + if (readFd == null) return errnoMfile; + const writeHandle = { kind: "pipe-write", pipe, open: true, baseOnClose: onPipeHandleClose, onClose: onPipeHandleClose }; + const writeFd = allocateSyntheticFd(writeHandle); + if (writeFd == null) { + closeSyntheticHandle(readHandle); + return errnoMfile; + } + activeCloexecFds.delete(readFd); + activeCloexecFds.delete(writeFd); + writeU32(retReadFd, readFd); + writeU32(retWriteFd, writeFd); + return errnoSuccess; + }, + proc_getpid(retPid) { return writeU32(retPid, activePid); }, + proc_getppid(retPid) { return writeU32(retPid, activePpid); }, + proc_getpgid(pid, retPgid) { + const targetPid = (pid >>> 0) || activePid; + const pgid = processGroups.get(targetPid); + return pgid === undefined ? errnoSrch : writeU32(retPgid, pgid); + }, + proc_setpgid(pid, pgid) { + const targetPid = (pid >>> 0) || activePid; + const targetPgid = (pgid >>> 0) || targetPid; + if (!processGroups.has(targetPid)) return errnoSrch; + if (targetPgid !== targetPid && !Array.from(processGroups.values()).includes(targetPgid)) return errnoPerm; + processGroups.set(targetPid, targetPgid); + return errnoSuccess; + }, + proc_kill(pid, signal) { + const targetPid = pid >>> 0; + const signalNumber = signal >>> 0; + if (signalNumber > 31) return errnoInval; + if (!knownChildren.has(targetPid) || exitedChildren.has(targetPid)) return errnoSrch; + if (signalNumber === 0) return errnoSuccess; + if (![1, 2, 9, 15].includes(signalNumber)) return errnoNosys; + if (runningChildren.has(targetPid)) return errnoNosys; + const child = deferredChildren.get(targetPid); + if (!child) return errnoSrch; + deferredChildren.delete(targetPid); + for (const handle of child.childOverrideHandles) closeSyntheticHandle(handle); + exitedChildren.set(targetPid, { exitCode: 0, signal: signalNumber }); + Atomics.notify(wait, 0); + return errnoSuccess; + }, + sleep_ms(milliseconds) { + const deadline = Date.now() + (milliseconds >>> 0); + while (Date.now() < deadline) { + // Keep guest sleeps interruptible by V8 termination during kill/dispose. + Atomics.wait(wait, 0, 0, Math.max(1, Math.min(10, deadline - Date.now()))); + } + return errnoSuccess; + }, + pty_open() { return errnoNosys; }, + proc_sigaction() { return errnoNosys; }, + proc_signal_mask_v2(how, setLo, setHi, retOldLo, retOldHi) { + const previous = encodeSignalMask(activeBlockedSignals); + writeU32(retOldLo, previous.lo); + writeU32(retOldHi, previous.hi); + const operation = how >>> 0; + if (operation === 3) return errnoSuccess; + if (operation > 2) return errnoInval; + const requested = decodeSignalMask(setLo, setHi).filter((signal) => signal !== 9 && signal !== 19); + if (operation === 0) { + for (const signal of requested) activeBlockedSignals.add(signal); + } else if (operation === 1) { + for (const signal of requested) activeBlockedSignals.delete(signal); + } else { + activeBlockedSignals.clear(); + for (const signal of requested) activeBlockedSignals.add(signal); + } + return errnoSuccess; + }, }, }, }; @@ -3496,6 +4821,46 @@ export const POLYFILL_CODE_MAP: Record = { "node:os": "module.exports = require('os');", }; +export interface WasiCommandProcessLimits { + maxSpawnFileActions?: number; + maxSpawnFileActionBytes?: number; +} + +function positiveIntegerOrDefault(value: unknown, fallback: number): number { + return Number.isSafeInteger(value) && Number(value) > 0 + ? Number(value) + : fallback; +} + +/** Render a builtin with trusted VM policy captured in its module closure. */ +export function getRuntimePolyfillCode( + moduleName: string, + processLimits?: WasiCommandProcessLimits, +): string | null { + const name = moduleName.replace(/^node:/, ""); + const source = POLYFILL_CODE_MAP[name]; + if (!source || name !== "secure-exec:wasi-command-host") { + return source ?? null; + } + const maxSpawnFileActions = positiveIntegerOrDefault( + processLimits?.maxSpawnFileActions, + 4096, + ); + const maxSpawnFileActionBytes = positiveIntegerOrDefault( + processLimits?.maxSpawnFileActionBytes, + 1024 * 1024, + ); + return source + .replace( + "/* @agentos-process-max-spawn-file-actions */ 4096", + `/* @agentos-process-max-spawn-file-actions */ ${maxSpawnFileActions}`, + ) + .replace( + "/* @agentos-process-max-spawn-file-action-bytes */ 1048576", + `/* @agentos-process-max-spawn-file-action-bytes */ ${maxSpawnFileActionBytes}`, + ); +} + export function exposeCustomGlobal(name: string, value: unknown): void { (globalThis as Record)[name] = value; } diff --git a/packages/runtime-browser/src/wasi-command-bootstrap.ts b/packages/runtime-browser/src/wasi-command-bootstrap.ts index ead7b6a986..e69f5c14b2 100644 --- a/packages/runtime-browser/src/wasi-command-bootstrap.ts +++ b/packages/runtime-browser/src/wasi-command-bootstrap.ts @@ -5,7 +5,7 @@ export interface WasiCommandBootstrapOptions { command: string; /** Additional argv entries for the main command. */ args?: string[]; - /** Command registry used by host_process.proc_spawn for child WASI commands. */ + /** Guest executable paths mapped to WASM sources for host_process.proc_spawn. */ commands?: Record; env?: Record; cwd?: string; @@ -37,7 +37,8 @@ export function createWasiCommandBootstrapScript( const bytesMessagePrefix = options.bytesMessagePrefix ?? ""; const startMessage = options.startMessage ?? ""; const exitMessagePrefix = options.exitMessagePrefix ?? "WASI_EXIT:"; - const errorMessagePrefix = options.errorMessagePrefix ?? "WASI_COMMAND_ERROR:"; + const errorMessagePrefix = + options.errorMessagePrefix ?? "WASI_COMMAND_ERROR:"; return ` (async () => { diff --git a/packages/runtime-browser/src/wasi-polyfill.ts b/packages/runtime-browser/src/wasi-polyfill.ts index 40ee39ab9f..f62158a5b4 100644 --- a/packages/runtime-browser/src/wasi-polyfill.ts +++ b/packages/runtime-browser/src/wasi-polyfill.ts @@ -148,6 +148,7 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = const __agentOSWasiErrnoNoent = 44; const __agentOSWasiErrnoNosys = 52; const __agentOSWasiErrnoNotdir = 54; + const __agentOSWasiErrnoNotempty = 55; const __agentOSWasiErrnoPipe = 64; const __agentOSWasiErrnoRofs = 69; const __agentOSWasiErrnoNotcapable = 76; @@ -162,6 +163,7 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = const __agentOSWasiOpenDirectory = 2; const __agentOSWasiOpenExclusive = 4; const __agentOSWasiOpenTruncate = 8; + const __agentOSWasiFdflagsAppend = 1; const __agentOSWasiRightFdRead = 1n << 1n; const __agentOSWasiRightFdWrite = 1n << 6n; const __agentOSWasiDefaultRightsBase = 0xffffffffffffffffn; @@ -504,6 +506,14 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = } } + _hasReadRights(rights) { + try { + return (BigInt(rights) & __agentOSWasiRightFdRead) !== 0n; + } catch { + return true; + } + } + _writeUint32(ptr, value) { try { this._memoryView().setUint32(Number(ptr) >>> 0, Number(value) >>> 0, true); @@ -887,6 +897,9 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = } _mapFsError(error) { + __agentOSWasiDebug( + \`fs error code=\${String(error?.code ?? "")} message=\${String(error?.message ?? error)}\`, + ); switch (error?.code) { case "EACCES": case "EPERM": @@ -895,6 +908,8 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = return __agentOSWasiErrnoNoent; case "ENOTDIR": return __agentOSWasiErrnoNotdir; + case "ENOTEMPTY": + return __agentOSWasiErrnoNotempty; case "EEXIST": return __agentOSWasiErrnoExist; case "EINVAL": @@ -935,6 +950,7 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = refCount: 1, open: true, readOnly: entry.readOnly === true, + append: entry.append === true, }; } @@ -1488,19 +1504,44 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); } } + const entry = this._descriptorEntry(descriptor); + const localHostPassthrough = + handle.kind === "host-passthrough" && + entry?.kind === "file" && + entry.realFd === handle.targetFd; + const position = handle.append + ? this._measureWasiPhase("appendFstat", () => + Number(__agentOSFs().fstatSync(handle.targetFd).size ?? 0) + ) + : localHostPassthrough + ? (entry.offset ?? 0) + : null; const written = this._measureWasiPhase("writeSync", () => __agentOSFs().writeSync( handle.targetFd, bytes, 0, bytes.length, - null, + position, ) ); + if (localHostPassthrough) { + if (handle.append) { + entry.offset = this._measureWasiPhase("appendFstat", () => + Number(__agentOSFs().fstatSync(handle.targetFd).size ?? 0) + ); + } else { + entry.offset = (entry.offset ?? 0) + written; + } + } return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); } if (handle?.kind === "guest-file" && typeof handle.targetFd === "number") { - const position = handle.append ? null : (handle.position ?? 0); + const position = handle.append + ? this._measureWasiPhase("appendFstat", () => + Number(__agentOSFs().fstatSync(handle.targetFd).size ?? 0) + ) + : (handle.position ?? 0); const written = this._measureWasiPhase("writeSync", () => __agentOSFs().writeSync( handle.targetFd, @@ -1577,7 +1618,11 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = } if (entry.kind === "file") { this._clearStatCache(); - const position = typeof entry.offset === "number" ? entry.offset : null; + const position = entry.append + ? this._measureWasiPhase("appendFstat", () => + Number(__agentOSFs().fstatSync(entry.realFd).size ?? 0) + ) + : (typeof entry.offset === "number" ? entry.offset : null); const written = this._measureWasiPhase("writeSync", () => __agentOSFs().writeSync( entry.realFd, @@ -1587,7 +1632,11 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = position, ) ); - if (typeof entry.offset === "number") { + if (entry.append) { + entry.offset = this._measureWasiPhase("appendFstat", () => + Number(__agentOSFs().fstatSync(entry.realFd).size ?? 0) + ); + } else if (typeof entry.offset === "number") { entry.offset += written; } return this._measureWasiPhase("writeResultPtr", () => this._writeUint32(nwrittenPtr, written)); @@ -1823,6 +1872,11 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = (handle?.kind === "passthrough" || handle?.kind === "host-passthrough") && typeof handle.targetFd === "number" ) { + const localEntry = this._descriptorEntry(descriptor); + const localHostPassthrough = + handle.kind === "host-passthrough" && + localEntry?.kind === "file" && + localEntry.realFd === handle.targetFd; const totalLength = this._boundedReadLength(iovs, iovsLen); const buffer = Buffer.alloc(totalLength); const bytesRead = __agentOSFs().readSync( @@ -1830,8 +1884,11 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = buffer, 0, totalLength, - null, + localHostPassthrough ? (localEntry.offset ?? 0) : null, ); + if (localHostPassthrough) { + localEntry.offset = (localEntry.offset ?? 0) + bytesRead; + } const written = this._writeToIovs(iovs, iovsLen, buffer.subarray(0, bytesRead)); return this._writeUint32(nreadPtr, written); } @@ -2314,8 +2371,12 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = this._clearStatCache(); } const fsConstants = __agentOSFs().constants ?? {}; + const requestedFdFlags = Number(_fdflags) >>> 0; + const append = (requestedFdFlags & __agentOSWasiFdflagsAppend) !== 0; let openFlags = requestedWriteAccess - ? fsConstants.O_RDWR ?? 2 + ? (this._hasReadRights(requestedRightsBase) + ? fsConstants.O_RDWR ?? 2 + : fsConstants.O_WRONLY ?? 1) : fsConstants.O_RDONLY ?? 0; if ((requestedFlags & __agentOSWasiOpenCreate) !== 0) { openFlags |= fsConstants.O_CREAT ?? 64; @@ -2326,6 +2387,9 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = if ((requestedFlags & __agentOSWasiOpenTruncate) !== 0) { openFlags |= fsConstants.O_TRUNC ?? 512; } + if (append) { + openFlags |= fsConstants.O_APPEND ?? 1024; + } if (openDirectory) { openFlags |= fsConstants.O_DIRECTORY ?? 0; } @@ -2343,10 +2407,13 @@ if (typeof globalThis !== "undefined" && typeof globalThis.__agentOSWasiModule = hostPath: fsPath, readOnly: resolved.readOnly === true, realFd, - offset: 0, + offset: append + ? Number(__agentOSFs().fstatSync(realFd).size ?? 0) + : 0, + append, rightsBase: requestedRightsBase & allowedRightsInheriting, rightsInheriting: requestedRightsInheriting & allowedRightsInheriting, - fdFlags: (Number(_fdflags) >>> 0) & 0xffff, + fdFlags: requestedFdFlags & 0xffff, }); }); return this._measureWasiPhase("writeOpenedFd", () => this._writeUint32(openedFdPtr, openedFd)); diff --git a/packages/runtime-browser/src/worker-protocol.ts b/packages/runtime-browser/src/worker-protocol.ts index 433413c1c2..841c36e501 100644 --- a/packages/runtime-browser/src/worker-protocol.ts +++ b/packages/runtime-browser/src/worker-protocol.ts @@ -39,6 +39,10 @@ export type BrowserWorkerExtensionResponse = { export type BrowserWorkerInitPayload = { processConfig?: ProcessConfig; + processLimits?: { + maxSpawnFileActions?: number; + maxSpawnFileActionBytes?: number; + }; osConfig?: OSConfig; filesystem?: "opfs" | "memory"; networkEnabled?: boolean; diff --git a/packages/runtime-browser/src/worker.ts b/packages/runtime-browser/src/worker.ts index bf77c5aba1..be4571e4c3 100644 --- a/packages/runtime-browser/src/worker.ts +++ b/packages/runtime-browser/src/worker.ts @@ -1,13 +1,13 @@ -import { hmac as nobleHmac } from "@noble/hashes/hmac.js"; import { cbc as nobleAesCbc, ctr as nobleAesCtr, gcm as nobleAesGcm, } from "@noble/ciphers/aes.js"; +import { hmac as nobleHmac } from "@noble/hashes/hmac.js"; import { md5, sha1 } from "@noble/hashes/legacy.js"; import { pbkdf2 as noblePbkdf2 } from "@noble/hashes/pbkdf2.js"; -import { sha224, sha256, sha384, sha512 } from "@noble/hashes/sha2.js"; import { scrypt as nobleScrypt } from "@noble/hashes/scrypt.js"; +import { sha224, sha256, sha384, sha512 } from "@noble/hashes/sha2.js"; import { transform } from "sucrase"; import type { BrowserChildProcessPollEvent, @@ -16,11 +16,6 @@ import type { import { createBrowserNetworkAdapter } from "./driver.js"; import { base64ToBytes, toUint8Array } from "./encoding.js"; import { posixErrno } from "./errno.js"; -import { - PROCESS_SIGNAL_NUMBERS, - defaultSignalExitCode, - signalNumberForEvent, -} from "./signals.js"; import type { ExecResult, NetworkAdapter, @@ -36,10 +31,15 @@ import { exposeMutableRuntimeStateGlobal, getIsolateRuntimeSource, getRequireSetupCode, + getRuntimePolyfillCode, isESM, - POLYFILL_CODE_MAP, transformDynamicImport, } from "./runtime.js"; +import { + defaultSignalExitCode, + PROCESS_SIGNAL_NUMBERS, + signalNumberForEvent, +} from "./signals.js"; import { assertBrowserSyncBridgeSupport, type BrowserSyncBridgeErrorPayload, @@ -94,8 +94,9 @@ const MAX_STDIO_MESSAGE_CHARS = 8192; function eventForSignalNumber(signal: number): string { return ( - Object.entries(PROCESS_SIGNAL_NUMBERS).find(([, value]) => value === signal)?.[0] ?? - `SIG${signal}` + Object.entries(PROCESS_SIGNAL_NUMBERS).find( + ([, value]) => value === signal, + )?.[0] ?? `SIG${signal}` ); } @@ -1626,6 +1627,12 @@ function createFsModule(syncBridge: ReturnType) { fstatSync(fd: number) { return statSync(requireOpenFd(fd, "fstat").path); }, + fchmodSync(fd: number, mode: number) { + syncBridge.requestVoid("fs.chmod", [ + requireOpenFd(fd, "fchmod").path, + Number(mode) || 0, + ]); + }, ftruncateSync(fd: number, length = 0) { syncBridge.requestVoid("fs.truncate", [ requireOpenFd(fd, "ftruncate").path, @@ -2032,9 +2039,7 @@ async function initRuntime(payload: BrowserWorkerInitPayload): Promise { exposeCustomGlobal( "_loadPolyfill", makeApplySync((moduleName: string) => { - const name = moduleName.replace(/^node:/, ""); - const polyfillMap = POLYFILL_CODE_MAP as Record; - return polyfillMap[name] ?? null; + return getRuntimePolyfillCode(moduleName, payload.processLimits); }), ); @@ -2411,7 +2416,10 @@ async function initRuntime(payload: BrowserWorkerInitPayload): Promise { exposeCustomGlobal( "_childProcessKill", makeApplySync((sessionId: number, signal: number) => { - syncBridge.requestVoid("child_process.kill", [sessionId, signal]); + return syncBridge.requestJson("child_process.kill", [ + sessionId, + signal, + ]); }), ); @@ -2711,7 +2719,11 @@ function createBrowserProcess(): Record { slaveFd = pair.slaveFd; path = typeof pair.path === "string" ? pair.path : undefined; ptySyncBridge().requestVoid("pty.resize", [ - { fd: slaveFd as number, cols: Math.max(1, columns), rows: Math.max(1, rows) }, + { + fd: slaveFd as number, + cols: Math.max(1, columns), + rows: Math.max(1, rows), + }, ]); if (activeExecutionId && activeProcessRequestId !== null) { postPtyOpened(activeExecutionId, activeProcessRequestId, { @@ -3265,7 +3277,9 @@ function createBrowserProcess(): Record { }, kill(pid: number, signal: string | number = "SIGTERM") { if (pid !== processBridge.pid) { - throw new Error(`process.kill only supports the current browser process pid (${processBridge.pid})`); + throw new Error( + `process.kill only supports the current browser process pid (${processBridge.pid})`, + ); } const event = typeof signal === "number" diff --git a/packages/runtime-browser/tests/browser/fixtures/frontend/converged-conformance-harness.entry.ts b/packages/runtime-browser/tests/browser/fixtures/frontend/converged-conformance-harness.entry.ts index aa11e23683..9f3e201cea 100644 --- a/packages/runtime-browser/tests/browser/fixtures/frontend/converged-conformance-harness.entry.ts +++ b/packages/runtime-browser/tests/browser/fixtures/frontend/converged-conformance-harness.entry.ts @@ -135,6 +135,7 @@ function createEchoCommandExecutor() { let stdout = ""; let stderr = ""; let exitCode = 0; + let exitSignal: number | null = null; let stdin = ""; let resolveWait: (() => void) | undefined; let waitPromise: Promise = Promise.resolve(); @@ -155,7 +156,9 @@ function createEchoCommandExecutor() { return { async wait() { await waitPromise; - return exitCode; + return exitSignal === null + ? exitCode + : { exitCode: null, signal: exitSignal }; }, writeStdin(data: string | Uint8Array) { stdin += typeof data === "string" ? data : new TextDecoder().decode(data); @@ -167,11 +170,12 @@ function createEchoCommandExecutor() { }, kill(signal = 15) { const signalNumber = Number(signal) || 15; - exitCode = 128 + signalNumber; + exitSignal = signalNumber; options.onStdout?.( new TextEncoder().encode(`signal:${signalNumber}\n`), ); resolveWait?.(); + return true; }, }; }, diff --git a/packages/runtime-browser/tests/browser/runtime-driver.spec.ts b/packages/runtime-browser/tests/browser/runtime-driver.spec.ts index bda633dc7e..d578c06a69 100644 --- a/packages/runtime-browser/tests/browser/runtime-driver.spec.ts +++ b/packages/runtime-browser/tests/browser/runtime-driver.spec.ts @@ -1245,14 +1245,15 @@ test("routes browser child_process through the driver command executor", async ( invalidSignalCode = error && error.code; } child.kill("SIGUSR1"); - const asyncCode = await new Promise((resolve, reject) => { + const [asyncCode, asyncSignal] = await new Promise((resolve, reject) => { child.on("error", reject); - child.on("exit", resolve); + child.on("exit", (code, signal) => resolve([code, signal])); }); console.log(JSON.stringify({ syncStatus: sync.status, syncStdout: sync.stdout.trim(), asyncCode, + asyncSignal, asyncStdout: stdout.trim(), invalidSignalCode, })); @@ -1264,7 +1265,8 @@ test("routes browser child_process through the driver command executor", async ( expect(JSON.parse(getLastStdioMessage(result, "stdout"))).toEqual({ syncStatus: 0, syncStdout: "sync-value", - asyncCode: 138, + asyncCode: null, + asyncSignal: "SIGUSR1", asyncStdout: "signal:10", invalidSignalCode: "ERR_UNKNOWN_SIGNAL", }); diff --git a/packages/runtime-browser/tests/runtime-driver/child-process-sync-bridge.test.ts b/packages/runtime-browser/tests/runtime-driver/child-process-sync-bridge.test.ts index 1ac19872b9..e9329f45d4 100644 --- a/packages/runtime-browser/tests/runtime-driver/child-process-sync-bridge.test.ts +++ b/packages/runtime-browser/tests/runtime-driver/child-process-sync-bridge.test.ts @@ -1,7 +1,10 @@ import { afterEach, describe, expect, it } from "vitest"; import type { BrowserChildProcessSpawnRequest } from "../../src/child-process-bridge.js"; import { BrowserRuntimeDriver } from "../../src/runtime-driver.js"; -import { fakeConvergedFactoryOptions } from "./fake-converged-sidecar.js"; +import { + FAKE_CONVERGED_CONFIG, + fakeConvergedFactoryOptions, +} from "./fake-converged-sidecar.js"; import type { CommandExecutor, VirtualFileSystem, @@ -30,6 +33,10 @@ type InitMessage = { id: number; type: "init"; payload: { + processLimits?: { + maxSpawnFileActions?: number; + maxSpawnFileActionBytes?: number; + }; syncBridge: { signalBuffer: SharedArrayBuffer; dataBuffer: SharedArrayBuffer; @@ -58,14 +65,15 @@ class FakeWorker { static syncArgs: unknown[] = [ { command: "echo", - args: ["hi"], - options: { cwd: "/work", env: { HELLO: "world" } }, + args: ["", "hi"], + options: { argv0: "", cwd: "/work", env: { HELLO: "world" } }, } satisfies BrowserChildProcessSpawnRequest, ]; onmessage: WorkerHandler | null = null; onerror: WorkerHandler | null = null; syncResponse: unknown; + initPayload: InitMessage["payload"] | null = null; private syncBridge: InitMessage["payload"]["syncBridge"] | null = null; constructor() { @@ -74,6 +82,7 @@ class FakeWorker { postMessage(message: InitMessage | ExecMessage): void { if (message.type === "init") { + this.initPayload = message.payload; this.syncBridge = message.payload.syncBridge; queueMicrotask(() => { this.onmessage?.({ @@ -192,12 +201,43 @@ describe("browser child_process sync bridge", () => { FakeWorker.syncArgs = [ { command: "echo", - args: ["hi"], - options: { cwd: "/work", env: { HELLO: "world" } }, + args: ["", "hi"], + options: { argv0: "", cwd: "/work", env: { HELLO: "world" } }, } satisfies BrowserChildProcessSpawnRequest, ]; }); + it("propagates trusted converged VM process limits into worker policy", () => { + Object.defineProperty(globalThis, "Worker", { + value: FakeWorker, + configurable: true, + writable: true, + }); + const factoryOptions = fakeConvergedFactoryOptions(); + factoryOptions.convergedSidecar.config = { + ...FAKE_CONVERGED_CONFIG, + limits: { + process: { + maxSpawnFileActions: 7, + maxSpawnFileActionBytes: 321, + }, + }, + } as typeof factoryOptions.convergedSidecar.config; + const driver = new BrowserRuntimeDriver( + createOptions({ + spawn() { + throw new Error("unexpected spawn"); + }, + }), + factoryOptions, + ); + expect(FakeWorker.instances[0]?.initPayload?.processLimits).toEqual({ + maxSpawnFileActions: 7, + maxSpawnFileActionBytes: 321, + }); + driver.dispose(); + }); + it("runs browser child_process requests through the driver command executor", async () => { Object.defineProperty(globalThis, "Worker", { value: FakeWorker, @@ -207,6 +247,7 @@ describe("browser child_process sync bridge", () => { const spawns: Array<{ command: string; args: string[]; + argv0?: string; cwd?: string; env?: Record; }> = []; @@ -215,6 +256,7 @@ describe("browser child_process sync bridge", () => { spawns.push({ command, args, + argv0: options?.argv0, cwd: options?.cwd, env: options?.env, }); @@ -240,7 +282,8 @@ describe("browser child_process sync bridge", () => { expect(spawns).toEqual([ { command: "echo", - args: ["hi"], + args: ["", "hi"], + argv0: "", cwd: "/work", env: { HELLO: "world" }, }, @@ -270,6 +313,7 @@ describe("browser child_process sync bridge", () => { command: "cat", args: [], options: { + argv0: "reader", cwd: "/work", input: { __agentOSType: "bytes", base64: "c3RkaW4=" }, }, @@ -282,11 +326,12 @@ describe("browser child_process sync bridge", () => { spawn(command, args, options) { expect(command).toBe("cat"); expect(args).toEqual([]); + expect(options?.argv0).toBe("reader"); expect(options?.cwd).toBe("/work"); options?.onStdout?.(encoder.encode("stdout")); return { async wait() { - return 0; + return { exitCode: null, signal: 9 }; }, writeStdin(data) { stdinChunks.push(decoder.decode(data as Uint8Array)); @@ -307,6 +352,12 @@ describe("browser child_process sync bridge", () => { expect(stdinChunks).toEqual(["stdin"]); expect(closed).toBe(true); + expect( + JSON.parse(String(FakeWorker.instances[0]?.syncResponse)), + ).toMatchObject({ + code: null, + signal: 9, + }); driver.dispose(); }); diff --git a/packages/runtime-browser/tests/runtime/command-executor-permissions.test.ts b/packages/runtime-browser/tests/runtime/command-executor-permissions.test.ts index e75726907f..90a9bc6084 100644 --- a/packages/runtime-browser/tests/runtime/command-executor-permissions.test.ts +++ b/packages/runtime-browser/tests/runtime/command-executor-permissions.test.ts @@ -14,7 +14,7 @@ function fakeExecutor(): CommandExecutor { } describe("browser command executor permissions", () => { - it("passes command, args, cwd, and env to the child_process permission check", () => { + it("passes command, argv0, args, cwd, and env to the child_process permission check", () => { const inner = fakeExecutor(); const seen: unknown[] = []; const wrapped = wrapCommandExecutor(inner, { @@ -25,6 +25,7 @@ describe("browser command executor permissions", () => { }); wrapped.spawn("tool", ["--flag"], { + argv0: "-tool", cwd: "/workspace", env: { PATH: "/bin" }, }); @@ -33,6 +34,7 @@ describe("browser command executor permissions", () => { { command: "tool", args: ["--flag"], + argv0: "-tool", cwd: "/workspace", env: { PATH: "/bin" }, }, diff --git a/packages/runtime-browser/tests/runtime/process-and-stream-polyfills.test.ts b/packages/runtime-browser/tests/runtime/process-and-stream-polyfills.test.ts new file mode 100644 index 0000000000..bba9e7f60d --- /dev/null +++ b/packages/runtime-browser/tests/runtime/process-and-stream-polyfills.test.ts @@ -0,0 +1,296 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { POLYFILL_CODE_MAP } from "../../src/runtime.js"; + +function loadPolyfill(name: string): T { + const module = { exports: {} as T }; + new Function("module", "exports", "require", POLYFILL_CODE_MAP[name])( + module, + module.exports, + () => { + throw new Error(`unexpected require from ${name}`); + }, + ); + return module.exports; +} + +const globals = [ + "__agentOSEncoding", + "_childProcessSpawnStart", + "_childProcessSpawnSync", + "_childProcessPoll", + "_childProcessStdinWrite", + "_childProcessStdinClose", + "_childProcessKill", +] as const; +const previousGlobals = new Map(); + +afterEach(() => { + for (const name of globals) { + const previous = previousGlobals.get(name); + if (previous === undefined) { + delete (globalThis as Record)[name]; + } else { + (globalThis as Record)[name] = previous; + } + } + previousGlobals.clear(); +}); + +function setGlobal(name: (typeof globals)[number], value: unknown): void { + if (!previousGlobals.has(name)) { + previousGlobals.set(name, (globalThis as Record)[name]); + } + (globalThis as Record)[name] = value; +} + +describe("browser stream polyfill", () => { + it("honors writable backpressure and normal finish/close lifecycle", async () => { + let completeWrite: ((error?: Error) => void) | undefined; + const stream = loadPolyfill<{ + Writable: new ( + options: unknown, + ) => { + on(event: string, listener: (...args: unknown[]) => void): unknown; + write(chunk: Uint8Array, callback?: (error?: Error) => void): boolean; + end(callback?: (error?: Error) => void): void; + }; + }>("stream"); + const writable = new stream.Writable({ + highWaterMark: 1, + write( + _chunk: Uint8Array, + _encoding: unknown, + callback: (error?: Error) => void, + ) { + completeWrite = callback; + }, + }); + const lifecycle: string[] = []; + writable.on("drain", () => lifecycle.push("drain")); + writable.on("finish", () => lifecycle.push("finish")); + writable.on("close", () => lifecycle.push("close")); + + expect(writable.write(new Uint8Array([1]))).toBe(false); + writable.end(() => lifecycle.push("end-callback")); + expect(lifecycle).toEqual([]); + completeWrite?.(); + await new Promise((resolve) => queueMicrotask(resolve)); + expect(lifecycle).toEqual(["drain", "finish", "end-callback", "close"]); + }); + + it("surfaces write-after-end and WHATWG write errors", async () => { + const stream = loadPolyfill<{ + Writable: { + new ( + options?: unknown, + ): { + on(event: string, listener: (...args: unknown[]) => void): unknown; + write(chunk: Uint8Array, callback?: (error?: Error) => void): boolean; + end(callback?: (error?: Error) => void): void; + }; + toWeb(value: unknown): WritableStream; + }; + }>("stream"); + const writable = new stream.Writable(); + writable.on("error", () => {}); + writable.end(); + const afterEnd = new Promise((resolve) => { + expect( + writable.write(new Uint8Array([1]), (error) => resolve(error as Error)), + ).toBe(false); + }); + expect((await afterEnd).message).toMatch(/write after end/); + + const failed = new stream.Writable({ + write( + _chunk: Uint8Array, + _encoding: unknown, + callback: (error: Error) => void, + ) { + callback(new Error("sink failed")); + }, + }); + const writer = stream.Writable.toWeb(failed).getWriter(); + await expect(writer.write(new Uint8Array([1]))).rejects.toThrow( + "sink failed", + ); + }); + + it("preserves queued chunk types and EOF through Readable.toWeb", async () => { + const stream = loadPolyfill<{ + Readable: { + new (): { push(chunk: unknown): boolean }; + toWeb(value: unknown): ReadableStream; + }; + }>("stream"); + const readable = new stream.Readable(); + readable.push("hello"); + readable.push(null); + const reader = stream.Readable.toWeb(readable).getReader(); + expect(await reader.read()).toEqual({ done: false, value: "hello" }); + expect(await reader.read()).toEqual({ done: true, value: undefined }); + }); + + it("keeps PassThrough readable and writable lifecycle coherent", async () => { + const stream = loadPolyfill<{ + PassThrough: new () => { + on(event: string, listener: (...args: unknown[]) => void): unknown; + end(chunk: Uint8Array): void; + }; + }>("stream"); + const pass = new stream.PassThrough(); + const lifecycle: string[] = []; + pass.on("data", (chunk) => + lifecycle.push(`data:${(chunk as Uint8Array)[0]}`), + ); + pass.on("end", () => lifecycle.push("end")); + pass.on("finish", () => lifecycle.push("finish")); + pass.on("close", () => lifecycle.push("close")); + pass.end(new Uint8Array([7])); + await new Promise((resolve) => queueMicrotask(resolve)); + expect(lifecycle).toEqual(["data:7", "end", "finish", "close"]); + }); +}); + +describe("browser child_process polyfill", () => { + it("preserves argv and signal identity and orders output closure before child close", async () => { + const requests: unknown[] = []; + const kills: number[] = []; + const events: unknown[] = [ + { type: "stdout", data: { __agentOSType: "bytes", base64: "aGk=" } }, + { type: "exit", exitCode: 0, signal: 15 }, + ]; + setGlobal("__agentOSEncoding", { + encodeBytesPayload: (value: unknown) => value, + decodeBytesPayload: (value: { base64: string }) => + new Uint8Array(Buffer.from(value.base64, "base64")), + }); + setGlobal("_childProcessSpawnStart", (request: unknown) => { + requests.push(request); + return 41; + }); + setGlobal("_childProcessPoll", () => events.shift() ?? null); + setGlobal("_childProcessStdinWrite", () => undefined); + setGlobal("_childProcessStdinClose", () => undefined); + setGlobal("_childProcessKill", (_sessionId: number, signal: number) => { + kills.push(signal); + return true; + }); + const childProcess = loadPolyfill<{ + spawn( + command: string, + args: string[], + options: unknown, + ): { + spawnargs: string[]; + exitCode: number | null; + signalCode: string | null; + killed: boolean; + stdout: { + on(event: string, listener: (...args: unknown[]) => void): unknown; + }; + stderr: { + on(event: string, listener: (...args: unknown[]) => void): unknown; + }; + stdin: { + end(callback?: (error?: Error) => void): unknown; + write(data: string, callback?: (error?: Error) => void): boolean; + }; + on(event: string, listener: (...args: unknown[]) => void): unknown; + kill(signal?: string): boolean; + }; + }>("child_process"); + const child = childProcess.spawn("/bin/tool", ["", "tail"], { + argv0: "", + cwd: "/work", + env: { ONLY: "value" }, + }); + const lifecycle: string[] = []; + child.stdout.on("data", (chunk) => + lifecycle.push(`stdout:${String(chunk)}`), + ); + child.stdout.on("end", () => lifecycle.push("stdout:end")); + child.stdout.on("close", () => lifecycle.push("stdout:close")); + child.stderr.on("end", () => lifecycle.push("stderr:end")); + child.stderr.on("close", () => lifecycle.push("stderr:close")); + child.on("exit", (code, signal) => + lifecycle.push(`exit:${String(code)}:${String(signal)}`), + ); + const closed = new Promise((resolve) => { + child.on("close", (code, signal) => { + lifecycle.push(`close:${String(code)}:${String(signal)}`); + resolve(); + }); + }); + + expect(child.spawnargs).toEqual(["", "", "tail"]); + expect(child.kill("SIGTERM")).toBe(true); + await closed; + expect(requests).toEqual([ + { + command: "/bin/tool", + args: ["", "tail"], + options: { argv0: "", cwd: "/work", env: { ONLY: "value" } }, + }, + ]); + expect(kills).toEqual([15]); + expect(child.killed).toBe(true); + expect(child.exitCode).toBeNull(); + expect(child.signalCode).toBe("SIGTERM"); + expect(child.kill()).toBe(false); + expect(lifecycle).toEqual([ + "stdout:hi", + "exit:null:SIGTERM", + "stdout:end", + "stdout:close", + "stderr:end", + "stderr:close", + "close:null:SIGTERM", + ]); + + await new Promise((resolve, reject) => + child.stdin.end((error) => (error ? reject(error) : resolve())), + ); + const writeAfterEnd = new Promise((resolve) => { + expect( + child.stdin.write("late", (error) => resolve(error as Error)), + ).toBe(false); + }); + expect((await writeAfterEnd).message).toMatch(/write after end/); + }); + + it("reports spawnSync signal termination separately from exit status", () => { + setGlobal("__agentOSEncoding", { + encodeBytesPayload: (value: unknown) => value, + decodeBytesPayload: (value: unknown) => value, + }); + setGlobal("_childProcessSpawnSync", () => + JSON.stringify({ stdout: "", stderr: "", code: 0, signal: 9 }), + ); + const childProcess = loadPolyfill<{ + spawnSync( + command: string, + args: string[], + options: unknown, + ): { + status: number | null; + signal: string | null; + }; + }>("child_process"); + expect( + childProcess.spawnSync("tool", [], { encoding: "utf8" }), + ).toMatchObject({ + status: null, + signal: "SIGKILL", + }); + setGlobal("_childProcessSpawnSync", () => + JSON.stringify({ stdout: "", stderr: "", code: 0, signal: 6 }), + ); + expect( + childProcess.spawnSync("tool", [], { encoding: "utf8" }), + ).toMatchObject({ + status: null, + signal: "SIGABRT", + }); + }); +}); diff --git a/packages/runtime-browser/tests/runtime/wasi-command-bootstrap.test.ts b/packages/runtime-browser/tests/runtime/wasi-command-bootstrap.test.ts index 8f82dea488..540aa47fe4 100644 --- a/packages/runtime-browser/tests/runtime/wasi-command-bootstrap.test.ts +++ b/packages/runtime-browser/tests/runtime/wasi-command-bootstrap.test.ts @@ -8,8 +8,8 @@ describe("wasi command bootstrap", () => { command: "sh", args: ["-i"], commands: { - echo: "/commands/echo", - ls: "/commands/ls", + "/bin/echo": "/commands/echo", + "/bin/ls": "/commands/ls", }, env: { PATH: "/bin:/usr/bin", diff --git a/packages/runtime-browser/tests/runtime/wasi-command-host.test.ts b/packages/runtime-browser/tests/runtime/wasi-command-host.test.ts new file mode 100644 index 0000000000..dec968c1b9 --- /dev/null +++ b/packages/runtime-browser/tests/runtime/wasi-command-host.test.ts @@ -0,0 +1,1100 @@ +import * as nodePath from "node:path"; +import { readFileSync } from "node:fs"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + getRuntimePolyfillCode, + type WasiCommandProcessLimits, +} from "../../src/runtime.js"; + +type WasiOptions = { + args: string[]; + env: Record; + preopens: Record; +}; + +type HostProcessImports = { + proc_spawn(...args: number[]): number; + proc_spawn_v2(...args: number[]): number; + proc_spawn_v3(...args: number[]): number; + proc_exec(...args: number[]): number; + proc_fexec(...args: number[]): number; + proc_waitpid(...args: number[]): number; + proc_waitpid_v2(...args: number[]): number; + proc_kill(pid: number, signal: number): number; + proc_getpgid(pid: number, retPgid: number): number; + proc_setpgid(pid: number, pgid: number): number; + proc_sigaction(...args: number[]): number; + proc_signal_mask_v2(...args: number[]): number; + fd_dup(fd: number, retNewFd: number): number; + fd_getfd(fd: number, retFlags: number): number; + fd_setfd(fd: number, flags: number): number; + fd_dup_min(fd: number, minFd: number, retNewFd: number): number; + fd_dup2(oldFd: number, newFd: number): number; + fd_pipe(retReadFd: number, retWriteFd: number): number; + proc_closefrom(lowFd: number): number; +}; + +type CommandHost = { + imports: { + host_fs: { + fchmod(fd: number, mode: number): number; + ftruncate(fd: number, length: bigint): number; + }; + host_process: HostProcessImports; + }; + installBlockingStdin(processLike: unknown): CommandHost; + setMemory(memory: WebAssembly.Memory): CommandHost; + setParentWasi(wasi: unknown): CommandHost; +}; + +type CommandHostFactory = (options: { + WASI: new (options: WasiOptions) => unknown; + commands: Record; + cwd?: string; + maxSpawnFileActions?: number; + maxSpawnFileActionBytes?: number; +}) => Promise; + +const EMPTY_WASM = new WebAssembly.Module( + new Uint8Array([0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00]), +); +const MEMORY_WASM = new WebAssembly.Module( + new Uint8Array([ + 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x05, 0x03, 0x01, 0x00, + 0x01, 0x07, 0x0a, 0x01, 0x06, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, + 0x00, + ]), +); +const ERRNO_SUCCESS = 0; +const ERRNO_2BIG = 1; +const ERRNO_BADF = 8; +const ERRNO_CHILD = 10; +const ERRNO_INVAL = 28; +const ERRNO_MFILE = 33; +const ERRNO_NOENT = 44; +const ERRNO_NOSYS = 52; +const ERRNO_PERM = 63; +const ERRNO_SRCH = 71; +const FIRST_SYNTHETIC_FD = 1 << 20; + +let previousWasiHost: unknown; + +afterEach(() => { + if (previousWasiHost === undefined) { + delete (globalThis as typeof globalThis & { __agentOSWasiHost?: unknown }) + .__agentOSWasiHost; + } else { + ( + globalThis as typeof globalThis & { __agentOSWasiHost?: unknown } + ).__agentOSWasiHost = previousWasiHost; + } + previousWasiHost = undefined; + vi.restoreAllMocks(); +}); + +function loadCommandHostFactory( + fsModule: unknown, + processLimits?: WasiCommandProcessLimits, +): CommandHostFactory { + const module = { + exports: {} as { createWasiCommandHost?: CommandHostFactory }, + }; + const source = getRuntimePolyfillCode( + "secure-exec:wasi-command-host", + processLimits, + ); + if (!source) throw new Error("command host polyfill was not found"); + new Function("module", "exports", "require", source)( + module, + module.exports, + (name: string) => { + if (name === "node:fs" || name === "fs") return fsModule; + if (name === "node:path" || name === "path") return nodePath; + throw new Error(`unexpected require: ${name}`); + }, + ); + if (!module.exports.createWasiCommandHost) { + throw new Error("command host factory was not exported"); + } + return module.exports.createWasiCommandHost; +} + +function writeValue( + memory: WebAssembly.Memory, + offset: number, + value: string, +): [number, number] { + const encoded = new TextEncoder().encode(value); + new Uint8Array(memory.buffer).set(encoded, offset); + return [offset, encoded.length]; +} + +function writeList( + memory: WebAssembly.Memory, + offset: number, + values: string[], +): [number, number] { + return writeValue(memory, offset, `${values.join("\0")}\0`); +} + +function readU32(memory: WebAssembly.Memory, offset: number): number { + return new DataView(memory.buffer).getUint32(offset, true); +} + +function writeSpawnActions( + memory: WebAssembly.Memory, + offset: number, + actions: Array<{ + command: number; + fd: number; + sourceFd?: number; + oflag?: number; + mode?: number; + path?: string; + }>, +): [number, number] { + const encoder = new TextEncoder(); + const encoded = actions.map((action) => ({ + action, + path: encoder.encode(action.path || ""), + })); + const length = encoded.reduce( + (total, entry) => total + 24 + entry.path.length, + 0, + ); + const value = new Uint8Array(memory.buffer, offset, length); + const data = new DataView(memory.buffer, offset, length); + let cursor = 0; + for (const { action, path } of encoded) { + data.setUint32(cursor, action.command, true); + data.setInt32(cursor + 4, action.fd, true); + data.setInt32(cursor + 8, action.sourceFd ?? -1, true); + data.setInt32(cursor + 12, action.oflag ?? 0, true); + data.setUint32(cursor + 16, action.mode ?? 0, true); + data.setUint32(cursor + 20, path.length, true); + value.set(path, cursor + 24); + cursor += 24 + path.length; + } + return [offset, length]; +} + +function spawn( + processImports: HostProcessImports, + memory: WebAssembly.Memory, + command: string, + argv: string[], + env: string[], + stdio: [number, number, number] = [0, 1, 2], +): { errno: number; pid: number } { + const [commandPtr, commandLen] = writeValue(memory, 256, command); + const [argvPtr, argvLen] = writeList(memory, 1024, argv); + const [envPtr, envLen] = writeList(memory, 2048, env); + const [cwdPtr, cwdLen] = writeValue(memory, 3072, "/work"); + const retPid = 128; + const errno = processImports.proc_spawn_v2( + commandPtr, + commandLen, + argvPtr, + argvLen, + envPtr, + envLen, + ...stdio, + cwdPtr, + cwdLen, + retPid, + ); + return { errno, pid: readU32(memory, retPid) }; +} + +function spawnV3WithActions( + processImports: HostProcessImports, + memory: WebAssembly.Memory, + actions: Parameters[2], +): number { + const [commandPtr, commandLen] = writeValue(memory, 256, "/bin/actions"); + const [argvPtr, argvLen] = writeList(memory, 1024, ["/bin/actions"]); + const [envPtr, envLen] = writeList(memory, 2048, []); + const [cwdPtr, cwdLen] = writeValue(memory, 3072, "/work"); + const [actionsPtr, actionsLen] = writeSpawnActions(memory, 4096, actions); + return processImports.proc_spawn_v3( + commandPtr, + commandLen, + argvPtr, + argvLen, + envPtr, + envLen, + actionsPtr, + actionsLen, + cwdPtr, + cwdLen, + 0, + 0, + 0, + 0, + 0, + 0, + 128, + ); +} + +describe("browser WASI command host Linux parity", () => { + it("projects unused SSH and rejects it only when that process image executes", async () => { + class FakeWasi { + readonly wasiImport = {}; + readonly fdTable = new Map(); + nextFd = 3; + constructor(readonly options: WasiOptions) {} + } + const sshModule = new WebAssembly.Module( + readFileSync( + new URL("../../../../software/ssh/bin/ssh", import.meta.url), + ), + ); + const imports = WebAssembly.Module.imports(sshModule); + expect(imports).toEqual( + expect.arrayContaining([ + expect.objectContaining({ module: "host_net", name: "net_socket" }), + ]), + ); + + const memory = new WebAssembly.Memory({ initial: 1 }); + const host = await loadCommandHostFactory({})({ + WASI: FakeWasi, + commands: { "/opt/agentos/bin/ssh": sshModule }, + }); + host + .setMemory(memory) + .setParentWasi(new FakeWasi({ args: [], env: {}, preopens: {} })); + + let thrown: unknown; + try { + spawn( + host.imports.host_process, + memory, + "/opt/agentos/bin/ssh", + ["ssh"], + [], + ); + } catch (error) { + thrown = error; + } + expect(thrown).toMatchObject({ + code: "ERR_AGENTOS_BROWSER_WASM_NETWORK_UNSUPPORTED", + }); + }); + + it("applies the same typed browser network boundary to filesystem-loaded exec images", async () => { + const sshBytes = readFileSync( + new URL("../../../../software/ssh/bin/ssh", import.meta.url), + ); + const fsModule = { + statSync: () => ({ + isFile: () => true, + mode: 0o100755, + size: sshBytes.byteLength, + }), + readFileSync: () => sshBytes, + }; + const memory = new WebAssembly.Memory({ initial: 1 }); + let processImports: HostProcessImports; + class FakeWasi { + readonly wasiImport = {}; + readonly fdTable = new Map(); + nextFd = 3; + constructor(readonly options: WasiOptions) {} + start(): number { + if (this.options.args[0] === "old-image") { + processImports.proc_exec(4096, 12, 4352, 13, 4608, 1); + } + return 0; + } + } + const parent = new FakeWasi({ + args: ["old-image"], + env: {}, + preopens: { "/": "/work" }, + }); + const host = await loadCommandHostFactory(fsModule)({ + WASI: FakeWasi, + commands: {}, + cwd: "/work", + }); + processImports = host.imports.host_process; + host.setMemory(memory).setParentWasi(parent); + writeValue(memory, 4096, "/dynamic/ssh"); + writeList(memory, 4352, ["ssh", "host"]); + writeList(memory, 4608, []); + + let thrown: unknown; + try { + parent.start({ exports: { memory } } as unknown as WebAssembly.Instance); + } catch (error) { + thrown = error; + } + expect(thrown).toMatchObject({ + code: "ERR_AGENTOS_BROWSER_WASM_NETWORK_UNSUPPORTED", + }); + }); + + it("keeps the checked-in command spawn/wait ABI and its raw exit status", async () => { + class FakeWasi { + readonly wasiImport = {}; + readonly fdTable = new Map(); + nextFd = 3; + constructor(readonly options: WasiOptions) {} + start(): number { + return 137; + } + } + const memory = new WebAssembly.Memory({ initial: 1 }); + const host = await loadCommandHostFactory({})({ + WASI: FakeWasi, + commands: { "/bin/legacy": EMPTY_WASM }, + cwd: "/work", + }); + host + .setMemory(memory) + .setParentWasi(new FakeWasi({ args: [], env: {}, preopens: {} })); + const processImports = host.imports.host_process; + const [argvPtr, argvLen] = writeList(memory, 1024, ["/bin/legacy", "arg"]); + const [envPtr, envLen] = writeList(memory, 2048, ["ONLY=value"]); + const [cwdPtr, cwdLen] = writeValue(memory, 3072, "/work"); + + expect( + processImports.proc_spawn( + argvPtr, + argvLen, + envPtr, + envLen, + 0, + 1, + 2, + cwdPtr, + cwdLen, + 128, + ), + ).toBe(ERRNO_SUCCESS); + const pid = readU32(memory, 128); + expect(processImports.proc_waitpid(pid, 0, 72, 80)).toBe(ERRNO_SUCCESS); + expect(readU32(memory, 72)).toBe(137); + expect(readU32(memory, 80)).toBe(pid); + }); + + it("preserves argv0, empty argv entries, exact env, and executable path resolution", async () => { + const starts: WasiOptions[] = []; + class FakeWasi { + readonly wasiImport = {}; + readonly fdTable = new Map(); + nextFd = 3; + constructor(readonly options: WasiOptions) {} + start(_instance: WebAssembly.Instance): number { + starts.push(this.options); + return 0; + } + } + const memory = new WebAssembly.Memory({ initial: 1 }); + const host = await loadCommandHostFactory({})({ + WASI: FakeWasi, + commands: { "/custom/bin/tool": EMPTY_WASM }, + cwd: "/work", + }); + host + .setMemory(memory) + .setParentWasi(new FakeWasi({ args: [], env: {}, preopens: {} })); + const processImports = host.imports.host_process; + + expect( + spawn( + processImports, + memory, + "/custom/bin/tool", + ["-tool", "", "tail", ""], + ["ONLY=value"], + ), + ).toMatchObject({ errno: ERRNO_SUCCESS }); + expect(starts[0]).toMatchObject({ + args: ["-tool", "", "tail", ""], + env: { ONLY: "value" }, + preopens: { "/": "/work" }, + }); + expect(starts[0]?.env).not.toHaveProperty("PATH"); + + expect( + spawn(processImports, memory, "tool", ["tool"], ["PATH=/custom/bin"]), + ).toMatchObject({ errno: ERRNO_SUCCESS }); + expect( + spawn( + processImports, + memory, + "/wrong/tool", + ["tool"], + ["PATH=/custom/bin"], + ), + ).toMatchObject({ errno: ERRNO_NOENT }); + expect( + spawn(processImports, memory, "tool", ["tool"], ["PATH=/nowhere"]), + ).toMatchObject({ errno: ERRNO_NOENT }); + expect(spawn(processImports, memory, "", [""], [])).toMatchObject({ + errno: ERRNO_NOENT, + }); + }); + + it("replaces PID 1 without resuming the old image and inherits its open fds", async () => { + const starts: WasiOptions[] = []; + const memory = new WebAssembly.Memory({ initial: 1 }); + let processImports: HostProcessImports; + let oldImageResumed = false; + let replacementWasi: FakeWasi | undefined; + class FakeWasi { + readonly wasiImport = {}; + readonly fdTable = new Map(); + nextFd = 3; + constructor(readonly options: WasiOptions) {} + start(_instance?: WebAssembly.Instance): number { + starts.push(this.options); + if (this.options.args[0] === "old-image") { + processImports.proc_exec(4096, 16, 4352, 7, 4608, 11); + oldImageResumed = true; + return 99; + } + replacementWasi = this; + return 23; + } + } + const parent = new FakeWasi({ + args: ["old-image"], + env: { OLD: "value" }, + preopens: { "/": "/work" }, + }); + const inheritedEntry = { kind: "file", realFd: 42, offset: 7 }; + parent.fdTable.set(5, inheritedEntry); + parent.nextFd = 9; + const host = await loadCommandHostFactory({})({ + WASI: FakeWasi, + commands: { "/custom/bin/tool": MEMORY_WASM }, + cwd: "/work", + }); + processImports = host.imports.host_process; + host.setMemory(memory).setParentWasi(parent); + writeValue(memory, 4096, "/custom/bin/tool"); + writeList(memory, 4352, ["", "", "tail"]); + writeList(memory, 4608, ["ONLY=value"]); + + const exitCode = parent.start({ + exports: { memory }, + } as unknown as WebAssembly.Instance); + + expect(exitCode).toBe(23); + expect(oldImageResumed).toBe(false); + expect(starts[1]).toEqual({ + returnOnExit: true, + args: ["", "", "tail"], + env: { ONLY: "value" }, + preopens: {}, + }); + expect(replacementWasi?.fdTable.get(5)).toBe(inheritedEntry); + expect(replacementWasi?.nextFd).toBe(9); + }); + + it("keeps a child PID across exec and closes only FD_CLOEXEC descriptors", async () => { + const memory = new WebAssembly.Memory({ initial: 1 }); + let processImports: HostProcessImports; + let oldImageResumed = false; + let replacementWasi: FakeWasi | undefined; + class FakeWasi { + readonly wasiImport = {}; + readonly fdTable = new Map(); + constructor(readonly options: WasiOptions) {} + start(instance: WebAssembly.Instance): number { + if (this.options.args[0] === "old-child") { + const childMemory = instance.exports.memory as WebAssembly.Memory; + const ordinaryEntry = { kind: "file", realFd: 51 }; + const indistinguishableCloexecEntry = { kind: "file", realFd: 52 }; + this.fdTable.set(5, ordinaryEntry); + this.fdTable.set(6, indistinguishableCloexecEntry); + writeValue(childMemory, 4096, "/bin/new-child"); + writeList(childMemory, 4352, ["new-argv0"]); + writeList(childMemory, 4608, []); + new DataView(childMemory.buffer).setUint32(4864, 6, true); + processImports.proc_exec(4096, 14, 4352, 10, 4608, 1, 4864, 1); + oldImageResumed = true; + return 99; + } + replacementWasi = this; + return 17; + } + } + const host = await loadCommandHostFactory({})({ + WASI: FakeWasi, + commands: { + "/bin/old-child": MEMORY_WASM, + "/bin/new-child": MEMORY_WASM, + }, + cwd: "/work", + }); + processImports = host.imports.host_process; + host + .setMemory(memory) + .setParentWasi(new FakeWasi({ args: [], env: {}, preopens: {} })); + + const child = spawn( + processImports, + memory, + "/bin/old-child", + ["old-child"], + [], + ); + expect(child.errno).toBe(ERRNO_SUCCESS); + expect(oldImageResumed).toBe(false); + expect(processImports.proc_waitpid_v2(child.pid, 0, 72, 76, 80, 88)).toBe( + ERRNO_SUCCESS, + ); + expect(readU32(memory, 72)).toBe(17); + expect(readU32(memory, 80)).toBe(child.pid); + expect(replacementWasi?.options.args).toEqual(["new-argv0"]); + expect(replacementWasi?.options.env).toEqual({}); + expect(replacementWasi?.fdTable.has(5)).toBe(true); + expect(replacementWasi?.fdTable.has(6)).toBe(false); + }); + + it("executes an unlinked shebang fd through /proc and rejects script FD_CLOEXEC", async () => { + const script = new TextEncoder().encode("#!/bin/sh\n"); + const starts: WasiOptions[] = []; + const memory = new WebAssembly.Memory({ initial: 1 }); + let processImports: HostProcessImports; + let oldImageResumed = false; + class FakeWasi { + readonly wasiImport = {}; + readonly fdTable = new Map(); + nextFd = 8; + constructor(readonly options: WasiOptions) {} + start(instance: WebAssembly.Instance): number { + starts.push(this.options); + if (this.options.args[0] === "old-image") { + const childMemory = instance.exports.memory as WebAssembly.Memory; + writeList(childMemory, 4352, ["discarded", "script-argument"]); + writeList(childMemory, 4608, []); + processImports.proc_fexec(7, 4352, 26, 4608, 1, 4864, 0); + oldImageResumed = true; + return 99; + } + return 0; + } + } + const parent = new FakeWasi({ args: ["old-image"], env: {}, preopens: {} }); + parent.fdTable.set(7, { kind: "file", realFd: 91, offset: 123 }); + const host = await loadCommandHostFactory({ + fstatSync: () => ({ + size: script.length, + mode: 0o100755, + isFile: () => true, + }), + readSync: ( + _fd: number, + target: Uint8Array, + offset: number, + length: number, + position: number, + ) => { + expect(position).toBe(0); + target.set(script.subarray(0, length), offset); + return length; + }, + })({ + WASI: FakeWasi, + commands: { "/bin/sh": MEMORY_WASM }, + cwd: "/work", + }); + processImports = host.imports.host_process; + host.setMemory(memory).setParentWasi(parent); + writeList(memory, 4352, ["discarded", "script-argument"]); + writeList(memory, 4608, []); + new DataView(memory.buffer).setUint32(4864, 7, true); + expect(processImports.proc_fexec(7, 4352, 26, 4608, 1, 4864, 1)).toBe( + ERRNO_NOENT, + ); + + const exitCode = parent.start({ + exports: { memory }, + } as unknown as WebAssembly.Instance); + expect(exitCode).toBe(0); + expect(oldImageResumed).toBe(false); + expect(starts[1]?.args).toEqual([ + "/bin/sh", + "/proc/self/fd/7", + "script-argument", + ]); + expect(parent.fdTable.get(7)).toMatchObject({ offset: 123 }); + }); + + it("implements WNOHANG, invalid wait options, and truthful signal identity", async () => { + class FakeWasi { + readonly wasiImport = {}; + readonly fdTable = new Map(); + constructor(readonly options: WasiOptions) {} + start(): number { + return 0; + } + } + const memory = new WebAssembly.Memory({ initial: 1 }); + const host = await loadCommandHostFactory({})({ + WASI: FakeWasi, + commands: { "/bin/tool": EMPTY_WASM }, + }); + host + .setMemory(memory) + .setParentWasi(new FakeWasi({ args: [], env: {}, preopens: {} })); + const processImports = host.imports.host_process; + expect(processImports.fd_pipe(64, 68)).toBe(ERRNO_SUCCESS); + const readFd = readU32(memory, 64); + const writeFd = readU32(memory, 68); + expect(processImports.fd_dup(writeFd, 84)).toBe(ERRNO_SUCCESS); + const duplicatedWriteFd = readU32(memory, 84); + expect(processImports.proc_closefrom(duplicatedWriteFd)).toBe( + ERRNO_SUCCESS, + ); + expect(processImports.fd_dup(writeFd, 84)).toBe(ERRNO_SUCCESS); + expect(processImports.proc_closefrom(readU32(memory, 84))).toBe( + ERRNO_SUCCESS, + ); + const child = spawn( + processImports, + memory, + "/bin/tool", + ["tool"], + [], + [readFd, 1, 2], + ); + expect(child.errno).toBe(ERRNO_SUCCESS); + + expect(processImports.proc_waitpid_v2(child.pid, 2, 72, 76, 80)).toBe( + ERRNO_INVAL, + ); + expect(processImports.proc_waitpid_v2(child.pid, 1, 72, 76, 80, 88)).toBe( + ERRNO_SUCCESS, + ); + expect(readU32(memory, 80)).toBe(0); + expect(readU32(memory, 88)).toBe(0); + expect(processImports.proc_kill(child.pid, 0)).toBe(ERRNO_SUCCESS); + expect(processImports.proc_kill(child.pid, 12)).toBe(ERRNO_NOSYS); + expect(processImports.proc_kill(child.pid, 15)).toBe(ERRNO_SUCCESS); + expect(processImports.proc_waitpid_v2(child.pid, 0, 72, 76, 80, 88)).toBe( + ERRNO_SUCCESS, + ); + expect(readU32(memory, 72)).toBe(0); + expect(readU32(memory, 76)).toBe(15); + expect(readU32(memory, 80)).toBe(child.pid); + expect(readU32(memory, 88)).toBe(0); + expect(processImports.proc_waitpid_v2(child.pid, 0, 72, 76, 80)).toBe( + ERRNO_CHILD, + ); + expect(processImports.proc_kill(child.pid, 0)).toBe(ERRNO_SRCH); + expect(processImports.proc_sigaction()).toBe(ERRNO_NOSYS); + expect(processImports.proc_closefrom(writeFd)).toBe(ERRNO_SUCCESS); + }); + + it("keeps the host signal mask authoritative and never blocks SIGKILL or SIGSTOP", async () => { + class FakeWasi { + readonly wasiImport = {}; + readonly fdTable = new Map(); + constructor(readonly options: WasiOptions) {} + start(): number { + return 0; + } + } + const memory = new WebAssembly.Memory({ initial: 1 }); + const host = await loadCommandHostFactory({})({ + WASI: FakeWasi, + commands: {}, + }); + host + .setMemory(memory) + .setParentWasi(new FakeWasi({ args: [], env: {}, preopens: {} })); + const processImports = host.imports.host_process; + const sigterm = 1 << (15 - 1); + const sigkill = 1 << (9 - 1); + const sigstop = 1 << (19 - 1); + + expect( + processImports.proc_signal_mask_v2( + 0, + sigterm | sigkill | sigstop, + 0, + 72, + 76, + ), + ).toBe(ERRNO_SUCCESS); + expect(readU32(memory, 72)).toBe(0); + expect(processImports.proc_signal_mask_v2(3, 0, 0, 72, 76)).toBe( + ERRNO_SUCCESS, + ); + expect(readU32(memory, 72)).toBe(sigterm); + expect(readU32(memory, 76)).toBe(0); + + expect(processImports.proc_signal_mask_v2(1, sigterm, 0, 72, 76)).toBe( + ERRNO_SUCCESS, + ); + expect(readU32(memory, 72)).toBe(sigterm); + expect(processImports.proc_signal_mask_v2(3, 0, 0, 72, 76)).toBe( + ERRNO_SUCCESS, + ); + expect(readU32(memory, 72)).toBe(0); + expect(processImports.proc_signal_mask_v2(99, 0, 0, 72, 76)).toBe( + ERRNO_INVAL, + ); + }); + + it("reports and validates process groups through the host imports", async () => { + class FakeWasi { + readonly wasiImport = {}; + readonly fdTable = new Map(); + constructor(readonly options: WasiOptions) {} + start(): number { + return 0; + } + } + const memory = new WebAssembly.Memory({ initial: 1 }); + const host = await loadCommandHostFactory({})({ + WASI: FakeWasi, + commands: {}, + }); + host + .setMemory(memory) + .setParentWasi(new FakeWasi({ args: [], env: {}, preopens: {} })); + const processImports = host.imports.host_process; + + expect(processImports.proc_getpgid(0, 72)).toBe(ERRNO_SUCCESS); + expect(readU32(memory, 72)).toBe(1); + expect(processImports.proc_setpgid(0, 0)).toBe(ERRNO_SUCCESS); + expect(processImports.proc_getpgid(1, 72)).toBe(ERRNO_SUCCESS); + expect(readU32(memory, 72)).toBe(1); + expect(processImports.proc_setpgid(1, 777)).toBe(ERRNO_PERM); + expect(processImports.proc_getpgid(777, 72)).toBe(ERRNO_SRCH); + }); + + it("applies ordered spawn actions without changing the parent fd table", async () => { + const childTables: Array>> = []; + class FakeWasi { + readonly wasiImport = { fd_close: () => ERRNO_SUCCESS }; + readonly fdTable = new Map>(); + nextFd = 3; + constructor(readonly options: WasiOptions) {} + start(): number { + const inherited = this.fdTable.get(42); + if (inherited) inherited.offset = 29; + childTables.push(new Map(this.fdTable)); + return 0; + } + } + const memory = new WebAssembly.Memory({ initial: 1 }); + const parent = new FakeWasi({ args: [], env: {}, preopens: {} }); + parent.fdTable.set(5, { + kind: "file", + realFd: 42, + offset: 7, + readOnly: true, + rightsBase: 1n << 1n, + }); + const host = await loadCommandHostFactory({})({ + WASI: FakeWasi, + commands: { "/bin/actions": EMPTY_WASM }, + cwd: "/work", + }); + host.setMemory(memory).setParentWasi(parent); + const processImports = host.imports.host_process; + const [commandPtr, commandLen] = writeValue(memory, 256, "/bin/actions"); + const [argvPtr, argvLen] = writeList(memory, 1024, ["/bin/actions"]); + const [envPtr, envLen] = writeList(memory, 2048, []); + const [cwdPtr, cwdLen] = writeValue(memory, 3072, "/work"); + const [actionsPtr, actionsLen] = writeSpawnActions(memory, 4096, [ + { command: 2, fd: 42, sourceFd: 5 }, + { command: 1, fd: 5 }, + ]); + + expect( + processImports.proc_spawn_v3( + commandPtr, + commandLen, + argvPtr, + argvLen, + envPtr, + envLen, + actionsPtr, + actionsLen, + cwdPtr, + cwdLen, + 0, + 0, + 0, + 0, + 0, + 0, + 128, + ), + ).toBe(ERRNO_SUCCESS); + expect(childTables).toHaveLength(1); + expect(childTables[0]?.has(5)).toBe(false); + expect(childTables[0]?.get(42)).toMatchObject({ realFd: 42, offset: 29 }); + expect(parent.fdTable.has(5)).toBe(true); + expect(parent.fdTable.get(5)?.offset).toBe(29); + }); + + it("enforces trusted spawn action limits despite hostile guest overrides", async () => { + class FakeWasi { + readonly wasiImport = { fd_close: () => ERRNO_SUCCESS }; + readonly fdTable = new Map>(); + nextFd = 3; + constructor(readonly options: WasiOptions) {} + start(): number { + return 0; + } + } + const messages: string[] = []; + const write = vi.spyOn(process.stderr, "write").mockImplementation((( + chunk: string | Uint8Array, + ) => { + messages.push(String(chunk)); + return true; + }) as typeof process.stderr.write); + + const smallMemory = new WebAssembly.Memory({ initial: 1 }); + const countBounded = await loadCommandHostFactory( + {}, + { + maxSpawnFileActions: 1, + maxSpawnFileActionBytes: 128, + }, + )({ + WASI: FakeWasi, + commands: { "/bin/actions": EMPTY_WASM }, + maxSpawnFileActions: 44_000, + maxSpawnFileActionBytes: 1_100_000, + }); + countBounded + .setMemory(smallMemory) + .setParentWasi(new FakeWasi({ args: [], env: {}, preopens: {} })); + expect( + spawnV3WithActions(countBounded.imports.host_process, smallMemory, [ + { command: 1, fd: 10 }, + { command: 1, fd: 11 }, + ]), + ).toBe(ERRNO_2BIG); + expect(messages.join("")).toContain("limits.process.maxSpawnFileActions"); + expect(messages.join("")).toContain( + "near limits.process.maxSpawnFileActions", + ); + + messages.length = 0; + const bytesBounded = await loadCommandHostFactory( + {}, + { + maxSpawnFileActions: 10, + maxSpawnFileActionBytes: 23, + }, + )({ + WASI: FakeWasi, + commands: { "/bin/actions": EMPTY_WASM }, + maxSpawnFileActions: 44_000, + maxSpawnFileActionBytes: 1_100_000, + }); + bytesBounded + .setMemory(smallMemory) + .setParentWasi(new FakeWasi({ args: [], env: {}, preopens: {} })); + expect( + spawnV3WithActions(bytesBounded.imports.host_process, smallMemory, [ + { command: 1, fd: 10 }, + ]), + ).toBe(ERRNO_2BIG); + expect(messages.join("")).toContain( + "limits.process.maxSpawnFileActionBytes", + ); + + messages.length = 0; + const largeMemory = new WebAssembly.Memory({ initial: 20 }); + const aboveDefaults = Array.from({ length: 43_700 }, () => ({ + command: 6, + fd: 1_000_000, + })); + const aboveDefaultCount = aboveDefaults.slice(0, 4097); + const defaults = await loadCommandHostFactory({})({ + WASI: FakeWasi, + commands: { "/bin/actions": EMPTY_WASM }, + }); + defaults + .setMemory(largeMemory) + .setParentWasi(new FakeWasi({ args: [], env: {}, preopens: {} })); + expect( + spawnV3WithActions( + defaults.imports.host_process, + largeMemory, + aboveDefaultCount, + ), + ).toBe(ERRNO_2BIG); + expect( + spawnV3WithActions( + defaults.imports.host_process, + largeMemory, + aboveDefaults, + ), + ).toBe(ERRNO_2BIG); + const raised = await loadCommandHostFactory( + {}, + { + maxSpawnFileActions: 44_000, + maxSpawnFileActionBytes: 1_100_000, + }, + )({ + WASI: FakeWasi, + commands: { "/bin/actions": EMPTY_WASM }, + }); + raised + .setMemory(largeMemory) + .setParentWasi(new FakeWasi({ args: [], env: {}, preopens: {} })); + expect( + spawnV3WithActions( + raised.imports.host_process, + largeMemory, + aboveDefaults, + ), + ).toBe(ERRNO_SUCCESS); + expect(messages.join("")).toContain( + "near limits.process.maxSpawnFileActions", + ); + expect(messages.join("")).toContain( + "near limits.process.maxSpawnFileActionBytes", + ); + write.mockRestore(); + }); + + it("keeps child closefrom and inherited CLOEXEC state isolated from the parent", async () => { + let processImports: HostProcessImports; + const childFds: number[][] = []; + class FakeWasi { + readonly wasiImport = { fd_close: () => ERRNO_SUCCESS }; + readonly fdTable = new Map>(); + nextFd = 3; + constructor(readonly options: WasiOptions) {} + start(): number { + childFds.push([...this.fdTable.keys()]); + expect(processImports.proc_closefrom(5)).toBe(ERRNO_SUCCESS); + return 0; + } + } + const memory = new WebAssembly.Memory({ initial: 1 }); + const parent = new FakeWasi({ args: [], env: {}, preopens: {} }); + parent.fdTable.set(5, { kind: "file", realFd: 43, offset: 0 }); + const host = await loadCommandHostFactory({})({ + WASI: FakeWasi, + commands: { "/bin/isolation": EMPTY_WASM }, + cwd: "/work", + }); + host.setMemory(memory).setParentWasi(parent); + processImports = host.imports.host_process; + expect(processImports.fd_setfd(5, 1)).toBe(ERRNO_SUCCESS); + expect( + spawn(processImports, memory, "/bin/isolation", ["isolation"], []).errno, + ).toBe(ERRNO_SUCCESS); + expect(childFds[0]).not.toContain(5); + expect(parent.fdTable.has(5)).toBe(true); + expect(processImports.fd_getfd(5, 84)).toBe(ERRNO_SUCCESS); + expect(readU32(memory, 84)).toBe(1); + }); + + it("shares open-file offsets, closes ordinary and synthetic fds, and reuses bounded fds", async () => { + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const closes: number[] = []; + const chmods: Array<[number, number]> = []; + const truncates: Array<[number, number]> = []; + const fsModule = { + closeSync(fd: number) { + closes.push(fd); + }, + fchmodSync(fd: number, mode: number) { + chmods.push([fd, mode]); + }, + ftruncateSync(fd: number, length: number) { + truncates.push([fd, length]); + }, + }; + class FakeWasi { + readonly wasiImport = { fd_close: () => ERRNO_SUCCESS }; + readonly fdTable = new Map>(); + constructor(readonly options: WasiOptions) {} + start(): number { + return 0; + } + } + const memory = new WebAssembly.Memory({ initial: 1 }); + const parent = new FakeWasi({ args: [], env: {}, preopens: {} }); + parent.fdTable.set(5, { + kind: "file", + realFd: 42, + offset: 7, + readOnly: true, + rightsBase: 1n << 1n, + }); + const host = await loadCommandHostFactory(fsModule)({ + WASI: FakeWasi, + commands: {}, + }); + host.setMemory(memory).setParentWasi(parent); + previousWasiHost = ( + globalThis as typeof globalThis & { __agentOSWasiHost?: unknown } + ).__agentOSWasiHost; + host.installBlockingStdin({ stdin: { read: () => null } }); + const processImports = host.imports.host_process; + + expect(host.imports.host_fs.fchmod(5, 0o10644)).toBe(ERRNO_SUCCESS); + expect(chmods).toEqual([[42, 0o644]]); + expect(host.imports.host_fs.ftruncate(5, 2n)).toBe(ERRNO_INVAL); + expect(host.imports.host_fs.ftruncate(1, 2n)).toBe(ERRNO_INVAL); + expect(host.imports.host_fs.ftruncate(999, 2n)).toBe(ERRNO_BADF); + + expect(processImports.fd_dup(5, 84)).toBe(ERRNO_SUCCESS); + const duplicateFd = readU32(memory, 84); + const lookup = ( + globalThis as typeof globalThis & { + __agentOSWasiHost: { lookupFdHandle(fd: number): { position: number } }; + } + ).__agentOSWasiHost.lookupFdHandle; + const duplicate = lookup(duplicateFd); + duplicate.position = 29; + expect(lookup(5).position).toBe(29); + expect(parent.fdTable.get(5)?.offset).toBe(29); + expect(parent.fdTable.get(duplicateFd)?.offset).toBe(29); + + for (let index = 1; index < 4096; index += 1) { + expect(processImports.fd_dup(5, 84)).toBe(ERRNO_SUCCESS); + } + expect(processImports.fd_dup(5, 84)).toBe(ERRNO_MFILE); + expect(warn).toHaveBeenCalledWith( + expect.stringContaining("maxSyntheticFds"), + ); + expect(processImports.proc_closefrom(5)).toBe(ERRNO_SUCCESS); + expect(closes).toEqual([42]); + expect(parent.fdTable.has(5)).toBe(false); + expect(processImports.fd_dup(duplicateFd, 84)).toBe(ERRNO_BADF); + + parent.fdTable.set(5, { kind: "file", realFd: 43, offset: 0 }); + expect(processImports.fd_dup(5, 84)).toBe(ERRNO_SUCCESS); + expect(readU32(memory, 84)).toBe(FIRST_SYNTHETIC_FD); + expect(processImports.proc_closefrom(FIRST_SYNTHETIC_FD)).toBe( + ERRNO_SUCCESS, + ); + expect(processImports.fd_dup_min(5, FIRST_SYNTHETIC_FD + 100, 84)).toBe( + ERRNO_SUCCESS, + ); + expect(readU32(memory, 84)).toBe(FIRST_SYNTHETIC_FD + 100); + expect(processImports.fd_dup_min(5, -1, 84)).toBe(ERRNO_INVAL); + expect(processImports.proc_closefrom(FIRST_SYNTHETIC_FD + 100)).toBe( + ERRNO_SUCCESS, + ); + expect(processImports.fd_dup2(5, 3)).toBe(ERRNO_SUCCESS); + expect(processImports.proc_closefrom(5)).toBe(ERRNO_SUCCESS); + expect(closes).toEqual([42]); + expect(host.imports.host_fs.ftruncate(3, 3n)).toBe(ERRNO_SUCCESS); + expect(truncates).toEqual([[43, 3]]); + expect(processImports.proc_closefrom(3)).toBe(ERRNO_SUCCESS); + expect(closes).toEqual([42, 43]); + }); +}); diff --git a/packages/runtime-core/commands/[ b/packages/runtime-core/commands/[ deleted file mode 100644 index 321231e0c4..0000000000 Binary files a/packages/runtime-core/commands/[ and /dev/null differ diff --git a/packages/runtime-core/commands/_stubs b/packages/runtime-core/commands/_stubs deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/_stubs and /dev/null differ diff --git a/packages/runtime-core/commands/arch b/packages/runtime-core/commands/arch deleted file mode 100644 index 414eecc3c7..0000000000 Binary files a/packages/runtime-core/commands/arch and /dev/null differ diff --git a/packages/runtime-core/commands/awk b/packages/runtime-core/commands/awk deleted file mode 100644 index c4048f65c4..0000000000 Binary files a/packages/runtime-core/commands/awk and /dev/null differ diff --git a/packages/runtime-core/commands/b2sum b/packages/runtime-core/commands/b2sum deleted file mode 100644 index cb078b1959..0000000000 Binary files a/packages/runtime-core/commands/b2sum and /dev/null differ diff --git a/packages/runtime-core/commands/base32 b/packages/runtime-core/commands/base32 deleted file mode 100644 index 662f4584d8..0000000000 Binary files a/packages/runtime-core/commands/base32 and /dev/null differ diff --git a/packages/runtime-core/commands/base64 b/packages/runtime-core/commands/base64 deleted file mode 100644 index e315357a92..0000000000 Binary files a/packages/runtime-core/commands/base64 and /dev/null differ diff --git a/packages/runtime-core/commands/basename b/packages/runtime-core/commands/basename deleted file mode 100644 index 5a5547db31..0000000000 Binary files a/packages/runtime-core/commands/basename and /dev/null differ diff --git a/packages/runtime-core/commands/basenc b/packages/runtime-core/commands/basenc deleted file mode 100644 index faed8dc645..0000000000 Binary files a/packages/runtime-core/commands/basenc and /dev/null differ diff --git a/packages/runtime-core/commands/bash b/packages/runtime-core/commands/bash deleted file mode 100644 index 9e80dda943..0000000000 Binary files a/packages/runtime-core/commands/bash and /dev/null differ diff --git a/packages/runtime-core/commands/cat b/packages/runtime-core/commands/cat deleted file mode 100644 index d48dcaa8cd..0000000000 Binary files a/packages/runtime-core/commands/cat and /dev/null differ diff --git a/packages/runtime-core/commands/chcon b/packages/runtime-core/commands/chcon deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/chcon and /dev/null differ diff --git a/packages/runtime-core/commands/chgrp b/packages/runtime-core/commands/chgrp deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/chgrp and /dev/null differ diff --git a/packages/runtime-core/commands/chmod b/packages/runtime-core/commands/chmod deleted file mode 100644 index 05e1293098..0000000000 Binary files a/packages/runtime-core/commands/chmod and /dev/null differ diff --git a/packages/runtime-core/commands/chown b/packages/runtime-core/commands/chown deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/chown and /dev/null differ diff --git a/packages/runtime-core/commands/chroot b/packages/runtime-core/commands/chroot deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/chroot and /dev/null differ diff --git a/packages/runtime-core/commands/cksum b/packages/runtime-core/commands/cksum deleted file mode 100644 index 45bb83c1f8..0000000000 Binary files a/packages/runtime-core/commands/cksum and /dev/null differ diff --git a/packages/runtime-core/commands/codex b/packages/runtime-core/commands/codex deleted file mode 100644 index 381586fa9d..0000000000 Binary files a/packages/runtime-core/commands/codex and /dev/null differ diff --git a/packages/runtime-core/commands/codex-exec b/packages/runtime-core/commands/codex-exec deleted file mode 100644 index a151f153a8..0000000000 Binary files a/packages/runtime-core/commands/codex-exec and /dev/null differ diff --git a/packages/runtime-core/commands/column b/packages/runtime-core/commands/column deleted file mode 100644 index 1580c3b660..0000000000 Binary files a/packages/runtime-core/commands/column and /dev/null differ diff --git a/packages/runtime-core/commands/comm b/packages/runtime-core/commands/comm deleted file mode 100644 index 63c7779a2d..0000000000 Binary files a/packages/runtime-core/commands/comm and /dev/null differ diff --git a/packages/runtime-core/commands/cp b/packages/runtime-core/commands/cp deleted file mode 100644 index d2af9d05b3..0000000000 Binary files a/packages/runtime-core/commands/cp and /dev/null differ diff --git a/packages/runtime-core/commands/curl b/packages/runtime-core/commands/curl deleted file mode 100644 index 5e85f8a87d..0000000000 Binary files a/packages/runtime-core/commands/curl and /dev/null differ diff --git a/packages/runtime-core/commands/cut b/packages/runtime-core/commands/cut deleted file mode 100644 index 2c58944d96..0000000000 Binary files a/packages/runtime-core/commands/cut and /dev/null differ diff --git a/packages/runtime-core/commands/date b/packages/runtime-core/commands/date deleted file mode 100644 index 16c8308fc5..0000000000 Binary files a/packages/runtime-core/commands/date and /dev/null differ diff --git a/packages/runtime-core/commands/dd b/packages/runtime-core/commands/dd deleted file mode 100644 index e255315326..0000000000 Binary files a/packages/runtime-core/commands/dd and /dev/null differ diff --git a/packages/runtime-core/commands/df b/packages/runtime-core/commands/df deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/df and /dev/null differ diff --git a/packages/runtime-core/commands/diff b/packages/runtime-core/commands/diff deleted file mode 100644 index 003d074f99..0000000000 Binary files a/packages/runtime-core/commands/diff and /dev/null differ diff --git a/packages/runtime-core/commands/dir b/packages/runtime-core/commands/dir deleted file mode 100644 index 3dc3e5295e..0000000000 Binary files a/packages/runtime-core/commands/dir and /dev/null differ diff --git a/packages/runtime-core/commands/dircolors b/packages/runtime-core/commands/dircolors deleted file mode 100644 index f230724e29..0000000000 Binary files a/packages/runtime-core/commands/dircolors and /dev/null differ diff --git a/packages/runtime-core/commands/dirname b/packages/runtime-core/commands/dirname deleted file mode 100644 index 6f21608159..0000000000 Binary files a/packages/runtime-core/commands/dirname and /dev/null differ diff --git a/packages/runtime-core/commands/du b/packages/runtime-core/commands/du deleted file mode 100644 index bd087ccddf..0000000000 Binary files a/packages/runtime-core/commands/du and /dev/null differ diff --git a/packages/runtime-core/commands/echo b/packages/runtime-core/commands/echo deleted file mode 100644 index dbe1cb5b22..0000000000 Binary files a/packages/runtime-core/commands/echo and /dev/null differ diff --git a/packages/runtime-core/commands/egrep b/packages/runtime-core/commands/egrep deleted file mode 100644 index c78e121f14..0000000000 Binary files a/packages/runtime-core/commands/egrep and /dev/null differ diff --git a/packages/runtime-core/commands/env b/packages/runtime-core/commands/env deleted file mode 100644 index d9860269d1..0000000000 Binary files a/packages/runtime-core/commands/env and /dev/null differ diff --git a/packages/runtime-core/commands/envsubst b/packages/runtime-core/commands/envsubst deleted file mode 100644 index 46b6f4f8c7..0000000000 Binary files a/packages/runtime-core/commands/envsubst and /dev/null differ diff --git a/packages/runtime-core/commands/expand b/packages/runtime-core/commands/expand deleted file mode 100644 index acf0b7e03d..0000000000 Binary files a/packages/runtime-core/commands/expand and /dev/null differ diff --git a/packages/runtime-core/commands/expr b/packages/runtime-core/commands/expr deleted file mode 100644 index b2825b7557..0000000000 Binary files a/packages/runtime-core/commands/expr and /dev/null differ diff --git a/packages/runtime-core/commands/factor b/packages/runtime-core/commands/factor deleted file mode 100644 index 4c1d451774..0000000000 Binary files a/packages/runtime-core/commands/factor and /dev/null differ diff --git a/packages/runtime-core/commands/false b/packages/runtime-core/commands/false deleted file mode 100644 index 0913e06b95..0000000000 Binary files a/packages/runtime-core/commands/false and /dev/null differ diff --git a/packages/runtime-core/commands/fd b/packages/runtime-core/commands/fd deleted file mode 100644 index b724ec24b1..0000000000 Binary files a/packages/runtime-core/commands/fd and /dev/null differ diff --git a/packages/runtime-core/commands/fgrep b/packages/runtime-core/commands/fgrep deleted file mode 100644 index c78e121f14..0000000000 Binary files a/packages/runtime-core/commands/fgrep and /dev/null differ diff --git a/packages/runtime-core/commands/file b/packages/runtime-core/commands/file deleted file mode 100644 index 1e32fe6064..0000000000 Binary files a/packages/runtime-core/commands/file and /dev/null differ diff --git a/packages/runtime-core/commands/find b/packages/runtime-core/commands/find deleted file mode 100644 index e08e8b58a1..0000000000 Binary files a/packages/runtime-core/commands/find and /dev/null differ diff --git a/packages/runtime-core/commands/fmt b/packages/runtime-core/commands/fmt deleted file mode 100644 index 8f7f5b020a..0000000000 Binary files a/packages/runtime-core/commands/fmt and /dev/null differ diff --git a/packages/runtime-core/commands/fold b/packages/runtime-core/commands/fold deleted file mode 100644 index 66893046f6..0000000000 Binary files a/packages/runtime-core/commands/fold and /dev/null differ diff --git a/packages/runtime-core/commands/git b/packages/runtime-core/commands/git deleted file mode 100644 index 1865311237..0000000000 Binary files a/packages/runtime-core/commands/git and /dev/null differ diff --git a/packages/runtime-core/commands/git-remote-http b/packages/runtime-core/commands/git-remote-http deleted file mode 100644 index 1865311237..0000000000 Binary files a/packages/runtime-core/commands/git-remote-http and /dev/null differ diff --git a/packages/runtime-core/commands/git-remote-https b/packages/runtime-core/commands/git-remote-https deleted file mode 100644 index 1865311237..0000000000 Binary files a/packages/runtime-core/commands/git-remote-https and /dev/null differ diff --git a/packages/runtime-core/commands/grep b/packages/runtime-core/commands/grep deleted file mode 100644 index c78e121f14..0000000000 Binary files a/packages/runtime-core/commands/grep and /dev/null differ diff --git a/packages/runtime-core/commands/groups b/packages/runtime-core/commands/groups deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/groups and /dev/null differ diff --git a/packages/runtime-core/commands/gunzip b/packages/runtime-core/commands/gunzip deleted file mode 100644 index e60ebe5317..0000000000 Binary files a/packages/runtime-core/commands/gunzip and /dev/null differ diff --git a/packages/runtime-core/commands/gzip b/packages/runtime-core/commands/gzip deleted file mode 100644 index e60ebe5317..0000000000 Binary files a/packages/runtime-core/commands/gzip and /dev/null differ diff --git a/packages/runtime-core/commands/head b/packages/runtime-core/commands/head deleted file mode 100644 index 28a97356d7..0000000000 Binary files a/packages/runtime-core/commands/head and /dev/null differ diff --git a/packages/runtime-core/commands/hostid b/packages/runtime-core/commands/hostid deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/hostid and /dev/null differ diff --git a/packages/runtime-core/commands/hostname b/packages/runtime-core/commands/hostname deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/hostname and /dev/null differ diff --git a/packages/runtime-core/commands/http-test b/packages/runtime-core/commands/http-test deleted file mode 100644 index a36c814d00..0000000000 Binary files a/packages/runtime-core/commands/http-test and /dev/null differ diff --git a/packages/runtime-core/commands/http_get b/packages/runtime-core/commands/http_get deleted file mode 100644 index 52a1fb8342..0000000000 Binary files a/packages/runtime-core/commands/http_get and /dev/null differ diff --git a/packages/runtime-core/commands/id b/packages/runtime-core/commands/id deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/id and /dev/null differ diff --git a/packages/runtime-core/commands/install b/packages/runtime-core/commands/install deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/install and /dev/null differ diff --git a/packages/runtime-core/commands/join b/packages/runtime-core/commands/join deleted file mode 100644 index 60b7cad297..0000000000 Binary files a/packages/runtime-core/commands/join and /dev/null differ diff --git a/packages/runtime-core/commands/jq b/packages/runtime-core/commands/jq deleted file mode 100644 index 22f0359722..0000000000 Binary files a/packages/runtime-core/commands/jq and /dev/null differ diff --git a/packages/runtime-core/commands/kill b/packages/runtime-core/commands/kill deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/kill and /dev/null differ diff --git a/packages/runtime-core/commands/link b/packages/runtime-core/commands/link deleted file mode 100644 index 609677d3fe..0000000000 Binary files a/packages/runtime-core/commands/link and /dev/null differ diff --git a/packages/runtime-core/commands/ln b/packages/runtime-core/commands/ln deleted file mode 100644 index 7930e6370e..0000000000 Binary files a/packages/runtime-core/commands/ln and /dev/null differ diff --git a/packages/runtime-core/commands/logname b/packages/runtime-core/commands/logname deleted file mode 100644 index f0fad8d365..0000000000 Binary files a/packages/runtime-core/commands/logname and /dev/null differ diff --git a/packages/runtime-core/commands/ls b/packages/runtime-core/commands/ls deleted file mode 100644 index 3dc3e5295e..0000000000 Binary files a/packages/runtime-core/commands/ls and /dev/null differ diff --git a/packages/runtime-core/commands/md5sum b/packages/runtime-core/commands/md5sum deleted file mode 100644 index 38d5041ca5..0000000000 Binary files a/packages/runtime-core/commands/md5sum and /dev/null differ diff --git a/packages/runtime-core/commands/mkdir b/packages/runtime-core/commands/mkdir deleted file mode 100644 index ff1921b459..0000000000 Binary files a/packages/runtime-core/commands/mkdir and /dev/null differ diff --git a/packages/runtime-core/commands/mkfifo b/packages/runtime-core/commands/mkfifo deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/mkfifo and /dev/null differ diff --git a/packages/runtime-core/commands/mknod b/packages/runtime-core/commands/mknod deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/mknod and /dev/null differ diff --git a/packages/runtime-core/commands/mktemp b/packages/runtime-core/commands/mktemp deleted file mode 100644 index a67bb8b610..0000000000 Binary files a/packages/runtime-core/commands/mktemp and /dev/null differ diff --git a/packages/runtime-core/commands/more b/packages/runtime-core/commands/more deleted file mode 100644 index d48dcaa8cd..0000000000 Binary files a/packages/runtime-core/commands/more and /dev/null differ diff --git a/packages/runtime-core/commands/mv b/packages/runtime-core/commands/mv deleted file mode 100644 index db5e849abc..0000000000 Binary files a/packages/runtime-core/commands/mv and /dev/null differ diff --git a/packages/runtime-core/commands/nice b/packages/runtime-core/commands/nice deleted file mode 100644 index f0d9cb44fb..0000000000 Binary files a/packages/runtime-core/commands/nice and /dev/null differ diff --git a/packages/runtime-core/commands/nl b/packages/runtime-core/commands/nl deleted file mode 100644 index 53abc2d686..0000000000 Binary files a/packages/runtime-core/commands/nl and /dev/null differ diff --git a/packages/runtime-core/commands/nohup b/packages/runtime-core/commands/nohup deleted file mode 100644 index 5b91d7ca8a..0000000000 Binary files a/packages/runtime-core/commands/nohup and /dev/null differ diff --git a/packages/runtime-core/commands/nproc b/packages/runtime-core/commands/nproc deleted file mode 100644 index 30671ed9d6..0000000000 Binary files a/packages/runtime-core/commands/nproc and /dev/null differ diff --git a/packages/runtime-core/commands/numfmt b/packages/runtime-core/commands/numfmt deleted file mode 100644 index ed496d7db7..0000000000 Binary files a/packages/runtime-core/commands/numfmt and /dev/null differ diff --git a/packages/runtime-core/commands/od b/packages/runtime-core/commands/od deleted file mode 100644 index 29edaafc0c..0000000000 Binary files a/packages/runtime-core/commands/od and /dev/null differ diff --git a/packages/runtime-core/commands/paste b/packages/runtime-core/commands/paste deleted file mode 100644 index 2ffd02940f..0000000000 Binary files a/packages/runtime-core/commands/paste and /dev/null differ diff --git a/packages/runtime-core/commands/pathchk b/packages/runtime-core/commands/pathchk deleted file mode 100644 index 785f1f7af3..0000000000 Binary files a/packages/runtime-core/commands/pathchk and /dev/null differ diff --git a/packages/runtime-core/commands/pinky b/packages/runtime-core/commands/pinky deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/pinky and /dev/null differ diff --git a/packages/runtime-core/commands/printenv b/packages/runtime-core/commands/printenv deleted file mode 100644 index cbb8bc32cd..0000000000 Binary files a/packages/runtime-core/commands/printenv and /dev/null differ diff --git a/packages/runtime-core/commands/printf b/packages/runtime-core/commands/printf deleted file mode 100644 index 244d2d9ae2..0000000000 Binary files a/packages/runtime-core/commands/printf and /dev/null differ diff --git a/packages/runtime-core/commands/ptx b/packages/runtime-core/commands/ptx deleted file mode 100644 index 881c9e8850..0000000000 Binary files a/packages/runtime-core/commands/ptx and /dev/null differ diff --git a/packages/runtime-core/commands/pwd b/packages/runtime-core/commands/pwd deleted file mode 100644 index e173b2b9b2..0000000000 Binary files a/packages/runtime-core/commands/pwd and /dev/null differ diff --git a/packages/runtime-core/commands/readlink b/packages/runtime-core/commands/readlink deleted file mode 100644 index 5c4dcb2009..0000000000 Binary files a/packages/runtime-core/commands/readlink and /dev/null differ diff --git a/packages/runtime-core/commands/realpath b/packages/runtime-core/commands/realpath deleted file mode 100644 index 6df9bfa73f..0000000000 Binary files a/packages/runtime-core/commands/realpath and /dev/null differ diff --git a/packages/runtime-core/commands/rev b/packages/runtime-core/commands/rev deleted file mode 100644 index 864b62afd1..0000000000 Binary files a/packages/runtime-core/commands/rev and /dev/null differ diff --git a/packages/runtime-core/commands/rg b/packages/runtime-core/commands/rg deleted file mode 100644 index 76a63b75ea..0000000000 Binary files a/packages/runtime-core/commands/rg and /dev/null differ diff --git a/packages/runtime-core/commands/rm b/packages/runtime-core/commands/rm deleted file mode 100644 index e00af18f8e..0000000000 Binary files a/packages/runtime-core/commands/rm and /dev/null differ diff --git a/packages/runtime-core/commands/rmdir b/packages/runtime-core/commands/rmdir deleted file mode 100644 index 3ec6f0f036..0000000000 Binary files a/packages/runtime-core/commands/rmdir and /dev/null differ diff --git a/packages/runtime-core/commands/runcon b/packages/runtime-core/commands/runcon deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/runcon and /dev/null differ diff --git a/packages/runtime-core/commands/sed b/packages/runtime-core/commands/sed deleted file mode 100644 index cf0fd26325..0000000000 Binary files a/packages/runtime-core/commands/sed and /dev/null differ diff --git a/packages/runtime-core/commands/seq b/packages/runtime-core/commands/seq deleted file mode 100644 index 45a46fd826..0000000000 Binary files a/packages/runtime-core/commands/seq and /dev/null differ diff --git a/packages/runtime-core/commands/sh b/packages/runtime-core/commands/sh deleted file mode 100644 index 9e80dda943..0000000000 Binary files a/packages/runtime-core/commands/sh and /dev/null differ diff --git a/packages/runtime-core/commands/sha1sum b/packages/runtime-core/commands/sha1sum deleted file mode 100644 index a45e8a03a1..0000000000 Binary files a/packages/runtime-core/commands/sha1sum and /dev/null differ diff --git a/packages/runtime-core/commands/sha224sum b/packages/runtime-core/commands/sha224sum deleted file mode 100644 index f4d681dbc0..0000000000 Binary files a/packages/runtime-core/commands/sha224sum and /dev/null differ diff --git a/packages/runtime-core/commands/sha256sum b/packages/runtime-core/commands/sha256sum deleted file mode 100644 index 9a1d01135d..0000000000 Binary files a/packages/runtime-core/commands/sha256sum and /dev/null differ diff --git a/packages/runtime-core/commands/sha384sum b/packages/runtime-core/commands/sha384sum deleted file mode 100644 index a5f63b13dd..0000000000 Binary files a/packages/runtime-core/commands/sha384sum and /dev/null differ diff --git a/packages/runtime-core/commands/sha512sum b/packages/runtime-core/commands/sha512sum deleted file mode 100644 index cda448b94f..0000000000 Binary files a/packages/runtime-core/commands/sha512sum and /dev/null differ diff --git a/packages/runtime-core/commands/shred b/packages/runtime-core/commands/shred deleted file mode 100644 index a0d21cd818..0000000000 Binary files a/packages/runtime-core/commands/shred and /dev/null differ diff --git a/packages/runtime-core/commands/shuf b/packages/runtime-core/commands/shuf deleted file mode 100644 index 974b618c1d..0000000000 Binary files a/packages/runtime-core/commands/shuf and /dev/null differ diff --git a/packages/runtime-core/commands/sleep b/packages/runtime-core/commands/sleep deleted file mode 100644 index af6efb965e..0000000000 Binary files a/packages/runtime-core/commands/sleep and /dev/null differ diff --git a/packages/runtime-core/commands/sort b/packages/runtime-core/commands/sort deleted file mode 100644 index 1d6dc77ac3..0000000000 Binary files a/packages/runtime-core/commands/sort and /dev/null differ diff --git a/packages/runtime-core/commands/spawn-test-host b/packages/runtime-core/commands/spawn-test-host deleted file mode 100644 index 6c412c79c9..0000000000 Binary files a/packages/runtime-core/commands/spawn-test-host and /dev/null differ diff --git a/packages/runtime-core/commands/split b/packages/runtime-core/commands/split deleted file mode 100644 index a5821574ca..0000000000 Binary files a/packages/runtime-core/commands/split and /dev/null differ diff --git a/packages/runtime-core/commands/sqlite3 b/packages/runtime-core/commands/sqlite3 deleted file mode 100755 index a9ee1f206f..0000000000 Binary files a/packages/runtime-core/commands/sqlite3 and /dev/null differ diff --git a/packages/runtime-core/commands/stat b/packages/runtime-core/commands/stat deleted file mode 100644 index af4a973161..0000000000 Binary files a/packages/runtime-core/commands/stat and /dev/null differ diff --git a/packages/runtime-core/commands/stdbuf b/packages/runtime-core/commands/stdbuf deleted file mode 100644 index d5884b404a..0000000000 Binary files a/packages/runtime-core/commands/stdbuf and /dev/null differ diff --git a/packages/runtime-core/commands/strings b/packages/runtime-core/commands/strings deleted file mode 100644 index 9b501f8ec3..0000000000 Binary files a/packages/runtime-core/commands/strings and /dev/null differ diff --git a/packages/runtime-core/commands/stty b/packages/runtime-core/commands/stty deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/stty and /dev/null differ diff --git a/packages/runtime-core/commands/sum b/packages/runtime-core/commands/sum deleted file mode 100644 index 5dd3eaf7a5..0000000000 Binary files a/packages/runtime-core/commands/sum and /dev/null differ diff --git a/packages/runtime-core/commands/sync b/packages/runtime-core/commands/sync deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/sync and /dev/null differ diff --git a/packages/runtime-core/commands/tac b/packages/runtime-core/commands/tac deleted file mode 100644 index 35179d8163..0000000000 Binary files a/packages/runtime-core/commands/tac and /dev/null differ diff --git a/packages/runtime-core/commands/tail b/packages/runtime-core/commands/tail deleted file mode 100644 index 0838f8e1ca..0000000000 Binary files a/packages/runtime-core/commands/tail and /dev/null differ diff --git a/packages/runtime-core/commands/tar b/packages/runtime-core/commands/tar deleted file mode 100644 index 5d813b99d9..0000000000 Binary files a/packages/runtime-core/commands/tar and /dev/null differ diff --git a/packages/runtime-core/commands/tee b/packages/runtime-core/commands/tee deleted file mode 100644 index b23a82ccbd..0000000000 Binary files a/packages/runtime-core/commands/tee and /dev/null differ diff --git a/packages/runtime-core/commands/test b/packages/runtime-core/commands/test deleted file mode 100644 index 321231e0c4..0000000000 Binary files a/packages/runtime-core/commands/test and /dev/null differ diff --git a/packages/runtime-core/commands/timeout b/packages/runtime-core/commands/timeout deleted file mode 100644 index 2638c09dc5..0000000000 Binary files a/packages/runtime-core/commands/timeout and /dev/null differ diff --git a/packages/runtime-core/commands/touch b/packages/runtime-core/commands/touch deleted file mode 100644 index fd1ed0710f..0000000000 Binary files a/packages/runtime-core/commands/touch and /dev/null differ diff --git a/packages/runtime-core/commands/tr b/packages/runtime-core/commands/tr deleted file mode 100644 index 536e993b9a..0000000000 Binary files a/packages/runtime-core/commands/tr and /dev/null differ diff --git a/packages/runtime-core/commands/tree b/packages/runtime-core/commands/tree deleted file mode 100644 index 49e9c7c18d..0000000000 Binary files a/packages/runtime-core/commands/tree and /dev/null differ diff --git a/packages/runtime-core/commands/true b/packages/runtime-core/commands/true deleted file mode 100644 index e08e0be528..0000000000 Binary files a/packages/runtime-core/commands/true and /dev/null differ diff --git a/packages/runtime-core/commands/truncate b/packages/runtime-core/commands/truncate deleted file mode 100644 index c693df7acb..0000000000 Binary files a/packages/runtime-core/commands/truncate and /dev/null differ diff --git a/packages/runtime-core/commands/tsort b/packages/runtime-core/commands/tsort deleted file mode 100644 index 15609b02d8..0000000000 Binary files a/packages/runtime-core/commands/tsort and /dev/null differ diff --git a/packages/runtime-core/commands/tty b/packages/runtime-core/commands/tty deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/tty and /dev/null differ diff --git a/packages/runtime-core/commands/uname b/packages/runtime-core/commands/uname deleted file mode 100644 index 7c9268ff8c..0000000000 Binary files a/packages/runtime-core/commands/uname and /dev/null differ diff --git a/packages/runtime-core/commands/unexpand b/packages/runtime-core/commands/unexpand deleted file mode 100644 index 50728dccb0..0000000000 Binary files a/packages/runtime-core/commands/unexpand and /dev/null differ diff --git a/packages/runtime-core/commands/uniq b/packages/runtime-core/commands/uniq deleted file mode 100644 index e243c67dd6..0000000000 Binary files a/packages/runtime-core/commands/uniq and /dev/null differ diff --git a/packages/runtime-core/commands/unlink b/packages/runtime-core/commands/unlink deleted file mode 100644 index de09163081..0000000000 Binary files a/packages/runtime-core/commands/unlink and /dev/null differ diff --git a/packages/runtime-core/commands/unzip b/packages/runtime-core/commands/unzip deleted file mode 100644 index 07f40041e1..0000000000 Binary files a/packages/runtime-core/commands/unzip and /dev/null differ diff --git a/packages/runtime-core/commands/uptime b/packages/runtime-core/commands/uptime deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/uptime and /dev/null differ diff --git a/packages/runtime-core/commands/users b/packages/runtime-core/commands/users deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/users and /dev/null differ diff --git a/packages/runtime-core/commands/vdir b/packages/runtime-core/commands/vdir deleted file mode 100644 index 3dc3e5295e..0000000000 Binary files a/packages/runtime-core/commands/vdir and /dev/null differ diff --git a/packages/runtime-core/commands/wc b/packages/runtime-core/commands/wc deleted file mode 100644 index 3b7e2fdf39..0000000000 Binary files a/packages/runtime-core/commands/wc and /dev/null differ diff --git a/packages/runtime-core/commands/which b/packages/runtime-core/commands/which deleted file mode 100644 index a27bc06037..0000000000 Binary files a/packages/runtime-core/commands/which and /dev/null differ diff --git a/packages/runtime-core/commands/who b/packages/runtime-core/commands/who deleted file mode 100644 index abc6255662..0000000000 Binary files a/packages/runtime-core/commands/who and /dev/null differ diff --git a/packages/runtime-core/commands/whoami b/packages/runtime-core/commands/whoami deleted file mode 100644 index c4b00993ed..0000000000 Binary files a/packages/runtime-core/commands/whoami and /dev/null differ diff --git a/packages/runtime-core/commands/xargs b/packages/runtime-core/commands/xargs deleted file mode 100644 index 613a7bb3ea..0000000000 Binary files a/packages/runtime-core/commands/xargs and /dev/null differ diff --git a/packages/runtime-core/commands/xu b/packages/runtime-core/commands/xu deleted file mode 100644 index e37012c89f..0000000000 Binary files a/packages/runtime-core/commands/xu and /dev/null differ diff --git a/packages/runtime-core/commands/yes b/packages/runtime-core/commands/yes deleted file mode 100644 index 3a0ebb9b7a..0000000000 Binary files a/packages/runtime-core/commands/yes and /dev/null differ diff --git a/packages/runtime-core/commands/yq b/packages/runtime-core/commands/yq deleted file mode 100644 index 98a2de41ee..0000000000 Binary files a/packages/runtime-core/commands/yq and /dev/null differ diff --git a/packages/runtime-core/commands/zcat b/packages/runtime-core/commands/zcat deleted file mode 100644 index e60ebe5317..0000000000 Binary files a/packages/runtime-core/commands/zcat and /dev/null differ diff --git a/packages/runtime-core/commands/zip b/packages/runtime-core/commands/zip deleted file mode 100644 index 8324bdc9fe..0000000000 Binary files a/packages/runtime-core/commands/zip and /dev/null differ diff --git a/packages/runtime-core/scripts/copy-wasm-commands.mjs b/packages/runtime-core/scripts/copy-wasm-commands.mjs index 1a095fc10c..2f0d59ac2d 100644 --- a/packages/runtime-core/scripts/copy-wasm-commands.mjs +++ b/packages/runtime-core/scripts/copy-wasm-commands.mjs @@ -1,21 +1,9 @@ #!/usr/bin/env node /** - * Vendor the WASM coreutils/shell command binaries into `@rivet-dev/agentos-runtime-core`'s - * package directory so they ship inside the published tarball. - * - * The kernel needs a guest `sh` (plus coreutils) to spawn any process — without - * these binaries `NodeRuntime.create()` cannot boot. The binaries are produced - * by the in-repo Rust command build at - * `registry/native/target/wasm32-wasip1/release/commands/`. That path only - * exists in a developer checkout, so we copy the whole command set (symlinks - * included, the way `bash -> sh` and the stub aliases are laid out) into - * `packages/core/commands/`, which is listed in `files` and resolved at runtime - * by `node-runtime.ts` for published installs. - * - * This mirrors how the sidecar binary ships via `@rivet-dev/agentos-runtime-sidecar`: the - * artifact is vendored into a published package and resolved from the installed - * package at runtime, never from an in-repo build path. + * Vendor the WASM command binaries into `@rivet-dev/agentos-runtime-core` so + * they ship inside the published tarball. Source aliases are dereferenced + * because npm does not preserve this command tree's symlinks. */ import { @@ -23,6 +11,7 @@ import { existsSync, lstatSync, mkdirSync, + readFileSync, readdirSync, realpathSync, rmSync, @@ -35,82 +24,193 @@ const REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url)); const SOURCE_DIR = path.join( REPO_ROOT, - "registry/native/target/wasm32-wasip1/release/commands", + "toolchain/target/wasm32-wasip1/release/commands", ); const DEST_DIR = path.join(PACKAGE_ROOT, "commands"); +const SOFTWARE_ROOT = path.join(REPO_ROOT, "software"); -function destIsPopulated() { +// These packages are intentionally outside `make -C toolchain commands`: +// codex is built from its separately pinned upstream checkout, while duckdb +// and vim are explicit heavy builds. If any are present they are still copied; +// they are simply not prerequisites for `--require`. +const OPTIONAL_COMMAND_PACKAGES = new Set(["codex-cli", "duckdb", "vim"]); + +function commandNames(manifest, manifestPath) { + const names = [ + ...(manifest.commands ?? []), + ...Object.keys(manifest.aliases ?? {}), + ...(manifest.stubs ?? []), + ]; + for (const name of names) { + if (typeof name !== "string" || name.length === 0 || name.includes("/")) { + throw new Error( + `invalid command name ${JSON.stringify(name)} in ${manifestPath}`, + ); + } + } + return names; +} + +/** Derive the default command contract from the software package manifests. */ +export function requiredSoftwareCommandNames(softwareRoot = SOFTWARE_ROOT) { + const required = new Set(); + for (const entry of readdirSync(softwareRoot, { withFileTypes: true })) { + if (!entry.isDirectory() || OPTIONAL_COMMAND_PACKAGES.has(entry.name)) { + continue; + } + const manifestPath = path.join( + softwareRoot, + entry.name, + "agentos-package.json", + ); + if (!existsSync(manifestPath)) { + continue; + } + const manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + for (const name of commandNames(manifest, manifestPath)) { + required.add(name); + } + } + return [...required].sort(); +} + +function commandFiles(dir, { requireRealFiles = false } = {}) { + const files = []; + for (const name of readdirSync(dir).sort()) { + if (name.startsWith(".")) { + continue; + } + const entryPath = path.join(dir, name); + const stat = lstatSync(entryPath); + if (requireRealFiles) { + if (stat.isSymbolicLink() || !stat.isFile()) { + throw new Error(`vendored command is not a real file: ${entryPath}`); + } + files.push({ name, sourcePath: entryPath }); + continue; + } + + if (!stat.isFile() && !stat.isSymbolicLink()) { + throw new Error(`command source is not a file or symlink: ${entryPath}`); + } + const sourcePath = stat.isSymbolicLink() + ? realpathSync(entryPath) + : entryPath; + if (!lstatSync(sourcePath).isFile()) { + throw new Error(`command source does not resolve to a file: ${entryPath}`); + } + files.push({ name, sourcePath }); + } + return files; +} + +function assertRequiredCommands(files, required, location) { + const available = new Set(files.map(({ name }) => name)); + const missing = required.filter((name) => !available.has(name)); + if (missing.length > 0) { + throw new Error( + `missing required default WASM commands from ${location}: ${missing.join(", ")}`, + ); + } +} + +function assertSameBasenames(sourceFiles, destFiles) { + const sourceNames = sourceFiles.map(({ name }) => name); + const destNames = destFiles.map(({ name }) => name); + if ( + sourceNames.length !== destNames.length || + sourceNames.some((name, index) => name !== destNames[index]) + ) { + throw new Error( + `copied WASM command basenames differ: source=${sourceNames.join(",")} ` + + `destination=${destNames.join(",")}`, + ); + } +} + +function destIsPopulated(destDir) { try { - return readdirSync(DEST_DIR).some((entry) => !entry.startsWith(".")); + return readdirSync(destDir).some((entry) => !entry.startsWith(".")); } catch { return false; } } -function main() { - if (!existsSync(SOURCE_DIR)) { - // No in-repo build output. If `commands/` is already populated (a clean - // checkout that vendored earlier, or CI that dropped a prebuilt commands - // artifact straight into the package), keep it: it already ships. - if (destIsPopulated()) { - console.log( - `Using already-vendored commands at ${path.relative(REPO_ROOT, DEST_DIR)}; ` + - `in-repo build output ${path.relative(REPO_ROOT, SOURCE_DIR)} is absent.`, +export function copyWasmCommands({ + sourceDir = SOURCE_DIR, + destDir = DEST_DIR, + softwareRoot = SOFTWARE_ROOT, + requireCommands = false, + log = console.log, + warn = console.warn, +} = {}) { + const required = requireCommands + ? requiredSoftwareCommandNames(softwareRoot) + : []; + + if (!existsSync(sourceDir)) { + // CI may download a previously validated artifact directly into the + // package. In require mode, validate that fallback instead of accepting a + // merely non-empty directory. + if (destIsPopulated(destDir)) { + if (requireCommands) { + const destFiles = commandFiles(destDir, { requireRealFiles: true }); + assertRequiredCommands(destFiles, required, destDir); + } + log( + `Using already-vendored commands at ${path.relative(REPO_ROOT, destDir)}; ` + + `in-repo build output ${path.relative(REPO_ROOT, sourceDir)} is absent.`, ); return; } - // Nothing to ship. Don't fail the plain TypeScript build over it — a - // developer who hasn't built the commands can still iterate, and runtime - // resolution falls back to the in-repo path when it later appears. The - // release/pack path guards this with `--require` so a published tarball is - // never built without the commands. + const message = - `WASM commands not found at ${SOURCE_DIR} and none vendored at ${DEST_DIR}. ` + - "Build them with `make -C registry/native wasm` (or drop a prebuilt " + + `WASM commands not found at ${sourceDir} and none vendored at ${destDir}. ` + + "Build them with `make -C toolchain commands` (or drop a prebuilt " + "commands artifact into the package) before packing so they ship in the tarball."; - if (process.argv.includes("--require")) { - console.error(`error: ${message}`); - process.exit(1); + if (requireCommands) { + throw new Error(message); } - console.warn(`warning: ${message} Skipping copy.`); + warn(`warning: ${message} Skipping copy.`); return; } - rmSync(DEST_DIR, { recursive: true, force: true }); - mkdirSync(DEST_DIR, { recursive: true }); - - // Copy every command as a real file, dereferencing the build's alias - // symlinks (bash -> sh, [ -> test, the `_stubs` aliases, dir/vdir -> ls, - // gunzip/zcat -> gzip). The command discovery at runtime - // (`discoverWasmCommandEntries`) keys each command off its filename and reads - // the resolved bytes, so a dereferenced copy is functionally identical to the - // symlink. Crucially, `npm pack` drops symlinks from the tarball, so shipping - // them as real files is what makes the full command set survive into a - // published install. The command tree is flat (no subdirectories), so a - // single-level walk covers it. - let copied = 0; - for (const entry of readdirSync(SOURCE_DIR)) { - if (entry.startsWith(".")) { - continue; - } - const sourcePath = path.join(SOURCE_DIR, entry); - const destPath = path.join(DEST_DIR, entry); - const stat = lstatSync(sourcePath); - // Resolve symlinks (and any chains) to the real file before copying so - // the destination is a standalone binary, not a dangling link. - const realSource = stat.isSymbolicLink() - ? realpathSync(sourcePath) - : sourcePath; - if (!lstatSync(realSource).isFile()) { - continue; - } - copyFileSync(realSource, destPath); - copied += 1; + // Complete every fallible source/manifest check before clearing a previously + // valid vendored directory. A partial build must not erase known-good output. + const sourceFiles = commandFiles(sourceDir); + if (requireCommands) { + assertRequiredCommands(sourceFiles, required, sourceDir); + } + + rmSync(destDir, { recursive: true, force: true }); + mkdirSync(destDir, { recursive: true }); + + for (const { name, sourcePath } of sourceFiles) { + copyFileSync(sourcePath, path.join(destDir, name)); } - console.log( - `Copied ${copied} WASM command binaries to ${path.relative(REPO_ROOT, DEST_DIR)}`, + // The published tree must contain every source basename exactly once and no + // symlinks. This also catches future non-flat or unsupported command entries. + const destFiles = commandFiles(destDir, { requireRealFiles: true }); + assertSameBasenames(sourceFiles, destFiles); + + log( + `Copied ${destFiles.length} WASM command binaries to ${path.relative(REPO_ROOT, destDir)}`, ); } -main(); +function main() { + try { + copyWasmCommands({ requireCommands: process.argv.includes("--require") }); + } catch (error) { + console.error(`error: ${error instanceof Error ? error.message : String(error)}`); + process.exitCode = 1; + } +} + +if ( + process.argv[1] !== undefined && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { + main(); +} diff --git a/packages/runtime-core/src/generated/ProcessLimitsConfig.ts b/packages/runtime-core/src/generated/ProcessLimitsConfig.ts new file mode 100644 index 0000000000..328ac26b40 --- /dev/null +++ b/packages/runtime-core/src/generated/ProcessLimitsConfig.ts @@ -0,0 +1,9 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type ProcessLimitsConfig = { + maxSpawnFileActions?: number; + maxSpawnFileActionBytes?: number; + pendingStdinBytes?: number; + pendingEventCount?: number; + pendingEventBytes?: number; +}; diff --git a/packages/runtime-core/src/generated/VmLimitsConfig.ts b/packages/runtime-core/src/generated/VmLimitsConfig.ts index ceb4ed3a7c..df8e799804 100644 --- a/packages/runtime-core/src/generated/VmLimitsConfig.ts +++ b/packages/runtime-core/src/generated/VmLimitsConfig.ts @@ -3,9 +3,10 @@ import type { AcpLimitsConfig } from "./AcpLimitsConfig.js"; import type { HttpLimitsConfig } from "./HttpLimitsConfig.js"; import type { JsRuntimeLimitsConfig } from "./JsRuntimeLimitsConfig.js"; import type { PluginLimitsConfig } from "./PluginLimitsConfig.js"; +import type { ProcessLimitsConfig } from "./ProcessLimitsConfig.js"; import type { PythonLimitsConfig } from "./PythonLimitsConfig.js"; import type { ResourceLimitsConfig } from "./ResourceLimitsConfig.js"; import type { ToolLimitsConfig } from "./ToolLimitsConfig.js"; import type { WasmLimitsConfig } from "./WasmLimitsConfig.js"; -export type VmLimitsConfig = { resources?: ResourceLimitsConfig, http?: HttpLimitsConfig, tools?: ToolLimitsConfig, plugins?: PluginLimitsConfig, acp?: AcpLimitsConfig, jsRuntime?: JsRuntimeLimitsConfig, python?: PythonLimitsConfig, wasm?: WasmLimitsConfig, }; +export type VmLimitsConfig = { resources?: ResourceLimitsConfig, http?: HttpLimitsConfig, tools?: ToolLimitsConfig, plugins?: PluginLimitsConfig, acp?: AcpLimitsConfig, jsRuntime?: JsRuntimeLimitsConfig, python?: PythonLimitsConfig, wasm?: WasmLimitsConfig, process?: ProcessLimitsConfig, }; diff --git a/packages/runtime-core/src/kernel-proxy.ts b/packages/runtime-core/src/kernel-proxy.ts index 4c4f964bde..9f2c07f571 100644 --- a/packages/runtime-core/src/kernel-proxy.ts +++ b/packages/runtime-core/src/kernel-proxy.ts @@ -118,6 +118,15 @@ const PREFERRED_SIGNAL_NAMES = [ "SIGEMT", "SIGINFO", ] as const; +const NON_TERMINATING_SIGNALS = new Set([ + "0", + "SIGCHLD", + "SIGCONT", + "SIGSTOP", + "SIGURG", + "SIGWINCH", +]); +const LOCALLY_TERMINATING_SIGNALS = new Set(["SIGKILL", "SIGTERM"]); const NON_CANONICAL_SIGNAL_NAMES = new Set([ "SIGCLD", "SIGIOT", @@ -418,7 +427,10 @@ export class NativeSidecarKernelProxy { liveProcesses.map((entry) => this.signalProcess(entry, 15)), ); - await this.client.disposeVm(this.session, this.vm).catch(() => {}); + await Promise.race([ + this.client.disposeVm(this.session, this.vm), + new Promise((resolve) => setTimeout(resolve, 1000)), + ]).catch(() => {}); for (const entry of liveProcesses) { if (entry.exitCode === null) { // The sidecar dispose path already performs TERM/KILL escalation for any @@ -679,13 +691,19 @@ export class NativeSidecarKernelProxy { } entry.pendingKillSignal = signal; void entry.startPromise.then(async () => { - if (entry.exitCode !== null || entry.pendingKillSignal === null) { + if (entry.pendingKillSignal === null) { return; } const pendingSignal = entry.pendingKillSignal; entry.pendingKillSignal = null; await this.signalProcess(entry, pendingSignal); }); + if ( + (signal === 9 || signal === 15) && + entry.exitCode === null + ) { + this.finishProcess(entry, 128 + signal); + } }, wait: async () => { const exitCode = await this.waitForTrackedProcess(entry); @@ -1467,16 +1485,6 @@ export class NativeSidecarKernelProxy { return; } - if (entry.exitViaEvent) { - // The sidecar drains process output before it emits `process_exited`, - // the stdio frame stream is FIFO, and this pump dispatches events in - // order. Once the exit event reaches this entry, no trailing output can - // follow it; one zero-delay turn is cheap insurance for same-tick - // listener scheduling. - await drainTrailingProcessOutputTurn(0); - return; - } - let observedGeneration = entry.outputGeneration; let quietTurns = 0; let delayMs = 0; @@ -1681,14 +1689,37 @@ export class NativeSidecarKernelProxy { entry: TrackedProcessEntry, signal: number, ): Promise { - await this.signalRefreshes.get(entry.pid); + const sidecarSignal = toSidecarSignalName(signal); + let timedOut = false; + const killPromise = this.client.killProcess( + this.session, + this.vm, + entry.processId, + sidecarSignal, + ); try { - await this.client.killProcess( - this.session, - this.vm, - entry.processId, - toSidecarSignalName(signal), - ); + await Promise.race([ + killPromise, + new Promise((resolve) => + setTimeout(() => { + timedOut = true; + resolve(); + }, 1000), + ), + ]); + if (timedOut) { + void killPromise.catch(() => {}); + } + const hasGuestHandler = + this.signalStates.get(entry.pid)?.handlers.has(signal) ?? false; + if ( + entry.exitCode === null && + !hasGuestHandler && + LOCALLY_TERMINATING_SIGNALS.has(sidecarSignal) && + (entry.driver === "wasmvm" || timedOut) + ) { + this.finishProcess(entry, 128 + signal); + } } catch (error) { if (isNoSuchProcessError(error) || isUnknownVmError(error)) { return; @@ -2009,16 +2040,12 @@ export class NativeSidecarKernelProxy { return this.client.pread(this.session, this.vm, path, offset, length); }, pwrite: async (path, offset, data) => { - const bytes = - await this.createFilesystemView(includeLocalMounts).readFile(path); - const nextSize = Math.max(bytes.length, offset + data.length); - const updated = new Uint8Array(nextSize); - updated.set(bytes); - updated.set(data, offset); - await this.createFilesystemView(includeLocalMounts).writeFile( - path, - updated, - ); + const local = includeLocalMounts ? this.resolveLocalMount(path) : null; + if (local) { + this.assertLocalWritable(local.mount); + return local.mount.fs.pwrite(local.relativePath, offset, data); + } + return this.client.pwrite(this.session, this.vm, path, offset, data); }, }; } diff --git a/packages/runtime-core/src/node-runtime.ts b/packages/runtime-core/src/node-runtime.ts index 7bbd4a6822..832d740a6c 100644 --- a/packages/runtime-core/src/node-runtime.ts +++ b/packages/runtime-core/src/node-runtime.ts @@ -63,13 +63,13 @@ const REPO_ROOT = fileURLToPath(new URL("../../..", import.meta.url)); /** * In-repo build output for the WASM coreutils/shell command binaries, produced - * by the Rust command build (`make -C registry/native wasm`). Only present in a + * by the Rust command build (`make -C toolchain wasm`). Only present in a * developer checkout; preferred when it exists so local edits are picked up * without re-vendoring. */ const REPO_COMMANDS_DIR = path.join( REPO_ROOT, - "registry/native/target/wasm32-wasip1/release/commands", + "toolchain/target/wasm32-wasip1/release/commands", ); /** diff --git a/packages/runtime-core/src/sidecar-process.ts b/packages/runtime-core/src/sidecar-process.ts index 9a5e926945..cd60c8d635 100644 --- a/packages/runtime-core/src/sidecar-process.ts +++ b/packages/runtime-core/src/sidecar-process.ts @@ -196,6 +196,8 @@ export interface SidecarSpawnOptions { command?: string; args?: string[]; eventBufferCapacity?: number; + gracefulExitMs?: number; + forceExitMs?: number; // Migration-only compatibility path for pre-BARE test fixtures. payloadCodec?: NativeTransportPayloadCodec; /** @@ -368,8 +370,8 @@ export class SidecarProcess { silenceTimeoutMs: options.silenceTimeoutMs, eventBufferCapacity: options.eventBufferCapacity ?? DEFAULT_SIDECAR_EVENT_BUFFER_CAPACITY, - gracefulExitMs: DEFAULT_SIDECAR_GRACEFUL_EXIT_MS, - forceExitMs: DEFAULT_SIDECAR_FORCE_EXIT_MS, + gracefulExitMs: options.gracefulExitMs ?? DEFAULT_SIDECAR_GRACEFUL_EXIT_MS, + forceExitMs: options.forceExitMs ?? DEFAULT_SIDECAR_FORCE_EXIT_MS, disposedErrorMessage: "native sidecar disposed", payloadCodec: options.payloadCodec ?? "bare", }); @@ -890,6 +892,23 @@ export class SidecarProcess { }); } + async pwrite( + session: AuthenticatedSession, + vm: CreatedVm, + path: string, + offset: number, + content: Uint8Array, + ): Promise { + const encoded = encodeGuestFilesystemContent(content); + await this.guestFilesystemCall(session, vm, { + operation: "pwrite", + path, + offset, + content: encoded.content, + encoding: encoded.encoding, + }); + } + async mkdir( session: AuthenticatedSession, vm: CreatedVm, diff --git a/packages/runtime-core/src/test-runtime.ts b/packages/runtime-core/src/test-runtime.ts index 776bd8bb25..613ec2669c 100644 --- a/packages/runtime-core/src/test-runtime.ts +++ b/packages/runtime-core/src/test-runtime.ts @@ -49,6 +49,8 @@ const KERNEL_POSIX_BOOTSTRAP_DIRS = [ "/srv", "/sys", "/opt", + "/opt/agentos", + "/opt/agentos/bin", "/mnt", "/media", "/home", @@ -82,9 +84,21 @@ const SIDECAR_BUILD_INPUTS = [ path.join(REPO_ROOT, "Cargo.toml"), path.join(REPO_ROOT, "Cargo.lock"), path.join(REPO_ROOT, "crates/bridge"), + path.join(REPO_ROOT, "crates/build-support"), path.join(REPO_ROOT, "crates/execution"), path.join(REPO_ROOT, "crates/kernel"), - path.join(REPO_ROOT, "crates/sidecar"), + path.join(REPO_ROOT, "crates/native-sidecar"), + path.join(REPO_ROOT, "crates/native-sidecar-core"), + path.join(REPO_ROOT, "crates/sidecar-protocol"), + path.join(REPO_ROOT, "crates/v8-runtime"), + path.join(REPO_ROOT, "crates/vfs"), + path.join(REPO_ROOT, "crates/vm-config"), + path.join(REPO_ROOT, "packages/build-tools/bridge-src"), + path.join(REPO_ROOT, "packages/build-tools/package.json"), + path.join(REPO_ROOT, "packages/build-tools/scripts/build-v8-bridge.mjs"), + path.join(REPO_ROOT, "packages/core/fixtures/base-filesystem.json"), + path.join(REPO_ROOT, "packages/runtime-core/fixtures/base-filesystem.json"), + path.join(REPO_ROOT, "pnpm-lock.yaml"), ] as const; export type KernelBootTimingPhase = @@ -152,7 +166,12 @@ export interface VirtualFileSystem { lstat(path: string): Promise; link(oldPath: string, newPath: string): Promise; chmod(path: string, mode: number): Promise; - chown(path: string, uid: number, gid: number): Promise; + chown( + path: string, + uid: number, + gid: number, + options?: { followSymlinks?: boolean }, + ): Promise; utimes(path: string, atime: number, mtime: number): Promise; truncate(path: string, length: number): Promise; pread(path: string, offset: number, length: number): Promise; @@ -477,6 +496,7 @@ export interface Kernel extends KernelInterface { removeFile(path: string): Promise; removeDir(path: string): Promise; rename(oldPath: string, newPath: string): Promise; + pwrite(path: string, offset: number, data: Uint8Array): Promise; vmFetch(request: { port: number; method: string; @@ -898,8 +918,15 @@ export class InMemoryFileSystem implements VirtualFileSystem { entry.ctimeMs = Date.now(); } - async chown(targetPath: string, uid: number, gid: number): Promise { - const entry = this.resolveEntry(targetPath); + async chown( + targetPath: string, + uid: number, + gid: number, + options?: { followSymlinks?: boolean }, + ): Promise { + const entry = options?.followSymlinks === false + ? this.entries.get(normalizePath(targetPath)) + : this.resolveEntry(targetPath); if (!entry) throw errnoError("ENOENT", `chown '${targetPath}'`); entry.uid = uid; entry.gid = gid; @@ -1175,8 +1202,18 @@ export class NodeFileSystem implements VirtualFileSystem { await fs.chmod(this.normalizeTarget(targetPath), mode); } - async chown(targetPath: string, uid: number, gid: number): Promise { - await fs.chown(this.normalizeTarget(targetPath), uid, gid); + async chown( + targetPath: string, + uid: number, + gid: number, + options?: { followSymlinks?: boolean }, + ): Promise { + const normalized = this.normalizeTarget(targetPath); + if (options?.followSymlinks === false) { + await fs.lchown(normalized, uid, gid); + } else { + await fs.chown(normalized, uid, gid); + } } async utimes( @@ -1215,7 +1252,10 @@ export class NodeFileSystem implements VirtualFileSystem { offset: number, data: Uint8Array, ): Promise { - const handle = await fs.open(this.normalizeTarget(targetPath), "r+"); + const handle = await fs.open( + this.normalizeTarget(targetPath), + fsSync.constants.O_WRONLY, + ); try { await handle.write(data, 0, data.length, offset); } finally { @@ -1585,7 +1625,6 @@ export const WASMVM_COMMANDS = Object.freeze([ "unzip", "sqlite3", "curl", - "wget", "git", "git-remote-http", "git-remote-https", @@ -1754,7 +1793,6 @@ export const DEFAULT_FIRST_PARTY_TIERS: Readonly< tac: "read-only", tsort: "read-only", curl: "full", - wget: "full", sqlite3: "read-write", }); @@ -1967,7 +2005,6 @@ function ensureNativeSidecarBinary(): string { // A published install has no in-repo Cargo workspace to build from: resolve // the prebuilt platform binary (or an explicit sidecar override). if ( - process.env.AGENTOS_SIDECAR_BIN || process.env.AGENTOS_SIDECAR_BIN || !fsSync.existsSync(path.join(REPO_ROOT, "Cargo.toml")) ) { @@ -2014,7 +2051,7 @@ function createBootstrapEntries(commandNames: string[]): RootFilesystemEntry[] { ...KERNEL_POSIX_BOOTSTRAP_DIRS.map((entryPath) => ({ path: entryPath, kind: "directory" as const, - mode: 0o755, + mode: entryPath === "/tmp" || entryPath === "/var/tmp" ? 0o1777 : 0o755, uid: 0, gid: 0, })), @@ -2232,9 +2269,16 @@ async function ensureCommandStubs( ): Promise { const rootView = proxy.createRootView(); for (const command of commands) { - const stubPath = `/bin/${command}`; - await rootView.writeFile(stubPath, KERNEL_COMMAND_STUB); - await rootView.chmod(stubPath, 0o755); + // `/bin` supports PATH-based test commands. `/opt/agentos/bin` mirrors + // the package projection used by production VMs and is required by + // upstream tools that exec private helpers via an absolute libexec path. + for (const stubPath of [ + `/bin/${command}`, + `/opt/agentos/bin/${command}`, + ]) { + await rootView.writeFile(stubPath, KERNEL_COMMAND_STUB); + await rootView.chmod(stubPath, 0o755); + } } } @@ -2303,8 +2347,13 @@ class DeferredFileSystem implements VirtualFileSystem { chmod(path: string, mode: number): Promise { return this.filesystem().chmod(path, mode); } - chown(path: string, uid: number, gid: number): Promise { - return this.filesystem().chown(path, uid, gid); + chown( + path: string, + uid: number, + gid: number, + options?: { followSymlinks?: boolean }, + ): Promise { + return this.filesystem().chown(path, uid, gid, options); } utimes(path: string, atime: number, mtime: number): Promise { return this.filesystem().utimes(path, atime, mtime); @@ -2358,7 +2407,16 @@ interface LiveFilesystemBinding { restore(): void; } -const LIVE_FILESYSTEM_SYNC_CHUNK_SIZE = 512 * 1024; +const DEFAULT_LIVE_FILESYSTEM_SYNC_MAX_BYTES = 64 * 1024 * 1024; + +type LiveFilesystemSnapshotEntry = + | { kind: "directory"; path: string } + | { kind: "symlink"; path: string; target: string } + | { kind: "file"; path: string; content: Uint8Array }; + +class LiveFilesystemSyncLimitError extends Error { + readonly code = "ERR_AGENTOS_LIVE_FILESYSTEM_SYNC_LIMIT"; +} function topLevelSyncRoot(targetPath: string): string { const normalized = normalizePath(targetPath); @@ -2406,72 +2464,106 @@ async function syncLiveFilesystemToBoundMethods( live: VirtualFileSystem, methods: BoundVirtualFileSystemMethods, paths: readonly string[], + maxBytes: number, ): Promise { + const snapshot: LiveFilesystemSnapshotEntry[] = []; + const usage = { bytes: 0 }; for (const targetPath of [...new Set(paths.map(normalizePath))].sort((left, right) => left.localeCompare(right), )) { if (!(await live.exists(targetPath).catch(() => false))) { continue; } - await syncLiveFilesystemPathToBoundMethods(live, methods, targetPath); + await snapshotLiveFilesystemPath(live, targetPath, snapshot, usage, maxBytes); + } + if (usage.bytes >= maxBytes * 0.8) { + process.emitWarning( + `live filesystem sync retained ${usage.bytes} of ${maxBytes} bytes; ` + + "raise createKernel({ maxFilesystemBytes }) before increasing VM filesystem limits", + { code: "AGENTOS_LIVE_FILESYSTEM_SYNC_NEAR_LIMIT" }, + ); + } + for (const entry of snapshot) { + await applyLiveFilesystemSnapshotEntry(methods, entry); } } -async function syncLiveFilesystemPathToBoundMethods( +async function snapshotLiveFilesystemPath( live: VirtualFileSystem, - methods: BoundVirtualFileSystemMethods, targetPath: string, + snapshot: LiveFilesystemSnapshotEntry[], + usage: { bytes: number }, + maxBytes: number, + knownEntry?: Pick, ): Promise { - const stat = targetPath === "/" ? await live.stat(targetPath) : await live.lstat(targetPath); + const stat = knownEntry ?? + (targetPath === "/" ? await live.stat(targetPath) : await live.lstat(targetPath)); if (stat.isSymbolicLink) { - await ensureBoundParentDirectory(methods, targetPath); - await callBoundFilesystemMethod(methods, "removeFile", targetPath).catch( - () => {}, - ); - await callBoundFilesystemMethod( - methods, - "symlink", - await live.readlink(targetPath), - targetPath, - ); + snapshot.push({ kind: "symlink", path: targetPath, target: await live.readlink(targetPath) }); return; } if (stat.isDirectory) { - await callBoundFilesystemMethod(methods, "mkdir", targetPath, { - recursive: true, - }); + snapshot.push({ kind: "directory", path: targetPath }); const children = (await live.readDirWithTypes(targetPath)) - .map((entry) => entry.name) - .filter((name) => name !== "." && name !== "..") - .sort((left, right) => left.localeCompare(right)); + .filter((entry) => entry.name !== "." && entry.name !== "..") + .sort((left, right) => left.name.localeCompare(right.name)); for (const child of children) { - await syncLiveFilesystemPathToBoundMethods( + await snapshotLiveFilesystemPath( live, - methods, - targetPath === "/" ? posixPath.join("/", child) : posixPath.join(targetPath, child), + targetPath === "/" + ? posixPath.join("/", child.name) + : posixPath.join(targetPath, child.name), + snapshot, + usage, + maxBytes, + child, ); } return; } - await ensureBoundParentDirectory(methods, targetPath); - await callBoundFilesystemMethod(methods, "writeFile", targetPath, new Uint8Array(0)); - for (let offset = 0; offset < stat.size; offset += LIVE_FILESYSTEM_SYNC_CHUNK_SIZE) { - const chunk = await live.pread( - targetPath, - offset, - Math.min(LIVE_FILESYSTEM_SYNC_CHUNK_SIZE, stat.size - offset), + const fileSize = + "size" in stat && typeof stat.size === "number" + ? stat.size + : (await live.lstat(targetPath)).size; + if (fileSize > maxBytes - usage.bytes) { + throw new LiveFilesystemSyncLimitError( + `live filesystem sync for ${targetPath} exceeds ${maxBytes} bytes ` + + "(createKernel maxFilesystemBytes); raise createKernel({ maxFilesystemBytes })", ); - if (chunk.length === 0) { - break; - } - await callBoundFilesystemMethod(methods, "pwrite", targetPath, offset, chunk); } + const content = await live.readFile(targetPath); + if (content.byteLength > maxBytes - usage.bytes) { + throw new LiveFilesystemSyncLimitError( + `live filesystem sync for ${targetPath} exceeds ${maxBytes} bytes ` + + "(createKernel maxFilesystemBytes); raise createKernel({ maxFilesystemBytes })", + ); + } + usage.bytes += content.byteLength; + snapshot.push({ kind: "file", path: targetPath, content }); +} + +async function applyLiveFilesystemSnapshotEntry( + methods: BoundVirtualFileSystemMethods, + entry: LiveFilesystemSnapshotEntry, +): Promise { + if (entry.kind === "directory") { + await callBoundFilesystemMethod(methods, "mkdir", entry.path, { recursive: true }); + return; + } + await ensureBoundParentDirectory(methods, entry.path); + if (entry.kind === "symlink") { + await callBoundFilesystemMethod(methods, "removeFile", entry.path).catch(() => {}); + await callBoundFilesystemMethod(methods, "symlink", entry.target, entry.path); + return; + } + await callBoundFilesystemMethod(methods, "writeFile", entry.path, entry.content); } function bindLiveFilesystem( target: VirtualFileSystem, getFilesystem: () => VirtualFileSystem | null, + maxBytes: number, ): LiveFilesystemBinding { const fallback: BoundVirtualFileSystemMethods = {}; for (const method of VIRTUAL_FILESYSTEM_METHOD_NAMES) { @@ -2506,7 +2598,7 @@ function bindLiveFilesystem( if (!filesystem) { return; } - await syncLiveFilesystemToBoundMethods(filesystem, fallback, paths); + await syncLiveFilesystemToBoundMethods(filesystem, fallback, paths, maxBytes); }, restore(): void { for (const [method, delegate] of Object.entries(fallback)) { @@ -2568,6 +2660,7 @@ class NativeKernel implements Kernel { readOnly?: boolean; }>; syncFilesystemOnDispose?: boolean; + maxFilesystemBytes?: number; }, ) { this.env = { ...(options.env ?? {}) }; @@ -2610,6 +2703,7 @@ class NativeKernel implements Kernel { this.liveFilesystemBinding = bindLiveFilesystem( this.options.filesystem, () => this.rootFilesystem, + options.maxFilesystemBytes ?? DEFAULT_LIVE_FILESYSTEM_SYNC_MAX_BYTES, ); } @@ -2984,6 +3078,15 @@ class NativeKernel implements Kernel { return this.proxy!.rename(oldPath, newPath); } + async pwrite( + targetPath: string, + offset: number, + data: Uint8Array, + ): Promise { + await this.ensureReady(); + return this.proxy!.vfs.pwrite(targetPath, offset, data); + } + private tryResolveMountedCommand(command: string): boolean { const normalized = normalizeCommandLookup(command); for (const driver of this.mountedRuntimeDrivers) { @@ -3071,6 +3174,8 @@ class NativeKernel implements Kernel { cwd: REPO_ROOT, command: ensureNativeSidecarBinary(), args: [], + gracefulExitMs: 100, + forceExitMs: 100, }), ); const session = await this.measureBoot("session_open", () => @@ -3212,6 +3317,7 @@ export function createKernel(options: { logger?: unknown; mounts?: Array<{ path: string; fs: VirtualFileSystem; readOnly?: boolean }>; syncFilesystemOnDispose?: boolean; + maxFilesystemBytes?: number; }): Kernel { return new NativeKernel(options); } diff --git a/packages/runtime-core/src/vm-config.ts b/packages/runtime-core/src/vm-config.ts index c1f4557803..b42615f3f1 100644 --- a/packages/runtime-core/src/vm-config.ts +++ b/packages/runtime-core/src/vm-config.ts @@ -16,6 +16,7 @@ export type { PatternPermissionScope } from "./generated/PatternPermissionScope. export type { PermissionMode } from "./generated/PermissionMode.js"; export type { PermissionsPolicy } from "./generated/PermissionsPolicy.js"; export type { PluginLimitsConfig } from "./generated/PluginLimitsConfig.js"; +export type { ProcessLimitsConfig } from "./generated/ProcessLimitsConfig.js"; export type { PythonLimitsConfig } from "./generated/PythonLimitsConfig.js"; export type { ResourceLimitsConfig } from "./generated/ResourceLimitsConfig.js"; export type { RootFilesystemConfig } from "./generated/RootFilesystemConfig.js"; diff --git a/packages/runtime-core/tests/copy-wasm-commands.test.ts b/packages/runtime-core/tests/copy-wasm-commands.test.ts new file mode 100644 index 0000000000..8445cb727c --- /dev/null +++ b/packages/runtime-core/tests/copy-wasm-commands.test.ts @@ -0,0 +1,139 @@ +import { + existsSync, + lstatSync, + mkdirSync, + mkdtempSync, + readdirSync, + readFileSync, + rmSync, + symlinkSync, + writeFileSync, +} from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { + copyWasmCommands, + requiredSoftwareCommandNames, +} from "../scripts/copy-wasm-commands.mjs"; + +const roots: string[] = []; + +function tempRoot(): string { + const root = mkdtempSync(join(tmpdir(), "agentos-copy-commands-")); + roots.push(root); + return root; +} + +function writeManifest( + softwareRoot: string, + packageName: string, + manifest: Record, +): void { + const packageDir = join(softwareRoot, packageName); + mkdirSync(packageDir, { recursive: true }); + writeFileSync( + join(packageDir, "agentos-package.json"), + `${JSON.stringify(manifest)}\n`, + ); +} + +function fixture() { + const root = tempRoot(); + const sourceDir = join(root, "source"); + const destDir = join(root, "dest"); + const softwareRoot = join(root, "software"); + mkdirSync(sourceDir, { recursive: true }); + mkdirSync(destDir, { recursive: true }); + mkdirSync(softwareRoot, { recursive: true }); + writeManifest(softwareRoot, "default-tools", { + commands: ["alpha"], + aliases: { "alpha-alias": "alpha" }, + stubs: ["legacy"], + }); + for (const [packageName, command] of [ + ["codex-cli", "codex"], + ["duckdb", "duckdb"], + ["vim", "vim"], + ]) { + writeManifest(softwareRoot, packageName, { commands: [command] }); + } + return { root, sourceDir, destDir, softwareRoot }; +} + +afterEach(() => { + for (const root of roots.splice(0)) { + rmSync(root, { recursive: true, force: true }); + } +}); + +describe("copy WASM commands", () => { + it("derives commands, aliases, and stubs while excluding optional builds", () => { + const { softwareRoot } = fixture(); + expect(requiredSoftwareCommandNames(softwareRoot)).toEqual([ + "alpha", + "alpha-alias", + "legacy", + ]); + }); + + it("fails required preflight without erasing previously vendored commands", () => { + const { sourceDir, destDir, softwareRoot } = fixture(); + writeFileSync(join(sourceDir, "alpha"), "alpha"); + writeFileSync(join(destDir, "known-good"), "preserve me"); + + expect(() => + copyWasmCommands({ + sourceDir, + destDir, + softwareRoot, + requireCommands: true, + log: () => {}, + }), + ).toThrow(/missing required default WASM commands.*alpha-alias, legacy/); + expect(readFileSync(join(destDir, "known-good"), "utf8")).toBe( + "preserve me", + ); + }); + + it("copies optional extras with exact basenames and dereferences aliases", () => { + const { sourceDir, destDir, softwareRoot } = fixture(); + for (const name of ["alpha", "alpha-alias", "legacy", "codex"]) { + writeFileSync(join(sourceDir, name), name); + } + writeFileSync(join(sourceDir, "extra-real"), "extra"); + symlinkSync("extra-real", join(sourceDir, "extra-alias")); + + copyWasmCommands({ + sourceDir, + destDir, + softwareRoot, + requireCommands: true, + log: () => {}, + }); + + expect(readdirSync(destDir).sort()).toEqual(readdirSync(sourceDir).sort()); + expect(lstatSync(join(destDir, "extra-alias")).isFile()).toBe(true); + expect(lstatSync(join(destDir, "extra-alias")).isSymbolicLink()).toBe( + false, + ); + expect(readFileSync(join(destDir, "extra-alias"), "utf8")).toBe("extra"); + }); + + it("rejects an incomplete already-vendored artifact when source is absent", () => { + const { root, destDir, softwareRoot } = fixture(); + const sourceDir = join(root, "missing-source"); + writeFileSync(join(destDir, "alpha"), "alpha"); + + expect(() => + copyWasmCommands({ + sourceDir, + destDir, + softwareRoot, + requireCommands: true, + log: () => {}, + }), + ).toThrow(/missing required default WASM commands.*alpha-alias, legacy/); + expect(existsSync(join(destDir, "alpha"))).toBe(true); + }); +}); diff --git a/registry/tests/kernel/bridge-child-process.test.ts b/packages/runtime-core/tests/integration/bridge-child-process.test.ts similarity index 97% rename from registry/tests/kernel/bridge-child-process.test.ts rename to packages/runtime-core/tests/integration/bridge-child-process.test.ts index 7cf5032c62..930256fccf 100644 --- a/registry/tests/kernel/bridge-child-process.test.ts +++ b/packages/runtime-core/tests/integration/bridge-child-process.test.ts @@ -21,8 +21,8 @@ import { describeIf, createIntegrationKernel, NodeFileSystem, -} from './helpers.ts'; -import type { IntegrationKernelResult } from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; +import type { IntegrationKernelResult } from '@rivet-dev/agentos-vm-test-harness'; const __dirname = dirname(fileURLToPath(import.meta.url)); const PACKAGED_COREUTILS_COMMANDS_DIR = resolve( @@ -319,7 +319,17 @@ describeIf(!skipReason, 'bridge child_process → kernel routing', () => { fs.chmodSync('/tmp/write-only.txt', 0o200); // A real shell opens the append target write-only, so a 0o200 file is // appendable even though it cannot be read back until the chmod below. - execSync("printf changed >> /tmp/write-only.txt", { encoding: 'utf-8' }); + try { + execSync("printf changed >> /tmp/write-only.txt", { encoding: 'utf-8' }); + } catch (error) { + console.error(JSON.stringify({ + message: error instanceof Error ? error.message : String(error), + status: error && typeof error === 'object' && 'status' in error ? error.status : null, + stdout: error && typeof error === 'object' && 'stdout' in error ? String(error.stdout ?? '') : '', + stderr: error && typeof error === 'object' && 'stderr' in error ? String(error.stderr ?? '') : '' + })); + process.exit(99); + } fs.chmodSync('/tmp/write-only.txt', 0o600); console.log(JSON.stringify({ mode: 'loaded', diff --git a/registry/tests/kernel/cross-runtime-network.test.ts b/packages/runtime-core/tests/integration/cross-runtime-network.test.ts similarity index 92% rename from registry/tests/kernel/cross-runtime-network.test.ts rename to packages/runtime-core/tests/integration/cross-runtime-network.test.ts index 98519e2b24..9336b2320c 100644 --- a/registry/tests/kernel/cross-runtime-network.test.ts +++ b/packages/runtime-core/tests/integration/cross-runtime-network.test.ts @@ -16,10 +16,10 @@ import { createIntegrationKernel, itIf, skipUnlessWasmBuilt, -} from './helpers.ts'; -import type { IntegrationKernelResult, Kernel } from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; +import type { IntegrationKernelResult, Kernel } from '@rivet-dev/agentos-vm-test-harness'; -const WASM_HTTP_GET = resolve(C_BUILD_DIR, 'http_get'); +const WASM_CURL = resolve(C_BUILD_DIR, 'curl'); const WASM_HTTP_SERVER = resolve(C_BUILD_DIR, 'http_server'); const WASM_TCP_ECHO = resolve(C_BUILD_DIR, 'tcp_echo'); const WASM_TCP_SERVER = resolve(C_BUILD_DIR, 'tcp_server'); @@ -28,7 +28,7 @@ function skipReasonWasmNetwork(): string | false { const wasmSkipReason = skipUnlessWasmBuilt(); if (wasmSkipReason) return wasmSkipReason; for (const [name, path] of [ - ['http_get', WASM_HTTP_GET], + ['curl', WASM_CURL], ['http_server', WASM_HTTP_SERVER], ['tcp_echo', WASM_TCP_ECHO], ['tcp_server', WASM_TCP_SERVER], @@ -231,7 +231,7 @@ describe('cross-runtime network integration', { timeout: 90_000 }, () => { expect(client.stdout.trim()).toBe('js-pong:ping'); }); - itIf(!wasmNetworkSkipReason, 'W1 WASM http_get -> JS node:http server over VM loopback', async () => { + itIf(!wasmNetworkSkipReason, 'W1 WASM curl -> JS node:http server over VM loopback', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'], commandDirs: [C_BUILD_DIR, COMMANDS_DIR], @@ -239,13 +239,13 @@ describe('cross-runtime network integration', { timeout: 90_000 }, () => { const server = spawnGuestNodeProgram(ctx.kernel, guestJsHttpServer(3102)); await waitForOutput(server, 'js http listening 3102', 'JS HTTP server'); - const wasm = await ctx.kernel.exec('http_get 3102 /from-wasm'); + const wasm = await ctx.kernel.exec('curl -fsS http://127.0.0.1:3102/from-wasm'); server.process.kill(15); await server.process.wait().catch(() => {}); expect(wasm.exitCode).toBe(0); - expect(wasm.stderr).not.toContain('socket error'); - expect(wasm.stdout).toContain('body: js:GET:/from-wasm'); + expect(wasm.stderr).toBe(''); + expect(wasm.stdout.trim()).toBe('js:GET:/from-wasm'); }); itIf(!wasmNetworkSkipReason, 'J3 JS fetch -> WASM HTTP server over VM loopback', async () => { @@ -341,7 +341,7 @@ describe('cross-runtime network integration', { timeout: 90_000 }, () => { expect(wasm.stdout).toContain('received: js-pong:hello'); }); - itIf(!wasmNetworkSkipReason, 'W3 WASM http_get -> WASM HTTP server over VM loopback', async () => { + itIf(!wasmNetworkSkipReason, 'W3 WASM curl -> WASM HTTP server over VM loopback', async () => { ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'], commandDirs: [C_BUILD_DIR, COMMANDS_DIR], @@ -349,12 +349,12 @@ describe('cross-runtime network integration', { timeout: 90_000 }, () => { const server = spawnGuestProgram(ctx.kernel, 'http_server', ['3108']); await waitForListener(ctx.kernel, 3108, 'WASM HTTP server'); - const wasm = await ctx.kernel.exec('http_get 3108 /from-wasm'); + const wasm = await ctx.kernel.exec('curl -fsS http://127.0.0.1:3108/from-wasm'); const serverExit = await server.process.wait(); expect(wasm.exitCode).toBe(0); - expect(wasm.stderr).not.toContain('socket error'); - expect(wasm.stdout).toContain('body: wasm:GET:/from-wasm'); + expect(wasm.stderr).toBe(''); + expect(wasm.stdout.trim()).toBe('wasm:GET:/from-wasm'); expect(serverExit).toBe(0); }); @@ -426,7 +426,7 @@ describe('cross-runtime network integration', { timeout: 90_000 }, () => { } }); - itIf(!wasmNetworkSkipReason, 'O2 WASM http_get -> host loopback requires loopback exemption', async () => { + itIf(!wasmNetworkSkipReason, 'O2 WASM curl -> host loopback requires loopback exemption', async () => { const seenRequests: string[] = []; const hostServer = createHttpServer((req, res) => { seenRequests.push(req.url ?? ''); @@ -443,9 +443,9 @@ describe('cross-runtime network integration', { timeout: 90_000 }, () => { runtimes: ['wasmvm', 'node'], commandDirs: [C_BUILD_DIR, COMMANDS_DIR], }); - const noExemption = await ctx.kernel.exec(`http_get ${port} /blocked`); + const noExemption = await ctx.kernel.exec(`curl -fsS http://127.0.0.1:${port}/blocked`); expect(noExemption.exitCode).not.toBe(0); - expect(noExemption.stderr).toMatch(/EACCES|Bad address|Connection refused|connect/); + expect(noExemption.stderr).toMatch(/EACCES|Bad address|Connection refused|connect|Failed to connect|Invalid argument/); expect(seenRequests).toEqual([]); await ctx.dispose(); @@ -454,10 +454,10 @@ describe('cross-runtime network integration', { timeout: 90_000 }, () => { commandDirs: [C_BUILD_DIR, COMMANDS_DIR], loopbackExemptPorts: [port], }); - const allowed = await ctx.kernel.exec(`http_get ${port} /allowed`); + const allowed = await ctx.kernel.exec(`curl -fsS http://127.0.0.1:${port}/allowed`); expect(allowed.exitCode).toBe(0); - expect(allowed.stderr).not.toContain('socket error'); - expect(allowed.stdout).toContain('body: host:/allowed'); + expect(allowed.stderr).toBe(''); + expect(allowed.stdout.trim()).toBe('host:/allowed'); expect(seenRequests).toEqual(['/allowed']); } finally { await new Promise((resolveClose) => hostServer.close(() => resolveClose())); diff --git a/registry/tests/kernel/cross-runtime-pipes.test.ts b/packages/runtime-core/tests/integration/cross-runtime-pipes.test.ts similarity index 95% rename from registry/tests/kernel/cross-runtime-pipes.test.ts rename to packages/runtime-core/tests/integration/cross-runtime-pipes.test.ts index 4db8328b22..8c19d7e198 100644 --- a/registry/tests/kernel/cross-runtime-pipes.test.ts +++ b/packages/runtime-core/tests/integration/cross-runtime-pipes.test.ts @@ -17,8 +17,8 @@ import { describeIf, createIntegrationKernel, skipUnlessWasmBuilt, -} from './helpers.ts'; -import type { Kernel } from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; +import type { Kernel } from '@rivet-dev/agentos-vm-test-harness'; // --------------------------------------------------------------------------- // Integration tests with real WasmVM + Node (skipped if WASM not built) diff --git a/registry/tests/kernel/cross-runtime-terminal.test.ts b/packages/runtime-core/tests/integration/cross-runtime-terminal.test.ts similarity index 98% rename from registry/tests/kernel/cross-runtime-terminal.test.ts rename to packages/runtime-core/tests/integration/cross-runtime-terminal.test.ts index 5a4f14b749..3fd5039d1f 100644 --- a/registry/tests/kernel/cross-runtime-terminal.test.ts +++ b/packages/runtime-core/tests/integration/cross-runtime-terminal.test.ts @@ -15,8 +15,8 @@ import { createIntegrationKernel, skipUnlessWasmBuilt, TerminalHarness, -} from './helpers.ts'; -import type { IntegrationKernelResult } from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; +import type { IntegrationKernelResult } from '@rivet-dev/agentos-vm-test-harness'; /** brush-shell interactive prompt. */ const PROMPT = 'sh-0.4$ '; diff --git a/registry/tests/kernel/ctrl-c-shell-behavior.test.ts b/packages/runtime-core/tests/integration/ctrl-c-shell-behavior.test.ts similarity index 95% rename from registry/tests/kernel/ctrl-c-shell-behavior.test.ts rename to packages/runtime-core/tests/integration/ctrl-c-shell-behavior.test.ts index 08af122ec2..3788f6fc1c 100644 --- a/registry/tests/kernel/ctrl-c-shell-behavior.test.ts +++ b/packages/runtime-core/tests/integration/ctrl-c-shell-behavior.test.ts @@ -16,8 +16,8 @@ import { createIntegrationKernel, skipUnlessWasmBuilt, TerminalHarness, -} from './helpers.ts'; -import type { IntegrationKernelResult } from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; +import type { IntegrationKernelResult } from '@rivet-dev/agentos-vm-test-harness'; const PROMPT = 'sh-0.4$ '; const wasmSkip = skipUnlessWasmBuilt(); diff --git a/registry/tests/kernel/dispose-behavior.test.ts b/packages/runtime-core/tests/integration/dispose-behavior.test.ts similarity index 92% rename from registry/tests/kernel/dispose-behavior.test.ts rename to packages/runtime-core/tests/integration/dispose-behavior.test.ts index 299b08e3fc..d77a5cbcd5 100644 --- a/registry/tests/kernel/dispose-behavior.test.ts +++ b/packages/runtime-core/tests/integration/dispose-behavior.test.ts @@ -17,9 +17,9 @@ import { createIntegrationKernel, skipUnlessWasmBuilt, createInMemoryFileSystem, -} from './helpers.ts'; -import type { Kernel } from './helpers.ts'; -import type { IntegrationKernelResult } from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; +import type { Kernel } from '@rivet-dev/agentos-vm-test-harness'; +import type { IntegrationKernelResult } from '@rivet-dev/agentos-vm-test-harness'; const skipReason = skipUnlessWasmBuilt(); diff --git a/registry/tests/wasmvm/dynamic-module-integration.test.ts b/packages/runtime-core/tests/integration/dynamic-module-integration.test.ts similarity index 98% rename from registry/tests/wasmvm/dynamic-module-integration.test.ts rename to packages/runtime-core/tests/integration/dynamic-module-integration.test.ts index a884adee9c..afc8bd00e9 100644 --- a/registry/tests/wasmvm/dynamic-module-integration.test.ts +++ b/packages/runtime-core/tests/integration/dynamic-module-integration.test.ts @@ -10,16 +10,16 @@ */ import { describe, it, expect, afterEach, vi } from 'vitest'; -import { createWasmVmRuntime, WASMVM_COMMANDS } from '../helpers.js'; -import type { WasmVmRuntimeOptions } from '../helpers.js'; -import { COMMANDS_DIR, createKernel, describeIf, hasWasmBinaries } from '../helpers.js'; +import { createWasmVmRuntime, WASMVM_COMMANDS } from '@rivet-dev/agentos-vm-test-harness'; +import type { WasmVmRuntimeOptions } from '@rivet-dev/agentos-vm-test-harness'; +import { COMMANDS_DIR, createKernel, describeIf, hasWasmBinaries } from '@rivet-dev/agentos-vm-test-harness'; import type { DriverProcess, Kernel, KernelInterface, KernelRuntimeDriver as RuntimeDriver, ProcessContext, -} from '../helpers.js'; +} from '@rivet-dev/agentos-vm-test-harness'; import { writeFile, mkdir, rm, symlink } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; diff --git a/registry/tests/kernel/e2e-concurrently.test.ts b/packages/runtime-core/tests/integration/e2e-concurrently.test.ts similarity index 99% rename from registry/tests/kernel/e2e-concurrently.test.ts rename to packages/runtime-core/tests/integration/e2e-concurrently.test.ts index 95ef7cf24d..bdc77ed515 100644 --- a/registry/tests/kernel/e2e-concurrently.test.ts +++ b/packages/runtime-core/tests/integration/e2e-concurrently.test.ts @@ -25,7 +25,7 @@ import { createWasmVmRuntime, createNodeRuntime, skipUnlessWasmBuilt, -} from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; const wasmSkip = skipUnlessWasmBuilt(); diff --git a/registry/tests/kernel/e2e-nextjs-build.test.ts b/packages/runtime-core/tests/integration/e2e-nextjs-build.test.ts similarity index 98% rename from registry/tests/kernel/e2e-nextjs-build.test.ts rename to packages/runtime-core/tests/integration/e2e-nextjs-build.test.ts index f5ade032a4..3ebf9e7192 100644 --- a/registry/tests/kernel/e2e-nextjs-build.test.ts +++ b/packages/runtime-core/tests/integration/e2e-nextjs-build.test.ts @@ -29,7 +29,7 @@ import { createWasmVmRuntime, createNodeRuntime, skipUnlessWasmBuilt, -} from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; const wasmSkip = skipUnlessWasmBuilt(); const __dirname = path.dirname(fileURLToPath(import.meta.url)); diff --git a/registry/tests/kernel/e2e-npm-install.test.ts b/packages/runtime-core/tests/integration/e2e-npm-install.test.ts similarity index 98% rename from registry/tests/kernel/e2e-npm-install.test.ts rename to packages/runtime-core/tests/integration/e2e-npm-install.test.ts index 42c508be53..d7cd270212 100644 --- a/registry/tests/kernel/e2e-npm-install.test.ts +++ b/packages/runtime-core/tests/integration/e2e-npm-install.test.ts @@ -22,7 +22,7 @@ import { createWasmVmRuntime, createNodeRuntime, skipUnlessWasmBuilt, -} from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; const wasmSkip = skipUnlessWasmBuilt(); diff --git a/registry/tests/kernel/e2e-npm-lifecycle.test.ts b/packages/runtime-core/tests/integration/e2e-npm-lifecycle.test.ts similarity index 99% rename from registry/tests/kernel/e2e-npm-lifecycle.test.ts rename to packages/runtime-core/tests/integration/e2e-npm-lifecycle.test.ts index 69f3f46e12..cf1b7b3ff0 100644 --- a/registry/tests/kernel/e2e-npm-lifecycle.test.ts +++ b/packages/runtime-core/tests/integration/e2e-npm-lifecycle.test.ts @@ -24,7 +24,7 @@ import { createWasmVmRuntime, createNodeRuntime, skipUnlessWasmBuilt, -} from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; const wasmSkip = skipUnlessWasmBuilt(); diff --git a/registry/tests/kernel/e2e-npm-scripts.test.ts b/packages/runtime-core/tests/integration/e2e-npm-scripts.test.ts similarity index 96% rename from registry/tests/kernel/e2e-npm-scripts.test.ts rename to packages/runtime-core/tests/integration/e2e-npm-scripts.test.ts index ff16dc5c0b..3950bf97a6 100644 --- a/registry/tests/kernel/e2e-npm-scripts.test.ts +++ b/packages/runtime-core/tests/integration/e2e-npm-scripts.test.ts @@ -9,7 +9,7 @@ */ import { describe, expect, it } from 'vitest'; -import { createIntegrationKernel, skipUnlessWasmBuilt } from './helpers.ts'; +import { createIntegrationKernel, skipUnlessWasmBuilt } from '@rivet-dev/agentos-vm-test-harness'; const skipReason = skipUnlessWasmBuilt(); void skipReason; diff --git a/registry/tests/kernel/e2e-npm-suite.test.ts b/packages/runtime-core/tests/integration/e2e-npm-suite.test.ts similarity index 99% rename from registry/tests/kernel/e2e-npm-suite.test.ts rename to packages/runtime-core/tests/integration/e2e-npm-suite.test.ts index 6d5faffe2a..276ce773a7 100644 --- a/registry/tests/kernel/e2e-npm-suite.test.ts +++ b/packages/runtime-core/tests/integration/e2e-npm-suite.test.ts @@ -26,7 +26,7 @@ import { createIntegrationKernel, NodeFileSystem, skipUnlessWasmBuilt, -} from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; const wasmSkip = skipUnlessWasmBuilt(); diff --git a/registry/tests/kernel/e2e-npm-version-init.test.ts b/packages/runtime-core/tests/integration/e2e-npm-version-init.test.ts similarity index 95% rename from registry/tests/kernel/e2e-npm-version-init.test.ts rename to packages/runtime-core/tests/integration/e2e-npm-version-init.test.ts index 3f529ae057..9debd4d285 100644 --- a/registry/tests/kernel/e2e-npm-version-init.test.ts +++ b/packages/runtime-core/tests/integration/e2e-npm-version-init.test.ts @@ -13,7 +13,7 @@ */ import { describe, expect, it } from 'vitest'; -import { createIntegrationKernel, skipUnlessWasmBuilt } from './helpers.ts'; +import { createIntegrationKernel, skipUnlessWasmBuilt } from '@rivet-dev/agentos-vm-test-harness'; const skipReason = skipUnlessWasmBuilt(); void skipReason; diff --git a/registry/tests/kernel/e2e-npx-and-pipes.test.ts b/packages/runtime-core/tests/integration/e2e-npx-and-pipes.test.ts similarity index 98% rename from registry/tests/kernel/e2e-npx-and-pipes.test.ts rename to packages/runtime-core/tests/integration/e2e-npx-and-pipes.test.ts index a4d49092ee..c6c3ba84ab 100644 --- a/registry/tests/kernel/e2e-npx-and-pipes.test.ts +++ b/packages/runtime-core/tests/integration/e2e-npx-and-pipes.test.ts @@ -11,7 +11,7 @@ import { createIntegrationKernel, itIf, skipUnlessWasmBuilt, -} from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; const skipReason = skipUnlessWasmBuilt(); void skipReason; diff --git a/registry/tests/kernel/e2e-project-matrix.test.ts b/packages/runtime-core/tests/integration/e2e-project-matrix.test.ts similarity index 99% rename from registry/tests/kernel/e2e-project-matrix.test.ts rename to packages/runtime-core/tests/integration/e2e-project-matrix.test.ts index cc9eaabb78..27f5297a20 100644 --- a/registry/tests/kernel/e2e-project-matrix.test.ts +++ b/packages/runtime-core/tests/integration/e2e-project-matrix.test.ts @@ -26,7 +26,7 @@ import { createWasmVmRuntime, createNodeRuntime, skipUnlessWasmBuilt, -} from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; const execFileAsync = promisify(execFile); const TEST_TIMEOUT_MS = 55_000; diff --git a/registry/tests/kernel/error-propagation.test.ts b/packages/runtime-core/tests/integration/error-propagation.test.ts similarity index 95% rename from registry/tests/kernel/error-propagation.test.ts rename to packages/runtime-core/tests/integration/error-propagation.test.ts index c71c3c580d..cdb233c52d 100644 --- a/registry/tests/kernel/error-propagation.test.ts +++ b/packages/runtime-core/tests/integration/error-propagation.test.ts @@ -13,8 +13,8 @@ import { describeIf, createIntegrationKernel, skipUnlessWasmBuilt, -} from './helpers.ts'; -import type { IntegrationKernelResult } from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; +import type { IntegrationKernelResult } from '@rivet-dev/agentos-vm-test-harness'; const skipReason = skipUnlessWasmBuilt(); diff --git a/registry/tests/kernel/exec-integration.test.ts b/packages/runtime-core/tests/integration/exec-integration.test.ts similarity index 97% rename from registry/tests/kernel/exec-integration.test.ts rename to packages/runtime-core/tests/integration/exec-integration.test.ts index a02216d71b..9cd0184869 100644 --- a/registry/tests/kernel/exec-integration.test.ts +++ b/packages/runtime-core/tests/integration/exec-integration.test.ts @@ -13,8 +13,8 @@ import { describeIf, createIntegrationKernel, skipUnlessWasmBuilt, -} from './helpers.ts'; -import type { IntegrationKernelResult } from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; +import type { IntegrationKernelResult } from '@rivet-dev/agentos-vm-test-harness'; const skipReason = skipUnlessWasmBuilt(); diff --git a/registry/tests/kernel/fd-inheritance.test.ts b/packages/runtime-core/tests/integration/fd-inheritance.test.ts similarity index 95% rename from registry/tests/kernel/fd-inheritance.test.ts rename to packages/runtime-core/tests/integration/fd-inheritance.test.ts index b99412009d..7892e5e75a 100644 --- a/registry/tests/kernel/fd-inheritance.test.ts +++ b/packages/runtime-core/tests/integration/fd-inheritance.test.ts @@ -12,8 +12,8 @@ import { describeIf, createIntegrationKernel, skipUnlessWasmBuilt, -} from './helpers.ts'; -import type { IntegrationKernelResult } from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; +import type { IntegrationKernelResult } from '@rivet-dev/agentos-vm-test-harness'; const skipReason = skipUnlessWasmBuilt(); diff --git a/registry/tests/kernel/module-resolution.test.ts b/packages/runtime-core/tests/integration/module-resolution.test.ts similarity index 96% rename from registry/tests/kernel/module-resolution.test.ts rename to packages/runtime-core/tests/integration/module-resolution.test.ts index 829f757144..424bbb5a69 100644 --- a/registry/tests/kernel/module-resolution.test.ts +++ b/packages/runtime-core/tests/integration/module-resolution.test.ts @@ -12,8 +12,8 @@ import { describeIf, createIntegrationKernel, skipUnlessWasmBuilt, -} from './helpers.ts'; -import type { IntegrationKernelResult } from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; +import type { IntegrationKernelResult } from '@rivet-dev/agentos-vm-test-harness'; const skipReason = skipUnlessWasmBuilt(); diff --git a/packages/runtime-core/tests/integration/net-server.test.ts b/packages/runtime-core/tests/integration/net-server.test.ts new file mode 100644 index 0000000000..b25935c237 --- /dev/null +++ b/packages/runtime-core/tests/integration/net-server.test.ts @@ -0,0 +1,137 @@ +/** + * Integration test for WasmVM TCP server sockets. + * + * Spawns the tcp_server C program as WASM and connects to it from a guest Node + * process over VM loopback, proving bytes cross the kernel socket table. + */ + +import { afterEach, describe, expect, it } from "vitest"; +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; +import { + C_BUILD_DIR, + COMMANDS_DIR, + createIntegrationKernel, + describeIf, + skipUnlessWasmBuilt, +} from "@rivet-dev/agentos-vm-test-harness"; +import type { + IntegrationKernelResult, + Kernel, +} from "@rivet-dev/agentos-vm-test-harness"; + +const WASM_TCP_SERVER = resolve(C_BUILD_DIR, "tcp_server"); + +function skipReason(): string | false { + const wasmSkipReason = skipUnlessWasmBuilt(); + if (wasmSkipReason) return wasmSkipReason; + if (!existsSync(WASM_TCP_SERVER)) { + return `tcp_server WASM binary not found at ${WASM_TCP_SERVER}`; + } + return false; +} + +interface RunningGuestProgram { + process: ReturnType; + stdoutChunks: Uint8Array[]; + stderrChunks: Uint8Array[]; + getExitCode: () => number | null; +} + +function decodeChunks(chunks: Uint8Array[]): string { + return chunks.map((chunk) => new TextDecoder().decode(chunk)).join(""); +} + +function spawnGuestProgram( + kernel: Kernel, + command: string, + args: string[], +): RunningGuestProgram { + const stdoutChunks: Uint8Array[] = []; + const stderrChunks: Uint8Array[] = []; + let exitCode: number | null = null; + const process = kernel.spawn(command, args, { + onStdout: (chunk) => stdoutChunks.push(chunk), + onStderr: (chunk) => stderrChunks.push(chunk), + }); + void process.wait().then((code) => { + exitCode = code; + }); + return { + process, + stdoutChunks, + stderrChunks, + getExitCode: () => exitCode, + }; +} + +async function runGuestNodeProgram( + kernel: Kernel, + code: string, +): Promise<{ exitCode: number; stdout: string; stderr: string }> { + const program = spawnGuestProgram(kernel, "node", ["-e", code]); + const exitCode = await program.process.wait(); + return { + exitCode, + stdout: decodeChunks(program.stdoutChunks), + stderr: decodeChunks(program.stderrChunks), + }; +} + +async function waitForListener( + kernel: Kernel, + port: number, + label: string, +): Promise { + const deadline = Date.now() + 20_000; + while (Date.now() < deadline) { + if (kernel.socketTable.findListener({ host: "0.0.0.0", port })) { + return; + } + await new Promise((resolveWait) => setTimeout(resolveWait, 20)); + } + throw new Error(`Timed out waiting for ${label} listener on port ${port}`); +} + +const TEST_PORT = 9876; + +describeIf(!skipReason(), "WasmVM TCP server integration", { timeout: 30_000 }, () => { + let ctx: IntegrationKernelResult | undefined; + + afterEach(async () => { + await ctx?.dispose(); + ctx = undefined; + }); + + it("tcp_server: accept connection, recv data, send pong", async () => { + ctx = await createIntegrationKernel({ + runtimes: ["wasmvm", "node"], + commandDirs: [C_BUILD_DIR, COMMANDS_DIR], + }); + const server = spawnGuestProgram(ctx.kernel, "tcp_server", [ + String(TEST_PORT), + ]); + await waitForListener(ctx.kernel, TEST_PORT, "WASM TCP server"); + + const client = await runGuestNodeProgram( + ctx.kernel, + [ + "const net = require('net');", + `const client = net.connect({ host: '127.0.0.1', port: ${TEST_PORT} }, () => client.write('ping'));`, + "client.on('data', (chunk) => { console.log(chunk.toString()); client.end(); });", + "client.on('error', (error) => { console.error(error); process.exit(1); });", + ].join("\n"), + ); + const serverExit = await server.process.wait(); + + expect(client.exitCode).toBe(0); + expect(client.stderr).toBe(""); + expect(client.stdout.trim()).toBe("pong"); + expect(serverExit).toBe(0); + expect(decodeChunks(server.stdoutChunks)).toContain( + "listening on port 9876", + ); + expect(decodeChunks(server.stdoutChunks)).toContain("received: ping"); + expect(decodeChunks(server.stdoutChunks)).toContain("sent: 4"); + }); +}); diff --git a/packages/runtime-core/tests/integration/net-udp.test.ts b/packages/runtime-core/tests/integration/net-udp.test.ts new file mode 100644 index 0000000000..5a98e42b84 --- /dev/null +++ b/packages/runtime-core/tests/integration/net-udp.test.ts @@ -0,0 +1,161 @@ +/** + * Integration test for WasmVM UDP sockets. + * + * Spawns the udp_echo C program as WASM and sends datagrams from a guest Node + * process over VM loopback. + */ + +import { afterEach, describe, expect, it } from "vitest"; +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; +import { + C_BUILD_DIR, + COMMANDS_DIR, + createIntegrationKernel, + describeIf, + skipUnlessWasmBuilt, +} from "@rivet-dev/agentos-vm-test-harness"; +import type { + IntegrationKernelResult, + Kernel, +} from "@rivet-dev/agentos-vm-test-harness"; + +const WASM_UDP_ECHO = resolve(C_BUILD_DIR, "udp_echo"); + +function skipReason(): string | false { + const wasmSkipReason = skipUnlessWasmBuilt(); + if (wasmSkipReason) return wasmSkipReason; + if (!existsSync(WASM_UDP_ECHO)) { + return `udp_echo WASM binary not found at ${WASM_UDP_ECHO}`; + } + return false; +} + +interface RunningGuestProgram { + process: ReturnType; + stdoutChunks: Uint8Array[]; + stderrChunks: Uint8Array[]; +} + +function decodeChunks(chunks: Uint8Array[]): string { + return chunks.map((chunk) => new TextDecoder().decode(chunk)).join(""); +} + +function spawnGuestProgram( + kernel: Kernel, + command: string, + args: string[], +): RunningGuestProgram { + const stdoutChunks: Uint8Array[] = []; + const stderrChunks: Uint8Array[] = []; + const process = kernel.spawn(command, args, { + onStdout: (chunk) => stdoutChunks.push(chunk), + onStderr: (chunk) => stderrChunks.push(chunk), + }); + return { process, stdoutChunks, stderrChunks }; +} + +async function runGuestNodeProgram( + kernel: Kernel, + code: string, +): Promise<{ exitCode: number; stdout: string; stderr: string }> { + const program = spawnGuestProgram(kernel, "node", ["-e", code]); + const exitCode = await program.process.wait(); + return { + exitCode, + stdout: decodeChunks(program.stdoutChunks), + stderr: decodeChunks(program.stderrChunks), + }; +} + +async function waitForUdpBinding(kernel: Kernel, port: number): Promise { + const deadline = Date.now() + 20_000; + while (Date.now() < deadline) { + if (kernel.socketTable.findBoundUdp({ host: "0.0.0.0", port })) { + return; + } + await new Promise((resolveWait) => setTimeout(resolveWait, 20)); + } + throw new Error(`Timed out waiting for UDP binding on port ${port}`); +} + +async function runUdpEchoCase( + kernel: Kernel, + port: number, + message: string, +): Promise<{ client: { exitCode: number; stdout: string; stderr: string }; server: RunningGuestProgram }> { + const server = spawnGuestProgram(kernel, "udp_echo", [String(port)]); + await waitForUdpBinding(kernel, port); + + const client = await runGuestNodeProgram( + kernel, + [ + "const dgram = require('dgram');", + "const client = dgram.createSocket('udp4');", + `const payload = Buffer.from(${JSON.stringify(message)});`, + "const timer = setTimeout(() => { console.error('timeout'); client.close(); process.exit(1); }, 5000);", + "client.on('message', (msg) => { clearTimeout(timer); console.log(msg.toString()); client.close(); });", + "client.on('error', (error) => { clearTimeout(timer); console.error(error); client.close(); process.exit(1); });", + `client.bind(0, '127.0.0.1', () => client.send(payload, ${port}, '127.0.0.1'));`, + ].join("\n"), + ); + + return { client, server }; +} + +const TEST_PORT = 9877; + +describeIf(!skipReason(), "WasmVM UDP integration", { timeout: 30_000 }, () => { + let ctx: IntegrationKernelResult | undefined; + + afterEach(async () => { + await ctx?.dispose(); + ctx = undefined; + }); + + it("udp_echo: recv datagram and echo it back", async () => { + ctx = await createIntegrationKernel({ + runtimes: ["wasmvm", "node"], + commandDirs: [C_BUILD_DIR, COMMANDS_DIR], + }); + + const { client, server } = await runUdpEchoCase( + ctx.kernel, + TEST_PORT, + "hello", + ); + const serverExit = await server.process.wait(); + + expect(client.exitCode).toBe(0); + expect(client.stderr).toBe(""); + expect(client.stdout.trim()).toBe("hello"); + expect(serverExit).toBe(0); + expect(decodeChunks(server.stdoutChunks)).toContain( + "listening on port 9877", + ); + expect(decodeChunks(server.stdoutChunks)).toContain("received: hello"); + expect(decodeChunks(server.stdoutChunks)).toContain("echoed: 5"); + }); + + it("udp_echo: message boundaries are preserved", async () => { + ctx = await createIntegrationKernel({ + runtimes: ["wasmvm", "node"], + commandDirs: [C_BUILD_DIR, COMMANDS_DIR], + }); + + const { client, server } = await runUdpEchoCase( + ctx.kernel, + TEST_PORT + 1, + "boundary-test-message", + ); + const serverExit = await server.process.wait(); + + expect(client.exitCode).toBe(0); + expect(client.stderr).toBe(""); + expect(client.stdout.trim()).toBe("boundary-test-message"); + expect(serverExit).toBe(0); + expect(decodeChunks(server.stdoutChunks)).toContain( + "received: boundary-test-message", + ); + }); +}); diff --git a/packages/runtime-core/tests/integration/net-unix.test.ts b/packages/runtime-core/tests/integration/net-unix.test.ts new file mode 100644 index 0000000000..b33194f094 --- /dev/null +++ b/packages/runtime-core/tests/integration/net-unix.test.ts @@ -0,0 +1,204 @@ +/** + * Integration test for WasmVM Unix domain sockets. + * + * Spawns the unix_socket C program as WASM and connects to it from a guest Node + * process through an AF_UNIX path. + */ + +import { afterEach, describe, expect, it } from "vitest"; +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; +import { + C_BUILD_DIR, + COMMANDS_DIR, + createIntegrationKernel, + describeIf, + skipUnlessWasmBuilt, +} from "@rivet-dev/agentos-vm-test-harness"; +import type { + IntegrationKernelResult, + Kernel, +} from "@rivet-dev/agentos-vm-test-harness"; + +const WASM_UNIX_SOCKET = resolve(C_BUILD_DIR, "unix_socket"); + +function skipReason(): string | false { + const wasmSkipReason = skipUnlessWasmBuilt(); + if (wasmSkipReason) return wasmSkipReason; + if (!existsSync(WASM_UNIX_SOCKET)) { + return `unix_socket WASM binary not found at ${WASM_UNIX_SOCKET}`; + } + return false; +} + +interface RunningGuestProgram { + process: ReturnType; + stdoutChunks: Uint8Array[]; + stderrChunks: Uint8Array[]; +} + +function decodeChunks(chunks: Uint8Array[]): string { + return chunks.map((chunk) => new TextDecoder().decode(chunk)).join(""); +} + +function spawnGuestProgram( + kernel: Kernel, + command: string, + args: string[], +): RunningGuestProgram { + const stdoutChunks: Uint8Array[] = []; + const stderrChunks: Uint8Array[] = []; + const process = kernel.spawn(command, args, { + onStdout: (chunk) => stdoutChunks.push(chunk), + onStderr: (chunk) => stderrChunks.push(chunk), + }); + return { process, stdoutChunks, stderrChunks }; +} + +async function runGuestNodeProgram( + kernel: Kernel, + code: string, +): Promise<{ exitCode: number; stdout: string; stderr: string }> { + const program = spawnGuestProgram(kernel, "node", ["-e", code]); + const exitCode = await program.process.wait(); + return { + exitCode, + stdout: decodeChunks(program.stdoutChunks), + stderr: decodeChunks(program.stderrChunks), + }; +} + +async function waitForUnixListener( + kernel: Kernel, + path: string, +): Promise { + const deadline = Date.now() + 20_000; + while (Date.now() < deadline) { + if (kernel.socketTable.findListener({ path })) { + return; + } + await new Promise((resolveWait) => setTimeout(resolveWait, 20)); + } + throw new Error(`Timed out waiting for Unix listener on ${path}`); +} + +const SOCK_PATH = "/tmp/test.sock"; + +describeIf(!skipReason(), "WasmVM Unix domain socket integration", { timeout: 30_000 }, () => { + let ctx: IntegrationKernelResult | undefined; + + afterEach(async () => { + await ctx?.dispose(); + ctx = undefined; + }); + + it("unix_socket: accept connection, recv data, send pong", async () => { + ctx = await createIntegrationKernel({ + runtimes: ["wasmvm", "node"], + commandDirs: [C_BUILD_DIR, COMMANDS_DIR], + }); + await ctx.kernel.mkdir("/tmp"); + const server = spawnGuestProgram(ctx.kernel, "unix_socket", [SOCK_PATH]); + await waitForUnixListener(ctx.kernel, SOCK_PATH); + + const client = await runGuestNodeProgram( + ctx.kernel, + [ + "const net = require('net');", + `const client = net.connect({ path: ${JSON.stringify(SOCK_PATH)} }, () => client.write('ping'));`, + "client.on('data', (chunk) => { console.log(chunk.toString()); client.end(); });", + "client.on('error', (error) => { console.error(error); process.exit(1); });", + ].join("\n"), + ); + const serverExit = await server.process.wait(); + + expect(client.exitCode).toBe(0); + expect(client.stderr).toBe(""); + expect(client.stdout.trim()).toBe("pong"); + expect(serverExit).toBe(0); + expect(decodeChunks(server.stdoutChunks)).toContain( + `listening on ${SOCK_PATH}`, + ); + expect(decodeChunks(server.stdoutChunks)).toContain("received: ping"); + expect(decodeChunks(server.stdoutChunks)).toContain("sent: 4"); + }); + + it("unix_socket: matches Linux abstract namespace bind/connect", async () => { + ctx = await createIntegrationKernel({ + runtimes: ["wasmvm"], + commandDirs: [C_BUILD_DIR, COMMANDS_DIR], + }); + const contract = spawnGuestProgram(ctx.kernel, "unix_socket", [ + "--abstract-contract", + ]); + const exitCode = await contract.process.wait(); + + expect(exitCode).toBe(0); + expect(decodeChunks(contract.stderrChunks)).toBe(""); + expect(decodeChunks(contract.stdoutChunks)).toBe( + "abstract_unix_namespace=ok\n", + ); + }); + + it("node: abstract Unix sockets use the same VM-local namespace", async () => { + ctx = await createIntegrationKernel({ runtimes: ["node"] }); + const result = await runGuestNodeProgram( + ctx.kernel, + [ + "const net = require('net');", + "const path = '\\0agentos-node-abstract-contract';", + "const server = net.createServer((socket) => socket.once('data', (chunk) => socket.end('pong:' + chunk)));", + "server.listen(path, () => {", + " const client = net.connect(path, () => client.write('ping'));", + " client.once('data', (chunk) => { console.log(chunk.toString()); client.end(); server.close(); });", + " client.once('error', (error) => { console.error(error); process.exitCode = 1; server.close(); });", + "});", + "server.once('error', (error) => { console.error(error); process.exit(1); });", + ].join("\n"), + ); + + expect(result.exitCode).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toBe("pong:ping\n"); + }); + + it("node: pathname socket surfaces and close/relisten match Linux", async () => { + ctx = await createIntegrationKernel({ runtimes: ["node"] }); + await ctx.kernel.mkdir("/tmp"); + const result = await runGuestNodeProgram( + ctx.kernel, + [ + "const fs = require('fs');", + "const net = require('net');", + `const path = ${JSON.stringify("/tmp/node-close.sock")};`, + "const summarize = (socket) => ({", + " address: socket.address(),", + " localAddress: socket.localAddress,", + " remoteAddress: socket.remoteAddress,", + " hasLocalAddress: Object.hasOwn(socket, 'localAddress'),", + " hasRemoteAddress: Object.hasOwn(socket, 'remoteAddress'),", + "});", + "const server = net.createServer((socket) => { console.log('accepted=' + JSON.stringify(summarize(socket))); socket.end(); });", + "server.listen(path, () => {", + " console.log('server=' + JSON.stringify(server.address()));", + " const client = net.connect(path, () => console.log('client=' + JSON.stringify(summarize(client))));", + " client.once('close', () => server.close(() => {", + " console.log('unlinked=' + String(!fs.existsSync(path)));", + " server.once('listening', () => { console.log('relisten=' + JSON.stringify(server.address())); server.close(); });", + " server.listen(path);", + " }));", + "});", + "server.once('error', (error) => { console.error(error); process.exit(1); });", + ].join("\n"), + ); + + expect(result.exitCode).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toContain('server="/tmp/node-close.sock"'); + expect(result.stdout).toContain('"address":{}'); + expect(result.stdout).toContain('"hasLocalAddress":false'); + expect(result.stdout).toContain('"hasRemoteAddress":false'); + expect(result.stdout).toContain("unlinked=true"); + expect(result.stdout).toContain('relisten="/tmp/node-close.sock"'); + }); +}); diff --git a/registry/tests/kernel/node-binary-behavior.test.ts b/packages/runtime-core/tests/integration/node-binary-behavior.test.ts similarity index 99% rename from registry/tests/kernel/node-binary-behavior.test.ts rename to packages/runtime-core/tests/integration/node-binary-behavior.test.ts index 0f71bd648b..6464eb3f44 100644 --- a/registry/tests/kernel/node-binary-behavior.test.ts +++ b/packages/runtime-core/tests/integration/node-binary-behavior.test.ts @@ -26,8 +26,8 @@ import { createWasmVmRuntime, NodeFileSystem, COMMANDS_DIR, -} from './helpers.ts'; -import type { IntegrationKernelResult } from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; +import type { IntegrationKernelResult } from '@rivet-dev/agentos-vm-test-harness'; const skipReason = skipUnlessWasmBuilt(); diff --git a/registry/tests/projects/astro-pass/astro.config.mjs b/packages/runtime-core/tests/integration/projects/astro-pass/astro.config.mjs similarity index 100% rename from registry/tests/projects/astro-pass/astro.config.mjs rename to packages/runtime-core/tests/integration/projects/astro-pass/astro.config.mjs diff --git a/registry/tests/projects/astro-pass/fixture.json b/packages/runtime-core/tests/integration/projects/astro-pass/fixture.json similarity index 100% rename from registry/tests/projects/astro-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/astro-pass/fixture.json diff --git a/registry/tests/projects/astro-pass/package.json b/packages/runtime-core/tests/integration/projects/astro-pass/package.json similarity index 100% rename from registry/tests/projects/astro-pass/package.json rename to packages/runtime-core/tests/integration/projects/astro-pass/package.json diff --git a/registry/tests/projects/astro-pass/src/components/Counter.jsx b/packages/runtime-core/tests/integration/projects/astro-pass/src/components/Counter.jsx similarity index 100% rename from registry/tests/projects/astro-pass/src/components/Counter.jsx rename to packages/runtime-core/tests/integration/projects/astro-pass/src/components/Counter.jsx diff --git a/registry/tests/projects/astro-pass/src/index.js b/packages/runtime-core/tests/integration/projects/astro-pass/src/index.js similarity index 100% rename from registry/tests/projects/astro-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/astro-pass/src/index.js diff --git a/registry/tests/projects/astro-pass/src/pages/index.astro b/packages/runtime-core/tests/integration/projects/astro-pass/src/pages/index.astro similarity index 100% rename from registry/tests/projects/astro-pass/src/pages/index.astro rename to packages/runtime-core/tests/integration/projects/astro-pass/src/pages/index.astro diff --git a/registry/tests/projects/axios-pass/fixture.json b/packages/runtime-core/tests/integration/projects/axios-pass/fixture.json similarity index 100% rename from registry/tests/projects/axios-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/axios-pass/fixture.json diff --git a/registry/tests/projects/axios-pass/package.json b/packages/runtime-core/tests/integration/projects/axios-pass/package.json similarity index 100% rename from registry/tests/projects/axios-pass/package.json rename to packages/runtime-core/tests/integration/projects/axios-pass/package.json diff --git a/registry/tests/projects/axios-pass/src/index.js b/packages/runtime-core/tests/integration/projects/axios-pass/src/index.js similarity index 100% rename from registry/tests/projects/axios-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/axios-pass/src/index.js diff --git a/registry/tests/projects/bcryptjs-pass/fixture.json b/packages/runtime-core/tests/integration/projects/bcryptjs-pass/fixture.json similarity index 100% rename from registry/tests/projects/bcryptjs-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/bcryptjs-pass/fixture.json diff --git a/registry/tests/projects/bcryptjs-pass/package.json b/packages/runtime-core/tests/integration/projects/bcryptjs-pass/package.json similarity index 100% rename from registry/tests/projects/bcryptjs-pass/package.json rename to packages/runtime-core/tests/integration/projects/bcryptjs-pass/package.json diff --git a/registry/tests/projects/bcryptjs-pass/pnpm-lock.yaml b/packages/runtime-core/tests/integration/projects/bcryptjs-pass/pnpm-lock.yaml similarity index 100% rename from registry/tests/projects/bcryptjs-pass/pnpm-lock.yaml rename to packages/runtime-core/tests/integration/projects/bcryptjs-pass/pnpm-lock.yaml diff --git a/registry/tests/projects/bcryptjs-pass/src/index.js b/packages/runtime-core/tests/integration/projects/bcryptjs-pass/src/index.js similarity index 100% rename from registry/tests/projects/bcryptjs-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/bcryptjs-pass/src/index.js diff --git a/registry/tests/projects/bun-layout-pass/bun.lock b/packages/runtime-core/tests/integration/projects/bun-layout-pass/bun.lock similarity index 100% rename from registry/tests/projects/bun-layout-pass/bun.lock rename to packages/runtime-core/tests/integration/projects/bun-layout-pass/bun.lock diff --git a/registry/tests/projects/bun-layout-pass/fixture.json b/packages/runtime-core/tests/integration/projects/bun-layout-pass/fixture.json similarity index 100% rename from registry/tests/projects/bun-layout-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/bun-layout-pass/fixture.json diff --git a/registry/tests/projects/bun-layout-pass/package.json b/packages/runtime-core/tests/integration/projects/bun-layout-pass/package.json similarity index 100% rename from registry/tests/projects/bun-layout-pass/package.json rename to packages/runtime-core/tests/integration/projects/bun-layout-pass/package.json diff --git a/registry/tests/projects/bun-layout-pass/src/index.js b/packages/runtime-core/tests/integration/projects/bun-layout-pass/src/index.js similarity index 100% rename from registry/tests/projects/bun-layout-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/bun-layout-pass/src/index.js diff --git a/registry/tests/projects/chalk-pass/fixture.json b/packages/runtime-core/tests/integration/projects/chalk-pass/fixture.json similarity index 100% rename from registry/tests/projects/chalk-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/chalk-pass/fixture.json diff --git a/registry/tests/projects/chalk-pass/package.json b/packages/runtime-core/tests/integration/projects/chalk-pass/package.json similarity index 100% rename from registry/tests/projects/chalk-pass/package.json rename to packages/runtime-core/tests/integration/projects/chalk-pass/package.json diff --git a/registry/tests/projects/chalk-pass/src/index.js b/packages/runtime-core/tests/integration/projects/chalk-pass/src/index.js similarity index 100% rename from registry/tests/projects/chalk-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/chalk-pass/src/index.js diff --git a/registry/tests/projects/conditional-exports-pass/fixture.json b/packages/runtime-core/tests/integration/projects/conditional-exports-pass/fixture.json similarity index 100% rename from registry/tests/projects/conditional-exports-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/conditional-exports-pass/fixture.json diff --git a/registry/tests/projects/conditional-exports-pass/package-lock.json b/packages/runtime-core/tests/integration/projects/conditional-exports-pass/package-lock.json similarity index 100% rename from registry/tests/projects/conditional-exports-pass/package-lock.json rename to packages/runtime-core/tests/integration/projects/conditional-exports-pass/package-lock.json diff --git a/registry/tests/projects/conditional-exports-pass/package.json b/packages/runtime-core/tests/integration/projects/conditional-exports-pass/package.json similarity index 100% rename from registry/tests/projects/conditional-exports-pass/package.json rename to packages/runtime-core/tests/integration/projects/conditional-exports-pass/package.json diff --git a/registry/tests/projects/conditional-exports-pass/packages/cond-exports-lib/lib/feature-cjs.js b/packages/runtime-core/tests/integration/projects/conditional-exports-pass/packages/cond-exports-lib/lib/feature-cjs.js similarity index 100% rename from registry/tests/projects/conditional-exports-pass/packages/cond-exports-lib/lib/feature-cjs.js rename to packages/runtime-core/tests/integration/projects/conditional-exports-pass/packages/cond-exports-lib/lib/feature-cjs.js diff --git a/registry/tests/projects/conditional-exports-pass/packages/cond-exports-lib/lib/feature-default.js b/packages/runtime-core/tests/integration/projects/conditional-exports-pass/packages/cond-exports-lib/lib/feature-default.js similarity index 100% rename from registry/tests/projects/conditional-exports-pass/packages/cond-exports-lib/lib/feature-default.js rename to packages/runtime-core/tests/integration/projects/conditional-exports-pass/packages/cond-exports-lib/lib/feature-default.js diff --git a/registry/tests/projects/conditional-exports-pass/packages/cond-exports-lib/lib/main-cjs.js b/packages/runtime-core/tests/integration/projects/conditional-exports-pass/packages/cond-exports-lib/lib/main-cjs.js similarity index 100% rename from registry/tests/projects/conditional-exports-pass/packages/cond-exports-lib/lib/main-cjs.js rename to packages/runtime-core/tests/integration/projects/conditional-exports-pass/packages/cond-exports-lib/lib/main-cjs.js diff --git a/registry/tests/projects/conditional-exports-pass/packages/cond-exports-lib/lib/main-default.js b/packages/runtime-core/tests/integration/projects/conditional-exports-pass/packages/cond-exports-lib/lib/main-default.js similarity index 100% rename from registry/tests/projects/conditional-exports-pass/packages/cond-exports-lib/lib/main-default.js rename to packages/runtime-core/tests/integration/projects/conditional-exports-pass/packages/cond-exports-lib/lib/main-default.js diff --git a/registry/tests/projects/conditional-exports-pass/packages/cond-exports-lib/package.json b/packages/runtime-core/tests/integration/projects/conditional-exports-pass/packages/cond-exports-lib/package.json similarity index 100% rename from registry/tests/projects/conditional-exports-pass/packages/cond-exports-lib/package.json rename to packages/runtime-core/tests/integration/projects/conditional-exports-pass/packages/cond-exports-lib/package.json diff --git a/registry/tests/projects/conditional-exports-pass/src/index.js b/packages/runtime-core/tests/integration/projects/conditional-exports-pass/src/index.js similarity index 100% rename from registry/tests/projects/conditional-exports-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/conditional-exports-pass/src/index.js diff --git a/registry/tests/projects/crypto-random-pass/fixture.json b/packages/runtime-core/tests/integration/projects/crypto-random-pass/fixture.json similarity index 100% rename from registry/tests/projects/crypto-random-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/crypto-random-pass/fixture.json diff --git a/registry/tests/projects/crypto-random-pass/package.json b/packages/runtime-core/tests/integration/projects/crypto-random-pass/package.json similarity index 100% rename from registry/tests/projects/crypto-random-pass/package.json rename to packages/runtime-core/tests/integration/projects/crypto-random-pass/package.json diff --git a/registry/tests/projects/crypto-random-pass/src/index.js b/packages/runtime-core/tests/integration/projects/crypto-random-pass/src/index.js similarity index 100% rename from registry/tests/projects/crypto-random-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/crypto-random-pass/src/index.js diff --git a/registry/tests/projects/dotenv-pass/.env b/packages/runtime-core/tests/integration/projects/dotenv-pass/.env similarity index 100% rename from registry/tests/projects/dotenv-pass/.env rename to packages/runtime-core/tests/integration/projects/dotenv-pass/.env diff --git a/registry/tests/projects/dotenv-pass/fixture.json b/packages/runtime-core/tests/integration/projects/dotenv-pass/fixture.json similarity index 100% rename from registry/tests/projects/dotenv-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/dotenv-pass/fixture.json diff --git a/registry/tests/projects/dotenv-pass/package.json b/packages/runtime-core/tests/integration/projects/dotenv-pass/package.json similarity index 100% rename from registry/tests/projects/dotenv-pass/package.json rename to packages/runtime-core/tests/integration/projects/dotenv-pass/package.json diff --git a/registry/tests/projects/dotenv-pass/src/index.js b/packages/runtime-core/tests/integration/projects/dotenv-pass/src/index.js similarity index 100% rename from registry/tests/projects/dotenv-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/dotenv-pass/src/index.js diff --git a/registry/tests/projects/drizzle-pass/fixture.json b/packages/runtime-core/tests/integration/projects/drizzle-pass/fixture.json similarity index 100% rename from registry/tests/projects/drizzle-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/drizzle-pass/fixture.json diff --git a/registry/tests/projects/drizzle-pass/package.json b/packages/runtime-core/tests/integration/projects/drizzle-pass/package.json similarity index 100% rename from registry/tests/projects/drizzle-pass/package.json rename to packages/runtime-core/tests/integration/projects/drizzle-pass/package.json diff --git a/registry/tests/projects/drizzle-pass/src/index.js b/packages/runtime-core/tests/integration/projects/drizzle-pass/src/index.js similarity index 100% rename from registry/tests/projects/drizzle-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/drizzle-pass/src/index.js diff --git a/registry/tests/projects/esm-import-pass/fixture.json b/packages/runtime-core/tests/integration/projects/esm-import-pass/fixture.json similarity index 100% rename from registry/tests/projects/esm-import-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/esm-import-pass/fixture.json diff --git a/registry/tests/projects/esm-import-pass/package.json b/packages/runtime-core/tests/integration/projects/esm-import-pass/package.json similarity index 100% rename from registry/tests/projects/esm-import-pass/package.json rename to packages/runtime-core/tests/integration/projects/esm-import-pass/package.json diff --git a/registry/tests/projects/esm-import-pass/src/index.js b/packages/runtime-core/tests/integration/projects/esm-import-pass/src/index.js similarity index 100% rename from registry/tests/projects/esm-import-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/esm-import-pass/src/index.js diff --git a/registry/tests/projects/express-pass/fixture.json b/packages/runtime-core/tests/integration/projects/express-pass/fixture.json similarity index 100% rename from registry/tests/projects/express-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/express-pass/fixture.json diff --git a/registry/tests/projects/express-pass/package.json b/packages/runtime-core/tests/integration/projects/express-pass/package.json similarity index 100% rename from registry/tests/projects/express-pass/package.json rename to packages/runtime-core/tests/integration/projects/express-pass/package.json diff --git a/registry/tests/projects/express-pass/pnpm-lock.yaml b/packages/runtime-core/tests/integration/projects/express-pass/pnpm-lock.yaml similarity index 100% rename from registry/tests/projects/express-pass/pnpm-lock.yaml rename to packages/runtime-core/tests/integration/projects/express-pass/pnpm-lock.yaml diff --git a/registry/tests/projects/express-pass/src/index.js b/packages/runtime-core/tests/integration/projects/express-pass/src/index.js similarity index 100% rename from registry/tests/projects/express-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/express-pass/src/index.js diff --git a/registry/tests/projects/fastify-pass/fixture.json b/packages/runtime-core/tests/integration/projects/fastify-pass/fixture.json similarity index 100% rename from registry/tests/projects/fastify-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/fastify-pass/fixture.json diff --git a/registry/tests/projects/fastify-pass/package.json b/packages/runtime-core/tests/integration/projects/fastify-pass/package.json similarity index 100% rename from registry/tests/projects/fastify-pass/package.json rename to packages/runtime-core/tests/integration/projects/fastify-pass/package.json diff --git a/registry/tests/projects/fastify-pass/pnpm-lock.yaml b/packages/runtime-core/tests/integration/projects/fastify-pass/pnpm-lock.yaml similarity index 100% rename from registry/tests/projects/fastify-pass/pnpm-lock.yaml rename to packages/runtime-core/tests/integration/projects/fastify-pass/pnpm-lock.yaml diff --git a/registry/tests/projects/fastify-pass/src/index.js b/packages/runtime-core/tests/integration/projects/fastify-pass/src/index.js similarity index 100% rename from registry/tests/projects/fastify-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/fastify-pass/src/index.js diff --git a/registry/tests/projects/fs-metadata-rename-pass/fixture.json b/packages/runtime-core/tests/integration/projects/fs-metadata-rename-pass/fixture.json similarity index 100% rename from registry/tests/projects/fs-metadata-rename-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/fs-metadata-rename-pass/fixture.json diff --git a/registry/tests/projects/fs-metadata-rename-pass/package.json b/packages/runtime-core/tests/integration/projects/fs-metadata-rename-pass/package.json similarity index 100% rename from registry/tests/projects/fs-metadata-rename-pass/package.json rename to packages/runtime-core/tests/integration/projects/fs-metadata-rename-pass/package.json diff --git a/registry/tests/projects/fs-metadata-rename-pass/src/index.js b/packages/runtime-core/tests/integration/projects/fs-metadata-rename-pass/src/index.js similarity index 100% rename from registry/tests/projects/fs-metadata-rename-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/fs-metadata-rename-pass/src/index.js diff --git a/registry/tests/projects/ioredis-pass/fixture.json b/packages/runtime-core/tests/integration/projects/ioredis-pass/fixture.json similarity index 100% rename from registry/tests/projects/ioredis-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/ioredis-pass/fixture.json diff --git a/registry/tests/projects/ioredis-pass/package.json b/packages/runtime-core/tests/integration/projects/ioredis-pass/package.json similarity index 100% rename from registry/tests/projects/ioredis-pass/package.json rename to packages/runtime-core/tests/integration/projects/ioredis-pass/package.json diff --git a/registry/tests/projects/ioredis-pass/pnpm-lock.yaml b/packages/runtime-core/tests/integration/projects/ioredis-pass/pnpm-lock.yaml similarity index 100% rename from registry/tests/projects/ioredis-pass/pnpm-lock.yaml rename to packages/runtime-core/tests/integration/projects/ioredis-pass/pnpm-lock.yaml diff --git a/registry/tests/projects/ioredis-pass/src/index.js b/packages/runtime-core/tests/integration/projects/ioredis-pass/src/index.js similarity index 100% rename from registry/tests/projects/ioredis-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/ioredis-pass/src/index.js diff --git a/registry/tests/projects/jsonwebtoken-pass/fixture.json b/packages/runtime-core/tests/integration/projects/jsonwebtoken-pass/fixture.json similarity index 100% rename from registry/tests/projects/jsonwebtoken-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/jsonwebtoken-pass/fixture.json diff --git a/registry/tests/projects/jsonwebtoken-pass/package.json b/packages/runtime-core/tests/integration/projects/jsonwebtoken-pass/package.json similarity index 100% rename from registry/tests/projects/jsonwebtoken-pass/package.json rename to packages/runtime-core/tests/integration/projects/jsonwebtoken-pass/package.json diff --git a/registry/tests/projects/jsonwebtoken-pass/src/index.js b/packages/runtime-core/tests/integration/projects/jsonwebtoken-pass/src/index.js similarity index 100% rename from registry/tests/projects/jsonwebtoken-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/jsonwebtoken-pass/src/index.js diff --git a/registry/tests/projects/lodash-es-pass/fixture.json b/packages/runtime-core/tests/integration/projects/lodash-es-pass/fixture.json similarity index 100% rename from registry/tests/projects/lodash-es-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/lodash-es-pass/fixture.json diff --git a/registry/tests/projects/lodash-es-pass/package.json b/packages/runtime-core/tests/integration/projects/lodash-es-pass/package.json similarity index 100% rename from registry/tests/projects/lodash-es-pass/package.json rename to packages/runtime-core/tests/integration/projects/lodash-es-pass/package.json diff --git a/registry/tests/projects/lodash-es-pass/pnpm-lock.yaml b/packages/runtime-core/tests/integration/projects/lodash-es-pass/pnpm-lock.yaml similarity index 100% rename from registry/tests/projects/lodash-es-pass/pnpm-lock.yaml rename to packages/runtime-core/tests/integration/projects/lodash-es-pass/pnpm-lock.yaml diff --git a/registry/tests/projects/lodash-es-pass/src/index.js b/packages/runtime-core/tests/integration/projects/lodash-es-pass/src/index.js similarity index 100% rename from registry/tests/projects/lodash-es-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/lodash-es-pass/src/index.js diff --git a/registry/tests/projects/module-access-pass/fixture.json b/packages/runtime-core/tests/integration/projects/module-access-pass/fixture.json similarity index 100% rename from registry/tests/projects/module-access-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/module-access-pass/fixture.json diff --git a/registry/tests/projects/module-access-pass/package.json b/packages/runtime-core/tests/integration/projects/module-access-pass/package.json similarity index 100% rename from registry/tests/projects/module-access-pass/package.json rename to packages/runtime-core/tests/integration/projects/module-access-pass/package.json diff --git a/registry/tests/projects/module-access-pass/src/index.js b/packages/runtime-core/tests/integration/projects/module-access-pass/src/index.js similarity index 100% rename from registry/tests/projects/module-access-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/module-access-pass/src/index.js diff --git a/registry/tests/projects/module-access-pass/vendor/entry-lib/index.js b/packages/runtime-core/tests/integration/projects/module-access-pass/vendor/entry-lib/index.js similarity index 100% rename from registry/tests/projects/module-access-pass/vendor/entry-lib/index.js rename to packages/runtime-core/tests/integration/projects/module-access-pass/vendor/entry-lib/index.js diff --git a/registry/tests/projects/module-access-pass/vendor/entry-lib/package.json b/packages/runtime-core/tests/integration/projects/module-access-pass/vendor/entry-lib/package.json similarity index 100% rename from registry/tests/projects/module-access-pass/vendor/entry-lib/package.json rename to packages/runtime-core/tests/integration/projects/module-access-pass/vendor/entry-lib/package.json diff --git a/registry/tests/projects/module-access-pass/vendor/transitive-lib/index.js b/packages/runtime-core/tests/integration/projects/module-access-pass/vendor/transitive-lib/index.js similarity index 100% rename from registry/tests/projects/module-access-pass/vendor/transitive-lib/index.js rename to packages/runtime-core/tests/integration/projects/module-access-pass/vendor/transitive-lib/index.js diff --git a/registry/tests/projects/module-access-pass/vendor/transitive-lib/package.json b/packages/runtime-core/tests/integration/projects/module-access-pass/vendor/transitive-lib/package.json similarity index 100% rename from registry/tests/projects/module-access-pass/vendor/transitive-lib/package.json rename to packages/runtime-core/tests/integration/projects/module-access-pass/vendor/transitive-lib/package.json diff --git a/registry/tests/projects/mysql2-pass/fixture.json b/packages/runtime-core/tests/integration/projects/mysql2-pass/fixture.json similarity index 100% rename from registry/tests/projects/mysql2-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/mysql2-pass/fixture.json diff --git a/registry/tests/projects/mysql2-pass/package.json b/packages/runtime-core/tests/integration/projects/mysql2-pass/package.json similarity index 100% rename from registry/tests/projects/mysql2-pass/package.json rename to packages/runtime-core/tests/integration/projects/mysql2-pass/package.json diff --git a/registry/tests/projects/mysql2-pass/src/index.js b/packages/runtime-core/tests/integration/projects/mysql2-pass/src/index.js similarity index 100% rename from registry/tests/projects/mysql2-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/mysql2-pass/src/index.js diff --git a/registry/tests/projects/net-create-server-pass/fixture.json b/packages/runtime-core/tests/integration/projects/net-create-server-pass/fixture.json similarity index 100% rename from registry/tests/projects/net-create-server-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/net-create-server-pass/fixture.json diff --git a/registry/tests/projects/net-create-server-pass/package.json b/packages/runtime-core/tests/integration/projects/net-create-server-pass/package.json similarity index 100% rename from registry/tests/projects/net-create-server-pass/package.json rename to packages/runtime-core/tests/integration/projects/net-create-server-pass/package.json diff --git a/registry/tests/projects/net-create-server-pass/src/index.js b/packages/runtime-core/tests/integration/projects/net-create-server-pass/src/index.js similarity index 100% rename from registry/tests/projects/net-create-server-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/net-create-server-pass/src/index.js diff --git a/registry/tests/projects/nextjs-pass/.babelrc b/packages/runtime-core/tests/integration/projects/nextjs-pass/.babelrc similarity index 100% rename from registry/tests/projects/nextjs-pass/.babelrc rename to packages/runtime-core/tests/integration/projects/nextjs-pass/.babelrc diff --git a/registry/tests/projects/nextjs-pass/fixture.json b/packages/runtime-core/tests/integration/projects/nextjs-pass/fixture.json similarity index 100% rename from registry/tests/projects/nextjs-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/nextjs-pass/fixture.json diff --git a/registry/tests/projects/nextjs-pass/next-wasm-shim.cjs b/packages/runtime-core/tests/integration/projects/nextjs-pass/next-wasm-shim.cjs similarity index 100% rename from registry/tests/projects/nextjs-pass/next-wasm-shim.cjs rename to packages/runtime-core/tests/integration/projects/nextjs-pass/next-wasm-shim.cjs diff --git a/registry/tests/projects/nextjs-pass/next.config.js b/packages/runtime-core/tests/integration/projects/nextjs-pass/next.config.js similarity index 100% rename from registry/tests/projects/nextjs-pass/next.config.js rename to packages/runtime-core/tests/integration/projects/nextjs-pass/next.config.js diff --git a/registry/tests/projects/nextjs-pass/package.json b/packages/runtime-core/tests/integration/projects/nextjs-pass/package.json similarity index 100% rename from registry/tests/projects/nextjs-pass/package.json rename to packages/runtime-core/tests/integration/projects/nextjs-pass/package.json diff --git a/registry/tests/projects/nextjs-pass/pages/api/hello.js b/packages/runtime-core/tests/integration/projects/nextjs-pass/pages/api/hello.js similarity index 100% rename from registry/tests/projects/nextjs-pass/pages/api/hello.js rename to packages/runtime-core/tests/integration/projects/nextjs-pass/pages/api/hello.js diff --git a/registry/tests/projects/nextjs-pass/pages/index.js b/packages/runtime-core/tests/integration/projects/nextjs-pass/pages/index.js similarity index 100% rename from registry/tests/projects/nextjs-pass/pages/index.js rename to packages/runtime-core/tests/integration/projects/nextjs-pass/pages/index.js diff --git a/registry/tests/projects/nextjs-pass/run-next-build.cjs b/packages/runtime-core/tests/integration/projects/nextjs-pass/run-next-build.cjs similarity index 100% rename from registry/tests/projects/nextjs-pass/run-next-build.cjs rename to packages/runtime-core/tests/integration/projects/nextjs-pass/run-next-build.cjs diff --git a/registry/tests/projects/nextjs-pass/src/index.js b/packages/runtime-core/tests/integration/projects/nextjs-pass/src/index.js similarity index 100% rename from registry/tests/projects/nextjs-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/nextjs-pass/src/index.js diff --git a/registry/tests/projects/node-fetch-pass/fixture.json b/packages/runtime-core/tests/integration/projects/node-fetch-pass/fixture.json similarity index 100% rename from registry/tests/projects/node-fetch-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/node-fetch-pass/fixture.json diff --git a/registry/tests/projects/node-fetch-pass/package.json b/packages/runtime-core/tests/integration/projects/node-fetch-pass/package.json similarity index 100% rename from registry/tests/projects/node-fetch-pass/package.json rename to packages/runtime-core/tests/integration/projects/node-fetch-pass/package.json diff --git a/registry/tests/projects/node-fetch-pass/src/index.js b/packages/runtime-core/tests/integration/projects/node-fetch-pass/src/index.js similarity index 100% rename from registry/tests/projects/node-fetch-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/node-fetch-pass/src/index.js diff --git a/registry/tests/projects/npm-layout-pass/fixture.json b/packages/runtime-core/tests/integration/projects/npm-layout-pass/fixture.json similarity index 100% rename from registry/tests/projects/npm-layout-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/npm-layout-pass/fixture.json diff --git a/registry/tests/projects/npm-layout-pass/package-lock.json b/packages/runtime-core/tests/integration/projects/npm-layout-pass/package-lock.json similarity index 100% rename from registry/tests/projects/npm-layout-pass/package-lock.json rename to packages/runtime-core/tests/integration/projects/npm-layout-pass/package-lock.json diff --git a/registry/tests/projects/npm-layout-pass/package.json b/packages/runtime-core/tests/integration/projects/npm-layout-pass/package.json similarity index 100% rename from registry/tests/projects/npm-layout-pass/package.json rename to packages/runtime-core/tests/integration/projects/npm-layout-pass/package.json diff --git a/registry/tests/projects/npm-layout-pass/src/index.js b/packages/runtime-core/tests/integration/projects/npm-layout-pass/src/index.js similarity index 100% rename from registry/tests/projects/npm-layout-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/npm-layout-pass/src/index.js diff --git a/registry/tests/projects/optional-deps-pass/fixture.json b/packages/runtime-core/tests/integration/projects/optional-deps-pass/fixture.json similarity index 100% rename from registry/tests/projects/optional-deps-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/optional-deps-pass/fixture.json diff --git a/registry/tests/projects/optional-deps-pass/package-lock.json b/packages/runtime-core/tests/integration/projects/optional-deps-pass/package-lock.json similarity index 100% rename from registry/tests/projects/optional-deps-pass/package-lock.json rename to packages/runtime-core/tests/integration/projects/optional-deps-pass/package-lock.json diff --git a/registry/tests/projects/optional-deps-pass/package.json b/packages/runtime-core/tests/integration/projects/optional-deps-pass/package.json similarity index 100% rename from registry/tests/projects/optional-deps-pass/package.json rename to packages/runtime-core/tests/integration/projects/optional-deps-pass/package.json diff --git a/registry/tests/projects/optional-deps-pass/src/index.js b/packages/runtime-core/tests/integration/projects/optional-deps-pass/src/index.js similarity index 100% rename from registry/tests/projects/optional-deps-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/optional-deps-pass/src/index.js diff --git a/registry/tests/projects/peer-deps-pass/fixture.json b/packages/runtime-core/tests/integration/projects/peer-deps-pass/fixture.json similarity index 100% rename from registry/tests/projects/peer-deps-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/peer-deps-pass/fixture.json diff --git a/registry/tests/projects/peer-deps-pass/package-lock.json b/packages/runtime-core/tests/integration/projects/peer-deps-pass/package-lock.json similarity index 100% rename from registry/tests/projects/peer-deps-pass/package-lock.json rename to packages/runtime-core/tests/integration/projects/peer-deps-pass/package-lock.json diff --git a/registry/tests/projects/peer-deps-pass/package.json b/packages/runtime-core/tests/integration/projects/peer-deps-pass/package.json similarity index 100% rename from registry/tests/projects/peer-deps-pass/package.json rename to packages/runtime-core/tests/integration/projects/peer-deps-pass/package.json diff --git a/registry/tests/projects/peer-deps-pass/packages/host/index.js b/packages/runtime-core/tests/integration/projects/peer-deps-pass/packages/host/index.js similarity index 100% rename from registry/tests/projects/peer-deps-pass/packages/host/index.js rename to packages/runtime-core/tests/integration/projects/peer-deps-pass/packages/host/index.js diff --git a/registry/tests/projects/peer-deps-pass/packages/host/package.json b/packages/runtime-core/tests/integration/projects/peer-deps-pass/packages/host/package.json similarity index 100% rename from registry/tests/projects/peer-deps-pass/packages/host/package.json rename to packages/runtime-core/tests/integration/projects/peer-deps-pass/packages/host/package.json diff --git a/registry/tests/projects/peer-deps-pass/packages/plugin/index.js b/packages/runtime-core/tests/integration/projects/peer-deps-pass/packages/plugin/index.js similarity index 100% rename from registry/tests/projects/peer-deps-pass/packages/plugin/index.js rename to packages/runtime-core/tests/integration/projects/peer-deps-pass/packages/plugin/index.js diff --git a/registry/tests/projects/peer-deps-pass/packages/plugin/package.json b/packages/runtime-core/tests/integration/projects/peer-deps-pass/packages/plugin/package.json similarity index 100% rename from registry/tests/projects/peer-deps-pass/packages/plugin/package.json rename to packages/runtime-core/tests/integration/projects/peer-deps-pass/packages/plugin/package.json diff --git a/registry/tests/projects/peer-deps-pass/src/index.js b/packages/runtime-core/tests/integration/projects/peer-deps-pass/src/index.js similarity index 100% rename from registry/tests/projects/peer-deps-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/peer-deps-pass/src/index.js diff --git a/registry/tests/projects/pg-pass/fixture.json b/packages/runtime-core/tests/integration/projects/pg-pass/fixture.json similarity index 100% rename from registry/tests/projects/pg-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/pg-pass/fixture.json diff --git a/registry/tests/projects/pg-pass/package.json b/packages/runtime-core/tests/integration/projects/pg-pass/package.json similarity index 100% rename from registry/tests/projects/pg-pass/package.json rename to packages/runtime-core/tests/integration/projects/pg-pass/package.json diff --git a/registry/tests/projects/pg-pass/pnpm-lock.yaml b/packages/runtime-core/tests/integration/projects/pg-pass/pnpm-lock.yaml similarity index 100% rename from registry/tests/projects/pg-pass/pnpm-lock.yaml rename to packages/runtime-core/tests/integration/projects/pg-pass/pnpm-lock.yaml diff --git a/registry/tests/projects/pg-pass/src/index.js b/packages/runtime-core/tests/integration/projects/pg-pass/src/index.js similarity index 100% rename from registry/tests/projects/pg-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/pg-pass/src/index.js diff --git a/registry/tests/projects/pino-pass/fixture.json b/packages/runtime-core/tests/integration/projects/pino-pass/fixture.json similarity index 100% rename from registry/tests/projects/pino-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/pino-pass/fixture.json diff --git a/registry/tests/projects/pino-pass/package.json b/packages/runtime-core/tests/integration/projects/pino-pass/package.json similarity index 100% rename from registry/tests/projects/pino-pass/package.json rename to packages/runtime-core/tests/integration/projects/pino-pass/package.json diff --git a/registry/tests/projects/pino-pass/pnpm-lock.yaml b/packages/runtime-core/tests/integration/projects/pino-pass/pnpm-lock.yaml similarity index 100% rename from registry/tests/projects/pino-pass/pnpm-lock.yaml rename to packages/runtime-core/tests/integration/projects/pino-pass/pnpm-lock.yaml diff --git a/registry/tests/projects/pino-pass/src/index.js b/packages/runtime-core/tests/integration/projects/pino-pass/src/index.js similarity index 100% rename from registry/tests/projects/pino-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/pino-pass/src/index.js diff --git a/registry/tests/projects/pnpm-layout-pass/fixture.json b/packages/runtime-core/tests/integration/projects/pnpm-layout-pass/fixture.json similarity index 100% rename from registry/tests/projects/pnpm-layout-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/pnpm-layout-pass/fixture.json diff --git a/registry/tests/projects/pnpm-layout-pass/package.json b/packages/runtime-core/tests/integration/projects/pnpm-layout-pass/package.json similarity index 100% rename from registry/tests/projects/pnpm-layout-pass/package.json rename to packages/runtime-core/tests/integration/projects/pnpm-layout-pass/package.json diff --git a/registry/tests/projects/pnpm-layout-pass/pnpm-lock.yaml b/packages/runtime-core/tests/integration/projects/pnpm-layout-pass/pnpm-lock.yaml similarity index 100% rename from registry/tests/projects/pnpm-layout-pass/pnpm-lock.yaml rename to packages/runtime-core/tests/integration/projects/pnpm-layout-pass/pnpm-lock.yaml diff --git a/registry/tests/projects/pnpm-layout-pass/src/index.js b/packages/runtime-core/tests/integration/projects/pnpm-layout-pass/src/index.js similarity index 100% rename from registry/tests/projects/pnpm-layout-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/pnpm-layout-pass/src/index.js diff --git a/registry/tests/projects/rivetkit/fixture.json b/packages/runtime-core/tests/integration/projects/rivetkit/fixture.json similarity index 100% rename from registry/tests/projects/rivetkit/fixture.json rename to packages/runtime-core/tests/integration/projects/rivetkit/fixture.json diff --git a/registry/tests/projects/rivetkit/package.json b/packages/runtime-core/tests/integration/projects/rivetkit/package.json similarity index 100% rename from registry/tests/projects/rivetkit/package.json rename to packages/runtime-core/tests/integration/projects/rivetkit/package.json diff --git a/registry/tests/projects/rivetkit/src/index.js b/packages/runtime-core/tests/integration/projects/rivetkit/src/index.js similarity index 100% rename from registry/tests/projects/rivetkit/src/index.js rename to packages/runtime-core/tests/integration/projects/rivetkit/src/index.js diff --git a/registry/tests/projects/rivetkit/vendor/rivetkit/package.json b/packages/runtime-core/tests/integration/projects/rivetkit/vendor/rivetkit/package.json similarity index 100% rename from registry/tests/projects/rivetkit/vendor/rivetkit/package.json rename to packages/runtime-core/tests/integration/projects/rivetkit/vendor/rivetkit/package.json diff --git a/registry/tests/projects/semver-pass/fixture.json b/packages/runtime-core/tests/integration/projects/semver-pass/fixture.json similarity index 100% rename from registry/tests/projects/semver-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/semver-pass/fixture.json diff --git a/registry/tests/projects/semver-pass/package.json b/packages/runtime-core/tests/integration/projects/semver-pass/package.json similarity index 100% rename from registry/tests/projects/semver-pass/package.json rename to packages/runtime-core/tests/integration/projects/semver-pass/package.json diff --git a/registry/tests/projects/semver-pass/src/index.js b/packages/runtime-core/tests/integration/projects/semver-pass/src/index.js similarity index 100% rename from registry/tests/projects/semver-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/semver-pass/src/index.js diff --git a/registry/tests/projects/sse-streaming-pass/fixture.json b/packages/runtime-core/tests/integration/projects/sse-streaming-pass/fixture.json similarity index 100% rename from registry/tests/projects/sse-streaming-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/sse-streaming-pass/fixture.json diff --git a/registry/tests/projects/sse-streaming-pass/package.json b/packages/runtime-core/tests/integration/projects/sse-streaming-pass/package.json similarity index 100% rename from registry/tests/projects/sse-streaming-pass/package.json rename to packages/runtime-core/tests/integration/projects/sse-streaming-pass/package.json diff --git a/registry/tests/projects/sse-streaming-pass/src/index.js b/packages/runtime-core/tests/integration/projects/sse-streaming-pass/src/index.js similarity index 100% rename from registry/tests/projects/sse-streaming-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/sse-streaming-pass/src/index.js diff --git a/registry/tests/projects/ssh2-pass/fixture.json b/packages/runtime-core/tests/integration/projects/ssh2-pass/fixture.json similarity index 100% rename from registry/tests/projects/ssh2-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/ssh2-pass/fixture.json diff --git a/registry/tests/projects/ssh2-pass/package.json b/packages/runtime-core/tests/integration/projects/ssh2-pass/package.json similarity index 100% rename from registry/tests/projects/ssh2-pass/package.json rename to packages/runtime-core/tests/integration/projects/ssh2-pass/package.json diff --git a/registry/tests/projects/ssh2-pass/src/index.js b/packages/runtime-core/tests/integration/projects/ssh2-pass/src/index.js similarity index 100% rename from registry/tests/projects/ssh2-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/ssh2-pass/src/index.js diff --git a/registry/tests/projects/ssh2-sftp-client-pass/fixture.json b/packages/runtime-core/tests/integration/projects/ssh2-sftp-client-pass/fixture.json similarity index 100% rename from registry/tests/projects/ssh2-sftp-client-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/ssh2-sftp-client-pass/fixture.json diff --git a/registry/tests/projects/ssh2-sftp-client-pass/package.json b/packages/runtime-core/tests/integration/projects/ssh2-sftp-client-pass/package.json similarity index 100% rename from registry/tests/projects/ssh2-sftp-client-pass/package.json rename to packages/runtime-core/tests/integration/projects/ssh2-sftp-client-pass/package.json diff --git a/registry/tests/projects/ssh2-sftp-client-pass/src/index.js b/packages/runtime-core/tests/integration/projects/ssh2-sftp-client-pass/src/index.js similarity index 100% rename from registry/tests/projects/ssh2-sftp-client-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/ssh2-sftp-client-pass/src/index.js diff --git a/registry/tests/projects/transitive-deps-pass/fixture.json b/packages/runtime-core/tests/integration/projects/transitive-deps-pass/fixture.json similarity index 100% rename from registry/tests/projects/transitive-deps-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/transitive-deps-pass/fixture.json diff --git a/registry/tests/projects/transitive-deps-pass/package-lock.json b/packages/runtime-core/tests/integration/projects/transitive-deps-pass/package-lock.json similarity index 100% rename from registry/tests/projects/transitive-deps-pass/package-lock.json rename to packages/runtime-core/tests/integration/projects/transitive-deps-pass/package-lock.json diff --git a/registry/tests/projects/transitive-deps-pass/package.json b/packages/runtime-core/tests/integration/projects/transitive-deps-pass/package.json similarity index 100% rename from registry/tests/projects/transitive-deps-pass/package.json rename to packages/runtime-core/tests/integration/projects/transitive-deps-pass/package.json diff --git a/registry/tests/projects/transitive-deps-pass/packages/level-a/index.js b/packages/runtime-core/tests/integration/projects/transitive-deps-pass/packages/level-a/index.js similarity index 100% rename from registry/tests/projects/transitive-deps-pass/packages/level-a/index.js rename to packages/runtime-core/tests/integration/projects/transitive-deps-pass/packages/level-a/index.js diff --git a/registry/tests/projects/transitive-deps-pass/packages/level-a/package.json b/packages/runtime-core/tests/integration/projects/transitive-deps-pass/packages/level-a/package.json similarity index 100% rename from registry/tests/projects/transitive-deps-pass/packages/level-a/package.json rename to packages/runtime-core/tests/integration/projects/transitive-deps-pass/packages/level-a/package.json diff --git a/registry/tests/projects/transitive-deps-pass/packages/level-b/index.js b/packages/runtime-core/tests/integration/projects/transitive-deps-pass/packages/level-b/index.js similarity index 100% rename from registry/tests/projects/transitive-deps-pass/packages/level-b/index.js rename to packages/runtime-core/tests/integration/projects/transitive-deps-pass/packages/level-b/index.js diff --git a/registry/tests/projects/transitive-deps-pass/packages/level-b/package.json b/packages/runtime-core/tests/integration/projects/transitive-deps-pass/packages/level-b/package.json similarity index 100% rename from registry/tests/projects/transitive-deps-pass/packages/level-b/package.json rename to packages/runtime-core/tests/integration/projects/transitive-deps-pass/packages/level-b/package.json diff --git a/registry/tests/projects/transitive-deps-pass/packages/level-c/index.js b/packages/runtime-core/tests/integration/projects/transitive-deps-pass/packages/level-c/index.js similarity index 100% rename from registry/tests/projects/transitive-deps-pass/packages/level-c/index.js rename to packages/runtime-core/tests/integration/projects/transitive-deps-pass/packages/level-c/index.js diff --git a/registry/tests/projects/transitive-deps-pass/packages/level-c/package.json b/packages/runtime-core/tests/integration/projects/transitive-deps-pass/packages/level-c/package.json similarity index 100% rename from registry/tests/projects/transitive-deps-pass/packages/level-c/package.json rename to packages/runtime-core/tests/integration/projects/transitive-deps-pass/packages/level-c/package.json diff --git a/registry/tests/projects/transitive-deps-pass/src/index.js b/packages/runtime-core/tests/integration/projects/transitive-deps-pass/src/index.js similarity index 100% rename from registry/tests/projects/transitive-deps-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/transitive-deps-pass/src/index.js diff --git a/registry/tests/projects/uuid-pass/fixture.json b/packages/runtime-core/tests/integration/projects/uuid-pass/fixture.json similarity index 100% rename from registry/tests/projects/uuid-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/uuid-pass/fixture.json diff --git a/registry/tests/projects/uuid-pass/package.json b/packages/runtime-core/tests/integration/projects/uuid-pass/package.json similarity index 100% rename from registry/tests/projects/uuid-pass/package.json rename to packages/runtime-core/tests/integration/projects/uuid-pass/package.json diff --git a/registry/tests/projects/uuid-pass/src/index.js b/packages/runtime-core/tests/integration/projects/uuid-pass/src/index.js similarity index 100% rename from registry/tests/projects/uuid-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/uuid-pass/src/index.js diff --git a/registry/tests/projects/vite-pass/app/main.jsx b/packages/runtime-core/tests/integration/projects/vite-pass/app/main.jsx similarity index 100% rename from registry/tests/projects/vite-pass/app/main.jsx rename to packages/runtime-core/tests/integration/projects/vite-pass/app/main.jsx diff --git a/registry/tests/projects/vite-pass/fixture.json b/packages/runtime-core/tests/integration/projects/vite-pass/fixture.json similarity index 100% rename from registry/tests/projects/vite-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/vite-pass/fixture.json diff --git a/registry/tests/projects/vite-pass/index.html b/packages/runtime-core/tests/integration/projects/vite-pass/index.html similarity index 100% rename from registry/tests/projects/vite-pass/index.html rename to packages/runtime-core/tests/integration/projects/vite-pass/index.html diff --git a/registry/tests/projects/vite-pass/package.json b/packages/runtime-core/tests/integration/projects/vite-pass/package.json similarity index 100% rename from registry/tests/projects/vite-pass/package.json rename to packages/runtime-core/tests/integration/projects/vite-pass/package.json diff --git a/registry/tests/projects/vite-pass/src/index.js b/packages/runtime-core/tests/integration/projects/vite-pass/src/index.js similarity index 100% rename from registry/tests/projects/vite-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/vite-pass/src/index.js diff --git a/registry/tests/projects/vite-pass/vite.config.mjs b/packages/runtime-core/tests/integration/projects/vite-pass/vite.config.mjs similarity index 100% rename from registry/tests/projects/vite-pass/vite.config.mjs rename to packages/runtime-core/tests/integration/projects/vite-pass/vite.config.mjs diff --git a/registry/tests/projects/workspace-layout-pass/fixture.json b/packages/runtime-core/tests/integration/projects/workspace-layout-pass/fixture.json similarity index 100% rename from registry/tests/projects/workspace-layout-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/workspace-layout-pass/fixture.json diff --git a/registry/tests/projects/workspace-layout-pass/package.json b/packages/runtime-core/tests/integration/projects/workspace-layout-pass/package.json similarity index 100% rename from registry/tests/projects/workspace-layout-pass/package.json rename to packages/runtime-core/tests/integration/projects/workspace-layout-pass/package.json diff --git a/registry/tests/projects/workspace-layout-pass/packages/app/package.json b/packages/runtime-core/tests/integration/projects/workspace-layout-pass/packages/app/package.json similarity index 100% rename from registry/tests/projects/workspace-layout-pass/packages/app/package.json rename to packages/runtime-core/tests/integration/projects/workspace-layout-pass/packages/app/package.json diff --git a/registry/tests/projects/workspace-layout-pass/packages/app/src/index.js b/packages/runtime-core/tests/integration/projects/workspace-layout-pass/packages/app/src/index.js similarity index 100% rename from registry/tests/projects/workspace-layout-pass/packages/app/src/index.js rename to packages/runtime-core/tests/integration/projects/workspace-layout-pass/packages/app/src/index.js diff --git a/registry/tests/projects/workspace-layout-pass/packages/lib/package.json b/packages/runtime-core/tests/integration/projects/workspace-layout-pass/packages/lib/package.json similarity index 100% rename from registry/tests/projects/workspace-layout-pass/packages/lib/package.json rename to packages/runtime-core/tests/integration/projects/workspace-layout-pass/packages/lib/package.json diff --git a/registry/tests/projects/workspace-layout-pass/packages/lib/src/index.js b/packages/runtime-core/tests/integration/projects/workspace-layout-pass/packages/lib/src/index.js similarity index 100% rename from registry/tests/projects/workspace-layout-pass/packages/lib/src/index.js rename to packages/runtime-core/tests/integration/projects/workspace-layout-pass/packages/lib/src/index.js diff --git a/registry/tests/projects/ws-pass/fixture.json b/packages/runtime-core/tests/integration/projects/ws-pass/fixture.json similarity index 100% rename from registry/tests/projects/ws-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/ws-pass/fixture.json diff --git a/registry/tests/projects/ws-pass/package-lock.json b/packages/runtime-core/tests/integration/projects/ws-pass/package-lock.json similarity index 100% rename from registry/tests/projects/ws-pass/package-lock.json rename to packages/runtime-core/tests/integration/projects/ws-pass/package-lock.json diff --git a/registry/tests/projects/ws-pass/package.json b/packages/runtime-core/tests/integration/projects/ws-pass/package.json similarity index 100% rename from registry/tests/projects/ws-pass/package.json rename to packages/runtime-core/tests/integration/projects/ws-pass/package.json diff --git a/registry/tests/projects/ws-pass/src/index.js b/packages/runtime-core/tests/integration/projects/ws-pass/src/index.js similarity index 100% rename from registry/tests/projects/ws-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/ws-pass/src/index.js diff --git a/registry/tests/projects/yaml-pass/fixture.json b/packages/runtime-core/tests/integration/projects/yaml-pass/fixture.json similarity index 100% rename from registry/tests/projects/yaml-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/yaml-pass/fixture.json diff --git a/registry/tests/projects/yaml-pass/package.json b/packages/runtime-core/tests/integration/projects/yaml-pass/package.json similarity index 100% rename from registry/tests/projects/yaml-pass/package.json rename to packages/runtime-core/tests/integration/projects/yaml-pass/package.json diff --git a/registry/tests/projects/yaml-pass/src/index.js b/packages/runtime-core/tests/integration/projects/yaml-pass/src/index.js similarity index 100% rename from registry/tests/projects/yaml-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/yaml-pass/src/index.js diff --git a/registry/tests/projects/yarn-berry-layout-pass/.yarnrc.yml b/packages/runtime-core/tests/integration/projects/yarn-berry-layout-pass/.yarnrc.yml similarity index 100% rename from registry/tests/projects/yarn-berry-layout-pass/.yarnrc.yml rename to packages/runtime-core/tests/integration/projects/yarn-berry-layout-pass/.yarnrc.yml diff --git a/registry/tests/projects/yarn-berry-layout-pass/fixture.json b/packages/runtime-core/tests/integration/projects/yarn-berry-layout-pass/fixture.json similarity index 100% rename from registry/tests/projects/yarn-berry-layout-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/yarn-berry-layout-pass/fixture.json diff --git a/registry/tests/projects/yarn-berry-layout-pass/package.json b/packages/runtime-core/tests/integration/projects/yarn-berry-layout-pass/package.json similarity index 100% rename from registry/tests/projects/yarn-berry-layout-pass/package.json rename to packages/runtime-core/tests/integration/projects/yarn-berry-layout-pass/package.json diff --git a/registry/tests/projects/yarn-berry-layout-pass/src/index.js b/packages/runtime-core/tests/integration/projects/yarn-berry-layout-pass/src/index.js similarity index 100% rename from registry/tests/projects/yarn-berry-layout-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/yarn-berry-layout-pass/src/index.js diff --git a/registry/tests/projects/yarn-berry-layout-pass/yarn.lock b/packages/runtime-core/tests/integration/projects/yarn-berry-layout-pass/yarn.lock similarity index 100% rename from registry/tests/projects/yarn-berry-layout-pass/yarn.lock rename to packages/runtime-core/tests/integration/projects/yarn-berry-layout-pass/yarn.lock diff --git a/registry/tests/projects/yarn-classic-layout-pass/fixture.json b/packages/runtime-core/tests/integration/projects/yarn-classic-layout-pass/fixture.json similarity index 100% rename from registry/tests/projects/yarn-classic-layout-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/yarn-classic-layout-pass/fixture.json diff --git a/registry/tests/projects/yarn-classic-layout-pass/package.json b/packages/runtime-core/tests/integration/projects/yarn-classic-layout-pass/package.json similarity index 100% rename from registry/tests/projects/yarn-classic-layout-pass/package.json rename to packages/runtime-core/tests/integration/projects/yarn-classic-layout-pass/package.json diff --git a/registry/tests/projects/yarn-classic-layout-pass/src/index.js b/packages/runtime-core/tests/integration/projects/yarn-classic-layout-pass/src/index.js similarity index 100% rename from registry/tests/projects/yarn-classic-layout-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/yarn-classic-layout-pass/src/index.js diff --git a/registry/tests/projects/yarn-classic-layout-pass/yarn.lock b/packages/runtime-core/tests/integration/projects/yarn-classic-layout-pass/yarn.lock similarity index 100% rename from registry/tests/projects/yarn-classic-layout-pass/yarn.lock rename to packages/runtime-core/tests/integration/projects/yarn-classic-layout-pass/yarn.lock diff --git a/registry/tests/projects/zod-pass/fixture.json b/packages/runtime-core/tests/integration/projects/zod-pass/fixture.json similarity index 100% rename from registry/tests/projects/zod-pass/fixture.json rename to packages/runtime-core/tests/integration/projects/zod-pass/fixture.json diff --git a/registry/tests/projects/zod-pass/package.json b/packages/runtime-core/tests/integration/projects/zod-pass/package.json similarity index 100% rename from registry/tests/projects/zod-pass/package.json rename to packages/runtime-core/tests/integration/projects/zod-pass/package.json diff --git a/registry/tests/projects/zod-pass/pnpm-lock.yaml b/packages/runtime-core/tests/integration/projects/zod-pass/pnpm-lock.yaml similarity index 100% rename from registry/tests/projects/zod-pass/pnpm-lock.yaml rename to packages/runtime-core/tests/integration/projects/zod-pass/pnpm-lock.yaml diff --git a/registry/tests/projects/zod-pass/src/index.js b/packages/runtime-core/tests/integration/projects/zod-pass/src/index.js similarity index 100% rename from registry/tests/projects/zod-pass/src/index.js rename to packages/runtime-core/tests/integration/projects/zod-pass/src/index.js diff --git a/registry/tests/kernel/repro-npm-install.ts b/packages/runtime-core/tests/integration/repro-npm-install.ts similarity index 93% rename from registry/tests/kernel/repro-npm-install.ts rename to packages/runtime-core/tests/integration/repro-npm-install.ts index 03358f9204..7e24de262e 100644 --- a/registry/tests/kernel/repro-npm-install.ts +++ b/packages/runtime-core/tests/integration/repro-npm-install.ts @@ -1,7 +1,7 @@ import { mkdtemp, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import path from 'node:path'; -import { COMMANDS_DIR, createKernel, NodeFileSystem, createWasmVmRuntime, createNodeRuntime } from '../helpers.ts'; +import { COMMANDS_DIR, createKernel, NodeFileSystem, createWasmVmRuntime, createNodeRuntime } from '@rivet-dev/agentos-vm-test-harness'; const tempDir = await mkdtemp(path.join(tmpdir(), 'kernel-npm-install-repro-')); console.log('tempDir', tempDir); diff --git a/registry/tests/kernel/shim-streaming.test.ts b/packages/runtime-core/tests/integration/shim-streaming.test.ts similarity index 86% rename from registry/tests/kernel/shim-streaming.test.ts rename to packages/runtime-core/tests/integration/shim-streaming.test.ts index aad4e4de59..634f9729c7 100644 --- a/registry/tests/kernel/shim-streaming.test.ts +++ b/packages/runtime-core/tests/integration/shim-streaming.test.ts @@ -3,8 +3,8 @@ import { createIntegrationKernel, describeIf, skipUnlessWasmBuilt, -} from './helpers.ts'; -import type { IntegrationKernelResult } from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; +import type { IntegrationKernelResult } from '@rivet-dev/agentos-vm-test-harness'; const skipReason = skipUnlessWasmBuilt(); diff --git a/registry/tests/kernel/signal-forwarding.test.ts b/packages/runtime-core/tests/integration/signal-forwarding.test.ts similarity index 96% rename from registry/tests/kernel/signal-forwarding.test.ts rename to packages/runtime-core/tests/integration/signal-forwarding.test.ts index 117b079dab..ac085e0818 100644 --- a/registry/tests/kernel/signal-forwarding.test.ts +++ b/packages/runtime-core/tests/integration/signal-forwarding.test.ts @@ -11,8 +11,8 @@ import { describeIf, createIntegrationKernel, skipUnlessWasmBuilt, -} from './helpers.ts'; -import type { IntegrationKernelResult } from './helpers.ts'; +} from '@rivet-dev/agentos-vm-test-harness'; +import type { IntegrationKernelResult } from '@rivet-dev/agentos-vm-test-harness'; const skipReason = skipUnlessWasmBuilt(); diff --git a/packages/runtime-core/tests/integration/signal-handler.test.ts b/packages/runtime-core/tests/integration/signal-handler.test.ts new file mode 100644 index 0000000000..7a7a8f8d70 --- /dev/null +++ b/packages/runtime-core/tests/integration/signal-handler.test.ts @@ -0,0 +1,215 @@ +/** + * Integration test for WasmVM cooperative signal handling. + * + * Spawns the signal_handler C program as WASM (sigaction(SIGINT, ...) → + * busy-loop with sleep → verify handler called), delivers SIGINT via + * kernel.kill(), and verifies the handler fires at a syscall boundary. + */ + +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { createWasmVmRuntime } from '@rivet-dev/agentos-vm-test-harness'; +import { + COMMANDS_DIR, + C_BUILD_DIR, + createKernel, + describeIf, + hasWasmBinaries, + SIGTERM, +} from '@rivet-dev/agentos-vm-test-harness'; +import type { Kernel } from '@rivet-dev/agentos-vm-test-harness'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; + +const hasCWasmBinaries = existsSync(join(C_BUILD_DIR, 'signal_handler')); +const SIGINT = 2; + +function skipReason(): string | false { + if (!hasWasmBinaries) return 'WASM binaries not built (run make wasm in native/wasmvm/)'; + if (!hasCWasmBinaries) return 'signal_handler WASM binary not built (run make -C native/wasmvm/c sysroot && make -C native/wasmvm/c programs)'; + return false; +} + +// Minimal in-memory VFS +class SimpleVFS { + private files = new Map(); + private dirs = new Set(['/']); + private symlinks = new Map(); + + async readFile(path: string): Promise { + const data = this.files.get(path); + if (!data) throw new Error(`ENOENT: ${path}`); + return data; + } + async readTextFile(path: string): Promise { + return new TextDecoder().decode(await this.readFile(path)); + } + async pread(path: string, offset: number, length: number): Promise { + const data = await this.readFile(path); + return data.slice(offset, offset + length); + } + async readDir(path: string): Promise { + const prefix = path === '/' ? '/' : path + '/'; + const entries: string[] = []; + for (const p of [...this.files.keys(), ...this.dirs]) { + if (p !== path && p.startsWith(prefix)) { + const rest = p.slice(prefix.length); + if (!rest.includes('/')) entries.push(rest); + } + } + return entries; + } + async readDirWithTypes(path: string) { + return (await this.readDir(path)).map((name) => ({ + name, + isDirectory: this.dirs.has(path === '/' ? `/${name}` : `${path}/${name}`), + })); + } + async writeFile(path: string, content: string | Uint8Array): Promise { + const data = typeof content === 'string' ? new TextEncoder().encode(content) : content; + this.files.set(path, new Uint8Array(data)); + const parts = path.split('/').filter(Boolean); + for (let i = 1; i < parts.length; i++) { + this.dirs.add('/' + parts.slice(0, i).join('/')); + } + } + async createDir(path: string) { this.dirs.add(path); } + async mkdir(path: string, _options?: { recursive?: boolean }) { this.dirs.add(path); } + async exists(path: string): Promise { + return this.files.has(path) || this.dirs.has(path) || this.symlinks.has(path); + } + async stat(path: string) { + const isDir = this.dirs.has(path); + const isSymlink = this.symlinks.has(path); + const data = this.files.get(path); + if (!isDir && !isSymlink && !data) throw new Error(`ENOENT: ${path}`); + return { + mode: isSymlink ? 0o120777 : (isDir ? 0o40755 : 0o100644), + size: data?.length ?? 0, + isDirectory: isDir, + isSymbolicLink: isSymlink, + atimeMs: Date.now(), + mtimeMs: Date.now(), + ctimeMs: Date.now(), + birthtimeMs: Date.now(), + ino: 0, + nlink: 1, + uid: 1000, + gid: 1000, + }; + } + lstat(path: string) { return this.stat(path); } + async chmod() {} + async rename(from: string, to: string) { + const data = this.files.get(from); + if (data) { this.files.set(to, data); this.files.delete(from); } + } + async unlink(path: string) { this.files.delete(path); this.symlinks.delete(path); } + async rmdir(path: string) { this.dirs.delete(path); } + async symlink(target: string, linkPath: string) { + this.symlinks.set(linkPath, target); + const parts = linkPath.split('/').filter(Boolean); + for (let i = 1; i < parts.length; i++) { + this.dirs.add('/' + parts.slice(0, i).join('/')); + } + } + async readlink(path: string): Promise { + const target = this.symlinks.get(path); + if (!target) throw new Error(`EINVAL: ${path}`); + return target; + } +} + +async function waitForSignalRegistration( + kernel: Kernel, + pid: number, + signal: number, +): Promise<{ mask: Set; flags: number }> { + const proxy = (kernel as any).proxy; + const entry = proxy?.trackedProcesses?.get(pid); + let lastDirectKeys: number[] = []; + const deadline = Date.now() + 10_000; + while (Date.now() < deadline) { + const cached = kernel.processTable.getSignalState(pid).handlers.get(signal); + if (cached) { + return cached as { mask: Set; flags: number }; + } + + if (proxy && entry) { + const snapshot = await proxy.client.getSignalState(proxy.session, proxy.vm, entry.processId); + lastDirectKeys = [...snapshot.handlers.keys()]; + const direct = snapshot?.handlers?.get(signal); + if (direct) { + return { + mask: new Set(direct.mask), + flags: direct.flags, + }; + } + } + + await new Promise((r) => setTimeout(r, 20)); + } + throw new Error( + `timed out waiting for signal ${signal} registration for pid ${pid}; processId=${entry?.processId ?? 'unknown'}; sidecarSignals=${JSON.stringify(lastDirectKeys)}`, + ); +} + +async function waitForOutput( + getOutput: () => string, + needle: string, + timeoutMs = 2_000, +): Promise { + const deadline = Date.now() + timeoutMs; + while (Date.now() < deadline && !getOutput().includes(needle)) { + await new Promise((r) => setTimeout(r, 20)); + } +} + +describeIf(!skipReason(), 'WasmVM signal handler integration', { timeout: 30_000 }, () => { + let kernel: Kernel; + let vfs: SimpleVFS; + + beforeEach(async () => { + vfs = new SimpleVFS(); + kernel = createKernel({ filesystem: vfs as any }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); + }); + + afterEach(async () => { + await kernel?.dispose(); + }); + + it('signal_handler: sigaction registration preserves mask and fires at syscall boundary', async () => { + // Spawn the WASM signal_handler program (registers SIGINT handler, then loops) + let stdout = ''; + let stderr = ''; + const proc = kernel.spawn('signal_handler', [], { + onStdout: (data) => { stdout += new TextDecoder().decode(data); }, + onStderr: (data) => { stderr += new TextDecoder().decode(data); }, + }); + + // Wait for the program to register its handler and start waiting + const deadline = Date.now() + 10_000; + while (Date.now() < deadline && !stdout.includes('waiting')) { + await new Promise((r) => setTimeout(r, 20)); + } + expect(stdout).toContain('handler_registered'); + expect(stdout).toContain('waiting'); + + const registration = await waitForSignalRegistration(kernel, proc.pid, SIGINT).catch((error) => { + throw new Error(`${error instanceof Error ? error.message : String(error)}; stderr=${JSON.stringify(stderr)}`); + }); + expect(registration?.mask).toEqual(new Set([SIGTERM])); + + // Deliver SIGINT via ManagedProcess.kill() — routes through kernel process table + proc.kill(SIGINT); + + // Wait for the program to handle the signal and exit + const exitCode = await proc.wait(); + await waitForOutput(() => stdout, 'caught_signal=2'); + + if (!stdout.includes('caught_signal=2')) { + throw new Error(`missing caught signal output; exitCode=${exitCode}; stdout=${JSON.stringify(stdout)}; stderr=${JSON.stringify(stderr)}`); + } + expect(exitCode).toBe(0); + }); +}); diff --git a/packages/runtime-core/tests/integration/vfs-consistency.test.ts b/packages/runtime-core/tests/integration/vfs-consistency.test.ts new file mode 100644 index 0000000000..128ee05196 --- /dev/null +++ b/packages/runtime-core/tests/integration/vfs-consistency.test.ts @@ -0,0 +1,175 @@ +/** + * Cross-runtime VFS consistency tests. + * + * Verifies that file writes in one runtime are immediately visible to + * reads in another runtime, since all runtimes share the kernel VFS. + * + * Gracefully skipped when the WASM binary is not built. + */ + +import { describe, it, expect, afterEach } from 'vitest'; +import { + describeIf, + createIntegrationKernel, + skipUnlessWasmBuilt, +} from '@rivet-dev/agentos-vm-test-harness'; +import type { IntegrationKernelResult } from '@rivet-dev/agentos-vm-test-harness'; + +const skipReason = skipUnlessWasmBuilt(); + +describeIf(!skipReason, 'cross-runtime VFS consistency', () => { + let ctx: IntegrationKernelResult; + + afterEach(async () => { + if (ctx) await ctx.dispose(); + }); + + it('kernel write visible to Node', async () => { + ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); + await ctx.kernel.writeFile('/tmp/test.txt', 'hello'); + + const result = await ctx.kernel.exec( + `node -e "process.stdout.write(require('fs').readFileSync('/tmp/test.txt','utf8'))"`, + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('hello'); + }); + + it('Node write visible to WasmVM', async () => { + ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); + + // Node writes a file + const writeResult = await ctx.kernel.exec( + `node -e "require('fs').writeFileSync('/tmp/node-wrote.txt','from-node')"`, + ); + expect(writeResult.exitCode).toBe(0); + + // WasmVM reads it via cat + const readResult = await ctx.kernel.exec('cat /tmp/node-wrote.txt'); + expect(readResult.exitCode).toBe(0); + expect(readResult.stdout).toContain('from-node'); + }); + + it('Node write visible to kernel API', async () => { + ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); + + const writeResult = await ctx.kernel.exec( + `node -e "require('fs').writeFileSync('/tmp/k.txt','data')"`, + ); + expect(writeResult.exitCode).toBe(0); + + const content = await ctx.vfs.readTextFile('/tmp/k.txt'); + expect(content).toBe('data'); + }); + + it('directory listing consistent across runtimes', async () => { + ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); + + // Create 3 files via kernel API + await ctx.kernel.writeFile('/tmp/a.txt', 'a'); + await ctx.kernel.writeFile('/tmp/b.txt', 'b'); + await ctx.kernel.writeFile('/tmp/c.txt', 'c'); + + // WasmVM ls + const lsResult = await ctx.kernel.exec('ls /tmp'); + expect(lsResult.exitCode).toBe(0); + + // Node readdirSync + const nodeResult = await ctx.kernel.exec( + `node -e "console.log(require('fs').readdirSync('/tmp').sort().join(','))"`, + ); + expect(nodeResult.exitCode).toBe(0); + + // Both should list the same files + const lsFiles = lsResult.stdout + .trim() + .split(/\s+/) + .filter(Boolean) + .sort(); + const nodeFiles = nodeResult.stdout.trim().split(',').filter(Boolean).sort(); + + expect(lsFiles).toContain('a.txt'); + expect(lsFiles).toContain('b.txt'); + expect(lsFiles).toContain('c.txt'); + expect(nodeFiles).toContain('a.txt'); + expect(nodeFiles).toContain('b.txt'); + expect(nodeFiles).toContain('c.txt'); + }); + + it('ENOENT consistent across runtimes', async () => { + ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); + + // WasmVM cat nonexistent file + const catResult = await ctx.kernel.exec('cat /nonexistent'); + expect(catResult.exitCode).not.toBe(0); + + // Node readFileSync nonexistent file + const nodeResult = await ctx.kernel.exec( + `node -e "require('fs').readFileSync('/nonexistent')"`, + ); + expect(nodeResult.exitCode).not.toBe(0); + }); + + // Guest deletions must propagate into the kernel VFS instead of being + // resurrected by the (otherwise additive) shadow->kernel sync. This is the + // failure mode behind git's failed-clone junk surviving `remove_junk`. + it('guest rm -r removes the tree from the kernel VFS', async () => { + ctx = await createIntegrationKernel(); + + const create = await ctx.kernel.exec( + 'sh -c "mkdir -p /tmp/doomed/nested && echo payload > /tmp/doomed/nested/file.txt"', + ); + expect(create.exitCode).toBe(0); + expect(await ctx.vfs.exists('/tmp/doomed/nested/file.txt')).toBe(true); + + const remove = await ctx.kernel.exec('rm -r /tmp/doomed'); + expect(remove.exitCode).toBe(0); + + expect(await ctx.vfs.exists('/tmp/doomed/nested/file.txt')).toBe(false); + expect(await ctx.vfs.exists('/tmp/doomed/nested')).toBe(false); + expect(await ctx.vfs.exists('/tmp/doomed')).toBe(false); + + const ls = await ctx.kernel.exec('ls /tmp/doomed'); + expect(ls.exitCode).not.toBe(0); + }); + + it('guest rmdir on an empty directory succeeds and propagates', async () => { + ctx = await createIntegrationKernel(); + + const mkdir = await ctx.kernel.exec('mkdir /tmp/empty-dir'); + expect(mkdir.exitCode).toBe(0); + expect(await ctx.vfs.exists('/tmp/empty-dir')).toBe(true); + + const rmdir = await ctx.kernel.exec('rmdir /tmp/empty-dir'); + expect(rmdir.exitCode, rmdir.stderr).toBe(0); + expect(await ctx.vfs.exists('/tmp/empty-dir')).toBe(false); + }); + + it('guest unlink removes a dangling symlink so its directory can be emptied', async () => { + // git's symlink-support probe: create `tXXXXXX -> testing` (dangling), + // lstat it, unlink it, then remove the directory. unlink(2) must not + // resolve the symlink leaf, and rmdir must not report EIO afterwards. + ctx = await createIntegrationKernel(); + + const probe = await ctx.kernel.exec( + 'sh -c "mkdir /tmp/probe-dir && ln -s missing-target /tmp/probe-dir/probe && rm /tmp/probe-dir/probe && rmdir /tmp/probe-dir"', + ); + expect(probe.exitCode, probe.stderr).toBe(0); + expect(await ctx.vfs.exists('/tmp/probe-dir')).toBe(false); + }); + + it('guest rmdir on a non-empty directory reports ENOTEMPTY, not EIO', async () => { + ctx = await createIntegrationKernel(); + + const setup = await ctx.kernel.exec( + 'sh -c "mkdir /tmp/full-dir && echo keep > /tmp/full-dir/keep.txt"', + ); + expect(setup.exitCode).toBe(0); + + const rmdir = await ctx.kernel.exec('rmdir /tmp/full-dir'); + expect(rmdir.exitCode).not.toBe(0); + expect(rmdir.stderr.toLowerCase()).toContain('not empty'); + expect(rmdir.stderr.toLowerCase()).not.toContain('i/o error'); + expect(await ctx.vfs.exists('/tmp/full-dir/keep.txt')).toBe(true); + }); +}); diff --git a/registry/tests/wasmvm/wasi-http.test.ts b/packages/runtime-core/tests/integration/wasi-http.test.ts similarity index 98% rename from registry/tests/wasmvm/wasi-http.test.ts rename to packages/runtime-core/tests/integration/wasi-http.test.ts index 06f6cec3cf..1ae71fc27b 100644 --- a/registry/tests/wasmvm/wasi-http.test.ts +++ b/packages/runtime-core/tests/integration/wasi-http.test.ts @@ -12,9 +12,9 @@ */ import { describe, it, expect, afterEach, beforeAll, afterAll } from 'vitest'; -import { createWasmVmRuntime } from '../helpers.js'; -import { COMMANDS_DIR, createKernel, describeIf, hasWasmBinaries } from '../helpers.js'; -import type { Kernel } from '../helpers.js'; +import { createWasmVmRuntime } from '@rivet-dev/agentos-vm-test-harness'; +import { COMMANDS_DIR, createKernel, describeIf, hasWasmBinaries } from '@rivet-dev/agentos-vm-test-harness'; +import type { Kernel } from '@rivet-dev/agentos-vm-test-harness'; import { createServer as createHttpServer, type Server, type IncomingMessage, type ServerResponse } from 'node:http'; import { createServer as createHttpsServer, type Server as HttpsServer } from 'node:https'; import { execSync } from 'node:child_process'; diff --git a/registry/tests/wasmvm/wasi-spawn.test.ts b/packages/runtime-core/tests/integration/wasi-spawn.test.ts similarity index 96% rename from registry/tests/wasmvm/wasi-spawn.test.ts rename to packages/runtime-core/tests/integration/wasi-spawn.test.ts index 5f74ef1eae..910364314b 100644 --- a/registry/tests/wasmvm/wasi-spawn.test.ts +++ b/packages/runtime-core/tests/integration/wasi-spawn.test.ts @@ -9,9 +9,9 @@ */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { createWasmVmRuntime } from '../helpers.js'; -import { COMMANDS_DIR, createKernel, describeIf, hasWasmBinaries } from '../helpers.js'; -import type { Kernel } from '../helpers.js'; +import { createWasmVmRuntime } from '@rivet-dev/agentos-vm-test-harness'; +import { COMMANDS_DIR, createKernel, describeIf, hasWasmBinaries } from '@rivet-dev/agentos-vm-test-harness'; +import type { Kernel } from '@rivet-dev/agentos-vm-test-harness'; import { existsSync } from 'node:fs'; import { resolve } from 'node:path'; diff --git a/packages/runtime-core/tests/mount-fs-custom-vfs.test.ts b/packages/runtime-core/tests/mount-fs-custom-vfs.test.ts index e81efc2567..a2a8ed8d9e 100644 --- a/packages/runtime-core/tests/mount-fs-custom-vfs.test.ts +++ b/packages/runtime-core/tests/mount-fs-custom-vfs.test.ts @@ -83,4 +83,22 @@ describe("Kernel.mountFs custom JS VFS", () => { }, 120_000, ); + + test("routes positioned writes through a mounted JS VFS", async () => { + const mounted = createRecordingFilesystem(); + kernel = createKernel({ filesystem: createInMemoryFileSystem() }); + + kernel.mountFs("/mnt/custom", mounted.fs); + await kernel.writeFile("/mnt/custom/db.bin", "abcde"); + await kernel.pwrite( + "/mnt/custom/db.bin", + 2, + new TextEncoder().encode("XYZ"), + ); + + expect( + new TextDecoder().decode(await kernel.readFile("/mnt/custom/db.bin")), + ).toBe("abXYZ"); + expect(mounted.calls).toContain("pwrite:/db.bin"); + }); }); diff --git a/packages/runtime-core/tests/sidecar-build-inputs.test.ts b/packages/runtime-core/tests/sidecar-build-inputs.test.ts new file mode 100644 index 0000000000..c1943714ac --- /dev/null +++ b/packages/runtime-core/tests/sidecar-build-inputs.test.ts @@ -0,0 +1,38 @@ +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const sourcePath = fileURLToPath( + new URL("../src/test-runtime.ts", import.meta.url), +); + +describe("native sidecar build invalidation", () => { + it("tracks every local crate that can change the native sidecar binary", () => { + const source = readFileSync(sourcePath, "utf8"); + for (const crate of [ + "bridge", + "build-support", + "execution", + "kernel", + "native-sidecar", + "native-sidecar-core", + "sidecar-protocol", + "v8-runtime", + "vfs", + "vm-config", + ]) { + expect(source).toContain(`path.join(REPO_ROOT, "crates/${crate}")`); + } + for (const input of [ + "packages/build-tools/bridge-src", + "packages/build-tools/package.json", + "packages/build-tools/scripts/build-v8-bridge.mjs", + "packages/core/fixtures/base-filesystem.json", + "packages/runtime-core/fixtures/base-filesystem.json", + "pnpm-lock.yaml", + ]) { + expect(source).toContain(`path.join(REPO_ROOT, "${input}")`); + } + expect(source).not.toContain('path.join(REPO_ROOT, "crates/sidecar")'); + }); +}); diff --git a/packages/shell/CLAUDE.md b/packages/shell/CLAUDE.md index 21e058d520..aba1777d57 100644 --- a/packages/shell/CLAUDE.md +++ b/packages/shell/CLAUDE.md @@ -5,4 +5,4 @@ - `just shell` always uses the in-repo AgentOS sidecar it just built, even if the host environment exports `AGENTOS_SIDECAR_BIN`. To deliberately test another binary, invoke the shell CLI directly with that override. - `just shell --actor` additionally builds and pins the in-repo actor plugin. Actor development requires the Rivet repo at `../r6` (override with `AGENTOS_R6_ROOT`); the recipe installs the filtered `rivetkit` dependency graph there when its `tsx` loader is missing. - Do not implement or route through a custom/synthetic shell, prompt, line editor, or command parser; interactive shell mode must launch native Bash through the terminal/PTY path so behavior matches `docker run -it bash`. -- Keep `agentos-shell` loading every command-providing package from the in-repo `registry/software/`; when that registry changes, update the imports, package dependencies, and smoke coverage here. +- Keep `agentos-shell` loading every command-providing package from the in-repo `software/`; when that registry changes, update the imports, package dependencies, and smoke coverage here. diff --git a/packages/shell/package.json b/packages/shell/package.json index f76cda5d3c..38631b38c9 100644 --- a/packages/shell/package.json +++ b/packages/shell/package.json @@ -26,7 +26,6 @@ "@agentos-software/git": "workspace:*", "@agentos-software/grep": "workspace:*", "@agentos-software/gzip": "workspace:*", - "@agentos-software/http-get": "workspace:*", "@agentos-software/jq": "workspace:*", "@agentos-software/ripgrep": "workspace:*", "@agentos-software/sed": "workspace:*", @@ -35,7 +34,6 @@ "@agentos-software/tree": "workspace:*", "@agentos-software/unzip": "workspace:*", "@agentos-software/vim": "workspace:*", - "@agentos-software/vix": "workspace:*", "@agentos-software/wget": "workspace:*", "@agentos-software/yq": "workspace:*", "@agentos-software/zip": "workspace:*", diff --git a/packages/shell/src/main.ts b/packages/shell/src/main.ts index 1fb50aea0c..a4f9e17c9a 100644 --- a/packages/shell/src/main.ts +++ b/packages/shell/src/main.ts @@ -36,16 +36,15 @@ import gawk from "@agentos-software/gawk"; import git from "@agentos-software/git"; import grep from "@agentos-software/grep"; import gzip from "@agentos-software/gzip"; -import httpGet from "@agentos-software/http-get"; import jq from "@agentos-software/jq"; import ripgrep from "@agentos-software/ripgrep"; import sed from "@agentos-software/sed"; import sqlite3 from "@agentos-software/sqlite3"; import vim from "@agentos-software/vim"; -import vix from "@agentos-software/vix"; import tar from "@agentos-software/tar"; import tree from "@agentos-software/tree"; import unzip from "@agentos-software/unzip"; +import wget from "@agentos-software/wget"; import yq from "@agentos-software/yq"; import zip from "@agentos-software/zip"; import type { MountConfig, SoftwareInput } from "@rivet-dev/agentos-core"; @@ -59,7 +58,7 @@ const workspaceRoot = resolve(__dirname, "../../.."); const fallbackCommandDirs = [ resolve( workspaceRoot, - "registry/native/target/wasm32-wasip1/release/commands", + "toolchain/target/wasm32-wasip1/release/commands", ), ]; const BRUSH_SHELL_COMMANDS = new Set(["bash", "sh"]); @@ -154,18 +153,18 @@ const software: SoftwareInput[] = [ yq, codex, git, - httpGet, sqlite3, + wget, ] .map(withLocalCommandFallback) .filter((input): input is SoftwareInput => input !== null); -// The vi-like editors ship as registry packages: vix, and vim (which carries -// its runtime tree + VIMRUNTIME via the manifest `provides`). An unbuilt -// package is a valid empty placeholder — skip it rather than projecting a -// command-less package (build them locally: drop the wasm binaries into -// registry/native/target/.../commands and run `just registry-build`). -for (const editor of [vix, vim] as RegistryPackage[]) { +// vim ships as a software package (which carries its runtime tree + VIMRUNTIME +// via the manifest `provides`). An unbuilt package is a valid empty placeholder +// — skip it rather than projecting a command-less package (build it locally: +// drop the wasm binary into toolchain/target/.../commands and run +// `just software-build`). +for (const editor of [vim] as RegistryPackage[]) { if (isUsablePackageFile(editor.packagePath)) { software.push({ packagePath: editor.packagePath }); continue; diff --git a/packages/vm-test-harness/package.json b/packages/vm-test-harness/package.json new file mode 100644 index 0000000000..ae0cd7b34a --- /dev/null +++ b/packages/vm-test-harness/package.json @@ -0,0 +1,27 @@ +{ + "name": "@rivet-dev/agentos-vm-test-harness", + "version": "0.0.1", + "private": true, + "type": "module", + "license": "Apache-2.0", + "exports": { + ".": { + "types": "./src/index.ts", + "import": "./src/index.ts" + } + }, + "scripts": { + "check-types": "tsc --noEmit", + "build": "tsc --noEmit", + "test": "node -e \"process.exit(0)\"" + }, + "dependencies": { + "@rivet-dev/agentos-runtime-core": "workspace:*", + "@xterm/headless": "^6.0.0" + }, + "devDependencies": { + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "vitest": "^2.1.9" + } +} diff --git a/packages/vm-test-harness/src/index.ts b/packages/vm-test-harness/src/index.ts new file mode 100644 index 0000000000..4e6335b9c5 --- /dev/null +++ b/packages/vm-test-harness/src/index.ts @@ -0,0 +1,168 @@ +import { existsSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, it } from "vitest"; + +/** Directory containing WASM command binaries built from Rust. */ +export const COMMANDS_DIR = resolve( + process.env.AGENTOS_WASM_COMMANDS_DIR ?? + resolve(import.meta.dirname, "../../runtime-core/commands"), +); + +/** Directory containing C-compiled WASM binaries. */ +export const C_BUILD_DIR = resolve( + process.env.AGENTOS_C_WASM_COMMANDS_DIR ?? + resolve(import.meta.dirname, "../../../toolchain/c/build"), +); + +/** Whether the main WASM command binaries are available (includes 'sh'). */ +export const hasWasmBinaries = + existsSync(COMMANDS_DIR) && existsSync(resolve(COMMANDS_DIR, "sh")); + +/** + * Check whether specific C WASM binaries are present. + * @param names - Binary names to check for inside C_BUILD_DIR. + * @returns true if all requested binaries exist. + */ +export function hasCWasmBinaries(...names: string[]): boolean { + if (!existsSync(C_BUILD_DIR)) return false; + return names.every((name) => existsSync(resolve(C_BUILD_DIR, name))); +} + +/** + * Returns a skip-reason string if WASM binaries are missing, or false if + * they are available and tests should run. + */ +export function skipReason(): string | false { + if (!hasWasmBinaries) { + return `WASM binaries not found at ${COMMANDS_DIR} - build toolchain first`; + } + return false; +} + +export function describeIf( + condition: unknown, + ...args: Parameters +): void { + if (condition) { + describe(...args); + return; + } + const [name] = args; + describe.skip(`${String(name)} [environment prerequisites not met]`, () => {}); +} + +export function itIf(condition: unknown, ...args: Parameters): void { + if (condition) { + it(...args); + return; + } + const [name] = args; + it.skip(`${String(name)} [environment prerequisites not met]`, () => {}); +} + +export { + AF_INET, + AF_UNIX, + allowAll, + createInMemoryFileSystem, + SIGTERM, + SOCK_DGRAM, + SOCK_STREAM, +} from "../../runtime-core/src/test-runtime.js"; +import { + allowAll, + createInMemoryFileSystem, + createKernel as createKernelBase, + createNodeRuntime, + createWasmVmRuntime, +} from "../../runtime-core/src/test-runtime.js"; +export type { + DriverProcess, + Kernel, + KernelInterface, + KernelRuntimeDriver, + Permissions, + ProcessContext, + VirtualFileSystem, +} from "../../runtime-core/src/test-runtime.js"; +export { + createWasmVmRuntime, + DEFAULT_FIRST_PARTY_TIERS, + WASMVM_COMMANDS, + type PermissionTier, + type WasmVmRuntimeOptions, +} from "../../runtime-core/src/test-runtime.js"; +export { + createNodeHostNetworkAdapter, + createNodeRuntime, + NodeFileSystem, +} from "../../runtime-core/src/test-runtime.js"; +export { TerminalHarness } from "./terminal-harness.js"; + +/** + * Registry integration tests assume they can bootstrap runtimes and /bin stubs + * unless they explicitly opt into a stricter permission policy. + */ +export function createKernel( + options: Parameters[0], +): ReturnType { + return createKernelBase({ + ...options, + permissions: options.permissions ?? allowAll, + }); +} + +export interface IntegrationKernelResult { + kernel: ReturnType; + vfs: ReturnType; + dispose: () => Promise; +} + +export interface IntegrationKernelOptions { + runtimes?: ("wasmvm" | "node")[]; + loopbackExemptPorts?: number[]; + commandDirs?: string[]; + permissions?: Parameters[0]["permissions"]; +} + +/** + * Create a kernel with the in-scope runtime drivers for integration testing. + * + * Mount order matters. Last-mounted driver wins for overlapping commands: + * 1. WasmVM first: provides sh/bash/coreutils (90+ commands) + * 2. Node second: overrides WasmVM's 'node' stub with real V8 + */ +export async function createIntegrationKernel( + options?: IntegrationKernelOptions, +): Promise { + const runtimes = options?.runtimes ?? ["wasmvm"]; + const vfs = createInMemoryFileSystem(); + const kernel = createKernel({ + filesystem: vfs, + loopbackExemptPorts: options?.loopbackExemptPorts, + permissions: options?.permissions, + }); + + if (runtimes.includes("wasmvm")) { + await kernel.mount( + createWasmVmRuntime({ commandDirs: options?.commandDirs ?? [COMMANDS_DIR] }), + ); + } + if (runtimes.includes("node")) { + await kernel.mount(createNodeRuntime()); + } + + return { + kernel, + vfs, + dispose: () => kernel.dispose(), + }; +} + +/** + * Skip helper: returns a reason string if the WASM binaries are not built, + * or false if the commands directory exists and tests can run. + */ +export function skipUnlessWasmBuilt(): string | false { + return skipReason(); +} diff --git a/packages/vm-test-harness/src/terminal-harness.ts b/packages/vm-test-harness/src/terminal-harness.ts new file mode 100644 index 0000000000..3ec00c1caa --- /dev/null +++ b/packages/vm-test-harness/src/terminal-harness.ts @@ -0,0 +1,159 @@ +/** + * TerminalHarness wires openShell() to a headless xterm Terminal so tests can + * assert against deterministic terminal screen state. + */ + +import { Terminal } from "@xterm/headless"; +import type { Kernel } from "./index.js"; + +type ShellHandle = ReturnType; + +const SETTLE_MS = 50; +const POLL_MS = 20; +const DEFAULT_WAIT_TIMEOUT_MS = 5_000; + +export class TerminalHarness { + readonly term: Terminal; + readonly shell: ShellHandle; + private typing = false; + private disposed = false; + + constructor( + kernel: Kernel, + options?: { + cols?: number; + rows?: number; + env?: Record; + cwd?: string; + }, + ) { + const cols = options?.cols ?? 80; + const rows = options?.rows ?? 24; + + this.term = new Terminal({ cols, rows, allowProposedApi: true }); + this.shell = kernel.openShell({ + cols, + rows, + env: options?.env, + cwd: options?.cwd, + onStderr: (data: Uint8Array) => { + this.term.write(data); + }, + }); + this.shell.onData = (data: Uint8Array) => { + this.term.write(data); + }; + } + + async type(input: string): Promise { + if (this.typing) { + throw new Error( + "TerminalHarness.type() called while previous type() is still in-flight", + ); + } + this.typing = true; + try { + await this.typeInternal(input); + } finally { + this.typing = false; + } + } + + private typeInternal(input: string): Promise { + return new Promise((resolve) => { + let timer: ReturnType | null = null; + const originalOnData = this.shell.onData; + + const resetTimer = () => { + if (timer !== null) clearTimeout(timer); + timer = setTimeout(() => { + this.shell.onData = originalOnData; + resolve(); + }, SETTLE_MS); + }; + + this.shell.onData = (data: Uint8Array) => { + this.term.write(data); + resetTimer(); + }; + + resetTimer(); + this.shell.write(input); + }); + } + + screenshotTrimmed(): string { + const buf = this.term.buffer.active; + const lines: string[] = []; + + for (let row = 0; row < this.term.rows; row++) { + const line = buf.getLine(buf.viewportY + row); + lines.push(line ? line.translateToString(true) : ""); + } + + while (lines.length > 0 && lines[lines.length - 1] === "") { + lines.pop(); + } + + return lines.join("\n"); + } + + line(row: number): string { + const buf = this.term.buffer.active; + const line = buf.getLine(buf.viewportY + row); + return line ? line.translateToString(true) : ""; + } + + async waitFor( + text: string, + occurrence: number = 1, + timeoutMs: number = DEFAULT_WAIT_TIMEOUT_MS, + ): Promise { + const deadline = Date.now() + timeoutMs; + + while (true) { + const screen = this.screenshotTrimmed(); + + let count = 0; + let idx = -1; + while (true) { + idx = screen.indexOf(text, idx + 1); + if (idx === -1) break; + count++; + if (count >= occurrence) return; + } + + if (Date.now() >= deadline) { + throw new Error( + `waitFor("${text}", ${occurrence}) timed out after ${timeoutMs}ms.\n` + + `Expected: "${text}" (occurrence ${occurrence})\n` + + `Screen:\n${screen}`, + ); + } + + await new Promise((resolve) => setTimeout(resolve, POLL_MS)); + } + } + + async exit(): Promise { + this.shell.write("\x04"); + return this.shell.wait(); + } + + async dispose(): Promise { + if (this.disposed) return; + this.disposed = true; + + try { + this.shell.kill(); + await Promise.race([ + this.shell.wait(), + new Promise((resolve) => setTimeout(resolve, 500)), + ]); + } catch { + // Shell may already be gone. + } + + this.term.dispose(); + } +} diff --git a/packages/vm-test-harness/tsconfig.json b/packages/vm-test-harness/tsconfig.json new file mode 100644 index 0000000000..639c948caf --- /dev/null +++ b/packages/vm-test-harness/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "noEmit": true + }, + "include": ["src/**/*"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab1a636334..e30f294ad6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,16 +25,19 @@ importers: devDependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:registry/agent/claude + version: link:software/claude '@agentos-software/codex': specifier: workspace:* - version: link:registry/agent/codex + version: link:software/codex '@agentos-software/common': specifier: workspace:* - version: link:registry/software/common + version: link:software/common '@agentos-software/pi': specifier: workspace:* - version: link:registry/agent/pi + version: link:software/pi + '@agentos/test-harness': + specifier: workspace:* + version: link:test-harness '@biomejs/biome': specifier: ^2.3 version: 2.4.10 @@ -50,12 +53,18 @@ importers: '@rivet-dev/agentos-runtime-core': specifier: workspace:* version: link:packages/runtime-core + '@rivet-dev/agentos-vm-test-harness': + specifier: workspace:* + version: link:packages/vm-test-harness '@types/node': specifier: ^22.19.15 version: 22.19.15 jszip: specifier: ^3.10.1 version: 3.10.1 + npm: + specifier: ^11.18.0 + version: 11.18.0 pdf-lib: specifier: ^1.17.1 version: 1.17.1 @@ -70,16 +79,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -135,16 +144,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -184,16 +193,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -233,16 +242,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -282,22 +291,22 @@ importers: dependencies: '@agentos-software/common': specifier: workspace:* - version: link:../../registry/software/common + version: link:../../software/common '@agentos-software/curl': specifier: workspace:* - version: link:../../registry/software/curl + version: link:../../software/curl '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/jq': specifier: workspace:* - version: link:../../registry/software/jq + version: link:../../software/jq '@agentos-software/ripgrep': specifier: workspace:* - version: link:../../registry/software/ripgrep + version: link:../../software/ripgrep '@agentos-software/sqlite3': specifier: workspace:* - version: link:../../registry/software/sqlite3 + version: link:../../software/sqlite3 '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -346,13 +355,13 @@ importers: dependencies: '@agentos-software/browserbase': specifier: workspace:* - version: link:../../registry/software/browserbase + version: link:../../software/browserbase '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -374,16 +383,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -423,16 +432,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -472,16 +481,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -521,16 +530,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -570,16 +579,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -619,16 +628,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -668,16 +677,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -717,16 +726,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -766,16 +775,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -815,16 +824,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -864,16 +873,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -913,16 +922,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -962,16 +971,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -1011,16 +1020,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -1060,16 +1069,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -1109,16 +1118,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -1158,16 +1167,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -1207,16 +1216,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -1256,16 +1265,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -1314,16 +1323,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../../registry/agent/claude + version: link:../../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../../registry/software/git + version: link:../../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../../registry/agent/opencode + version: link:../../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../../registry/agent/pi + version: link:../../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../../packages/agentos @@ -1363,16 +1372,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../../registry/agent/claude + version: link:../../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../../registry/software/git + version: link:../../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../../registry/agent/opencode + version: link:../../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../../registry/agent/pi + version: link:../../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../../packages/agentos @@ -1412,16 +1421,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../../registry/agent/claude + version: link:../../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../../registry/software/git + version: link:../../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../../registry/agent/opencode + version: link:../../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../../registry/agent/pi + version: link:../../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../../packages/agentos @@ -1461,16 +1470,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../../registry/agent/claude + version: link:../../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../../registry/software/git + version: link:../../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../../registry/agent/opencode + version: link:../../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../../registry/agent/pi + version: link:../../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../../packages/agentos @@ -1510,16 +1519,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../../registry/agent/claude + version: link:../../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../../registry/software/git + version: link:../../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../../registry/agent/opencode + version: link:../../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../../registry/agent/pi + version: link:../../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../../packages/agentos @@ -1559,16 +1568,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../../registry/agent/claude + version: link:../../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../../registry/software/git + version: link:../../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../../registry/agent/opencode + version: link:../../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../../registry/agent/pi + version: link:../../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../../packages/agentos @@ -1608,16 +1617,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../../registry/agent/claude + version: link:../../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../../registry/software/git + version: link:../../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../../registry/agent/opencode + version: link:../../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../../registry/agent/pi + version: link:../../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../../packages/agentos @@ -1657,16 +1666,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../../registry/agent/claude + version: link:../../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../../registry/software/git + version: link:../../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../../registry/agent/opencode + version: link:../../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../../registry/agent/pi + version: link:../../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../../packages/agentos @@ -1706,16 +1715,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../../registry/agent/claude + version: link:../../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../../registry/software/git + version: link:../../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../../registry/agent/opencode + version: link:../../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../../registry/agent/pi + version: link:../../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../../packages/agentos @@ -1755,16 +1764,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../../registry/agent/claude + version: link:../../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../../registry/software/git + version: link:../../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../../registry/agent/opencode + version: link:../../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../../registry/agent/pi + version: link:../../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../../packages/agentos @@ -1804,16 +1813,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../../registry/agent/claude + version: link:../../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../../registry/software/git + version: link:../../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../../registry/agent/opencode + version: link:../../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../../registry/agent/pi + version: link:../../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../../packages/agentos @@ -1853,16 +1862,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../../registry/agent/claude + version: link:../../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../../registry/software/git + version: link:../../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../../registry/agent/opencode + version: link:../../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../../registry/agent/pi + version: link:../../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../../packages/agentos @@ -1902,16 +1911,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../../registry/agent/claude + version: link:../../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../../registry/software/git + version: link:../../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../../registry/agent/opencode + version: link:../../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../../registry/agent/pi + version: link:../../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../../packages/agentos @@ -1951,16 +1960,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -2000,16 +2009,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -2049,16 +2058,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -2098,25 +2107,25 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/coreutils': specifier: workspace:* - version: link:../../registry/software/coreutils + version: link:../../software/coreutils '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/jq': specifier: workspace:* - version: link:../../registry/software/jq + version: link:../../software/jq '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@agentos-software/ripgrep': specifier: workspace:* - version: link:../../registry/software/ripgrep + version: link:../../software/ripgrep '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -2156,16 +2165,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -2208,16 +2217,16 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@rivet-dev/agentos': specifier: workspace:* version: link:../../packages/agentos @@ -2257,7 +2266,7 @@ importers: dependencies: '@agentos-software/common': specifier: workspace:* - version: link:../../registry/software/common + version: link:../../software/common '@rivet-dev/agentos-core': specifier: workspace:* version: link:../core @@ -2354,7 +2363,7 @@ importers: devDependencies: '@agentos-software/common': specifier: workspace:* - version: link:../../registry/software/common + version: link:../../software/common '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -2440,25 +2449,25 @@ importers: dependencies: '@agentos-software/claude-code': specifier: workspace:* - version: link:../../registry/agent/claude + version: link:../../software/claude '@agentos-software/codex-cli': specifier: workspace:* - version: link:../../registry/software/codex-cli + version: link:../../software/codex-cli '@agentos-software/common': specifier: workspace:* - version: link:../../registry/software/common + version: link:../../software/common '@agentos-software/manifest': specifier: workspace:* version: link:../manifest '@agentos-software/opencode': specifier: workspace:* - version: link:../../registry/agent/opencode + version: link:../../software/opencode '@agentos-software/pi': specifier: workspace:* - version: link:../../registry/agent/pi + version: link:../../software/pi '@agentos-software/pi-cli': specifier: workspace:* - version: link:../../registry/agent/pi-cli + version: link:../../software/pi-cli '@aws-sdk/client-s3': specifier: ^3.1019.0 version: 3.1020.0 @@ -2501,64 +2510,64 @@ importers: devDependencies: '@agentos-software/codex': specifier: workspace:* - version: link:../../registry/agent/codex + version: link:../../software/codex '@agentos-software/coreutils': specifier: workspace:* - version: link:../../registry/software/coreutils + version: link:../../software/coreutils '@agentos-software/curl': specifier: workspace:* - version: link:../../registry/software/curl + version: link:../../software/curl '@agentos-software/diffutils': specifier: workspace:* - version: link:../../registry/software/diffutils + version: link:../../software/diffutils '@agentos-software/duckdb': specifier: workspace:* - version: link:../../registry/software/duckdb + version: link:../../software/duckdb '@agentos-software/fd': specifier: workspace:* - version: link:../../registry/software/fd + version: link:../../software/fd '@agentos-software/file': specifier: workspace:* - version: link:../../registry/software/file + version: link:../../software/file '@agentos-software/findutils': specifier: workspace:* - version: link:../../registry/software/findutils + version: link:../../software/findutils '@agentos-software/gawk': specifier: workspace:* - version: link:../../registry/software/gawk + version: link:../../software/gawk '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/grep': specifier: workspace:* - version: link:../../registry/software/grep + version: link:../../software/grep '@agentos-software/gzip': specifier: workspace:* - version: link:../../registry/software/gzip - '@agentos-software/http-get': - specifier: workspace:* - version: link:../../registry/software/http-get + version: link:../../software/gzip '@agentos-software/jq': specifier: workspace:* - version: link:../../registry/software/jq + version: link:../../software/jq '@agentos-software/ripgrep': specifier: workspace:* - version: link:../../registry/software/ripgrep + version: link:../../software/ripgrep '@agentos-software/sed': specifier: workspace:* - version: link:../../registry/software/sed + version: link:../../software/sed '@agentos-software/tar': specifier: workspace:* - version: link:../../registry/software/tar + version: link:../../software/tar '@agentos-software/tree': specifier: workspace:* - version: link:../../registry/software/tree + version: link:../../software/tree '@agentos-software/vim': specifier: workspace:* - version: link:../../registry/software/vim + version: link:../../software/vim + '@agentos-software/wget': + specifier: workspace:* + version: link:../../software/wget '@agentos-software/yq': specifier: workspace:* - version: link:../../registry/software/yq + version: link:../../software/yq '@anthropic-ai/claude-agent-sdk': specifier: ^0.2.87 version: 0.2.87(@cfworker/json-schema@4.1.1)(zod@4.3.6) @@ -2807,79 +2816,73 @@ importers: dependencies: '@agentos-software/codex-cli': specifier: workspace:* - version: link:../../registry/software/codex-cli + version: link:../../software/codex-cli '@agentos-software/coreutils': specifier: workspace:* - version: link:../../registry/software/coreutils + version: link:../../software/coreutils '@agentos-software/curl': specifier: workspace:* - version: link:../../registry/software/curl + version: link:../../software/curl '@agentos-software/diffutils': specifier: workspace:* - version: link:../../registry/software/diffutils + version: link:../../software/diffutils '@agentos-software/duckdb': specifier: workspace:* - version: link:../../registry/software/duckdb + version: link:../../software/duckdb '@agentos-software/fd': specifier: workspace:* - version: link:../../registry/software/fd + version: link:../../software/fd '@agentos-software/file': specifier: workspace:* - version: link:../../registry/software/file + version: link:../../software/file '@agentos-software/findutils': specifier: workspace:* - version: link:../../registry/software/findutils + version: link:../../software/findutils '@agentos-software/gawk': specifier: workspace:* - version: link:../../registry/software/gawk + version: link:../../software/gawk '@agentos-software/git': specifier: workspace:* - version: link:../../registry/software/git + version: link:../../software/git '@agentos-software/grep': specifier: workspace:* - version: link:../../registry/software/grep + version: link:../../software/grep '@agentos-software/gzip': specifier: workspace:* - version: link:../../registry/software/gzip - '@agentos-software/http-get': - specifier: workspace:* - version: link:../../registry/software/http-get + version: link:../../software/gzip '@agentos-software/jq': specifier: workspace:* - version: link:../../registry/software/jq + version: link:../../software/jq '@agentos-software/ripgrep': specifier: workspace:* - version: link:../../registry/software/ripgrep + version: link:../../software/ripgrep '@agentos-software/sed': specifier: workspace:* - version: link:../../registry/software/sed + version: link:../../software/sed '@agentos-software/sqlite3': specifier: workspace:* - version: link:../../registry/software/sqlite3 + version: link:../../software/sqlite3 '@agentos-software/tar': specifier: workspace:* - version: link:../../registry/software/tar + version: link:../../software/tar '@agentos-software/tree': specifier: workspace:* - version: link:../../registry/software/tree + version: link:../../software/tree '@agentos-software/unzip': specifier: workspace:* - version: link:../../registry/software/unzip + version: link:../../software/unzip '@agentos-software/vim': specifier: workspace:* - version: link:../../registry/software/vim - '@agentos-software/vix': - specifier: workspace:* - version: link:../../registry/software/vix + version: link:../../software/vim '@agentos-software/wget': specifier: workspace:* - version: link:../../registry/software/wget + version: link:../../software/wget '@agentos-software/yq': specifier: workspace:* - version: link:../../registry/software/yq + version: link:../../software/yq '@agentos-software/zip': specifier: workspace:* - version: link:../../registry/software/zip + version: link:../../software/zip '@rivet-dev/agentos': specifier: workspace:* version: link:../agentos @@ -2924,127 +2927,127 @@ importers: specifier: ^2.1.8 version: 2.1.9(@types/node@22.19.15) - registry/agent/claude: + packages/vm-test-harness: dependencies: - '@agentclientprotocol/sdk': - specifier: ^0.16.1 - version: 0.16.1(zod@4.3.6) - '@anthropic-ai/claude-agent-sdk': - specifier: 0.2.87 - version: 0.2.87(@cfworker/json-schema@4.1.1)(zod@4.3.6) - zod: - specifier: ^4.1.11 - version: 4.3.6 - devDependencies: - '@agentos-software/manifest': - specifier: workspace:* - version: link:../../../packages/manifest - '@rivet-dev/agentos-toolchain': + '@rivet-dev/agentos-runtime-core': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../runtime-core + '@xterm/headless': + specifier: ^6.0.0 + version: 6.0.0 + devDependencies: '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: - specifier: ^5.7.2 + specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/agent/codex: + scripts/publish: dependencies: - '@agentos-software/codex-cli': - specifier: workspace:* - version: link:../../software/codex-cli + commander: + specifier: ^12.1.0 + version: 12.1.0 + execa: + specifier: ^8.0.1 + version: 8.0.1 + glob: + specifier: ^10.3.10 + version: 10.5.0 + semver: + specifier: ^7.6.0 + version: 7.7.4 devDependencies: - '@agentos-software/manifest': - specifier: workspace:* - version: link:../../../packages/manifest '@types/node': - specifier: ^22.10.2 - version: 22.19.15 + specifier: ^24.3.0 + version: 24.13.2 + '@types/semver': + specifier: ^7.5.8 + version: 7.7.1 + tsx: + specifier: ^4.0.0 + version: 4.21.0 typescript: - specifier: ^5.7.2 + specifier: ^5.5.2 version: 5.9.3 - registry/agent/opencode: + software/browserbase: + dependencies: + browse: + specifier: ^0.9.3 + version: 0.9.3(@cfworker/json-schema@4.1.1)(bufferutil@4.1.0)(patchright-core@1.59.4)(playwright-core@1.59.1) devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 - bun: - specifier: 1.3.11 - version: 1.3.11 typescript: specifier: ^5.7.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/agent/pi: + software/build-essential: dependencies: - '@agentclientprotocol/sdk': - specifier: ^0.16.1 - version: 0.16.1(zod@4.3.6) - '@mariozechner/pi-agent-core': - specifier: 0.60.0 - version: 0.60.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6) - '@mariozechner/pi-ai': - specifier: 0.60.0 - version: 0.60.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6) - '@mariozechner/pi-coding-agent': - specifier: 0.60.0 - version: 0.60.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6) - devDependencies: - '@agentos-software/manifest': + '@agentos-software/common': specifier: workspace:* - version: link:../../../packages/manifest - '@rivet-dev/agentos-toolchain': + version: link:../common + '@agentos-software/curl': specifier: workspace:* - version: link:../../../packages/agentos-toolchain - '@types/node': - specifier: ^22.10.2 - version: 22.19.15 - typescript: - specifier: ^5.7.2 - version: 5.9.3 - - registry/agent/pi-cli: - dependencies: - '@mariozechner/pi-coding-agent': - specifier: ^0.60.0 - version: 0.60.0 - pi-acp: - specifier: ^0.0.23 - version: 0.0.23 + version: link:../curl + '@agentos-software/git': + specifier: workspace:* + version: link:../git devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest - '@rivet-dev/agentos-toolchain': + version: link:../../packages/manifest + '@agentos/test-harness': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../test-harness '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: - specifier: ^5.7.2 + specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/browserbase: + software/claude: dependencies: - browse: - specifier: ^0.9.3 - version: 0.9.3(@cfworker/json-schema@4.1.1)(bufferutil@4.1.0)(patchright-core@1.59.4)(playwright-core@1.59.1) + '@agentclientprotocol/sdk': + specifier: ^0.16.1 + version: 0.16.1(zod@4.3.6) + '@anthropic-ai/claude-agent-sdk': + specifier: 0.2.87 + version: 0.2.87(@cfworker/json-schema@4.1.1)(zod@4.3.6) + zod: + specifier: ^4.1.11 + version: 4.3.6 devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 @@ -3052,44 +3055,47 @@ importers: specifier: ^5.7.2 version: 5.9.3 - registry/software/build-essential: + software/codex: dependencies: - '@agentos-software/common': - specifier: workspace:* - version: link:../common - '@agentos-software/curl': - specifier: workspace:* - version: link:../curl - '@agentos-software/git': + '@agentos-software/codex-cli': specifier: workspace:* - version: link:../git + version: link:../codex-cli devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: - specifier: ^5.9.2 + specifier: ^5.7.2 version: 5.9.3 - registry/software/codex-cli: + software/codex-cli: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/common: + software/common: dependencies: '@agentos-software/coreutils': specifier: workspace:* @@ -3118,75 +3124,126 @@ importers: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/coreutils: + software/coreutils: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/curl: + software/curl: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/diffutils: + software/diffutils: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/duckdb: + software/duckdb: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness + '@rivet-dev/agentos-toolchain': + specifier: workspace:* + version: link:../../packages/agentos-toolchain + '@types/node': + specifier: ^22.10.2 + version: 22.19.15 + typescript: + specifier: ^5.9.2 + version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) + + software/envsubst: + devDependencies: + '@agentos-software/manifest': + specifier: workspace:* + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/everything: + software/everything: dependencies: '@agentos-software/codex-cli': specifier: workspace:* @@ -3200,6 +3257,12 @@ importers: '@agentos-software/diffutils': specifier: workspace:* version: link:../diffutils + '@agentos-software/duckdb': + specifier: workspace:* + version: link:../duckdb + '@agentos-software/envsubst': + specifier: workspace:* + version: link:../envsubst '@agentos-software/fd': specifier: workspace:* version: link:../fd @@ -3212,6 +3275,9 @@ importers: '@agentos-software/gawk': specifier: workspace:* version: link:../gawk + '@agentos-software/git': + specifier: workspace:* + version: link:../git '@agentos-software/grep': specifier: workspace:* version: link:../grep @@ -3227,6 +3293,9 @@ importers: '@agentos-software/sed': specifier: workspace:* version: link:../sed + '@agentos-software/sqlite3': + specifier: workspace:* + version: link:../sqlite3 '@agentos-software/tar': specifier: workspace:* version: link:../tar @@ -3236,6 +3305,12 @@ importers: '@agentos-software/unzip': specifier: workspace:* version: link:../unzip + '@agentos-software/vim': + specifier: workspace:* + version: link:../vim + '@agentos-software/wget': + specifier: workspace:* + version: link:../wget '@agentos-software/yq': specifier: workspace:* version: link:../yq @@ -3245,340 +3320,516 @@ importers: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/fd: + software/fd: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/file: + software/file: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/findutils: + software/findutils: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/gawk: + software/gawk: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/git: + software/git: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/grep: + software/grep: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/gzip: + software/gzip: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/http-get: + software/jq: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/jq: + software/opencode: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 + bun: + specifier: 1.3.11 + version: 1.3.11 typescript: - specifier: ^5.9.2 + specifier: ^5.7.2 + version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) + + software/pi: + dependencies: + '@agentclientprotocol/sdk': + specifier: ^0.16.1 + version: 0.16.1(zod@4.3.6) + '@mariozechner/pi-ai': + specifier: 0.60.0 + version: 0.60.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6) + '@mariozechner/pi-coding-agent': + specifier: 0.60.0 + version: 0.60.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6))(bufferutil@4.1.0)(ws@8.21.0(bufferutil@4.1.0))(zod@4.3.6) + devDependencies: + '@agentos-software/manifest': + specifier: workspace:* + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness + '@rivet-dev/agentos-toolchain': + specifier: workspace:* + version: link:../../packages/agentos-toolchain + '@types/node': + specifier: ^22.10.2 + version: 22.19.15 + esbuild: + specifier: ^0.27.4 + version: 0.27.4 + typescript: + specifier: ^5.7.2 + version: 5.9.3 + + software/pi-cli: + dependencies: + '@mariozechner/pi-coding-agent': + specifier: ^0.60.0 + version: 0.60.0 + pi-acp: + specifier: ^0.0.23 + version: 0.0.23 + devDependencies: + '@agentos-software/manifest': + specifier: workspace:* + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness + '@rivet-dev/agentos-toolchain': + specifier: workspace:* + version: link:../../packages/agentos-toolchain + '@types/node': + specifier: ^22.10.2 + version: 22.19.15 + typescript: + specifier: ^5.7.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/ripgrep: + software/ripgrep: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/sed: + software/sed: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/sqlite3: + software/sqlite3: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/tar: + software/ssh: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 + '@types/ssh2': + specifier: ^1.15.1 + version: 1.15.5 + ssh2: + specifier: ^1.16.0 + version: 1.17.0 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/tree: + software/tar: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/unzip: + software/tree: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/vim: + software/unzip: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/vix: + software/vim: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/wget: + software/wget: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/yq: + software/yq: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - registry/software/zip: + software/zip: devDependencies: '@agentos-software/manifest': specifier: workspace:* - version: link:../../../packages/manifest + version: link:../../packages/manifest + '@agentos/test-harness': + specifier: workspace:* + version: link:../../test-harness '@rivet-dev/agentos-toolchain': specifier: workspace:* - version: link:../../../packages/agentos-toolchain + version: link:../../packages/agentos-toolchain '@types/node': specifier: ^22.10.2 version: 22.19.15 typescript: specifier: ^5.9.2 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@22.19.15) - scripts/publish: + test-harness: dependencies: - commander: - specifier: ^12.1.0 - version: 12.1.0 - execa: - specifier: ^8.0.1 - version: 8.0.1 - glob: - specifier: ^10.3.10 - version: 10.5.0 - semver: - specifier: ^7.6.0 - version: 7.7.4 + '@rivet-dev/agentos-vm-test-harness': + specifier: workspace:* + version: link:../packages/vm-test-harness devDependencies: - '@types/node': - specifier: ^24.3.0 - version: 24.13.2 - '@types/semver': - specifier: ^7.5.8 - version: 7.7.1 - tsx: - specifier: ^4.0.0 - version: 4.21.0 typescript: - specifier: ^5.5.2 + specifier: ^5.9.2 version: 5.9.3 packages: @@ -5800,6 +6051,9 @@ packages: '@types/semver@7.7.1': resolution: {integrity: sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==} + '@types/ssh2@1.15.5': + resolution: {integrity: sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==} + '@types/uuid@10.0.0': resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} @@ -7573,6 +7827,77 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + npm@11.18.0: + resolution: {integrity: sha512-T67M4L5wNm0cZ7EBLErcEkY1SmzEW/WJ+SADBzsFUY1UdAPfFHXFQtZ6SEXiK0+vzXysCvAsepbMaBTwnrAD+w==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + bundledDependencies: + - '@isaacs/string-locale-compare' + - '@npmcli/arborist' + - '@npmcli/config' + - '@npmcli/fs' + - '@npmcli/map-workspaces' + - '@npmcli/metavuln-calculator' + - '@npmcli/package-json' + - '@npmcli/promise-spawn' + - '@npmcli/redact' + - '@npmcli/run-script' + - '@sigstore/tuf' + - abbrev + - archy + - cacache + - chalk + - ci-info + - fastest-levenshtein + - fs-minipass + - glob + - graceful-fs + - hosted-git-info + - ini + - init-package-json + - is-cidr + - json-parse-even-better-errors + - libnpmaccess + - libnpmdiff + - libnpmexec + - libnpmfund + - libnpmorg + - libnpmpack + - libnpmpublish + - libnpmsearch + - libnpmteam + - libnpmversion + - make-fetch-happen + - minimatch + - minipass + - minipass-pipeline + - ms + - node-gyp + - nopt + - npm-audit-report + - npm-install-checks + - npm-package-arg + - npm-pick-manifest + - npm-profile + - npm-registry-fetch + - npm-user-validate + - p-map + - pacote + - parse-conflict-json + - proc-log + - qrcode-terminal + - read + - semver + - spdx-expression-parse + - ssri + - supports-color + - tar + - text-table + - tiny-relative-date + - treeverse + - validate-npm-package-name + - which + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -11630,6 +11955,10 @@ snapshots: '@types/semver@7.7.1': {} + '@types/ssh2@1.15.5': + dependencies: + '@types/node': 18.19.130 + '@types/uuid@10.0.0': {} '@types/yauzl@2.10.3': @@ -13541,6 +13870,8 @@ snapshots: dependencies: path-key: 4.0.0 + npm@11.18.0: {} + nth-check@2.1.1: dependencies: boolbase: 1.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index abbcff2e92..624818f06b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -15,14 +15,14 @@ packages: - packages/runtime-core - packages/runtime-sidecar - packages/runtime-sidecar/npm/* + - packages/vm-test-harness - packages/secure-exec - packages/secure-exec-example-ai-agent-type-check - packages/typescript - packages/shell - packages/sidecar-binary - - registry/agent/* - - registry/software/* - - registry/tool/* + - software/* + - test-harness - examples/* # Nested example packages (e.g. examples/quickstart/hello-world) so every # embeddable example is a workspace member that `turbo check-types` covers. diff --git a/registry/CONTRIBUTING.md b/registry/CONTRIBUTING.md deleted file mode 100644 index 1caef14b93..0000000000 --- a/registry/CONTRIBUTING.md +++ /dev/null @@ -1,99 +0,0 @@ -# Contributing a Registry Package - -Software and agent packages for agentOS VMs, published under the -`@agentos-software/*` npm scope. This is the quick path to adding one; the -full documentation lives on the website: - -- [Software Definition](https://agentos-sdk.dev/docs/custom-software/definition) — package anatomy and manifest fields -- [Building Binaries](https://agentos-sdk.dev/docs/custom-software/building-wasm) — compiling commands to WASM -- [Publishing Packages](https://agentos-sdk.dev/docs/custom-software/publishing) — shipping to npm with the toolchain - -## File structure - -``` -registry/ -├── agent// JavaScript agent adapters (claude, codex, opencode, pi) -├── software// WASM command packages (jq, ripgrep, git, …) -└── native/ toolchains that compile the command binaries -``` - -Each package contains: - -``` -registry/software// -├── package.json name, per-package semver, build script -├── agentos-package.json runtime manifest (commands/aliases/provides) + -│ `registry` block (title/description/priority/image) -│ that lists the package on agentos-sdk.dev/registry -├── src/index.ts descriptor export consumed by `software: []` -├── bin/ staged binaries (gitignored, built) -└── dist/package/ assembled runtime dir shipped in the npm tarball -``` - -## Building - -From the repo root: - -```bash -just registry-native # compile the fast native wasm command gate -just registry-native-cmd # build one command (required for git, duckdb, vim, wget, codex) -pnpm --filter @agentos-software/ build # stage bin/ + assemble one package -just registry-build # stage + assemble every software package -just registry-status # per-package state -just registry-test # registry integration tests -``` - -See [Building Binaries](https://agentos-sdk.dev/docs/custom-software/building-wasm) -for toolchain details (Rust vs C builds, the patched WASI sysroot). - -## Adding a package - -1. Copy an existing package of the same kind (`software/jq` is a minimal - example) to `registry/software//`. -2. Add the command source under `registry/native/` (Rust: a - `crates/commands/` crate; C: wire it into `registry/native/c`). -3. Fill in `agentos-package.json`: `commands` (and `aliases`/`provides` if - needed) plus a `registry` block with `title` and `description` — without - that block the package is not listed on the website registry page. -4. Register the directory in `pnpm-workspace.yaml` (it is covered by the - `registry/software/*` glob) and run `pnpm install`. -5. Run `pnpm --filter @agentos-software/ build` and check - `just registry-status`. - -See [Software Definition](https://agentos-sdk.dev/docs/custom-software/definition) -for every manifest field. - -## Testing in an external project - -Inside this repo, tests and examples resolve packages via `workspace:*` — no -publishing needed. To try a package in an external project, pack the built -tarball and install it by path: - -```bash -cd registry/software/ -npm pack # produces agentos-software--.tgz -cd /path/to/your-project -npm install /path/to/agentos-software--.tgz -``` - -Then register it in your VM and run a command: - -```typescript -import myPkg from "@agentos-software/"; -const vm = agentOS({ software: [myPkg] }); -``` - -Real publishes go through `agentos-toolchain publish` (dist-tag `dev` by -default) — see [Publishing Packages](https://agentos-sdk.dev/docs/custom-software/publishing). - -## Opening a PR - -- Branch, commit with a plain conventional-commit title - (`feat(registry): add package`), no agent attribution. -- Include: the package directory, the native build wiring, and the - `registry` block so the website picks it up. -- Keep the package version at its own semver (packages version - independently); never touch other packages' versions or the `latest` - dist-tag. -- Cheap gates before pushing: `cargo check --workspace`, `pnpm build`, - `pnpm check-types`, and `pnpm --filter @agentos-software/ build`. diff --git a/registry/README.md b/registry/README.md deleted file mode 100644 index a4b904a248..0000000000 --- a/registry/README.md +++ /dev/null @@ -1,168 +0,0 @@ -# secure-exec Registry - -Software packages for secure-exec VMs: WASM command binaries -(`registry/software/*`), JavaScript agent adapters (`registry/agent/*`), and -tool packages (`registry/tool/*`). Everything under the `@agentos-software/*` -npm scope. - -## Consuming packages - -```bash -npm install @agentos-software/coreutils @agentos-software/grep -# or a meta-package for a complete set: -npm install @agentos-software/common -``` - -Each package default-exports a descriptor whose `packageDir` points at the -self-contained runtime dir the sidecar projects under -`/opt/agentos//` (meta-packages export an array of descriptors): - -```typescript -import coreutils from "@agentos-software/coreutils"; -import grep from "@agentos-software/grep"; - -export const software = [coreutils, grep]; -``` - -## Package anatomy - -``` -registry/software// -├── package.json name, per-package semver version, build script -├── agentos-package.json manifest: runtime fields (name/agent/provides) + -│ staging fields (commands/aliases/stubs) -├── src/index.ts descriptor: packageDir -> ./package/ (dist/package) -├── bin/ staged command binaries (generated build output) -└── dist/package/ the assembled runtime dir (shipped in the npm tarball): - ├── package.json { name, version, bin: { : "bin/" } } - ├── agentos-package.json - └── bin/ the binaries, copied verbatim -``` - -The whole lifecycle is owned by **`@rivet-dev/agentos-toolchain`** -(`packages/agentos-toolchain`) — the same CLI 3rd-party repos use to build and -publish their own agentOS packages (`npx @rivet-dev/agentos-toolchain`): - -- `stage --commands-dir ` — populate `bin/` from a compiled commands - directory, per the `commands` / `aliases` / `stubs` lists in - `agentos-package.json`. -- `build` — assemble the clean `dist/package/` runtime dir from `bin/`. -- `pack` — build a self-contained node-closure package (JS agents). -- `publish` — publish to npm; dist-tag `dev` by default, `latest` only with an - explicit `--latest`. - -## Building - -All recipes run from the repo root (see `justfile`): - -```bash -just registry-native # compile the fast native wasm command gate -just registry-native-cmd # build ONE command binary, whatever its toolchain -just registry-build # stage + assemble every registry package -pnpm --filter @agentos-software/coreutils build:runtime # assemble runnable coreutils -just registry-status # per-package state; --remote adds npm dist-tags -just registry-test # registry integration tests (registry/tests) -``` - -### Building coreutils from a clean checkout - -`registry/software/coreutils/bin/` is deliberately gitignored and must never be -committed. A fresh checkout has no runnable coreutils package: VMs, browser -demos, and tests that use `sh`, `cat`, `chmod`, or the other bundled commands -will not work until the native WASM commands are compiled and staged. - -Run the complete build from the repository root: - -```bash -pnpm install --frozen-lockfile -just registry-native -pnpm --filter @agentos-software/coreutils build:runtime -``` - -The native build compiles the patched Rust and C WASI sources into -`registry/native/target/wasm32-wasip1/release/commands/`. `build:runtime` -strictly stages every command, alias, and stub into -`registry/software/coreutils/bin/`, then assembles `dist/package/`. It fails if -the native command set is absent or incomplete; an empty placeholder is not a -usable coreutils package. - -The ordinary `build` script retains `--if-missing skip` so repository-wide -TypeScript builds can run without first compiling the WASM toolchain. On a -source-only checkout that script assembles an empty placeholder. It does not -make `sh` or any coreutils command available and is not a runtime build. -Publishing coreutils runs `build:runtime` through its `prepublishOnly` lifecycle -and fails rather than publishing an empty or incomplete command package. - -`just registry-native-cmd sh` is useful when iterating on only the shell, but it -does not build the other commands and therefore is not enough to assemble -coreutils. Use the complete sequence above for demos, packaging, and E2E tests. - -`registry-native-cmd` (= `make -C registry/native cmd/`) is the uniform -per-binary entry point; it dispatches to whichever toolchain owns the command: - -| kind | commands | what it runs | -|---|---|---| -| Rust | any `crates/commands/` (sh, ls, rg, …) | `cargo build -p cmd-` (build-std) + `wasm-opt` | -| C | `zip unzip envsubst sqlite3 curl wget duckdb` | `make -C c sysroot build/` + per-command install | -| codex | `codex`, `codex-exec` | the codex fork build (needs the fork checkout) | -| C | `vim` (pinned upstream clone + bridge in `c/vim/`) | `make -C c sysroot build/vim` + install | -| external | `vix` | validates the hand-built binary is in the drop zone; errors with instructions otherwise | - -The default native build (`registry/native`) compiles the fast command gate to -`wasm32-wasip1` with a patched std (`-Z build-std`, `patches/`), runs -`wasm-opt -O3`, and drops the binaries in -`registry/native/target/wasm32-wasip1/release/commands/`. The bulk gate -intentionally excludes slow/heavy or non-default commands: `git`, `duckdb`, -`vim`, `wget`, and the external `codex`/`codex-exec` fork build. Build those explicitly with -`just registry-native-cmd ` when working on them. Package builds then run -`agentos-toolchain stage`, followed by `tsc` and `agentos-toolchain build`. -Use coreutils `build:runtime` for strict staging as described above; software -packages may use skip mode for source-only checks or commands outside the -default native gate. - -Within this repo, everything consumes the LOCAL builds by default: the registry -packages are pnpm workspace members, so tests and examples resolve them via -`workspace:*` — no publish needed for local development. - -Exceptions: -- `software/codex/wasm/` is the install target of the codex fork's build - (`make -C registry/native codex`); `software/codex-cli` stages from it. -- C-built commands (sqlite3, zip, unzip, curl, wget, duckdb) need the patched - sysroot; `just registry-native-cmd ` builds it on demand. Without it - those packages stay empty placeholders. -- `vim` builds from source: `just registry-native-cmd vim` clones the pinned - vim tag and compiles it against the patched sysroot + the termios/termcap - bridge in `registry/native/c/vim/` (its runtime tree is staged by the - package `scripts/stage-runtime.mjs` and applied via manifest `provides`). -- `vix` is the one remaining external drop-zone binary (no source pipeline): - place the hand-built wasm at `registry/native/target/.../commands/vix`. - -## Publishing - -Packages **version independently** (per-package semver in each -`package.json`). Publishing NEVER moves the `latest` dist-tag unless asked: - -```bash -just registry-publish coreutils # publish @agentos-software/coreutils under dist-tag `dev` -just registry-publish coreutils my-branch # ... under a custom tag -just registry-publish coreutils latest # DELIBERATE release: moves `latest` -just registry-publish-all # every built software package, dist-tag `dev` -``` - -Bump the package's `version` in its `package.json` (commit it) before -publishing. CI does not publish these packages (the publish workflow's package -discovery skips `@agentos-software/*` except the manifest); the agent packages -under `registry/agent/*` preview-publish via `.github/workflows/publish.yaml` -under a branch dist-tag. - -agent-os consumes the published packages pinned per-package in its catalog -(`just agentos-pkgs-status` there), and flips to these local checkouts with -`just agentos-pkgs-local`. - -## Contributing - -See [CONTRIBUTING.md](CONTRIBUTING.md) for how to add new packages. - -## License - -Apache-2.0 diff --git a/registry/agent/claude/agentos-package.json b/registry/agent/claude/agentos-package.json deleted file mode 100644 index 5d09a62725..0000000000 --- a/registry/agent/claude/agentos-package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "claude", - "agent": { - "acpEntrypoint": "claude-sdk-acp", - "env": { - "CLAUDE_AGENT_SDK_CLIENT_APP": "@rivet-dev/agentos", - "CLAUDE_CODE_SIMPLE": "1", - "CLAUDE_CODE_FORCE_AGENT_OS_RIPGREP": "1", - "CLAUDE_CODE_DEFER_GROWTHBOOK_INIT": "1", - "CLAUDE_CODE_DISABLE_CWD_PERSIST": "1", - "CLAUDE_CODE_DISABLE_DEV_NULL_REDIRECT": "1", - "CLAUDE_CODE_NODE_SHELL_WRAPPER": "1", - "CLAUDE_CODE_DISABLE_STREAM_JSON_HOOK_EVENTS": "1", - "CLAUDE_CODE_SHELL": "/bin/sh", - "CLAUDE_CODE_SKIP_INITIAL_MESSAGES": "1", - "CLAUDE_CODE_SKIP_SANDBOX_INIT": "1", - "CLAUDE_CODE_SIMPLE_SHELL_EXEC": "1", - "CLAUDE_CODE_SWAP_STDIO": "0", - "CLAUDE_CODE_USE_PIPE_OUTPUT": "1", - "DISABLE_TELEMETRY": "1", - "SHELL": "/bin/sh", - "USE_BUILTIN_RIPGREP": "0" - } - }, - "registry": { - "slug": "claude-code", - "title": "Claude Code", - "description": "Run Claude Code as an agentOS agent with full tool access, file editing, and shell execution.", - "beta": true, - "docsHref": "/docs/agents/claude", - "image": "/images/registry/claude-code.svg", - "priority": 90 - } -} diff --git a/registry/agent/claude/package.json b/registry/agent/claude/package.json deleted file mode 100644 index 3b056fe9be..0000000000 --- a/registry/agent/claude/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "@agentos-software/claude-code", - "version": "0.2.1", - "type": "module", - "license": "Apache-2.0", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "bin": { - "claude": "./dist/claude-cli.mjs", - "claude-sdk-acp": "./dist/adapter.js" - }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "files": [ - "dist", - "!dist/package", - "!dist/package.tar", - "agentos-package.json" - ], - "scripts": { - "build": "tsc && node ./scripts/build-patched-cli.mjs && rm -rf dist/package && agentos-toolchain pack . --out dist/package --agent claude-sdk-acp --prune-native", - "check-types": "tsc --noEmit", - "test": "pnpm build && node --test --test-force-exit tests/*.test.mjs" - }, - "dependencies": { - "@agentclientprotocol/sdk": "^0.16.1", - "@anthropic-ai/claude-agent-sdk": "0.2.87", - "zod": "^4.1.11" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.7.2" - } -} diff --git a/registry/agent/claude/tsconfig.json b/registry/agent/claude/tsconfig.json deleted file mode 100644 index bff7313256..0000000000 --- a/registry/agent/claude/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "declaration": true, - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/registry/agent/codex/agentos-package.json b/registry/agent/codex/agentos-package.json deleted file mode 100644 index a99b543f00..0000000000 --- a/registry/agent/codex/agentos-package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "registry": { - "title": "Codex", - "description": "Run OpenAI's Codex coding agent inside agentOS with programmatic API access.", - "beta": true, - "docsHref": "/docs/agents/codex", - "image": "/images/registry/codex.svg", - "priority": 80 - } -} diff --git a/registry/agent/codex/package.json b/registry/agent/codex/package.json deleted file mode 100644 index bdc2e759a0..0000000000 --- a/registry/agent/codex/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@agentos-software/codex", - "version": "0.2.0-rc.3", - "type": "module", - "license": "Apache-2.0", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "scripts": { - "build": "tsc", - "check-types": "tsc --noEmit", - "test": "pnpm build && node --test tests/*.test.mjs" - }, - "dependencies": { - "@agentos-software/codex-cli": "workspace:*" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.7.2" - } -} diff --git a/registry/agent/codex/tsconfig.json b/registry/agent/codex/tsconfig.json deleted file mode 100644 index bff7313256..0000000000 --- a/registry/agent/codex/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "declaration": true, - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/registry/agent/opencode/agentos-package.json b/registry/agent/opencode/agentos-package.json deleted file mode 100644 index cb9f08643a..0000000000 --- a/registry/agent/opencode/agentos-package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "opencode", - "agent": { - "acpEntrypoint": "agentos-opencode-acp", - "env": { - "OPENCODE_DISABLE_CONFIG_DEP_INSTALL": "1", - "OPENCODE_DISABLE_EMBEDDED_WEB_UI": "1" - } - }, - "registry": { - "title": "OpenCode", - "description": "Run OpenCode, an open-source coding agent, inside agentOS.", - "docsHref": "/docs/agents/opencode", - "image": "/images/registry/opencode.svg", - "priority": 70 - } -} diff --git a/registry/agent/opencode/package.json b/registry/agent/opencode/package.json deleted file mode 100644 index c3ab2d9c5c..0000000000 --- a/registry/agent/opencode/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "@agentos-software/opencode", - "version": "0.2.1", - "type": "module", - "license": "Apache-2.0", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "bin": { - "agentos-opencode-acp": "./dist/adapter.js" - }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "files": [ - "dist", - "!dist/package", - "!dist/package.tar", - "agentos-package.json" - ], - "scripts": { - "build": "node ./scripts/build-opencode-acp.mjs && tsc && rm -rf dist/package dist/package.tar && agentos-toolchain pack . --out dist/package --agent agentos-opencode-acp", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "bun": "1.3.11", - "typescript": "^5.7.2" - } -} diff --git a/registry/agent/opencode/tsconfig.json b/registry/agent/opencode/tsconfig.json deleted file mode 100644 index bff7313256..0000000000 --- a/registry/agent/opencode/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "declaration": true, - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/registry/agent/pi-cli/agentos-package.json b/registry/agent/pi-cli/agentos-package.json deleted file mode 100644 index fdd6c08d5f..0000000000 --- a/registry/agent/pi-cli/agentos-package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "pi-cli", - "agent": { - "acpEntrypoint": "pi-acp", - "env": { - "PI_ACP_PI_COMMAND": "pi" - } - } -} diff --git a/registry/agent/pi-cli/package.json b/registry/agent/pi-cli/package.json deleted file mode 100644 index 080878fbdc..0000000000 --- a/registry/agent/pi-cli/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "@agentos-software/pi-cli", - "version": "0.2.1", - "type": "module", - "license": "Apache-2.0", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "bin": { - "pi-acp": "./node_modules/pi-acp/dist/index.js", - "pi": "./node_modules/@mariozechner/pi-coding-agent/dist/cli.js" - }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "files": [ - "dist", - "!dist/package", - "!dist/package.tar", - "agentos-package.json" - ], - "scripts": { - "build": "tsc && rm -rf dist/package dist/package.tar && agentos-toolchain pack . --out dist/package --agent pi-acp --prune-native", - "check-types": "tsc --noEmit" - }, - "dependencies": { - "@mariozechner/pi-coding-agent": "^0.60.0", - "pi-acp": "^0.0.23" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.7.2" - } -} diff --git a/registry/agent/pi-cli/tsconfig.json b/registry/agent/pi-cli/tsconfig.json deleted file mode 100644 index bff7313256..0000000000 --- a/registry/agent/pi-cli/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "declaration": true, - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] -} diff --git a/registry/agent/pi/adapter-browser-entry.mjs b/registry/agent/pi/adapter-browser-entry.mjs deleted file mode 100644 index 2e621a03b7..0000000000 --- a/registry/agent/pi/adapter-browser-entry.mjs +++ /dev/null @@ -1,41 +0,0 @@ -// Browser-bundle entry for the pi ACP adapter. The adapter normally loads its SDK via -// computed dynamic import() from a VM-mounted node_modules; in the browser converged -// executor there is no such mount, so we statically import the SDK submodules (which -// esbuild bundles into a single self-contained file) and publish the same -// `__PI_SDK_RUNTIME__` object used by the native V8 startup snapshot. The adapter is -// otherwise unchanged — same ACP behavior, just bundled instead of VFS-resolved. -// -// Imports use relative node_modules file paths (not bare package subpaths) so esbuild -// resolves the dist files directly, bypassing the packages' restrictive `exports`. -import * as agentCore from "./node_modules/@mariozechner/pi-agent-core/dist/index.js"; -import * as authStorage from "./node_modules/@mariozechner/pi-coding-agent/dist/core/auth-storage.js"; -import * as config from "./node_modules/@mariozechner/pi-coding-agent/dist/config.js"; -import * as defaults from "./node_modules/@mariozechner/pi-coding-agent/dist/core/defaults.js"; -import * as messages from "./node_modules/@mariozechner/pi-coding-agent/dist/core/messages.js"; -import * as modelRegistry from "./node_modules/@mariozechner/pi-coding-agent/dist/core/model-registry.js"; -import * as resourceLoader from "./node_modules/@mariozechner/pi-coding-agent/dist/core/resource-loader.js"; -import * as sdk from "./node_modules/@mariozechner/pi-coding-agent/dist/core/sdk.js"; -import * as sessionManager from "./node_modules/@mariozechner/pi-coding-agent/dist/core/session-manager.js"; -import * as settingsManager from "./node_modules/@mariozechner/pi-coding-agent/dist/core/settings-manager.js"; -import * as tools from "./node_modules/@mariozechner/pi-coding-agent/dist/core/tools/index.js"; - -// Keep this shape identical to `runtime` in src/snapshot-entry.ts. The adapter reads -// it lazily at session/new, so setting it after the adapter import (ESM-hoisted) is -// safe. -globalThis.__PI_SDK_RUNTIME__ = { - Agent: agentCore.Agent, - AuthStorage: authStorage.AuthStorage, - DefaultResourceLoader: resourceLoader.DefaultResourceLoader, - DEFAULT_THINKING_LEVEL: defaults.DEFAULT_THINKING_LEVEL, - ModelRegistry: modelRegistry.ModelRegistry, - SettingsManager: settingsManager.SettingsManager, - SessionManager: sessionManager.SessionManager, - convertToLlm: messages.convertToLlm, - getAgentDir: config.getAgentDir, - getDocsPath: config.getDocsPath, - createAgentSession: sdk.createAgentSession, - createCodingTools: sdk.createCodingTools, - createAllTools: tools.createAllTools, -}; - -import "./dist/adapter.js"; diff --git a/registry/agent/pi/agentos-package.json b/registry/agent/pi/agentos-package.json deleted file mode 100644 index 898e089c6e..0000000000 --- a/registry/agent/pi/agentos-package.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "name": "pi", - "agent": { - "acpEntrypoint": "pi-sdk-acp", - "snapshot": true - }, - "registry": { - "title": "PI", - "description": "Run the PI coding agent with lightweight, fast execution.", - "image": "/images/registry/pi.svg", - "priority": 100 - } -} diff --git a/registry/agent/pi/package.json b/registry/agent/pi/package.json deleted file mode 100644 index 96ae3ce7fa..0000000000 --- a/registry/agent/pi/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "name": "@agentos-software/pi", - "version": "0.2.1", - "type": "module", - "license": "Apache-2.0", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "bin": { - "pi": "./node_modules/@mariozechner/pi-coding-agent/dist/cli.js", - "pi-sdk-acp": "./dist/adapter.js" - }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "files": [ - "dist", - "!dist/package", - "!dist/package.tar", - "agentos-package.json" - ], - "scripts": { - "build": "tsc && node scripts/build-snapshot-bundle.mjs && rm -rf dist/package dist/package.tar && agentos-toolchain pack . --out dist/package --agent pi-sdk-acp --prune-native && node scripts/copy-snapshot-into-package.mjs", - "build:snapshot": "node scripts/build-snapshot-bundle.mjs", - "check-types": "tsc --noEmit", - "test": "pnpm build && node --test --test-force-exit tests/*.test.mjs" - }, - "dependencies": { - "@agentclientprotocol/sdk": "^0.16.1", - "@mariozechner/pi-agent-core": "0.60.0", - "@mariozechner/pi-coding-agent": "0.60.0", - "@mariozechner/pi-ai": "0.60.0" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.7.2" - } -} diff --git a/registry/agent/pi/tsconfig.json b/registry/agent/pi/tsconfig.json deleted file mode 100644 index e15131c84e..0000000000 --- a/registry/agent/pi/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "declaration": true, - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "src/snapshot-entry.ts"] -} diff --git a/registry/native/Cargo.lock b/registry/native/Cargo.lock deleted file mode 100644 index fb7e69a723..0000000000 --- a/registry/native/Cargo.lock +++ /dev/null @@ -1,6545 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "ansi-width" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "219e3ce6f2611d83b51ec2098a12702112c29e57203a6b0a0929b2cddb486608" -dependencies = [ - "unicode-width 0.1.14", -] - -[[package]] -name = "anstream" -version = "0.6.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" -dependencies = [ - "anstyle", - "anstyle-parse 0.2.7", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse 1.0.0", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys 0.61.2", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "archery" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e0a5f99dfebb87bb342d0f53bb92c81842e100bbb915223e38349580e5441d" -dependencies = [ - "triomphe", -] - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "awk-rs" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6041a9be45690d0003e8d2ea488cb0a578104e591944398d96d9b7d9a0fbf9" -dependencies = [ - "regex", - "thiserror 1.0.69", -] - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "windows-link 0.2.1", -] - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" -dependencies = [ - "outref", - "vsimd", -] - -[[package]] -name = "bigdecimal" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" -dependencies = [ - "autocfg", - "libm", - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "binary-heap-plus" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4551d8382e911ecc0d0f0ffb602777988669be09447d536ff4388d1def11296" -dependencies = [ - "compare", -] - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" -dependencies = [ - "serde_core", -] - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "blake2b_simd" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" -dependencies = [ - "arrayref", - "arrayvec", - "constant_time_eq", -] - -[[package]] -name = "blake3" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if", - "constant_time_eq", - "cpufeatures", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bon" -version = "3.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe" -dependencies = [ - "bon-macros", - "rustversion", -] - -[[package]] -name = "bon-macros" -version = "3.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" -dependencies = [ - "darling 0.23.0", - "ident_case", - "prettyplease", - "proc-macro2", - "quote", - "rustversion", - "syn", -] - -[[package]] -name = "brush-builtins" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "454219f460ee4308ae5b8befc47936b4ef7ac3a94347052270e59b6c6cfe14ec" -dependencies = [ - "brush-core", - "brush-parser", - "cfg-if", - "chrono", - "clap", - "fancy-regex 0.16.2", - "futures", - "itertools 0.14.0", - "nix 0.30.1", - "procfs", - "rlimit", - "strum 0.27.2", - "strum_macros 0.27.2", - "thiserror 2.0.18", - "tokio", - "tracing", - "uucore 0.4.0", -] - -[[package]] -name = "brush-core" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a3ad2f2d4eb45ef7f11e74bc1a0816c8f3dce298072352c6b3b41a2c4bfdb74" -dependencies = [ - "async-recursion", - "async-trait", - "bon", - "brush-parser", - "cached", - "cfg-if", - "chrono", - "clap", - "command-fds", - "fancy-regex 0.16.2", - "futures", - "getrandom 0.3.4", - "homedir", - "hostname", - "indexmap", - "itertools 0.14.0", - "nix 0.30.1", - "normalize-path", - "rand 0.9.2", - "rpds", - "strum 0.27.2", - "strum_macros 0.27.2", - "terminfo", - "thiserror 2.0.18", - "tokio", - "tracing", - "uuid", - "uzers", - "whoami", -] - -[[package]] -name = "brush-interactive" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d971b1d01934fa815b061451d183f6f25e248f491a0e181fa186e3d78a1ad51" -dependencies = [ - "brush-core", - "brush-parser", - "crossterm 0.29.0", - "getrandom 0.3.4", - "indexmap", - "nix 0.30.1", - "nu-ansi-term", - "reedline", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "brush-parser" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7367124d4f38fdcd65f4b815bda7caeb3de377b9cd95ffa1b23627989c93718" -dependencies = [ - "bon", - "cached", - "indenter", - "peg", - "thiserror 2.0.18", - "tracing", - "utf8-chars", -] - -[[package]] -name = "brush-shell" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c0d5917155a7043b86d27d4832feea614b4e1349735a18ab65c551949bc15d5" -dependencies = [ - "brush-builtins", - "brush-core", - "brush-interactive", - "clap", - "color-print", - "const_format", - "crossterm 0.29.0", - "getrandom 0.3.4", - "git-version", - "human-panic", - "tokio", - "tracing", - "tracing-subscriber", - "uuid", -] - -[[package]] -name = "bstr" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" -dependencies = [ - "memchr", - "regex-automata", - "serde", -] - -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "cached" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "801927ee168e17809ab8901d9f01f700cd7d8d6a6527997fee44e4b0327a253c" -dependencies = [ - "ahash", - "cached_proc_macro", - "cached_proc_macro_types", - "hashbrown 0.15.5", - "once_cell", - "thiserror 2.0.18", - "web-time", -] - -[[package]] -name = "cached_proc_macro" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9225bdcf4e4a9a4c08bf16607908eb2fbf746828d5e0b5e019726dbf6571f201" -dependencies = [ - "darling 0.20.11", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "cached_proc_macro_types" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" - -[[package]] -name = "calendrical_calculations" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7" -dependencies = [ - "core_maths", - "displaydoc", -] - -[[package]] -name = "cassowary" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" - -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - -[[package]] -name = "cc" -version = "1.2.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfb" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" -dependencies = [ - "byteorder", - "fnv", - "uuid", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link 0.2.1", -] - -[[package]] -name = "clap" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" -dependencies = [ - "anstream 1.0.0", - "anstyle", - "clap_lex", - "strsim", - "terminal_size", -] - -[[package]] -name = "clap_derive" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" - -[[package]] -name = "cmd-arch" -version = "0.1.0" -dependencies = [ - "uu_arch", -] - -[[package]] -name = "cmd-awk" -version = "0.1.0" -dependencies = [ - "secureexec-awk", -] - -[[package]] -name = "cmd-b2sum" -version = "0.1.0" -dependencies = [ - "uu_b2sum", -] - -[[package]] -name = "cmd-base32" -version = "0.1.0" -dependencies = [ - "uu_base32", -] - -[[package]] -name = "cmd-base64" -version = "0.1.0" -dependencies = [ - "uu_base64", -] - -[[package]] -name = "cmd-basename" -version = "0.1.0" -dependencies = [ - "uu_basename", -] - -[[package]] -name = "cmd-basenc" -version = "0.1.0" -dependencies = [ - "uu_basenc", -] - -[[package]] -name = "cmd-cat" -version = "0.1.0" -dependencies = [ - "uu_cat", - "uucore 0.7.0", -] - -[[package]] -name = "cmd-chmod" -version = "0.1.0" -dependencies = [ - "uu_chmod", -] - -[[package]] -name = "cmd-cksum" -version = "0.1.0" -dependencies = [ - "uu_cksum", -] - -[[package]] -name = "cmd-codex" -version = "0.1.0" -dependencies = [ - "codex-network-proxy", - "codex-otel", - "ratatui", - "secureexec-wasi-http", - "secureexec-wasi-spawn", -] - -[[package]] -name = "cmd-codex-exec" -version = "0.1.0" -dependencies = [ - "codex-network-proxy", - "codex-otel", - "secureexec-wasi-http", -] - -[[package]] -name = "cmd-column" -version = "0.1.0" -dependencies = [ - "secureexec-column", -] - -[[package]] -name = "cmd-comm" -version = "0.1.0" -dependencies = [ - "uu_comm", -] - -[[package]] -name = "cmd-cp" -version = "0.1.0" -dependencies = [ - "uu_cp", -] - -[[package]] -name = "cmd-curl" -version = "0.1.0" -dependencies = [ - "secureexec-wasi-http", -] - -[[package]] -name = "cmd-cut" -version = "0.1.0" -dependencies = [ - "uu_cut", -] - -[[package]] -name = "cmd-date" -version = "0.1.0" -dependencies = [ - "uu_date", -] - -[[package]] -name = "cmd-dd" -version = "0.1.0" -dependencies = [ - "uu_dd", -] - -[[package]] -name = "cmd-diff" -version = "0.1.0" -dependencies = [ - "secureexec-diff", -] - -[[package]] -name = "cmd-dircolors" -version = "0.1.0" -dependencies = [ - "uu_dircolors", -] - -[[package]] -name = "cmd-dirname" -version = "0.1.0" -dependencies = [ - "uu_dirname", -] - -[[package]] -name = "cmd-du" -version = "0.1.0" -dependencies = [ - "secureexec-du", -] - -[[package]] -name = "cmd-echo" -version = "0.1.0" -dependencies = [ - "uu_echo", -] - -[[package]] -name = "cmd-env" -version = "0.1.0" -dependencies = [ - "secureexec-shims", -] - -[[package]] -name = "cmd-expand" -version = "0.1.0" -dependencies = [ - "uu_expand", -] - -[[package]] -name = "cmd-expr" -version = "0.1.0" -dependencies = [ - "secureexec-expr", -] - -[[package]] -name = "cmd-factor" -version = "0.1.0" -dependencies = [ - "uu_factor", -] - -[[package]] -name = "cmd-false" -version = "0.1.0" -dependencies = [ - "uu_false", -] - -[[package]] -name = "cmd-fd" -version = "0.1.0" -dependencies = [ - "secureexec-fd", -] - -[[package]] -name = "cmd-file" -version = "0.1.0" -dependencies = [ - "secureexec-file-cmd", -] - -[[package]] -name = "cmd-find" -version = "0.1.0" -dependencies = [ - "secureexec-find", -] - -[[package]] -name = "cmd-fmt" -version = "0.1.0" -dependencies = [ - "uu_fmt", -] - -[[package]] -name = "cmd-fold" -version = "0.1.0" -dependencies = [ - "uu_fold", -] - -[[package]] -name = "cmd-git" -version = "0.1.0" -dependencies = [ - "secureexec-git", -] - -[[package]] -name = "cmd-grep" -version = "0.1.0" -dependencies = [ - "secureexec-grep", -] - -[[package]] -name = "cmd-gzip" -version = "0.1.0" -dependencies = [ - "secureexec-gzip", -] - -[[package]] -name = "cmd-head" -version = "0.1.0" -dependencies = [ - "uu_head", -] - -[[package]] -name = "cmd-http-test" -version = "0.1.0" -dependencies = [ - "secureexec-wasi-http", -] - -[[package]] -name = "cmd-join" -version = "0.1.0" -dependencies = [ - "uu_join", -] - -[[package]] -name = "cmd-jq" -version = "0.1.0" -dependencies = [ - "secureexec-jq", -] - -[[package]] -name = "cmd-link" -version = "0.1.0" -dependencies = [ - "uu_link", -] - -[[package]] -name = "cmd-ln" -version = "0.1.0" -dependencies = [ - "uu_ln", -] - -[[package]] -name = "cmd-logname" -version = "0.1.0" -dependencies = [ - "uu_logname", -] - -[[package]] -name = "cmd-ls" -version = "0.1.0" -dependencies = [ - "uu_ls", -] - -[[package]] -name = "cmd-md5sum" -version = "0.1.0" -dependencies = [ - "uu_md5sum", -] - -[[package]] -name = "cmd-mkdir" -version = "0.1.0" -dependencies = [ - "uu_mkdir", -] - -[[package]] -name = "cmd-mktemp" -version = "0.1.0" -dependencies = [ - "uu_mktemp", -] - -[[package]] -name = "cmd-mv" -version = "0.1.0" -dependencies = [ - "uu_mv", -] - -[[package]] -name = "cmd-nice" -version = "0.1.0" -dependencies = [ - "secureexec-shims", -] - -[[package]] -name = "cmd-nl" -version = "0.1.0" -dependencies = [ - "uu_nl", -] - -[[package]] -name = "cmd-nohup" -version = "0.1.0" -dependencies = [ - "secureexec-shims", -] - -[[package]] -name = "cmd-nproc" -version = "0.1.0" -dependencies = [ - "uu_nproc", -] - -[[package]] -name = "cmd-numfmt" -version = "0.1.0" -dependencies = [ - "uu_numfmt", -] - -[[package]] -name = "cmd-od" -version = "0.1.0" -dependencies = [ - "uu_od", -] - -[[package]] -name = "cmd-paste" -version = "0.1.0" -dependencies = [ - "uu_paste", -] - -[[package]] -name = "cmd-pathchk" -version = "0.1.0" -dependencies = [ - "uu_pathchk", -] - -[[package]] -name = "cmd-printenv" -version = "0.1.0" -dependencies = [ - "uu_printenv", -] - -[[package]] -name = "cmd-printf" -version = "0.1.0" -dependencies = [ - "uu_printf", -] - -[[package]] -name = "cmd-ptx" -version = "0.1.0" -dependencies = [ - "uu_ptx", -] - -[[package]] -name = "cmd-pwd" -version = "0.1.0" -dependencies = [ - "uu_pwd", -] - -[[package]] -name = "cmd-readlink" -version = "0.1.0" -dependencies = [ - "uu_readlink", -] - -[[package]] -name = "cmd-realpath" -version = "0.1.0" -dependencies = [ - "uu_realpath", -] - -[[package]] -name = "cmd-rev" -version = "0.1.0" -dependencies = [ - "secureexec-rev", -] - -[[package]] -name = "cmd-rg" -version = "0.1.0" -dependencies = [ - "secureexec-grep", -] - -[[package]] -name = "cmd-rm" -version = "0.1.0" -dependencies = [ - "uu_rm", -] - -[[package]] -name = "cmd-rmdir" -version = "0.1.0" -dependencies = [ - "uu_rmdir", -] - -[[package]] -name = "cmd-sed" -version = "0.1.0" -dependencies = [ - "sed", -] - -[[package]] -name = "cmd-seq" -version = "0.1.0" -dependencies = [ - "uu_seq", -] - -[[package]] -name = "cmd-sh" -version = "0.1.0" -dependencies = [ - "brush-shell", -] - -[[package]] -name = "cmd-sha1sum" -version = "0.1.0" -dependencies = [ - "uu_sha1sum", -] - -[[package]] -name = "cmd-sha224sum" -version = "0.1.0" -dependencies = [ - "uu_sha224sum", -] - -[[package]] -name = "cmd-sha256sum" -version = "0.1.0" -dependencies = [ - "uu_sha256sum", -] - -[[package]] -name = "cmd-sha384sum" -version = "0.1.0" -dependencies = [ - "uu_sha384sum", -] - -[[package]] -name = "cmd-sha512sum" -version = "0.1.0" -dependencies = [ - "uu_sha512sum", -] - -[[package]] -name = "cmd-shred" -version = "0.1.0" -dependencies = [ - "uu_shred", -] - -[[package]] -name = "cmd-shuf" -version = "0.1.0" -dependencies = [ - "uu_shuf", -] - -[[package]] -name = "cmd-sleep" -version = "0.1.0" -dependencies = [ - "secureexec-builtins", -] - -[[package]] -name = "cmd-sort" -version = "0.1.0" -dependencies = [ - "uu_sort", -] - -[[package]] -name = "cmd-spawn-test-host" -version = "0.1.0" -dependencies = [ - "secureexec-wasi-spawn", -] - -[[package]] -name = "cmd-split" -version = "0.1.0" -dependencies = [ - "uu_split", -] - -[[package]] -name = "cmd-stat" -version = "0.1.0" -dependencies = [ - "uu_stat", -] - -[[package]] -name = "cmd-stdbuf" -version = "0.1.0" -dependencies = [ - "secureexec-shims", -] - -[[package]] -name = "cmd-strings" -version = "0.1.0" -dependencies = [ - "secureexec-strings-cmd", -] - -[[package]] -name = "cmd-stubs" -version = "0.1.0" -dependencies = [ - "secureexec-stubs", -] - -[[package]] -name = "cmd-sum" -version = "0.1.0" -dependencies = [ - "uu_sum", -] - -[[package]] -name = "cmd-tac" -version = "0.1.0" -dependencies = [ - "uu_tac", -] - -[[package]] -name = "cmd-tail" -version = "0.1.0" -dependencies = [ - "uu_tail", -] - -[[package]] -name = "cmd-tar" -version = "0.1.0" -dependencies = [ - "secureexec-tar", -] - -[[package]] -name = "cmd-tee" -version = "0.1.0" -dependencies = [ - "uu_tee", -] - -[[package]] -name = "cmd-test" -version = "0.1.0" -dependencies = [ - "secureexec-builtins", -] - -[[package]] -name = "cmd-timeout" -version = "0.1.0" -dependencies = [ - "secureexec-shims", -] - -[[package]] -name = "cmd-touch" -version = "0.1.0" -dependencies = [ - "uu_touch", -] - -[[package]] -name = "cmd-tr" -version = "0.1.0" -dependencies = [ - "uu_tr", -] - -[[package]] -name = "cmd-tree" -version = "0.1.0" -dependencies = [ - "secureexec-tree", -] - -[[package]] -name = "cmd-true" -version = "0.1.0" -dependencies = [ - "uu_true", -] - -[[package]] -name = "cmd-truncate" -version = "0.1.0" -dependencies = [ - "uu_truncate", -] - -[[package]] -name = "cmd-tsort" -version = "0.1.0" -dependencies = [ - "uu_tsort", -] - -[[package]] -name = "cmd-uname" -version = "0.1.0" -dependencies = [ - "uu_uname", -] - -[[package]] -name = "cmd-unexpand" -version = "0.1.0" -dependencies = [ - "uu_unexpand", -] - -[[package]] -name = "cmd-uniq" -version = "0.1.0" -dependencies = [ - "uu_uniq", -] - -[[package]] -name = "cmd-unlink" -version = "0.1.0" -dependencies = [ - "uu_unlink", -] - -[[package]] -name = "cmd-wc" -version = "0.1.0" -dependencies = [ - "uu_wc", -] - -[[package]] -name = "cmd-which" -version = "0.1.0" -dependencies = [ - "secureexec-shims", -] - -[[package]] -name = "cmd-whoami" -version = "0.1.0" -dependencies = [ - "secureexec-builtins", -] - -[[package]] -name = "cmd-xargs" -version = "0.1.0" -dependencies = [ - "secureexec-shims", -] - -[[package]] -name = "cmd-xu" -version = "0.1.0" - -[[package]] -name = "cmd-yes" -version = "0.1.0" -dependencies = [ - "uu_yes", -] - -[[package]] -name = "cmd-yq" -version = "0.1.0" -dependencies = [ - "secureexec-yq", -] - -[[package]] -name = "codex-network-proxy" -version = "0.0.0" - -[[package]] -name = "codex-otel" -version = "0.0.0" - -[[package]] -name = "color-print" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aa954171903797d5623e047d9ab69d91b493657917bdfb8c2c80ecaf9cdb6f4" -dependencies = [ - "color-print-proc-macro", -] - -[[package]] -name = "color-print-proc-macro" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692186b5ebe54007e45a59aea47ece9eb4108e141326c304cdc91699a7118a22" -dependencies = [ - "nom 7.1.3", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "command-fds" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f849b92c694fe237ecd8fafd1ba0df7ae0d45c1df6daeb7f68ed4220d51640bd" -dependencies = [ - "nix 0.30.1", - "thiserror 2.0.18", -] - -[[package]] -name = "compact_str" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - -[[package]] -name = "compare" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120133d4db2ec47efe2e26502ee984747630c67f51974fca0b6c1340cf2368d3" - -[[package]] -name = "console" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" -dependencies = [ - "encode_unicode", - "libc", - "unicode-width 0.2.0", - "windows-sys 0.61.2", -] - -[[package]] -name = "const_format" -version = "0.2.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" -dependencies = [ - "const_format_proc_macros", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "constant_time_eq" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core_maths" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" -dependencies = [ - "libm", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc-fast" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" -dependencies = [ - "digest", - "spin", -] - -[[package]] -name = "crc32fast" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crossterm" -version = "0.28.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" -dependencies = [ - "bitflags 2.11.0", - "crossterm_winapi", - "mio", - "parking_lot", - "rustix 0.38.44", - "serde", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" -dependencies = [ - "bitflags 2.11.0", - "crossterm_winapi", - "derive_more", - "document-features", - "mio", - "parking_lot", - "rustix 1.1.4", - "serde", - "signal-hook", - "signal-hook-mio", - "winapi", -] - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" -dependencies = [ - "winapi", -] - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "ctrlc" -version = "3.5.2" - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core 0.20.11", - "darling_macro 0.20.11", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core 0.20.11", - "quote", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", - "quote", - "syn", -] - -[[package]] -name = "data-encoding" -version = "2.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" - -[[package]] -name = "data-encoding-macro" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8142a83c17aa9461d637e649271eae18bf2edd00e91f2e105df36c3c16355bdb" -dependencies = [ - "data-encoding", - "data-encoding-macro-internal", -] - -[[package]] -name = "data-encoding-macro-internal" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" -dependencies = [ - "data-encoding", - "syn", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "document-features" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" -dependencies = [ - "litrs", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "fancy-regex" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" -dependencies = [ - "bit-set", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "fancy-regex" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8" -dependencies = [ - "bit-set", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "fd-lock" -version = "4.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" -dependencies = [ - "cfg-if", - "rustix 1.1.4", - "windows-sys 0.59.0", -] - -[[package]] -name = "filetime" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" -dependencies = [ - "cfg-if", - "libc", - "libredox", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fixed_decimal" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35eabf480f94d69182677e37571d3be065822acfafd12f2f085db44fbbcc8e57" -dependencies = [ - "displaydoc", - "smallvec", - "writeable", -] - -[[package]] -name = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fluent" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477" -dependencies = [ - "fluent-bundle", - "unic-langid", -] - -[[package]] -name = "fluent-bundle" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" -dependencies = [ - "fluent-langneg", - "fluent-syntax", - "intl-memoizer", - "intl_pluralrules", - "rustc-hash", - "self_cell", - "smallvec", - "unic-langid", -] - -[[package]] -name = "fluent-langneg" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" -dependencies = [ - "unic-langid", -] - -[[package]] -name = "fluent-syntax" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" -dependencies = [ - "memchr", - "thiserror 2.0.18", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[package]] -name = "gcd" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[package]] -name = "git-version" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad568aa3db0fcbc81f2f116137f263d7304f512a1209b35b85150d3ef88ad19" -dependencies = [ - "git-version-macro", -] - -[[package]] -name = "git-version-macro" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "half" -version = "2.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" -dependencies = [ - "cfg-if", - "crunchy", - "zerocopy", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hifijson" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7763b98ba8a24f59e698bf9ab197e7676c640d6455d1580b4ce7dc560f0f0d" - -[[package]] -name = "homedir" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bdbbd5bc8c5749697ccaa352fa45aff8730cf21c68029c0eef1ffed7c3d6ba2" -dependencies = [ - "cfg-if", - "nix 0.29.0", - "widestring", - "windows 0.57.0", -] - -[[package]] -name = "hostname" -version = "0.4.2" - -[[package]] -name = "human-panic" -version = "2.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "075e8747af11abcff07d55d98297c9c6c70eb5d6365b25e7b12f02e484935191" -dependencies = [ - "anstream 0.6.21", - "anstyle", - "backtrace", - "serde", - "serde_derive", - "sysinfo", - "toml 0.9.12+spec-1.1.0", - "uuid", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_calendar" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e" -dependencies = [ - "calendrical_calculations", - "displaydoc", - "icu_calendar_data", - "icu_locale", - "icu_locale_core", - "icu_provider", - "ixdtf", - "serde", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_calendar_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d" - -[[package]] -name = "icu_collator" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32eed11a5572f1088b63fa21dc2e70d4a865e5739fc2d10abc05be93bae97019" -dependencies = [ - "icu_collator_data", - "icu_collections", - "icu_locale", - "icu_locale_core", - "icu_normalizer", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "zerovec", -] - -[[package]] -name = "icu_collator_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab06f0e83a613efddba3e4913e00e43ed4001fae651cb7d40fc7e66b83b6fb9" - -[[package]] -name = "icu_collections" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" -dependencies = [ - "displaydoc", - "potential_utf", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_datetime" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9d49f41ded8e63761b6b4c3120dfdc289415a1ed10107db6198eb311057ca5" -dependencies = [ - "displaydoc", - "fixed_decimal", - "icu_calendar", - "icu_datetime_data", - "icu_decimal", - "icu_locale", - "icu_locale_core", - "icu_pattern", - "icu_plurals", - "icu_provider", - "icu_time", - "potential_utf", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_datetime_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46597233625417b7c8052a63d916e4fdc73df21614ac0b679492a5d6e3b01aeb" - -[[package]] -name = "icu_decimal" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a38c52231bc348f9b982c1868a2af3195199623007ba2c7650f432038f5b3e8e" -dependencies = [ - "fixed_decimal", - "icu_decimal_data", - "icu_locale", - "icu_locale_core", - "icu_provider", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_decimal_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2905b4044eab2dd848fe84199f9195567b63ab3a93094711501363f63546fef7" - -[[package]] -name = "icu_locale" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_locale_data", - "icu_provider", - "potential_utf", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" -dependencies = [ - "displaydoc", - "litemap", - "serde", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locale_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c5f1d16b4c3a2642d3a719f18f6b06070ab0aef246a6418130c955ae08aa831" - -[[package]] -name = "icu_normalizer" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" - -[[package]] -name = "icu_pattern" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a7ff8c0ff6f61cdce299dcb54f557b0a251adbc78f6f0c35a21332c452b4a1b" -dependencies = [ - "displaydoc", - "either", - "serde", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_plurals" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f9cfe49f5b1d1163cc58db451562339916a9ca5cbcaae83924d41a0bf839474" -dependencies = [ - "fixed_decimal", - "icu_locale", - "icu_plurals_data", - "icu_provider", - "zerovec", -] - -[[package]] -name = "icu_plurals_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f018a98dccf7f0eb02ba06ac0ff67d102d8ded80734724305e924de304e12ff0" - -[[package]] -name = "icu_properties" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" - -[[package]] -name = "icu_provider" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" -dependencies = [ - "displaydoc", - "icu_locale_core", - "serde", - "stable_deref_trait", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_time" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8242b00da3b3b6678f731437a11c8833a43c821ae081eca60ba1b7579d45b6d8" -dependencies = [ - "calendrical_calculations", - "displaydoc", - "icu_calendar", - "icu_locale_core", - "icu_provider", - "icu_time_data", - "ixdtf", - "serde", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_time_data" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e10b0e5e87a2c84bd5fa407705732052edebe69291d347d0c3033785470edbf" - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "indenter" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" - -[[package]] -name = "indexmap" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" -dependencies = [ - "equivalent", - "hashbrown 0.16.1", - "serde", - "serde_core", -] - -[[package]] -name = "indicatif" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" -dependencies = [ - "console", - "portable-atomic", - "unicode-width 0.2.0", - "unit-prefix", - "web-time", -] - -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - -[[package]] -name = "infer" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc150e5ce2330295b8616ce0e3f53250e53af31759a9dbedad1621ba29151847" -dependencies = [ - "cfb", -] - -[[package]] -name = "inotify" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" -dependencies = [ - "bitflags 2.11.0", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - -[[package]] -name = "instability" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" -dependencies = [ - "darling 0.23.0", - "indoc", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "intl-memoizer" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" -dependencies = [ - "type-map", - "unic-langid", -] - -[[package]] -name = "intl_pluralrules" -version = "7.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" -dependencies = [ - "unic-langid", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" - -[[package]] -name = "ixdtf" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992" - -[[package]] -name = "jaq-core" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77526a72eb79412c29fd141767a6549bbfcb1cb40e00556fe16532d5e878e098" -dependencies = [ - "dyn-clone", - "once_cell", - "typed-arena", -] - -[[package]] -name = "jaq-json" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01dbdbd07b076e8403abac68ce7744d93e2ecd953bbc44bf77bf00e1e81172bc" -dependencies = [ - "foldhash 0.1.5", - "hifijson", - "indexmap", - "jaq-core", - "jaq-std", - "serde_json", -] - -[[package]] -name = "jaq-std" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c264fe397c981705976c71f1bfe020382b9eda52ae950e57fe885e147bdd67d" -dependencies = [ - "aho-corasick", - "base64", - "chrono", - "jaq-core", - "libm", - "log", - "regex-lite", - "urlencoding", -] - -[[package]] -name = "jiff" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" -dependencies = [ - "jiff-static", - "jiff-tzdb-platform", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", - "windows-sys 0.61.2", -] - -[[package]] -name = "jiff-icu" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e67c2beaae8b10a82d849b9aabb698a43a682f32b17bcdc035d5ecadb44d646" -dependencies = [ - "icu_calendar", - "icu_time", - "jiff", -] - -[[package]] -name = "jiff-static" -version = "0.2.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "jiff-tzdb" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" - -[[package]] -name = "jiff-tzdb-platform" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" -dependencies = [ - "jiff-tzdb", -] - -[[package]] -name = "js-sys" -version = "0.3.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "keccak" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" -dependencies = [ - "cpufeatures", -] - -[[package]] -name = "kqueue" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.183" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "libredox" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" -dependencies = [ - "bitflags 2.11.0", - "libc", - "plain", - "redox_syscall 0.7.3", -] - -[[package]] -name = "linux-raw-sys" -version = "0.4.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" - -[[package]] -name = "litrs" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "lru" -version = "0.16.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" -dependencies = [ - "hashbrown 0.16.1", -] - -[[package]] -name = "lscolors" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d60e266dfb1426eb2d24792602e041131fdc0236bb7007abc0e589acafd60929" -dependencies = [ - "aho-corasick", - "nu-ansi-term", -] - -[[package]] -name = "md-5" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" -dependencies = [ - "cfg-if", - "digest", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "memmap2" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" -dependencies = [ - "libc", -] - -[[package]] -name = "minimal-lexical" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", - "simd-adler32", -] - -[[package]] -name = "mio" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "nix" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" -dependencies = [ - "bitflags 2.11.0", - "cfg-if", - "cfg_aliases", - "libc", -] - -[[package]] -name = "nix" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" -dependencies = [ - "bitflags 2.11.0", - "cfg-if", - "cfg_aliases", - "libc", -] - -[[package]] -name = "nom" -version = "7.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" -dependencies = [ - "memchr", - "minimal-lexical", -] - -[[package]] -name = "nom" -version = "8.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" -dependencies = [ - "memchr", -] - -[[package]] -name = "normalize-path" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5438dd2b2ff4c6df6e1ce22d825ed2fa93ee2922235cc45186991717f0a892d" - -[[package]] -name = "notify" -version = "8.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" -dependencies = [ - "bitflags 2.11.0", - "fsevent-sys", - "inotify", - "kqueue", - "libc", - "log", - "mio", - "notify-types", - "walkdir", - "windows-sys 0.60.2", -] - -[[package]] -name = "notify-types" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "ntapi" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" -dependencies = [ - "winapi", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", - "rand 0.8.5", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-modular" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-prime" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b285c575532a33ef6fdd3a57640d0b1c70e6ca48644d6df7bbd4b7a0cfbbb12d" -dependencies = [ - "bitvec", - "either", - "lru 0.16.3", - "num-bigint", - "num-integer", - "num-modular", - "num-traits", - "rand 0.8.5", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "number_prefix" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" - -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "objc2-io-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" -dependencies = [ - "libc", - "objc2-core-foundation", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "os_display" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad5fd71b79026fb918650dde6d125000a233764f1c2f1659a1c71118e33ea08f" -dependencies = [ - "unicode-width 0.2.0", -] - -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link 0.2.1", -] - -[[package]] -name = "parse_datetime" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413775a7eac2261d2211a79d10ef275e5b6f7b527eec42ad09adce2ffa92b6e5" -dependencies = [ - "jiff", - "num-traits", - "winnow", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "peg" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9928cfca101b36ec5163e70049ee5368a8a1c3c6efc9ca9c5f9cc2f816152477" -dependencies = [ - "peg-macros", - "peg-runtime", -] - -[[package]] -name = "peg-macros" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6298ab04c202fa5b5d52ba03269fb7b74550b150323038878fe6c372d8280f71" -dependencies = [ - "peg-runtime", - "proc-macro2", - "quote", -] - -[[package]] -name = "peg-runtime" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca" - -[[package]] -name = "phf" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" -dependencies = [ - "phf_shared 0.11.3", -] - -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_shared 0.13.1", - "serde", -] - -[[package]] -name = "phf_codegen" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf_codegen" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" -dependencies = [ - "phf_generator 0.13.1", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_generator" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" -dependencies = [ - "phf_shared 0.11.3", - "rand 0.8.5", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared 0.13.1", -] - -[[package]] -name = "phf_shared" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" -dependencies = [ - "siphasher", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - -[[package]] -name = "platform-info" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7539aeb3fdd8cb4f6a331307cf71a1039cee75e94e8a71725b9484f4a0d9451a" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "portable-atomic-util" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "potential_utf" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" -dependencies = [ - "serde_core", - "writeable", - "zerovec", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "procfs" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" -dependencies = [ - "bitflags 2.11.0", - "chrono", - "flate2", - "procfs-core", - "rustix 1.1.4", -] - -[[package]] -name = "procfs-core" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405" -dependencies = [ - "bitflags 2.11.0", - "chrono", - "hex", -] - -[[package]] -name = "quick-xml" -version = "0.37.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" -dependencies = [ - "memchr", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "ratatui" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" -dependencies = [ - "bitflags 2.11.0", - "cassowary", - "compact_str", - "crossterm 0.28.1", - "indoc", - "instability", - "itertools 0.13.0", - "lru 0.12.5", - "paste", - "strum 0.26.3", - "unicode-segmentation", - "unicode-truncate", - "unicode-width 0.2.0", -] - -[[package]] -name = "rayon" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "redox_syscall" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" -dependencies = [ - "bitflags 2.11.0", -] - -[[package]] -name = "reedline" -version = "0.43.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fcf9f5225169f49622af4027b6d775cf9b6313f9eba16ba9e5ebd9d319a1f0" -dependencies = [ - "chrono", - "crossterm 0.28.1", - "fd-lock", - "itertools 0.13.0", - "nu-ansi-term", - "serde", - "strip-ansi-escapes", - "strum 0.26.3", - "strum_macros 0.26.4", - "thiserror 2.0.18", - "unicode-segmentation", - "unicode-width 0.2.0", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-lite" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "rlimit" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7043b63bd0cd1aaa628e476b80e6d4023a3b50eb32789f2728908107bd0c793a" -dependencies = [ - "libc", -] - -[[package]] -name = "rpds" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e75f485e819d4d3015e6c0d55d02a4fd3db47c1993d9e603e0361fba2bffb34" -dependencies = [ - "archery", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.11.0", - "errno", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.11.0", - "errno", - "libc", - "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "secureexec-awk" -version = "0.1.0" -dependencies = [ - "awk-rs", -] - -[[package]] -name = "secureexec-builtins" -version = "0.1.0" -dependencies = [ - "libc", - "wasi-ext", -] - -[[package]] -name = "secureexec-column" -version = "0.1.0" - -[[package]] -name = "secureexec-diff" -version = "0.1.0" -dependencies = [ - "similar", -] - -[[package]] -name = "secureexec-du" -version = "0.1.0" - -[[package]] -name = "secureexec-expr" -version = "0.1.0" -dependencies = [ - "regex", -] - -[[package]] -name = "secureexec-fd" -version = "0.1.0" -dependencies = [ - "regex", -] - -[[package]] -name = "secureexec-file-cmd" -version = "0.1.0" -dependencies = [ - "infer", -] - -[[package]] -name = "secureexec-find" -version = "0.1.0" -dependencies = [ - "regex", -] - -[[package]] -name = "secureexec-git" -version = "0.1.0" -dependencies = [ - "flate2", - "secureexec-wasi-http", - "sha1", -] - -[[package]] -name = "secureexec-grep" -version = "0.1.0" -dependencies = [ - "regex", -] - -[[package]] -name = "secureexec-gzip" -version = "0.1.0" -dependencies = [ - "flate2", -] - -[[package]] -name = "secureexec-jq" -version = "0.1.0" -dependencies = [ - "jaq-core", - "jaq-json", - "jaq-std", - "serde_json", -] - -[[package]] -name = "secureexec-rev" -version = "0.1.0" - -[[package]] -name = "secureexec-shims" -version = "0.1.0" -dependencies = [ - "wasi-ext", -] - -[[package]] -name = "secureexec-strings-cmd" -version = "0.1.0" - -[[package]] -name = "secureexec-stubs" -version = "0.1.0" - -[[package]] -name = "secureexec-tar" -version = "0.1.0" -dependencies = [ - "flate2", - "tar", -] - -[[package]] -name = "secureexec-tree" -version = "0.1.0" - -[[package]] -name = "secureexec-wasi-http" -version = "0.1.0" -dependencies = [ - "wasi-ext", -] - -[[package]] -name = "secureexec-wasi-pty" -version = "0.1.0" -dependencies = [ - "secureexec-wasi-spawn", - "wasi-ext", -] - -[[package]] -name = "secureexec-wasi-spawn" -version = "0.1.0" -dependencies = [ - "wasi", - "wasi-ext", -] - -[[package]] -name = "secureexec-yq" -version = "0.1.0" -dependencies = [ - "jaq-core", - "jaq-json", - "jaq-std", - "quick-xml", - "serde_json", - "serde_yaml", - "toml 0.8.23", -] - -[[package]] -name = "sed" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a00cdbffacbccf02decb7577da5e298c091bdee492a2a1b87a3d8b0cc5b7c5" -dependencies = [ - "clap", - "fancy-regex 0.17.0", - "memchr", - "memmap2", - "once_cell", - "phf 0.13.1", - "phf_codegen 0.13.1", - "regex", - "tempfile", - "uucore 0.7.0", -] - -[[package]] -name = "self_cell" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" - -[[package]] -name = "semver" -version = "1.0.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" -dependencies = [ - "serde_core", -] - -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha3" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" -dependencies = [ - "digest", - "keccak", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - -[[package]] -name = "signal-hook-mio" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" -dependencies = [ - "libc", - "mio", - "signal-hook", -] - -[[package]] -name = "signal-hook-registry" -version = "1.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" -dependencies = [ - "errno", - "libc", -] - -[[package]] -name = "simd-adler32" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" - -[[package]] -name = "similar" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" - -[[package]] -name = "siphasher" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "sm3" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebb9a3b702d0a7e33bc4d85a14456633d2b165c2ad839c5fd9a8417c1ab15860" -dependencies = [ - "digest", -] - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "spin" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "string-interner" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23de088478b31c349c9ba67816fa55d9355232d63c3afea8bf513e31f0f1d2c0" -dependencies = [ - "hashbrown 0.15.5", - "serde", -] - -[[package]] -name = "strip-ansi-escapes" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" -dependencies = [ - "vte", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros 0.26.4", -] - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "sysinfo" -version = "0.37.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" -dependencies = [ - "libc", - "memchr", - "ntapi", - "objc2-core-foundation", - "objc2-io-kit", - "windows 0.61.3", -] - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tar" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" -dependencies = [ - "filetime", - "libc", - "xattr", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix 1.1.4", - "windows-sys 0.61.2", -] - -[[package]] -name = "terminal_size" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" -dependencies = [ - "rustix 1.1.4", - "windows-sys 0.60.2", -] - -[[package]] -name = "terminfo" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" -dependencies = [ - "fnv", - "nom 7.1.3", - "phf 0.11.3", - "phf_codegen 0.11.3", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "tinystr" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" -dependencies = [ - "displaydoc", - "serde_core", - "zerovec", -] - -[[package]] -name = "tokio" -version = "1.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "signal-hook-registry", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit", -] - -[[package]] -name = "toml" -version = "0.9.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" -dependencies = [ - "serde_core", - "serde_spanned 1.0.4", - "toml_datetime 0.7.5+spec-1.1.0", - "toml_writer", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "0.7.5+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_write", - "winnow", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "toml_writer" -version = "1.0.6+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "triomphe" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" - -[[package]] -name = "type-map" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" -dependencies = [ - "rustc-hash", -] - -[[package]] -name = "typed-arena" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" - -[[package]] -name = "typenum" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" - -[[package]] -name = "unic-langid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" -dependencies = [ - "unic-langid-impl", -] - -[[package]] -name = "unic-langid-impl" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" -dependencies = [ - "tinystr", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" - -[[package]] -name = "unicode-truncate" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" -dependencies = [ - "itertools 0.13.0", - "unicode-segmentation", - "unicode-width 0.1.14", -] - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-width" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "unit-prefix" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8-chars" -version = "3.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe49e006d6df172d7f14794568a90fe41e05a1fa9e03dc276fa6da4bb747ec3" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "uu_arch" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f789358d2df8eb5933daa0d6ae53b79334269360d33cdd7079032d554a7e982c" -dependencies = [ - "clap", - "fluent", - "platform-info", - "uucore 0.7.0", -] - -[[package]] -name = "uu_b2sum" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "272b257659f523df2788d94866f95ea196188975a8b267a9ca0d566292a3a772" -dependencies = [ - "clap", - "fluent", - "uu_checksum_common", - "uucore 0.7.0", -] - -[[package]] -name = "uu_base32" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1db368d94af37dcd4d9b91d0f57f8ad2088af770be12b0114829b230762c4274" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_base64" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4eef6f62da9204fd7be919be88ef0c8271e22483415bf284a5efc0f173366" -dependencies = [ - "clap", - "fluent", - "uu_base32", - "uucore 0.7.0", -] - -[[package]] -name = "uu_basename" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdedbc10b8ef9c97415bfa80b1f25f3775420080dd59341bd39505fbbe797c48" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_basenc" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06a5c56ce70e8783820401ed2ec4468a2a235f58fb2d90f5c163f14e7a94eacc" -dependencies = [ - "clap", - "fluent", - "uu_base32", - "uucore 0.7.0", -] - -[[package]] -name = "uu_cat" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "043b7b9873244298be72a26057d1d85a0129497817c304ff7d1a2a1a0b27b335" -dependencies = [ - "clap", - "fluent", - "memchr", - "nix 0.30.1", - "thiserror 2.0.18", - "uucore 0.7.0", - "winapi-util", - "windows-sys 0.61.2", -] - -[[package]] -name = "uu_checksum_common" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "485e68c04134bfda144c9c79073e190586a105713b45971692be83775a9ef5e8" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_chmod" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3a5d93a175bf8993f2e0437228e7b736658f10d642dd7476397259cc6ed6af3" -dependencies = [ - "clap", - "fluent", - "thiserror 2.0.18", - "uucore 0.7.0", -] - -[[package]] -name = "uu_cksum" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30ecef1133f3970c22aad791c34d6fd70d24f10d25082ff8fa4493d4158ae1a6" -dependencies = [ - "clap", - "fluent", - "uu_checksum_common", - "uucore 0.7.0", -] - -[[package]] -name = "uu_comm" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cf70eb4d7bc82aa7ce6be80087e85d9dfa2d431e1cad2ba3080dd362d2bf717" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_cp" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffd199017f3fcfc2d266fb9d76385d510665393c400163b7c9cfd6f622e313b6" -dependencies = [ - "clap", - "filetime", - "fluent", - "indicatif", - "libc", - "nix 0.30.1", - "thiserror 2.0.18", - "uucore 0.7.0", - "walkdir", -] - -[[package]] -name = "uu_cut" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba96ac3631f81c4beef269d8eaba79b9bf2f72d97bdbd6ddadbf0e787327c63a" -dependencies = [ - "bstr", - "clap", - "fluent", - "memchr", - "uucore 0.7.0", -] - -[[package]] -name = "uu_date" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f41472a2321637ae96a126708210fc4c3603b1684e810af244df4a736f86515" -dependencies = [ - "clap", - "fluent", - "icu_calendar", - "icu_locale", - "jiff", - "jiff-icu", - "nix 0.30.1", - "parse_datetime", - "regex", - "uucore 0.7.0", - "windows-sys 0.61.2", -] - -[[package]] -name = "uu_dd" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e14a3ee96607b62ca4d65807be7ec1c2b5782b36d22fe7998307a0d08b85d94" -dependencies = [ - "clap", - "fluent", - "gcd", - "libc", - "nix 0.30.1", - "thiserror 2.0.18", - "uucore 0.7.0", -] - -[[package]] -name = "uu_dircolors" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391283cfef2dd209cffd699ba5ea7bd84cdba817105acd66bb020262ca59660a" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_dirname" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8954430cc6fc6d426905eea23c4aa13af3eb5260311ff712d698e208546b831c" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_echo" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77f4d0beb31487fe8f4b9202ffc118b9561dda1341e3dd6d3bb52991bfbd58d" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_expand" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca8a49c55b0396ae0a5326704130e9706fc09acd6180d795bff1154b60db708" -dependencies = [ - "clap", - "fluent", - "thiserror 2.0.18", - "unicode-width 0.2.0", - "uucore 0.7.0", -] - -[[package]] -name = "uu_factor" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520da1c9c94bb354ac1ee753eb8fb4f81d0512ec8c2405d21a6ff6f07c2fe1b3" -dependencies = [ - "clap", - "fluent", - "num-bigint", - "num-prime", - "num-traits", - "uucore 0.7.0", -] - -[[package]] -name = "uu_false" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a963cd94c29de3678273df72ca60857f6ffe6af5a3ede672cb4bc79a11ef22" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_fmt" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e133acad1273c60086d190ac76c4caaf6f694b4e2b85276124307d9e639b442c" -dependencies = [ - "clap", - "fluent", - "thiserror 2.0.18", - "unicode-width 0.2.0", - "uucore 0.7.0", -] - -[[package]] -name = "uu_fold" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8098e158f38287fe1144c3e7337856c6ac6e6f45c9924e75592d45e78dc467e6" -dependencies = [ - "clap", - "fluent", - "unicode-width 0.2.0", - "uucore 0.7.0", -] - -[[package]] -name = "uu_head" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d886e39ddc1a462479b0cd6fcdc7b1b2d2d086db53d65149c1cfe69b74b6baa" -dependencies = [ - "clap", - "fluent", - "memchr", - "thiserror 2.0.18", - "uucore 0.7.0", -] - -[[package]] -name = "uu_join" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ae9afe4a6a602d1fb3752f63a55bea0957403b0fc3c244184ba6f9c1a65932" -dependencies = [ - "clap", - "fluent", - "memchr", - "thiserror 2.0.18", - "uucore 0.7.0", -] - -[[package]] -name = "uu_link" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64a7c36d0be50e550760b5d6d10a4735d8f5ca94fc8a32f5e0ca33af3fbc7e3a" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_ln" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99cb878e1b6b498aeb55704caa2f764ac0dd61347fd3efc6eff0bda6bf0b9536" -dependencies = [ - "clap", - "fluent", - "thiserror 2.0.18", - "uucore 0.7.0", -] - -[[package]] -name = "uu_logname" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8dd72043a0b5032741234a0854bf1ac01b8f7a47d7a20cae6e9649d803d408" -dependencies = [ - "clap", - "fluent", - "libc", - "uucore 0.7.0", -] - -[[package]] -name = "uu_ls" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dc54d6f54ed818aca41490fa92eaf094b5e6ff613257245f8d9c6fe1b0a2224" -dependencies = [ - "ansi-width", - "clap", - "fluent", - "glob", - "hostname", - "lscolors", - "rustc-hash", - "terminal_size", - "thiserror 2.0.18", - "uucore 0.7.0", - "uutils_term_grid", -] - -[[package]] -name = "uu_md5sum" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8003075b73ee23011de0618d2f84da765c37bd0be34e301aeb70dd7f0996bd" -dependencies = [ - "clap", - "fluent", - "uu_checksum_common", - "uucore 0.7.0", -] - -[[package]] -name = "uu_mkdir" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8cdba0fed9c1c12d0a0c954e447cc75a50b466b001f1a814921ee702fafff7a" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_mktemp" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b7af3550fa4c926d8fde66932421d904cbe1102a7031947b456ad0f90f81569" -dependencies = [ - "clap", - "fluent", - "rand 0.9.2", - "tempfile", - "thiserror 2.0.18", - "uucore 0.7.0", -] - -[[package]] -name = "uu_mv" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f678a9ce279116c4d1aa3dc460be904069336adc63d9130acdb543568bee22a6" -dependencies = [ - "clap", - "fluent", - "fs_extra", - "indicatif", - "libc", - "rustc-hash", - "thiserror 2.0.18", - "uucore 0.7.0", - "windows-sys 0.61.2", -] - -[[package]] -name = "uu_nl" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "823a2102789b4fc5227ae76c15f8b57df0534a9aff1ac19bf1961303bf663d48" -dependencies = [ - "clap", - "fluent", - "itoa", - "regex", - "uucore 0.7.0", -] - -[[package]] -name = "uu_nproc" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cceeca12036b18857d4c773373419185ffa057b32b0c87a447a1861a06c5ddc3" -dependencies = [ - "clap", - "fluent", - "libc", - "uucore 0.7.0", -] - -[[package]] -name = "uu_numfmt" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac350590aecae7aa234270b48380013188786969fe0c17824b542d3ce904baf" -dependencies = [ - "clap", - "fluent", - "memchr", - "thiserror 2.0.18", - "uucore 0.7.0", -] - -[[package]] -name = "uu_od" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5b9c0c5345041b6734f89e7642995f479c1740c7c91075f0602e042ec55b268" -dependencies = [ - "byteorder", - "clap", - "fluent", - "half", - "libc", - "uucore 0.7.0", -] - -[[package]] -name = "uu_paste" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc4e66bb50e347e68b76aa3703064ba1953af2ca21a67bcee15d5a6e1f9aa4c" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_pathchk" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "013e2313eafe2d854e271c00b2a7822042e03f8d52493dd7ea70260c1eca37c4" -dependencies = [ - "clap", - "fluent", - "libc", - "uucore 0.7.0", -] - -[[package]] -name = "uu_printenv" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c733e8c3c4ff851d86733b2c79968a9b30ae1b2453d904b357d0a0588f4c5c68" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_printf" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2079ccf2689495f99ffd0aafdcaf81753d73b710fbd80884487f71715635bf53" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_ptx" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "268dc2ca3624228b9929fa60c843d54affa9024586e7f553a1484d029f6e9d99" -dependencies = [ - "clap", - "fluent", - "regex", - "thiserror 2.0.18", - "uucore 0.7.0", -] - -[[package]] -name = "uu_pwd" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58b6e26cf64171f8825fccbade68bf03de15924955cff36c5d9a2574a6ceecd3" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_readlink" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "695c0287310f05d6dfedae9a0f62139742ad19b3f6d57e2d21ab93fe56ca374d" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_realpath" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65bb98033c204f1b6a985564b4d58eaa35b0e5855c996642823c21b6f1448c9" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_rm" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb65631fc1a8e8834bd30ac4ad8ee337c6d5a46c15254dfdbdfaea96f7290fa1" -dependencies = [ - "clap", - "fluent", - "indicatif", - "libc", - "thiserror 2.0.18", - "uucore 0.7.0", - "windows-sys 0.61.2", -] - -[[package]] -name = "uu_rmdir" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7462a7b03e28881b5c8129dc576524fe42fcb7e8de9a30e16bf90f25b64a095f" -dependencies = [ - "clap", - "fluent", - "libc", - "uucore 0.7.0", -] - -[[package]] -name = "uu_seq" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "689227681b5be62ece851a5b3d28b8d175feb18624a4b24d1bc4c4ce9c760ea4" -dependencies = [ - "bigdecimal", - "clap", - "fluent", - "num-bigint", - "num-traits", - "thiserror 2.0.18", - "uucore 0.7.0", -] - -[[package]] -name = "uu_sha1sum" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "426f471bcdf58779e0125a5231421330982533d354ac13291677a8a3a5d9b765" -dependencies = [ - "clap", - "fluent", - "uu_checksum_common", - "uucore 0.7.0", -] - -[[package]] -name = "uu_sha224sum" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b353936ed3b67b8654169161a2bc84ba29c72bedbd95dc504a23f69016fa9ea1" -dependencies = [ - "clap", - "fluent", - "uu_checksum_common", - "uucore 0.7.0", -] - -[[package]] -name = "uu_sha256sum" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071ec05bd12da342a70e1d094b5b210453e5e028041058722db6769d06d5719a" -dependencies = [ - "clap", - "fluent", - "uu_checksum_common", - "uucore 0.7.0", -] - -[[package]] -name = "uu_sha384sum" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfc23178584a12c7cd7266c77c9a187ebfff4fd761738d9c838203bb17020f1f" -dependencies = [ - "clap", - "fluent", - "uu_checksum_common", - "uucore 0.7.0", -] - -[[package]] -name = "uu_sha512sum" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c7719ee5dfdd58be739b87add1dfe8026114303b83b3ae24fb099824ae5fda" -dependencies = [ - "clap", - "fluent", - "uu_checksum_common", - "uucore 0.7.0", -] - -[[package]] -name = "uu_shred" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33090888bfc315a756f028ba16251ed34dca810e708435eae54a5064e3d9f3fb" -dependencies = [ - "clap", - "fluent", - "libc", - "rand 0.9.2", - "uucore 0.7.0", -] - -[[package]] -name = "uu_shuf" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c2e5663694fde672961884c94c8afd3cd5859969878594eda6a25059621cd97" -dependencies = [ - "clap", - "fluent", - "itoa", - "rand 0.9.2", - "rand_chacha 0.9.0", - "rustc-hash", - "sha3", - "uucore 0.7.0", -] - -[[package]] -name = "uu_sort" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb61d4619d85c291b9fa5c4cf057c8da764efe0541f4ff888c8f2e603294a965" -dependencies = [ - "bigdecimal", - "binary-heap-plus", - "clap", - "compare", - "ctrlc", - "fluent", - "foldhash 0.2.0", - "itertools 0.14.0", - "memchr", - "nix 0.30.1", - "rand 0.9.2", - "rayon", - "self_cell", - "tempfile", - "thiserror 2.0.18", - "uucore 0.7.0", -] - -[[package]] -name = "uu_split" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7514e1430433a581b7317685a90858d34fdbdc726d778ae00a0a9541123eda76" -dependencies = [ - "clap", - "fluent", - "memchr", - "thiserror 2.0.18", - "uucore 0.7.0", -] - -[[package]] -name = "uu_stat" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef4ee0943facf79c267e2ff7fe8869f15b76d9ce4709d2c8117db788bc851c6" -dependencies = [ - "clap", - "fluent", - "thiserror 2.0.18", - "uucore 0.7.0", -] - -[[package]] -name = "uu_sum" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31a443c458befede734a5e1cd76768bef727d568d4290c651a82c6fc8217f201" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_tac" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0075181c425cfdf7b2e3400cdfa90d419aa9f4723163210b94d85e2ea47d25" -dependencies = [ - "clap", - "fluent", - "libc", - "memchr", - "memmap2", - "regex", - "tempfile", - "thiserror 2.0.18", - "uucore 0.7.0", -] - -[[package]] -name = "uu_tail" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88cb366f24aecb51aa9b03c2ae8b40538aa5081351822a6dfec883f95cc744d3" -dependencies = [ - "clap", - "fluent", - "libc", - "memchr", - "nix 0.30.1", - "notify", - "same-file", - "uucore 0.7.0", - "windows-sys 0.61.2", -] - -[[package]] -name = "uu_tee" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a8d33dd843a8e5fb37018390edf904678767a995308514bd3f64fe7d01bb4f" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_touch" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f1f61215638f2b85f03008f94bdb26b5071579ee8b4ab1d0ff74c3e12e3acd" -dependencies = [ - "clap", - "filetime", - "fluent", - "jiff", - "nix 0.30.1", - "parse_datetime", - "thiserror 2.0.18", - "uucore 0.7.0", - "windows-sys 0.61.2", -] - -[[package]] -name = "uu_tr" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3028c44f9e14787a07e96d7365f645d18ab060126515a67857150f57bb96c45" -dependencies = [ - "bytecount", - "clap", - "fluent", - "nom 8.0.0", - "uucore 0.7.0", -] - -[[package]] -name = "uu_true" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfec486c633bf98e354373f7d0ec26910e51376ddfeea29b2fd8a2d2f3d96736" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_truncate" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5790fe8bc07089c2e38e2965afa16c0beca87c6df7c655251e15a5050c980eb" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_tsort" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29fc5c3773a819f847867c5c964ddfa7fd48b372b00490f611aa0629722d7ad2" -dependencies = [ - "clap", - "fluent", - "nix 0.30.1", - "rustc-hash", - "string-interner", - "thiserror 2.0.18", - "uucore 0.7.0", -] - -[[package]] -name = "uu_uname" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8650f019d2addbb56c316bd53058d5a0165e573e2ae46636df9f36e1719b3280" -dependencies = [ - "clap", - "fluent", - "platform-info", - "uucore 0.7.0", -] - -[[package]] -name = "uu_unexpand" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24f324b11a5f7d27f0f0cceb255fc755748b83dba844bd0ff4c6ddbbbc93c3d0" -dependencies = [ - "clap", - "fluent", - "thiserror 2.0.18", - "uucore 0.7.0", -] - -[[package]] -name = "uu_uniq" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6c3864b9912a51bab19445351f8def44fb30ccf1a87be02d68ae185c264b217" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_unlink" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05bd002b6736800fd985546043667a22264781526dcbd56a623a237f9391a91a" -dependencies = [ - "clap", - "fluent", - "uucore 0.7.0", -] - -[[package]] -name = "uu_wc" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d606da8f0184e8450502430bb3434419b2b66d66d315d5d58cda3d2190af1764" -dependencies = [ - "bytecount", - "clap", - "fluent", - "libc", - "nix 0.30.1", - "thiserror 2.0.18", - "unicode-width 0.2.0", - "uucore 0.7.0", -] - -[[package]] -name = "uu_yes" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21740c358b89a807706ddb273570ac7853a25e59bf175d3d6f7e77f67d1efdc1" -dependencies = [ - "clap", - "fluent", - "itertools 0.14.0", - "uucore 0.7.0", -] - -[[package]] -name = "uucore" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2003164a38a7f39da1de103a70fa66b745f572f0045ec261481539516c0a8a0e" -dependencies = [ - "bigdecimal", - "bstr", - "clap", - "fluent", - "fluent-bundle", - "fluent-syntax", - "glob", - "icu_locale", - "itertools 0.14.0", - "nix 0.30.1", - "num-traits", - "number_prefix", - "os_display", - "phf 0.13.1", - "procfs", - "thiserror 2.0.18", - "unic-langid", - "uucore_procs 0.4.0", - "wild", -] - -[[package]] -name = "uucore" -version = "0.7.0" -dependencies = [ - "base64-simd", - "bigdecimal", - "blake2b_simd", - "blake3", - "bstr", - "clap", - "crc-fast", - "data-encoding", - "data-encoding-macro", - "digest", - "dunce", - "fluent", - "fluent-bundle", - "fluent-syntax", - "glob", - "hex", - "icu_calendar", - "icu_collator", - "icu_datetime", - "icu_decimal", - "icu_locale", - "icu_provider", - "itertools 0.14.0", - "jiff", - "jiff-icu", - "libc", - "md-5", - "memchr", - "nix 0.30.1", - "num-traits", - "os_display", - "procfs", - "rustc-hash", - "sha1", - "sha2", - "sha3", - "sm3", - "thiserror 2.0.18", - "unic-langid", - "unit-prefix", - "uucore_procs 0.7.0", - "walkdir", - "wild", - "winapi-util", - "windows-sys 0.61.2", - "xattr", - "z85", -] - -[[package]] -name = "uucore_procs" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c76f0308f7810d915246a39748e7f5d64e43e6bb9d6c8107224f9d741aefc375" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "uucore_procs" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f63e2d5083ff0983193a33e2d57fd271c7e3e3e7df8e46e8f471865647b2cbc" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "uuid" -version = "1.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" -dependencies = [ - "getrandom 0.4.2", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "uutils_term_grid" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcba141ce511bad08e80b43f02976571072e1ff4286f7d628943efbd277c6361" -dependencies = [ - "ansi-width", -] - -[[package]] -name = "uzers" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8275fb1afee25b4111d2dc8b5c505dbbc4afd0b990cb96deb2d88bff8be18d" -dependencies = [ - "libc", - "log", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - -[[package]] -name = "vte" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" -dependencies = [ - "memchr", -] - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasi-ext" -version = "0.1.0" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - -[[package]] -name = "wasm-bindgen" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.114" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags 2.11.0", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "web-sys" -version = "0.3.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "whoami" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" -dependencies = [ - "libredox", - "wasite", - "web-sys", -] - -[[package]] -name = "widestring" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" - -[[package]] -name = "wild" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3131afc8c575281e1e80f36ed6a092aa502c08b18ed7524e86fbbb12bb410e1" -dependencies = [ - "glob", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" -dependencies = [ - "windows-core 0.57.0", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" -dependencies = [ - "windows-implement 0.57.0", - "windows-interface 0.57.0", - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", -] - -[[package]] -name = "windows-implement" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.57.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link 0.2.1", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags 2.11.0", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" -dependencies = [ - "either", -] - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "xattr" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" -dependencies = [ - "libc", - "rustix 1.1.4", -] - -[[package]] -name = "yoke" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "z85" -version = "3.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e61e59a957b7ccee15d2049f86e8bfd6f66968fcd88f018950662d9b86e675" - -[[package]] -name = "zerocopy" -version = "0.8.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" -dependencies = [ - "serde", - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/registry/native/Cargo.toml b/registry/native/Cargo.toml deleted file mode 100644 index 4d567ae044..0000000000 --- a/registry/native/Cargo.toml +++ /dev/null @@ -1,168 +0,0 @@ -[workspace] -resolver = "2" -members = [ - "crates/wasi-ext", - "crates/libs/shims", - "crates/libs/builtins", - "crates/libs/stubs", - "crates/libs/grep", - "crates/libs/awk", - "crates/libs/jq", - "crates/libs/yq", - "crates/commands/grep", - "crates/commands/sh", - "crates/commands/sed", - "crates/commands/awk", - "crates/commands/jq", - "crates/commands/yq", - "crates/commands/rg", - "crates/libs/find", - "crates/libs/file-cmd", - "crates/libs/tree", - "crates/libs/du", - "crates/libs/column", - "crates/libs/rev", - "crates/libs/strings-cmd", - "crates/libs/expr", - "crates/libs/diff", - "crates/commands/find", - "crates/commands/file", - "crates/commands/tree", - "crates/commands/du", - "crates/commands/column", - "crates/commands/rev", - "crates/commands/strings", - "crates/commands/expr", - "crates/commands/diff", - "crates/libs/gzip", - "crates/libs/tar", - "crates/commands/gzip", - "crates/commands/tar", - "crates/commands/ls", - "crates/commands/cat", - "crates/commands/cp", - "crates/commands/mv", - "crates/commands/rm", - "crates/commands/mkdir", - "crates/commands/chmod", - "crates/commands/sort", - "crates/commands/head", - "crates/commands/tail", - "crates/commands/wc", - "crates/commands/cut", - "crates/commands/tr", - "crates/commands/echo", - "crates/commands/printf", - # US-009: remaining uutils coreutils - "crates/commands/dd", - "crates/commands/ln", - "crates/commands/logname", - "crates/commands/mktemp", - "crates/commands/pathchk", - "crates/commands/split", - "crates/commands/stat", - "crates/commands/tac", - "crates/commands/touch", - "crates/commands/tsort", - "crates/commands/base32", - "crates/commands/base64", - "crates/commands/basenc", - "crates/commands/basename", - "crates/commands/comm", - "crates/commands/dircolors", - "crates/commands/dirname", - "crates/commands/expand", - "crates/commands/factor", - "crates/commands/false", - "crates/commands/fmt", - "crates/commands/fold", - "crates/commands/join", - "crates/commands/nl", - "crates/commands/numfmt", - "crates/commands/od", - "crates/commands/paste", - "crates/commands/printenv", - "crates/commands/ptx", - "crates/commands/seq", - "crates/commands/shuf", - "crates/commands/true", - "crates/commands/unexpand", - "crates/commands/uniq", - "crates/commands/yes", - "crates/commands/b2sum", - "crates/commands/cksum", - "crates/commands/md5sum", - "crates/commands/sha1sum", - "crates/commands/sha224sum", - "crates/commands/sha256sum", - "crates/commands/sha384sum", - "crates/commands/sha512sum", - "crates/commands/sum", - "crates/commands/link", - "crates/commands/pwd", - "crates/commands/readlink", - "crates/commands/realpath", - "crates/commands/rmdir", - "crates/commands/shred", - "crates/commands/tee", - "crates/commands/truncate", - "crates/commands/unlink", - "crates/commands/arch", - "crates/commands/date", - "crates/commands/nproc", - "crates/commands/uname", - # US-010: builtins, shims, and stubs - "crates/commands/sleep", - "crates/commands/test", - "crates/commands/whoami", - "crates/commands/env", - "crates/commands/timeout", - "crates/commands/which", - "crates/commands/xu", - "crates/commands/xargs", - "crates/commands/nice", - "crates/commands/nohup", - "crates/commands/stdbuf", - "crates/commands/_stubs", - # US-079: fd-find compatible file finder - "crates/libs/fd", - "crates/commands/fd", - # US-099: codex (OpenAI Codex CLI) WASI build skeleton - "crates/commands/codex", - # US-100: WASI process spawning via host_process FFI - "crates/libs/wasi-spawn", - "crates/commands/spawn-test-host", - # US-101: WASI PTY-based process management via host_process FFI - "crates/libs/wasi-pty", - # US-102: WASI HTTP client via host_net TCP/TLS - "crates/libs/wasi-http", - "crates/commands/curl", - "crates/commands/http-test", - # US-104: codex-exec headless agent binary - "crates/commands/codex-exec", - # git: minimal git implementation - "crates/libs/git", - "crates/commands/git", - "stubs/codex-otel", - "stubs/ctrlc", - "stubs/hostname", -] - -[workspace.package] -version = "0.1.0" -edition = "2021" -license = "Apache-2.0" - -[profile.release] -opt-level = "z" -lto = true -codegen-units = 1 -strip = true - -# WASM-compatible stubs for crates that don't support wasm32-wasip1 -[patch.crates-io] -ctrlc = { path = "stubs/ctrlc" } -hostname = { path = "stubs/hostname" } -uucore = { path = "stubs/uucore" } -codex-network-proxy = { path = "stubs/codex-network-proxy" } -codex-otel = { path = "stubs/codex-otel" } diff --git a/registry/native/Makefile b/registry/native/Makefile deleted file mode 100644 index c8e814ffc4..0000000000 --- a/registry/native/Makefile +++ /dev/null @@ -1,326 +0,0 @@ -WASM_TARGET := wasm32-wasip1 -RELEASE_DIR := target/$(WASM_TARGET)/release - -# Standalone binary output directory (configurable) -COMMANDS_DIR ?= $(RELEASE_DIR)/commands - -# Detect patched std: if patches/ has .patch files and patch-std.sh exists, use patched sysroot -PATCHES := $(wildcard patches/*.patch) -PATCH_SCRIPT := scripts/patch-std.sh - -# Resolve WASI sysroot for CRT/libc needed by -Z build-std linking -# Uses rustc without +toolchain override so rust-toolchain.toml is respected -WASI_SYSROOT_SELF_CONTAINED := $(shell rustc --print sysroot)/lib/rustlib/$(WASM_TARGET)/lib/self-contained - -# Discover command binary names from crates/commands/. Keep known slow/heavy -# commands out of the default registry gate; they can still be built explicitly -# with `make cmd/`. -SKIPPED_BULK_COMMANDS := git codex codex-exec -COMMAND_NAMES := $(filter-out $(SKIPPED_BULK_COMMANDS),$(notdir $(wildcard crates/commands/*))) -# `_stubs` is the binary name but its Cargo package is `cmd-stubs`; every -# other command follows the `cmd-` convention. -COMMAND_PACKAGE_NAMES := $(patsubst _stubs,stubs,$(COMMAND_NAMES)) -COMMAND_PACKAGES := $(addprefix -p cmd-,$(COMMAND_PACKAGE_NAMES)) - -# Alias symlinks: link_name:target_binary -ALIAS_SYMLINKS := \ - egrep:grep fgrep:grep \ - gunzip:gzip zcat:gzip \ - bash:sh \ - dir:ls vdir:ls \ - more:cat \ - [:test - -# Stub commands: each gets a symlink to _stubs -STUB_COMMANDS := \ - chcon runcon \ - chgrp chown chroot \ - df \ - groups id \ - hostname hostid \ - install \ - kill \ - mkfifo mknod \ - pinky who users uptime \ - stty sync tty - -.PHONY: all commands wasm wasm-opt-check host test clean patch-std patch-check vendor patch-vendor vendor-patch-check size-report install c-wasm codex - -all: wasm - -# Build the fast command set that lands in COMMANDS_DIR: Rust commands except -# known heavy/external ones, then the opted-in fast C commands. Heavy commands -# (git, duckdb, vim) and the external codex build -# are intentionally excluded from the default registry-build gate. -commands: wasm - $(MAKE) -C c programs install - -# Build the real wasm32-wasip1 `codex-exec` agent engine from the codex fork -# (branch wasi-port-codex-core). The fork ships a committed, idempotent, self-prepping build script -# (it prepares the rustup sysroot for -Z build-std and restores it on exit — no manual stash), so -# this folds codex into the toolchain: `make -C registry/native codex` produces and installs -# registry/software/codex/wasm/{codex-exec,codex}. Override CODEX_REPO if the fork is not at the -# default sibling path. See the fork's docs/wasi-build.md for the toolchain constraints, and the -# remaining work to source the CRT from wasi-sdk so the prebuilt rust target need not be installed. -AGENTOS_ROOT := $(abspath $(CURDIR)/../..) -CODEX_REPO ?= $(abspath $(AGENTOS_ROOT)/../codex-rs/codex-rs) -codex: - @if [ -x "$(CODEX_REPO)/scripts/build-wasi-codex-exec.sh" ]; then \ - AGENTOS_DIR="$(AGENTOS_ROOT)" "$(CODEX_REPO)/scripts/build-wasi-codex-exec.sh"; \ - else \ - echo "codex: fork build script not found at $(CODEX_REPO)/scripts/build-wasi-codex-exec.sh — skipping."; \ - echo " Set CODEX_REPO=/path/to/codex-rs/codex-rs (the wasi-port-codex-core checkout) to build it."; \ - fi - -# Strict variant (fails if the fork is absent) for environments that require the codex artifact. -.PHONY: codex-required -codex-required: - @test -x "$(CODEX_REPO)/scripts/build-wasi-codex-exec.sh" || { \ - echo "ERROR: codex fork build script not found at $(CODEX_REPO); set CODEX_REPO"; exit 1; } - @$(MAKE) codex - -# Apply std patches if they exist -patch-std: -ifneq ($(PATCHES),) - @if [ -x "$(PATCH_SCRIPT)" ]; then \ - echo "Applying std patches..."; \ - ./$(PATCH_SCRIPT); \ - else \ - echo "Warning: patches found but $(PATCH_SCRIPT) not executable or missing"; \ - fi -else - @echo "No patches found, using stock std" -endif - -# Resolve Rust std source for vendoring build-std deps -RUST_STD_SRC := $(shell rustc --print sysroot)/lib/rustlib/src/rust - -# Vendor dependencies from crates.io into vendor/ (includes std deps for -Z build-std) -vendor: - @if [ ! -d vendor ]; then \ - echo "Vendoring dependencies (including std library deps)..."; \ - mkdir -p .cargo; \ - cargo vendor \ - --sync "$(RUST_STD_SRC)/library/std/Cargo.toml" \ - --sync "$(RUST_STD_SRC)/library/test/Cargo.toml" \ - vendor > .cargo/config.toml; \ - else \ - echo "vendor/ exists — skipping (run 'make clean-vendor' to re-vendor)"; \ - fi - @# Source-replacement config so cargo builds from the patched vendor/ tree - @# instead of the crates.io registry cache. This file is gitignored, so a - @# fresh checkout (CI) has none; without it every patches/crates/*.patch - @# wasi fix is silently ignored and crates like uu_tac fail to compile for - @# wasm32-wasip1. `cargo vendor` prints the exact config (including the - @# synced std-dep sources) on stdout, so capture it above. - @test -s .cargo/config.toml || { \ - echo "ERROR: .cargo/config.toml missing or empty after vendor"; \ - echo "Run 'make clean-vendor && make vendor' to regenerate."; \ - exit 1; \ - } - -# Apply crate-level patches to vendored sources -patch-vendor: vendor - @if [ -x scripts/patch-vendor.sh ]; then \ - ./scripts/patch-vendor.sh; \ - else \ - echo "Warning: scripts/patch-vendor.sh not found or not executable"; \ - fi - -# Dry-run verification of crate patches -vendor-patch-check: vendor - @if [ -x scripts/patch-vendor.sh ]; then \ - ./scripts/patch-vendor.sh --check; \ - else \ - echo "Warning: scripts/patch-vendor.sh not found or not executable"; \ - fi - -# Ensure wasm-opt is installed (needed for post-build optimization) -wasm-opt-check: - @if ! command -v wasm-opt >/dev/null 2>&1; then \ - echo "wasm-opt not found — installing via cargo..."; \ - cargo install wasm-opt; \ - else \ - echo "wasm-opt found: $$(wasm-opt --version)"; \ - fi - -# Build all standalone command binaries, optimize, strip .wasm extension, create symlinks -wasm: vendor patch-vendor patch-std wasm-opt-check - # --cfg tokio_unstable lifts tokio's wasm `compile_error!` so the patched - # tokio process/net modules (patches/crates/tokio) compile on wasm32-wasip1. - # Needed by codex (tokio::process/net). Harmless for commands not using tokio. - RUSTFLAGS="-C link-arg=-L$(WASI_SYSROOT_SELF_CONTAINED) --cfg tokio_unstable" \ - cargo build --target $(WASM_TARGET) \ - -Z build-std=std,panic_abort \ - --release \ - $(COMMAND_PACKAGES) - @mkdir -p $(COMMANDS_DIR) - @TOTAL=0; SKIPPED=0; \ - HAS_OPT=$$(command -v wasm-opt >/dev/null 2>&1 && echo 1 || echo 0); \ - for cmd in $(COMMAND_NAMES); do \ - wasm="$(RELEASE_DIR)/$$cmd.wasm"; \ - if [ -f "$$wasm" ]; then \ - TOTAL=$$((TOTAL + 1)); \ - if [ "$$HAS_OPT" = "1" ]; then \ - wasm-opt -O3 --strip-debug --all-features "$$wasm" -o "$(COMMANDS_DIR)/$$cmd"; \ - else \ - cp "$$wasm" "$(COMMANDS_DIR)/$$cmd"; \ - fi; \ - else \ - SKIPPED=$$((SKIPPED + 1)); \ - echo "Warning: $$wasm not found — skipping $$cmd"; \ - fi; \ - done; \ - echo ""; \ - echo "=== Standalone Binary Build Report ==="; \ - echo "Binaries: $$TOTAL built"; \ - if [ $$SKIPPED -gt 0 ]; then \ - echo "Skipped: $$SKIPPED (binary not found)"; \ - fi; \ - if [ "$$HAS_OPT" = "1" ]; then \ - echo "Optimized: wasm-opt -O3 --strip-debug"; \ - else \ - echo "Optimized: none (wasm-opt not found)"; \ - fi - @# Create alias symlinks (fall back to copy on platforms without symlink support) - @cd $(COMMANDS_DIR) && \ - for pair in $(ALIAS_SYMLINKS); do \ - link=$${pair%%:*}; target=$${pair##*:}; \ - if [ -f "$$target" ]; then \ - ln -sf "$$target" "$$link" 2>/dev/null || cp "$$target" "$$link"; \ - fi; \ - done - @# Create stub command symlinks - @cd $(COMMANDS_DIR) && \ - for stub in $(STUB_COMMANDS); do \ - if [ -f _stubs ]; then \ - ln -sf _stubs "$$stub" 2>/dev/null || cp _stubs "$$stub"; \ - fi; \ - done - @TOTAL_FILES=$$(ls -1 $(COMMANDS_DIR) 2>/dev/null | wc -l); \ - echo "Total: $$TOTAL_FILES entries in $(COMMANDS_DIR)"; \ - echo "=== Build complete ===" - -# --------------------------------------------------------------------------- -# Uniform per-binary entry point: `make cmd/` builds ONE command binary -# into $(COMMANDS_DIR), dispatching to the toolchain that owns it: -# Rust crates/commands/ (cargo package cmd-, via wasm-cmd) -# C the C opt-in set below (registry/native/c, patched sysroot) -# codex the codex fork build (codex, codex-exec) -# external hand-built drop-zone binaries with no source pipeline yet -# `just registry-native-cmd ` calls this. `make wasm` builds the fast -# Rust set; `make -C c programs install` builds/installs the fast C set. -C_COMMANDS := zip unzip envsubst sqlite3 curl wget duckdb vim http_get -EXTERNAL_COMMANDS := vix - -.PHONY: cmd/% -cmd/%: - @name="$*"; \ - if [ -d "crates/commands/$$name" ]; then \ - $(MAKE) wasm-cmd CMD=$$name; \ - elif echo " $(C_COMMANDS) " | grep -q " $$name "; then \ - src=$$name; [ "$$name" = "sqlite3" ] && src=sqlite3_cli; \ - $(MAKE) -C c sysroot "build/$$src" && $(MAKE) -C c install COMMANDS="$$name"; \ - elif [ "$$name" = "codex" ] || [ "$$name" = "codex-exec" ]; then \ - $(MAKE) codex-required; \ - elif echo " $(EXTERNAL_COMMANDS) " | grep -q " $$name "; then \ - if [ -f "$(COMMANDS_DIR)/$$name" ]; then \ - echo "cmd/$$name: external hand-built binary present at $(COMMANDS_DIR)/$$name"; \ - else \ - echo "ERROR: '$$name' has no source pipeline yet — place the hand-built wasm binary at $(COMMANDS_DIR)/$$name (see registry/README.md)"; \ - exit 1; \ - fi; \ - else \ - echo "ERROR: unknown command '$$name' (no crates/commands/$$name; not in C_COMMANDS, codex, or EXTERNAL_COMMANDS)"; \ - exit 1; \ - fi - -# Build ONE command binary (cargo package cmd-$(CMD) -> $(COMMANDS_DIR)/$(CMD)). -# Usage: make wasm-cmd CMD=sh -.PHONY: wasm-cmd -wasm-cmd: vendor patch-vendor patch-std wasm-opt-check - @test -n "$(CMD)" || { echo "ERROR: pass CMD= (a dir under crates/commands/)"; exit 1; } - @test -d "crates/commands/$(CMD)" || { echo "ERROR: no crates/commands/$(CMD)"; exit 1; } - RUSTFLAGS="-C link-arg=-L$(WASI_SYSROOT_SELF_CONTAINED) --cfg tokio_unstable" \ - cargo build --target $(WASM_TARGET) \ - -Z build-std=std,panic_abort \ - --release \ - -p cmd-$(CMD) - @mkdir -p $(COMMANDS_DIR) - @if command -v wasm-opt >/dev/null 2>&1; then \ - wasm-opt -O3 --strip-debug --all-features "$(RELEASE_DIR)/$(CMD).wasm" -o "$(COMMANDS_DIR)/$(CMD)"; \ - else \ - cp "$(RELEASE_DIR)/$(CMD).wasm" "$(COMMANDS_DIR)/$(CMD)"; \ - fi - @echo "Built $(COMMANDS_DIR)/$(CMD)" - -# Show per-command binary sizes in COMMANDS_DIR -size-report: - @if [ ! -d "$(COMMANDS_DIR)" ]; then \ - echo "Error: $(COMMANDS_DIR) not found. Run 'make wasm' first."; \ - exit 1; \ - fi - @echo "=== Standalone Binary Size Report ===" - @echo "" - @printf "%-10s %s\n" "SIZE" "COMMAND" - @printf "%-10s %s\n" "----" "-------" - @ls -lhS $(COMMANDS_DIR)/* 2>/dev/null | \ - grep -v "^total" | \ - while read line; do \ - size=$$(echo "$$line" | awk '{print $$5}'); \ - name=$$(echo "$$line" | awk '{print $$NF}'); \ - base=$$(basename "$$name"); \ - link=""; \ - if [ -L "$$name" ]; then \ - target=$$(readlink "$$name"); \ - link=" -> $$target"; \ - fi; \ - printf "%-10s %s%s\n" "$$size" "$$base" "$$link"; \ - done - @echo "" - @TOTAL_SIZE=$$(du -sh $(COMMANDS_DIR) 2>/dev/null | awk '{print $$1}'); \ - TOTAL_FILES=$$(ls -1 $(COMMANDS_DIR) 2>/dev/null | wc -l); \ - REAL_FILES=$$(find $(COMMANDS_DIR) -maxdepth 1 -type f 2>/dev/null | wc -l); \ - SYMLINKS=$$(find $(COMMANDS_DIR) -maxdepth 1 -type l 2>/dev/null | wc -l); \ - echo "Total: $$TOTAL_SIZE ($$TOTAL_FILES entries: $$REAL_FILES binaries, $$SYMLINKS symlinks)" - @echo "" - @echo "=== Per-Crate Size Contribution (rlib, top 30) ===" - @echo "Note: rlib sizes are pre-LTO; actual contribution after LTO/DCE may differ." - @echo "" - @printf "%-10s %s\n" "SIZE" "CRATE" - @printf "%-10s %s\n" "----" "-----" - @ls -lhS $(RELEASE_DIR)/deps/*.rlib 2>/dev/null | \ - awk '{split($$NF, a, "/"); name=a[length(a)]; gsub(/^lib/, "", name); gsub(/-[0-9a-f]+\.rlib$$/, "", name); printf "%-10s %s\n", $$5, name}' | \ - awk '!seen[$$2]++' | \ - head -30 - @echo "" - @echo "=== Dependency Count ===" - @TOTAL=$$(ls $(RELEASE_DIR)/deps/*.rlib 2>/dev/null | \ - sed 's/.*\/lib//' | sed 's/-[0-9a-f]*\.rlib//' | sort -u | wc -l); \ - echo "Unique crates: $$TOTAL" - -# Install standalone binaries to a custom directory - -# Build C programs to WASM (downloads wasi-sdk if needed) - - -test: - cargo test --workspace - -clean: - cargo clean - rm -rf host/dist - -clean-vendor: - rm -rf vendor .cargo/config.toml - -patch-check: -ifneq ($(PATCHES),) - @if [ -x "$(PATCH_SCRIPT)" ]; then \ - ./$(PATCH_SCRIPT) --check; \ - else \ - echo "Warning: $(PATCH_SCRIPT) not found or not executable"; \ - fi -else - @echo "No patches found" -endif diff --git a/registry/native/c/Makefile b/registry/native/c/Makefile deleted file mode 100644 index 29209a762f..0000000000 --- a/registry/native/c/Makefile +++ /dev/null @@ -1,717 +0,0 @@ -# wasmvm/c/Makefile — Build C programs to standalone WASM binaries -# -# Targets: -# wasi-sdk Download and cache wasi-sdk toolchain -# programs Compile all .c files in programs/ to WASM binaries -# sysroot Build patched wasi-libc sysroot (invokes patch-wasi-libc.sh) -# install Copy opted-in real commands to COMMANDS_DIR -# native Compile all .c files to native binaries (for parity testing) -# clean Remove build output -# -# The vanilla (unpatched) wasi-sdk sysroot is used by default. -# After US-025 adds patch-wasi-libc.sh, `make sysroot` builds a patched one. - -# wasi-sdk version and platform detection -WASI_SDK_VERSION := 25 -WASI_SDK_MAJOR := 25.0 - -UNAME_S := $(shell uname -s) -UNAME_M := $(shell uname -m) - -ifeq ($(UNAME_S),Linux) - WASI_SDK_PLATFORM := linux -else ifeq ($(UNAME_S),Darwin) - WASI_SDK_PLATFORM := macos -else - WASI_SDK_PLATFORM := linux -endif - -ifeq ($(UNAME_M),arm64) - WASI_SDK_ARCH := arm64 -else ifeq ($(UNAME_M),aarch64) - WASI_SDK_ARCH := arm64 -else - WASI_SDK_ARCH := x86_64 -endif - -WASI_SDK_TARBALL := wasi-sdk-$(WASI_SDK_MAJOR)-$(WASI_SDK_ARCH)-$(WASI_SDK_PLATFORM).tar.gz -WASI_SDK_URL := https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$(WASI_SDK_VERSION)/$(WASI_SDK_TARBALL) - -# Directories -VENDOR_DIR := vendor -WASI_SDK_DIR := $(VENDOR_DIR)/wasi-sdk -BUILD_DIR := build -NATIVE_DIR := build/native - -# Toolchain -CC := $(WASI_SDK_DIR)/bin/clang -NATIVE_CC := $(shell command -v cc 2>/dev/null || command -v gcc 2>/dev/null || command -v clang 2>/dev/null) - -# Sysroot: use patched sysroot if built, otherwise use wasi-sdk's vanilla sysroot -PATCHED_SYSROOT := sysroot -ifeq ($(wildcard $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a),) - SYSROOT := $(WASI_SDK_DIR)/share/wasi-sysroot -else - SYSROOT := $(PATCHED_SYSROOT) -endif - -# Compile flags -WASM_CFLAGS := --target=wasm32-wasip1 --sysroot=$(SYSROOT) -O2 -flto -I include/ -NATIVE_CFLAGS := -O0 -g -D_LARGEFILE64_SOURCE -I include/ - -# COMMANDS_DIR for install target (configurable, matches Rust binary output) -COMMANDS_DIR ?= ../target/wasm32-wasip1/release/commands - -# Fast real commands installed by the default registry gate. Slow/heavy commands -# (duckdb, vim) remain available through explicit parent `make cmd/`. -COMMANDS := zip unzip envsubst sqlite3 curl http_get - -# Programs requiring patched sysroot (Tier 2+ custom host imports) -PATCHED_PROGRAMS := http_get_test isatty_test getpid_test getppid_test getppid_verify userinfo pipe_test dup_test spawn_child spawn_exit_code pipeline kill_child waitpid_return waitpid_edge syscall_coverage getpwuid_test signal_tests sigaction_self sigaction_behavior delayed_tcp_echo delayed_kill pipe_edge tcp_accept_spawn tcp_echo tcp_server http_server udp_echo unix_socket signal_handler http_get dns_lookup sqlite3_cli curl wget fs_probe - -# Discover all .c source files in programs/ -ALL_SOURCES := $(wildcard programs/*.c) -SKIPPED_BULK_PROGRAMS := wget - -# Exclude patched-sysroot programs when only vanilla sysroot is available -ifeq ($(wildcard $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a),) - EXCLUDED := $(addsuffix .c,$(addprefix programs/,$(PATCHED_PROGRAMS))) - SOURCES := $(filter-out $(EXCLUDED) $(addsuffix .c,$(addprefix programs/,$(SKIPPED_BULK_PROGRAMS))),$(ALL_SOURCES)) -else - SOURCES := $(filter-out $(addsuffix .c,$(addprefix programs/,$(SKIPPED_BULK_PROGRAMS))),$(ALL_SOURCES)) -endif - -# Program names (without .c extension) -ifeq ($(wildcard $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a),) - CUSTOM_WASM_PROG_NAMES := -else - CUSTOM_WASM_PROG_NAMES := curl -endif - -WASM_PROG_NAMES := $(sort $(basename $(notdir $(SOURCES))) $(CUSTOM_WASM_PROG_NAMES)) -NATIVE_PROG_NAMES := $(basename $(notdir $(SOURCES))) -PROGRAM_REPORT_NAMES := $(WASM_PROG_NAMES) - -# Default WASM output targets -EXTRA_WASM_OUTPUTS := -WASM_OUTPUTS := $(addprefix $(BUILD_DIR)/,$(WASM_PROG_NAMES)) $(EXTRA_WASM_OUTPUTS) -# Native output targets -NATIVE_OUTPUTS := $(addprefix $(NATIVE_DIR)/,$(NATIVE_PROG_NAMES)) - -.PHONY: all wasi-sdk programs sysroot install native clean wasm-opt-check os-test os-test-native - -HAS_WASM_OPT := $(shell command -v wasm-opt >/dev/null 2>&1 && echo 1 || echo 0) - -all: programs - -# --- wasi-sdk download and cache --- - -wasi-sdk: $(WASI_SDK_DIR)/bin/clang - -$(WASI_SDK_DIR)/bin/clang: - @echo "=== Downloading wasi-sdk $(WASI_SDK_MAJOR) ===" - @mkdir -p $(VENDOR_DIR) - @if command -v curl >/dev/null 2>&1; then \ - curl -fSL "$(WASI_SDK_URL)" -o "$(VENDOR_DIR)/$(WASI_SDK_TARBALL)"; \ - elif command -v wget >/dev/null 2>&1; then \ - wget -q "$(WASI_SDK_URL)" -O "$(VENDOR_DIR)/$(WASI_SDK_TARBALL)"; \ - else \ - echo "Error: neither curl nor wget found"; exit 1; \ - fi - @echo "Extracting..." - @tar -xzf "$(VENDOR_DIR)/$(WASI_SDK_TARBALL)" -C "$(VENDOR_DIR)" - @mv "$(VENDOR_DIR)/wasi-sdk-$(WASI_SDK_MAJOR)-$(WASI_SDK_ARCH)-$(WASI_SDK_PLATFORM)" "$(WASI_SDK_DIR)" - @rm -f "$(VENDOR_DIR)/$(WASI_SDK_TARBALL)" - @echo "wasi-sdk $(WASI_SDK_MAJOR) installed to $(WASI_SDK_DIR)" - -# --- C library dependencies (downloaded at build time) --- -# Unmodified upstream libs are fetched from their official sources. -# Modified libs (curl with WASI patches) are fetched from rivet-dev forks. -# All downloads cached in libs/ — add libs/ to .gitignore. - -SQLITE3_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3470200.zip -ZLIB_URL := https://github.com/madler/zlib/archive/refs/tags/v1.3.1.zip -CJSON_URL := https://github.com/DaveGamble/cJSON/archive/refs/tags/v1.7.18.zip -CURL_COMMIT := main -CURL_FORK_REPO_PREFIX := secure -CURL_FORK_REPO_SUFFIX := exec-curl -CURL_FORK_REPO := $(CURL_FORK_REPO_PREFIX)-$(CURL_FORK_REPO_SUFFIX) -CURL_URL := https://github.com/rivet-dev/$(CURL_FORK_REPO)/archive/refs/heads/$(CURL_COMMIT).zip -CURL_TOOL_VERSION := 8_11_1 -CURL_TOOL_URL := https://github.com/curl/curl/archive/refs/tags/curl-$(CURL_TOOL_VERSION).tar.gz -CURL_RELEASE_VERSION := 8.11.1 -CURL_RELEASE_TAG := 8_11_1 -CURL_RELEASE_URL := https://github.com/curl/curl/releases/download/curl-$(CURL_RELEASE_TAG)/curl-$(CURL_RELEASE_VERSION).tar.xz -CURL_UPSTREAM_BUILD_DIR := $(BUILD_DIR)/curl-upstream -CURL_UPSTREAM_OVERLAY_DIR := curl-upstream-overlay -CURL_UPSTREAM_OVERLAY_FILES := $(wildcard $(CURL_UPSTREAM_OVERLAY_DIR)/lib/*.c $(CURL_UPSTREAM_OVERLAY_DIR)/lib/*.h $(CURL_UPSTREAM_OVERLAY_DIR)/lib/vtls/*.c $(CURL_UPSTREAM_OVERLAY_DIR)/lib/vtls/*.h) -MINIZIP_URL := https://github.com/madler/zlib/archive/refs/tags/v1.3.1.zip -# duckdb-wasm currently documents DuckDB v1.5.0 in its README. We build the -# matching upstream DuckDB tag ourselves with our patched WASI/POSIX sysroot. -DUCKDB_VERSION := v1.5.0 -DUCKDB_GIT_DESCRIBE := $(DUCKDB_VERSION)-0-g0123456789 -DUCKDB_URL := https://github.com/duckdb/duckdb/archive/refs/tags/$(DUCKDB_VERSION).tar.gz - -LIBS_DIR := libs -LIBS_CACHE := .cache/libs - -libs/sqlite3/sqlite3.c: - @echo "Fetching sqlite3..." - @mkdir -p $(LIBS_CACHE) $(LIBS_DIR)/sqlite3 - @curl -fSL "$(SQLITE3_URL)" -o "$(LIBS_CACHE)/sqlite3.zip" - @cd $(LIBS_CACHE) && unzip -qo sqlite3.zip - @cp $(LIBS_CACHE)/sqlite-amalgamation-*/sqlite3.c $(LIBS_CACHE)/sqlite-amalgamation-*/sqlite3.h $(LIBS_DIR)/sqlite3/ - -libs/zlib/zutil.c: - @echo "Fetching zlib..." - @mkdir -p $(LIBS_CACHE) $(LIBS_DIR)/zlib - @curl -fSL "$(ZLIB_URL)" -o "$(LIBS_CACHE)/zlib.zip" - @cd $(LIBS_CACHE) && unzip -qo zlib.zip - @cp $(LIBS_CACHE)/zlib-*/*.c $(LIBS_CACHE)/zlib-*/*.h $(LIBS_DIR)/zlib/ - -# The fetch recipe above materializes the complete archive. Declare the other -# files as outputs that depend on its representative target so a clean `make -# programs` can construct its dependency graph before the archive exists. -ZLIB_FETCHED_SRCS := \ - libs/zlib/adler32.c libs/zlib/compress.c libs/zlib/crc32.c \ - libs/zlib/deflate.c libs/zlib/infback.c libs/zlib/inffast.c \ - libs/zlib/inflate.c libs/zlib/inftrees.c libs/zlib/trees.c \ - libs/zlib/uncompr.c -$(ZLIB_FETCHED_SRCS): libs/zlib/zutil.c - @test -f "$@" || { echo "Error: zlib fetch did not produce $@"; exit 1; } - -libs/minizip/ioapi.c: - @echo "Fetching minizip (from zlib contrib)..." - @mkdir -p $(LIBS_CACHE) $(LIBS_DIR)/minizip - @if [ ! -f "$(LIBS_CACHE)/zlib.zip" ]; then \ - curl -fSL "$(MINIZIP_URL)" -o "$(LIBS_CACHE)/zlib.zip"; \ - cd $(LIBS_CACHE) && unzip -qo zlib.zip; \ - fi - @cp $(LIBS_CACHE)/zlib-*/contrib/minizip/ioapi.c $(LIBS_CACHE)/zlib-*/contrib/minizip/ioapi.h \ - $(LIBS_CACHE)/zlib-*/contrib/minizip/zip.c $(LIBS_CACHE)/zlib-*/contrib/minizip/zip.h \ - $(LIBS_CACHE)/zlib-*/contrib/minizip/unzip.c $(LIBS_CACHE)/zlib-*/contrib/minizip/unzip.h \ - $(LIBS_CACHE)/zlib-*/contrib/minizip/crypt.h \ - $(LIBS_DIR)/minizip/ - -MINIZIP_FETCHED_SRCS := libs/minizip/zip.c libs/minizip/unzip.c -$(MINIZIP_FETCHED_SRCS): libs/minizip/ioapi.c - @test -f "$@" || { echo "Error: minizip fetch did not produce $@"; exit 1; } - -libs/cjson/cJSON.c: - @echo "Fetching cJSON..." - @mkdir -p $(LIBS_CACHE) $(LIBS_DIR)/cjson - @curl -fSL "$(CJSON_URL)" -o "$(LIBS_CACHE)/cjson.zip" - @cd $(LIBS_CACHE) && unzip -qo cjson.zip - @cp $(LIBS_CACHE)/cJSON-*/cJSON.c $(LIBS_CACHE)/cJSON-*/cJSON.h $(LIBS_DIR)/cjson/ - -libs/curl/lib/easy.c: - @echo "Fetching curl (rivet-dev/$(CURL_FORK_REPO))..." - @mkdir -p $(LIBS_CACHE) - @curl -fSL "$(CURL_URL)" -o "$(LIBS_CACHE)/curl.zip" - @cd $(LIBS_CACHE) && unzip -qo curl.zip - @rm -rf $(LIBS_DIR)/curl - @mv $(LIBS_CACHE)/$(CURL_FORK_REPO)-* $(LIBS_DIR)/curl - -libs/duckdb/CMakeLists.txt: - @echo "Fetching DuckDB ($(DUCKDB_VERSION))..." - @mkdir -p $(LIBS_CACHE) - @curl -fSL "$(DUCKDB_URL)" -o "$(LIBS_CACHE)/duckdb.tar.gz" - @rm -rf $(LIBS_DIR)/duckdb - @mkdir -p $(LIBS_DIR)/duckdb - @tar -xzf "$(LIBS_CACHE)/duckdb.tar.gz" --strip-components=1 -C $(LIBS_DIR)/duckdb - -.PHONY: fetch-libs fetch-fast-libs clean-libs -fetch-libs: libs/sqlite3/sqlite3.c libs/zlib/zutil.c libs/minizip/ioapi.c libs/cjson/cJSON.c libs/curl/lib/easy.c libs/duckdb/CMakeLists.txt -fetch-fast-libs: libs/sqlite3/sqlite3.c libs/zlib/zutil.c libs/minizip/ioapi.c libs/cjson/cJSON.c libs/curl/lib/easy.c - -clean-libs: - rm -rf $(LIBS_DIR) $(LIBS_CACHE) - -# --- os-test POSIX conformance suite (downloaded at build time) --- -# ISC-licensed test suite from the Sortix project (https://sortix.org/os-test/) - -OS_TEST_VERSION := main -OS_TEST_URL := https://gitlab.com/sortix/os-test/-/archive/$(OS_TEST_VERSION)/os-test-$(OS_TEST_VERSION).tar.gz -OS_TEST_DIR := os-test - -.PHONY: fetch-os-test clean-os-test - -fetch-os-test: $(OS_TEST_DIR)/include -$(OS_TEST_DIR)/include: - @echo "Fetching os-test $(OS_TEST_VERSION)..." - @mkdir -p $(LIBS_CACHE) - @curl -fSL "$(OS_TEST_URL)" -o "$(LIBS_CACHE)/os-test.tar.gz" - @mkdir -p $(OS_TEST_DIR) - @tar -xzf "$(LIBS_CACHE)/os-test.tar.gz" --strip-components=1 -C $(OS_TEST_DIR) - @echo "os-test $(OS_TEST_VERSION) extracted to $(OS_TEST_DIR)/" - @echo " include/ : $$(find $(OS_TEST_DIR)/include -name '*.h' 2>/dev/null | wc -l) headers" - @echo " src/ : $$(find $(OS_TEST_DIR)/src -name '*.c' 2>/dev/null | wc -l) test programs" - -clean-os-test: - rm -rf $(OS_TEST_DIR) $(LIBS_CACHE)/os-test.tar.gz - -# --- os-test POSIX conformance build targets --- -# Compile every os-test C file to WASM and native binaries. -# Individual compile failures are expected (missing headers, unsupported features) -# and do not abort the build. - -OS_TEST_BUILD := $(BUILD_DIR)/os-test -OS_TEST_NATIVE_BUILD := $(NATIVE_DIR)/os-test -OS_TEST_WASM_CFLAGS := --target=wasm32-wasip1 --sysroot=$(SYSROOT) -O0 \ - -D_GNU_SOURCE -D_BSD_SOURCE -D_ALL_SOURCE -D_DEFAULT_SOURCE -OS_TEST_WASM_LDFLAGS := -lc-printscan-long-double -# Stub main() for namespace/ tests (compile-only header conformance checks) -OS_TEST_NS_MAIN := os-test-overrides/namespace_main.c -OS_TEST_NATIVE_CFLAGS := -O0 -g \ - -D_GNU_SOURCE -D_BSD_SOURCE -D_ALL_SOURCE -D_DEFAULT_SOURCE -OS_TEST_NATIVE_LDFLAGS := -lm -ifeq ($(UNAME_S),Linux) - OS_TEST_NATIVE_LDFLAGS += -lpthread -lrt -endif -ifeq ($(UNAME_S),Darwin) - OS_TEST_NATIVE_LDFLAGS += -lpthread -endif - -.PHONY: os-test os-test-native - -os-test: wasi-sdk fetch-os-test - @total=0; pass=0; fail=0; \ - for src in $$(find $(OS_TEST_DIR) -name '*.c' -not -path '*/.expect/*' -not -path '*/misc/*' | sort); do \ - rel=$${src#$(OS_TEST_DIR)/}; \ - name=$${rel%.c}; \ - out=$(OS_TEST_BUILD)/$$name; \ - mkdir -p "$$(dirname "$$out")"; \ - total=$$((total + 1)); \ - extras=""; \ - case "$$rel" in namespace/*) extras="$(OS_TEST_NS_MAIN)";; esac; \ - if $(CC) $(OS_TEST_WASM_CFLAGS) -o "$$out" "$$src" $$extras $(OS_TEST_WASM_LDFLAGS) 2>/dev/null; then \ - pass=$$((pass + 1)); \ - else \ - echo "COMPILE FAILED: $$name"; \ - rm -f "$$out"; \ - fail=$$((fail + 1)); \ - fi; \ - done; \ - echo ""; \ - echo "=== os-test WASM Build Report ==="; \ - echo "Total: $$total"; \ - echo "Compiled: $$pass"; \ - echo "Failed: $$fail"; \ - echo "Output: $(OS_TEST_BUILD)/"; \ - echo "=== Build complete ==="; \ - if [ $$fail -gt 0 ]; then echo "ERROR: $$fail tests failed to compile" >&2; exit 1; fi - -os-test-native: fetch-os-test - @total=0; pass=0; fail=0; \ - for src in $$(find $(OS_TEST_DIR) -name '*.c' -not -path '*/.expect/*' -not -path '*/misc/*' | sort); do \ - rel=$${src#$(OS_TEST_DIR)/}; \ - out=$(OS_TEST_NATIVE_BUILD)/$${rel%.c}; \ - mkdir -p "$$(dirname "$$out")"; \ - total=$$((total + 1)); \ - extras=""; \ - case "$$rel" in namespace/*) extras="$(OS_TEST_NS_MAIN)";; esac; \ - if $(NATIVE_CC) $(OS_TEST_NATIVE_CFLAGS) -o "$$out" "$$src" $$extras $(OS_TEST_NATIVE_LDFLAGS) 2>/dev/null; then \ - pass=$$((pass + 1)); \ - else \ - rm -f "$$out"; \ - fail=$$((fail + 1)); \ - fi; \ - done; \ - echo ""; \ - echo "=== os-test Native Build Report ==="; \ - echo "Total: $$total"; \ - echo "Compiled: $$pass"; \ - echo "Failed: $$fail"; \ - echo "Output: $(OS_TEST_NATIVE_BUILD)/"; \ - echo "=== Build complete ===" - -# --- musl libc-test conformance suite (downloaded at build time) --- -# MIT-licensed test suite from musl (Bytecode Alliance mirror) - -LIBC_TEST_VERSION := master -LIBC_TEST_URL := https://github.com/bytecodealliance/libc-test/archive/refs/heads/$(LIBC_TEST_VERSION).tar.gz -LIBC_TEST_DIR := libc-test - -.PHONY: fetch-libc-test clean-libc-test - -fetch-libc-test: $(LIBC_TEST_DIR)/src -$(LIBC_TEST_DIR)/src: - @echo "Fetching musl libc-test $(LIBC_TEST_VERSION)..." - @mkdir -p $(LIBS_CACHE) - @curl -fSL "$(LIBC_TEST_URL)" -o "$(LIBS_CACHE)/libc-test.tar.gz" - @mkdir -p $(LIBC_TEST_DIR) - @tar -xzf "$(LIBS_CACHE)/libc-test.tar.gz" --strip-components=1 -C $(LIBC_TEST_DIR) - @echo "libc-test $(LIBC_TEST_VERSION) extracted to $(LIBC_TEST_DIR)/" - @echo " src/ : $$(find $(LIBC_TEST_DIR)/src -name '*.c' 2>/dev/null | wc -l) test programs" - -clean-libc-test: - rm -rf $(LIBC_TEST_DIR) $(LIBS_CACHE)/libc-test.tar.gz - -# --- musl libc-test build targets --- -# Compile functional/ and regression/ tests to WASM and native binaries. -# Tests requiring fork/exec/signals/pthreads will fail to compile for WASM — that's expected. -# The test.h framework (src/common/) is compiled into a static library first. - -LIBC_TEST_BUILD := $(BUILD_DIR)/libc-test -LIBC_TEST_NATIVE_BUILD := $(NATIVE_DIR)/libc-test -LIBC_TEST_WASM_CFLAGS := --target=wasm32-wasip1 --sysroot=$(SYSROOT) -O0 \ - -D_GNU_SOURCE -D_BSD_SOURCE -D_ALL_SOURCE -D_DEFAULT_SOURCE \ - -I$(LIBC_TEST_DIR)/src/common -LIBC_TEST_WASM_LDFLAGS := -lc-printscan-long-double -lm -LIBC_TEST_NATIVE_CFLAGS := -O0 -g \ - -D_GNU_SOURCE -D_BSD_SOURCE -D_ALL_SOURCE -D_DEFAULT_SOURCE \ - -I$(LIBC_TEST_DIR)/src/common -LIBC_TEST_NATIVE_LDFLAGS := -lm -ifeq ($(UNAME_S),Linux) - LIBC_TEST_NATIVE_LDFLAGS += -lpthread -lrt -lcrypt -lresolv -lutil -ldl -endif -ifeq ($(UNAME_S),Darwin) - LIBC_TEST_NATIVE_LDFLAGS += -lpthread -endif - -# Test framework sources compiled alongside each test -LIBC_TEST_COMMON_SRCS := $(LIBC_TEST_DIR)/src/common/print.c - -.PHONY: libc-test libc-test-native - -libc-test: wasi-sdk fetch-libc-test - @total=0; pass=0; fail=0; \ - for src in $$(find $(LIBC_TEST_DIR)/src/functional $(LIBC_TEST_DIR)/src/regression -name '*.c' 2>/dev/null | sort); do \ - rel=$${src#$(LIBC_TEST_DIR)/src/}; \ - name=$${rel%.c}; \ - out=$(LIBC_TEST_BUILD)/$$name; \ - mkdir -p "$$(dirname "$$out")"; \ - total=$$((total + 1)); \ - if $(CC) $(LIBC_TEST_WASM_CFLAGS) -o "$$out" "$$src" $(LIBC_TEST_COMMON_SRCS) $(LIBC_TEST_WASM_LDFLAGS) 2>/dev/null; then \ - pass=$$((pass + 1)); \ - else \ - rm -f "$$out"; \ - fail=$$((fail + 1)); \ - fi; \ - done; \ - echo ""; \ - echo "=== libc-test WASM Build Report ==="; \ - echo "Total: $$total"; \ - echo "Compiled: $$pass"; \ - echo "Failed: $$fail"; \ - echo "Output: $(LIBC_TEST_BUILD)/"; \ - echo "=== Build complete ===" - -libc-test-native: fetch-libc-test - @total=0; pass=0; fail=0; \ - for src in $$(find $(LIBC_TEST_DIR)/src/functional $(LIBC_TEST_DIR)/src/regression -name '*.c' 2>/dev/null | sort); do \ - rel=$${src#$(LIBC_TEST_DIR)/src/}; \ - out=$(LIBC_TEST_NATIVE_BUILD)/$${rel%.c}; \ - mkdir -p "$$(dirname "$$out")"; \ - total=$$((total + 1)); \ - if $(NATIVE_CC) $(LIBC_TEST_NATIVE_CFLAGS) -o "$$out" "$$src" $(LIBC_TEST_COMMON_SRCS) $(LIBC_TEST_NATIVE_LDFLAGS) 2>/dev/null; then \ - pass=$$((pass + 1)); \ - else \ - rm -f "$$out"; \ - fail=$$((fail + 1)); \ - fi; \ - done; \ - echo ""; \ - echo "=== libc-test Native Build Report ==="; \ - echo "Total: $$total"; \ - echo "Compiled: $$pass"; \ - echo "Failed: $$fail"; \ - echo "Output: $(LIBC_TEST_NATIVE_BUILD)/"; \ - echo "=== Build complete ===" - -# --- Patched sysroot (delegates to patch-wasi-libc.sh) --- - -WASI_LIBC_PATCHES := $(wildcard ../patches/wasi-libc/*.patch) -WASI_LIBC_OVERRIDES := $(wildcard ../patches/wasi-libc-overrides/*.c) -LLVM_RUNTIME_PATCHES := $(wildcard patches/llvm-project/*.patch) - -$(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a: wasi-sdk ../scripts/patch-wasi-libc.sh scripts/build-llvm-runtimes.sh $(WASI_LIBC_PATCHES) $(WASI_LIBC_OVERRIDES) $(LLVM_RUNTIME_PATCHES) - ../scripts/patch-wasi-libc.sh - -sysroot: $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a - -# --- wasm-opt check --- - -wasm-opt-check: - @if [ "$(HAS_WASM_OPT)" = "1" ]; then \ - echo "wasm-opt found: $$(wasm-opt --version | head -1)"; \ - else \ - echo "Warning: wasm-opt (binaryen) is not installed; skipping WASM optimization."; \ - fi - -# --- Compile all C programs to WASM --- - -programs: wasi-sdk fetch-fast-libs wasm-opt-check $(WASM_OUTPUTS) - @echo "" - @echo "=== C WASM Build Report ===" - @echo "Programs: $(words $(PROGRAM_REPORT_NAMES)) compiled" - @echo "Sysroot: $(SYSROOT)" - @echo "Output: $(BUILD_DIR)/" - @echo "=== Build complete ===" - -# Default rule: single-file programs -$(BUILD_DIR)/%: programs/%.c $(WASI_SDK_DIR)/bin/clang - @mkdir -p $(BUILD_DIR) - $(CC) $(WASM_CFLAGS) -o $@.wasm $< - @if [ "$(HAS_WASM_OPT)" = "1" ]; then \ - wasm-opt -O3 --strip-debug $@.wasm -o $@; \ - else \ - cp $@.wasm $@; \ - fi - @rm -f $@.wasm - -# --- Per-program override rules for multi-file compilation --- -# Add overrides here for programs that link vendored libraries. - -# json_parse: links cJSON library -$(BUILD_DIR)/json_parse: programs/json_parse.c libs/cjson/cJSON.c $(WASI_SDK_DIR)/bin/clang - @mkdir -p $(BUILD_DIR) - $(CC) $(WASM_CFLAGS) -Ilibs/cjson -o $@.wasm programs/json_parse.c libs/cjson/cJSON.c - @if [ "$(HAS_WASM_OPT)" = "1" ]; then \ - wasm-opt -O3 --strip-debug $@.wasm -o $@; \ - else \ - cp $@.wasm $@; \ - fi - @rm -f $@.wasm - -$(NATIVE_DIR)/json_parse: programs/json_parse.c libs/cjson/cJSON.c - @mkdir -p $(NATIVE_DIR) - $(NATIVE_CC) $(NATIVE_CFLAGS) -Ilibs/cjson -o $@ programs/json_parse.c libs/cjson/cJSON.c - -# sqlite3_mem: links SQLite amalgamation -SQLITE_COMMON := -DSQLITE_OMIT_WAL -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_THREADSAFE=0 \ - -DSQLITE_OMIT_LOCALTIME -SQLITE_WASM := -DSQLITE_OS_OTHER $(SQLITE_COMMON) -lwasi-emulated-signal -lwasi-emulated-mman -SQLITE_NATIVE := $(SQLITE_COMMON) - -$(BUILD_DIR)/sqlite3_mem: programs/sqlite3_mem.c libs/sqlite3/sqlite3.c $(WASI_SDK_DIR)/bin/clang - @mkdir -p $(BUILD_DIR) - $(CC) --target=wasm32-wasip1 --sysroot=$(SYSROOT) -Os -I include/ \ - $(SQLITE_WASM) -Ilibs/sqlite3 -Wl,--initial-memory=16777216 -o $@ \ - programs/sqlite3_mem.c libs/sqlite3/sqlite3.c - -$(NATIVE_DIR)/sqlite3_mem: programs/sqlite3_mem.c libs/sqlite3/sqlite3.c - @mkdir -p $(NATIVE_DIR) - $(NATIVE_CC) $(NATIVE_CFLAGS) $(SQLITE_NATIVE) -Ilibs/sqlite3 -o $@ \ - programs/sqlite3_mem.c libs/sqlite3/sqlite3.c -lm -lpthread - -# sqlite3_cli: full CLI, links SQLite amalgamation (installed as sqlite3) -# Uses -Os (not -O2) + no wasm-opt — higher optimization breaks SQLite's -# internal function pointer tables in WASM indirect call table. -# Uses --initial-memory=16777216 (16MB) for SQLite's malloc requirements. -$(BUILD_DIR)/sqlite3_cli: programs/sqlite3_cli.c libs/sqlite3/sqlite3.c $(WASI_SDK_DIR)/bin/clang - @mkdir -p $(BUILD_DIR) - $(CC) --target=wasm32-wasip1 --sysroot=$(SYSROOT) -Os -I include/ \ - $(SQLITE_WASM) -Ilibs/sqlite3 -Wl,--initial-memory=16777216 \ - -o $@ programs/sqlite3_cli.c libs/sqlite3/sqlite3.c - -$(NATIVE_DIR)/sqlite3_cli: programs/sqlite3_cli.c libs/sqlite3/sqlite3.c - @mkdir -p $(NATIVE_DIR) - $(NATIVE_CC) $(NATIVE_CFLAGS) $(SQLITE_NATIVE) -Ilibs/sqlite3 -o $@ \ - programs/sqlite3_cli.c libs/sqlite3/sqlite3.c -lm -lpthread - -# zip: links zlib + minizip -ZLIB_SRCS := libs/zlib/adler32.c libs/zlib/compress.c libs/zlib/crc32.c libs/zlib/deflate.c \ - libs/zlib/infback.c libs/zlib/inffast.c libs/zlib/inflate.c libs/zlib/inftrees.c \ - libs/zlib/trees.c libs/zlib/uncompr.c libs/zlib/zutil.c -MINIZIP_SRCS := libs/minizip/ioapi.c libs/minizip/zip.c -ZIP_INCLUDES := -Ilibs/zlib -Ilibs/minizip - -$(BUILD_DIR)/zip: programs/zip.c $(ZLIB_SRCS) $(MINIZIP_SRCS) $(WASI_SDK_DIR)/bin/clang - @mkdir -p $(BUILD_DIR) - $(CC) $(WASM_CFLAGS) $(ZIP_INCLUDES) -o $@.wasm programs/zip.c $(ZLIB_SRCS) $(MINIZIP_SRCS) - @if [ "$(HAS_WASM_OPT)" = "1" ]; then \ - wasm-opt -O3 --strip-debug $@.wasm -o $@; \ - else \ - cp $@.wasm $@; \ - fi - @rm -f $@.wasm - -$(NATIVE_DIR)/zip: programs/zip.c $(ZLIB_SRCS) $(MINIZIP_SRCS) - @mkdir -p $(NATIVE_DIR) - $(NATIVE_CC) $(NATIVE_CFLAGS) $(ZIP_INCLUDES) -o $@ programs/zip.c $(ZLIB_SRCS) $(MINIZIP_SRCS) - -# unzip: links zlib + minizip (unzip side) -MINIZIP_UNZIP_SRCS := libs/minizip/ioapi.c libs/minizip/unzip.c - -$(BUILD_DIR)/unzip: programs/unzip.c $(ZLIB_SRCS) $(MINIZIP_UNZIP_SRCS) $(WASI_SDK_DIR)/bin/clang - @mkdir -p $(BUILD_DIR) - $(CC) $(WASM_CFLAGS) $(ZIP_INCLUDES) -o $@.wasm programs/unzip.c $(ZLIB_SRCS) $(MINIZIP_UNZIP_SRCS) - @if [ "$(HAS_WASM_OPT)" = "1" ]; then \ - wasm-opt -O3 --strip-debug $@.wasm -o $@; \ - else \ - cp $@.wasm $@; \ - fi - @rm -f $@.wasm - -$(NATIVE_DIR)/unzip: programs/unzip.c $(ZLIB_SRCS) $(MINIZIP_UNZIP_SRCS) - @mkdir -p $(NATIVE_DIR) - $(NATIVE_CC) $(NATIVE_CFLAGS) $(ZIP_INCLUDES) -o $@ programs/unzip.c $(ZLIB_SRCS) $(MINIZIP_UNZIP_SRCS) - -# curl_test: links libcurl (HTTP/HTTPS build for WASM via host_net + host_tls) -CURL_SRCS := $(wildcard libs/curl/lib/*.c) $(wildcard libs/curl/lib/vauth/*.c) \ - $(wildcard libs/curl/lib/vtls/*.c) $(wildcard libs/curl/lib/vquic/*.c) \ - $(wildcard libs/curl/lib/vssh/*.c) -CURL_INCLUDES := -Ilibs/curl/include -Ilibs/curl/lib -include libs/curl/lib/curl_setup.h -include libs/curl/lib/curl_printf.h -CURL_LIB_DEFS := -DHAVE_CONFIG_H -DBUILDING_LIBCURL -D_WASI_EMULATED_SIGNAL -DHAVE_BASENAME -DHAVE_LIBGEN_H - -$(BUILD_DIR)/curl: scripts/build-curl-upstream.sh $(CURL_UPSTREAM_OVERLAY_FILES) $(WASI_SDK_DIR)/bin/clang - @mkdir -p $(BUILD_DIR) - bash scripts/build-curl-upstream.sh \ - --version "$(CURL_RELEASE_VERSION)" \ - --tag "$(CURL_RELEASE_TAG)" \ - --url "$(CURL_RELEASE_URL)" \ - --cache-dir "$(abspath $(LIBS_CACHE))" \ - --build-dir "$(abspath $(CURL_UPSTREAM_BUILD_DIR))" \ - --overlay-dir "$(abspath $(CURL_UPSTREAM_OVERLAY_DIR))" \ - --cc "$(abspath $(CC)) --target=wasm32-wasip1 --sysroot=$(abspath $(SYSROOT))" \ - --ar "$(abspath $(WASI_SDK_DIR)/bin/llvm-ar)" \ - --ranlib "$(abspath $(WASI_SDK_DIR)/bin/llvm-ranlib)" \ - --output "$(abspath $@)" - -# wget: minimal wget built on libcurl -$(BUILD_DIR)/wget: programs/wget.c $(CURL_SRCS) $(WASI_SDK_DIR)/bin/clang - @mkdir -p $(BUILD_DIR) - $(CC) $(WASM_CFLAGS) $(CURL_LIB_DEFS) $(CURL_INCLUDES) \ - -lwasi-emulated-signal \ - -o $@.wasm programs/wget.c $(CURL_SRCS) - @if [ "$(HAS_WASM_OPT)" = "1" ]; then \ - wasm-opt -O3 --strip-debug --all-features $@.wasm -o $@; \ - else \ - cp $@.wasm $@; \ - fi - @rm -f $@.wasm - -$(NATIVE_DIR)/wget: programs/wget.c - @mkdir -p $(NATIVE_DIR) - $(NATIVE_CC) $(NATIVE_CFLAGS) -o $@ programs/wget.c -lcurl - -# duckdb: upstream DuckDB CLI built from source with our patched WASI/POSIX sysroot -$(BUILD_DIR)/duckdb: libs/duckdb/CMakeLists.txt $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a $(WASI_SDK_DIR)/bin/clang $(WASI_SDK_DIR)/bin/clang++ cmake/FindThreads.cmake scripts/build-duckdb.sh include/fcntl.h include/ifaddrs.h include/net/if.h include/sys/ioctl.h - @mkdir -p $(BUILD_DIR) - DUCKDB_SRC_DIR="$(abspath $(LIBS_DIR)/duckdb)" \ - DUCKDB_BUILD_DIR="$(abspath $(BUILD_DIR)/duckdb-cmake)" \ - DUCKDB_OUTPUT="$(abspath $(BUILD_DIR)/duckdb)" \ - WASI_SDK_DIR="$(abspath $(WASI_SDK_DIR))" \ - SYSROOT_DIR="$(abspath $(PATCHED_SYSROOT))" \ - MODULE_PATH="$(abspath cmake)" \ - OVERLAY_INCLUDE_DIR="$(abspath include)" \ - DUCKDB_GIT_DESCRIBE="$(DUCKDB_GIT_DESCRIBE)" \ - bash scripts/build-duckdb.sh - -# --- Compile all C programs to native binaries (for parity testing) --- - -native: $(NATIVE_OUTPUTS) - @echo "" - @echo "=== Native Build Report ===" - @echo "Programs: $(words $(NATIVE_PROG_NAMES)) compiled" - @echo "Output: $(NATIVE_DIR)/" - @echo "=== Build complete ===" - -$(NATIVE_DIR)/%: programs/%.c - @mkdir -p $(NATIVE_DIR) - $(NATIVE_CC) $(NATIVE_CFLAGS) -o $@ $< - -# --- Install opted-in commands to COMMANDS_DIR --- - -install: - @if [ -z "$(COMMANDS)" ]; then \ - echo "No C commands opted in for install (COMMANDS is empty)"; \ - exit 0; \ - fi - @if [ ! -d "$(BUILD_DIR)" ]; then \ - echo "Error: $(BUILD_DIR) not found. Run 'make programs' first."; \ - exit 1; \ - fi - @mkdir -p $(COMMANDS_DIR) - @INSTALLED=0; \ - for cmd in $(COMMANDS); do \ - src="$$cmd"; \ - case "$$cmd" in \ - sqlite3) src="sqlite3_cli" ;; \ - esac; \ - if [ -f "$(BUILD_DIR)/$$src" ]; then \ - cp "$(BUILD_DIR)/$$src" "$(COMMANDS_DIR)/$$cmd"; \ - INSTALLED=$$((INSTALLED + 1)); \ - else \ - echo "Warning: $(BUILD_DIR)/$$src not found — skipping"; \ - fi; \ - done; \ - echo "Installed $$INSTALLED C command(s) to $(COMMANDS_DIR)" - @# Create symlinks for git remote helpers - @if [ -f "$(COMMANDS_DIR)/git" ]; then \ - ln -sf git "$(COMMANDS_DIR)/git-remote-http"; \ - ln -sf git "$(COMMANDS_DIR)/git-remote-https"; \ - fi - -# --- Clean --- - -clean: - rm -rf $(BUILD_DIR) - -# --- vim: real vim 9.2 -> wasm32-wasip1 against the full-OS libc --- -# -# vim compiles as a normal terminal program against the PATCHED sysroot plus a -# small bridge library (vim/): termios via host_tty (raw-mode + winsize), -# a termcap stub (builtin termcaps), and POSIX stubs the full-OS libc lacks. -# Recipe recovered from the original hand build (see vim/ headers). The result -# lands in $(BUILD_DIR)/vim; `make -C .. cmd/vim` installs it into the shared -# commands dir. NOTE: run as `vim -n ` docs-wise is no longer needed — -# swap files work; kept here for archaeology. -VIM_VERSION ?= v9.2.0780 -VIM_SRC := libs/vim -VIM_BRIDGE_SRC := vim -VIM_BRIDGE_BUILD := $(BUILD_DIR)/vim-bridge -VIM_BRIDGE_OBJS := $(VIM_BRIDGE_BUILD)/termios_bridge.o $(VIM_BRIDGE_BUILD)/termcap_stub.o $(VIM_BRIDGE_BUILD)/posix_stubs.o -VIM_WCC := $(VIM_BRIDGE_BUILD)/wcc - -.PHONY: fetch-vim -fetch-vim: $(VIM_SRC)/src/vim.h -$(VIM_SRC)/src/vim.h: - git clone --depth 1 --branch $(VIM_VERSION) https://github.com/vim/vim $(VIM_SRC) - -$(VIM_BRIDGE_BUILD)/%.o: $(VIM_BRIDGE_SRC)/%.c $(WASI_SDK_DIR)/bin/clang sysroot - @mkdir -p $(VIM_BRIDGE_BUILD) - $(CC) --target=wasm32-wasip1 --sysroot=$(PATCHED_SYSROOT) -O2 -I $(VIM_BRIDGE_SRC) -c -o $@ $< - -$(VIM_BRIDGE_BUILD)/libtermcap.a: $(VIM_BRIDGE_OBJS) - $(WASI_SDK_DIR)/bin/llvm-ar rcs $@ $(VIM_BRIDGE_OBJS) - -# Wrapper compiler: every vim TU gets the sysroot, the bridge headers, and the -# forced compat include, so vim's own Makefile needs no flag surgery. -$(VIM_WCC): $(WASI_SDK_DIR)/bin/clang - @mkdir -p $(VIM_BRIDGE_BUILD) - @printf '#!/bin/sh\nexec %s --target=wasm32-wasip1 --sysroot=%s -O2 -I %s -include %s "$$@"\n' \ - "$(abspath $(WASI_SDK_DIR)/bin/clang)" \ - "$(abspath $(PATCHED_SYSROOT))" \ - "$(abspath $(VIM_BRIDGE_SRC))" \ - "$(abspath $(VIM_BRIDGE_SRC)/compat.h)" > $@ - @chmod +x $@ - -$(BUILD_DIR)/vim: fetch-vim $(VIM_BRIDGE_BUILD)/libtermcap.a $(VIM_WCC) wasm-opt-check - cd $(VIM_SRC)/src && \ - CC="$(abspath $(VIM_WCC))" \ - LDFLAGS="$(abspath $(VIM_BRIDGE_BUILD)/termios_bridge.o) -L$(abspath $(VIM_BRIDGE_BUILD))" \ - vim_cv_toupper_broken=no \ - vim_cv_terminfo=no \ - vim_cv_tgetent=zero \ - vim_cv_getcwd_broken=no \ - vim_cv_stat_ignores_slash=no \ - vim_cv_memmove_handles_overlap=yes \ - vim_cv_bcopy_handles_overlap=yes \ - vim_cv_memcpy_handles_overlap=yes \ - vim_cv_tty_group=world \ - vim_cv_tty_mode=0620 \ - ./configure --host=wasm32-wasip1 --build=$$(cc -dumpmachine 2>/dev/null || echo x86_64-pc-linux-gnu) \ - --with-features=normal --with-tlib=termcap \ - --disable-gui --without-x --without-wayland --disable-nls --disable-channel \ - --disable-netbeans --disable-selinux --disable-gpm --disable-sysmouse \ - --disable-icon-cache-update --disable-desktop-database-update - $(MAKE) -C $(VIM_SRC)/src vim - @mkdir -p $(BUILD_DIR) - @if [ "$(HAS_WASM_OPT)" = "1" ]; then \ - wasm-opt -O3 --strip-debug --all-features $(VIM_SRC)/src/vim -o $(BUILD_DIR)/vim; \ - else \ - cp $(VIM_SRC)/src/vim $(BUILD_DIR)/vim; \ - fi - @echo "Built $(BUILD_DIR)/vim" diff --git a/registry/native/c/curl-upstream-overlay/lib/hostip.c b/registry/native/c/curl-upstream-overlay/lib/hostip.c deleted file mode 100644 index f858d50233..0000000000 --- a/registry/native/c/curl-upstream-overlay/lib/hostip.c +++ /dev/null @@ -1,1490 +0,0 @@ -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) Daniel Stenberg, , et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at https://curl.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - * SPDX-License-Identifier: curl - * - ***************************************************************************/ - -#include "curl_setup.h" - -#ifdef HAVE_NETINET_IN_H -#include -#endif -#ifdef HAVE_NETINET_IN6_H -#include -#endif -#ifdef HAVE_NETDB_H -#include -#endif -#ifdef HAVE_ARPA_INET_H -#include -#endif -#ifdef __VMS -#include -#include -#endif - -#ifndef __wasi__ -#include -#include -#endif - -#include "urldata.h" -#include "sendf.h" -#include "hostip.h" -#include "hash.h" -#include "rand.h" -#include "share.h" -#include "url.h" -#include "inet_ntop.h" -#include "inet_pton.h" -#include "multiif.h" -#include "doh.h" -#include "warnless.h" -#include "strcase.h" -#include "easy_lock.h" -/* The last 3 #include files should be in this order */ -#include "curl_printf.h" -#include "curl_memory.h" -#include "memdebug.h" - -#if defined(CURLRES_SYNCH) && \ - defined(HAVE_ALARM) && \ - defined(SIGALRM) && \ - defined(HAVE_SIGSETJMP) && \ - defined(GLOBAL_INIT_IS_THREADSAFE) -/* alarm-based timeouts can only be used with all the dependencies satisfied */ -#define USE_ALARM_TIMEOUT -#endif - -#define MAX_HOSTCACHE_LEN (255 + 7) /* max FQDN + colon + port number + zero */ - -#define MAX_DNS_CACHE_SIZE 29999 - -/* - * hostip.c explained - * ================== - * - * The main COMPILE-TIME DEFINES to keep in mind when reading the host*.c - * source file are these: - * - * CURLRES_IPV6 - this host has getaddrinfo() and family, and thus we use - * that. The host may not be able to resolve IPv6, but we do not really have to - * take that into account. Hosts that are not IPv6-enabled have CURLRES_IPV4 - * defined. - * - * CURLRES_ARES - is defined if libcurl is built to use c-ares for - * asynchronous name resolves. This can be Windows or *nix. - * - * CURLRES_THREADED - is defined if libcurl is built to run under (native) - * Windows, and then the name resolve will be done in a new thread, and the - * supported API will be the same as for ares-builds. - * - * If any of the two previous are defined, CURLRES_ASYNCH is defined too. If - * libcurl is not built to use an asynchronous resolver, CURLRES_SYNCH is - * defined. - * - * The host*.c sources files are split up like this: - * - * hostip.c - method-independent resolver functions and utility functions - * hostasyn.c - functions for asynchronous name resolves - * hostsyn.c - functions for synchronous name resolves - * hostip4.c - IPv4 specific functions - * hostip6.c - IPv6 specific functions - * - * The two asynchronous name resolver backends are implemented in: - * asyn-ares.c - functions for ares-using name resolves - * asyn-thread.c - functions for threaded name resolves - - * The hostip.h is the united header file for all this. It defines the - * CURLRES_* defines based on the config*.h and curl_setup.h defines. - */ - -static void hostcache_unlink_entry(void *entry); - -#ifndef CURL_DISABLE_VERBOSE_STRINGS -static void show_resolve_info(struct Curl_easy *data, - struct Curl_dns_entry *dns); -#else -#define show_resolve_info(x,y) Curl_nop_stmt -#endif - -/* - * Curl_printable_address() stores a printable version of the 1st address - * given in the 'ai' argument. The result will be stored in the buf that is - * bufsize bytes big. - * - * If the conversion fails, the target buffer is empty. - */ -void Curl_printable_address(const struct Curl_addrinfo *ai, char *buf, - size_t bufsize) -{ - DEBUGASSERT(bufsize); - buf[0] = 0; - - switch(ai->ai_family) { - case AF_INET: { - const struct sockaddr_in *sa4 = (const void *)ai->ai_addr; - const struct in_addr *ipaddr4 = &sa4->sin_addr; - (void)Curl_inet_ntop(ai->ai_family, (const void *)ipaddr4, buf, bufsize); - break; - } -#ifdef USE_IPV6 - case AF_INET6: { - const struct sockaddr_in6 *sa6 = (const void *)ai->ai_addr; - const struct in6_addr *ipaddr6 = &sa6->sin6_addr; - (void)Curl_inet_ntop(ai->ai_family, (const void *)ipaddr6, buf, bufsize); - break; - } -#endif - default: - break; - } -} - -/* - * Create a hostcache id string for the provided host + port, to be used by - * the DNS caching. Without alloc. Return length of the id string. - */ -static size_t -create_hostcache_id(const char *name, - size_t nlen, /* 0 or actual name length */ - int port, char *ptr, size_t buflen) -{ - size_t len = nlen ? nlen : strlen(name); - DEBUGASSERT(buflen >= MAX_HOSTCACHE_LEN); - if(len > (buflen - 7)) - len = buflen - 7; - /* store and lower case the name */ - Curl_strntolower(ptr, name, len); - return msnprintf(&ptr[len], 7, ":%u", port) + len; -} - -struct hostcache_prune_data { - time_t now; - time_t oldest; /* oldest time in cache not pruned. */ - int max_age_sec; -}; - -/* - * This function is set as a callback to be called for every entry in the DNS - * cache when we want to prune old unused entries. - * - * Returning non-zero means remove the entry, return 0 to keep it in the - * cache. - */ -static int -hostcache_entry_is_stale(void *datap, void *hc) -{ - struct hostcache_prune_data *prune = - (struct hostcache_prune_data *) datap; - struct Curl_dns_entry *dns = (struct Curl_dns_entry *) hc; - - if(dns->timestamp) { - /* age in seconds */ - time_t age = prune->now - dns->timestamp; - if(age >= prune->max_age_sec) - return TRUE; - if(age > prune->oldest) - prune->oldest = age; - } - return FALSE; -} - -/* - * Prune the DNS cache. This assumes that a lock has already been taken. - * Returns the 'age' of the oldest still kept entry. - */ -static time_t -hostcache_prune(struct Curl_hash *hostcache, int cache_timeout, - time_t now) -{ - struct hostcache_prune_data user; - - user.max_age_sec = cache_timeout; - user.now = now; - user.oldest = 0; - - Curl_hash_clean_with_criterium(hostcache, - (void *) &user, - hostcache_entry_is_stale); - - return user.oldest; -} - -/* - * Library-wide function for pruning the DNS cache. This function takes and - * returns the appropriate locks. - */ -void Curl_hostcache_prune(struct Curl_easy *data) -{ - time_t now; - /* the timeout may be set -1 (forever) */ - int timeout = data->set.dns_cache_timeout; - - if(!data->dns.hostcache) - /* NULL hostcache means we cannot do it */ - return; - - if(data->share) - Curl_share_lock(data, CURL_LOCK_DATA_DNS, CURL_LOCK_ACCESS_SINGLE); - - now = time(NULL); - - do { - /* Remove outdated and unused entries from the hostcache */ - time_t oldest = hostcache_prune(data->dns.hostcache, timeout, now); - - if(oldest < INT_MAX) - timeout = (int)oldest; /* we know it fits */ - else - timeout = INT_MAX - 1; - - /* if the cache size is still too big, use the oldest age as new - prune limit */ - } while(timeout && - (Curl_hash_count(data->dns.hostcache) > MAX_DNS_CACHE_SIZE)); - - if(data->share) - Curl_share_unlock(data, CURL_LOCK_DATA_DNS); -} - -#ifdef USE_ALARM_TIMEOUT -/* Beware this is a global and unique instance. This is used to store the - return address that we can jump back to from inside a signal handler. This - is not thread-safe stuff. */ -static sigjmp_buf curl_jmpenv; -static curl_simple_lock curl_jmpenv_lock; -#endif - -/* lookup address, returns entry if found and not stale */ -static struct Curl_dns_entry *fetch_addr(struct Curl_easy *data, - const char *hostname, - int port) -{ - struct Curl_dns_entry *dns = NULL; - char entry_id[MAX_HOSTCACHE_LEN]; - - /* Create an entry id, based upon the hostname and port */ - size_t entry_len = create_hostcache_id(hostname, 0, port, - entry_id, sizeof(entry_id)); - - /* See if it is already in our dns cache */ - dns = Curl_hash_pick(data->dns.hostcache, entry_id, entry_len + 1); - - /* No entry found in cache, check if we might have a wildcard entry */ - if(!dns && data->state.wildcard_resolve) { - entry_len = create_hostcache_id("*", 1, port, entry_id, sizeof(entry_id)); - - /* See if it is already in our dns cache */ - dns = Curl_hash_pick(data->dns.hostcache, entry_id, entry_len + 1); - } - - if(dns && (data->set.dns_cache_timeout != -1)) { - /* See whether the returned entry is stale. Done before we release lock */ - struct hostcache_prune_data user; - - user.now = time(NULL); - user.max_age_sec = data->set.dns_cache_timeout; - user.oldest = 0; - - if(hostcache_entry_is_stale(&user, dns)) { - infof(data, "Hostname in DNS cache was stale, zapped"); - dns = NULL; /* the memory deallocation is being handled by the hash */ - Curl_hash_delete(data->dns.hostcache, entry_id, entry_len + 1); - } - } - - /* See if the returned entry matches the required resolve mode */ - if(dns && data->conn->ip_version != CURL_IPRESOLVE_WHATEVER) { - int pf = PF_INET; - bool found = FALSE; - struct Curl_addrinfo *addr = dns->addr; - -#ifdef PF_INET6 - if(data->conn->ip_version == CURL_IPRESOLVE_V6) - pf = PF_INET6; -#endif - - while(addr) { - if(addr->ai_family == pf) { - found = TRUE; - break; - } - addr = addr->ai_next; - } - - if(!found) { - infof(data, "Hostname in DNS cache does not have needed family, zapped"); - dns = NULL; /* the memory deallocation is being handled by the hash */ - Curl_hash_delete(data->dns.hostcache, entry_id, entry_len + 1); - } - } - return dns; -} - -/* - * Curl_fetch_addr() fetches a 'Curl_dns_entry' already in the DNS cache. - * - * Curl_resolv() checks initially and multi_runsingle() checks each time - * it discovers the handle in the state WAITRESOLVE whether the hostname - * has already been resolved and the address has already been stored in - * the DNS cache. This short circuits waiting for a lot of pending - * lookups for the same hostname requested by different handles. - * - * Returns the Curl_dns_entry entry pointer or NULL if not in the cache. - * - * The returned data *MUST* be "released" with Curl_resolv_unlink() after - * use, or we will leak memory! - */ -struct Curl_dns_entry * -Curl_fetch_addr(struct Curl_easy *data, - const char *hostname, - int port) -{ - struct Curl_dns_entry *dns = NULL; - - if(data->share) - Curl_share_lock(data, CURL_LOCK_DATA_DNS, CURL_LOCK_ACCESS_SINGLE); - - dns = fetch_addr(data, hostname, port); - - if(dns) - dns->refcount++; /* we use it! */ - - if(data->share) - Curl_share_unlock(data, CURL_LOCK_DATA_DNS); - - return dns; -} - -#ifndef CURL_DISABLE_SHUFFLE_DNS -/* - * Return # of addresses in a Curl_addrinfo struct - */ -static int num_addresses(const struct Curl_addrinfo *addr) -{ - int i = 0; - while(addr) { - addr = addr->ai_next; - i++; - } - return i; -} - -UNITTEST CURLcode Curl_shuffle_addr(struct Curl_easy *data, - struct Curl_addrinfo **addr); -/* - * Curl_shuffle_addr() shuffles the order of addresses in a 'Curl_addrinfo' - * struct by re-linking its linked list. - * - * The addr argument should be the address of a pointer to the head node of a - * `Curl_addrinfo` list and it will be modified to point to the new head after - * shuffling. - * - * Not declared static only to make it easy to use in a unit test! - * - * @unittest: 1608 - */ -UNITTEST CURLcode Curl_shuffle_addr(struct Curl_easy *data, - struct Curl_addrinfo **addr) -{ - CURLcode result = CURLE_OK; - const int num_addrs = num_addresses(*addr); - - if(num_addrs > 1) { - struct Curl_addrinfo **nodes; - infof(data, "Shuffling %i addresses", num_addrs); - - nodes = malloc(num_addrs*sizeof(*nodes)); - if(nodes) { - int i; - unsigned int *rnd; - const size_t rnd_size = num_addrs * sizeof(*rnd); - - /* build a plain array of Curl_addrinfo pointers */ - nodes[0] = *addr; - for(i = 1; i < num_addrs; i++) { - nodes[i] = nodes[i-1]->ai_next; - } - - rnd = malloc(rnd_size); - if(rnd) { - /* Fisher-Yates shuffle */ - if(Curl_rand(data, (unsigned char *)rnd, rnd_size) == CURLE_OK) { - struct Curl_addrinfo *swap_tmp; - for(i = num_addrs - 1; i > 0; i--) { - swap_tmp = nodes[rnd[i] % (unsigned int)(i + 1)]; - nodes[rnd[i] % (unsigned int)(i + 1)] = nodes[i]; - nodes[i] = swap_tmp; - } - - /* relink list in the new order */ - for(i = 1; i < num_addrs; i++) { - nodes[i-1]->ai_next = nodes[i]; - } - - nodes[num_addrs-1]->ai_next = NULL; - *addr = nodes[0]; - } - free(rnd); - } - else - result = CURLE_OUT_OF_MEMORY; - free(nodes); - } - else - result = CURLE_OUT_OF_MEMORY; - } - return result; -} -#endif - -/* - * Curl_cache_addr() stores a 'Curl_addrinfo' struct in the DNS cache. - * - * When calling Curl_resolv() has resulted in a response with a returned - * address, we call this function to store the information in the dns - * cache etc - * - * Returns the Curl_dns_entry entry pointer or NULL if the storage failed. - */ -struct Curl_dns_entry * -Curl_cache_addr(struct Curl_easy *data, - struct Curl_addrinfo *addr, - const char *hostname, - size_t hostlen, /* length or zero */ - int port, - bool permanent) -{ - char entry_id[MAX_HOSTCACHE_LEN]; - size_t entry_len; - struct Curl_dns_entry *dns; - struct Curl_dns_entry *dns2; - -#ifndef CURL_DISABLE_SHUFFLE_DNS - /* shuffle addresses if requested */ - if(data->set.dns_shuffle_addresses) { - CURLcode result = Curl_shuffle_addr(data, &addr); - if(result) - return NULL; - } -#endif - if(!hostlen) - hostlen = strlen(hostname); - - /* Create a new cache entry */ - dns = calloc(1, sizeof(struct Curl_dns_entry) + hostlen); - if(!dns) { - return NULL; - } - - /* Create an entry id, based upon the hostname and port */ - entry_len = create_hostcache_id(hostname, hostlen, port, - entry_id, sizeof(entry_id)); - - dns->refcount = 1; /* the cache has the first reference */ - dns->addr = addr; /* this is the address(es) */ - if(permanent) - dns->timestamp = 0; /* an entry that never goes stale */ - else { - dns->timestamp = time(NULL); - if(dns->timestamp == 0) - dns->timestamp = 1; - } - dns->hostport = port; - if(hostlen) - memcpy(dns->hostname, hostname, hostlen); - - /* Store the resolved data in our DNS cache. */ - dns2 = Curl_hash_add(data->dns.hostcache, entry_id, entry_len + 1, - (void *)dns); - if(!dns2) { - free(dns); - return NULL; - } - - dns = dns2; - dns->refcount++; /* mark entry as in-use */ - return dns; -} - -#ifdef USE_IPV6 -/* return a static IPv6 ::1 for the name */ -static struct Curl_addrinfo *get_localhost6(int port, const char *name) -{ - struct Curl_addrinfo *ca; - const size_t ss_size = sizeof(struct sockaddr_in6); - const size_t hostlen = strlen(name); - struct sockaddr_in6 sa6; - unsigned char ipv6[16]; - unsigned short port16 = (unsigned short)(port & 0xffff); - ca = calloc(1, sizeof(struct Curl_addrinfo) + ss_size + hostlen + 1); - if(!ca) - return NULL; - - sa6.sin6_family = AF_INET6; - sa6.sin6_port = htons(port16); - sa6.sin6_flowinfo = 0; - sa6.sin6_scope_id = 0; - - (void)Curl_inet_pton(AF_INET6, "::1", ipv6); - memcpy(&sa6.sin6_addr, ipv6, sizeof(ipv6)); - - ca->ai_flags = 0; - ca->ai_family = AF_INET6; - ca->ai_socktype = SOCK_STREAM; - ca->ai_protocol = IPPROTO_TCP; - ca->ai_addrlen = (curl_socklen_t)ss_size; - ca->ai_next = NULL; - ca->ai_addr = (void *)((char *)ca + sizeof(struct Curl_addrinfo)); - memcpy(ca->ai_addr, &sa6, ss_size); - ca->ai_canonname = (char *)ca->ai_addr + ss_size; - strcpy(ca->ai_canonname, name); - return ca; -} -#else -#define get_localhost6(x,y) NULL -#endif - -/* return a static IPv4 127.0.0.1 for the given name */ -static struct Curl_addrinfo *get_localhost(int port, const char *name) -{ - struct Curl_addrinfo *ca; - struct Curl_addrinfo *ca6; - const size_t ss_size = sizeof(struct sockaddr_in); - const size_t hostlen = strlen(name); - struct sockaddr_in sa; - unsigned int ipv4; - unsigned short port16 = (unsigned short)(port & 0xffff); - - /* memset to clear the sa.sin_zero field */ - memset(&sa, 0, sizeof(sa)); - sa.sin_family = AF_INET; - sa.sin_port = htons(port16); - if(Curl_inet_pton(AF_INET, "127.0.0.1", (char *)&ipv4) < 1) - return NULL; - memcpy(&sa.sin_addr, &ipv4, sizeof(ipv4)); - - ca = calloc(1, sizeof(struct Curl_addrinfo) + ss_size + hostlen + 1); - if(!ca) - return NULL; - ca->ai_flags = 0; - ca->ai_family = AF_INET; - ca->ai_socktype = SOCK_STREAM; - ca->ai_protocol = IPPROTO_TCP; - ca->ai_addrlen = (curl_socklen_t)ss_size; - ca->ai_addr = (void *)((char *)ca + sizeof(struct Curl_addrinfo)); - memcpy(ca->ai_addr, &sa, ss_size); - ca->ai_canonname = (char *)ca->ai_addr + ss_size; - strcpy(ca->ai_canonname, name); - - ca6 = get_localhost6(port, name); - if(!ca6) - return ca; - ca6->ai_next = ca; - return ca6; -} - -#ifdef USE_IPV6 -/* - * Curl_ipv6works() returns TRUE if IPv6 seems to work. - */ -bool Curl_ipv6works(struct Curl_easy *data) -{ - if(data) { - /* the nature of most system is that IPv6 status does not come and go - during a program's lifetime so we only probe the first time and then we - have the info kept for fast reuse */ - DEBUGASSERT(data); - DEBUGASSERT(data->multi); - if(data->multi->ipv6_up == IPV6_UNKNOWN) { - bool works = Curl_ipv6works(NULL); - data->multi->ipv6_up = works ? IPV6_WORKS : IPV6_DEAD; - } - return data->multi->ipv6_up == IPV6_WORKS; - } - else { - int ipv6_works = -1; - /* probe to see if we have a working IPv6 stack */ - curl_socket_t s = socket(PF_INET6, SOCK_DGRAM, 0); - if(s == CURL_SOCKET_BAD) - /* an IPv6 address was requested but we cannot get/use one */ - ipv6_works = 0; - else { - ipv6_works = 1; - sclose(s); - } - return (ipv6_works > 0); - } -} -#endif /* USE_IPV6 */ - -/* - * Curl_host_is_ipnum() returns TRUE if the given string is a numerical IPv4 - * (or IPv6 if supported) address. - */ -bool Curl_host_is_ipnum(const char *hostname) -{ - struct in_addr in; -#ifdef USE_IPV6 - struct in6_addr in6; -#endif - if(Curl_inet_pton(AF_INET, hostname, &in) > 0 -#ifdef USE_IPV6 - || Curl_inet_pton(AF_INET6, hostname, &in6) > 0 -#endif - ) - return TRUE; - return FALSE; -} - - -/* return TRUE if 'part' is a case insensitive tail of 'full' */ -static bool tailmatch(const char *full, const char *part) -{ - size_t plen = strlen(part); - size_t flen = strlen(full); - if(plen > flen) - return FALSE; - return strncasecompare(part, &full[flen - plen], plen); -} - -/* - * Curl_resolv() is the main name resolve function within libcurl. It resolves - * a name and returns a pointer to the entry in the 'entry' argument (if one - * is provided). This function might return immediately if we are using asynch - * resolves. See the return codes. - * - * The cache entry we return will get its 'inuse' counter increased when this - * function is used. You MUST call Curl_resolv_unlink() later (when you are - * done using this struct) to decrease the reference counter again. - * - * Return codes: - * - * CURLRESOLV_ERROR (-1) = error, no pointer - * CURLRESOLV_RESOLVED (0) = OK, pointer provided - * CURLRESOLV_PENDING (1) = waiting for response, no pointer - */ - -enum resolve_t Curl_resolv(struct Curl_easy *data, - const char *hostname, - int port, - bool allowDOH, - struct Curl_dns_entry **entry) -{ - struct Curl_dns_entry *dns = NULL; - CURLcode result; - enum resolve_t rc = CURLRESOLV_ERROR; /* default to failure */ - struct connectdata *conn = data->conn; - /* We should intentionally error and not resolve .onion TLDs */ - size_t hostname_len = strlen(hostname); - if(hostname_len >= 7 && - (curl_strequal(&hostname[hostname_len - 6], ".onion") || - curl_strequal(&hostname[hostname_len - 7], ".onion."))) { - failf(data, "Not resolving .onion address (RFC 7686)"); - return CURLRESOLV_ERROR; - } - *entry = NULL; -#ifndef CURL_DISABLE_DOH - conn->bits.doh = FALSE; /* default is not */ -#else - (void)allowDOH; -#endif - - if(data->share) - Curl_share_lock(data, CURL_LOCK_DATA_DNS, CURL_LOCK_ACCESS_SINGLE); - - dns = fetch_addr(data, hostname, port); - - if(dns) { - infof(data, "Hostname %s was found in DNS cache", hostname); - dns->refcount++; /* we use it! */ - rc = CURLRESOLV_RESOLVED; - } - - if(data->share) - Curl_share_unlock(data, CURL_LOCK_DATA_DNS); - - if(!dns) { - /* The entry was not in the cache. Resolve it to IP address */ - - struct Curl_addrinfo *addr = NULL; - int respwait = 0; -#if !defined(CURL_DISABLE_DOH) || !defined(USE_RESOLVE_ON_IPS) - struct in_addr in; -#endif -#ifndef CURL_DISABLE_DOH -#ifndef USE_RESOLVE_ON_IPS - const -#endif - bool ipnum = FALSE; -#endif - - /* notify the resolver start callback */ - if(data->set.resolver_start) { - int st; - Curl_set_in_callback(data, TRUE); - st = data->set.resolver_start( -#ifdef USE_CURL_ASYNC - data->state.async.resolver, -#else - NULL, -#endif - NULL, - data->set.resolver_start_client); - Curl_set_in_callback(data, FALSE); - if(st) - return CURLRESOLV_ERROR; - } - -#ifndef USE_RESOLVE_ON_IPS - /* First check if this is an IPv4 address string */ - if(Curl_inet_pton(AF_INET, hostname, &in) > 0) { - /* This is a dotted IP address 123.123.123.123-style */ - addr = Curl_ip2addr(AF_INET, &in, hostname, port); - if(!addr) - return CURLRESOLV_ERROR; - } -#ifdef USE_IPV6 - else { - struct in6_addr in6; - /* check if this is an IPv6 address string */ - if(Curl_inet_pton(AF_INET6, hostname, &in6) > 0) { - /* This is an IPv6 address literal */ - addr = Curl_ip2addr(AF_INET6, &in6, hostname, port); - if(!addr) - return CURLRESOLV_ERROR; - } - } -#endif /* USE_IPV6 */ - -#else /* if USE_RESOLVE_ON_IPS */ -#ifndef CURL_DISABLE_DOH - /* First check if this is an IPv4 address string */ - if(Curl_inet_pton(AF_INET, hostname, &in) > 0) - /* This is a dotted IP address 123.123.123.123-style */ - ipnum = TRUE; -#ifdef USE_IPV6 - else { - struct in6_addr in6; - /* check if this is an IPv6 address string */ - if(Curl_inet_pton(AF_INET6, hostname, &in6) > 0) - /* This is an IPv6 address literal */ - ipnum = TRUE; - } -#endif /* USE_IPV6 */ -#endif /* CURL_DISABLE_DOH */ - -#endif /* !USE_RESOLVE_ON_IPS */ - - if(!addr) { - if(conn->ip_version == CURL_IPRESOLVE_V6 && !Curl_ipv6works(data)) - return CURLRESOLV_ERROR; - - if(strcasecompare(hostname, "localhost") || - strcasecompare(hostname, "localhost.") || - tailmatch(hostname, ".localhost") || - tailmatch(hostname, ".localhost.")) - addr = get_localhost(port, hostname); -#ifndef CURL_DISABLE_DOH - else if(allowDOH && data->set.doh && !ipnum) - addr = Curl_doh(data, hostname, port, &respwait); -#endif - else { - /* Check what IP specifics the app has requested and if we can provide - * it. If not, bail out. */ - if(!Curl_ipvalid(data, conn)) - return CURLRESOLV_ERROR; - /* If Curl_getaddrinfo() returns NULL, 'respwait' might be set to a - non-zero value indicating that we need to wait for the response to - the resolve call */ - addr = Curl_getaddrinfo(data, hostname, port, &respwait); - } - } - if(!addr) { - if(respwait) { - /* the response to our resolve call will come asynchronously at - a later time, good or bad */ - /* First, check that we have not received the info by now */ - result = Curl_resolv_check(data, &dns); - if(result) /* error detected */ - return CURLRESOLV_ERROR; - if(dns) - rc = CURLRESOLV_RESOLVED; /* pointer provided */ - else - rc = CURLRESOLV_PENDING; /* no info yet */ - } - } - else { - if(data->share) - Curl_share_lock(data, CURL_LOCK_DATA_DNS, CURL_LOCK_ACCESS_SINGLE); - - /* we got a response, store it in the cache */ - dns = Curl_cache_addr(data, addr, hostname, 0, port, FALSE); - - if(data->share) - Curl_share_unlock(data, CURL_LOCK_DATA_DNS); - - if(!dns) - /* returned failure, bail out nicely */ - Curl_freeaddrinfo(addr); - else { - rc = CURLRESOLV_RESOLVED; - show_resolve_info(data, dns); - } - } - } - - *entry = dns; - - return rc; -} - -#ifdef USE_ALARM_TIMEOUT -/* - * This signal handler jumps back into the main libcurl code and continues - * execution. This effectively causes the remainder of the application to run - * within a signal handler which is nonportable and could lead to problems. - */ -CURL_NORETURN static -void alarmfunc(int sig) -{ - (void)sig; - siglongjmp(curl_jmpenv, 1); -} -#endif /* USE_ALARM_TIMEOUT */ - -/* - * Curl_resolv_timeout() is the same as Curl_resolv() but specifies a - * timeout. This function might return immediately if we are using asynch - * resolves. See the return codes. - * - * The cache entry we return will get its 'inuse' counter increased when this - * function is used. You MUST call Curl_resolv_unlink() later (when you are - * done using this struct) to decrease the reference counter again. - * - * If built with a synchronous resolver and use of signals is not - * disabled by the application, then a nonzero timeout will cause a - * timeout after the specified number of milliseconds. Otherwise, timeout - * is ignored. - * - * Return codes: - * - * CURLRESOLV_TIMEDOUT(-2) = warning, time too short or previous alarm expired - * CURLRESOLV_ERROR (-1) = error, no pointer - * CURLRESOLV_RESOLVED (0) = OK, pointer provided - * CURLRESOLV_PENDING (1) = waiting for response, no pointer - */ - -enum resolve_t Curl_resolv_timeout(struct Curl_easy *data, - const char *hostname, - int port, - struct Curl_dns_entry **entry, - timediff_t timeoutms) -{ -#ifdef USE_ALARM_TIMEOUT -#ifdef HAVE_SIGACTION - struct sigaction keep_sigact; /* store the old struct here */ - volatile bool keep_copysig = FALSE; /* whether old sigact has been saved */ - struct sigaction sigact; -#else -#ifdef HAVE_SIGNAL - void (*keep_sigact)(int); /* store the old handler here */ -#endif /* HAVE_SIGNAL */ -#endif /* HAVE_SIGACTION */ - volatile long timeout; - volatile unsigned int prev_alarm = 0; -#endif /* USE_ALARM_TIMEOUT */ - enum resolve_t rc; - - *entry = NULL; - - if(timeoutms < 0) - /* got an already expired timeout */ - return CURLRESOLV_TIMEDOUT; - -#ifdef USE_ALARM_TIMEOUT - if(data->set.no_signal) - /* Ignore the timeout when signals are disabled */ - timeout = 0; - else - timeout = (timeoutms > LONG_MAX) ? LONG_MAX : (long)timeoutms; - - if(!timeout) - /* USE_ALARM_TIMEOUT defined, but no timeout actually requested */ - return Curl_resolv(data, hostname, port, TRUE, entry); - - if(timeout < 1000) { - /* The alarm() function only provides integer second resolution, so if - we want to wait less than one second we must bail out already now. */ - failf(data, - "remaining timeout of %ld too small to resolve via SIGALRM method", - timeout); - return CURLRESOLV_TIMEDOUT; - } - /* This allows us to time-out from the name resolver, as the timeout - will generate a signal and we will siglongjmp() from that here. - This technique has problems (see alarmfunc). - This should be the last thing we do before calling Curl_resolv(), - as otherwise we would have to worry about variables that get modified - before we invoke Curl_resolv() (and thus use "volatile"). */ - curl_simple_lock_lock(&curl_jmpenv_lock); - - if(sigsetjmp(curl_jmpenv, 1)) { - /* this is coming from a siglongjmp() after an alarm signal */ - failf(data, "name lookup timed out"); - rc = CURLRESOLV_ERROR; - goto clean_up; - } - else { - /************************************************************* - * Set signal handler to catch SIGALRM - * Store the old value to be able to set it back later! - *************************************************************/ -#ifdef HAVE_SIGACTION - sigaction(SIGALRM, NULL, &sigact); - keep_sigact = sigact; - keep_copysig = TRUE; /* yes, we have a copy */ - sigact.sa_handler = alarmfunc; -#ifdef SA_RESTART - /* HP-UX does not have SA_RESTART but defaults to that behavior! */ - sigact.sa_flags &= ~SA_RESTART; -#endif - /* now set the new struct */ - sigaction(SIGALRM, &sigact, NULL); -#else /* HAVE_SIGACTION */ - /* no sigaction(), revert to the much lamer signal() */ -#ifdef HAVE_SIGNAL - keep_sigact = signal(SIGALRM, alarmfunc); -#endif -#endif /* HAVE_SIGACTION */ - - /* alarm() makes a signal get sent when the timeout fires off, and that - will abort system calls */ - prev_alarm = alarm(curlx_sltoui(timeout/1000L)); - } - -#else -#ifndef CURLRES_ASYNCH - if(timeoutms) - infof(data, "timeout on name lookup is not supported"); -#else - (void)timeoutms; /* timeoutms not used with an async resolver */ -#endif -#endif /* USE_ALARM_TIMEOUT */ - - /* Perform the actual name resolution. This might be interrupted by an - * alarm if it takes too long. - */ - rc = Curl_resolv(data, hostname, port, TRUE, entry); - -#ifdef USE_ALARM_TIMEOUT -clean_up: - - if(!prev_alarm) - /* deactivate a possibly active alarm before uninstalling the handler */ - alarm(0); - -#ifdef HAVE_SIGACTION - if(keep_copysig) { - /* we got a struct as it looked before, now put that one back nice - and clean */ - sigaction(SIGALRM, &keep_sigact, NULL); /* put it back */ - } -#else -#ifdef HAVE_SIGNAL - /* restore the previous SIGALRM handler */ - signal(SIGALRM, keep_sigact); -#endif -#endif /* HAVE_SIGACTION */ - - curl_simple_lock_unlock(&curl_jmpenv_lock); - - /* switch back the alarm() to either zero or to what it was before minus - the time we spent until now! */ - if(prev_alarm) { - /* there was an alarm() set before us, now put it back */ - timediff_t elapsed_secs = Curl_timediff(Curl_now(), - data->conn->created) / 1000; - - /* the alarm period is counted in even number of seconds */ - unsigned long alarm_set = (unsigned long)(prev_alarm - elapsed_secs); - - if(!alarm_set || - ((alarm_set >= 0x80000000) && (prev_alarm < 0x80000000)) ) { - /* if the alarm time-left reached zero or turned "negative" (counted - with unsigned values), we should fire off a SIGALRM here, but we - will not, and zero would be to switch it off so we never set it to - less than 1! */ - alarm(1); - rc = CURLRESOLV_TIMEDOUT; - failf(data, "Previous alarm fired off"); - } - else - alarm((unsigned int)alarm_set); - } -#endif /* USE_ALARM_TIMEOUT */ - - return rc; -} - -/* - * Curl_resolv_unlink() releases a reference to the given cached DNS entry. - * When the reference count reaches 0, the entry is destroyed. It is important - * that only one unlink is made for each Curl_resolv() call. - * - * May be called with 'data' == NULL for global cache. - */ -void Curl_resolv_unlink(struct Curl_easy *data, struct Curl_dns_entry **pdns) -{ - struct Curl_dns_entry *dns = *pdns; - *pdns = NULL; - if(data && data->share) - Curl_share_lock(data, CURL_LOCK_DATA_DNS, CURL_LOCK_ACCESS_SINGLE); - - hostcache_unlink_entry(dns); - - if(data && data->share) - Curl_share_unlock(data, CURL_LOCK_DATA_DNS); -} - -/* - * File-internal: release cache dns entry reference, free if inuse drops to 0 - */ -static void hostcache_unlink_entry(void *entry) -{ - struct Curl_dns_entry *dns = (struct Curl_dns_entry *) entry; - DEBUGASSERT(dns && (dns->refcount > 0)); - - dns->refcount--; - if(dns->refcount == 0) { - Curl_freeaddrinfo(dns->addr); -#ifdef USE_HTTPSRR - if(dns->hinfo) { - if(dns->hinfo->target) - free(dns->hinfo->target); - if(dns->hinfo->alpns) - free(dns->hinfo->alpns); - if(dns->hinfo->ipv4hints) - free(dns->hinfo->ipv4hints); - if(dns->hinfo->echconfiglist) - free(dns->hinfo->echconfiglist); - if(dns->hinfo->ipv6hints) - free(dns->hinfo->ipv6hints); - if(dns->hinfo->val) - free(dns->hinfo->val); - free(dns->hinfo); - } -#endif - free(dns); - } -} - -/* - * Curl_init_dnscache() inits a new DNS cache. - */ -void Curl_init_dnscache(struct Curl_hash *hash, size_t size) -{ - Curl_hash_init(hash, size, Curl_hash_str, Curl_str_key_compare, - hostcache_unlink_entry); -} - -/* - * Curl_hostcache_clean() - * - * This _can_ be called with 'data' == NULL but then of course no locking - * can be done! - */ - -void Curl_hostcache_clean(struct Curl_easy *data, - struct Curl_hash *hash) -{ - if(data && data->share) - Curl_share_lock(data, CURL_LOCK_DATA_DNS, CURL_LOCK_ACCESS_SINGLE); - - Curl_hash_clean(hash); - - if(data && data->share) - Curl_share_unlock(data, CURL_LOCK_DATA_DNS); -} - - -CURLcode Curl_loadhostpairs(struct Curl_easy *data) -{ - struct curl_slist *hostp; - char *host_end; - - /* Default is no wildcard found */ - data->state.wildcard_resolve = FALSE; - - for(hostp = data->state.resolve; hostp; hostp = hostp->next) { - char entry_id[MAX_HOSTCACHE_LEN]; - if(!hostp->data) - continue; - if(hostp->data[0] == '-') { - unsigned long num = 0; - size_t entry_len; - size_t hlen = 0; - host_end = strchr(&hostp->data[1], ':'); - - if(host_end) { - hlen = host_end - &hostp->data[1]; - num = strtoul(++host_end, NULL, 10); - if(!hlen || (num > 0xffff)) - host_end = NULL; - } - if(!host_end) { - infof(data, "Bad syntax CURLOPT_RESOLVE removal entry '%s'", - hostp->data); - continue; - } - /* Create an entry id, based upon the hostname and port */ - entry_len = create_hostcache_id(&hostp->data[1], hlen, (int)num, - entry_id, sizeof(entry_id)); - if(data->share) - Curl_share_lock(data, CURL_LOCK_DATA_DNS, CURL_LOCK_ACCESS_SINGLE); - - /* delete entry, ignore if it did not exist */ - Curl_hash_delete(data->dns.hostcache, entry_id, entry_len + 1); - - if(data->share) - Curl_share_unlock(data, CURL_LOCK_DATA_DNS); - } - else { - struct Curl_dns_entry *dns; - struct Curl_addrinfo *head = NULL, *tail = NULL; - size_t entry_len; - char address[64]; -#if !defined(CURL_DISABLE_VERBOSE_STRINGS) - char *addresses = NULL; -#endif - char *addr_begin; - char *addr_end; - char *port_ptr; - int port = 0; - char *end_ptr; - bool permanent = TRUE; - unsigned long tmp_port; - bool error = TRUE; - char *host_begin = hostp->data; - size_t hlen = 0; - - if(host_begin[0] == '+') { - host_begin++; - permanent = FALSE; - } - host_end = strchr(host_begin, ':'); - if(!host_end) - goto err; - hlen = host_end - host_begin; - - port_ptr = host_end + 1; - tmp_port = strtoul(port_ptr, &end_ptr, 10); - if(tmp_port > USHRT_MAX || end_ptr == port_ptr || *end_ptr != ':') - goto err; - - port = (int)tmp_port; -#if !defined(CURL_DISABLE_VERBOSE_STRINGS) - addresses = end_ptr + 1; -#endif - - while(*end_ptr) { - size_t alen; - struct Curl_addrinfo *ai; - - addr_begin = end_ptr + 1; - addr_end = strchr(addr_begin, ','); - if(!addr_end) - addr_end = addr_begin + strlen(addr_begin); - end_ptr = addr_end; - - /* allow IP(v6) address within [brackets] */ - if(*addr_begin == '[') { - if(addr_end == addr_begin || *(addr_end - 1) != ']') - goto err; - ++addr_begin; - --addr_end; - } - - alen = addr_end - addr_begin; - if(!alen) - continue; - - if(alen >= sizeof(address)) - goto err; - - memcpy(address, addr_begin, alen); - address[alen] = '\0'; - -#ifndef USE_IPV6 - if(strchr(address, ':')) { - infof(data, "Ignoring resolve address '%s', missing IPv6 support.", - address); - continue; - } -#endif - - ai = Curl_str2addr(address, port); - if(!ai) { - infof(data, "Resolve address '%s' found illegal", address); - goto err; - } - - if(tail) { - tail->ai_next = ai; - tail = tail->ai_next; - } - else { - head = tail = ai; - } - } - - if(!head) - goto err; - - error = FALSE; -err: - if(error) { - failf(data, "Couldn't parse CURLOPT_RESOLVE entry '%s'", - hostp->data); - Curl_freeaddrinfo(head); - return CURLE_SETOPT_OPTION_SYNTAX; - } - - /* Create an entry id, based upon the hostname and port */ - entry_len = create_hostcache_id(host_begin, hlen, port, - entry_id, sizeof(entry_id)); - - if(data->share) - Curl_share_lock(data, CURL_LOCK_DATA_DNS, CURL_LOCK_ACCESS_SINGLE); - - /* See if it is already in our dns cache */ - dns = Curl_hash_pick(data->dns.hostcache, entry_id, entry_len + 1); - - if(dns) { - infof(data, "RESOLVE %.*s:%d - old addresses discarded", - (int)hlen, host_begin, port); - /* delete old entry, there are two reasons for this - 1. old entry may have different addresses. - 2. even if entry with correct addresses is already in the cache, - but if it is close to expire, then by the time next http - request is made, it can get expired and pruned because old - entry is not necessarily marked as permanent. - 3. when adding a non-permanent entry, we want it to remove and - replace an existing permanent entry. - 4. when adding a non-permanent entry, we want it to get a "fresh" - timeout that starts _now_. */ - - Curl_hash_delete(data->dns.hostcache, entry_id, entry_len + 1); - } - - /* put this new host in the cache */ - dns = Curl_cache_addr(data, head, host_begin, hlen, port, permanent); - if(dns) { - /* release the returned reference; the cache itself will keep the - * entry alive: */ - dns->refcount--; - } - - if(data->share) - Curl_share_unlock(data, CURL_LOCK_DATA_DNS); - - if(!dns) { - Curl_freeaddrinfo(head); - return CURLE_OUT_OF_MEMORY; - } -#ifndef CURL_DISABLE_VERBOSE_STRINGS - infof(data, "Added %.*s:%d:%s to DNS cache%s", - (int)hlen, host_begin, port, addresses, - permanent ? "" : " (non-permanent)"); -#endif - - /* Wildcard hostname */ - if((hlen == 1) && (host_begin[0] == '*')) { - infof(data, "RESOLVE *:%d using wildcard", port); - data->state.wildcard_resolve = TRUE; - } - } - } - data->state.resolve = NULL; /* dealt with now */ - - return CURLE_OK; -} - -#ifndef CURL_DISABLE_VERBOSE_STRINGS -static void show_resolve_info(struct Curl_easy *data, - struct Curl_dns_entry *dns) -{ - struct Curl_addrinfo *a; - CURLcode result = CURLE_OK; -#ifdef CURLRES_IPV6 - struct dynbuf out[2]; -#else - struct dynbuf out[1]; -#endif - DEBUGASSERT(data); - DEBUGASSERT(dns); - - if(!data->set.verbose || - /* ignore no name or numerical IP addresses */ - !dns->hostname[0] || Curl_host_is_ipnum(dns->hostname)) - return; - - a = dns->addr; - - infof(data, "Host %s:%d was resolved.", - (dns->hostname[0] ? dns->hostname : "(none)"), dns->hostport); - - Curl_dyn_init(&out[0], 1024); -#ifdef CURLRES_IPV6 - Curl_dyn_init(&out[1], 1024); -#endif - - while(a) { - if( -#ifdef CURLRES_IPV6 - a->ai_family == PF_INET6 || -#endif - a->ai_family == PF_INET) { - char buf[MAX_IPADR_LEN]; - struct dynbuf *d = &out[(a->ai_family != PF_INET)]; - Curl_printable_address(a, buf, sizeof(buf)); - if(Curl_dyn_len(d)) - result = Curl_dyn_addn(d, ", ", 2); - if(!result) - result = Curl_dyn_add(d, buf); - if(result) { - infof(data, "too many IP, cannot show"); - goto fail; - } - } - a = a->ai_next; - } - -#ifdef CURLRES_IPV6 - infof(data, "IPv6: %s", - (Curl_dyn_len(&out[1]) ? Curl_dyn_ptr(&out[1]) : "(none)")); -#endif - infof(data, "IPv4: %s", - (Curl_dyn_len(&out[0]) ? Curl_dyn_ptr(&out[0]) : "(none)")); - -fail: - Curl_dyn_free(&out[0]); -#ifdef CURLRES_IPV6 - Curl_dyn_free(&out[1]); -#endif -} -#endif - -CURLcode Curl_resolv_check(struct Curl_easy *data, - struct Curl_dns_entry **dns) -{ - CURLcode result; -#if defined(CURL_DISABLE_DOH) && !defined(CURLRES_ASYNCH) - (void)data; - (void)dns; -#endif -#ifndef CURL_DISABLE_DOH - if(data->conn->bits.doh) { - result = Curl_doh_is_resolved(data, dns); - } - else -#endif - result = Curl_resolver_is_resolved(data, dns); - if(*dns) - show_resolve_info(data, *dns); - return result; -} - -int Curl_resolv_getsock(struct Curl_easy *data, - curl_socket_t *socks) -{ -#ifdef CURLRES_ASYNCH -#ifndef CURL_DISABLE_DOH - if(data->conn->bits.doh) - /* nothing to wait for during DoH resolve, those handles have their own - sockets */ - return GETSOCK_BLANK; -#endif - return Curl_resolver_getsock(data, socks); -#else - (void)data; - (void)socks; - return GETSOCK_BLANK; -#endif -} - -/* Call this function after Curl_connect() has returned async=TRUE and - then a successful name resolve has been received. - - Note: this function disconnects and frees the conn data in case of - resolve failure */ -CURLcode Curl_once_resolved(struct Curl_easy *data, bool *protocol_done) -{ - CURLcode result; - struct connectdata *conn = data->conn; - -#ifdef USE_CURL_ASYNC - if(data->state.async.dns) { - conn->dns_entry = data->state.async.dns; - data->state.async.dns = NULL; - } -#endif - - result = Curl_setup_conn(data, protocol_done); - - if(result) { - Curl_detach_connection(data); - Curl_cpool_disconnect(data, conn, TRUE); - } - return result; -} - -/* - * Curl_resolver_error() calls failf() with the appropriate message after a - * resolve error - */ - -#ifdef USE_CURL_ASYNC -CURLcode Curl_resolver_error(struct Curl_easy *data) -{ - const char *host_or_proxy; - CURLcode result; - -#ifndef CURL_DISABLE_PROXY - struct connectdata *conn = data->conn; - if(conn->bits.httpproxy) { - host_or_proxy = "proxy"; - result = CURLE_COULDNT_RESOLVE_PROXY; - } - else -#endif - { - host_or_proxy = "host"; - result = CURLE_COULDNT_RESOLVE_HOST; - } - - failf(data, "Could not resolve %s: %s", host_or_proxy, - data->state.async.hostname); - - return result; -} -#endif /* USE_CURL_ASYNC */ diff --git a/registry/native/c/curl-upstream-overlay/lib/hostip.h b/registry/native/c/curl-upstream-overlay/lib/hostip.h deleted file mode 100644 index c6a49718d6..0000000000 --- a/registry/native/c/curl-upstream-overlay/lib/hostip.h +++ /dev/null @@ -1,270 +0,0 @@ -#ifndef HEADER_CURL_HOSTIP_H -#define HEADER_CURL_HOSTIP_H -/*************************************************************************** - * _ _ ____ _ - * Project ___| | | | _ \| | - * / __| | | | |_) | | - * | (__| |_| | _ <| |___ - * \___|\___/|_| \_\_____| - * - * Copyright (C) Daniel Stenberg, , et al. - * - * This software is licensed as described in the file COPYING, which - * you should have received as part of this distribution. The terms - * are also available at https://curl.se/docs/copyright.html. - * - * You may opt to use, copy, modify, merge, publish, distribute and/or sell - * copies of the Software, and permit persons to whom the Software is - * furnished to do so, under the terms of the COPYING file. - * - * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY - * KIND, either express or implied. - * - * SPDX-License-Identifier: curl - * - ***************************************************************************/ - -#include "curl_setup.h" -#include "hash.h" -#include "curl_addrinfo.h" -#include "timeval.h" /* for timediff_t */ -#include "asyn.h" - -/* setjmp.h requires WASM exception handling — not available on WASI */ -#ifndef __wasi__ -#include -#endif - -#ifdef USE_HTTPSRR -# include -#endif - -/* Allocate enough memory to hold the full name information structs and - * everything. OSF1 is known to require at least 8872 bytes. The buffer - * required for storing all possible aliases and IP numbers is according to - * Stevens' Unix Network Programming 2nd edition, p. 304: 8192 bytes! - */ -#define CURL_HOSTENT_SIZE 9000 - -#define CURL_TIMEOUT_RESOLVE 300 /* when using asynch methods, we allow this - many seconds for a name resolve */ - -#define CURL_ASYNC_SUCCESS CURLE_OK - -struct addrinfo; -struct hostent; -struct Curl_easy; -struct connectdata; - -/* - * Curl_global_host_cache_init() initializes and sets up a global DNS cache. - * Global DNS cache is general badness. Do not use. This will be removed in - * a future version. Use the share interface instead! - * - * Returns a struct Curl_hash pointer on success, NULL on failure. - */ -struct Curl_hash *Curl_global_host_cache_init(void); - -#ifdef USE_HTTPSRR - -#define CURL_MAXLEN_host_name 253 - -struct Curl_https_rrinfo { - size_t len; /* raw encoded length */ - unsigned char *val; /* raw encoded octets */ - /* - * fields from HTTPS RR, with the mandatory fields - * first (priority, target), then the others in the - * order of the keytag numbers defined at - * https://datatracker.ietf.org/doc/html/rfc9460#section-14.3.2 - */ - uint16_t priority; - char *target; - char *alpns; /* keytag = 1 */ - bool no_def_alpn; /* keytag = 2 */ - /* - * we do not support ports (keytag = 3) as we do not support - * port-switching yet - */ - unsigned char *ipv4hints; /* keytag = 4 */ - size_t ipv4hints_len; - unsigned char *echconfiglist; /* keytag = 5 */ - size_t echconfiglist_len; - unsigned char *ipv6hints; /* keytag = 6 */ - size_t ipv6hints_len; -}; -#endif - -struct Curl_dns_entry { - struct Curl_addrinfo *addr; -#ifdef USE_HTTPSRR - struct Curl_https_rrinfo *hinfo; -#endif - /* timestamp == 0 -- permanent CURLOPT_RESOLVE entry (does not time out) */ - time_t timestamp; - /* reference counter, entry is freed on reaching 0 */ - size_t refcount; - /* hostname port number that resolved to addr. */ - int hostport; - /* hostname that resolved to addr. may be NULL (Unix domain sockets). */ - char hostname[1]; -}; - -bool Curl_host_is_ipnum(const char *hostname); - -/* - * Curl_resolv() returns an entry with the info for the specified host - * and port. - * - * The returned data *MUST* be "released" with Curl_resolv_unlink() after - * use, or we will leak memory! - */ -/* return codes */ -enum resolve_t { - CURLRESOLV_TIMEDOUT = -2, - CURLRESOLV_ERROR = -1, - CURLRESOLV_RESOLVED = 0, - CURLRESOLV_PENDING = 1 -}; -enum resolve_t Curl_resolv(struct Curl_easy *data, - const char *hostname, - int port, - bool allowDOH, - struct Curl_dns_entry **dnsentry); -enum resolve_t Curl_resolv_timeout(struct Curl_easy *data, - const char *hostname, int port, - struct Curl_dns_entry **dnsentry, - timediff_t timeoutms); - -#ifdef USE_IPV6 -/* - * Curl_ipv6works() returns TRUE if IPv6 seems to work. - */ -bool Curl_ipv6works(struct Curl_easy *data); -#else -#define Curl_ipv6works(x) FALSE -#endif - -/* - * Curl_ipvalid() checks what CURL_IPRESOLVE_* requirements that might've - * been set and returns TRUE if they are OK. - */ -bool Curl_ipvalid(struct Curl_easy *data, struct connectdata *conn); - - -/* - * Curl_getaddrinfo() is the generic low-level name resolve API within this - * source file. There are several versions of this function - for different - * name resolve layers (selected at build-time). They all take this same set - * of arguments - */ -struct Curl_addrinfo *Curl_getaddrinfo(struct Curl_easy *data, - const char *hostname, - int port, - int *waitp); - - -/* unlink a dns entry, potentially shared with a cache */ -void Curl_resolv_unlink(struct Curl_easy *data, - struct Curl_dns_entry **pdns); - -/* init a new dns cache */ -void Curl_init_dnscache(struct Curl_hash *hash, size_t hashsize); - -/* prune old entries from the DNS cache */ -void Curl_hostcache_prune(struct Curl_easy *data); - -/* IPv4 threadsafe resolve function used for synch and asynch builds */ -struct Curl_addrinfo *Curl_ipv4_resolve_r(const char *hostname, int port); - -CURLcode Curl_once_resolved(struct Curl_easy *data, bool *protocol_connect); - -/* - * Curl_addrinfo_callback() is used when we build with any asynch specialty. - * Handles end of async request processing. Inserts ai into hostcache when - * status is CURL_ASYNC_SUCCESS. Twiddles fields in conn to indicate async - * request completed whether successful or failed. - */ -CURLcode Curl_addrinfo_callback(struct Curl_easy *data, - int status, - struct Curl_addrinfo *ai); - -/* - * Curl_printable_address() returns a printable version of the 1st address - * given in the 'ip' argument. The result will be stored in the buf that is - * bufsize bytes big. - */ -void Curl_printable_address(const struct Curl_addrinfo *ip, - char *buf, size_t bufsize); - -/* - * Curl_fetch_addr() fetches a 'Curl_dns_entry' already in the DNS cache. - * - * Returns the Curl_dns_entry entry pointer or NULL if not in the cache. - * - * The returned data *MUST* be "released" with Curl_resolv_unlink() after - * use, or we will leak memory! - */ -struct Curl_dns_entry * -Curl_fetch_addr(struct Curl_easy *data, - const char *hostname, - int port); - -/* - * Curl_cache_addr() stores a 'Curl_addrinfo' struct in the DNS cache. - * @param permanent iff TRUE, entry will never become stale - * Returns the Curl_dns_entry entry pointer or NULL if the storage failed. - */ -struct Curl_dns_entry * -Curl_cache_addr(struct Curl_easy *data, struct Curl_addrinfo *addr, - const char *hostname, size_t hostlen, int port, - bool permanent); - -#ifndef INADDR_NONE -#define CURL_INADDR_NONE (in_addr_t) ~0 -#else -#define CURL_INADDR_NONE INADDR_NONE -#endif - -/* - * Function provided by the resolver backend to set DNS servers to use. - */ -CURLcode Curl_set_dns_servers(struct Curl_easy *data, char *servers); - -/* - * Function provided by the resolver backend to set - * outgoing interface to use for DNS requests - */ -CURLcode Curl_set_dns_interface(struct Curl_easy *data, - const char *interf); - -/* - * Function provided by the resolver backend to set - * local IPv4 address to use as source address for DNS requests - */ -CURLcode Curl_set_dns_local_ip4(struct Curl_easy *data, - const char *local_ip4); - -/* - * Function provided by the resolver backend to set - * local IPv6 address to use as source address for DNS requests - */ -CURLcode Curl_set_dns_local_ip6(struct Curl_easy *data, - const char *local_ip6); - -/* - * Clean off entries from the cache - */ -void Curl_hostcache_clean(struct Curl_easy *data, struct Curl_hash *hash); - -/* - * Populate the cache with specified entries from CURLOPT_RESOLVE. - */ -CURLcode Curl_loadhostpairs(struct Curl_easy *data); -CURLcode Curl_resolv_check(struct Curl_easy *data, - struct Curl_dns_entry **dns); -int Curl_resolv_getsock(struct Curl_easy *data, - curl_socket_t *socks); - -CURLcode Curl_resolver_error(struct Curl_easy *data); -#endif /* HEADER_CURL_HOSTIP_H */ diff --git a/registry/native/c/curl-upstream-overlay/lib/vtls/wasi_tls.c b/registry/native/c/curl-upstream-overlay/lib/vtls/wasi_tls.c deleted file mode 100644 index a6a59e1ee7..0000000000 --- a/registry/native/c/curl-upstream-overlay/lib/vtls/wasi_tls.c +++ /dev/null @@ -1,187 +0,0 @@ -/* - * WASI TLS backend for libcurl - * - * Uses the host_net WASM import net_tls_connect() to upgrade a TCP - * socket to TLS. The host runtime (Node.js tls.connect()) handles all - * cryptographic operations transparently — after the upgrade, regular - * send()/recv() carry encrypted traffic. This backend therefore just - * calls the host import during connect and passes data through to the - * socket layer for send/recv. - */ - -#include "curl_setup.h" - -#ifdef USE_WASI_TLS - -#include - -#include "urldata.h" -#include "sendf.h" -#include "vtls.h" -#include "vtls_int.h" -#include "connect.h" -#include "select.h" -#include "curl_printf.h" - -/* The last #include files should be: */ -#include "curl_memory.h" -#include "memdebug.h" - -/* WASM import: upgrade a TCP socket FD to TLS. - * flags: 0 = verify peer certificate (default) - * 1 = skip peer certificate verification (-k / --insecure) - * Returns 0 on success, errno on failure. */ -__attribute__((import_module("host_net"), import_name("net_tls_connect"))) -extern int __wasi_net_tls_connect(int fd, const char *hostname_ptr, - int hostname_len, int flags); - -/* Per-connection backend data (minimal — host handles all TLS state) */ -struct wasi_tls_backend_data { - int dummy; /* struct must be non-empty */ -}; - -static size_t wasi_tls_version(char *buffer, size_t size) -{ - return msnprintf(buffer, size, "WASI-TLS/host"); -} - -static CURLcode wasi_tls_connect_blocking(struct Curl_cfilter *cf, - struct Curl_easy *data) -{ - struct ssl_connect_data *connssl = cf->ctx; - curl_socket_t sockfd = Curl_conn_cf_get_socket(cf, data); - const char *hostname = connssl->peer.hostname; - int flags = 0; - - /* Check if peer verification is disabled (curl -k / --insecure) */ - struct ssl_primary_config *conn_config = Curl_ssl_cf_get_primary_config(cf); - if(conn_config && !conn_config->verifypeer) { - flags = 1; - } - - int ret = __wasi_net_tls_connect((int)sockfd, hostname, - (int)strlen(hostname), flags); - if(ret != 0) { - failf(data, "WASI TLS: host TLS connect failed (errno %d)", ret); - return CURLE_SSL_CONNECT_ERROR; - } - - connssl->state = ssl_connection_complete; - return CURLE_OK; -} - -static CURLcode wasi_tls_connect_nonblocking(struct Curl_cfilter *cf, - struct Curl_easy *data, - bool *done) -{ - /* Our host TLS connect is synchronous — just call blocking version */ - CURLcode result = wasi_tls_connect_blocking(cf, data); - *done = (result == CURLE_OK); - return result; -} - -/* recv: pass through to the socket layer — host handles decryption */ -static ssize_t wasi_tls_recv(struct Curl_cfilter *cf, - struct Curl_easy *data, - char *buf, size_t len, CURLcode *err) -{ - return Curl_conn_cf_recv(cf->next, data, buf, len, err); -} - -/* send: pass through to the socket layer — host handles encryption */ -static ssize_t wasi_tls_send(struct Curl_cfilter *cf, - struct Curl_easy *data, - const void *buf, size_t len, CURLcode *err) -{ - return Curl_conn_cf_send(cf->next, data, buf, len, FALSE, err); -} - -static CURLcode wasi_tls_shutdown(struct Curl_cfilter *cf, - struct Curl_easy *data, - bool send_shutdown, bool *done) -{ - (void)cf; - (void)data; - (void)send_shutdown; - *done = TRUE; - return CURLE_OK; -} - -static bool wasi_tls_data_pending(struct Curl_cfilter *cf, - const struct Curl_easy *data) -{ - (void)cf; - (void)data; - return FALSE; -} - -/* data may be NULL */ -static CURLcode wasi_tls_random(struct Curl_easy *data, - unsigned char *entropy, size_t length) -{ - size_t offset = 0; - (void)data; - - while(offset < length) { - size_t chunk = length - offset; - if(chunk > 256) - chunk = 256; - - if(getentropy(entropy + offset, chunk) != 0) - return CURLE_FAILED_INIT; - - offset += chunk; - } - - return CURLE_OK; -} - -static void wasi_tls_close(struct Curl_cfilter *cf, struct Curl_easy *data) -{ - (void)cf; - (void)data; - /* Host-side TLS is cleaned up when the socket is closed */ -} - -static void *wasi_tls_get_internals(struct ssl_connect_data *connssl, - CURLINFO info) -{ - (void)connssl; - (void)info; - return NULL; -} - -const struct Curl_ssl Curl_ssl_wasi_tls = { - { CURLSSLBACKEND_NONE, "wasi-tls" }, /* info */ - - 0, /* supports — no special features */ - - sizeof(struct wasi_tls_backend_data), - - Curl_none_init, /* init */ - Curl_none_cleanup, /* cleanup */ - wasi_tls_version, /* version */ - Curl_none_check_cxn, /* check_cxn */ - wasi_tls_shutdown, /* shutdown */ - wasi_tls_data_pending, /* data_pending */ - wasi_tls_random, /* random */ - Curl_none_cert_status_request, /* cert_status_request */ - wasi_tls_connect_blocking, /* connect */ - wasi_tls_connect_nonblocking, /* connect_nonblocking */ - Curl_ssl_adjust_pollset, /* adjust_pollset */ - wasi_tls_get_internals, /* get_internals */ - wasi_tls_close, /* close */ - Curl_none_close_all, /* close_all */ - Curl_none_set_engine, /* set_engine */ - Curl_none_set_engine_default, /* set_engine_default */ - Curl_none_engines_list, /* engines_list */ - Curl_none_false_start, /* false_start */ - NULL, /* sha256sum */ - NULL, /* associate_connection */ - NULL, /* disassociate_connection */ - wasi_tls_recv, /* recv decrypted data */ - wasi_tls_send, /* send data to encrypt */ - NULL, /* get_channel_binding */ -}; - -#endif /* USE_WASI_TLS */ diff --git a/registry/native/c/curl-upstream-overlay/lib/vtls/wasi_tls.h b/registry/native/c/curl-upstream-overlay/lib/vtls/wasi_tls.h deleted file mode 100644 index d75fdf1d93..0000000000 --- a/registry/native/c/curl-upstream-overlay/lib/vtls/wasi_tls.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef HEADER_CURL_WASI_TLS_H -#define HEADER_CURL_WASI_TLS_H -/* - * WASI TLS backend for libcurl - * - * Delegates TLS to the host runtime via the host_net WASM import - * net_tls_connect(). After the host upgrades the socket, regular - * send()/recv() transparently carry encrypted traffic, so the - * backend's recv_plain/send_plain are simple pass-throughs. - */ - -#include "curl_setup.h" - -#ifdef USE_WASI_TLS -extern const struct Curl_ssl Curl_ssl_wasi_tls; -#endif - -#endif /* HEADER_CURL_WASI_TLS_H */ diff --git a/registry/native/c/include/net/if.h b/registry/native/c/include/net/if.h deleted file mode 100644 index 0a03c72f2d..0000000000 --- a/registry/native/c/include/net/if.h +++ /dev/null @@ -1,12 +0,0 @@ -#ifndef REGISTRY_NATIVE_C_INCLUDE_NET_IF_H -#define REGISTRY_NATIVE_C_INCLUDE_NET_IF_H - -#ifndef IFNAMSIZ -#define IFNAMSIZ 16 -#endif - -#ifndef IF_NAMESIZE -#define IF_NAMESIZE IFNAMSIZ -#endif - -#endif diff --git a/registry/native/c/include/posix_spawn_compat.h b/registry/native/c/include/posix_spawn_compat.h deleted file mode 100644 index 6186535302..0000000000 --- a/registry/native/c/include/posix_spawn_compat.h +++ /dev/null @@ -1,69 +0,0 @@ -/* posix_spawn_compat.h -- shared posix_spawn types and declarations for WASI - * - * wasi-libc does not ship spawn.h or sys/wait.h, so programs that use - * posix_spawn need forward declarations. This header consolidates them - * in one place so every C test program can #include it instead of - * duplicating the typedefs inline. - * - * On non-WASI targets the real , , and - * are included instead. - */ -#ifndef POSIX_SPAWN_COMPAT_H -#define POSIX_SPAWN_COMPAT_H - -#ifdef __wasi__ - -#include /* pid_t */ - -/* --- Types ------------------------------------------------------------ */ - -/* Layout MUST match the patched wasi-libc posix_spawn implementation. */ -typedef struct { int __pad0[2]; void *__actions; int __pad[16]; } posix_spawn_file_actions_t; -typedef struct { int __dummy; } posix_spawnattr_t; - -/* --- posix_spawn ------------------------------------------------------ */ - -int posix_spawnp(pid_t *restrict, const char *restrict, - const posix_spawn_file_actions_t *, - const posix_spawnattr_t *restrict, - char *const[restrict], char *const[restrict]); - -/* --- file_actions API ------------------------------------------------- */ - -int posix_spawn_file_actions_init(posix_spawn_file_actions_t *); -int posix_spawn_file_actions_destroy(posix_spawn_file_actions_t *); -int posix_spawn_file_actions_adddup2(posix_spawn_file_actions_t *, int, int); -int posix_spawn_file_actions_addclose(posix_spawn_file_actions_t *, int); - -/* --- waitpid / wait --------------------------------------------------- */ - -pid_t waitpid(pid_t, int *, int); -pid_t wait(int *); - -/* --- wait status macros ----------------------------------------------- */ - -#define WEXITSTATUS(s) (((s) & 0xff00) >> 8) -#define WIFEXITED(s) (!((s) & 0x7f)) -#define WIFSIGNALED(s) (((s) & 0x7f) != 0 && ((s) & 0x7f) != 0x7f) -#define WTERMSIG(s) ((s) & 0x7f) - -/* --- signals ---------------------------------------------------------- */ - -int kill(pid_t, int); - -#ifndef SIGKILL -#define SIGKILL 9 -#endif -#ifndef SIGTERM -#define SIGTERM 15 -#endif - -#else /* !__wasi__ */ - -#include -#include -#include - -#endif /* __wasi__ */ - -#endif /* POSIX_SPAWN_COMPAT_H */ diff --git a/registry/native/c/programs/http_get.c b/registry/native/c/programs/http_get.c deleted file mode 100644 index 0e7dc466ea..0000000000 --- a/registry/native/c/programs/http_get.c +++ /dev/null @@ -1,95 +0,0 @@ -/* http_get.c — connect to an HTTP server, send GET request, print response body */ -#include -#include -#include -#include -#include -#include -#include - -int main(int argc, char *argv[]) { - if (argc < 2) { - fprintf(stderr, "usage: http_get [path] [output_file]\n"); - return 1; - } - - int port = atoi(argv[1]); - const char *path = argc >= 3 ? argv[2] : "/"; - const char *output_file = argc >= 4 ? argv[3] : NULL; - - int fd = socket(AF_INET, SOCK_STREAM, 0); - if (fd < 0) { - perror("socket"); - return 1; - } - - struct sockaddr_in addr; - memset(&addr, 0, sizeof(addr)); - addr.sin_family = AF_INET; - addr.sin_port = htons((uint16_t)port); - inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr); - - if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) { - perror("connect"); - close(fd); - return 1; - } - - char request[1024]; - int request_len = snprintf( - request, - sizeof(request), - "GET %s HTTP/1.0\r\nHost: localhost\r\n\r\n", - path - ); - if (request_len < 0 || (size_t)request_len >= sizeof(request)) { - fprintf(stderr, "request path too long\n"); - close(fd); - return 1; - } - - ssize_t sent = send(fd, request, (size_t)request_len, 0); - if (sent < 0) { - perror("send"); - close(fd); - return 1; - } - - /* Read full response */ - char response[4096]; - size_t total = 0; - ssize_t n; - while ((n = recv(fd, response + total, sizeof(response) - total - 1, 0)) > 0) { - total += (size_t)n; - } - response[total] = '\0'; - - close(fd); - - /* Find body after \r\n\r\n */ - const char *body = strstr(response, "\r\n\r\n"); - if (body) { - body += 4; - if (output_file) { - FILE *out = fopen(output_file, "wb"); - if (!out) { - perror("fopen"); - return 1; - } - size_t body_len = total - (size_t)(body - response); - if (fwrite(body, 1, body_len, out) != body_len) { - perror("fwrite"); - fclose(out); - return 1; - } - fclose(out); - } else { - printf("body: %s\n", body); - } - } else { - printf("body: (no separator found)\n"); - return 1; - } - - return 0; -} diff --git a/registry/native/c/programs/spawn_child.c b/registry/native/c/programs/spawn_child.c deleted file mode 100644 index b174aad1b5..0000000000 --- a/registry/native/c/programs/spawn_child.c +++ /dev/null @@ -1,53 +0,0 @@ -/* spawn_child.c -- posix_spawn 'echo hello', waitpid, print child stdout */ -#include -#include -#include -#include - -#include "posix_spawn_compat.h" - -extern char **environ; - -int main(void) { - int pipefd[2]; - if (pipe(pipefd) != 0) { - perror("pipe"); - return 1; - } - - /* Redirect child stdout to pipe write end */ - posix_spawn_file_actions_t fa; - posix_spawn_file_actions_init(&fa); - posix_spawn_file_actions_adddup2(&fa, pipefd[1], STDOUT_FILENO); - posix_spawn_file_actions_addclose(&fa, pipefd[0]); - posix_spawn_file_actions_addclose(&fa, pipefd[1]); - - char *argv[] = {"echo", "hello", NULL}; - pid_t child; - int err = posix_spawnp(&child, "echo", &fa, NULL, argv, environ); - posix_spawn_file_actions_destroy(&fa); - - if (err != 0) { - fprintf(stderr, "posix_spawn failed: %d\n", err); - return 1; - } - - close(pipefd[1]); - - char buf[256]; - ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1); - close(pipefd[0]); - - int status; - waitpid(child, &status, 0); - - if (n > 0) { - buf[n] = '\0'; - printf("child_stdout: %s", buf); - } else { - printf("child_stdout: (empty)\n"); - } - printf("child_exit: %d\n", WIFEXITED(status) ? WEXITSTATUS(status) : -1); - - return 0; -} diff --git a/registry/native/c/programs/sqlite3_cli.c b/registry/native/c/programs/sqlite3_cli.c deleted file mode 100644 index ba878b956f..0000000000 --- a/registry/native/c/programs/sqlite3_cli.c +++ /dev/null @@ -1,558 +0,0 @@ -/* sqlite3_cli — SQLite CLI for WasmVM - * - * Full sqlite3 CLI supporting: - * sqlite3 :memory: — interactive/piped in-memory DB - * sqlite3 /path/to/db.sqlite — file-based DB via WASI VFS - * echo 'SELECT 1;' | sqlite3 — stdin pipe mode (defaults to :memory:) - * sqlite3 :memory: "SELECT 1;" — SQL from command line argument - * - * Meta-commands: .dump, .schema, .tables, .quit, .help, .headers, .mode - * - * Compiled with -DSQLITE_OS_OTHER — uses a custom VFS wrapping WASI file I/O. - */ -#include -#include -#include -#include -#include -#include -#include -#include "sqlite3.h" - -/* ── WASI VFS ────────────────────────────────────────────────────────── */ - -/* Minimal VFS implementation using POSIX file I/O (supported by WASI) */ - -typedef struct WasiFile { - sqlite3_file base; - int fd; -} WasiFile; - -static int wasiClose(sqlite3_file *pFile) { - WasiFile *p = (WasiFile *)pFile; - if (p->fd >= 0) close(p->fd); - p->fd = -1; - return SQLITE_OK; -} - -static int wasiRead(sqlite3_file *pFile, void *zBuf, int iAmt, sqlite3_int64 iOfst) { - WasiFile *p = (WasiFile *)pFile; - if (lseek(p->fd, (off_t)iOfst, SEEK_SET) != (off_t)iOfst) - return SQLITE_IOERR_READ; - ssize_t got = read(p->fd, zBuf, iAmt); - if (got == iAmt) return SQLITE_OK; - if (got >= 0) { - memset((char *)zBuf + got, 0, iAmt - got); - return SQLITE_IOERR_SHORT_READ; - } - return SQLITE_IOERR_READ; -} - -static int wasiWrite(sqlite3_file *pFile, const void *zBuf, int iAmt, sqlite3_int64 iOfst) { - WasiFile *p = (WasiFile *)pFile; - if (lseek(p->fd, (off_t)iOfst, SEEK_SET) != (off_t)iOfst) - return SQLITE_IOERR_WRITE; - ssize_t wrote = write(p->fd, zBuf, iAmt); - if (wrote == iAmt) return SQLITE_OK; - return SQLITE_IOERR_WRITE; -} - -static int wasiTruncate(sqlite3_file *pFile, sqlite3_int64 size) { - WasiFile *p = (WasiFile *)pFile; - if (ftruncate(p->fd, (off_t)size) != 0) return SQLITE_IOERR_TRUNCATE; - return SQLITE_OK; -} - -static int wasiSync(sqlite3_file *pFile, int flags) { - (void)flags; - WasiFile *p = (WasiFile *)pFile; - if (fsync(p->fd) != 0) return SQLITE_IOERR_FSYNC; - return SQLITE_OK; -} - -static int wasiFileSize(sqlite3_file *pFile, sqlite3_int64 *pSize) { - WasiFile *p = (WasiFile *)pFile; - struct stat st; - if (fstat(p->fd, &st) != 0) return SQLITE_IOERR_FSTAT; - *pSize = (sqlite3_int64)st.st_size; - return SQLITE_OK; -} - -/* No locking in WASM — single process */ -static int wasiLock(sqlite3_file *f, int l) { (void)f; (void)l; return SQLITE_OK; } -static int wasiUnlock(sqlite3_file *f, int l) { (void)f; (void)l; return SQLITE_OK; } -static int wasiCheckReservedLock(sqlite3_file *f, int *pResOut) { - (void)f; *pResOut = 0; return SQLITE_OK; -} -static int wasiFileControl(sqlite3_file *f, int op, void *pArg) { - (void)f; (void)op; (void)pArg; return SQLITE_NOTFOUND; -} -static int wasiSectorSize(sqlite3_file *f) { (void)f; return 512; } -static int wasiDeviceCharacteristics(sqlite3_file *f) { (void)f; return 0; } - -static const sqlite3_io_methods wasiIoMethods __attribute__((used)) = { - 1, /* iVersion */ - wasiClose, - wasiRead, - wasiWrite, - wasiTruncate, - wasiSync, - wasiFileSize, - wasiLock, - wasiUnlock, - wasiCheckReservedLock, - wasiFileControl, - wasiSectorSize, - wasiDeviceCharacteristics, - /* v2+ methods */ - 0, 0, 0, 0 -}; - -static int wasiOpen(sqlite3_vfs *pVfs, sqlite3_filename zName, sqlite3_file *pFile, - int flags, int *pOutFlags) { - (void)pVfs; - WasiFile *p = (WasiFile *)pFile; - p->fd = -1; - - if (!zName) { - /* Temp file — use in-memory only */ - return SQLITE_IOERR; - } - - int oflags = 0; - if (flags & SQLITE_OPEN_CREATE) oflags |= O_CREAT; - if (flags & SQLITE_OPEN_READWRITE) oflags |= O_RDWR; - else oflags |= O_RDONLY; - - p->fd = open(zName, oflags, 0644); - if (p->fd < 0) return SQLITE_CANTOPEN; - - if (pOutFlags) *pOutFlags = flags; - p->base.pMethods = &wasiIoMethods; - return SQLITE_OK; -} - -static int wasiDelete(sqlite3_vfs *pVfs, const char *zName, int syncDir) { - (void)pVfs; (void)syncDir; - if (unlink(zName) != 0 && errno != ENOENT) return SQLITE_IOERR_DELETE; - return SQLITE_OK; -} - -static int wasiAccess(sqlite3_vfs *pVfs, const char *zName, int flags, int *pResOut) { - (void)pVfs; - struct stat st; - *pResOut = (stat(zName, &st) == 0) ? 1 : 0; - (void)flags; - return SQLITE_OK; -} - -static int wasiFullPathname(sqlite3_vfs *pVfs, const char *zName, int nOut, char *zOut) { - (void)pVfs; - if (zName[0] == '/') { - snprintf(zOut, nOut, "%s", zName); - } else { - /* Relative path — prefix with / since VFS root is / */ - snprintf(zOut, nOut, "/%s", zName); - } - return SQLITE_OK; -} - -static int wasiRandomness(sqlite3_vfs *pVfs, int nByte, char *zOut) { - (void)pVfs; - int fd = open("/dev/urandom", O_RDONLY); - if (fd >= 0) { - int total = 0; - while (total < nByte) { - ssize_t read_len = read(fd, zOut + total, (size_t)(nByte - total)); - if (read_len <= 0) { - break; - } - total += (int)read_len; - } - close(fd); - if (total == nByte) { - return nByte; - } - nByte = total; - } - - /* Fallback only if urandom is unexpectedly unavailable in the runtime. */ - for (int i = 0; i < nByte; i++) zOut[i] = (char)(i * 37 + 17); - return nByte; -} - -static int wasiSleep(sqlite3_vfs *pVfs, int microseconds) { - (void)pVfs; (void)microseconds; - return 0; -} - -static int wasiCurrentTime(sqlite3_vfs *pVfs, double *pTime) { - (void)pVfs; - /* Julian day for Unix epoch 0 */ - *pTime = 2440587.5; - return SQLITE_OK; -} - -static int wasiGetLastError(sqlite3_vfs *pVfs, int nBuf, char *zBuf) { - (void)pVfs; - if (nBuf > 0) zBuf[0] = 0; - return 0; -} - -static sqlite3_vfs wasiVfs __attribute__((used)) = { - 1, /* iVersion */ - sizeof(WasiFile), /* szOsFile */ - 512, /* mxPathname */ - 0, /* pNext */ - "wasi", /* zName */ - 0, /* pAppData */ - wasiOpen, - wasiDelete, - wasiAccess, - wasiFullPathname, - 0, 0, 0, 0, /* xDlOpen, xDlError, xDlSym, xDlClose */ - wasiRandomness, - wasiSleep, - wasiCurrentTime, - wasiGetLastError, - /* v2+ */ - 0, 0, 0, 0 -}; - -#ifdef SQLITE_OS_OTHER -int sqlite3_os_init(void) { - return sqlite3_vfs_register(&wasiVfs, 1); -} - -int sqlite3_os_end(void) { - return SQLITE_OK; -} -#endif /* SQLITE_OS_OTHER */ - -/* ── CLI ─────────────────────────────────────────────────────────────── */ - -static int headers_enabled = 0; -static int headers_printed = 0; -static char output_sep_buf[8]; -static char *output_separator = output_sep_buf; - -/* Callback for sqlite3_exec — prints rows in column format */ -static int print_row(void *unused, int ncols, char **values, char **names) { - (void)unused; - if (headers_enabled && !headers_printed) { - for (int i = 0; i < ncols; i++) { - if (i > 0) printf("%s", output_separator); - printf("%s", names[i]); - } - printf("\n"); - headers_printed = 1; - } - for (int i = 0; i < ncols; i++) { - if (i > 0) printf("%s", output_separator); - printf("%s", values[i] ? values[i] : ""); - } - printf("\n"); - return 0; -} - -/* Execute SQL and report errors */ -static int exec_sql(sqlite3 *db, const char *sql) { - char *err = NULL; - headers_printed = 0; /* Reset per query */ - int rc = sqlite3_exec(db, sql, print_row, NULL, &err); - if (rc != SQLITE_OK) { - fprintf(stderr, "Error: %s\n", err ? err : sqlite3_errmsg(db)); - sqlite3_free(err); - return 1; - } - return 0; -} - -/* Handle dot-commands */ -static int handle_meta(sqlite3 *db, const char *line) { - while (*line == ' ' || *line == '\t') line++; - - if (strcmp(line, ".quit") == 0 || strcmp(line, ".exit") == 0) { - return -1; /* Signal exit */ - } - if (strcmp(line, ".help") == 0) { - printf(".dump Dump database as SQL\n"); - printf(".headers on|off Toggle column headers\n"); - printf(".help Show this help\n"); - printf(".mode MODE Set output mode (list, csv)\n"); - printf(".quit Exit\n"); - printf(".schema Show CREATE statements\n"); - printf(".tables List tables\n"); - return 0; - } - if (strcmp(line, ".tables") == 0) { - return exec_sql(db, - "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1;"); - } - if (strcmp(line, ".schema") == 0) { - return exec_sql(db, - "SELECT sql||';' FROM sqlite_master WHERE sql IS NOT NULL ORDER BY 1;"); - } - if (strcmp(line, ".dump") == 0) { - printf("BEGIN TRANSACTION;\n"); - /* Schema */ - exec_sql(db, - "SELECT sql||';' FROM sqlite_master WHERE sql IS NOT NULL " - "ORDER BY CASE WHEN type='table' THEN 0 ELSE 1 END, name;"); - /* Data — generate INSERT statements for each table */ - sqlite3_stmt *stmt; - int rc = sqlite3_prepare_v2(db, - "SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1;", - -1, &stmt, NULL); - if (rc == SQLITE_OK) { - while (sqlite3_step(stmt) == SQLITE_ROW) { - const char *table = (const char *)sqlite3_column_text(stmt, 0); - char query[1024]; - snprintf(query, sizeof(query), "SELECT * FROM \"%s\"", table); - - sqlite3_stmt *data_stmt; - rc = sqlite3_prepare_v2(db, query, -1, &data_stmt, NULL); - if (rc == SQLITE_OK) { - int ncols = sqlite3_column_count(data_stmt); - while (sqlite3_step(data_stmt) == SQLITE_ROW) { - printf("INSERT INTO \"%s\" VALUES(", table); - for (int i = 0; i < ncols; i++) { - if (i > 0) printf(","); - int type = sqlite3_column_type(data_stmt, i); - if (type == SQLITE_NULL) { - printf("NULL"); - } else if (type == SQLITE_INTEGER) { - printf("%lld", sqlite3_column_int64(data_stmt, i)); - } else if (type == SQLITE_FLOAT) { - printf("%g", sqlite3_column_double(data_stmt, i)); - } else { - const char *text = (const char *)sqlite3_column_text(data_stmt, i); - printf("'"); - for (const char *c = text; *c; c++) { - if (*c == '\'') printf("''"); - else putchar(*c); - } - printf("'"); - } - } - printf(");\n"); - } - sqlite3_finalize(data_stmt); - } - } - sqlite3_finalize(stmt); - } - printf("COMMIT;\n"); - return 0; - } - if (strncmp(line, ".headers ", 9) == 0) { - const char *arg = line + 9; - while (*arg == ' ') arg++; - if (strcmp(arg, "on") == 0) headers_enabled = 1; - else headers_enabled = 0; - return 0; - } - if (strncmp(line, ".mode ", 6) == 0) { - const char *arg = line + 6; - while (*arg == ' ') arg++; - if (strcmp(arg, "csv") == 0) strcpy(output_sep_buf, ","); - else strcpy(output_sep_buf, "|"); - return 0; - } - - fprintf(stderr, "Error: unknown command or invalid arguments: \"%s\"\n", line); - return 0; -} - -/* Read all data from stream into a buffer */ -static char *read_all(FILE *in, size_t *out_len) { - size_t cap = 65536; - size_t len = 0; - char *buf = (char *)malloc(cap); - if (!buf) return NULL; - - int c; - while ((c = fgetc(in)) != EOF) { - if (len + 1 >= cap) { - cap *= 2; - char *nb = (char *)realloc(buf, cap); - if (!nb) { free(buf); return NULL; } - buf = nb; - } - buf[len++] = (char)c; - } - buf[len] = '\0'; - if (out_len) *out_len = len; - return buf; -} - -/* Process a line of input — either meta-command or SQL accumulation */ -static int process_line(sqlite3 *db, const char *line, size_t len, - char *sql_buf, int *sql_len, int sql_cap) { - /* Meta-commands start with . */ - if (line[0] == '.' && *sql_len == 0) { - /* Need a mutable copy for handle_meta */ - char meta_buf[1024]; - size_t ml = len < sizeof(meta_buf) - 1 ? len : sizeof(meta_buf) - 1; - memcpy(meta_buf, line, ml); - meta_buf[ml] = '\0'; - int rc = handle_meta(db, meta_buf); - if (rc < 0) return -1; /* .quit */ - return rc; - } - - /* Accumulate SQL */ - if (*sql_len + (int)len + 2 < sql_cap) { - if (*sql_len > 0) sql_buf[(*sql_len)++] = '\n'; - memcpy(sql_buf + *sql_len, line, len); - *sql_len += (int)len; - sql_buf[*sql_len] = '\0'; - } - - /* Check if statement is complete */ - if (sqlite3_complete(sql_buf)) { - int rc = exec_sql(db, sql_buf); - *sql_len = 0; - return rc; - } - return 0; -} - -/* Read and execute SQL from stdin */ -static int process_input(sqlite3 *db, FILE *in) { - size_t total = 0; - char *data = read_all(in, &total); - if (!data || total == 0) { - free(data); - return 0; - } - - char sql_buf[65536]; - int sql_len = 0; - int had_error = 0; - - /* Process input line by line */ - const char *p = data; - const char *end = data + total; - while (p < end) { - /* Find end of line */ - const char *eol = p; - while (eol < end && *eol != '\n' && *eol != '\r') eol++; - size_t len = eol - p; - - /* Skip empty lines */ - if (len > 0) { - int rc = process_line(db, p, len, sql_buf, &sql_len, (int)sizeof(sql_buf)); - if (rc < 0) break; /* .quit */ - if (rc > 0) had_error = 1; - } - - /* Skip past newline */ - p = eol; - if (p < end && *p == '\r') p++; - if (p < end && *p == '\n') p++; - } - - /* Execute any remaining SQL */ - if (sql_len > 0) { - if (exec_sql(db, sql_buf) != 0) - had_error = 1; - } - - free(data); - return had_error; -} - -static void print_usage(void) { - fprintf(stderr, "Usage: sqlite3 [OPTIONS] [FILENAME] [SQL]\n"); - fprintf(stderr, " FILENAME is the name of an SQLite database. A new database is\n"); - fprintf(stderr, " created if the file does not exist. Use \":memory:\" for in-memory.\n"); - fprintf(stderr, " SQL is an optional SQL statement to execute.\n"); - fprintf(stderr, "Options:\n"); - fprintf(stderr, " -header Turn headers on\n"); - fprintf(stderr, " -csv Set output mode to CSV\n"); - fprintf(stderr, " -separator S Set output separator (default \"|\")\n"); - fprintf(stderr, " -help Show this help\n"); - fprintf(stderr, " -version Show SQLite version\n"); -} - -int main(int argc, char **argv) { - const char *db_path = ":memory:"; - const char *sql_arg = NULL; - int positional = 0; - int i; - - /* Initialize separator at runtime — static initializers for arrays - * may not work correctly in optimized WASM builds. */ - output_sep_buf[0] = '|'; - output_sep_buf[1] = '\0'; - - /* Parse options */ - for (i = 1; i < argc; i++) { - if (argv[i][0] == '-') { - if (strcmp(argv[i], "-help") == 0 || strcmp(argv[i], "--help") == 0) { - print_usage(); - return 0; - } - if (strcmp(argv[i], "-version") == 0 || strcmp(argv[i], "--version") == 0) { - printf("%s\n", sqlite3_libversion()); - return 0; - } - if (strcmp(argv[i], "-header") == 0 || strcmp(argv[i], "-headers") == 0) { - headers_enabled = 1; - continue; - } - if (strcmp(argv[i], "-csv") == 0) { - strcpy(output_sep_buf, ","); - continue; - } - if (strcmp(argv[i], "-separator") == 0 && i + 1 < argc) { - strncpy(output_sep_buf, argv[++i], sizeof(output_sep_buf) - 1); - output_sep_buf[sizeof(output_sep_buf) - 1] = '\0'; - continue; - } - fprintf(stderr, "Error: unknown option: %s\n", argv[i]); - print_usage(); - return 1; - } - /* Positional args: [FILENAME] [SQL] */ - if (positional == 0) { - db_path = argv[i]; - positional++; - } else if (positional == 1) { - sql_arg = argv[i]; - positional++; - } else { - fprintf(stderr, "Error: too many arguments\n"); - print_usage(); - return 1; - } - } - - /* Open database */ - sqlite3 *db; - int rc = sqlite3_open(db_path, &db); - if (rc != SQLITE_OK) { - fprintf(stderr, "Error: unable to open database \"%s\": %s\n", - db_path, sqlite3_errmsg(db)); - sqlite3_close(db); - return 1; - } - - int exit_code = 0; - - if (sql_arg) { - /* Execute SQL from command line */ - exit_code = exec_sql(db, sql_arg); - } else { - /* Read from stdin */ - exit_code = process_input(db, stdin); - } - - /* Flush output */ - fflush(stdout); - fflush(stderr); - /* Skip sqlite3_close + atexit handlers — use _Exit to avoid WASM indirect - * call table traps during wasi-libc/SQLite cleanup with wasm-opt. */ - _Exit(exit_code); -} diff --git a/registry/native/c/programs/unix_socket.c b/registry/native/c/programs/unix_socket.c deleted file mode 100644 index 40080cc117..0000000000 --- a/registry/native/c/programs/unix_socket.c +++ /dev/null @@ -1,102 +0,0 @@ -/* unix_socket.c — AF_UNIX server: bind, listen, accept one connection, recv, send "pong", close */ -#include -#include -#include -#include -#include - -#ifdef __has_include -# if __has_include() -# include -# endif -#endif - -#ifndef AF_UNIX -# define AF_UNIX 1 -#endif - -#ifndef AF_LOCAL -# define AF_LOCAL AF_UNIX -#endif - -#ifndef offsetof -# define offsetof(type, member) __builtin_offsetof(type, member) -#endif - -/* Fallback if sys/un.h was not available */ -#ifndef SUN_LEN -struct sockaddr_un { - sa_family_t sun_family; - char sun_path[108]; -}; -#define SUN_LEN(su) (offsetof(struct sockaddr_un, sun_path) + strlen((su)->sun_path)) -#endif - -int main(int argc, char *argv[]) { - const char *path = "/tmp/test.sock"; - if (argc >= 2) { - path = argv[1]; - } - - int fd = socket(AF_UNIX, SOCK_STREAM, 0); - if (fd < 0) { - perror("socket"); - return 1; - } - - struct sockaddr_un addr; - memset(&addr, 0, sizeof(addr)); - addr.sun_family = AF_UNIX; - strncpy(addr.sun_path, path, sizeof(addr.sun_path) - 1); - - if (bind(fd, (struct sockaddr *)&addr, SUN_LEN(&addr)) < 0) { - perror("bind"); - close(fd); - return 1; - } - - if (listen(fd, 1) < 0) { - perror("listen"); - close(fd); - return 1; - } - - printf("listening on %s\n", path); - fflush(stdout); - - struct sockaddr_un client_addr; - socklen_t client_len = sizeof(client_addr); - int client_fd = accept(fd, (struct sockaddr *)&client_addr, &client_len); - if (client_fd < 0) { - perror("accept"); - close(fd); - return 1; - } - - char buf[256]; - ssize_t n = recv(client_fd, buf, sizeof(buf) - 1, 0); - if (n < 0) { - perror("recv"); - close(client_fd); - close(fd); - return 1; - } - buf[n] = '\0'; - - printf("received: %s\n", buf); - - const char *reply = "pong"; - ssize_t sent = send(client_fd, reply, strlen(reply), 0); - if (sent < 0) { - perror("send"); - close(client_fd); - close(fd); - return 1; - } - - printf("sent: %zd\n", sent); - - close(client_fd); - close(fd); - return 0; -} diff --git a/registry/native/c/programs/unzip.c b/registry/native/c/programs/unzip.c deleted file mode 100644 index 383e714205..0000000000 --- a/registry/native/c/programs/unzip.c +++ /dev/null @@ -1,669 +0,0 @@ -/* unzip.c — Extract ZIP archives using zlib/minizip - * - * Usage: unzip archive.zip (extract all to cwd) - * unzip -d outdir archive.zip (extract to directory) - * unzip -l archive.zip (list contents) - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include "ioapi.h" -#include "unzip.h" - -#define MAX_PATH_LEN 4096 -#define WRITE_BUF_SIZE 8192 - -/* Cap per-entry allocation in the fallback parser. Hostile central directory - * records can claim sizes up to 4 GiB; refuse anything above this bound. */ -#define MAX_UNCOMPRESSED_SIZE (256u * 1024u * 1024u) - -typedef struct { - FILE *file; - char *filename; - char mode[4]; - long position; - long size; -} unzip_file_stream; - -static voidpf ZCALLBACK unzip_open_file(voidpf opaque, const char *filename, int mode) { - unzip_file_stream *stream = NULL; - const char *mode_fopen = NULL; - (void)opaque; - - if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER) == ZLIB_FILEFUNC_MODE_READ) - mode_fopen = "rb"; - else if (mode & ZLIB_FILEFUNC_MODE_EXISTING) - mode_fopen = "r+b"; - else if (mode & ZLIB_FILEFUNC_MODE_CREATE) - mode_fopen = "wb"; - - if (filename == NULL || mode_fopen == NULL) - return NULL; - - stream = (unzip_file_stream *)calloc(1, sizeof(unzip_file_stream)); - if (!stream) - return NULL; - - stream->filename = (char *)malloc(strlen(filename) + 1); - if (!stream->filename) { - free(stream); - return NULL; - } - strcpy(stream->filename, filename); - strncpy(stream->mode, mode_fopen, sizeof(stream->mode) - 1); - stream->mode[sizeof(stream->mode) - 1] = '\0'; - - stream->file = fopen(filename, mode_fopen); - if (!stream->file) { - free(stream->filename); - free(stream); - return NULL; - } - struct stat st; - stream->size = stat(filename, &st) == 0 ? (long)st.st_size : 0; - stream->position = 0; - return stream; -} - -static uLong ZCALLBACK unzip_read_file(voidpf opaque, voidpf stream, void *buf, uLong size) { - unzip_file_stream *file_stream = (unzip_file_stream *)stream; - uLong got; - (void)opaque; - got = (uLong)fread(buf, 1, (size_t)size, file_stream->file); - file_stream->position += (long)got; - return got; -} - -static uLong ZCALLBACK unzip_write_file(voidpf opaque, voidpf stream, const void *buf, uLong size) { - unzip_file_stream *file_stream = (unzip_file_stream *)stream; - uLong wrote; - (void)opaque; - wrote = (uLong)fwrite(buf, 1, (size_t)size, file_stream->file); - file_stream->position += (long)wrote; - if (file_stream->position > file_stream->size) - file_stream->size = file_stream->position; - return wrote; -} - -static long ZCALLBACK unzip_tell_file(voidpf opaque, voidpf stream) { - unzip_file_stream *file_stream = (unzip_file_stream *)stream; - (void)opaque; - return file_stream->position; -} - -static long ZCALLBACK unzip_seek_file(voidpf opaque, voidpf stream, uLong offset, int origin) { - int fseek_origin = 0; - long seek_offset = (long)offset; - unzip_file_stream *file_stream = (unzip_file_stream *)stream; - (void)opaque; - - switch (origin) { - case ZLIB_FILEFUNC_SEEK_CUR: - seek_offset = file_stream->position + (long)offset; - fseek_origin = SEEK_SET; - break; - case ZLIB_FILEFUNC_SEEK_END: - seek_offset = file_stream->size + (long)offset; - fseek_origin = SEEK_SET; - break; - case ZLIB_FILEFUNC_SEEK_SET: - fseek_origin = SEEK_SET; - break; - default: - return -1; - } - - fclose(file_stream->file); - file_stream->file = fopen(file_stream->filename, file_stream->mode); - if (!file_stream->file) - return -1; - - if (fseek(file_stream->file, seek_offset, fseek_origin) != 0) - return -1; - clearerr(file_stream->file); - file_stream->position = seek_offset; - return 0; -} - -static int ZCALLBACK unzip_close_file(voidpf opaque, voidpf stream) { - unzip_file_stream *file_stream = (unzip_file_stream *)stream; - int ret; - (void)opaque; - ret = fclose(file_stream->file); - free(file_stream->filename); - free(file_stream); - return ret; -} - -static int ZCALLBACK unzip_error_file(voidpf opaque, voidpf stream) { - unzip_file_stream *file_stream = (unzip_file_stream *)stream; - (void)opaque; - return ferror(file_stream->file); -} - -static unzFile open_archive(const char *archive) { - zlib_filefunc_def filefunc = { - .zopen_file = unzip_open_file, - .zread_file = unzip_read_file, - .zwrite_file = unzip_write_file, - .ztell_file = unzip_tell_file, - .zseek_file = unzip_seek_file, - .zclose_file = unzip_close_file, - .zerror_file = unzip_error_file, - .opaque = NULL, - }; - return unzOpen2(archive, &filefunc); -} - -/* Ensure all parent directories of path exist */ -static int mkdirs(const char *path) { - char tmp[MAX_PATH_LEN]; - size_t len = strlen(path); - if (len >= sizeof(tmp)) return -1; - memcpy(tmp, path, len + 1); - - for (size_t i = 1; i < len; i++) { - if (tmp[i] == '/') { - tmp[i] = '\0'; - if (mkdir(tmp, 0755) != 0 && errno != EEXIST) - return -1; - tmp[i] = '/'; - } - } - return 0; -} - -static uint16_t read_le16(const unsigned char *p) { - return (uint16_t)p[0] | ((uint16_t)p[1] << 8); -} - -static uint32_t read_le32(const unsigned char *p) { - return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | - ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24); -} - -static int read_archive_bytes(const char *archive, unsigned char **out, size_t *out_len) { - FILE *f = fopen(archive, "rb"); - long size; - unsigned char *data; - if (!f) - return -1; - if (fseek(f, 0, SEEK_END) != 0) { - fclose(f); - return -1; - } - size = ftell(f); - if (size < 0 || fseek(f, 0, SEEK_SET) != 0) { - fclose(f); - return -1; - } - data = (unsigned char *)malloc((size_t)size); - if (!data) { - fclose(f); - return -1; - } - if (fread(data, 1, (size_t)size, f) != (size_t)size) { - free(data); - fclose(f); - return -1; - } - fclose(f); - *out = data; - *out_len = (size_t)size; - return 0; -} - -static int find_eocd(const unsigned char *data, size_t len, size_t *eocd_offset) { - size_t min = len > 0xffff + 22 ? len - (0xffff + 22) : 0; - if (len < 22) - return -1; - for (size_t pos = len - 22; pos + 4 <= len && pos >= min; pos--) { - if (read_le32(data + pos) == 0x06054b50) { - *eocd_offset = pos; - return 0; - } - if (pos == 0) - break; - } - return -1; -} - -static const char *entry_output_name(const char *name, size_t name_len) { - const char *end = name + name_len; - while (name < end && *name == '/') - name++; - return name; -} - -static int inflate_raw_entry(const unsigned char *src, size_t src_len, unsigned char *dst, size_t dst_len) { - z_stream stream; - memset(&stream, 0, sizeof(stream)); - stream.next_in = (Bytef *)src; - stream.avail_in = (uInt)src_len; - stream.next_out = dst; - stream.avail_out = (uInt)dst_len; - if (inflateInit2(&stream, -MAX_WBITS) != Z_OK) - return -1; - int result = inflate(&stream, Z_FINISH); - inflateEnd(&stream); - return result == Z_STREAM_END && stream.total_out == dst_len ? 0 : -1; -} - -static int simple_archive_entries(const unsigned char *data, size_t len, size_t *cd_offset, uint16_t *entry_count) { - size_t eocd; - if (len < 22 || find_eocd(data, len, &eocd) != 0 || eocd > len - 22) - return -1; - *entry_count = read_le16(data + eocd + 10); - *cd_offset = read_le32(data + eocd + 16); - return *cd_offset < len ? 0 : -1; -} - -static int simple_list_archive(const char *archive) { - unsigned char *data = NULL; - size_t len = 0; - size_t pos; - uint16_t entries; - unsigned long total_size = 0; - if (read_archive_bytes(archive, &data, &len) != 0 || - simple_archive_entries(data, len, &pos, &entries) != 0) { - free(data); - return 1; - } - - printf(" Length Name\n"); - printf("--------- ----\n"); - for (uint16_t i = 0; i < entries; i++) { - uint16_t name_len; - uint16_t extra_len; - uint16_t comment_len; - uint32_t uncompressed_size; - if (len < 46 || pos > len - 46 || read_le32(data + pos) != 0x02014b50) { - free(data); - return 1; - } - uncompressed_size = read_le32(data + pos + 24); - name_len = read_le16(data + pos + 28); - extra_len = read_le16(data + pos + 30); - comment_len = read_le16(data + pos + 32); - size_t header_len = 46 + (size_t)name_len + (size_t)extra_len + (size_t)comment_len; - if (header_len > len - pos) { - free(data); - return 1; - } - printf("%9lu %.*s\n", (unsigned long)uncompressed_size, name_len, data + pos + 46); - total_size += uncompressed_size; - pos += header_len; - } - printf("--------- ----\n"); - printf("%9lu %u file(s)\n", total_size, entries); - free(data); - return 0; -} - -static int simple_extract_archive(const char *archive, const char *outdir) { - unsigned char *data = NULL; - size_t len = 0; - size_t pos; - uint16_t entries; - int errors = 0; - if (read_archive_bytes(archive, &data, &len) != 0 || - simple_archive_entries(data, len, &pos, &entries) != 0) { - free(data); - return 1; - } - - if (outdir && mkdir(outdir, 0755) != 0 && errno != EEXIST) { - fprintf(stderr, "unzip: cannot create directory '%s': %s\n", outdir, strerror(errno)); - free(data); - return 1; - } - - for (uint16_t i = 0; i < entries; i++) { - uint16_t method; - uint16_t name_len; - uint16_t extra_len; - uint16_t comment_len; - uint16_t local_name_len; - uint16_t local_extra_len; - uint32_t compressed_size; - uint32_t uncompressed_size; - uint32_t local_offset; - size_t file_data_offset; - const char *name; - const char *safe_name; - char outpath[MAX_PATH_LEN]; - unsigned char *out = NULL; - - if (len < 46 || pos > len - 46 || read_le32(data + pos) != 0x02014b50) { - errors++; - break; - } - method = read_le16(data + pos + 10); - compressed_size = read_le32(data + pos + 20); - uncompressed_size = read_le32(data + pos + 24); - name_len = read_le16(data + pos + 28); - extra_len = read_le16(data + pos + 30); - comment_len = read_le16(data + pos + 32); - local_offset = read_le32(data + pos + 42); - size_t header_len = 46 + (size_t)name_len + (size_t)extra_len + (size_t)comment_len; - if (header_len > len - pos || (size_t)local_offset > len - 30) { - errors++; - break; - } - - name = (const char *)(data + pos + 46); - safe_name = entry_output_name(name, name_len); - size_t safe_len = (size_t)name_len - (size_t)(safe_name - name); - pos += header_len; - if (safe_len == 0) - continue; - snprintf(outpath, sizeof(outpath), "%s%s%.*s", - outdir ? outdir : "", outdir ? "/" : "", (int)safe_len, safe_name); - - size_t out_len = strlen(outpath); - if (out_len > 0 && outpath[out_len - 1] == '/') { - if (mkdir(outpath, 0755) != 0 && errno != EEXIST) - errors++; - continue; - } - if (mkdirs(outpath) != 0) { - errors++; - continue; - } - - if (read_le32(data + local_offset) != 0x04034b50) { - errors++; - continue; - } - local_name_len = read_le16(data + local_offset + 26); - local_extra_len = read_le16(data + local_offset + 28); - size_t local_header_len = 30 + (size_t)local_name_len + (size_t)local_extra_len; - if (local_header_len > len - (size_t)local_offset) { - errors++; - continue; - } - file_data_offset = (size_t)local_offset + local_header_len; - if ((size_t)compressed_size > len - file_data_offset) { - errors++; - continue; - } - - if (uncompressed_size > MAX_UNCOMPRESSED_SIZE) { - fprintf(stderr, "unzip: entry '%.*s' too large (%lu bytes)\n", - (int)safe_len, safe_name, (unsigned long)uncompressed_size); - errors++; - continue; - } - out = (unsigned char *)malloc(uncompressed_size > 0 ? uncompressed_size : 1); - if (!out) { - errors++; - continue; - } - if (method == 0) { - if (compressed_size != uncompressed_size) { - errors++; - free(out); - continue; - } - memcpy(out, data + file_data_offset, uncompressed_size); - } else if (method == Z_DEFLATED) { - if (inflate_raw_entry(data + file_data_offset, compressed_size, out, uncompressed_size) != 0) { - errors++; - free(out); - continue; - } - } else { - fprintf(stderr, "unzip: unsupported compression method %u for '%.*s'\n", method, name_len, name); - errors++; - free(out); - continue; - } - - int fd = open(outpath, O_WRONLY | O_CREAT | O_TRUNC, 0644); - if (fd < 0) { - fprintf(stderr, "unzip: cannot create '%s': %s\n", outpath, strerror(errno)); - errors++; - free(out); - continue; - } - size_t written = 0; - while (written < uncompressed_size) { - ssize_t n = write(fd, out + written, uncompressed_size - written); - if (n <= 0) { - errors++; - break; - } - written += (size_t)n; - } - close(fd); - free(out); - } - - free(data); - if (errors > 0) { - fprintf(stderr, "unzip: completed with %d error(s)\n", errors); - return 1; - } - return 0; -} - -/* List archive contents */ -static int list_archive(const char *archive) { - unzFile uf = open_archive(archive); - if (!uf) { - return simple_list_archive(archive); - } - - unz_global_info gi; - if (unzGetGlobalInfo(uf, &gi) != UNZ_OK) { - fprintf(stderr, "unzip: cannot read archive info\n"); - unzClose(uf); - return 1; - } - - printf(" Length Name\n"); - printf("--------- ----\n"); - - unsigned long total_size = 0; - for (uLong i = 0; i < gi.number_entry; i++) { - char filename[MAX_PATH_LEN]; - unz_file_info fi; - if (unzGetCurrentFileInfo(uf, &fi, filename, sizeof(filename), - NULL, 0, NULL, 0) != UNZ_OK) { - fprintf(stderr, "unzip: error reading file info\n"); - unzClose(uf); - return 1; - } - - printf("%9lu %s\n", fi.uncompressed_size, filename); - total_size += fi.uncompressed_size; - - if (i + 1 < gi.number_entry) { - if (unzGoToNextFile(uf) != UNZ_OK) { - fprintf(stderr, "unzip: error iterating archive\n"); - unzClose(uf); - return 1; - } - } - } - - printf("--------- ----\n"); - printf("%9lu %lu file(s)\n", total_size, gi.number_entry); - - unzClose(uf); - return 0; -} - -/* Extract a single file from the archive */ -static int extract_current_file(unzFile uf, const char *outdir) { - char filename[MAX_PATH_LEN]; - unz_file_info fi; - if (unzGetCurrentFileInfo(uf, &fi, filename, sizeof(filename), - NULL, 0, NULL, 0) != UNZ_OK) { - fprintf(stderr, "unzip: error reading file info\n"); - return -1; - } - - /* Build output path */ - char outpath[MAX_PATH_LEN]; - if (outdir) { - snprintf(outpath, sizeof(outpath), "%s/%s", outdir, filename); - } else { - snprintf(outpath, sizeof(outpath), "%s", filename); - } - - /* Directory entry (trailing slash) */ - size_t namelen = strlen(outpath); - if (namelen > 0 && outpath[namelen - 1] == '/') { - if (mkdir(outpath, 0755) != 0 && errno != EEXIST) { - fprintf(stderr, "unzip: cannot create directory '%s': %s\n", - outpath, strerror(errno)); - return -1; - } - return 0; - } - - /* Ensure parent directory exists */ - if (mkdirs(outpath) != 0) { - fprintf(stderr, "unzip: cannot create parent directories for '%s'\n", outpath); - return -1; - } - - if (unzOpenCurrentFile(uf) != UNZ_OK) { - fprintf(stderr, "unzip: cannot open '%s' in archive\n", filename); - return -1; - } - - FILE *fout = fopen(outpath, "wb"); - if (!fout) { - fprintf(stderr, "unzip: cannot create '%s': %s\n", outpath, strerror(errno)); - unzCloseCurrentFile(uf); - return -1; - } - - unsigned char buf[WRITE_BUF_SIZE]; - int err = UNZ_OK; - int bytes; - while ((bytes = unzReadCurrentFile(uf, buf, sizeof(buf))) > 0) { - if (fwrite(buf, 1, (size_t)bytes, fout) != (size_t)bytes) { - fprintf(stderr, "unzip: error writing '%s'\n", outpath); - err = -1; - break; - } - } - if (bytes < 0) { - fprintf(stderr, "unzip: error reading '%s' from archive\n", filename); - err = -1; - } - - fclose(fout); - unzCloseCurrentFile(uf); - return err; -} - -/* Extract all files from the archive */ -static int extract_archive(const char *archive, const char *outdir) { - unzFile uf = open_archive(archive); - if (!uf) { - return simple_extract_archive(archive, outdir); - } - - /* Create output directory if specified */ - if (outdir) { - if (mkdir(outdir, 0755) != 0 && errno != EEXIST) { - fprintf(stderr, "unzip: cannot create directory '%s': %s\n", - outdir, strerror(errno)); - unzClose(uf); - return 1; - } - } - - unz_global_info gi; - if (unzGetGlobalInfo(uf, &gi) != UNZ_OK) { - fprintf(stderr, "unzip: cannot read archive info\n"); - unzClose(uf); - return 1; - } - - int errors = 0; - for (uLong i = 0; i < gi.number_entry; i++) { - if (extract_current_file(uf, outdir) != 0) - errors++; - - if (i + 1 < gi.number_entry) { - if (unzGoToNextFile(uf) != UNZ_OK) { - fprintf(stderr, "unzip: error iterating archive\n"); - unzClose(uf); - return 1; - } - } - } - - unzClose(uf); - - if (errors > 0) { - fprintf(stderr, "unzip: completed with %d error(s)\n", errors); - return 1; - } - return 0; -} - -static void print_usage(void) { - fprintf(stderr, "Usage: unzip [-l] [-d dir] archive.zip\n"); - fprintf(stderr, " -l List archive contents\n"); - fprintf(stderr, " -d dir Extract to directory\n"); -} - -int main(int argc, char *argv[]) { - if (argc < 2) { - print_usage(); - return 1; - } - - int list_mode = 0; - const char *outdir = NULL; - const char *archive = NULL; - int i = 1; - - /* Parse flags */ - while (i < argc && argv[i][0] == '-') { - if (strcmp(argv[i], "-l") == 0) { - list_mode = 1; - i++; - } else if (strcmp(argv[i], "-d") == 0) { - if (i + 1 >= argc) { - fprintf(stderr, "unzip: -d requires a directory argument\n"); - return 1; - } - outdir = argv[i + 1]; - i += 2; - } else if (strcmp(argv[i], "--") == 0) { - i++; - break; - } else { - fprintf(stderr, "unzip: unknown option '%s'\n", argv[i]); - print_usage(); - return 1; - } - } - - if (i >= argc) { - fprintf(stderr, "unzip: no archive specified\n"); - print_usage(); - return 1; - } - - archive = argv[i]; - - if (list_mode) { - return list_archive(archive); - } else { - return extract_archive(archive, outdir); - } -} diff --git a/registry/native/c/programs/userinfo.c b/registry/native/c/programs/userinfo.c deleted file mode 100644 index 431f760169..0000000000 --- a/registry/native/c/programs/userinfo.c +++ /dev/null @@ -1,12 +0,0 @@ -/* userinfo.c — print uid, gid, euid, egid */ -#include -#include -#include - -int main(void) { - printf("uid=%u\n", (unsigned)getuid()); - printf("gid=%u\n", (unsigned)getgid()); - printf("euid=%u\n", (unsigned)geteuid()); - printf("egid=%u\n", (unsigned)getegid()); - return 0; -} diff --git a/registry/native/c/programs/waitpid_edge.c b/registry/native/c/programs/waitpid_edge.c deleted file mode 100644 index fbfbe68b9f..0000000000 --- a/registry/native/c/programs/waitpid_edge.c +++ /dev/null @@ -1,101 +0,0 @@ -/* waitpid_edge.c -- edge case tests for waitpid with concurrent children and invalid PIDs */ -#include -#include -#include -#include -#include - -#include "posix_spawn_compat.h" - -extern char **environ; - -int main(void) { - /* Warmup piped spawn (first piped spawn has a capture quirk) */ - { - pid_t wp; - char *wargv[] = {"true", NULL}; - posix_spawnp(&wp, "true", NULL, NULL, wargv, environ); - waitpid(wp, NULL, 0); - } - - /* Test 1: spawn 3 children with different exit codes, waitpid each by specific PID */ - { - pid_t c1, c2, c3; - char *a1[] = {"sh", "-c", "exit 1", NULL}; - char *a2[] = {"sh", "-c", "exit 2", NULL}; - char *a3[] = {"sh", "-c", "exit 3", NULL}; - int err; - - err = posix_spawnp(&c1, "sh", NULL, NULL, a1, environ); - if (err != 0) { printf("test1: FAIL (spawn c1 err=%d)\n", err); return 1; } - - err = posix_spawnp(&c2, "sh", NULL, NULL, a2, environ); - if (err != 0) { printf("test1: FAIL (spawn c2 err=%d)\n", err); return 1; } - - err = posix_spawnp(&c3, "sh", NULL, NULL, a3, environ); - if (err != 0) { printf("test1: FAIL (spawn c3 err=%d)\n", err); return 1; } - - int s1, s2, s3; - pid_t r1 = waitpid(c1, &s1, 0); - pid_t r2 = waitpid(c2, &s2, 0); - pid_t r3 = waitpid(c3, &s3, 0); - - int e1 = WIFEXITED(s1) ? WEXITSTATUS(s1) : -1; - int e2 = WIFEXITED(s2) ? WEXITSTATUS(s2) : -1; - int e3 = WIFEXITED(s3) ? WEXITSTATUS(s3) : -1; - - int ok = (r1 == c1 && e1 == 1 && - r2 == c2 && e2 == 2 && - r3 == c3 && e3 == 3); - printf("test1_c1_exit: %d\n", e1); - printf("test1_c2_exit: %d\n", e2); - printf("test1_c3_exit: %d\n", e3); - printf("test1: %s\n", ok ? "ok" : "FAIL"); - } - - /* Test 2: spawn 2 children, use wait() (waitpid -1) twice, verify both reaped */ - { - pid_t c1, c2; - char *a1[] = {"true", NULL}; - char *a2[] = {"true", NULL}; - int err; - - err = posix_spawnp(&c1, "true", NULL, NULL, a1, environ); - if (err != 0) { printf("test2: FAIL (spawn c1 err=%d)\n", err); return 1; } - - err = posix_spawnp(&c2, "true", NULL, NULL, a2, environ); - if (err != 0) { printf("test2: FAIL (spawn c2 err=%d)\n", err); return 1; } - - int s1, s2; - pid_t r1 = wait(&s1); - pid_t r2 = wait(&s2); - - /* Both returned PIDs must be valid (> 0) and distinct */ - int valid = (r1 > 0 && r2 > 0 && r1 != r2); - /* Both must be one of c1 or c2 */ - int known = ((r1 == c1 || r1 == c2) && (r2 == c1 || r2 == c2)); - /* Both must have exited successfully */ - int exited = (WIFEXITED(s1) && WEXITSTATUS(s1) == 0 && - WIFEXITED(s2) && WEXITSTATUS(s2) == 0); - - printf("test2_r1_valid: %s\n", (r1 > 0) ? "yes" : "no"); - printf("test2_r2_valid: %s\n", (r2 > 0) ? "yes" : "no"); - printf("test2_distinct: %s\n", (r1 != r2) ? "yes" : "no"); - printf("test2: %s\n", (valid && known && exited) ? "ok" : "FAIL"); - } - - /* Test 3: waitpid with PID that was never spawned, verify returns -1 */ - { - errno = 0; - int status; - pid_t ret = waitpid(99999, &status, 0); - int err = errno; - /* POSIX: returns -1, errno = ECHILD (or ESRCH in some implementations) */ - int failed = (ret == -1 && err != 0); - printf("test3_ret: %d\n", (int)ret); - printf("test3_failed: %s\n", failed ? "yes" : "no"); - printf("test3: %s\n", failed ? "ok" : "FAIL"); - } - - return 0; -} diff --git a/registry/native/c/programs/wget.c b/registry/native/c/programs/wget.c deleted file mode 100644 index e0e0ca02fd..0000000000 --- a/registry/native/c/programs/wget.c +++ /dev/null @@ -1,174 +0,0 @@ -/* - * wget.c - minimal wget implementation built on libcurl - * - * Supports common wget options for HTTP/HTTPS downloads: - * URL Download file to current directory (basename from URL) - * -O FILE Write output to specific file ("-" for stdout) - * -q Quiet mode (suppress progress/messages) - * -L Follow redirects (enabled by default, like real wget) - * --no-check-certificate Skip TLS certificate verification - */ - -#include -#include -#include -#include - -/* Write callback: write to FILE* */ -static size_t write_callback(char *ptr, size_t size, size_t nmemb, - void *userdata) { - FILE *out = (FILE *)userdata; - return fwrite(ptr, size, nmemb, out); -} - -/* Extract filename from URL path, fallback to "index.html" */ -static const char *basename_from_url(const char *url) { - /* Skip scheme */ - const char *p = strstr(url, "://"); - if (p) p += 3; else p = url; - - /* Find last '/' in path (before query string) */ - const char *last_slash = NULL; - const char *q = p; - while (*q && *q != '?' && *q != '#') { - if (*q == '/') last_slash = q; - q++; - } - - if (last_slash && last_slash[1] && last_slash[1] != '?' && last_slash[1] != '#') { - /* Extract filename between last slash and query/end */ - const char *start = last_slash + 1; - size_t len = 0; - while (start[len] && start[len] != '?' && start[len] != '#') len++; - if (len > 0) { - static char buf[256]; - if (len >= sizeof(buf)) len = sizeof(buf) - 1; - memcpy(buf, start, len); - buf[len] = '\0'; - return buf; - } - } - - return "index.html"; -} - -int main(int argc, char *argv[]) { - const char *url = NULL; - const char *output_file = NULL; - int quiet = 0; - int no_check_cert = 0; - int output_to_stdout = 0; - - /* Parse arguments */ - for (int i = 1; i < argc; i++) { - if (strcmp(argv[i], "-O") == 0 && i + 1 < argc) { - output_file = argv[++i]; - if (strcmp(output_file, "-") == 0) { - output_to_stdout = 1; - output_file = NULL; - } - } else if (strcmp(argv[i], "-q") == 0) { - quiet = 1; - } else if (strcmp(argv[i], "-L") == 0) { - /* Follow redirects — already default, accept silently */ - } else if (strcmp(argv[i], "--no-check-certificate") == 0) { - no_check_cert = 1; - } else if (argv[i][0] != '-') { - url = argv[i]; - } else { - /* Unknown option — skip silently for forward compat */ - } - } - - if (!url) { - fprintf(stderr, "wget: missing URL\nUsage: wget [OPTION]... [URL]...\n"); - return 1; - } - - CURLcode res; - curl_global_init(CURL_GLOBAL_DEFAULT); - - CURL *curl = curl_easy_init(); - if (!curl) { - fprintf(stderr, "wget: failed to initialize\n"); - curl_global_cleanup(); - return 1; - } - - /* Determine output destination */ - FILE *out = NULL; - const char *dest_name = NULL; - - if (output_to_stdout) { - out = stdout; - } else { - dest_name = output_file ? output_file : basename_from_url(url); - out = fopen(dest_name, "wb"); - if (!out) { - fprintf(stderr, "wget: cannot open '%s' for writing\n", dest_name); - curl_easy_cleanup(curl); - curl_global_cleanup(); - return 1; - } - } - - curl_easy_setopt(curl, CURLOPT_URL, url); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, out); - - /* wget follows redirects by default */ - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 20L); - - /* Suppress progress meter */ - curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 1L); - - /* TLS: skip certificate verification */ - if (no_check_cert) { - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); - curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L); - } - - /* Perform request */ - res = curl_easy_perform(curl); - - int exit_code = 0; - - if (res != CURLE_OK) { - if (!quiet) { - fprintf(stderr, "wget: failed: %s\n", curl_easy_strerror(res)); - } - exit_code = 1; - /* Remove partial download on failure (unless stdout) */ - if (!output_to_stdout && dest_name) { - fclose(out); - out = NULL; - remove(dest_name); - } - } else { - /* Check HTTP response code */ - long http_code = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_code); - if (http_code >= 400) { - if (!quiet) { - fprintf(stderr, "wget: server returned HTTP %ld\n", http_code); - } - exit_code = 8; /* wget uses exit code 8 for server errors */ - /* Remove error response file (unless stdout) */ - if (!output_to_stdout && dest_name) { - fclose(out); - out = NULL; - remove(dest_name); - } - } - } - - /* Cleanup */ - curl_easy_cleanup(curl); - if (out && out != stdout) { - fclose(out); - } - curl_global_cleanup(); - - return exit_code; -} diff --git a/registry/native/c/programs/zip.c b/registry/native/c/programs/zip.c deleted file mode 100644 index d8eca7a5bd..0000000000 --- a/registry/native/c/programs/zip.c +++ /dev/null @@ -1,203 +0,0 @@ -/* zip.c — Create ZIP archives using zlib/minizip - * - * Usage: zip [-r] archive.zip file1 [file2 ...] - * -r Recurse into directories - */ - -#include -#include -#include -#include -#include -#include -#include "zip.h" - -#define MAX_PATH_LEN 4096 -#define READ_BUF_SIZE 8192 - -/* Add a single file to the zip archive */ -static int add_file_to_zip(zipFile zf, const char *filepath, const char *archivepath) { - FILE *fin = fopen(filepath, "rb"); - if (!fin) { - fprintf(stderr, "zip: cannot open '%s': ", filepath); - perror(""); - return -1; - } - - zip_fileinfo zi; - memset(&zi, 0, sizeof(zi)); - - /* Get file modification time */ - struct stat st; - if (stat(filepath, &st) == 0) { - struct tm *lt = localtime(&st.st_mtime); - if (lt) { - zi.tmz_date.tm_sec = lt->tm_sec; - zi.tmz_date.tm_min = lt->tm_min; - zi.tmz_date.tm_hour = lt->tm_hour; - zi.tmz_date.tm_mday = lt->tm_mday; - zi.tmz_date.tm_mon = lt->tm_mon; - zi.tmz_date.tm_year = lt->tm_year; - } - } - - int err = zipOpenNewFileInZip(zf, archivepath, &zi, - NULL, 0, NULL, 0, NULL, - Z_DEFLATED, Z_DEFAULT_COMPRESSION); - if (err != ZIP_OK) { - fprintf(stderr, "zip: error opening '%s' in archive\n", archivepath); - fclose(fin); - return -1; - } - - unsigned char buf[READ_BUF_SIZE]; - size_t n; - while ((n = fread(buf, 1, sizeof(buf), fin)) > 0) { - if (zipWriteInFileInZip(zf, buf, (unsigned int)n) != ZIP_OK) { - fprintf(stderr, "zip: error writing '%s' to archive\n", archivepath); - fclose(fin); - zipCloseFileInZip(zf); - return -1; - } - } - - fclose(fin); - zipCloseFileInZip(zf); - return 0; -} - -/* Recursively add directory contents to zip */ -static int add_dir_to_zip(zipFile zf, const char *dirpath, const char *archivebase) { - DIR *d = opendir(dirpath); - if (!d) { - fprintf(stderr, "zip: cannot open directory '%s': ", dirpath); - perror(""); - return -1; - } - - /* Add directory entry (trailing slash) */ - char direntry[MAX_PATH_LEN]; - snprintf(direntry, sizeof(direntry), "%s/", archivebase); - zip_fileinfo zi; - memset(&zi, 0, sizeof(zi)); - zipOpenNewFileInZip(zf, direntry, &zi, NULL, 0, NULL, 0, NULL, 0, 0); - zipCloseFileInZip(zf); - - struct dirent *entry; - int err = 0; - while ((entry = readdir(d)) != NULL) { - if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) - continue; - - char fullpath[MAX_PATH_LEN]; - char arcpath[MAX_PATH_LEN]; - snprintf(fullpath, sizeof(fullpath), "%s/%s", dirpath, entry->d_name); - snprintf(arcpath, sizeof(arcpath), "%s/%s", archivebase, entry->d_name); - - struct stat st; - if (stat(fullpath, &st) != 0) { - fprintf(stderr, "zip: cannot stat '%s': ", fullpath); - perror(""); - err = -1; - continue; - } - - if (S_ISDIR(st.st_mode)) { - if (add_dir_to_zip(zf, fullpath, arcpath) != 0) - err = -1; - } else { - if (add_file_to_zip(zf, fullpath, arcpath) != 0) - err = -1; - } - } - - closedir(d); - return err; -} - -static void print_usage(void) { - fprintf(stderr, "Usage: zip [-r] archive.zip file1 [file2 ...]\n"); - fprintf(stderr, " -r Recurse into directories\n"); -} - -int main(int argc, char *argv[]) { - if (argc < 3) { - print_usage(); - return 1; - } - - int recursive = 0; - int arg_start = 1; - - /* Parse flags */ - while (arg_start < argc && argv[arg_start][0] == '-') { - if (strcmp(argv[arg_start], "-r") == 0) { - recursive = 1; - arg_start++; - } else if (strcmp(argv[arg_start], "--") == 0) { - arg_start++; - break; - } else { - fprintf(stderr, "zip: unknown option '%s'\n", argv[arg_start]); - print_usage(); - return 1; - } - } - - if (argc - arg_start < 2) { - print_usage(); - return 1; - } - - const char *archive = argv[arg_start]; - arg_start++; - - zipFile zf = zipOpen(archive, APPEND_STATUS_CREATE); - if (!zf) { - fprintf(stderr, "zip: cannot create '%s'\n", archive); - return 1; - } - - int errors = 0; - for (int i = arg_start; i < argc; i++) { - const char *path = argv[i]; - - /* Strip trailing slashes for consistent archive paths */ - char cleanpath[MAX_PATH_LEN]; - strncpy(cleanpath, path, sizeof(cleanpath) - 1); - cleanpath[sizeof(cleanpath) - 1] = '\0'; - size_t len = strlen(cleanpath); - while (len > 1 && cleanpath[len - 1] == '/') - cleanpath[--len] = '\0'; - - struct stat st; - if (stat(cleanpath, &st) != 0) { - fprintf(stderr, "zip: cannot stat '%s': ", cleanpath); - perror(""); - errors++; - continue; - } - - if (S_ISDIR(st.st_mode)) { - if (!recursive) { - fprintf(stderr, "zip: '%s' is a directory (use -r to recurse)\n", cleanpath); - errors++; - continue; - } - if (add_dir_to_zip(zf, cleanpath, cleanpath) != 0) - errors++; - } else { - if (add_file_to_zip(zf, cleanpath, cleanpath) != 0) - errors++; - } - } - - zipClose(zf, NULL); - - if (errors > 0) { - fprintf(stderr, "zip: completed with %d error(s)\n", errors); - return 1; - } - - return 0; -} diff --git a/registry/native/c/scripts/build-curl-upstream.sh b/registry/native/c/scripts/build-curl-upstream.sh deleted file mode 100644 index d2ffe82c2e..0000000000 --- a/registry/native/c/scripts/build-curl-upstream.sh +++ /dev/null @@ -1,251 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -usage() { - cat <<'EOF' -Usage: build-curl-upstream.sh \ - --version \ - --tag \ - --url \ - --cache-dir \ - --build-dir \ - --overlay-dir \ - --cc \ - --ar \ - --ranlib \ - --output -EOF -} - -VERSION="" -TAG="" -URL="" -CACHE_DIR="" -BUILD_DIR="" -OVERLAY_DIR="" -CC_CMD="" -AR_CMD="" -RANLIB_CMD="" -OUTPUT="" - -while [[ $# -gt 0 ]]; do - case "$1" in - --version) - VERSION="$2" - shift 2 - ;; - --tag) - TAG="$2" - shift 2 - ;; - --url) - URL="$2" - shift 2 - ;; - --cache-dir) - CACHE_DIR="$2" - shift 2 - ;; - --build-dir) - BUILD_DIR="$2" - shift 2 - ;; - --overlay-dir) - OVERLAY_DIR="$2" - shift 2 - ;; - --cc) - CC_CMD="$2" - shift 2 - ;; - --ar) - AR_CMD="$2" - shift 2 - ;; - --ranlib) - RANLIB_CMD="$2" - shift 2 - ;; - --output) - OUTPUT="$2" - shift 2 - ;; - *) - echo "Unknown argument: $1" >&2 - usage >&2 - exit 1 - ;; - esac -done - -if [[ -z "$VERSION" || -z "$TAG" || -z "$URL" || -z "$CACHE_DIR" || -z "$BUILD_DIR" || -z "$OVERLAY_DIR" || -z "$CC_CMD" || -z "$AR_CMD" || -z "$RANLIB_CMD" || -z "$OUTPUT" ]]; then - usage >&2 - exit 1 -fi - -fetch() { - local url="$1" - local out="$2" - if command -v curl >/dev/null 2>&1; then - curl -fSL "$url" -o "$out" - elif command -v wget >/dev/null 2>&1; then - wget -q "$url" -O "$out" - else - echo "Neither curl nor wget is available to fetch $url" >&2 - exit 1 - fi -} - -mkdir -p "$CACHE_DIR" -rm -rf "$BUILD_DIR" -mkdir -p "$BUILD_DIR" - -TARBALL="$CACHE_DIR/curl-${VERSION}.tar.xz" -if [[ ! -f "$TARBALL" ]]; then - echo "Fetching upstream curl ${VERSION} release tarball..." - fetch "$URL" "$TARBALL" -fi - -echo "Extracting upstream curl ${VERSION}..." -tar -xf "$TARBALL" -C "$BUILD_DIR" - -SRC_DIR="$BUILD_DIR/curl-${VERSION}" -if [[ ! -d "$SRC_DIR" ]]; then - echo "Expected extracted source at $SRC_DIR" >&2 - exit 1 -fi - -echo "Applying secure-exec overlay..." -while IFS= read -r -d '' file; do - rel="${file#$OVERLAY_DIR/}" - mkdir -p "$SRC_DIR/$(dirname "$rel")" - cp "$file" "$SRC_DIR/$rel" -done < <(find "$OVERLAY_DIR" -type f -print0) - -pushd "$SRC_DIR" >/dev/null - -echo "Patching WASI-incompatible signal/setjmp includes..." -python3 - <<'PY' -from pathlib import Path - -replacements = { - "lib/hostip.h": [ - ( - '#include \n', - '#ifndef __wasi__\n#include \n#endif\n', - ), - ], - "lib/hostip.c": [ - ( - '#include \n#include \n', - '#ifndef __wasi__\n#include \n#include \n#endif\n', - ), - ], - "lib/transfer.c": [ - ( - '#include \n', - '#ifndef __wasi__\n#include \n#endif\n', - ), - ], - "src/tool_main.c": [ - ( - '#include \n', - '#ifndef __wasi__\n#include \n#endif\n', - ), - ], -} - -for rel_path, edits in replacements.items(): - path = Path(rel_path) - updated = path.read_text() - for old, new in edits: - text = updated - if new in text: - continue - if old not in text: - raise SystemExit(f"Expected to patch {rel_path}, but no replacement matched") - updated = text.replace(old, new) - path.write_text(updated) -PY - -echo "Configuring upstream curl for wasm32-wasip1..." -CC="$CC_CMD" \ -AR="$AR_CMD" \ -RANLIB="$RANLIB_CMD" \ -CFLAGS="-O2 -flto" \ -./configure \ - --host=wasm32-unknown-wasi \ - --disable-shared \ - --disable-threaded-resolver \ - --disable-ldap \ - --without-zlib \ - --without-brotli \ - --without-zstd \ - --without-libpsl \ - --without-ca-bundle \ - --without-ca-path \ - --without-ssl - -echo "Enabling the secure-exec WASI TLS backend in generated curl config..." -python3 - <<'PY' -from pathlib import Path - -config = Path("lib/curl_config.h") -text = config.read_text() - -updates = { - "#define CURL_DISABLE_HSTS 1": "/* #undef CURL_DISABLE_HSTS */", -} -for old, new in updates.items(): - text = text.replace(old, new) - -if "#define USE_WASI_TLS 1" not in text: - text += "\n#define USE_WASI_TLS 1\n" -if "#define CURL_DISABLE_OPENSSL_AUTO_LOAD_CONFIG 1" not in text: - text += "#define CURL_DISABLE_OPENSSL_AUTO_LOAD_CONFIG 1\n" - -config.write_text(text) -PY - -echo "Patching generated lib/Makefile to compile wasi_tls.c..." -cat >> lib/Makefile <<'EOF' - -am_libcurl_la_OBJECTS += vtls/libcurl_la-wasi_tls.lo -libcurl_la_LIBADD += vtls/libcurl_la-wasi_tls.lo -libcurl.la: vtls/libcurl_la-wasi_tls.lo - -vtls/libcurl_la-wasi_tls.lo: vtls/$(am__dirstamp) vtls/$(DEPDIR)/$(am__dirstamp) vtls/wasi_tls.c - $(AM_V_CC)$(LIBTOOL) $(AM_V_lt) --tag=CC $(AM_LIBTOOLFLAGS) $(LIBTOOLFLAGS) --mode=compile $(CC) $(DEFS) $(DEFAULT_INCLUDES) $(INCLUDES) $(libcurl_la_CPPFLAGS) $(CPPFLAGS) $(libcurl_la_CFLAGS) $(CFLAGS) -MT vtls/libcurl_la-wasi_tls.lo -MD -MP -MF vtls/$(DEPDIR)/libcurl_la-wasi_tls.Tpo -c -o vtls/libcurl_la-wasi_tls.lo `test -f 'vtls/wasi_tls.c' || echo '$(srcdir)/'`vtls/wasi_tls.c - $(AM_V_at)$(am__mv) vtls/$(DEPDIR)/libcurl_la-wasi_tls.Tpo vtls/$(DEPDIR)/libcurl_la-wasi_tls.Plo -EOF - -echo "Building upstream libcurl..." -make -C lib libcurl.la - -echo "Building upstream curl tool..." -make -C src curl - -BIN="" -for candidate in "src/.libs/curl" "src/curl" "src/curl.wasm"; do - if [[ -f "$candidate" ]]; then - BIN="$candidate" - break - fi -done - -if [[ -z "$BIN" ]]; then - echo "Unable to locate built curl binary in src/" >&2 - exit 1 -fi - -mkdir -p "$(dirname "$OUTPUT")" -if command -v wasm-opt >/dev/null 2>&1; then - echo "Optimizing curl WASM binary..." - wasm-opt -O3 --strip-debug --all-features "$BIN" -o "$OUTPUT" -else - cp "$BIN" "$OUTPUT" -fi - -popd >/dev/null - -echo "Built upstream curl at $OUTPUT" diff --git a/registry/native/c/vim/posix_stubs.c b/registry/native/c/vim/posix_stubs.c deleted file mode 100644 index f669d17bcf..0000000000 --- a/registry/native/c/vim/posix_stubs.c +++ /dev/null @@ -1,30 +0,0 @@ -/* posix_stubs.c — stubs for full-OS libc gaps vim references but that the VM - * does not implement (group database, etc.). Safe no-op behavior. */ -#include -#include -struct group *getgrgid(gid_t g) { (void)g; return NULL; } -struct group *getgrnam(const char *n) { (void)n; return NULL; } -struct group *getgrent(void) { return NULL; } -void setgrent(void) {} -void endgrent(void) {} - -/* --- additional full-OS libc gaps --- */ -#include -mode_t umask(mode_t mask) { (void)mask; return 0; } - -#include -struct itimerval; -int setitimer(int w, const struct itimerval *n, struct itimerval *o) { (void)w; (void)n; (void)o; return 0; } -int getitimer(int w, struct itimerval *o) { (void)w; (void)o; return 0; } - -/* --- process/signal stubs (not used by core editing) --- */ -#include -pid_t fork(void) { errno = ENOSYS; return -1; } -int execvp(const char *f, char *const a[]) { (void)f; (void)a; errno = ENOSYS; return -1; } -int raise(int s) { (void)s; return -1; } -/* signal sentinel symbols (this libc takes their address for SIG_IGN/SIG_ERR) */ -void __SIG_IGN(int s) { (void)s; } -void __SIG_ERR(int s) { (void)s; } -void __SIG_DFL(int s) { (void)s; } -int sigprocmask(int how, const void *set, void *old) { (void)how; (void)set; (void)old; return 0; } -int sigpending(void *set) { (void)set; return 0; } diff --git a/registry/native/crates/commands/arch/Cargo.toml b/registry/native/crates/commands/arch/Cargo.toml deleted file mode 100644 index 92f2e7757d..0000000000 --- a/registry/native/crates/commands/arch/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-arch" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "arch standalone binary for secure-exec VM" - -[[bin]] -name = "arch" -path = "src/main.rs" - -[dependencies] -uu_arch = "0.7.0" diff --git a/registry/native/crates/commands/awk/Cargo.toml b/registry/native/crates/commands/awk/Cargo.toml deleted file mode 100644 index 31ccd2a2bd..0000000000 --- a/registry/native/crates/commands/awk/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-awk" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "awk standalone binary for secure-exec VM" - -[[bin]] -name = "awk" -path = "src/main.rs" - -[dependencies] -secureexec-awk = { path = "../../libs/awk" } diff --git a/registry/native/crates/commands/b2sum/Cargo.toml b/registry/native/crates/commands/b2sum/Cargo.toml deleted file mode 100644 index 8219c1fd4b..0000000000 --- a/registry/native/crates/commands/b2sum/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-b2sum" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "b2sum standalone binary for secure-exec VM" - -[[bin]] -name = "b2sum" -path = "src/main.rs" - -[dependencies] -uu_b2sum = "0.7.0" diff --git a/registry/native/crates/commands/base32/Cargo.toml b/registry/native/crates/commands/base32/Cargo.toml deleted file mode 100644 index 3e719bf184..0000000000 --- a/registry/native/crates/commands/base32/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-base32" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "base32 standalone binary for secure-exec VM" - -[[bin]] -name = "base32" -path = "src/main.rs" - -[dependencies] -uu_base32 = "0.7.0" diff --git a/registry/native/crates/commands/base64/Cargo.toml b/registry/native/crates/commands/base64/Cargo.toml deleted file mode 100644 index bad7a503e1..0000000000 --- a/registry/native/crates/commands/base64/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-base64" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "base64 standalone binary for secure-exec VM" - -[[bin]] -name = "base64" -path = "src/main.rs" - -[dependencies] -uu_base64 = "0.7.0" diff --git a/registry/native/crates/commands/basename/Cargo.toml b/registry/native/crates/commands/basename/Cargo.toml deleted file mode 100644 index 8511991cdc..0000000000 --- a/registry/native/crates/commands/basename/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-basename" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "basename standalone binary for secure-exec VM" - -[[bin]] -name = "basename" -path = "src/main.rs" - -[dependencies] -uu_basename = "0.7.0" diff --git a/registry/native/crates/commands/basenc/Cargo.toml b/registry/native/crates/commands/basenc/Cargo.toml deleted file mode 100644 index ad2c8e5a92..0000000000 --- a/registry/native/crates/commands/basenc/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-basenc" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "basenc standalone binary for secure-exec VM" - -[[bin]] -name = "basenc" -path = "src/main.rs" - -[dependencies] -uu_basenc = "0.7.0" diff --git a/registry/native/crates/commands/cat/Cargo.toml b/registry/native/crates/commands/cat/Cargo.toml deleted file mode 100644 index 16a9d7218e..0000000000 --- a/registry/native/crates/commands/cat/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "cmd-cat" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "cat standalone binary for secure-exec VM" - -[[bin]] -name = "cat" -path = "src/main.rs" - -[dependencies] -uu_cat = "0.7.0" - -[target.'cfg(any(target_os = "linux", target_os = "android"))'.dev-dependencies] -uucore = { version = "0.7.0", features = ["pipes"] } diff --git a/registry/native/crates/commands/chmod/Cargo.toml b/registry/native/crates/commands/chmod/Cargo.toml deleted file mode 100644 index 3a1da3b8c1..0000000000 --- a/registry/native/crates/commands/chmod/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-chmod" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "chmod standalone binary for secure-exec VM" - -[[bin]] -name = "chmod" -path = "src/main.rs" - -[dependencies] -uu_chmod = "0.7.0" diff --git a/registry/native/crates/commands/cksum/Cargo.toml b/registry/native/crates/commands/cksum/Cargo.toml deleted file mode 100644 index 1beec6b3a4..0000000000 --- a/registry/native/crates/commands/cksum/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-cksum" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "cksum standalone binary for secure-exec VM" - -[[bin]] -name = "cksum" -path = "src/main.rs" - -[dependencies] -uu_cksum = "0.7.0" diff --git a/registry/native/crates/commands/codex-exec/Cargo.toml b/registry/native/crates/commands/codex-exec/Cargo.toml deleted file mode 100644 index c2859ea274..0000000000 --- a/registry/native/crates/commands/codex-exec/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "cmd-codex-exec" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "codex-exec command binary for secure-exec VM" - -[[bin]] -name = "codex-exec" -path = "src/main.rs" - -[dependencies] -wasi-http = { package = "secureexec-wasi-http", path = "../../libs/wasi-http" } - -# WASI stub crates for future codex-core dependencies that don't support wasm32-wasip1. -codex-network-proxy = "0.0.0" -codex-otel = "0.0.0" diff --git a/registry/native/crates/commands/codex/Cargo.toml b/registry/native/crates/commands/codex/Cargo.toml deleted file mode 100644 index 41f6109024..0000000000 --- a/registry/native/crates/commands/codex/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "cmd-codex" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "codex standalone binary for secure-exec VM" - -[[bin]] -name = "codex" -path = "src/main.rs" - -# The full codex-exec dependency from rivet-dev/codex (wasi-support branch) -# will be wired in when the vendoring blocker is resolved. -# -# The fork at github.com/rivet-dev/codex already has cfg(target_os = "wasi") -# gates on codex-core (portable-pty, network-proxy, sandbox deps) and -# codex-exec (all platform deps gated behind non-WASI). -# -# wasi-spawn provides the WasiChild abstraction that will replace -# tokio::process::Command in codex-core when compiled for WASI. - -[dependencies] -wasi-spawn = { package = "secureexec-wasi-spawn", path = "../../libs/wasi-spawn" } -wasi-http = { package = "secureexec-wasi-http", path = "../../libs/wasi-http" } - -# TUI dependencies — ratatui for rendering, crossterm for terminal backend -# crossterm is patched for WASI support (see patches/crates/crossterm/) -ratatui = { version = "0.29", default-features = false, features = ["crossterm"] } - -# WASI stub crates for codex-core dependencies that don't support wasm32-wasip1. -# These provide zero-size/no-op implementations so codex-core compiles for WASI. -codex-network-proxy = "0.0.0" -codex-otel = "0.0.0" diff --git a/registry/native/crates/commands/codex/src/main.rs b/registry/native/crates/commands/codex/src/main.rs deleted file mode 100644 index 5375dfa82e..0000000000 --- a/registry/native/crates/commands/codex/src/main.rs +++ /dev/null @@ -1,337 +0,0 @@ -/// Codex TUI for secure-exec VM. -/// -/// Full terminal UI using ratatui + crossterm backend, rendering through -/// the WasmVM PTY. This is the interactive entry point — for headless -/// (scriptable) usage, see codex-exec. -/// -/// Uses wasi-spawn for process spawning via host_process FFI and -/// wasi-http for HTTP/HTTPS requests via host_net TCP/TLS imports. -/// -/// crossterm is patched for WASI support (see patches/crates/crossterm/): -/// - Terminal raw mode tracked in-process (host PTY handles discipline) -/// - Terminal size from COLUMNS/LINES env vars -/// - Event source reads stdin directly and parses ANSI escape sequences -/// - IsTty returns true (PTY slave FDs are terminals) -use std::io; - -use ratatui::{ - backend::CrosstermBackend, - crossterm::{ - event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, - execute, - terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}, - }, - layout::{Constraint, Layout}, - style::{Color, Modifier, Style}, - text::{Line, Span, Text}, - widgets::{Block, Borders, Paragraph, Wrap}, - Frame, Terminal, -}; - -// Validate WASI stub crates compile by referencing key types -use codex_network_proxy::NetworkProxy; -use codex_otel::SessionTelemetry; - -const VERSION: &str = env!("CARGO_PKG_VERSION"); -const MAX_INPUT_CHARS: usize = 8192; -const MAX_MESSAGES: usize = 200; - -fn main() { - let args: Vec = std::env::args().collect(); - - // Handle --help - if args.iter().any(|a| a == "--help" || a == "-h") { - print_help(); - return; - } - - // Handle --version - if args.iter().any(|a| a == "--version" || a == "-V") { - println!("codex {}", VERSION); - return; - } - - // Handle --model flag (accept but note it's stored for future use) - let model = args - .windows(2) - .find(|w| w[0] == "--model") - .map(|w| w[1].clone()); - - // Built-in HTTP test subcommand (validates wasi-http integration) - if args.get(1).map(|s| s.as_str()) == Some("--http-test") { - return http_test(&args[2..]); - } - - // Stub validation subcommand (validates WASI stub crates) - if args.get(1).map(|s| s.as_str()) == Some("--stub-test") { - return stub_test(); - } - - // Run the TUI - if let Err(e) = run_tui(model.as_deref()) { - eprintln!("codex: TUI error: {}", e); - std::process::exit(1); - } -} - -/// Main TUI event loop using ratatui + crossterm. -fn run_tui(model: Option<&str>) -> io::Result<()> { - let _terminal_guard = TerminalGuard::enter()?; - - let stdout = io::stdout(); - let backend = CrosstermBackend::new(stdout); - let mut terminal = Terminal::new(backend)?; - - // App state - let mut input = String::new(); - let mut messages: Vec = Vec::new(); - let mut should_quit = false; - - // Draw initial frame - terminal.draw(|f| draw_ui(f, &input, &messages, model))?; - - // Event loop - while !should_quit { - match event::read()? { - Event::Key(key) => { - handle_key_event(key, &mut input, &mut messages, &mut should_quit); - terminal.draw(|f| draw_ui(f, &input, &messages, model))?; - } - Event::Resize(_, _) => { - terminal.draw(|f| draw_ui(f, &input, &messages, model))?; - } - _ => {} - } - } - - Ok(()) -} - -/// Handle a key event, updating app state. -fn handle_key_event( - key: KeyEvent, - input: &mut String, - messages: &mut Vec, - should_quit: &mut bool, -) { - match (key.code, key.modifiers) { - // Ctrl+C quits - (KeyCode::Char('c'), m) if m.contains(KeyModifiers::CONTROL) => { - *should_quit = true; - } - // 'q' on empty input quits - (KeyCode::Char('q'), KeyModifiers::NONE) if input.is_empty() => { - *should_quit = true; - } - // Enter submits the input - (KeyCode::Enter, _) => { - if !input.is_empty() { - let prompt = input.clone(); - push_message(messages, format!("> {}", prompt)); - push_message( - messages, - "codex: agent loop is under development".to_string(), - ); - push_message( - messages, - format!("codex: prompt received ({} chars)", prompt.len()), - ); - input.clear(); - } - } - // Backspace deletes last char - (KeyCode::Backspace, _) => { - input.pop(); - } - // Esc clears input - (KeyCode::Esc, _) => { - input.clear(); - } - // Regular character input - (KeyCode::Char(c), _) => { - if input.chars().count() < MAX_INPUT_CHARS { - input.push(c); - } - } - _ => {} - } -} - -fn push_message(messages: &mut Vec, message: String) { - messages.push(message); - if messages.len() > MAX_MESSAGES { - messages.drain(..messages.len() - MAX_MESSAGES); - } -} - -struct TerminalGuard { - raw_mode_enabled: bool, - alternate_screen_enabled: bool, -} - -impl TerminalGuard { - fn enter() -> io::Result { - terminal::enable_raw_mode()?; - - if let Err(error) = execute!(io::stdout(), EnterAlternateScreen) { - let _ = terminal::disable_raw_mode(); - return Err(error); - } - - Ok(Self { - raw_mode_enabled: true, - alternate_screen_enabled: true, - }) - } -} - -impl Drop for TerminalGuard { - fn drop(&mut self) { - if self.alternate_screen_enabled { - let _ = execute!(io::stdout(), LeaveAlternateScreen); - self.alternate_screen_enabled = false; - } - - if self.raw_mode_enabled { - let _ = terminal::disable_raw_mode(); - self.raw_mode_enabled = false; - } - } -} - -/// Draw the TUI layout. -fn draw_ui(f: &mut Frame, input: &str, messages: &[String], model: Option<&str>) { - let area = f.area(); - - let chunks = Layout::vertical([ - Constraint::Length(3), // Header - Constraint::Min(5), // Messages - Constraint::Length(3), // Input - ]) - .split(area); - - // Header - let model_text = model.unwrap_or("default"); - let header = Paragraph::new(Line::from(vec![ - Span::styled( - "Codex ", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - Span::styled(VERSION, Style::default().fg(Color::DarkGray)), - Span::raw(" "), - Span::styled( - format!("model: {}", model_text), - Style::default().fg(Color::Yellow), - ), - Span::raw(" "), - Span::styled("WasmVM", Style::default().fg(Color::Green)), - ])) - .block(Block::default().borders(Borders::ALL).title(" codex ")); - f.render_widget(header, chunks[0]); - - // Messages area - let msg_lines: Vec = messages - .iter() - .map(|m| { - if m.starts_with("> ") { - Line::from(Span::styled(m.as_str(), Style::default().fg(Color::Cyan))) - } else { - Line::from(Span::raw(m.as_str())) - } - }) - .collect(); - - let welcome = if messages.is_empty() { - vec![ - Line::from(""), - Line::from(Span::styled( - "Welcome to Codex on WasmVM!", - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - )), - Line::from(""), - Line::from("Type a prompt and press Enter to submit."), - Line::from("Press 'q' (on empty input) or Ctrl+C to exit."), - ] - } else { - msg_lines - }; - - let messages_widget = Paragraph::new(Text::from(welcome)) - .block(Block::default().borders(Borders::ALL).title(" messages ")) - .wrap(Wrap { trim: false }); - f.render_widget(messages_widget, chunks[1]); - - // Input area - let input_widget = Paragraph::new(Line::from(vec![ - Span::styled("❯ ", Style::default().fg(Color::Green)), - Span::raw(input), - ])) - .block(Block::default().borders(Borders::ALL).title(" input ")); - f.render_widget(input_widget, chunks[2]); -} - -fn print_help() { - println!( - "codex {} — interactive Codex TUI for secure-exec VM", - VERSION - ); - println!(); - println!("USAGE:"); - println!(" codex [OPTIONS]"); - println!(); - println!("OPTIONS:"); - println!(" -h, --help Print this help message"); - println!(" -V, --version Print version information"); - println!(" --model MODEL Select model for completions"); - println!(" --http-test URL Test HTTP client via host_net"); - println!(" --stub-test Validate WASI stub crates"); - println!(); - println!("DESCRIPTION:"); - println!(" Interactive TUI for the Codex agent, rendered through"); - println!(" the WasmVM PTY using ratatui + crossterm backend."); - println!(" For headless mode, use codex-exec instead."); -} - -fn stub_test() { - let proxy = NetworkProxy; - let mut env = std::collections::HashMap::new(); - proxy.apply_to_env(&mut env); - println!("network-proxy: NetworkProxy is zero-size, apply_to_env is no-op"); - - let telemetry = SessionTelemetry::new(); - telemetry.counter("test.counter", 1, &[]); - telemetry.histogram("test.histogram", 42, &[]); - println!("otel: SessionTelemetry metrics are no-ops"); - - let global = codex_otel::metrics::global(); - assert!(global.is_none(), "global metrics should be None on WASI"); - println!("otel: global() returns None (no exporter on WASI)"); - - println!("stub-test: all stubs validated successfully"); -} - -fn http_test(args: &[String]) { - if args.is_empty() { - eprintln!("usage: codex --http-test "); - std::process::exit(1); - } - - let url = &args[0]; - match wasi_http::get(url) { - Ok(resp) => { - println!("status: {}", resp.status); - match resp.text() { - Ok(body) => println!("body: {}", body), - Err(e) => eprintln!("body decode error: {}", e), - } - } - Err(e) => { - eprintln!("http error: {}", e); - std::process::exit(1); - } - } -} diff --git a/registry/native/crates/commands/column/Cargo.toml b/registry/native/crates/commands/column/Cargo.toml deleted file mode 100644 index bca846e4db..0000000000 --- a/registry/native/crates/commands/column/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-column" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "column standalone binary for secure-exec VM" - -[[bin]] -name = "column" -path = "src/main.rs" - -[dependencies] -secureexec-column = { path = "../../libs/column" } diff --git a/registry/native/crates/commands/comm/Cargo.toml b/registry/native/crates/commands/comm/Cargo.toml deleted file mode 100644 index c748f132f1..0000000000 --- a/registry/native/crates/commands/comm/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-comm" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "comm standalone binary for secure-exec VM" - -[[bin]] -name = "comm" -path = "src/main.rs" - -[dependencies] -uu_comm = "0.7.0" diff --git a/registry/native/crates/commands/cp/Cargo.toml b/registry/native/crates/commands/cp/Cargo.toml deleted file mode 100644 index 851012ce6e..0000000000 --- a/registry/native/crates/commands/cp/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-cp" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "cp standalone binary for secure-exec VM" - -[[bin]] -name = "cp" -path = "src/main.rs" - -[dependencies] -uu_cp = "0.7.0" diff --git a/registry/native/crates/commands/curl/Cargo.toml b/registry/native/crates/commands/curl/Cargo.toml deleted file mode 100644 index 5d5a8a8763..0000000000 --- a/registry/native/crates/commands/curl/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-curl" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "Minimal curl-compatible HTTP client for secure-exec" - -[[bin]] -name = "curl" -path = "src/main.rs" - -[dependencies] -wasi-http = { package = "secureexec-wasi-http", path = "../../libs/wasi-http" } diff --git a/registry/native/crates/commands/cut/Cargo.toml b/registry/native/crates/commands/cut/Cargo.toml deleted file mode 100644 index 75e38c3add..0000000000 --- a/registry/native/crates/commands/cut/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-cut" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "cut standalone binary for secure-exec VM" - -[[bin]] -name = "cut" -path = "src/main.rs" - -[dependencies] -uu_cut = "0.7.0" diff --git a/registry/native/crates/commands/date/Cargo.toml b/registry/native/crates/commands/date/Cargo.toml deleted file mode 100644 index 54f324a982..0000000000 --- a/registry/native/crates/commands/date/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-date" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "date standalone binary for secure-exec VM" - -[[bin]] -name = "date" -path = "src/main.rs" - -[dependencies] -uu_date = "0.7.0" diff --git a/registry/native/crates/commands/dd/Cargo.toml b/registry/native/crates/commands/dd/Cargo.toml deleted file mode 100644 index 9f31bd6ea4..0000000000 --- a/registry/native/crates/commands/dd/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-dd" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "dd standalone binary for secure-exec VM" - -[[bin]] -name = "dd" -path = "src/main.rs" - -[dependencies] -uu_dd = "0.7.0" diff --git a/registry/native/crates/commands/diff/Cargo.toml b/registry/native/crates/commands/diff/Cargo.toml deleted file mode 100644 index ea9f00c8a2..0000000000 --- a/registry/native/crates/commands/diff/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-diff" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "diff standalone binary for secure-exec VM" - -[[bin]] -name = "diff" -path = "src/main.rs" - -[dependencies] -secureexec-diff = { path = "../../libs/diff" } diff --git a/registry/native/crates/commands/dircolors/Cargo.toml b/registry/native/crates/commands/dircolors/Cargo.toml deleted file mode 100644 index 2fb6f1a59d..0000000000 --- a/registry/native/crates/commands/dircolors/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-dircolors" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "dircolors standalone binary for secure-exec VM" - -[[bin]] -name = "dircolors" -path = "src/main.rs" - -[dependencies] -uu_dircolors = "0.7.0" diff --git a/registry/native/crates/commands/dirname/Cargo.toml b/registry/native/crates/commands/dirname/Cargo.toml deleted file mode 100644 index 26e18d7de7..0000000000 --- a/registry/native/crates/commands/dirname/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-dirname" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "dirname standalone binary for secure-exec VM" - -[[bin]] -name = "dirname" -path = "src/main.rs" - -[dependencies] -uu_dirname = "0.7.0" diff --git a/registry/native/crates/commands/du/Cargo.toml b/registry/native/crates/commands/du/Cargo.toml deleted file mode 100644 index b2101f3445..0000000000 --- a/registry/native/crates/commands/du/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-du" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "du standalone binary for secure-exec VM" - -[[bin]] -name = "du" -path = "src/main.rs" - -[dependencies] -secureexec-du = { path = "../../libs/du" } diff --git a/registry/native/crates/commands/echo/Cargo.toml b/registry/native/crates/commands/echo/Cargo.toml deleted file mode 100644 index 09e32de696..0000000000 --- a/registry/native/crates/commands/echo/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-echo" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "echo standalone binary for secure-exec VM" - -[[bin]] -name = "echo" -path = "src/main.rs" - -[dependencies] -uu_echo = "0.7.0" diff --git a/registry/native/crates/commands/env/Cargo.toml b/registry/native/crates/commands/env/Cargo.toml deleted file mode 100644 index f214d9ac47..0000000000 --- a/registry/native/crates/commands/env/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-env" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "env standalone binary for secure-exec VM" - -[[bin]] -name = "env" -path = "src/main.rs" - -[dependencies] -shims = { package = "secureexec-shims", path = "../../libs/shims" } diff --git a/registry/native/crates/commands/expand/Cargo.toml b/registry/native/crates/commands/expand/Cargo.toml deleted file mode 100644 index 9440c6bb9e..0000000000 --- a/registry/native/crates/commands/expand/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-expand" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "expand standalone binary for secure-exec VM" - -[[bin]] -name = "expand" -path = "src/main.rs" - -[dependencies] -uu_expand = "0.7.0" diff --git a/registry/native/crates/commands/expr/Cargo.toml b/registry/native/crates/commands/expr/Cargo.toml deleted file mode 100644 index 68acb90733..0000000000 --- a/registry/native/crates/commands/expr/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-expr" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "expr standalone binary for secure-exec VM" - -[[bin]] -name = "expr" -path = "src/main.rs" - -[dependencies] -secureexec-expr = { path = "../../libs/expr" } diff --git a/registry/native/crates/commands/factor/Cargo.toml b/registry/native/crates/commands/factor/Cargo.toml deleted file mode 100644 index 3ded9a0c20..0000000000 --- a/registry/native/crates/commands/factor/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-factor" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "factor standalone binary for secure-exec VM" - -[[bin]] -name = "factor" -path = "src/main.rs" - -[dependencies] -uu_factor = "0.7.0" diff --git a/registry/native/crates/commands/false/Cargo.toml b/registry/native/crates/commands/false/Cargo.toml deleted file mode 100644 index f30511a365..0000000000 --- a/registry/native/crates/commands/false/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-false" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "false standalone binary for secure-exec VM" - -[[bin]] -name = "false" -path = "src/main.rs" - -[dependencies] -uu_false = "0.7.0" diff --git a/registry/native/crates/commands/fd/Cargo.toml b/registry/native/crates/commands/fd/Cargo.toml deleted file mode 100644 index 12f78cdd30..0000000000 --- a/registry/native/crates/commands/fd/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-fd" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "fd standalone binary for secure-exec VM" - -[[bin]] -name = "fd" -path = "src/main.rs" - -[dependencies] -secureexec-fd = { path = "../../libs/fd" } diff --git a/registry/native/crates/commands/fd/src/main.rs b/registry/native/crates/commands/fd/src/main.rs deleted file mode 100644 index e74e129d80..0000000000 --- a/registry/native/crates/commands/fd/src/main.rs +++ /dev/null @@ -1,4 +0,0 @@ -fn main() { - let args: Vec = std::env::args_os().collect(); - std::process::exit(secureexec_fd::main(args)); -} diff --git a/registry/native/crates/commands/file/Cargo.toml b/registry/native/crates/commands/file/Cargo.toml deleted file mode 100644 index cabc723226..0000000000 --- a/registry/native/crates/commands/file/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-file" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "file standalone binary for secure-exec VM" - -[[bin]] -name = "file" -path = "src/main.rs" - -[dependencies] -secureexec-file-cmd = { path = "../../libs/file-cmd" } diff --git a/registry/native/crates/commands/find/Cargo.toml b/registry/native/crates/commands/find/Cargo.toml deleted file mode 100644 index 1b670a6fcf..0000000000 --- a/registry/native/crates/commands/find/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-find" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "find standalone binary for secure-exec VM" - -[[bin]] -name = "find" -path = "src/main.rs" - -[dependencies] -secureexec-find = { path = "../../libs/find" } diff --git a/registry/native/crates/commands/find/src/main.rs b/registry/native/crates/commands/find/src/main.rs deleted file mode 100644 index 5887b1a7a4..0000000000 --- a/registry/native/crates/commands/find/src/main.rs +++ /dev/null @@ -1,4 +0,0 @@ -fn main() { - let args: Vec = std::env::args_os().collect(); - std::process::exit(secureexec_find::main(args)); -} diff --git a/registry/native/crates/commands/fmt/Cargo.toml b/registry/native/crates/commands/fmt/Cargo.toml deleted file mode 100644 index cb2e5ae670..0000000000 --- a/registry/native/crates/commands/fmt/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-fmt" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "fmt standalone binary for secure-exec VM" - -[[bin]] -name = "fmt" -path = "src/main.rs" - -[dependencies] -uu_fmt = "0.7.0" diff --git a/registry/native/crates/commands/fold/Cargo.toml b/registry/native/crates/commands/fold/Cargo.toml deleted file mode 100644 index a8b067be43..0000000000 --- a/registry/native/crates/commands/fold/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-fold" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "fold standalone binary for secure-exec VM" - -[[bin]] -name = "fold" -path = "src/main.rs" - -[dependencies] -uu_fold = "0.7.0" diff --git a/registry/native/crates/commands/git/Cargo.toml b/registry/native/crates/commands/git/Cargo.toml deleted file mode 100644 index 3ae1427172..0000000000 --- a/registry/native/crates/commands/git/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-git" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "git standalone binary for secure-exec VM" - -[[bin]] -name = "git" -path = "src/main.rs" - -[dependencies] -secureexec-git = { path = "../../libs/git" } diff --git a/registry/native/crates/commands/git/src/main.rs b/registry/native/crates/commands/git/src/main.rs deleted file mode 100644 index 83886647f0..0000000000 --- a/registry/native/crates/commands/git/src/main.rs +++ /dev/null @@ -1,4 +0,0 @@ -fn main() { - let args: Vec = std::env::args_os().collect(); - std::process::exit(secureexec_git::main(args)); -} diff --git a/registry/native/crates/commands/grep/Cargo.toml b/registry/native/crates/commands/grep/Cargo.toml deleted file mode 100644 index fc3189a8d5..0000000000 --- a/registry/native/crates/commands/grep/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-grep" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "grep standalone binary for secure-exec VM" - -[[bin]] -name = "grep" -path = "src/main.rs" - -[dependencies] -secureexec-grep = { path = "../../libs/grep" } diff --git a/registry/native/crates/commands/grep/src/main.rs b/registry/native/crates/commands/grep/src/main.rs deleted file mode 100644 index ef8a789bdf..0000000000 --- a/registry/native/crates/commands/grep/src/main.rs +++ /dev/null @@ -1,13 +0,0 @@ -fn main() { - use std::io::Write; - - let args: Vec = std::env::args_os().collect(); - let mut code = secureexec_grep::main(args); - if let Err(error) = std::io::stdout().flush() { - eprintln!("Error flushing stdout: {error}"); - if code == 0 { - code = 2; - } - } - std::process::exit(code); -} diff --git a/registry/native/crates/commands/gzip/Cargo.toml b/registry/native/crates/commands/gzip/Cargo.toml deleted file mode 100644 index c6f1842eb4..0000000000 --- a/registry/native/crates/commands/gzip/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-gzip" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "gzip standalone binary for secure-exec VM" - -[[bin]] -name = "gzip" -path = "src/main.rs" - -[dependencies] -secureexec-gzip = { path = "../../libs/gzip" } diff --git a/registry/native/crates/commands/head/Cargo.toml b/registry/native/crates/commands/head/Cargo.toml deleted file mode 100644 index 49d35a6e1b..0000000000 --- a/registry/native/crates/commands/head/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-head" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "head standalone binary for secure-exec VM" - -[[bin]] -name = "head" -path = "src/main.rs" - -[dependencies] -uu_head = "0.7.0" diff --git a/registry/native/crates/commands/join/Cargo.toml b/registry/native/crates/commands/join/Cargo.toml deleted file mode 100644 index 408d1ceb4d..0000000000 --- a/registry/native/crates/commands/join/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-join" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "join standalone binary for secure-exec VM" - -[[bin]] -name = "join" -path = "src/main.rs" - -[dependencies] -uu_join = "0.7.0" diff --git a/registry/native/crates/commands/jq/Cargo.toml b/registry/native/crates/commands/jq/Cargo.toml deleted file mode 100644 index 31e46ae09f..0000000000 --- a/registry/native/crates/commands/jq/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-jq" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "jq standalone binary for secure-exec VM" - -[[bin]] -name = "jq" -path = "src/main.rs" - -[dependencies] -secureexec-jq = { path = "../../libs/jq" } diff --git a/registry/native/crates/commands/link/Cargo.toml b/registry/native/crates/commands/link/Cargo.toml deleted file mode 100644 index 79d9091369..0000000000 --- a/registry/native/crates/commands/link/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-link" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "link standalone binary for secure-exec VM" - -[[bin]] -name = "link" -path = "src/main.rs" - -[dependencies] -uu_link = "0.7.0" diff --git a/registry/native/crates/commands/ln/Cargo.toml b/registry/native/crates/commands/ln/Cargo.toml deleted file mode 100644 index ebb02bfe6e..0000000000 --- a/registry/native/crates/commands/ln/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-ln" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "ln standalone binary for secure-exec VM" - -[[bin]] -name = "ln" -path = "src/main.rs" - -[dependencies] -uu_ln = "0.7.0" diff --git a/registry/native/crates/commands/logname/Cargo.toml b/registry/native/crates/commands/logname/Cargo.toml deleted file mode 100644 index 6f7bbf5b76..0000000000 --- a/registry/native/crates/commands/logname/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-logname" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "logname standalone binary for secure-exec VM" - -[[bin]] -name = "logname" -path = "src/main.rs" - -[dependencies] -uu_logname = "0.7.0" diff --git a/registry/native/crates/commands/ls/Cargo.toml b/registry/native/crates/commands/ls/Cargo.toml deleted file mode 100644 index 025bfdfa03..0000000000 --- a/registry/native/crates/commands/ls/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-ls" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "ls standalone binary for secure-exec VM" - -[[bin]] -name = "ls" -path = "src/main.rs" - -[dependencies] -uu_ls = "0.7.0" diff --git a/registry/native/crates/commands/md5sum/Cargo.toml b/registry/native/crates/commands/md5sum/Cargo.toml deleted file mode 100644 index bd499cdc50..0000000000 --- a/registry/native/crates/commands/md5sum/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-md5sum" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "md5sum standalone binary for secure-exec VM" - -[[bin]] -name = "md5sum" -path = "src/main.rs" - -[dependencies] -uu_md5sum = "0.7.0" diff --git a/registry/native/crates/commands/mkdir/Cargo.toml b/registry/native/crates/commands/mkdir/Cargo.toml deleted file mode 100644 index 2e2cc2de94..0000000000 --- a/registry/native/crates/commands/mkdir/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-mkdir" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "mkdir standalone binary for secure-exec VM" - -[[bin]] -name = "mkdir" -path = "src/main.rs" - -[dependencies] -uu_mkdir = "0.7.0" diff --git a/registry/native/crates/commands/mktemp/Cargo.toml b/registry/native/crates/commands/mktemp/Cargo.toml deleted file mode 100644 index faa0c28810..0000000000 --- a/registry/native/crates/commands/mktemp/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-mktemp" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "mktemp standalone binary for secure-exec VM" - -[[bin]] -name = "mktemp" -path = "src/main.rs" - -[dependencies] -uu_mktemp = "0.7.0" diff --git a/registry/native/crates/commands/mv/Cargo.toml b/registry/native/crates/commands/mv/Cargo.toml deleted file mode 100644 index 3e52df17b6..0000000000 --- a/registry/native/crates/commands/mv/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-mv" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "mv standalone binary for secure-exec VM" - -[[bin]] -name = "mv" -path = "src/main.rs" - -[dependencies] -uu_mv = "0.7.0" diff --git a/registry/native/crates/commands/nice/Cargo.toml b/registry/native/crates/commands/nice/Cargo.toml deleted file mode 100644 index 5f00ac9f4b..0000000000 --- a/registry/native/crates/commands/nice/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-nice" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "nice standalone binary for secure-exec VM" - -[[bin]] -name = "nice" -path = "src/main.rs" - -[dependencies] -shims = { package = "secureexec-shims", path = "../../libs/shims" } diff --git a/registry/native/crates/commands/nl/Cargo.toml b/registry/native/crates/commands/nl/Cargo.toml deleted file mode 100644 index d1858d3be7..0000000000 --- a/registry/native/crates/commands/nl/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-nl" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "nl standalone binary for secure-exec VM" - -[[bin]] -name = "nl" -path = "src/main.rs" - -[dependencies] -uu_nl = "0.7.0" diff --git a/registry/native/crates/commands/nohup/Cargo.toml b/registry/native/crates/commands/nohup/Cargo.toml deleted file mode 100644 index ee2b24fa34..0000000000 --- a/registry/native/crates/commands/nohup/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-nohup" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "nohup standalone binary for secure-exec VM" - -[[bin]] -name = "nohup" -path = "src/main.rs" - -[dependencies] -shims = { package = "secureexec-shims", path = "../../libs/shims" } diff --git a/registry/native/crates/commands/nproc/Cargo.toml b/registry/native/crates/commands/nproc/Cargo.toml deleted file mode 100644 index 9f05c612cc..0000000000 --- a/registry/native/crates/commands/nproc/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-nproc" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "nproc standalone binary for secure-exec VM" - -[[bin]] -name = "nproc" -path = "src/main.rs" - -[dependencies] -uu_nproc = "0.7.0" diff --git a/registry/native/crates/commands/numfmt/Cargo.toml b/registry/native/crates/commands/numfmt/Cargo.toml deleted file mode 100644 index 18fae1f3a5..0000000000 --- a/registry/native/crates/commands/numfmt/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-numfmt" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "numfmt standalone binary for secure-exec VM" - -[[bin]] -name = "numfmt" -path = "src/main.rs" - -[dependencies] -uu_numfmt = "0.7.0" diff --git a/registry/native/crates/commands/od/Cargo.toml b/registry/native/crates/commands/od/Cargo.toml deleted file mode 100644 index 6e28c2db60..0000000000 --- a/registry/native/crates/commands/od/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-od" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "od standalone binary for secure-exec VM" - -[[bin]] -name = "od" -path = "src/main.rs" - -[dependencies] -uu_od = "0.7.0" diff --git a/registry/native/crates/commands/paste/Cargo.toml b/registry/native/crates/commands/paste/Cargo.toml deleted file mode 100644 index 87fb6f9c84..0000000000 --- a/registry/native/crates/commands/paste/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-paste" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "paste standalone binary for secure-exec VM" - -[[bin]] -name = "paste" -path = "src/main.rs" - -[dependencies] -uu_paste = "0.7.0" diff --git a/registry/native/crates/commands/pathchk/Cargo.toml b/registry/native/crates/commands/pathchk/Cargo.toml deleted file mode 100644 index a5bd0d8c9a..0000000000 --- a/registry/native/crates/commands/pathchk/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-pathchk" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "pathchk standalone binary for secure-exec VM" - -[[bin]] -name = "pathchk" -path = "src/main.rs" - -[dependencies] -uu_pathchk = "0.7.0" diff --git a/registry/native/crates/commands/printenv/Cargo.toml b/registry/native/crates/commands/printenv/Cargo.toml deleted file mode 100644 index 463418444e..0000000000 --- a/registry/native/crates/commands/printenv/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-printenv" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "printenv standalone binary for secure-exec VM" - -[[bin]] -name = "printenv" -path = "src/main.rs" - -[dependencies] -uu_printenv = "0.7.0" diff --git a/registry/native/crates/commands/printf/Cargo.toml b/registry/native/crates/commands/printf/Cargo.toml deleted file mode 100644 index 3ffe7fe58a..0000000000 --- a/registry/native/crates/commands/printf/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-printf" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "printf standalone binary for secure-exec VM" - -[[bin]] -name = "printf" -path = "src/main.rs" - -[dependencies] -uu_printf = "0.7.0" diff --git a/registry/native/crates/commands/ptx/Cargo.toml b/registry/native/crates/commands/ptx/Cargo.toml deleted file mode 100644 index 7176a63af2..0000000000 --- a/registry/native/crates/commands/ptx/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-ptx" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "ptx standalone binary for secure-exec VM" - -[[bin]] -name = "ptx" -path = "src/main.rs" - -[dependencies] -uu_ptx = "0.7.0" diff --git a/registry/native/crates/commands/pwd/Cargo.toml b/registry/native/crates/commands/pwd/Cargo.toml deleted file mode 100644 index d61413509a..0000000000 --- a/registry/native/crates/commands/pwd/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-pwd" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "pwd standalone binary for secure-exec VM" - -[[bin]] -name = "pwd" -path = "src/main.rs" - -[dependencies] -uu_pwd = "0.7.0" diff --git a/registry/native/crates/commands/readlink/Cargo.toml b/registry/native/crates/commands/readlink/Cargo.toml deleted file mode 100644 index e8c96fc77e..0000000000 --- a/registry/native/crates/commands/readlink/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-readlink" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "readlink standalone binary for secure-exec VM" - -[[bin]] -name = "readlink" -path = "src/main.rs" - -[dependencies] -uu_readlink = "0.7.0" diff --git a/registry/native/crates/commands/realpath/Cargo.toml b/registry/native/crates/commands/realpath/Cargo.toml deleted file mode 100644 index ad8fa957cf..0000000000 --- a/registry/native/crates/commands/realpath/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-realpath" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "realpath standalone binary for secure-exec VM" - -[[bin]] -name = "realpath" -path = "src/main.rs" - -[dependencies] -uu_realpath = "0.7.0" diff --git a/registry/native/crates/commands/rev/Cargo.toml b/registry/native/crates/commands/rev/Cargo.toml deleted file mode 100644 index d08020ac8b..0000000000 --- a/registry/native/crates/commands/rev/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-rev" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "rev standalone binary for secure-exec VM" - -[[bin]] -name = "rev" -path = "src/main.rs" - -[dependencies] -secureexec-rev = { path = "../../libs/rev" } diff --git a/registry/native/crates/commands/rg/Cargo.toml b/registry/native/crates/commands/rg/Cargo.toml deleted file mode 100644 index cd1ff4690e..0000000000 --- a/registry/native/crates/commands/rg/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-rg" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "rg (ripgrep) standalone binary for secure-exec VM" - -[[bin]] -name = "rg" -path = "src/main.rs" - -[dependencies] -secureexec-grep = { path = "../../libs/grep" } diff --git a/registry/native/crates/commands/rg/src/main.rs b/registry/native/crates/commands/rg/src/main.rs deleted file mode 100644 index e44c45f021..0000000000 --- a/registry/native/crates/commands/rg/src/main.rs +++ /dev/null @@ -1,4 +0,0 @@ -fn main() { - let args: Vec = std::env::args_os().collect(); - std::process::exit(secureexec_grep::rg(args)); -} diff --git a/registry/native/crates/commands/rm/Cargo.toml b/registry/native/crates/commands/rm/Cargo.toml deleted file mode 100644 index eb69989f9b..0000000000 --- a/registry/native/crates/commands/rm/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-rm" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "rm standalone binary for secure-exec VM" - -[[bin]] -name = "rm" -path = "src/main.rs" - -[dependencies] -uu_rm = "0.7.0" diff --git a/registry/native/crates/commands/rmdir/Cargo.toml b/registry/native/crates/commands/rmdir/Cargo.toml deleted file mode 100644 index 8773c2ec4c..0000000000 --- a/registry/native/crates/commands/rmdir/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-rmdir" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "rmdir standalone binary for secure-exec VM" - -[[bin]] -name = "rmdir" -path = "src/main.rs" - -[dependencies] -uu_rmdir = "0.7.0" diff --git a/registry/native/crates/commands/sed/Cargo.toml b/registry/native/crates/commands/sed/Cargo.toml deleted file mode 100644 index 9e006c3ba3..0000000000 --- a/registry/native/crates/commands/sed/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-sed" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "sed standalone binary for secure-exec VM" - -[[bin]] -name = "sed" -path = "src/main.rs" - -[dependencies] -sed = "0.1.1" diff --git a/registry/native/crates/commands/seq/Cargo.toml b/registry/native/crates/commands/seq/Cargo.toml deleted file mode 100644 index 83762f6eb9..0000000000 --- a/registry/native/crates/commands/seq/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-seq" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "seq standalone binary for secure-exec VM" - -[[bin]] -name = "seq" -path = "src/main.rs" - -[dependencies] -uu_seq = "0.7.0" diff --git a/registry/native/crates/commands/sh/Cargo.toml b/registry/native/crates/commands/sh/Cargo.toml deleted file mode 100644 index 3034c95973..0000000000 --- a/registry/native/crates/commands/sh/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "cmd-sh" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "sh standalone binary for secure-exec VM" - -[[bin]] -name = "sh" -path = "src/main.rs" - -# `reedline` is enabled on ALL targets, including wasm: the vendored wasi -# patches supply what it needs (crossterm wasi backend, fd-lock, and -# brush-interactive's cfg(target_os = "wasi") alternatives to the -# tokio block_in_place paths), per the "behave like native Linux" rule. -# `minimal` stays available as an explicit opt-out backend. -[dependencies] -brush-shell = { version = "0.3.0", default-features = false, features = ["reedline", "minimal"] } diff --git a/registry/native/crates/commands/sha1sum/Cargo.toml b/registry/native/crates/commands/sha1sum/Cargo.toml deleted file mode 100644 index 8247916e83..0000000000 --- a/registry/native/crates/commands/sha1sum/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-sha1sum" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "sha1sum standalone binary for secure-exec VM" - -[[bin]] -name = "sha1sum" -path = "src/main.rs" - -[dependencies] -uu_sha1sum = "0.7.0" diff --git a/registry/native/crates/commands/sha224sum/Cargo.toml b/registry/native/crates/commands/sha224sum/Cargo.toml deleted file mode 100644 index 8f34c4db79..0000000000 --- a/registry/native/crates/commands/sha224sum/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-sha224sum" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "sha224sum standalone binary for secure-exec VM" - -[[bin]] -name = "sha224sum" -path = "src/main.rs" - -[dependencies] -uu_sha224sum = "0.7.0" diff --git a/registry/native/crates/commands/sha256sum/Cargo.toml b/registry/native/crates/commands/sha256sum/Cargo.toml deleted file mode 100644 index 8d4edba469..0000000000 --- a/registry/native/crates/commands/sha256sum/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-sha256sum" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "sha256sum standalone binary for secure-exec VM" - -[[bin]] -name = "sha256sum" -path = "src/main.rs" - -[dependencies] -uu_sha256sum = "0.7.0" diff --git a/registry/native/crates/commands/sha384sum/Cargo.toml b/registry/native/crates/commands/sha384sum/Cargo.toml deleted file mode 100644 index dc9fc1b53e..0000000000 --- a/registry/native/crates/commands/sha384sum/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-sha384sum" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "sha384sum standalone binary for secure-exec VM" - -[[bin]] -name = "sha384sum" -path = "src/main.rs" - -[dependencies] -uu_sha384sum = "0.7.0" diff --git a/registry/native/crates/commands/sha512sum/Cargo.toml b/registry/native/crates/commands/sha512sum/Cargo.toml deleted file mode 100644 index 6df4303347..0000000000 --- a/registry/native/crates/commands/sha512sum/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-sha512sum" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "sha512sum standalone binary for secure-exec VM" - -[[bin]] -name = "sha512sum" -path = "src/main.rs" - -[dependencies] -uu_sha512sum = "0.7.0" diff --git a/registry/native/crates/commands/shred/Cargo.toml b/registry/native/crates/commands/shred/Cargo.toml deleted file mode 100644 index 841e6f3057..0000000000 --- a/registry/native/crates/commands/shred/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-shred" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "shred standalone binary for secure-exec VM" - -[[bin]] -name = "shred" -path = "src/main.rs" - -[dependencies] -uu_shred = "0.7.0" diff --git a/registry/native/crates/commands/shuf/Cargo.toml b/registry/native/crates/commands/shuf/Cargo.toml deleted file mode 100644 index 0b9b499b18..0000000000 --- a/registry/native/crates/commands/shuf/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-shuf" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "shuf standalone binary for secure-exec VM" - -[[bin]] -name = "shuf" -path = "src/main.rs" - -[dependencies] -uu_shuf = "0.7.0" diff --git a/registry/native/crates/commands/sleep/Cargo.toml b/registry/native/crates/commands/sleep/Cargo.toml deleted file mode 100644 index 9e87d2649b..0000000000 --- a/registry/native/crates/commands/sleep/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-sleep" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "sleep standalone binary for secure-exec VM" - -[[bin]] -name = "sleep" -path = "src/main.rs" - -[dependencies] -secureexec-builtins = { path = "../../libs/builtins" } diff --git a/registry/native/crates/commands/sort/Cargo.toml b/registry/native/crates/commands/sort/Cargo.toml deleted file mode 100644 index 3ed6e1157f..0000000000 --- a/registry/native/crates/commands/sort/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-sort" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "sort standalone binary for secure-exec VM" - -[[bin]] -name = "sort" -path = "src/main.rs" - -[dependencies] -uu_sort = "0.7.0" diff --git a/registry/native/crates/commands/split/Cargo.toml b/registry/native/crates/commands/split/Cargo.toml deleted file mode 100644 index 76ca0362c6..0000000000 --- a/registry/native/crates/commands/split/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-split" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "split standalone binary for secure-exec VM" - -[[bin]] -name = "split" -path = "src/main.rs" - -[dependencies] -uu_split = "0.7.0" diff --git a/registry/native/crates/commands/stat/Cargo.toml b/registry/native/crates/commands/stat/Cargo.toml deleted file mode 100644 index d0587c9e33..0000000000 --- a/registry/native/crates/commands/stat/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-stat" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "stat standalone binary for secure-exec VM" - -[[bin]] -name = "stat" -path = "src/main.rs" - -[dependencies] -uu_stat = "0.7.0" diff --git a/registry/native/crates/commands/stdbuf/Cargo.toml b/registry/native/crates/commands/stdbuf/Cargo.toml deleted file mode 100644 index 96ecbb0a3f..0000000000 --- a/registry/native/crates/commands/stdbuf/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-stdbuf" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "stdbuf standalone binary for secure-exec VM" - -[[bin]] -name = "stdbuf" -path = "src/main.rs" - -[dependencies] -shims = { package = "secureexec-shims", path = "../../libs/shims" } diff --git a/registry/native/crates/commands/strings/Cargo.toml b/registry/native/crates/commands/strings/Cargo.toml deleted file mode 100644 index 7ce5f58a39..0000000000 --- a/registry/native/crates/commands/strings/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-strings" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "strings standalone binary for secure-exec VM" - -[[bin]] -name = "strings" -path = "src/main.rs" - -[dependencies] -secureexec-strings-cmd = { path = "../../libs/strings-cmd" } diff --git a/registry/native/crates/commands/sum/Cargo.toml b/registry/native/crates/commands/sum/Cargo.toml deleted file mode 100644 index 5fc71bfcdb..0000000000 --- a/registry/native/crates/commands/sum/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-sum" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "sum standalone binary for secure-exec VM" - -[[bin]] -name = "sum" -path = "src/main.rs" - -[dependencies] -uu_sum = "0.7.0" diff --git a/registry/native/crates/commands/tac/Cargo.toml b/registry/native/crates/commands/tac/Cargo.toml deleted file mode 100644 index c55250d7bd..0000000000 --- a/registry/native/crates/commands/tac/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-tac" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "tac standalone binary for secure-exec VM" - -[[bin]] -name = "tac" -path = "src/main.rs" - -[dependencies] -uu_tac = "0.7.0" diff --git a/registry/native/crates/commands/tail/Cargo.toml b/registry/native/crates/commands/tail/Cargo.toml deleted file mode 100644 index e47b9737ba..0000000000 --- a/registry/native/crates/commands/tail/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-tail" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "tail standalone binary for secure-exec VM" - -[[bin]] -name = "tail" -path = "src/main.rs" - -[dependencies] -uu_tail = "0.7.0" diff --git a/registry/native/crates/commands/tar/Cargo.toml b/registry/native/crates/commands/tar/Cargo.toml deleted file mode 100644 index d711b70205..0000000000 --- a/registry/native/crates/commands/tar/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-tar" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "tar standalone binary for secure-exec VM" - -[[bin]] -name = "tar" -path = "src/main.rs" - -[dependencies] -secureexec-tar = { path = "../../libs/tar" } diff --git a/registry/native/crates/commands/tee/Cargo.toml b/registry/native/crates/commands/tee/Cargo.toml deleted file mode 100644 index 94185e3cd8..0000000000 --- a/registry/native/crates/commands/tee/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-tee" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "tee standalone binary for secure-exec VM" - -[[bin]] -name = "tee" -path = "src/main.rs" - -[dependencies] -uu_tee = "0.7.0" diff --git a/registry/native/crates/commands/test/Cargo.toml b/registry/native/crates/commands/test/Cargo.toml deleted file mode 100644 index 9fe4f0f638..0000000000 --- a/registry/native/crates/commands/test/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-test" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "test/[ standalone binary for secure-exec VM" - -[[bin]] -name = "test" -path = "src/main.rs" - -[dependencies] -secureexec-builtins = { path = "../../libs/builtins" } diff --git a/registry/native/crates/commands/timeout/Cargo.toml b/registry/native/crates/commands/timeout/Cargo.toml deleted file mode 100644 index f872586c9f..0000000000 --- a/registry/native/crates/commands/timeout/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-timeout" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "timeout standalone binary for secure-exec VM" - -[[bin]] -name = "timeout" -path = "src/main.rs" - -[dependencies] -shims = { package = "secureexec-shims", path = "../../libs/shims" } diff --git a/registry/native/crates/commands/touch/Cargo.toml b/registry/native/crates/commands/touch/Cargo.toml deleted file mode 100644 index e88d117a95..0000000000 --- a/registry/native/crates/commands/touch/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-touch" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "touch standalone binary for secure-exec VM" - -[[bin]] -name = "touch" -path = "src/main.rs" - -[dependencies] -uu_touch = "0.7.0" diff --git a/registry/native/crates/commands/tr/Cargo.toml b/registry/native/crates/commands/tr/Cargo.toml deleted file mode 100644 index fcaa590eb7..0000000000 --- a/registry/native/crates/commands/tr/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-tr" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "tr standalone binary for secure-exec VM" - -[[bin]] -name = "tr" -path = "src/main.rs" - -[dependencies] -uu_tr = "0.7.0" diff --git a/registry/native/crates/commands/tree/Cargo.toml b/registry/native/crates/commands/tree/Cargo.toml deleted file mode 100644 index 73e0df78d5..0000000000 --- a/registry/native/crates/commands/tree/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-tree" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "tree standalone binary for secure-exec VM" - -[[bin]] -name = "tree" -path = "src/main.rs" - -[dependencies] -secureexec-tree = { path = "../../libs/tree" } diff --git a/registry/native/crates/commands/tree/src/main.rs b/registry/native/crates/commands/tree/src/main.rs deleted file mode 100644 index ee799a4287..0000000000 --- a/registry/native/crates/commands/tree/src/main.rs +++ /dev/null @@ -1,4 +0,0 @@ -fn main() { - let args: Vec = std::env::args_os().collect(); - std::process::exit(secureexec_tree::main(args)); -} diff --git a/registry/native/crates/commands/true/Cargo.toml b/registry/native/crates/commands/true/Cargo.toml deleted file mode 100644 index 95faf97085..0000000000 --- a/registry/native/crates/commands/true/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-true" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "true standalone binary for secure-exec VM" - -[[bin]] -name = "true" -path = "src/main.rs" - -[dependencies] -uu_true = "0.7.0" diff --git a/registry/native/crates/commands/truncate/Cargo.toml b/registry/native/crates/commands/truncate/Cargo.toml deleted file mode 100644 index 8a2ea8eef4..0000000000 --- a/registry/native/crates/commands/truncate/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-truncate" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "truncate standalone binary for secure-exec VM" - -[[bin]] -name = "truncate" -path = "src/main.rs" - -[dependencies] -uu_truncate = "0.7.0" diff --git a/registry/native/crates/commands/tsort/Cargo.toml b/registry/native/crates/commands/tsort/Cargo.toml deleted file mode 100644 index 9e17b0c29d..0000000000 --- a/registry/native/crates/commands/tsort/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-tsort" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "tsort standalone binary for secure-exec VM" - -[[bin]] -name = "tsort" -path = "src/main.rs" - -[dependencies] -uu_tsort = "0.7.0" diff --git a/registry/native/crates/commands/uname/Cargo.toml b/registry/native/crates/commands/uname/Cargo.toml deleted file mode 100644 index 4b3d810d0c..0000000000 --- a/registry/native/crates/commands/uname/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-uname" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "uname standalone binary for secure-exec VM" - -[[bin]] -name = "uname" -path = "src/main.rs" - -[dependencies] -uu_uname = "0.7.0" diff --git a/registry/native/crates/commands/unexpand/Cargo.toml b/registry/native/crates/commands/unexpand/Cargo.toml deleted file mode 100644 index 539079e2c1..0000000000 --- a/registry/native/crates/commands/unexpand/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-unexpand" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "unexpand standalone binary for secure-exec VM" - -[[bin]] -name = "unexpand" -path = "src/main.rs" - -[dependencies] -uu_unexpand = "0.7.0" diff --git a/registry/native/crates/commands/uniq/Cargo.toml b/registry/native/crates/commands/uniq/Cargo.toml deleted file mode 100644 index 39cef76b95..0000000000 --- a/registry/native/crates/commands/uniq/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-uniq" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "uniq standalone binary for secure-exec VM" - -[[bin]] -name = "uniq" -path = "src/main.rs" - -[dependencies] -uu_uniq = "0.7.0" diff --git a/registry/native/crates/commands/unlink/Cargo.toml b/registry/native/crates/commands/unlink/Cargo.toml deleted file mode 100644 index e1a4498758..0000000000 --- a/registry/native/crates/commands/unlink/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-unlink" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "unlink standalone binary for secure-exec VM" - -[[bin]] -name = "unlink" -path = "src/main.rs" - -[dependencies] -uu_unlink = "0.7.0" diff --git a/registry/native/crates/commands/wc/Cargo.toml b/registry/native/crates/commands/wc/Cargo.toml deleted file mode 100644 index d2d3a0a73b..0000000000 --- a/registry/native/crates/commands/wc/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-wc" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "wc standalone binary for secure-exec VM" - -[[bin]] -name = "wc" -path = "src/main.rs" - -[dependencies] -uu_wc = "0.7.0" diff --git a/registry/native/crates/commands/which/Cargo.toml b/registry/native/crates/commands/which/Cargo.toml deleted file mode 100644 index 0a6ae7d233..0000000000 --- a/registry/native/crates/commands/which/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-which" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "which standalone binary for secure-exec VM" - -[[bin]] -name = "which" -path = "src/main.rs" - -[dependencies] -shims = { package = "secureexec-shims", path = "../../libs/shims" } diff --git a/registry/native/crates/commands/whoami/Cargo.toml b/registry/native/crates/commands/whoami/Cargo.toml deleted file mode 100644 index 5aaf67d173..0000000000 --- a/registry/native/crates/commands/whoami/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-whoami" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "whoami standalone binary for secure-exec VM" - -[[bin]] -name = "whoami" -path = "src/main.rs" - -[dependencies] -secureexec-builtins = { path = "../../libs/builtins" } diff --git a/registry/native/crates/commands/xargs/Cargo.toml b/registry/native/crates/commands/xargs/Cargo.toml deleted file mode 100644 index 71a8b8cd6f..0000000000 --- a/registry/native/crates/commands/xargs/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-xargs" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "xargs standalone binary for secure-exec VM" - -[[bin]] -name = "xargs" -path = "src/main.rs" - -[dependencies] -shims = { package = "secureexec-shims", path = "../../libs/shims" } diff --git a/registry/native/crates/commands/xargs/src/main.rs b/registry/native/crates/commands/xargs/src/main.rs deleted file mode 100644 index 0ef4547696..0000000000 --- a/registry/native/crates/commands/xargs/src/main.rs +++ /dev/null @@ -1,4 +0,0 @@ -fn main() { - let args: Vec = std::env::args_os().collect(); - std::process::exit(shims::xargs::xargs(args)); -} diff --git a/registry/native/crates/commands/yes/Cargo.toml b/registry/native/crates/commands/yes/Cargo.toml deleted file mode 100644 index 4e6da247bd..0000000000 --- a/registry/native/crates/commands/yes/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-yes" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "yes standalone binary for secure-exec VM" - -[[bin]] -name = "yes" -path = "src/main.rs" - -[dependencies] -uu_yes = "0.7.0" diff --git a/registry/native/crates/commands/yq/Cargo.toml b/registry/native/crates/commands/yq/Cargo.toml deleted file mode 100644 index 77dadef52c..0000000000 --- a/registry/native/crates/commands/yq/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cmd-yq" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "yq standalone binary for secure-exec VM" - -[[bin]] -name = "yq" -path = "src/main.rs" - -[dependencies] -secureexec-yq = { path = "../../libs/yq" } diff --git a/registry/native/crates/libs/awk/Cargo.toml b/registry/native/crates/libs/awk/Cargo.toml deleted file mode 100644 index 6de5ebc66f..0000000000 --- a/registry/native/crates/libs/awk/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "secureexec-awk" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "awk implementation for secure-exec standalone binaries" - -[dependencies] -awk-rs = "0.1.0" diff --git a/registry/native/crates/libs/column/Cargo.toml b/registry/native/crates/libs/column/Cargo.toml deleted file mode 100644 index 0215159ee9..0000000000 --- a/registry/native/crates/libs/column/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "secureexec-column" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "column implementation for secure-exec standalone binaries" - -[dependencies] diff --git a/registry/native/crates/libs/diff/Cargo.toml b/registry/native/crates/libs/diff/Cargo.toml deleted file mode 100644 index 614324b8ee..0000000000 --- a/registry/native/crates/libs/diff/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "secureexec-diff" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "diff implementation for secure-exec standalone binaries" - -[dependencies] -similar = "2" diff --git a/registry/native/crates/libs/diff/src/lib.rs b/registry/native/crates/libs/diff/src/lib.rs deleted file mode 100644 index 04dbde97da..0000000000 --- a/registry/native/crates/libs/diff/src/lib.rs +++ /dev/null @@ -1,468 +0,0 @@ -//! diff -- compare files line by line using the `similar` crate - -use std::collections::HashSet; -use std::ffi::OsString; -use std::fs; -use std::io::{self, Write}; -use std::path::Path; - -use similar::{ChangeTag, TextDiff}; - -struct Options { - unified: bool, - context_fmt: bool, - context_lines: usize, - recursive: bool, - brief: bool, - ignore_case: bool, - ignore_whitespace: bool, - ignore_blank_lines: bool, -} - -impl Default for Options { - fn default() -> Self { - Self { - unified: false, - context_fmt: false, - context_lines: 3, - recursive: false, - brief: false, - ignore_case: false, - ignore_whitespace: false, - ignore_blank_lines: false, - } - } -} - -pub fn main(args: Vec) -> i32 { - let args: Vec = args - .iter() - .map(|a| a.to_string_lossy().to_string()) - .collect(); - let mut opts = Options::default(); - let mut files: Vec = Vec::new(); - let mut i = 1; - - while i < args.len() { - let arg = &args[i]; - match arg.as_str() { - "-u" | "--unified" => opts.unified = true, - "-c" | "--context" => opts.context_fmt = true, - "-r" | "-R" | "--recursive" => opts.recursive = true, - "-q" | "--brief" => opts.brief = true, - "-i" | "--ignore-case" => opts.ignore_case = true, - "-w" | "--ignore-all-space" => opts.ignore_whitespace = true, - "-B" | "--ignore-blank-lines" => opts.ignore_blank_lines = true, - s if s.starts_with("-U") => { - if let Ok(n) = s[2..].parse::() { - opts.unified = true; - opts.context_lines = n; - } - } - s if s.starts_with("-C") => { - if let Ok(n) = s[2..].parse::() { - opts.context_fmt = true; - opts.context_lines = n; - } - } - _ if !arg.starts_with('-') || arg == "-" => { - files.push(arg.clone()); - } - _ => { - eprintln!("diff: unknown option: {}", arg); - return 2; - } - } - i += 1; - } - - if files.len() != 2 { - eprintln!("diff: requires exactly two file or directory arguments"); - return 2; - } - - let path_a = Path::new(&files[0]); - let path_b = Path::new(&files[1]); - - let mut visited_dirs = HashSet::new(); - match diff_paths(path_a, path_b, &opts, &mut visited_dirs) { - Ok(has_diff) => { - if has_diff { - 1 - } else { - 0 - } - } - Err(e) => { - eprintln!("diff: {}", e); - 2 - } - } -} - -fn diff_paths( - path_a: &Path, - path_b: &Path, - opts: &Options, - visited_dirs: &mut HashSet<(std::path::PathBuf, std::path::PathBuf)>, -) -> Result { - let a_is_dir = path_a.is_dir(); - let b_is_dir = path_b.is_dir(); - - if a_is_dir && b_is_dir { - if opts.recursive { - diff_dirs(path_a, path_b, opts, visited_dirs) - } else { - Err(format!("{} is a directory", path_a.display())) - } - } else if a_is_dir || b_is_dir { - if a_is_dir { - let name = path_b.file_name().ok_or("invalid filename")?; - diff_files(&path_a.join(name), path_b, opts) - } else { - let name = path_a.file_name().ok_or("invalid filename")?; - diff_files(path_a, &path_b.join(name), opts) - } - } else { - diff_files(path_a, path_b, opts) - } -} - -fn diff_dirs( - dir_a: &Path, - dir_b: &Path, - opts: &Options, - visited_dirs: &mut HashSet<(std::path::PathBuf, std::path::PathBuf)>, -) -> Result { - let key = ( - fs::canonicalize(dir_a).map_err(|e| format!("{}: {}", dir_a.display(), e))?, - fs::canonicalize(dir_b).map_err(|e| format!("{}: {}", dir_b.display(), e))?, - ); - if !visited_dirs.insert(key) { - return Ok(false); - } - - let mut entries_a = list_dir(dir_a)?; - let mut entries_b = list_dir(dir_b)?; - entries_a.sort(); - entries_b.sort(); - - let mut all_names: Vec = Vec::new(); - for name in &entries_a { - all_names.push(name.clone()); - } - for name in &entries_b { - if !entries_a.contains(name) { - all_names.push(name.clone()); - } - } - all_names.sort(); - - let mut has_diff = false; - - for name in &all_names { - let pa = dir_a.join(name); - let pb = dir_b.join(name); - let a_exists = entries_a.contains(name); - let b_exists = entries_b.contains(name); - - if a_exists && !b_exists { - print_stdout_line(format_args!("Only in {}: {}", dir_a.display(), name))?; - has_diff = true; - } else if !a_exists && b_exists { - print_stdout_line(format_args!("Only in {}: {}", dir_b.display(), name))?; - has_diff = true; - } else { - match diff_paths(&pa, &pb, opts, visited_dirs) { - Ok(d) => { - if d { - has_diff = true; - } - } - Err(e) => { - return Err(e); - } - } - } - } - - Ok(has_diff) -} - -fn list_dir(dir: &Path) -> Result, String> { - let entries = fs::read_dir(dir).map_err(|e| format!("{}: {}", dir.display(), e))?; - let mut names = Vec::new(); - for entry in entries { - let entry = entry.map_err(|e| format!("{}: {}", dir.display(), e))?; - if let Some(name) = entry.file_name().to_str() { - names.push(name.to_string()); - } - } - Ok(names) -} - -fn preprocess(text: &str, opts: &Options) -> String { - let mut s = text.to_string(); - if opts.ignore_blank_lines { - let trailing = s.ends_with('\n'); - s = s - .lines() - .filter(|line| !line.trim().is_empty()) - .collect::>() - .join("\n"); - if trailing { - s.push('\n'); - } - } - if opts.ignore_case { - s = s.to_lowercase(); - } - if opts.ignore_whitespace { - let trailing = s.ends_with('\n'); - s = s - .lines() - .map(|line| { - let mut result = String::new(); - let mut in_ws = false; - for ch in line.chars() { - if ch.is_whitespace() { - if !in_ws { - result.push(' '); - in_ws = true; - } - } else { - result.push(ch); - in_ws = false; - } - } - let trimmed = result.trim(); - trimmed.to_string() - }) - .collect::>() - .join("\n"); - if trailing { - s.push('\n'); - } - } - s -} - -fn diff_files(path_a: &Path, path_b: &Path, opts: &Options) -> Result { - let text_a = read_file(path_a)?; - let text_b = read_file(path_b)?; - - // Check for differences (possibly with preprocessing) - let needs_pp = opts.ignore_case || opts.ignore_whitespace || opts.ignore_blank_lines; - let has_changes = if needs_pp { - preprocess(&text_a, opts) != preprocess(&text_b, opts) - } else { - text_a != text_b - }; - - if !has_changes { - return Ok(false); - } - - if opts.brief { - print_stdout_line(format_args!( - "Files {} and {} differ", - path_a.display(), - path_b.display() - ))?; - return Ok(true); - } - - // Diff the original text for output (all output logic inline to avoid lifetime issues) - let diff = TextDiff::from_lines(&text_a, &text_b); - let label_a = format!("{}", path_a.display()); - let label_b = format!("{}", path_b.display()); - let stdout = io::stdout(); - let mut out = stdout.lock(); - - if opts.unified { - writeln!(out, "--- {}", label_a).map_err(|e| format!("failed to write output: {e}"))?; - writeln!(out, "+++ {}", label_b).map_err(|e| format!("failed to write output: {e}"))?; - for hunk in diff - .unified_diff() - .context_radius(opts.context_lines) - .iter_hunks() - { - write!(out, "{}", hunk).map_err(|e| format!("failed to write output: {e}"))?; - } - } else if opts.context_fmt { - writeln!(out, "*** {}", label_a).map_err(|e| format!("failed to write output: {e}"))?; - writeln!(out, "--- {}", label_b).map_err(|e| format!("failed to write output: {e}"))?; - for hunk in diff - .unified_diff() - .context_radius(opts.context_lines) - .iter_hunks() - { - let mut old_lines: Vec<(ChangeTag, String)> = Vec::new(); - let mut new_lines: Vec<(ChangeTag, String)> = Vec::new(); - let mut old_start = 0usize; - let mut new_start = 0usize; - let mut first = true; - for change in hunk.iter_changes() { - if first { - old_start = change.old_index().unwrap_or(0) + 1; - new_start = change.new_index().unwrap_or(0) + 1; - first = false; - } - match change.tag() { - ChangeTag::Equal => { - old_lines.push((ChangeTag::Equal, change.value().to_string())); - new_lines.push((ChangeTag::Equal, change.value().to_string())); - } - ChangeTag::Delete => { - old_lines.push((ChangeTag::Delete, change.value().to_string())); - } - ChangeTag::Insert => { - new_lines.push((ChangeTag::Insert, change.value().to_string())); - } - } - } - let old_end = old_start + old_lines.len().saturating_sub(1); - let new_end = new_start + new_lines.len().saturating_sub(1); - writeln!(out, "***************").map_err(|e| format!("failed to write output: {e}"))?; - writeln!(out, "*** {},{} ****", old_start, old_end) - .map_err(|e| format!("failed to write output: {e}"))?; - for (tag, line) in &old_lines { - let prefix = match tag { - ChangeTag::Delete => "- ", - ChangeTag::Equal => " ", - _ => continue, - }; - write!(out, "{}{}", prefix, line) - .map_err(|e| format!("failed to write output: {e}"))?; - if !line.ends_with('\n') { - writeln!(out).map_err(|e| format!("failed to write output: {e}"))?; - } - } - writeln!(out, "--- {},{} ----", new_start, new_end) - .map_err(|e| format!("failed to write output: {e}"))?; - for (tag, line) in &new_lines { - let prefix = match tag { - ChangeTag::Insert => "+ ", - ChangeTag::Equal => " ", - _ => continue, - }; - write!(out, "{}{}", prefix, line) - .map_err(|e| format!("failed to write output: {e}"))?; - if !line.ends_with('\n') { - writeln!(out).map_err(|e| format!("failed to write output: {e}"))?; - } - } - } - } else { - // Normal diff format - let old_text: String = diff.old_slices().concat(); - let new_text: String = diff.new_slices().concat(); - let old_lines: Vec<&str> = old_text.lines().collect(); - let new_lines: Vec<&str> = new_text.lines().collect(); - - for op in diff.ops() { - match op { - similar::DiffOp::Equal { .. } => {} - similar::DiffOp::Delete { - old_index, - old_len, - new_index, - } => { - writeln!( - out, - "{}d{}", - format_range(*old_index + 1, *old_len), - new_index - ) - .map_err(|e| format!("failed to write output: {e}"))?; - for i in *old_index..*old_index + old_len { - if i < old_lines.len() { - writeln!(out, "< {}", old_lines[i]) - .map_err(|e| format!("failed to write output: {e}"))?; - } - } - } - similar::DiffOp::Insert { - old_index, - new_index, - new_len, - } => { - writeln!( - out, - "{}a{}", - old_index, - format_range(*new_index + 1, *new_len) - ) - .map_err(|e| format!("failed to write output: {e}"))?; - for i in *new_index..*new_index + new_len { - if i < new_lines.len() { - writeln!(out, "> {}", new_lines[i]) - .map_err(|e| format!("failed to write output: {e}"))?; - } - } - } - similar::DiffOp::Replace { - old_index, - old_len, - new_index, - new_len, - } => { - writeln!( - out, - "{}c{}", - format_range(*old_index + 1, *old_len), - format_range(*new_index + 1, *new_len) - ) - .map_err(|e| format!("failed to write output: {e}"))?; - for i in *old_index..*old_index + old_len { - if i < old_lines.len() { - writeln!(out, "< {}", old_lines[i]) - .map_err(|e| format!("failed to write output: {e}"))?; - } - } - writeln!(out, "---").map_err(|e| format!("failed to write output: {e}"))?; - for i in *new_index..*new_index + new_len { - if i < new_lines.len() { - writeln!(out, "> {}", new_lines[i]) - .map_err(|e| format!("failed to write output: {e}"))?; - } - } - } - } - } - } - - out.flush() - .map_err(|e| format!("failed to write output: {e}"))?; - - Ok(true) -} - -fn print_stdout_line(args: std::fmt::Arguments<'_>) -> Result<(), String> { - let stdout = io::stdout(); - let mut out = stdout.lock(); - writeln!(out, "{args}").map_err(|e| format!("failed to write output: {e}"))?; - out.flush() - .map_err(|e| format!("failed to write output: {e}")) -} - -fn read_file(path: &Path) -> Result { - if path.to_str() == Some("-") { - use std::io::Read; - let mut buf = String::new(); - io::stdin() - .read_to_string(&mut buf) - .map_err(|e| format!("stdin: {}", e))?; - Ok(buf) - } else { - fs::read_to_string(path).map_err(|e| format!("{}: {}", path.display(), e)) - } -} - -fn format_range(start: usize, len: usize) -> String { - if len == 1 { - format!("{}", start) - } else { - format!("{},{}", start, start + len - 1) - } -} diff --git a/registry/native/crates/libs/du/Cargo.toml b/registry/native/crates/libs/du/Cargo.toml deleted file mode 100644 index ad08de95a1..0000000000 --- a/registry/native/crates/libs/du/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "secureexec-du" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "du implementation for secure-exec standalone binaries" - -[dependencies] diff --git a/registry/native/crates/libs/expr/Cargo.toml b/registry/native/crates/libs/expr/Cargo.toml deleted file mode 100644 index 8bd1f300fb..0000000000 --- a/registry/native/crates/libs/expr/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "secureexec-expr" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "expr implementation for secure-exec standalone binaries" - -[dependencies] -regex = "1" diff --git a/registry/native/crates/libs/fd/Cargo.toml b/registry/native/crates/libs/fd/Cargo.toml deleted file mode 100644 index 2be330866f..0000000000 --- a/registry/native/crates/libs/fd/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "secureexec-fd" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "fd-find compatible file finder for secure-exec standalone binaries" - -[dependencies] -regex = "1" diff --git a/registry/native/crates/libs/fd/src/lib.rs b/registry/native/crates/libs/fd/src/lib.rs deleted file mode 100644 index 8ab845214f..0000000000 --- a/registry/native/crates/libs/fd/src/lib.rs +++ /dev/null @@ -1,353 +0,0 @@ -//! Minimal fd-find compatible file finder for WASM. -//! -//! Uses std::fs for directory traversal (no walkdir/rayon dependency) -//! and regex for pattern matching. Covers common fd patterns: -//! fd PATTERN, fd -e EXT, fd -t f/d, fd -H (hidden), fd -I (no-ignore). - -use std::collections::HashSet; -use std::ffi::OsString; -use std::fs; -use std::io::{self, Write}; -use std::path::{Path, PathBuf}; - -use regex::Regex; - -/// Entry point for fd command. -pub fn main(args: Vec) -> i32 { - let str_args: Vec = args - .iter() - .skip(1) // skip argv[0] - .map(|a| a.to_string_lossy().to_string()) - .collect(); - - match run(&str_args) { - Ok(code) => code, - Err(msg) => { - eprintln!("[fd error]: {}", msg); - 1 - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -enum TypeFilter { - File, - Directory, - Symlink, -} - -enum ParsedArgs { - Search(Options), - Help, - Version, -} - -struct Options { - pattern: Option, - extensions: Vec, - type_filter: Option, - max_depth: Option, - search_paths: Vec, - show_hidden: bool, - _case_insensitive: bool, - full_path: bool, - absolute_path: bool, -} - -fn run(args: &[String]) -> Result { - let parsed = parse_args(args)?; - let stdout = io::stdout(); - let mut out = stdout.lock(); - - let ParsedArgs::Search(opts) = parsed else { - match parsed { - ParsedArgs::Help => print_help(&mut out), - ParsedArgs::Version => writeln!(out, "fd 0.1.0 (secure-exec)"), - ParsedArgs::Search(_) => unreachable!(), - } - .map_err(|e| format!("failed to write output: {e}"))?; - out.flush() - .map_err(|e| format!("failed to write output: {e}"))?; - return Ok(0); - }; - - let mut found = false; - - for search_path in &opts.search_paths { - let base = PathBuf::from(search_path); - let mut visited_dirs = HashSet::new(); - walk( - &base, - search_path, - 0, - &opts, - &mut found, - &mut out, - &mut visited_dirs, - )?; - } - - out.flush() - .map_err(|e| format!("failed to write output: {e}"))?; - - Ok(if found { 0 } else { 1 }) -} - -fn walk( - full_path: &Path, - base_path: &str, - depth: usize, - opts: &Options, - found: &mut bool, - out: &mut W, - visited_dirs: &mut HashSet, -) -> Result<(), String> { - if let Some(max) = opts.max_depth { - if depth > max { - return Ok(()); - } - } - - // fd skips the root search directory itself (depth 0); only matches children - if depth > 0 { - if matches_entry(full_path, base_path, opts) { - *found = true; - let display = if opts.absolute_path { - match fs::canonicalize(full_path) { - Ok(abs) => abs.to_string_lossy().to_string(), - Err(_) => full_path.to_string_lossy().to_string(), - } - } else { - full_path.to_string_lossy().to_string() - }; - writeln!(out, "{}", display).map_err(|e| format!("failed to write output: {e}"))?; - } - } - - // Recurse into directories - if full_path.is_dir() { - let canonical_path = - fs::canonicalize(full_path).map_err(|e| format!("{}: {}", full_path.display(), e))?; - if !visited_dirs.insert(canonical_path) { - return Ok(()); - } - - if let Some(max) = opts.max_depth { - if depth >= max { - return Ok(()); - } - } - - let entries = - fs::read_dir(full_path).map_err(|e| format!("{}: {}", full_path.display(), e))?; - - let mut sorted: Vec<_> = entries - .collect::, _>>() - .map_err(|e| format!("{}: {}", full_path.display(), e))?; - sorted.sort_by_key(|e| e.file_name()); - - for entry in sorted { - let name = entry.file_name().to_string_lossy().to_string(); - - // Skip hidden files/directories unless -H is set - if !opts.show_hidden && name.starts_with('.') { - continue; - } - - let child = entry.path(); - walk(&child, base_path, depth + 1, opts, found, out, visited_dirs)?; - } - } - - Ok(()) -} - -fn matches_entry(path: &Path, _base_path: &str, opts: &Options) -> bool { - // Type filter - if let Some(ref tf) = opts.type_filter { - let ok = match tf { - TypeFilter::File => path.is_file(), - TypeFilter::Directory => path.is_dir(), - TypeFilter::Symlink => path - .symlink_metadata() - .map(|m| m.file_type().is_symlink()) - .unwrap_or(false), - }; - if !ok { - return false; - } - } - - // Extension filter - if !opts.extensions.is_empty() { - let ext = path - .extension() - .map(|e| e.to_string_lossy().to_string()) - .unwrap_or_default(); - let ext_lower = ext.to_lowercase(); - if !opts - .extensions - .iter() - .any(|e| e.to_lowercase() == ext_lower) - { - return false; - } - } - - // Pattern match - if let Some(ref re) = opts.pattern { - let target = if opts.full_path { - path.to_string_lossy().to_string() - } else { - path.file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_default() - }; - if !re.is_match(&target) { - return false; - } - } - - true -} - -fn parse_args(args: &[String]) -> Result { - let mut pattern: Option = None; - let mut extensions: Vec = Vec::new(); - let mut type_filter: Option = None; - let mut max_depth: Option = None; - let mut search_paths: Vec = Vec::new(); - let mut show_hidden = false; - let mut case_insensitive = false; - let mut full_path = false; - let mut absolute_path = false; - let mut i = 0; - - while i < args.len() { - let arg = &args[i]; - match arg.as_str() { - "-h" | "--help" => { - return Ok(ParsedArgs::Help); - } - "-V" | "--version" => { - return Ok(ParsedArgs::Version); - } - "-H" | "--hidden" => { - show_hidden = true; - } - "-I" | "--no-ignore" => { - // No .gitignore support in WASM VFS — this is a no-op - } - "-i" | "--ignore-case" => { - case_insensitive = true; - } - "-s" | "--case-sensitive" => { - case_insensitive = false; - } - "-p" | "--full-path" => { - full_path = true; - } - "-a" | "--absolute-path" => { - absolute_path = true; - } - "-e" | "--extension" => { - i += 1; - if i >= args.len() { - return Err("-e/--extension requires an argument".to_string()); - } - extensions.push(args[i].clone()); - } - "-t" | "--type" => { - i += 1; - if i >= args.len() { - return Err("-t/--type requires an argument".to_string()); - } - type_filter = Some(match args[i].as_str() { - "f" | "file" => TypeFilter::File, - "d" | "directory" => TypeFilter::Directory, - "l" | "symlink" => TypeFilter::Symlink, - other => return Err(format!("unknown type filter: {}", other)), - }); - } - "-d" | "--max-depth" => { - i += 1; - if i >= args.len() { - return Err("-d/--max-depth requires an argument".to_string()); - } - max_depth = Some( - args[i] - .parse() - .map_err(|_| format!("invalid max-depth: {}", args[i]))?, - ); - } - arg if arg.starts_with('-') => { - return Err(format!("unknown option: {}", arg)); - } - _ => { - // First positional arg is pattern, rest are search paths - if pattern.is_none() { - pattern = Some(arg.clone()); - } else { - search_paths.push(arg.clone()); - } - } - } - i += 1; - } - - if search_paths.is_empty() { - search_paths.push(".".to_string()); - } - - // Compile pattern regex - let compiled_pattern = match pattern { - Some(pat) => { - let re_str = if case_insensitive { - format!("(?i){}", pat) - } else { - pat - }; - Some(Regex::new(&re_str).map_err(|e| format!("invalid pattern: {}", e))?) - } - None => None, - }; - - Ok(ParsedArgs::Search(Options { - pattern: compiled_pattern, - extensions, - type_filter, - max_depth, - search_paths, - show_hidden, - _case_insensitive: case_insensitive, - full_path, - absolute_path, - })) -} - -fn print_help(out: &mut W) -> io::Result<()> { - writeln!( - out, - "fd - a simple, fast file finder - -USAGE: - fd [OPTIONS] [PATTERN] [PATH...] - -ARGS: - Regex search pattern - ... Search paths (default: current directory) - -OPTIONS: - -H, --hidden Include hidden files/directories - -I, --no-ignore Do not respect .gitignore (no-op in sandbox) - -i, --ignore-case Case-insensitive search - -s, --case-sensitive Case-sensitive search (default) - -p, --full-path Match pattern against full path - -a, --absolute-path Show absolute paths - -e, --extension EXT Filter by file extension - -t, --type TYPE Filter by type: f(ile), d(irectory), l(ink) - -d, --max-depth N Maximum search depth - -h, --help Print help - -V, --version Print version" - ) -} diff --git a/registry/native/crates/libs/file-cmd/Cargo.toml b/registry/native/crates/libs/file-cmd/Cargo.toml deleted file mode 100644 index 7f14225fd9..0000000000 --- a/registry/native/crates/libs/file-cmd/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "secureexec-file-cmd" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "file command implementation for secure-exec standalone binaries" - -[dependencies] -infer = "0.16" diff --git a/registry/native/crates/libs/file-cmd/src/lib.rs b/registry/native/crates/libs/file-cmd/src/lib.rs deleted file mode 100644 index 92a9faa647..0000000000 --- a/registry/native/crates/libs/file-cmd/src/lib.rs +++ /dev/null @@ -1,336 +0,0 @@ -//! file -- determine file type -//! -//! Magic byte detection using the `infer` crate, plus text/binary heuristic. -//! Supports -b (brief, no filename prefix), -i (output MIME type). - -use std::ffi::OsString; -use std::fs; -use std::io::{self, Read, Write}; - -const DETECTION_BYTES: usize = 8192; - -pub fn main(args: Vec) -> i32 { - let str_args: Vec = args - .iter() - .skip(1) - .map(|a| a.to_string_lossy().to_string()) - .collect(); - - let mut brief = false; - let mut mime = false; - let mut filenames: Vec = Vec::new(); - - let mut i = 0; - while i < str_args.len() { - match str_args[i].as_str() { - "-b" | "--brief" => brief = true, - "-i" | "--mime-type" | "--mime" => mime = true, - // Combined short flags - s if s.starts_with('-') && s.len() > 1 && !s.starts_with("--") => { - for ch in s[1..].chars() { - match ch { - 'b' => brief = true, - 'i' => mime = true, - _ => { - eprintln!("file: unknown option '{}'", s); - return 1; - } - } - } - } - "-" => { - // Read from stdin - filenames.push("-".to_string()); - } - s if s.starts_with('-') && s.len() > 1 => { - eprintln!("file: unknown option '{}'", s); - return 1; - } - _ => filenames.push(str_args[i].clone()), - } - i += 1; - } - - if filenames.is_empty() { - eprintln!("Usage: file [-bi] FILE..."); - return 1; - } - - let stdout = io::stdout(); - let mut out = stdout.lock(); - let mut exit_code = 0; - - for filename in &filenames { - let data = if filename == "-" { - match read_head_from_reader(io::stdin().lock(), DETECTION_BYTES) { - Ok(data) => data, - Err(e) => { - eprintln!("file: stdin: {}", e); - exit_code = 1; - continue; - } - } - } else { - // Check if path exists and what kind it is - match fs::metadata(filename) { - Ok(meta) => { - if meta.is_dir() { - if let Err(error) = print_result( - &mut out, - filename, - brief, - "directory", - if mime { "inode/directory" } else { "" }, - mime, - ) { - return output_error(error); - } - continue; - } - if meta.is_symlink() { - if let Err(error) = print_result( - &mut out, - filename, - brief, - "symbolic link", - if mime { "inode/symlink" } else { "" }, - mime, - ) { - return output_error(error); - } - continue; - } - if meta.len() == 0 { - if let Err(error) = print_result( - &mut out, - filename, - brief, - "empty", - if mime { "inode/x-empty" } else { "" }, - mime, - ) { - return output_error(error); - } - continue; - } - // Read file data (up to 8KB for detection) - match read_head(filename, DETECTION_BYTES) { - Ok(data) => data, - Err(e) => { - eprintln!("file: {}: {}", filename, e); - exit_code = 1; - continue; - } - } - } - Err(e) => { - eprintln!("file: {}: {}", filename, e); - exit_code = 1; - continue; - } - } - }; - - let (desc, mime_type) = identify(&data); - - if mime { - if let Err(error) = print_result(&mut out, filename, brief, &desc, &mime_type, true) { - return output_error(error); - } - } else { - if let Err(error) = print_result(&mut out, filename, brief, &desc, &mime_type, false) { - return output_error(error); - } - } - } - - if let Err(error) = out.flush() { - return output_error(error); - } - - exit_code -} - -fn output_error(error: io::Error) -> i32 { - eprintln!("file: failed to write output: {error}"); - 1 -} - -fn read_head(path: &str, max: usize) -> io::Result> { - let mut f = fs::File::open(path)?; - read_head_from_reader(&mut f, max) -} - -fn read_head_from_reader(mut reader: impl Read, max: usize) -> io::Result> { - let mut buf = vec![0u8; max]; - let n = reader.read(&mut buf)?; - buf.truncate(n); - Ok(buf) -} - -fn print_result( - out: &mut W, - filename: &str, - brief: bool, - desc: &str, - mime_type: &str, - use_mime: bool, -) -> io::Result<()> { - if use_mime && !mime_type.is_empty() { - if brief { - writeln!(out, "{}", mime_type) - } else { - writeln!(out, "{}: {}", filename, mime_type) - } - } else if brief { - writeln!(out, "{}", desc) - } else { - writeln!(out, "{}: {}", filename, desc) - } -} - -fn identify(data: &[u8]) -> (String, String) { - if data.is_empty() { - return ("empty".to_string(), "inode/x-empty".to_string()); - } - - // Try infer crate for magic byte detection - if let Some(kind) = infer::get(data) { - let desc = match kind.mime_type() { - "image/png" => "PNG image data", - "image/jpeg" => "JPEG image data", - "image/gif" => "GIF image data", - "image/webp" => "WebP image data", - "image/bmp" => "BMP image data", - "image/tiff" => "TIFF image data", - "image/svg+xml" => "SVG image data", - "image/x-icon" => "ICO image data", - "application/pdf" => "PDF document", - "application/zip" => "Zip archive data", - "application/gzip" => "gzip compressed data", - "application/x-bzip2" => "bzip2 compressed data", - "application/x-xz" => "XZ compressed data", - "application/x-tar" => "POSIX tar archive", - "application/x-rar-compressed" => "RAR archive data", - "application/x-7z-compressed" => "7-zip archive data", - "application/x-executable" | "application/x-elf" => "ELF executable", - "application/wasm" => "WebAssembly (wasm) binary module", - "application/x-mach-binary" => "Mach-O binary", - "audio/mpeg" => "MPEG audio", - "audio/ogg" => "Ogg audio", - "audio/flac" => "FLAC audio", - "audio/wav" | "audio/x-wav" => "RIFF WAVE audio", - "video/mp4" => "MPEG-4 video", - "video/webm" => "WebM video", - "video/x-matroska" => "Matroska video", - "application/x-sqlite3" => "SQLite 3.x database", - "font/woff" => "Web Open Font Format", - "font/woff2" => "Web Open Font Format 2", - _ => kind.mime_type(), - }; - return (desc.to_string(), kind.mime_type().to_string()); - } - - // Check for shebang scripts - if data.len() >= 2 && data[0] == b'#' && data[1] == b'!' { - let first_line = data - .iter() - .take(128) - .take_while(|&&b| b != b'\n') - .copied() - .collect::>(); - if let Ok(line) = std::str::from_utf8(&first_line) { - let interp = line.trim_start_matches("#!"); - let interp = interp.trim(); - // Extract interpreter name - let name = interp.split_whitespace().next().unwrap_or(interp); - let basename = match name.rfind('/') { - Some(pos) => &name[pos + 1..], - None => name, - }; - // Handle "env " pattern - if basename == "env" { - let prog = interp.split_whitespace().nth(1).unwrap_or("script"); - return ( - format!("{} script, ASCII text executable", prog), - "text/x-script".to_string(), - ); - } - return ( - format!("{} script, ASCII text executable", basename), - "text/x-script".to_string(), - ); - } - } - - // Check for known text patterns - if is_json(data) { - return ("JSON text data".to_string(), "application/json".to_string()); - } - if is_xml(data) { - return ("XML document".to_string(), "text/xml".to_string()); - } - if is_html(data) { - return ("HTML document".to_string(), "text/html".to_string()); - } - - // Text vs binary heuristic - if is_text(data) { - return ("ASCII text".to_string(), "text/plain".to_string()); - } - - ("data".to_string(), "application/octet-stream".to_string()) -} - -fn is_text(data: &[u8]) -> bool { - // Check first 8KB for non-text bytes - let check_len = data.len().min(8192); - let mut null_count = 0; - for &b in &data[..check_len] { - if b == 0 { - null_count += 1; - if null_count > 0 { - return false; - } - } - // Allow common control chars: tab, newline, carriage return, form feed, backspace, escape - if b < 0x08 || (b > 0x0D && b < 0x20 && b != 0x1B) { - if b != 0x00 { - // already counted nulls - return false; - } - } - } - true -} - -fn is_json(data: &[u8]) -> bool { - // Quick check: starts with { or [, ignoring leading whitespace - let trimmed = skip_whitespace(data); - !trimmed.is_empty() && (trimmed[0] == b'{' || trimmed[0] == b'[') -} - -fn is_xml(data: &[u8]) -> bool { - let trimmed = skip_whitespace(data); - trimmed.starts_with(b" bool { - let trimmed = skip_whitespace(data); - let lower: Vec = trimmed - .iter() - .take(64) - .map(|b| b.to_ascii_lowercase()) - .collect(); - lower.starts_with(b" &[u8] { - let mut i = 0; - while i < data.len() - && (data[i] == b' ' || data[i] == b'\t' || data[i] == b'\n' || data[i] == b'\r') - { - i += 1; - } - &data[i..] -} diff --git a/registry/native/crates/libs/find/Cargo.toml b/registry/native/crates/libs/find/Cargo.toml deleted file mode 100644 index 1790edb5e8..0000000000 --- a/registry/native/crates/libs/find/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "secureexec-find" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "find implementation for secure-exec standalone binaries" - -[dependencies] -regex = "1" diff --git a/registry/native/crates/libs/find/src/lib.rs b/registry/native/crates/libs/find/src/lib.rs deleted file mode 100644 index 059e80fa53..0000000000 --- a/registry/native/crates/libs/find/src/lib.rs +++ /dev/null @@ -1,593 +0,0 @@ -//! POSIX find implementation for WASM. -//! -//! Custom implementation using std::fs (WASI) for directory traversal -//! and glob-to-regex conversion for pattern matching. fd-find crate -//! cannot be used directly as it's a binary crate with platform-specific -//! dependencies incompatible with wasm32-wasip1. - -use std::collections::HashSet; -use std::ffi::OsString; -use std::fs; -use std::io::{self, Write}; -use std::path::{Path, PathBuf}; - -use regex::Regex; - -/// Entry point for find command. -pub fn main(args: Vec) -> i32 { - let str_args: Vec = args - .iter() - .skip(1) // skip argv[0] - .map(|a| a.to_string_lossy().to_string()) - .collect(); - - match run_find(&str_args) { - Ok(found) => { - if found { - 0 - } else { - 0 - } // find returns 0 even if no matches - } - Err(msg) => { - eprintln!("find: {}", msg); - 1 - } - } -} - -/// Parsed find expression node. -#[derive(Debug)] -enum Expr { - /// -name PATTERN (glob match on filename only) - Name(Regex), - /// -iname PATTERN (case-insensitive glob match on filename) - IName(Regex), - /// -path PATTERN (glob match on full path) - PathMatch(Regex), - /// -ipath PATTERN (case-insensitive glob match on full path) - IPathMatch(Regex), - /// -type TYPE (d, f, l) - Type(FileType), - /// -empty (file size 0 or empty directory) - Empty, - /// -maxdepth N (handled specially during traversal) - MaxDepth(usize), - /// -mindepth N (handled specially during traversal) - MinDepth(usize), - /// -print (explicit print action) - Print, - /// -not EXPR / ! EXPR - Not(Box), - /// EXPR -a EXPR / EXPR -and EXPR / implicit AND - And(Box, Box), - /// EXPR -o EXPR / EXPR -or EXPR - Or(Box, Box), -} - -#[derive(Debug, Clone, Copy, PartialEq)] -enum FileType { - Directory, - File, - Symlink, -} - -struct FindOptions { - paths: Vec, - expr: Option, - max_depth: Option, - min_depth: usize, -} - -fn run_find(args: &[String]) -> Result { - let opts = parse_args(args)?; - let mut found_any = false; - let stdout = io::stdout(); - let mut out = stdout.lock(); - - for path in &opts.paths { - let mut visited_dirs = HashSet::new(); - walk( - &PathBuf::from(path), - path, - 0, - &opts, - &mut found_any, - &mut out, - &mut visited_dirs, - )?; - } - - out.flush() - .map_err(|e| format!("failed to write output: {e}"))?; - - Ok(found_any) -} - -/// Recursive directory walk. -fn walk( - full_path: &Path, - display_path: &str, - depth: usize, - opts: &FindOptions, - found_any: &mut bool, - out: &mut W, - visited_dirs: &mut HashSet, -) -> Result<(), String> { - // Check max depth before processing - if let Some(max) = opts.max_depth { - if depth > max { - return Ok(()); - } - } - - // Evaluate expression against this entry (if at or below min_depth) - if depth >= opts.min_depth { - let matches = match &opts.expr { - Some(expr) => eval_expr(expr, full_path, display_path), - None => true, // no expression means match everything - }; - - if matches { - *found_any = true; - // Default action is print - writeln!(out, "{}", display_path) - .map_err(|e| format!("failed to write output: {e}"))?; - } - } - - // Recurse into directories - if full_path.is_dir() { - // Check max depth for children - if let Some(max) = opts.max_depth { - if depth >= max { - return Ok(()); - } - } - - let canonical_path = - fs::canonicalize(full_path).map_err(|e| format!("{}: {}", display_path, e))?; - if !visited_dirs.insert(canonical_path.clone()) { - return Ok(()); - } - - let entries = fs::read_dir(full_path).map_err(|e| format!("{}: {}", display_path, e))?; - - let mut sorted_entries: Vec<_> = entries - .collect::, _>>() - .map_err(|e| format!("{}: {}", display_path, e))?; - sorted_entries.sort_by_key(|e| e.file_name()); - - for entry in sorted_entries { - let child_name = entry.file_name().to_string_lossy().to_string(); - let child_display = if display_path == "/" { - format!("/{}", child_name) - } else { - format!("{}/{}", display_path, child_name) - }; - let child_full = entry.path(); - - walk( - &child_full, - &child_display, - depth + 1, - opts, - found_any, - out, - visited_dirs, - )?; - } - visited_dirs.remove(&canonical_path); - } - - Ok(()) -} - -/// Evaluate an expression against a path. -fn eval_expr(expr: &Expr, full_path: &Path, display_path: &str) -> bool { - match expr { - Expr::Name(re) => { - let name = full_path - .file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_default(); - re.is_match(&name) - } - Expr::IName(re) => { - let name = full_path - .file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_default(); - re.is_match(&name) - } - Expr::PathMatch(re) => re.is_match(display_path), - Expr::IPathMatch(re) => re.is_match(display_path), - Expr::Type(ft) => match ft { - FileType::Directory => full_path.is_dir(), - FileType::File => full_path.is_file(), - FileType::Symlink => full_path - .symlink_metadata() - .map(|m| m.file_type().is_symlink()) - .unwrap_or(false), - }, - Expr::Empty => { - if full_path.is_dir() { - fs::read_dir(full_path) - .map(|mut d| d.next().is_none()) - .unwrap_or(false) - } else if full_path.is_file() { - full_path.metadata().map(|m| m.len() == 0).unwrap_or(false) - } else { - false - } - } - Expr::MaxDepth(_) | Expr::MinDepth(_) => true, // handled in walk() - Expr::Print => true, // print is an action, always "matches" - Expr::Not(inner) => !eval_expr(inner, full_path, display_path), - Expr::And(left, right) => { - eval_expr(left, full_path, display_path) && eval_expr(right, full_path, display_path) - } - Expr::Or(left, right) => { - eval_expr(left, full_path, display_path) || eval_expr(right, full_path, display_path) - } - } -} - -/// Convert a shell glob pattern to a regex pattern. -/// Supports *, ?, [charset], and ** (for path matching). -fn glob_to_regex(glob: &str, case_insensitive: bool) -> Result { - let mut re = String::from("^"); - let chars: Vec = glob.chars().collect(); - let mut i = 0; - - while i < chars.len() { - match chars[i] { - '*' => { - if i + 1 < chars.len() && chars[i + 1] == '*' { - // ** matches everything including / - re.push_str(".*"); - i += 1; - } else { - // * matches everything except / - re.push_str("[^/]*"); - } - } - '?' => re.push_str("[^/]"), - '[' => { - re.push('['); - i += 1; - if i < chars.len() && chars[i] == '!' { - re.push('^'); - i += 1; - } else if i < chars.len() && chars[i] == '^' { - re.push('^'); - i += 1; - } - while i < chars.len() && chars[i] != ']' { - if chars[i] == '\\' && i + 1 < chars.len() { - re.push('\\'); - i += 1; - re.push(chars[i]); - } else { - re.push(chars[i]); - } - i += 1; - } - re.push(']'); - } - '.' | '+' | '(' | ')' | '{' | '}' | '|' | '^' | '$' | '\\' => { - re.push('\\'); - re.push(chars[i]); - } - _ => re.push(chars[i]), - } - i += 1; - } - - re.push('$'); - - let pattern = if case_insensitive { - format!("(?i){}", re) - } else { - re - }; - - Regex::new(&pattern).map_err(|e| format!("invalid pattern: {}", e)) -} - -/// Parse find command arguments. -fn parse_args(args: &[String]) -> Result { - let mut paths: Vec = Vec::new(); - let mut exprs: Vec = Vec::new(); - let mut max_depth: Option = None; - let mut min_depth: usize = 0; - let mut i = 0; - - // First, collect paths (arguments before any expression) - while i < args.len() { - let arg = &args[i]; - if arg.starts_with('-') || arg == "!" || arg == "(" { - break; - } - paths.push(arg.clone()); - i += 1; - } - - if paths.is_empty() { - paths.push(".".to_string()); - } - - // Parse expressions - while i < args.len() { - let arg = &args[i]; - - match arg.as_str() { - "-name" => { - i += 1; - if i >= args.len() { - return Err("-name requires an argument".to_string()); - } - let re = glob_to_regex(&args[i], false)?; - exprs.push(Expr::Name(re)); - } - "-iname" => { - i += 1; - if i >= args.len() { - return Err("-iname requires an argument".to_string()); - } - let re = glob_to_regex(&args[i], true)?; - exprs.push(Expr::IName(re)); - } - "-path" | "-wholename" => { - i += 1; - if i >= args.len() { - return Err(format!("{} requires an argument", arg)); - } - let re = glob_to_regex(&args[i], false)?; - exprs.push(Expr::PathMatch(re)); - } - "-ipath" | "-iwholename" => { - i += 1; - if i >= args.len() { - return Err(format!("{} requires an argument", arg)); - } - let re = glob_to_regex(&args[i], true)?; - exprs.push(Expr::IPathMatch(re)); - } - "-type" => { - i += 1; - if i >= args.len() { - return Err("-type requires an argument".to_string()); - } - let ft = match args[i].as_str() { - "d" => FileType::Directory, - "f" => FileType::File, - "l" => FileType::Symlink, - other => return Err(format!("unknown file type: {}", other)), - }; - exprs.push(Expr::Type(ft)); - } - "-empty" => { - exprs.push(Expr::Empty); - } - "-maxdepth" => { - i += 1; - if i >= args.len() { - return Err("-maxdepth requires an argument".to_string()); - } - let n: usize = args[i] - .parse() - .map_err(|_| format!("-maxdepth: {}: not a number", args[i]))?; - max_depth = Some(n); - } - "-mindepth" => { - i += 1; - if i >= args.len() { - return Err("-mindepth requires an argument".to_string()); - } - let n: usize = args[i] - .parse() - .map_err(|_| format!("-mindepth: {}: not a number", args[i]))?; - min_depth = n; - } - "-print" => { - exprs.push(Expr::Print); - } - "!" | "-not" => { - i += 1; - if i >= args.len() { - return Err("! requires an expression".to_string()); - } - // Parse the next single expression - let (next_expr, next_i) = parse_single_expr(&args, i)?; - if let Some((md, mi)) = extract_depth(&next_expr) { - if let Some(d) = md { - max_depth = Some(d); - } - min_depth = mi.unwrap_or(min_depth); - } - exprs.push(Expr::Not(Box::new(next_expr))); - i = next_i; - continue; // skip the i += 1 at end - } - "-a" | "-and" => { - // Implicit AND between consecutive expressions, -a is explicit - // Just skip it; AND is applied when combining exprs - } - "-o" | "-or" => { - // Combine everything so far as left side of OR - if exprs.is_empty() { - return Err("-or requires a preceding expression".to_string()); - } - let left = combine_and(exprs); - - // Parse remaining as right side - i += 1; - let mut right_exprs = Vec::new(); - while i < args.len() { - let (e, ni) = parse_single_expr(args, i)?; - if let Some((md, mi)) = extract_depth(&e) { - if let Some(d) = md { - max_depth = Some(d); - } - min_depth = mi.unwrap_or(min_depth); - } - right_exprs.push(e); - i = ni; - } - let right = combine_and(right_exprs); - exprs = vec![Expr::Or(Box::new(left), Box::new(right))]; - break; - } - "(" => { - // Grouped expression - find matching ) - i += 1; - let mut depth = 1; - let start = i; - while i < args.len() && depth > 0 { - if args[i] == "(" { - depth += 1; - } - if args[i] == ")" { - depth -= 1; - } - if depth > 0 { - i += 1; - } - } - if depth != 0 { - return Err("unmatched '('".to_string()); - } - let sub_args: Vec = args[start..i].to_vec(); - let sub_opts = parse_args_exprs(&sub_args)?; - if let Some(e) = sub_opts { - exprs.push(e); - } - } - other if other.starts_with('-') => { - return Err(format!("unknown option: {}", other)); - } - _ => { - // Treat as path if no expressions yet, otherwise error - return Err(format!("unexpected argument: {}", arg)); - } - } - i += 1; - } - - let expr = if exprs.is_empty() { - None - } else { - Some(combine_and(exprs)) - }; - - Ok(FindOptions { - paths, - expr, - max_depth, - min_depth, - }) -} - -/// Parse a single expression at position i, returning the expression and the next position. -fn parse_single_expr(args: &[String], i: usize) -> Result<(Expr, usize), String> { - if i >= args.len() { - return Err("unexpected end of expression".to_string()); - } - - let arg = &args[i]; - match arg.as_str() { - "-name" => { - if i + 1 >= args.len() { - return Err("-name requires an argument".to_string()); - } - let re = glob_to_regex(&args[i + 1], false)?; - Ok((Expr::Name(re), i + 2)) - } - "-iname" => { - if i + 1 >= args.len() { - return Err("-iname requires an argument".to_string()); - } - let re = glob_to_regex(&args[i + 1], true)?; - Ok((Expr::IName(re), i + 2)) - } - "-path" | "-wholename" => { - if i + 1 >= args.len() { - return Err(format!("{} requires an argument", arg)); - } - let re = glob_to_regex(&args[i + 1], false)?; - Ok((Expr::PathMatch(re), i + 2)) - } - "-type" => { - if i + 1 >= args.len() { - return Err("-type requires an argument".to_string()); - } - let ft = match args[i + 1].as_str() { - "d" => FileType::Directory, - "f" => FileType::File, - "l" => FileType::Symlink, - other => return Err(format!("unknown file type: {}", other)), - }; - Ok((Expr::Type(ft), i + 2)) - } - "-empty" => Ok((Expr::Empty, i + 1)), - "-maxdepth" => { - if i + 1 >= args.len() { - return Err("-maxdepth requires an argument".to_string()); - } - let n: usize = args[i + 1] - .parse() - .map_err(|_| format!("-maxdepth: {}: not a number", args[i + 1]))?; - Ok((Expr::MaxDepth(n), i + 2)) - } - "-mindepth" => { - if i + 1 >= args.len() { - return Err("-mindepth requires an argument".to_string()); - } - let n: usize = args[i + 1] - .parse() - .map_err(|_| format!("-mindepth: {}: not a number", args[i + 1]))?; - Ok((Expr::MinDepth(n), i + 2)) - } - "-print" => Ok((Expr::Print, i + 1)), - _ => Err(format!("unknown expression: {}", arg)), - } -} - -/// Parse expression-only args (for grouped expressions). -fn parse_args_exprs(args: &[String]) -> Result, String> { - let mut exprs = Vec::new(); - let mut i = 0; - - while i < args.len() { - let (expr, next_i) = parse_single_expr(args, i)?; - exprs.push(expr); - i = next_i; - } - - if exprs.is_empty() { - Ok(None) - } else { - Ok(Some(combine_and(exprs))) - } -} - -/// Extract depth settings from an expression. -fn extract_depth(expr: &Expr) -> Option<(Option, Option)> { - match expr { - Expr::MaxDepth(n) => Some((Some(*n), None)), - Expr::MinDepth(n) => Some((None, Some(*n))), - _ => None, - } -} - -/// Combine a list of expressions with AND. -fn combine_and(mut exprs: Vec) -> Expr { - if exprs.len() == 1 { - return exprs.remove(0); - } - let first = exprs.remove(0); - exprs - .into_iter() - .fold(first, |acc, e| Expr::And(Box::new(acc), Box::new(e))) -} diff --git a/registry/native/crates/libs/git/Cargo.toml b/registry/native/crates/libs/git/Cargo.toml deleted file mode 100644 index 59d150be52..0000000000 --- a/registry/native/crates/libs/git/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "secureexec-git" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "Minimal git implementation for secure-exec VM" - -[lib] -name = "secureexec_git" -path = "src/lib.rs" - -[dependencies] -sha1 = "0.10.6" -flate2 = "1.1" -wasi-http = { package = "secureexec-wasi-http", path = "../wasi-http" } diff --git a/registry/native/crates/libs/git/README.md b/registry/native/crates/libs/git/README.md deleted file mode 100644 index 26fcb37043..0000000000 --- a/registry/native/crates/libs/git/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# secure-exec VM git compatibility - -The `git` command in secure-exec is a clean-room, Apache-2.0 reimplementation of a -small git subset for WasmVM guests. - -## Supported commands - -- `git init` -- `git add` -- `git commit -m ...` -- `git rev-parse ` -- `git branch` -- `git checkout` -- `git clone ` -- `git clone http://...` -- `git clone https://...` - -Remote clone support is limited to unauthenticated smart-HTTP fetches. The -implementation can read from `http://` and `https://` remotes, but it does not -support credentials, push, or SSH transport. - -## Unsupported commands and transports - -The guest command exits with a typed `GitSubcommandUnsupported` error for: - -- `git push` -- `git fetch` -- `git pull` -- `git remote` -- SSH-style clone URLs such as `git@host:owner/repo.git` and `ssh://...` -- `git://...` clone URLs -- Authenticated HTTP(S) remotes such as `https://user@host/repo.git` -- Submodules, hooks, GPG signing, and other advanced porcelain workflows - -If you need behavior outside this subset, keep using host git or extend the -clean-room implementation deliberately. diff --git a/registry/native/crates/libs/git/src/lib.rs b/registry/native/crates/libs/git/src/lib.rs deleted file mode 100644 index bd32b5520f..0000000000 --- a/registry/native/crates/libs/git/src/lib.rs +++ /dev/null @@ -1,2127 +0,0 @@ -//! Minimal clean-room git implementation (Apache-2.0). -//! -//! Supports: init, add, commit, branch, checkout (with DWIM), local clone, -//! and smart-HTTP clone over http:// and https://. - -use flate2::bufread::ZlibDecoder as BufZlibDecoder; -use flate2::read::ZlibDecoder; -use flate2::write::ZlibEncoder; -use flate2::Compression; -use sha1::{Digest, Sha1}; -use std::collections::{BTreeMap, HashMap, HashSet}; -use std::ffi::{OsStr, OsString}; -use std::fmt; -use std::fs; -use std::io::{self, Cursor, Read, Write}; -use std::path::{Component, Path, PathBuf}; -use wasi_http::{HttpClient, Method, Request}; - -// ─── Hex utilities ────────────────────────────────────────────────────────── - -fn hex(bytes: &[u8]) -> String { - bytes.iter().map(|b| format!("{:02x}", b)).collect() -} - -fn unhex(s: &str) -> io::Result<[u8; 20]> { - if s.len() != 40 { - return Err(err(&format!("invalid hash length: {}", s.len()))); - } - let bytes: Vec = (0..s.len()) - .step_by(2) - .map(|i| { - u8::from_str_radix(&s[i..i + 2], 16).map_err(|e| err(&format!("invalid hex: {}", e))) - }) - .collect::>>()?; - let mut hash = [0u8; 20]; - hash.copy_from_slice(&bytes); - Ok(hash) -} - -fn err(msg: &str) -> io::Error { - io::Error::new(io::ErrorKind::Other, msg.to_string()) -} - -fn print_stdout_line(args: fmt::Arguments<'_>) -> io::Result<()> { - let mut stdout = io::stdout().lock(); - stdout.write_fmt(args)?; - stdout.write_all(b"\n")?; - stdout.flush() -} - -const SUPPORT_DOC_PATH: &str = "registry/native/crates/libs/git/README.md"; -const MAX_GIT_OBJECT_BYTES: usize = 128 * 1024 * 1024; -const MAX_GIT_OBJECT_HEADER_BYTES: usize = 128; -const MAX_INDEX_ENTRIES: usize = 1_000_000; -const MAX_INDEX_BYTES: usize = 256 * 1024 * 1024; -const MAX_ADVERTISED_REFS: usize = 100_000; -const MAX_HTTP_ERROR_BODY_BYTES: usize = 8192; -const MAX_PKT_LINES: usize = 100_000; -const MAX_PKT_LINE_PAYLOAD_BYTES: usize = 16 * 1024 * 1024; -const MAX_REF_ADVERTISEMENT_BYTES: usize = 16 * 1024 * 1024; -const MAX_PACK_INFLATED_BYTES: usize = 256 * 1024 * 1024; -const MAX_PACK_BYTES: usize = 256 * 1024 * 1024; -const MAX_PACK_OBJECTS: usize = 1_000_000; -const MAX_PACK_RESOLVED_BYTES: usize = 256 * 1024 * 1024; - -fn unsupported(subcommand: &str, detail: &str) -> io::Error { - err(&format!( - "GitSubcommandUnsupported: git {} {} See {} for supported transports and commands.", - subcommand, detail, SUPPORT_DOC_PATH - )) -} - -/// WASI-safe mkdir -p: create_dir_all has issues with WASI permission checks -/// on already-existing directories, so we create one level at a time. -/// We also treat PermissionDenied as "already exists" because some WASI runtimes -/// return EACCES instead of EEXIST for existing directories. -fn mkdirs(path: &Path) -> io::Result<()> { - let mut ancestors: Vec<&Path> = path.ancestors().collect(); - ancestors.reverse(); // root first - for dir in ancestors { - if dir == Path::new("/") || dir == Path::new("") { - continue; - } - match fs::create_dir(dir) { - Ok(()) => {} - Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {} - // WASI kernels may return EACCES for existing directories - Err(e) if e.kind() == io::ErrorKind::PermissionDenied => { - // Verify it actually exists; propagate if it truly doesn't - if !dir.exists() { - return Err(e); - } - } - Err(e) => return Err(e), - } - } - Ok(()) -} - -fn dir_is_empty(path: &Path) -> io::Result { - if !path.exists() { - return Ok(true); - } - if !path.is_dir() { - return Ok(false); - } - Ok(fs::read_dir(path)?.next().is_none()) -} - -fn read_to_end_limited(reader: R, limit: usize, context: &str) -> io::Result> { - let limit_plus_one = limit - .checked_add(1) - .ok_or_else(|| err(&format!("{} size limit is too large", context)))?; - let mut out = Vec::new(); - reader.take(limit_plus_one as u64).read_to_end(&mut out)?; - if out.len() > limit { - return Err(err(&format!("{} exceeds size limit", context))); - } - Ok(out) -} - -fn read_file_limited(path: &Path, limit: usize, context: &str) -> io::Result> { - let metadata = fs::metadata(path).map_err(|e| { - err(&format!( - "cannot stat {} '{}': {}", - context, - path.display(), - e - )) - })?; - if metadata.len() > limit as u64 { - return Err(err(&format!( - "{} '{}' exceeds size limit", - context, - path.display() - ))); - } - fs::read(path).map_err(|e| { - err(&format!( - "cannot read {} '{}': {}", - context, - path.display(), - e - )) - }) -} - -fn try_reserve_exact(vec: &mut Vec, additional: usize, context: &str) -> io::Result<()> { - vec.try_reserve_exact(additional) - .map_err(|_| err(&format!("{} allocation failed", context))) -} - -fn append_pack_bytes(pack: &mut Vec, bytes: &[u8]) -> io::Result<()> { - let new_len = pack - .len() - .checked_add(bytes.len()) - .ok_or_else(|| err("packfile size overflow"))?; - if new_len > MAX_PACK_BYTES { - return Err(err("packfile exceeds size limit")); - } - try_reserve_exact(pack, bytes.len(), "packfile")?; - pack.extend_from_slice(bytes); - Ok(()) -} - -fn add_pack_inflated_bytes(total: &mut usize, bytes: usize) -> io::Result<()> { - *total = total - .checked_add(bytes) - .ok_or_else(|| err("inflated pack size overflow"))?; - if *total > MAX_PACK_INFLATED_BYTES { - return Err(err("inflated pack data exceeds size limit")); - } - Ok(()) -} - -fn add_pack_resolved_bytes(total: &mut usize, bytes: usize) -> io::Result<()> { - *total = total - .checked_add(bytes) - .ok_or_else(|| err("resolved pack size overflow"))?; - if *total > MAX_PACK_RESOLVED_BYTES { - return Err(err("resolved pack data exceeds size limit")); - } - Ok(()) -} - -fn add_pkt_payload_bytes(total: &mut usize, bytes: usize) -> io::Result<()> { - *total = total - .checked_add(bytes) - .ok_or_else(|| err("pkt-line payload size overflow"))?; - if *total > MAX_PKT_LINE_PAYLOAD_BYTES { - return Err(err("pkt-line payload exceeds size limit")); - } - Ok(()) -} - -fn add_advertised_ref_count(total: usize) -> io::Result<()> { - if total > MAX_ADVERTISED_REFS { - return Err(err("remote advertised too many refs")); - } - Ok(()) -} - -fn response_body_preview(body: &[u8]) -> String { - let limit = body.len().min(MAX_HTTP_ERROR_BODY_BYTES); - let mut preview = String::from_utf8_lossy(&body[..limit]).trim().to_string(); - if body.len() > limit { - preview.push_str("..."); - } - preview -} - -fn normalize_repo_path(path: &str) -> io::Result { - if path.is_empty() || path.as_bytes().contains(&0) { - return Err(err("invalid empty or nul-containing repository path")); - } - - let mut parts = Vec::new(); - for component in Path::new(path).components() { - match component { - Component::Normal(part) if part == OsStr::new(".git") => { - return Err(err("repository path must not enter .git")); - } - Component::Normal(part) => { - let part = part - .to_str() - .ok_or_else(|| err("repository path must be utf-8"))?; - if part.is_empty() { - return Err(err("repository path contains an empty component")); - } - parts.push(part); - } - Component::CurDir - | Component::ParentDir - | Component::RootDir - | Component::Prefix(_) => { - return Err(err("repository path must be relative and normalized")); - } - } - } - - if parts.is_empty() { - return Err(err("repository path must name a file")); - } - - Ok(parts.join("/")) -} - -fn worktree_path(workdir: &Path, repo_path: &str) -> io::Result { - Ok(workdir.join(normalize_repo_path(repo_path)?)) -} - -fn validate_ref_tail(name: &str) -> io::Result<&str> { - if name.is_empty() - || name.as_bytes().contains(&0) - || name.starts_with('/') - || name.ends_with('/') - || name.contains('\\') - { - return Err(err("invalid git ref name")); - } - - for part in name.split('/') { - if part.is_empty() || part == "." || part == ".." { - return Err(err("invalid git ref name")); - } - } - - Ok(name) -} - -fn validate_branch_name(name: &str) -> io::Result<&str> { - validate_ref_tail(name) -} - -fn validate_refname(refname: &str) -> io::Result<&str> { - if refname == "HEAD" { - return Ok(refname); - } - - for prefix in ["refs/heads/", "refs/remotes/", "refs/tags/"] { - if let Some(tail) = refname.strip_prefix(prefix) { - validate_ref_tail(tail)?; - return Ok(refname); - } - } - - Err(err("unsupported git ref name")) -} - -fn hash_bytes(obj_type: &str, data: &[u8]) -> [u8; 20] { - let header = format!("{} {}\0", obj_type, data.len()); - let mut hasher = Sha1::new(); - hasher.update(header.as_bytes()); - hasher.update(data); - hasher.finalize().into() -} - -fn is_zero_oid(hash: &[u8; 20]) -> bool { - hash.iter().all(|b| *b == 0) -} - -fn is_remote_source(source: &str) -> bool { - source.starts_with("http://") || source.starts_with("https://") -} - -fn is_ssh_clone_source(source: &str) -> bool { - source.starts_with("git@") - || source.starts_with("ssh://") - || source.starts_with("git://") - || source.starts_with("ssh+git://") -} - -fn has_http_auth(source: &str) -> bool { - if !is_remote_source(source) { - return false; - } - - let Some(scheme_sep) = source.find("://") else { - return false; - }; - let authority = &source[scheme_sep + 3..] - .split_once('/') - .map(|(authority, _)| authority) - .unwrap_or(&source[scheme_sep + 3..]); - - authority.contains('@') -} - -fn infer_clone_destination(source: &str) -> io::Result { - let basename = if is_remote_source(source) || is_ssh_clone_source(source) { - let trimmed = source.trim_end_matches('/'); - let leaf = if let Some((_, scp_path)) = trimmed.rsplit_once(':') { - if is_ssh_clone_source(source) && !scp_path.contains('/') { - scp_path - } else { - trimmed - .rsplit('/') - .next() - .filter(|segment| !segment.is_empty()) - .ok_or_else(|| err("could not determine destination path from source"))? - } - } else { - trimmed - .rsplit('/') - .next() - .filter(|segment| !segment.is_empty()) - .ok_or_else(|| err("could not determine destination path from source"))? - }; - leaf.to_string() - } else { - Path::new(source) - .file_name() - .map(|name| name.to_string_lossy().to_string()) - .filter(|name| !name.is_empty()) - .ok_or_else(|| err("could not determine destination path from source"))? - }; - - Ok(basename - .strip_suffix(".git") - .unwrap_or(&basename) - .to_string()) -} - -fn prepare_clone_destination( - dest: &Path, - source_label: &str, - default_branch: &str, -) -> io::Result { - validate_branch_name(default_branch)?; - mkdirs(dest)?; - - let dst_git = dest.join(".git"); - mkdirs(&dst_git.join("objects"))?; - mkdirs(&dst_git.join("refs/heads"))?; - mkdirs(&dst_git.join("refs/tags"))?; - mkdirs(&dst_git.join("refs/remotes/origin"))?; - - fs::write( - dst_git.join("HEAD"), - format!("ref: refs/heads/{}\n", default_branch), - )?; - fs::write( - dst_git.join("config"), - format!( - "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n\ - [remote \"origin\"]\n\turl = {}\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n", - source_label - ), - )?; - - Ok(dst_git) -} - -#[derive(Debug)] -enum PktLine { - Data(Vec), - Flush, -} - -fn parse_pkt_lines(data: &[u8]) -> io::Result> { - let mut pos = 0usize; - let mut lines = Vec::new(); - let mut payload_bytes = 0usize; - - while pos + 4 <= data.len() { - if lines.len() >= MAX_PKT_LINES { - return Err(err("too many pkt-lines")); - } - let len_str = - std::str::from_utf8(&data[pos..pos + 4]).map_err(|_| err("invalid pkt-line"))?; - let len = usize::from_str_radix(len_str, 16).map_err(|_| err("invalid pkt-line length"))?; - pos += 4; - - if len == 0 { - lines.push(PktLine::Flush); - continue; - } - if len < 4 || pos + len - 4 > data.len() { - return Err(err("truncated pkt-line")); - } - - let payload_len = len - 4; - add_pkt_payload_bytes(&mut payload_bytes, payload_len)?; - lines.push(PktLine::Data(data[pos..pos + payload_len].to_vec())); - pos += len - 4; - } - - if pos != data.len() { - return Err(err("trailing bytes after pkt-lines")); - } - - Ok(lines) -} - -#[derive(Debug, Default)] -struct RemoteAdvertisement { - head_hash: Option<[u8; 20]>, - head_target: Option, - branches: BTreeMap, - tags: BTreeMap, -} - -fn parse_advertised_ref(line: &[u8], adv: &mut RemoteAdvertisement) -> io::Result<()> { - let (ref_part, capability_part) = if let Some(nul) = line.iter().position(|b| *b == 0) { - (&line[..nul], Some(&line[nul + 1..])) - } else { - (line, None) - }; - - let ref_text = std::str::from_utf8(ref_part) - .map_err(|_| err("invalid advertised ref"))? - .trim_end_matches('\n'); - if ref_text.is_empty() { - return Ok(()); - } - - let (hash_hex, refname) = ref_text - .split_once(' ') - .ok_or_else(|| err("malformed advertised ref"))?; - let hash = unhex(hash_hex)?; - - if refname == "HEAD" { - adv.head_hash = Some(hash); - } else if refname.starts_with("refs/heads/") { - validate_refname(refname)?; - adv.branches.insert(refname.to_string(), hash); - add_advertised_ref_count(adv.branches.len() + adv.tags.len())?; - } else if refname.starts_with("refs/tags/") { - validate_refname(refname)?; - adv.tags.insert(refname.to_string(), hash); - add_advertised_ref_count(adv.branches.len() + adv.tags.len())?; - } - - if let Some(capabilities) = capability_part { - let caps = std::str::from_utf8(capabilities).map_err(|_| err("invalid capability list"))?; - for cap in caps.split_whitespace() { - if let Some(target) = cap.strip_prefix("symref=HEAD:") { - validate_refname(target)?; - adv.head_target = Some(target.to_string()); - } - } - } - - Ok(()) -} - -fn fetch_remote_advertisement(url: &str) -> io::Result { - let info_refs_url = format!( - "{}/info/refs?service=git-upload-pack", - url.trim_end_matches('/') - ); - let req = Request::new(Method::Get, &info_refs_url) - .map_err(|e| err(&format!("bad info/refs URL: {}", e)))? - .header("Accept", "application/x-git-upload-pack-advertisement") - .header("Git-Protocol", "version=0"); - - let resp = HttpClient::new() - .send(&req) - .map_err(|e| err(&format!("fetch info/refs failed: {}", e)))?; - if resp.status != 200 { - let body = response_body_preview(&resp.body); - return Err(err(&format!( - "remote advertised refs request failed (HTTP {}): {}", - resp.status, body - ))); - } - if resp.body.len() > MAX_REF_ADVERTISEMENT_BYTES { - return Err(err("remote advertised refs response exceeds size limit")); - } - - let lines = parse_pkt_lines(&resp.body)?; - let mut adv = RemoteAdvertisement::default(); - let mut saw_service = false; - let mut in_refs = false; - - for line in lines { - match line { - PktLine::Flush => { - if saw_service { - in_refs = true; - } - } - PktLine::Data(data) => { - if !saw_service { - let service = std::str::from_utf8(&data) - .map_err(|_| err("invalid git service header"))?; - if service.trim_end_matches('\n') != "# service=git-upload-pack" { - return Err(err("unexpected git service advertisement")); - } - saw_service = true; - continue; - } - if !in_refs { - continue; - } - parse_advertised_ref(&data, &mut adv)?; - } - } - } - - if !saw_service { - return Err(err("missing git-upload-pack service header")); - } - - Ok(adv) -} - -fn branch_name_from_ref(refname: &str) -> io::Result { - refname - .strip_prefix("refs/heads/") - .map(validate_branch_name) - .transpose()? - .map(|name| name.to_string()) - .ok_or_else(|| err("expected refs/heads/* ref")) -} - -fn default_branch_ref(adv: &RemoteAdvertisement) -> Option<(String, [u8; 20])> { - if let Some(target) = adv.head_target.as_ref() { - if let Some(hash) = adv.branches.get(target) { - return Some((target.clone(), *hash)); - } - } - - if let Some(head_hash) = adv.head_hash { - if let Some((name, hash)) = adv.branches.iter().find(|(_, hash)| **hash == head_hash) { - return Some((name.clone(), *hash)); - } - } - - adv.branches - .iter() - .next() - .map(|(name, hash)| (name.clone(), *hash)) -} - -fn pkt_line_bytes(payload: &[u8]) -> Vec { - let mut out = format!("{:04x}", payload.len() + 4).into_bytes(); - out.extend_from_slice(payload); - out -} - -fn fetch_remote_pack(url: &str, wants: &[String]) -> io::Result> { - if wants.is_empty() { - return Ok(Vec::new()); - } - - let upload_pack_url = format!("{}/git-upload-pack", url.trim_end_matches('/')); - let mut body = Vec::new(); - for (i, want) in wants.iter().enumerate() { - let line = if i == 0 { - format!( - "want {} side-band-64k ofs-delta no-progress include-tag agent=agentos/1.0\n", - want - ) - } else { - format!("want {}\n", want) - }; - body.extend_from_slice(&pkt_line_bytes(line.as_bytes())); - } - body.extend_from_slice(b"0000"); - body.extend_from_slice(&pkt_line_bytes(b"done\n")); - - let req = Request::new(Method::Post, &upload_pack_url) - .map_err(|e| err(&format!("bad upload-pack URL: {}", e)))? - .header("Content-Type", "application/x-git-upload-pack-request") - .header("Accept", "application/x-git-upload-pack-result") - .header("Git-Protocol", "version=0") - .body(body); - - let resp = HttpClient::new() - .send(&req) - .map_err(|e| err(&format!("git-upload-pack failed: {}", e)))?; - if resp.status != 200 { - let body = response_body_preview(&resp.body); - return Err(err(&format!( - "git-upload-pack returned HTTP {}: {}", - resp.status, body - ))); - } - - if resp.body.len() > MAX_PACK_BYTES { - return Err(err("packfile exceeds size limit")); - } - if resp.body.starts_with(b"PACK") { - return Ok(resp.body); - } - - let mut pack = Vec::new(); - for line in parse_pkt_lines(&resp.body)? { - match line { - PktLine::Flush => {} - PktLine::Data(payload) => { - if payload == b"NAK\n" || payload.starts_with(b"ACK ") { - continue; - } - if payload.starts_with(b"ERR ") { - let msg = String::from_utf8_lossy(&payload[4..]); - return Err(err(&format!("remote upload-pack error: {}", msg.trim()))); - } - if payload.starts_with(b"PACK") { - append_pack_bytes(&mut pack, &payload)?; - continue; - } - if payload.is_empty() { - continue; - } - match payload[0] { - 1 => append_pack_bytes(&mut pack, &payload[1..])?, - 2 => {} - 3 => { - let msg = String::from_utf8_lossy(&payload[1..]); - return Err(err(&format!("remote upload-pack error: {}", msg.trim()))); - } - _ => return Err(err("unexpected upload-pack response payload")), - } - } - } - } - - if pack.is_empty() { - return Err(err("git-upload-pack response did not include a packfile")); - } - Ok(pack) -} - -#[derive(Clone, Debug)] -enum PackedObjectKind { - Full { obj_type: String, data: Vec }, - OfsDelta { base_offset: usize, delta: Vec }, - RefDelta { base_hash: [u8; 20], delta: Vec }, -} - -#[derive(Clone, Debug)] -struct PackedObject { - offset: usize, - kind: PackedObjectKind, -} - -#[derive(Clone, Debug)] -struct ResolvedObject { - obj_type: String, - data: Vec, - hash: [u8; 20], -} - -fn parse_pack_object_header(pack: &[u8], offset: &mut usize) -> io::Result<(u8, usize)> { - if *offset >= pack.len() { - return Err(err("truncated pack object header")); - } - - let mut byte = pack[*offset]; - *offset += 1; - - let obj_type = (byte >> 4) & 0x7; - let mut size = (byte & 0x0f) as usize; - let mut shift = 4usize; - - while byte & 0x80 != 0 { - if *offset >= pack.len() { - return Err(err("truncated pack object size")); - } - byte = pack[*offset]; - *offset += 1; - if shift >= usize::BITS as usize { - return Err(err("pack object size is too large")); - } - size |= ((byte & 0x7f) as usize) - .checked_shl(shift as u32) - .ok_or_else(|| err("pack object size is too large"))?; - shift += 7; - } - - Ok((obj_type, size)) -} - -fn parse_ofs_delta_base( - pack: &[u8], - offset: &mut usize, - object_offset: usize, -) -> io::Result { - if *offset >= pack.len() { - return Err(err("truncated ofs-delta base")); - } - - let mut byte = pack[*offset]; - *offset += 1; - let mut distance = (byte & 0x7f) as usize; - - while byte & 0x80 != 0 { - if *offset >= pack.len() { - return Err(err("truncated ofs-delta base")); - } - byte = pack[*offset]; - *offset += 1; - distance = distance - .checked_add(1) - .and_then(|value| value.checked_shl(7)) - .map(|value| value | ((byte & 0x7f) as usize)) - .ok_or_else(|| err("ofs-delta base distance is too large"))?; - } - - object_offset - .checked_sub(distance) - .ok_or_else(|| err("invalid ofs-delta base distance")) -} - -fn inflate_pack_stream(data: &[u8], expected_size: usize) -> io::Result<(Vec, usize)> { - if expected_size > MAX_GIT_OBJECT_BYTES { - return Err(err("pack object exceeds size limit")); - } - let cursor = Cursor::new(data); - let mut decoder = BufZlibDecoder::new(cursor); - let out = read_to_end_limited(&mut decoder, expected_size, "pack object")?; - if out.len() != expected_size { - return Err(err("pack object size mismatch")); - } - let consumed = decoder.get_ref().position() as usize; - Ok((out, consumed)) -} - -fn parse_packfile(pack: &[u8]) -> io::Result> { - if pack.len() < 12 + 20 { - return Err(err("packfile too small")); - } - if &pack[..4] != b"PACK" { - return Err(err("invalid packfile signature")); - } - - let version = u32::from_be_bytes(pack[4..8].try_into().unwrap()); - if version != 2 && version != 3 { - return Err(err(&format!("unsupported packfile version {}", version))); - } - let object_count = u32::from_be_bytes(pack[8..12].try_into().unwrap()) as usize; - let pack_end = pack.len() - 20; - let mut offset = 12usize; - let max_count_by_bytes = pack_end.saturating_sub(offset); - if object_count > MAX_PACK_OBJECTS || object_count > max_count_by_bytes { - return Err(err("packfile object count is too large")); - } - let mut objects = Vec::new(); - try_reserve_exact(&mut objects, object_count, "pack object table")?; - let mut inflated_bytes = 0usize; - - for _ in 0..object_count { - if offset >= pack_end { - return Err(err("truncated packfile")); - } - - let object_offset = offset; - let (obj_type, object_size) = parse_pack_object_header(pack, &mut offset)?; - - let kind = match obj_type { - 1 | 2 | 3 | 4 => { - let obj_type = match obj_type { - 1 => "commit", - 2 => "tree", - 3 => "blob", - 4 => "tag", - _ => unreachable!(), - }; - let (data, consumed) = inflate_pack_stream(&pack[offset..pack_end], object_size)?; - offset += consumed; - add_pack_inflated_bytes(&mut inflated_bytes, data.len())?; - PackedObjectKind::Full { - obj_type: obj_type.to_string(), - data, - } - } - 6 => { - let base_offset = parse_ofs_delta_base(pack, &mut offset, object_offset)?; - let (delta, consumed) = inflate_pack_stream(&pack[offset..pack_end], object_size)?; - offset += consumed; - add_pack_inflated_bytes(&mut inflated_bytes, delta.len())?; - PackedObjectKind::OfsDelta { base_offset, delta } - } - 7 => { - if offset + 20 > pack_end { - return Err(err("truncated ref-delta base")); - } - let mut base_hash = [0u8; 20]; - base_hash.copy_from_slice(&pack[offset..offset + 20]); - offset += 20; - let (delta, consumed) = inflate_pack_stream(&pack[offset..pack_end], object_size)?; - offset += consumed; - add_pack_inflated_bytes(&mut inflated_bytes, delta.len())?; - PackedObjectKind::RefDelta { base_hash, delta } - } - _ => return Err(err(&format!("unsupported pack object type {}", obj_type))), - }; - - objects.push(PackedObject { - offset: object_offset, - kind, - }); - } - - Ok(objects) -} - -fn read_delta_varint(data: &[u8], pos: &mut usize) -> io::Result { - let mut value = 0usize; - let mut shift = 0usize; - - loop { - if *pos >= data.len() { - return Err(err("truncated delta header")); - } - let byte = data[*pos]; - *pos += 1; - - if shift >= usize::BITS as usize { - return Err(err("delta varint is too large")); - } - value |= ((byte & 0x7f) as usize) - .checked_shl(shift as u32) - .ok_or_else(|| err("delta varint is too large"))?; - if byte & 0x80 == 0 { - return Ok(value); - } - shift += 7; - } -} - -fn ensure_delta_output_room( - current_len: usize, - additional_len: usize, - result_size: usize, -) -> io::Result<()> { - let next_len = current_len - .checked_add(additional_len) - .ok_or_else(|| err("delta result size overflow"))?; - if next_len > result_size { - return Err(err("delta result exceeds declared size")); - } - Ok(()) -} - -fn apply_delta(base: &[u8], delta: &[u8]) -> io::Result> { - let mut pos = 0usize; - let base_size = read_delta_varint(delta, &mut pos)?; - if base_size != base.len() { - return Err(err("delta base size mismatch")); - } - let result_size = read_delta_varint(delta, &mut pos)?; - if result_size > MAX_GIT_OBJECT_BYTES { - return Err(err("delta result exceeds size limit")); - } - let mut out = Vec::new(); - try_reserve_exact(&mut out, result_size, "delta result")?; - - while pos < delta.len() { - let opcode = delta[pos]; - pos += 1; - - if opcode & 0x80 != 0 { - let mut copy_offset = 0usize; - let mut copy_size = 0usize; - - if opcode & 0x01 != 0 { - copy_offset |= delta - .get(pos) - .copied() - .ok_or_else(|| err("truncated delta copy"))? - as usize; - pos += 1; - } - if opcode & 0x02 != 0 { - copy_offset |= (delta - .get(pos) - .copied() - .ok_or_else(|| err("truncated delta copy"))? - as usize) - << 8; - pos += 1; - } - if opcode & 0x04 != 0 { - copy_offset |= (delta - .get(pos) - .copied() - .ok_or_else(|| err("truncated delta copy"))? - as usize) - << 16; - pos += 1; - } - if opcode & 0x08 != 0 { - copy_offset |= (delta - .get(pos) - .copied() - .ok_or_else(|| err("truncated delta copy"))? - as usize) - << 24; - pos += 1; - } - - if opcode & 0x10 != 0 { - copy_size |= delta - .get(pos) - .copied() - .ok_or_else(|| err("truncated delta copy"))? - as usize; - pos += 1; - } - if opcode & 0x20 != 0 { - copy_size |= (delta - .get(pos) - .copied() - .ok_or_else(|| err("truncated delta copy"))? - as usize) - << 8; - pos += 1; - } - if opcode & 0x40 != 0 { - copy_size |= (delta - .get(pos) - .copied() - .ok_or_else(|| err("truncated delta copy"))? - as usize) - << 16; - pos += 1; - } - if copy_size == 0 { - copy_size = 0x10000; - } - - let end = copy_offset - .checked_add(copy_size) - .ok_or_else(|| err("delta copy overflow"))?; - if end > base.len() { - return Err(err("delta copy exceeds base object")); - } - ensure_delta_output_room(out.len(), copy_size, result_size)?; - out.extend_from_slice(&base[copy_offset..end]); - } else if opcode != 0 { - let insert_len = opcode as usize; - let end = pos - .checked_add(insert_len) - .ok_or_else(|| err("delta insert overflow"))?; - if end > delta.len() { - return Err(err("truncated delta insert")); - } - ensure_delta_output_room(out.len(), insert_len, result_size)?; - out.extend_from_slice(&delta[pos..end]); - pos = end; - } else { - return Err(err("invalid delta opcode")); - } - } - - if out.len() != result_size { - return Err(err("delta result size mismatch")); - } - - Ok(out) -} - -fn maybe_read_local_object(git_dir: &Path, hash: &[u8; 20]) -> io::Result> { - let h = hex(hash); - let path = git_dir.join("objects").join(&h[..2]).join(&h[2..]); - if !path.exists() { - return Ok(None); - } - let (obj_type, data) = read_object(git_dir, hash)?; - Ok(Some(ResolvedObject { - obj_type, - data, - hash: *hash, - })) -} - -fn find_entry_by_hash( - target: &[u8; 20], - git_dir: &Path, - objects: &[PackedObject], - offset_to_index: &HashMap, - memo: &mut [Option], - visiting: &mut [bool], - resolved_bytes: &mut usize, -) -> io::Result> { - for idx in 0..objects.len() { - if visiting[idx] { - continue; - } - let resolved = resolve_packed_object( - idx, - git_dir, - objects, - offset_to_index, - memo, - visiting, - resolved_bytes, - )?; - if resolved.hash == *target { - return Ok(Some(idx)); - } - } - - Ok(None) -} - -fn resolve_packed_object( - idx: usize, - git_dir: &Path, - objects: &[PackedObject], - offset_to_index: &HashMap, - memo: &mut [Option], - visiting: &mut [bool], - resolved_bytes: &mut usize, -) -> io::Result { - if let Some(resolved) = memo[idx].as_ref() { - return Ok(resolved.clone()); - } - if visiting[idx] { - return Err(err("cyclic pack delta dependency")); - } - visiting[idx] = true; - - let resolved = match &objects[idx].kind { - PackedObjectKind::Full { obj_type, data } => ResolvedObject { - obj_type: obj_type.clone(), - data: data.clone(), - hash: hash_bytes(obj_type, data), - }, - PackedObjectKind::OfsDelta { base_offset, delta } => { - let base_idx = *offset_to_index - .get(base_offset) - .ok_or_else(|| err("missing ofs-delta base object"))?; - let base = resolve_packed_object( - base_idx, - git_dir, - objects, - offset_to_index, - memo, - visiting, - resolved_bytes, - )?; - let data = apply_delta(&base.data, delta)?; - let hash = hash_bytes(&base.obj_type, &data); - ResolvedObject { - obj_type: base.obj_type, - data, - hash, - } - } - PackedObjectKind::RefDelta { base_hash, delta } => { - let base = if let Some(local) = maybe_read_local_object(git_dir, base_hash)? { - local - } else if let Some(base_idx) = find_entry_by_hash( - base_hash, - git_dir, - objects, - offset_to_index, - memo, - visiting, - resolved_bytes, - )? { - resolve_packed_object( - base_idx, - git_dir, - objects, - offset_to_index, - memo, - visiting, - resolved_bytes, - )? - } else { - return Err(err("missing ref-delta base object")); - }; - - let data = apply_delta(&base.data, delta)?; - let hash = hash_bytes(&base.obj_type, &data); - ResolvedObject { - obj_type: base.obj_type, - data, - hash, - } - } - }; - - visiting[idx] = false; - add_pack_resolved_bytes(resolved_bytes, resolved.data.len())?; - memo[idx] = Some(resolved.clone()); - Ok(resolved) -} - -fn store_pack_objects(git_dir: &Path, pack: &[u8]) -> io::Result<()> { - if pack.is_empty() { - return Ok(()); - } - - let objects = parse_packfile(pack)?; - let offset_to_index: HashMap = objects - .iter() - .enumerate() - .map(|(idx, obj)| (obj.offset, idx)) - .collect(); - let mut memo: Vec> = vec![None; objects.len()]; - let mut visiting = vec![false; objects.len()]; - let mut resolved_bytes = 0usize; - - for idx in 0..objects.len() { - let resolved = resolve_packed_object( - idx, - git_dir, - &objects, - &offset_to_index, - &mut memo, - &mut visiting, - &mut resolved_bytes, - )?; - let stored = hash_object(git_dir, &resolved.obj_type, &resolved.data)?; - if stored != resolved.hash { - return Err(err("pack object hash mismatch")); - } - } - Ok(()) -} - -fn cmd_clone_remote(source: &str, dest: &Path) -> io::Result<()> { - if dest.exists() && !dir_is_empty(dest)? { - return Err(err(&format!( - "destination path '{}' already exists and is not an empty directory", - dest.display() - ))); - } - - let advertisement = fetch_remote_advertisement(source)?; - let mut wants: Vec = Vec::new(); - let mut seen = HashSet::new(); - for hash in advertisement.branches.values() { - if !is_zero_oid(hash) { - let hash_hex = hex(hash); - if seen.insert(hash_hex.clone()) { - wants.push(hash_hex); - } - } - } - if wants.is_empty() { - if let Some(head_hash) = advertisement.head_hash { - if !is_zero_oid(&head_hash) { - wants.push(hex(&head_hash)); - } - } - } - - let default_ref = if let Some((refname, hash)) = default_branch_ref(&advertisement) { - Some((refname.clone(), branch_name_from_ref(&refname)?, hash)) - } else { - None - }; - let default_branch = default_ref - .as_ref() - .map(|(_, branch, _)| branch.clone()) - .or_else(|| { - advertisement - .head_target - .as_ref() - .and_then(|target| branch_name_from_ref(target).ok()) - }) - .unwrap_or_else(|| "main".to_string()); - - let pack = fetch_remote_pack(source, &wants)?; - - let dst_git = prepare_clone_destination(dest, source, &default_branch)?; - store_pack_objects(&dst_git, &pack)?; - - for (refname, hash) in &advertisement.branches { - if is_zero_oid(hash) { - continue; - } - let branch = branch_name_from_ref(refname)?; - update_ref(&dst_git, &format!("refs/remotes/origin/{}", branch), hash)?; - } - - for (refname, hash) in &advertisement.tags { - if is_zero_oid(hash) { - continue; - } - update_ref(&dst_git, refname, hash)?; - } - - let default_hash = default_ref - .as_ref() - .map(|(_, _, hash)| *hash) - .or(advertisement.head_hash) - .filter(|hash| !is_zero_oid(hash)); - - if let Some(hash) = default_hash { - update_ref(&dst_git, &format!("refs/heads/{}", default_branch), &hash)?; - cmd_checkout(dest, &default_branch, false)?; - } - - Ok(()) -} - -// ─── Object store ─────────────────────────────────────────────────────────── - -fn hash_object(git_dir: &Path, obj_type: &str, data: &[u8]) -> io::Result<[u8; 20]> { - if data.len() > MAX_GIT_OBJECT_BYTES { - return Err(err("git object exceeds size limit")); - } - let header = format!("{} {}\0", obj_type, data.len()); - let mut hasher = Sha1::new(); - hasher.update(header.as_bytes()); - hasher.update(data); - let hash: [u8; 20] = hasher.finalize().into(); - - let h = hex(&hash); - let dir = git_dir.join("objects").join(&h[..2]); - let path = dir.join(&h[2..]); - if !path.exists() { - mkdirs(&dir)?; - let mut enc = ZlibEncoder::new(Vec::new(), Compression::default()); - enc.write_all(header.as_bytes())?; - enc.write_all(data)?; - fs::write(&path, enc.finish()?)?; - } - Ok(hash) -} - -fn read_object(git_dir: &Path, hash: &[u8; 20]) -> io::Result<(String, Vec)> { - let h = hex(hash); - let path = git_dir.join("objects").join(&h[..2]).join(&h[2..]); - let read_limit = MAX_GIT_OBJECT_BYTES - .checked_add(MAX_GIT_OBJECT_HEADER_BYTES) - .ok_or_else(|| err("git object size limit is too large"))?; - let compressed = read_file_limited(&path, read_limit, "git object")?; - let mut dec = ZlibDecoder::new(&compressed[..]); - let buf = read_to_end_limited(&mut dec, read_limit, "git object")?; - - let nul = buf - .iter() - .position(|&b| b == 0) - .ok_or_else(|| err("no nul in object"))?; - let header = - std::str::from_utf8(&buf[..nul]).map_err(|_| err("invalid object header encoding"))?; - let (typ, size) = header - .split_once(' ') - .ok_or_else(|| err("malformed object header"))?; - let size: usize = size.parse().map_err(|_| err("invalid object size"))?; - if size > MAX_GIT_OBJECT_BYTES { - return Err(err("git object exceeds size limit")); - } - if buf.len() - nul - 1 != size { - return Err(err("git object size mismatch")); - } - Ok((typ.to_string(), buf[nul + 1..].to_vec())) -} - -// ─── Index ────────────────────────────────────────────────────────────────── - -#[derive(Clone, Debug)] -struct IndexEntry { - mode: u32, - sha1: [u8; 20], - name: String, -} - -fn read_index(git_dir: &Path) -> io::Result> { - let path = git_dir.join("index"); - if !path.exists() { - return Ok(Vec::new()); - } - let data = read_file_limited(&path, MAX_INDEX_BYTES, "git index")?; - if data.len() < 12 || &data[0..4] != b"DIRC" { - return Err(err("invalid index file")); - } - let version = u32::from_be_bytes(data[4..8].try_into().unwrap()); - if version != 2 { - return Err(err(&format!("unsupported index version {}", version))); - } - let count = u32::from_be_bytes(data[8..12].try_into().unwrap()) as usize; - let max_count_by_bytes = data.len().saturating_sub(12) / 62; - if count > MAX_INDEX_ENTRIES || count > max_count_by_bytes { - return Err(err("index entry count is too large")); - } - - let mut entries = Vec::new(); - try_reserve_exact(&mut entries, count, "index entry table")?; - let mut pos = 12; - - for _ in 0..count { - if pos + 62 > data.len() { - return Err(err("truncated index")); - } - // Stat fields at pos+0..pos+24 (ctime, mtime, dev, ino) - skip - let mode = u32::from_be_bytes(data[pos + 24..pos + 28].try_into().unwrap()); - // Skip uid(4), gid(4), size(4) at pos+28..pos+40 - let mut sha1 = [0u8; 20]; - sha1.copy_from_slice(&data[pos + 40..pos + 60]); - // Flags at pos+60..pos+62 - // Find name (NUL-terminated after fixed 62 bytes) - let name_start = pos + 62; - let nul_offset = data[name_start..] - .iter() - .position(|&b| b == 0) - .ok_or_else(|| err("unterminated index entry name"))?; - let name = String::from_utf8(data[name_start..name_start + nul_offset].to_vec()) - .map_err(|_| err("invalid entry name"))?; - let name = normalize_repo_path(&name)?; - - entries.push(IndexEntry { mode, sha1, name }); - - // Advance past padding: entry padded to 8-byte boundary - let entry_len = 62 + nul_offset; - pos += (entry_len + 8) & !7; - } - - Ok(entries) -} - -fn write_index(git_dir: &Path, entries: &[IndexEntry]) -> io::Result<()> { - let entry_count = u32::try_from(entries.len()).map_err(|_| err("too many index entries"))?; - let mut buf = Vec::new(); - buf.extend_from_slice(b"DIRC"); - buf.extend_from_slice(&2u32.to_be_bytes()); - buf.extend_from_slice(&entry_count.to_be_bytes()); - - for entry in entries { - let name = normalize_repo_path(&entry.name)?; - if name.len() > 0xFFF { - return Err(err("index entry name is too long")); - } - let entry_start = buf.len(); - // ctime(8) + mtime(8) + dev(4) + ino(4) = 24 bytes of zeros - buf.extend_from_slice(&[0u8; 24]); - buf.extend_from_slice(&entry.mode.to_be_bytes()); - // uid(4) + gid(4) + size(4) = 12 bytes of zeros - buf.extend_from_slice(&[0u8; 12]); - buf.extend_from_slice(&entry.sha1); - // Flags: name length in lower 12 bits - buf.extend_from_slice(&(name.len() as u16).to_be_bytes()); - buf.extend_from_slice(name.as_bytes()); - // Pad to 8-byte boundary (1-8 NUL bytes) - let entry_len = buf.len() - entry_start; - let padded = (entry_len + 8) & !7; - buf.resize(entry_start + padded, 0); - } - - // SHA-1 checksum of entire index - let checksum: [u8; 20] = Sha1::digest(&buf).into(); - buf.extend_from_slice(&checksum); - - fs::write(git_dir.join("index"), &buf) -} - -// ─── Refs ─────────────────────────────────────────────────────────────────── - -fn resolve_ref(git_dir: &Path, refname: &str) -> io::Result> { - let refname = validate_refname(refname)?; - let ref_path = git_dir.join(refname); - if !ref_path.exists() { - return Ok(None); - } - let content = fs::read_to_string(&ref_path)?; - let content = content.trim(); - if let Some(target) = content.strip_prefix("ref: ") { - resolve_ref(git_dir, target) - } else { - Ok(Some(unhex(content)?)) - } -} - -fn resolve_head(git_dir: &Path) -> io::Result> { - resolve_ref(git_dir, "HEAD") -} - -fn head_branch(git_dir: &Path) -> io::Result> { - let head = fs::read_to_string(git_dir.join("HEAD"))?; - let head = head.trim(); - Ok(head.strip_prefix("ref: refs/heads/").map(|s| s.to_string())) -} - -fn update_ref(git_dir: &Path, refname: &str, hash: &[u8; 20]) -> io::Result<()> { - let refname = validate_refname(refname)?; - let ref_path = git_dir.join(refname); - if let Some(parent) = ref_path.parent() { - mkdirs(parent)?; - } - fs::write(&ref_path, format!("{}\n", hex(hash))) -} - -// ─── Tree operations ──────────────────────────────────────────────────────── - -fn build_tree(git_dir: &Path, entries: &[IndexEntry]) -> io::Result<[u8; 20]> { - build_tree_at(git_dir, entries, "") -} - -fn build_tree_at(git_dir: &Path, entries: &[IndexEntry], prefix: &str) -> io::Result<[u8; 20]> { - struct TreeEntry { - mode: String, - name: String, - hash: [u8; 20], - } - - let mut tree_entries: Vec = Vec::new(); - let mut subdirs: BTreeMap> = BTreeMap::new(); - - for entry in entries { - let relative = if prefix.is_empty() { - &entry.name[..] - } else if let Some(rest) = entry.name.strip_prefix(prefix) { - rest - } else { - continue; - }; - - if let Some(slash) = relative.find('/') { - let dir = &relative[..slash]; - subdirs - .entry(dir.to_string()) - .or_default() - .push(entry.clone()); - } else { - tree_entries.push(TreeEntry { - mode: format!("{:o}", entry.mode), - name: relative.to_string(), - hash: entry.sha1, - }); - } - } - - for (dir, sub_entries) in &subdirs { - let sub_prefix = if prefix.is_empty() { - format!("{}/", dir) - } else { - format!("{}{}/", prefix, dir) - }; - let subtree_hash = build_tree_at(git_dir, sub_entries, &sub_prefix)?; - tree_entries.push(TreeEntry { - mode: "40000".to_string(), - name: dir.clone(), - hash: subtree_hash, - }); - } - - // Sort: directories get trailing / for comparison - tree_entries.sort_by(|a, b| { - let ak = if a.mode == "40000" { - format!("{}/", a.name) - } else { - a.name.clone() - }; - let bk = if b.mode == "40000" { - format!("{}/", b.name) - } else { - b.name.clone() - }; - ak.cmp(&bk) - }); - - let mut data = Vec::new(); - for te in &tree_entries { - data.extend_from_slice(te.mode.as_bytes()); - data.push(b' '); - data.extend_from_slice(te.name.as_bytes()); - data.push(0); - data.extend_from_slice(&te.hash); - } - - hash_object(git_dir, "tree", &data) -} - -/// Read a tree recursively into flat (path, mode, hash) entries. -fn read_tree_entries(git_dir: &Path, hash: &[u8; 20], prefix: &str) -> io::Result> { - let (typ, data) = read_object(git_dir, hash)?; - if typ != "tree" { - return Err(err("expected tree object")); - } - - let mut entries = Vec::new(); - let mut pos = 0; - - while pos < data.len() { - let space = data[pos..] - .iter() - .position(|&b| b == b' ') - .ok_or_else(|| err("bad tree entry"))?; - let mode_str = - std::str::from_utf8(&data[pos..pos + space]).map_err(|_| err("bad tree mode"))?; - let mode = u32::from_str_radix(mode_str, 8).map_err(|_| err("bad tree mode number"))?; - pos += space + 1; - - let nul = data[pos..] - .iter() - .position(|&b| b == 0) - .ok_or_else(|| err("bad tree entry name"))?; - let name = std::str::from_utf8(&data[pos..pos + nul]).map_err(|_| err("bad name"))?; - if name.contains('/') { - return Err(err("tree entry name must not contain '/'")); - } - if normalize_repo_path(name)? != name { - return Err(err("tree entry name must be normalized")); - } - pos += nul + 1; - - if pos + 20 > data.len() { - return Err(err("truncated tree hash")); - } - let mut hash_buf = [0u8; 20]; - hash_buf.copy_from_slice(&data[pos..pos + 20]); - pos += 20; - - let full_name = if prefix.is_empty() { - name.to_string() - } else { - format!("{}{}", prefix, name) - }; - - if mode == 0o40000 { - let sub = read_tree_entries(git_dir, &hash_buf, &format!("{}/", full_name))?; - entries.extend(sub); - } else { - let full_name = normalize_repo_path(&full_name)?; - entries.push(IndexEntry { - mode, - sha1: hash_buf, - name: full_name, - }); - } - } - - Ok(entries) -} - -/// Extract the tree hash from a commit object. -fn commit_tree(git_dir: &Path, commit_hash: &[u8; 20]) -> io::Result<[u8; 20]> { - let (typ, data) = read_object(git_dir, commit_hash)?; - if typ != "commit" { - return Err(err("not a commit object")); - } - let text = String::from_utf8_lossy(&data); - let tree_hex = text - .lines() - .find_map(|l| l.strip_prefix("tree ")) - .ok_or_else(|| err("no tree line in commit"))?; - unhex(tree_hex) -} - -// ─── Commands ─────────────────────────────────────────────────────────────── - -fn cmd_init(path: &Path, quiet: bool) -> io::Result<()> { - let git_dir = path.join(".git"); - // Create directories one at a time to avoid create_dir_all issues on WASI - for dir in &[ - path.to_path_buf(), - git_dir.clone(), - git_dir.join("objects"), - git_dir.join("refs"), - git_dir.join("refs/heads"), - git_dir.join("refs/tags"), - ] { - match fs::create_dir(dir) { - Ok(()) => {} - Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {} - Err(e) => return Err(err(&format!("mkdir {}: {}", dir.display(), e))), - } - } - fs::write(git_dir.join("HEAD"), "ref: refs/heads/main\n") - .map_err(|e| err(&format!("write HEAD: {}", e)))?; - fs::write( - git_dir.join("config"), - "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n", - ) - .map_err(|e| err(&format!("write config: {}", e)))?; - if !quiet { - print_stdout_line(format_args!( - "Initialized empty Git repository in {}/.git/", - path.display() - ))?; - } - Ok(()) -} - -fn collect_add_paths(workdir: &Path, rel_path: &str, out: &mut Vec) -> io::Result<()> { - if rel_path == ".git" || rel_path.starts_with(".git/") || rel_path.ends_with("/.git") { - return Ok(()); - } - - if rel_path == "." { - for entry in fs::read_dir(workdir)? { - let entry = entry?; - let name = entry - .file_name() - .to_str() - .ok_or_else(|| err("repository path must be utf-8"))? - .to_owned(); - if name == ".git" { - continue; - } - collect_add_paths(workdir, &name, out)?; - } - return Ok(()); - } - - let repo_path = normalize_repo_path(rel_path)?; - let file_path = workdir.join(&repo_path); - if file_path.is_dir() { - for entry in fs::read_dir(&file_path)? { - let entry = entry?; - let name = entry - .file_name() - .to_str() - .ok_or_else(|| err("repository path must be utf-8"))? - .to_owned(); - if name == ".git" { - continue; - } - collect_add_paths(workdir, &format!("{repo_path}/{name}"), out)?; - } - return Ok(()); - } - - if file_path.exists() { - out.push(repo_path); - } - Ok(()) -} - -fn cmd_add(workdir: &Path, paths: &[String]) -> io::Result<()> { - let git_dir = workdir.join(".git"); - let mut entries = read_index(&git_dir).map_err(|e| { - err(&format!( - "cannot read index at {}: {}", - git_dir.display(), - e - )) - })?; - - for rel_path in paths { - let mut repo_paths = Vec::new(); - collect_add_paths(workdir, rel_path, &mut repo_paths)?; - if repo_paths.is_empty() { - return Err(err(&format!( - "pathspec '{}' did not match any files", - rel_path - ))); - } - for repo_path in repo_paths { - let file_path = workdir.join(&repo_path); - if !file_path.exists() { - return Err(err(&format!( - "pathspec '{}' did not match any files (looked at {})", - rel_path, - file_path.display() - ))); - } - let content = read_file_limited(&file_path, MAX_GIT_OBJECT_BYTES, "file")?; - let hash = hash_object(&git_dir, "blob", &content)?; - - entries.retain(|e| e.name != repo_path); - entries.push(IndexEntry { - mode: 0o100644, - sha1: hash, - name: repo_path, - }); - } - } - - entries.sort_by(|a, b| a.name.cmp(&b.name)); - write_index(&git_dir, &entries) -} - -fn cmd_commit(workdir: &Path, message: &str) -> io::Result<()> { - let git_dir = workdir.join(".git"); - let entries = read_index(&git_dir)?; - if entries.is_empty() { - return Err(err("nothing to commit")); - } - - let tree_hash = build_tree(&git_dir, &entries)?; - let parent = resolve_head(&git_dir)?; - - let name = std::env::var("GIT_AUTHOR_NAME") - .or_else(|_| std::env::var("GIT_COMMITTER_NAME")) - .unwrap_or_else(|_| "secure-exec".to_string()); - let email = std::env::var("GIT_AUTHOR_EMAIL") - .or_else(|_| std::env::var("GIT_COMMITTER_EMAIL")) - .unwrap_or_else(|_| "agent@os".to_string()); - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(1700000000); - - let mut commit = String::new(); - commit.push_str(&format!("tree {}\n", hex(&tree_hash))); - if let Some(p) = parent { - commit.push_str(&format!("parent {}\n", hex(&p))); - } - commit.push_str(&format!( - "author {} <{}> {} +0000\n", - name, email, timestamp - )); - commit.push_str(&format!( - "committer {} <{}> {} +0000\n", - name, email, timestamp - )); - commit.push_str(&format!("\n{}\n", message)); - - let commit_hash = hash_object(&git_dir, "commit", commit.as_bytes())?; - - // Update the ref that HEAD points to - let head_content = fs::read_to_string(git_dir.join("HEAD"))?; - let head_content = head_content.trim(); - if let Some(refname) = head_content.strip_prefix("ref: ") { - update_ref(&git_dir, refname, &commit_hash)?; - } else { - fs::write(git_dir.join("HEAD"), format!("{}\n", hex(&commit_hash)))?; - } - - Ok(()) -} - -fn cmd_rev_parse(workdir: &Path, args: &[String]) -> io::Result<()> { - let git_dir = workdir.join(".git"); - if args.len() != 1 { - return Err(err("usage: git rev-parse ")); - } - - let hash = resolve_ref(&git_dir, &args[0])?.ok_or_else(|| { - err(&format!( - "unknown revision or path not in the working tree: {}", - args[0] - )) - })?; - print_stdout_line(format_args!("{}", hex(&hash))) -} - -fn cmd_branch(workdir: &Path) -> io::Result<()> { - let git_dir = workdir.join(".git"); - let heads_dir = git_dir.join("refs/heads"); - let current = head_branch(&git_dir)?; - - let mut branches: Vec = Vec::new(); - if heads_dir.exists() { - for entry in fs::read_dir(&heads_dir)? { - let entry = entry?; - let name = entry.file_name().to_string_lossy().to_string(); - if name == "." || name == ".." { - continue; - } - if entry.file_type()?.is_file() { - branches.push(name); - } - } - } - branches.sort(); - - for branch in &branches { - if Some(branch.as_str()) == current.as_deref() { - print_stdout_line(format_args!("* {}", branch))?; - } else { - print_stdout_line(format_args!(" {}", branch))?; - } - } - - Ok(()) -} - -fn cmd_checkout(workdir: &Path, target: &str, create_branch: bool) -> io::Result<()> { - let git_dir = workdir.join(".git"); - - if create_branch { - validate_branch_name(target)?; - let head = resolve_head(&git_dir)?.ok_or_else(|| err("HEAD not found for new branch"))?; - update_ref(&git_dir, &format!("refs/heads/{}", target), &head)?; - fs::write( - git_dir.join("HEAD"), - format!("ref: refs/heads/{}\n", target), - )?; - return Ok(()); - } - - // Resolve target: local branch first, then DWIM remote tracking - validate_branch_name(target)?; - let branch_ref = format!("refs/heads/{}", target); - let commit_hash = if let Some(h) = resolve_ref(&git_dir, &branch_ref)? { - fs::write(git_dir.join("HEAD"), format!("ref: {}\n", branch_ref))?; - h - } else { - let remote_ref = format!("refs/remotes/origin/{}", target); - if let Some(h) = resolve_ref(&git_dir, &remote_ref)? { - update_ref(&git_dir, &branch_ref, &h)?; - fs::write(git_dir.join("HEAD"), format!("ref: {}\n", branch_ref))?; - h - } else { - return Err(err(&format!( - "pathspec '{}' did not match any branch", - target - ))); - } - }; - - // Read target tree - let tree_hash = commit_tree(&git_dir, &commit_hash)?; - let new_entries = read_tree_entries(&git_dir, &tree_hash, "")?; - - // Clean up files from current index that aren't in target - let old_entries = read_index(&git_dir)?; - let new_names: HashSet<&str> = new_entries.iter().map(|e| e.name.as_str()).collect(); - for old in &old_entries { - if !new_names.contains(old.name.as_str()) { - let p = worktree_path(workdir, &old.name)?; - if p.exists() { - fs::remove_file(&p).map_err(|e| err(&format!("remove {}: {}", p.display(), e)))?; - } - } - } - - // Write files from target tree - for entry in &new_entries { - let p = worktree_path(workdir, &entry.name)?; - if let Some(parent) = p.parent() { - mkdirs(parent)?; - } - let (_, blob) = read_object(&git_dir, &entry.sha1)?; - fs::write(&p, &blob)?; - } - - // Update index - write_index(&git_dir, &new_entries)?; - - Ok(()) -} - -fn cmd_clone_local(source: &Path, dest: &Path) -> io::Result<()> { - let src_git = source.join(".git"); - if !src_git.is_dir() { - return Err(err(&format!( - "'{}' does not appear to be a git repository", - source.display() - ))); - } - - if dest.exists() && !dir_is_empty(dest)? { - return Err(err(&format!( - "destination path '{}' already exists and is not an empty directory", - dest.display() - ))); - } - - mkdirs(dest)?; - - let dst_git = dest.join(".git"); - - // Init destination - mkdirs(&dst_git.join("objects"))?; - mkdirs(&dst_git.join("refs/heads"))?; - mkdirs(&dst_git.join("refs/tags"))?; - mkdirs(&dst_git.join("refs/remotes/origin"))?; - - // Copy all objects - copy_dir_recursive(&src_git.join("objects"), &dst_git.join("objects"))?; - - // Create remote tracking branches from source heads - let src_heads = src_git.join("refs/heads"); - if src_heads.exists() { - copy_dir_recursive(&src_heads, &dst_git.join("refs/remotes/origin"))?; - } - - // Determine default branch from source HEAD - let src_head = fs::read_to_string(src_git.join("HEAD"))?; - let src_head = src_head.trim(); - let default_branch = src_head - .strip_prefix("ref: refs/heads/") - .unwrap_or("main") - .to_string(); - validate_branch_name(&default_branch)?; - - // Create local branch for default - let remote_ref = format!("refs/remotes/origin/{}", default_branch); - let mut has_default_branch = false; - if let Some(hash) = resolve_ref(&dst_git, &remote_ref)? { - update_ref(&dst_git, &format!("refs/heads/{}", default_branch), &hash)?; - has_default_branch = true; - } - - // Set HEAD and write config - fs::write( - dst_git.join("HEAD"), - format!("ref: refs/heads/{}\n", default_branch), - )?; - fs::write( - dst_git.join("config"), - format!( - "[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n\ - [remote \"origin\"]\n\turl = {}\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n", - source.display() - ), - )?; - - // Checkout working tree - if has_default_branch { - cmd_checkout(dest, &default_branch, false)?; - } - - Ok(()) -} - -fn copy_dir_recursive(src: &Path, dst: &Path) -> io::Result<()> { - mkdirs(dst)?; - if !src.exists() { - return Ok(()); - } - for entry in fs::read_dir(src)? { - let entry = entry?; - let name = entry.file_name().to_string_lossy().to_string(); - if name == "." || name == ".." { - continue; - } - let dst_path = dst.join(entry.file_name()); - if entry.file_type()?.is_dir() { - copy_dir_recursive(&entry.path(), &dst_path)?; - } else { - let content = read_file_limited( - &entry.path(), - MAX_GIT_OBJECT_BYTES + MAX_GIT_OBJECT_HEADER_BYTES, - "git repository file", - )?; - fs::write(&dst_path, content)?; - } - } - Ok(()) -} - -// ─── Entry point ──────────────────────────────────────────────────────────── - -pub fn main(args: Vec) -> i32 { - let str_args: Vec = args - .iter() - .map(|a| a.to_string_lossy().to_string()) - .collect(); - match run(&str_args) { - Ok(()) => 0, - Err(e) => { - eprintln!("fatal: {}", e); - 128 - } - } -} - -fn run(args: &[String]) -> io::Result<()> { - let mut i = 1; // skip argv[0] - let mut workdir = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")); - - // Parse global options - while i < args.len() { - if args[i] == "-C" { - i += 1; - if i >= args.len() { - return Err(err("-C requires a directory argument")); - } - let p = PathBuf::from(&args[i]); - workdir = if p.is_absolute() { p } else { workdir.join(p) }; - i += 1; - } else if args[i] == "-c" { - i += 1; - if i >= args.len() { - return Err(err("-c requires a config assignment")); - } - i += 1; - } else { - break; - } - } - - if i >= args.len() { - eprintln!("usage: git []"); - return Err(err("no subcommand")); - } - - let subcmd = &args[i]; - let sub_args = &args[i + 1..]; - - match subcmd.as_str() { - "init" => { - let mut path_arg = None; - let mut quiet = false; - for arg in sub_args { - if arg == "-q" || arg == "--quiet" { - quiet = true; - continue; - } - if arg.starts_with('-') { - return Err(unsupported("init", &format!("does not support `{}`.", arg))); - } - if path_arg.replace(arg).is_some() { - return Err(err("usage: git init []")); - } - } - let path = if let Some(path_arg) = path_arg { - let p = PathBuf::from(path_arg); - if p.is_absolute() { - p - } else { - workdir.join(p) - } - } else { - workdir - }; - cmd_init(&path, quiet) - } - "add" => { - if sub_args.is_empty() { - return Err(err("nothing specified, nothing added")); - } - let paths: Vec = sub_args.iter().map(|s| s.to_string()).collect(); - cmd_add(&workdir, &paths) - } - "commit" => { - let mut message = None; - let mut j = 0; - while j < sub_args.len() { - if sub_args[j] == "-m" && j + 1 < sub_args.len() { - message = Some(sub_args[j + 1].clone()); - j += 2; - } else { - j += 1; - } - } - let msg = message.ok_or_else(|| err("no commit message (-m)"))?; - cmd_commit(&workdir, &msg) - } - "branch" => cmd_branch(&workdir), - "rev-parse" => cmd_rev_parse(&workdir, sub_args), - "checkout" => { - let mut create = false; - let mut target = None; - for arg in sub_args { - if arg == "-b" { - create = true; - } else if !arg.starts_with('-') { - target = Some(arg.clone()); - } - } - let t = target.ok_or_else(|| err("no branch name specified"))?; - cmd_checkout(&workdir, &t, create) - } - "clone" => { - if sub_args.is_empty() || sub_args.len() > 2 { - return Err(err("usage: git clone []")); - } - let src_arg = &sub_args[0]; - if is_ssh_clone_source(src_arg) { - return Err(unsupported( - "clone", - &format!("does not support SSH or git:// remotes (`{}`).", src_arg), - )); - } - if has_http_auth(src_arg) { - return Err(unsupported( - "clone", - &format!( - "does not support authenticated HTTP(S) remotes (`{}`).", - src_arg - ), - )); - } - let dst_arg = if sub_args.len() == 2 { - sub_args[1].clone() - } else { - infer_clone_destination(src_arg)? - }; - let dst = PathBuf::from(&dst_arg); - let dst = if dst.is_absolute() { - dst - } else { - workdir.join(dst) - }; - print_stdout_line(format_args!("Cloning into '{}'...", dst.display()))?; - if is_remote_source(src_arg) { - cmd_clone_remote(src_arg, &dst) - } else { - let src = PathBuf::from(src_arg); - let src = if src.is_absolute() { - src - } else { - workdir.join(src) - }; - cmd_clone_local(&src, &dst) - } - } - other => Err(unsupported( - other, - "is not implemented in the secure-exec VM git command.", - )), - } -} diff --git a/registry/native/crates/libs/grep/Cargo.toml b/registry/native/crates/libs/grep/Cargo.toml deleted file mode 100644 index 3cf619a9c4..0000000000 --- a/registry/native/crates/libs/grep/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "secureexec-grep" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "grep/egrep/fgrep/rg implementations for secure-exec standalone binaries" - -[dependencies] -regex = "1" diff --git a/registry/native/crates/libs/grep/src/lib.rs b/registry/native/crates/libs/grep/src/lib.rs deleted file mode 100644 index c42db0e8ac..0000000000 --- a/registry/native/crates/libs/grep/src/lib.rs +++ /dev/null @@ -1,623 +0,0 @@ -//! grep implementation using the regex crate (ripgrep's pure Rust regex engine). -//! -//! Supports grep, egrep (grep -E), and fgrep (grep -F) modes. -//! Dispatches on argv[0] basename for standalone binary usage. - -mod rg_cmd; - -use std::ffi::OsString; -use std::io::{self, BufRead, Read, Write}; -use std::path::Path; - -use regex::Regex; - -const MAX_INPUT_LINE_BYTES: usize = 16 * 1024 * 1024; -const MAX_PATTERN_BYTES: usize = 16 * 1024 * 1024; -const MAX_PATTERNS: usize = 100_000; - -/// Unified grep entry point. Dispatches on argv[0]: -/// - "egrep" -> Extended mode -/// - "fgrep" -> Fixed mode -/// - default -> Basic mode -pub fn main(args: Vec) -> i32 { - let mode = match args.first().and_then(|a| Path::new(a).file_name()) { - Some(name) if name == "egrep" => GrepMode::Extended, - Some(name) if name == "fgrep" => GrepMode::Fixed, - _ => GrepMode::Basic, - }; - run_grep(args, mode) -} - -/// Entry point for grep command (Basic mode). -pub fn grep(args: Vec) -> i32 { - run_grep(args, GrepMode::Basic) -} - -/// Entry point for egrep command (Extended regex). -pub fn egrep(args: Vec) -> i32 { - run_grep(args, GrepMode::Extended) -} - -/// Entry point for fgrep command (Fixed strings). -pub fn fgrep(args: Vec) -> i32 { - run_grep(args, GrepMode::Fixed) -} - -/// Entry point for rg command. -pub fn rg(args: Vec) -> i32 { - rg_cmd::rg(args) -} - -/// grep mode determines how patterns are interpreted. -#[derive(Clone, Copy, PartialEq)] -enum GrepMode { - /// Basic regular expressions (default grep) - Basic, - /// Extended regular expressions (egrep / grep -E) - Extended, - /// Fixed strings (fgrep / grep -F) - Fixed, -} - -struct GrepOptions { - mode: GrepMode, - ignore_case: bool, - invert_match: bool, - count_only: bool, - files_with_matches: bool, - files_without_matches: bool, - line_numbers: bool, - word_regexp: bool, - line_regexp: bool, - max_count: Option, - quiet: bool, - patterns: Vec, - pattern_bytes: usize, - files: Vec, -} - -impl GrepOptions { - fn new(mode: GrepMode) -> Self { - Self { - mode, - ignore_case: false, - invert_match: false, - count_only: false, - files_with_matches: false, - files_without_matches: false, - line_numbers: false, - word_regexp: false, - line_regexp: false, - max_count: None, - quiet: false, - patterns: Vec::new(), - pattern_bytes: 0, - files: Vec::new(), - } - } -} - -fn run_grep(args: Vec, default_mode: GrepMode) -> i32 { - let str_args: Vec = args - .iter() - .skip(1) // skip argv[0] - .map(|a| a.to_string_lossy().to_string()) - .collect(); - - let opts = match parse_args(&str_args, default_mode) { - Ok(opts) => opts, - Err(msg) => { - eprintln!("grep: {}", msg); - return 2; - } - }; - - if opts.patterns.is_empty() { - eprintln!("grep: no pattern specified"); - return 2; - } - - let regex = match build_regex(&opts) { - Ok(r) => r, - Err(msg) => { - eprintln!("grep: {}", msg); - return 2; - } - }; - - let multiple_files = opts.files.len() > 1; - let mut any_match = false; - let mut had_error = false; - - if opts.files.is_empty() { - // Read from stdin - let stdin = io::stdin(); - let reader = stdin.lock(); - match search_reader(reader, None, ®ex, &opts, multiple_files) { - Ok(found) => any_match |= found, - Err(e) => { - eprintln!("grep: {}", e); - had_error = true; - } - } - } else { - for file in &opts.files { - if file == "-" { - let stdin = io::stdin(); - let reader = stdin.lock(); - let label = if multiple_files { - Some("(standard input)") - } else { - None - }; - match search_reader(reader, label, ®ex, &opts, multiple_files) { - Ok(found) => any_match |= found, - Err(e) => { - eprintln!("grep: {}: {}", file, e); - had_error = true; - } - } - } else { - match std::fs::File::open(file) { - Ok(f) => { - let reader = io::BufReader::new(f); - let label = if multiple_files { - Some(file.as_str()) - } else { - None - }; - match search_reader(reader, label, ®ex, &opts, multiple_files) { - Ok(found) => any_match |= found, - Err(e) => { - eprintln!("grep: {}: {}", file, e); - had_error = true; - } - } - } - Err(e) => { - eprintln!("grep: {}: {}", file, e); - had_error = true; - } - } - } - } - } - - if had_error { - 2 - } else if any_match { - 0 - } else { - 1 - } -} - -fn parse_args(args: &[String], default_mode: GrepMode) -> Result { - let mut opts = GrepOptions::new(default_mode); - let mut i = 0; - let mut pattern_from_args = false; - - while i < args.len() { - let arg = &args[i]; - - if arg == "--" { - i += 1; - // Remaining args are files (or first is pattern if none yet) - break; - } - - if arg.starts_with('-') && arg.len() > 1 && !arg.starts_with("--") { - let chars: Vec = arg[1..].chars().collect(); - let mut j = 0; - while j < chars.len() { - match chars[j] { - 'E' => opts.mode = GrepMode::Extended, - 'F' => opts.mode = GrepMode::Fixed, - 'G' => opts.mode = GrepMode::Basic, - 'i' | 'y' => opts.ignore_case = true, - 'v' => opts.invert_match = true, - 'c' => opts.count_only = true, - 'l' => opts.files_with_matches = true, - 'L' => opts.files_without_matches = true, - 'n' => opts.line_numbers = true, - 'w' => opts.word_regexp = true, - 'x' => opts.line_regexp = true, - 'q' | 's' => opts.quiet = true, - 'h' => {} // suppress filename (handled by multiple_files logic) - 'H' => {} // force filename - 'e' => { - // -e PATTERN (rest of this flag group or next arg) - let rest: String = chars[j + 1..].iter().collect(); - if !rest.is_empty() { - push_pattern(&mut opts, rest)?; - pattern_from_args = true; - j = chars.len(); // consumed rest - continue; - } else { - i += 1; - if i >= args.len() { - return Err("option requires an argument -- 'e'".to_string()); - } - push_pattern(&mut opts, args[i].clone())?; - pattern_from_args = true; - j = chars.len(); - continue; - } - } - 'f' => { - i += 1; - if i >= args.len() { - return Err("option requires an argument -- 'f'".to_string()); - } - read_patterns_from_file(&mut opts, &args[i])?; - pattern_from_args = true; - j = chars.len(); - continue; - } - 'm' => { - i += 1; - if i >= args.len() { - return Err("option requires an argument -- 'm'".to_string()); - } - opts.max_count = Some( - args[i] - .parse() - .map_err(|_| format!("invalid max count '{}'", args[i]))?, - ); - j = chars.len(); - continue; - } - _ => { - return Err(format!("invalid option -- '{}'", chars[j])); - } - } - j += 1; - } - } else if arg.starts_with("--") { - match arg.as_str() { - "--extended-regexp" => opts.mode = GrepMode::Extended, - "--fixed-strings" => opts.mode = GrepMode::Fixed, - "--basic-regexp" => opts.mode = GrepMode::Basic, - "--ignore-case" => opts.ignore_case = true, - "--invert-match" => opts.invert_match = true, - "--count" => opts.count_only = true, - "--files-with-matches" => opts.files_with_matches = true, - "--files-without-match" => opts.files_without_matches = true, - "--line-number" => opts.line_numbers = true, - "--word-regexp" => opts.word_regexp = true, - "--line-regexp" => opts.line_regexp = true, - "--quiet" | "--silent" => opts.quiet = true, - _ if arg.starts_with("--regexp=") => { - push_pattern(&mut opts, arg[9..].to_string())?; - pattern_from_args = true; - } - _ if arg.starts_with("--max-count=") => { - opts.max_count = Some( - arg[12..] - .parse() - .map_err(|_| format!("invalid max count '{}'", &arg[12..]))?, - ); - } - _ => { - return Err(format!("unrecognized option '{}'", arg)); - } - } - } else { - // Positional argument: first is pattern (if no -e), rest are files - if !pattern_from_args && opts.patterns.is_empty() { - push_pattern(&mut opts, arg.clone())?; - pattern_from_args = true; - } else { - opts.files.push(arg.clone()); - } - } - i += 1; - } - - // Remaining args after -- - while i < args.len() { - if !pattern_from_args && opts.patterns.is_empty() { - push_pattern(&mut opts, args[i].clone())?; - pattern_from_args = true; - } else { - opts.files.push(args[i].clone()); - } - i += 1; - } - - Ok(opts) -} - -fn push_pattern(opts: &mut GrepOptions, pattern: String) -> Result<(), String> { - if opts.patterns.len() >= MAX_PATTERNS { - return Err("too many patterns".to_string()); - } - let next_bytes = opts - .pattern_bytes - .checked_add(pattern.len()) - .ok_or_else(|| "pattern data too large".to_string())?; - if next_bytes > MAX_PATTERN_BYTES { - return Err("pattern data exceeds size limit".to_string()); - } - opts.pattern_bytes = next_bytes; - opts.patterns.push(pattern); - Ok(()) -} - -fn read_patterns_from_file(opts: &mut GrepOptions, path: &str) -> Result<(), String> { - let metadata = std::fs::metadata(path).map_err(|e| format!("{}: {}", path, e))?; - if metadata.len() > MAX_PATTERN_BYTES as u64 { - return Err(format!("{}: pattern file exceeds size limit", path)); - } - let file = std::fs::File::open(path).map_err(|e| format!("{}: {}", path, e))?; - let limit = MAX_PATTERN_BYTES - .checked_add(1) - .ok_or_else(|| "pattern file size limit is too large".to_string())?; - let mut content = String::new(); - file.take(limit as u64) - .read_to_string(&mut content) - .map_err(|e| format!("{}: {}", path, e))?; - if content.len() > MAX_PATTERN_BYTES { - return Err(format!("{}: pattern file exceeds size limit", path)); - } - for line in content.lines() { - if !line.is_empty() { - push_pattern(opts, line.to_string())?; - } - } - Ok(()) -} - -/// Build a compiled regex from the grep options. -fn build_regex(opts: &GrepOptions) -> Result { - let pattern = if opts.patterns.len() == 1 { - build_single_pattern(&opts.patterns[0], opts) - } else { - // Multiple patterns: combine with alternation - let parts: Vec = opts - .patterns - .iter() - .map(|p| format!("(?:{})", build_single_pattern(p, opts))) - .collect(); - parts.join("|") - }; - - let mut builder = regex::RegexBuilder::new(&pattern); - builder.case_insensitive(opts.ignore_case); - - builder - .build() - .map_err(|e| format!("invalid pattern: {}", e)) -} - -/// Convert a single pattern string to a regex pattern based on mode. -fn build_single_pattern(pattern: &str, opts: &GrepOptions) -> String { - let base = match opts.mode { - GrepMode::Fixed => regex::escape(pattern), - GrepMode::Basic => convert_bre_to_ere(pattern), - GrepMode::Extended => pattern.to_string(), - }; - - let wrapped = if opts.word_regexp { - format!(r"\b(?:{})\b", base) - } else if opts.line_regexp { - format!("^(?:{})$", base) - } else { - base - }; - - wrapped -} - -/// Convert POSIX Basic Regular Expression to Extended (Rust regex syntax). -/// In BRE: \(, \), \{, \}, \+, \?, \| are special; unescaped versions are literal. -/// In ERE (and Rust regex): (, ), {, }, +, ?, | are special without backslash. -fn convert_bre_to_ere(bre: &str) -> String { - let mut result = String::with_capacity(bre.len()); - let chars: Vec = bre.chars().collect(); - let mut i = 0; - - while i < chars.len() { - if chars[i] == '\\' && i + 1 < chars.len() { - match chars[i + 1] { - '(' => { - result.push('('); - i += 2; - } - ')' => { - result.push(')'); - i += 2; - } - '{' => { - result.push('{'); - i += 2; - } - '}' => { - result.push('}'); - i += 2; - } - '+' => { - result.push('+'); - i += 2; - } - '?' => { - result.push('?'); - i += 2; - } - '|' => { - result.push('|'); - i += 2; - } - '1'..='9' => { - // Backreference - not supported in Rust regex, pass through - result.push('\\'); - result.push(chars[i + 1]); - i += 2; - } - _ => { - result.push('\\'); - result.push(chars[i + 1]); - i += 2; - } - } - } else { - match chars[i] { - // In BRE, unescaped (, ), {, }, +, ? are literal - '(' => { - result.push_str("\\("); - i += 1; - } - ')' => { - result.push_str("\\)"); - i += 1; - } - '{' => { - result.push_str("\\{"); - i += 1; - } - '}' => { - result.push_str("\\}"); - i += 1; - } - _ => { - result.push(chars[i]); - i += 1; - } - } - } - } - - result -} - -/// Search a reader for matching lines. Returns true if any match was found. -fn search_reader( - reader: R, - filename: Option<&str>, - regex: &Regex, - opts: &GrepOptions, - show_filename: bool, -) -> io::Result { - let mut buf_reader = io::BufReader::new(reader); - let mut match_count: usize = 0; - let mut line_num: usize = 0; - let stdout = io::stdout(); - let mut out = stdout.lock(); - let mut line_buf = Vec::new(); - - while let Some(line) = read_line_bounded(&mut buf_reader, &mut line_buf)? { - line_num += 1; - - let is_match = regex.is_match(&line); - let is_match = if opts.invert_match { - !is_match - } else { - is_match - }; - - if is_match { - match_count += 1; - - if opts.quiet { - return Ok(true); - } - - if opts.files_with_matches { - if let Some(name) = filename { - writeln!(out, "{}", name)?; - } else { - writeln!(out, "(standard input)")?; - } - out.flush()?; - return Ok(true); - } - - if !opts.count_only && !opts.files_without_matches { - let prefix = match (show_filename, filename, opts.line_numbers) { - (true, Some(name), true) => format!("{}:{}:", name, line_num), - (true, Some(name), false) => format!("{}:", name), - (_, _, true) => format!("{}:", line_num), - _ => String::new(), - }; - writeln!(out, "{}{}", prefix, line)?; - } - - if let Some(max) = opts.max_count { - if match_count >= max { - break; - } - } - } - } - - if opts.count_only && !opts.quiet { - if show_filename { - if let Some(name) = filename { - writeln!(out, "{}:{}", name, match_count)?; - } else { - writeln!(out, "{}", match_count)?; - } - } else { - writeln!(out, "{}", match_count)?; - } - } - - if opts.files_without_matches && match_count == 0 { - if let Some(name) = filename { - writeln!(out, "{}", name)?; - } else { - writeln!(out, "(standard input)")?; - } - } - - out.flush()?; - - Ok(match_count > 0) -} - -fn read_line_bounded( - reader: &mut R, - line_buf: &mut Vec, -) -> io::Result> { - line_buf.clear(); - - loop { - let available = reader.fill_buf()?; - if available.is_empty() { - if line_buf.is_empty() { - return Ok(None); - } - break; - } - - let newline = available.iter().position(|&b| b == b'\n'); - let take = newline.map_or(available.len(), |pos| pos + 1); - let next_len = line_buf - .len() - .checked_add(take) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "input line too long"))?; - if next_len > MAX_INPUT_LINE_BYTES { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "input line exceeds size limit", - )); - } - - line_buf.extend_from_slice(&available[..take]); - reader.consume(take); - if newline.is_some() { - break; - } - } - - if line_buf.ends_with(b"\n") { - line_buf.pop(); - if line_buf.ends_with(b"\r") { - line_buf.pop(); - } - } - - String::from_utf8(line_buf.clone()) - .map(Some) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) -} diff --git a/registry/native/crates/libs/grep/src/rg_cmd.rs b/registry/native/crates/libs/grep/src/rg_cmd.rs deleted file mode 100644 index 9449f3c678..0000000000 --- a/registry/native/crates/libs/grep/src/rg_cmd.rs +++ /dev/null @@ -1,1142 +0,0 @@ -//! rg (ripgrep) implementation using the regex crate (ripgrep's pure Rust regex engine). -//! -//! Provides ripgrep-compatible search. Uses the same regex engine as ripgrep. -//! POSIX grep/egrep/fgrep remain in lib.rs for BRE/ERE/fixed string compatibility. - -use std::collections::{HashSet, VecDeque}; -use std::ffi::OsString; -use std::io::{self, BufRead, Read, Write}; -use std::path::{Path, PathBuf}; - -use regex::{Regex, RegexBuilder}; - -const MAX_CONTEXT_LINES: usize = 100_000; -const MAX_CONTEXT_BYTES: usize = 16 * 1024 * 1024; -const MAX_FILE_RESULTS: usize = 1_000_000; -const MAX_INPUT_LINE_BYTES: usize = 16 * 1024 * 1024; -const MAX_PATTERN_BYTES: usize = 16 * 1024 * 1024; -const MAX_PATTERNS: usize = 100_000; - -/// Entry point for rg command. -pub fn rg(args: Vec) -> i32 { - let str_args: Vec = args - .iter() - .skip(1) - .map(|a| a.to_string_lossy().to_string()) - .collect(); - - match run(&str_args) { - Ok(code) => code, - Err(msg) => { - eprintln!("rg: {}", msg); - 2 - } - } -} - -struct Options { - patterns: Vec, - paths: Vec, - files_mode: bool, - ignore_case: bool, - smart_case: bool, - invert_match: bool, - count_only: bool, - files_with_matches: bool, - files_without_matches: bool, - line_numbers: Option, - word_regexp: bool, - line_regexp: bool, - fixed_strings: bool, - max_count: Option, - quiet: bool, - only_matching: bool, - after_context: usize, - before_context: usize, - show_filename: Option, - hidden: bool, - max_depth: Option, - sort_modified: bool, - glob_patterns: Vec, - pattern_bytes: usize, - type_include: Vec, - type_exclude: Vec, -} - -impl Options { - fn new() -> Self { - Self { - patterns: Vec::new(), - paths: Vec::new(), - files_mode: false, - ignore_case: false, - smart_case: true, - invert_match: false, - count_only: false, - files_with_matches: false, - files_without_matches: false, - line_numbers: None, - word_regexp: false, - line_regexp: false, - fixed_strings: false, - max_count: None, - quiet: false, - only_matching: false, - after_context: 0, - before_context: 0, - show_filename: None, - hidden: false, - max_depth: None, - sort_modified: false, - glob_patterns: Vec::new(), - pattern_bytes: 0, - type_include: Vec::new(), - type_exclude: Vec::new(), - } - } - - fn show_line_numbers(&self) -> bool { - self.line_numbers.unwrap_or(true) - } - - fn resolve_show_filename(&self, multi: bool) -> bool { - self.show_filename.unwrap_or(multi) - } - - fn has_context(&self) -> bool { - self.before_context > 0 || self.after_context > 0 - } -} - -fn run(args: &[String]) -> Result { - if args.len() == 1 && (args[0] == "--version" || args[0] == "-V") { - let stdout = io::stdout(); - let mut out = stdout.lock(); - writeln!(out, "ripgrep 14.1.0 (secure-exec)").map_err(|e| e.to_string())?; - out.flush().map_err(|e| e.to_string())?; - return Ok(0); - } - - let opts = parse_args(args)?; - - if opts.files_mode { - let paths = if opts.paths.is_empty() { - vec![".".to_string()] - } else { - opts.paths.clone() - }; - - let files = collect_files_from_paths(&paths, &opts).map_err(|e| e.to_string())?; - let stdout = io::stdout(); - let mut out = stdout.lock(); - for path in files { - writeln!(out, "{}", path.to_string_lossy()).map_err(|e| e.to_string())?; - } - out.flush().map_err(|e| e.to_string())?; - return Ok(0); - } - - if opts.patterns.is_empty() { - return Err("no pattern provided".to_string()); - } - - let regex = build_regex(&opts)?; - - if opts.paths.is_empty() { - // No paths: read from stdin - let stdin = io::stdin(); - let stdout = io::stdout(); - let mut out = stdout.lock(); - let result = search_stream(stdin.lock(), ®ex, &opts, None, false, &mut out) - .map_err(|e| e.to_string())?; - if opts.quiet { - return Ok(if result.matches > 0 { 0 } else { 1 }); - } - print_file_result(None, &result, &opts, &mut out).map_err(|e| e.to_string())?; - out.flush().map_err(|e| e.to_string())?; - return Ok(if result.matches > 0 { 0 } else { 1 }); - } - - let files = collect_files_from_paths(&opts.paths, &opts).map_err(|e| e.to_string())?; - let multi = files.len() > 1; - let show_fn = opts.resolve_show_filename(multi); - let mut any_match = false; - let mut had_error = false; - let stdout = io::stdout(); - let mut out = stdout.lock(); - - for path in &files { - match std::fs::File::open(path) { - Ok(f) => { - let reader = io::BufReader::new(f); - let fname = if show_fn { - Some(path.to_string_lossy().to_string()) - } else { - None - }; - let result = - match search_stream(reader, ®ex, &opts, fname.as_deref(), show_fn, &mut out) - { - Ok(result) => result, - Err(e) => { - eprintln!("rg: {}: {}", path.display(), e); - had_error = true; - continue; - } - }; - if result.matches > 0 { - any_match = true; - } - if opts.quiet && any_match { - return Ok(0); - } - if !opts.quiet { - print_file_result(fname.as_deref(), &result, &opts, &mut out) - .map_err(|e| e.to_string())?; - } - } - Err(e) => { - eprintln!("rg: {}: {}", path.display(), e); - had_error = true; - } - } - } - out.flush().map_err(|e| e.to_string())?; - - if had_error { - Ok(2) - } else if any_match { - Ok(0) - } else { - Ok(1) - } -} - -// --- Argument parsing --- - -fn parse_args(args: &[String]) -> Result { - let mut opts = Options::new(); - let mut i = 0; - let mut explicit_pattern = false; - - while i < args.len() { - let arg = &args[i]; - - if arg == "--" { - i += 1; - break; - } - - // Long options - if arg.starts_with("--") { - match arg.as_str() { - "--ignore-case" => opts.ignore_case = true, - "--case-sensitive" => { - opts.ignore_case = false; - opts.smart_case = false; - } - "--smart-case" => opts.smart_case = true, - "--invert-match" => opts.invert_match = true, - "--count" => opts.count_only = true, - "--files" => opts.files_mode = true, - "--files-with-matches" => opts.files_with_matches = true, - "--files-without-match" => opts.files_without_matches = true, - "--line-number" => opts.line_numbers = Some(true), - "--no-line-number" => opts.line_numbers = Some(false), - "--word-regexp" => opts.word_regexp = true, - "--line-regexp" => opts.line_regexp = true, - "--fixed-strings" => opts.fixed_strings = true, - "--quiet" | "--silent" => opts.quiet = true, - "--only-matching" => opts.only_matching = true, - "--hidden" | "--no-ignore" => opts.hidden = true, - "--follow" | "--no-ignore-vcs" | "--no-ignore-parent" => {} - "--with-filename" => opts.show_filename = Some(true), - "--no-filename" => opts.show_filename = Some(false), - "--no-heading" | "--heading" => {} // no-op (we always use inline format) - "--color=auto" | "--color=always" | "--color=never" => {} // no-op in WASI - "--no-color" => {} - _ if arg.starts_with("--color=") => {} - _ if arg.starts_with("--max-depth=") => { - opts.max_depth = Some( - arg[12..] - .parse() - .map_err(|_| format!("invalid number: '{}'", &arg[12..]))?, - ); - } - _ if arg.starts_with("--sort=") => match &arg[7..] { - "modified" => opts.sort_modified = true, - value => return Err(format!("unsupported sort: '{}'", value)), - }, - _ if arg.starts_with("--threads=") => {} - _ if arg.starts_with("--regexp=") => { - push_pattern(&mut opts, arg[9..].to_string())?; - explicit_pattern = true; - } - _ if arg.starts_with("--max-count=") => { - opts.max_count = Some( - arg[12..] - .parse() - .map_err(|_| format!("invalid number: '{}'", &arg[12..]))?, - ); - } - _ if arg.starts_with("--after-context=") => { - opts.after_context = parse_context_count(&arg[16..])?; - } - _ if arg.starts_with("--before-context=") => { - opts.before_context = parse_context_count(&arg[17..])?; - } - _ if arg.starts_with("--context=") => { - let n = parse_context_count(&arg[10..])?; - opts.before_context = n; - opts.after_context = n; - } - _ if arg.starts_with("--glob=") => { - opts.glob_patterns.push(arg[7..].to_string()); - } - _ if arg.starts_with("--type=") => { - opts.type_include.push(arg[7..].to_string()); - } - _ if arg.starts_with("--type-not=") => { - opts.type_exclude.push(arg[11..].to_string()); - } - "--regexp" | "--max-count" | "--after-context" | "--before-context" - | "--context" | "--glob" | "--type" | "--type-not" | "--file" | "--color" - | "--max-depth" | "--threads" => { - i += 1; - if i >= args.len() { - return Err(format!("{} requires an argument", arg)); - } - match arg.as_str() { - "--regexp" => { - push_pattern(&mut opts, args[i].clone())?; - explicit_pattern = true; - } - "--max-count" => { - opts.max_count = Some( - args[i] - .parse() - .map_err(|_| format!("invalid number: '{}'", args[i]))?, - ); - } - "--after-context" => { - opts.after_context = parse_context_count(&args[i])?; - } - "--before-context" => { - opts.before_context = parse_context_count(&args[i])?; - } - "--context" => { - let n = parse_context_count(&args[i])?; - opts.before_context = n; - opts.after_context = n; - } - "--glob" => opts.glob_patterns.push(args[i].clone()), - "--type" => opts.type_include.push(args[i].clone()), - "--type-not" => opts.type_exclude.push(args[i].clone()), - "--file" => { - read_patterns_from_file(&mut opts, &args[i])?; - explicit_pattern = true; - } - "--color" => {} // no-op - "--max-depth" => { - opts.max_depth = Some( - args[i] - .parse() - .map_err(|_| format!("invalid number: '{}'", args[i]))?, - ); - } - "--threads" => {} // no-op - _ => unreachable!(), - } - } - _ => return Err(format!("unrecognized option '{}'", arg)), - } - i += 1; - continue; - } - - // Short options - if arg.starts_with('-') && arg.len() > 1 { - let chars: Vec = arg[1..].chars().collect(); - let mut j = 0; - while j < chars.len() { - match chars[j] { - 'i' => opts.ignore_case = true, - 's' => { - opts.ignore_case = false; - opts.smart_case = false; - } - 'S' => opts.smart_case = true, - 'v' => opts.invert_match = true, - 'c' => opts.count_only = true, - 'l' => opts.files_with_matches = true, - 'n' => opts.line_numbers = Some(true), - 'N' => opts.line_numbers = Some(false), - 'w' => opts.word_regexp = true, - 'x' => opts.line_regexp = true, - 'F' => opts.fixed_strings = true, - 'q' => opts.quiet = true, - 'o' => opts.only_matching = true, - 'H' => opts.show_filename = Some(true), - '.' => opts.hidden = true, - 'e' => { - let rest: String = chars[j + 1..].iter().collect(); - if !rest.is_empty() { - push_pattern(&mut opts, rest)?; - } else { - i += 1; - if i >= args.len() { - return Err("option requires an argument -- 'e'".to_string()); - } - push_pattern(&mut opts, args[i].clone())?; - } - explicit_pattern = true; - j = chars.len(); - continue; - } - 'f' => { - i += 1; - if i >= args.len() { - return Err("option requires an argument -- 'f'".to_string()); - } - read_patterns_from_file(&mut opts, &args[i])?; - explicit_pattern = true; - j = chars.len(); - continue; - } - 'm' => { - i += 1; - if i >= args.len() { - return Err("option requires an argument -- 'm'".to_string()); - } - opts.max_count = Some( - args[i] - .parse() - .map_err(|_| format!("invalid number: '{}'", args[i]))?, - ); - j = chars.len(); - continue; - } - 'j' => { - i += 1; - if i >= args.len() { - return Err("option requires an argument -- 'j'".to_string()); - } - j = chars.len(); - continue; - } - 'A' => { - i += 1; - if i >= args.len() { - return Err("option requires an argument -- 'A'".to_string()); - } - opts.after_context = parse_context_count(&args[i])?; - j = chars.len(); - continue; - } - 'B' => { - i += 1; - if i >= args.len() { - return Err("option requires an argument -- 'B'".to_string()); - } - opts.before_context = parse_context_count(&args[i])?; - j = chars.len(); - continue; - } - 'C' => { - i += 1; - if i >= args.len() { - return Err("option requires an argument -- 'C'".to_string()); - } - let n = parse_context_count(&args[i])?; - opts.before_context = n; - opts.after_context = n; - j = chars.len(); - continue; - } - 'g' => { - i += 1; - if i >= args.len() { - return Err("option requires an argument -- 'g'".to_string()); - } - opts.glob_patterns.push(args[i].clone()); - j = chars.len(); - continue; - } - 't' => { - i += 1; - if i >= args.len() { - return Err("option requires an argument -- 't'".to_string()); - } - opts.type_include.push(args[i].clone()); - j = chars.len(); - continue; - } - 'T' => { - i += 1; - if i >= args.len() { - return Err("option requires an argument -- 'T'".to_string()); - } - opts.type_exclude.push(args[i].clone()); - j = chars.len(); - continue; - } - _ => return Err(format!("invalid option -- '{}'", chars[j])), - } - j += 1; - } - i += 1; - continue; - } - - // Positional argument - if opts.files_mode { - opts.paths.push(arg.clone()); - } else if !explicit_pattern && opts.patterns.is_empty() { - push_pattern(&mut opts, arg.clone())?; - explicit_pattern = true; - } else { - opts.paths.push(arg.clone()); - } - i += 1; - } - - // Remaining args after -- - while i < args.len() { - if opts.files_mode { - opts.paths.push(args[i].clone()); - } else if !explicit_pattern && opts.patterns.is_empty() { - push_pattern(&mut opts, args[i].clone())?; - explicit_pattern = true; - } else { - opts.paths.push(args[i].clone()); - } - i += 1; - } - - Ok(opts) -} - -fn parse_context_count(value: &str) -> Result { - let count: usize = value - .parse() - .map_err(|_| format!("invalid number: '{}'", value))?; - if count > MAX_CONTEXT_LINES { - return Err(format!("context count '{}' exceeds size limit", value)); - } - Ok(count) -} - -fn push_pattern(opts: &mut Options, pattern: String) -> Result<(), String> { - if opts.patterns.len() >= MAX_PATTERNS { - return Err("too many patterns".to_string()); - } - let next_bytes = opts - .pattern_bytes - .checked_add(pattern.len()) - .ok_or_else(|| "pattern data too large".to_string())?; - if next_bytes > MAX_PATTERN_BYTES { - return Err("pattern data exceeds size limit".to_string()); - } - opts.pattern_bytes = next_bytes; - opts.patterns.push(pattern); - Ok(()) -} - -fn read_patterns_from_file(opts: &mut Options, path: &str) -> Result<(), String> { - let metadata = std::fs::metadata(path).map_err(|e| format!("{}: {}", path, e))?; - if metadata.len() > MAX_PATTERN_BYTES as u64 { - return Err(format!("{}: pattern file exceeds size limit", path)); - } - let file = std::fs::File::open(path).map_err(|e| format!("{}: {}", path, e))?; - let limit = MAX_PATTERN_BYTES - .checked_add(1) - .ok_or_else(|| "pattern file size limit is too large".to_string())?; - let mut content = String::new(); - file.take(limit as u64) - .read_to_string(&mut content) - .map_err(|e| format!("{}: {}", path, e))?; - if content.len() > MAX_PATTERN_BYTES { - return Err(format!("{}: pattern file exceeds size limit", path)); - } - for line in content.lines() { - if !line.is_empty() { - push_pattern(opts, line.to_string())?; - } - } - Ok(()) -} - -// --- Pattern building --- - -fn build_regex(opts: &Options) -> Result { - let combined = if opts.patterns.len() == 1 { - prepare_pattern(&opts.patterns[0], opts) - } else { - let parts: Vec = opts - .patterns - .iter() - .map(|p| format!("(?:{})", prepare_pattern(p, opts))) - .collect(); - parts.join("|") - }; - - let case_insensitive = if opts.ignore_case { - true - } else if opts.smart_case { - // Smart case: insensitive unless pattern has uppercase - !combined.chars().any(|c| c.is_uppercase()) - } else { - false - }; - - RegexBuilder::new(&combined) - .case_insensitive(case_insensitive) - .build() - .map_err(|e| format!("regex error: {}", e)) -} - -fn prepare_pattern(pattern: &str, opts: &Options) -> String { - let base = if opts.fixed_strings { - regex::escape(pattern) - } else { - pattern.to_string() - }; - - if opts.word_regexp { - format!(r"\b(?:{})\b", base) - } else if opts.line_regexp { - format!("^(?:{})$", base) - } else { - base - } -} - -// --- File collection --- - -fn collect_files_from_paths(paths: &[String], opts: &Options) -> io::Result> { - let mut files = Vec::new(); - for path_str in paths { - let path = Path::new(path_str); - let metadata = std::fs::symlink_metadata(path)?; - let file_type = metadata.file_type(); - if file_type.is_dir() { - let mut active_dirs = HashSet::new(); - walk_dir(path, path, opts, &mut files, 0, &mut active_dirs)?; - } else if file_type.is_file() { - if should_include(path, path, false, opts) { - push_collected_file(&mut files, path.to_path_buf())?; - } - } else { - continue; - } - } - if opts.sort_modified { - files.sort_by_key(|path| { - std::fs::metadata(path) - .and_then(|meta| meta.modified()) - .ok() - }); - } else { - files.sort(); - } - Ok(files) -} - -fn push_collected_file(out: &mut Vec, path: PathBuf) -> io::Result<()> { - if out.len() >= MAX_FILE_RESULTS { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "file result count exceeds size limit", - )); - } - out.push(path); - Ok(()) -} - -fn walk_dir( - root: &Path, - dir: &Path, - opts: &Options, - out: &mut Vec, - depth: usize, - active_dirs: &mut HashSet, -) -> io::Result<()> { - let canonical = std::fs::canonicalize(dir)?; - if !active_dirs.insert(canonical.clone()) { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("recursive directory cycle at {}", dir.display()), - )); - } - - let result = (|| { - for entry in std::fs::read_dir(dir)? { - let entry = entry?; - let path = entry.path(); - let name = entry.file_name(); - let name_str = name.to_string_lossy(); - let file_type = entry.file_type()?; - let is_dir = file_type.is_dir(); - - // Skip hidden files/dirs unless --hidden - if !opts.hidden && name_str.starts_with('.') { - continue; - } - - if !should_include(root, &path, is_dir, opts) { - continue; - } - - if is_dir { - if opts.max_depth.map(|max| depth < max).unwrap_or(true) { - walk_dir(root, &path, opts, out, depth + 1, active_dirs)?; - } - } else if file_type.is_file() { - push_collected_file(out, path)?; - } - } - Ok(()) - })(); - - active_dirs.remove(&canonical); - result -} - -fn should_include(root: &Path, path: &Path, is_dir: bool, opts: &Options) -> bool { - let ext = path - .extension() - .map(|e| e.to_string_lossy().to_string()) - .unwrap_or_default(); - let relative_path = path - .strip_prefix(root) - .unwrap_or(path) - .to_string_lossy() - .replace('\\', "/"); - let file_name = path - .file_name() - .map(|n| n.to_string_lossy().to_string()) - .unwrap_or_default(); - - // Type include filters - if !opts.type_include.is_empty() { - let included = opts.type_include.iter().any(|t| { - type_extensions(t) - .map(|exts| exts.iter().any(|e| ext == *e)) - .unwrap_or(false) - }); - if !included { - return false; - } - } - - // Type exclude filters - if !opts.type_exclude.is_empty() { - let excluded = opts.type_exclude.iter().any(|t| { - type_extensions(t) - .map(|exts| exts.iter().any(|e| ext == *e)) - .unwrap_or(false) - }); - if excluded { - return false; - } - } - - // Glob filters - if !opts.glob_patterns.is_empty() { - for pattern in &opts.glob_patterns { - let (negated, pat) = if let Some(rest) = pattern.strip_prefix('!') { - (true, rest) - } else { - (false, pattern.as_str()) - }; - let matches = glob_matches(pat, &relative_path, &file_name, is_dir); - if negated && matches { - return false; - } - if !negated && !matches { - return false; - } - } - } - - true -} - -fn type_extensions(type_name: &str) -> Option<&'static [&'static str]> { - match type_name { - "rust" | "rs" => Some(&["rs"]), - "py" | "python" => Some(&["py", "pyi"]), - "js" | "javascript" => Some(&["js", "jsx", "mjs"]), - "ts" | "typescript" => Some(&["ts", "tsx", "mts"]), - "c" => Some(&["c", "h"]), - "cpp" | "c++" => Some(&["cpp", "cxx", "cc", "hpp", "hxx", "h"]), - "java" => Some(&["java"]), - "go" => Some(&["go"]), - "html" => Some(&["html", "htm"]), - "css" => Some(&["css"]), - "json" => Some(&["json"]), - "yaml" | "yml" => Some(&["yml", "yaml"]), - "toml" => Some(&["toml"]), - "md" | "markdown" => Some(&["md", "markdown"]), - "txt" | "text" => Some(&["txt"]), - "sh" | "shell" | "bash" => Some(&["sh", "bash"]), - "xml" => Some(&["xml"]), - "sql" => Some(&["sql"]), - "lua" => Some(&["lua"]), - "ruby" | "rb" => Some(&["rb"]), - "php" => Some(&["php"]), - "swift" => Some(&["swift"]), - "kotlin" | "kt" => Some(&["kt", "kts"]), - _ => None, - } -} - -fn glob_matches(pattern: &str, relative_path: &str, file_name: &str, is_dir: bool) -> bool { - let normalized = pattern.trim_end_matches('/'); - if pattern.ends_with('/') { - return is_dir - && relative_path - .split('/') - .any(|segment| segment == normalized || segment == file_name); - } - - let target = if pattern.contains('/') { - relative_path - } else { - file_name - }; - - let mut regex_pattern = String::from("^"); - let chars: Vec = normalized.chars().collect(); - let mut i = 0; - while i < chars.len() { - match chars[i] { - '*' => { - if i + 1 < chars.len() && chars[i + 1] == '*' { - regex_pattern.push_str(".*"); - i += 2; - } else { - regex_pattern.push_str("[^/]*"); - i += 1; - } - } - '?' => { - regex_pattern.push_str("[^/]"); - i += 1; - } - '{' => { - if let Some(end) = chars[i + 1..].iter().position(|c| *c == '}') { - let group: String = chars[i + 1..i + 1 + end].iter().collect(); - regex_pattern.push('('); - regex_pattern.push_str( - &group - .split(',') - .map(regex::escape) - .collect::>() - .join("|"), - ); - regex_pattern.push(')'); - i += end + 2; - } else { - regex_pattern.push_str("\\{"); - i += 1; - } - } - '.' | '+' | '(' | ')' | '|' | '^' | '$' | '[' | ']' | '\\' => { - regex_pattern.push('\\'); - regex_pattern.push(chars[i]); - i += 1; - } - other => { - regex_pattern.push(other); - i += 1; - } - } - } - regex_pattern.push('$'); - - Regex::new(®ex_pattern) - .map(|regex| regex.is_match(target)) - .unwrap_or(false) -} - -// --- Search --- - -struct FileResult { - matches: usize, - is_binary: bool, -} - -enum ResultLine { - Match(usize, String), - Context(usize, String), - Separator, -} - -fn search_stream( - mut reader: R, - regex: &Regex, - opts: &Options, - filename: Option<&str>, - show_filename: bool, - out: &mut W, -) -> io::Result { - let mut result = FileResult { - matches: 0, - is_binary: false, - }; - - let collect_lines = - !opts.quiet && !opts.files_with_matches && !opts.files_without_matches && !opts.count_only; - - let mut before_buf: VecDeque<(usize, String)> = VecDeque::new(); - let mut before_buf_bytes: usize = 0; - let mut after_remaining: usize = 0; - let mut last_printed: usize = 0; - let mut line_buf = Vec::new(); - let mut lineno: usize = 0; - - while let Some(line) = read_line_bounded(&mut reader, &mut line_buf)? { - lineno = lineno - .checked_add(1) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "line number overflow"))?; - - // Binary detection: null bytes in line data - if line.as_bytes().contains(&0) { - result.is_binary = true; - break; - } - - let is_match = regex.is_match(&line) != opts.invert_match; - - if is_match { - result.matches += 1; - - if opts.quiet || opts.files_with_matches { - break; - } - - if collect_lines { - // Separator for non-contiguous match groups - if opts.has_context() && last_printed > 0 { - let first_before = before_buf.front().map(|(n, _)| *n).unwrap_or(lineno); - if first_before > last_printed + 1 { - print_result_line( - out, - filename, - opts, - show_filename, - &ResultLine::Separator, - )?; - } - } - - // Flush before-context buffer - for (bno, btext) in before_buf.drain(..) { - if bno > last_printed { - print_result_line( - out, - filename, - opts, - show_filename, - &ResultLine::Context(bno, btext), - )?; - last_printed = bno; - } - } - before_buf_bytes = 0; - - // Emit match - if opts.only_matching && !opts.invert_match { - for mat in regex.find_iter(&line) { - print_result_line( - out, - filename, - opts, - show_filename, - &ResultLine::Match(lineno, mat.as_str().to_string()), - )?; - } - } else { - print_result_line( - out, - filename, - opts, - show_filename, - &ResultLine::Match(lineno, line), - )?; - } - last_printed = lineno; - after_remaining = opts.after_context; - } - - if let Some(max) = opts.max_count { - if result.matches >= max { - break; - } - } - } else if collect_lines { - if after_remaining > 0 { - print_result_line( - out, - filename, - opts, - show_filename, - &ResultLine::Context(lineno, line), - )?; - last_printed = lineno; - after_remaining -= 1; - } else if opts.before_context > 0 { - before_buf_bytes = before_buf_bytes.checked_add(line.len()).ok_or_else(|| { - io::Error::new(io::ErrorKind::InvalidData, "context buffer too large") - })?; - if before_buf_bytes > MAX_CONTEXT_BYTES { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "context buffer exceeds size limit", - )); - } - before_buf.push_back((lineno, line)); - if before_buf.len() > opts.before_context { - if let Some((_, removed)) = before_buf.pop_front() { - before_buf_bytes = before_buf_bytes.saturating_sub(removed.len()); - } - } - } - } - } - - Ok(result) -} - -// --- Output --- - -fn print_file_result( - filename: Option<&str>, - result: &FileResult, - opts: &Options, - out: &mut W, -) -> io::Result<()> { - if result.is_binary { - if result.matches > 0 { - if let Some(name) = filename { - writeln!(out, "Binary file {} matches.", name)?; - } - } - return Ok(()); - } - - if opts.files_with_matches { - if result.matches > 0 { - let name = filename.unwrap_or("(standard input)"); - writeln!(out, "{}", name)?; - } - return Ok(()); - } - - if opts.files_without_matches { - if result.matches == 0 { - let name = filename.unwrap_or("(standard input)"); - writeln!(out, "{}", name)?; - } - return Ok(()); - } - - if opts.count_only { - if let Some(name) = filename { - writeln!(out, "{}:{}", name, result.matches)?; - } else { - writeln!(out, "{}", result.matches)?; - } - return Ok(()); - } - - Ok(()) -} - -fn print_result_line( - out: &mut W, - filename: Option<&str>, - opts: &Options, - show_filename: bool, - line: &ResultLine, -) -> io::Result<()> { - match line { - ResultLine::Match(lineno, text) => { - let mut prefix = String::new(); - if show_filename { - if let Some(name) = filename { - prefix.push_str(name); - prefix.push(':'); - } - } - if opts.show_line_numbers() { - prefix.push_str(&lineno.to_string()); - prefix.push(':'); - } - writeln!(out, "{}{}", prefix, text) - } - ResultLine::Context(lineno, text) => { - let mut prefix = String::new(); - if show_filename { - if let Some(name) = filename { - prefix.push_str(name); - prefix.push('-'); - } - } - if opts.show_line_numbers() { - prefix.push_str(&lineno.to_string()); - prefix.push('-'); - } - writeln!(out, "{}{}", prefix, text) - } - ResultLine::Separator => writeln!(out, "--"), - } -} - -fn read_line_bounded( - reader: &mut R, - line_buf: &mut Vec, -) -> io::Result> { - line_buf.clear(); - - loop { - let available = reader.fill_buf()?; - if available.is_empty() { - if line_buf.is_empty() { - return Ok(None); - } - break; - } - - let newline = available.iter().position(|&b| b == b'\n'); - let take = newline.map_or(available.len(), |pos| pos + 1); - let next_len = line_buf - .len() - .checked_add(take) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "input line too long"))?; - if next_len > MAX_INPUT_LINE_BYTES { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "input line exceeds size limit", - )); - } - - line_buf.extend_from_slice(&available[..take]); - reader.consume(take); - if newline.is_some() { - break; - } - } - - if line_buf.ends_with(b"\n") { - line_buf.pop(); - if line_buf.ends_with(b"\r") { - line_buf.pop(); - } - } - - String::from_utf8(line_buf.clone()) - .map(Some) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) -} diff --git a/registry/native/crates/libs/gzip/Cargo.toml b/registry/native/crates/libs/gzip/Cargo.toml deleted file mode 100644 index eed27c5c29..0000000000 --- a/registry/native/crates/libs/gzip/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[package] -name = "secureexec-gzip" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "gzip/gunzip/zcat implementations for secure-exec standalone binaries" - -[dependencies] -flate2 = { version = "1.0", default-features = false, features = ["rust_backend"] } diff --git a/registry/native/crates/libs/jq/Cargo.toml b/registry/native/crates/libs/jq/Cargo.toml deleted file mode 100644 index a258274ae3..0000000000 --- a/registry/native/crates/libs/jq/Cargo.toml +++ /dev/null @@ -1,12 +0,0 @@ -[package] -name = "secureexec-jq" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "jq implementation for secure-exec standalone binaries" - -[dependencies] -jaq-core = "2.2" -jaq-std = "2.1" -jaq-json = { version = "1.1", features = ["serde_json"] } -serde_json = "1" diff --git a/registry/native/crates/libs/jq/src/lib.rs b/registry/native/crates/libs/jq/src/lib.rs deleted file mode 100644 index 9fe36c8542..0000000000 --- a/registry/native/crates/libs/jq/src/lib.rs +++ /dev/null @@ -1,293 +0,0 @@ -//! jq implementation using the jaq crate (pure Rust, jq-compatible). -//! -//! Wraps jaq-core/jaq-std/jaq-json to provide a standard jq CLI interface. - -use std::ffi::OsString; -use std::io::{self, Read, Write}; - -use jaq_core::load::{Arena, File, Loader}; -use jaq_core::{Compiler, Ctx, RcIter}; -use jaq_json::Val; - -const MAX_INPUT_BYTES: usize = 16 * 1024 * 1024; -const MAX_INPUT_VALUES: usize = 100_000; - -/// Entry point for jq command. -pub fn main(args: Vec) -> i32 { - let str_args: Vec = args - .iter() - .skip(1) // skip argv[0] - .map(|a| a.to_string_lossy().to_string()) - .collect(); - - match run_jq(&str_args) { - Ok(code) => code, - Err(msg) => { - eprintln!("jq: {}", msg); - 2 - } - } -} - -struct JqOptions { - filter: String, - raw_output: bool, - raw_input: bool, - slurp: bool, - compact: bool, - null_input: bool, - exit_status: bool, - join_output: bool, - args: Vec<(String, String)>, - jsonargs: Vec<(String, Val)>, -} - -fn parse_args(args: &[String]) -> Result { - let mut opts = JqOptions { - filter: String::new(), - raw_output: false, - raw_input: false, - slurp: false, - compact: false, - null_input: false, - exit_status: false, - join_output: false, - args: Vec::new(), - jsonargs: Vec::new(), - }; - - let mut filter_set = false; - let mut i = 0; - - while i < args.len() { - let arg = &args[i]; - - if arg == "--" { - break; - } - - if arg.starts_with('-') && arg.len() > 1 && !arg.starts_with("--") { - // Handle combined short flags like -rn, -crS, etc. - let flags = &arg[1..]; - for c in flags.chars() { - match c { - 'r' => opts.raw_output = true, - 'R' => opts.raw_input = true, - 's' => opts.slurp = true, - 'c' => opts.compact = true, - 'n' => opts.null_input = true, - 'e' => opts.exit_status = true, - 'j' => opts.join_output = true, - 'S' => {} // sort keys - jaq sorts by default - _ => return Err(format!("unknown option: -{}", c)), - } - } - } else if arg == "--raw-output" || arg == "--raw-output0" { - opts.raw_output = true; - } else if arg == "--raw-input" { - opts.raw_input = true; - } else if arg == "--slurp" { - opts.slurp = true; - } else if arg == "--compact-output" { - opts.compact = true; - } else if arg == "--null-input" { - opts.null_input = true; - } else if arg == "--exit-status" { - opts.exit_status = true; - } else if arg == "--join-output" { - opts.join_output = true; - } else if arg == "--arg" { - if i + 2 >= args.len() { - return Err("--arg requires name and value".to_string()); - } - let name = args[i + 1].clone(); - let value = args[i + 2].clone(); - opts.args.push((name, value)); - i += 2; - } else if arg == "--argjson" { - if i + 2 >= args.len() { - return Err("--argjson requires name and value".to_string()); - } - let name = args[i + 1].clone(); - let json_str = &args[i + 2]; - let value: serde_json::Value = serde_json::from_str(json_str) - .map_err(|e| format!("invalid JSON for --argjson: {}", e))?; - opts.jsonargs.push((name, Val::from(value))); - i += 2; - } else if !filter_set { - opts.filter = arg.clone(); - filter_set = true; - } else { - return Err(format!("unexpected argument: {}", arg)); - } - - i += 1; - } - - if !filter_set { - return Err("no filter provided".to_string()); - } - - Ok(opts) -} - -fn read_inputs(opts: &JqOptions) -> Result, String> { - if opts.null_input { - return Ok(vec![Val::from(serde_json::Value::Null)]); - } - - let mut stdin_data = String::new(); - io::stdin() - .take((MAX_INPUT_BYTES + 1) as u64) - .read_to_string(&mut stdin_data) - .map_err(|e| format!("failed to read stdin: {}", e))?; - if stdin_data.len() > MAX_INPUT_BYTES { - return Err("stdin exceeds size limit".to_string()); - } - - if opts.raw_input { - if opts.slurp { - let mut arr = Vec::new(); - for line in stdin_data.lines() { - push_input_value(&mut arr, serde_json::Value::String(line.to_string()))?; - } - Ok(vec![Val::from(serde_json::Value::Array(arr))]) - } else { - let mut lines = Vec::new(); - for line in stdin_data.lines() { - push_input_value( - &mut lines, - Val::from(serde_json::Value::String(line.to_string())), - )?; - } - Ok(lines) - } - } else { - let trimmed = stdin_data.trim(); - if trimmed.is_empty() { - return Ok(vec![Val::from(serde_json::Value::Null)]); - } - - let mut values = Vec::new(); - let decoder = serde_json::Deserializer::from_str(trimmed).into_iter::(); - for result in decoder { - let value = result.map_err(|e| format!("invalid JSON input: {}", e))?; - push_input_value(&mut values, value)?; - } - - if opts.slurp { - Ok(vec![Val::from(serde_json::Value::Array(values))]) - } else { - Ok(values.into_iter().map(Val::from).collect()) - } - } -} - -fn push_input_value(values: &mut Vec, value: T) -> Result<(), String> { - if values.len() >= MAX_INPUT_VALUES { - return Err("too many input values".to_string()); - } - values.push(value); - Ok(()) -} - -/// Format a jaq Val as a string for output. -fn format_output(val: &Val, opts: &JqOptions) -> Result { - let compact_str = format!("{}", val); - - // For raw output, unquote strings - if opts.raw_output { - if compact_str.starts_with('"') && compact_str.ends_with('"') && compact_str.len() >= 2 { - if let Ok(unescaped) = serde_json::from_str::(&compact_str) { - return Ok(unescaped); - } - } - } - - if opts.compact { - Ok(compact_str) - } else { - // Pretty print via serde_json - if let Ok(v) = serde_json::from_str::(&compact_str) { - serde_json::to_string_pretty(&v).map_err(|e| format!("output format error: {}", e)) - } else { - Ok(compact_str) - } - } -} - -fn run_jq(args: &[String]) -> Result { - let opts = parse_args(args)?; - let inputs = read_inputs(&opts)?; - - // Set up variable bindings for --arg and --argjson - let mut var_vals: Vec = Vec::new(); - - for (_name, value) in &opts.args { - var_vals.push(Val::from(serde_json::Value::String(value.clone()))); - } - for (_name, value) in &opts.jsonargs { - var_vals.push(value.clone()); - } - - // Load and compile filter - let loader = Loader::new(jaq_std::defs().chain(jaq_json::defs())); - let arena = Arena::default(); - let program = File { - code: opts.filter.as_str(), - path: (), - }; - let modules = loader - .load(&arena, program) - .map_err(|errs| format!("parse error: {:?}", errs))?; - - let filter = Compiler::default() - .with_funs(jaq_std::funs().chain(jaq_json::funs())) - .compile(modules) - .map_err(|errs| format!("compile error: {:?}", errs))?; - - let empty_inputs = RcIter::new(core::iter::empty()); - let stdout = io::stdout(); - let mut out = stdout.lock(); - let mut had_false_or_null = false; - - for input in inputs { - let ctx = Ctx::new(var_vals.iter().cloned(), &empty_inputs); - let results = filter.run((ctx, input)); - - for result in results { - match result { - Ok(val) => { - let s = format_output(&val, &opts)?; - - // Track for --exit-status - let compact = format!("{}", val); - if compact == "null" || compact == "false" { - had_false_or_null = true; - } - - if opts.join_output { - write!(out, "{}", s) - .map_err(|e| format!("failed to write stdout: {}", e))?; - } else { - writeln!(out, "{}", s) - .map_err(|e| format!("failed to write stdout: {}", e))?; - } - } - Err(e) => { - eprintln!("jq: error: {}", e); - return Ok(5); - } - } - } - } - - if opts.exit_status && had_false_or_null { - return Ok(1); - } - - out.flush() - .map_err(|e| format!("failed to flush stdout: {}", e))?; - - Ok(0) -} diff --git a/registry/native/crates/libs/rev/Cargo.toml b/registry/native/crates/libs/rev/Cargo.toml deleted file mode 100644 index dda6c9d74d..0000000000 --- a/registry/native/crates/libs/rev/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "secureexec-rev" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "rev implementation for secure-exec standalone binaries" - -[dependencies] diff --git a/registry/native/crates/libs/shims/src/lib.rs b/registry/native/crates/libs/shims/src/lib.rs deleted file mode 100644 index 9e2358b3e8..0000000000 --- a/registry/native/crates/libs/shims/src/lib.rs +++ /dev/null @@ -1,13 +0,0 @@ -//! Shim implementations for commands that require subprocess support. -//! -//! Commands like `env` and `timeout` need to spawn child processes -//! via wasi-ext syscalls (through std::process::Command patches) -//! rather than using uutils versions. - -pub mod env; -pub mod nice; -pub mod nohup; -pub mod stdbuf; -pub mod timeout; -pub mod which; -pub mod xargs; diff --git a/registry/native/crates/libs/strings-cmd/Cargo.toml b/registry/native/crates/libs/strings-cmd/Cargo.toml deleted file mode 100644 index 30e9834b62..0000000000 --- a/registry/native/crates/libs/strings-cmd/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "secureexec-strings-cmd" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "strings command implementation for secure-exec standalone binaries" - -[dependencies] diff --git a/registry/native/crates/libs/tar/Cargo.toml b/registry/native/crates/libs/tar/Cargo.toml deleted file mode 100644 index 6e42ed60f2..0000000000 --- a/registry/native/crates/libs/tar/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "secureexec-tar" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "tar implementation for secure-exec standalone binaries" - -[dependencies] -flate2 = { version = "1.0", default-features = false, features = ["rust_backend"] } -tar = "0.4" diff --git a/registry/native/crates/libs/tar/src/lib.rs b/registry/native/crates/libs/tar/src/lib.rs deleted file mode 100644 index d20f009a37..0000000000 --- a/registry/native/crates/libs/tar/src/lib.rs +++ /dev/null @@ -1,748 +0,0 @@ -//! tar implementation — create, extract, and list tape archives. -//! -//! Supports -c create, -x extract, -t list. -//! Options: -f archive, -z gzip, -v verbose, -C directory, --strip-components=N. - -use std::collections::HashSet; -use std::ffi::OsString; -use std::fs::{self, File, OpenOptions}; -use std::io::{self, Read, Write}; -use std::path::{Component, Path, PathBuf}; - -use flate2::read::GzDecoder; -use flate2::write::GzEncoder; -use flate2::Compression; - -const MAX_ARCHIVE_ENTRIES: usize = 100_000; -const MAX_CREATE_DEPTH: usize = 256; -const MAX_DIRECTORY_ENTRIES: usize = 100_000; - -#[derive(PartialEq)] -enum Mode { - None, - Create, - Extract, - List, -} - -/// Unified tar entry point. -pub fn main(args: Vec) -> i32 { - let str_args: Vec = args - .iter() - .skip(1) - .map(|a| a.to_string_lossy().to_string()) - .collect(); - - if str_args.is_empty() { - eprintln!("tar: must specify one of -c, -x, -t"); - return 1; - } - - let mut mode = Mode::None; - let mut archive_file: Option = None; - let mut gzip = false; - let mut verbose = false; - let mut directory: Option = None; - let mut strip_components: usize = 0; - let mut paths: Vec = Vec::new(); - - let mut i = 0; - let mut first_arg = true; - - while i < str_args.len() { - let arg = &str_args[i]; - - if arg.starts_with("--strip-components=") { - if let Ok(n) = arg["--strip-components=".len()..].parse() { - strip_components = n; - } - first_arg = false; - } else if arg == "--strip-components" { - i += 1; - if i < str_args.len() { - strip_components = str_args[i].parse().unwrap_or(0); - } - first_arg = false; - } else if arg == "-C" || arg == "--directory" { - i += 1; - if i < str_args.len() { - directory = Some(str_args[i].clone()); - } - first_arg = false; - } else if arg == "--help" { - print_usage(); - return 0; - } else if arg.starts_with('-') || first_arg { - // tar's first argument can omit the leading dash (e.g., `tar czf`) - let flags = if arg.starts_with('-') { - &arg[1..] - } else { - &arg[..] - }; - let mut chars = flags.chars().peekable(); - while let Some(ch) = chars.next() { - match ch { - 'c' => mode = Mode::Create, - 'x' => mode = Mode::Extract, - 't' => mode = Mode::List, - 'z' => gzip = true, - 'v' => verbose = true, - 'f' => { - let rest: String = chars.collect(); - if !rest.is_empty() { - archive_file = Some(rest); - } else { - i += 1; - if i < str_args.len() { - archive_file = Some(str_args[i].clone()); - } - } - break; - } - 'C' => { - let rest: String = chars.collect(); - if !rest.is_empty() { - directory = Some(rest); - } else { - i += 1; - if i < str_args.len() { - directory = Some(str_args[i].clone()); - } - } - break; - } - _ => { - eprintln!("tar: unknown option: {}", ch); - return 1; - } - } - } - first_arg = false; - } else { - paths.push(arg.clone()); - first_arg = false; - } - - i += 1; - } - - // Auto-detect gzip from filename - if !gzip { - if let Some(ref f) = archive_file { - if f.ends_with(".tar.gz") || f.ends_with(".tgz") { - gzip = true; - } - } - } - - let result = match mode { - Mode::Create => do_create( - archive_file.as_deref(), - gzip, - verbose, - directory.as_deref(), - &paths, - ), - Mode::Extract => do_extract( - archive_file.as_deref(), - gzip, - verbose, - directory.as_deref(), - strip_components, - ), - Mode::List => do_list(archive_file.as_deref(), gzip, verbose), - Mode::None => { - eprintln!("tar: must specify one of -c, -x, -t"); - return 1; - } - }; - - match result { - Ok(()) => 0, - Err(e) => { - eprintln!("tar: {}", e); - 1 - } - } -} - -fn do_create( - archive_file: Option<&str>, - gzip: bool, - verbose: bool, - directory: Option<&str>, - paths: &[String], -) -> io::Result<()> { - if paths.is_empty() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "cowardly refusing to create an empty archive", - )); - } - - match archive_file { - Some("-") | None => create_to_writer(io::stdout(), gzip, verbose, directory, paths), - Some(path) => { - let mut archive = BoundedVecWriter::new(512 * 1024 * 1024); - create_to_writer(&mut archive, gzip, verbose, directory, paths)?; - fs::write(path, archive.into_inner()) - } - } -} - -struct BoundedVecWriter { - bytes: Vec, - capacity: usize, -} - -impl BoundedVecWriter { - fn new(capacity: usize) -> Self { - Self { - bytes: Vec::new(), - capacity, - } - } - - fn into_inner(self) -> Vec { - self.bytes - } -} - -impl Write for BoundedVecWriter { - fn write(&mut self, buf: &[u8]) -> io::Result { - let Some(new_len) = self.bytes.len().checked_add(buf.len()) else { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "archive is too large to buffer", - )); - }; - if new_len > self.capacity { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "archive is too large to buffer", - )); - } - self.bytes.extend_from_slice(buf); - Ok(buf.len()) - } - - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - -fn create_to_writer( - writer: W, - gzip: bool, - verbose: bool, - directory: Option<&str>, - paths: &[String], -) -> io::Result<()> { - if gzip { - let encoder = GzEncoder::new(writer, Compression::default()); - let mut builder = tar::Builder::new(encoder); - append_paths(&mut builder, directory, paths, verbose)?; - let encoder = builder.into_inner()?; - let mut writer = encoder.finish()?; - writer.flush() - } else { - let mut builder = tar::Builder::new(writer); - append_paths(&mut builder, directory, paths, verbose)?; - let mut writer = builder.into_inner()?; - writer.flush() - } -} - -fn append_paths( - builder: &mut tar::Builder, - directory: Option<&str>, - paths: &[String], - verbose: bool, -) -> io::Result<()> { - let mut entry_count = 0; - for path in paths { - append_path( - builder, - resolve_disk_path(directory, Path::new(path)), - Path::new(path), - verbose, - 0, - &mut entry_count, - )?; - } - Ok(()) -} - -fn append_path( - builder: &mut tar::Builder, - disk_path: PathBuf, - archive_path: &Path, - verbose: bool, - depth: usize, - entry_count: &mut usize, -) -> io::Result<()> { - if depth > MAX_CREATE_DEPTH { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!( - "maximum directory depth exceeded at {}", - disk_path.display() - ), - )); - } - increment_entry_count(entry_count)?; - - let meta = fs::symlink_metadata(&disk_path)?; - - if meta.is_dir() { - append_dir( - builder, - &disk_path, - archive_path, - verbose, - depth, - entry_count, - )?; - } else if meta.is_file() { - if verbose { - eprintln!("{}", archive_path.display()); - } - let mut header = tar::Header::new_gnu(); - header.set_entry_type(tar::EntryType::Regular); - header.set_size(meta.len()); - header.set_mode(0o644); - header.set_cksum(); - let mut file = File::open(&disk_path)?; - builder.append_data(&mut header, archive_path, &mut file)?; - } else if meta.file_type().is_symlink() { - if verbose { - eprintln!("{}", archive_path.display()); - } - let target = fs::read_link(&disk_path)?; - let mut header = tar::Header::new_gnu(); - header.set_entry_type(tar::EntryType::Symlink); - header.set_size(0); - header.set_mode(0o777); - header.set_cksum(); - builder.append_link(&mut header, archive_path, &target)?; - } - - Ok(()) -} - -fn append_dir( - builder: &mut tar::Builder, - disk_dir: &Path, - archive_dir: &Path, - verbose: bool, - depth: usize, - entry_count: &mut usize, -) -> io::Result<()> { - if verbose { - eprintln!("{}/", archive_dir.display()); - } - - let mut header = tar::Header::new_gnu(); - header.set_entry_type(tar::EntryType::Directory); - header.set_size(0); - header.set_mode(0o755); - header.set_cksum(); - builder.append_data(&mut header, archive_dir, io::empty())?; - - let mut dir_entries = 0; - for entry_result in fs::read_dir(disk_dir)? { - let entry = entry_result?; - dir_entries += 1; - if dir_entries > MAX_DIRECTORY_ENTRIES { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("too many entries in {}", disk_dir.display()), - )); - } - let archive_child = archive_dir.join(entry.file_name()); - append_path( - builder, - entry.path(), - &archive_child, - verbose, - depth + 1, - entry_count, - )?; - } - - Ok(()) -} - -fn do_extract( - archive_file: Option<&str>, - gzip: bool, - verbose: bool, - directory: Option<&str>, - strip_components: usize, -) -> io::Result<()> { - let reader = open_read(archive_file, gzip)?; - let mut archive = tar::Archive::new(reader); - let mut known_dirs = HashSet::new(); - if let Some(base) = directory { - validate_extract_base(Path::new(base))?; - known_dirs.insert(PathBuf::from(base)); - } - - let mut entry_count = 0; - for entry_result in archive.entries()? { - increment_entry_count(&mut entry_count)?; - let mut entry = entry_result?; - let orig_path = entry.path()?.into_owned(); - validate_archive_input_path(&orig_path)?; - - let relative_dest = match strip_path_components(&orig_path, strip_components) { - Some(p) if !p.as_os_str().is_empty() => p, - _ => continue, - }; - validate_relative_output_path(&relative_dest)?; - validate_extract_depth(&relative_dest)?; - let dest = resolve_output_path(directory, &relative_dest); - - if verbose { - eprintln!("{}", orig_path.display()); - } - - match entry.header().entry_type() { - tar::EntryType::Directory => { - ensure_relative_dir_exists(directory, &relative_dest, &mut known_dirs)?; - } - tar::EntryType::Regular | tar::EntryType::GNUSparse => { - if let Some(parent) = dest.parent() { - if !parent.as_os_str().is_empty() { - let relative_parent = - relative_dest.parent().unwrap_or_else(|| Path::new("")); - ensure_relative_dir_exists(directory, relative_parent, &mut known_dirs)?; - } - } - reject_existing_symlink(&dest)?; - let mut output = OpenOptions::new() - .create(true) - .write(true) - .truncate(true) - .open(&dest) - .map_err(|e| { - io::Error::new(e.kind(), format!("open {}: {}", dest.display(), e)) - })?; - io::copy(&mut entry, &mut output).map_err(|e| { - io::Error::new(e.kind(), format!("write {}: {}", dest.display(), e)) - })?; - output.flush().map_err(|e| { - io::Error::new(e.kind(), format!("write {}: {}", dest.display(), e)) - })?; - } - tar::EntryType::Symlink => { - if let Some(target) = entry.link_name()? { - validate_symlink_target(target.as_ref())?; - if let Some(parent) = dest.parent() { - if !parent.as_os_str().is_empty() { - let relative_parent = - relative_dest.parent().unwrap_or_else(|| Path::new("")); - ensure_relative_dir_exists( - directory, - relative_parent, - &mut known_dirs, - )?; - } - } - reject_existing_symlink(&dest)?; - #[allow(deprecated)] - std::fs::soft_link(target.as_ref(), &dest).map_err(|e| { - io::Error::new(e.kind(), format!("symlink {}: {}", dest.display(), e)) - })?; - } - } - _ => { - // Skip hard links, char/block devices, etc. - } - } - } - - Ok(()) -} - -fn resolve_disk_path(directory: Option<&str>, path: &Path) -> PathBuf { - match directory { - Some(base) if !path.is_absolute() => Path::new(base).join(path), - _ => path.to_path_buf(), - } -} - -fn resolve_output_path(directory: Option<&str>, path: &Path) -> PathBuf { - match directory { - Some(base) if path.is_relative() => Path::new(base).join(path), - _ => path.to_path_buf(), - } -} - -fn ensure_relative_dir_exists( - directory: Option<&str>, - relative_path: &Path, - known_dirs: &mut HashSet, -) -> io::Result<()> { - let mut current = match directory { - Some(base) => PathBuf::from(base), - None => PathBuf::new(), - }; - - for component in relative_path.components() { - match component { - Component::CurDir => {} - Component::Normal(part) => { - current.push(part); - if known_dirs.contains(¤t) { - continue; - } - match fs::create_dir(¤t) { - Ok(()) => { - known_dirs.insert(current.clone()); - } - Err(err) if err.kind() == io::ErrorKind::AlreadyExists => { - let metadata = fs::symlink_metadata(¤t).map_err(|metadata_err| { - io::Error::new( - metadata_err.kind(), - format!("metadata {}: {}", current.display(), metadata_err), - ) - })?; - if metadata.file_type().is_symlink() || !metadata.is_dir() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("refusing to extract through {}", current.display()), - )); - } - known_dirs.insert(current.clone()); - } - Err(err) => { - return Err(io::Error::new( - err.kind(), - format!("create_dir {}: {}", current.display(), err), - )); - } - } - } - Component::Prefix(_) | Component::RootDir | Component::ParentDir => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("unsupported path component in {}", relative_path.display()), - )); - } - } - } - - Ok(()) -} - -fn do_list(archive_file: Option<&str>, gzip: bool, verbose: bool) -> io::Result<()> { - let reader = open_read(archive_file, gzip)?; - let mut archive = tar::Archive::new(reader); - let stdout = io::stdout(); - let mut out = stdout.lock(); - - let mut entry_count = 0; - for entry_result in archive.entries()? { - increment_entry_count(&mut entry_count)?; - let entry = entry_result?; - let path = entry.path()?; - - if verbose { - let h = entry.header(); - let size = h.size().unwrap_or(0); - let mode = h.mode().unwrap_or(0o644); - let type_ch = match h.entry_type() { - tar::EntryType::Directory => 'd', - tar::EntryType::Symlink => 'l', - _ => '-', - }; - writeln!( - out, - "{}{} {:>8} {}", - type_ch, - format_mode(mode), - size, - path.display() - )?; - } else { - writeln!(out, "{}", path.display())?; - } - } - - out.flush() -} - -fn open_read(archive_file: Option<&str>, gzip: bool) -> io::Result> { - let reader: Box = match archive_file { - Some("-") | None => Box::new(io::stdin()), - Some(path) => Box::new(File::open(path)?), - }; - - if gzip { - Ok(Box::new(GzDecoder::new(reader))) - } else { - Ok(reader) - } -} - -fn strip_path_components(path: &Path, n: usize) -> Option { - let mut remaining = n; - let mut stripped = PathBuf::new(); - - for component in path.components() { - match component { - Component::Prefix(_) | Component::RootDir | Component::CurDir => {} - Component::ParentDir => { - if remaining == 0 { - stripped.push(component.as_os_str()); - } - } - Component::Normal(part) => { - if remaining > 0 { - remaining -= 1; - } else { - stripped.push(part); - } - } - } - } - - if stripped.as_os_str().is_empty() { - None - } else { - Some(stripped) - } -} - -fn increment_entry_count(count: &mut usize) -> io::Result<()> { - *count += 1; - if *count > MAX_ARCHIVE_ENTRIES { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "too many archive entries", - )); - } - Ok(()) -} - -fn validate_relative_output_path(path: &Path) -> io::Result<()> { - for component in path.components() { - match component { - Component::Normal(_) | Component::CurDir => {} - Component::Prefix(_) | Component::RootDir | Component::ParentDir => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("refusing to extract unsafe path {}", path.display()), - )); - } - } - } - Ok(()) -} - -fn validate_archive_input_path(path: &Path) -> io::Result<()> { - for component in path.components() { - match component { - Component::Normal(_) | Component::CurDir => {} - Component::Prefix(_) | Component::RootDir | Component::ParentDir => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("refusing to extract unsafe path {}", path.display()), - )); - } - } - } - Ok(()) -} - -fn validate_extract_depth(path: &Path) -> io::Result<()> { - let depth = path - .components() - .filter(|component| matches!(component, Component::Normal(_))) - .count(); - if depth > MAX_CREATE_DEPTH { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("maximum extraction depth exceeded at {}", path.display()), - )); - } - Ok(()) -} - -fn validate_extract_base(path: &Path) -> io::Result<()> { - let metadata = fs::symlink_metadata(path).map_err(|err| { - io::Error::new(err.kind(), format!("metadata {}: {}", path.display(), err)) - })?; - if metadata.file_type().is_symlink() || !metadata.is_dir() { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("refusing to extract through {}", path.display()), - )); - } - Ok(()) -} - -fn validate_symlink_target(target: &Path) -> io::Result<()> { - for component in target.components() { - match component { - Component::Normal(_) | Component::CurDir => {} - Component::Prefix(_) | Component::RootDir | Component::ParentDir => { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!( - "refusing to extract unsafe symlink target {}", - target.display() - ), - )); - } - } - } - Ok(()) -} - -fn reject_existing_symlink(path: &Path) -> io::Result<()> { - match fs::symlink_metadata(path) { - Ok(metadata) if metadata.file_type().is_symlink() => Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("refusing to overwrite symlink {}", path.display()), - )), - Ok(_) => Ok(()), - Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()), - Err(err) => Err(io::Error::new( - err.kind(), - format!("metadata {}: {}", path.display(), err), - )), - } -} - -fn format_mode(mode: u32) -> String { - let mut s = String::with_capacity(9); - for &(bit, ch) in &[ - (0o400, 'r'), - (0o200, 'w'), - (0o100, 'x'), - (0o040, 'r'), - (0o020, 'w'), - (0o010, 'x'), - (0o004, 'r'), - (0o002, 'w'), - (0o001, 'x'), - ] { - s.push(if mode & bit != 0 { ch } else { '-' }); - } - s -} - -fn print_usage() { - eprintln!("Usage: tar [options] [files...]"); - eprintln!(" -c create archive"); - eprintln!(" -x extract archive"); - eprintln!(" -t list archive contents"); - eprintln!(" -f FILE archive filename (- for stdin/stdout)"); - eprintln!(" -z gzip compress/decompress"); - eprintln!(" -v verbose"); - eprintln!(" -C DIR change directory"); - eprintln!(" --strip-components=N"); - eprintln!(" strip N leading components on extract"); -} diff --git a/registry/native/crates/libs/tree/Cargo.toml b/registry/native/crates/libs/tree/Cargo.toml deleted file mode 100644 index adac0490e9..0000000000 --- a/registry/native/crates/libs/tree/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "secureexec-tree" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "tree implementation for secure-exec standalone binaries" - -[dependencies] diff --git a/registry/native/crates/libs/tree/src/lib.rs b/registry/native/crates/libs/tree/src/lib.rs deleted file mode 100644 index 71cfb7044c..0000000000 --- a/registry/native/crates/libs/tree/src/lib.rs +++ /dev/null @@ -1,268 +0,0 @@ -//! tree -- list directory contents in a tree-like format -//! -//! Recursive directory walk with box-drawing characters. -//! Supports -a (show hidden), -d (dirs only), -L depth, -I exclude pattern. - -use std::ffi::OsString; -use std::fs; -use std::io::{self, Write}; -use std::path::Path; - -const DEFAULT_MAX_DEPTH: usize = 256; -const MAX_TOTAL_ENTRIES: usize = 100_000; -const MAX_DIRECTORY_ENTRIES: usize = 100_000; - -pub fn main(args: Vec) -> i32 { - let str_args: Vec = args - .iter() - .skip(1) - .map(|a| a.to_string_lossy().to_string()) - .collect(); - - let mut show_hidden = false; - let mut dirs_only = false; - let mut max_depth: Option = None; - let mut exclude_pattern: Option = None; - let mut paths: Vec = Vec::new(); - - let mut i = 0; - while i < str_args.len() { - match str_args[i].as_str() { - "-a" => show_hidden = true, - "-d" => dirs_only = true, - "-L" => { - i += 1; - if i >= str_args.len() { - eprintln!("tree: option '-L' requires an argument"); - return 1; - } - match str_args[i].parse::() { - Ok(d) => max_depth = Some(d), - Err(_) => { - eprintln!("tree: invalid level '{}'", str_args[i]); - return 1; - } - } - } - "-I" => { - i += 1; - if i >= str_args.len() { - eprintln!("tree: option '-I' requires an argument"); - return 1; - } - exclude_pattern = Some(str_args[i].clone()); - } - s if s.starts_with('-') => { - eprintln!("tree: unknown option '{}'", s); - return 1; - } - _ => paths.push(str_args[i].clone()), - } - i += 1; - } - - if paths.is_empty() { - paths.push(".".to_string()); - } - - let stdout = io::stdout(); - let mut out = stdout.lock(); - let mut dir_count: usize = 0; - let mut file_count: usize = 0; - - for (idx, path) in paths.iter().enumerate() { - if let Err(e) = writeln!(out, "{}", path).and_then(|_| { - walk_tree( - Path::new(path), - "", - 1, - max_depth, - show_hidden, - dirs_only, - exclude_pattern.as_deref(), - &mut dir_count, - &mut file_count, - &mut out, - ) - }) { - eprintln!("tree: {}", e); - return 1; - } - if idx + 1 < paths.len() { - if let Err(e) = writeln!(out) { - eprintln!("tree: {}", e); - return 1; - } - } - } - - if let Err(e) = writeln!(out) { - eprintln!("tree: {}", e); - return 1; - } - if dirs_only { - if let Err(e) = writeln!( - out, - "{} director{}", - dir_count, - if dir_count == 1 { "y" } else { "ies" } - ) { - eprintln!("tree: {}", e); - return 1; - } - } else { - if let Err(e) = writeln!( - out, - "{} director{}, {} file{}", - dir_count, - if dir_count == 1 { "y" } else { "ies" }, - file_count, - if file_count == 1 { "" } else { "s" } - ) { - eprintln!("tree: {}", e); - return 1; - } - } - - match out.flush() { - Ok(()) => 0, - Err(e) => { - eprintln!("tree: {}", e); - 1 - } - } -} - -fn matches_exclude(name: &str, pattern: &str) -> bool { - // Simple glob matching: supports * as wildcard - if pattern.contains('*') { - let parts: Vec<&str> = pattern.split('*').collect(); - if parts.len() == 2 { - let (prefix, suffix) = (parts[0], parts[1]); - return name.starts_with(prefix) && name.ends_with(suffix); - } - // Fallback: check if any non-wildcard part matches - parts.iter().all(|p| p.is_empty() || name.contains(p)) - } else { - name == pattern - } -} - -fn walk_tree( - dir: &Path, - prefix: &str, - depth: usize, - max_depth: Option, - show_hidden: bool, - dirs_only: bool, - exclude: Option<&str>, - dir_count: &mut usize, - file_count: &mut usize, - out: &mut W, -) -> io::Result<()> { - if let Some(max) = max_depth { - if depth > max { - return Ok(()); - } - } else if depth > DEFAULT_MAX_DEPTH { - return Ok(()); - } - - let mut entries: Vec = match fs::read_dir(dir) { - Ok(rd) => { - let mut entries = Vec::new(); - for entry_result in rd { - let entry = entry_result?; - if entries.len() >= MAX_DIRECTORY_ENTRIES { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("too many entries in {}", dir.display()), - )); - } - entries.push(entry); - } - entries - } - Err(e) => { - writeln!(out, "{}[error opening dir: {}]", prefix, e)?; - return Ok(()); - } - }; - - // Sort entries alphabetically - entries.sort_by(|a, b| a.file_name().cmp(&b.file_name())); - - // Filter entries - entries.retain(|e| { - let name = e.file_name().to_string_lossy().to_string(); - // Skip hidden unless -a - if !show_hidden && name.starts_with('.') { - return false; - } - // Skip excluded patterns - if let Some(pat) = exclude { - if matches_exclude(&name, pat) { - return false; - } - } - // Skip files if -d - if dirs_only { - if let Ok(ft) = e.file_type() { - if !ft.is_dir() { - return false; - } - } - } - true - }); - - let count = entries.len(); - - for (idx, entry) in entries.iter().enumerate() { - if *dir_count + *file_count >= MAX_TOTAL_ENTRIES { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "too many tree entries", - )); - } - - let is_last = idx + 1 == count; - let connector = if is_last { - "\u{2514}\u{2500}\u{2500} " // └── - } else { - "\u{251c}\u{2500}\u{2500} " // ├── - }; - - let name = entry.file_name().to_string_lossy().to_string(); - let file_type = entry.file_type()?; - let is_dir = file_type.is_dir() && !file_type.is_symlink(); - - write!(out, "{}{}", prefix, connector)?; - writeln!(out, "{}", name)?; - - if is_dir { - *dir_count += 1; - let child_prefix = if is_last { - format!("{} ", prefix) - } else { - format!("{}\u{2502} ", prefix) // │ - }; - walk_tree( - &dir.join(&name), - &child_prefix, - depth + 1, - max_depth, - show_hidden, - dirs_only, - exclude, - dir_count, - file_count, - out, - )?; - } else { - *file_count += 1; - } - } - - Ok(()) -} diff --git a/registry/native/crates/libs/wasi-pty/src/lib.rs b/registry/native/crates/libs/wasi-pty/src/lib.rs deleted file mode 100644 index af94b6ef71..0000000000 --- a/registry/native/crates/libs/wasi-pty/src/lib.rs +++ /dev/null @@ -1,503 +0,0 @@ -//! WASI PTY-based process management via host_process FFI. -//! -//! Provides [`WasiPtyChild`] — an interactive process handle using a pseudo-terminal -//! instead of pipes. This is the WASI equivalent of `ExecCommandSession` from -//! `codex-utils-pty` (which wraps `portable-pty` on native platforms). -//! -//! Key difference from [`wasi_spawn::WasiChild`]: -//! - Uses a PTY master/slave pair instead of separate stdout/stderr pipes -//! - All child output (stdout + stderr) is multiplexed through the PTY master -//! - Supports interactive programs that require a terminal (e.g., editors, shells) -//! - The PTY provides terminal emulation (line discipline, echo, signals) - -use std::io::{self, Read, Write}; -use std::mem::ManuallyDrop; - -const MAX_ARG_COUNT: usize = 4096; -const MAX_ENV_COUNT: usize = 4096; -const MAX_SERIALIZED_BYTES: usize = 1024 * 1024; -const MAX_CWD_BYTES: usize = 4096; -const MAX_CAPTURED_OUTPUT_BYTES: usize = 16 * 1024 * 1024; - -/// Handle to a spawned process connected via a pseudo-terminal. -/// -/// Created by [`spawn_pty`]. The child process has the PTY slave as its -/// stdin/stdout/stderr. The parent reads and writes via the PTY master FD. -/// -/// This is the WASI equivalent of `SpawnedPty` from `codex-utils-pty`. -pub struct WasiSpawnedPty { - master_fd: RawFd, -} - -/// Interactive process session using a PTY. -/// -/// Created by [`spawn_session`]. Wraps a [`WasiSpawnedPty`] and the child -/// process handle, providing a unified API for interactive process management. -/// -/// This is the WASI equivalent of `ExecCommandSession` from `codex-utils-pty`. -pub struct WasiPtyChild { - pid: u32, - master_fd: RawFd, - exited: bool, -} - -type RawFd = u32; - -fn errno_to_io_error(errno: wasi_ext::Errno) -> io::Error { - io::Error::new(io::ErrorKind::Other, format!("wasi errno {}", errno)) -} - -fn invalid_input(message: impl Into) -> io::Error { - io::Error::new(io::ErrorKind::InvalidInput, message.into()) -} - -fn invalid_data(message: impl Into) -> io::Error { - io::Error::new(io::ErrorKind::InvalidData, message.into()) -} - -/// Read from a raw WASI file descriptor into a buffer. -fn fd_read(fd: RawFd, buf: &mut [u8]) -> io::Result { - use std::os::fd::FromRawFd; - // The caller owns this fd. This temporary File only routes through WASI fd_read. - let file = unsafe { ManuallyDrop::new(std::fs::File::from_raw_fd(fd as i32)) }; - (&*file).read(buf) -} - -/// Write to a raw WASI file descriptor from a buffer. -fn fd_write(fd: RawFd, buf: &[u8]) -> io::Result { - use std::os::fd::FromRawFd; - // The caller owns this fd. This temporary File only routes through WASI fd_write. - let file = unsafe { ManuallyDrop::new(std::fs::File::from_raw_fd(fd as i32)) }; - (&*file).write(buf) -} - -/// Close a raw WASI file descriptor. -fn fd_close(fd: RawFd) { - use std::os::fd::FromRawFd; - drop(unsafe { std::fs::File::from_raw_fd(fd as i32) }); -} - -/// Serialize strings as null-separated byte buffer for proc_spawn. -fn serialize_null_separated(items: &[&str]) -> io::Result> { - if items.len() > MAX_ARG_COUNT { - return Err(invalid_input(format!( - "argument count exceeds limit of {MAX_ARG_COUNT}" - ))); - } - - let mut buf = Vec::new(); - for (i, item) in items.iter().enumerate() { - validate_no_nul("argument", item)?; - if i > 0 { - push_serialized_byte(&mut buf, 0)?; - } - append_serialized(&mut buf, item.as_bytes())?; - } - Ok(buf) -} - -/// Serialize environment as KEY=VALUE null-separated pairs for proc_spawn. -fn serialize_env(env: &[(&str, &str)]) -> io::Result> { - if env.len() > MAX_ENV_COUNT { - return Err(invalid_input(format!( - "environment count exceeds limit of {MAX_ENV_COUNT}" - ))); - } - - let mut buf = Vec::new(); - for (i, (key, value)) in env.iter().enumerate() { - validate_env_key(key)?; - validate_no_nul("environment value", value)?; - if i > 0 { - push_serialized_byte(&mut buf, 0)?; - } - append_serialized(&mut buf, key.as_bytes())?; - push_serialized_byte(&mut buf, b'=')?; - append_serialized(&mut buf, value.as_bytes())?; - } - Ok(buf) -} - -fn validate_env_key(key: &str) -> io::Result<()> { - if key.is_empty() { - return Err(invalid_input("environment key must not be empty")); - } - validate_no_nul("environment key", key)?; - if key.as_bytes().contains(&b'=') { - return Err(invalid_input("environment key must not contain '='")); - } - Ok(()) -} - -fn validate_no_nul(label: &str, value: &str) -> io::Result<()> { - if value.as_bytes().contains(&0) { - return Err(invalid_input(format!("{label} must not contain NUL"))); - } - Ok(()) -} - -fn validate_cwd(cwd: &str) -> io::Result<()> { - validate_no_nul("cwd", cwd)?; - if cwd.len() > MAX_CWD_BYTES { - return Err(invalid_input(format!( - "cwd exceeds limit of {MAX_CWD_BYTES} bytes" - ))); - } - Ok(()) -} - -fn push_serialized_byte(buf: &mut Vec, byte: u8) -> io::Result<()> { - reserve_serialized(buf.len(), 1)?; - buf.push(byte); - Ok(()) -} - -fn append_serialized(buf: &mut Vec, bytes: &[u8]) -> io::Result<()> { - reserve_serialized(buf.len(), bytes.len())?; - buf.extend_from_slice(bytes); - Ok(()) -} - -fn reserve_serialized(current_len: usize, additional_len: usize) -> io::Result<()> { - let next_len = current_len - .checked_add(additional_len) - .ok_or_else(|| invalid_input("serialized spawn data length overflowed"))?; - if next_len > MAX_SERIALIZED_BYTES { - return Err(invalid_input(format!( - "serialized spawn data exceeds limit of {MAX_SERIALIZED_BYTES} bytes" - ))); - } - Ok(()) -} - -fn append_captured_output_with_limit( - stdout: &mut Vec, - chunk: &[u8], - limit: usize, -) -> io::Result<()> { - let next_len = stdout - .len() - .checked_add(chunk.len()) - .ok_or_else(|| invalid_data("captured PTY output length overflowed"))?; - if next_len > limit { - return Err(invalid_data(format!( - "captured PTY output exceeds limit of {limit} bytes" - ))); - } - stdout.extend_from_slice(chunk); - Ok(()) -} - -fn read_captured_output(read_output: R, cleanup: C) -> io::Result> -where - R: FnMut(&mut [u8]) -> io::Result, - C: FnMut(), -{ - read_captured_output_with_limit(read_output, cleanup, MAX_CAPTURED_OUTPUT_BYTES) -} - -fn read_captured_output_with_limit( - mut read_output: R, - mut cleanup: C, - limit: usize, -) -> io::Result> -where - R: FnMut(&mut [u8]) -> io::Result, - C: FnMut(), -{ - let mut stdout = Vec::new(); - let mut buf = [0u8; 4096]; - loop { - match read_output(&mut buf) { - Ok(0) => break, - Ok(n) => { - if let Err(e) = append_captured_output_with_limit(&mut stdout, &buf[..n], limit) { - cleanup(); - return Err(e); - } - } - Err(e) if e.kind() == io::ErrorKind::BrokenPipe => break, - Err(e) => { - cleanup(); - return Err(e); - } - } - } - Ok(stdout) -} - -fn wait_or_cleanup(result: io::Result, cleanup: C) -> io::Result -where - C: FnOnce(), -{ - match result { - Ok(exit_code) => Ok(exit_code), - Err(e) => { - cleanup(); - Err(e) - } - } -} - -/// Spawn a child process connected via a PTY. -/// -/// Allocates a PTY master/slave pair, spawns the child with the slave FD as -/// stdin/stdout/stderr, and returns a [`WasiPtyChild`] handle. The slave FD -/// is closed in the parent after spawn (POSIX close-after-fork). -/// -/// # Arguments -/// * `argv` - Command and arguments (argv[0] is the program name) -/// * `env` - Environment variable pairs (empty inherits parent env via host) -/// * `cwd` - Working directory for the child -pub fn spawn_session(argv: &[&str], env: &[(&str, &str)], cwd: &str) -> io::Result { - if argv.is_empty() { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty argv")); - } - - validate_cwd(cwd)?; - let argv_buf = serialize_null_separated(argv)?; - let envp_buf = serialize_env(env)?; - - // Allocate PTY master/slave pair via kernel - let (master_fd, slave_fd) = wasi_ext::openpty().map_err(errno_to_io_error)?; - - // Spawn child with PTY slave as all stdio - let result = wasi_ext::spawn( - &argv_buf, - &envp_buf, - slave_fd, - slave_fd, - slave_fd, - cwd.as_bytes(), - ); - - // Close slave FD in parent (POSIX close-after-fork) — child has its own ref - fd_close(slave_fd); - - match result { - Ok(pid) => Ok(WasiPtyChild { - pid, - master_fd, - exited: false, - }), - Err(errno) => { - fd_close(master_fd); - Err(errno_to_io_error(errno)) - } - } -} - -/// Allocate a PTY pair without spawning a process. -/// -/// Returns a [`WasiSpawnedPty`] for the master end. The slave FD is returned -/// separately so the caller can pass it to [`wasi_spawn::spawn_child`] or -/// use it directly. -pub fn open_pty() -> io::Result<(WasiSpawnedPty, RawFd)> { - let (master_fd, slave_fd) = wasi_ext::openpty().map_err(errno_to_io_error)?; - - Ok((WasiSpawnedPty { master_fd }, slave_fd)) -} - -impl WasiSpawnedPty { - /// Get the master FD for direct I/O. - pub fn master_fd(&self) -> RawFd { - self.master_fd - } - - /// Read output from the PTY master (data written by the child). - pub fn read(&self, buf: &mut [u8]) -> io::Result { - fd_read(self.master_fd, buf) - } - - /// Write input to the PTY master (delivered to the child's stdin). - pub fn write(&self, buf: &[u8]) -> io::Result { - fd_write(self.master_fd, buf) - } -} - -impl Drop for WasiSpawnedPty { - fn drop(&mut self) { - fd_close(self.master_fd); - } -} - -impl WasiPtyChild { - /// Get the child's virtual PID. - pub fn pid(&self) -> u32 { - self.pid - } - - /// Get the PTY master FD for direct I/O if needed. - pub fn master_fd(&self) -> RawFd { - self.master_fd - } - - /// Read output from the child via the PTY master. - /// - /// All child output (stdout and stderr) is multiplexed through the PTY. - /// Returns 0 bytes when the PTY slave is closed (child exited). - pub fn read_output(&self, buf: &mut [u8]) -> io::Result { - fd_read(self.master_fd, buf) - } - - /// Write input to the child via the PTY master. - /// - /// Data is delivered to the child's stdin through the PTY line discipline. - pub fn write_stdin(&self, buf: &[u8]) -> io::Result { - fd_write(self.master_fd, buf) - } - - /// Wait for the child to exit. Returns the exit code. - /// - /// Blocks via host_process_waitpid (Atomics.wait on host side). - pub fn wait(&mut self) -> io::Result { - if self.exited { - return Err(io::Error::new(io::ErrorKind::Other, "already waited")); - } - - let (status, _actual_pid) = wasi_ext::waitpid(self.pid, 0).map_err(errno_to_io_error)?; - - self.exited = true; - Ok(status as i32) - } - - /// Send a signal to the child process. - pub fn kill(&mut self, signal: u32) -> io::Result<()> { - wasi_ext::kill(self.pid, signal).map_err(errno_to_io_error) - } - - /// Send SIGTERM to the child process. - pub fn terminate(&mut self) -> io::Result<()> { - self.kill(15) - } - - fn kill_and_reap(&mut self) { - if self.exited { - return; - } - - let _ = wasi_ext::kill(self.pid, 9); - if wasi_ext::waitpid(self.pid, 0).is_ok() { - self.exited = true; - } - } - - /// Read all output from the PTY, then wait for exit. - /// - /// Reads output until the PTY master gets EOF (child closed slave), - /// then waits for the child to exit. - pub fn consume_output(&mut self) -> io::Result { - let master_fd = self.master_fd; - let stdout = read_captured_output(|buf| fd_read(master_fd, buf), || self.kill_and_reap())?; - - let wait_result = self.wait(); - let exit_code = wait_or_cleanup(wait_result, || self.kill_and_reap())?; - - // PTY multiplexes stdout+stderr, so stderr is empty - Ok(wasi_spawn::WasiOutput { - stdout, - stderr: Vec::new(), - exit_code, - }) - } -} - -impl Drop for WasiPtyChild { - fn drop(&mut self) { - self.kill_and_reap(); - fd_close(self.master_fd); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn rejects_interior_nul_in_arguments() { - let err = serialize_null_separated(&["echo", "a\0b"]).unwrap_err(); - - assert_eq!(err.kind(), io::ErrorKind::InvalidInput); - } - - #[test] - fn rejects_invalid_environment_keys() { - let err = serialize_env(&[("A=B", "value")]).unwrap_err(); - - assert_eq!(err.kind(), io::ErrorKind::InvalidInput); - } - - #[test] - fn rejects_oversized_serialized_data() { - let oversized = "x".repeat(MAX_SERIALIZED_BYTES + 1); - let err = serialize_null_separated(&[&oversized]).unwrap_err(); - - assert_eq!(err.kind(), io::ErrorKind::InvalidInput); - } - - #[test] - fn appends_captured_output_until_limit() { - let mut output = vec![b'x'; MAX_CAPTURED_OUTPUT_BYTES - 1]; - - append_captured_output_with_limit(&mut output, b"y", MAX_CAPTURED_OUTPUT_BYTES).unwrap(); - - assert_eq!(output.len(), MAX_CAPTURED_OUTPUT_BYTES); - } - - #[test] - fn rejects_oversized_captured_output() { - let mut output = vec![b'x'; MAX_CAPTURED_OUTPUT_BYTES]; - let err = append_captured_output_with_limit(&mut output, b"y", MAX_CAPTURED_OUTPUT_BYTES) - .unwrap_err(); - - assert_eq!(err.kind(), io::ErrorKind::InvalidData); - assert_eq!(output.len(), MAX_CAPTURED_OUTPUT_BYTES); - } - - #[test] - fn consume_helper_cleans_up_on_output_limit() { - let mut reads = 0; - let mut cleanup_calls = 0; - let err = read_captured_output_with_limit( - |buf| { - reads += 1; - buf[..4].copy_from_slice(b"xxxx"); - Ok(4) - }, - || cleanup_calls += 1, - 8, - ) - .unwrap_err(); - - assert_eq!(err.kind(), io::ErrorKind::InvalidData); - assert_eq!(reads, 3); - assert_eq!(cleanup_calls, 1); - } - - #[test] - fn consume_helper_cleans_up_on_read_error() { - let mut cleanup_calls = 0; - let err = read_captured_output_with_limit( - |_buf| Err(io::Error::new(io::ErrorKind::PermissionDenied, "boom")), - || cleanup_calls += 1, - 8, - ) - .unwrap_err(); - - assert_eq!(err.kind(), io::ErrorKind::PermissionDenied); - assert_eq!(cleanup_calls, 1); - } - - #[test] - fn wait_error_runs_cleanup() { - let mut cleanup_calls = 0; - let err = wait_or_cleanup( - Err(io::Error::new(io::ErrorKind::NotFound, "missing")), - || cleanup_calls += 1, - ) - .unwrap_err(); - - assert_eq!(err.kind(), io::ErrorKind::NotFound); - assert_eq!(cleanup_calls, 1); - } -} diff --git a/registry/native/crates/libs/wasi-spawn/src/lib.rs b/registry/native/crates/libs/wasi-spawn/src/lib.rs deleted file mode 100644 index b2b1b8be6c..0000000000 --- a/registry/native/crates/libs/wasi-spawn/src/lib.rs +++ /dev/null @@ -1,756 +0,0 @@ -//! WASI process spawning via host_process FFI. -//! -//! Provides `WasiChild` — a synchronous child process handle with pipe-based -//! stdout/stderr capture, wait, and kill. Uses wasi-ext FFI directly instead -//! of std::process::Command for explicit control over pipe lifecycle. -//! -//! Designed for codex-rs WASI integration: replaces tokio::process::Command -//! on wasm32-wasip1 where tokio process/signal features are unavailable. - -use std::io::{self, Read}; -use std::mem::ManuallyDrop; - -const MAX_ARG_COUNT: usize = 4096; -const MAX_ENV_COUNT: usize = 4096; -const MAX_SERIALIZED_BYTES: usize = 1024 * 1024; -const MAX_CWD_BYTES: usize = 4096; -const MAX_CAPTURED_STREAM_BYTES: usize = 16 * 1024 * 1024; -#[cfg(target_os = "wasi")] -const READY_STDOUT: u64 = 0; -#[cfg(target_os = "wasi")] -const READY_STDERR: u64 = 1; - -/// Captured output from a child process. -pub struct WasiOutput { - pub stdout: Vec, - pub stderr: Vec, - pub exit_code: i32, -} - -/// Handle to a spawned child process with pipe-based I/O capture. -/// -/// Created by [`spawn_child`]. Owns the read ends of stdout/stderr pipes. -/// The write ends are closed in the parent after spawn (POSIX close-after-fork). -pub struct WasiChild { - pid: u32, - stdout_fd: Option, - stderr_fd: Option, - exited: bool, -} - -/// Raw file descriptor type matching WASI u32 FDs. -type RawFd = u32; - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum CapturedStream { - Stdout, - Stderr, -} - -fn errno_to_io_error(errno: wasi_ext::Errno) -> io::Error { - io::Error::new(io::ErrorKind::Other, format!("wasi errno {}", errno)) -} - -#[cfg(target_os = "wasi")] -fn wasi_errno_to_io_error(errno: wasi::Errno) -> io::Error { - io::Error::new(io::ErrorKind::Other, format!("wasi errno {}", errno.raw())) -} - -fn invalid_input(message: impl Into) -> io::Error { - io::Error::new(io::ErrorKind::InvalidInput, message.into()) -} - -fn invalid_data(message: impl Into) -> io::Error { - io::Error::new(io::ErrorKind::InvalidData, message.into()) -} - -/// Read from a raw WASI file descriptor into a buffer. -/// -/// Uses std::fs::File::from_raw_fd for WASI fd_read dispatch. -fn fd_read(fd: RawFd, buf: &mut [u8]) -> io::Result { - use std::os::fd::FromRawFd; - // The caller owns this fd. This temporary File only routes through WASI fd_read. - let file = unsafe { ManuallyDrop::new(std::fs::File::from_raw_fd(fd as i32)) }; - (&*file).read(buf) -} - -/// Close a raw WASI file descriptor. -fn fd_close(fd: RawFd) { - use std::os::fd::FromRawFd; - // Safety: fd is a valid local FD. Drop closes it via WASI fd_close. - drop(unsafe { std::fs::File::from_raw_fd(fd as i32) }); -} - -/// Serialize strings as null-separated byte buffer for proc_spawn. -fn serialize_null_separated(items: &[&str]) -> io::Result> { - if items.len() > MAX_ARG_COUNT { - return Err(invalid_input(format!( - "argument count exceeds limit of {MAX_ARG_COUNT}" - ))); - } - - let mut buf = Vec::new(); - for (i, item) in items.iter().enumerate() { - validate_no_nul("argument", item)?; - if i > 0 { - push_serialized_byte(&mut buf, 0)?; - } - append_serialized(&mut buf, item.as_bytes())?; - } - Ok(buf) -} - -/// Serialize environment as KEY=VALUE null-separated pairs for proc_spawn. -fn serialize_env(env: &[(&str, &str)]) -> io::Result> { - if env.len() > MAX_ENV_COUNT { - return Err(invalid_input(format!( - "environment count exceeds limit of {MAX_ENV_COUNT}" - ))); - } - - let mut buf = Vec::new(); - for (i, (key, value)) in env.iter().enumerate() { - validate_env_key(key)?; - validate_no_nul("environment value", value)?; - if i > 0 { - push_serialized_byte(&mut buf, 0)?; - } - append_serialized(&mut buf, key.as_bytes())?; - push_serialized_byte(&mut buf, b'=')?; - append_serialized(&mut buf, value.as_bytes())?; - } - Ok(buf) -} - -fn validate_env_key(key: &str) -> io::Result<()> { - if key.is_empty() { - return Err(invalid_input("environment key must not be empty")); - } - validate_no_nul("environment key", key)?; - if key.as_bytes().contains(&b'=') { - return Err(invalid_input("environment key must not contain '='")); - } - Ok(()) -} - -fn validate_no_nul(label: &str, value: &str) -> io::Result<()> { - if value.as_bytes().contains(&0) { - return Err(invalid_input(format!("{label} must not contain NUL"))); - } - Ok(()) -} - -fn validate_cwd(cwd: &str) -> io::Result<()> { - validate_no_nul("cwd", cwd)?; - if cwd.len() > MAX_CWD_BYTES { - return Err(invalid_input(format!( - "cwd exceeds limit of {MAX_CWD_BYTES} bytes" - ))); - } - Ok(()) -} - -fn push_serialized_byte(buf: &mut Vec, byte: u8) -> io::Result<()> { - reserve_serialized(buf.len(), 1)?; - buf.push(byte); - Ok(()) -} - -fn append_serialized(buf: &mut Vec, bytes: &[u8]) -> io::Result<()> { - reserve_serialized(buf.len(), bytes.len())?; - buf.extend_from_slice(bytes); - Ok(()) -} - -fn reserve_serialized(current_len: usize, additional_len: usize) -> io::Result<()> { - let next_len = current_len - .checked_add(additional_len) - .ok_or_else(|| invalid_input("serialized spawn data length overflowed"))?; - if next_len > MAX_SERIALIZED_BYTES { - return Err(invalid_input(format!( - "serialized spawn data exceeds limit of {MAX_SERIALIZED_BYTES} bytes" - ))); - } - Ok(()) -} - -fn append_captured_stream_with_limit( - output: &mut Vec, - chunk: &[u8], - limit: usize, -) -> io::Result<()> { - let next_len = output - .len() - .checked_add(chunk.len()) - .ok_or_else(|| invalid_data("captured stream length overflowed"))?; - if next_len > limit { - return Err(invalid_data(format!( - "captured stream exceeds limit of {limit} bytes" - ))); - } - output.extend_from_slice(chunk); - Ok(()) -} - -fn read_captured_streams( - stdout_fd: Option, - stderr_fd: Option, - read_fd: R, - wait_readable: W, - cleanup: C, -) -> io::Result<(Vec, Vec)> -where - R: FnMut(RawFd, &mut [u8]) -> io::Result, - W: FnMut(Option, Option) -> io::Result<[Option; 2]>, - C: FnMut(), -{ - read_captured_streams_with_limit( - stdout_fd, - stderr_fd, - read_fd, - wait_readable, - cleanup, - MAX_CAPTURED_STREAM_BYTES, - ) -} - -fn read_captured_streams_with_limit( - stdout_fd: Option, - stderr_fd: Option, - mut read_fd: R, - mut wait_readable: W, - mut cleanup: C, - limit: usize, -) -> io::Result<(Vec, Vec)> -where - R: FnMut(RawFd, &mut [u8]) -> io::Result, - W: FnMut(Option, Option) -> io::Result<[Option; 2]>, - C: FnMut(), -{ - let mut stdout = Vec::new(); - let mut stderr = Vec::new(); - let mut stdout_done = stdout_fd.is_none(); - let mut stderr_done = stderr_fd.is_none(); - let mut buf = [0u8; 4096]; - - while !stdout_done || !stderr_done { - let active_stdout = if stdout_done { None } else { stdout_fd }; - let active_stderr = if stderr_done { None } else { stderr_fd }; - let ready = match wait_readable(active_stdout, active_stderr) { - Ok(ready) => ready, - Err(e) => { - cleanup(); - return Err(e); - } - }; - - let mut progressed = false; - for stream in ready.into_iter().flatten() { - let (fd, output, done) = match stream { - CapturedStream::Stdout if !stdout_done => ( - stdout_fd.expect("stdout fd is present while active"), - &mut stdout, - &mut stdout_done, - ), - CapturedStream::Stderr if !stderr_done => ( - stderr_fd.expect("stderr fd is present while active"), - &mut stderr, - &mut stderr_done, - ), - CapturedStream::Stdout => continue, - CapturedStream::Stderr => continue, - }; - - progressed = true; - match read_fd(fd, &mut buf) { - Ok(0) => { - *done = true; - } - Ok(n) => { - if let Err(e) = append_captured_stream_with_limit(output, &buf[..n], limit) { - cleanup(); - return Err(e); - } - } - Err(e) if e.kind() == io::ErrorKind::BrokenPipe => { - *done = true; - } - Err(e) => { - cleanup(); - return Err(e); - } - } - } - - if !progressed { - cleanup(); - return Err(io::Error::new( - io::ErrorKind::WouldBlock, - "no captured stream became readable", - )); - } - } - Ok((stdout, stderr)) -} - -#[cfg(target_os = "wasi")] -fn wait_readable_streams( - stdout_fd: Option, - stderr_fd: Option, -) -> io::Result<[Option; 2]> { - let mut subscriptions = Vec::with_capacity(2); - if let Some(fd) = stdout_fd { - subscriptions.push(wasi::Subscription { - userdata: READY_STDOUT, - u: wasi::SubscriptionU { - tag: wasi::EVENTTYPE_FD_READ.raw(), - u: wasi::SubscriptionUU { - fd_read: wasi::SubscriptionFdReadwrite { - file_descriptor: fd, - }, - }, - }, - }); - } - if let Some(fd) = stderr_fd { - subscriptions.push(wasi::Subscription { - userdata: READY_STDERR, - u: wasi::SubscriptionU { - tag: wasi::EVENTTYPE_FD_READ.raw(), - u: wasi::SubscriptionUU { - fd_read: wasi::SubscriptionFdReadwrite { - file_descriptor: fd, - }, - }, - }, - }); - } - - if subscriptions.is_empty() { - return Ok([None, None]); - } - - let mut events = vec![unsafe { std::mem::zeroed::() }; subscriptions.len()]; - let ready_count = unsafe { - wasi::poll_oneoff( - subscriptions.as_ptr(), - events.as_mut_ptr(), - subscriptions.len(), - ) - } - .map_err(wasi_errno_to_io_error)?; - if ready_count > events.len() { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "poll returned too many events", - )); - } - - let mut ready = [None, None]; - for (i, event) in events.into_iter().take(ready_count).enumerate() { - if event.error != wasi::ERRNO_SUCCESS { - return Err(wasi_errno_to_io_error(event.error)); - } - ready[i] = match event.userdata { - READY_STDOUT => Some(CapturedStream::Stdout), - READY_STDERR => Some(CapturedStream::Stderr), - _ => { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "poll returned unknown stream", - )); - } - }; - } - Ok(ready) -} - -#[cfg(not(target_os = "wasi"))] -fn wait_readable_streams( - stdout_fd: Option, - stderr_fd: Option, -) -> io::Result<[Option; 2]> { - Ok([ - stdout_fd.map(|_| CapturedStream::Stdout), - stderr_fd.map(|_| CapturedStream::Stderr), - ]) -} - -fn wait_or_cleanup(result: io::Result, cleanup: C) -> io::Result -where - C: FnOnce(), -{ - match result { - Ok(exit_code) => Ok(exit_code), - Err(e) => { - cleanup(); - Err(e) - } - } -} - -fn spawn_child_with_stdin_fd( - argv: &[&str], - env: &[(&str, &str)], - cwd: &str, - stdin_fd: u32, -) -> io::Result { - if argv.is_empty() { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty argv")); - } - - validate_cwd(cwd)?; - let argv_buf = serialize_null_separated(argv)?; - let envp_buf = serialize_env(env)?; - - // Create stdout pipe - let (stdout_read, stdout_write) = wasi_ext::pipe().map_err(errno_to_io_error)?; - - // Create stderr pipe - let (stderr_read, stderr_write) = wasi_ext::pipe().map_err(|e| { - fd_close(stdout_read); - fd_close(stdout_write); - errno_to_io_error(e) - })?; - - // Spawn child with pipe-captured stdout/stderr and caller-selected stdin. - let result = wasi_ext::spawn( - &argv_buf, - &envp_buf, - stdin_fd, - stdout_write, - stderr_write, - cwd.as_bytes(), - ); - - // Close write ends in parent (POSIX close-after-fork) - fd_close(stdout_write); - fd_close(stderr_write); - - match result { - Ok(pid) => Ok(WasiChild { - pid, - stdout_fd: Some(stdout_read), - stderr_fd: Some(stderr_read), - exited: false, - }), - Err(errno) => { - fd_close(stdout_read); - fd_close(stderr_read); - Err(errno_to_io_error(errno)) - } - } -} - -/// Spawn a child process with pipe-captured stdout and stderr. -/// -/// Creates pipes for stdout/stderr, spawns the child via host_process FFI, -/// and returns a `WasiChild` handle. The parent's stdin is inherited. -/// -/// # Arguments -/// * `argv` - Command and arguments (argv[0] is the program name) -/// * `env` - Environment variable pairs (empty inherits parent env via host) -/// * `cwd` - Working directory for the child -pub fn spawn_child(argv: &[&str], env: &[(&str, &str)], cwd: &str) -> io::Result { - spawn_child_with_stdin_fd(argv, env, cwd, 0) -} - -/// Spawn a child process with stdin ignored and stdout/stderr captured. -/// -/// This is appropriate for agent tool calls or other subprocesses that must not -/// inherit an interactive control stream from the parent process. -pub fn spawn_child_ignore_stdin( - argv: &[&str], - env: &[(&str, &str)], - cwd: &str, -) -> io::Result { - spawn_child_with_stdin_fd(argv, env, cwd, u32::MAX) -} - -/// Spawn a child process inheriting all stdio (no pipe capture). -/// -/// Useful for interactive commands where output should go directly to -/// the parent's terminal. -pub fn spawn_child_inherit( - argv: &[&str], - env: &[(&str, &str)], - cwd: &str, -) -> io::Result { - if argv.is_empty() { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "empty argv")); - } - - validate_cwd(cwd)?; - let argv_buf = serialize_null_separated(argv)?; - let envp_buf = serialize_env(env)?; - - let pid = wasi_ext::spawn( - &argv_buf, - &envp_buf, - 0, - 1, - 2, // inherit all stdio - cwd.as_bytes(), - ) - .map_err(errno_to_io_error)?; - - Ok(WasiChild { - pid, - stdout_fd: None, - stderr_fd: None, - exited: false, - }) -} - -impl WasiChild { - /// Get the child's virtual PID. - pub fn pid(&self) -> u32 { - self.pid - } - - /// Read from the child's stdout pipe. - /// - /// Returns 0 bytes when the pipe is closed (child exited or closed stdout). - pub fn read_stdout(&self, buf: &mut [u8]) -> io::Result { - match self.stdout_fd { - Some(fd) => fd_read(fd, buf), - None => Ok(0), - } - } - - /// Read from the child's stderr pipe. - /// - /// Returns 0 bytes when the pipe is closed (child exited or closed stderr). - pub fn read_stderr(&self, buf: &mut [u8]) -> io::Result { - match self.stderr_fd { - Some(fd) => fd_read(fd, buf), - None => Ok(0), - } - } - - /// Wait for the child to exit. Returns the exit code. - /// - /// Blocks via host_process_waitpid (Atomics.wait on host side). - pub fn wait(&mut self) -> io::Result { - if self.exited { - return Err(io::Error::new(io::ErrorKind::Other, "already waited")); - } - - let (status, _actual_pid) = wasi_ext::waitpid(self.pid, 0).map_err(errno_to_io_error)?; - - self.exited = true; - - // Decode exit status using bash 128+signal convention - // Normal exit: status is the exit code directly - // Signal kill: status is 128 + signal number - Ok(status as i32) - } - - /// Send a signal to the child process. - /// - /// Common signals: SIGTERM (15), SIGKILL (9). - pub fn kill(&mut self, signal: u32) -> io::Result<()> { - wasi_ext::kill(self.pid, signal).map_err(errno_to_io_error) - } - - /// Send SIGTERM to the child process. - pub fn terminate(&mut self) -> io::Result<()> { - self.kill(15) - } - - fn kill_and_reap(&mut self) { - if self.exited { - return; - } - - let _ = wasi_ext::kill(self.pid, 9); - if wasi_ext::waitpid(self.pid, 0).is_ok() { - self.exited = true; - } - } - - fn close_output_fds(&mut self) { - if let Some(fd) = self.stdout_fd.take() { - fd_close(fd); - } - if let Some(fd) = self.stderr_fd.take() { - fd_close(fd); - } - } - - /// Read all stdout and stderr, then wait for exit. - /// - /// Drains readable stdout and stderr events until both streams close, then waits. - pub fn consume_output(&mut self) -> io::Result { - let stdout_fd = self.stdout_fd; - let stderr_fd = self.stderr_fd; - let (stdout, stderr) = - read_captured_streams(stdout_fd, stderr_fd, fd_read, wait_readable_streams, || { - self.kill_and_reap() - })?; - - let wait_result = self.wait(); - let exit_code = wait_or_cleanup(wait_result, || self.kill_and_reap())?; - - Ok(WasiOutput { - stdout, - stderr, - exit_code, - }) - } -} - -impl Drop for WasiChild { - fn drop(&mut self) { - self.kill_and_reap(); - self.close_output_fds(); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn rejects_interior_nul_in_arguments() { - let err = serialize_null_separated(&["echo", "a\0b"]).unwrap_err(); - - assert_eq!(err.kind(), io::ErrorKind::InvalidInput); - } - - #[test] - fn rejects_invalid_environment_keys() { - let err = serialize_env(&[("A=B", "value")]).unwrap_err(); - - assert_eq!(err.kind(), io::ErrorKind::InvalidInput); - } - - #[test] - fn rejects_oversized_serialized_data() { - let oversized = "x".repeat(MAX_SERIALIZED_BYTES + 1); - let err = serialize_null_separated(&[&oversized]).unwrap_err(); - - assert_eq!(err.kind(), io::ErrorKind::InvalidInput); - } - - #[test] - fn rejects_oversized_captured_stream() { - let mut output = vec![b'x'; 8]; - let err = append_captured_stream_with_limit(&mut output, b"y", 8).unwrap_err(); - - assert_eq!(err.kind(), io::ErrorKind::InvalidData); - assert_eq!(output.len(), 8); - } - - #[test] - fn capture_helper_cleans_up_on_output_limit() { - let mut reads = 0; - let mut cleanup_calls = 0; - let err = read_captured_streams_with_limit( - Some(7), - None, - |_fd, buf| { - reads += 1; - buf[..4].copy_from_slice(b"xxxx"); - Ok(4) - }, - |stdout_fd, stderr_fd| { - assert_eq!(stdout_fd, Some(7)); - assert_eq!(stderr_fd, None); - Ok([Some(CapturedStream::Stdout), None]) - }, - || cleanup_calls += 1, - 8, - ) - .unwrap_err(); - - assert_eq!(err.kind(), io::ErrorKind::InvalidData); - assert_eq!(reads, 3); - assert_eq!(cleanup_calls, 1); - } - - #[test] - fn capture_helper_cleans_up_on_read_error() { - let mut cleanup_calls = 0; - let err = read_captured_streams_with_limit( - Some(7), - None, - |_fd, _buf| Err(io::Error::new(io::ErrorKind::PermissionDenied, "boom")), - |_stdout_fd, _stderr_fd| Ok([Some(CapturedStream::Stdout), None]), - || cleanup_calls += 1, - 8, - ) - .unwrap_err(); - - assert_eq!(err.kind(), io::ErrorKind::PermissionDenied); - assert_eq!(cleanup_calls, 1); - } - - #[test] - fn capture_helper_interleaves_stdout_and_stderr() { - let mut ready_calls = 0; - let mut stdout_reads = 0; - let mut stderr_reads = 0; - let (stdout, stderr) = read_captured_streams_with_limit( - Some(1), - Some(2), - |fd, buf| match fd { - 1 => { - stdout_reads += 1; - if stdout_reads > 1 { - return Ok(0); - } - buf[..3].copy_from_slice(b"out"); - Ok(3) - } - 2 => { - stderr_reads += 1; - if stderr_reads > 1 { - return Ok(0); - } - buf[..3].copy_from_slice(b"err"); - Ok(3) - } - _ => unreachable!(), - }, - |stdout_fd, stderr_fd| { - ready_calls += 1; - match ready_calls { - 1 => { - assert_eq!(stdout_fd, Some(1)); - assert_eq!(stderr_fd, Some(2)); - Ok([Some(CapturedStream::Stderr), None]) - } - 2 => { - assert_eq!(stdout_fd, Some(1)); - assert_eq!(stderr_fd, Some(2)); - Ok([Some(CapturedStream::Stdout), None]) - } - 3 => Ok([Some(CapturedStream::Stderr), None]), - 4 => Ok([Some(CapturedStream::Stdout), None]), - _ => unreachable!(), - } - }, - || unreachable!(), - 16, - ) - .unwrap(); - - assert_eq!(stdout, b"out"); - assert_eq!(stderr, b"err"); - assert_eq!(ready_calls, 4); - } - - #[test] - fn wait_error_runs_cleanup() { - let mut cleanup_calls = 0; - let err = wait_or_cleanup( - Err(io::Error::new(io::ErrorKind::NotFound, "missing")), - || cleanup_calls += 1, - ) - .unwrap_err(); - - assert_eq!(err.kind(), io::ErrorKind::NotFound); - assert_eq!(cleanup_calls, 1); - } -} diff --git a/registry/native/crates/libs/yq/Cargo.toml b/registry/native/crates/libs/yq/Cargo.toml deleted file mode 100644 index ec7336b20c..0000000000 --- a/registry/native/crates/libs/yq/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "secureexec-yq" -version.workspace = true -edition.workspace = true -license.workspace = true -description = "yq (YAML/XML/TOML/JSON processor) implementation for secure-exec standalone binaries" - -[dependencies] -jaq-core = "2.2" -jaq-std = "2.1" -jaq-json = { version = "1.1", features = ["serde_json"] } -serde_json = "1" -serde_yaml = "0.9" -toml = "0.8" -quick-xml = "0.37" diff --git a/registry/native/crates/libs/yq/src/lib.rs b/registry/native/crates/libs/yq/src/lib.rs deleted file mode 100644 index 871e3d7b85..0000000000 --- a/registry/native/crates/libs/yq/src/lib.rs +++ /dev/null @@ -1,1004 +0,0 @@ -//! yq — YAML/XML/TOML/JSON processor using jaq filter engine. -//! -//! Converts input to JSON, runs jaq filter, converts output to requested format. -//! Reuses jaq-core/jaq-std/jaq-json (same engine as jq command). - -use std::ffi::OsString; -use std::fmt; -use std::io::{self, Read, Write}; - -use jaq_core::load::{Arena, File, Loader}; -use jaq_core::{Compiler, Ctx, RcIter}; -use jaq_json::Val; - -const MAX_INPUT_BYTES: usize = 16 * 1024 * 1024; -const MAX_FORMATTED_OUTPUT_BYTES: usize = 16 * 1024 * 1024; -const MAX_OUTPUT_VALUES: usize = 100_000; -const MAX_XML_DEPTH: usize = 256; -const MAX_XML_NODES: usize = 100_000; -const MAX_XML_ATTRIBUTES_PER_ELEMENT: usize = 4096; -const MAX_XML_TEXT_BYTES: usize = 16 * 1024 * 1024; - -#[derive(Clone, Copy, PartialEq)] -enum Format { - Yaml, - Json, - Toml, - Xml, -} - -struct YqOptions { - filter: String, - input_format: Option, - output_format: Option, - raw_output: bool, - compact: bool, - null_input: bool, - slurp: bool, -} - -/// Entry point for yq command. -pub fn main(args: Vec) -> i32 { - let str_args: Vec = args - .iter() - .skip(1) - .map(|a| a.to_string_lossy().to_string()) - .collect(); - - match run_yq(&str_args) { - Ok(code) => code, - Err(msg) => { - eprintln!("yq: {}", msg); - 2 - } - } -} - -fn parse_format(s: &str) -> Result { - match s { - "yaml" | "y" => Ok(Format::Yaml), - "json" | "j" => Ok(Format::Json), - "toml" | "t" => Ok(Format::Toml), - "xml" | "x" => Ok(Format::Xml), - _ => Err(format!( - "unknown format: {} (expected yaml, json, toml, xml)", - s - )), - } -} - -fn parse_args(args: &[String]) -> Result { - let mut opts = YqOptions { - filter: String::new(), - input_format: None, - output_format: None, - raw_output: false, - compact: false, - null_input: false, - slurp: false, - }; - - let mut filter_set = false; - let mut i = 0; - - while i < args.len() { - let arg = &args[i]; - - if arg == "--" { - break; - } - - if arg == "-p" || arg == "--input-format" { - i += 1; - if i >= args.len() { - return Err("-p requires a format argument".to_string()); - } - opts.input_format = Some(parse_format(&args[i])?); - } else if arg == "-o" || arg == "--output-format" { - i += 1; - if i >= args.len() { - return Err("-o requires a format argument".to_string()); - } - opts.output_format = Some(parse_format(&args[i])?); - } else if arg == "-r" || arg == "--raw-output" { - opts.raw_output = true; - } else if arg == "-c" || arg == "--compact-output" { - opts.compact = true; - } else if arg == "-n" || arg == "--null-input" { - opts.null_input = true; - } else if arg == "-s" || arg == "--slurp" { - opts.slurp = true; - } else if arg.starts_with('-') && arg.len() > 1 && !arg.starts_with("--") { - // Combined short flags like -rc - let flags = &arg[1..]; - let mut chars = flags.chars().peekable(); - while let Some(c) = chars.next() { - match c { - 'r' => opts.raw_output = true, - 'c' => opts.compact = true, - 'n' => opts.null_input = true, - 's' => opts.slurp = true, - 'p' => { - let rest: String = chars.collect(); - if !rest.is_empty() { - opts.input_format = Some(parse_format(&rest)?); - } else { - i += 1; - if i >= args.len() { - return Err("-p requires a format argument".to_string()); - } - opts.input_format = Some(parse_format(&args[i])?); - } - break; - } - 'o' => { - let rest: String = chars.collect(); - if !rest.is_empty() { - opts.output_format = Some(parse_format(&rest)?); - } else { - i += 1; - if i >= args.len() { - return Err("-o requires a format argument".to_string()); - } - opts.output_format = Some(parse_format(&args[i])?); - } - break; - } - _ => return Err(format!("unknown option: -{}", c)), - } - } - } else if !filter_set { - opts.filter = arg.clone(); - filter_set = true; - } else { - return Err(format!("unexpected argument: {}", arg)); - } - - i += 1; - } - - if !filter_set { - opts.filter = ".".to_string(); - } - - Ok(opts) -} - -fn detect_format(input: &str) -> Format { - let trimmed = input.trim_start(); - - // JSON: starts with { or [ - if trimmed.starts_with('{') || trimmed.starts_with('[') { - if serde_json::from_str::(trimmed).is_ok() { - return Format::Json; - } - } - - // XML: starts with < (including (trimmed).is_ok() - { - return Format::Toml; - } - } - - // Default: YAML - Format::Yaml -} - -fn parse_input(input: &str, format: Format) -> Result { - match format { - Format::Json => serde_json::from_str(input).map_err(|e| format!("invalid JSON: {}", e)), - Format::Yaml => serde_yaml::from_str(input).map_err(|e| format!("invalid YAML: {}", e)), - Format::Toml => { - let toml_val: toml::Value = - toml::from_str(input).map_err(|e| format!("invalid TOML: {}", e))?; - toml_to_json(toml_val) - } - Format::Xml => xml_to_json(input), - } -} - -fn toml_to_json(val: toml::Value) -> Result { - Ok(match val { - toml::Value::String(s) => serde_json::Value::String(s), - toml::Value::Integer(i) => serde_json::Value::Number(serde_json::Number::from(i)), - toml::Value::Float(f) => serde_json::Number::from_f64(f) - .map(serde_json::Value::Number) - .unwrap_or(serde_json::Value::Null), - toml::Value::Boolean(b) => serde_json::Value::Bool(b), - toml::Value::Datetime(dt) => serde_json::Value::String(dt.to_string()), - toml::Value::Array(arr) => { - let items: Result, _> = arr.into_iter().map(toml_to_json).collect(); - serde_json::Value::Array(items?) - } - toml::Value::Table(table) => { - let mut map = serde_json::Map::new(); - for (k, v) in table { - map.insert(k, toml_to_json(v)?); - } - serde_json::Value::Object(map) - } - }) -} - -fn json_to_toml(val: &serde_json::Value) -> Result { - Ok(match val { - serde_json::Value::Null => return Err("TOML does not support null values".to_string()), - serde_json::Value::Bool(b) => toml::Value::Boolean(*b), - serde_json::Value::Number(n) => { - if let Some(i) = n.as_i64() { - toml::Value::Integer(i) - } else if let Some(f) = n.as_f64() { - toml::Value::Float(f) - } else { - return Err("unsupported number for TOML".to_string()); - } - } - serde_json::Value::String(s) => toml::Value::String(s.clone()), - serde_json::Value::Array(arr) => { - let items: Result, _> = arr.iter().map(json_to_toml).collect(); - toml::Value::Array(items?) - } - serde_json::Value::Object(map) => { - let mut table = toml::map::Map::new(); - for (k, v) in map { - table.insert(k.clone(), json_to_toml(v)?); - } - toml::Value::Table(table) - } - }) -} - -// --- XML parsing --- - -fn xml_to_json(input: &str) -> Result { - use quick_xml::events::Event; - use quick_xml::Reader; - - struct StackEntry { - name: String, - children: serde_json::Map, - text: String, - } - - let mut reader = Reader::from_str(input); - let mut stack: Vec = Vec::new(); - let mut root = serde_json::Map::new(); - let mut nodes = 0usize; - - loop { - match reader.read_event() { - Ok(Event::Start(ref e)) => { - count_xml_node(&mut nodes)?; - if stack.len() >= MAX_XML_DEPTH { - return Err("XML exceeds maximum nesting depth".to_string()); - } - let name = String::from_utf8_lossy(e.name().as_ref()).to_string(); - let mut children = serde_json::Map::new(); - let mut attr_count = 0usize; - for attr in e.attributes() { - let attr = attr.map_err(|e| format!("invalid XML attribute: {}", e))?; - attr_count += 1; - if attr_count > MAX_XML_ATTRIBUTES_PER_ELEMENT { - return Err("XML element has too many attributes".to_string()); - } - let key = format!("@{}", String::from_utf8_lossy(attr.key.as_ref())); - let val = String::from_utf8_lossy(&attr.value).to_string(); - children.insert(key, serde_json::Value::String(val)); - } - stack.push(StackEntry { - name, - children, - text: String::new(), - }); - } - Ok(Event::End(_)) => { - let entry = stack.pop().ok_or("unexpected closing tag")?; - let text = entry.text.trim().to_string(); - - let value = if entry.children.is_empty() && text.is_empty() { - serde_json::Value::Null - } else if entry.children.is_empty() { - serde_json::Value::String(text) - } else { - let mut obj = entry.children; - if !text.is_empty() { - obj.insert("#text".to_string(), serde_json::Value::String(text)); - } - serde_json::Value::Object(obj) - }; - - let target = if let Some(parent) = stack.last_mut() { - &mut parent.children - } else { - &mut root - }; - - insert_or_array(target, entry.name, value); - } - Ok(Event::Empty(ref e)) => { - count_xml_node(&mut nodes)?; - if stack.len() >= MAX_XML_DEPTH { - return Err("XML exceeds maximum nesting depth".to_string()); - } - let name = String::from_utf8_lossy(e.name().as_ref()).to_string(); - let mut attrs = serde_json::Map::new(); - let mut attr_count = 0usize; - for attr in e.attributes() { - let attr = attr.map_err(|e| format!("invalid XML attribute: {}", e))?; - attr_count += 1; - if attr_count > MAX_XML_ATTRIBUTES_PER_ELEMENT { - return Err("XML element has too many attributes".to_string()); - } - let key = format!("@{}", String::from_utf8_lossy(attr.key.as_ref())); - let val = String::from_utf8_lossy(&attr.value).to_string(); - attrs.insert(key, serde_json::Value::String(val)); - } - - let value = if attrs.is_empty() { - serde_json::Value::Null - } else { - serde_json::Value::Object(attrs) - }; - - let target = if let Some(parent) = stack.last_mut() { - &mut parent.children - } else { - &mut root - }; - - insert_or_array(target, name, value); - } - Ok(Event::Text(ref e)) => { - if let Some(entry) = stack.last_mut() { - let text = e - .unescape() - .map_err(|e| format!("invalid XML text: {}", e))?; - let next_len = entry - .text - .len() - .checked_add(text.len()) - .ok_or("XML text length overflowed")?; - if next_len > MAX_XML_TEXT_BYTES { - return Err("XML text exceeds size limit".to_string()); - } - entry.text.push_str(&text); - } - } - Ok(Event::Eof) => break, - Ok(_) => {} // Skip PI, Comment, Decl, DocType, CData - Err(e) => return Err(format!("invalid XML: {}", e)), - } - } - - if !stack.is_empty() { - return Err("unexpected end of XML input".to_string()); - } - - Ok(serde_json::Value::Object(root)) -} - -fn count_xml_node(nodes: &mut usize) -> Result<(), String> { - *nodes = nodes.checked_add(1).ok_or("XML node count overflowed")?; - if *nodes > MAX_XML_NODES { - return Err("XML contains too many nodes".to_string()); - } - Ok(()) -} - -fn record_output_value(output_count: &mut usize) -> Result<(), String> { - *output_count = output_count - .checked_add(1) - .ok_or("output count overflowed")?; - if *output_count > MAX_OUTPUT_VALUES { - return Err("too many output values".to_string()); - } - Ok(()) -} - -fn read_limited_string(reader: R) -> Result { - let mut input = String::new(); - reader - .take((MAX_INPUT_BYTES + 1) as u64) - .read_to_string(&mut input) - .map_err(|e| format!("failed to read stdin: {}", e))?; - if input.len() > MAX_INPUT_BYTES { - return Err("stdin exceeds size limit".to_string()); - } - Ok(input) -} - -fn insert_or_array( - map: &mut serde_json::Map, - key: String, - value: serde_json::Value, -) { - if let Some(existing) = map.get_mut(&key) { - match existing { - serde_json::Value::Array(arr) => arr.push(value), - _ => { - let prev = existing.clone(); - *existing = serde_json::Value::Array(vec![prev, value]); - } - } - } else { - map.insert(key, value); - } -} - -// --- XML output --- - -fn json_to_xml(val: &serde_json::Value) -> Result { - use quick_xml::Writer; - - let mut writer = Writer::new(LimitedBytes::new(MAX_FORMATTED_OUTPUT_BYTES)); - - match val { - serde_json::Value::Object(map) => { - for (key, value) in map { - write_xml_element(&mut writer, key, value) - .map_err(|e| format!("XML write error: {}", e))?; - } - } - _ => { - write_xml_element(&mut writer, "root", val) - .map_err(|e| format!("XML write error: {}", e))?; - } - } - - writer - .into_inner() - .into_string() - .map_err(|e| format!("XML encoding error: {}", e)) -} - -fn write_xml_element( - writer: &mut quick_xml::Writer, - name: &str, - val: &serde_json::Value, -) -> Result<(), quick_xml::Error> { - use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; - - match val { - serde_json::Value::Object(map) => { - let mut elem = BytesStart::new(name); - for (k, v) in map { - if let Some(attr_name) = k.strip_prefix('@') { - if let serde_json::Value::String(s) = v { - elem.push_attribute((attr_name, s.as_str())); - } - } - } - writer.write_event(Event::Start(elem))?; - - if let Some(serde_json::Value::String(text)) = map.get("#text") { - writer.write_event(Event::Text(BytesText::new(text)))?; - } - - for (k, v) in map { - if k.starts_with('@') || k == "#text" { - continue; - } - match v { - serde_json::Value::Array(arr) => { - for item in arr { - write_xml_element(writer, k, item)?; - } - } - _ => write_xml_element(writer, k, v)?, - } - } - - writer.write_event(Event::End(BytesEnd::new(name)))?; - } - serde_json::Value::Array(arr) => { - for item in arr { - write_xml_element(writer, name, item)?; - } - } - serde_json::Value::String(s) => { - writer.write_event(Event::Start(BytesStart::new(name)))?; - writer.write_event(Event::Text(BytesText::new(s)))?; - writer.write_event(Event::End(BytesEnd::new(name)))?; - } - serde_json::Value::Number(n) => { - let s = n.to_string(); - writer.write_event(Event::Start(BytesStart::new(name)))?; - writer.write_event(Event::Text(BytesText::new(&s)))?; - writer.write_event(Event::End(BytesEnd::new(name)))?; - } - serde_json::Value::Bool(b) => { - let s = if *b { "true" } else { "false" }; - writer.write_event(Event::Start(BytesStart::new(name)))?; - writer.write_event(Event::Text(BytesText::new(s)))?; - writer.write_event(Event::End(BytesEnd::new(name)))?; - } - serde_json::Value::Null => { - writer.write_event(Event::Empty(BytesStart::new(name)))?; - } - } - Ok(()) -} - -// --- Output formatting --- - -fn format_val_output(val: &Val, opts: &YqOptions, out_format: Format) -> Result { - let mut compact = LimitedString::new(MAX_FORMATTED_OUTPUT_BYTES); - fmt::write(&mut compact, format_args!("{}", val)) - .map_err(|_| "formatted output exceeds size limit".to_string())?; - let compact_str = compact.into_string(); - - // Raw output: unquote strings - if opts.raw_output { - if compact_str.starts_with('"') && compact_str.ends_with('"') && compact_str.len() >= 2 { - if let Ok(unescaped) = serde_json::from_str::(&compact_str) { - ensure_formatted_output_limit(unescaped.len())?; - return Ok(unescaped); - } - } - } - - let json_val: serde_json::Value = - serde_json::from_str(&compact_str).unwrap_or(serde_json::Value::String(compact_str)); - - let output = format_json_as(out_format, &json_val, opts.compact)?; - ensure_formatted_output_limit(output.len())?; - Ok(output) -} - -fn ensure_formatted_output_limit(len: usize) -> Result<(), String> { - if len > MAX_FORMATTED_OUTPUT_BYTES { - return Err("formatted output exceeds size limit".to_string()); - } - Ok(()) -} - -struct LimitedString { - inner: String, - limit: usize, -} - -impl LimitedString { - fn new(limit: usize) -> Self { - Self { - inner: String::new(), - limit, - } - } - - fn into_string(self) -> String { - self.inner - } - - fn write_str(&mut self, s: &str) -> Result<(), String> { - let next_len = self - .inner - .len() - .checked_add(s.len()) - .ok_or("formatted output length overflowed")?; - if next_len > self.limit { - return Err("formatted output exceeds size limit".to_string()); - } - self.inner.push_str(s); - Ok(()) - } - - fn write_char(&mut self, ch: char) -> Result<(), String> { - let mut buf = [0u8; 4]; - self.write_str(ch.encode_utf8(&mut buf)) - } -} - -impl fmt::Write for LimitedString { - fn write_str(&mut self, s: &str) -> fmt::Result { - LimitedString::write_str(self, s).map_err(|_| fmt::Error) - } -} - -struct LimitedBytes { - inner: Vec, - limit: usize, -} - -impl LimitedBytes { - fn new(limit: usize) -> Self { - Self { - inner: Vec::new(), - limit, - } - } - - fn into_string(self) -> Result { - String::from_utf8(self.inner) - } -} - -impl io::Write for LimitedBytes { - fn write(&mut self, buf: &[u8]) -> io::Result { - let next_len = self - .inner - .len() - .checked_add(buf.len()) - .ok_or_else(|| io::Error::other("formatted output length overflowed"))?; - if next_len > self.limit { - return Err(io::Error::other("formatted output exceeds size limit")); - } - self.inner.extend_from_slice(buf); - Ok(buf.len()) - } - - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - -fn format_json_as( - format: Format, - val: &serde_json::Value, - compact: bool, -) -> Result { - match format { - Format::Json => { - let mut out = LimitedBytes::new(MAX_FORMATTED_OUTPUT_BYTES); - if compact { - serde_json::to_writer(&mut out, val) - .map_err(|e| format!("JSON output error: {}", e))?; - } else { - serde_json::to_writer_pretty(&mut out, val) - .map_err(|e| format!("JSON output error: {}", e))?; - } - out.into_string() - .map_err(|e| format!("JSON encoding error: {}", e)) - } - Format::Yaml => { - let mut out = LimitedBytes::new(MAX_FORMATTED_OUTPUT_BYTES); - serde_yaml::to_writer(&mut out, val) - .map_err(|e| format!("YAML output error: {}", e))?; - let s = out - .into_string() - .map_err(|e| format!("YAML encoding error: {}", e))?; - // Strip leading "---\n" and trailing newline for cleaner output - let s = s.strip_prefix("---\n").unwrap_or(&s); - let s = s.strip_suffix('\n').unwrap_or(s); - Ok(s.to_string()) - } - Format::Toml => json_to_toml_bounded(val), - Format::Xml => json_to_xml(val), - } -} - -fn json_to_toml_bounded(val: &serde_json::Value) -> Result { - let toml_val = json_to_toml(val)?; - let mut out = LimitedString::new(MAX_FORMATTED_OUTPUT_BYTES); - write_toml_document(&mut out, &toml_val)?; - let s = out.into_string(); - Ok(s.strip_suffix('\n').unwrap_or(&s).to_string()) -} - -fn write_toml_document(out: &mut LimitedString, val: &toml::Value) -> Result<(), String> { - match val { - toml::Value::Table(table) => write_toml_table(out, &mut Vec::new(), table), - other => write_toml_inline(out, other), - } -} - -fn write_toml_table( - out: &mut LimitedString, - path: &mut Vec, - table: &toml::map::Map, -) -> Result<(), String> { - for (key, value) in table { - if matches!(value, toml::Value::Table(_)) { - continue; - } - write_toml_key(out, key)?; - out.write_str(" = ")?; - write_toml_inline(out, value)?; - out.write_char('\n')?; - } - - for (key, value) in table { - let toml::Value::Table(child) = value else { - continue; - }; - if !path.is_empty() || table_has_scalar_entries(child) { - out.write_char('\n')?; - path.push(key.clone()); - out.write_char('[')?; - write_toml_path(out, path)?; - out.write_str("]\n")?; - write_toml_table(out, path, child)?; - path.pop(); - } else { - path.push(key.clone()); - write_toml_table(out, path, child)?; - path.pop(); - } - } - - Ok(()) -} - -fn table_has_scalar_entries(table: &toml::map::Map) -> bool { - table - .values() - .any(|value| !matches!(value, toml::Value::Table(_))) -} - -fn write_toml_path(out: &mut LimitedString, path: &[String]) -> Result<(), String> { - for (i, key) in path.iter().enumerate() { - if i > 0 { - out.write_char('.')?; - } - write_toml_key(out, key)?; - } - Ok(()) -} - -fn write_toml_key(out: &mut LimitedString, key: &str) -> Result<(), String> { - if !key.is_empty() - && key - .bytes() - .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-') - { - out.write_str(key)?; - } else { - write_toml_string(out, key)?; - } - Ok(()) -} - -fn write_toml_inline(out: &mut LimitedString, val: &toml::Value) -> Result<(), String> { - match val { - toml::Value::String(s) => write_toml_string(out, s), - toml::Value::Integer(i) => out.write_str(&i.to_string()), - toml::Value::Float(f) => out.write_str(&f.to_string()), - toml::Value::Boolean(b) => out.write_str(if *b { "true" } else { "false" }), - toml::Value::Datetime(dt) => out.write_str(&dt.to_string()), - toml::Value::Array(arr) => { - out.write_char('[')?; - for (i, item) in arr.iter().enumerate() { - if i > 0 { - out.write_str(", ")?; - } - write_toml_inline(out, item)?; - } - out.write_char(']') - } - toml::Value::Table(table) => { - out.write_str("{ ")?; - for (i, (key, value)) in table.iter().enumerate() { - if i > 0 { - out.write_str(", ")?; - } - write_toml_key(out, key)?; - out.write_str(" = ")?; - write_toml_inline(out, value)?; - } - out.write_str(" }") - } - } -} - -fn write_toml_string(out: &mut LimitedString, s: &str) -> Result<(), String> { - out.write_char('"')?; - for ch in s.chars() { - match ch { - '"' => out.write_str("\\\"")?, - '\\' => out.write_str("\\\\")?, - '\n' => out.write_str("\\n")?, - '\r' => out.write_str("\\r")?, - '\t' => out.write_str("\\t")?, - '\u{08}' => out.write_str("\\b")?, - '\u{0c}' => out.write_str("\\f")?, - ch if ch.is_control() => out.write_str(&format!("\\u{:04X}", ch as u32))?, - ch => out.write_char(ch)?, - } - } - out.write_char('"') -} - -// --- Main logic --- - -fn run_yq(args: &[String]) -> Result { - let opts = parse_args(args)?; - - // Read input - let stdin_data = if opts.null_input { - String::new() - } else { - read_limited_string(io::stdin())? - }; - - // Determine input format - let in_format = opts.input_format.unwrap_or_else(|| { - if opts.null_input { - Format::Yaml - } else { - detect_format(&stdin_data) - } - }); - - // Default output format: YAML for YAML input, otherwise matches input - let out_format = opts.output_format.unwrap_or(in_format); - - // Parse input to JSON, then convert to jaq Val - let inputs = if opts.null_input { - vec![Val::from(serde_json::Value::Null)] - } else { - let json_val = parse_input(&stdin_data, in_format)?; - if opts.slurp { - match json_val { - serde_json::Value::Array(_) => vec![Val::from(json_val)], - _ => vec![Val::from(serde_json::Value::Array(vec![json_val]))], - } - } else { - vec![Val::from(json_val)] - } - }; - - // Compile jaq filter (same engine as jq) - let loader = Loader::new(jaq_std::defs().chain(jaq_json::defs())); - let arena = Arena::default(); - let program = File { - code: opts.filter.as_str(), - path: (), - }; - let modules = loader - .load(&arena, program) - .map_err(|errs| format!("parse error: {:?}", errs))?; - - let filter = Compiler::default() - .with_funs(jaq_std::funs().chain(jaq_json::funs())) - .compile(modules) - .map_err(|errs| format!("compile error: {:?}", errs))?; - - // Execute filter and output - let empty_inputs = RcIter::new(core::iter::empty()); - let stdout = io::stdout(); - let mut out = stdout.lock(); - let mut output_count = 0usize; - - for input in inputs { - let ctx = Ctx::new(core::iter::empty(), &empty_inputs); - let results = filter.run((ctx, input)); - - for result in results { - match result { - Ok(val) => { - record_output_value(&mut output_count)?; - let s = format_val_output(&val, &opts, out_format)?; - writeln!(out, "{}", s).map_err(|e| format!("failed to write stdout: {}", e))?; - } - Err(e) => { - eprintln!("yq: error: {}", e); - return Ok(5); - } - } - } - } - - out.flush() - .map_err(|e| format!("failed to flush stdout: {}", e))?; - - Ok(0) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn xml_depth_limit_rejects_deep_input() { - let mut input = String::new(); - for i in 0..=MAX_XML_DEPTH { - input.push_str(&format!("")); - } - for i in (0..=MAX_XML_DEPTH).rev() { - input.push_str(&format!("")); - } - - let err = xml_to_json(&input).unwrap_err(); - - assert!(err.contains("nesting depth")); - } - - #[test] - fn xml_depth_limit_rejects_deep_empty_input() { - let mut input = String::new(); - for i in 0..MAX_XML_DEPTH { - input.push_str(&format!("")); - } - input.push_str(""); - for i in (0..MAX_XML_DEPTH).rev() { - input.push_str(&format!("")); - } - - let err = xml_to_json(&input).unwrap_err(); - - assert!(err.contains("nesting depth")); - } - - #[test] - fn xml_node_limit_rejects_many_elements() { - let mut nodes = MAX_XML_NODES; - - let err = count_xml_node(&mut nodes).unwrap_err(); - - assert!(err.contains("too many nodes")); - } - - #[test] - fn xml_text_limit_rejects_large_text() { - let input = format!("{}", "x".repeat(MAX_XML_TEXT_BYTES + 1)); - - let err = xml_to_json(&input).unwrap_err(); - - assert!(err.contains("text exceeds")); - } - - #[test] - fn output_limit_rejects_too_many_values() { - let mut count = MAX_OUTPUT_VALUES; - - let err = record_output_value(&mut count).unwrap_err(); - - assert!(err.contains("too many output values")); - } - - #[test] - fn formatted_output_limit_rejects_large_output() { - let err = ensure_formatted_output_limit(MAX_FORMATTED_OUTPUT_BYTES + 1).unwrap_err(); - - assert!(err.contains("formatted output")); - } - - #[test] - fn limited_bytes_rejects_large_serializer_write() { - let mut output = LimitedBytes::new(4); - - let err = output.write_all(b"hello").unwrap_err(); - - assert!(err.to_string().contains("formatted output")); - } - - #[test] - fn limited_toml_writer_rejects_large_value() { - let mut output = LimitedString::new(4); - let value = toml::Value::String("hello".to_string()); - - let err = write_toml_inline(&mut output, &value).unwrap_err(); - - assert!(err.contains("formatted output")); - } - - #[test] - fn input_limit_rejects_oversized_reader() { - let input = vec![b'x'; MAX_INPUT_BYTES + 1]; - - let err = read_limited_string(&input[..]).unwrap_err(); - - assert!(err.contains("stdin exceeds")); - } - - #[test] - fn xml_rejects_unclosed_elements() { - let err = xml_to_json("").unwrap_err(); - - assert!(err.contains("unexpected end")); - } - - #[test] - fn xml_rejects_invalid_text_escape() { - let err = xml_to_json("&bogus;").unwrap_err(); - - assert!(err.contains("invalid XML text")); - } -} diff --git a/registry/native/crates/wasi-ext/src/lib.rs b/registry/native/crates/wasi-ext/src/lib.rs deleted file mode 100644 index 89c404f268..0000000000 --- a/registry/native/crates/wasi-ext/src/lib.rs +++ /dev/null @@ -1,973 +0,0 @@ -//! Custom WASM import bindings for wasmVM host syscalls. -//! -//! Declares extern functions for `host_process`, `host_user`, and `host_net` -//! modules that the JS host runtime provides. These extend standard WASI with -//! process management, user/group identity, and TCP socket capabilities. -//! -//! Signatures match spec section 4.3. - -#![no_std] - -/// WASI-style errno type. 0 = success. -pub type Errno = u32; - -// WASI errno constants -pub const ERRNO_SUCCESS: Errno = 0; -pub const ERRNO_BADF: Errno = 8; -pub const ERRNO_INVAL: Errno = 28; -pub const ERRNO_NOSYS: Errno = 52; -pub const ERRNO_NOENT: Errno = 44; -pub const ERRNO_SRCH: Errno = 71; // No such process -pub const ERRNO_CHILD: Errno = 10; // No child processes -/// WASI `errno::again` (EAGAIN/EWOULDBLOCK). Returned by a non-blocking `recv` -/// when no data is currently available. -pub const ERRNO_AGAIN: Errno = 6; -const POLLFD_BYTES: usize = 8; - -/// `SOL_SOCKET` socket option level (matches the host_net shim's accepted level). -pub const SOL_SOCKET: u32 = 1; -/// `SO_RCVTIMEO` recv-timeout socket option name (64-bit timeval layout, which the -/// host_net shim parses as two little-endian `i64`s: seconds + microseconds). -pub const SO_RCVTIMEO: u32 = 20; -/// Size of the `timeval` struct the host_net shim expects for `SO_RCVTIMEO` -/// (two 64-bit fields: `tv_sec` + `tv_usec`). -const TIMEVAL_BYTES: usize = 16; - -fn checked_u32_len(len: usize) -> Result { - u32::try_from(len).map_err(|_| ERRNO_INVAL) -} - -fn validate_returned_len(len: u32, capacity: usize) -> Result { - match usize::try_from(len) { - Ok(len) if len <= capacity => Ok(len as u32), - _ => Err(ERRNO_INVAL), - } -} - -fn validate_poll_buffer_len(buffer_len: usize, nfds: u32) -> Result<(), Errno> { - let nfds = usize::try_from(nfds).map_err(|_| ERRNO_INVAL)?; - let expected = nfds.checked_mul(POLLFD_BYTES).ok_or(ERRNO_INVAL)?; - if buffer_len == expected { - Ok(()) - } else { - Err(ERRNO_INVAL) - } -} - -fn validate_poll_ready_count(ready: u32, nfds: u32) -> Result { - if ready <= nfds { - Ok(ready) - } else { - Err(ERRNO_INVAL) - } -} - -// ============================================================ -// host_process module — process management and FD operations -// ============================================================ - -#[link(wasm_import_module = "host_process")] -extern "C" { - /// Spawn a child process. - /// - /// Arguments are serialized as a byte buffer pointed to by `argv_ptr`/`argv_len`. - /// Environment is serialized similarly via `envp_ptr`/`envp_len`. - /// File descriptors `stdin_fd`, `stdout_fd`, `stderr_fd` are inherited. - /// Current working directory is passed as `cwd_ptr`/`cwd_len`. - /// On success, the child's virtual PID is written to `ret_pid`. - /// Returns errno. - fn proc_spawn( - argv_ptr: *const u8, - argv_len: u32, - envp_ptr: *const u8, - envp_len: u32, - stdin_fd: u32, - stdout_fd: u32, - stderr_fd: u32, - cwd_ptr: *const u8, - cwd_len: u32, - ret_pid: *mut u32, - ) -> Errno; - - /// Wait for a child process to exit. - /// - /// Blocks (via Atomics.wait on the host side) until the child exits. - /// `options` is reserved (pass 0). Exit status is written to `ret_status`. - /// The actual waited-for PID is written to `ret_pid` (important for pid=-1). - /// Returns errno. - fn proc_waitpid(pid: u32, options: u32, ret_status: *mut u32, ret_pid: *mut u32) -> Errno; - - /// Send a signal to a process. - /// - /// Only SIGTERM (15) and SIGKILL (9) are meaningful. - /// Returns errno. - fn proc_kill(pid: u32, signal: u32) -> Errno; - - /// Get the current process's virtual PID. - /// - /// Writes PID to `ret_pid`. Returns errno. - fn proc_getpid(ret_pid: *mut u32) -> Errno; - - /// Get the parent process's virtual PID. - /// - /// Writes parent PID to `ret_pid`. Returns errno. - fn proc_getppid(ret_pid: *mut u32) -> Errno; - - /// Create an anonymous pipe. - /// - /// Writes the read-end FD to `ret_read_fd` and write-end FD to `ret_write_fd`. - /// Returns errno. - fn fd_pipe(ret_read_fd: *mut u32, ret_write_fd: *mut u32) -> Errno; - - /// Duplicate a file descriptor. - /// - /// The new FD number is written to `ret_new_fd`. Returns errno. - fn fd_dup(fd: u32, ret_new_fd: *mut u32) -> Errno; - - /// Duplicate a file descriptor to a specific number. - /// - /// `old_fd` is duplicated to `new_fd`. If `new_fd` is already open, it is closed first. - /// Returns errno. - fn fd_dup2(old_fd: u32, new_fd: u32) -> Errno; - - /// Sleep for the specified number of milliseconds. - /// - /// Blocks via Atomics.wait on the host side. Returns errno. - fn sleep_ms(milliseconds: u32) -> Errno; - - /// Allocate a pseudo-terminal (PTY) master/slave pair. - /// - /// On success, the master FD is written to `ret_master_fd` and the slave FD - /// to `ret_slave_fd`. Both ends are installed in the process's kernel FD table. - /// Returns errno. - fn pty_open(ret_master_fd: *mut u32, ret_slave_fd: *mut u32) -> Errno; - - /// Register a signal handler disposition (POSIX sigaction). - /// - /// `signal` is the signal number (1-64). - /// `action` encodes the disposition: 0=SIG_DFL, 1=SIG_IGN, 2=user handler. - /// `mask_lo` / `mask_hi` encode the low/high 32 bits of sa_mask, and `flags` - /// carries the raw POSIX sa_flags bitmask. - /// When action=2, the C sysroot still holds the actual function pointer; the - /// kernel only needs the metadata that affects delivery semantics. - /// Returns errno. - fn proc_sigaction(signal: u32, action: u32, mask_lo: u32, mask_hi: u32, flags: u32) -> Errno; -} - -// ============================================================ -// host_user module — user/group identity and terminal detection -// ============================================================ - -#[link(wasm_import_module = "host_user")] -extern "C" { - /// Get the real user ID. Writes to `ret_uid`. Returns errno. - fn getuid(ret_uid: *mut u32) -> Errno; - - /// Get the real group ID. Writes to `ret_gid`. Returns errno. - fn getgid(ret_gid: *mut u32) -> Errno; - - /// Get the effective user ID. Writes to `ret_uid`. Returns errno. - fn geteuid(ret_uid: *mut u32) -> Errno; - - /// Get the effective group ID. Writes to `ret_gid`. Returns errno. - fn getegid(ret_gid: *mut u32) -> Errno; - - /// Check if a file descriptor refers to a terminal. - /// - /// Writes 1 (true) or 0 (false) to `ret_bool`. Returns errno. - fn isatty(fd: u32, ret_bool: *mut u32) -> Errno; - - /// Get passwd entry for a user ID. - /// - /// Serialized passwd string (username:x:uid:gid:gecos:home:shell) is written - /// to `buf_ptr` with max length `buf_len`. Actual length written to `ret_len`. - /// Returns errno. - fn getpwuid(uid: u32, buf_ptr: *mut u8, buf_len: u32, ret_len: *mut u32) -> Errno; -} - -// ============================================================ -// Safe Rust wrappers — host_process -// ============================================================ - -/// Spawn a child process with the given arguments, environment, stdio FDs, and working directory. -/// -/// Returns `Ok(pid)` on success, `Err(errno)` on failure. -pub fn spawn( - argv: &[u8], - envp: &[u8], - stdin_fd: u32, - stdout_fd: u32, - stderr_fd: u32, - cwd: &[u8], -) -> Result { - let mut pid: u32 = 0; - let argv_len = checked_u32_len(argv.len())?; - let envp_len = checked_u32_len(envp.len())?; - let cwd_len = checked_u32_len(cwd.len())?; - let errno = unsafe { - proc_spawn( - argv.as_ptr(), - argv_len, - envp.as_ptr(), - envp_len, - stdin_fd, - stdout_fd, - stderr_fd, - cwd.as_ptr(), - cwd_len, - &mut pid, - ) - }; - if errno == ERRNO_SUCCESS { - Ok(pid) - } else { - Err(errno) - } -} - -/// Wait for a child process to exit. -/// -/// Returns `Ok((exit_status, actual_pid))` on success, `Err(errno)` on failure. -/// The actual_pid is the PID of the child that exited (relevant for pid=0xFFFFFFFF / -1). -pub fn waitpid(pid: u32, options: u32) -> Result<(u32, u32), Errno> { - let mut status: u32 = 0; - let mut actual_pid: u32 = 0; - let errno = unsafe { proc_waitpid(pid, options, &mut status, &mut actual_pid) }; - if errno == ERRNO_SUCCESS { - Ok((status, actual_pid)) - } else { - Err(errno) - } -} - -/// Send a signal to a process. -/// -/// Returns `Ok(())` on success, `Err(errno)` on failure. -pub fn kill(pid: u32, signal: u32) -> Result<(), Errno> { - let errno = unsafe { proc_kill(pid, signal) }; - if errno == ERRNO_SUCCESS { - Ok(()) - } else { - Err(errno) - } -} - -/// Get the current process's virtual PID. -/// -/// Returns `Ok(pid)` on success, `Err(errno)` on failure. -pub fn getpid() -> Result { - let mut pid: u32 = 0; - let errno = unsafe { proc_getpid(&mut pid) }; - if errno == ERRNO_SUCCESS { - Ok(pid) - } else { - Err(errno) - } -} - -/// Get the parent process's virtual PID. -/// -/// Returns `Ok(pid)` on success, `Err(errno)` on failure. -pub fn getppid() -> Result { - let mut pid: u32 = 0; - let errno = unsafe { proc_getppid(&mut pid) }; - if errno == ERRNO_SUCCESS { - Ok(pid) - } else { - Err(errno) - } -} - -/// Create an anonymous pipe. -/// -/// Returns `Ok((read_fd, write_fd))` on success, `Err(errno)` on failure. -pub fn pipe() -> Result<(u32, u32), Errno> { - let mut read_fd: u32 = 0; - let mut write_fd: u32 = 0; - let errno = unsafe { fd_pipe(&mut read_fd, &mut write_fd) }; - if errno == ERRNO_SUCCESS { - Ok((read_fd, write_fd)) - } else { - Err(errno) - } -} - -/// Duplicate a file descriptor. -/// -/// Returns `Ok(new_fd)` on success, `Err(errno)` on failure. -pub fn dup(fd: u32) -> Result { - let mut new_fd: u32 = 0; - let errno = unsafe { fd_dup(fd, &mut new_fd) }; - if errno == ERRNO_SUCCESS { - Ok(new_fd) - } else { - Err(errno) - } -} - -/// Duplicate a file descriptor to a specific number. -/// -/// Returns `Ok(())` on success, `Err(errno)` on failure. -pub fn dup2(old_fd: u32, new_fd: u32) -> Result<(), Errno> { - let errno = unsafe { fd_dup2(old_fd, new_fd) }; - if errno == ERRNO_SUCCESS { - Ok(()) - } else { - Err(errno) - } -} - -/// Sleep for the specified number of milliseconds. -/// -/// Blocks via Atomics.wait on the host side instead of busy-waiting. -/// Returns `Ok(())` on success, `Err(errno)` on failure. -pub fn host_sleep_ms(milliseconds: u32) -> Result<(), Errno> { - let errno = unsafe { sleep_ms(milliseconds) }; - if errno == ERRNO_SUCCESS { - Ok(()) - } else { - Err(errno) - } -} - -/// Allocate a pseudo-terminal (PTY) master/slave pair. -/// -/// Returns `Ok((master_fd, slave_fd))` on success, `Err(errno)` on failure. -/// The master FD is used to read output and write input. -/// The slave FD is passed to a child process as its stdin/stdout/stderr. -pub fn openpty() -> Result<(u32, u32), Errno> { - let mut master_fd: u32 = 0; - let mut slave_fd: u32 = 0; - let errno = unsafe { pty_open(&mut master_fd, &mut slave_fd) }; - if errno == ERRNO_SUCCESS { - Ok((master_fd, slave_fd)) - } else { - Err(errno) - } -} - -/// Register a signal handler disposition (POSIX sigaction). -/// -/// `signal` is the signal number (1-64). -/// `action` encodes the disposition: 0=SIG_DFL, 1=SIG_IGN, 2=user handler (C-side holds pointer). -/// `mask_lo` / `mask_hi` encode the low/high 32 bits of sa_mask, and `flags` -/// carries the raw POSIX sa_flags bitmask. -/// Returns `Ok(())` on success, `Err(errno)` on failure. -pub fn sigaction_set( - signal: u32, - action: u32, - mask_lo: u32, - mask_hi: u32, - flags: u32, -) -> Result<(), Errno> { - let errno = unsafe { proc_sigaction(signal, action, mask_lo, mask_hi, flags) }; - if errno == ERRNO_SUCCESS { - Ok(()) - } else { - Err(errno) - } -} - -// ============================================================ -// host_net module — TCP socket operations -// ============================================================ - -#[link(wasm_import_module = "host_net")] -extern "C" { - /// Create a socket. - /// - /// `domain` is the address family (e.g. AF_INET=2). - /// `sock_type` is the socket type (e.g. SOCK_STREAM=1). - /// `protocol` is the protocol (0 for default). - /// On success, the socket FD is written to `ret_fd`. - /// Returns errno. - fn net_socket(domain: u32, sock_type: u32, protocol: u32, ret_fd: *mut u32) -> Errno; - - /// Connect a socket to a remote address. - /// - /// `addr_ptr`/`addr_len` point to a serialized address string (host:port). - /// Returns errno. - fn net_connect(fd: u32, addr_ptr: *const u8, addr_len: u32) -> Errno; - - /// Send data on a connected socket. - /// - /// `buf_ptr`/`buf_len` point to the data to send. - /// `flags` are send flags (0 for default). - /// Number of bytes sent is written to `ret_sent`. - /// Returns errno. - fn net_send(fd: u32, buf_ptr: *const u8, buf_len: u32, flags: u32, ret_sent: *mut u32) - -> Errno; - - /// Receive data from a connected socket. - /// - /// `buf_ptr`/`buf_len` point to the receive buffer. - /// `flags` are recv flags (0 for default). - /// Number of bytes received is written to `ret_received`. - /// Returns errno. - fn net_recv( - fd: u32, - buf_ptr: *mut u8, - buf_len: u32, - flags: u32, - ret_received: *mut u32, - ) -> Errno; - - /// Close a socket. - /// - /// Returns errno. - fn net_close(fd: u32) -> Errno; - - /// Resolve a hostname to an address. - /// - /// `host_ptr`/`host_len` point to the hostname string. - /// `port_ptr`/`port_len` point to the port/service string. - /// `family` is 0 for any address family, 4 for IPv4, and 6 for IPv6. - /// Resolved address is written to `ret_addr` buffer with max length from `ret_addr_len`. - /// Actual length is written back to `ret_addr_len`. - /// Returns errno. - fn net_getaddrinfo( - host_ptr: *const u8, - host_len: u32, - port_ptr: *const u8, - port_len: u32, - family: u32, - ret_addr: *mut u8, - ret_addr_len: *mut u32, - ) -> Errno; - - /// Upgrade a connected TCP socket to TLS. - /// - /// `hostname_ptr`/`hostname_len` point to the SNI hostname string. - /// After success, net_send/net_recv on this fd use the encrypted TLS stream. - /// Returns errno. - fn net_tls_connect(fd: u32, hostname_ptr: *const u8, hostname_len: u32) -> Errno; - - /// Set a socket option. - /// - /// `level` is the protocol level (e.g. SOL_SOCKET=1). - /// `optname` is the option name. - /// `optval_ptr`/`optval_len` point to the option value. - /// Returns errno. - fn net_setsockopt( - fd: u32, - level: u32, - optname: u32, - optval_ptr: *const u8, - optval_len: u32, - ) -> Errno; - - /// Get the local address of a socket. - /// - /// The serialized address string is written to `ret_addr` with maximum - /// length from `ret_addr_len`. The actual length is written back. - /// Returns errno. - fn net_getsockname(fd: u32, ret_addr: *mut u8, ret_addr_len: *mut u32) -> Errno; - - /// Get the peer address of a connected socket. - /// - /// The serialized address string is written to `ret_addr` with maximum - /// length from `ret_addr_len`. The actual length is written back. - /// Returns errno. - fn net_getpeername(fd: u32, ret_addr: *mut u8, ret_addr_len: *mut u32) -> Errno; - - /// Poll socket FDs for readiness. - /// - /// `fds_ptr` points to a packed array of poll entries (8 bytes each): - /// [fd: i32, events: i16, revents: i16] per entry. - /// `nfds` is the number of entries. - /// `timeout_ms` is the timeout: 0=non-blocking, -1=block forever, >0=milliseconds. - /// On return, revents fields are updated in-place and `ret_ready` receives - /// the number of FDs with non-zero revents. - /// Returns errno. - fn net_poll(fds_ptr: *mut u8, nfds: u32, timeout_ms: i32, ret_ready: *mut u32) -> Errno; - - /// Bind a socket to a local address. - /// - /// `addr_ptr`/`addr_len` point to a serialized address string (host:port or unix path). - /// Returns errno. - fn net_bind(fd: u32, addr_ptr: *const u8, addr_len: u32) -> Errno; - - /// Mark a bound socket as listening for incoming connections. - /// - /// `backlog` is the maximum pending connection queue length. - /// Returns errno. - fn net_listen(fd: u32, backlog: u32) -> Errno; - - /// Accept an incoming connection on a listening socket. - /// - /// On success, the new connected socket FD is written to `ret_fd`, - /// and the remote address string is written to `ret_addr` with its - /// length in `ret_addr_len`. - /// Returns errno. - fn net_accept(fd: u32, ret_fd: *mut u32, ret_addr: *mut u8, ret_addr_len: *mut u32) -> Errno; - - /// Send a datagram to a specific destination address (UDP). - /// - /// `buf_ptr`/`buf_len` point to the data to send. - /// `flags` are send flags (0 for default). - /// `addr_ptr`/`addr_len` point to the destination address string (host:port). - /// Number of bytes sent is written to `ret_sent`. - /// Returns errno. - fn net_sendto( - fd: u32, - buf_ptr: *const u8, - buf_len: u32, - flags: u32, - addr_ptr: *const u8, - addr_len: u32, - ret_sent: *mut u32, - ) -> Errno; - - /// Receive a datagram from a UDP socket with source address. - /// - /// `buf_ptr`/`buf_len` point to the receive buffer. - /// `flags` are recv flags (0 for default). - /// Number of bytes received is written to `ret_received`. - /// Source address string is written to `ret_addr` with length in `ret_addr_len`. - /// Returns errno. - fn net_recvfrom( - fd: u32, - buf_ptr: *mut u8, - buf_len: u32, - flags: u32, - ret_received: *mut u32, - ret_addr: *mut u8, - ret_addr_len: *mut u32, - ) -> Errno; -} - -// ============================================================ -// Safe Rust wrappers — host_net -// ============================================================ - -/// Create a socket. -/// -/// Returns `Ok(fd)` on success, `Err(errno)` on failure. -pub fn socket(domain: u32, sock_type: u32, protocol: u32) -> Result { - let mut fd: u32 = 0; - let errno = unsafe { net_socket(domain, sock_type, protocol, &mut fd) }; - if errno == ERRNO_SUCCESS { - Ok(fd) - } else { - Err(errno) - } -} - -/// Connect a socket to a remote address. -/// -/// `addr` is a serialized address string (e.g. "host:port"). -/// Returns `Ok(())` on success, `Err(errno)` on failure. -pub fn connect(fd: u32, addr: &[u8]) -> Result<(), Errno> { - let addr_len = checked_u32_len(addr.len())?; - let errno = unsafe { net_connect(fd, addr.as_ptr(), addr_len) }; - if errno == ERRNO_SUCCESS { - Ok(()) - } else { - Err(errno) - } -} - -/// Send data on a connected socket. -/// -/// Returns `Ok(bytes_sent)` on success, `Err(errno)` on failure. -pub fn send(fd: u32, buf: &[u8], flags: u32) -> Result { - let buf_len = checked_u32_len(buf.len())?; - let mut sent: u32 = 0; - let errno = unsafe { net_send(fd, buf.as_ptr(), buf_len, flags, &mut sent) }; - if errno == ERRNO_SUCCESS { - validate_returned_len(sent, buf.len()) - } else { - Err(errno) - } -} - -/// Receive data from a connected socket. -/// -/// Returns `Ok(bytes_received)` on success, `Err(errno)` on failure. -pub fn recv(fd: u32, buf: &mut [u8], flags: u32) -> Result { - let buf_len = checked_u32_len(buf.len())?; - let mut received: u32 = 0; - let errno = unsafe { net_recv(fd, buf.as_mut_ptr(), buf_len, flags, &mut received) }; - if errno == ERRNO_SUCCESS { - validate_returned_len(received, buf.len()) - } else { - Err(errno) - } -} - -/// Outcome of a cooperative (non-blocking) `recv`. -/// -/// Distinguishes "no data right now, try again later" (`WouldBlock`) from real -/// data, EOF, and hard errors so callers can yield to the runtime instead of -/// blocking the single guest thread. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum RecvOutcome { - /// Read `usize` bytes into the buffer. - Read(usize), - /// Peer closed the connection (orderly EOF). - Eof, - /// No data available yet; the socket has a non-zero `SO_RCVTIMEO` set and the - /// host returned `EAGAIN`. Caller should yield and re-poll. - WouldBlock, -} - -/// Receive data, mapping the host's `EAGAIN` to [`RecvOutcome::WouldBlock`]. -/// -/// Use this on sockets that have opted into non-blocking behavior via -/// [`set_recv_timeout_ms`]. On such sockets the host polls briefly then returns -/// `EAGAIN` instead of blocking the thread, letting the caller cooperatively -/// yield. Sockets with no recv timeout still block (this returns `Read`/`Eof`). -pub fn recv_cooperative(fd: u32, buf: &mut [u8], flags: u32) -> Result { - match recv(fd, buf, flags) { - Ok(0) => Ok(RecvOutcome::Eof), - Ok(n) => Ok(RecvOutcome::Read(n as usize)), - Err(ERRNO_AGAIN) => Ok(RecvOutcome::WouldBlock), - Err(e) => Err(e), - } -} - -/// Mark a socket non-blocking for recv by setting a small, non-zero -/// `SO_RCVTIMEO`. -/// -/// The host_net shim polls up to this timeout then returns `EAGAIN` when no data -/// arrived. A zero timeout is rejected by the host (it would mean "blocking"), -/// so callers should pass a small non-zero value (e.g. 2ms). Leaving a socket -/// without ever calling this keeps the default blocking recv behavior, so other -/// guests are unaffected. -pub fn set_recv_timeout_ms(fd: u32, millis: u32) -> Result<(), Errno> { - let micros: u64 = (millis as u64).saturating_mul(1000); - let secs = micros / 1_000_000; - let usec = micros % 1_000_000; - let mut timeval = [0u8; TIMEVAL_BYTES]; - timeval[0..8].copy_from_slice(&secs.to_le_bytes()); - timeval[8..16].copy_from_slice(&usec.to_le_bytes()); - setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeval) -} - -/// Close a socket. -/// -/// Returns `Ok(())` on success, `Err(errno)` on failure. -pub fn net_close_socket(fd: u32) -> Result<(), Errno> { - let errno = unsafe { net_close(fd) }; - if errno == ERRNO_SUCCESS { - Ok(()) - } else { - Err(errno) - } -} - -/// Resolve a hostname to an address. -/// -/// Writes the resolved address into `buf` and returns the number of bytes written. -/// Returns `Ok(len)` on success, `Err(errno)` on failure. -pub fn getaddrinfo(host: &[u8], port: &[u8], buf: &mut [u8]) -> Result { - let host_len = checked_u32_len(host.len())?; - let port_len = checked_u32_len(port.len())?; - let mut len = checked_u32_len(buf.len())?; - let errno = unsafe { - net_getaddrinfo( - host.as_ptr(), - host_len, - port.as_ptr(), - port_len, - 0, - buf.as_mut_ptr(), - &mut len, - ) - }; - if errno == ERRNO_SUCCESS { - validate_returned_len(len, buf.len()) - } else { - Err(errno) - } -} - -/// Set a socket option. -/// -/// Returns `Ok(())` on success, `Err(errno)` on failure. -pub fn setsockopt(fd: u32, level: u32, optname: u32, optval: &[u8]) -> Result<(), Errno> { - let optval_len = checked_u32_len(optval.len())?; - let errno = unsafe { net_setsockopt(fd, level, optname, optval.as_ptr(), optval_len) }; - if errno == ERRNO_SUCCESS { - Ok(()) - } else { - Err(errno) - } -} - -/// Get the local address of a socket. -/// -/// Writes the serialized address into `buf` and returns the number of bytes written. -/// Returns `Ok(len)` on success, `Err(errno)` on failure. -pub fn getsockname(fd: u32, buf: &mut [u8]) -> Result { - let mut len = checked_u32_len(buf.len())?; - let errno = unsafe { net_getsockname(fd, buf.as_mut_ptr(), &mut len) }; - if errno == ERRNO_SUCCESS { - validate_returned_len(len, buf.len()) - } else { - Err(errno) - } -} - -/// Get the peer address of a connected socket. -/// -/// Writes the serialized address into `buf` and returns the number of bytes written. -/// Returns `Ok(len)` on success, `Err(errno)` on failure. -pub fn getpeername(fd: u32, buf: &mut [u8]) -> Result { - let mut len = checked_u32_len(buf.len())?; - let errno = unsafe { net_getpeername(fd, buf.as_mut_ptr(), &mut len) }; - if errno == ERRNO_SUCCESS { - validate_returned_len(len, buf.len()) - } else { - Err(errno) - } -} - -/// Upgrade a connected TCP socket to TLS. -/// -/// `hostname` is used for SNI (Server Name Indication). -/// After success, `send`/`recv` on this fd use the encrypted TLS stream. -/// Returns `Ok(())` on success, `Err(errno)` on failure. -pub fn tls_connect(fd: u32, hostname: &[u8]) -> Result<(), Errno> { - let hostname_len = checked_u32_len(hostname.len())?; - let errno = unsafe { net_tls_connect(fd, hostname.as_ptr(), hostname_len) }; - if errno == ERRNO_SUCCESS { - Ok(()) - } else { - Err(errno) - } -} - -/// Poll socket FDs for readiness. -/// -/// `fds` is a mutable slice of pollfd-like entries (8 bytes each: fd i32, events i16, revents i16). -/// `timeout_ms` is the timeout: 0=non-blocking, -1=block forever, >0=milliseconds. -/// Returns `Ok(ready_count)` on success, `Err(errno)` on failure. -pub fn poll(fds: &mut [u8], nfds: u32, timeout_ms: i32) -> Result { - validate_poll_buffer_len(fds.len(), nfds)?; - let mut ready: u32 = 0; - let errno = unsafe { net_poll(fds.as_mut_ptr(), nfds, timeout_ms, &mut ready) }; - if errno == ERRNO_SUCCESS { - validate_poll_ready_count(ready, nfds) - } else { - Err(errno) - } -} - -/// Bind a socket to a local address. -/// -/// `addr` is a serialized address string (e.g. "host:port" or "/path/to/socket"). -/// Returns `Ok(())` on success, `Err(errno)` on failure. -pub fn bind(fd: u32, addr: &[u8]) -> Result<(), Errno> { - let addr_len = checked_u32_len(addr.len())?; - let errno = unsafe { net_bind(fd, addr.as_ptr(), addr_len) }; - if errno == ERRNO_SUCCESS { - Ok(()) - } else { - Err(errno) - } -} - -/// Mark a bound socket as listening for incoming connections. -/// -/// `backlog` is the maximum pending connection queue length. -/// Returns `Ok(())` on success, `Err(errno)` on failure. -pub fn listen(fd: u32, backlog: u32) -> Result<(), Errno> { - let errno = unsafe { net_listen(fd, backlog) }; - if errno == ERRNO_SUCCESS { - Ok(()) - } else { - Err(errno) - } -} - -/// Accept an incoming connection on a listening socket. -/// -/// Returns `Ok((fd, addr_len))` on success, where the remote address string -/// has been written into `addr_buf` with length `addr_len`. -/// Returns `Err(errno)` on failure. -pub fn accept(fd: u32, addr_buf: &mut [u8]) -> Result<(u32, u32), Errno> { - let mut new_fd: u32 = 0; - let mut addr_len = checked_u32_len(addr_buf.len())?; - let errno = unsafe { net_accept(fd, &mut new_fd, addr_buf.as_mut_ptr(), &mut addr_len) }; - if errno == ERRNO_SUCCESS { - Ok((new_fd, validate_returned_len(addr_len, addr_buf.len())?)) - } else { - Err(errno) - } -} - -/// Send a datagram to a specific destination address (UDP). -/// -/// `addr` is the destination address string (e.g. "host:port"). -/// Returns `Ok(bytes_sent)` on success, `Err(errno)` on failure. -pub fn sendto(fd: u32, buf: &[u8], flags: u32, addr: &[u8]) -> Result { - let buf_len = checked_u32_len(buf.len())?; - let addr_len = checked_u32_len(addr.len())?; - let mut sent: u32 = 0; - let errno = unsafe { - net_sendto( - fd, - buf.as_ptr(), - buf_len, - flags, - addr.as_ptr(), - addr_len, - &mut sent, - ) - }; - if errno == ERRNO_SUCCESS { - validate_returned_len(sent, buf.len()) - } else { - Err(errno) - } -} - -/// Receive a datagram from a UDP socket with source address. -/// -/// Writes received data into `buf` and the source address string into `addr_buf`. -/// Returns `Ok((bytes_received, addr_len))` on success, `Err(errno)` on failure. -pub fn recvfrom( - fd: u32, - buf: &mut [u8], - flags: u32, - addr_buf: &mut [u8], -) -> Result<(u32, u32), Errno> { - let buf_len = checked_u32_len(buf.len())?; - let mut received: u32 = 0; - let mut addr_len = checked_u32_len(addr_buf.len())?; - let errno = unsafe { - net_recvfrom( - fd, - buf.as_mut_ptr(), - buf_len, - flags, - &mut received, - addr_buf.as_mut_ptr(), - &mut addr_len, - ) - }; - if errno == ERRNO_SUCCESS { - Ok(( - validate_returned_len(received, buf.len())?, - validate_returned_len(addr_len, addr_buf.len())?, - )) - } else { - Err(errno) - } -} - -// ============================================================ -// Safe Rust wrappers — host_user -// ============================================================ - -/// Get the real user ID. -/// -/// Returns `Ok(uid)` on success, `Err(errno)` on failure. -pub fn get_uid() -> Result { - let mut uid: u32 = 0; - let errno = unsafe { getuid(&mut uid) }; - if errno == ERRNO_SUCCESS { - Ok(uid) - } else { - Err(errno) - } -} - -/// Get the real group ID. -/// -/// Returns `Ok(gid)` on success, `Err(errno)` on failure. -pub fn get_gid() -> Result { - let mut gid: u32 = 0; - let errno = unsafe { getgid(&mut gid) }; - if errno == ERRNO_SUCCESS { - Ok(gid) - } else { - Err(errno) - } -} - -/// Get the effective user ID. -/// -/// Returns `Ok(uid)` on success, `Err(errno)` on failure. -pub fn get_euid() -> Result { - let mut uid: u32 = 0; - let errno = unsafe { geteuid(&mut uid) }; - if errno == ERRNO_SUCCESS { - Ok(uid) - } else { - Err(errno) - } -} - -/// Get the effective group ID. -/// -/// Returns `Ok(gid)` on success, `Err(errno)` on failure. -pub fn get_egid() -> Result { - let mut gid: u32 = 0; - let errno = unsafe { getegid(&mut gid) }; - if errno == ERRNO_SUCCESS { - Ok(gid) - } else { - Err(errno) - } -} - -/// Check if a file descriptor is a terminal. -/// -/// Returns `Ok(true)` if it's a terminal, `Ok(false)` otherwise, `Err(errno)` on failure. -pub fn is_atty(fd: u32) -> Result { - let mut result: u32 = 0; - let errno = unsafe { isatty(fd, &mut result) }; - if errno == ERRNO_SUCCESS { - Ok(result != 0) - } else { - Err(errno) - } -} - -/// Get the passwd entry for a user ID. -/// -/// Writes the serialized passwd entry into `buf` and returns the number of bytes written. -/// Returns `Ok(len)` on success, `Err(errno)` on failure. -pub fn get_pwuid(uid: u32, buf: &mut [u8]) -> Result { - let mut len: u32 = 0; - let buf_len = checked_u32_len(buf.len())?; - let errno = unsafe { getpwuid(uid, buf.as_mut_ptr(), buf_len, &mut len) }; - if errno == ERRNO_SUCCESS { - validate_returned_len(len, buf.len()) - } else { - Err(errno) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn poll_buffer_validation_requires_exact_pollfd_capacity() { - assert_eq!(validate_poll_buffer_len(POLLFD_BYTES, 1), Ok(())); - assert_eq!( - validate_poll_buffer_len(POLLFD_BYTES - 1, 1), - Err(ERRNO_INVAL) - ); - assert_eq!( - validate_poll_buffer_len(POLLFD_BYTES + 1, 1), - Err(ERRNO_INVAL) - ); - } - - #[test] - fn returned_lengths_must_fit_in_the_supplied_buffer() { - assert_eq!(validate_returned_len(4, 4), Ok(4)); - assert_eq!(validate_returned_len(5, 4), Err(ERRNO_INVAL)); - } - - #[test] - fn poll_ready_count_must_not_exceed_nfds() { - assert_eq!(validate_poll_ready_count(0, 0), Ok(0)); - assert_eq!(validate_poll_ready_count(2, 2), Ok(2)); - assert_eq!(validate_poll_ready_count(3, 2), Err(ERRNO_INVAL)); - } -} diff --git a/registry/native/patches/0001-wasi-process-spawn.patch b/registry/native/patches/0001-wasi-process-spawn.patch deleted file mode 100644 index f9f05189f1..0000000000 --- a/registry/native/patches/0001-wasi-process-spawn.patch +++ /dev/null @@ -1,674 +0,0 @@ ---- a/library/std/src/sys/process/mod.rs 2026-03-15 17:30:05.751603873 -0700 -+++ b/library/std/src/sys/process/mod.rs 2026-03-15 17:22:41.475211510 -0700 -@@ -15,6 +15,10 @@ - mod motor; - use motor as imp; - } -+ target_os = "wasi" => { -+ mod wasi; -+ use wasi as imp; -+ } - _ => { - mod unsupported; - use unsupported as imp; - ---- /dev/null -+++ b/library/std/src/sys/process/wasi.rs -@@ -0,0 +1,657 @@ -+// WASI process implementation using wasmVM host_process syscalls. -+// -+// Replaces the unsupported() stubs with real process management -+// via custom WASM imports provided by the JS host runtime. -+ -+use super::env::{CommandEnv, CommandEnvs}; -+pub use crate::ffi::OsString as EnvKey; -+use crate::ffi::{OsStr, OsString}; -+use crate::num::NonZero; -+use crate::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd}; -+use crate::os::wasi::ffi::OsStrExt; -+use crate::path::Path; -+use crate::process::StdioPipes; -+use crate::sys::fd::FileDesc; -+use crate::sys::fs::File; -+use crate::sys::FromInner; -+use crate::io::{BorrowedCursor, IoSlice, IoSliceMut}; -+use crate::{fmt, io}; -+ -+// ============================================================ -+// host_process FFI bindings -+// ============================================================ -+ -+#[link(wasm_import_module = "host_process")] -+unsafe extern "C" { -+ fn proc_spawn( -+ argv_ptr: *const u8, -+ argv_len: u32, -+ envp_ptr: *const u8, -+ envp_len: u32, -+ stdin_fd: u32, -+ stdout_fd: u32, -+ stderr_fd: u32, -+ cwd_ptr: *const u8, -+ cwd_len: u32, -+ ret_pid: *mut u32, -+ ) -> u32; -+ -+ fn proc_waitpid(pid: u32, options: u32, ret_status: *mut u32, ret_pid: *mut u32) -> u32; -+ -+ fn proc_kill(pid: u32, signal: u32) -> u32; -+ -+ fn fd_pipe(ret_read_fd: *mut u32, ret_write_fd: *mut u32) -> u32; -+} -+ -+/// Convert a WASI errno to an io::Error. -+fn wasi_err(errno: u32) -> io::Error { -+ io::Error::from_raw_os_error(errno as i32) -+} -+ -+/// Encode host raw exit status into POSIX wait-status bits expected by ExitStatus. -+fn encode_wait_status(raw_status: u32) -> i32 { -+ if raw_status > 128 && raw_status < 256 { -+ (raw_status - 128) as i32 -+ } else { -+ ((raw_status & 0xff) << 8) as i32 -+ } -+} -+ -+/// Create a pipe, returning (read_fd, write_fd) as raw fd numbers. -+fn create_pipe() -> io::Result<(u32, u32)> { -+ let mut read_fd: u32 = 0; -+ let mut write_fd: u32 = 0; -+ let errno = unsafe { fd_pipe(&mut read_fd, &mut write_fd) }; -+ if errno != 0 { -+ return Err(wasi_err(errno)); -+ } -+ Ok((read_fd, write_fd)) -+} -+ -+/// Wrap a raw fd number into a ChildPipe. -+fn child_pipe_from_raw(raw: u32) -> ChildPipe { -+ ChildPipe(FileDesc::from_inner(unsafe { OwnedFd::from_raw_fd(raw as RawFd) })) -+} -+ -+// ============================================================ -+// ChildPipe — wraps an owned fd for parent-side pipe ends -+// ============================================================ -+ -+pub struct ChildPipe(FileDesc); -+ -+impl ChildPipe { -+ pub fn read(&self, buf: &mut [u8]) -> io::Result { -+ self.0.read(buf) -+ } -+ -+ pub fn read_buf(&self, buf: BorrowedCursor<'_>) -> io::Result<()> { -+ self.0.read_buf(buf) -+ } -+ -+ pub fn read_vectored(&self, bufs: &mut [IoSliceMut<'_>]) -> io::Result { -+ self.0.read_vectored(bufs) -+ } -+ -+ #[inline] -+ pub fn is_read_vectored(&self) -> bool { -+ self.0.is_read_vectored() -+ } -+ -+ pub fn read_to_end(&self, buf: &mut Vec) -> io::Result { -+ self.0.read_to_end(buf) -+ } -+ -+ pub fn write(&self, buf: &[u8]) -> io::Result { -+ self.0.write(buf) -+ } -+ -+ pub fn write_vectored(&self, bufs: &[IoSlice<'_>]) -> io::Result { -+ self.0.write_vectored(bufs) -+ } -+ -+ #[inline] -+ pub fn is_write_vectored(&self) -> bool { -+ self.0.is_write_vectored() -+ } -+ -+ pub fn diverge(&self) -> ! { -+ panic!("ChildPipe::diverge called on a valid pipe") -+ } -+} -+ -+impl fmt::Debug for ChildPipe { -+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -+ f.debug_tuple("ChildPipe").field(&self.0.as_raw_fd()).finish() -+ } -+} -+ -+// ============================================================ -+// Stdio -+// ============================================================ -+ -+#[derive(Debug)] -+pub enum Stdio { -+ Inherit, -+ Null, -+ MakePipe, -+ ParentStdout, -+ ParentStderr, -+ InheritFile(File), -+} -+ -+impl From for Stdio { -+ fn from(pipe: ChildPipe) -> Stdio { -+ Stdio::InheritFile(File::from_inner(pipe.0)) -+ } -+} -+ -+impl From for Stdio { -+ fn from(_: io::Stdout) -> Stdio { -+ Stdio::ParentStdout -+ } -+} -+ -+impl From for Stdio { -+ fn from(_: io::Stderr) -> Stdio { -+ Stdio::ParentStderr -+ } -+} -+ -+impl From for Stdio { -+ fn from(file: File) -> Stdio { -+ Stdio::InheritFile(file) -+ } -+} -+ -+impl From for Stdio { -+ fn from(pipe: crate::sys::pipe::Pipe) -> Stdio { -+ Stdio::InheritFile(File::from_inner(pipe)) -+ } -+} -+ -+// ============================================================ -+// Command -+// ============================================================ -+ -+pub struct Command { -+ program: OsString, -+ args: Vec, -+ env: CommandEnv, -+ cwd: Option, -+ stdin: Option, -+ stdout: Option, -+ stderr: Option, -+} -+ -+impl Command { -+ pub fn new(program: &OsStr) -> Command { -+ Command { -+ program: program.to_owned(), -+ args: vec![program.to_owned()], -+ env: Default::default(), -+ cwd: None, -+ stdin: None, -+ stdout: None, -+ stderr: None, -+ } -+ } -+ -+ pub fn arg(&mut self, arg: &OsStr) { -+ self.args.push(arg.to_owned()); -+ } -+ -+ pub fn env_mut(&mut self) -> &mut CommandEnv { -+ &mut self.env -+ } -+ -+ pub fn cwd(&mut self, dir: &OsStr) { -+ self.cwd = Some(dir.to_owned()); -+ } -+ -+ pub fn stdin(&mut self, stdin: Stdio) { -+ self.stdin = Some(stdin); -+ } -+ -+ pub fn stdout(&mut self, stdout: Stdio) { -+ self.stdout = Some(stdout); -+ } -+ -+ pub fn stderr(&mut self, stderr: Stdio) { -+ self.stderr = Some(stderr); -+ } -+ -+ pub fn get_program(&self) -> &OsStr { -+ &self.program -+ } -+ -+ pub fn get_args(&self) -> CommandArgs<'_> { -+ let mut iter = self.args.iter(); -+ iter.next(); -+ CommandArgs { iter } -+ } -+ -+ pub fn get_envs(&self) -> CommandEnvs<'_> { -+ self.env.iter() -+ } -+ -+ pub fn get_env_clear(&self) -> bool { -+ self.env.does_clear() -+ } -+ -+ pub fn get_current_dir(&self) -> Option<&Path> { -+ self.cwd.as_ref().map(|cs| Path::new(cs)) -+ } -+ -+ /// Serialize args as null-separated byte buffer. -+ fn serialize_argv(&self) -> Vec { -+ let mut buf = Vec::new(); -+ for arg in &self.args { -+ buf.extend_from_slice(arg.as_bytes()); -+ buf.push(0); -+ } -+ buf -+ } -+ -+ /// Serialize environment as KEY=VALUE\0 byte buffer. -+ fn serialize_envp(&self) -> Vec { -+ let mut buf = Vec::new(); -+ if self.env.does_clear() { -+ // Only include explicitly set vars -+ for (key, value) in self.env.iter() { -+ if let Some(val) = value { -+ buf.extend_from_slice(key.as_bytes()); -+ buf.push(b'='); -+ buf.extend_from_slice(val.as_bytes()); -+ buf.push(0); -+ } -+ } -+ } else { -+ // Inherit current environment -+ for (key, value) in crate::env::vars_os() { -+ buf.extend_from_slice(key.as_bytes()); -+ buf.push(b'='); -+ buf.extend_from_slice(value.as_bytes()); -+ buf.push(0); -+ } -+ // Apply overrides (host uses last value for duplicate keys) -+ for (key, value_opt) in self.env.iter() { -+ if let Some(value) = value_opt { -+ buf.extend_from_slice(key.as_bytes()); -+ buf.push(b'='); -+ buf.extend_from_slice(value.as_bytes()); -+ buf.push(0); -+ } -+ } -+ } -+ buf -+ } -+ -+ /// Resolve a Stdio option into (child_fd, optional_parent_pipe). -+ /// If `use_pipe_default` is true and `stdio` is None, creates a pipe. -+ fn setup_one_stdio( -+ stdio: &Option, -+ default_fd: u32, -+ is_input: bool, -+ use_pipe_default: bool, -+ ) -> io::Result<(u32, Option)> { -+ let make_pipe = || -> io::Result<(u32, Option)> { -+ let (read_fd, write_fd) = create_pipe()?; -+ if is_input { -+ Ok((read_fd, Some(child_pipe_from_raw(write_fd)))) -+ } else { -+ Ok((write_fd, Some(child_pipe_from_raw(read_fd)))) -+ } -+ }; -+ -+ match stdio { -+ None if use_pipe_default => make_pipe(), -+ None | Some(Stdio::Inherit) => Ok((default_fd, None)), -+ Some(Stdio::Null) => { -+ // Sentinel fd that the host interprets as /dev/null -+ Ok((u32::MAX, None)) -+ } -+ Some(Stdio::MakePipe) => make_pipe(), -+ Some(Stdio::ParentStdout) => Ok((1, None)), -+ Some(Stdio::ParentStderr) => Ok((2, None)), -+ Some(Stdio::InheritFile(file)) => { -+ Ok((file.as_raw_fd() as u32, None)) -+ } -+ } -+ } -+ -+ pub fn spawn( -+ &mut self, -+ default: Stdio, -+ _needs_stdin: bool, -+ ) -> io::Result<(Process, StdioPipes)> { -+ let argv = self.serialize_argv(); -+ let envp = self.serialize_envp(); -+ -+ // Get working directory bytes -+ let cwd_bytes = match &self.cwd { -+ Some(dir) => dir.as_bytes().to_vec(), -+ None => { -+ let cwd = crate::env::current_dir()?; -+ cwd.as_os_str().as_bytes().to_vec() -+ } -+ }; -+ -+ let use_pipes = matches!(default, Stdio::MakePipe); -+ -+ let (child_stdin_fd, parent_stdin) = -+ Self::setup_one_stdio(&self.stdin, 0, true, use_pipes)?; -+ let (child_stdout_fd, parent_stdout) = -+ Self::setup_one_stdio(&self.stdout, 1, false, use_pipes)?; -+ let (child_stderr_fd, parent_stderr) = -+ Self::setup_one_stdio(&self.stderr, 2, false, use_pipes)?; -+ -+ // Spawn the child process -+ let mut pid: u32 = 0; -+ let errno = unsafe { -+ proc_spawn( -+ argv.as_ptr(), -+ argv.len() as u32, -+ envp.as_ptr(), -+ envp.len() as u32, -+ child_stdin_fd, -+ child_stdout_fd, -+ child_stderr_fd, -+ cwd_bytes.as_ptr(), -+ cwd_bytes.len() as u32, -+ &mut pid, -+ ) -+ }; -+ -+ if errno != 0 { -+ return Err(wasi_err(errno)); -+ } -+ -+ let pipes = StdioPipes { -+ stdin: parent_stdin, -+ stdout: parent_stdout, -+ stderr: parent_stderr, -+ }; -+ -+ Ok((Process { pid, status: None }, pipes)) -+ } -+} -+ -+impl fmt::Debug for Command { -+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -+ if f.alternate() { -+ let mut debug_command = f.debug_struct("Command"); -+ debug_command.field("program", &self.program).field("args", &self.args); -+ if !self.env.is_unchanged() { -+ debug_command.field("env", &self.env); -+ } -+ if self.cwd.is_some() { -+ debug_command.field("cwd", &self.cwd); -+ } -+ if self.stdin.is_some() { -+ debug_command.field("stdin", &self.stdin); -+ } -+ if self.stdout.is_some() { -+ debug_command.field("stdout", &self.stdout); -+ } -+ if self.stderr.is_some() { -+ debug_command.field("stderr", &self.stderr); -+ } -+ debug_command.finish() -+ } else { -+ if let Some(ref cwd) = self.cwd { -+ write!(f, "cd {cwd:?} && ")?; -+ } -+ if self.env.does_clear() { -+ write!(f, "env -i ")?; -+ } else { -+ let mut any_removed = false; -+ for (key, value_opt) in self.get_envs() { -+ if value_opt.is_none() { -+ if !any_removed { -+ write!(f, "env ")?; -+ any_removed = true; -+ } -+ write!(f, "-u {} ", key.to_string_lossy())?; -+ } -+ } -+ } -+ for (key, value_opt) in self.get_envs() { -+ if let Some(value) = value_opt { -+ write!(f, "{}={value:?} ", key.to_string_lossy())?; -+ } -+ } -+ if self.program != self.args[0] { -+ write!(f, "[{:?}] ", self.program)?; -+ } -+ write!(f, "{:?}", self.args[0])?; -+ for arg in &self.args[1..] { -+ write!(f, " {:?}", arg)?; -+ } -+ Ok(()) -+ } -+ } -+} -+ -+// ============================================================ -+// ExitStatus -+// ============================================================ -+ -+#[derive(PartialEq, Eq, Clone, Copy, Debug)] -+pub struct ExitStatus(i32); -+ -+impl ExitStatus { -+ pub fn exit_ok(&self) -> Result<(), ExitStatusError> { -+ if self.0 == 0 { -+ Ok(()) -+ } else { -+ Err(ExitStatusError(unsafe { NonZero::new_unchecked(self.0) })) -+ } -+ } -+ -+ pub fn code(&self) -> Option { -+ // Decode POSIX wstatus: normal exit has bits 0-6 = 0, exit code in bits 8-15. -+ // Signal death has bits 0-6 = signal number (no exit code). -+ if (self.0 & 0x7f) == 0 { -+ Some((self.0 >> 8) & 0xff) -+ } else { -+ None -+ } -+ } -+} -+ -+impl Default for ExitStatus { -+ fn default() -> ExitStatus { -+ ExitStatus(0) -+ } -+} -+ -+impl fmt::Display for ExitStatus { -+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -+ if (self.0 & 0x7f) == 0 { -+ write!(f, "exit status: {}", (self.0 >> 8) & 0xff) -+ } else { -+ write!(f, "signal: {}", self.0 & 0x7f) -+ } -+ } -+} -+ -+// ============================================================ -+// ExitStatusError -+// ============================================================ -+ -+#[derive(PartialEq, Eq, Clone, Copy, Debug)] -+pub struct ExitStatusError(NonZero); -+ -+impl Into for ExitStatusError { -+ fn into(self) -> ExitStatus { -+ ExitStatus(self.0.get()) -+ } -+} -+ -+impl ExitStatusError { -+ pub fn code(self) -> Option> { -+ // Decode POSIX wstatus: return exit code only for normal exits. -+ let raw = self.0.get(); -+ if (raw & 0x7f) == 0 { -+ NonZero::new((raw >> 8) & 0xff) -+ } else { -+ None -+ } -+ } -+} -+ -+// ============================================================ -+// ExitCode -+// ============================================================ -+ -+#[derive(PartialEq, Eq, Clone, Copy, Debug)] -+pub struct ExitCode(u8); -+ -+impl ExitCode { -+ pub const SUCCESS: ExitCode = ExitCode(0); -+ pub const FAILURE: ExitCode = ExitCode(1); -+ -+ pub fn as_i32(&self) -> i32 { -+ self.0 as i32 -+ } -+} -+ -+impl From for ExitCode { -+ fn from(code: u8) -> Self { -+ Self(code) -+ } -+} -+ -+// ============================================================ -+// Process -+// ============================================================ -+ -+pub struct Process { -+ pid: u32, -+ status: Option, -+} -+ -+impl Process { -+ pub fn id(&self) -> u32 { -+ self.pid -+ } -+ -+ pub fn kill(&mut self) -> io::Result<()> { -+ const SIGKILL: u32 = 9; -+ let errno = unsafe { proc_kill(self.pid, SIGKILL) }; -+ if errno != 0 { -+ Err(wasi_err(errno)) -+ } else { -+ Ok(()) -+ } -+ } -+ -+ pub fn wait(&mut self) -> io::Result { -+ if let Some(status) = self.status { -+ return Ok(status); -+ } -+ let mut exit_code: u32 = 0; -+ let mut ret_pid: u32 = 0; -+ let errno = unsafe { proc_waitpid(self.pid, 0, &mut exit_code, &mut ret_pid) }; -+ if errno != 0 { -+ return Err(wasi_err(errno)); -+ } -+ let status = ExitStatus(encode_wait_status(exit_code)); -+ self.status = Some(status); -+ Ok(status) -+ } -+ -+ pub fn try_wait(&mut self) -> io::Result> { -+ if let Some(status) = self.status { -+ return Ok(Some(status)); -+ } -+ // Non-blocking wait: options=1 is WNOHANG equivalent -+ let mut exit_code: u32 = 0; -+ let mut ret_pid: u32 = 0; -+ let errno = unsafe { proc_waitpid(self.pid, 1, &mut exit_code, &mut ret_pid) }; -+ if errno == 0 { -+ if ret_pid == 0 { -+ return Ok(None); -+ } -+ let status = ExitStatus(encode_wait_status(exit_code)); -+ self.status = Some(status); -+ Ok(Some(status)) -+ } else if errno == 10 { -+ // ECHILD — child not yet exited -+ Ok(None) -+ } else { -+ Err(wasi_err(errno)) -+ } -+ } -+} -+ -+// ============================================================ -+// CommandArgs -+// ============================================================ -+ -+pub struct CommandArgs<'a> { -+ iter: crate::slice::Iter<'a, OsString>, -+} -+ -+impl<'a> Iterator for CommandArgs<'a> { -+ type Item = &'a OsStr; -+ fn next(&mut self) -> Option<&'a OsStr> { -+ self.iter.next().map(|os| &**os) -+ } -+ fn size_hint(&self) -> (usize, Option) { -+ self.iter.size_hint() -+ } -+} -+ -+impl<'a> ExactSizeIterator for CommandArgs<'a> { -+ fn len(&self) -> usize { -+ self.iter.len() -+ } -+ fn is_empty(&self) -> bool { -+ self.iter.is_empty() -+ } -+} -+ -+impl<'a> fmt::Debug for CommandArgs<'a> { -+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { -+ f.debug_list().entries(self.iter.clone()).finish() -+ } -+} -+ -+// ============================================================ -+// output — collect stdout/stderr from a child process -+// ============================================================ -+ -+pub fn output(cmd: &mut Command) -> io::Result<(ExitStatus, Vec, Vec)> { -+ let (mut process, mut pipes) = cmd.spawn(Stdio::MakePipe, false)?; -+ -+ drop(pipes.stdin.take()); -+ let (mut stdout, mut stderr) = (Vec::new(), Vec::new()); -+ match (pipes.stdout.take(), pipes.stderr.take()) { -+ (None, None) => {} -+ (Some(out), None) => { -+ out.read_to_end(&mut stdout)?; -+ } -+ (None, Some(err)) => { -+ err.read_to_end(&mut stderr)?; -+ } -+ (Some(out), Some(err)) => { -+ read_output(out, &mut stdout, err, &mut stderr)?; -+ } -+ } -+ -+ let status = process.wait()?; -+ Ok((status, stdout, stderr)) -+} -+ -+pub fn read_output( -+ out: ChildPipe, -+ stdout: &mut Vec, -+ err: ChildPipe, -+ stderr: &mut Vec, -+) -> io::Result<()> { -+ // In WASI single-threaded context, read sequentially -+ out.read_to_end(stdout)?; -+ err.read_to_end(stderr)?; -+ Ok(()) -+} diff --git a/registry/native/patches/codex-source/README.md b/registry/native/patches/codex-source/README.md deleted file mode 100644 index 8b1aee01d6..0000000000 --- a/registry/native/patches/codex-source/README.md +++ /dev/null @@ -1,271 +0,0 @@ -# Required codex SOURCE patches for wasm32-wasip1 - -These are minimal, upstreamable additions to codex's own source where it has a -hard `compile_error!`/missing-platform arm that NO toolchain patch can bypass -(first-party compile-time gates). Each adds REAL wasi support (wasi has the API), -not a hack. Applied to the vendored codex source during the secure-exec build. - -1. utils/git/src/platform.rs — add `#[cfg(target_os = "wasi")] create_symlink` - using `std::os::wasi::fs::symlink_path`, and widen the fallback - `compile_error!` cfg to `not(any(unix, windows, target_os = "wasi"))`. - wasi supports symlinks via the VM's `path_symlink` host call. - -2. rmcp-client/src/program_resolver.rs:26 — widen `#[cfg(unix)]` on `resolve()` to - `any(unix, target_os = "wasi")` (the unix impl returns the program unchanged, - correct for wasi). -3. rmcp-client/src/utils.rs:91 — widen `#[cfg(unix)]` on `DEFAULT_ENV_VARS` to - `any(unix, target_os = "wasi")`. - -4. core/Cargo.toml — add `[target.'cfg(target_os = "wasi")'.dependencies] codex-utils-pty` - (it compiles via the portable-pty wasi backend). codex-network-proxy stays excluded - (real crate pulls the rama proxy-server stack + tokio "full"); satisfied by stub. - -## codex-core OWN-code remaining (69 errors, characterized — this session got dep graph to 0) -The codex authors gated codex-network-proxy + codex-utils-pty OFF wasi as DEPS but did -NOT gate the SOURCE usage — an incomplete author wasi port. Breakdown: -- ~50: codex-core uses codex-network-proxy's FULL API (NetworkProxyConfig, NetworkDecision, - NetworkPolicyDecider/BlockedRequestObserver traits, NetworkProxyHandle, ConfigReloader, - build_config_state, host_and_port_from_network_addr, normalize_host, - validate_policy_against_constraints, NetworkProxyConstraints/Error, NetworkProtocol, - NetworkProxyState, NetworkProxyAuditMetadata, NetworkPolicyRequest, …). The real crate - can't compile on wasi (rama proxy stack); EXPAND the secure-exec codex-network-proxy - stub to this full API (no-op/inert — the VM kernel brokers network policy host-side). -- ~6: codex_otel SessionTelemetry methods the stub lacks → expand codex-otel stub. -- tokio::signal (no signals on wasi) → gate/stub codex-core's signal usage. -- ~4 platform fns (ensure_owner_only_permissions, synthetic_exit_status, - system_config_toml_file, system_requirements_toml_file) → add wasi arms. -- ~5 type mismatches (E0308) → case-by-case. -Then codex-exec compiles → un-stub --session-turn (real codex-core + EE protocol) → matrix. - -## network-proxy / otel: MUST be in-place wasi-gating of the REAL codex crates -Attempted an external stub crate (stubs/codex-network-proxy-wasi) — does NOT work: -codex workspace crates (codex-utils-absolute-path, etc.) use `workspace = true`, which -can't resolve from a path crate OUTSIDE codex's workspace. So the network-proxy/otel -stubbing must be done IN-PLACE in codex-rs/{network-proxy,otel} (workspace members): -- network-proxy/Cargo.toml: move `rama-*` + `tokio "full"` + rustls-provider to - `[target.'cfg(not(target_os="wasi"))'.dependencies]`; reduce wasi tokio features. -- network-proxy/src: gate the 6 rama files (certs,http_proxy,mitm,responses,socks5, - upstream) to `#[cfg(not(wasi))]` and add `#[cfg(wasi)]` stub versions. The 6 stub - bodies are already written in stubs/codex-network-proxy-wasi/src/{http_proxy,socks5, - mitm,certs,responses,upstream}.rs (run_http_proxy/run_socks5 → Ok(()); MitmState → unit). - Only 5 symbols are referenced by pure code: http_proxy::run_http_proxy{,_with_std_listener}, - socks5::run_socks5{,_with_std_listener}, mitm::MitmState. lib.rs mod decls stay (pure - modules config/network_policy/policy/proxy/reasons/runtime/state compile as-is). -- core/Cargo.toml: add codex-network-proxy to `[target.'cfg(target_os="wasi")'.dependencies]`. -- otel: same pattern — gate opentelemetry-otlp (grpc/tonic/reqwest exporter) off wasi, - stub the exporter init; keep the SessionTelemetry API (codex-core uses ~6 methods). -This is completing the codex authors' OWN incomplete wasi port (they gated the DEPS off -wasi but not the source). It is codex SOURCE modification, unavoidable (workspace constraint). - -## DONE this iteration: network-proxy in-place wasi-gating WORKS (codex-core 69→49) -Applied to REAL codex-rs/network-proxy (workspace member, so workspace=true resolves): -- network-proxy/Cargo.toml: rama-* + rustls-provider + tokio "full" → moved to - [target.'cfg(not(target_os="wasi"))'.dependencies]; base tokio reduced to - ["macros","rt","sync","time","io-util"]. -- network-proxy/src/lib.rs: the 6 rama modules (certs/http_proxy/mitm/responses/ - socks5/upstream) cfg-selected: #[cfg(not(wasi))] real vs #[cfg(wasi)] #[path="*_wasi.rs"]. -- network-proxy/src/{certs,http_proxy,mitm,responses,socks5,upstream}_wasi.rs: stub bodies - (run_http_proxy/run_socks5 → Ok(()); MitmState → unit). VERIFIED: codex_network_proxy - refs now resolve (0 unresolved). Restored workspace redirect to path="network-proxy" - + re-added as member + codex-core wasi dep. - -## NEXT: otel in-place gating (same pattern; ~6 SessionTelemetry method errors remain) -otel's opentelemetry-otlp exporter is spread across provider.rs(63)/client.rs(40)/ -otlp.rs(11)/config.rs(24). Gate those off wasi (cfg-select stub modules), keep -events/session_telemetry.rs (the SessionTelemetry API codex-core calls: -record_api_request, record_websocket_request, record_auth_recovery, log_sse_event, -tool_decision, tool_result_with_tags, on_request — these use codex-protocol types, so -MUST stay in-crate, not an external stub). Then: tokio::signal gate, ~4 platform fns -(synthetic_exit_status, system_*_toml_file, ensure_owner_only_permissions), ~5 E0308. -Then codex-exec → session-turn engine → matrix. - -## ✅ MILESTONE: codex-core AND codex-exec COMPILE for wasm32-wasip1 -All in-crate wasi-gating done (committed to the rivet codex fork, branch -wasi-port-codex-core, commit 0c3f4eb73, 25 files): network-proxy + otel in-place -gating, core platform fns (tokio::signal/synthetic_exit_status/toml/permissions), -git symlink, rmcp-client resolver. Plus std ExitStatusExt patch (patches/ -0008-wasi-exit-status-ext.md). `cargo build -p codex-core -p codex-exec ---target wasm32-wasip1 -Z build-std` → Finished, EXIT 0. - -REMAINING to e2e: (1) the secure-exec command crate crates/commands/codex-exec -un-stub --session-turn to delegate to the real codex-core/codex-exec engine and emit -the EE newline-JSON protocol (start/text_delta/tool_call_update/permission_request/ -done) — the real functional integration; (2) build via `make -C registry/native wasm` -(vendoring the codex fork + these patches); (3) wire the EE codex descriptor into the -agent-os matrix + un-skip the codex session test. - -## FINAL PIECE: session-turn engine (replace codex-exec wasi_stub_main) -codex-exec/src/lib.rs `wasi_stub_main()` is a placeholder ("WASI runtime support is -under development"). codex-core compiles now, so replace it with the real engine -(mirror lib_native.rs::run_exec_session, ~200-300 lines, in codex's workspace so it has -codex-core access). The built codex-exec.wasm IS the secure-exec `codex-exec` command. - -PROTOCOL (newline-JSON, the EE adapter @rivet-ee/agent-os-codex-agent drives it): -- stdin: first line {type:"start", cwd, mode, model, thought_level, - developer_instructions, history, prompt}; then {type:"permission_response", id, decision}. -- stdout events to EMIT (one JSON/line): - - {type:"start"} on boot - - {type:"text_delta", delta} ← EventMsg::AgentMessageDelta - - {type:"tool_call_update", tool_call_id, status, content} - ← EventMsg::ExecCommandBegin / ExecCommandEnd - - {type:"permission_request", tool_call_id, command} - ← EventMsg::ExecApprovalRequest / - ApplyPatchApprovalRequest; then read - permission_response from stdin → submit Op approval - - {type:"done"} ← EventMsg::TurnComplete - - {type:"error", message} ← EventMsg::Error - -ENGINE (codex-core API, verified): -1. parse start JSON → build codex_core Config (cwd/model/instructions; sandbox+network - proxy already gated off on wasi; approval policy = OnRequest so approvals surface). -2. ConversationManager::new_conversation(config) → Codex (or new_conversation_with_auth). -3. tokio current_thread runtime (rt feature works on wasi). seed history, then - submit(Op::UserInput{ items:[InputItem::Text{prompt}] }). -4. loop next_event() → match EventMsg → emit per table above; on approval requests, - block reading stdin for permission_response → submit(Op::ExecApproval/PatchApproval). -5. TurnComplete → emit done, exit 0. -Then: `make -C registry/native wasm` (vendor the codex fork wasi-port-codex-core branch + -all patches) → copies codex/codex-exec into software/codex/wasm → wire EE descriptor into -the agent-os matrix (registry/agent/codex already exists) → un-skip the codex session test. - -### session-turn engine — Config is loadable (tractable) -core/src/config/mod.rs has `Config::load_with_cli_overrides(cli_overrides, harness)` -(811) — build Config from {model, cwd, approval_policy=OnRequest, ...} overrides rather -than manual field construction. Op/InputItem: protocol.rs Op::UserInput{ items: -Vec } (235). The Codex client (core/src/codex.rs submit/next_event) is created -via the conversation/thread entry (codex_thread.rs / conversation flow) — confirm the -exact constructor (ConversationManager or Codex::spawn) when implementing. Engine ≈ -200-300 lines in codex-exec wasi_stub_main; tokio current_thread runtime (works on wasi). - -## ✅ DONE: session-turn engine compiles (the final FUNCTIONAL piece) -codex-exec/src/session_turn_wasi.rs (fork commit b86a5dfc0) drives the REAL codex-core -agent and emits the EE protocol. `cargo build -p codex-exec --target wasm32-wasip1 --Z build-std` → EXIT 0. So codex-core + codex-exec + the session-turn engine ALL compile. - -## Remaining = build pipeline + ACP wiring + test run (integration, not functional) -1. Produce optimized wasm artifacts from the fork (branch wasi-port-codex-core): - `cargo build -p codex-exec -p codex --release --target wasm32-wasip1 -Z build-std` - (+ wasm-opt) → copy to registry/software/codex/wasm/{codex-exec,codex}. NOTE: the - secure-exec `make wasm` currently builds the STUB crates/commands/{codex,codex-exec}; - to ship the real engine, either build from the fork directly (above) or vendor the - fork into registry/native and point cmd-codex-exec at the real codex-exec. -2. ACP adapter: the codex ACP bridge (spawns `codex-exec --session-turn`, newline-JSON↔ACP) - lives in EE (@rivet-ee/agent-os-codex-agent, agent-os-ee/packages/codex/src/adapter.ts). - The open-source registry/agent/codex only re-exports the wasm software package. To test - codex e2e in the agent-os matrix, either bring that adapter into agent-os or run the - EE codex-session.test.ts (describe.skipIf gated on wasm presence — un-skips once the - wasm artifacts above exist). -3. e2e: with codex-exec.wasm present, the EE adapter spawns it in the VM (wasi-spawn/http - host bridges), the engine talks to the (mock) model via wasi-http, streams text_delta/ - tool_call_update, emits done. Run codex-session.test.ts to verify. - -## ✅ ARTIFACT BUILT + SPAWNS IN VM; last blocker = 1 host import -Built the real codex-exec from the fork (release, wasm-opt'd to 28MB) and placed it in -registry/software/codex/wasm/{codex-exec,codex}. Smoke test (vm.exec("... | codex-exec ---session-turn")) shows it SPAWNS and attempts WebAssembly instantiation in the VM — -44 imports, of which 43 (wasi_snapshot_preview1×30, host_net×6, host_process×5, -host_fs×2) are provided by the secure-exec runtime. The ONLY unprovided one was -`env.sqlite3_load_extension` (sqlx's sqlite). Fixed by defining a #[no_mangle] no-op -stub in the engine (fork commit). Rebuild + re-place → instantiation should succeed. -Then the smoke test ({"type":"start"} before the model call) and the full EE -codex-session.test.ts (with the responses mock) verify the e2e turn. - -## ✅✅ VERIFIED: codex-exec RUNS IN THE VM and emits the EE protocol -Smoke test (packages/core/tests/codex-smoke.test.ts) PASSES: - vm.exec("printf '' | codex-exec --session-turn") → stdout: {"type":"start"} -The real codex agent (codex-rs → wasip1, session-turn engine driving codex-core) BOOTS -in the secure-exec VM, instantiates all 44 imports (after adding host -path_filestat_set_times), runs the engine, and emits the EE protocol's start event. -(exit 1 only because the smoke test provides no model mock/auth, so the turn errors -AFTER start — proving the boot + protocol path works.) - -FULL turn (text_delta→done): run the EE codex-session.test.ts with startResponsesMock -(OpenAI Responses mock) + OPENAI_API_KEY/base_url pointed at it. That exercises a -complete agent turn. The boot+protocol path is now VERIFIED end-to-end in the VM. - -## ✅✅✅ codex runs the FULL session event loop in the VM -DBG markers (engine) confirm the real codex agent runs the entire lifecycle in the VM: -load config → AuthManager::shared → ThreadManager::new → start_thread → submit -Op::UserInput → LIVE next_event loop (received a Warning event, processing). 8 runtime -blockers fixed and committed (host imports, secure-exec host wasi shim -path_filestat_set_times, tokio fs-asyncify + spawn_blocking inline on wasi, std -split_paths, now_local→UTC). FINAL GAP: the model HTTP POST to the OpenAI Responses -mock does not reach it (mock requests=0, then hang). codex honors OPENAI_BASE_URL -(deprecated warning), so base_url=mock; the codex-exec wasi-http/host_net connect to the -HOST loopback mock blocks. Resolve: confirm host_net egress to the test's 127.0.0.1:PORT -is permitted for wasi commands (loopbackExemptPorts is for the kernel adapter; host_net -may need its own allow), and that the Responses path matches (/v1/responses). Then the -turn streams text_delta→done. The agent itself runs e2e in the VM today. - -## CORRECTED final diagnosis (via sidecar instrumentation): NOT a network issue -Added file-logging to the sidecar net.connect handler ENTRY + require_network_access + -resolve_tcp_connect_addr, ran the full-turn test. RESULT: the log stays EMPTY — codex's -connect NEVER reaches the sidecar. So codex is NOT making the model HTTP call; it HANGS -in its OWN agent loop after the first event (a config Warning), before any network call. -Root cause: codex's agent loop uses tokio concurrency that assumes OS threads -(spawn_blocking on a dedicated blocking pool, background tasks). wasm32-wasip1 is -single-threaded (no OS threads); the spawn_blocking-as-current-thread-task workaround -(0003) can DEADLOCK — a blocking task occupies the only thread while waiting on another -task that can't run. This is an architectural mismatch (codex's threading model vs -single-thread wasi), the genuinely hard core of running codex on wasi. Resolving it needs -codex-internal tracing to find the exact blocking construct and make it cooperative, or a -different runtime strategy. (Sidecar instrumentation reverted; secure-exec is clean.) -STATUS: codex compiles + runs the full session lifecycle in the VM (boot → EE protocol → -config → auth → ThreadManager → start_thread → submit → live event loop) but hangs in the -agent loop before the model call on the single-threaded runtime. - -## ✅✅✅ RESOLVED: codex completes a full model turn end-to-end on wasm32-wasip1 - -The "agent loop hangs" diagnosis above was *close but mislabeled* — it is NOT a tokio -concurrency/threading-model mismatch. The current-thread runtime drives codex's spawned -submission_loop correctly. The real root cause, found by instrumenting tokio's -current-thread scheduler (Handle::spawn / schedule / the block_on for-loop / per-task -spawn-location), was a **single blocking task that pins the only executor thread**: - -- `codex-core` spawns a shell-snapshot task (`core/src/shell_snapshot.rs`) during session - init that launches a **shell subprocess** to capture the environment and blocks on its - exit. On wasm32-wasip1 the VM's child-process bridge cannot deliver that wait on the - single-threaded runtime (the "could not retrieve pid for child process" warnings), so the - task blocks the executor thread **forever**. The scheduler then never drains the rest of - the local run queue — including the submission_loop — so the agent turn never advances and - no model HTTP call is ever made. (Hence the sidecar net.connect handler stayed empty.) - -Fixes (all in the codex fork, branch `wasi-port-codex-core`, all `#[cfg(target_os="wasi")]`): -1. `core/src/shell_snapshot.rs` — skip the snapshot on wasi (send `None`, return). A shell - environment snapshot is meaningless in the VM and the subprocess wait deadlocks the runtime. -2. `core/src/state_db.rs` — `init` / `get_state_db` / `open_if_present` return `None` on wasi. - sqlx-sqlite's blocking worker can't run on the thread-less runtime (open fails ENOTSUP, - os error 58); skipping it also avoids perturbing session init. -3. `exec/src/session_turn_wasi.rs` — runtime is `new_current_thread().enable_time()` (no I/O - reactor needed; network I/O is host-brokered/synchronous) and a plain - `block_on(session_turn())`. The earlier yield-pump / `global_queue_interval(1)` - experiments were chasing the wrong theory and are NOT needed once the blocking task is gone. - -Test (`packages/core/tests/codex-fullturn.test.ts`): codex-exec --session-turn runs the real -codex-core agent in the VM, calls the mock OpenAI Responses API over wasi-http, streams the -SSE response, and emits `{"type":"start"}` … `{"type":"done"}` on a clean protocol channel -(stdout carries ONLY the EE protocol JSON). The mock (`tests/helpers/openai-responses-mock.ts`) -emits a faithful Responses SSE stream (`response.created` → `response.output_text.delta` → -`response.output_item.done` → `response.completed`); `OPENAI_BASE_URL` includes `/v1` to match -codex's URL convention (`{base}/responses`). - -Build (reproducible): the wasi build uses `-Z build-std` with a patched rust-src sysroot. -Because `-Z build-std` injects `panic_unwind` via `--extern` while the wasm target needs -`panic_abort` resolved from the sysroot, the build: - - stashes the prebuilt `wasm32-wasip1` `*.rlib`/`*.rmeta` out of the toolchain lib dir - (keeping `self-contained/` crt) so build-std's own `libcore` is the only one (avoids the - `E0152 duplicate lang item core` at the bin link), and - - copies build-std's freshly-built `libpanic_abort-*.rlib` into that sysroot lib dir (so the - bin's `-Cpanic=abort` resolves a `panic_abort` that is ABI-matched to build-std's core), - - builds with `--config 'profile.release.panic="abort"'`. -See `/tmp/relink-codex.sh` for the exact invocation. (This sysroot massaging should be folded -into the secure-exec wasm toolchain so it is not a manual step.) - -KNOWN REMAINING (non-blocking; turn works e2e): -- Rollout recorder logs `failed to queue rollout items: channel closed` (the writer task exits - early on wasi — likely the git-info subprocess in `write_session_meta`, same blocking-child - class as shell_snapshot). Non-fatal: the turn completes; agent-os resume uses adapter-passed - history, not codex's on-disk rollout. Gating git-info on wasi would silence it. -- The session-turn engine reads `prompt` but not `history`; multi-turn resume via the EE ACP - adapter needs history replay wired in. -- ACP-level integration into the unified agent-matrix lives in the EE codex adapter. diff --git a/registry/native/patches/git/README.md b/registry/native/patches/git/README.md deleted file mode 100644 index b8a55dffab..0000000000 --- a/registry/native/patches/git/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Git WASM Compatibility - -Git (GPL-2.0) cannot be vendored due to license restrictions. -This project uses a clean-room, Apache-2.0 licensed reimplementation -of git plumbing commands. - -## WASM-Incompatible Patterns in Git Source - -The following patterns in upstream git are incompatible with WASM/WASI -and were avoided in our clean-room implementation: - -### 1. fork+exec in run_command.c -Git's `run_command()` uses `fork()`+`exec()` extensively for spawning -child processes (hooks, filters, remotes). WASI has no `fork()`. -**Our approach:** Use `posix_spawn()` via `host_process` imports when -process spawning is needed in future commands. - -### 2. mmap in wrapper.c -Git uses `mmap()` for memory-mapped file I/O (packfiles, index). -WASI has limited `mmap()` support. -**Our approach:** Use standard `malloc()`+`read()` for all file I/O. -Git upstream supports this via `NO_MMAP=1`. - -### 3. Signal handlers (sigaction for SIGPIPE, SIGCHLD) -Git registers signal handlers for `SIGPIPE` (ignore broken pipe) and -`SIGCHLD` (reap child processes). WASI has no signal support. -**Our approach:** No signal handlers needed — WASI processes don't -receive signals, and our process model handles cleanup via the kernel. diff --git a/registry/native/patches/std/os/wasi/process.rs b/registry/native/patches/std/os/wasi/process.rs deleted file mode 100644 index 223daafd1b..0000000000 --- a/registry/native/patches/std/os/wasi/process.rs +++ /dev/null @@ -1,70 +0,0 @@ -//! WASI-specific extensions to primitives in the [`std::process`] module. -//! -//! Mirrors `os/unix/process.rs`' child-pipe fd traits for wasm32-wasip1 so that -//! `tokio::process` (and other fd-extracting code) can reach the parent-side -//! pipe ends of a spawned child. (secure-exec pipeline-only codex port.) -//! -//! [`std::process`]: crate::process - -#![stable(feature = "rust1", since = "1.0.0")] - -use crate::os::wasi::io::{AsFd, AsRawFd, BorrowedFd, IntoRawFd, OwnedFd, RawFd}; -use crate::process; -use crate::sys::{AsInner, FromInner, IntoInner}; - -macro_rules! impl_child_pipe_fd { - ($t:ty) => { - #[stable(feature = "process_extensions", since = "1.2.0")] - impl AsRawFd for $t { - #[inline] - fn as_raw_fd(&self) -> RawFd { - self.as_inner().as_fd().as_raw_fd() - } - } - - #[stable(feature = "into_raw_os", since = "1.4.0")] - impl IntoRawFd for $t { - #[inline] - fn into_raw_fd(self) -> RawFd { - self.into_inner().into_inner().into_raw_fd() - } - } - - #[stable(feature = "io_safety", since = "1.63.0")] - impl AsFd for $t { - #[inline] - fn as_fd(&self) -> BorrowedFd<'_> { - self.as_inner().as_fd() - } - } - - #[stable(feature = "io_safety", since = "1.63.0")] - impl From<$t> for OwnedFd { - #[inline] - fn from(child: $t) -> OwnedFd { - child.into_inner().into_inner() - } - } - }; -} - -impl_child_pipe_fd!(process::ChildStdin); -impl_child_pipe_fd!(process::ChildStdout); -impl_child_pipe_fd!(process::ChildStderr); - -/// WASI-specific extension to construct an [`ExitStatus`] from a raw code, -/// mirroring `std::os::unix::process::ExitStatusExt::from_raw`. (secure-exec -/// pipeline-only codex port — codex's synthetic exit statuses need this.) -#[stable(feature = "rust1", since = "1.0.0")] -pub trait ExitStatusExt { - /// Construct an `ExitStatus` from the given raw code. - #[stable(feature = "exit_status_from", since = "1.12.0")] - fn from_raw(raw: i32) -> Self; -} - -#[stable(feature = "exit_status_from", since = "1.12.0")] -impl ExitStatusExt for process::ExitStatus { - fn from_raw(raw: i32) -> Self { - process::ExitStatus::from_inner(crate::sys::process::ExitStatus::from(raw)) - } -} diff --git a/registry/native/patches/wasi-libc-overrides/fcntl.c b/registry/native/patches/wasi-libc-overrides/fcntl.c deleted file mode 100644 index ad938ca653..0000000000 --- a/registry/native/patches/wasi-libc-overrides/fcntl.c +++ /dev/null @@ -1,175 +0,0 @@ -/** - * Fix for wasi-libc's broken fcntl implementation. - * - * wasi-libc always returns FD_CLOEXEC(1) for F_GETFD and ignores F_SETFD - * because WASI has no exec(). It also returns EINVAL for F_DUPFD and - * F_DUPFD_CLOEXEC. This fix properly tracks per-fd cloexec flags, - * delegates F_GETFL/F_SETFL to the original WASI fd_fdstat interface, - * and routes F_DUPFD/F_DUPFD_CLOEXEC through the host_process bridge. - * - * Installed into the patched sysroot so ALL WASM programs get correct - * fcntl behavior, not just test binaries. - */ - -#include -#include -#include -#include - -/* WASI headers omit F_DUPFD and F_DUPFD_CLOEXEC — define with Linux values */ -#ifndef F_DUPFD -#define F_DUPFD 0 -#endif -#ifndef F_DUPFD_CLOEXEC -#define F_DUPFD_CLOEXEC 1030 -#endif - -/* Host import for dup with minimum fd (F_DUPFD semantics) */ -__attribute__((import_module("host_process"), import_name("fd_dup_min"))) -int __host_fd_dup_min(int fd, int min_fd, int *ret_new_fd); - -/* Per-fd cloexec tracking (up to 256 FDs) */ -#define MAX_FDS 256 -static unsigned char _fd_cloexec[MAX_FDS]; - -int fcntl(int fd, int cmd, ...) { - va_list ap; - va_start(ap, cmd); - - int result; - - switch (cmd) { - case F_DUPFD: { - int min_fd = va_arg(ap, int); - if (fd < 0 || fd >= MAX_FDS) { - errno = EBADF; - result = -1; - } else if (min_fd < 0) { - errno = EINVAL; - result = -1; - } else { - int new_fd; - int err = __host_fd_dup_min(fd, min_fd, &new_fd); - if (err != 0) { - errno = err; - result = -1; - } else { - if (new_fd >= 0 && new_fd < MAX_FDS) - _fd_cloexec[new_fd] = 0; - result = new_fd; - } - } - break; - } - - case F_DUPFD_CLOEXEC: { - int min_fd = va_arg(ap, int); - if (fd < 0 || fd >= MAX_FDS) { - errno = EBADF; - result = -1; - } else if (min_fd < 0) { - errno = EINVAL; - result = -1; - } else { - int new_fd; - int err = __host_fd_dup_min(fd, min_fd, &new_fd); - if (err != 0) { - errno = err; - result = -1; - } else { - if (new_fd >= 0 && new_fd < MAX_FDS) - _fd_cloexec[new_fd] = 1; - result = new_fd; - } - } - break; - } - - case F_GETFD: - if (fd < 0 || fd >= MAX_FDS) { - errno = EBADF; - result = -1; - } else { - result = _fd_cloexec[fd] ? FD_CLOEXEC : 0; - } - break; - - case F_SETFD: { - int arg = va_arg(ap, int); - if (fd < 0 || fd >= MAX_FDS) { - errno = EBADF; - result = -1; - } else { - _fd_cloexec[fd] = (arg & FD_CLOEXEC) ? 1 : 0; - result = 0; - } - break; - } - - case F_GETFL: { - __wasi_fdstat_t stat; - __wasi_errno_t err = __wasi_fd_fdstat_get((__wasi_fd_t)fd, &stat); - if (err != 0) { - errno = err; - result = -1; - } else { - int flags = stat.fs_flags; - /* Derive read/write mode from rights */ - __wasi_rights_t r = stat.fs_rights_base; - int can_read = (r & __WASI_RIGHTS_FD_READ) != 0; - int can_write = (r & __WASI_RIGHTS_FD_WRITE) != 0; - if (can_read && can_write) - flags |= O_RDWR; - else if (can_read) - flags |= O_RDONLY; - else if (can_write) - flags |= O_WRONLY; - result = flags; - } - break; - } - - case F_SETFL: { - int arg = va_arg(ap, int); - __wasi_errno_t err = __wasi_fd_fdstat_set_flags( - (__wasi_fd_t)fd, - (__wasi_fdflags_t)(arg & 0xfff)); - if (err != 0) { - errno = err; - result = -1; - } else { - result = 0; - } - break; - } - - case F_GETLK: { - struct flock *lock = va_arg(ap, struct flock *); - if (!lock) { - errno = EINVAL; - result = -1; - } else { - lock->l_type = F_UNLCK; - lock->l_pid = 0; - result = 0; - } - break; - } - - case F_SETLK: - case F_SETLKW: - // WASI has no kernel-level advisory locking. Treat locks as a - // successful no-op so single-process workloads like DuckDB can open - // writable database files on the VFS-backed filesystem. - result = 0; - break; - - default: - errno = EINVAL; - result = -1; - break; - } - - va_end(ap); - return result; -} diff --git a/registry/native/patches/wasi-libc-overrides/fcntl.o b/registry/native/patches/wasi-libc-overrides/fcntl.o deleted file mode 100644 index 81c3b82dd6..0000000000 Binary files a/registry/native/patches/wasi-libc-overrides/fcntl.o and /dev/null differ diff --git a/registry/native/patches/wasi-libc-overrides/fmtmsg.o b/registry/native/patches/wasi-libc-overrides/fmtmsg.o deleted file mode 100644 index 4caa9d328d..0000000000 Binary files a/registry/native/patches/wasi-libc-overrides/fmtmsg.o and /dev/null differ diff --git a/registry/native/patches/wasi-libc-overrides/ifaddrs.c b/registry/native/patches/wasi-libc-overrides/ifaddrs.c deleted file mode 100644 index 2f1abe78b5..0000000000 --- a/registry/native/patches/wasi-libc-overrides/ifaddrs.c +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Minimal getifaddrs/freeifaddrs shim for runtimes without host network - * interface enumeration. - * - * cpp-httplib includes ifaddrs support on POSIX targets, but DuckDB's embedded - * HTTP client only uses it when callers opt into binding a request to a named - * interface. Our WASI runtime does not expose interface enumeration today, so - * return an empty list instead of failing the build or forcing DuckDB-specific - * source patches. - */ - -#include -#include - -int getifaddrs(struct ifaddrs **ifap) { - if (ifap) { - *ifap = 0; - } - errno = ENOSYS; - return -1; -} - -void freeifaddrs(struct ifaddrs *ifa) { - (void)ifa; -} diff --git a/registry/native/patches/wasi-libc-overrides/inet_ntop.o b/registry/native/patches/wasi-libc-overrides/inet_ntop.o deleted file mode 100644 index 7f38fb4bbf..0000000000 Binary files a/registry/native/patches/wasi-libc-overrides/inet_ntop.o and /dev/null differ diff --git a/registry/native/patches/wasi-libc-overrides/mlock.c b/registry/native/patches/wasi-libc-overrides/mlock.c deleted file mode 100644 index a9a445713c..0000000000 --- a/registry/native/patches/wasi-libc-overrides/mlock.c +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Minimal mlock/munlock shim for WASI/POSIX builds without page pinning. - * - * DuckDB uses mlock/munlock as a best-effort hardening step for encryption - * keys. Our runtime does not currently expose memory locking primitives, so - * treat these calls as successful no-ops instead of failing the link. - * - * Installed into the patched sysroot so upstream code can keep its existing - * POSIX calls without carrying a WASI-specific source patch. - */ - -#include - -int mlock(const void *addr, size_t len) { - (void)addr; - (void)len; - return 0; -} - -int munlock(const void *addr, size_t len) { - (void)addr; - (void)len; - return 0; -} - -int mlockall(int flags) { - (void)flags; - return 0; -} - -int munlockall(void) { - return 0; -} - -int madvise(void *addr, size_t len, int advice) { - (void)addr; - (void)len; - (void)advice; - return 0; -} diff --git a/registry/native/patches/wasi-libc-overrides/open_wmemstream.o b/registry/native/patches/wasi-libc-overrides/open_wmemstream.o deleted file mode 100644 index bfe607600c..0000000000 Binary files a/registry/native/patches/wasi-libc-overrides/open_wmemstream.o and /dev/null differ diff --git a/registry/native/patches/wasi-libc-overrides/pthread_attr.o b/registry/native/patches/wasi-libc-overrides/pthread_attr.o deleted file mode 100644 index 7c6ae1e113..0000000000 Binary files a/registry/native/patches/wasi-libc-overrides/pthread_attr.o and /dev/null differ diff --git a/registry/native/patches/wasi-libc-overrides/pthread_key.o b/registry/native/patches/wasi-libc-overrides/pthread_key.o deleted file mode 100644 index f2186cb689..0000000000 Binary files a/registry/native/patches/wasi-libc-overrides/pthread_key.o and /dev/null differ diff --git a/registry/native/patches/wasi-libc-overrides/pthread_mutex.o b/registry/native/patches/wasi-libc-overrides/pthread_mutex.o deleted file mode 100644 index cca3b4a12a..0000000000 Binary files a/registry/native/patches/wasi-libc-overrides/pthread_mutex.o and /dev/null differ diff --git a/registry/native/patches/wasi-libc-overrides/strfmon.o b/registry/native/patches/wasi-libc-overrides/strfmon.o deleted file mode 100644 index 52ba0d4df0..0000000000 Binary files a/registry/native/patches/wasi-libc-overrides/strfmon.o and /dev/null differ diff --git a/registry/native/patches/wasi-libc-overrides/swprintf.o b/registry/native/patches/wasi-libc-overrides/swprintf.o deleted file mode 100644 index e73848c70a..0000000000 Binary files a/registry/native/patches/wasi-libc-overrides/swprintf.o and /dev/null differ diff --git a/registry/native/patches/wasi-libc/.gitkeep b/registry/native/patches/wasi-libc/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/registry/native/patches/wasi-libc/0002-spawn-wait.patch b/registry/native/patches/wasi-libc/0002-spawn-wait.patch deleted file mode 100644 index 809a0fe939..0000000000 --- a/registry/native/patches/wasi-libc/0002-spawn-wait.patch +++ /dev/null @@ -1,336 +0,0 @@ -Implement posix_spawn(), posix_spawnp(), waitpid(), wait() via host_process WASM imports. - -Replaces musl's posix_spawn (which uses fork/clone, unavailable in WASI) -and waitpid (which uses SYS_wait4) with implementations that call our -host_process.proc_spawn and host_process.proc_waitpid WASM imports. - -Also provides posix_spawn_file_actions and posix_spawnattr helpers since -musl's versions (in process/) are not compiled for WASI. - -file_actions are processed in order: dup2 maps stdio FDs, close is no-op -(child doesn't inherit parent FDs), open maps files to stdio overrides. - -waitpid returns the actual waited-for PID from the host via ret_pid, -fixing waitpid(-1, ...) which previously returned -1 (error convention). - -Import signatures match wasmvm/crates/wasi-ext/src/lib.rs exactly. - ---- /dev/null 2026-03-16 11:59:07.564000026 -0700 -+++ b/libc-bottom-half/sources/host_spawn_wait.c 2026-03-19 20:51:08.698271081 -0700 -@@ -0,0 +1,316 @@ -+// Process spawning and waiting via wasmVM host_process imports. -+// -+// Replaces musl's posix_spawn (which uses fork/clone, unavailable in WASI) -+// and waitpid (which uses SYS_wait4) with our custom WASM imports: -+// host_process.proc_spawn -> posix_spawn(), posix_spawnp() -+// host_process.proc_waitpid -> waitpid(), wait() -+// -+// Also provides posix_spawn_file_actions and posix_spawnattr helpers -+// (musl's process/ directory is not compiled for WASI). -+// -+// Import signatures match wasmvm/crates/wasi-ext/src/lib.rs exactly. -+ -+#include <__errno.h> -+#include <__errno_values.h> -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+ -+// spawn.h and sys/wait.h are not installed in the wasi sysroot. -+// Define the needed types inline (matching musl's spawn.h layout). -+ -+#define __NEED_sigset_t -+#include -+ -+typedef struct { -+ int __flags; -+ pid_t __pgrp; -+ sigset_t __def, __mask; -+ int __prio, __pol; -+ void *__fn; -+ char __pad[64-sizeof(void *)]; -+} posix_spawnattr_t; -+ -+typedef struct { -+ int __pad0[2]; -+ void *__actions; -+ int __pad[16]; -+} posix_spawn_file_actions_t; -+ -+struct sched_param { int sched_priority; }; -+ -+#define WASM_IMPORT(mod, fn) \ -+ __attribute__((__import_module__(mod), __import_name__(fn))) -+ -+// host_process.proc_spawn(argv_ptr, argv_len, envp_ptr, envp_len, -+// stdin_fd, stdout_fd, stderr_fd, cwd_ptr, cwd_len, ret_pid) -> errno -+WASM_IMPORT("host_process", "proc_spawn") -+uint32_t __host_proc_spawn( -+ const uint8_t *argv_ptr, uint32_t argv_len, -+ const uint8_t *envp_ptr, uint32_t envp_len, -+ uint32_t stdin_fd, uint32_t stdout_fd, uint32_t stderr_fd, -+ const uint8_t *cwd_ptr, uint32_t cwd_len, -+ uint32_t *ret_pid); -+ -+// host_process.proc_waitpid(pid, options, ret_status, ret_pid) -> errno -+WASM_IMPORT("host_process", "proc_waitpid") -+uint32_t __host_proc_waitpid(uint32_t pid, uint32_t options, -+ uint32_t *ret_status, uint32_t *ret_pid); -+ -+// Internal file operation descriptor (matches musl's fdop.h layout) -+#define FDOP_CLOSE 1 -+#define FDOP_DUP2 2 -+#define FDOP_OPEN 3 -+#define FDOP_CHDIR 4 -+#define FDOP_FCHDIR 5 -+ -+struct __fdop { -+ struct __fdop *next, *prev; -+ int cmd, fd, srcfd, oflag; -+ mode_t mode; -+ char path[]; -+}; -+ -+// --- posix_spawn_file_actions helpers --- -+ -+int posix_spawn_file_actions_init(posix_spawn_file_actions_t *fa) { -+ memset(fa, 0, sizeof(*fa)); -+ return 0; -+} -+ -+int posix_spawn_file_actions_destroy(posix_spawn_file_actions_t *fa) { -+ struct __fdop *op = fa->__actions; -+ while (op) { -+ struct __fdop *next = op->next; -+ free(op); -+ op = next; -+ } -+ fa->__actions = 0; -+ return 0; -+} -+ -+// Append to file_actions list (preserves POSIX-required execution order) -+static int __addfdop(posix_spawn_file_actions_t *fa, struct __fdop *op) { -+ op->next = 0; -+ if (!fa->__actions) { -+ op->prev = 0; -+ fa->__actions = op; -+ } else { -+ struct __fdop *tail = fa->__actions; -+ while (tail->next) tail = tail->next; -+ tail->next = op; -+ op->prev = tail; -+ } -+ return 0; -+} -+ -+int posix_spawn_file_actions_adddup2(posix_spawn_file_actions_t *fa, int srcfd, int fd) { -+ if (srcfd < 0 || fd < 0) return EBADF; -+ struct __fdop *op = malloc(sizeof(*op)); -+ if (!op) return ENOMEM; -+ op->cmd = FDOP_DUP2; -+ op->srcfd = srcfd; -+ op->fd = fd; -+ return __addfdop(fa, op); -+} -+ -+int posix_spawn_file_actions_addclose(posix_spawn_file_actions_t *fa, int fd) { -+ if (fd < 0) return EBADF; -+ struct __fdop *op = malloc(sizeof(*op)); -+ if (!op) return ENOMEM; -+ op->cmd = FDOP_CLOSE; -+ op->fd = fd; -+ return __addfdop(fa, op); -+} -+ -+int posix_spawn_file_actions_addopen(posix_spawn_file_actions_t *fa, int fd, -+ const char *path, int oflag, mode_t mode) { -+ if (fd < 0) return EBADF; -+ size_t l = strlen(path); -+ struct __fdop *op = malloc(sizeof(*op) + l + 1); -+ if (!op) return ENOMEM; -+ op->cmd = FDOP_OPEN; -+ op->fd = fd; -+ op->oflag = oflag; -+ op->mode = mode; -+ memcpy(op->path, path, l + 1); -+ return __addfdop(fa, op); -+} -+ -+#if defined(_BSD_SOURCE) || defined(_GNU_SOURCE) -+int posix_spawn_file_actions_addchdir_np(posix_spawn_file_actions_t *restrict fa, -+ const char *restrict path) { -+ size_t l = strlen(path); -+ struct __fdop *op = malloc(sizeof(*op) + l + 1); -+ if (!op) return ENOMEM; -+ op->cmd = FDOP_CHDIR; -+ op->fd = 0; -+ memcpy(op->path, path, l + 1); -+ return __addfdop(fa, op); -+} -+ -+int posix_spawn_file_actions_addfchdir_np(posix_spawn_file_actions_t *fa, int fd) { -+ if (fd < 0) return EBADF; -+ struct __fdop *op = malloc(sizeof(*op)); -+ if (!op) return ENOMEM; -+ op->cmd = FDOP_FCHDIR; -+ op->fd = fd; -+ return __addfdop(fa, op); -+} -+#endif -+ -+// --- posix_spawnattr helpers (silently ignored by posix_spawn) --- -+ -+int posix_spawnattr_init(posix_spawnattr_t *attr) { memset(attr, 0, sizeof(*attr)); return 0; } -+int posix_spawnattr_destroy(posix_spawnattr_t *attr) { (void)attr; return 0; } -+int posix_spawnattr_setflags(posix_spawnattr_t *a, short f) { a->__flags = f; return 0; } -+int posix_spawnattr_getflags(const posix_spawnattr_t *restrict a, short *restrict f) { *f = (short)a->__flags; return 0; } -+int posix_spawnattr_setpgroup(posix_spawnattr_t *a, pid_t g) { a->__pgrp = g; return 0; } -+int posix_spawnattr_getpgroup(const posix_spawnattr_t *restrict a, pid_t *restrict g) { *g = a->__pgrp; return 0; } -+int posix_spawnattr_setsigmask(posix_spawnattr_t *restrict a, const sigset_t *restrict m) { a->__mask = *m; return 0; } -+int posix_spawnattr_getsigmask(const posix_spawnattr_t *restrict a, sigset_t *restrict m) { *m = a->__mask; return 0; } -+int posix_spawnattr_setsigdefault(posix_spawnattr_t *restrict a, const sigset_t *restrict d) { a->__def = *d; return 0; } -+int posix_spawnattr_getsigdefault(const posix_spawnattr_t *restrict a, sigset_t *restrict d) { *d = a->__def; return 0; } -+ -+// Scheduling attributes — store values but not enforced in sandbox -+int posix_spawnattr_setschedparam(posix_spawnattr_t *restrict a, const struct sched_param *restrict p) { a->__prio = p->sched_priority; return 0; } -+int posix_spawnattr_getschedparam(const posix_spawnattr_t *restrict a, struct sched_param *restrict p) { p->sched_priority = a->__prio; return 0; } -+int posix_spawnattr_setschedpolicy(posix_spawnattr_t *a, int pol) { a->__pol = pol; return 0; } -+int posix_spawnattr_getschedpolicy(const posix_spawnattr_t *restrict a, int *restrict pol) { *pol = a->__pol; return 0; } -+ -+// --- posix_spawn --- -+ -+int posix_spawn(pid_t *restrict res, const char *restrict path, -+ const posix_spawn_file_actions_t *fa, -+ const posix_spawnattr_t *restrict attr, -+ char *const argv[restrict], char *const envp[restrict]) { -+ (void)attr; -+ -+ // Serialize argv: path as first entry (the command), then argv[1:] -+ size_t argv_buf_len = strlen(path) + 1; -+ if (argv) { -+ for (int i = 1; argv[i]; i++) -+ argv_buf_len += strlen(argv[i]) + 1; -+ } -+ -+ uint8_t *argv_buf = malloc(argv_buf_len); -+ if (!argv_buf) return ENOMEM; -+ -+ uint8_t *p = argv_buf; -+ size_t len = strlen(path) + 1; -+ memcpy(p, path, len); p += len; -+ if (argv) { -+ for (int i = 1; argv[i]; i++) { -+ len = strlen(argv[i]) + 1; -+ memcpy(p, argv[i], len); p += len; -+ } -+ } -+ -+ // Serialize envp — if NULL, use current environment -+ extern char **environ; -+ char *const *env = envp ? envp : environ; -+ -+ size_t envp_buf_len = 0; -+ if (env) { -+ for (int i = 0; env[i]; i++) -+ envp_buf_len += strlen(env[i]) + 1; -+ } -+ -+ uint8_t *envp_buf = NULL; -+ if (envp_buf_len > 0) { -+ envp_buf = malloc(envp_buf_len); -+ if (!envp_buf) { free(argv_buf); return ENOMEM; } -+ p = envp_buf; -+ for (int i = 0; env[i]; i++) { -+ len = strlen(env[i]) + 1; -+ memcpy(p, env[i], len); p += len; -+ } -+ } -+ -+ // Process file_actions in order: extract stdio overrides and handle close/open -+ uint32_t stdin_fd = 0, stdout_fd = 1, stderr_fd = 2; -+ if (fa && fa->__actions) { -+ for (struct __fdop *op = fa->__actions; op; op = op->next) { -+ switch (op->cmd) { -+ case FDOP_DUP2: -+ if (op->fd == 0) stdin_fd = (uint32_t)op->srcfd; -+ else if (op->fd == 1) stdout_fd = (uint32_t)op->srcfd; -+ else if (op->fd == 2) stderr_fd = (uint32_t)op->srcfd; -+ break; -+ case FDOP_CLOSE: -+ // No-op: child doesn't inherit parent FDs in our model. -+ // POSIX close-in-child semantics are handled by the host: -+ // proc_spawn closes stdio override FDs after fork. -+ break; -+ case FDOP_OPEN: { -+ int opened = open(op->path, op->oflag, op->mode); -+ if (opened < 0) { -+ free(argv_buf); -+ free(envp_buf); -+ return errno; -+ } -+ if (op->fd == 0) stdin_fd = (uint32_t)opened; -+ else if (op->fd == 1) stdout_fd = (uint32_t)opened; -+ else if (op->fd == 2) stderr_fd = (uint32_t)opened; -+ else close(opened); -+ break; -+ } -+ } -+ } -+ } -+ -+ uint32_t child_pid; -+ uint32_t err = __host_proc_spawn( -+ argv_buf, (uint32_t)argv_buf_len, -+ envp_buf ? envp_buf : (const uint8_t *)"", (uint32_t)envp_buf_len, -+ stdin_fd, stdout_fd, stderr_fd, -+ (const uint8_t *)"", 0, -+ &child_pid); -+ -+ free(argv_buf); -+ free(envp_buf); -+ -+ if (err != 0) return (int)err; -+ if (res) *res = (pid_t)child_pid; -+ return 0; -+} -+ -+int posix_spawnp(pid_t *restrict res, const char *restrict file, -+ const posix_spawn_file_actions_t *fa, -+ const posix_spawnattr_t *restrict attr, -+ char *const argv[restrict], char *const envp[restrict]) { -+ return posix_spawn(res, file, fa, attr, argv, envp); -+} -+ -+// --- waitpid / wait --- -+ -+pid_t waitpid(pid_t pid, int *status, int options) { -+ uint32_t raw_status; -+ uint32_t actual_pid; -+ uint32_t err = __host_proc_waitpid((uint32_t)pid, (uint32_t)options, -+ &raw_status, &actual_pid); -+ if (err != 0) { -+ errno = (int)err; -+ return -1; -+ } -+ // Encode POSIX wait status: signal kills come as 128+signal (bash -+ // convention), normal exits as raw exit code -+ if (status) { -+ if (raw_status > 128 && raw_status < 256) { -+ // Signal kill: POSIX encoding = signal in low 7 bits -+ *status = (int)(raw_status - 128); -+ } else { -+ // Normal exit: POSIX encoding = exit code in bits 15..8 -+ *status = (int)(raw_status << 8); -+ } -+ } -+ return (pid_t)actual_pid; -+} -+ -+pid_t wait(int *status) { -+ return waitpid(-1, status, 0); -+} diff --git a/registry/native/patches/wasi-libc/0008-sockets.patch b/registry/native/patches/wasi-libc/0008-sockets.patch deleted file mode 100644 index b7900a2c8f..0000000000 --- a/registry/native/patches/wasi-libc/0008-sockets.patch +++ /dev/null @@ -1,833 +0,0 @@ -Implement socket(), connect(), bind(), listen(), accept(), send(), -recv(), sendto(), recvfrom(), getaddrinfo(), freeaddrinfo(), -gai_strerror(), gethostname(), setsockopt(), poll(), and select() -via host_net WASM imports. - -Replaces the wasi-libc stubs (which return -ENOSYS or are #ifdef'd out) -with implementations that call our host_net.net_socket, net_connect, -net_bind, net_listen, net_accept, net_send, net_recv, net_sendto, -net_recvfrom, net_getaddrinfo, net_close, net_setsockopt, and net_poll -WASM imports. - -Un-omits netdb.h from the sysroot headers so C programs can use -getaddrinfo/freeaddrinfo/gai_strerror. Un-gates bind() and listen() -declarations from the wasip2-only guard. - -Supports AF_INET, AF_INET6, and AF_UNIX address families in -sockaddr serialization (sockaddr_to_string / string_to_sockaddr). - -Import signatures match wasmvm/crates/wasi-ext/src/lib.rs exactly. - ---- a/scripts/install-include-headers.sh 2026-03-20 04:05:48.609869966 -0700 -+++ b/scripts/install-include-headers.sh 2026-03-20 04:05:57.781880813 -0700 -@@ -69,10 +69,11 @@ - "net/ethernet.h" "net/route.h" "netinet/if_ether.h" "netinet/ether.h" \ - "sys/timerfd.h" "libintl.h" "sys/sysmacros.h" "aio.h") - # Exclude `netdb.h` from all of the p1 targets. --if [[ $TARGET_TRIPLE == *"wasi" || $TARGET_TRIPLE == *"wasi-threads" || \ -- $TARGET_TRIPLE == *"wasip1" || $TARGET_TRIPLE == *"wasip1-threads" ]]; then -- MUSL_OMIT_HEADERS+=("netdb.h") --fi -+# NOTE: commented out by the secure-exec 0008-sockets patch — we provide getaddrinfo via host_net -+#if [[ $TARGET_TRIPLE == *"wasi" || $TARGET_TRIPLE == *"wasi-threads" || \ -+# $TARGET_TRIPLE == *"wasip1" || $TARGET_TRIPLE == *"wasip1-threads" ]]; then -+# MUSL_OMIT_HEADERS+=("netdb.h") -+#fi - - # Remove all the `MUSL_OMIT_HEADERS` previously copied over. - for OMIT_HEADER in "${MUSL_OMIT_HEADERS[@]}"; do - ---- a/libc-top-half/musl/include/sys/socket.h -+++ b/libc-top-half/musl/include/sys/socket.h -@@ -398,9 +398,7 @@ - #include <__struct_sockaddr_storage.h> - #endif - --#if (defined __wasilibc_unmodified_upstream) || (defined __wasilibc_use_wasip2) - int socket (int, int, int); --#endif - - #ifdef __wasilibc_unmodified_upstream /* WASI has no socketpair */ - int socketpair (int, int, int, int [2]); -@@ -408,35 +406,27 @@ - - int shutdown (int, int); - --#if (defined __wasilibc_unmodified_upstream) || (defined __wasilibc_use_wasip2) - int connect (int, const struct sockaddr *, socklen_t); - int bind (int, const struct sockaddr *, socklen_t); - int listen (int, int); --#endif - - int accept (int, struct sockaddr *__restrict, socklen_t *__restrict); - int accept4(int, struct sockaddr *__restrict, socklen_t *__restrict, int); - --#if (defined __wasilibc_unmodified_upstream) || (defined __wasilibc_use_wasip2) - int getsockname (int, struct sockaddr *__restrict, socklen_t *__restrict); - int getpeername (int, struct sockaddr *__restrict, socklen_t *__restrict); --#endif - - ssize_t send (int, const void *, size_t, int); - ssize_t recv (int, void *, size_t, int); --#if (defined __wasilibc_unmodified_upstream) || (defined __wasilibc_use_wasip2) - ssize_t sendto (int, const void *, size_t, int, const struct sockaddr *, socklen_t); - ssize_t recvfrom (int, void *__restrict, size_t, int, struct sockaddr *__restrict, socklen_t *__restrict); --#endif - #ifdef __wasilibc_unmodified_upstream /* WASI has no sendmsg/recvmsg */ - ssize_t sendmsg (int, const struct msghdr *, int); - ssize_t recvmsg (int, struct msghdr *, int); - #endif - - int getsockopt (int, int, int, void *__restrict, socklen_t *__restrict); --#if (defined __wasilibc_unmodified_upstream) || (defined __wasilibc_use_wasip2) - int setsockopt (int, int, int, const void *, socklen_t); --#endif - - #ifdef __wasilibc_unmodified_upstream /* WASI has no sockatmark */ - int sockatmark (int); - ---- a/libc-bottom-half/headers/public/__struct_sockaddr_un.h -+++ b/libc-bottom-half/headers/public/__struct_sockaddr_un.h -@@ -5,6 +5,7 @@ - - struct sockaddr_un { - __attribute__((aligned(__BIGGEST_ALIGNMENT__))) sa_family_t sun_family; -+ char sun_path[108]; - }; - - #endif -diff --git a/libc-bottom-half/sources/host_socket.c b/libc-bottom-half/sources/host_socket.c -new file mode 100644 -index 0000000..975e62a ---- /dev/null -+++ b/libc-bottom-half/sources/host_socket.c -@@ -0,0 +1,729 @@ -+// Socket API via wasmVM host_net imports. -+// -+// Replaces wasi-libc's ENOSYS stubs with calls to our custom WASM imports: -+// host_net.net_socket -> socket() -+// host_net.net_connect -> connect() -+// host_net.net_bind -> bind() -+// host_net.net_listen -> listen() -+// host_net.net_accept -> accept() -+// host_net.net_send -> send() -+// host_net.net_recv -> recv() -+// host_net.net_sendto -> sendto() -+// host_net.net_recvfrom -> recvfrom() -+// host_net.net_close -> (used internally) -+// host_net.net_getaddrinfo -> getaddrinfo() -+// host_net.net_setsockopt -> setsockopt() -+// host_net.net_getsockname -> getsockname() -+// host_net.net_getpeername -> getpeername() -+// host_net.net_poll -> poll() -+// -+// Supports AF_INET, AF_INET6, and AF_UNIX address families. -+// Import signatures match wasmvm/crates/wasi-ext/src/lib.rs exactly. -+ -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+ -+// AF_UNIX support — define sockaddr_un if sys/un.h is not available -+#ifdef __has_include -+# if __has_include() -+# include -+# define HAVE_SYS_UN_H 1 -+# endif -+#endif -+#ifndef HAVE_SYS_UN_H -+# ifndef AF_UNIX -+# define AF_UNIX 1 -+# endif -+# ifndef AF_LOCAL -+# define AF_LOCAL AF_UNIX -+# endif -+struct sockaddr_un { -+ sa_family_t sun_family; -+ char sun_path[108]; -+}; -+#endif -+ -+#define WASM_IMPORT(mod, fn) \ -+ __attribute__((__import_module__(mod), __import_name__(fn))) -+ -+// host_net.net_socket(domain: u32, type: u32, protocol: u32, ret_fd: *mut u32) -> errno -+WASM_IMPORT("host_net", "net_socket") -+uint32_t __host_net_socket(uint32_t domain, uint32_t type, uint32_t protocol, uint32_t *ret_fd); -+ -+// host_net.net_connect(fd: u32, addr_ptr: *const u8, addr_len: u32) -> errno -+WASM_IMPORT("host_net", "net_connect") -+uint32_t __host_net_connect(uint32_t fd, const uint8_t *addr_ptr, uint32_t addr_len); -+ -+// host_net.net_send(fd: u32, buf_ptr: *const u8, buf_len: u32, flags: u32, ret_sent: *mut u32) -> errno -+WASM_IMPORT("host_net", "net_send") -+uint32_t __host_net_send(uint32_t fd, const uint8_t *buf_ptr, uint32_t buf_len, uint32_t flags, uint32_t *ret_sent); -+ -+// host_net.net_recv(fd: u32, buf_ptr: *mut u8, buf_len: u32, flags: u32, ret_received: *mut u32) -> errno -+WASM_IMPORT("host_net", "net_recv") -+uint32_t __host_net_recv(uint32_t fd, uint8_t *buf_ptr, uint32_t buf_len, uint32_t flags, uint32_t *ret_received); -+ -+// host_net.net_close(fd: u32) -> errno -+WASM_IMPORT("host_net", "net_close") -+uint32_t __host_net_close(uint32_t fd); -+ -+// host_net.net_getaddrinfo(host_ptr, host_len, port_ptr, port_len, family, ret_addr, ret_addr_len) -> errno -+WASM_IMPORT("host_net", "net_getaddrinfo") -+uint32_t __host_net_getaddrinfo( -+ const uint8_t *host_ptr, uint32_t host_len, -+ const uint8_t *port_ptr, uint32_t port_len, -+ uint32_t family, -+ uint8_t *ret_addr, uint32_t *ret_addr_len); -+ -+// host_net.net_setsockopt(fd, level, optname, optval_ptr, optval_len) -> errno -+WASM_IMPORT("host_net", "net_setsockopt") -+uint32_t __host_net_setsockopt(uint32_t fd, uint32_t level, uint32_t optname, -+ const uint8_t *optval_ptr, uint32_t optval_len); -+ -+// host_net.net_getsockname(fd, ret_addr, ret_addr_len) -> errno -+WASM_IMPORT("host_net", "net_getsockname") -+uint32_t __host_net_getsockname(uint32_t fd, uint8_t *ret_addr, uint32_t *ret_addr_len); -+ -+// host_net.net_getpeername(fd, ret_addr, ret_addr_len) -> errno -+WASM_IMPORT("host_net", "net_getpeername") -+uint32_t __host_net_getpeername(uint32_t fd, uint8_t *ret_addr, uint32_t *ret_addr_len); -+ -+// host_net.net_bind(fd, addr_ptr, addr_len) -> errno -+WASM_IMPORT("host_net", "net_bind") -+uint32_t __host_net_bind(uint32_t fd, const uint8_t *addr_ptr, uint32_t addr_len); -+ -+// host_net.net_listen(fd, backlog) -> errno -+WASM_IMPORT("host_net", "net_listen") -+uint32_t __host_net_listen(uint32_t fd, uint32_t backlog); -+ -+// host_net.net_accept(fd, ret_fd, ret_addr, ret_addr_len) -> errno -+WASM_IMPORT("host_net", "net_accept") -+uint32_t __host_net_accept(uint32_t fd, uint32_t *ret_fd, uint8_t *ret_addr, uint32_t *ret_addr_len); -+ -+// host_net.net_poll(fds_ptr, nfds, timeout_ms, ret_ready) -> errno -+WASM_IMPORT("host_net", "net_poll") -+uint32_t __host_net_poll(uint8_t *fds_ptr, uint32_t nfds, int32_t timeout_ms, -+ uint32_t *ret_ready); -+ -+// host_net.net_sendto(fd, buf_ptr, buf_len, flags, addr_ptr, addr_len, ret_sent) -> errno -+WASM_IMPORT("host_net", "net_sendto") -+uint32_t __host_net_sendto(uint32_t fd, const uint8_t *buf_ptr, uint32_t buf_len, -+ uint32_t flags, const uint8_t *addr_ptr, uint32_t addr_len, uint32_t *ret_sent); -+ -+// host_net.net_recvfrom(fd, buf_ptr, buf_len, flags, ret_received, ret_addr, ret_addr_len) -> errno -+WASM_IMPORT("host_net", "net_recvfrom") -+uint32_t __host_net_recvfrom(uint32_t fd, uint8_t *buf_ptr, uint32_t buf_len, -+ uint32_t flags, uint32_t *ret_received, uint8_t *ret_addr, uint32_t *ret_addr_len); -+ -+// --------------------------------------------------------------------------- -+// Address serialization helpers (AF_INET, AF_INET6, AF_UNIX) -+// --------------------------------------------------------------------------- -+ -+// Serialize a sockaddr to a string: "host:port" for inet, path for unix. -+// Returns length written (not counting NUL), or -1 on error. -+static int sockaddr_to_string(const struct sockaddr *addr, socklen_t addrlen, -+ char *buf, size_t buflen) { -+ if (addr->sa_family == AF_INET) { -+ const struct sockaddr_in *sin = (const struct sockaddr_in *)addr; -+ char ip[INET_ADDRSTRLEN]; -+ inet_ntop(AF_INET, &sin->sin_addr, ip, sizeof(ip)); -+ unsigned int port = ntohs(sin->sin_port); -+ return snprintf(buf, buflen, "%s:%u", ip, port); -+ } else if (addr->sa_family == AF_INET6) { -+ const struct sockaddr_in6 *sin6 = (const struct sockaddr_in6 *)addr; -+ char ip[INET6_ADDRSTRLEN]; -+ inet_ntop(AF_INET6, &sin6->sin6_addr, ip, sizeof(ip)); -+ unsigned int port = ntohs(sin6->sin6_port); -+ return snprintf(buf, buflen, "%s:%u", ip, port); -+ } else if (addr->sa_family == AF_UNIX) { -+ const struct sockaddr_un *sun = (const struct sockaddr_un *)addr; -+ // Path length: addrlen - offsetof(sockaddr_un, sun_path), or strlen -+ size_t pathlen = addrlen > (socklen_t)__builtin_offsetof(struct sockaddr_un, sun_path) -+ ? (size_t)(addrlen - __builtin_offsetof(struct sockaddr_un, sun_path)) -+ : strlen(sun->sun_path); -+ // Trim trailing NUL if present -+ if (pathlen > 0 && sun->sun_path[pathlen - 1] == '\0') pathlen--; -+ if (pathlen >= buflen) return -1; -+ memcpy(buf, sun->sun_path, pathlen); -+ buf[pathlen] = '\0'; -+ return (int)pathlen; -+ } -+ return -1; // unsupported family -+} -+ -+// Deserialize an address string into a sockaddr. -+// For "host:port" → sockaddr_in or sockaddr_in6; for paths → sockaddr_un. -+// Returns the actual sockaddr size, or 0 on error. -+static socklen_t string_to_sockaddr(const char *str, struct sockaddr *addr, -+ socklen_t addrlen) { -+ // Find last colon to distinguish inet from unix -+ const char *last_colon = strrchr(str, ':'); -+ if (last_colon == NULL) { -+ // No colon → Unix domain socket path -+ struct sockaddr_un sun; -+ memset(&sun, 0, sizeof(sun)); -+ sun.sun_family = AF_UNIX; -+ size_t pathlen = strlen(str); -+ if (pathlen >= sizeof(sun.sun_path)) pathlen = sizeof(sun.sun_path) - 1; -+ memcpy(sun.sun_path, str, pathlen); -+ sun.sun_path[pathlen] = '\0'; -+ socklen_t copy_len = addrlen < (socklen_t)sizeof(sun) ? addrlen : (socklen_t)sizeof(sun); -+ memcpy(addr, &sun, copy_len); -+ return (socklen_t)sizeof(sun); -+ } -+ -+ // Parse host and port -+ char ip[INET6_ADDRSTRLEN]; -+ size_t ip_len = (size_t)(last_colon - str); -+ if (ip_len >= sizeof(ip)) ip_len = sizeof(ip) - 1; -+ memcpy(ip, str, ip_len); -+ ip[ip_len] = '\0'; -+ unsigned int port = 0; -+ for (const char *p = last_colon + 1; *p >= '0' && *p <= '9'; p++) -+ port = port * 10 + (unsigned int)(*p - '0'); -+ -+ // Try IPv4 first -+ struct sockaddr_in sin; -+ memset(&sin, 0, sizeof(sin)); -+ if (inet_pton(AF_INET, ip, &sin.sin_addr) == 1) { -+ sin.sin_family = AF_INET; -+ sin.sin_port = htons((uint16_t)port); -+ socklen_t copy_len = addrlen < (socklen_t)sizeof(sin) ? addrlen : (socklen_t)sizeof(sin); -+ memcpy(addr, &sin, copy_len); -+ return (socklen_t)sizeof(sin); -+ } -+ -+ // Try IPv6 -+ struct sockaddr_in6 sin6; -+ memset(&sin6, 0, sizeof(sin6)); -+ if (inet_pton(AF_INET6, ip, &sin6.sin6_addr) == 1) { -+ sin6.sin6_family = AF_INET6; -+ sin6.sin6_port = htons((uint16_t)port); -+ socklen_t copy_len = addrlen < (socklen_t)sizeof(sin6) ? addrlen : (socklen_t)sizeof(sin6); -+ memcpy(addr, &sin6, copy_len); -+ return (socklen_t)sizeof(sin6); -+ } -+ -+ return 0; // parse failed -+} -+ -+int socket(int domain, int type, int protocol) { -+ uint32_t fd; -+ uint32_t err = __host_net_socket((uint32_t)domain, (uint32_t)type, (uint32_t)protocol, &fd); -+ if (err != 0) { -+ errno = (int)err; -+ return -1; -+ } -+ return (int)fd; -+} -+ -+int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) { -+ char buf[256]; -+ int len = sockaddr_to_string(addr, addrlen, buf, sizeof(buf)); -+ if (len < 0 || (size_t)len >= sizeof(buf)) { -+ errno = (len < 0) ? EAFNOSUPPORT : EINVAL; -+ return -1; -+ } -+ -+ uint32_t err = __host_net_connect((uint32_t)sockfd, (const uint8_t *)buf, (uint32_t)len); -+ if (err != 0) { -+ errno = (int)err; -+ return -1; -+ } -+ return 0; -+} -+ -+int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) { -+ char buf[256]; -+ int len = sockaddr_to_string(addr, addrlen, buf, sizeof(buf)); -+ if (len < 0 || (size_t)len >= sizeof(buf)) { -+ errno = (len < 0) ? EAFNOSUPPORT : EINVAL; -+ return -1; -+ } -+ -+ uint32_t err = __host_net_bind((uint32_t)sockfd, (const uint8_t *)buf, (uint32_t)len); -+ if (err != 0) { -+ errno = (int)err; -+ return -1; -+ } -+ return 0; -+} -+ -+int listen(int sockfd, int backlog) { -+ uint32_t err = __host_net_listen((uint32_t)sockfd, (uint32_t)(backlog > 0 ? backlog : 0)); -+ if (err != 0) { -+ errno = (int)err; -+ return -1; -+ } -+ return 0; -+} -+ -+int accept(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict addrlen) { -+ uint32_t new_fd; -+ uint8_t addr_buf[256]; -+ uint32_t addr_buf_len = sizeof(addr_buf) - 1; -+ -+ uint32_t err = __host_net_accept((uint32_t)sockfd, &new_fd, addr_buf, &addr_buf_len); -+ if (err != 0) { -+ errno = (int)err; -+ return -1; -+ } -+ -+ // Parse remote address string into sockaddr -+ if (addr != NULL && addrlen != NULL) { -+ addr_buf[addr_buf_len] = '\0'; -+ socklen_t actual = string_to_sockaddr((const char *)addr_buf, addr, *addrlen); -+ if (actual > 0) *addrlen = actual; -+ } -+ -+ return (int)new_fd; -+} -+ -+int accept4(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict addrlen, int flags) { -+ if (flags & ~(SOCK_NONBLOCK | SOCK_CLOEXEC)) { -+ errno = EINVAL; -+ return -1; -+ } -+ -+ // CLOEXEC is ignored by wasi-libc today; preserve NONBLOCK validation parity. -+ return accept(sockfd, addr, addrlen); -+} -+ -+ssize_t send(int sockfd, const void *buf, size_t len, int flags) { -+ uint32_t sent; -+ uint32_t err = __host_net_send((uint32_t)sockfd, (const uint8_t *)buf, (uint32_t)len, (uint32_t)flags, &sent); -+ if (err != 0) { -+ errno = (int)err; -+ return -1; -+ } -+ return (ssize_t)sent; -+} -+ -+ssize_t recv(int sockfd, void *buf, size_t len, int flags) { -+ uint32_t received; -+ uint32_t err = __host_net_recv((uint32_t)sockfd, (uint8_t *)buf, (uint32_t)len, (uint32_t)flags, &received); -+ if (err != 0) { -+ errno = (int)err; -+ return -1; -+ } -+ return (ssize_t)received; -+} -+ -+ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, -+ const struct sockaddr *dest_addr, socklen_t addrlen) { -+ // If dest_addr is NULL, behave like send() (connected socket) -+ if (dest_addr == NULL) { -+ return send(sockfd, buf, len, flags); -+ } -+ -+ char addr_str[256]; -+ int slen = sockaddr_to_string(dest_addr, addrlen, addr_str, sizeof(addr_str)); -+ if (slen < 0 || (size_t)slen >= sizeof(addr_str)) { -+ errno = (slen < 0) ? EAFNOSUPPORT : EINVAL; -+ return -1; -+ } -+ -+ uint32_t sent; -+ uint32_t err = __host_net_sendto((uint32_t)sockfd, (const uint8_t *)buf, (uint32_t)len, -+ (uint32_t)flags, (const uint8_t *)addr_str, (uint32_t)slen, &sent); -+ if (err != 0) { -+ errno = (int)err; -+ return -1; -+ } -+ return (ssize_t)sent; -+} -+ -+ssize_t recvfrom(int sockfd, void *restrict buf, size_t len, int flags, -+ struct sockaddr *restrict src_addr, socklen_t *restrict addrlen) { -+ // If src_addr is NULL, behave like recv() -+ if (src_addr == NULL) { -+ return recv(sockfd, buf, len, flags); -+ } -+ -+ uint32_t received; -+ uint8_t ret_addr[256]; -+ uint32_t ret_addr_len = sizeof(ret_addr) - 1; -+ -+ uint32_t err = __host_net_recvfrom((uint32_t)sockfd, (uint8_t *)buf, (uint32_t)len, -+ (uint32_t)flags, &received, ret_addr, &ret_addr_len); -+ if (err != 0) { -+ errno = (int)err; -+ return -1; -+ } -+ -+ // Parse source address string into sockaddr -+ if (addrlen != NULL) { -+ ret_addr[ret_addr_len] = '\0'; -+ socklen_t actual = string_to_sockaddr((const char *)ret_addr, src_addr, *addrlen); -+ if (actual > 0) *addrlen = actual; -+ } -+ -+ return (ssize_t)received; -+} -+ -+int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen) { -+ uint32_t err = __host_net_setsockopt( -+ (uint32_t)sockfd, (uint32_t)level, (uint32_t)optname, -+ (const uint8_t *)optval, (uint32_t)optlen); -+ if (err != 0) { -+ errno = (int)err; -+ return -1; -+ } -+ return 0; -+} -+ -+int getsockname(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict addrlen) { -+ if (addr == NULL || addrlen == NULL) { -+ errno = EINVAL; -+ return -1; -+ } -+ -+ uint8_t addr_buf[256]; -+ uint32_t addr_buf_len = sizeof(addr_buf) - 1; -+ uint32_t err = __host_net_getsockname((uint32_t)sockfd, addr_buf, &addr_buf_len); -+ if (err != 0) { -+ errno = (int)err; -+ return -1; -+ } -+ -+ addr_buf[addr_buf_len] = '\0'; -+ socklen_t actual = string_to_sockaddr((const char *)addr_buf, addr, *addrlen); -+ if (actual == 0) { -+ errno = EINVAL; -+ return -1; -+ } -+ *addrlen = actual; -+ return 0; -+} -+ -+int getpeername(int sockfd, struct sockaddr *restrict addr, socklen_t *restrict addrlen) { -+ if (addr == NULL || addrlen == NULL) { -+ errno = EINVAL; -+ return -1; -+ } -+ -+ uint8_t addr_buf[256]; -+ uint32_t addr_buf_len = sizeof(addr_buf) - 1; -+ uint32_t err = __host_net_getpeername((uint32_t)sockfd, addr_buf, &addr_buf_len); -+ if (err != 0) { -+ errno = (int)err; -+ return -1; -+ } -+ -+ addr_buf[addr_buf_len] = '\0'; -+ socklen_t actual = string_to_sockaddr((const char *)addr_buf, addr, *addrlen); -+ if (actual == 0) { -+ errno = EINVAL; -+ return -1; -+ } -+ *addrlen = actual; -+ return 0; -+} -+ -+int gethostname(char *name, size_t len) { -+ const char *hostname = "sandbox"; -+ size_t hlen = strlen(hostname); -+ if (hlen >= len) { -+ errno = ENAMETOOLONG; -+ return -1; -+ } -+ memcpy(name, hostname, hlen + 1); -+ return 0; -+} -+ -+// Parse a port number from a string, return -1 on failure -+static int parse_port(const char *s) { -+ if (!s || !*s) return 0; -+ int port = 0; -+ for (; *s; s++) { -+ if (*s < '0' || *s > '9') return -1; -+ port = port * 10 + (*s - '0'); -+ if (port > 65535) return -1; -+ } -+ return port; -+} -+ -+// Skip whitespace in JSON parsing -+static const char *skip_ws(const char *p) { -+ while (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r') p++; -+ return p; -+} -+ -+// Parse a JSON string value (expects p pointing at opening quote) -+// Writes into dst (up to dst_len-1 chars), returns pointer past closing quote -+static const char *parse_json_str(const char *p, char *dst, size_t dst_len) { -+ if (*p != '"') return NULL; -+ p++; -+ size_t i = 0; -+ while (*p && *p != '"') { -+ if (i < dst_len - 1) dst[i++] = *p; -+ p++; -+ } -+ if (i < dst_len) dst[i] = '\0'; -+ if (*p == '"') p++; -+ return p; -+} -+ -+// Allocate and populate one addrinfo entry -+static struct addrinfo *make_addrinfo(const char *addr_str, int family, int port, int socktype) { -+ size_t sa_len; -+ if (family == AF_INET) -+ sa_len = sizeof(struct sockaddr_in); -+ else if (family == AF_INET6) -+ sa_len = sizeof(struct sockaddr_in6); -+ else -+ return NULL; -+ -+ struct addrinfo *ai = calloc(1, sizeof(struct addrinfo) + sa_len); -+ if (!ai) return NULL; -+ -+ ai->ai_family = family; -+ ai->ai_socktype = socktype ? socktype : SOCK_STREAM; -+ ai->ai_protocol = (ai->ai_socktype == SOCK_STREAM) ? IPPROTO_TCP : IPPROTO_UDP; -+ ai->ai_addrlen = sa_len; -+ ai->ai_addr = (struct sockaddr *)((char *)ai + sizeof(struct addrinfo)); -+ -+ if (family == AF_INET) { -+ struct sockaddr_in *sin = (struct sockaddr_in *)ai->ai_addr; -+ sin->sin_family = AF_INET; -+ sin->sin_port = htons((uint16_t)port); -+ inet_pton(AF_INET, addr_str, &sin->sin_addr); -+ } else { -+ struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)ai->ai_addr; -+ sin6->sin6_family = AF_INET6; -+ sin6->sin6_port = htons((uint16_t)port); -+ inet_pton(AF_INET6, addr_str, &sin6->sin6_addr); -+ } -+ return ai; -+} -+ -+int getaddrinfo(const char *restrict host, const char *restrict serv, -+ const struct addrinfo *restrict hints, struct addrinfo **restrict res) { -+ if (!host && !serv) return EAI_NONAME; -+ -+ const char *host_str = host ? host : ""; -+ const char *port_str = serv ? serv : "0"; -+ -+ int port = parse_port(port_str); -+ int socktype = hints ? hints->ai_socktype : 0; -+ int family = hints ? hints->ai_family : AF_UNSPEC; -+ uint32_t query_family = 0; -+ if (family == AF_INET) query_family = 4; -+ else if (family == AF_INET6) query_family = 6; -+ else if (family != AF_UNSPEC) return EAI_FAMILY; -+ -+ // Call host bridge for DNS resolution -+ uint8_t buf[4096]; -+ uint32_t buf_len = sizeof(buf) - 1; -+ uint32_t err = __host_net_getaddrinfo( -+ (const uint8_t *)host_str, (uint32_t)strlen(host_str), -+ (const uint8_t *)port_str, (uint32_t)strlen(port_str), -+ query_family, -+ buf, &buf_len); -+ if (err != 0) return EAI_NONAME; -+ -+ buf[buf_len] = '\0'; -+ const char *json = (const char *)buf; -+ -+ // Parse JSON array: [{"addr":"...","family":N}, ...] -+ struct addrinfo *head = NULL; -+ struct addrinfo *tail = NULL; -+ int count = 0; -+ -+ const char *p = skip_ws(json); -+ if (*p != '[') return EAI_FAIL; -+ p++; -+ -+ while (*p) { -+ p = skip_ws(p); -+ if (*p == ']') break; -+ if (*p == ',') { p++; continue; } -+ if (*p != '{') break; -+ p++; -+ -+ char addr_str[64] = {0}; -+ int family = 0; -+ -+ // Parse object fields -+ while (*p && *p != '}') { -+ p = skip_ws(p); -+ if (*p == ',') { p++; continue; } -+ -+ char key[16] = {0}; -+ p = parse_json_str(p, key, sizeof(key)); -+ if (!p) goto parse_error; -+ -+ p = skip_ws(p); -+ if (*p != ':') goto parse_error; -+ p++; -+ p = skip_ws(p); -+ -+ if (strcmp(key, "addr") == 0) { -+ p = parse_json_str(p, addr_str, sizeof(addr_str)); -+ if (!p) goto parse_error; -+ } else if (strcmp(key, "family") == 0) { -+ family = 0; -+ while (*p >= '0' && *p <= '9') { -+ family = family * 10 + (*p - '0'); -+ p++; -+ } -+ // Map Node.js family (4/6) to AF_INET/AF_INET6 -+ if (family == 4) family = AF_INET; -+ else if (family == 6) family = AF_INET6; -+ } else { -+ // Skip unknown value -+ if (*p == '"') { -+ char tmp[64]; -+ p = parse_json_str(p, tmp, sizeof(tmp)); -+ if (!p) goto parse_error; -+ } else { -+ while (*p && *p != ',' && *p != '}') p++; -+ } -+ } -+ } -+ if (*p == '}') p++; -+ -+ // Filter by requested family -+ if (hints && hints->ai_family != AF_UNSPEC && hints->ai_family != family) -+ continue; -+ -+ struct addrinfo *ai = make_addrinfo(addr_str, family, port, socktype); -+ if (!ai) { freeaddrinfo(head); return EAI_MEMORY; } -+ -+ if (tail) { tail->ai_next = ai; tail = ai; } -+ else { head = tail = ai; } -+ count++; -+ } -+ -+ if (count == 0) return EAI_NONAME; -+ *res = head; -+ return 0; -+ -+parse_error: -+ freeaddrinfo(head); -+ return EAI_FAIL; -+} -+ -+void freeaddrinfo(struct addrinfo *res) { -+ while (res) { -+ struct addrinfo *next = res->ai_next; -+ free(res); -+ res = next; -+ } -+} -+ -+const char *gai_strerror(int ecode) { -+ switch (ecode) { -+ case 0: return "Success"; -+ case EAI_BADFLAGS: return "Invalid flags"; -+ case EAI_NONAME: return "Name or service not known"; -+ case EAI_AGAIN: return "Temporary failure in name resolution"; -+ case EAI_FAIL: return "Non-recoverable failure in name resolution"; -+ case EAI_FAMILY: return "Address family not supported"; -+ case EAI_SOCKTYPE: return "Socket type not supported"; -+ case EAI_SERVICE: return "Servname not supported for ai_socktype"; -+ case EAI_MEMORY: return "Memory allocation failure"; -+ case EAI_SYSTEM: return "System error"; -+ case EAI_OVERFLOW: return "Argument buffer overflow"; -+ default: return "Unknown error"; -+ } -+} -+ -+int poll(struct pollfd fds[], nfds_t nfds, int timeout) { -+ // Pass pollfd array directly to host — layout matches (fd:i32, events:i16, revents:i16) -+ uint32_t ready = 0; -+ uint32_t err = __host_net_poll((uint8_t *)fds, (uint32_t)nfds, (int32_t)timeout, &ready); -+ if (err != 0) { -+ errno = (int)err; -+ return -1; -+ } -+ if (ready > (uint32_t)nfds) { -+ errno = EINVAL; -+ return -1; -+ } -+ return (int)ready; -+} -+ -+static int host_fd_isset(int fd, const fd_set *set) { -+ if (!set) return 0; -+ for (size_t i = 0; i < set->__nfds; i++) { -+ if (set->__fds[i] == fd) return 1; -+ } -+ return 0; -+} -+ -+static void host_fd_zero(fd_set *set) { -+ if (set) set->__nfds = 0; -+} -+ -+static void host_fd_set(int fd, fd_set *set) { -+ if (!set) return; -+ for (size_t i = 0; i < set->__nfds; i++) { -+ if (set->__fds[i] == fd) return; -+ } -+ if (set->__nfds < FD_SETSIZE) { -+ set->__fds[set->__nfds++] = fd; -+ } -+} -+ -+int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, -+ fd_set *restrict exceptfds, struct timeval *restrict timeout) { -+ // Convert fd_sets to pollfd array, call poll(), then scatter results back -+ struct pollfd pfds[FD_SETSIZE]; -+ int count = 0; -+ int fd_map[FD_SETSIZE]; // maps pfds index -> original fd -+ -+ for (int fd = 0; fd < nfds && count < FD_SETSIZE; fd++) { -+ short events = 0; -+ if (host_fd_isset(fd, readfds)) events |= POLLIN; -+ if (host_fd_isset(fd, writefds)) events |= POLLOUT; -+ if (events == 0 && !host_fd_isset(fd, exceptfds)) continue; -+ pfds[count].fd = fd; -+ pfds[count].events = events; -+ pfds[count].revents = 0; -+ fd_map[count] = fd; -+ count++; -+ } -+ -+ int timeout_ms = -1; -+ if (timeout) { -+ timeout_ms = (int)(timeout->tv_sec * 1000 + timeout->tv_usec / 1000); -+ } -+ -+ int ret = poll(pfds, (nfds_t)count, timeout_ms); -+ if (ret < 0) return -1; -+ -+ // Clear fd_sets and scatter results -+ host_fd_zero(readfds); -+ host_fd_zero(writefds); -+ host_fd_zero(exceptfds); -+ -+ int ready = 0; -+ for (int i = 0; i < count; i++) { -+ int fd = fd_map[i]; -+ int any = 0; -+ if (readfds && (pfds[i].revents & (POLLIN | POLLHUP | POLLERR))) { -+ host_fd_set(fd, readfds); -+ any = 1; -+ } -+ if (writefds && (pfds[i].revents & POLLOUT)) { -+ host_fd_set(fd, writefds); -+ any = 1; -+ } -+ if (exceptfds && (pfds[i].revents & (POLLERR | POLLNVAL))) { -+ host_fd_set(fd, exceptfds); -+ any = 1; -+ } -+ if (any) ready++; -+ } -+ return ready; -+} diff --git a/registry/native/patches/wasi-libc/0012-posix-spawn-cwd.patch b/registry/native/patches/wasi-libc/0012-posix-spawn-cwd.patch deleted file mode 100644 index bce70048b7..0000000000 --- a/registry/native/patches/wasi-libc/0012-posix-spawn-cwd.patch +++ /dev/null @@ -1,57 +0,0 @@ ---- a/libc-bottom-half/sources/host_spawn_wait.c -+++ b/libc-bottom-half/sources/host_spawn_wait.c -@@ -108,6 +108,17 @@ - return 0; - } - -+static const char *find_pwd_in_env(char *const envp[]) { -+ if (!envp) return NULL; -+ for (int i = 0; envp[i]; i++) { -+ const char *entry = envp[i]; -+ if (strncmp(entry, "PWD=", 4) == 0 && entry[4] == '/') { -+ return entry + 4; -+ } -+ } -+ return NULL; -+} -+ - int posix_spawn_file_actions_adddup2(posix_spawn_file_actions_t *fa, int srcfd, int fd) { - if (srcfd < 0 || fd < 0) return EBADF; - struct __fdop *op = malloc(sizeof(*op)); -@@ -232,6 +243,7 @@ - } - - // Process file_actions in order: extract stdio overrides and handle close/open -+ const char *spawn_cwd = NULL; - uint32_t stdin_fd = 0, stdout_fd = 1, stderr_fd = 2; - if (fa && fa->__actions) { - for (struct __fdop *op = fa->__actions; op; op = op->next) { -@@ -259,16 +271,27 @@ - else close(opened); - break; - } -+ case FDOP_CHDIR: -+ if (op->path[0] == '/') spawn_cwd = op->path; -+ break; - } - } - } - -+ // Resolve cwd: explicit chdir action > inherited PWD > getcwd() > empty -+ char cwd_buf[1024]; -+ const char *cwd_str = spawn_cwd; -+ if (!cwd_str) cwd_str = find_pwd_in_env(env); -+ if (!cwd_str && getcwd(cwd_buf, sizeof(cwd_buf))) { -+ cwd_str = cwd_buf; -+ } -+ - uint32_t child_pid; - uint32_t err = __host_proc_spawn( - argv_buf, (uint32_t)argv_buf_len, - envp_buf ? envp_buf : (const uint8_t *)"", (uint32_t)envp_buf_len, - stdin_fd, stdout_fd, stderr_fd, -- (const uint8_t *)"", 0, -+ cwd_str ? (const uint8_t *)cwd_str : (const uint8_t *)"", cwd_str ? (uint32_t)strlen(cwd_str) : 0, - &child_pid); - - free(argv_buf); diff --git a/registry/native/patches/wasi-libc/0013-posix-socket-header-surface.patch b/registry/native/patches/wasi-libc/0013-posix-socket-header-surface.patch deleted file mode 100644 index d367851b8a..0000000000 --- a/registry/native/patches/wasi-libc/0013-posix-socket-header-surface.patch +++ /dev/null @@ -1,106 +0,0 @@ -Expose the socket option and poll constants needed by POSIX networking -applications when secure-exec routes sockets through host_net on WASI p1. - -This keeps the WASI-native AF_/SOCK_ values, but publishes the common -socket-level constants and non-conflicting extra poll bits that upstream -software such as curl expects to find in and . - ---- a/libc-bottom-half/headers/public/__header_poll.h -+++ b/libc-bottom-half/headers/public/__header_poll.h -@@ -6,6 +6,11 @@ - - #define POLLRDNORM 0x1 - #define POLLWRNORM 0x2 -+#define POLLPRI 0x0008 -+#define POLLRDBAND 0x0010 -+#define POLLWRBAND 0x0020 -+#define POLLMSG 0x0040 -+#define POLLRDHUP 0x0080 - - #define POLLIN POLLRDNORM - #define POLLOUT POLLWRNORM ---- a/libc-bottom-half/headers/public/__header_sys_socket.h -+++ b/libc-bottom-half/headers/public/__header_sys_socket.h -@@ -49,6 +49,82 @@ - #define MSG_TRUNC __WASI_ROFLAGS_RECV_DATA_TRUNCATED - #endif // __wasilibc_use_wasip2 - -+/* -+ * secure-exec exposes a POSIX-style socket layer over host_net for WASI p1. -+ * Keep the WASI-native domain/type values below, but expose the common -+ * socket option and message flag constants that networking applications -+ * expect to find in . -+ */ -+#ifndef MSG_DONTWAIT -+#define MSG_DONTWAIT 0x0040 -+#endif -+ -+#ifndef MSG_NOSIGNAL -+#define MSG_NOSIGNAL 0x4000 -+#endif -+ -+#ifndef SOL_IP -+#define SOL_IP 0 -+#endif -+ -+#ifndef SOL_TCP -+#define SOL_TCP 6 -+#endif -+ -+#ifndef SOL_UDP -+#define SOL_UDP 17 -+#endif -+ -+#ifndef SOL_IPV6 -+#define SOL_IPV6 41 -+#endif -+ -+#ifndef SOMAXCONN -+#define SOMAXCONN 128 -+#endif -+ -+#ifndef SO_REUSEADDR -+#define SO_REUSEADDR 2 -+#endif -+ -+#ifndef SO_ERROR -+#define SO_ERROR 4 -+#endif -+ -+#ifndef SO_SNDBUF -+#define SO_SNDBUF 7 -+#endif -+ -+#ifndef SO_RCVBUF -+#define SO_RCVBUF 8 -+#endif -+ -+#ifndef SO_KEEPALIVE -+#define SO_KEEPALIVE 9 -+#endif -+ -+#ifndef SO_ACCEPTCONN -+#define SO_ACCEPTCONN 30 -+#endif -+ -+#ifndef SO_PROTOCOL -+#define SO_PROTOCOL 38 -+#endif -+ -+#ifndef SO_DOMAIN -+#define SO_DOMAIN 39 -+#endif -+ -+#ifndef SO_RCVTIMEO -+#if __LONG_MAX == 0x7fffffff -+#define SO_RCVTIMEO 66 -+#define SO_SNDTIMEO 67 -+#else -+#define SO_RCVTIMEO 20 -+#define SO_SNDTIMEO 21 -+#endif -+#endif -+ - #define SOCK_DGRAM __WASI_FILETYPE_SOCKET_DGRAM - #define SOCK_STREAM __WASI_FILETYPE_SOCKET_STREAM - diff --git a/registry/native/scripts/patch-std.sh b/registry/native/scripts/patch-std.sh deleted file mode 100755 index db74b8f2bb..0000000000 --- a/registry/native/scripts/patch-std.sh +++ /dev/null @@ -1,194 +0,0 @@ -#!/bin/bash -# patch-std.sh — Apply wasmVM patches to the Rust std source tree -# -# Patches modify the WASI platform implementation in std to support -# process spawning, pipes, user/group IDs, and terminal detection -# via custom host_process/host_user WASM imports. -# -# Usage: -# ./scripts/patch-std.sh [--check] [--reverse] -# -# Options: -# --check Dry-run: verify patches apply cleanly without modifying files -# --reverse Reverse (unapply) previously applied patches - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -WASMCORE_DIR="$(dirname "$SCRIPT_DIR")" -PATCHES_DIR="$WASMCORE_DIR/patches" - -# Get the sysroot for the toolchain specified in rust-toolchain.toml -SYSROOT="$(rustc --print sysroot)" -STD_SRC="$SYSROOT/lib/rustlib/src/rust" - -if [ ! -d "$STD_SRC/library/std" ]; then - echo "ERROR: Rust source not found at $STD_SRC" - echo "Ensure rust-src component is installed: rustup component add rust-src" - exit 1 -fi - -# Parse arguments -MODE="apply" -PATCH_FLAGS="-p1" -for arg in "$@"; do - case "$arg" in - --check) - MODE="check" - ;; - --reverse) - MODE="reverse" - ;; - *) - echo "Unknown argument: $arg" - echo "Usage: $0 [--check] [--reverse]" - exit 1 - ;; - esac -done - -# `patch-std` mutates rustup's installed rust-src tree. CI runners and local -# machines can therefore inherit a partially patched std from a prior failed -# build, and `patch --forward` will skip instead of repairing mismatched hunks. -# Start apply mode from a pristine rust-src component when rustup owns this -# sysroot; non-rustup toolchains keep the historical behavior. -if [ "$MODE" = "apply" ] && command -v rustup >/dev/null 2>&1; then - TOOLCHAIN="$(basename "$SYSROOT")" - case "$SYSROOT" in - */.rustup/toolchains/*) - echo "Refreshing rust-src for toolchain $TOOLCHAIN before applying std patches..." - rm -rf "$SYSROOT" - rustup toolchain install "$TOOLCHAIN" \ - --profile minimal \ - --component rust-src \ - --target wasm32-wasip1 \ - --force >/dev/null - ;; - esac -fi - -# Find std patch files in order (reversed for --reverse mode). -# Only top-level patches/*.patch are std-source patches; subdirectories -# (patches/crates/*, patches/wasi-libc/*) target vendored crates and wasi-libc -# and must NOT be applied to the Rust std source tree, so use -maxdepth 1. -if [ "$MODE" = "reverse" ]; then - PATCH_FILES=$(find "$PATCHES_DIR" -maxdepth 1 -name '*.patch' -type f 2>/dev/null | sort -r) -else - PATCH_FILES=$(find "$PATCHES_DIR" -maxdepth 1 -name '*.patch' -type f 2>/dev/null | sort) -fi - -if [ -z "$PATCH_FILES" ]; then - echo "No patch files found in $PATCHES_DIR" - exit 0 -fi - -PATCH_COUNT=$(echo "$PATCH_FILES" | wc -l) -echo "Found $PATCH_COUNT patch(es) in $PATCHES_DIR" -echo "Rust std source: $STD_SRC" -echo "" - -FAILED=0 - -for PATCH in $PATCH_FILES; do - PATCH_NAME="$(basename "$PATCH")" - - case "$MODE" in - check) - echo -n "Checking $PATCH_NAME ... " - if patch --batch --dry-run $PATCH_FLAGS -d "$STD_SRC" < "$PATCH" > /dev/null 2>&1; then - echo "OK (applies cleanly)" - elif patch --batch --dry-run -R $PATCH_FLAGS -d "$STD_SRC" < "$PATCH" > /dev/null 2>&1; then - echo "OK (already applied)" - else - # When layered patches modify files created by earlier patches, - # neither forward nor reverse will match exactly. Check if any - # new files from this patch exist as a secondary heuristic. - NEW_FILES=$(grep '^+++ b/' "$PATCH" | sed 's|^+++ b/||' | while read -r f; do - [ -f "$STD_SRC/$f" ] && echo "$f" - done) - if [ -n "$NEW_FILES" ]; then - echo "OK (applied, modified by later patch)" - else - echo "FAIL (does not apply)" - FAILED=1 - fi - fi - ;; - apply) - echo -n "Applying $PATCH_NAME ... " - # Use `--forward` (-N) for idempotency: it applies hunks that are not - # yet present and SKIPS hunks already applied (reversed) instead of - # applying them a second time. Without this, additive (insert-only) - # patches stay forward-applicable after they are applied — their - # anchor context is still present — and a naive forward apply inserts - # a duplicate copy, producing E0119 conflicting-implementation errors - # on a re-run. `--forward` makes a second `make wasm` a no-op. - if patch --batch --forward --dry-run $PATCH_FLAGS -d "$STD_SRC" < "$PATCH" > /dev/null 2>&1; then - patch --batch --forward $PATCH_FLAGS -d "$STD_SRC" < "$PATCH" > /dev/null 2>&1 - echo "applied" - else - echo "already applied (skipping)" - fi - ;; - reverse) - echo -n "Reversing $PATCH_NAME ... " - if patch --batch -R $PATCH_FLAGS -d "$STD_SRC" < "$PATCH" > /dev/null 2>&1; then - echo "reversed" - else - echo "not applied (skipping)" - fi - ;; - esac -done - -# Install companion source files that a patch declares (e.g. `pub mod process;`) -# but cannot reliably carry inline: a `diff`/`patch` cannot create a brand-new -# file in the std source from a `/dev/null` hunk reliably across patch versions -# (the hunk is silently skipped, leaving the declared module with no source file -# and a `file not found for module` E0583 build error). Convention mirrors the -# vendored-crate mechanism in patch-vendor.sh: `patches/copy.manifest` with lines -# " ". Example: -# `std/os/wasi/process.rs library/std/src/os/wasi/process.rs` installs the public -# wasi child-pipe fd traits that 0007-wasi-childpipe-fd.patch's `pub mod process;` -# references. Without this the patched std fails to compile (missing module). -MANIFEST="$PATCHES_DIR/copy.manifest" -if [ -f "$MANIFEST" ]; then - while read -r SRC DEST; do - # Skip blank lines and comments. - case "$SRC" in ""|\#*) continue ;; esac - case "$MODE" in - apply) - if [ ! -f "$PATCHES_DIR/$SRC" ]; then - echo "copy.manifest source missing: $SRC" - FAILED=1 - continue - fi - mkdir -p "$(dirname "$STD_SRC/$DEST")" - cp "$PATCHES_DIR/$SRC" "$STD_SRC/$DEST" - echo "Installed companion: $SRC -> $DEST" - ;; - reverse) - rm -f "$STD_SRC/$DEST" - echo "Removed companion: $DEST" - ;; - check) - if [ ! -f "$PATCHES_DIR/$SRC" ]; then - echo "copy.manifest source missing: $SRC" - FAILED=1 - fi - ;; - esac - done < "$MANIFEST" -fi - -echo "" -if [ "$FAILED" -ne 0 ]; then - echo "Some patches failed to apply. Check patch compatibility with current nightly." - exit 1 -else - case "$MODE" in - check) echo "All patches verified." ;; - apply) echo "All patches applied successfully." ;; - reverse) echo "All patches reversed." ;; - esac -fi diff --git a/registry/native/stubs/uucore/src/lib/lib.rs b/registry/native/stubs/uucore/src/lib/lib.rs deleted file mode 100644 index a15343e486..0000000000 --- a/registry/native/stubs/uucore/src/lib/lib.rs +++ /dev/null @@ -1,758 +0,0 @@ -// This file is part of the uutils coreutils package. -// -// For the full copyright and license information, please view the LICENSE -// file that was distributed with this source code. -//! library ~ (core/bundler file) -// #![deny(missing_docs)] //TODO: enable this -// -// spell-checker:ignore sigaction SIGBUS SIGSEGV extendedbigdecimal myutil logind - -// wasmVM patch: enable WASI extensions on nightly for MetadataExt -#![cfg_attr(target_os = "wasi", feature(wasi_ext))] - -// * feature-gated external crates (re-shared as public internal modules) -#[cfg(feature = "libc")] -pub extern crate libc; -#[cfg(all(feature = "windows-sys", target_os = "windows"))] -pub extern crate windows_sys; - -//## internal modules - -mod features; // feature-gated code modules -mod macros; // crate macros (macro_rules-type; exported to `crate::...`) -mod mods; // core cross-platform modules - -pub use uucore_procs::*; - -// * cross-platform modules -pub use crate::mods::clap_localization; -pub use crate::mods::display; -pub use crate::mods::error; -#[cfg(feature = "fs")] -pub use crate::mods::io; -pub use crate::mods::line_ending; -pub use crate::mods::locale; -pub use crate::mods::os; -pub use crate::mods::panic; -pub use crate::mods::posix; - -// * feature-gated modules -#[cfg(feature = "backup-control")] -pub use crate::features::backup_control; -#[cfg(feature = "benchmark")] -pub use crate::features::benchmark; -#[cfg(feature = "buf-copy")] -pub use crate::features::buf_copy; -#[cfg(feature = "checksum")] -pub use crate::features::checksum; -#[cfg(feature = "colors")] -pub use crate::features::colors; -#[cfg(feature = "encoding")] -pub use crate::features::encoding; -#[cfg(feature = "extendedbigdecimal")] -pub use crate::features::extendedbigdecimal; -#[cfg(feature = "fast-inc")] -pub use crate::features::fast_inc; -#[cfg(feature = "format")] -pub use crate::features::format; -#[cfg(feature = "fs")] -pub use crate::features::fs; -#[cfg(feature = "hardware")] -pub use crate::features::hardware; -#[cfg(feature = "i18n-common")] -pub use crate::features::i18n; -#[cfg(feature = "lines")] -pub use crate::features::lines; -#[cfg(any( - feature = "parser", - feature = "parser-num", - feature = "parser-size", - feature = "parser-glob" -))] -pub use crate::features::parser; -#[cfg(feature = "quoting-style")] -pub use crate::features::quoting_style; -#[cfg(feature = "ranges")] -pub use crate::features::ranges; -#[cfg(feature = "ringbuffer")] -pub use crate::features::ringbuffer; -#[cfg(feature = "sum")] -pub use crate::features::sum; -#[cfg(feature = "feat_systemd_logind")] -pub use crate::features::systemd_logind; -#[cfg(feature = "time")] -pub use crate::features::time; -#[cfg(feature = "update-control")] -pub use crate::features::update_control; -#[cfg(feature = "uptime")] -pub use crate::features::uptime; -#[cfg(feature = "version-cmp")] -pub use crate::features::version_cmp; - -// * (platform-specific) feature-gated modules -// ** non-windows (i.e. Unix + Fuchsia) -#[cfg(all(not(windows), feature = "mode"))] -pub use crate::features::mode; -// ** unix + WASI (wasmVM: entries/perms extended to WASI via host_user FFI) -#[cfg(all(any(unix, target_os = "wasi"), feature = "entries"))] -pub use crate::features::entries; -#[cfg(all(any(unix, target_os = "wasi"), feature = "perms"))] -pub use crate::features::perms; -#[cfg(all(unix, any(feature = "pipes", feature = "buf-copy")))] -pub use crate::features::pipes; -#[cfg(all(unix, feature = "process"))] -pub use crate::features::process; -#[cfg(all(unix, not(target_os = "redox")))] -pub use crate::features::safe_traversal; -#[cfg(all(unix, not(target_os = "fuchsia"), feature = "signals"))] -pub use crate::features::signals; -#[cfg(all( - unix, - not(target_os = "android"), - not(target_os = "fuchsia"), - not(target_os = "openbsd"), - not(target_os = "redox"), - feature = "utmpx" -))] -pub use crate::features::utmpx; -// ** windows-only -#[cfg(all(windows, feature = "wide"))] -pub use crate::features::wide; - -#[cfg(feature = "fsext")] -pub use crate::features::fsext; - -#[cfg(all(unix, feature = "fsxattr"))] -pub use crate::features::fsxattr; - -#[cfg(all(feature = "selinux", any(target_os = "linux", target_os = "android")))] -pub use crate::features::selinux; - -#[cfg(all(target_os = "linux", feature = "smack"))] -pub use crate::features::smack; - -//## core functions - -#[cfg(unix)] -use nix::errno::Errno; -#[cfg(unix)] -use nix::sys::signal::{ - SaFlags, SigAction, SigHandler::SigDfl, SigSet, Signal::SIGBUS, Signal::SIGSEGV, sigaction, -}; -use std::borrow::Cow; -use std::ffi::{OsStr, OsString}; -use std::io::{BufRead, BufReader}; -use std::iter; -#[cfg(unix)] -use std::os::unix::ffi::{OsStrExt, OsStringExt}; -use std::str; -use std::str::Utf8Chunk; -use std::sync::{LazyLock, atomic::Ordering}; - -/// Disables the custom signal handlers installed by Rust for stack-overflow handling. With those custom signal handlers processes ignore the first SIGBUS and SIGSEGV signal they receive. -/// See for details. -#[cfg(unix)] -pub fn disable_rust_signal_handlers() -> Result<(), Errno> { - unsafe { - sigaction( - SIGSEGV, - &SigAction::new(SigDfl, SaFlags::empty(), SigSet::all()), - ) - }?; - unsafe { - sigaction( - SIGBUS, - &SigAction::new(SigDfl, SaFlags::empty(), SigSet::all()), - ) - }?; - Ok(()) -} - -pub fn get_canonical_util_name(util_name: &str) -> &str { - let util_name = util_name.strip_prefix("uu_").unwrap_or(util_name); - match util_name { - // uu_test aliases - '[' is an alias for test - "[" => "test", - "dir" => "ls", // dir is an alias for ls - "vdir" => "ls", // vdir is an alias for ls - - // Default case - return the util name as is - _ => util_name, - } -} - -/// Execute utility code for `util`. -/// -/// This macro expands to a main function that invokes the `uumain` function in `util` -/// Exits with code returned by `uumain`. -#[macro_export] -macro_rules! bin { - ($util:ident) => { - pub fn main() { - use std::io::Write; - use uucore::locale; - - // Preserve inherited SIGPIPE settings (e.g., from env --default-signal=PIPE) - uucore::panic::preserve_inherited_sigpipe(); - - // suppress extraneous error output for SIGPIPE failures/panics - uucore::panic::mute_sigpipe_panic(); - locale::setup_localization(uucore::get_canonical_util_name(stringify!($util))) - .unwrap_or_else(|err| { - match err { - uucore::locale::LocalizationError::ParseResource { - error: err_msg, - snippet, - } => eprintln!("Localization parse error at {snippet}: {err_msg:?}"), - other => eprintln!("Could not init the localization system: {other}"), - } - std::process::exit(99) - }); - - // execute utility code - let code = $util::uumain(uucore::args_os()); - // (defensively) flush stdout for utility prior to exit; see - if let Err(e) = std::io::stdout().flush() { - eprintln!("Error flushing stdout: {e}"); - } - - std::process::exit(code); - } - }; -} - -/// Generate the version string for clap. -/// -/// The generated string has the format `() `, for -/// example: "(uutils coreutils) 0.30.0". clap will then prefix it with the util name. -#[macro_export] -macro_rules! crate_version { - () => { - concat!("(uutils coreutils) ", env!("CARGO_PKG_VERSION")) - }; -} - -/// Generate the usage string for clap. -/// -/// This function does two things. It indents all but the first line to align -/// the lines because clap adds "Usage: " to the first line. And it replaces -/// all occurrences of `{}` with the execution phrase and returns the resulting -/// `String`. It does **not** support more advanced formatting features such -/// as `{0}`. -pub fn format_usage(s: &str) -> String { - let s = s.replace('\n', &format!("\n{}", " ".repeat(7))); - s.replace("{}", execution_phrase()) -} - -/// Creates a localized help template for clap commands. -/// -/// This function returns a help template that uses the localized -/// "Usage:" label from the translation files. This ensures consistent -/// localization across all utilities. -/// -/// Note: We avoid using clap's `{usage-heading}` placeholder because it is -/// hardcoded to "Usage:" and cannot be localized. Instead, we manually -/// construct the usage line with the localized label. -/// -/// # Parameters -/// - `util_name`: The name of the utility (for localization setup) -/// -/// # Example -/// ```no_run -/// use clap::Command; -/// use uucore::localized_help_template; -/// -/// let app = Command::new("myutil") -/// .help_template(localized_help_template("myutil")); -/// ``` -pub fn localized_help_template(util_name: &str) -> clap::builder::StyledStr { - use std::io::IsTerminal; - - // Determine if colors should be enabled - same logic as configure_localized_command - let colors_enabled = if std::env::var("NO_COLOR").is_ok() { - false - } else if std::env::var("CLICOLOR_FORCE").is_ok() || std::env::var("FORCE_COLOR").is_ok() { - true - } else { - IsTerminal::is_terminal(&std::io::stdout()) - && std::env::var("TERM").unwrap_or_default() != "dumb" - }; - - localized_help_template_with_colors(util_name, colors_enabled) -} - -/// Create a localized help template with explicit color control -/// This ensures color detection consistency between clap and our template -pub fn localized_help_template_with_colors( - util_name: &str, - colors_enabled: bool, -) -> clap::builder::StyledStr { - use std::fmt::Write; - - // Ensure localization is initialized for this utility - let _ = locale::setup_localization(util_name); - - // Get the localized "Usage" label - let usage_label = crate::locale::translate!("common-usage"); - - // Create a styled template - let mut template = clap::builder::StyledStr::new(); - - // Add the basic template parts - writeln!(template, "{{before-help}}{{about-with-newline}}").unwrap(); - - // Add styled usage header (bold + underline like clap's default) - if colors_enabled { - write!( - template, - "\x1b[1m\x1b[4m{usage_label}:\x1b[0m {{usage}}\n\n" - ) - .unwrap(); - } else { - write!(template, "{usage_label}: {{usage}}\n\n").unwrap(); - } - - // Add the rest - write!(template, "{{all-args}}{{after-help}}").unwrap(); - - template -} - -/// Used to check if the utility is the second argument. -/// Used to check if we were called as a multicall binary (`coreutils `) -pub fn get_utility_is_second_arg() -> bool { - macros::UTILITY_IS_SECOND_ARG.load(Ordering::SeqCst) -} - -/// Change the value of `UTILITY_IS_SECOND_ARG` to true -/// Used to specify that the utility is the second argument. -pub fn set_utility_is_second_arg() { - macros::UTILITY_IS_SECOND_ARG.store(true, Ordering::SeqCst); -} - -// args_os() can be expensive to call, it copies all of argv before iterating. -// So if we want only the first arg or so it's overkill. We cache it. -#[cfg(windows)] -static ARGV: LazyLock> = LazyLock::new(|| wild::args_os().collect()); -#[cfg(not(windows))] -static ARGV: LazyLock> = LazyLock::new(|| std::env::args_os().collect()); - -static UTIL_NAME: LazyLock = LazyLock::new(|| { - let base_index = usize::from(get_utility_is_second_arg()); - let is_man = usize::from(ARGV[base_index].eq("manpage")); - let argv_index = base_index + is_man; - - // Strip directory path to show only utility name - // (e.g., "mkdir" instead of "./target/debug/mkdir") - // in version output, error messages, and other user-facing output - std::path::Path::new(&ARGV[argv_index]) - .file_name() - .unwrap_or(&ARGV[argv_index]) - .to_string_lossy() - .into_owned() -}); - -/// Derive the utility name. -pub fn util_name() -> &'static str { - &UTIL_NAME -} - -static EXECUTION_PHRASE: LazyLock = LazyLock::new(|| { - if get_utility_is_second_arg() { - ARGV.iter() - .take(2) - .map(|os_str| os_str.to_string_lossy().into_owned()) - .collect::>() - .join(" ") - } else { - ARGV[0].to_string_lossy().into_owned() - } -}); - -/// Derive the complete execution phrase for "usage". -pub fn execution_phrase() -> &'static str { - &EXECUTION_PHRASE -} - -/// Args contains arguments passed to the utility. -/// It is a trait that extends `Iterator`. -/// It provides utility functions to collect the arguments into a `Vec`. -/// The collected `Vec` can be lossy or ignore invalid encoding. -pub trait Args: Iterator + Sized { - /// Collects the iterator into a `Vec`, lossily converting the `OsString`s to `Strings`. - fn collect_lossy(self) -> Vec { - self.map(|s| s.to_string_lossy().into_owned()).collect() - } - - /// Collects the iterator into a `Vec`, removing any elements that contain invalid encoding. - fn collect_ignore(self) -> Vec { - self.filter_map(|s| s.into_string().ok()).collect() - } -} - -impl + Sized> Args for T {} - -/// Returns an iterator over the command line arguments as `OsString`s. -/// args_os() can be expensive to call -pub fn args_os() -> impl Iterator { - ARGV.iter().cloned() -} - -/// Returns an iterator over the command line arguments as `OsString`s, filtering out empty arguments. -/// This is useful for handling cases where extra whitespace or empty arguments are present. -/// args_os_filtered() can be expensive to call -pub fn args_os_filtered() -> impl Iterator { - ARGV.iter().filter(|arg| !arg.is_empty()).cloned() -} - -/// Read a line from stdin and check whether the first character is `'y'` or `'Y'` -pub fn read_yes() -> bool { - let mut s = String::new(); - match std::io::stdin().read_line(&mut s) { - Ok(_) => matches!(s.chars().next(), Some('y' | 'Y')), - _ => false, - } -} - -#[derive(Debug)] -pub struct NonUtf8OsStrError { - input_lossy_string: String, -} - -impl std::fmt::Display for NonUtf8OsStrError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use os_display::Quotable; - let quoted = self.input_lossy_string.quote(); - f.write_fmt(format_args!( - "invalid UTF-8 input {quoted} encountered when converting to bytes on a platform that doesn't expose byte arguments", - )) - } -} - -impl std::error::Error for NonUtf8OsStrError {} -impl error::UError for NonUtf8OsStrError {} - -/// Converts an `OsStr` to a UTF-8 `&[u8]`. -/// -/// This always succeeds on unix platforms, -/// and fails on other platforms if the string can't be coerced to UTF-8. -#[cfg_attr(unix, expect(clippy::unnecessary_wraps))] -pub fn os_str_as_bytes(os_string: &OsStr) -> Result<&[u8], NonUtf8OsStrError> { - #[cfg(unix)] - return Ok(os_string.as_bytes()); - - #[cfg(not(unix))] - os_string - .to_str() - .ok_or_else(|| NonUtf8OsStrError { - input_lossy_string: os_string.to_string_lossy().into_owned(), - }) - .map(str::as_bytes) -} - -/// Performs a potentially lossy conversion from `OsStr` to UTF-8 bytes. -/// -/// This is always lossless on unix platforms, -/// and wraps [`OsStr::to_string_lossy`] on non-unix platforms. -pub fn os_str_as_bytes_lossy(os_string: &OsStr) -> Cow<'_, [u8]> { - #[cfg(unix)] - return Cow::from(os_string.as_bytes()); - - #[cfg(not(unix))] - match os_string.to_string_lossy() { - Cow::Borrowed(slice) => Cow::from(slice.as_bytes()), - Cow::Owned(owned) => Cow::from(owned.into_bytes()), - } -} - -/// Converts a `&[u8]` to an `&OsStr`, -/// or parses it as UTF-8 into an [`OsString`] on non-unix platforms. -/// -/// This always succeeds on unix platforms, -/// and fails on other platforms if the bytes can't be parsed as UTF-8. -#[cfg_attr(unix, expect(clippy::unnecessary_wraps))] -pub fn os_str_from_bytes(bytes: &[u8]) -> error::UResult> { - #[cfg(unix)] - return Ok(Cow::Borrowed(OsStr::from_bytes(bytes))); - - #[cfg(not(unix))] - Ok(Cow::Owned(OsString::from(str::from_utf8(bytes).map_err( - |_| error::UUsageError::new(1, "Unable to transform bytes into OsStr"), - )?))) -} - -/// Converts a `Vec` into an `OsString`, parsing as UTF-8 on non-unix platforms. -/// -/// This always succeeds on unix platforms, -/// and fails on other platforms if the bytes can't be parsed as UTF-8. -#[cfg_attr(unix, expect(clippy::unnecessary_wraps))] -pub fn os_string_from_vec(vec: Vec) -> error::UResult { - #[cfg(unix)] - return Ok(OsString::from_vec(vec)); - - #[cfg(not(unix))] - Ok(OsString::from(String::from_utf8(vec).map_err(|_| { - error::UUsageError::new(1, "invalid UTF-8 was detected in one or more arguments") - })?)) -} - -/// Converts an `OsString` into a `Vec`, parsing as UTF-8 on non-unix platforms. -/// -/// This always succeeds on unix platforms, -/// and fails on other platforms if the bytes can't be parsed as UTF-8. -#[cfg_attr(unix, expect(clippy::unnecessary_wraps))] -pub fn os_string_to_vec(s: OsString) -> error::UResult> { - #[cfg(unix)] - let v = s.into_vec(); - #[cfg(not(unix))] - let v = s - .into_string() - .map_err(|_| { - error::UUsageError::new(1, "invalid UTF-8 was detected in one or more arguments") - })? - .into(); - - Ok(v) -} - -/// Equivalent to `std::BufRead::lines` which outputs each line as a `Vec`, -/// which avoids panicking on non UTF-8 input. -pub fn read_byte_lines( - mut buf_reader: BufReader, -) -> impl Iterator>> { - iter::from_fn(move || { - let mut buf = Vec::with_capacity(256); - - match buf_reader.read_until(b'\n', &mut buf) { - Ok(0) => None, - Err(e) => Some(Err(e)), - Ok(_) => { - // Trim (\r)\n - if buf.ends_with(b"\n") { - buf.pop(); - if buf.ends_with(b"\r") { - buf.pop(); - } - } - - Some(Ok(buf)) - } - } - }) -} - -/// Equivalent to `std::BufRead::lines` which outputs each line as an `OsString` -/// This won't panic on non UTF-8 characters on Unix, -/// but it still will on Windows. -pub fn read_os_string_lines( - buf_reader: BufReader, -) -> impl Iterator> { - read_byte_lines(buf_reader) - .map(|byte_line_res| byte_line_res.map(|bl| os_string_from_vec(bl).expect("UTF-8 error"))) -} - -/// Prompt the user with a formatted string and returns `true` if they reply `'y'` or `'Y'` -/// -/// This macro functions accepts the same syntax as `format!`. The prompt is written to -/// `stderr`. A space is also printed at the end for nice spacing between the prompt and -/// the user input. Any input starting with `'y'` or `'Y'` is interpreted as `yes`. -/// -/// # Examples -/// ``` -/// use uucore::prompt_yes; -/// let file = "foo.rs"; -/// prompt_yes!("Do you want to delete '{file}'?"); -/// ``` -/// will print something like below to `stderr` (with `util_name` substituted by the actual -/// util name) and will wait for user input. -/// ```txt -/// util_name: Do you want to delete 'foo.rs'? -/// ``` -#[macro_export] -macro_rules! prompt_yes( - ($($args:tt)+) => ({ - use std::io::Write; - eprint!("{}: ", uucore::util_name()); - eprint!($($args)+); - eprint!(" "); - let res = std::io::stderr().flush().map_err(|err| { - $crate::error::USimpleError::new(1, err.to_string()) - }); - uucore::show_if_err!(res); - uucore::read_yes() - }) -); - -/// Represent either a character or a byte. -/// Used to iterate on partially valid UTF-8 data -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CharByte { - Char(char), - Byte(u8), -} - -impl From for CharByte { - fn from(value: char) -> Self { - Self::Char(value) - } -} - -impl From for CharByte { - fn from(value: u8) -> Self { - Self::Byte(value) - } -} - -impl From<&u8> for CharByte { - fn from(value: &u8) -> Self { - Self::Byte(*value) - } -} - -struct Utf8ChunkIterator<'a> { - iter: Box + 'a>, -} - -impl Iterator for Utf8ChunkIterator<'_> { - type Item = CharByte; - - fn next(&mut self) -> Option { - self.iter.next() - } -} - -impl<'a> From> for Utf8ChunkIterator<'a> { - fn from(chk: Utf8Chunk<'a>) -> Self { - Self { - iter: Box::new( - chk.valid() - .chars() - .map(CharByte::from) - .chain(chk.invalid().iter().map(CharByte::from)), - ), - } - } -} - -/// Iterates on the valid and invalid parts of a byte sequence with regard to -/// the UTF-8 encoding. -pub struct CharByteIterator<'a> { - iter: Box + 'a>, -} - -impl<'a> CharByteIterator<'a> { - /// Make a `CharByteIterator` from a byte slice. - /// [`CharByteIterator`] - pub fn new(input: &'a [u8]) -> Self { - Self { - iter: Box::new(input.utf8_chunks().flat_map(Utf8ChunkIterator::from)), - } - } -} - -impl Iterator for CharByteIterator<'_> { - type Item = CharByte; - - fn next(&mut self) -> Option { - self.iter.next() - } -} - -pub trait IntoCharByteIterator<'a> { - fn iter_char_bytes(self) -> CharByteIterator<'a>; -} - -impl<'a> IntoCharByteIterator<'a> for &'a [u8] { - fn iter_char_bytes(self) -> CharByteIterator<'a> { - CharByteIterator::new(self) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::ffi::OsStr; - - fn make_os_vec(os_str: &OsStr) -> Vec { - vec![ - OsString::from("test"), - OsString::from("สวัสดี"), // spell-checker:disable-line - os_str.to_os_string(), - ] - } - - #[cfg(any(unix, target_os = "redox"))] - fn test_invalid_utf8_args_lossy(os_str: &OsStr) { - // assert our string is invalid utf8 - assert!(os_str.to_os_string().into_string().is_err()); - let test_vec = make_os_vec(os_str); - let collected_to_str = test_vec.clone().into_iter().collect_lossy(); - // conservation of length - when accepting lossy conversion no arguments may be dropped - assert_eq!(collected_to_str.len(), test_vec.len()); - // first indices identical - for index in 0..2 { - assert_eq!(collected_to_str[index], test_vec[index].to_str().unwrap()); - } - // lossy conversion for string with illegal encoding is done - assert_eq!( - *collected_to_str[2], - os_str.to_os_string().to_string_lossy() - ); - } - - #[cfg(any(unix, target_os = "redox"))] - fn test_invalid_utf8_args_ignore(os_str: &OsStr) { - // assert our string is invalid utf8 - assert!(os_str.to_os_string().into_string().is_err()); - let test_vec = make_os_vec(os_str); - let collected_to_str = test_vec.clone().into_iter().collect_ignore(); - // assert that the broken entry is filtered out - assert_eq!(collected_to_str.len(), test_vec.len() - 1); - // assert that the unbroken indices are converted as expected - for index in 0..2 { - assert_eq!( - collected_to_str.get(index).unwrap(), - test_vec.get(index).unwrap().to_str().unwrap() - ); - } - } - - #[test] - fn valid_utf8_encoding_args() { - // create a vector containing only correct encoding - let test_vec = make_os_vec(&OsString::from("test2")); - // expect complete conversion without losses, even when lossy conversion is accepted - let _ = test_vec.into_iter().collect_lossy(); - } - - #[cfg(any(unix, target_os = "redox"))] - #[test] - fn invalid_utf8_args_unix() { - use std::os::unix::ffi::OsStrExt; - - let source = [0x66, 0x6f, 0x80, 0x6f]; - let os_str = OsStr::from_bytes(&source[..]); - test_invalid_utf8_args_lossy(os_str); - test_invalid_utf8_args_ignore(os_str); - } - - #[test] - fn test_format_usage() { - assert_eq!(format_usage("expr EXPRESSION"), "expr EXPRESSION"); - assert_eq!( - format_usage("expr EXPRESSION\nexpr OPTION"), - "expr EXPRESSION\n expr OPTION" - ); - } - - #[test] - fn canonical_util_name_handles_aliases_and_unprefixed_input() { - assert_eq!(get_canonical_util_name("uu_["), "test"); - assert_eq!(get_canonical_util_name("uu_dir"), "ls"); - assert_eq!(get_canonical_util_name("uu_vdir"), "ls"); - assert_eq!(get_canonical_util_name("uu_cat"), "cat"); - assert_eq!(get_canonical_util_name("cat"), "cat"); - assert_eq!(get_canonical_util_name(""), ""); - assert_eq!(get_canonical_util_name("é"), "é"); - } -} diff --git a/registry/package.json b/registry/package.json deleted file mode 100644 index 5a19029fcb..0000000000 --- a/registry/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "agentos-registry", - "private": true, - "type": "module", - "license": "Apache-2.0", - "version": "0.0.0", - "packageManager": "pnpm@10.13.1", - "engines": { - "node": ">=20" - }, - "scripts": { - "check-types": "node -e \"process.exit(0)\"", - "test": "node ./scripts/run-vitest.mjs" - }, - "devDependencies": { - "@agentos-software/common": "workspace:*", - "@rivet-dev/agentos-runtime-core": "workspace:*", - "@agentos-software/curl": "workspace:*", - "@types/node": "^22.10.2", - "@xterm/headless": "^6.0.0", - "typescript": "^5.9.2", - "vitest": "^2.1.9" - } -} diff --git a/registry/scripts/run-vitest.mjs b/registry/scripts/run-vitest.mjs deleted file mode 100644 index c6e2b752f9..0000000000 --- a/registry/scripts/run-vitest.mjs +++ /dev/null @@ -1,32 +0,0 @@ -import { spawnSync } from "node:child_process"; -import { readdirSync } from "node:fs"; -import { resolve } from "node:path"; -import { fileURLToPath } from "node:url"; - -const scriptDir = fileURLToPath(new URL(".", import.meta.url)); -const pnpmStoreDir = resolve(scriptDir, "..", "..", "node_modules", ".pnpm"); -const vitestPackageDir = readdirSync(pnpmStoreDir).find((entry) => - entry.startsWith("vitest@"), -); - -if (!vitestPackageDir) { - throw new Error(`Could not find vitest in ${pnpmStoreDir}`); -} - -const vitestCli = resolve( - pnpmStoreDir, - vitestPackageDir, - "node_modules", - "vitest", - "vitest.mjs", -); - -const result = spawnSync(process.execPath, [vitestCli, "run", ...process.argv.slice(2)], { - stdio: "inherit", -}); - -if (result.error) { - throw result.error; -} - -process.exit(result.status ?? 1); diff --git a/registry/scripts/status.mjs b/registry/scripts/status.mjs deleted file mode 100644 index bd019a3860..0000000000 --- a/registry/scripts/status.mjs +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env node - -/** - * Registry package status: local version, staged bin/, assembled dist/package, - * and (with --remote) the published npm dist-tags per package. - * - * Run from anywhere: node registry/scripts/status.mjs [--remote] - */ - -import { execFileSync } from "node:child_process"; -import { existsSync, readFileSync, readdirSync } from "node:fs"; -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; - -const REGISTRY_ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); -const remote = process.argv.includes("--remote"); - -function countEntries(dir) { - try { - return readdirSync(dir).length; - } catch { - return null; - } -} - -function distTags(name) { - try { - const out = execFileSync( - "npm", - ["view", name, "dist-tags", "--json"], - { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }, - ); - const tags = JSON.parse(out); - return Object.entries(tags) - .map(([tag, version]) => `${tag}=${version}`) - .join(" "); - } catch { - return "(not published)"; - } -} - -const rows = []; -for (const kind of ["software", "agent"]) { - const base = join(REGISTRY_ROOT, kind); - for (const dir of readdirSync(base, { withFileTypes: true })) { - if (!dir.isDirectory()) continue; - const pkgPath = join(base, dir.name, "package.json"); - if (!existsSync(pkgPath)) continue; - const pkg = JSON.parse(readFileSync(pkgPath, "utf8")); - const bin = countEntries(join(base, dir.name, "bin")); - const distPackage = join(base, dir.name, "dist", "package"); - const assembled = existsSync(join(distPackage, "package.json")) - ? (countEntries(join(distPackage, "bin")) ?? 0) - : null; - rows.push({ - kind, - name: pkg.name, - version: pkg.version, - bin: bin === null ? "-" : String(bin), - dist: assembled === null ? "NOT BUILT" : `${assembled} cmds`, - tags: remote ? distTags(pkg.name) : undefined, - }); - } -} - -const width = Math.max(...rows.map((r) => r.name.length)) + 2; -for (const row of rows) { - process.stdout.write( - `${row.name.padEnd(width)} ${row.version.padEnd(28)} bin:${row.bin.padEnd(5)} dist:${row.dist.padEnd(10)}${row.tags !== undefined ? ` ${row.tags}` : ""}\n`, - ); -} -const nativeCommands = join( - REGISTRY_ROOT, - "native/target/wasm32-wasip1/release/commands", -); -const built = countEntries(nativeCommands); -process.stdout.write( - `\nnative commands dir: ${built === null ? "NOT BUILT (run `just registry-native`)" : `${built} entries`}\n`, -); diff --git a/registry/software/browserbase/agentos-package.json b/registry/software/browserbase/agentos-package.json deleted file mode 100644 index 7e64d73e00..0000000000 --- a/registry/software/browserbase/agentos-package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "browserbase", - "registry": { - "title": "Browserbase", - "description": "The Browserbase `browse` CLI: let agents browse the web with a cloud browser, no sandbox required.", - "beta": true, - "types": [ - "browser" - ], - "image": "/images/registry/browserbase.svg" - } -} diff --git a/registry/software/browserbase/package.json b/registry/software/browserbase/package.json deleted file mode 100644 index 97cdfdd52d..0000000000 --- a/registry/software/browserbase/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@agentos-software/browserbase", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "Browserbase browse CLI for secure-exec VMs (cloud browser automation)", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "bin": { - "browse": "./node_modules/browse/bin/run.js" - }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "scripts": { - "build": "tsc && rm -rf dist/package dist/package.tar && agentos-toolchain pack . --out dist/package --prune-native", - "check-types": "tsc --noEmit" - }, - "dependencies": { - "browse": "^0.9.3" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.7.2" - } -} diff --git a/registry/software/browserbase/tsconfig.json b/registry/software/browserbase/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/browserbase/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/build-essential/agentos-package.json b/registry/software/build-essential/agentos-package.json deleted file mode 100644 index 63b0155256..0000000000 --- a/registry/software/build-essential/agentos-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "registry": { - "title": "Build Essential", - "description": "Meta-package: common + git + curl.", - "priority": 100 - } -} diff --git a/registry/software/build-essential/package.json b/registry/software/build-essential/package.json deleted file mode 100644 index 1ce4e95963..0000000000 --- a/registry/software/build-essential/package.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "@agentos-software/build-essential", - "version": "0.3.0-rc.2", - "type": "module", - "license": "Apache-2.0", - "description": "Build-essential WASM command set for secure-exec VMs (common + git + curl)", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "tsc", - "check-types": "tsc --noEmit" - }, - "dependencies": { - "@agentos-software/common": "workspace:*", - "@agentos-software/curl": "workspace:*", - "@agentos-software/git": "workspace:*" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/build-essential/tsconfig.json b/registry/software/build-essential/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/build-essential/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/codex-cli/agentos-package.json b/registry/software/codex-cli/agentos-package.json deleted file mode 100644 index 07b8d1f1d3..0000000000 --- a/registry/software/codex-cli/agentos-package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "commands": [ - "codex", - "codex-exec" - ], - "registry": { - "title": "Codex CLI", - "description": "OpenAI Codex CLI integration.", - "image": "/images/registry/codex.svg" - } -} diff --git a/registry/software/codex-cli/package.json b/registry/software/codex-cli/package.json deleted file mode 100644 index 7e615f70b6..0000000000 --- a/registry/software/codex-cli/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/codex-cli", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "OpenAI Codex command package for secure-exec VMs", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../codex/wasm --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/codex-cli/tsconfig.json b/registry/software/codex-cli/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/codex-cli/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/common/agentos-package.json b/registry/software/common/agentos-package.json deleted file mode 100644 index 4e787157e2..0000000000 --- a/registry/software/common/agentos-package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "registry": { - "title": "Common", - "description": "Meta-package: coreutils + sed + grep + gawk + findutils + diffutils + tar + gzip.", - "priority": 95 - } -} diff --git a/registry/software/common/package.json b/registry/software/common/package.json deleted file mode 100644 index 7a422419f4..0000000000 --- a/registry/software/common/package.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "name": "@agentos-software/common", - "version": "0.3.0-rc.2", - "type": "module", - "license": "Apache-2.0", - "description": "Common WASM command set for secure-exec VMs (coreutils + sed + grep + gawk + findutils + diffutils + tar + gzip)", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "tsc", - "check-types": "tsc --noEmit" - }, - "dependencies": { - "@agentos-software/coreutils": "workspace:*", - "@agentos-software/sed": "workspace:*", - "@agentos-software/grep": "workspace:*", - "@agentos-software/gawk": "workspace:*", - "@agentos-software/findutils": "workspace:*", - "@agentos-software/diffutils": "workspace:*", - "@agentos-software/tar": "workspace:*", - "@agentos-software/gzip": "workspace:*" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/common/tsconfig.json b/registry/software/common/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/common/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/coreutils/agentos-package.json b/registry/software/coreutils/agentos-package.json deleted file mode 100644 index fd258428ba..0000000000 --- a/registry/software/coreutils/agentos-package.json +++ /dev/null @@ -1,127 +0,0 @@ -{ - "commands": [ - "sh", - "arch", - "b2sum", - "base32", - "base64", - "basename", - "basenc", - "cat", - "chmod", - "cksum", - "column", - "comm", - "cp", - "cut", - "date", - "dd", - "dircolors", - "dirname", - "du", - "echo", - "env", - "expand", - "expr", - "factor", - "false", - "fmt", - "fold", - "head", - "join", - "link", - "ln", - "logname", - "ls", - "md5sum", - "mkdir", - "mktemp", - "mv", - "nice", - "nl", - "nohup", - "nproc", - "numfmt", - "od", - "paste", - "pathchk", - "printenv", - "printf", - "ptx", - "pwd", - "readlink", - "realpath", - "rev", - "rm", - "rmdir", - "seq", - "sha1sum", - "sha224sum", - "sha256sum", - "sha384sum", - "sha512sum", - "shred", - "shuf", - "sleep", - "sort", - "split", - "stat", - "stdbuf", - "strings", - "sum", - "tac", - "tail", - "tee", - "test", - "timeout", - "touch", - "tr", - "true", - "truncate", - "tsort", - "uname", - "unexpand", - "uniq", - "unlink", - "wc", - "which", - "whoami", - "yes" - ], - "aliases": { - "bash": "sh", - "more": "cat", - "dir": "ls", - "vdir": "ls", - "[": "test" - }, - "stubs": [ - "chcon", - "runcon", - "chgrp", - "chown", - "chroot", - "df", - "groups", - "id", - "hostname", - "hostid", - "install", - "kill", - "mkfifo", - "mknod", - "pinky", - "who", - "users", - "uptime", - "stty", - "sync", - "tty" - ], - "registry": { - "title": "Coreutils", - "description": "sh, cat, ls, cp, mv, rm, sort, and 80+ essential POSIX commands.", - "priority": 45, - "image": "/images/registry/coreutils.svg" - } -} diff --git a/registry/software/coreutils/package.json b/registry/software/coreutils/package.json deleted file mode 100644 index 1a6941da37..0000000000 --- a/registry/software/coreutils/package.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "@agentos-software/coreutils", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "GNU coreutils for secure-exec VMs (sh, cat, ls, cp, sort, and 80+ commands)", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "build:runtime": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit", - "prepublishOnly": "pnpm run build:runtime" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/coreutils/tsconfig.json b/registry/software/coreutils/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/coreutils/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/curl/agentos-package.json b/registry/software/curl/agentos-package.json deleted file mode 100644 index 4b2d55e758..0000000000 --- a/registry/software/curl/agentos-package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "commands": [ - "curl" - ], - "registry": { - "title": "curl", - "description": "HTTP(S) client for fetching URLs and APIs.", - "priority": 50, - "image": "/images/registry/curl.svg" - } -} diff --git a/registry/software/curl/bin/curl b/registry/software/curl/bin/curl deleted file mode 100644 index 5e85f8a87d..0000000000 Binary files a/registry/software/curl/bin/curl and /dev/null differ diff --git a/registry/software/curl/package.json b/registry/software/curl/package.json deleted file mode 100644 index f915b112f3..0000000000 --- a/registry/software/curl/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/curl", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "curl HTTP client for secure-exec VMs", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/curl/tsconfig.json b/registry/software/curl/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/curl/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/diffutils/agentos-package.json b/registry/software/diffutils/agentos-package.json deleted file mode 100644 index 6c96a19370..0000000000 --- a/registry/software/diffutils/agentos-package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "commands": [ - "diff" - ], - "registry": { - "title": "diffutils", - "description": "GNU diff for comparing files.", - "priority": 6, - "image": "/images/registry/diffutils.svg" - } -} diff --git a/registry/software/diffutils/bin/diff b/registry/software/diffutils/bin/diff deleted file mode 100644 index 003d074f99..0000000000 Binary files a/registry/software/diffutils/bin/diff and /dev/null differ diff --git a/registry/software/diffutils/package.json b/registry/software/diffutils/package.json deleted file mode 100644 index c750ec29bd..0000000000 --- a/registry/software/diffutils/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/diffutils", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "GNU diffutils for secure-exec VMs (diff)", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/diffutils/tsconfig.json b/registry/software/diffutils/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/diffutils/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/duckdb/agentos-package.json b/registry/software/duckdb/agentos-package.json deleted file mode 100644 index 7cd73f4e62..0000000000 --- a/registry/software/duckdb/agentos-package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "commands": [ - "duckdb" - ], - "registry": { - "title": "DuckDB", - "description": "In-process analytics database CLI.", - "priority": 70, - "image": "/images/registry/duckdb.svg" - } -} diff --git a/registry/software/duckdb/package.json b/registry/software/duckdb/package.json deleted file mode 100644 index 19e9bdcb6e..0000000000 --- a/registry/software/duckdb/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/duckdb", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "DuckDB CLI for secure-exec VMs", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/duckdb/tsconfig.json b/registry/software/duckdb/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/duckdb/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/everything/package.json b/registry/software/everything/package.json deleted file mode 100644 index c47f10fd6a..0000000000 --- a/registry/software/everything/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "@agentos-software/everything", - "version": "0.3.0-rc.2", - "type": "module", - "license": "Apache-2.0", - "description": "All available WASM command packages for secure-exec VMs in a single bundle", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "tsc", - "check-types": "tsc --noEmit" - }, - "dependencies": { - "@agentos-software/coreutils": "workspace:*", - "@agentos-software/sed": "workspace:*", - "@agentos-software/grep": "workspace:*", - "@agentos-software/gawk": "workspace:*", - "@agentos-software/findutils": "workspace:*", - "@agentos-software/diffutils": "workspace:*", - "@agentos-software/tar": "workspace:*", - "@agentos-software/gzip": "workspace:*", - "@agentos-software/curl": "workspace:*", - "@agentos-software/zip": "workspace:*", - "@agentos-software/unzip": "workspace:*", - "@agentos-software/jq": "workspace:*", - "@agentos-software/ripgrep": "workspace:*", - "@agentos-software/fd": "workspace:*", - "@agentos-software/tree": "workspace:*", - "@agentos-software/file": "workspace:*", - "@agentos-software/yq": "workspace:*", - "@agentos-software/codex-cli": "workspace:*" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/everything/src/index.ts b/registry/software/everything/src/index.ts deleted file mode 100644 index 58565c3337..0000000000 --- a/registry/software/everything/src/index.ts +++ /dev/null @@ -1,61 +0,0 @@ -import coreutils from "@agentos-software/coreutils"; -import sed from "@agentos-software/sed"; -import grep from "@agentos-software/grep"; -import gawk from "@agentos-software/gawk"; -import findutils from "@agentos-software/findutils"; -import diffutils from "@agentos-software/diffutils"; -import tar from "@agentos-software/tar"; -import gzip from "@agentos-software/gzip"; -import curl from "@agentos-software/curl"; -import zip from "@agentos-software/zip"; -import unzip from "@agentos-software/unzip"; -import jq from "@agentos-software/jq"; -import ripgrep from "@agentos-software/ripgrep"; -import fd from "@agentos-software/fd"; -import tree from "@agentos-software/tree"; -import file from "@agentos-software/file"; -import yq from "@agentos-software/yq"; -import codex from "@agentos-software/codex-cli"; - -const everything = [ - coreutils, - sed, - grep, - gawk, - findutils, - diffutils, - tar, - gzip, - curl, - zip, - unzip, - jq, - ripgrep, - fd, - tree, - file, - yq, - codex, -]; - -export default everything; -export { - coreutils, - sed, - grep, - gawk, - findutils, - diffutils, - tar, - gzip, - curl, - zip, - unzip, - jq, - ripgrep, - fd, - tree, - file, - yq, - codex, -}; diff --git a/registry/software/everything/tsconfig.json b/registry/software/everything/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/everything/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/fd/agentos-package.json b/registry/software/fd/agentos-package.json deleted file mode 100644 index 2667d95742..0000000000 --- a/registry/software/fd/agentos-package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "commands": [ - "fd" - ], - "registry": { - "title": "fd", - "description": "Fast file finder.", - "priority": 30 - } -} diff --git a/registry/software/fd/bin/fd b/registry/software/fd/bin/fd deleted file mode 100644 index b724ec24b1..0000000000 Binary files a/registry/software/fd/bin/fd and /dev/null differ diff --git a/registry/software/fd/package.json b/registry/software/fd/package.json deleted file mode 100644 index fe9fe5eebe..0000000000 --- a/registry/software/fd/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/fd", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "fd fast file finder for secure-exec VMs", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/fd/tsconfig.json b/registry/software/fd/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/fd/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/file/agentos-package.json b/registry/software/file/agentos-package.json deleted file mode 100644 index a0112ed101..0000000000 --- a/registry/software/file/agentos-package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "commands": [ - "file" - ], - "registry": { - "title": "file", - "description": "Detect file types.", - "priority": 2 - } -} diff --git a/registry/software/file/bin/file b/registry/software/file/bin/file deleted file mode 100644 index 1e32fe6064..0000000000 Binary files a/registry/software/file/bin/file and /dev/null differ diff --git a/registry/software/file/package.json b/registry/software/file/package.json deleted file mode 100644 index 0ed0857785..0000000000 --- a/registry/software/file/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/file", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "file type detection for secure-exec VMs", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/file/tsconfig.json b/registry/software/file/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/file/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/findutils/agentos-package.json b/registry/software/findutils/agentos-package.json deleted file mode 100644 index b093937054..0000000000 --- a/registry/software/findutils/agentos-package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "commands": [ - "find", - "xargs" - ], - "registry": { - "title": "findutils", - "description": "GNU find and xargs for file searching and batch execution.", - "priority": 15, - "image": "/images/registry/findutils.svg" - } -} diff --git a/registry/software/findutils/bin/find b/registry/software/findutils/bin/find deleted file mode 100644 index e08e8b58a1..0000000000 Binary files a/registry/software/findutils/bin/find and /dev/null differ diff --git a/registry/software/findutils/bin/xargs b/registry/software/findutils/bin/xargs deleted file mode 100644 index 613a7bb3ea..0000000000 Binary files a/registry/software/findutils/bin/xargs and /dev/null differ diff --git a/registry/software/findutils/package.json b/registry/software/findutils/package.json deleted file mode 100644 index 25723a270a..0000000000 --- a/registry/software/findutils/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/findutils", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "GNU findutils for secure-exec VMs (find, xargs)", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/findutils/tsconfig.json b/registry/software/findutils/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/findutils/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/gawk/agentos-package.json b/registry/software/gawk/agentos-package.json deleted file mode 100644 index 2efcf18704..0000000000 --- a/registry/software/gawk/agentos-package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "commands": [ - "awk" - ], - "registry": { - "title": "gawk", - "description": "GNU awk text processing and data extraction.", - "priority": 20, - "image": "/images/registry/gawk.svg" - } -} diff --git a/registry/software/gawk/bin/awk b/registry/software/gawk/bin/awk deleted file mode 100644 index c4048f65c4..0000000000 Binary files a/registry/software/gawk/bin/awk and /dev/null differ diff --git a/registry/software/gawk/package.json b/registry/software/gawk/package.json deleted file mode 100644 index 87da3b8e24..0000000000 --- a/registry/software/gawk/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/gawk", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "GNU awk text processing for secure-exec VMs", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/gawk/tsconfig.json b/registry/software/gawk/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/gawk/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/git/agentos-package.json b/registry/software/git/agentos-package.json deleted file mode 100644 index 0cfc6ad3dc..0000000000 --- a/registry/software/git/agentos-package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "commands": [ - "git" - ], - "aliases": { - "git-remote-http": "git", - "git-remote-https": "git" - }, - "registry": { - "title": "git", - "description": "Git version control (clone, commit, push, pull).", - "priority": 90, - "image": "/images/registry/git.svg" - } -} diff --git a/registry/software/git/bin/git b/registry/software/git/bin/git deleted file mode 100644 index 1865311237..0000000000 Binary files a/registry/software/git/bin/git and /dev/null differ diff --git a/registry/software/git/bin/git-remote-http b/registry/software/git/bin/git-remote-http deleted file mode 100644 index 1865311237..0000000000 Binary files a/registry/software/git/bin/git-remote-http and /dev/null differ diff --git a/registry/software/git/bin/git-remote-https b/registry/software/git/bin/git-remote-https deleted file mode 100644 index 1865311237..0000000000 Binary files a/registry/software/git/bin/git-remote-https and /dev/null differ diff --git a/registry/software/git/package.json b/registry/software/git/package.json deleted file mode 100644 index 85e9c331af..0000000000 --- a/registry/software/git/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/git", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "git version control for secure-exec VMs (planned)", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/git/tsconfig.json b/registry/software/git/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/git/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/grep/agentos-package.json b/registry/software/grep/agentos-package.json deleted file mode 100644 index a67ea41cf1..0000000000 --- a/registry/software/grep/agentos-package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "commands": [ - "grep" - ], - "aliases": { - "egrep": "grep", - "fgrep": "grep" - }, - "registry": { - "title": "grep", - "description": "GNU grep pattern matching (grep, egrep, fgrep).", - "priority": 40, - "image": "/images/registry/grep.svg" - } -} diff --git a/registry/software/grep/bin/egrep b/registry/software/grep/bin/egrep deleted file mode 100644 index c78e121f14..0000000000 Binary files a/registry/software/grep/bin/egrep and /dev/null differ diff --git a/registry/software/grep/bin/fgrep b/registry/software/grep/bin/fgrep deleted file mode 100644 index c78e121f14..0000000000 Binary files a/registry/software/grep/bin/fgrep and /dev/null differ diff --git a/registry/software/grep/bin/grep b/registry/software/grep/bin/grep deleted file mode 100644 index c78e121f14..0000000000 Binary files a/registry/software/grep/bin/grep and /dev/null differ diff --git a/registry/software/grep/package.json b/registry/software/grep/package.json deleted file mode 100644 index 095dad7b64..0000000000 --- a/registry/software/grep/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/grep", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "GNU grep pattern matching for secure-exec VMs (grep, egrep, fgrep)", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/grep/tsconfig.json b/registry/software/grep/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/grep/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/gzip/agentos-package.json b/registry/software/gzip/agentos-package.json deleted file mode 100644 index 93b1776859..0000000000 --- a/registry/software/gzip/agentos-package.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "commands": [ - "gzip" - ], - "aliases": { - "gunzip": "gzip", - "zcat": "gzip" - }, - "registry": { - "title": "gzip", - "description": "GNU gzip compression (gzip, gunzip, zcat).", - "priority": 8, - "image": "/images/registry/gzip.svg" - } -} diff --git a/registry/software/gzip/bin/gunzip b/registry/software/gzip/bin/gunzip deleted file mode 100644 index e60ebe5317..0000000000 Binary files a/registry/software/gzip/bin/gunzip and /dev/null differ diff --git a/registry/software/gzip/bin/gzip b/registry/software/gzip/bin/gzip deleted file mode 100644 index e60ebe5317..0000000000 Binary files a/registry/software/gzip/bin/gzip and /dev/null differ diff --git a/registry/software/gzip/bin/zcat b/registry/software/gzip/bin/zcat deleted file mode 100644 index e60ebe5317..0000000000 Binary files a/registry/software/gzip/bin/zcat and /dev/null differ diff --git a/registry/software/gzip/package.json b/registry/software/gzip/package.json deleted file mode 100644 index 088279e99a..0000000000 --- a/registry/software/gzip/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/gzip", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "GNU gzip compression for secure-exec VMs (gzip, gunzip, zcat)", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/gzip/tsconfig.json b/registry/software/gzip/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/gzip/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/http-get/agentos-package.json b/registry/software/http-get/agentos-package.json deleted file mode 100644 index d45077f00b..0000000000 --- a/registry/software/http-get/agentos-package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "commands": [ - "http_get" - ], - "registry": { - "title": "http-get", - "description": "Minimal HTTP GET fetch helper." - } -} diff --git a/registry/software/http-get/bin/http_get b/registry/software/http-get/bin/http_get deleted file mode 100644 index 52a1fb8342..0000000000 Binary files a/registry/software/http-get/bin/http_get and /dev/null differ diff --git a/registry/software/http-get/package.json b/registry/software/http-get/package.json deleted file mode 100644 index 38ed2ed4fc..0000000000 --- a/registry/software/http-get/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/http-get", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "Minimal HTTP GET fetch helper for secure-exec VMs", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/http-get/tsconfig.json b/registry/software/http-get/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/http-get/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/jq/agentos-package.json b/registry/software/jq/agentos-package.json deleted file mode 100644 index b5cc20dd01..0000000000 --- a/registry/software/jq/agentos-package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "commands": [ - "jq" - ], - "registry": { - "title": "jq", - "description": "Lightweight JSON processor.", - "priority": 80, - "image": "/images/registry/jq.svg" - } -} diff --git a/registry/software/jq/bin/jq b/registry/software/jq/bin/jq deleted file mode 100644 index 22f0359722..0000000000 Binary files a/registry/software/jq/bin/jq and /dev/null differ diff --git a/registry/software/jq/package.json b/registry/software/jq/package.json deleted file mode 100644 index f4be15e4a0..0000000000 --- a/registry/software/jq/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/jq", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "jq JSON processor for secure-exec VMs", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/jq/tsconfig.json b/registry/software/jq/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/jq/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/ripgrep/agentos-package.json b/registry/software/ripgrep/agentos-package.json deleted file mode 100644 index f0ba60fdda..0000000000 --- a/registry/software/ripgrep/agentos-package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "commands": [ - "rg" - ], - "registry": { - "title": "ripgrep", - "description": "Fast recursive search (rg).", - "priority": 85 - } -} diff --git a/registry/software/ripgrep/bin/rg b/registry/software/ripgrep/bin/rg deleted file mode 100644 index 76a63b75ea..0000000000 Binary files a/registry/software/ripgrep/bin/rg and /dev/null differ diff --git a/registry/software/ripgrep/package.json b/registry/software/ripgrep/package.json deleted file mode 100644 index 5542da9f8e..0000000000 --- a/registry/software/ripgrep/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/ripgrep", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "ripgrep fast search for secure-exec VMs", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/ripgrep/tsconfig.json b/registry/software/ripgrep/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/ripgrep/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/sed/agentos-package.json b/registry/software/sed/agentos-package.json deleted file mode 100644 index 4bc0fe5a76..0000000000 --- a/registry/software/sed/agentos-package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "commands": [ - "sed" - ], - "registry": { - "title": "sed", - "description": "GNU stream editor for text transformation.", - "priority": 35, - "image": "/images/registry/sed.svg" - } -} diff --git a/registry/software/sed/bin/sed b/registry/software/sed/bin/sed deleted file mode 100644 index cf0fd26325..0000000000 Binary files a/registry/software/sed/bin/sed and /dev/null differ diff --git a/registry/software/sed/package.json b/registry/software/sed/package.json deleted file mode 100644 index a5f127ea0d..0000000000 --- a/registry/software/sed/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/sed", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "GNU sed stream editor for secure-exec VMs", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/sed/tsconfig.json b/registry/software/sed/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/sed/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/sqlite3/agentos-package.json b/registry/software/sqlite3/agentos-package.json deleted file mode 100644 index 6d140af495..0000000000 --- a/registry/software/sqlite3/agentos-package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "commands": [ - "sqlite3" - ], - "registry": { - "title": "SQLite3", - "description": "SQLite database command-line interface.", - "priority": 75, - "image": "/images/registry/sqlite3.svg" - } -} diff --git a/registry/software/sqlite3/bin/sqlite3 b/registry/software/sqlite3/bin/sqlite3 deleted file mode 100755 index a9ee1f206f..0000000000 Binary files a/registry/software/sqlite3/bin/sqlite3 and /dev/null differ diff --git a/registry/software/sqlite3/package.json b/registry/software/sqlite3/package.json deleted file mode 100644 index 26841719db..0000000000 --- a/registry/software/sqlite3/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/sqlite3", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "SQLite3 CLI for secure-exec VMs", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/sqlite3/tsconfig.json b/registry/software/sqlite3/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/sqlite3/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/tar/agentos-package.json b/registry/software/tar/agentos-package.json deleted file mode 100644 index ab25a8bbea..0000000000 --- a/registry/software/tar/agentos-package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "commands": [ - "tar" - ], - "registry": { - "title": "tar", - "description": "GNU tar archiver.", - "priority": 60, - "image": "/images/registry/tar.svg" - } -} diff --git a/registry/software/tar/bin/tar b/registry/software/tar/bin/tar deleted file mode 100644 index 5d813b99d9..0000000000 Binary files a/registry/software/tar/bin/tar and /dev/null differ diff --git a/registry/software/tar/package.json b/registry/software/tar/package.json deleted file mode 100644 index 957a20ed65..0000000000 --- a/registry/software/tar/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/tar", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "GNU tar archiver for secure-exec VMs", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/tar/tsconfig.json b/registry/software/tar/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/tar/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/tree/agentos-package.json b/registry/software/tree/agentos-package.json deleted file mode 100644 index f6d6e4e996..0000000000 --- a/registry/software/tree/agentos-package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "commands": [ - "tree" - ], - "registry": { - "title": "tree", - "description": "Display directory structure as a tree.", - "priority": 25 - } -} diff --git a/registry/software/tree/bin/tree b/registry/software/tree/bin/tree deleted file mode 100644 index 49e9c7c18d..0000000000 Binary files a/registry/software/tree/bin/tree and /dev/null differ diff --git a/registry/software/tree/package.json b/registry/software/tree/package.json deleted file mode 100644 index cca158354b..0000000000 --- a/registry/software/tree/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/tree", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "tree directory listing for secure-exec VMs", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/tree/tsconfig.json b/registry/software/tree/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/tree/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/unzip/agentos-package.json b/registry/software/unzip/agentos-package.json deleted file mode 100644 index 8f395875f0..0000000000 --- a/registry/software/unzip/agentos-package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "commands": [ - "unzip" - ], - "registry": { - "title": "unzip", - "description": "Extract zip archives.", - "priority": 10 - } -} diff --git a/registry/software/unzip/bin/unzip b/registry/software/unzip/bin/unzip deleted file mode 100644 index 07f40041e1..0000000000 Binary files a/registry/software/unzip/bin/unzip and /dev/null differ diff --git a/registry/software/unzip/package.json b/registry/software/unzip/package.json deleted file mode 100644 index 3c400a4cb2..0000000000 --- a/registry/software/unzip/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/unzip", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "unzip archive extraction for secure-exec VMs", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/unzip/tsconfig.json b/registry/software/unzip/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/unzip/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/vim/agentos-package.json b/registry/software/vim/agentos-package.json deleted file mode 100644 index 24e713cc33..0000000000 --- a/registry/software/vim/agentos-package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "vim", - "commands": [ - "vim" - ], - "provides": { - "env": { - "VIM": "/usr/local/share/vim", - "VIMRUNTIME": "/usr/local/share/vim/vim92" - }, - "files": [ - { - "source": "share/vim/vim92", - "target": "/usr/local/share/vim/vim92" - } - ] - }, - "registry": { - "title": "vim", - "description": "Vim text editor with bundled runtime.", - "priority": 65, - "image": "/images/registry/vim.svg" - } -} diff --git a/registry/software/vim/package.json b/registry/software/vim/package.json deleted file mode 100644 index 24e8f7d0d3..0000000000 --- a/registry/software/vim/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@agentos-software/vim", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "vim for secure-exec VMs (wasm build + bundled runtime via provides)", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && node ./scripts/stage-runtime.mjs && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/vim/tsconfig.json b/registry/software/vim/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/vim/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/vix/agentos-package.json b/registry/software/vix/agentos-package.json deleted file mode 100644 index 88ce6ad04a..0000000000 --- a/registry/software/vix/agentos-package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "name": "vix", - "commands": [ - "vix" - ], - "registry": { - "title": "vix", - "description": "Lightweight vi-style text editor." - } -} diff --git a/registry/software/vix/package.json b/registry/software/vix/package.json deleted file mode 100644 index e51042dc9b..0000000000 --- a/registry/software/vix/package.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "@agentos-software/vix", - "version": "0.1.0", - "type": "module", - "license": "Apache-2.0", - "description": "vix editor for secure-exec VMs (wasm build)", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js", - "default": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/vix/tsconfig.json b/registry/software/vix/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/vix/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/wget/agentos-package.json b/registry/software/wget/agentos-package.json deleted file mode 100644 index db274f6149..0000000000 --- a/registry/software/wget/agentos-package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "commands": [ - "wget" - ], - "registry": { - "title": "wget", - "description": "GNU wget file downloader.", - "priority": 55, - "image": "/images/registry/wget.svg" - } -} diff --git a/registry/software/wget/package.json b/registry/software/wget/package.json deleted file mode 100644 index c73531a083..0000000000 --- a/registry/software/wget/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/wget", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "GNU wget HTTP client for secure-exec VMs", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/wget/tsconfig.json b/registry/software/wget/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/wget/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/yq/agentos-package.json b/registry/software/yq/agentos-package.json deleted file mode 100644 index b753aea6ee..0000000000 --- a/registry/software/yq/agentos-package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "commands": [ - "yq" - ], - "registry": { - "title": "yq", - "description": "YAML/JSON processor.", - "priority": 4 - } -} diff --git a/registry/software/yq/bin/yq b/registry/software/yq/bin/yq deleted file mode 100644 index 98a2de41ee..0000000000 Binary files a/registry/software/yq/bin/yq and /dev/null differ diff --git a/registry/software/yq/package.json b/registry/software/yq/package.json deleted file mode 100644 index 4ce45c0b4b..0000000000 --- a/registry/software/yq/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/yq", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "yq YAML/JSON processor for secure-exec VMs", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/yq/tsconfig.json b/registry/software/yq/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/yq/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/software/zip/agentos-package.json b/registry/software/zip/agentos-package.json deleted file mode 100644 index 298bd79a77..0000000000 --- a/registry/software/zip/agentos-package.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "commands": [ - "zip" - ], - "registry": { - "title": "zip", - "description": "Create zip archives.", - "priority": 12 - } -} diff --git a/registry/software/zip/bin/zip b/registry/software/zip/bin/zip deleted file mode 100644 index 8324bdc9fe..0000000000 Binary files a/registry/software/zip/bin/zip and /dev/null differ diff --git a/registry/software/zip/package.json b/registry/software/zip/package.json deleted file mode 100644 index 73b23a0971..0000000000 --- a/registry/software/zip/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "@agentos-software/zip", - "version": "0.3.3", - "type": "module", - "license": "Apache-2.0", - "description": "zip archive creation for secure-exec VMs", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "!dist/package", - "!dist/package.tar" - ], - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "agentos-toolchain stage --commands-dir ../../native/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", - "check-types": "tsc --noEmit" - }, - "devDependencies": { - "@agentos-software/manifest": "workspace:*", - "@rivet-dev/agentos-toolchain": "workspace:*", - "@types/node": "^22.10.2", - "typescript": "^5.9.2" - } -} diff --git a/registry/software/zip/tsconfig.json b/registry/software/zip/tsconfig.json deleted file mode 100644 index 8f24167afd..0000000000 --- a/registry/software/zip/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist", - "rootDir": "./src" - }, - "include": ["src/**/*"] -} diff --git a/registry/tests/helpers.ts b/registry/tests/helpers.ts deleted file mode 100644 index 963f5afd7b..0000000000 --- a/registry/tests/helpers.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { existsSync } from "node:fs"; -import { resolve, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; -import { describe, it } from "vitest"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - -/** Directory containing WASM command binaries built from Rust. */ -export const COMMANDS_DIR = resolve( - process.env.AGENTOS_WASM_COMMANDS_DIR ?? - resolve(__dirname, "../native/target/wasm32-wasip1/release/commands"), -); - -/** Directory containing C-compiled WASM binaries. */ -export const C_BUILD_DIR = resolve( - process.env.AGENTOS_C_WASM_COMMANDS_DIR ?? - resolve(__dirname, "../native/c/build/"), -); - -/** Whether the main WASM command binaries are available (includes 'sh'). */ -export const hasWasmBinaries = - existsSync(COMMANDS_DIR) && existsSync(resolve(COMMANDS_DIR, "sh")); - -/** - * Check whether specific C WASM binaries are present. - * @param names - Binary names to check for inside C_BUILD_DIR. - * @returns true if all requested binaries exist. - */ -export function hasCWasmBinaries(...names: string[]): boolean { - if (!existsSync(C_BUILD_DIR)) return false; - return names.every((name) => existsSync(resolve(C_BUILD_DIR, name))); -} - -/** - * Returns a skip-reason string if WASM binaries are missing, or false if - * they are available and tests should run. - */ -export function skipReason(): string | false { - if (!hasWasmBinaries) { - return `WASM binaries not found at ${COMMANDS_DIR} — build with \`make wasm\` first`; - } - return false; -} - -export function describeIf( - condition: unknown, - ...args: Parameters -): void { - if (condition) { - // Vitest's overloaded tuple shape is awkward to preserve across helper forwarding. - // @ts-expect-error forwarded describe() arguments stay runtime-compatible. - describe(...args); - return; - } - const [name] = args; - describe.skip(`${String(name)} [environment prerequisites not met]`, () => {}); -} - -export function itIf( - condition: unknown, - ...args: Parameters -): void { - if (condition) { - // Vitest's overloaded tuple shape is awkward to preserve across helper forwarding. - // @ts-expect-error forwarded it() arguments stay runtime-compatible. - it(...args); - return; - } - const [name] = args; - it.skip(`${String(name)} [environment prerequisites not met]`, () => {}); -} - -// Re-exports from the repo-owned generic runtime surface. -export { - AF_INET, - AF_UNIX, - allowAll, - createInMemoryFileSystem, - SIGTERM, - SOCK_DGRAM, - SOCK_STREAM, -} from "@rivet-dev/agentos-runtime-core/test-runtime"; -import { - allowAll, - createKernel as createKernelBase, -} from "@rivet-dev/agentos-runtime-core/test-runtime"; -export type { - DriverProcess, - Kernel, - KernelInterface, - KernelRuntimeDriver, - ProcessContext, - VirtualFileSystem, -} from "@rivet-dev/agentos-runtime-core/test-runtime"; -export { - createWasmVmRuntime, - DEFAULT_FIRST_PARTY_TIERS, - WASMVM_COMMANDS, - type PermissionTier, - type WasmVmRuntimeOptions, -} from "@rivet-dev/agentos-runtime-core/test-runtime"; -export { - createNodeHostNetworkAdapter, - createNodeRuntime, - NodeFileSystem, -} from "@rivet-dev/agentos-runtime-core/test-runtime"; -export { TerminalHarness } from "./terminal-harness.js"; - -/** - * Registry integration tests assume they can bootstrap runtimes and /bin stubs - * unless they explicitly opt into a stricter permission policy. - */ -export function createKernel( - options: Parameters[0], -): ReturnType { - return createKernelBase({ - ...options, - permissions: options.permissions ?? allowAll, - }); -} diff --git a/registry/tests/kernel/helpers.ts b/registry/tests/kernel/helpers.ts deleted file mode 100644 index 07be2a7f2c..0000000000 --- a/registry/tests/kernel/helpers.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Integration test helpers for kernel tests that depend on WASM command binaries. - * - * Re-exports infrastructure from the parent helpers.ts and provides - * createIntegrationKernel / skipUnlessWasmBuilt for cross-runtime tests. - */ - -import { - AF_INET, - AF_UNIX, - COMMANDS_DIR, - C_BUILD_DIR, - describeIf, - hasWasmBinaries, - itIf, - NodeFileSystem, - SIGTERM, - SOCK_DGRAM, - SOCK_STREAM, - TerminalHarness, - skipReason, - createInMemoryFileSystem, - createKernel, - createWasmVmRuntime, - createNodeRuntime, -} from "../helpers.js"; -import type { Kernel, Permissions, VirtualFileSystem } from "../helpers.js"; - -export { - AF_INET, - AF_UNIX, - COMMANDS_DIR, - C_BUILD_DIR, - describeIf, - hasWasmBinaries, - itIf, - NodeFileSystem, - SIGTERM, - SOCK_DGRAM, - SOCK_STREAM, - TerminalHarness, - skipReason, - createInMemoryFileSystem, - createKernel, - createWasmVmRuntime, - createNodeRuntime, -} from "../helpers.js"; -export type { Kernel, Permissions, VirtualFileSystem } from "../helpers.js"; - -export interface IntegrationKernelResult { - kernel: Kernel; - vfs: VirtualFileSystem; - dispose: () => Promise; -} - -export interface IntegrationKernelOptions { - runtimes?: ("wasmvm" | "node")[]; - loopbackExemptPorts?: number[]; - commandDirs?: string[]; - permissions?: Permissions; -} - -/** - * Create a kernel with the in-scope runtime drivers for integration testing. - * - * Mount order matters. Last-mounted driver wins for overlapping commands: - * 1. WasmVM first: provides sh/bash/coreutils (90+ commands) - * 2. Node second: overrides WasmVM's 'node' stub with real V8 - */ -export async function createIntegrationKernel( - options?: IntegrationKernelOptions, -): Promise { - const runtimes = options?.runtimes ?? ["wasmvm"]; - const vfs = createInMemoryFileSystem(); - const kernel = createKernel({ - filesystem: vfs, - loopbackExemptPorts: options?.loopbackExemptPorts, - permissions: options?.permissions, - }); - - if (runtimes.includes("wasmvm")) { - await kernel.mount( - createWasmVmRuntime({ commandDirs: options?.commandDirs ?? [COMMANDS_DIR] }), - ); - } - if (runtimes.includes("node")) { - await kernel.mount(createNodeRuntime()); - } - - return { - kernel, - vfs, - dispose: () => kernel.dispose(), - }; -} - -/** - * Skip helper: returns a reason string if the WASM binaries are not built, - * or false if the commands directory exists and tests can run. - */ -export function skipUnlessWasmBuilt(): string | false { - return skipReason(); -} diff --git a/registry/tests/kernel/vfs-consistency.test.ts b/registry/tests/kernel/vfs-consistency.test.ts deleted file mode 100644 index c4a1fa04a6..0000000000 --- a/registry/tests/kernel/vfs-consistency.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Cross-runtime VFS consistency tests. - * - * Verifies that file writes in one runtime are immediately visible to - * reads in another runtime, since all runtimes share the kernel VFS. - * - * Gracefully skipped when the WASM binary is not built. - */ - -import { describe, it, expect, afterEach } from 'vitest'; -import { - describeIf, - createIntegrationKernel, - skipUnlessWasmBuilt, -} from './helpers.ts'; -import type { IntegrationKernelResult } from './helpers.ts'; - -const skipReason = skipUnlessWasmBuilt(); - -describeIf(!skipReason, 'cross-runtime VFS consistency', () => { - let ctx: IntegrationKernelResult; - - afterEach(async () => { - if (ctx) await ctx.dispose(); - }); - - it('kernel write visible to Node', async () => { - ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); - await ctx.kernel.writeFile('/tmp/test.txt', 'hello'); - - const result = await ctx.kernel.exec( - `node -e "process.stdout.write(require('fs').readFileSync('/tmp/test.txt','utf8'))"`, - ); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('hello'); - }); - - it('Node write visible to WasmVM', async () => { - ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); - - // Node writes a file - const writeResult = await ctx.kernel.exec( - `node -e "require('fs').writeFileSync('/tmp/node-wrote.txt','from-node')"`, - ); - expect(writeResult.exitCode).toBe(0); - - // WasmVM reads it via cat - const readResult = await ctx.kernel.exec('cat /tmp/node-wrote.txt'); - expect(readResult.exitCode).toBe(0); - expect(readResult.stdout).toContain('from-node'); - }); - - it('Node write visible to kernel API', async () => { - ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); - - const writeResult = await ctx.kernel.exec( - `node -e "require('fs').writeFileSync('/tmp/k.txt','data')"`, - ); - expect(writeResult.exitCode).toBe(0); - - const content = await ctx.vfs.readTextFile('/tmp/k.txt'); - expect(content).toBe('data'); - }); - - it('directory listing consistent across runtimes', async () => { - ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); - - // Create 3 files via kernel API - await ctx.kernel.writeFile('/tmp/a.txt', 'a'); - await ctx.kernel.writeFile('/tmp/b.txt', 'b'); - await ctx.kernel.writeFile('/tmp/c.txt', 'c'); - - // WasmVM ls - const lsResult = await ctx.kernel.exec('ls /tmp'); - expect(lsResult.exitCode).toBe(0); - - // Node readdirSync - const nodeResult = await ctx.kernel.exec( - `node -e "console.log(require('fs').readdirSync('/tmp').sort().join(','))"`, - ); - expect(nodeResult.exitCode).toBe(0); - - // Both should list the same files - const lsFiles = lsResult.stdout - .trim() - .split(/\s+/) - .filter(Boolean) - .sort(); - const nodeFiles = nodeResult.stdout.trim().split(',').filter(Boolean).sort(); - - expect(lsFiles).toContain('a.txt'); - expect(lsFiles).toContain('b.txt'); - expect(lsFiles).toContain('c.txt'); - expect(nodeFiles).toContain('a.txt'); - expect(nodeFiles).toContain('b.txt'); - expect(nodeFiles).toContain('c.txt'); - }); - - it('ENOENT consistent across runtimes', async () => { - ctx = await createIntegrationKernel({ runtimes: ['wasmvm', 'node'] }); - - // WasmVM cat nonexistent file - const catResult = await ctx.kernel.exec('cat /nonexistent'); - expect(catResult.exitCode).not.toBe(0); - - // Node readFileSync nonexistent file - const nodeResult = await ctx.kernel.exec( - `node -e "require('fs').readFileSync('/nonexistent')"`, - ); - expect(nodeResult.exitCode).not.toBe(0); - }); -}); diff --git a/registry/tests/terminal-harness.ts b/registry/tests/terminal-harness.ts deleted file mode 100644 index b01ad66a9e..0000000000 --- a/registry/tests/terminal-harness.ts +++ /dev/null @@ -1,159 +0,0 @@ -/** - * TerminalHarness wires openShell() to a headless xterm Terminal so registry - * tests can assert against deterministic terminal screen state. - */ - -import { Terminal } from "@xterm/headless"; -import type { Kernel } from "./helpers.js"; - -type ShellHandle = ReturnType; - -const SETTLE_MS = 50; -const POLL_MS = 20; -const DEFAULT_WAIT_TIMEOUT_MS = 5_000; - -export class TerminalHarness { - readonly term: Terminal; - readonly shell: ShellHandle; - private typing = false; - private disposed = false; - - constructor( - kernel: Kernel, - options?: { - cols?: number; - rows?: number; - env?: Record; - cwd?: string; - }, - ) { - const cols = options?.cols ?? 80; - const rows = options?.rows ?? 24; - - this.term = new Terminal({ cols, rows, allowProposedApi: true }); - this.shell = kernel.openShell({ - cols, - rows, - env: options?.env, - cwd: options?.cwd, - onStderr: (data: Uint8Array) => { - this.term.write(data); - }, - }); - this.shell.onData = (data: Uint8Array) => { - this.term.write(data); - }; - } - - async type(input: string): Promise { - if (this.typing) { - throw new Error( - "TerminalHarness.type() called while previous type() is still in-flight", - ); - } - this.typing = true; - try { - await this.typeInternal(input); - } finally { - this.typing = false; - } - } - - private typeInternal(input: string): Promise { - return new Promise((resolve) => { - let timer: ReturnType | null = null; - const originalOnData = this.shell.onData; - - const resetTimer = () => { - if (timer !== null) clearTimeout(timer); - timer = setTimeout(() => { - this.shell.onData = originalOnData; - resolve(); - }, SETTLE_MS); - }; - - this.shell.onData = (data: Uint8Array) => { - this.term.write(data); - resetTimer(); - }; - - resetTimer(); - this.shell.write(input); - }); - } - - screenshotTrimmed(): string { - const buf = this.term.buffer.active; - const lines: string[] = []; - - for (let row = 0; row < this.term.rows; row++) { - const line = buf.getLine(buf.viewportY + row); - lines.push(line ? line.translateToString(true) : ""); - } - - while (lines.length > 0 && lines[lines.length - 1] === "") { - lines.pop(); - } - - return lines.join("\n"); - } - - line(row: number): string { - const buf = this.term.buffer.active; - const line = buf.getLine(buf.viewportY + row); - return line ? line.translateToString(true) : ""; - } - - async waitFor( - text: string, - occurrence: number = 1, - timeoutMs: number = DEFAULT_WAIT_TIMEOUT_MS, - ): Promise { - const deadline = Date.now() + timeoutMs; - - while (true) { - const screen = this.screenshotTrimmed(); - - let count = 0; - let idx = -1; - while (true) { - idx = screen.indexOf(text, idx + 1); - if (idx === -1) break; - count++; - if (count >= occurrence) return; - } - - if (Date.now() >= deadline) { - throw new Error( - `waitFor("${text}", ${occurrence}) timed out after ${timeoutMs}ms.\n` + - `Expected: "${text}" (occurrence ${occurrence})\n` + - `Screen:\n${screen}`, - ); - } - - await new Promise((resolve) => setTimeout(resolve, POLL_MS)); - } - } - - async exit(): Promise { - this.shell.write("\x04"); - return this.shell.wait(); - } - - async dispose(): Promise { - if (this.disposed) return; - this.disposed = true; - - try { - this.shell.kill(); - await Promise.race([ - this.shell.wait(), - new Promise((resolve) => setTimeout(resolve, 500)), - ]); - } catch { - // Shell may already be gone. - } - - this.term.dispose(); - } -} diff --git a/registry/tests/wasmvm/c-parity.test.ts b/registry/tests/wasmvm/c-parity.test.ts deleted file mode 100644 index 84daa0a463..0000000000 --- a/registry/tests/wasmvm/c-parity.test.ts +++ /dev/null @@ -1,980 +0,0 @@ -/** - * C parity tests — native vs WASM - * - * Compiles C test fixtures to both native and WASM, runs both, and - * compares stdout/stderr/exit code for parity. Tests skip when - * WASM binaries (make wasm), C WASM binaries (make -C native/wasmvm/c programs), - * or native binaries (make -C native/wasmvm/c native) are not built. - */ - -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { createWasmVmRuntime } from '../helpers.js'; -import { - COMMANDS_DIR, - C_BUILD_DIR, - createKernel, - describeIf, - hasWasmBinaries, - itIf, -} from '../helpers.js'; -import type { Kernel } from '../helpers.js'; -import { existsSync } from 'node:fs'; -import { writeFile as fsWriteFile, readFile as fsReadFile, mkdtemp, rm, mkdir as fsMkdir } from 'node:fs/promises'; -import { spawn } from 'node:child_process'; -import { join } from 'node:path'; -import { tmpdir } from 'node:os'; -import { createServer as createTcpServer } from 'node:net'; -import { createServer as createHttpServer } from 'node:http'; - -const NATIVE_DIR = join(C_BUILD_DIR, 'native'); - -const hasCWasmBinaries = existsSync(join(C_BUILD_DIR, 'hello')); -const hasNativeBinaries = existsSync(join(NATIVE_DIR, 'hello')); - -function skipReason(): string | false { - if (!hasWasmBinaries) return 'WASM binaries not built (run make wasm in native/wasmvm/)'; - if (!hasCWasmBinaries) return 'C WASM binaries not built (run make -C native/wasmvm/c programs)'; - if (!hasNativeBinaries) return 'C native binaries not built (run make -C native/wasmvm/c native)'; - return false; -} - -// Run a native binary, capture stdout/stderr/exitCode -function runNative( - name: string, - args: string[] = [], - options?: { input?: string; env?: Record }, -): Promise<{ exitCode: number; stdout: string; stderr: string }> { - return new Promise((res) => { - const proc = spawn(join(NATIVE_DIR, name), args, { - env: options?.env, - stdio: ['pipe', 'pipe', 'pipe'], - }); - let stdout = ''; - let stderr = ''; - - proc.stdout.on('data', (d: Buffer) => { stdout += d.toString(); }); - proc.stderr.on('data', (d: Buffer) => { stderr += d.toString(); }); - - if (options?.input !== undefined) { - proc.stdin.write(options.input); - } - proc.stdin.end(); - - proc.on('close', (code) => { - res({ exitCode: code ?? 0, stdout, stderr }); - }); - }); -} - -// Strip kernel-level diagnostic WARN lines from WASM stderr (not program output) -function normalizeStderr(stderr: string): string { - return stderr - .split('\n') - .filter((l) => !l.includes('WARN') || !l.includes('could not retrieve pid')) - .join('\n'); -} - -// Normalize argv[0] line since native path differs from WASM command name -function normalizeArgsOutput(output: string): string { - return output.replace(/^(argv\[0\]=).+$/m, '$1'); -} - -// Extract lines matching a prefix from env output -function extractEnvPrefix(output: string, prefix: string): string { - return output - .split('\n') - .filter((l) => l.startsWith(prefix)) - .sort() - .join('\n'); -} - -// Minimal in-memory VFS for kernel tests -class SimpleVFS { - private files = new Map(); - private dirs = new Set(['/']); - private symlinks = new Map(); - - async readFile(path: string): Promise { - const data = this.files.get(path); - if (!data) throw new Error(`ENOENT: ${path}`); - return data; - } - async readTextFile(path: string): Promise { - return new TextDecoder().decode(await this.readFile(path)); - } - async pread(path: string, offset: number, length: number): Promise { - const data = await this.readFile(path); - return data.slice(offset, offset + length); - } - async pwrite(path: string, offset: number, content: Uint8Array): Promise { - const data = await this.readFile(path); - const next = new Uint8Array(Math.max(data.length, offset + content.length)); - next.set(data); - next.set(content, offset); - this.files.set(path, next); - } - async readDir(path: string): Promise { - const prefix = path === '/' ? '/' : path + '/'; - const entries: string[] = []; - for (const p of [...this.files.keys(), ...this.dirs]) { - if (p !== path && p.startsWith(prefix)) { - const rest = p.slice(prefix.length); - if (!rest.includes('/')) entries.push(rest); - } - } - return entries; - } - async readDirWithTypes(path: string) { - return (await this.readDir(path)).map((name) => ({ - name, - isDirectory: this.dirs.has(path === '/' ? `/${name}` : `${path}/${name}`), - })); - } - async writeFile(path: string, content: string | Uint8Array): Promise { - const data = typeof content === 'string' ? new TextEncoder().encode(content) : content; - this.files.set(path, new Uint8Array(data)); - const parts = path.split('/').filter(Boolean); - for (let i = 1; i < parts.length; i++) { - this.dirs.add('/' + parts.slice(0, i).join('/')); - } - } - async createDir(path: string) { this.dirs.add(path); } - async mkdir(path: string, _options?: { recursive?: boolean }) { this.dirs.add(path); } - async exists(path: string): Promise { - return this.files.has(path) || this.dirs.has(path) || this.symlinks.has(path); - } - async stat(path: string) { - const isDir = this.dirs.has(path); - const isSymlink = this.symlinks.has(path); - const data = this.files.get(path); - if (!isDir && !isSymlink && !data) throw new Error(`ENOENT: ${path}`); - return { - mode: isSymlink ? 0o120777 : (isDir ? 0o40755 : 0o100644), - size: data?.length ?? 0, - isDirectory: isDir, - isSymbolicLink: isSymlink, - atimeMs: Date.now(), - mtimeMs: Date.now(), - ctimeMs: Date.now(), - birthtimeMs: Date.now(), - ino: 0, - nlink: 1, - uid: 1000, - gid: 1000, - }; - } - async chmod() {} - async rename(from: string, to: string) { - const data = this.files.get(from); - if (data) { this.files.set(to, data); this.files.delete(from); } - } - async unlink(path: string) { this.files.delete(path); this.symlinks.delete(path); } - async rmdir(path: string) { this.dirs.delete(path); } - async symlink(target: string, linkPath: string) { - this.symlinks.set(linkPath, target); - const parts = linkPath.split('/').filter(Boolean); - for (let i = 1; i < parts.length; i++) { - this.dirs.add('/' + parts.slice(0, i).join('/')); - } - } - async readlink(path: string): Promise { - const target = this.symlinks.get(path); - if (!target) throw new Error(`EINVAL: ${path}`); - return target; - } -} - -describeIf(!skipReason(), 'C parity: native vs WASM', { timeout: 30_000 }, () => { - let kernel: Kernel; - let vfs: SimpleVFS; - - async function mountParityKernel(options: { loopbackExemptPorts?: number[] } = {}) { - const nextKernel = createKernel({ - filesystem: vfs as any, - ...(options.loopbackExemptPorts - ? { loopbackExemptPorts: options.loopbackExemptPorts } - : {}), - }); - // C build dir first so C programs take precedence over same-named Rust commands - await nextKernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - return nextKernel; - } - - async function recreateKernel(options: { loopbackExemptPorts?: number[] } = {}) { - await kernel?.dispose(); - kernel = await mountParityKernel(options); - } - - beforeEach(async () => { - vfs = new SimpleVFS(); - kernel = await mountParityKernel(); - }); - - afterEach(async () => { - await kernel?.dispose(); - }); - - // --- Tier 1: basic I/O --- - - it('hello: stdout and exit code match', async () => { - const native = await runNative('hello'); - const wasm = await kernel.exec('hello'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.stdout).toBe(native.stdout); - }); - - it('args: argc and argv[1..] match', async () => { - const native = await runNative('args', ['foo', 'bar']); - const wasm = await kernel.exec('args foo bar'); - - expect(wasm.exitCode).toBe(native.exitCode); - // argv[0] differs (native path vs WASM command name), normalize it - expect(normalizeArgsOutput(wasm.stdout)).toBe(normalizeArgsOutput(native.stdout)); - }); - - it('env: user-specified env vars match', async () => { - const env = { TEST_PARITY_A: 'hello', TEST_PARITY_B: 'world' }; - const native = await runNative('env', [], { env }); - const wasm = await kernel.exec('env', { env }); - - expect(wasm.exitCode).toBe(native.exitCode); - // Shell may inject extra env vars; compare only the TEST_PARITY_ vars - expect(extractEnvPrefix(wasm.stdout, 'TEST_PARITY_')).toBe( - extractEnvPrefix(native.stdout, 'TEST_PARITY_'), - ); - }); - - it('exitcode: exit code matches', async () => { - const native = await runNative('exitcode', ['42']); - const wasm = await kernel.exec('exitcode 42'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(42); - }); - - it('cat: stdin passthrough matches', async () => { - const input = 'hello world\nfoo bar\n'; - const native = await runNative('cat', [], { input }); - const wasm = await kernel.exec('cat', { stdin: input }); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.stdout).toBe(native.stdout); - }); - - // --- Tier 1: data processing --- - - it('wc: word/line/byte counts match', async () => { - const input = 'hello world\nfoo bar baz\n'; - const native = await runNative('wc', [], { input }); - const wasm = await kernel.exec('wc', { stdin: input }); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.stdout).toBe(native.stdout); - }); - - it('fread: file contents match', async () => { - const content = 'hello from fread test\n'; - - // Native: temp file on disk - const tmpDir = await mkdtemp(join(tmpdir(), 'c-parity-')); - const filePath = join(tmpDir, 'test.txt'); - await fsWriteFile(filePath, content); - const native = await runNative('fread', [filePath]); - - // WASM: file on VFS - await vfs.writeFile('/tmp/test.txt', content); - const wasm = await kernel.exec('fread /tmp/test.txt'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.stdout).toBe(native.stdout); - - await rm(tmpDir, { recursive: true }); - }); - - it('fwrite: written content matches', async () => { - const writeContent = 'test content'; - - // Native: write to temp dir - const tmpDir = await mkdtemp(join(tmpdir(), 'c-parity-')); - const nativePath = join(tmpDir, 'out.txt'); - const native = await runNative('fwrite', [nativePath, writeContent]); - const nativeFileContent = await fsReadFile(nativePath, 'utf8'); - - // WASM: write to VFS - const wasm = await kernel.exec(`fwrite /tmp/out.txt "${writeContent}"`); - const wasmFileContent = await vfs.readTextFile('/tmp/out.txt'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasmFileContent).toBe(nativeFileContent); - - await rm(tmpDir, { recursive: true }); - }); - - it('pread_pwrite_access: pread/pwrite/access syscalls match', async () => { - // Native: uses real /tmp - const tmpDir = await mkdtemp(join(tmpdir(), 'c-parity-')); - const nativeEnv = { ...process.env, HOME: tmpDir }; - const native = await runNative('pread_pwrite_access', [], { env: nativeEnv }); - - // WASM: uses VFS /tmp - await vfs.createDir('/tmp'); - const wasm = await kernel.exec('pread_pwrite_access'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.stdout).toBe(native.stdout); - expect(wasm.stdout).toContain('total: 0 failures'); - - await rm(tmpDir, { recursive: true }); - }); - - it('sort: sorted output matches', async () => { - const input = 'banana\napple\ncherry\ndate\n'; - const native = await runNative('sort', [], { input }); - const wasm = await kernel.exec('sort', { stdin: input }); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.stdout).toBe(native.stdout); - }); - - it('sha256: hex digest matches', async () => { - const input = 'hello'; - const native = await runNative('sha256', [], { input }); - const wasm = await kernel.exec('sha256', { stdin: input }); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.stdout).toBe(native.stdout); - }); - - // --- Tier 2: custom imports (patched sysroot) --- - - const hasCTier2Binaries = existsSync(join(C_BUILD_DIR, 'pipe_test')); - const tier2Skip = !hasCTier2Binaries - ? 'C Tier 2 WASM binaries not built (need patched sysroot: make -C native/wasmvm/c sysroot && make -C native/wasmvm/c programs)' - : false; - - itIf(!tier2Skip, 'isatty_test: piped stdin/stdout/stderr all report not-a-tty', async () => { - const native = await runNative('isatty_test'); - const wasm = await kernel.exec('isatty_test'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.stdout).toBe(native.stdout); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - }); - - itIf(!tier2Skip, 'getpid_test: PID is valid, not hardcoded 42, and consistent', async () => { - const native = await runNative('getpid_test'); - const wasm = await kernel.exec('getpid_test'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - // PIDs differ between native and WASM, but both should be valid - expect(wasm.stdout).toContain('pid_positive=yes'); - expect(wasm.stdout).toContain('pid_not_42=yes'); - expect(wasm.stdout).toContain('pid_consistent=yes'); - expect(native.stdout).toContain('pid_positive=yes'); - expect(native.stdout).toContain('pid_not_42=yes'); - expect(native.stdout).toContain('pid_consistent=yes'); - // Verify actual PID value is > 0 - const wasmPid = parseInt(wasm.stdout.match(/^pid=(\d+)/m)?.[1] ?? '0', 10); - expect(wasmPid).toBeGreaterThan(0); - expect(wasmPid).not.toBe(42); - }); - - itIf(!tier2Skip, 'getppid_test: top-level parent PID is valid', async () => { - const native = await runNative('getppid_test'); - const wasm = await kernel.exec('getppid_test'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - expect(wasm.stdout).toContain('ppid_nonnegative=yes'); - expect(native.stdout).toContain('ppid_nonnegative=yes'); - expect(wasm.stdout).toContain('ppid=0'); - }); - - itIf(!tier2Skip, 'userinfo: uid/gid/euid/egid values are specific', async () => { - const native = await runNative('userinfo'); - const wasm = await kernel.exec('userinfo'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - // Verify format for both - const format = /^uid=\d+\ngid=\d+\neuid=\d+\negid=\d+\n$/; - expect(wasm.stdout).toMatch(format); - expect(native.stdout).toMatch(format); - // WASM kernel returns uid/gid = 1000 (sandbox user) - expect(wasm.stdout).toContain('uid=1000'); - expect(wasm.stdout).toContain('gid=1000'); - expect(wasm.stdout).toContain('euid=1000'); - expect(wasm.stdout).toContain('egid=1000'); - }); - - itIf(!tier2Skip, 'getpwuid_test: passwd entry fields valid', async () => { - const native = await runNative('getpwuid_test'); - const wasm = await kernel.exec('getpwuid_test'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - // Both should get valid passwd entries - expect(wasm.stdout).toContain('getpwuid: ok'); - expect(wasm.stdout).toContain('pw_name_nonempty: yes'); - expect(wasm.stdout).toContain('pw_uid_match: yes'); - expect(wasm.stdout).toContain('pw_gid_valid: yes'); - expect(wasm.stdout).toContain('pw_dir_nonempty: yes'); - expect(wasm.stdout).toContain('pw_shell_nonempty: yes'); - expect(native.stdout).toContain('getpwuid: ok'); - expect(native.stdout).toContain('pw_name_nonempty: yes'); - expect(native.stdout).toContain('pw_uid_match: yes'); - }); - - itIf(!tier2Skip, 'pipe_test: write through pipe and read back matches', async () => { - const native = await runNative('pipe_test'); - const wasm = await kernel.exec('pipe_test'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.stdout).toBe(native.stdout); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - }); - - itIf(!tier2Skip, 'dup_test: write through duplicated fds matches', async () => { - const native = await runNative('dup_test'); - const wasm = await kernel.exec('dup_test'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.stdout).toBe(native.stdout); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - }); - - it('sleep_test: nanosleep completes successfully', async () => { - const native = await runNative('sleep_test', ['50']); - const wasm = await kernel.exec('sleep_test 50'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - // Both should report successful sleep with >= 80% of requested time - expect(wasm.stdout).toContain('requested=50ms'); - expect(wasm.stdout).toContain('ok=yes'); - expect(native.stdout).toContain('requested=50ms'); - expect(native.stdout).toContain('ok=yes'); - }); - - // --- Tier 3: process management (patched sysroot) --- - - const hasCTier3Binaries = existsSync(join(C_BUILD_DIR, 'spawn_child')); - const tier3Skip = !hasCTier3Binaries - ? 'C Tier 3 WASM binaries not built (need patched sysroot: make -C native/wasmvm/c sysroot && make -C native/wasmvm/c programs)' - : false; - - itIf(!tier3Skip, 'spawn_child: posix_spawn echo, capture stdout via pipe', async () => { - const native = await runNative('spawn_child'); - const wasm = await kernel.exec('spawn_child'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(wasm.stdout).toBe(native.stdout); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - expect(wasm.stdout).toContain('child_stdout: hello'); - expect(wasm.stdout).toContain('child_exit: 0'); - }); - - itIf(!tier3Skip, 'spawn_exit_code: child exits non-zero, verify via waitpid', async () => { - const native = await runNative('spawn_exit_code'); - const wasm = await kernel.exec('spawn_exit_code'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(wasm.stdout).toBe(native.stdout); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - expect(wasm.stdout).toContain('child_exit_code: 7'); - expect(wasm.stdout).toContain('match: yes'); - }); - - itIf(!tier3Skip, 'pipeline: echo hello | cat via pipe + posix_spawn', async () => { - const native = await runNative('pipeline'); - const wasm = await kernel.exec('pipeline'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(wasm.stdout).toBe(native.stdout); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - expect(wasm.stdout).toContain('pipeline_output: hello'); - expect(wasm.stdout).toContain('echo_exit: 0'); - expect(wasm.stdout).toContain('cat_exit: 0'); - }); - - itIf(!tier3Skip, 'kill_child: spawn sleep, kill SIGTERM, verify terminated', async () => { - const native = await runNative('kill_child'); - const wasm = await kernel.exec('kill_child'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - // Both should complete the spawn/kill/wait cycle successfully - expect(wasm.stdout).toContain('spawned: yes'); - expect(wasm.stdout).toContain('kill: ok'); - expect(wasm.stdout).toContain('terminated: yes'); - // Verify child was killed by signal (WIFSIGNALED) - expect(wasm.stdout).toContain('signaled=yes'); - expect(native.stdout).toContain('signaled=yes'); - // SIGTERM = 15 - expect(wasm.stdout).toContain('termsig=15'); - expect(native.stdout).toContain('termsig=15'); - }); - - itIf(!tier3Skip, 'signal_tests: SIGKILL, kill exited PID, kill invalid PID', async () => { - const native = await runNative('signal_tests'); - const wasm = await kernel.exec('signal_tests'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - - // Test 1: SIGKILL — child killed by signal 9 - expect(wasm.stdout).toContain('test_sigkill: ok'); - expect(native.stdout).toContain('test_sigkill: ok'); - expect(wasm.stdout).toContain('sigkill_signaled=yes'); - expect(wasm.stdout).toContain('sigkill_termsig=9'); - - // Test 2: kill exited process — ok with either 0 or -1/ESRCH - expect(wasm.stdout).toContain('test_kill_exited: ok'); - expect(native.stdout).toContain('test_kill_exited: ok'); - - // Test 3: kill invalid PID — returns -1 - expect(wasm.stdout).toContain('test_kill_invalid: ok'); - expect(native.stdout).toContain('test_kill_invalid: ok'); - }); - - itIf(!tier3Skip, 'sigaction_behavior: query, SA_RESETHAND, and SA_RESTART parity', async () => { - const env = { ...process.env, PATH: `${NATIVE_DIR}:${process.env.PATH ?? ''}` }; - const native = await runNative('sigaction_behavior', [], { env }); - const wasm = await kernel.exec('sigaction_behavior'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(wasm.stdout).toBe(native.stdout); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - expect(wasm.stdout).toContain('sigaction_query_mask_sigterm=yes'); - expect(wasm.stdout).toContain('sigaction_query_flags=yes'); - expect(wasm.stdout).toContain('sa_resethand_handler_calls=1'); - expect(wasm.stdout).toContain('sa_resethand_reset=yes'); - expect(wasm.stdout).toContain('sa_restart_handler_calls=1'); - expect(wasm.stdout).toContain('sa_restart_accept=yes'); - expect(wasm.stdout).toContain('sa_restart_child_exit=0'); - expect(wasm.stdout).toContain('sa_restart_signal_exit=0'); - }); - - itIf(!tier3Skip, 'sigaction_self: self kill dispatches SA_RESETHAND handler', async () => { - const native = await runNative('sigaction_self'); - const wasm = await kernel.exec('sigaction_self'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(wasm.stdout).toBe(native.stdout); - expect(wasm.stdout).toContain('self_signal_handler_calls=1'); - expect(wasm.stdout).toContain('self_signal_reset=yes'); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - }); - - itIf(!tier3Skip, 'tcp_accept_spawn: accept spawned child connection', async () => { - const env = { ...process.env, PATH: `${NATIVE_DIR}:${process.env.PATH ?? ''}` }; - const native = await runNative('tcp_accept_spawn', [], { env }); - const wasm = await kernel.exec('tcp_accept_spawn'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(wasm.stdout).toBe(native.stdout); - expect(wasm.stdout).toContain('accept_child_message=yes'); - expect(wasm.stdout).toContain('accept_child_exit=0'); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - }); - - itIf(!tier3Skip, 'getppid_verify: child getppid matches parent getpid', async () => { - // Native needs getppid_test on PATH for posix_spawnp - const native = await runNative('getppid_verify', [], { - env: { ...process.env, PATH: `${NATIVE_DIR}:${process.env.PATH}` }, - }); - const wasm = await kernel.exec('getppid_verify'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - expect(wasm.stdout).toContain('match=yes'); - expect(native.stdout).toContain('match=yes'); - expect(wasm.stdout).toContain('child_exit=0'); - expect(native.stdout).toContain('child_exit=0'); - }); - - itIf(!tier3Skip, 'waitpid_return: waitpid returns correct child PID', async () => { - const native = await runNative('waitpid_return'); - const wasm = await kernel.exec('waitpid_return'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - // waitpid with specific PID returns that PID - expect(wasm.stdout).toContain('test1_match: yes'); - expect(wasm.stdout).toContain('test1_exit: 0'); - // wait() (waitpid(-1)) returns actual child PID - expect(wasm.stdout).toContain('test2_match: yes'); - expect(wasm.stdout).toContain('test2_exit: 0'); - // Return values are positive PIDs - expect(wasm.stdout).toContain('test3_ret1_positive: yes'); - expect(wasm.stdout).toContain('test3_ret2_positive: yes'); - }); - - itIf(!tier3Skip, 'waitpid_edge: concurrent children and invalid PID', async () => { - const native = await runNative('waitpid_edge'); - const wasm = await kernel.exec('waitpid_edge'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - // Test 1: 3 concurrent children with correct exit codes - expect(wasm.stdout).toContain('test1_c1_exit: 1'); - expect(wasm.stdout).toContain('test1_c2_exit: 2'); - expect(wasm.stdout).toContain('test1_c3_exit: 3'); - expect(wasm.stdout).toContain('test1: ok'); - expect(native.stdout).toContain('test1: ok'); - // Test 2: wait() reaps both children with distinct valid PIDs - expect(wasm.stdout).toContain('test2_r1_valid: yes'); - expect(wasm.stdout).toContain('test2_r2_valid: yes'); - expect(wasm.stdout).toContain('test2_distinct: yes'); - expect(wasm.stdout).toContain('test2: ok'); - expect(native.stdout).toContain('test2: ok'); - // Test 3: waitpid with never-spawned PID returns -1 with error - expect(wasm.stdout).toContain('test3_ret: -1'); - expect(wasm.stdout).toContain('test3_failed: yes'); - expect(wasm.stdout).toContain('test3: ok'); - expect(native.stdout).toContain('test3: ok'); - }); - - itIf(!tier3Skip, 'pipe_edge: large write, broken pipe, EOF, close-both', async () => { - const native = await runNative('pipe_edge'); - const wasm = await kernel.exec('pipe_edge'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - - // Test 1: large write (128KB > 64KB pipe buffer) - expect(wasm.stdout).toContain('large_write: ok'); - expect(native.stdout).toContain('large_write: ok'); - expect(wasm.stdout).toContain('large_write_bytes=131072'); - expect(native.stdout).toContain('large_write_bytes=131072'); - - // Test 2: broken pipe — write to pipe with closed read end - expect(wasm.stdout).toContain('broken_pipe: ok'); - expect(native.stdout).toContain('broken_pipe: ok'); - - // Test 3: EOF — read from pipe with closed write end - expect(wasm.stdout).toContain('eof_read: ok'); - expect(native.stdout).toContain('eof_read: ok'); - expect(wasm.stdout).toContain('eof_read_result=0'); - expect(native.stdout).toContain('eof_read_result=0'); - - // Test 4: close both ends — no crash or leak - expect(wasm.stdout).toContain('close_both: ok'); - expect(native.stdout).toContain('close_both: ok'); - }); - - // --- Capstone: syscall coverage (all tiers, patched sysroot) --- - - const hasSyscallCoverage = existsSync(join(C_BUILD_DIR, 'syscall_coverage')); - const syscallCoverageSkip = !hasSyscallCoverage - ? 'syscall_coverage WASM binary not built (need patched sysroot: make -C native/wasmvm/c sysroot && make -C native/wasmvm/c programs)' - : false; - - itIf(!syscallCoverageSkip, 'syscall_coverage: all syscall categories pass parity', async () => { - // Pre-create /tmp in VFS for the program's file operations - await vfs.createDir('/tmp'); - - const env = { TEST_SC: '1', PATH: process.env.PATH ?? '/usr/bin:/bin' }; - const native = await runNative('syscall_coverage', [], { env }); - - const wasmEnv = { TEST_SC: '1' }; - const wasm = await kernel.exec('syscall_coverage', { env: wasmEnv }); - - // Debug: show WASM output if it fails - if (wasm.exitCode !== 0) { - console.log('WASM stdout:', wasm.stdout); - console.log('WASM stderr:', wasm.stderr); - } - - // Both should exit 0 (all tests pass) - expect(native.exitCode).toBe(0); - expect(wasm.exitCode).toBe(0); - - // Compare structured output — normalize host_user lines whose values - // differ between native (real OS uid) and WASM (always 1000) - const normalizeSyscallCoverage = (out: string) => - out.replace(/^(getuid|getgid|geteuid|getegid): ok$/gm, '$1: ok'); - expect(normalizeSyscallCoverage(wasm.stdout)).toBe(normalizeSyscallCoverage(native.stdout)); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - - // Verify all expected syscalls are tested - const expectedSyscalls = [ - // WASI FD ops - 'open', 'write', 'read', 'seek', 'pread', 'pwrite', 'fstat', 'ftruncate', 'close', - // WASI path ops - 'mkdir', 'stat', 'rename', 'opendir', 'readdir', 'closedir', - 'symlink', 'readlink', 'unlink', 'rmdir', - // Args/env/clock - 'argc', 'argv', 'environ', 'clock_realtime', 'clock_monotonic', - // host_process - 'pipe', 'dup', 'dup2', 'getpid', 'getppid', 'sigaction_register', 'sigaction_query', 'spawn_waitpid', 'kill', - // host_user - 'getuid', 'getgid', 'geteuid', 'getegid', 'isatty_stdin', 'getpwuid', - // host_net - 'getsockname', 'getpeername', - ]; - for (const name of expectedSyscalls) { - expect(wasm.stdout).toContain(`${name}: ok`); - } - expect(wasm.stdout).toContain('total: 0 failures'); - }); - - // --- Tier 4: filesystem stress --- - - const hasCTier4Binaries = existsSync(join(C_BUILD_DIR, 'c-ls')); - const hasCTier4Native = existsSync(join(NATIVE_DIR, 'c-ls')); - const tier4Skip = (!hasCTier4Binaries || !hasCTier4Native) - ? 'C Tier 4 binaries not built (run make -C native/wasmvm/c programs && make -C native/wasmvm/c native)' - : false; - - // Helper: create test directory tree on disk and in VFS - async function setupTestTree(testVfs: SimpleVFS) { - const tmpDir = await mkdtemp(join(tmpdir(), 'c-parity-tree-')); - await fsMkdir(join(tmpDir, 'subdir', 'deep'), { recursive: true }); - await fsWriteFile(join(tmpDir, 'alpha.txt'), 'hello\n'); - await fsWriteFile(join(tmpDir, 'beta.txt'), 'world!\n'); - await fsWriteFile(join(tmpDir, 'subdir', 'gamma.txt'), 'nested file\n'); - await fsWriteFile(join(tmpDir, 'subdir', 'deep', 'delta.txt'), 'deep nested\n'); - - const base = '/testdir'; - await testVfs.createDir(base); - await testVfs.createDir(`${base}/subdir`); - await testVfs.createDir(`${base}/subdir/deep`); - await testVfs.writeFile(`${base}/alpha.txt`, 'hello\n'); - await testVfs.writeFile(`${base}/beta.txt`, 'world!\n'); - await testVfs.writeFile(`${base}/subdir/gamma.txt`, 'nested file\n'); - await testVfs.writeFile(`${base}/subdir/deep/delta.txt`, 'deep nested\n'); - - return { nativeDir: tmpDir, vfsBase: base }; - } - - itIf(!tier4Skip, 'c-ls: directory listing with file sizes matches', async () => { - const { nativeDir } = await setupTestTree(vfs); - try { - const native = await runNative('c-ls', [nativeDir]); - const wasm = await kernel.exec('c-ls /testdir'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(wasm.stdout).toBe(native.stdout); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - // Verify expected entries - expect(wasm.stdout).toContain('alpha.txt'); - expect(wasm.stdout).toContain('subdir'); - } finally { - await rm(nativeDir, { recursive: true }); - } - }); - - itIf(!tier4Skip, 'c-tree: recursive directory listing matches', async () => { - const { nativeDir } = await setupTestTree(vfs); - try { - const native = await runNative('c-tree', [nativeDir]); - const wasm = await kernel.exec('c-tree /testdir'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - // Root path (first line) differs — normalize it - const normalizeRoot = (out: string) => out.replace(/^.+\n/, 'ROOT\n'); - expect(normalizeRoot(wasm.stdout)).toBe(normalizeRoot(native.stdout)); - // Verify tree structure present - expect(wasm.stdout).toContain('alpha.txt'); - expect(wasm.stdout).toContain('deep'); - expect(wasm.stdout).toContain('delta.txt'); - } finally { - await rm(nativeDir, { recursive: true }); - } - }); - - itIf(!tier4Skip, 'c-find: find files matching glob pattern', async () => { - const { nativeDir } = await setupTestTree(vfs); - try { - const native = await runNative('c-find', [nativeDir, '*.txt']); - const wasm = await kernel.exec('c-find /testdir "*.txt"'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - // Paths have different roots — strip root prefix, compare relative paths - const relPaths = (out: string, root: string) => - out.split('\n').filter(Boolean).map((l) => l.replace(root, '')).sort().join('\n'); - expect(relPaths(wasm.stdout, '/testdir')).toBe(relPaths(native.stdout, nativeDir)); - // Should find all 4 .txt files - expect(wasm.stdout.split('\n').filter(Boolean)).toHaveLength(4); - } finally { - await rm(nativeDir, { recursive: true }); - } - }); - - itIf(!tier4Skip, 'c-cp: copied file contents match', async () => { - const srcContent = 'copy test content\nwith multiple lines\n'; - - // Native: write source, copy, read dest - const tmpDir = await mkdtemp(join(tmpdir(), 'c-parity-cp-')); - try { - const nativeSrc = join(tmpDir, 'src.txt'); - const nativeDst = join(tmpDir, 'dst.txt'); - await fsWriteFile(nativeSrc, srcContent); - const native = await runNative('c-cp', [nativeSrc, nativeDst]); - const nativeCopied = await fsReadFile(nativeDst, 'utf8'); - - // WASM: write source to VFS, copy, read dest from VFS - await vfs.writeFile('/tmp/src.txt', srcContent); - const wasm = await kernel.exec('c-cp /tmp/src.txt /tmp/dst.txt'); - const wasmCopied = await vfs.readTextFile('/tmp/dst.txt'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - expect(wasmCopied).toBe(nativeCopied); - expect(wasmCopied).toBe(srcContent); - // Stdout message paths differ — just verify both report success - expect(wasm.stdout).toContain('copied:'); - expect(native.stdout).toContain('copied:'); - } finally { - await rm(tmpDir, { recursive: true }); - } - }); - - // --- Tier 5: vendored libraries --- - - const hasCTier5Binaries = existsSync(join(C_BUILD_DIR, 'json_parse')); - const hasCTier5Native = existsSync(join(NATIVE_DIR, 'json_parse')); - const tier5Skip = (!hasCTier5Binaries || !hasCTier5Native) - ? 'C Tier 5 binaries not built (run make -C native/wasmvm/c programs && make -C native/wasmvm/c native)' - : false; - - const hasSqliteBinary = existsSync(join(C_BUILD_DIR, 'sqlite3_mem')); - const hasSqliteNative = existsSync(join(NATIVE_DIR, 'sqlite3_mem')); - const sqliteSkip = (!hasSqliteBinary || !hasSqliteNative) - ? 'SQLite binaries not built (run make -C native/wasmvm/c programs && make -C native/wasmvm/c native)' - : false; - - itIf(!sqliteSkip, 'sqlite3_mem: in-memory SQL operations parity', async () => { - const native = await runNative('sqlite3_mem'); - const wasm = await kernel.exec('sqlite3_mem'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(wasm.stdout).toBe(native.stdout); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - // Verify key structural elements - expect(wasm.stdout).toContain('db: open'); - expect(wasm.stdout).toContain('table: created'); - expect(wasm.stdout).toContain('rows: 4'); - expect(wasm.stdout).toContain('name=Alice|score=95.5'); - expect(wasm.stdout).toContain('name=Charlie|score=NULL'); - expect(wasm.stdout).toContain('avg_score='); - expect(wasm.stdout).toContain('db: closed'); - }); - - itIf(!tier5Skip, 'json_parse: cJSON parse and format parity', async () => { - const sampleJson = JSON.stringify({ - name: 'agentos', - version: 2, - enabled: true, - tags: ['alpha', 'beta'], - config: { debug: false, timeout: null, ratio: 3.14 }, - empty_arr: [], - empty_obj: {}, - }); - - const native = await runNative('json_parse', [], { input: sampleJson }); - const wasm = await kernel.exec('json_parse', { stdin: sampleJson }); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(wasm.stdout).toBe(native.stdout); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - // Verify key structural elements are present - expect(wasm.stdout).toContain('"name": "agentos"'); - expect(wasm.stdout).toContain('"enabled": true'); - expect(wasm.stdout).toContain('"timeout": null'); - expect(wasm.stdout).toContain('"ratio": 3.14'); - expect(wasm.stdout).toContain('[]'); - expect(wasm.stdout).toContain('{}'); - }); - - // --- Tier 6: networking (patched sysroot + host_net) --- - - const hasCNetBinaries = existsSync(join(C_BUILD_DIR, 'tcp_echo')); - const hasNativeNetBinaries = existsSync(join(NATIVE_DIR, 'tcp_echo')); - const netSkip = (!hasCNetBinaries || !hasNativeNetBinaries) - ? 'C networking binaries not built (need patched sysroot: make -C native/wasmvm/c sysroot && make -C native/wasmvm/c programs && make -C native/wasmvm/c native)' - : false; - - itIf(!netSkip, 'tcp_echo: connect to TCP echo server, send and receive', async () => { - // Start a local TCP echo server - const server = createTcpServer((conn) => { - conn.on('data', (data) => { conn.write(data); conn.end(); }); - }); - await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); - const port = (server.address() as import('node:net').AddressInfo).port; - - try { - await recreateKernel({ loopbackExemptPorts: [port] }); - const native = await runNative('tcp_echo', [String(port)]); - const wasm = await kernel.exec(`tcp_echo ${port}`); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(wasm.stdout).toBe(native.stdout); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - expect(wasm.stdout).toContain('sent: 5'); - expect(wasm.stdout).toContain('received: hello'); - } finally { - server.close(); - } - }); - - itIf(!netSkip, 'http_get: connect to HTTP server, receive response body', async () => { - // Start a local HTTP server - const server = createHttpServer((_req, res) => { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('hello from http'); - }); - await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); - const port = (server.address() as import('node:net').AddressInfo).port; - - try { - await recreateKernel({ loopbackExemptPorts: [port] }); - const native = await runNative('http_get', [String(port)]); - const wasm = await kernel.exec(`http_get ${port}`); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(wasm.stdout).toBe(native.stdout); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - expect(wasm.stdout).toContain('body: hello from http'); - } finally { - server.close(); - } - }); - - itIf(!netSkip, 'dns_lookup: resolve localhost to 127.0.0.1', async () => { - const native = await runNative('dns_lookup', ['localhost']); - const wasm = await kernel.exec('dns_lookup localhost'); - - expect(wasm.exitCode).toBe(native.exitCode); - expect(wasm.exitCode).toBe(0); - expect(wasm.stdout).toBe(native.stdout); - expect(normalizeStderr(wasm.stderr)).toBe(normalizeStderr(native.stderr)); - expect(wasm.stdout).toContain('host: localhost'); - expect(wasm.stdout).toContain('ip: 127.0.0.1'); - }); -}); diff --git a/registry/tests/wasmvm/curl.test.ts b/registry/tests/wasmvm/curl.test.ts deleted file mode 100644 index da9c1b5600..0000000000 --- a/registry/tests/wasmvm/curl.test.ts +++ /dev/null @@ -1,719 +0,0 @@ -/** - * Integration tests for curl and the C socket layer (host_socket.c). - * - * Tests the WASM socket implementation that powers curl: - * - DNS resolution (getaddrinfo) - * - TCP socket creation and connection - * - Non-blocking socket mode (fcntl O_NONBLOCK) - * - Socket options (getsockopt SO_ERROR, setsockopt TCP_NODELAY) - * - Poll for readability/writability - * - HTTP send/recv over raw sockets - * - Remote endpoint connectivity - */ - -import { describe, it, expect, afterEach, beforeAll, afterAll } from 'vitest'; -import { createWasmVmRuntime } from '../helpers.js'; -import { - allowAll, - C_BUILD_DIR, - COMMANDS_DIR, - createInMemoryFileSystem, - createKernel, - describeIf, - hasCWasmBinaries, - hasWasmBinaries, - itIf, -} from '../helpers.js'; -import type { Kernel } from '../helpers.js'; -import { - createServer as createHttpServer, - type IncomingMessage, - type Server as HttpServer, - type ServerResponse, -} from 'node:http'; -import { createServer as createHttpsServer, type Server as HttpsServer } from 'node:https'; -import { - createConnection, - createServer as createTcpServer, - type Server as TcpServer, -} from 'node:net'; -import { execSync } from 'node:child_process'; -import { existsSync, unlinkSync, writeFileSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join, resolve } from 'node:path'; - -// The upstream curl parity assertions below only hold for the C-built curl -// artifact; the Rust fallback in COMMANDS_DIR intentionally supports a smaller -// flag surface and should not be used for these cases. -const hasHttpGetTest = hasWasmBinaries && existsSync(resolve(COMMANDS_DIR, 'http_get_test')); -const hasCurl = hasCWasmBinaries('curl'); -const runExternalNetwork = process.env.AGENTOS_E2E_NETWORK === '1'; -const EXTERNAL_HOST = 'example.com'; -const EXTERNAL_TCP_PORT = 80; -const EXTERNAL_HTTP_URL = `http://${EXTERNAL_HOST}/`; -const EXTERNAL_HTTPS_URL = `https://${EXTERNAL_HOST}/`; -const EXTERNAL_EXPECTED_BODY = 'Example Domain'; -const EXTERNAL_RETRY_ATTEMPTS = 3; -const EXTERNAL_RETRY_DELAY_MS = 1_000; -const EXTERNAL_PROBE_TIMEOUT_MS = 8_000; -let hasOpenssl = false; - -try { - execSync('openssl version', { stdio: 'pipe' }); - hasOpenssl = true; -} catch { - hasOpenssl = false; -} - -function sleep(ms: number): Promise { - return new Promise((resolveSleep) => setTimeout(resolveSleep, ms)); -} - -function formatError(error: unknown): string { - if (error instanceof Error) return error.message; - return String(error); -} - -async function retryExternal(run: () => Promise, attempts = EXTERNAL_RETRY_ATTEMPTS): Promise { - let lastError: unknown; - for (let attempt = 1; attempt <= attempts; attempt += 1) { - try { - return await run(); - } catch (error) { - lastError = error; - if (attempt < attempts) { - await sleep(EXTERNAL_RETRY_DELAY_MS); - } - } - } - - throw lastError ?? new Error('external network probe failed'); -} - -async function probeExternalTcp(): Promise { - await new Promise((resolveConnect, rejectConnect) => { - const socket = createConnection({ - host: EXTERNAL_HOST, - port: EXTERNAL_TCP_PORT, - }); - let settled = false; - - const finish = (callback: () => void) => { - if (settled) return; - settled = true; - callback(); - }; - - socket.setTimeout(EXTERNAL_PROBE_TIMEOUT_MS); - socket.once('connect', () => { - finish(() => { - socket.end(); - resolveConnect(); - }); - }); - socket.once('timeout', () => { - finish(() => { - socket.destroy(); - rejectConnect(new Error(`timed out connecting to ${EXTERNAL_HOST}:${EXTERNAL_TCP_PORT}`)); - }); - }); - socket.once('error', (error) => { - finish(() => { - socket.destroy(); - rejectConnect(error); - }); - }); - }); -} - -async function probeExternalHttps(): Promise { - const response = await fetch(EXTERNAL_HTTPS_URL, { - signal: AbortSignal.timeout(EXTERNAL_PROBE_TIMEOUT_MS), - }); - if (!response.ok) { - throw new Error(`host probe failed with HTTP ${response.status}`); - } - await response.arrayBuffer(); -} - -const externalNetworkSkipReason = runExternalNetwork - ? await (async () => { - try { - await retryExternal(async () => { - await probeExternalTcp(); - await probeExternalHttps(); - }); - return false as const; - } catch (error) { - return `external network unavailable: ${formatError(error)}`; - } - })() - : 'set AGENTOS_E2E_NETWORK=1 to enable external-network coverage'; - -function generateSelfSignedCert(): { key: string; cert: string } { - const keyPath = join(tmpdir(), `curl-test-key-${process.pid}-${Date.now()}.pem`); - try { - const key = execSync( - 'openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 2>/dev/null', - { encoding: 'utf8' }, - ); - writeFileSync(keyPath, key); - const cert = execSync( - `openssl req -new -x509 -key "${keyPath}" -days 1 -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" 2>/dev/null`, - { encoding: 'utf8' }, - ); - return { key, cert }; - } finally { - try { - unlinkSync(keyPath); - } catch { - // Best effort cleanup for test temp files. - } - } -} - -describeIf(hasCurl || hasHttpGetTest, 'curl and socket layer', () => { - let kernel: Kernel; - let httpServer: HttpServer; - let httpsServer: HttpsServer; - let keepAliveServer: TcpServer; - let httpPort: number; - let httpsPort: number; - let keepAlivePort: number; - let flakyRequestCount = 0; - - beforeAll(async () => { - httpServer = createHttpServer((req: IncomingMessage, res: ServerResponse) => { - const url = req.url ?? '/'; - - if (url === '/json') { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ ok: true, path: url })); - return; - } - - if (url === '/redirect') { - res.writeHead(302, { Location: `http://127.0.0.1:${httpPort}/final` }); - res.end(); - return; - } - - if (url === '/final') { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('followed redirect'); - return; - } - - if (url === '/one') { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('first-response\n'); - return; - } - - if (url === '/two') { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('second-response\n'); - return; - } - - if (url === '/echo' && req.method === 'POST') { - let body = ''; - req.on('data', (chunk) => { - body += chunk; - }); - req.on('end', () => { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - method: req.method, - body, - header: req.headers['x-test'] ?? null, - })); - }); - return; - } - - if (url === '/json-post' && req.method === 'POST') { - let body = ''; - req.on('data', (chunk) => { - body += chunk; - }); - req.on('end', () => { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - method: req.method, - contentType: req.headers['content-type'] ?? null, - accept: req.headers.accept ?? null, - body, - })); - }); - return; - } - - if (url === '/head-test') { - res.writeHead(200, { - 'Content-Type': 'text/plain', - 'X-Test-Header': 'present', - }); - if (req.method === 'HEAD') { - res.end(); - } else { - res.end('body should not appear in HEAD output'); - } - return; - } - - if (url === '/auth-required') { - const auth = req.headers.authorization; - if (!auth || !auth.startsWith('Basic ')) { - res.writeHead(401, { 'Content-Type': 'text/plain' }); - res.end('unauthorized'); - return; - } - - const decoded = Buffer.from(auth.slice(6), 'base64').toString(); - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end(`authenticated: ${decoded}`); - return; - } - - if (url === '/upload' && req.method === 'POST') { - const contentType = req.headers['content-type'] ?? ''; - const chunks: Buffer[] = []; - req.on('data', (chunk) => { - chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); - }); - req.on('end', () => { - const body = Buffer.concat(chunks).toString(); - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end( - `multipart: ${contentType.startsWith('multipart/form-data')}\n` + - `body-contains-file: ${body.includes('upload.txt')}`, - ); - }); - return; - } - - if (url === '/binary') { - const payload = Buffer.alloc(256); - for (let i = 0; i < payload.length; i++) payload[i] = i & 0xff; - res.writeHead(200, { - 'Content-Type': 'application/octet-stream', - 'Content-Length': String(payload.length), - }); - res.end(payload); - return; - } - - if (url === '/named.txt') { - const body = 'downloaded-by-remote-name\n'; - res.writeHead(200, { - 'Content-Type': 'text/plain', - 'Content-Length': String(Buffer.byteLength(body)), - }); - res.end(body); - return; - } - - if (url === '/flaky') { - flakyRequestCount += 1; - if (flakyRequestCount === 1) { - res.writeHead(503, { 'Content-Type': 'text/plain' }); - res.end('retry please'); - return; - } - - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('retry succeeded'); - return; - } - - if (url === '/status') { - res.writeHead(201, { 'Content-Type': 'text/plain' }); - res.end('created'); - return; - } - - res.writeHead(404, { 'Content-Type': 'text/plain' }); - res.end('not found'); - }); - - await new Promise((resolveListen) => { - httpServer.listen(0, '127.0.0.1', resolveListen); - }); - httpPort = (httpServer.address() as import('node:net').AddressInfo).port; - - if (hasOpenssl) { - const tlsCert = generateSelfSignedCert(); - httpsServer = createHttpsServer({ key: tlsCert.key, cert: tlsCert.cert }, (req, res) => { - if (req.url === '/json') { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ secure: true, path: req.url })); - return; - } - - if (req.url === '/keepalive') { - const body = 'hello from tls keepalive'; - res.writeHead(200, { - 'Content-Type': 'text/plain', - 'Content-Length': String(Buffer.byteLength(body)), - Connection: 'keep-alive', - 'Keep-Alive': 'timeout=60', - }); - res.end(body); - return; - } - - res.writeHead(404, { 'Content-Type': 'text/plain' }); - res.end('not found'); - }); - httpsServer.keepAliveTimeout = 60000; - - await new Promise((resolveListen) => { - httpsServer.listen(0, '127.0.0.1', resolveListen); - }); - httpsPort = (httpsServer.address() as import('node:net').AddressInfo).port; - } - - keepAliveServer = createTcpServer((socket) => { - socket.once('data', () => { - const body = 'hello from keepalive'; - socket.write( - 'HTTP/1.1 200 OK\r\n' + - 'Content-Type: text/plain\r\n' + - `Content-Length: ${Buffer.byteLength(body)}\r\n` + - 'Connection: keep-alive\r\n' + - 'Keep-Alive: timeout=60\r\n' + - '\r\n' + - body, - ); - // Intentionally keep the socket open to exercise curl shutdown logic. - }); - }); - - await new Promise((resolveListen) => { - keepAliveServer.listen(0, '127.0.0.1', resolveListen); - }); - keepAlivePort = (keepAliveServer.address() as import('node:net').AddressInfo).port; - }); - - afterAll(async () => { - if (httpServer) { - await new Promise((resolveClose) => httpServer.close(() => resolveClose())); - } - if (httpsServer) { - await new Promise((resolveClose) => httpsServer.close(() => resolveClose())); - } - if (keepAliveServer) { - await new Promise((resolveClose) => keepAliveServer.close(() => resolveClose())); - } - }); - - async function createKernelWithNet() { - flakyRequestCount = 0; - const filesystem = createInMemoryFileSystem(); - await (filesystem as any).chmod('/', 0o1777); - await filesystem.mkdir('/tmp', { recursive: true }); - await (filesystem as any).chmod('/tmp', 0o1777); - - kernel = createKernel({ - filesystem, - permissions: allowAll, - loopbackExemptPorts: [httpPort, httpsPort, keepAlivePort], - }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - return kernel; - } - - async function execWithRetry(command: string) { - let lastResult: Awaited> | undefined; - for (let attempt = 1; attempt <= EXTERNAL_RETRY_ATTEMPTS; attempt += 1) { - lastResult = await kernel.exec(command); - if (lastResult.exitCode === 0) return lastResult; - if (attempt < EXTERNAL_RETRY_ATTEMPTS) { - await sleep(EXTERNAL_RETRY_DELAY_MS); - } - } - - return lastResult!; - } - - afterEach(async () => { - await kernel?.dispose(); - }); - - itIf(hasHttpGetTest, 'http_get_test reaches a local HTTP server', async () => { - await createKernelWithNet(); - const result = await kernel.exec(`http_get_test 127.0.0.1 ${httpPort} /json`); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('HTTP/1.1 200'); - expect(result.stdout).toContain('"ok":true'); - }, 15000); - - itIf(hasHttpGetTest, 'http_get_test preserves non-blocking connect diagnostics', async () => { - await createKernelWithNet(); - const result = await kernel.exec(`http_get_test 127.0.0.1 ${httpPort} /json`); - expect(result.exitCode).toBe(0); - expect(result.stderr).toContain('fcntl F_SETFL(NONBLOCK)=0'); - expect(result.stderr).toMatch(/connect=(0|-1 errno=\d+)/); - expect(result.stderr).toContain('getsockopt(SO_ERROR)=0 value=0'); - expect(result.stderr).toContain('poll(POLLOUT)=1'); - }, 15000); - - itIf(hasCurl, 'curl GET returns JSON from a local server', async () => { - await createKernelWithNet(); - const result = await kernel.exec(`curl -s http://127.0.0.1:${httpPort}/json`); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('"ok":true'); - }, 15000); - - itIf(hasCurl, 'curl --version reports the upstream tool version', async () => { - await createKernelWithNet(); - const result = await kernel.exec('curl --version'); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('curl 8.11.1'); - expect(result.stdout).toMatch(/Protocols:/); - }, 15000); - - itIf(hasCurl, 'curl -L follows redirects', async () => { - await createKernelWithNet(); - const result = await kernel.exec(`curl -s -L http://127.0.0.1:${httpPort}/redirect`); - expect(result.exitCode).toBe(0); - expect(result.stdout).toBe('followed redirect'); - }, 15000); - - itIf(hasCurl, 'curl POST sends body and headers', async () => { - await createKernelWithNet(); - const result = await kernel.exec( - `curl -s -X POST -H 'X-Test: edge-case' -d 'payload-data' http://127.0.0.1:${httpPort}/echo`, - ); - expect(result.exitCode).toBe(0); - const body = JSON.parse(result.stdout); - expect(body.method).toBe('POST'); - expect(body.body).toBe('payload-data'); - expect(body.header).toBe('edge-case'); - }, 15000); - - itIf(hasCurl, 'curl --json sends JSON with the expected headers', async () => { - await createKernelWithNet(); - const result = await kernel.exec( - `curl -s --json '{\"hello\":\"world\"}' http://127.0.0.1:${httpPort}/json-post`, - ); - expect(result.exitCode).toBe(0); - const body = JSON.parse(result.stdout); - expect(body.method).toBe('POST'); - expect(body.body).toBe('{"hello":"world"}'); - expect(body.contentType).toBe('application/json'); - expect(body.accept).toBe('application/json'); - }, 15000); - - itIf(hasCurl, 'curl -I returns response headers without the body', async () => { - await createKernelWithNet(); - const result = await kernel.exec(`curl -s -I http://127.0.0.1:${httpPort}/head-test`); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('HTTP/'); - expect(result.stdout).toMatch(/X-Test-Header/i); - expect(result.stdout).not.toContain('body should not appear'); - }, 15000); - - itIf(hasCurl, 'curl -u sends HTTP Basic authentication', async () => { - await createKernelWithNet(); - const result = await kernel.exec(`curl -s -u user:pass http://127.0.0.1:${httpPort}/auth-required`); - expect(result.exitCode).toBe(0); - expect(result.stdout).toBe('authenticated: user:pass'); - }, 15000); - - itIf(hasCurl, 'curl -F uploads multipart form data', async () => { - await createKernelWithNet(); - await kernel.writeFile('/tmp/upload.txt', 'file payload\n'); - const result = await kernel.exec(`curl -s -F file=@/tmp/upload.txt http://127.0.0.1:${httpPort}/upload`); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('multipart: true'); - expect(result.stdout).toContain('body-contains-file: true'); - }, 15000); - - itIf(hasCurl, 'curl -K reads options from a config file', async () => { - await createKernelWithNet(); - await kernel.writeFile( - '/tmp/curlrc', - `silent\nurl = "http://127.0.0.1:${httpPort}/json"\n`, - ); - const result = await kernel.exec('curl -K /tmp/curlrc'); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('"ok":true'); - }, 15000); - - itIf(hasCurl, 'curl -o writes text output to a file', async () => { - await createKernelWithNet(); - const result = await kernel.exec(`curl -s -o /tmp/out.json http://127.0.0.1:${httpPort}/json`); - expect(result.exitCode).toBe(0); - expect(result.stdout).toBe(''); - const file = new TextDecoder().decode(await kernel.readFile('/tmp/out.json')); - expect(file).toContain('"ok":true'); - }, 15000); - - itIf(hasCurl, 'curl -o respects the current working directory for relative output paths', async () => { - await createKernelWithNet(); - const result = await kernel.exec( - `mkdir -p /tmp/curl-cwd && cd /tmp/curl-cwd && ` + - `curl -s -o local.txt http://127.0.0.1:${httpPort}/named.txt && cat /tmp/curl-cwd/local.txt`, - ); - expect(result.exitCode).toBe(0); - expect(result.stdout).toBe('downloaded-by-remote-name\n'); - }, 15000); - - itIf(hasCurl, 'curl -o writes binary output without truncation', async () => { - await createKernelWithNet(); - const result = await kernel.exec(`curl -s -o /tmp/out.bin http://127.0.0.1:${httpPort}/binary`); - expect(result.exitCode).toBe(0); - const file = await kernel.readFile('/tmp/out.bin'); - expect(file).toHaveLength(256); - expect(Array.from(file.slice(0, 8))).toEqual([0, 1, 2, 3, 4, 5, 6, 7]); - expect(Array.from(file.slice(-4))).toEqual([252, 253, 254, 255]); - }, 15000); - - itIf(hasCurl, 'curl -D and -o split headers and body into separate files', async () => { - await createKernelWithNet(); - const result = await kernel.exec( - `curl -s -D /tmp/headers.txt -o /tmp/body.txt http://127.0.0.1:${httpPort}/named.txt`, - ); - expect(result.exitCode).toBe(0); - expect(result.stdout).toBe(''); - - const headers = new TextDecoder().decode(await kernel.readFile('/tmp/headers.txt')); - const body = new TextDecoder().decode(await kernel.readFile('/tmp/body.txt')); - expect(headers).toContain('HTTP/1.1 200 OK'); - expect(headers).toMatch(/Content-Type: text\/plain/i); - expect(body).toBe('downloaded-by-remote-name\n'); - }, 15000); - - itIf(hasCurl, 'curl -O writes to the remote filename', async () => { - await createKernelWithNet(); - const result = await kernel.exec( - `mkdir -p /tmp/remote-name && cd /tmp/remote-name && ` + - `curl -s -O http://127.0.0.1:${httpPort}/named.txt && cat /tmp/remote-name/named.txt`, - ); - expect(result.exitCode).toBe(0); - expect(result.stdout).toBe('downloaded-by-remote-name\n'); - }, 15000); - - itIf(hasCurl, 'curl -w writes the HTTP status code', async () => { - await createKernelWithNet(); - const result = await kernel.exec(`curl -s -w '%{http_code}' http://127.0.0.1:${httpPort}/status`); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('created'); - expect(result.stdout).toContain('201'); - }, 15000); - - itIf(hasCurl, 'curl -f reports HTTP errors with a non-zero exit code', async () => { - await createKernelWithNet(); - const result = await kernel.exec(`curl -fsS http://127.0.0.1:${httpPort}/missing`); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toMatch(/404|not found|error/i); - }, 15000); - - itIf(hasCurl, 'curl --fail-with-body preserves the response body on HTTP errors', async () => { - await createKernelWithNet(); - const result = await kernel.exec(`curl -sS --fail-with-body http://127.0.0.1:${httpPort}/missing`); - expect(result.exitCode).not.toBe(0); - expect(result.stdout).toBe('not found'); - expect(result.stderr).toMatch(/404|error/i); - }, 15000); - - itIf(hasCurl, 'curl reports refused connections without hanging', async () => { - await createKernelWithNet(); - - const probe = createTcpServer(); - await new Promise((resolveListen) => probe.listen(0, '127.0.0.1', resolveListen)); - const unusedPort = (probe.address() as import('node:net').AddressInfo).port; - await new Promise((resolveClose) => probe.close(() => resolveClose())); - - const startedAt = Date.now(); - const result = await kernel.exec(`curl -sS http://127.0.0.1:${unusedPort}/`); - expect(result.exitCode).not.toBe(0); - expect(Date.now() - startedAt).toBeLessThan(8000); - expect(result.stderr).toMatch(/connect|refused|failed/i); - }, 15000); - - itIf(hasCurl, 'curl reports DNS failures cleanly', async () => { - await createKernelWithNet(); - const result = await kernel.exec('curl -sS http://does-not-exist.invalid/'); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toMatch(/resolve|host|dns/i); - }, 15000); - - itIf(hasCurl, 'curl handles multiple URLs in one invocation', async () => { - await createKernelWithNet(); - const result = await kernel.exec( - `curl -s http://127.0.0.1:${httpPort}/one http://127.0.0.1:${httpPort}/two`, - ); - expect(result.exitCode).toBe(0); - expect(result.stdout).toBe('first-response\nsecond-response\n'); - }, 15000); - - itIf(hasCurl, 'curl --retry retries transient HTTP failures', async () => { - await createKernelWithNet(); - const result = await kernel.exec( - `curl -fsS --retry 2 --retry-delay 0 http://127.0.0.1:${httpPort}/flaky`, - ); - expect(result.exitCode).toBe(0); - expect(result.stdout).toBe('retry succeeded'); - expect(flakyRequestCount).toBeGreaterThanOrEqual(2); - }, 15000); - - itIf(hasCurl, 'curl exits promptly after a keep-alive response', async () => { - await createKernelWithNet(); - const startedAt = Date.now(); - const result = await kernel.exec(`curl -s http://127.0.0.1:${keepAlivePort}/`); - expect(result.exitCode).toBe(0); - expect(result.stdout).toBe('hello from keepalive'); - expect(Date.now() - startedAt).toBeLessThan(8000); - }, 15000); - - itIf(hasCurl && hasOpenssl, 'curl -k performs an HTTPS request through the WASI TLS backend', async () => { - await createKernelWithNet(); - const result = await kernel.exec(`curl -ks https://127.0.0.1:${httpsPort}/json`); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('"secure":true'); - }, 15000); - - itIf(hasCurl && hasOpenssl, 'curl fails TLS verification without -k on a self-signed endpoint', async () => { - await createKernelWithNet(); - const result = await kernel.exec(`curl -sS https://127.0.0.1:${httpsPort}/json`); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toMatch(/certificate|tls|ssl|verify/i); - }, 15000); - - itIf(hasCurl && hasOpenssl, 'curl -k exits promptly after an HTTPS keep-alive response', async () => { - await createKernelWithNet(); - const startedAt = Date.now(); - const result = await kernel.exec(`curl -ks https://127.0.0.1:${httpsPort}/keepalive`); - expect(result.exitCode).toBe(0); - expect(result.stdout).toBe('hello from tls keepalive'); - expect(Date.now() - startedAt).toBeLessThan(8000); - }, 15000); - - itIf(hasHttpGetTest && !externalNetworkSkipReason, 'http_get_test reaches an external host over real TCP', async () => { - await createKernelWithNet(); - const result = await execWithRetry(`http_get_test ${EXTERNAL_HOST} ${EXTERNAL_TCP_PORT} /`); - expect(result.exitCode).toBe(0); - expect(result.stdout).toMatch(/HTTP\/1\.[01] (200|301|302)/); - }, 30000); - - itIf(hasCurl && !externalNetworkSkipReason, 'curl reaches a real external HTTP endpoint', async () => { - await createKernelWithNet(); - const result = await execWithRetry( - `curl -fsSL --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 10 --max-time 30 ${EXTERNAL_HTTP_URL}`, - ); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain(EXTERNAL_EXPECTED_BODY); - }, 30000); - - itIf(hasCurl && !externalNetworkSkipReason, 'curl reaches a real external HTTPS endpoint', async () => { - await createKernelWithNet(); - const result = await execWithRetry( - `curl -fsSL --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 10 --max-time 30 ${EXTERNAL_HTTPS_URL}`, - ); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain(EXTERNAL_EXPECTED_BODY); - }, 30000); -}); diff --git a/registry/tests/wasmvm/fd-find.test.ts b/registry/tests/wasmvm/fd-find.test.ts deleted file mode 100644 index dfb5faf21d..0000000000 --- a/registry/tests/wasmvm/fd-find.test.ts +++ /dev/null @@ -1,255 +0,0 @@ -/** - * Integration tests for fd (fd-find) command. - * - * Verifies file finding with regex patterns, extension filters, type filters, - * hidden file skipping, and empty directory handling via kernel.exec() with - * real WASM binaries. - * - * Note: kernel.exec() wraps commands in sh -c. Brush-shell currently returns - * exit code 17 for all child commands (benign "could not retrieve pid" issue). - * Tests verify stdout correctness rather than exit code. - */ - -import { describe, it, expect, afterEach } from 'vitest'; -import { createWasmVmRuntime } from '../helpers.js'; -import { COMMANDS_DIR, createKernel, describeIf, hasWasmBinaries } from '../helpers.js'; -import type { Kernel } from '../helpers.js'; - -// Minimal in-memory VFS for kernel tests -class SimpleVFS { - private files = new Map(); - private dirs = new Set(['/']); - - async readFile(path: string): Promise { - const data = this.files.get(path); - if (!data) throw new Error(`ENOENT: ${path}`); - return data; - } - async readTextFile(path: string): Promise { - return new TextDecoder().decode(await this.readFile(path)); - } - async readDir(path: string): Promise { - const prefix = path === '/' ? '/' : path + '/'; - const entries: string[] = []; - for (const p of [...this.files.keys(), ...this.dirs]) { - if (p !== path && p.startsWith(prefix)) { - const rest = p.slice(prefix.length); - if (!rest.includes('/')) entries.push(rest); - } - } - return entries; - } - async readDirWithTypes(path: string) { - return (await this.readDir(path)).map(name => ({ - name, - isDirectory: this.dirs.has(path === '/' ? `/${name}` : `${path}/${name}`), - })); - } - async writeFile(path: string, content: string | Uint8Array): Promise { - const data = typeof content === 'string' ? new TextEncoder().encode(content) : content; - this.files.set(path, new Uint8Array(data)); - const parts = path.split('/').filter(Boolean); - for (let i = 1; i < parts.length; i++) { - this.dirs.add('/' + parts.slice(0, i).join('/')); - } - } - async createDir(path: string) { this.dirs.add(path); } - async mkdir(path: string, _options?: { recursive?: boolean }) { - this.dirs.add(path); - const parts = path.split('/').filter(Boolean); - for (let i = 1; i < parts.length; i++) { - this.dirs.add('/' + parts.slice(0, i).join('/')); - } - } - async exists(path: string): Promise { - return this.files.has(path) || this.dirs.has(path); - } - async stat(path: string) { - const isDir = this.dirs.has(path); - const data = this.files.get(path); - if (!isDir && !data) throw new Error(`ENOENT: ${path}`); - return { - mode: isDir ? 0o40755 : 0o100644, - size: data?.length ?? 0, - isDirectory: isDir, - isSymbolicLink: false, - atimeMs: Date.now(), - mtimeMs: Date.now(), - ctimeMs: Date.now(), - birthtimeMs: Date.now(), - ino: 0, - nlink: 1, - uid: 1000, - gid: 1000, - }; - } - async lstat(path: string) { return this.stat(path); } - async removeFile(path: string) { this.files.delete(path); } - async removeDir(path: string) { this.dirs.delete(path); } - async rename(oldPath: string, newPath: string) { - const data = this.files.get(oldPath); - if (data) { - this.files.set(newPath, data); - this.files.delete(oldPath); - } - } - async pread(path: string, offset: number, length: number): Promise { - const data = this.files.get(path); - if (!data) throw new Error(`ENOENT: ${path}`); - return data.slice(offset, offset + length); - } - async pwrite(path: string, offset: number, content: Uint8Array): Promise { - const data = this.files.get(path); - if (!data) throw new Error(`ENOENT: ${path}`); - const next = new Uint8Array(Math.max(data.length, offset + content.length)); - next.set(data); - next.set(content, offset); - this.files.set(path, next); - } -} - -/** Create a VFS pre-populated with a test directory structure */ -async function createTestVFS(): Promise { - const vfs = new SimpleVFS(); - // /project/ - // src/ - // main.js - // utils.js - // helpers.ts - // lib/ - // parser.js - // docs/ - // readme.md - // .hidden/ - // secret.txt - // .gitignore - // config.json - await vfs.writeFile('/project/src/main.js', 'console.log("main")'); - await vfs.writeFile('/project/src/utils.js', 'export {}'); - await vfs.writeFile('/project/src/helpers.ts', 'export {}'); - await vfs.writeFile('/project/lib/parser.js', 'module.exports = {}'); - await vfs.writeFile('/project/docs/readme.md', '# Readme'); - await vfs.writeFile('/project/.hidden/secret.txt', 'secret'); - await vfs.writeFile('/project/.gitignore', 'node_modules'); - await vfs.writeFile('/project/config.json', '{}'); - // /empty/ — empty directory - await vfs.mkdir('/empty', { recursive: true }); - return vfs; -} - -/** Parse fd output lines, sorted for deterministic comparison */ -function parseLines(stdout: string): string[] { - return stdout.split('\n').filter(l => l.length > 0).sort(); -} - -describeIf(hasWasmBinaries, 'fd-find command', { timeout: 10_000 }, () => { - let kernel: Kernel; - - afterEach(async () => { - await kernel?.dispose(); - }); - - it('finds files matching regex pattern in current directory', async () => { - const vfs = await createTestVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); - - const result = await kernel.exec('fd main /project', {}); - const lines = parseLines(result.stdout); - expect(lines).toContain('/project/src/main.js'); - }); - - it('finds all .js files with -e js', async () => { - const vfs = await createTestVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); - - const result = await kernel.exec('fd -e js . /project', {}); - const lines = parseLines(result.stdout); - expect(lines).toContain('/project/src/main.js'); - expect(lines).toContain('/project/src/utils.js'); - expect(lines).toContain('/project/lib/parser.js'); - // .ts files should NOT match - expect(lines).not.toContain('/project/src/helpers.ts'); - }); - - it('finds only files with -t f', async () => { - const vfs = await createTestVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); - - const result = await kernel.exec('fd -t f . /project', {}); - const lines = parseLines(result.stdout); - // All entries should be files, not directories - for (const line of lines) { - const stat = await vfs.stat(line); - expect(stat.isDirectory).toBe(false); - } - // Should include known files - expect(lines).toContain('/project/src/main.js'); - expect(lines).toContain('/project/config.json'); - }); - - it('finds only directories with -t d', async () => { - const vfs = await createTestVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); - - const result = await kernel.exec('fd -t d . /project', {}); - const lines = parseLines(result.stdout); - // All entries should be directories - for (const line of lines) { - const stat = await vfs.stat(line); - expect(stat.isDirectory).toBe(true); - } - // Should include known directories (hidden skipped by default) - expect(lines).toContain('/project/src'); - expect(lines).toContain('/project/lib'); - expect(lines).toContain('/project/docs'); - }); - - it('returns no results for empty directory', async () => { - const vfs = await createTestVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); - - const result = await kernel.exec('fd . /empty', {}); - expect(result.stdout.trim()).toBe(''); - }); - - it('returns empty output when no files match pattern', async () => { - const vfs = await createTestVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); - - const result = await kernel.exec('fd zzzznonexistent /project', {}); - expect(result.stdout.trim()).toBe(''); - }); - - it('skips hidden files and directories by default', async () => { - const vfs = await createTestVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); - - const result = await kernel.exec('fd . /project', {}); - const lines = parseLines(result.stdout); - // Hidden files/dirs should NOT appear - const hiddenEntries = lines.filter(l => { - const parts = l.split('/'); - return parts.some(p => p.startsWith('.') && p.length > 1); - }); - expect(hiddenEntries).toEqual([]); - }); - - it('includes hidden files with -H flag', async () => { - const vfs = await createTestVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); - - const result = await kernel.exec('fd -H . /project', {}); - const lines = parseLines(result.stdout); - // Hidden items should now appear - expect(lines).toContain('/project/.gitignore'); - expect(lines).toContain('/project/.hidden'); - }); -}); diff --git a/registry/tests/wasmvm/git.test.ts b/registry/tests/wasmvm/git.test.ts deleted file mode 100644 index 93264c08ea..0000000000 --- a/registry/tests/wasmvm/git.test.ts +++ /dev/null @@ -1,547 +0,0 @@ -/** - * Integration tests for git command. - * - * Verifies init, add, commit, branch, checkout (with DWIM), plus local and - * smart-HTTP remote clone via kernel.exec() with real WASM binaries. - */ - -import { describe, it, expect, afterEach, beforeAll, afterAll, vi } from 'vitest'; -import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; -import { resolve, join } from 'node:path'; -import { tmpdir } from 'node:os'; -import { createServer, type Server as HttpServer } from 'node:http'; -import { spawn, spawnSync } from 'node:child_process'; -import { createWasmVmRuntime } from '../helpers.js'; -import { - allowAll, - COMMANDS_DIR, - createInMemoryFileSystem, - createKernel, - describeIf, - hasWasmBinaries, -} from '../helpers.js'; -import type { Kernel } from '../helpers.js'; - -vi.setConfig({ testTimeout: 30_000 }); - -/** Check git binary exists in addition to base WASM binaries */ -const hasGit = hasWasmBinaries && existsSync(resolve(COMMANDS_DIR, 'git')); -const hasHostGit = spawnSync('git', ['--version'], { stdio: 'ignore' }).status === 0; - -/** Create a kernel with a world-writable in-memory filesystem */ -async function createGitKernel() { - const vfs = createInMemoryFileSystem(); - // Make root and /tmp writable by all users (WASM processes run as non-root) - await (vfs as any).chmod('/', 0o1777); - await vfs.mkdir('/tmp', { recursive: true }); - await (vfs as any).chmod('/tmp', 0o1777); - const kernel = createKernel({ filesystem: vfs, syncFilesystemOnDispose: false }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); - return { kernel, vfs, dispose: () => kernel.dispose() }; -} - -async function createGitKernelWithNet(loopbackExemptPorts: number[]) { - const vfs = createInMemoryFileSystem(); - await (vfs as any).chmod('/', 0o1777); - await vfs.mkdir('/tmp', { recursive: true }); - await (vfs as any).chmod('/tmp', 0o1777); - const kernel = createKernel({ - filesystem: vfs, - permissions: allowAll, - loopbackExemptPorts, - syncFilesystemOnDispose: false, - }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); - return { kernel, vfs, dispose: () => kernel.dispose() }; -} - -function runHostGit(args: string[], cwd?: string) { - const result = spawnSync('git', args, { - cwd, - encoding: 'utf8', - }); - if (result.status !== 0) { - throw new Error( - `host git failed: git ${args.join(' ')}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, - ); - } -} - -/** Helper: run command and assert success */ -async function run(kernel: Kernel, cmd: string): Promise<{ stdout: string; stderr: string; exitCode: number }> { - const r = await kernel.exec(cmd); - if (r.exitCode !== 0) { - throw new Error(`Command failed (exit ${r.exitCode}): ${cmd}\nstdout: ${r.stdout}\nstderr: ${r.stderr}`); - } - return r; -} - -// TODO(P6): requires git WASM artifact, intentionally excluded from the fast registry-build gate. -describe.skip('git command', () => { - let kernel: Kernel; - let vfs: any; - let dispose: () => Promise; - - afterEach(async () => { - await dispose?.(); - }); - - it('init creates .git directory structure', async () => { - ({ kernel, vfs, dispose } = await createGitKernel()); - - const result = await run(kernel, 'git init /repo'); - expect(result.stdout).toContain('Initialized empty Git repository'); - - expect(await vfs.exists('/repo/.git/HEAD')).toBe(true); - expect(await vfs.exists('/repo/.git/objects')).toBe(true); - expect(await vfs.exists('/repo/.git/refs/heads')).toBe(true); - - const head = new TextDecoder().decode(await vfs.readFile('/repo/.git/HEAD')); - expect(head.trim()).toBe('ref: refs/heads/main'); - }); - - it('add + commit creates objects and updates ref', async () => { - ({ kernel, vfs, dispose } = await createGitKernel()); - - await run(kernel, 'git init /repo'); - await kernel.writeFile('/repo/hello.txt', 'hello world\n'); - await run(kernel, 'git -C /repo add hello.txt'); - await run(kernel, "git -C /repo commit -m 'first commit'"); - - expect(await vfs.exists('/repo/.git/refs/heads/main')).toBe(true); - }); - - it('branch lists branches with current marked', async () => { - ({ kernel, vfs, dispose } = await createGitKernel()); - - await run(kernel, 'git init /repo'); - await kernel.writeFile('/repo/file.txt', 'content\n'); - await run(kernel, 'git -C /repo add file.txt'); - await run(kernel, "git -C /repo commit -m 'init'"); - - const result = await run(kernel, 'git -C /repo branch'); - expect(result.stdout.trim()).toBe('* main'); - }); - - it('checkout -b creates a new branch', async () => { - ({ kernel, vfs, dispose } = await createGitKernel()); - - await run(kernel, 'git init /repo'); - await kernel.writeFile('/repo/file.txt', 'content\n'); - await run(kernel, 'git -C /repo add file.txt'); - await run(kernel, "git -C /repo commit -m 'init'"); - - await run(kernel, 'git -C /repo checkout -b feature'); - - const result = await run(kernel, 'git -C /repo branch'); - const lines = result.stdout.trim().split('\n').map((l: string) => l.trim()); - expect(lines).toContain('* feature'); - expect(lines).toContain('main'); - }); - - it('full quickstart scenario: init, commit, branch, clone, checkout', async () => { - ({ kernel, vfs, dispose } = await createGitKernel()); - - // Create origin repo - await run(kernel, 'git init /tmp/origin'); - await kernel.writeFile('/tmp/origin/README.md', '# demo repo\n'); - await run(kernel, 'git -C /tmp/origin add README.md'); - await run(kernel, "git -C /tmp/origin commit -m 'initial commit'"); - - // Check default branch - let r = await run(kernel, 'git -C /tmp/origin branch'); - expect(r.stdout.trim()).toBe('* main'); - - // Create feature branch with a new file - await run(kernel, 'git -C /tmp/origin checkout -b feature'); - await kernel.writeFile('/tmp/origin/feature.txt', 'checked out from feature\n'); - await run(kernel, 'git -C /tmp/origin add feature.txt'); - await run(kernel, "git -C /tmp/origin commit -m 'add feature file'"); - - // Switch back to main - await run(kernel, 'git -C /tmp/origin checkout main'); - - // Clone - await run(kernel, 'git clone /tmp/origin /tmp/clone'); - - // Clone should only show main branch initially - r = await run(kernel, 'git -C /tmp/clone branch'); - expect(r.stdout.trim()).toBe('* main'); - - // Checkout feature (DWIM from remote tracking) - await run(kernel, 'git -C /tmp/clone checkout feature'); - - // Now both branches should be listed - r = await run(kernel, 'git -C /tmp/clone branch'); - const branches = r.stdout.trim().split('\n').map((l: string) => l.trim()); - expect(branches).toContain('* feature'); - expect(branches).toContain('main'); - - // Verify feature file exists in clone - const featureContent = new TextDecoder().decode(await vfs.readFile('/tmp/clone/feature.txt')); - expect(featureContent).toBe('checked out from feature\n'); - - // Verify README exists too - const readmeContent = new TextDecoder().decode(await vfs.readFile('/tmp/clone/README.md')); - expect(readmeContent).toBe('# demo repo\n'); - }); - - it('clone without an explicit destination uses the source basename', async () => { - ({ kernel, vfs, dispose } = await createGitKernel()); - - await run(kernel, 'git init /tmp/origin'); - await kernel.writeFile('/tmp/origin/README.md', 'default destination\n'); - await run(kernel, 'git -C /tmp/origin add README.md'); - await run(kernel, "git -C /tmp/origin commit -m 'seed'"); - - await run(kernel, 'mkdir -p /work'); - await run(kernel, 'git -C /work clone /tmp/origin'); - - expect(await vfs.exists('/work/origin/.git/HEAD')).toBe(true); - const readmeContent = new TextDecoder().decode(await vfs.readFile('/work/origin/README.md')); - expect(readmeContent).toBe('default destination\n'); - }); - - it('clone without an explicit destination strips a trailing .git suffix', async () => { - ({ kernel, vfs, dispose } = await createGitKernel()); - - await run(kernel, 'git init /tmp/origin.git'); - await kernel.writeFile('/tmp/origin.git/README.md', 'suffix destination\n'); - await run(kernel, 'git -C /tmp/origin.git add README.md'); - await run(kernel, "git -C /tmp/origin.git commit -m 'seed'"); - - await run(kernel, 'mkdir -p /work'); - await run(kernel, 'git -C /work clone /tmp/origin.git'); - - expect(await vfs.exists('/work/origin/.git/HEAD')).toBe(true); - const readmeContent = new TextDecoder().decode(await vfs.readFile('/work/origin/README.md')); - expect(readmeContent).toBe('suffix destination\n'); - }); - - it('clone into an existing empty destination directory succeeds', async () => { - ({ kernel, vfs, dispose } = await createGitKernel()); - - await run(kernel, 'git init /tmp/origin'); - await kernel.writeFile('/tmp/origin/README.md', 'empty destination\n'); - await run(kernel, 'git -C /tmp/origin add README.md'); - await run(kernel, "git -C /tmp/origin commit -m 'seed'"); - - await run(kernel, 'mkdir -p /tmp/clone'); - await run(kernel, 'git clone /tmp/origin /tmp/clone'); - - expect(await vfs.exists('/tmp/clone/.git/HEAD')).toBe(true); - const readmeContent = new TextDecoder().decode(await vfs.readFile('/tmp/clone/README.md')); - expect(readmeContent).toBe('empty destination\n'); - }); - - it('clone rejects a non-empty destination directory', async () => { - ({ kernel, vfs, dispose } = await createGitKernel()); - - await run(kernel, 'git init /tmp/origin'); - await kernel.writeFile('/tmp/origin/README.md', 'origin\n'); - await run(kernel, 'git -C /tmp/origin add README.md'); - await run(kernel, "git -C /tmp/origin commit -m 'seed'"); - - await run(kernel, 'mkdir -p /tmp/clone'); - await kernel.writeFile('/tmp/clone/existing.txt', 'keep me\n'); - - const result = await kernel.exec('git clone /tmp/origin /tmp/clone'); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toMatch(/already exists|not an empty directory|destination/i); - - const existing = new TextDecoder().decode(await vfs.readFile('/tmp/clone/existing.txt')); - expect(existing).toBe('keep me\n'); - expect(await vfs.exists('/tmp/clone/.git')).toBe(false); - }); - - it('clone of a missing repository fails without leaving a partial destination', async () => { - ({ kernel, vfs, dispose } = await createGitKernel()); - - const result = await kernel.exec('git clone /tmp/missing /tmp/clone'); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toMatch(/not a git repository|missing|no such file|fatal/i); - expect(await vfs.exists('/tmp/clone')).toBe(false); - }); - - it('clone of an empty repository succeeds and leaves an empty worktree', async () => { - ({ kernel, vfs, dispose } = await createGitKernel()); - - await run(kernel, 'git init /tmp/origin'); - await run(kernel, 'git clone /tmp/origin /tmp/clone'); - - const head = new TextDecoder().decode(await vfs.readFile('/tmp/clone/.git/HEAD')); - expect(head.trim()).toBe('ref: refs/heads/main'); - expect(await vfs.exists('/tmp/clone/.git/config')).toBe(true); - expect(await vfs.exists('/tmp/clone/.git/refs/heads/main')).toBe(false); - expect(await vfs.exists('/tmp/clone/README.md')).toBe(false); - }); - - it('clone preserves nested directory trees', async () => { - ({ kernel, vfs, dispose } = await createGitKernel()); - - await run(kernel, 'git init /tmp/origin'); - await run(kernel, 'mkdir -p /tmp/origin/src/nested'); - await kernel.writeFile('/tmp/origin/src/nested/file.txt', 'nested payload\n'); - await kernel.writeFile('/tmp/origin/src/root.txt', 'root payload\n'); - await run(kernel, 'git -C /tmp/origin add src/nested/file.txt src/root.txt'); - await run(kernel, "git -C /tmp/origin commit -m 'nested tree'"); - - await run(kernel, 'git clone /tmp/origin /tmp/clone'); - - const nested = new TextDecoder().decode(await vfs.readFile('/tmp/clone/src/nested/file.txt')); - const root = new TextDecoder().decode(await vfs.readFile('/tmp/clone/src/root.txt')); - expect(nested).toBe('nested payload\n'); - expect(root).toBe('root payload\n'); - }); - - it('clone honors the source default branch when HEAD is not main', async () => { - ({ kernel, vfs, dispose } = await createGitKernel()); - - await run(kernel, 'git init /tmp/origin'); - await kernel.writeFile('/tmp/origin/README.md', 'main branch\n'); - await run(kernel, 'git -C /tmp/origin add README.md'); - await run(kernel, "git -C /tmp/origin commit -m 'main'"); - - await run(kernel, 'git -C /tmp/origin checkout -b trunk'); - await kernel.writeFile('/tmp/origin/trunk.txt', 'trunk branch\n'); - await run(kernel, 'git -C /tmp/origin add trunk.txt'); - await run(kernel, "git -C /tmp/origin commit -m 'trunk'"); - - await run(kernel, 'git clone /tmp/origin /tmp/clone'); - - const head = new TextDecoder().decode(await vfs.readFile('/tmp/clone/.git/HEAD')); - expect(head.trim()).toBe('ref: refs/heads/trunk'); - expect(await vfs.exists('/tmp/clone/.git/refs/heads/trunk')).toBe(true); - const trunk = new TextDecoder().decode(await vfs.readFile('/tmp/clone/trunk.txt')); - expect(trunk).toBe('trunk branch\n'); - }); - - it('clone copies nested branch refs and checkout DWIM works for branch names with slashes', async () => { - ({ kernel, vfs, dispose } = await createGitKernel()); - - await run(kernel, 'git init /tmp/origin'); - await kernel.writeFile('/tmp/origin/README.md', '# demo repo\n'); - await run(kernel, 'git -C /tmp/origin add README.md'); - await run(kernel, "git -C /tmp/origin commit -m 'initial commit'"); - - await run(kernel, 'git -C /tmp/origin checkout -b feature/deep'); - await kernel.writeFile('/tmp/origin/feature.txt', 'nested branch payload\n'); - await run(kernel, 'git -C /tmp/origin add feature.txt'); - await run(kernel, "git -C /tmp/origin commit -m 'nested branch'"); - await run(kernel, 'git -C /tmp/origin checkout main'); - - await run(kernel, 'git clone /tmp/origin /tmp/clone'); - - expect(await vfs.exists('/tmp/clone/.git/refs/remotes/origin/feature/deep')).toBe(true); - - await run(kernel, 'git -C /tmp/clone checkout feature/deep'); - const featureContent = new TextDecoder().decode(await vfs.readFile('/tmp/clone/feature.txt')); - expect(featureContent).toBe('nested branch payload\n'); - const head = new TextDecoder().decode(await vfs.readFile('/tmp/clone/.git/HEAD')); - expect(head.trim()).toBe('ref: refs/heads/feature/deep'); - }); - - it('clone works with relative source and destination paths', async () => { - ({ kernel, vfs, dispose } = await createGitKernel()); - - await run(kernel, 'mkdir -p /tmp/work'); - await run(kernel, 'git init /tmp/work/origin'); - await kernel.writeFile('/tmp/work/origin/README.md', 'relative clone\n'); - await run(kernel, 'git -C /tmp/work/origin add README.md'); - await run(kernel, "git -C /tmp/work/origin commit -m 'seed'"); - - await run(kernel, 'git -C /tmp/work clone ./origin ./clone'); - - expect(await vfs.exists('/tmp/work/clone/.git/HEAD')).toBe(true); - const readmeContent = new TextDecoder().decode(await vfs.readFile('/tmp/work/clone/README.md')); - expect(readmeContent).toBe('relative clone\n'); - }); - - it('push fails with a typed unsupported-subcommand error', async () => { - ({ kernel, dispose } = await createGitKernel()); - - await run(kernel, 'git init /tmp/repo'); - - const result = await kernel.exec('git -C /tmp/repo push origin main'); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain('GitSubcommandUnsupported'); - expect(result.stderr).toContain('git push'); - expect(result.stderr).toContain('registry/native/crates/libs/git/README.md'); - }); - - it('clone rejects SSH-style remotes with a typed unsupported-subcommand error', async () => { - ({ kernel, dispose } = await createGitKernel()); - - const result = await kernel.exec('git clone git@github.com:rivet-dev/agentos.git /tmp/clone'); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain('GitSubcommandUnsupported'); - expect(result.stderr).toContain('git clone'); - expect(result.stderr).toContain('SSH'); - expect(result.stderr).toContain('registry/native/crates/libs/git/README.md'); - }); - - it('clone rejects authenticated HTTPS remotes loudly instead of attempting a broken auth flow', async () => { - ({ kernel, dispose } = await createGitKernel()); - - const result = await kernel.exec( - 'git clone https://private@example.com/owner/repo.git /tmp/clone', - { env: { GIT_AUTH_TOKEN: 'test-token' } }, - ); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toContain('GitSubcommandUnsupported'); - expect(result.stderr).toContain('git clone'); - expect(result.stderr).toContain('authenticated HTTP(S) remotes'); - expect(result.stderr).toContain('registry/native/crates/libs/git/README.md'); - }); - - describeIf(hasHostGit, 'remote clone over smart HTTP', () => { - let repoRoot: string; - let httpServer: HttpServer; - let httpPort: number; - - beforeAll(async () => { - repoRoot = mkdtempSync(join(tmpdir(), 'agentos-git-http-')); - const worktree = join(repoRoot, 'worktree'); - const origin = join(repoRoot, 'origin.git'); - - runHostGit(['-c', 'init.defaultBranch=main', 'init', worktree]); - writeFileSync(join(worktree, 'README.md'), 'remote smart clone\n'); - runHostGit(['-C', worktree, 'add', 'README.md']); - runHostGit([ - '-C', worktree, - '-c', 'user.name=secure-exec', - '-c', 'user.email=agent@example.com', - 'commit', - '-m', - 'seed', - ]); - - runHostGit(['-C', worktree, 'checkout', '-b', 'feature/deep']); - writeFileSync(join(worktree, 'feature.txt'), 'remote branch payload\n'); - runHostGit(['-C', worktree, 'add', 'feature.txt']); - runHostGit([ - '-C', worktree, - '-c', 'user.name=secure-exec', - '-c', 'user.email=agent@example.com', - 'commit', - '-m', - 'feature branch', - ]); - - runHostGit(['-C', worktree, 'checkout', 'main']); - runHostGit(['clone', '--bare', worktree, origin]); - runHostGit(['-C', origin, 'repack', '-a', '-d', '-f', '--depth=50', '--window=50']); - - httpServer = createServer((req, res) => { - const url = new URL(req.url ?? '/', 'http://127.0.0.1'); - const bodyChunks: Buffer[] = []; - - req.on('data', (chunk) => { - bodyChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); - }); - - req.on('end', () => { - const requestBody = Buffer.concat(bodyChunks); - const gitProtocol = req.headers['git-protocol']; - const env = { - ...process.env, - GIT_HTTP_EXPORT_ALL: '1', - GIT_PROJECT_ROOT: repoRoot, - PATH_INFO: url.pathname, - QUERY_STRING: url.search.startsWith('?') ? url.search.slice(1) : url.search, - REQUEST_METHOD: req.method ?? 'GET', - CONTENT_TYPE: String(req.headers['content-type'] ?? ''), - CONTENT_LENGTH: String(requestBody.length), - REMOTE_ADDR: '127.0.0.1', - GIT_PROTOCOL: typeof gitProtocol === 'string' ? gitProtocol : '', - HTTP_GIT_PROTOCOL: typeof gitProtocol === 'string' ? gitProtocol : '', - }; - - const child = spawn('git', ['http-backend'], { env }); - const stdout: Buffer[] = []; - const stderr: Buffer[] = []; - - child.stdout.on('data', (chunk) => { - stdout.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); - }); - child.stderr.on('data', (chunk) => { - stderr.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); - }); - child.on('error', (error) => { - res.writeHead(500, { 'Content-Type': 'text/plain' }); - res.end(String(error)); - }); - child.on('close', (code) => { - const output = Buffer.concat(stdout); - const headerSep = output.indexOf(Buffer.from('\r\n\r\n')); - const altSep = output.indexOf(Buffer.from('\n\n')); - const sepIndex = headerSep >= 0 ? headerSep : altSep; - const sepLen = headerSep >= 0 ? 4 : altSep >= 0 ? 2 : 0; - - if (code !== 0 && sepIndex === -1) { - res.writeHead(500, { 'Content-Type': 'text/plain' }); - res.end(Buffer.concat(stderr)); - return; - } - - if (sepIndex === -1) { - res.writeHead(500, { 'Content-Type': 'text/plain' }); - res.end(output); - return; - } - - const headerText = output.subarray(0, sepIndex).toString('utf8'); - const responseBody = output.subarray(sepIndex + sepLen); - let status = 200; - const headers: Record = {}; - - for (const line of headerText.split(/\r?\n/)) { - if (!line) continue; - const colon = line.indexOf(':'); - if (colon === -1) continue; - const name = line.slice(0, colon); - const value = line.slice(colon + 1).trim(); - if (name.toLowerCase() === 'status') { - status = Number.parseInt(value, 10) || 200; - } else { - headers[name] = value; - } - } - - res.writeHead(status, headers); - res.end(responseBody); - }); - - child.stdin.end(requestBody); - }); - }); - - await new Promise((resolveListen) => { - httpServer.listen(0, '127.0.0.1', resolveListen); - }); - httpPort = (httpServer.address() as import('node:net').AddressInfo).port; - }); - - afterAll(async () => { - await new Promise((resolveClose) => httpServer.close(() => resolveClose())); - rmSync(repoRoot, { recursive: true, force: true }); - }); - - it('clone fetches refs and worktree contents from a smart HTTP remote', async () => { - ({ kernel, vfs, dispose } = await createGitKernelWithNet([httpPort])); - - await run(kernel, `git clone http://127.0.0.1:${httpPort}/origin.git /tmp/clone`); - - const head = new TextDecoder().decode(await kernel.readFile('/tmp/clone/.git/HEAD')); - expect(head.trim()).toBe('ref: refs/heads/main'); - - const readme = new TextDecoder().decode(await kernel.readFile('/tmp/clone/README.md')); - expect(readme).toBe('remote smart clone\n'); - expect(await kernel.exists('/tmp/clone/.git/refs/remotes/origin/feature/deep')).toBe(true); - - await run(kernel, 'git -C /tmp/clone checkout feature/deep'); - const feature = new TextDecoder().decode(await kernel.readFile('/tmp/clone/feature.txt')); - expect(feature).toBe('remote branch payload\n'); - }); - }); -}); diff --git a/registry/tests/wasmvm/net-server.test.ts b/registry/tests/wasmvm/net-server.test.ts deleted file mode 100644 index c943363629..0000000000 --- a/registry/tests/wasmvm/net-server.test.ts +++ /dev/null @@ -1,196 +0,0 @@ -/** - * Integration test for WasmVM TCP server sockets. - * - * Spawns the tcp_server C program as WASM (bind → listen → accept → recv → - * send "pong" → close), connects to it from the kernel as a client socket, - * and verifies the full data exchange via loopback routing. - */ - -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { createWasmVmRuntime } from '../helpers.js'; -import { - AF_INET, - COMMANDS_DIR, - C_BUILD_DIR, - createKernel, - describeIf, - hasWasmBinaries, - SOCK_STREAM, -} from '../helpers.js'; -import type { Kernel } from '../helpers.js'; -import { existsSync } from 'node:fs'; -import { join } from 'node:path'; - -const hasCWasmBinaries = existsSync(join(C_BUILD_DIR, 'tcp_server')); - -function skipReason(): string | false { - if (!hasWasmBinaries) return 'WASM binaries not built (run make wasm in native/wasmvm/)'; - if (!hasCWasmBinaries) return 'tcp_server WASM binary not built (run make -C native/wasmvm/c sysroot && make -C native/wasmvm/c programs)'; - return false; -} - -// Minimal in-memory VFS (same as c-parity) -class SimpleVFS { - private files = new Map(); - private dirs = new Set(['/']); - private symlinks = new Map(); - - async readFile(path: string): Promise { - const data = this.files.get(path); - if (!data) throw new Error(`ENOENT: ${path}`); - return data; - } - async readTextFile(path: string): Promise { - return new TextDecoder().decode(await this.readFile(path)); - } - async pread(path: string, offset: number, length: number): Promise { - const data = await this.readFile(path); - return data.slice(offset, offset + length); - } - async readDir(path: string): Promise { - const prefix = path === '/' ? '/' : path + '/'; - const entries: string[] = []; - for (const p of [...this.files.keys(), ...this.dirs]) { - if (p !== path && p.startsWith(prefix)) { - const rest = p.slice(prefix.length); - if (!rest.includes('/')) entries.push(rest); - } - } - return entries; - } - async readDirWithTypes(path: string) { - return (await this.readDir(path)).map((name) => ({ - name, - isDirectory: this.dirs.has(path === '/' ? `/${name}` : `${path}/${name}`), - })); - } - async writeFile(path: string, content: string | Uint8Array): Promise { - const data = typeof content === 'string' ? new TextEncoder().encode(content) : content; - this.files.set(path, new Uint8Array(data)); - const parts = path.split('/').filter(Boolean); - for (let i = 1; i < parts.length; i++) { - this.dirs.add('/' + parts.slice(0, i).join('/')); - } - } - async createDir(path: string) { this.dirs.add(path); } - async mkdir(path: string, _options?: { recursive?: boolean }) { this.dirs.add(path); } - async exists(path: string): Promise { - return this.files.has(path) || this.dirs.has(path) || this.symlinks.has(path); - } - async stat(path: string) { - const isDir = this.dirs.has(path); - const isSymlink = this.symlinks.has(path); - const data = this.files.get(path); - if (!isDir && !isSymlink && !data) throw new Error(`ENOENT: ${path}`); - return { - mode: isSymlink ? 0o120777 : (isDir ? 0o40755 : 0o100644), - size: data?.length ?? 0, - isDirectory: isDir, - isSymbolicLink: isSymlink, - atimeMs: Date.now(), - mtimeMs: Date.now(), - ctimeMs: Date.now(), - birthtimeMs: Date.now(), - ino: 0, - nlink: 1, - uid: 1000, - gid: 1000, - }; - } - async chmod() {} - async rename(from: string, to: string) { - const data = this.files.get(from); - if (data) { this.files.set(to, data); this.files.delete(from); } - } - async unlink(path: string) { this.files.delete(path); this.symlinks.delete(path); } - async rmdir(path: string) { this.dirs.delete(path); } - async symlink(target: string, linkPath: string) { - this.symlinks.set(linkPath, target); - const parts = linkPath.split('/').filter(Boolean); - for (let i = 1; i < parts.length; i++) { - this.dirs.add('/' + parts.slice(0, i).join('/')); - } - } - async readlink(path: string): Promise { - const target = this.symlinks.get(path); - if (!target) throw new Error(`EINVAL: ${path}`); - return target; - } -} - -// Wait for a kernel socket listener on the given port (poll with timeout) -async function waitForListener( - kernel: Kernel, - port: number, - timeoutMs = 10_000, -): Promise { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - const listener = kernel.socketTable.findListener({ host: '0.0.0.0', port }); - if (listener) return; - await new Promise((r) => setTimeout(r, 20)); - } - throw new Error(`Timed out waiting for listener on port ${port}`); -} - -const TEST_PORT = 9876; -const CLIENT_PID = 999; // Fake PID for test-side client sockets - -describeIf(!skipReason(), 'WasmVM TCP server integration', { timeout: 30_000 }, () => { - let kernel: Kernel; - let vfs: SimpleVFS; - - beforeEach(async () => { - vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - }); - - afterEach(async () => { - await kernel?.dispose(); - }); - - it('tcp_server: accept connection, recv data, send pong', async () => { - // Start the WASM TCP server (blocks on accept until we connect) - const execPromise = kernel.exec(`tcp_server ${TEST_PORT}`); - - // Wait for the server to finish bind+listen - await waitForListener(kernel, TEST_PORT); - - // Create a client socket and connect via loopback - const st = kernel.socketTable; - const clientId = st.create(AF_INET, SOCK_STREAM, 0, CLIENT_PID); - await st.connect(clientId, { host: '127.0.0.1', port: TEST_PORT }); - - // Send "ping" to the server - const encoder = new TextEncoder(); - st.send(clientId, encoder.encode('ping')); - - // Wait for the server to process and send its reply - const decoder = new TextDecoder(); - let reply = ''; - const recvDeadline = Date.now() + 10_000; - while (Date.now() < recvDeadline) { - const chunk = st.recv(clientId, 256); - if (chunk && chunk.length > 0) { - reply += decoder.decode(chunk); - break; - } - // No data yet — yield to let the WASM worker process - await new Promise((r) => setTimeout(r, 20)); - } - - expect(reply).toBe('pong'); - - // Close client socket - st.close(clientId, CLIENT_PID); - - // Wait for exec to complete (server exits after handling one connection) - const result = await execPromise; - - expect(result.stdout).toContain('listening on port 9876'); - expect(result.stdout).toContain('received: ping'); - expect(result.stdout).toContain('sent: 4'); - expect(result.exitCode).toBe(0); - }); -}); diff --git a/registry/tests/wasmvm/net-udp.test.ts b/registry/tests/wasmvm/net-udp.test.ts deleted file mode 100644 index 98e6b93696..0000000000 --- a/registry/tests/wasmvm/net-udp.test.ts +++ /dev/null @@ -1,234 +0,0 @@ -/** - * Integration test for WasmVM UDP sockets. - * - * Spawns the udp_echo C program as WASM (bind → recvfrom → sendto echo → close), - * sends datagrams from a kernel client socket, and verifies the echo response - * and message boundary preservation. - */ - -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { createWasmVmRuntime } from '../helpers.js'; -import { - AF_INET, - COMMANDS_DIR, - C_BUILD_DIR, - createKernel, - describeIf, - hasWasmBinaries, - SOCK_DGRAM, -} from '../helpers.js'; -import type { Kernel } from '../helpers.js'; -import { existsSync } from 'node:fs'; -import { join } from 'node:path'; - -const hasCWasmBinaries = existsSync(join(C_BUILD_DIR, 'udp_echo')); - -function skipReason(): string | false { - if (!hasWasmBinaries) return 'WASM binaries not built (run make wasm in native/wasmvm/)'; - if (!hasCWasmBinaries) return 'udp_echo WASM binary not built (run make -C native/wasmvm/c sysroot && make -C native/wasmvm/c programs)'; - return false; -} - -// Minimal in-memory VFS (same as net-server) -class SimpleVFS { - private files = new Map(); - private dirs = new Set(['/']); - private symlinks = new Map(); - - async readFile(path: string): Promise { - const data = this.files.get(path); - if (!data) throw new Error(`ENOENT: ${path}`); - return data; - } - async readTextFile(path: string): Promise { - return new TextDecoder().decode(await this.readFile(path)); - } - async pread(path: string, offset: number, length: number): Promise { - const data = await this.readFile(path); - return data.slice(offset, offset + length); - } - async readDir(path: string): Promise { - const prefix = path === '/' ? '/' : path + '/'; - const entries: string[] = []; - for (const p of [...this.files.keys(), ...this.dirs]) { - if (p !== path && p.startsWith(prefix)) { - const rest = p.slice(prefix.length); - if (!rest.includes('/')) entries.push(rest); - } - } - return entries; - } - async readDirWithTypes(path: string) { - return (await this.readDir(path)).map((name) => ({ - name, - isDirectory: this.dirs.has(path === '/' ? `/${name}` : `${path}/${name}`), - })); - } - async writeFile(path: string, content: string | Uint8Array): Promise { - const data = typeof content === 'string' ? new TextEncoder().encode(content) : content; - this.files.set(path, new Uint8Array(data)); - const parts = path.split('/').filter(Boolean); - for (let i = 1; i < parts.length; i++) { - this.dirs.add('/' + parts.slice(0, i).join('/')); - } - } - async createDir(path: string) { this.dirs.add(path); } - async mkdir(path: string, _options?: { recursive?: boolean }) { this.dirs.add(path); } - async exists(path: string): Promise { - return this.files.has(path) || this.dirs.has(path) || this.symlinks.has(path); - } - async stat(path: string) { - const isDir = this.dirs.has(path); - const isSymlink = this.symlinks.has(path); - const data = this.files.get(path); - if (!isDir && !isSymlink && !data) throw new Error(`ENOENT: ${path}`); - return { - mode: isSymlink ? 0o120777 : (isDir ? 0o40755 : 0o100644), - size: data?.length ?? 0, - isDirectory: isDir, - isSymbolicLink: isSymlink, - atimeMs: Date.now(), - mtimeMs: Date.now(), - ctimeMs: Date.now(), - birthtimeMs: Date.now(), - ino: 0, - nlink: 1, - uid: 1000, - gid: 1000, - }; - } - async chmod() {} - async rename(from: string, to: string) { - const data = this.files.get(from); - if (data) { this.files.set(to, data); this.files.delete(from); } - } - async unlink(path: string) { this.files.delete(path); this.symlinks.delete(path); } - async rmdir(path: string) { this.dirs.delete(path); } - async symlink(target: string, linkPath: string) { - this.symlinks.set(linkPath, target); - const parts = linkPath.split('/').filter(Boolean); - for (let i = 1; i < parts.length; i++) { - this.dirs.add('/' + parts.slice(0, i).join('/')); - } - } - async readlink(path: string): Promise { - const target = this.symlinks.get(path); - if (!target) throw new Error(`EINVAL: ${path}`); - return target; - } -} - -// Wait for a kernel UDP binding on the given port (poll with timeout) -async function waitForUdpBinding( - kernel: Kernel, - port: number, - timeoutMs = 10_000, -): Promise { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - const bound = kernel.socketTable.findBoundUdp({ host: '0.0.0.0', port }); - if (bound) return; - await new Promise((r) => setTimeout(r, 20)); - } - throw new Error(`Timed out waiting for UDP binding on port ${port}`); -} - -const TEST_PORT = 9877; -const CLIENT_PID = 999; // Fake PID for test-side client sockets - -describeIf(!skipReason(), 'WasmVM UDP integration', { timeout: 30_000 }, () => { - let kernel: Kernel; - let vfs: SimpleVFS; - - beforeEach(async () => { - vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - }); - - afterEach(async () => { - await kernel?.dispose(); - }); - - it('udp_echo: recv datagram and echo it back', async () => { - // Start the WASM UDP echo server (blocks on recvfrom until we send) - const execPromise = kernel.exec(`udp_echo ${TEST_PORT}`); - - // Wait for the server to finish bind - await waitForUdpBinding(kernel, TEST_PORT); - - // Create a client UDP socket and bind to an ephemeral port - const st = kernel.socketTable; - const clientId = st.create(AF_INET, SOCK_DGRAM, 0, CLIENT_PID); - await st.bind(clientId, { host: '127.0.0.1', port: 0 }); - - // Send "hello" to the echo server - const encoder = new TextEncoder(); - st.sendTo(clientId, encoder.encode('hello'), 0, { host: '127.0.0.1', port: TEST_PORT }); - - // Wait for the echo response - const decoder = new TextDecoder(); - let reply = ''; - const recvDeadline = Date.now() + 10_000; - while (Date.now() < recvDeadline) { - const result = st.recvFrom(clientId, 1024); - if (result && result.data.length > 0) { - reply = decoder.decode(result.data); - break; - } - await new Promise((r) => setTimeout(r, 20)); - } - - expect(reply).toBe('hello'); - - // Close client socket - st.close(clientId, CLIENT_PID); - - // Wait for exec to complete (server exits after echoing one datagram) - const result = await execPromise; - - expect(result.stdout).toContain('listening on port 9877'); - expect(result.stdout).toContain('received: hello'); - expect(result.stdout).toContain('echoed: 5'); - expect(result.exitCode).toBe(0); - }); - - it('udp_echo: message boundaries are preserved', async () => { - // Start the WASM UDP echo server - const execPromise = kernel.exec(`udp_echo ${TEST_PORT + 1}`); - - // Wait for the server to finish bind - await waitForUdpBinding(kernel, TEST_PORT + 1); - - // Create a client UDP socket - const st = kernel.socketTable; - const clientId = st.create(AF_INET, SOCK_DGRAM, 0, CLIENT_PID); - await st.bind(clientId, { host: '127.0.0.1', port: 0 }); - - // Send a message — the echo server echoes exactly one datagram - const encoder = new TextEncoder(); - const msg = 'boundary-test-message'; - st.sendTo(clientId, encoder.encode(msg), 0, { host: '127.0.0.1', port: TEST_PORT + 1 }); - - // Receive the echo — it must be the exact message (not fragmented/merged) - const decoder = new TextDecoder(); - let reply = ''; - const recvDeadline = Date.now() + 10_000; - while (Date.now() < recvDeadline) { - const result = st.recvFrom(clientId, 1024); - if (result && result.data.length > 0) { - reply = decoder.decode(result.data); - break; - } - await new Promise((r) => setTimeout(r, 20)); - } - - // Message boundary preserved: exact content, exact length - expect(reply).toBe(msg); - expect(reply.length).toBe(msg.length); - - st.close(clientId, CLIENT_PID); - const result = await execPromise; - expect(result.exitCode).toBe(0); - }); -}); diff --git a/registry/tests/wasmvm/net-unix.test.ts b/registry/tests/wasmvm/net-unix.test.ts deleted file mode 100644 index b4bac046ba..0000000000 --- a/registry/tests/wasmvm/net-unix.test.ts +++ /dev/null @@ -1,197 +0,0 @@ -/** - * Integration test for WasmVM Unix domain sockets. - * - * Spawns the unix_socket C program as WASM (socket(AF_UNIX) → bind → listen → - * accept → recv → send "pong" → close), connects from the kernel as a client, - * and verifies data exchange via in-kernel loopback routing. - */ - -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { createWasmVmRuntime } from '../helpers.js'; -import { - AF_UNIX, - COMMANDS_DIR, - C_BUILD_DIR, - createKernel, - describeIf, - hasWasmBinaries, - SOCK_STREAM, -} from '../helpers.js'; -import type { Kernel } from '../helpers.js'; -import { existsSync } from 'node:fs'; -import { join } from 'node:path'; - -const hasCWasmBinaries = existsSync(join(C_BUILD_DIR, 'unix_socket')); - -function skipReason(): string | false { - if (!hasWasmBinaries) return 'WASM binaries not built (run make wasm in native/wasmvm/)'; - if (!hasCWasmBinaries) return 'unix_socket WASM binary not built (run make -C native/wasmvm/c sysroot && make -C native/wasmvm/c programs)'; - return false; -} - -// Minimal in-memory VFS (same as net-server) -class SimpleVFS { - private files = new Map(); - private dirs = new Set(['/']); - private symlinks = new Map(); - - async readFile(path: string): Promise { - const data = this.files.get(path); - if (!data) throw new Error(`ENOENT: ${path}`); - return data; - } - async readTextFile(path: string): Promise { - return new TextDecoder().decode(await this.readFile(path)); - } - async pread(path: string, offset: number, length: number): Promise { - const data = await this.readFile(path); - return data.slice(offset, offset + length); - } - async readDir(path: string): Promise { - const prefix = path === '/' ? '/' : path + '/'; - const entries: string[] = []; - for (const p of [...this.files.keys(), ...this.dirs]) { - if (p !== path && p.startsWith(prefix)) { - const rest = p.slice(prefix.length); - if (!rest.includes('/')) entries.push(rest); - } - } - return entries; - } - async readDirWithTypes(path: string) { - return (await this.readDir(path)).map((name) => ({ - name, - isDirectory: this.dirs.has(path === '/' ? `/${name}` : `${path}/${name}`), - })); - } - async writeFile(path: string, content: string | Uint8Array): Promise { - const data = typeof content === 'string' ? new TextEncoder().encode(content) : content; - this.files.set(path, new Uint8Array(data)); - const parts = path.split('/').filter(Boolean); - for (let i = 1; i < parts.length; i++) { - this.dirs.add('/' + parts.slice(0, i).join('/')); - } - } - async createDir(path: string) { this.dirs.add(path); } - async mkdir(path: string, _options?: { recursive?: boolean }) { this.dirs.add(path); } - async exists(path: string): Promise { - return this.files.has(path) || this.dirs.has(path) || this.symlinks.has(path); - } - async stat(path: string) { - const isDir = this.dirs.has(path); - const isSymlink = this.symlinks.has(path); - const data = this.files.get(path); - if (!isDir && !isSymlink && !data) throw new Error(`ENOENT: ${path}`); - return { - mode: isSymlink ? 0o120777 : (isDir ? 0o40755 : 0o100644), - size: data?.length ?? 0, - isDirectory: isDir, - isSymbolicLink: isSymlink, - atimeMs: Date.now(), - mtimeMs: Date.now(), - ctimeMs: Date.now(), - birthtimeMs: Date.now(), - ino: 0, - nlink: 1, - uid: 1000, - gid: 1000, - }; - } - async chmod() {} - async rename(from: string, to: string) { - const data = this.files.get(from); - if (data) { this.files.set(to, data); this.files.delete(from); } - } - async unlink(path: string) { this.files.delete(path); this.symlinks.delete(path); } - async rmdir(path: string) { this.dirs.delete(path); } - async symlink(target: string, linkPath: string) { - this.symlinks.set(linkPath, target); - const parts = linkPath.split('/').filter(Boolean); - for (let i = 1; i < parts.length; i++) { - this.dirs.add('/' + parts.slice(0, i).join('/')); - } - } - async readlink(path: string): Promise { - const target = this.symlinks.get(path); - if (!target) throw new Error(`EINVAL: ${path}`); - return target; - } -} - -// Wait for a kernel Unix domain socket listener at the given path (poll with timeout) -async function waitForUnixListener( - kernel: Kernel, - path: string, - timeoutMs = 10_000, -): Promise { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - const listener = kernel.socketTable.findListener({ path }); - if (listener) return; - await new Promise((r) => setTimeout(r, 20)); - } - throw new Error(`Timed out waiting for Unix listener on ${path}`); -} - -const SOCK_PATH = '/tmp/test.sock'; -const CLIENT_PID = 999; // Fake PID for test-side client sockets - -describeIf(!skipReason(), 'WasmVM Unix domain socket integration', { timeout: 30_000 }, () => { - let kernel: Kernel; - let vfs: SimpleVFS; - - beforeEach(async () => { - vfs = new SimpleVFS(); - // Create /tmp so the socket file can be created - await vfs.mkdir('/tmp'); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - }); - - afterEach(async () => { - await kernel?.dispose(); - }); - - it('unix_socket: accept connection, recv data, send pong', async () => { - // Start the WASM Unix socket server (blocks on accept until we connect) - const execPromise = kernel.exec(`unix_socket ${SOCK_PATH}`); - - // Wait for the server to finish bind+listen - await waitForUnixListener(kernel, SOCK_PATH); - - // Create a client socket and connect via loopback - const st = kernel.socketTable; - const clientId = st.create(AF_UNIX, SOCK_STREAM, 0, CLIENT_PID); - await st.connect(clientId, { path: SOCK_PATH }); - - // Send "ping" to the server - const encoder = new TextEncoder(); - st.send(clientId, encoder.encode('ping')); - - // Wait for the server to process and send its reply - const decoder = new TextDecoder(); - let reply = ''; - const recvDeadline = Date.now() + 10_000; - while (Date.now() < recvDeadline) { - const chunk = st.recv(clientId, 256); - if (chunk && chunk.length > 0) { - reply += decoder.decode(chunk); - break; - } - await new Promise((r) => setTimeout(r, 20)); - } - - expect(reply).toBe('pong'); - - // Close client socket - st.close(clientId, CLIENT_PID); - - // Wait for exec to complete (server exits after handling one connection) - const result = await execPromise; - - expect(result.stdout).toContain(`listening on ${SOCK_PATH}`); - expect(result.stdout).toContain('received: ping'); - expect(result.stdout).toContain('sent: 4'); - expect(result.exitCode).toBe(0); - }); -}); diff --git a/registry/tests/wasmvm/shell-redirect.test.ts b/registry/tests/wasmvm/shell-redirect.test.ts deleted file mode 100644 index ccee94a39b..0000000000 --- a/registry/tests/wasmvm/shell-redirect.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { describe, it, expect, afterEach } from "vitest"; -import { - createInMemoryFileSystem, - createKernel, - createWasmVmRuntime, - COMMANDS_DIR, - describeIf, - hasWasmBinaries, - type Kernel, -} from "../helpers.js"; - -describeIf(hasWasmBinaries, "wasmvm shell redirects", () => { - let kernel: Kernel | undefined; - - afterEach(async () => { - await kernel?.dispose(); - kernel = undefined; - }); - - it("creates a redirected file relative to the changed cwd", async () => { - const vfs = createInMemoryFileSystem(); - await (vfs as any).chmod("/", 0o1777); - await vfs.mkdir("/tmp", { recursive: true }); - await (vfs as any).chmod("/tmp", 0o1777); - kernel = createKernel({ filesystem: vfs, syncFilesystemOnDispose: false }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); - - const result = await kernel.exec( - 'sh -c "mkdir -p /tmp/r && cd /tmp/r && echo hi > a.txt && cat a.txt"', - ); - - expect(result.exitCode).toBe(0); - expect(result.stdout).toBe("hi\n"); - expect(await vfs.exists("/tmp/r/a.txt")).toBe(true); - }, 15_000); -}); diff --git a/registry/tests/wasmvm/signal-handler.test.ts b/registry/tests/wasmvm/signal-handler.test.ts deleted file mode 100644 index 9feeed21c8..0000000000 --- a/registry/tests/wasmvm/signal-handler.test.ts +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Integration test for WasmVM cooperative signal handling. - * - * Spawns the signal_handler C program as WASM (sigaction(SIGINT, ...) → - * busy-loop with sleep → verify handler called), delivers SIGINT via - * kernel.kill(), and verifies the handler fires at a syscall boundary. - */ - -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { createWasmVmRuntime } from '../helpers.js'; -import { - COMMANDS_DIR, - C_BUILD_DIR, - createKernel, - describeIf, - hasWasmBinaries, - SIGTERM, -} from '../helpers.js'; -import type { Kernel } from '../helpers.js'; -import { existsSync } from 'node:fs'; -import { join } from 'node:path'; - -const hasCWasmBinaries = existsSync(join(C_BUILD_DIR, 'signal_handler')); -const EXPECTED_SIGACTION_FLAGS = (0x10000000 | 0x80000000) >>> 0; - -function skipReason(): string | false { - if (!hasWasmBinaries) return 'WASM binaries not built (run make wasm in native/wasmvm/)'; - if (!hasCWasmBinaries) return 'signal_handler WASM binary not built (run make -C native/wasmvm/c sysroot && make -C native/wasmvm/c programs)'; - return false; -} - -// Minimal in-memory VFS -class SimpleVFS { - private files = new Map(); - private dirs = new Set(['/']); - private symlinks = new Map(); - - async readFile(path: string): Promise { - const data = this.files.get(path); - if (!data) throw new Error(`ENOENT: ${path}`); - return data; - } - async readTextFile(path: string): Promise { - return new TextDecoder().decode(await this.readFile(path)); - } - async pread(path: string, offset: number, length: number): Promise { - const data = await this.readFile(path); - return data.slice(offset, offset + length); - } - async readDir(path: string): Promise { - const prefix = path === '/' ? '/' : path + '/'; - const entries: string[] = []; - for (const p of [...this.files.keys(), ...this.dirs]) { - if (p !== path && p.startsWith(prefix)) { - const rest = p.slice(prefix.length); - if (!rest.includes('/')) entries.push(rest); - } - } - return entries; - } - async readDirWithTypes(path: string) { - return (await this.readDir(path)).map((name) => ({ - name, - isDirectory: this.dirs.has(path === '/' ? `/${name}` : `${path}/${name}`), - })); - } - async writeFile(path: string, content: string | Uint8Array): Promise { - const data = typeof content === 'string' ? new TextEncoder().encode(content) : content; - this.files.set(path, new Uint8Array(data)); - const parts = path.split('/').filter(Boolean); - for (let i = 1; i < parts.length; i++) { - this.dirs.add('/' + parts.slice(0, i).join('/')); - } - } - async createDir(path: string) { this.dirs.add(path); } - async mkdir(path: string, _options?: { recursive?: boolean }) { this.dirs.add(path); } - async exists(path: string): Promise { - return this.files.has(path) || this.dirs.has(path) || this.symlinks.has(path); - } - async stat(path: string) { - const isDir = this.dirs.has(path); - const isSymlink = this.symlinks.has(path); - const data = this.files.get(path); - if (!isDir && !isSymlink && !data) throw new Error(`ENOENT: ${path}`); - return { - mode: isSymlink ? 0o120777 : (isDir ? 0o40755 : 0o100644), - size: data?.length ?? 0, - isDirectory: isDir, - isSymbolicLink: isSymlink, - atimeMs: Date.now(), - mtimeMs: Date.now(), - ctimeMs: Date.now(), - birthtimeMs: Date.now(), - ino: 0, - nlink: 1, - uid: 1000, - gid: 1000, - }; - } - lstat(path: string) { return this.stat(path); } - async chmod() {} - async rename(from: string, to: string) { - const data = this.files.get(from); - if (data) { this.files.set(to, data); this.files.delete(from); } - } - async unlink(path: string) { this.files.delete(path); this.symlinks.delete(path); } - async rmdir(path: string) { this.dirs.delete(path); } - async symlink(target: string, linkPath: string) { - this.symlinks.set(linkPath, target); - const parts = linkPath.split('/').filter(Boolean); - for (let i = 1; i < parts.length; i++) { - this.dirs.add('/' + parts.slice(0, i).join('/')); - } - } - async readlink(path: string): Promise { - const target = this.symlinks.get(path); - if (!target) throw new Error(`EINVAL: ${path}`); - return target; - } -} - -describeIf(!skipReason(), 'WasmVM signal handler integration', { timeout: 30_000 }, () => { - let kernel: Kernel; - let vfs: SimpleVFS; - - beforeEach(async () => { - vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - }); - - afterEach(async () => { - await kernel?.dispose(); - }); - - it('signal_handler: sigaction registration preserves mask/flags and fires at syscall boundary', async () => { - // Spawn the WASM signal_handler program (registers SIGINT handler, then loops) - let stdout = ''; - const proc = kernel.spawn('signal_handler', [], { - onStdout: (data) => { stdout += new TextDecoder().decode(data); }, - }); - - // Wait for the program to register its handler and start waiting - const deadline = Date.now() + 10_000; - while (Date.now() < deadline && !stdout.includes('waiting')) { - await new Promise((r) => setTimeout(r, 20)); - } - expect(stdout).toContain('handler_registered'); - expect(stdout).toContain('waiting'); - - const registration = kernel.processTable.getSignalState(proc.pid).handlers.get(2); - expect(registration?.mask).toEqual(new Set([SIGTERM])); - expect(registration?.flags).toBe(EXPECTED_SIGACTION_FLAGS); - - // Deliver SIGINT via ManagedProcess.kill() — routes through kernel process table - proc.kill(2 /* SIGINT */); - - // Wait for the program to handle the signal and exit - const exitCode = await proc.wait(); - - expect(stdout).toContain('caught_signal=2'); - expect(exitCode).toBe(0); - }); -}); diff --git a/registry/tests/wasmvm/sqlite3.test.ts b/registry/tests/wasmvm/sqlite3.test.ts deleted file mode 100644 index 2457ce3e1a..0000000000 --- a/registry/tests/wasmvm/sqlite3.test.ts +++ /dev/null @@ -1,332 +0,0 @@ -/** - * Integration tests for sqlite3 C command. - * - * Verifies SQLite CLI operations via kernel.exec() with real WASM binaries: - * - In-memory databases (:memory:) - * - Stdin pipe mode for simple queries - * - SQL from command line arguments for multi-statement operations - * - Meta-commands (.dump, .schema, .tables) - * - * Note: kernel.exec() wraps commands in sh -c. Brush-shell currently returns - * exit code 17 for all child commands. Tests verify stdout correctness. - * - * Multi-statement SQL via stdin is not yet reliable in WASM (fgetc buffering - * issues with the WASI polyfill). Tests use SQL-as-argument for complex cases. - */ - -import { describe, it, expect, afterEach } from 'vitest'; -import { createWasmVmRuntime } from '../helpers.js'; -import { - C_BUILD_DIR, - COMMANDS_DIR, - createKernel, - describeIf, - hasCWasmBinaries, -} from '../helpers.js'; -import type { Kernel } from '../helpers.js'; - -// Minimal in-memory VFS for kernel tests -class SimpleVFS { - private files = new Map(); - private dirs = new Set(['/']); - - async readFile(path: string): Promise { - const data = this.files.get(path); - if (!data) throw new Error(`ENOENT: ${path}`); - return data; - } - async readTextFile(path: string): Promise { - return new TextDecoder().decode(await this.readFile(path)); - } - async readDir(path: string): Promise { - const prefix = path === '/' ? '/' : path + '/'; - const entries: string[] = []; - for (const p of [...this.files.keys(), ...this.dirs]) { - if (p !== path && p.startsWith(prefix)) { - const rest = p.slice(prefix.length); - if (!rest.includes('/')) entries.push(rest); - } - } - return entries; - } - async readDirWithTypes(path: string) { - return (await this.readDir(path)).map(name => ({ - name, - isDirectory: this.dirs.has(path === '/' ? `/${name}` : `${path}/${name}`), - })); - } - async writeFile(path: string, content: string | Uint8Array): Promise { - const data = typeof content === 'string' ? new TextEncoder().encode(content) : content; - this.files.set(path, new Uint8Array(data)); - const parts = path.split('/').filter(Boolean); - for (let i = 1; i < parts.length; i++) { - this.dirs.add('/' + parts.slice(0, i).join('/')); - } - } - async createDir(path: string) { this.dirs.add(path); } - async mkdir(path: string, _options?: { recursive?: boolean }) { - this.dirs.add(path); - const parts = path.split('/').filter(Boolean); - for (let i = 1; i < parts.length; i++) { - this.dirs.add('/' + parts.slice(0, i).join('/')); - } - } - async exists(path: string): Promise { - return this.files.has(path) || this.dirs.has(path); - } - async stat(path: string) { - const isDir = this.dirs.has(path); - const data = this.files.get(path); - if (!isDir && !data) throw new Error(`ENOENT: ${path}`); - return { - mode: isDir ? 0o40755 : 0o100644, - size: data?.length ?? 0, - isDirectory: isDir, - isSymbolicLink: false, - atimeMs: Date.now(), - mtimeMs: Date.now(), - ctimeMs: Date.now(), - birthtimeMs: Date.now(), - ino: 0, - nlink: 1, - uid: 1000, - gid: 1000, - }; - } - async chmod(_path: string, _mode: number) {} - async lstat(path: string) { return this.stat(path); } - async removeFile(path: string) { this.files.delete(path); } - async removeDir(path: string) { this.dirs.delete(path); } - async rename(oldPath: string, newPath: string) { - const data = this.files.get(oldPath); - if (data) { - this.files.set(newPath, data); - this.files.delete(oldPath); - } - } - async pread(path: string, buffer: Uint8Array, offset: number, length: number, position: number): Promise { - const data = this.files.get(path); - if (!data) throw new Error(`ENOENT: ${path}`); - const available = Math.min(length, data.length - position); - if (available <= 0) return 0; - buffer.set(data.subarray(position, position + available), offset); - return available; - } -} - -describeIf(hasCWasmBinaries('sqlite3'), 'sqlite3 command', () => { - let kernel: Kernel; - - afterEach(async () => { - await kernel?.dispose(); - }); - - it('executes SQL from stdin pipe on in-memory database', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - const result = await kernel.exec('sqlite3 :memory:', { - stdin: 'SELECT 1+1 AS result;\n', - }); - expect(result.stdout.trim()).toBe('2'); - }); - - it('executes multi-statement SQL as command argument', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - // Multi-statement SQL passed as command argument (more reliable than stdin in WASM) - const sql = 'CREATE TABLE t(x INTEGER); INSERT INTO t VALUES(10); INSERT INTO t VALUES(20); INSERT INTO t VALUES(30); SELECT * FROM t ORDER BY x;'; - const result = await kernel.exec(`sqlite3 :memory: "${sql}"`); - expect(result.stdout.trim()).toBe('10\n20\n30'); - }); - - it('supports .tables meta-command via SQL setup', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - // Create tables via SQL argument, then query sqlite_master - const sql = "CREATE TABLE alpha(x); CREATE TABLE beta(y); SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1;"; - const result = await kernel.exec(`sqlite3 :memory: "${sql}"`); - const tables = result.stdout.trim().split('\n').sort(); - expect(tables).toEqual(['alpha', 'beta']); - }); - - it('supports .schema via sqlite_master query', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - const sql = "CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT NOT NULL); SELECT sql FROM sqlite_master WHERE name='users';"; - const result = await kernel.exec(`sqlite3 :memory: "${sql}"`); - expect(result.stdout.trim()).toContain('CREATE TABLE users'); - }); - - it('supports .dump style output via SQL', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - const sql = "CREATE TABLE t(x INTEGER, y TEXT); INSERT INTO t VALUES(1,'hello'); SELECT sql FROM sqlite_master; SELECT * FROM t;"; - const result = await kernel.exec(`sqlite3 :memory: "${sql}"`); - const output = result.stdout.trim(); - expect(output).toContain('CREATE TABLE t'); - expect(output).toContain("1|hello"); - }); - - it('handles SELECT with multiple columns', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - const result = await kernel.exec('sqlite3 :memory:', { - stdin: "SELECT 'hello' AS greeting, 42 AS number, 3.14 AS pi;\n", - }); - expect(result.stdout.trim()).toBe('hello|42|3.14'); - }); - - it('handles NULL values in output', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - const result = await kernel.exec('sqlite3 :memory:', { - stdin: 'SELECT NULL;\n', - }); - // SQLite CLI outputs empty string for NULL - expect(result.stdout.trim()).toBe(''); - }); - - it('reports SQL errors on stderr', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - const result = await kernel.exec(`sqlite3 :memory: "SELECT * FROM nonexistent_table;"`); - expect(result.stderr).toContain('no such table'); - }); - - it('defaults to :memory: when no database specified', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - const result = await kernel.exec('sqlite3', { - stdin: 'SELECT 99;\n', - }); - expect(result.stdout.trim()).toBe('99'); - }); - - it('CREATE TABLE, INSERT, SELECT roundtrip via piped SQL', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - // Multi-statement SQL via command arg (stdin multi-statement has fgetc buffering issues in WASM) - const sql = "CREATE TABLE items(id INTEGER PRIMARY KEY, name TEXT); INSERT INTO items VALUES(1,'apple'); INSERT INTO items VALUES(2,'banana'); SELECT id, name FROM items ORDER BY id;"; - const result = await kernel.exec(`sqlite3 :memory: "${sql}"`); - expect(result.stdout.trim()).toBe('1|apple\n2|banana'); - }); - - it('.tables meta-command lists created tables', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - // Multi-statement stdin has fgetc buffering limitations in WASM, - // use SQL command arg to verify table listing behavior - const sql = "CREATE TABLE alpha(x); CREATE TABLE beta(y); SELECT name FROM sqlite_master WHERE type='table' ORDER BY 1;"; - const result = await kernel.exec(`sqlite3 :memory: "${sql}"`); - const tables = result.stdout.trim().split('\n').sort(); - expect(tables).toEqual(['alpha', 'beta']); - }); - - it('.schema meta-command shows CREATE TABLE statements', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - // Query schema via sqlite_master (equivalent to .schema output) - const sql = "CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT NOT NULL); SELECT sql FROM sqlite_master WHERE name='users';"; - const result = await kernel.exec(`sqlite3 :memory: "${sql}"`); - expect(result.stdout).toContain('CREATE TABLE users'); - expect(result.stdout).toContain('id INTEGER PRIMARY KEY'); - }); - - it('.dump meta-command outputs INSERT statements for data', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - // Verify dump-equivalent output: schema + data via SQL queries - const sql = "CREATE TABLE t(x INTEGER, y TEXT); INSERT INTO t VALUES(1,'hello'); INSERT INTO t VALUES(2,'world'); SELECT sql FROM sqlite_master WHERE name='t'; SELECT '---'; SELECT x||','||y FROM t ORDER BY x;"; - const result = await kernel.exec(`sqlite3 :memory: "${sql}"`); - const output = result.stdout; - // Schema is preserved - expect(output).toContain('CREATE TABLE t'); - // Data is preserved and retrievable - expect(output).toContain("1,hello"); - expect(output).toContain("2,world"); - }); - - it('file-based DB persists data across separate exec calls', async () => { - const vfs = new SimpleVFS(); - await vfs.mkdir('/tmp', { recursive: true }); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - // Use shell pipe to create table and insert data, then query back - // Note: file-based DB uses WASI VFS (open/write/fstat/ftruncate) which - // requires full POSIX file I/O support through the kernel - const createSql = "CREATE TABLE t(x INTEGER); INSERT INTO t VALUES(42); INSERT INTO t VALUES(99);"; - const createResult = await kernel.exec(`sqlite3 /tmp/test.db "${createSql}"`); - - // Check if file-based DB is supported (fstat/ftruncate may not be available) - const hasError = createResult.stderr.includes('disk I/O error') || - createResult.stderr.includes('unable to open database'); - if (hasError) { - // Fall back: verify file-based behavior via VFS write/read simulation - // Write a pre-populated DB, then verify sqlite3 can read from VFS-provided data - // For now, verify in-memory DB persistence within single exec - const result = await kernel.exec( - 'sqlite3 :memory: "CREATE TABLE t(x INTEGER); INSERT INTO t VALUES(42); INSERT INTO t VALUES(99); SELECT * FROM t ORDER BY x;"' - ); - expect(result.stdout.trim()).toBe('42\n99'); - return; - } - - // Verify file was created in VFS - const dbData = await vfs.readFile('/tmp/test.db'); - expect(dbData.length).toBeGreaterThan(0); - - // Second exec: reopen and query persisted data via stdin - const result = await kernel.exec('sqlite3 /tmp/test.db', { - stdin: 'SELECT * FROM t ORDER BY x;\n', - }); - expect(result.stdout.trim()).toBe('42\n99'); - }); - - it('multi-statement input separated by semicolons', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - // Multi-statement SQL via command arg (semicolons separate statements) - const sql = "CREATE TABLE nums(v); INSERT INTO nums VALUES(10); INSERT INTO nums VALUES(20); SELECT v FROM nums ORDER BY v;"; - const result = await kernel.exec(`sqlite3 :memory: "${sql}"`); - expect(result.stdout.trim()).toBe('10\n20'); - }); - - it('SQL syntax error produces error on stderr with non-zero exit', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - // Syntax error via command arg (reliable error output) - const result = await kernel.exec('sqlite3 :memory: "SELEC INVALID SYNTAX;"'); - expect(result.stderr).toContain('Error'); - }); -}); diff --git a/registry/tests/wasmvm/terminal-harness.ts b/registry/tests/wasmvm/terminal-harness.ts deleted file mode 100644 index d5d80772f0..0000000000 --- a/registry/tests/wasmvm/terminal-harness.ts +++ /dev/null @@ -1,172 +0,0 @@ -/** - * TerminalHarness — wires openShell() to a headless xterm Terminal for - * deterministic screen-state assertions in tests. - * - * Duplicated from packages/kernel/test/terminal-harness.ts because cross-package - * test imports aren't supported. - */ - -import { Terminal } from "@xterm/headless"; -import type { Kernel } from "../helpers.js"; - -type ShellHandle = ReturnType; - -/** Settlement window: resolve type() after this many ms of no new output. */ -const SETTLE_MS = 50; -/** Poll interval for waitFor(). */ -const POLL_MS = 20; -/** Default waitFor() timeout. */ -const DEFAULT_WAIT_TIMEOUT_MS = 10_000; - -export class TerminalHarness { - readonly term: Terminal; - readonly shell: ShellHandle; - private typing = false; - private disposed = false; - - constructor(kernel: Kernel, options?: { cols?: number; rows?: number; env?: Record; cwd?: string }) { - const cols = options?.cols ?? 80; - const rows = options?.rows ?? 24; - - this.term = new Terminal({ cols, rows, allowProposedApi: true }); - - this.shell = kernel.openShell({ cols, rows, env: options?.env, cwd: options?.cwd }); - - // Wire shell output → xterm - this.shell.onData = (data: Uint8Array) => { - this.term.write(data); - }; - } - - /** - * Send input through the PTY. Resolves after output settles (no new bytes - * received for SETTLE_MS). - */ - async type(input: string): Promise { - if (this.typing) { - throw new Error("TerminalHarness.type() called while previous type() is still in-flight"); - } - this.typing = true; - try { - await this.typeInternal(input); - } finally { - this.typing = false; - } - } - - private typeInternal(input: string): Promise { - return new Promise((resolve) => { - let timer: ReturnType | null = null; - - const resetTimer = () => { - if (timer !== null) clearTimeout(timer); - timer = setTimeout(() => { - this.shell.onData = originalOnData; - resolve(); - }, SETTLE_MS); - }; - - const originalOnData = this.shell.onData; - this.shell.onData = (data: Uint8Array) => { - this.term.write(data); - resetTimer(); - }; - - resetTimer(); - this.shell.write(input); - }); - } - - /** - * Full screen as a string: viewport rows only (not scrollback), trailing - * whitespace trimmed per line, trailing empty lines dropped, joined with '\n'. - */ - screenshotTrimmed(): string { - const buf = this.term.buffer.active; - const rows = this.term.rows; - const lines: string[] = []; - - for (let y = 0; y < rows; y++) { - const line = buf.getLine(buf.viewportY + y); - lines.push(line ? line.translateToString(true) : ""); - } - - while (lines.length > 0 && lines[lines.length - 1] === "") { - lines.pop(); - } - - return lines.join("\n"); - } - - /** - * Single trimmed row from the screen buffer (0-indexed from viewport top). - */ - line(row: number): string { - const buf = this.term.buffer.active; - const line = buf.getLine(buf.viewportY + row); - return line ? line.translateToString(true) : ""; - } - - /** - * Poll screen buffer every POLL_MS until `text` is found. Throws a - * descriptive error on timeout. - */ - async waitFor( - text: string, - occurrence: number = 1, - timeoutMs: number = DEFAULT_WAIT_TIMEOUT_MS, - ): Promise { - const deadline = Date.now() + timeoutMs; - - while (true) { - const screen = this.screenshotTrimmed(); - - let count = 0; - let idx = -1; - while (true) { - idx = screen.indexOf(text, idx + 1); - if (idx === -1) break; - count++; - if (count >= occurrence) return; - } - - if (Date.now() >= deadline) { - throw new Error( - `waitFor("${text}", ${occurrence}) timed out after ${timeoutMs}ms.\n` + - `Expected: "${text}" (occurrence ${occurrence})\n` + - `Screen:\n${screen}`, - ); - } - - await new Promise((r) => setTimeout(r, POLL_MS)); - } - } - - /** - * Send ^D on empty line and await shell exit. Returns exit code. - */ - async exit(): Promise { - this.shell.write("\x04"); - return this.shell.wait(); - } - - /** - * Kill shell and dispose terminal. Safe to call multiple times. - */ - async dispose(): Promise { - if (this.disposed) return; - this.disposed = true; - - try { - this.shell.kill(); - await Promise.race([ - this.shell.wait(), - new Promise((r) => setTimeout(r, 500)), - ]); - } catch { - // Shell may already be dead - } - - this.term.dispose(); - } -} diff --git a/registry/tests/wasmvm/wget.test.ts b/registry/tests/wasmvm/wget.test.ts deleted file mode 100644 index e1121f6b36..0000000000 --- a/registry/tests/wasmvm/wget.test.ts +++ /dev/null @@ -1,239 +0,0 @@ -/** - * Integration tests for wget C command (libcurl-based). - * - * Verifies HTTP download operations via kernel.exec() with real WASM binaries: - * - Basic GET download to file - * - Download to specified file (-O) - * - Quiet mode (-q) - * - Error handling for 404 URLs - * - Follow redirects (default behavior) - * - * Tests start a local HTTP server in beforeAll and make wget requests against it. - */ - -import { describe, it, expect, afterEach, beforeAll, afterAll } from 'vitest'; -import { createWasmVmRuntime } from '../helpers.js'; -import { C_BUILD_DIR, COMMANDS_DIR, createKernel, describeIf, hasCWasmBinaries } from '../helpers.js'; -import type { Kernel } from '../helpers.js'; -import { createServer, type Server, type IncomingMessage, type ServerResponse } from 'node:http'; - -// Minimal in-memory VFS for kernel tests -class SimpleVFS { - private files = new Map(); - private dirs = new Set(['/']); - - async readFile(path: string): Promise { - const data = this.files.get(path); - if (!data) throw new Error(`ENOENT: ${path}`); - return data; - } - async readTextFile(path: string): Promise { - return new TextDecoder().decode(await this.readFile(path)); - } - async readDir(path: string): Promise { - const prefix = path === '/' ? '/' : path + '/'; - const entries: string[] = []; - for (const p of [...this.files.keys(), ...this.dirs]) { - if (p !== path && p.startsWith(prefix)) { - const rest = p.slice(prefix.length); - if (!rest.includes('/')) entries.push(rest); - } - } - return entries; - } - async readDirWithTypes(path: string) { - return (await this.readDir(path)).map(name => ({ - name, - isDirectory: this.dirs.has(path === '/' ? `/${name}` : `${path}/${name}`), - })); - } - async writeFile(path: string, content: string | Uint8Array): Promise { - const data = typeof content === 'string' ? new TextEncoder().encode(content) : content; - this.files.set(path, new Uint8Array(data)); - const parts = path.split('/').filter(Boolean); - for (let i = 1; i < parts.length; i++) { - this.dirs.add('/' + parts.slice(0, i).join('/')); - } - } - async createDir(path: string) { this.dirs.add(path); } - async mkdir(path: string, _options?: { recursive?: boolean }) { - this.dirs.add(path); - const parts = path.split('/').filter(Boolean); - for (let i = 1; i < parts.length; i++) { - this.dirs.add('/' + parts.slice(0, i).join('/')); - } - } - async exists(path: string): Promise { - return this.files.has(path) || this.dirs.has(path); - } - async stat(path: string) { - const isDir = this.dirs.has(path); - const data = this.files.get(path); - if (!isDir && !data) throw new Error(`ENOENT: ${path}`); - return { - mode: isDir ? 0o40755 : 0o100644, - size: data?.length ?? 0, - isDirectory: isDir, - isSymbolicLink: false, - atimeMs: Date.now(), - mtimeMs: Date.now(), - ctimeMs: Date.now(), - birthtimeMs: Date.now(), - ino: 0, - nlink: 1, - uid: 1000, - gid: 1000, - }; - } - async chmod(_path: string, _mode: number) {} - async lstat(path: string) { return this.stat(path); } - async removeFile(path: string) { this.files.delete(path); } - async removeDir(path: string) { this.dirs.delete(path); } - async rename(oldPath: string, newPath: string) { - const data = this.files.get(oldPath); - if (data) { - this.files.set(newPath, data); - this.files.delete(oldPath); - } - } - async pread(path: string, buffer: Uint8Array, offset: number, length: number, position: number): Promise { - const data = this.files.get(path); - if (!data) throw new Error(`ENOENT: ${path}`); - const available = Math.min(length, data.length - position); - if (available <= 0) return 0; - buffer.set(data.subarray(position, position + available), offset); - return available; - } - - has(path: string): boolean { - return this.files.has(path); - } - getContent(path: string): string | undefined { - const data = this.files.get(path); - return data ? new TextDecoder().decode(data) : undefined; - } - getRawContent(path: string): Uint8Array | undefined { - return this.files.get(path); - } -} - -// TODO(P6): requires wget WASM artifact, intentionally excluded from the fast registry-build gate. -describe.skip('wget command', () => { - let kernel: Kernel; - let server: Server; - let port: number; - - beforeAll(async () => { - server = createServer((req: IncomingMessage, res: ServerResponse) => { - const url = req.url ?? '/'; - - if (url === '/file.txt') { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('downloaded content'); - return; - } - - if (url === '/data.json') { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ status: 'ok' })); - return; - } - - if (url === '/redirect') { - const addr = server.address() as import('node:net').AddressInfo; - res.writeHead(302, { 'Location': `http://127.0.0.1:${addr.port}/redirected` }); - res.end(); - return; - } - - if (url === '/redirected') { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('arrived after redirect'); - return; - } - - if (url === '/binary') { - const buf = Buffer.alloc(1024); - for (let i = 0; i < buf.length; i++) buf[i] = i & 0xff; - res.writeHead(200, { - 'Content-Type': 'application/octet-stream', - 'Content-Length': String(buf.length), - }); - res.end(buf); - return; - } - - res.writeHead(404, { 'Content-Type': 'text/plain' }); - res.end('not found'); - }); - - await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); - port = (server.address() as import('node:net').AddressInfo).port; - }); - - afterAll(async () => { - await new Promise((resolve) => server.close(() => resolve())); - }); - - afterEach(async () => { - await kernel?.dispose(); - }); - - it('downloads file to VFS using URL basename', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - await kernel.exec(`wget http://127.0.0.1:${port}/file.txt`); - - const content = vfs.getContent('/file.txt'); - expect(content).toBe('downloaded content'); - }); - - it('-O saves to specified filename', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - await kernel.exec(`wget -O /output.txt http://127.0.0.1:${port}/data.json`); - - const content = vfs.getContent('/output.txt'); - expect(content).toBeDefined(); - expect(content).toContain('"status":"ok"'); - }); - - it('-q suppresses progress output', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - const result = await kernel.exec(`wget -q -O /output.txt http://127.0.0.1:${port}/file.txt`); - - // Quiet mode should produce no stderr - expect(result.stderr).toBe(''); - // File should still be downloaded - expect(vfs.getContent('/output.txt')).toBe('downloaded content'); - }); - - it('returns non-zero exit code for 404 URL', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - const result = await kernel.exec(`wget http://127.0.0.1:${port}/nonexistent`); - - // Should report error on stderr - expect(result.stderr).toMatch(/wget|404|error|server/i); - }); - - it('follows redirects by default', async () => { - const vfs = new SimpleVFS(); - kernel = createKernel({ filesystem: vfs as any }); - await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); - - await kernel.exec(`wget -O /output.txt http://127.0.0.1:${port}/redirect`); - - const content = vfs.getContent('/output.txt'); - expect(content).toBe('arrived after redirect'); - }); -}); diff --git a/registry/tests/wasmvm/zip-unzip.test.ts b/registry/tests/wasmvm/zip-unzip.test.ts deleted file mode 100644 index db330c1e59..0000000000 --- a/registry/tests/wasmvm/zip-unzip.test.ts +++ /dev/null @@ -1,249 +0,0 @@ -/** - * Integration tests for zip and unzip C commands. - * - * Verifies zip/unzip roundtrip, recursive compression, list mode, - * and extract-to-directory via kernel.exec() with real WASM binaries. - */ - -import { describe, it, expect, afterEach } from 'vitest'; -import { createInMemoryFileSystem, createWasmVmRuntime } from '../helpers.js'; -import { C_BUILD_DIR, COMMANDS_DIR, createKernel } from '../helpers.js'; -import type { Kernel } from '../helpers.js'; - -interface HostileEntry { - name: string; - method: number; // 0 = store, 8 = deflate - compressedSize: number; - uncompressedSize: number; - localOffset: number; -} - -/** Builds a ZIP whose EOCD cd-size field is corrupt so minizip rejects it and - * unzip's raw central-directory fallback parser is exercised. The nonzero - * version fields on each central directory record also make minizip reject - * the archive under the VM's stream semantics, where its reopen-based seek - * callback reads EOCD fields from offset 0 instead of the EOCD record. - * `prefix` bytes (e.g. a real local file header) are placed at offset 0. */ -function buildFallbackArchive(prefix: Uint8Array, entries: HostileEntry[]): Uint8Array { - const enc = new TextEncoder(); - const cdParts: Uint8Array[] = []; - for (const e of entries) { - const nameBytes = enc.encode(e.name); - const cd = new Uint8Array(46 + nameBytes.length); - const dv = new DataView(cd.buffer); - dv.setUint32(0, 0x02014b50, true); // central directory signature - dv.setUint16(4, 20, true); // version made by - dv.setUint16(6, 20, true); // version needed to extract - dv.setUint16(10, e.method, true); - dv.setUint32(20, e.compressedSize, true); - dv.setUint32(24, e.uncompressedSize, true); - dv.setUint16(28, nameBytes.length, true); - dv.setUint32(42, e.localOffset, true); - cd.set(nameBytes, 46); - cdParts.push(cd); - } - const cdOffset = prefix.length; - const cdLen = cdParts.reduce((n, p) => n + p.length, 0); - const eocd = new Uint8Array(22); - const dv = new DataView(eocd.buffer); - dv.setUint32(0, 0x06054b50, true); // EOCD signature - dv.setUint16(8, entries.length, true); // entries on this disk - dv.setUint16(10, entries.length, true);// total entries - dv.setUint32(12, 0xffffffff, true); // corrupt cd size: forces the fallback parser - dv.setUint32(16, cdOffset, true); - const out = new Uint8Array(prefix.length + cdLen + 22); - out.set(prefix, 0); - let off = cdOffset; - for (const p of cdParts) { out.set(p, off); off += p.length; } - out.set(eocd, off); - return out; -} - -describe('zip/unzip commands', () => { - let kernel: Kernel; - - afterEach(async () => { - await kernel?.dispose(); - }); - - it('zip creates valid archive, unzip extracts it, contents match', async () => { - const vfs = createInMemoryFileSystem(); - await vfs.writeFile('/hello.txt', 'Hello, World!\n'); - - kernel = createKernel({ filesystem: vfs }); - await kernel.mount( - createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), - ); - - // Create zip archive - const zipResult = await kernel.exec('zip /archive.zip /hello.txt'); - expect(zipResult.exitCode, zipResult.stderr).toBe(0); - - // Verify archive was created - expect(await vfs.exists('/archive.zip')).toBe(true); - - // Extract to a different directory - const unzipResult = await kernel.exec('unzip -d /extracted /archive.zip'); - expect(unzipResult.exitCode, unzipResult.stderr).toBe(0); - - // Verify extracted content matches original - const extracted = await vfs.readTextFile('/extracted/hello.txt'); - expect(extracted).toBe('Hello, World!\n'); - }); - - it('zip -r compresses directory recursively', async () => { - const vfs = createInMemoryFileSystem(); - await vfs.mkdir('/mydir'); - await vfs.writeFile('/mydir/a.txt', 'file a\n'); - await vfs.writeFile('/mydir/b.txt', 'file b\n'); - - kernel = createKernel({ filesystem: vfs }); - await kernel.mount( - createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), - ); - - const zipResult = await kernel.exec('zip -r /dir.zip /mydir'); - expect(zipResult.exitCode, zipResult.stderr).toBe(0); - expect(await vfs.exists('/dir.zip')).toBe(true); - - // Extract and verify - const unzipResult = await kernel.exec('unzip -d /out /dir.zip'); - expect(unzipResult.exitCode, unzipResult.stderr).toBe(0); - - const a = await vfs.readTextFile('/out/mydir/a.txt'); - const b = await vfs.readTextFile('/out/mydir/b.txt'); - expect(a).toBe('file a\n'); - expect(b).toBe('file b\n'); - }); - - it('unzip -l lists archive contents with sizes', async () => { - const vfs = createInMemoryFileSystem(); - await vfs.writeFile('/data.txt', 'some data content\n'); - - kernel = createKernel({ filesystem: vfs }); - await kernel.mount( - createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), - ); - - // Create archive first - const zipResult = await kernel.exec('zip /list-test.zip /data.txt'); - expect(zipResult.exitCode, zipResult.stderr).toBe(0); - - // List contents - const listResult = await kernel.exec('unzip -l /list-test.zip'); - expect(listResult.exitCode, listResult.stderr).toBe(0); - expect(listResult.stdout).toContain('data.txt'); - // Should show the file size (18 bytes) - expect(listResult.stdout).toContain('18'); - // Should show summary line with file count - expect(listResult.stdout).toMatch(/1 file/); - }); - - it('zip/unzip roundtrip preserves file contents exactly', async () => { - const vfs = createInMemoryFileSystem(); - // Binary-like content with various byte values - const content = new Uint8Array(256); - for (let i = 0; i < 256; i++) content[i] = i; - await vfs.writeFile('/binary.bin', content); - - kernel = createKernel({ filesystem: vfs }); - await kernel.mount( - createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), - ); - - const zipResult = await kernel.exec('zip /roundtrip.zip /binary.bin'); - expect(zipResult.exitCode, zipResult.stderr).toBe(0); - - const unzipResult = await kernel.exec('unzip -d /rt-out /roundtrip.zip'); - expect(unzipResult.exitCode, unzipResult.stderr).toBe(0); - - const extracted = await vfs.readFile('/rt-out/binary.bin'); - expect(extracted.length).toBe(256); - for (let i = 0; i < 256; i++) { - expect(extracted[i]).toBe(i); - } - }); - - it('unzip -d extracts to specified directory', async () => { - const vfs = createInMemoryFileSystem(); - await vfs.writeFile('/src.txt', 'target content\n'); - - kernel = createKernel({ filesystem: vfs }); - await kernel.mount( - createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), - ); - - const zipResult = await kernel.exec('zip /dest-test.zip /src.txt'); - expect(zipResult.exitCode, zipResult.stderr).toBe(0); - - // Extract to a new directory - const unzipResult = await kernel.exec('unzip -d /custom-dir /dest-test.zip'); - expect(unzipResult.exitCode, unzipResult.stderr).toBe(0); - - expect(await vfs.exists('/custom-dir/src.txt')).toBe(true); - const extracted = await vfs.readTextFile('/custom-dir/src.txt'); - expect(extracted).toBe('target content\n'); - }); - - it('fallback parser rejects an entry with a wrapping local offset', async () => { - const vfs = createInMemoryFileSystem(); - const bytes = buildFallbackArchive(new Uint8Array(0), [ - { name: 'evil.txt', method: 0, compressedSize: 4, uncompressedSize: 4, localOffset: 0xfffffff0 }, - ]); - await vfs.writeFile('/evil.zip', bytes); - - kernel = createKernel({ filesystem: vfs }); - await kernel.mount( - createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), - ); - - const result = await kernel.exec('unzip -d /out /evil.zip'); - expect(result.exitCode, result.stderr).toBe(1); - expect(result.stderr).toMatch(/error/); - expect(await vfs.exists('/out/evil.txt')).toBe(false); - }); - - it('fallback parser skips an entry whose normalized name is empty', async () => { - const vfs = createInMemoryFileSystem(); - const bytes = buildFallbackArchive(new Uint8Array(0), [ - { name: '/', method: 0, compressedSize: 0, uncompressedSize: 0, localOffset: 0 }, - ]); - await vfs.writeFile('/empty-name.zip', bytes); - - kernel = createKernel({ filesystem: vfs }); - await kernel.mount( - createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), - ); - - const result = await kernel.exec('unzip /empty-name.zip'); - expect(result.exitCode, result.stderr).toBe(0); - expect(result.stdout).not.toMatch(/error/); - expect(result.stderr).not.toMatch(/error/); - }); - - it('fallback parser caps hostile uncompressed sizes before allocating', async () => { - const vfs = createInMemoryFileSystem(); - // A real 31-byte local header for a 1-byte stored payload. - const prefix = new Uint8Array(31); - const pdv = new DataView(prefix.buffer); - pdv.setUint32(0, 0x04034b50, true); // local file header signature - pdv.setUint16(4, 20, true); // version needed to extract - pdv.setUint16(26, 0, true); // name length - pdv.setUint16(28, 0, true); // extra length - prefix[30] = 0x41; // one payload byte - const bytes = buildFallbackArchive(prefix, [ - { name: 'big.bin', method: 0, compressedSize: 1, uncompressedSize: 0xffffffff, localOffset: 0 }, - ]); - await vfs.writeFile('/big.zip', bytes); - - kernel = createKernel({ filesystem: vfs }); - await kernel.mount( - createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), - ); - - const result = await kernel.exec('unzip -d /cap-out /big.zip'); - expect(result.exitCode, result.stderr).toBe(1); - expect(result.stderr).toMatch(/too large/); - expect(await vfs.exists('/cap-out/big.bin')).toBe(false); - }); -}); diff --git a/scripts/benchmarks/session.bench.ts b/scripts/benchmarks/session.bench.ts index e93a600d16..13b80b8562 100644 --- a/scripts/benchmarks/session.bench.ts +++ b/scripts/benchmarks/session.bench.ts @@ -299,7 +299,7 @@ async function loadPiSoftware(): Promise { // non-snapshot fallback path and hides the optimization entirely. const local = join( import.meta.dirname, - "../../../secure-exec/registry/agent/pi/dist/index.js", + "../../../secure-exec/software/pi/dist/index.js", ); if (existsSync(local)) return (await import(local)).default; // Fallback: the published/installed software package. Variable specifier so @@ -309,7 +309,7 @@ async function loadPiSoftware(): Promise { return (await import(piPkg)).default; } catch { throw new Error( - "Could not resolve the pi software package (../secure-exec/registry/agent/pi/dist or @agentos-software/pi). Build it first.", + "Could not resolve the pi software package (../secure-exec/software/pi/dist or @agentos-software/pi). Build it first.", ); } } @@ -323,7 +323,7 @@ function findPiSdkRoot(): string | null { createRequire( join( import.meta.dirname, - "../../../secure-exec/registry/agent/pi/package.json", + "../../../secure-exec/software/pi/package.json", ), ), ]; @@ -373,7 +373,7 @@ function resolvePiSdkRootOrThrow(): string { * The bare-node session-creation script, run in a FRESH node process per sample * so each pays the full cold SDK load (the VM lane reloads the SDK in a fresh V8 * isolate every session, so this is the apples-to-apples "Node.js equivalent"). - * Mirrors secure-exec registry/agent/pi/src/adapter.ts `newSession`, with no VM. It times the + * Mirrors secure-exec software/pi/src/adapter.ts `newSession`, with no VM. It times the * SDK load + session construction internally and prints `__MS__=`. */ function bareNodeScript(root: string): string { diff --git a/scripts/check-layout.mjs b/scripts/check-layout.mjs new file mode 100644 index 0000000000..971600ce27 --- /dev/null +++ b/scripts/check-layout.mjs @@ -0,0 +1,126 @@ +import { execFileSync } from "node:child_process"; +import { + existsSync, + readdirSync, + readFileSync, + statSync, +} from "node:fs"; +import { join, relative, sep } from "node:path"; + +const root = process.cwd(); +const failures = []; + +const fail = (message) => failures.push(message); +const rel = (path) => relative(root, path).split(sep).join("/"); +const readJson = (path) => JSON.parse(readFileSync(path, "utf8")); + +if (existsSync(join(root, "registry"))) { + fail("registry/ must not exist; use software/ and toolchain/ instead"); +} + +const ignoredDirs = new Set([ + ".git", + "node_modules", + "target", + "dist", + "build", + "vendor", + ".turbo", +]); + +const walk = (dir, visit) => { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.isDirectory() && ignoredDirs.has(entry.name)) continue; + const path = join(dir, entry.name); + if (entry.isDirectory()) { + walk(path, visit); + } else { + visit(path); + } + } +}; + +const allowedTestHomes = [ + /^software\/[^/]+\/test\/.+\.test\.ts$/, + /^toolchain\/conformance\/.+\.test\.ts$/, + /^packages\/[^/]+\/tests\/.+\.test\.ts$/, + /^scripts\/.+\.test\.ts$/, +]; + +walk(root, (path) => { + const normalized = rel(path); + if (!normalized.endsWith(".test.ts")) return; + if (!allowedTestHomes.some((pattern) => pattern.test(normalized))) { + fail(`${normalized} is not in an allowed test home`); + } +}); + +const softwareRoot = join(root, "software"); +if (existsSync(softwareRoot)) { + for (const dir of readdirSync(softwareRoot, { withFileTypes: true })) { + if (!dir.isDirectory()) continue; + const packageDir = join(softwareRoot, dir.name); + const manifestPath = join(packageDir, "agentos-package.json"); + if (!existsSync(manifestPath)) continue; + const manifest = readJson(manifestPath); + const commands = manifest.commands ?? []; + if (commands.length > 0 && !existsSync(join(packageDir, "test"))) { + fail(`software/${dir.name} declares commands but has no test/ directory`); + } + if (manifest.registry && !manifest.registry.category) { + fail(`software/${dir.name} registry block is missing category`); + } + } +} + +const colocatedCargoTomls = []; +const collectCargoTomls = (dir) => { + if (!existsSync(dir)) return; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const path = join(dir, entry.name); + if (entry.isDirectory()) { + collectCargoTomls(path); + } else if (entry.name === "Cargo.toml") { + colocatedCargoTomls.push(path); + } + } +}; +collectCargoTomls(join(root, "software")); + +for (const cargoToml of colocatedCargoTomls) { + const normalized = rel(cargoToml); + if (!/\/native\/crates\/[^/]+\/Cargo\.toml$/.test(normalized)) continue; + const text = readFileSync(cargoToml, "utf8"); + if (!/^\[package\]\nworkspace = "\.\.\/\.\.\/\.\.\/\.\.\/\.\.\/toolchain"/m.test(text)) { + fail(`${normalized} must explicitly belong to the toolchain workspace`); + } +} + +try { + const metadata = JSON.parse( + execFileSync("cargo", ["metadata", "--format-version", "1", "--no-deps"], { + cwd: root, + encoding: "utf8", + stdio: ["ignore", "pipe", "pipe"], + }), + ); + for (const pkg of metadata.packages ?? []) { + const manifestPath = pkg.manifest_path.split(sep).join("/"); + if (manifestPath.includes("/software/")) { + fail(`main Cargo workspace claims colocated crate ${manifestPath}`); + } + } +} catch (error) { + fail(`cargo metadata failed for root workspace: ${error.message}`); +} + +if (failures.length > 0) { + for (const failure of failures) { + console.error(`check-layout: ${failure}`); + } + process.exit(1); +} + +console.log( + `check-layout: OK (${colocatedCargoTomls.length} colocated Cargo.toml files checked)`, +); diff --git a/scripts/ci.sh b/scripts/ci.sh index ffa6b8fbf0..e65118086e 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -33,6 +33,7 @@ run_step node --test scripts/check-rust-package-metadata.test.mjs 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 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 diff --git a/scripts/generate-secure-exec-mirror.mjs b/scripts/generate-secure-exec-mirror.mjs index 2b568fa85b..2d6bc759b4 100644 --- a/scripts/generate-secure-exec-mirror.mjs +++ b/scripts/generate-secure-exec-mirror.mjs @@ -168,6 +168,22 @@ function writeRoot(root, npmShims, rustShims) { join(root, "pnpm-workspace.yaml"), "packages:\n - packages/*\n\nonlyBuiltDependencies: []\n", ); + write( + join(root, "turbo.json"), + json({ + $schema: "https://turbo.build/schema.json", + tasks: { + build: { + dependsOn: ["^build"], + outputs: ["dist/**"], + }, + "check-types": { + dependsOn: ["^check-types"], + outputs: [], + }, + }, + }), + ); write( join(root, "tsconfig.base.json"), json({ diff --git a/scripts/publish/src/lib/packages.ts b/scripts/publish/src/lib/packages.ts index 5969746863..6860e23f96 100644 --- a/scripts/publish/src/lib/packages.ts +++ b/scripts/publish/src/lib/packages.ts @@ -161,7 +161,7 @@ export function discoverPackages( } } - // 2. pnpm workspace packages. Skip the registry/software/* WASM command + // 2. pnpm workspace packages. Skip the software/* WASM command // packages. They are built and shipped separately, never published to npm // from this flow. const pnpmList = execSync("pnpm -r list --json --depth -1", { @@ -183,11 +183,11 @@ export function discoverPackages( ) { continue; } - // registry/software packages version independently and publish from + // software packages version independently and publish from // local via the toolchain — EXCEPT `common`, which core hard-depends on // and therefore ships on the main release track in lockstep. if ( - p.path.includes("/registry/software/") && + p.path.includes("/software/") && p.name !== "@agentos-software/common" ) { continue; diff --git a/scripts/secure-exec-agentos-map.json b/scripts/secure-exec-agentos-map.json index 5af919bb85..f257f17672 100644 --- a/scripts/secure-exec-agentos-map.json +++ b/scripts/secure-exec-agentos-map.json @@ -247,7 +247,7 @@ { "packageGlob": "@agentos-software/*", "sourcePathGlob": "secure-exec/registry/{agent,software}/*", - "targetPathGlob": "agentos/registry/{agent,software}/*" + "targetPathGlob": "agentos/software/*" } ], "scriptedReplacementOrder": [ diff --git a/scripts/verify-check-types.mjs b/scripts/verify-check-types.mjs index a2b7b4fdf6..69b85d4cab 100644 --- a/scripts/verify-check-types.mjs +++ b/scripts/verify-check-types.mjs @@ -27,7 +27,7 @@ const found = execSync( '-not -path "*/.cache/*"', '-not -path "*/.turbo/*"', '-not -path "*/vendor/*"', - '-not -path "./registry/tests/projects/*"', + '-not -path "./packages/runtime-core/tests/integration/projects/*"', '-not -path "./crates/execution/assets/undici-shims/*"', ].join(" "), { encoding: "utf8", cwd: root }, diff --git a/scripts/verify-fixed-versions.mjs b/scripts/verify-fixed-versions.mjs index c6bc103fff..0e0933777e 100644 --- a/scripts/verify-fixed-versions.mjs +++ b/scripts/verify-fixed-versions.mjs @@ -35,7 +35,6 @@ function isExcluded(relPath) { if (relPath === "node_modules" || relPath.startsWith("node_modules/")) return true; if (relPath === ".claude" || relPath.startsWith(".claude/")) return true; if (relPath === "scripts/publish" || relPath.startsWith("scripts/publish/")) return true; - if (relPath === "registry/tests" || relPath.startsWith("registry/tests/")) return true; return relPath .split("/") .some((part) => part === "fixtures" || part === "vendor" || part === "tests"); diff --git a/software/CONTRIBUTING.md b/software/CONTRIBUTING.md new file mode 100644 index 0000000000..ac86bfa6c5 --- /dev/null +++ b/software/CONTRIBUTING.md @@ -0,0 +1,94 @@ +# Contributing a Registry Package + +Software and agent packages for agentOS VMs, published under the +`@agentos-software/*` npm scope. This is the quick path to adding one; the +full documentation lives on the website: + +- [Software Definition](https://agentos-sdk.dev/docs/custom-software/definition) — package anatomy and manifest fields +- [Building Binaries](https://agentos-sdk.dev/docs/custom-software/building-wasm) — compiling commands to WASM +- [Publishing Packages](https://agentos-sdk.dev/docs/custom-software/publishing) — shipping to npm with the toolchain + +## File structure + +``` +software// WASM command packages and JavaScript agent adapters +toolchain/ shared build infrastructure for command binaries +``` + +Each package contains: + +``` +software// +├── package.json name, per-package semver, build script +├── agentos-package.json runtime manifest (commands/aliases/provides) + +│ `registry` block (title/description/priority/image) +│ that lists the package on agentos-sdk.dev/registry +├── src/index.ts descriptor export consumed by `software: []` +├── bin/ staged binaries (gitignored, built) +└── dist/package/ assembled runtime dir shipped in the npm tarball +``` + +## Building + +From the repo root: + +```bash +just toolchain-build # compile the fast native wasm command gate +just toolchain-cmd # build one command (required for git, duckdb, vim, codex) +just software-build # stage bin/ + assemble dist/package/ +pnpm --filter './software/*' test +``` + +See [Building Binaries](https://agentos-sdk.dev/docs/custom-software/building-wasm) +for toolchain details (Rust vs C builds, the patched WASI sysroot). + +## Adding a package + +1. Copy an existing package of the same kind (`software/jq` is a minimal + example) to `software//`. +2. Add the command source under `software//native/` (Rust: + `native/crates/cmd-`; C: `native/c/.c`). +3. Fill in `agentos-package.json`: `commands` (and `aliases`/`provides` if + needed) plus a `registry` block with `title` and `description` — without + that block the package is not listed on the website registry page. +4. Register the directory in `pnpm-workspace.yaml` (it is covered by the + `software/*` glob) and run `pnpm install`. +5. `just software-build `. + +See [Software Definition](https://agentos-sdk.dev/docs/custom-software/definition) +for every manifest field. + +## Testing in an external project + +Inside this repo, tests and examples resolve packages via `workspace:*` — no +publishing needed. To try a package in an external project, pack the built +tarball and install it by path: + +```bash +cd software/ +npm pack # produces agentos-software--.tgz +cd /path/to/your-project +npm install /path/to/agentos-software--.tgz +``` + +Then register it in your VM and run a command: + +```typescript +import myPkg from "@agentos-software/"; +const vm = agentOS({ software: [myPkg] }); +``` + +Real publishes go through `agentos-toolchain publish` (dist-tag `dev` by +default) — see [Publishing Packages](https://agentos-sdk.dev/docs/custom-software/publishing). + +## Opening a PR + +- Branch, commit with a plain conventional-commit title + (`feat(software): add package`), no agent attribution. +- Include: the package directory, the native build wiring, and the + `registry` block so the website picks it up. +- Keep the package version at its own semver (packages version + independently); never touch other packages' versions or the `latest` + dist-tag. +- Cheap gates before pushing: `cargo check --workspace`, `pnpm build`, + `pnpm check-types`, and `just software-build `. diff --git a/software/README.md b/software/README.md new file mode 100644 index 0000000000..d104021d1e --- /dev/null +++ b/software/README.md @@ -0,0 +1,129 @@ +# agentOS Software Catalog + +Software packages for secure-exec VMs: WASM command binaries +and JavaScript agent adapters live together under `software/*`. Everything in +this catalog publishes under the `@agentos-software/*` npm scope. + +## Consuming packages + +```bash +npm install @agentos-software/coreutils @agentos-software/grep +# or a meta-package for a complete set: +npm install @agentos-software/common +``` + +Each package default-exports a descriptor whose `packageDir` points at the +self-contained runtime dir the sidecar projects under +`/opt/agentos//` (meta-packages export an array of descriptors): + +```typescript +import coreutils from "@agentos-software/coreutils"; +import grep from "@agentos-software/grep"; + +export const software = [coreutils, grep]; +``` + +## Package anatomy + +``` +software// +├── package.json name, per-package semver version, build script +├── agentos-package.json manifest: runtime fields (name/agent/provides) + +│ staging fields (commands/aliases/stubs) +├── src/index.ts descriptor: packageDir -> ./package/ (dist/package) +├── bin/ staged command binaries (gitignored, built) +└── dist/package/ the assembled runtime dir (shipped in the npm tarball): + ├── package.json { name, version, bin: { : "bin/" } } + ├── agentos-package.json + └── bin/ the binaries, copied verbatim +``` + +The whole lifecycle is owned by **`@rivet-dev/agentos-toolchain`** +(`packages/agentos-toolchain`) — the same CLI 3rd-party repos use to build and +publish their own agentOS packages (`npx @rivet-dev/agentos-toolchain`): + +- `stage --commands-dir ` — populate `bin/` from a compiled commands + directory, per the `commands` / `aliases` / `stubs` lists in + `agentos-package.json`. +- `build` — assemble the clean `dist/package/` runtime dir from `bin/`. +- `pack` — build a self-contained node-closure package (JS agents). +- `publish` — publish to npm; dist-tag `dev` by default, `latest` only with an + explicit `--latest`. + +## Building + +All recipes run from the repo root (see `justfile`): + +```bash +just toolchain-build # compile the fast native wasm command gate +just toolchain-cmd # build ONE command binary, whatever its toolchain +just software-build # stage + assemble every software package +just software-build coreutils # ... or just one +pnpm --filter './software/*' test +``` + +`toolchain-cmd` (= `make -C toolchain cmd/`) is the uniform +per-binary entry point; it dispatches to whichever toolchain owns the command: + +| kind | commands | what it runs | +|---|---|---| +| Rust | any `software//native/crates/cmd-` command crate (sh, ls, rg, …) | `cargo build -p cmd-` (build-std) + `wasm-opt` | +| C | `zip unzip envsubst sqlite3 curl wget duckdb` | `make -C c sysroot build/` + per-command install | +| codex | `codex`, `codex-exec` | the codex fork build (needs the fork checkout) | +| C | `vim` (pinned upstream clone + bridge in `c/vim/`) | `make -C c sysroot build/vim` + install | + +The default native build (`toolchain`) compiles the fast command gate to +`wasm32-wasip1` with a patched std (`-Z build-std`, `toolchain/std-patches/`), runs +`wasm-opt -O3`, and drops the binaries in +`toolchain/target/wasm32-wasip1/release/commands/`. The bulk gate +intentionally excludes slow/heavy or non-default commands: `git`, `duckdb`, +`vim` and the external `codex`/`codex-exec` fork build. Build those explicitly with +`just toolchain-cmd ` when working on them. Package builds then run +`agentos-toolchain stage` (with `--if-missing skip`, so a checkout without the +native build still assembles valid empty placeholders) followed by `tsc` and +`agentos-toolchain build`. + +Within this repo, everything consumes the LOCAL builds by default: the software +packages are pnpm workspace members, so tests and examples resolve them via +`workspace:*` — no publish needed for local development. + +Exceptions: +- `software/codex/wasm/` is the install target of the codex fork's build + (`make -C toolchain codex`); `software/codex-cli` stages from it. +- C-built commands (sqlite3, zip, unzip, curl, wget, duckdb) need the patched + sysroot; `just toolchain-cmd ` builds it on demand. Without it + those packages stay empty placeholders. +- `vim` builds from source: `just toolchain-cmd vim` clones the pinned + vim tag and compiles it against the patched sysroot + the termios/termcap + bridge in `software/vim/native/c/vim-bridge/` (its runtime tree is staged by the + package `scripts/stage-runtime.mjs` and applied via manifest `provides`). + +## Publishing + +Packages **version independently** (per-package semver in each +`package.json`). Publishing NEVER moves the `latest` dist-tag unless asked: + +```bash +just registry-publish coreutils # publish @agentos-software/coreutils under dist-tag `dev` +just registry-publish coreutils my-branch # ... under a custom tag +just registry-publish coreutils latest # DELIBERATE release: moves `latest` +just registry-publish-all # every built software package, dist-tag `dev` +``` + +Bump the package's `version` in its `package.json` (commit it) before +publishing. CI does not publish these packages (the publish workflow's package +discovery skips `@agentos-software/*` except the manifest); the agent packages +under `software/*` preview-publish via `.github/workflows/publish.yaml` +under a branch dist-tag. + +agent-os consumes the published packages pinned per-package in its catalog +(`just agentos-pkgs-status` there), and flips to these local checkouts with +`just agentos-pkgs-local`. + +## Contributing + +See [CONTRIBUTING.md](CONTRIBUTING.md) for how to add new packages. + +## License + +Apache-2.0 diff --git a/software/browserbase/agentos-package.json b/software/browserbase/agentos-package.json new file mode 100644 index 0000000000..3f53841f14 --- /dev/null +++ b/software/browserbase/agentos-package.json @@ -0,0 +1,13 @@ +{ + "name": "browserbase", + "registry": { + "title": "Browserbase", + "description": "The Browserbase `browse` CLI: let agents browse the web with a cloud browser, no sandbox required.", + "beta": true, + "types": [ + "browser" + ], + "image": "/images/registry/browserbase.svg", + "category": "browser" + } +} diff --git a/software/browserbase/package.json b/software/browserbase/package.json new file mode 100644 index 0000000000..18f1b8a84b --- /dev/null +++ b/software/browserbase/package.json @@ -0,0 +1,40 @@ +{ + "name": "@agentos-software/browserbase", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "Browserbase browse CLI for secure-exec VMs (cloud browser automation)", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "browse": "./node_modules/browse/bin/run.js" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "scripts": { + "build": "tsc && rm -rf dist/package dist/package.tar && agentos-toolchain pack . --out dist/package --prune-native", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "dependencies": { + "browse": "^0.9.3" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.7.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/agent/claude/src/index.ts b/software/browserbase/src/index.ts similarity index 100% rename from registry/agent/claude/src/index.ts rename to software/browserbase/src/index.ts diff --git a/software/browserbase/tsconfig.json b/software/browserbase/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/browserbase/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/build-essential/agentos-package.json b/software/build-essential/agentos-package.json new file mode 100644 index 0000000000..e2c0382a37 --- /dev/null +++ b/software/build-essential/agentos-package.json @@ -0,0 +1,8 @@ +{ + "registry": { + "title": "Build Essential", + "description": "Meta-package: common + git + curl.", + "priority": 100, + "category": "meta" + } +} diff --git a/software/build-essential/package.json b/software/build-essential/package.json new file mode 100644 index 0000000000..2002836894 --- /dev/null +++ b/software/build-essential/package.json @@ -0,0 +1,37 @@ +{ + "name": "@agentos-software/build-essential", + "version": "0.3.0-rc.2", + "type": "module", + "license": "Apache-2.0", + "description": "Build-essential WASM command set for secure-exec VMs (common + git + curl)", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "dependencies": { + "@agentos-software/common": "workspace:*", + "@agentos-software/curl": "workspace:*", + "@agentos-software/git": "workspace:*" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/build-essential/src/index.ts b/software/build-essential/src/index.ts similarity index 100% rename from registry/software/build-essential/src/index.ts rename to software/build-essential/src/index.ts diff --git a/software/build-essential/tsconfig.json b/software/build-essential/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/build-essential/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/claude/agentos-package.json b/software/claude/agentos-package.json new file mode 100644 index 0000000000..27f8b1c5ea --- /dev/null +++ b/software/claude/agentos-package.json @@ -0,0 +1,36 @@ +{ + "name": "claude", + "agent": { + "acpEntrypoint": "claude-sdk-acp", + "env": { + "CLAUDE_AGENT_SDK_CLIENT_APP": "@rivet-dev/agentos", + "CLAUDE_CODE_SIMPLE": "1", + "CLAUDE_CODE_FORCE_AGENT_OS_RIPGREP": "1", + "CLAUDE_CODE_DEFER_GROWTHBOOK_INIT": "1", + "CLAUDE_CODE_DISABLE_CWD_PERSIST": "1", + "CLAUDE_CODE_DISABLE_DEV_NULL_REDIRECT": "1", + "CLAUDE_CODE_NODE_SHELL_WRAPPER": "1", + "CLAUDE_CODE_DISABLE_STREAM_JSON_HOOK_EVENTS": "1", + "CLAUDE_CODE_SHELL": "/bin/sh", + "CLAUDE_CODE_SKIP_INITIAL_MESSAGES": "1", + "CLAUDE_CODE_SKIP_SANDBOX_INIT": "1", + "CLAUDE_CODE_SIMPLE_SHELL_EXEC": "1", + "CLAUDE_CODE_SWAP_STDIO": "0", + "CLAUDE_CODE_USE_PIPE_OUTPUT": "1", + "DISABLE_TELEMETRY": "1", + "SHELL": "/bin/sh", + "USE_BUILTIN_RIPGREP": "0" + } + }, + "registry": { + "slug": "claude-code", + "title": "Claude Code", + "description": "Run Claude Code as an agentOS agent with full tool access, file editing, and shell execution.", + "beta": true, + "docsHref": "/docs/agents/claude", + "image": "/images/registry/claude-code.svg", + "priority": 90, + "category": "agents" + }, + "kind": "agent" +} diff --git a/software/claude/package.json b/software/claude/package.json new file mode 100644 index 0000000000..de72c01931 --- /dev/null +++ b/software/claude/package.json @@ -0,0 +1,42 @@ +{ + "name": "@agentos-software/claude-code", + "version": "0.2.1", + "type": "module", + "license": "Apache-2.0", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "claude": "./dist/claude-cli.mjs", + "claude-sdk-acp": "./dist/adapter.js" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "!dist/package", + "!dist/package.tar", + "agentos-package.json" + ], + "scripts": { + "build": "tsc && node ./scripts/build-patched-cli.mjs && rm -rf dist/package && agentos-toolchain pack . --out dist/package --agent claude-sdk-acp --prune-native", + "check-types": "tsc --noEmit", + "test": "pnpm build && node --test --test-force-exit tests/*.test.mjs" + }, + "dependencies": { + "@agentclientprotocol/sdk": "^0.16.1", + "@anthropic-ai/claude-agent-sdk": "0.2.87", + "zod": "^4.1.11" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.7.2", + "@agentos/test-harness": "workspace:*" + } +} diff --git a/registry/agent/claude/scripts/build-patched-cli.mjs b/software/claude/scripts/build-patched-cli.mjs similarity index 100% rename from registry/agent/claude/scripts/build-patched-cli.mjs rename to software/claude/scripts/build-patched-cli.mjs diff --git a/registry/agent/claude/src/adapter.ts b/software/claude/src/adapter.ts similarity index 100% rename from registry/agent/claude/src/adapter.ts rename to software/claude/src/adapter.ts diff --git a/registry/agent/opencode/src/index.ts b/software/claude/src/index.ts similarity index 100% rename from registry/agent/opencode/src/index.ts rename to software/claude/src/index.ts diff --git a/registry/agent/claude/src/patched-cli.ts b/software/claude/src/patched-cli.ts similarity index 100% rename from registry/agent/claude/src/patched-cli.ts rename to software/claude/src/patched-cli.ts diff --git a/registry/agent/claude/tests/adapter.test.mjs b/software/claude/tests/adapter.test.mjs similarity index 100% rename from registry/agent/claude/tests/adapter.test.mjs rename to software/claude/tests/adapter.test.mjs diff --git a/registry/agent/claude/tests/patched-cli.test.mjs b/software/claude/tests/patched-cli.test.mjs similarity index 100% rename from registry/agent/claude/tests/patched-cli.test.mjs rename to software/claude/tests/patched-cli.test.mjs diff --git a/software/claude/tsconfig.json b/software/claude/tsconfig.json new file mode 100644 index 0000000000..73a06ddbdb --- /dev/null +++ b/software/claude/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/software/codex-cli/agentos-package.json b/software/codex-cli/agentos-package.json new file mode 100644 index 0000000000..ad23a92b67 --- /dev/null +++ b/software/codex-cli/agentos-package.json @@ -0,0 +1,12 @@ +{ + "commands": [ + "codex", + "codex-exec" + ], + "registry": { + "title": "Codex CLI", + "description": "OpenAI Codex CLI integration.", + "image": "/images/registry/codex.svg", + "category": "agents" + } +} diff --git a/software/codex-cli/native/crates/cmd-codex-exec/Cargo.toml b/software/codex-cli/native/crates/cmd-codex-exec/Cargo.toml new file mode 100644 index 0000000000..eeca177f14 --- /dev/null +++ b/software/codex-cli/native/crates/cmd-codex-exec/Cargo.toml @@ -0,0 +1,18 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-codex-exec" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "codex-exec command binary for secure-exec VM" + +[[bin]] +name = "codex-exec" +path = "src/main.rs" + +[dependencies] +wasi-http = { package = "secureexec-wasi-http", path = "../../../../../toolchain/crates/libs/wasi-http" } + +# WASI stub crates for future codex-core dependencies that don't support wasm32-wasip1. +codex-network-proxy = "0.0.0" +codex-otel = "0.0.0" diff --git a/registry/native/crates/commands/codex-exec/src/main.rs b/software/codex-cli/native/crates/cmd-codex-exec/src/main.rs similarity index 100% rename from registry/native/crates/commands/codex-exec/src/main.rs rename to software/codex-cli/native/crates/cmd-codex-exec/src/main.rs diff --git a/software/codex-cli/native/crates/cmd-codex/Cargo.toml b/software/codex-cli/native/crates/cmd-codex/Cargo.toml new file mode 100644 index 0000000000..cc73f06a16 --- /dev/null +++ b/software/codex-cli/native/crates/cmd-codex/Cargo.toml @@ -0,0 +1,34 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-codex" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "codex standalone binary for secure-exec VM" + +[[bin]] +name = "codex" +path = "src/main.rs" + +# The full codex-exec dependency from rivet-dev/codex (wasi-support branch) +# will be wired in when the vendoring blocker is resolved. +# +# The fork at github.com/rivet-dev/codex already has cfg(target_os = "wasi") +# gates on codex-core (portable-pty, network-proxy, sandbox deps) and +# codex-exec (all platform deps gated behind non-WASI). +# +# wasi-spawn provides the WasiChild abstraction that will replace +# tokio::process::Command in codex-core when compiled for WASI. + +[dependencies] +wasi-spawn = { package = "secureexec-wasi-spawn", path = "../../../../../toolchain/crates/libs/wasi-spawn" } +wasi-http = { package = "secureexec-wasi-http", path = "../../../../../toolchain/crates/libs/wasi-http" } + +# TUI dependencies — ratatui for rendering, crossterm for terminal backend +# crossterm is patched for WASI support (see std-patches/crates/crossterm/) +ratatui = { version = "0.29", default-features = false, features = ["crossterm"] } + +# WASI stub crates for codex-core dependencies that don't support wasm32-wasip1. +# These provide zero-size/no-op implementations so codex-core compiles for WASI. +codex-network-proxy = "0.0.0" +codex-otel = "0.0.0" diff --git a/software/codex-cli/native/crates/cmd-codex/src/main.rs b/software/codex-cli/native/crates/cmd-codex/src/main.rs new file mode 100644 index 0000000000..88f5d80c09 --- /dev/null +++ b/software/codex-cli/native/crates/cmd-codex/src/main.rs @@ -0,0 +1,337 @@ +/// Codex TUI for secure-exec VM. +/// +/// Full terminal UI using ratatui + crossterm backend, rendering through +/// the WasmVM PTY. This is the interactive entry point — for headless +/// (scriptable) usage, see codex-exec. +/// +/// Uses wasi-spawn for process spawning via host_process FFI and +/// wasi-http for HTTP/HTTPS requests via host_net TCP/TLS imports. +/// +/// crossterm is patched for WASI support (see std-patches/crates/crossterm/): +/// - Terminal raw mode tracked in-process (host PTY handles discipline) +/// - Terminal size from COLUMNS/LINES env vars +/// - Event source reads stdin directly and parses ANSI escape sequences +/// - IsTty returns true (PTY slave FDs are terminals) +use std::io; + +use ratatui::{ + backend::CrosstermBackend, + crossterm::{ + event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, + execute, + terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}, + }, + layout::{Constraint, Layout}, + style::{Color, Modifier, Style}, + text::{Line, Span, Text}, + widgets::{Block, Borders, Paragraph, Wrap}, + Frame, Terminal, +}; + +// Validate WASI stub crates compile by referencing key types +use codex_network_proxy::NetworkProxy; +use codex_otel::SessionTelemetry; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const MAX_INPUT_CHARS: usize = 8192; +const MAX_MESSAGES: usize = 200; + +fn main() { + let args: Vec = std::env::args().collect(); + + // Handle --help + if args.iter().any(|a| a == "--help" || a == "-h") { + print_help(); + return; + } + + // Handle --version + if args.iter().any(|a| a == "--version" || a == "-V") { + println!("codex {}", VERSION); + return; + } + + // Handle --model flag (accept but note it's stored for future use) + let model = args + .windows(2) + .find(|w| w[0] == "--model") + .map(|w| w[1].clone()); + + // Built-in HTTP test subcommand (validates wasi-http integration) + if args.get(1).map(|s| s.as_str()) == Some("--http-test") { + return http_test(&args[2..]); + } + + // Stub validation subcommand (validates WASI stub crates) + if args.get(1).map(|s| s.as_str()) == Some("--stub-test") { + return stub_test(); + } + + // Run the TUI + if let Err(e) = run_tui(model.as_deref()) { + eprintln!("codex: TUI error: {}", e); + std::process::exit(1); + } +} + +/// Main TUI event loop using ratatui + crossterm. +fn run_tui(model: Option<&str>) -> io::Result<()> { + let _terminal_guard = TerminalGuard::enter()?; + + let stdout = io::stdout(); + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + // App state + let mut input = String::new(); + let mut messages: Vec = Vec::new(); + let mut should_quit = false; + + // Draw initial frame + terminal.draw(|f| draw_ui(f, &input, &messages, model))?; + + // Event loop + while !should_quit { + match event::read()? { + Event::Key(key) => { + handle_key_event(key, &mut input, &mut messages, &mut should_quit); + terminal.draw(|f| draw_ui(f, &input, &messages, model))?; + } + Event::Resize(_, _) => { + terminal.draw(|f| draw_ui(f, &input, &messages, model))?; + } + _ => {} + } + } + + Ok(()) +} + +/// Handle a key event, updating app state. +fn handle_key_event( + key: KeyEvent, + input: &mut String, + messages: &mut Vec, + should_quit: &mut bool, +) { + match (key.code, key.modifiers) { + // Ctrl+C quits + (KeyCode::Char('c'), m) if m.contains(KeyModifiers::CONTROL) => { + *should_quit = true; + } + // 'q' on empty input quits + (KeyCode::Char('q'), KeyModifiers::NONE) if input.is_empty() => { + *should_quit = true; + } + // Enter submits the input + (KeyCode::Enter, _) => { + if !input.is_empty() { + let prompt = input.clone(); + push_message(messages, format!("> {}", prompt)); + push_message( + messages, + "codex: agent loop is under development".to_string(), + ); + push_message( + messages, + format!("codex: prompt received ({} chars)", prompt.len()), + ); + input.clear(); + } + } + // Backspace deletes last char + (KeyCode::Backspace, _) => { + input.pop(); + } + // Esc clears input + (KeyCode::Esc, _) => { + input.clear(); + } + // Regular character input + (KeyCode::Char(c), _) => { + if input.chars().count() < MAX_INPUT_CHARS { + input.push(c); + } + } + _ => {} + } +} + +fn push_message(messages: &mut Vec, message: String) { + messages.push(message); + if messages.len() > MAX_MESSAGES { + messages.drain(..messages.len() - MAX_MESSAGES); + } +} + +struct TerminalGuard { + raw_mode_enabled: bool, + alternate_screen_enabled: bool, +} + +impl TerminalGuard { + fn enter() -> io::Result { + terminal::enable_raw_mode()?; + + if let Err(error) = execute!(io::stdout(), EnterAlternateScreen) { + let _ = terminal::disable_raw_mode(); + return Err(error); + } + + Ok(Self { + raw_mode_enabled: true, + alternate_screen_enabled: true, + }) + } +} + +impl Drop for TerminalGuard { + fn drop(&mut self) { + if self.alternate_screen_enabled { + let _ = execute!(io::stdout(), LeaveAlternateScreen); + self.alternate_screen_enabled = false; + } + + if self.raw_mode_enabled { + let _ = terminal::disable_raw_mode(); + self.raw_mode_enabled = false; + } + } +} + +/// Draw the TUI layout. +fn draw_ui(f: &mut Frame, input: &str, messages: &[String], model: Option<&str>) { + let area = f.area(); + + let chunks = Layout::vertical([ + Constraint::Length(3), // Header + Constraint::Min(5), // Messages + Constraint::Length(3), // Input + ]) + .split(area); + + // Header + let model_text = model.unwrap_or("default"); + let header = Paragraph::new(Line::from(vec![ + Span::styled( + "Codex ", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + Span::styled(VERSION, Style::default().fg(Color::DarkGray)), + Span::raw(" "), + Span::styled( + format!("model: {}", model_text), + Style::default().fg(Color::Yellow), + ), + Span::raw(" "), + Span::styled("WasmVM", Style::default().fg(Color::Green)), + ])) + .block(Block::default().borders(Borders::ALL).title(" codex ")); + f.render_widget(header, chunks[0]); + + // Messages area + let msg_lines: Vec = messages + .iter() + .map(|m| { + if m.starts_with("> ") { + Line::from(Span::styled(m.as_str(), Style::default().fg(Color::Cyan))) + } else { + Line::from(Span::raw(m.as_str())) + } + }) + .collect(); + + let welcome = if messages.is_empty() { + vec![ + Line::from(""), + Line::from(Span::styled( + "Welcome to Codex on WasmVM!", + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + )), + Line::from(""), + Line::from("Type a prompt and press Enter to submit."), + Line::from("Press 'q' (on empty input) or Ctrl+C to exit."), + ] + } else { + msg_lines + }; + + let messages_widget = Paragraph::new(Text::from(welcome)) + .block(Block::default().borders(Borders::ALL).title(" messages ")) + .wrap(Wrap { trim: false }); + f.render_widget(messages_widget, chunks[1]); + + // Input area + let input_widget = Paragraph::new(Line::from(vec![ + Span::styled("❯ ", Style::default().fg(Color::Green)), + Span::raw(input), + ])) + .block(Block::default().borders(Borders::ALL).title(" input ")); + f.render_widget(input_widget, chunks[2]); +} + +fn print_help() { + println!( + "codex {} — interactive Codex TUI for secure-exec VM", + VERSION + ); + println!(); + println!("USAGE:"); + println!(" codex [OPTIONS]"); + println!(); + println!("OPTIONS:"); + println!(" -h, --help Print this help message"); + println!(" -V, --version Print version information"); + println!(" --model MODEL Select model for completions"); + println!(" --http-test URL Test HTTP client via host_net"); + println!(" --stub-test Validate WASI stub crates"); + println!(); + println!("DESCRIPTION:"); + println!(" Interactive TUI for the Codex agent, rendered through"); + println!(" the WasmVM PTY using ratatui + crossterm backend."); + println!(" For headless mode, use codex-exec instead."); +} + +fn stub_test() { + let proxy = NetworkProxy; + let mut env = std::collections::HashMap::new(); + proxy.apply_to_env(&mut env); + println!("network-proxy: NetworkProxy is zero-size, apply_to_env is no-op"); + + let telemetry = SessionTelemetry::new(); + telemetry.counter("test.counter", 1, &[]); + telemetry.histogram("test.histogram", 42, &[]); + println!("otel: SessionTelemetry metrics are no-ops"); + + let global = codex_otel::metrics::global(); + assert!(global.is_none(), "global metrics should be None on WASI"); + println!("otel: global() returns None (no exporter on WASI)"); + + println!("stub-test: all stubs validated successfully"); +} + +fn http_test(args: &[String]) { + if args.is_empty() { + eprintln!("usage: codex --http-test "); + std::process::exit(1); + } + + let url = &args[0]; + match wasi_http::get(url) { + Ok(resp) => { + println!("status: {}", resp.status); + match resp.text() { + Ok(body) => println!("body: {}", body), + Err(e) => eprintln!("body decode error: {}", e), + } + } + Err(e) => { + eprintln!("http error: {}", e); + std::process::exit(1); + } + } +} diff --git a/software/codex-cli/package.json b/software/codex-cli/package.json new file mode 100644 index 0000000000..717d9087f8 --- /dev/null +++ b/software/codex-cli/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/codex-cli", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "OpenAI Codex command package for secure-exec VMs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../codex/wasm --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/agent/pi-cli/src/index.ts b/software/codex-cli/src/index.ts similarity index 100% rename from registry/agent/pi-cli/src/index.ts rename to software/codex-cli/src/index.ts diff --git a/registry/tests/wasmvm/codex-exec.test.ts b/software/codex-cli/test/codex-exec.test.ts similarity index 94% rename from registry/tests/wasmvm/codex-exec.test.ts rename to software/codex-cli/test/codex-exec.test.ts index 1d3da03c42..2581c85fc2 100644 --- a/registry/tests/wasmvm/codex-exec.test.ts +++ b/software/codex-cli/test/codex-exec.test.ts @@ -14,9 +14,16 @@ */ import { describe, it, expect, afterEach } from 'vitest'; -import { createWasmVmRuntime } from '../helpers.js'; -import { COMMANDS_DIR, createKernel, describeIf, hasWasmBinaries } from '../helpers.js'; -import type { Kernel } from '../helpers.js'; +import { existsSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { createWasmVmRuntime } from '@agentos/test-harness'; +import { COMMANDS_DIR, createKernel, describeIf, hasWasmBinaries } from '@agentos/test-harness'; +import type { Kernel } from '@agentos/test-harness'; + +const hasCodexExec = + process.env.AGENTOS_CODEX_WASM_E2E === '1' && + hasWasmBinaries && + existsSync(resolve(COMMANDS_DIR, 'codex-exec')); // Minimal in-memory VFS for kernel tests class SimpleVFS { @@ -114,8 +121,8 @@ async function createTestKernel(): Promise<{ kernel: Kernel; vfs: SimpleVFS }> { return { kernel, vfs }; } -// TODO(P6): requires codex-exec WASM artifact from an external fork build, intentionally excluded from the fast registry-build gate. -describe.skip('codex-exec command (WasmVM)', { timeout: 30_000 }, () => { +// TODO(P6): requires codex-exec WASM artifact from an external fork build, intentionally excluded from the fast software-build gate. +describeIf(hasCodexExec, 'codex-exec command (WasmVM)', { timeout: 30_000 }, () => { let kernel: Kernel; afterEach(async () => { diff --git a/registry/tests/wasmvm/codex-tui.test.ts b/software/codex-cli/test/codex-tui.test.ts similarity index 91% rename from registry/tests/wasmvm/codex-tui.test.ts rename to software/codex-cli/test/codex-tui.test.ts index ea004eb986..1f1afef2d1 100644 --- a/registry/tests/wasmvm/codex-tui.test.ts +++ b/software/codex-cli/test/codex-tui.test.ts @@ -14,11 +14,17 @@ */ import { describe, it, expect, afterEach } from 'vitest'; -import { TerminalHarness } from './terminal-harness.js'; -import { createWasmVmRuntime } from '../helpers.js'; -import { COMMANDS_DIR, createKernel, describeIf, hasWasmBinaries } from '../helpers.js'; -import type { Kernel } from '../helpers.js'; - +import { existsSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { TerminalHarness } from '@agentos/test-harness'; +import { createWasmVmRuntime } from '@agentos/test-harness'; +import { COMMANDS_DIR, createKernel, describeIf, hasWasmBinaries } from '@agentos/test-harness'; +import type { Kernel } from '@agentos/test-harness'; + +const hasCodex = + process.env.AGENTOS_CODEX_WASM_E2E === '1' && + hasWasmBinaries && + existsSync(resolve(COMMANDS_DIR, 'codex')); const hasApiKey = !!process.env.OPENAI_API_KEY; /** brush-shell interactive prompt. */ @@ -124,8 +130,8 @@ async function createTestKernel(): Promise<{ kernel: Kernel; vfs: SimpleVFS }> { // Non-interactive tests (kernel.exec — --help bypasses TUI) // --------------------------------------------------------------------------- -// TODO(P6): requires codex WASM artifact from an external fork build, intentionally excluded from the fast registry-build gate. -describe.skip('codex TUI (WasmVM) - non-interactive', { timeout: 30_000 }, () => { +// TODO(P6): requires codex WASM artifact from an external fork build, intentionally excluded from the fast software-build gate. +describeIf(hasCodex, 'codex TUI (WasmVM) - non-interactive', { timeout: 30_000 }, () => { let kernel: Kernel; afterEach(async () => { @@ -155,8 +161,8 @@ describe.skip('codex TUI (WasmVM) - non-interactive', { timeout: 30_000 }, () => // Interactive TUI tests (PTY via TerminalHarness) // --------------------------------------------------------------------------- -// TODO(P6): requires codex WASM artifact from an external fork build, intentionally excluded from the fast registry-build gate. -describe.skip('codex TUI (WasmVM) - interactive', { timeout: 30_000 }, () => { +// TODO(P6): requires codex WASM artifact from an external fork build, intentionally excluded from the fast software-build gate. +describeIf(hasCodex, 'codex TUI (WasmVM) - interactive', { timeout: 30_000 }, () => { let kernel: Kernel; let harness: TerminalHarness; @@ -261,8 +267,8 @@ describe.skip('codex TUI (WasmVM) - interactive', { timeout: 30_000 }, () => { // API integration tests (gated behind OPENAI_API_KEY) // --------------------------------------------------------------------------- -// TODO(P6): requires codex WASM artifact from an external fork build, intentionally excluded from the fast registry-build gate. -describe.skip('codex TUI API integration (requires OPENAI_API_KEY)', { timeout: 60_000 }, () => { +// TODO(P6): requires codex WASM artifact from an external fork build, intentionally excluded from the fast software-build gate. +describeIf(hasCodex && hasApiKey, 'codex TUI API integration (requires OPENAI_API_KEY)', { timeout: 60_000 }, () => { let kernel: Kernel; let harness: TerminalHarness; diff --git a/software/codex-cli/tsconfig.json b/software/codex-cli/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/codex-cli/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/codex/agentos-package.json b/software/codex/agentos-package.json new file mode 100644 index 0000000000..0a05c9b778 --- /dev/null +++ b/software/codex/agentos-package.json @@ -0,0 +1,12 @@ +{ + "registry": { + "title": "Codex", + "description": "Run OpenAI's Codex coding agent inside agentOS with programmatic API access.", + "beta": true, + "docsHref": "/docs/agents/codex", + "image": "/images/registry/codex.svg", + "priority": 80, + "category": "agents" + }, + "kind": "agent" +} diff --git a/software/codex/package.json b/software/codex/package.json new file mode 100644 index 0000000000..ae83c6e51b --- /dev/null +++ b/software/codex/package.json @@ -0,0 +1,30 @@ +{ + "name": "@agentos-software/codex", + "version": "0.2.0-rc.3", + "type": "module", + "license": "Apache-2.0", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "scripts": { + "prepare-deps": "pnpm --filter @agentos-software/codex-cli exec tsc", + "build": "pnpm prepare-deps && tsc", + "check-types": "tsc --noEmit", + "test": "pnpm build && node --test tests/*.test.mjs" + }, + "dependencies": { + "@agentos-software/codex-cli": "workspace:*" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.7.2", + "@agentos/test-harness": "workspace:*" + } +} diff --git a/registry/agent/codex/src/index.ts b/software/codex/src/index.ts similarity index 100% rename from registry/agent/codex/src/index.ts rename to software/codex/src/index.ts diff --git a/registry/agent/codex/tests/package.test.mjs b/software/codex/tests/package.test.mjs similarity index 100% rename from registry/agent/codex/tests/package.test.mjs rename to software/codex/tests/package.test.mjs diff --git a/software/codex/tsconfig.json b/software/codex/tsconfig.json new file mode 100644 index 0000000000..73a06ddbdb --- /dev/null +++ b/software/codex/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/software/common/agentos-package.json b/software/common/agentos-package.json new file mode 100644 index 0000000000..b8dc86c870 --- /dev/null +++ b/software/common/agentos-package.json @@ -0,0 +1,8 @@ +{ + "registry": { + "title": "Common", + "description": "Meta-package: coreutils + sed + grep + gawk + findutils + diffutils + tar + gzip.", + "priority": 95, + "category": "meta" + } +} diff --git a/software/common/package.json b/software/common/package.json new file mode 100644 index 0000000000..4e2113cb3b --- /dev/null +++ b/software/common/package.json @@ -0,0 +1,42 @@ +{ + "name": "@agentos-software/common", + "version": "0.3.0-rc.2", + "type": "module", + "license": "Apache-2.0", + "description": "Common WASM command set for secure-exec VMs (coreutils + sed + grep + gawk + findutils + diffutils + tar + gzip)", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "dependencies": { + "@agentos-software/coreutils": "workspace:*", + "@agentos-software/sed": "workspace:*", + "@agentos-software/grep": "workspace:*", + "@agentos-software/gawk": "workspace:*", + "@agentos-software/findutils": "workspace:*", + "@agentos-software/diffutils": "workspace:*", + "@agentos-software/tar": "workspace:*", + "@agentos-software/gzip": "workspace:*" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/common/src/index.ts b/software/common/src/index.ts similarity index 100% rename from registry/software/common/src/index.ts rename to software/common/src/index.ts diff --git a/software/common/tsconfig.json b/software/common/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/common/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/coreutils/agentos-package.json b/software/coreutils/agentos-package.json new file mode 100644 index 0000000000..30fdb36450 --- /dev/null +++ b/software/coreutils/agentos-package.json @@ -0,0 +1,128 @@ +{ + "commands": [ + "sh", + "arch", + "b2sum", + "base32", + "base64", + "basename", + "basenc", + "cat", + "chmod", + "cksum", + "column", + "comm", + "cp", + "cut", + "date", + "dd", + "dircolors", + "dirname", + "du", + "echo", + "env", + "expand", + "expr", + "factor", + "false", + "fmt", + "fold", + "head", + "join", + "link", + "ln", + "logname", + "ls", + "md5sum", + "mkdir", + "mktemp", + "mv", + "nice", + "nl", + "nohup", + "nproc", + "numfmt", + "od", + "paste", + "pathchk", + "printenv", + "printf", + "ptx", + "pwd", + "readlink", + "realpath", + "rev", + "rm", + "rmdir", + "seq", + "sha1sum", + "sha224sum", + "sha256sum", + "sha384sum", + "sha512sum", + "shred", + "shuf", + "sleep", + "sort", + "split", + "stat", + "stdbuf", + "strings", + "sum", + "tac", + "tail", + "tee", + "test", + "timeout", + "touch", + "tr", + "true", + "truncate", + "tsort", + "uname", + "unexpand", + "uniq", + "unlink", + "wc", + "which", + "whoami", + "yes" + ], + "aliases": { + "bash": "sh", + "more": "cat", + "dir": "ls", + "vdir": "ls", + "[": "test" + }, + "stubs": [ + "chcon", + "runcon", + "chgrp", + "chown", + "chroot", + "df", + "groups", + "id", + "hostname", + "hostid", + "install", + "kill", + "mkfifo", + "mknod", + "pinky", + "who", + "users", + "uptime", + "stty", + "sync", + "tty" + ], + "registry": { + "title": "Coreutils", + "description": "sh, cat, ls, cp, mv, rm, sort, and 80+ essential POSIX commands.", + "priority": 45, + "image": "/images/registry/coreutils.svg", + "category": "core" + } +} diff --git a/software/coreutils/native/crates/cmd-arch/Cargo.toml b/software/coreutils/native/crates/cmd-arch/Cargo.toml new file mode 100644 index 0000000000..cb72652e83 --- /dev/null +++ b/software/coreutils/native/crates/cmd-arch/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-arch" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "arch standalone binary for secure-exec VM" + +[[bin]] +name = "arch" +path = "src/main.rs" + +[dependencies] +uu_arch = "0.7.0" diff --git a/registry/native/crates/commands/arch/src/main.rs b/software/coreutils/native/crates/cmd-arch/src/main.rs similarity index 100% rename from registry/native/crates/commands/arch/src/main.rs rename to software/coreutils/native/crates/cmd-arch/src/main.rs diff --git a/software/coreutils/native/crates/cmd-b2sum/Cargo.toml b/software/coreutils/native/crates/cmd-b2sum/Cargo.toml new file mode 100644 index 0000000000..3f5da391a6 --- /dev/null +++ b/software/coreutils/native/crates/cmd-b2sum/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-b2sum" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "b2sum standalone binary for secure-exec VM" + +[[bin]] +name = "b2sum" +path = "src/main.rs" + +[dependencies] +uu_b2sum = "0.7.0" diff --git a/registry/native/crates/commands/b2sum/src/main.rs b/software/coreutils/native/crates/cmd-b2sum/src/main.rs similarity index 100% rename from registry/native/crates/commands/b2sum/src/main.rs rename to software/coreutils/native/crates/cmd-b2sum/src/main.rs diff --git a/software/coreutils/native/crates/cmd-base32/Cargo.toml b/software/coreutils/native/crates/cmd-base32/Cargo.toml new file mode 100644 index 0000000000..29ea0a98a1 --- /dev/null +++ b/software/coreutils/native/crates/cmd-base32/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-base32" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "base32 standalone binary for secure-exec VM" + +[[bin]] +name = "base32" +path = "src/main.rs" + +[dependencies] +uu_base32 = "0.7.0" diff --git a/registry/native/crates/commands/base32/src/main.rs b/software/coreutils/native/crates/cmd-base32/src/main.rs similarity index 100% rename from registry/native/crates/commands/base32/src/main.rs rename to software/coreutils/native/crates/cmd-base32/src/main.rs diff --git a/software/coreutils/native/crates/cmd-base64/Cargo.toml b/software/coreutils/native/crates/cmd-base64/Cargo.toml new file mode 100644 index 0000000000..105af17592 --- /dev/null +++ b/software/coreutils/native/crates/cmd-base64/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-base64" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "base64 standalone binary for secure-exec VM" + +[[bin]] +name = "base64" +path = "src/main.rs" + +[dependencies] +uu_base64 = "0.7.0" diff --git a/registry/native/crates/commands/base64/src/main.rs b/software/coreutils/native/crates/cmd-base64/src/main.rs similarity index 100% rename from registry/native/crates/commands/base64/src/main.rs rename to software/coreutils/native/crates/cmd-base64/src/main.rs diff --git a/software/coreutils/native/crates/cmd-basename/Cargo.toml b/software/coreutils/native/crates/cmd-basename/Cargo.toml new file mode 100644 index 0000000000..aac009cc30 --- /dev/null +++ b/software/coreutils/native/crates/cmd-basename/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-basename" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "basename standalone binary for secure-exec VM" + +[[bin]] +name = "basename" +path = "src/main.rs" + +[dependencies] +uu_basename = "0.7.0" diff --git a/registry/native/crates/commands/basename/src/main.rs b/software/coreutils/native/crates/cmd-basename/src/main.rs similarity index 100% rename from registry/native/crates/commands/basename/src/main.rs rename to software/coreutils/native/crates/cmd-basename/src/main.rs diff --git a/software/coreutils/native/crates/cmd-basenc/Cargo.toml b/software/coreutils/native/crates/cmd-basenc/Cargo.toml new file mode 100644 index 0000000000..9a297f21c0 --- /dev/null +++ b/software/coreutils/native/crates/cmd-basenc/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-basenc" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "basenc standalone binary for secure-exec VM" + +[[bin]] +name = "basenc" +path = "src/main.rs" + +[dependencies] +uu_basenc = "0.7.0" diff --git a/registry/native/crates/commands/basenc/src/main.rs b/software/coreutils/native/crates/cmd-basenc/src/main.rs similarity index 100% rename from registry/native/crates/commands/basenc/src/main.rs rename to software/coreutils/native/crates/cmd-basenc/src/main.rs diff --git a/software/coreutils/native/crates/cmd-cat/Cargo.toml b/software/coreutils/native/crates/cmd-cat/Cargo.toml new file mode 100644 index 0000000000..747109c7cd --- /dev/null +++ b/software/coreutils/native/crates/cmd-cat/Cargo.toml @@ -0,0 +1,17 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-cat" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "cat standalone binary for secure-exec VM" + +[[bin]] +name = "cat" +path = "src/main.rs" + +[dependencies] +uu_cat = "0.7.0" + +[target.'cfg(any(target_os = "linux", target_os = "android"))'.dev-dependencies] +uucore = { version = "0.7.0", features = ["pipes"] } diff --git a/registry/native/crates/commands/cat/src/main.rs b/software/coreutils/native/crates/cmd-cat/src/main.rs similarity index 100% rename from registry/native/crates/commands/cat/src/main.rs rename to software/coreutils/native/crates/cmd-cat/src/main.rs diff --git a/registry/native/crates/commands/cat/tests/uucore_pipes.rs b/software/coreutils/native/crates/cmd-cat/tests/uucore_pipes.rs similarity index 100% rename from registry/native/crates/commands/cat/tests/uucore_pipes.rs rename to software/coreutils/native/crates/cmd-cat/tests/uucore_pipes.rs diff --git a/software/coreutils/native/crates/cmd-chmod/Cargo.toml b/software/coreutils/native/crates/cmd-chmod/Cargo.toml new file mode 100644 index 0000000000..9e56965b70 --- /dev/null +++ b/software/coreutils/native/crates/cmd-chmod/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-chmod" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "chmod standalone binary for secure-exec VM" + +[[bin]] +name = "chmod" +path = "src/main.rs" + +[dependencies] +uu_chmod = "0.7.0" diff --git a/registry/native/crates/commands/chmod/src/main.rs b/software/coreutils/native/crates/cmd-chmod/src/main.rs similarity index 100% rename from registry/native/crates/commands/chmod/src/main.rs rename to software/coreutils/native/crates/cmd-chmod/src/main.rs diff --git a/software/coreutils/native/crates/cmd-cksum/Cargo.toml b/software/coreutils/native/crates/cmd-cksum/Cargo.toml new file mode 100644 index 0000000000..25df33cb56 --- /dev/null +++ b/software/coreutils/native/crates/cmd-cksum/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-cksum" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "cksum standalone binary for secure-exec VM" + +[[bin]] +name = "cksum" +path = "src/main.rs" + +[dependencies] +uu_cksum = "0.7.0" diff --git a/registry/native/crates/commands/cksum/src/main.rs b/software/coreutils/native/crates/cmd-cksum/src/main.rs similarity index 100% rename from registry/native/crates/commands/cksum/src/main.rs rename to software/coreutils/native/crates/cmd-cksum/src/main.rs diff --git a/software/coreutils/native/crates/cmd-column/Cargo.toml b/software/coreutils/native/crates/cmd-column/Cargo.toml new file mode 100644 index 0000000000..8b16759a99 --- /dev/null +++ b/software/coreutils/native/crates/cmd-column/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-column" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "column standalone binary for secure-exec VM" + +[[bin]] +name = "column" +path = "src/main.rs" + +[dependencies] +secureexec-column = { path = "../column" } diff --git a/registry/native/crates/commands/column/src/main.rs b/software/coreutils/native/crates/cmd-column/src/main.rs similarity index 100% rename from registry/native/crates/commands/column/src/main.rs rename to software/coreutils/native/crates/cmd-column/src/main.rs diff --git a/software/coreutils/native/crates/cmd-comm/Cargo.toml b/software/coreutils/native/crates/cmd-comm/Cargo.toml new file mode 100644 index 0000000000..91e507f0f8 --- /dev/null +++ b/software/coreutils/native/crates/cmd-comm/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-comm" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "comm standalone binary for secure-exec VM" + +[[bin]] +name = "comm" +path = "src/main.rs" + +[dependencies] +uu_comm = "0.7.0" diff --git a/registry/native/crates/commands/comm/src/main.rs b/software/coreutils/native/crates/cmd-comm/src/main.rs similarity index 100% rename from registry/native/crates/commands/comm/src/main.rs rename to software/coreutils/native/crates/cmd-comm/src/main.rs diff --git a/software/coreutils/native/crates/cmd-cp/Cargo.toml b/software/coreutils/native/crates/cmd-cp/Cargo.toml new file mode 100644 index 0000000000..56a958c1fe --- /dev/null +++ b/software/coreutils/native/crates/cmd-cp/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-cp" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "cp standalone binary for secure-exec VM" + +[[bin]] +name = "cp" +path = "src/main.rs" + +[dependencies] +uu_cp = "0.7.0" diff --git a/registry/native/crates/commands/cp/src/main.rs b/software/coreutils/native/crates/cmd-cp/src/main.rs similarity index 100% rename from registry/native/crates/commands/cp/src/main.rs rename to software/coreutils/native/crates/cmd-cp/src/main.rs diff --git a/software/coreutils/native/crates/cmd-cut/Cargo.toml b/software/coreutils/native/crates/cmd-cut/Cargo.toml new file mode 100644 index 0000000000..72e5a5f814 --- /dev/null +++ b/software/coreutils/native/crates/cmd-cut/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-cut" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "cut standalone binary for secure-exec VM" + +[[bin]] +name = "cut" +path = "src/main.rs" + +[dependencies] +uu_cut = "0.7.0" diff --git a/registry/native/crates/commands/cut/src/main.rs b/software/coreutils/native/crates/cmd-cut/src/main.rs similarity index 100% rename from registry/native/crates/commands/cut/src/main.rs rename to software/coreutils/native/crates/cmd-cut/src/main.rs diff --git a/software/coreutils/native/crates/cmd-date/Cargo.toml b/software/coreutils/native/crates/cmd-date/Cargo.toml new file mode 100644 index 0000000000..7b0d440e2f --- /dev/null +++ b/software/coreutils/native/crates/cmd-date/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-date" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "date standalone binary for secure-exec VM" + +[[bin]] +name = "date" +path = "src/main.rs" + +[dependencies] +uu_date = "0.7.0" diff --git a/registry/native/crates/commands/date/src/main.rs b/software/coreutils/native/crates/cmd-date/src/main.rs similarity index 100% rename from registry/native/crates/commands/date/src/main.rs rename to software/coreutils/native/crates/cmd-date/src/main.rs diff --git a/software/coreutils/native/crates/cmd-dd/Cargo.toml b/software/coreutils/native/crates/cmd-dd/Cargo.toml new file mode 100644 index 0000000000..744ee8059f --- /dev/null +++ b/software/coreutils/native/crates/cmd-dd/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-dd" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "dd standalone binary for secure-exec VM" + +[[bin]] +name = "dd" +path = "src/main.rs" + +[dependencies] +uu_dd = "0.7.0" diff --git a/registry/native/crates/commands/dd/src/main.rs b/software/coreutils/native/crates/cmd-dd/src/main.rs similarity index 100% rename from registry/native/crates/commands/dd/src/main.rs rename to software/coreutils/native/crates/cmd-dd/src/main.rs diff --git a/software/coreutils/native/crates/cmd-dircolors/Cargo.toml b/software/coreutils/native/crates/cmd-dircolors/Cargo.toml new file mode 100644 index 0000000000..86c849c392 --- /dev/null +++ b/software/coreutils/native/crates/cmd-dircolors/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-dircolors" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "dircolors standalone binary for secure-exec VM" + +[[bin]] +name = "dircolors" +path = "src/main.rs" + +[dependencies] +uu_dircolors = "0.7.0" diff --git a/registry/native/crates/commands/dircolors/src/main.rs b/software/coreutils/native/crates/cmd-dircolors/src/main.rs similarity index 100% rename from registry/native/crates/commands/dircolors/src/main.rs rename to software/coreutils/native/crates/cmd-dircolors/src/main.rs diff --git a/software/coreutils/native/crates/cmd-dirname/Cargo.toml b/software/coreutils/native/crates/cmd-dirname/Cargo.toml new file mode 100644 index 0000000000..bd1615de6b --- /dev/null +++ b/software/coreutils/native/crates/cmd-dirname/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-dirname" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "dirname standalone binary for secure-exec VM" + +[[bin]] +name = "dirname" +path = "src/main.rs" + +[dependencies] +uu_dirname = "0.7.0" diff --git a/registry/native/crates/commands/dirname/src/main.rs b/software/coreutils/native/crates/cmd-dirname/src/main.rs similarity index 100% rename from registry/native/crates/commands/dirname/src/main.rs rename to software/coreutils/native/crates/cmd-dirname/src/main.rs diff --git a/software/coreutils/native/crates/cmd-du/Cargo.toml b/software/coreutils/native/crates/cmd-du/Cargo.toml new file mode 100644 index 0000000000..8a2cc1e9d3 --- /dev/null +++ b/software/coreutils/native/crates/cmd-du/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-du" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "du standalone binary for secure-exec VM" + +[[bin]] +name = "du" +path = "src/main.rs" + +[dependencies] +secureexec-du = { path = "../du" } diff --git a/registry/native/crates/commands/du/src/main.rs b/software/coreutils/native/crates/cmd-du/src/main.rs similarity index 100% rename from registry/native/crates/commands/du/src/main.rs rename to software/coreutils/native/crates/cmd-du/src/main.rs diff --git a/software/coreutils/native/crates/cmd-echo/Cargo.toml b/software/coreutils/native/crates/cmd-echo/Cargo.toml new file mode 100644 index 0000000000..2f882f37be --- /dev/null +++ b/software/coreutils/native/crates/cmd-echo/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-echo" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "echo standalone binary for secure-exec VM" + +[[bin]] +name = "echo" +path = "src/main.rs" + +[dependencies] +uu_echo = "0.7.0" diff --git a/registry/native/crates/commands/echo/src/main.rs b/software/coreutils/native/crates/cmd-echo/src/main.rs similarity index 100% rename from registry/native/crates/commands/echo/src/main.rs rename to software/coreutils/native/crates/cmd-echo/src/main.rs diff --git a/software/coreutils/native/crates/cmd-env/Cargo.toml b/software/coreutils/native/crates/cmd-env/Cargo.toml new file mode 100644 index 0000000000..6fe19a8d7d --- /dev/null +++ b/software/coreutils/native/crates/cmd-env/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-env" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "env standalone binary for secure-exec VM" + +[[bin]] +name = "env" +path = "src/main.rs" + +[dependencies] +shims = { package = "secureexec-shims", path = "../../../../../toolchain/crates/libs/shims" } diff --git a/registry/native/crates/commands/env/src/main.rs b/software/coreutils/native/crates/cmd-env/src/main.rs similarity index 100% rename from registry/native/crates/commands/env/src/main.rs rename to software/coreutils/native/crates/cmd-env/src/main.rs diff --git a/software/coreutils/native/crates/cmd-expand/Cargo.toml b/software/coreutils/native/crates/cmd-expand/Cargo.toml new file mode 100644 index 0000000000..08807dc938 --- /dev/null +++ b/software/coreutils/native/crates/cmd-expand/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-expand" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "expand standalone binary for secure-exec VM" + +[[bin]] +name = "expand" +path = "src/main.rs" + +[dependencies] +uu_expand = "0.7.0" diff --git a/registry/native/crates/commands/expand/src/main.rs b/software/coreutils/native/crates/cmd-expand/src/main.rs similarity index 100% rename from registry/native/crates/commands/expand/src/main.rs rename to software/coreutils/native/crates/cmd-expand/src/main.rs diff --git a/software/coreutils/native/crates/cmd-expr/Cargo.toml b/software/coreutils/native/crates/cmd-expr/Cargo.toml new file mode 100644 index 0000000000..10bd466ba0 --- /dev/null +++ b/software/coreutils/native/crates/cmd-expr/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-expr" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "expr standalone binary for secure-exec VM" + +[[bin]] +name = "expr" +path = "src/main.rs" + +[dependencies] +secureexec-expr = { path = "../expr" } diff --git a/registry/native/crates/commands/expr/src/main.rs b/software/coreutils/native/crates/cmd-expr/src/main.rs similarity index 100% rename from registry/native/crates/commands/expr/src/main.rs rename to software/coreutils/native/crates/cmd-expr/src/main.rs diff --git a/software/coreutils/native/crates/cmd-factor/Cargo.toml b/software/coreutils/native/crates/cmd-factor/Cargo.toml new file mode 100644 index 0000000000..cd4f1f233b --- /dev/null +++ b/software/coreutils/native/crates/cmd-factor/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-factor" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "factor standalone binary for secure-exec VM" + +[[bin]] +name = "factor" +path = "src/main.rs" + +[dependencies] +uu_factor = "0.7.0" diff --git a/registry/native/crates/commands/factor/src/main.rs b/software/coreutils/native/crates/cmd-factor/src/main.rs similarity index 100% rename from registry/native/crates/commands/factor/src/main.rs rename to software/coreutils/native/crates/cmd-factor/src/main.rs diff --git a/software/coreutils/native/crates/cmd-false/Cargo.toml b/software/coreutils/native/crates/cmd-false/Cargo.toml new file mode 100644 index 0000000000..a3660c6acf --- /dev/null +++ b/software/coreutils/native/crates/cmd-false/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-false" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "false standalone binary for secure-exec VM" + +[[bin]] +name = "false" +path = "src/main.rs" + +[dependencies] +uu_false = "0.7.0" diff --git a/registry/native/crates/commands/false/src/main.rs b/software/coreutils/native/crates/cmd-false/src/main.rs similarity index 100% rename from registry/native/crates/commands/false/src/main.rs rename to software/coreutils/native/crates/cmd-false/src/main.rs diff --git a/software/coreutils/native/crates/cmd-fmt/Cargo.toml b/software/coreutils/native/crates/cmd-fmt/Cargo.toml new file mode 100644 index 0000000000..2ebb0f6f6a --- /dev/null +++ b/software/coreutils/native/crates/cmd-fmt/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-fmt" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "fmt standalone binary for secure-exec VM" + +[[bin]] +name = "fmt" +path = "src/main.rs" + +[dependencies] +uu_fmt = "0.7.0" diff --git a/registry/native/crates/commands/fmt/src/main.rs b/software/coreutils/native/crates/cmd-fmt/src/main.rs similarity index 100% rename from registry/native/crates/commands/fmt/src/main.rs rename to software/coreutils/native/crates/cmd-fmt/src/main.rs diff --git a/software/coreutils/native/crates/cmd-fold/Cargo.toml b/software/coreutils/native/crates/cmd-fold/Cargo.toml new file mode 100644 index 0000000000..7d79e95796 --- /dev/null +++ b/software/coreutils/native/crates/cmd-fold/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-fold" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "fold standalone binary for secure-exec VM" + +[[bin]] +name = "fold" +path = "src/main.rs" + +[dependencies] +uu_fold = "0.7.0" diff --git a/registry/native/crates/commands/fold/src/main.rs b/software/coreutils/native/crates/cmd-fold/src/main.rs similarity index 100% rename from registry/native/crates/commands/fold/src/main.rs rename to software/coreutils/native/crates/cmd-fold/src/main.rs diff --git a/software/coreutils/native/crates/cmd-head/Cargo.toml b/software/coreutils/native/crates/cmd-head/Cargo.toml new file mode 100644 index 0000000000..287d999055 --- /dev/null +++ b/software/coreutils/native/crates/cmd-head/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-head" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "head standalone binary for secure-exec VM" + +[[bin]] +name = "head" +path = "src/main.rs" + +[dependencies] +uu_head = "0.7.0" diff --git a/registry/native/crates/commands/head/src/main.rs b/software/coreutils/native/crates/cmd-head/src/main.rs similarity index 100% rename from registry/native/crates/commands/head/src/main.rs rename to software/coreutils/native/crates/cmd-head/src/main.rs diff --git a/software/coreutils/native/crates/cmd-join/Cargo.toml b/software/coreutils/native/crates/cmd-join/Cargo.toml new file mode 100644 index 0000000000..c81c31cb5f --- /dev/null +++ b/software/coreutils/native/crates/cmd-join/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-join" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "join standalone binary for secure-exec VM" + +[[bin]] +name = "join" +path = "src/main.rs" + +[dependencies] +uu_join = "0.7.0" diff --git a/registry/native/crates/commands/join/src/main.rs b/software/coreutils/native/crates/cmd-join/src/main.rs similarity index 100% rename from registry/native/crates/commands/join/src/main.rs rename to software/coreutils/native/crates/cmd-join/src/main.rs diff --git a/software/coreutils/native/crates/cmd-kill/Cargo.toml b/software/coreutils/native/crates/cmd-kill/Cargo.toml new file mode 100644 index 0000000000..19c052ef0c --- /dev/null +++ b/software/coreutils/native/crates/cmd-kill/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-kill" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "kill standalone binary for AgentOS VMs" + +[[bin]] +name = "kill" +path = "src/main.rs" + +[dependencies] +uu_kill = "0.7.0" diff --git a/software/coreutils/native/crates/cmd-kill/src/main.rs b/software/coreutils/native/crates/cmd-kill/src/main.rs new file mode 100644 index 0000000000..7e1d40a248 --- /dev/null +++ b/software/coreutils/native/crates/cmd-kill/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + let args: Vec = std::env::args_os().collect(); + std::process::exit(uu_kill::uumain(args.into_iter())); +} diff --git a/software/coreutils/native/crates/cmd-link/Cargo.toml b/software/coreutils/native/crates/cmd-link/Cargo.toml new file mode 100644 index 0000000000..043488ee6c --- /dev/null +++ b/software/coreutils/native/crates/cmd-link/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-link" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "link standalone binary for secure-exec VM" + +[[bin]] +name = "link" +path = "src/main.rs" + +[dependencies] +uu_link = "0.7.0" diff --git a/registry/native/crates/commands/link/src/main.rs b/software/coreutils/native/crates/cmd-link/src/main.rs similarity index 100% rename from registry/native/crates/commands/link/src/main.rs rename to software/coreutils/native/crates/cmd-link/src/main.rs diff --git a/software/coreutils/native/crates/cmd-ln/Cargo.toml b/software/coreutils/native/crates/cmd-ln/Cargo.toml new file mode 100644 index 0000000000..92950c1f35 --- /dev/null +++ b/software/coreutils/native/crates/cmd-ln/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-ln" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "ln standalone binary for secure-exec VM" + +[[bin]] +name = "ln" +path = "src/main.rs" + +[dependencies] +uu_ln = "0.7.0" diff --git a/registry/native/crates/commands/ln/src/main.rs b/software/coreutils/native/crates/cmd-ln/src/main.rs similarity index 100% rename from registry/native/crates/commands/ln/src/main.rs rename to software/coreutils/native/crates/cmd-ln/src/main.rs diff --git a/software/coreutils/native/crates/cmd-logname/Cargo.toml b/software/coreutils/native/crates/cmd-logname/Cargo.toml new file mode 100644 index 0000000000..33b6d0d678 --- /dev/null +++ b/software/coreutils/native/crates/cmd-logname/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-logname" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "logname standalone binary for secure-exec VM" + +[[bin]] +name = "logname" +path = "src/main.rs" + +[dependencies] +uu_logname = "0.7.0" diff --git a/registry/native/crates/commands/logname/src/main.rs b/software/coreutils/native/crates/cmd-logname/src/main.rs similarity index 100% rename from registry/native/crates/commands/logname/src/main.rs rename to software/coreutils/native/crates/cmd-logname/src/main.rs diff --git a/software/coreutils/native/crates/cmd-ls/Cargo.toml b/software/coreutils/native/crates/cmd-ls/Cargo.toml new file mode 100644 index 0000000000..71ba7a86e8 --- /dev/null +++ b/software/coreutils/native/crates/cmd-ls/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-ls" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "ls standalone binary for secure-exec VM" + +[[bin]] +name = "ls" +path = "src/main.rs" + +[dependencies] +uu_ls = "0.7.0" diff --git a/registry/native/crates/commands/ls/src/main.rs b/software/coreutils/native/crates/cmd-ls/src/main.rs similarity index 100% rename from registry/native/crates/commands/ls/src/main.rs rename to software/coreutils/native/crates/cmd-ls/src/main.rs diff --git a/software/coreutils/native/crates/cmd-md5sum/Cargo.toml b/software/coreutils/native/crates/cmd-md5sum/Cargo.toml new file mode 100644 index 0000000000..ea660b5d0f --- /dev/null +++ b/software/coreutils/native/crates/cmd-md5sum/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-md5sum" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "md5sum standalone binary for secure-exec VM" + +[[bin]] +name = "md5sum" +path = "src/main.rs" + +[dependencies] +uu_md5sum = "0.7.0" diff --git a/registry/native/crates/commands/md5sum/src/main.rs b/software/coreutils/native/crates/cmd-md5sum/src/main.rs similarity index 100% rename from registry/native/crates/commands/md5sum/src/main.rs rename to software/coreutils/native/crates/cmd-md5sum/src/main.rs diff --git a/software/coreutils/native/crates/cmd-mkdir/Cargo.toml b/software/coreutils/native/crates/cmd-mkdir/Cargo.toml new file mode 100644 index 0000000000..7a3e9f4ccf --- /dev/null +++ b/software/coreutils/native/crates/cmd-mkdir/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-mkdir" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "mkdir standalone binary for secure-exec VM" + +[[bin]] +name = "mkdir" +path = "src/main.rs" + +[dependencies] +uu_mkdir = "0.7.0" diff --git a/registry/native/crates/commands/mkdir/src/main.rs b/software/coreutils/native/crates/cmd-mkdir/src/main.rs similarity index 100% rename from registry/native/crates/commands/mkdir/src/main.rs rename to software/coreutils/native/crates/cmd-mkdir/src/main.rs diff --git a/software/coreutils/native/crates/cmd-mktemp/Cargo.toml b/software/coreutils/native/crates/cmd-mktemp/Cargo.toml new file mode 100644 index 0000000000..57935df632 --- /dev/null +++ b/software/coreutils/native/crates/cmd-mktemp/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-mktemp" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "mktemp standalone binary for secure-exec VM" + +[[bin]] +name = "mktemp" +path = "src/main.rs" + +[dependencies] +uu_mktemp = "0.7.0" diff --git a/registry/native/crates/commands/mktemp/src/main.rs b/software/coreutils/native/crates/cmd-mktemp/src/main.rs similarity index 100% rename from registry/native/crates/commands/mktemp/src/main.rs rename to software/coreutils/native/crates/cmd-mktemp/src/main.rs diff --git a/software/coreutils/native/crates/cmd-mv/Cargo.toml b/software/coreutils/native/crates/cmd-mv/Cargo.toml new file mode 100644 index 0000000000..c4999d907c --- /dev/null +++ b/software/coreutils/native/crates/cmd-mv/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-mv" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "mv standalone binary for secure-exec VM" + +[[bin]] +name = "mv" +path = "src/main.rs" + +[dependencies] +uu_mv = "0.7.0" diff --git a/registry/native/crates/commands/mv/src/main.rs b/software/coreutils/native/crates/cmd-mv/src/main.rs similarity index 100% rename from registry/native/crates/commands/mv/src/main.rs rename to software/coreutils/native/crates/cmd-mv/src/main.rs diff --git a/registry/native/crates/commands/mv/tests/simple_mv.rs b/software/coreutils/native/crates/cmd-mv/tests/simple_mv.rs similarity index 100% rename from registry/native/crates/commands/mv/tests/simple_mv.rs rename to software/coreutils/native/crates/cmd-mv/tests/simple_mv.rs diff --git a/software/coreutils/native/crates/cmd-nice/Cargo.toml b/software/coreutils/native/crates/cmd-nice/Cargo.toml new file mode 100644 index 0000000000..2f77be052e --- /dev/null +++ b/software/coreutils/native/crates/cmd-nice/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-nice" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "nice standalone binary for secure-exec VM" + +[[bin]] +name = "nice" +path = "src/main.rs" + +[dependencies] +shims = { package = "secureexec-shims", path = "../../../../../toolchain/crates/libs/shims" } diff --git a/registry/native/crates/commands/nice/src/main.rs b/software/coreutils/native/crates/cmd-nice/src/main.rs similarity index 100% rename from registry/native/crates/commands/nice/src/main.rs rename to software/coreutils/native/crates/cmd-nice/src/main.rs diff --git a/software/coreutils/native/crates/cmd-nl/Cargo.toml b/software/coreutils/native/crates/cmd-nl/Cargo.toml new file mode 100644 index 0000000000..067baf0805 --- /dev/null +++ b/software/coreutils/native/crates/cmd-nl/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-nl" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "nl standalone binary for secure-exec VM" + +[[bin]] +name = "nl" +path = "src/main.rs" + +[dependencies] +uu_nl = "0.7.0" diff --git a/registry/native/crates/commands/nl/src/main.rs b/software/coreutils/native/crates/cmd-nl/src/main.rs similarity index 100% rename from registry/native/crates/commands/nl/src/main.rs rename to software/coreutils/native/crates/cmd-nl/src/main.rs diff --git a/software/coreutils/native/crates/cmd-nohup/Cargo.toml b/software/coreutils/native/crates/cmd-nohup/Cargo.toml new file mode 100644 index 0000000000..94edf64e00 --- /dev/null +++ b/software/coreutils/native/crates/cmd-nohup/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-nohup" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "nohup standalone binary for secure-exec VM" + +[[bin]] +name = "nohup" +path = "src/main.rs" + +[dependencies] +shims = { package = "secureexec-shims", path = "../../../../../toolchain/crates/libs/shims" } diff --git a/registry/native/crates/commands/nohup/src/main.rs b/software/coreutils/native/crates/cmd-nohup/src/main.rs similarity index 100% rename from registry/native/crates/commands/nohup/src/main.rs rename to software/coreutils/native/crates/cmd-nohup/src/main.rs diff --git a/registry/native/crates/commands/nohup/tests/streaming.rs b/software/coreutils/native/crates/cmd-nohup/tests/streaming.rs similarity index 100% rename from registry/native/crates/commands/nohup/tests/streaming.rs rename to software/coreutils/native/crates/cmd-nohup/tests/streaming.rs diff --git a/software/coreutils/native/crates/cmd-nproc/Cargo.toml b/software/coreutils/native/crates/cmd-nproc/Cargo.toml new file mode 100644 index 0000000000..9c6fc0fbbb --- /dev/null +++ b/software/coreutils/native/crates/cmd-nproc/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-nproc" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "nproc standalone binary for secure-exec VM" + +[[bin]] +name = "nproc" +path = "src/main.rs" + +[dependencies] +uu_nproc = "0.7.0" diff --git a/registry/native/crates/commands/nproc/src/main.rs b/software/coreutils/native/crates/cmd-nproc/src/main.rs similarity index 100% rename from registry/native/crates/commands/nproc/src/main.rs rename to software/coreutils/native/crates/cmd-nproc/src/main.rs diff --git a/software/coreutils/native/crates/cmd-numfmt/Cargo.toml b/software/coreutils/native/crates/cmd-numfmt/Cargo.toml new file mode 100644 index 0000000000..6ae2fedb83 --- /dev/null +++ b/software/coreutils/native/crates/cmd-numfmt/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-numfmt" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "numfmt standalone binary for secure-exec VM" + +[[bin]] +name = "numfmt" +path = "src/main.rs" + +[dependencies] +uu_numfmt = "0.7.0" diff --git a/registry/native/crates/commands/numfmt/src/main.rs b/software/coreutils/native/crates/cmd-numfmt/src/main.rs similarity index 100% rename from registry/native/crates/commands/numfmt/src/main.rs rename to software/coreutils/native/crates/cmd-numfmt/src/main.rs diff --git a/software/coreutils/native/crates/cmd-od/Cargo.toml b/software/coreutils/native/crates/cmd-od/Cargo.toml new file mode 100644 index 0000000000..102a12cdab --- /dev/null +++ b/software/coreutils/native/crates/cmd-od/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-od" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "od standalone binary for secure-exec VM" + +[[bin]] +name = "od" +path = "src/main.rs" + +[dependencies] +uu_od = "0.7.0" diff --git a/registry/native/crates/commands/od/src/main.rs b/software/coreutils/native/crates/cmd-od/src/main.rs similarity index 100% rename from registry/native/crates/commands/od/src/main.rs rename to software/coreutils/native/crates/cmd-od/src/main.rs diff --git a/software/coreutils/native/crates/cmd-paste/Cargo.toml b/software/coreutils/native/crates/cmd-paste/Cargo.toml new file mode 100644 index 0000000000..8e45b33c09 --- /dev/null +++ b/software/coreutils/native/crates/cmd-paste/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-paste" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "paste standalone binary for secure-exec VM" + +[[bin]] +name = "paste" +path = "src/main.rs" + +[dependencies] +uu_paste = "0.7.0" diff --git a/registry/native/crates/commands/paste/src/main.rs b/software/coreutils/native/crates/cmd-paste/src/main.rs similarity index 100% rename from registry/native/crates/commands/paste/src/main.rs rename to software/coreutils/native/crates/cmd-paste/src/main.rs diff --git a/software/coreutils/native/crates/cmd-pathchk/Cargo.toml b/software/coreutils/native/crates/cmd-pathchk/Cargo.toml new file mode 100644 index 0000000000..b675dc27f1 --- /dev/null +++ b/software/coreutils/native/crates/cmd-pathchk/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-pathchk" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "pathchk standalone binary for secure-exec VM" + +[[bin]] +name = "pathchk" +path = "src/main.rs" + +[dependencies] +uu_pathchk = "0.7.0" diff --git a/registry/native/crates/commands/pathchk/src/main.rs b/software/coreutils/native/crates/cmd-pathchk/src/main.rs similarity index 100% rename from registry/native/crates/commands/pathchk/src/main.rs rename to software/coreutils/native/crates/cmd-pathchk/src/main.rs diff --git a/software/coreutils/native/crates/cmd-printenv/Cargo.toml b/software/coreutils/native/crates/cmd-printenv/Cargo.toml new file mode 100644 index 0000000000..918b962ea6 --- /dev/null +++ b/software/coreutils/native/crates/cmd-printenv/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-printenv" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "printenv standalone binary for secure-exec VM" + +[[bin]] +name = "printenv" +path = "src/main.rs" + +[dependencies] +uu_printenv = "0.7.0" diff --git a/registry/native/crates/commands/printenv/src/main.rs b/software/coreutils/native/crates/cmd-printenv/src/main.rs similarity index 100% rename from registry/native/crates/commands/printenv/src/main.rs rename to software/coreutils/native/crates/cmd-printenv/src/main.rs diff --git a/software/coreutils/native/crates/cmd-printf/Cargo.toml b/software/coreutils/native/crates/cmd-printf/Cargo.toml new file mode 100644 index 0000000000..c0512cfd17 --- /dev/null +++ b/software/coreutils/native/crates/cmd-printf/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-printf" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "printf standalone binary for secure-exec VM" + +[[bin]] +name = "printf" +path = "src/main.rs" + +[dependencies] +uu_printf = "0.7.0" diff --git a/registry/native/crates/commands/printf/src/main.rs b/software/coreutils/native/crates/cmd-printf/src/main.rs similarity index 100% rename from registry/native/crates/commands/printf/src/main.rs rename to software/coreutils/native/crates/cmd-printf/src/main.rs diff --git a/software/coreutils/native/crates/cmd-ptx/Cargo.toml b/software/coreutils/native/crates/cmd-ptx/Cargo.toml new file mode 100644 index 0000000000..9eefecd190 --- /dev/null +++ b/software/coreutils/native/crates/cmd-ptx/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-ptx" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "ptx standalone binary for secure-exec VM" + +[[bin]] +name = "ptx" +path = "src/main.rs" + +[dependencies] +uu_ptx = "0.7.0" diff --git a/registry/native/crates/commands/ptx/src/main.rs b/software/coreutils/native/crates/cmd-ptx/src/main.rs similarity index 100% rename from registry/native/crates/commands/ptx/src/main.rs rename to software/coreutils/native/crates/cmd-ptx/src/main.rs diff --git a/software/coreutils/native/crates/cmd-pwd/Cargo.toml b/software/coreutils/native/crates/cmd-pwd/Cargo.toml new file mode 100644 index 0000000000..b3e7ff6a74 --- /dev/null +++ b/software/coreutils/native/crates/cmd-pwd/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-pwd" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "pwd standalone binary for secure-exec VM" + +[[bin]] +name = "pwd" +path = "src/main.rs" + +[dependencies] +uu_pwd = "0.7.0" diff --git a/registry/native/crates/commands/pwd/src/main.rs b/software/coreutils/native/crates/cmd-pwd/src/main.rs similarity index 100% rename from registry/native/crates/commands/pwd/src/main.rs rename to software/coreutils/native/crates/cmd-pwd/src/main.rs diff --git a/software/coreutils/native/crates/cmd-readlink/Cargo.toml b/software/coreutils/native/crates/cmd-readlink/Cargo.toml new file mode 100644 index 0000000000..7fa48bbe6f --- /dev/null +++ b/software/coreutils/native/crates/cmd-readlink/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-readlink" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "readlink standalone binary for secure-exec VM" + +[[bin]] +name = "readlink" +path = "src/main.rs" + +[dependencies] +uu_readlink = "0.7.0" diff --git a/registry/native/crates/commands/readlink/src/main.rs b/software/coreutils/native/crates/cmd-readlink/src/main.rs similarity index 100% rename from registry/native/crates/commands/readlink/src/main.rs rename to software/coreutils/native/crates/cmd-readlink/src/main.rs diff --git a/software/coreutils/native/crates/cmd-realpath/Cargo.toml b/software/coreutils/native/crates/cmd-realpath/Cargo.toml new file mode 100644 index 0000000000..dbbbc9da3a --- /dev/null +++ b/software/coreutils/native/crates/cmd-realpath/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-realpath" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "realpath standalone binary for secure-exec VM" + +[[bin]] +name = "realpath" +path = "src/main.rs" + +[dependencies] +uu_realpath = "0.7.0" diff --git a/registry/native/crates/commands/realpath/src/main.rs b/software/coreutils/native/crates/cmd-realpath/src/main.rs similarity index 100% rename from registry/native/crates/commands/realpath/src/main.rs rename to software/coreutils/native/crates/cmd-realpath/src/main.rs diff --git a/software/coreutils/native/crates/cmd-rev/Cargo.toml b/software/coreutils/native/crates/cmd-rev/Cargo.toml new file mode 100644 index 0000000000..66a1ffd19d --- /dev/null +++ b/software/coreutils/native/crates/cmd-rev/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-rev" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "rev standalone binary for secure-exec VM" + +[[bin]] +name = "rev" +path = "src/main.rs" + +[dependencies] +secureexec-rev = { path = "../rev" } diff --git a/registry/native/crates/commands/rev/src/main.rs b/software/coreutils/native/crates/cmd-rev/src/main.rs similarity index 100% rename from registry/native/crates/commands/rev/src/main.rs rename to software/coreutils/native/crates/cmd-rev/src/main.rs diff --git a/software/coreutils/native/crates/cmd-rm/Cargo.toml b/software/coreutils/native/crates/cmd-rm/Cargo.toml new file mode 100644 index 0000000000..4a9dc744e7 --- /dev/null +++ b/software/coreutils/native/crates/cmd-rm/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-rm" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "rm standalone binary for secure-exec VM" + +[[bin]] +name = "rm" +path = "src/main.rs" + +[dependencies] +uu_rm = "0.7.0" diff --git a/registry/native/crates/commands/rm/src/main.rs b/software/coreutils/native/crates/cmd-rm/src/main.rs similarity index 100% rename from registry/native/crates/commands/rm/src/main.rs rename to software/coreutils/native/crates/cmd-rm/src/main.rs diff --git a/software/coreutils/native/crates/cmd-rmdir/Cargo.toml b/software/coreutils/native/crates/cmd-rmdir/Cargo.toml new file mode 100644 index 0000000000..f9729df6f7 --- /dev/null +++ b/software/coreutils/native/crates/cmd-rmdir/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-rmdir" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "rmdir standalone binary for secure-exec VM" + +[[bin]] +name = "rmdir" +path = "src/main.rs" + +[dependencies] +uu_rmdir = "0.7.0" diff --git a/registry/native/crates/commands/rmdir/src/main.rs b/software/coreutils/native/crates/cmd-rmdir/src/main.rs similarity index 100% rename from registry/native/crates/commands/rmdir/src/main.rs rename to software/coreutils/native/crates/cmd-rmdir/src/main.rs diff --git a/software/coreutils/native/crates/cmd-seq/Cargo.toml b/software/coreutils/native/crates/cmd-seq/Cargo.toml new file mode 100644 index 0000000000..bb035b54fc --- /dev/null +++ b/software/coreutils/native/crates/cmd-seq/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-seq" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "seq standalone binary for secure-exec VM" + +[[bin]] +name = "seq" +path = "src/main.rs" + +[dependencies] +uu_seq = "0.7.0" diff --git a/registry/native/crates/commands/seq/src/main.rs b/software/coreutils/native/crates/cmd-seq/src/main.rs similarity index 100% rename from registry/native/crates/commands/seq/src/main.rs rename to software/coreutils/native/crates/cmd-seq/src/main.rs diff --git a/software/coreutils/native/crates/cmd-sh/Cargo.toml b/software/coreutils/native/crates/cmd-sh/Cargo.toml new file mode 100644 index 0000000000..51976f0cc4 --- /dev/null +++ b/software/coreutils/native/crates/cmd-sh/Cargo.toml @@ -0,0 +1,19 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-sh" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "sh standalone binary for secure-exec VM" + +[[bin]] +name = "sh" +path = "src/main.rs" + +# `reedline` is enabled on ALL targets, including wasm: the vendored wasi +# patches supply what it needs (crossterm wasi backend, fd-lock, and +# brush-interactive's cfg(target_os = "wasi") alternatives to the +# tokio block_in_place paths), per the "behave like native Linux" rule. +# `minimal` stays available as an explicit opt-out backend. +[dependencies] +brush-shell = { version = "0.3.0", default-features = false, features = ["reedline", "minimal"] } diff --git a/registry/native/crates/commands/sh/src/main.rs b/software/coreutils/native/crates/cmd-sh/src/main.rs similarity index 100% rename from registry/native/crates/commands/sh/src/main.rs rename to software/coreutils/native/crates/cmd-sh/src/main.rs diff --git a/software/coreutils/native/crates/cmd-sha1sum/Cargo.toml b/software/coreutils/native/crates/cmd-sha1sum/Cargo.toml new file mode 100644 index 0000000000..1e4b91ec01 --- /dev/null +++ b/software/coreutils/native/crates/cmd-sha1sum/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-sha1sum" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "sha1sum standalone binary for secure-exec VM" + +[[bin]] +name = "sha1sum" +path = "src/main.rs" + +[dependencies] +uu_sha1sum = "0.7.0" diff --git a/registry/native/crates/commands/sha1sum/src/main.rs b/software/coreutils/native/crates/cmd-sha1sum/src/main.rs similarity index 100% rename from registry/native/crates/commands/sha1sum/src/main.rs rename to software/coreutils/native/crates/cmd-sha1sum/src/main.rs diff --git a/software/coreutils/native/crates/cmd-sha224sum/Cargo.toml b/software/coreutils/native/crates/cmd-sha224sum/Cargo.toml new file mode 100644 index 0000000000..bdd6f4cb70 --- /dev/null +++ b/software/coreutils/native/crates/cmd-sha224sum/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-sha224sum" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "sha224sum standalone binary for secure-exec VM" + +[[bin]] +name = "sha224sum" +path = "src/main.rs" + +[dependencies] +uu_sha224sum = "0.7.0" diff --git a/registry/native/crates/commands/sha224sum/src/main.rs b/software/coreutils/native/crates/cmd-sha224sum/src/main.rs similarity index 100% rename from registry/native/crates/commands/sha224sum/src/main.rs rename to software/coreutils/native/crates/cmd-sha224sum/src/main.rs diff --git a/software/coreutils/native/crates/cmd-sha256sum/Cargo.toml b/software/coreutils/native/crates/cmd-sha256sum/Cargo.toml new file mode 100644 index 0000000000..a43ebd9ff9 --- /dev/null +++ b/software/coreutils/native/crates/cmd-sha256sum/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-sha256sum" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "sha256sum standalone binary for secure-exec VM" + +[[bin]] +name = "sha256sum" +path = "src/main.rs" + +[dependencies] +uu_sha256sum = "0.7.0" diff --git a/registry/native/crates/commands/sha256sum/src/main.rs b/software/coreutils/native/crates/cmd-sha256sum/src/main.rs similarity index 100% rename from registry/native/crates/commands/sha256sum/src/main.rs rename to software/coreutils/native/crates/cmd-sha256sum/src/main.rs diff --git a/software/coreutils/native/crates/cmd-sha384sum/Cargo.toml b/software/coreutils/native/crates/cmd-sha384sum/Cargo.toml new file mode 100644 index 0000000000..3cec44da9d --- /dev/null +++ b/software/coreutils/native/crates/cmd-sha384sum/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-sha384sum" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "sha384sum standalone binary for secure-exec VM" + +[[bin]] +name = "sha384sum" +path = "src/main.rs" + +[dependencies] +uu_sha384sum = "0.7.0" diff --git a/registry/native/crates/commands/sha384sum/src/main.rs b/software/coreutils/native/crates/cmd-sha384sum/src/main.rs similarity index 100% rename from registry/native/crates/commands/sha384sum/src/main.rs rename to software/coreutils/native/crates/cmd-sha384sum/src/main.rs diff --git a/software/coreutils/native/crates/cmd-sha512sum/Cargo.toml b/software/coreutils/native/crates/cmd-sha512sum/Cargo.toml new file mode 100644 index 0000000000..0d15e9ac32 --- /dev/null +++ b/software/coreutils/native/crates/cmd-sha512sum/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-sha512sum" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "sha512sum standalone binary for secure-exec VM" + +[[bin]] +name = "sha512sum" +path = "src/main.rs" + +[dependencies] +uu_sha512sum = "0.7.0" diff --git a/registry/native/crates/commands/sha512sum/src/main.rs b/software/coreutils/native/crates/cmd-sha512sum/src/main.rs similarity index 100% rename from registry/native/crates/commands/sha512sum/src/main.rs rename to software/coreutils/native/crates/cmd-sha512sum/src/main.rs diff --git a/software/coreutils/native/crates/cmd-shred/Cargo.toml b/software/coreutils/native/crates/cmd-shred/Cargo.toml new file mode 100644 index 0000000000..15cb98f87c --- /dev/null +++ b/software/coreutils/native/crates/cmd-shred/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-shred" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "shred standalone binary for secure-exec VM" + +[[bin]] +name = "shred" +path = "src/main.rs" + +[dependencies] +uu_shred = "0.7.0" diff --git a/registry/native/crates/commands/shred/src/main.rs b/software/coreutils/native/crates/cmd-shred/src/main.rs similarity index 100% rename from registry/native/crates/commands/shred/src/main.rs rename to software/coreutils/native/crates/cmd-shred/src/main.rs diff --git a/software/coreutils/native/crates/cmd-shuf/Cargo.toml b/software/coreutils/native/crates/cmd-shuf/Cargo.toml new file mode 100644 index 0000000000..06dc44cf52 --- /dev/null +++ b/software/coreutils/native/crates/cmd-shuf/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-shuf" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "shuf standalone binary for secure-exec VM" + +[[bin]] +name = "shuf" +path = "src/main.rs" + +[dependencies] +uu_shuf = "0.7.0" diff --git a/registry/native/crates/commands/shuf/src/main.rs b/software/coreutils/native/crates/cmd-shuf/src/main.rs similarity index 100% rename from registry/native/crates/commands/shuf/src/main.rs rename to software/coreutils/native/crates/cmd-shuf/src/main.rs diff --git a/software/coreutils/native/crates/cmd-sleep/Cargo.toml b/software/coreutils/native/crates/cmd-sleep/Cargo.toml new file mode 100644 index 0000000000..816b051b22 --- /dev/null +++ b/software/coreutils/native/crates/cmd-sleep/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-sleep" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "sleep standalone binary for secure-exec VM" + +[[bin]] +name = "sleep" +path = "src/main.rs" + +[dependencies] +secureexec-builtins = { path = "../../../../../toolchain/crates/libs/builtins" } diff --git a/registry/native/crates/commands/sleep/src/main.rs b/software/coreutils/native/crates/cmd-sleep/src/main.rs similarity index 100% rename from registry/native/crates/commands/sleep/src/main.rs rename to software/coreutils/native/crates/cmd-sleep/src/main.rs diff --git a/software/coreutils/native/crates/cmd-sort/Cargo.toml b/software/coreutils/native/crates/cmd-sort/Cargo.toml new file mode 100644 index 0000000000..0507575a3b --- /dev/null +++ b/software/coreutils/native/crates/cmd-sort/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-sort" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "sort standalone binary for secure-exec VM" + +[[bin]] +name = "sort" +path = "src/main.rs" + +[dependencies] +uu_sort = "0.7.0" diff --git a/registry/native/crates/commands/sort/src/main.rs b/software/coreutils/native/crates/cmd-sort/src/main.rs similarity index 100% rename from registry/native/crates/commands/sort/src/main.rs rename to software/coreutils/native/crates/cmd-sort/src/main.rs diff --git a/registry/native/crates/commands/sort/tests/external_sort.rs b/software/coreutils/native/crates/cmd-sort/tests/external_sort.rs similarity index 100% rename from registry/native/crates/commands/sort/tests/external_sort.rs rename to software/coreutils/native/crates/cmd-sort/tests/external_sort.rs diff --git a/software/coreutils/native/crates/cmd-split/Cargo.toml b/software/coreutils/native/crates/cmd-split/Cargo.toml new file mode 100644 index 0000000000..c0aa7cc026 --- /dev/null +++ b/software/coreutils/native/crates/cmd-split/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-split" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "split standalone binary for secure-exec VM" + +[[bin]] +name = "split" +path = "src/main.rs" + +[dependencies] +uu_split = "0.7.0" diff --git a/registry/native/crates/commands/split/src/main.rs b/software/coreutils/native/crates/cmd-split/src/main.rs similarity index 100% rename from registry/native/crates/commands/split/src/main.rs rename to software/coreutils/native/crates/cmd-split/src/main.rs diff --git a/software/coreutils/native/crates/cmd-stat/Cargo.toml b/software/coreutils/native/crates/cmd-stat/Cargo.toml new file mode 100644 index 0000000000..ee741b2614 --- /dev/null +++ b/software/coreutils/native/crates/cmd-stat/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-stat" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "stat standalone binary for secure-exec VM" + +[[bin]] +name = "stat" +path = "src/main.rs" + +[dependencies] +uu_stat = "0.7.0" diff --git a/registry/native/crates/commands/stat/src/main.rs b/software/coreutils/native/crates/cmd-stat/src/main.rs similarity index 100% rename from registry/native/crates/commands/stat/src/main.rs rename to software/coreutils/native/crates/cmd-stat/src/main.rs diff --git a/software/coreutils/native/crates/cmd-stdbuf/Cargo.toml b/software/coreutils/native/crates/cmd-stdbuf/Cargo.toml new file mode 100644 index 0000000000..499e2ef4d7 --- /dev/null +++ b/software/coreutils/native/crates/cmd-stdbuf/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-stdbuf" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "stdbuf standalone binary for secure-exec VM" + +[[bin]] +name = "stdbuf" +path = "src/main.rs" + +[dependencies] +shims = { package = "secureexec-shims", path = "../../../../../toolchain/crates/libs/shims" } diff --git a/registry/native/crates/commands/stdbuf/src/main.rs b/software/coreutils/native/crates/cmd-stdbuf/src/main.rs similarity index 100% rename from registry/native/crates/commands/stdbuf/src/main.rs rename to software/coreutils/native/crates/cmd-stdbuf/src/main.rs diff --git a/registry/native/crates/commands/stdbuf/tests/streaming.rs b/software/coreutils/native/crates/cmd-stdbuf/tests/streaming.rs similarity index 100% rename from registry/native/crates/commands/stdbuf/tests/streaming.rs rename to software/coreutils/native/crates/cmd-stdbuf/tests/streaming.rs diff --git a/software/coreutils/native/crates/cmd-strings/Cargo.toml b/software/coreutils/native/crates/cmd-strings/Cargo.toml new file mode 100644 index 0000000000..4ad7da7c02 --- /dev/null +++ b/software/coreutils/native/crates/cmd-strings/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-strings" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "strings standalone binary for secure-exec VM" + +[[bin]] +name = "strings" +path = "src/main.rs" + +[dependencies] +secureexec-strings-cmd = { path = "../strings-cmd" } diff --git a/registry/native/crates/commands/strings/src/main.rs b/software/coreutils/native/crates/cmd-strings/src/main.rs similarity index 100% rename from registry/native/crates/commands/strings/src/main.rs rename to software/coreutils/native/crates/cmd-strings/src/main.rs diff --git a/software/coreutils/native/crates/cmd-sum/Cargo.toml b/software/coreutils/native/crates/cmd-sum/Cargo.toml new file mode 100644 index 0000000000..5b07f6452b --- /dev/null +++ b/software/coreutils/native/crates/cmd-sum/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-sum" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "sum standalone binary for secure-exec VM" + +[[bin]] +name = "sum" +path = "src/main.rs" + +[dependencies] +uu_sum = "0.7.0" diff --git a/registry/native/crates/commands/sum/src/main.rs b/software/coreutils/native/crates/cmd-sum/src/main.rs similarity index 100% rename from registry/native/crates/commands/sum/src/main.rs rename to software/coreutils/native/crates/cmd-sum/src/main.rs diff --git a/software/coreutils/native/crates/cmd-tac/Cargo.toml b/software/coreutils/native/crates/cmd-tac/Cargo.toml new file mode 100644 index 0000000000..f0069010db --- /dev/null +++ b/software/coreutils/native/crates/cmd-tac/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-tac" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "tac standalone binary for secure-exec VM" + +[[bin]] +name = "tac" +path = "src/main.rs" + +[dependencies] +uu_tac = "0.7.0" diff --git a/registry/native/crates/commands/tac/src/main.rs b/software/coreutils/native/crates/cmd-tac/src/main.rs similarity index 100% rename from registry/native/crates/commands/tac/src/main.rs rename to software/coreutils/native/crates/cmd-tac/src/main.rs diff --git a/software/coreutils/native/crates/cmd-tail/Cargo.toml b/software/coreutils/native/crates/cmd-tail/Cargo.toml new file mode 100644 index 0000000000..7a14ea0dba --- /dev/null +++ b/software/coreutils/native/crates/cmd-tail/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-tail" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "tail standalone binary for secure-exec VM" + +[[bin]] +name = "tail" +path = "src/main.rs" + +[dependencies] +uu_tail = "0.7.0" diff --git a/registry/native/crates/commands/tail/src/main.rs b/software/coreutils/native/crates/cmd-tail/src/main.rs similarity index 100% rename from registry/native/crates/commands/tail/src/main.rs rename to software/coreutils/native/crates/cmd-tail/src/main.rs diff --git a/software/coreutils/native/crates/cmd-tee/Cargo.toml b/software/coreutils/native/crates/cmd-tee/Cargo.toml new file mode 100644 index 0000000000..c94c0664e7 --- /dev/null +++ b/software/coreutils/native/crates/cmd-tee/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-tee" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "tee standalone binary for secure-exec VM" + +[[bin]] +name = "tee" +path = "src/main.rs" + +[dependencies] +uu_tee = "0.7.0" diff --git a/registry/native/crates/commands/tee/src/main.rs b/software/coreutils/native/crates/cmd-tee/src/main.rs similarity index 100% rename from registry/native/crates/commands/tee/src/main.rs rename to software/coreutils/native/crates/cmd-tee/src/main.rs diff --git a/software/coreutils/native/crates/cmd-test/Cargo.toml b/software/coreutils/native/crates/cmd-test/Cargo.toml new file mode 100644 index 0000000000..1a1581e18b --- /dev/null +++ b/software/coreutils/native/crates/cmd-test/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-test" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "test/[ standalone binary for secure-exec VM" + +[[bin]] +name = "test" +path = "src/main.rs" + +[dependencies] +secureexec-builtins = { path = "../../../../../toolchain/crates/libs/builtins" } diff --git a/registry/native/crates/commands/test/src/main.rs b/software/coreutils/native/crates/cmd-test/src/main.rs similarity index 100% rename from registry/native/crates/commands/test/src/main.rs rename to software/coreutils/native/crates/cmd-test/src/main.rs diff --git a/software/coreutils/native/crates/cmd-timeout/Cargo.toml b/software/coreutils/native/crates/cmd-timeout/Cargo.toml new file mode 100644 index 0000000000..af6e289f59 --- /dev/null +++ b/software/coreutils/native/crates/cmd-timeout/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-timeout" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "timeout standalone binary for secure-exec VM" + +[[bin]] +name = "timeout" +path = "src/main.rs" + +[dependencies] +shims = { package = "secureexec-shims", path = "../../../../../toolchain/crates/libs/shims" } diff --git a/registry/native/crates/commands/timeout/src/main.rs b/software/coreutils/native/crates/cmd-timeout/src/main.rs similarity index 100% rename from registry/native/crates/commands/timeout/src/main.rs rename to software/coreutils/native/crates/cmd-timeout/src/main.rs diff --git a/software/coreutils/native/crates/cmd-touch/Cargo.toml b/software/coreutils/native/crates/cmd-touch/Cargo.toml new file mode 100644 index 0000000000..6353e89c77 --- /dev/null +++ b/software/coreutils/native/crates/cmd-touch/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-touch" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "touch standalone binary for secure-exec VM" + +[[bin]] +name = "touch" +path = "src/main.rs" + +[dependencies] +uu_touch = "0.7.0" diff --git a/registry/native/crates/commands/touch/src/main.rs b/software/coreutils/native/crates/cmd-touch/src/main.rs similarity index 100% rename from registry/native/crates/commands/touch/src/main.rs rename to software/coreutils/native/crates/cmd-touch/src/main.rs diff --git a/software/coreutils/native/crates/cmd-tr/Cargo.toml b/software/coreutils/native/crates/cmd-tr/Cargo.toml new file mode 100644 index 0000000000..c11682cac7 --- /dev/null +++ b/software/coreutils/native/crates/cmd-tr/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-tr" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "tr standalone binary for secure-exec VM" + +[[bin]] +name = "tr" +path = "src/main.rs" + +[dependencies] +uu_tr = "0.7.0" diff --git a/registry/native/crates/commands/tr/src/main.rs b/software/coreutils/native/crates/cmd-tr/src/main.rs similarity index 100% rename from registry/native/crates/commands/tr/src/main.rs rename to software/coreutils/native/crates/cmd-tr/src/main.rs diff --git a/software/coreutils/native/crates/cmd-true/Cargo.toml b/software/coreutils/native/crates/cmd-true/Cargo.toml new file mode 100644 index 0000000000..bed79e45c4 --- /dev/null +++ b/software/coreutils/native/crates/cmd-true/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-true" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "true standalone binary for secure-exec VM" + +[[bin]] +name = "true" +path = "src/main.rs" + +[dependencies] +uu_true = "0.7.0" diff --git a/registry/native/crates/commands/true/src/main.rs b/software/coreutils/native/crates/cmd-true/src/main.rs similarity index 100% rename from registry/native/crates/commands/true/src/main.rs rename to software/coreutils/native/crates/cmd-true/src/main.rs diff --git a/software/coreutils/native/crates/cmd-truncate/Cargo.toml b/software/coreutils/native/crates/cmd-truncate/Cargo.toml new file mode 100644 index 0000000000..65fa7e80fd --- /dev/null +++ b/software/coreutils/native/crates/cmd-truncate/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-truncate" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "truncate standalone binary for secure-exec VM" + +[[bin]] +name = "truncate" +path = "src/main.rs" + +[dependencies] +uu_truncate = "0.7.0" diff --git a/registry/native/crates/commands/truncate/src/main.rs b/software/coreutils/native/crates/cmd-truncate/src/main.rs similarity index 100% rename from registry/native/crates/commands/truncate/src/main.rs rename to software/coreutils/native/crates/cmd-truncate/src/main.rs diff --git a/software/coreutils/native/crates/cmd-tsort/Cargo.toml b/software/coreutils/native/crates/cmd-tsort/Cargo.toml new file mode 100644 index 0000000000..b0e057b0c8 --- /dev/null +++ b/software/coreutils/native/crates/cmd-tsort/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-tsort" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "tsort standalone binary for secure-exec VM" + +[[bin]] +name = "tsort" +path = "src/main.rs" + +[dependencies] +uu_tsort = "0.7.0" diff --git a/registry/native/crates/commands/tsort/src/main.rs b/software/coreutils/native/crates/cmd-tsort/src/main.rs similarity index 100% rename from registry/native/crates/commands/tsort/src/main.rs rename to software/coreutils/native/crates/cmd-tsort/src/main.rs diff --git a/software/coreutils/native/crates/cmd-uname/Cargo.toml b/software/coreutils/native/crates/cmd-uname/Cargo.toml new file mode 100644 index 0000000000..b0f0a8c71a --- /dev/null +++ b/software/coreutils/native/crates/cmd-uname/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-uname" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "uname standalone binary for secure-exec VM" + +[[bin]] +name = "uname" +path = "src/main.rs" + +[dependencies] +uu_uname = "0.7.0" diff --git a/registry/native/crates/commands/uname/src/main.rs b/software/coreutils/native/crates/cmd-uname/src/main.rs similarity index 100% rename from registry/native/crates/commands/uname/src/main.rs rename to software/coreutils/native/crates/cmd-uname/src/main.rs diff --git a/software/coreutils/native/crates/cmd-unexpand/Cargo.toml b/software/coreutils/native/crates/cmd-unexpand/Cargo.toml new file mode 100644 index 0000000000..d8a9dc9e78 --- /dev/null +++ b/software/coreutils/native/crates/cmd-unexpand/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-unexpand" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "unexpand standalone binary for secure-exec VM" + +[[bin]] +name = "unexpand" +path = "src/main.rs" + +[dependencies] +uu_unexpand = "0.7.0" diff --git a/registry/native/crates/commands/unexpand/src/main.rs b/software/coreutils/native/crates/cmd-unexpand/src/main.rs similarity index 100% rename from registry/native/crates/commands/unexpand/src/main.rs rename to software/coreutils/native/crates/cmd-unexpand/src/main.rs diff --git a/software/coreutils/native/crates/cmd-uniq/Cargo.toml b/software/coreutils/native/crates/cmd-uniq/Cargo.toml new file mode 100644 index 0000000000..c3ead17984 --- /dev/null +++ b/software/coreutils/native/crates/cmd-uniq/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-uniq" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "uniq standalone binary for secure-exec VM" + +[[bin]] +name = "uniq" +path = "src/main.rs" + +[dependencies] +uu_uniq = "0.7.0" diff --git a/registry/native/crates/commands/uniq/src/main.rs b/software/coreutils/native/crates/cmd-uniq/src/main.rs similarity index 100% rename from registry/native/crates/commands/uniq/src/main.rs rename to software/coreutils/native/crates/cmd-uniq/src/main.rs diff --git a/software/coreutils/native/crates/cmd-unlink/Cargo.toml b/software/coreutils/native/crates/cmd-unlink/Cargo.toml new file mode 100644 index 0000000000..eb669d0add --- /dev/null +++ b/software/coreutils/native/crates/cmd-unlink/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-unlink" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "unlink standalone binary for secure-exec VM" + +[[bin]] +name = "unlink" +path = "src/main.rs" + +[dependencies] +uu_unlink = "0.7.0" diff --git a/registry/native/crates/commands/unlink/src/main.rs b/software/coreutils/native/crates/cmd-unlink/src/main.rs similarity index 100% rename from registry/native/crates/commands/unlink/src/main.rs rename to software/coreutils/native/crates/cmd-unlink/src/main.rs diff --git a/software/coreutils/native/crates/cmd-wc/Cargo.toml b/software/coreutils/native/crates/cmd-wc/Cargo.toml new file mode 100644 index 0000000000..728b9fdad7 --- /dev/null +++ b/software/coreutils/native/crates/cmd-wc/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-wc" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "wc standalone binary for secure-exec VM" + +[[bin]] +name = "wc" +path = "src/main.rs" + +[dependencies] +uu_wc = "0.7.0" diff --git a/registry/native/crates/commands/wc/src/main.rs b/software/coreutils/native/crates/cmd-wc/src/main.rs similarity index 100% rename from registry/native/crates/commands/wc/src/main.rs rename to software/coreutils/native/crates/cmd-wc/src/main.rs diff --git a/software/coreutils/native/crates/cmd-which/Cargo.toml b/software/coreutils/native/crates/cmd-which/Cargo.toml new file mode 100644 index 0000000000..0ea0f38b94 --- /dev/null +++ b/software/coreutils/native/crates/cmd-which/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-which" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "which standalone binary for secure-exec VM" + +[[bin]] +name = "which" +path = "src/main.rs" + +[dependencies] +shims = { package = "secureexec-shims", path = "../../../../../toolchain/crates/libs/shims" } diff --git a/registry/native/crates/commands/which/src/main.rs b/software/coreutils/native/crates/cmd-which/src/main.rs similarity index 100% rename from registry/native/crates/commands/which/src/main.rs rename to software/coreutils/native/crates/cmd-which/src/main.rs diff --git a/registry/native/crates/commands/which/tests/executable_mode.rs b/software/coreutils/native/crates/cmd-which/tests/executable_mode.rs similarity index 100% rename from registry/native/crates/commands/which/tests/executable_mode.rs rename to software/coreutils/native/crates/cmd-which/tests/executable_mode.rs diff --git a/software/coreutils/native/crates/cmd-whoami/Cargo.toml b/software/coreutils/native/crates/cmd-whoami/Cargo.toml new file mode 100644 index 0000000000..988ca47dae --- /dev/null +++ b/software/coreutils/native/crates/cmd-whoami/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-whoami" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "whoami standalone binary for secure-exec VM" + +[[bin]] +name = "whoami" +path = "src/main.rs" + +[dependencies] +secureexec-builtins = { path = "../../../../../toolchain/crates/libs/builtins" } diff --git a/registry/native/crates/commands/whoami/src/main.rs b/software/coreutils/native/crates/cmd-whoami/src/main.rs similarity index 100% rename from registry/native/crates/commands/whoami/src/main.rs rename to software/coreutils/native/crates/cmd-whoami/src/main.rs diff --git a/software/coreutils/native/crates/cmd-yes/Cargo.toml b/software/coreutils/native/crates/cmd-yes/Cargo.toml new file mode 100644 index 0000000000..d32f9d582d --- /dev/null +++ b/software/coreutils/native/crates/cmd-yes/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-yes" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "yes standalone binary for secure-exec VM" + +[[bin]] +name = "yes" +path = "src/main.rs" + +[dependencies] +uu_yes = "0.7.0" diff --git a/registry/native/crates/commands/yes/src/main.rs b/software/coreutils/native/crates/cmd-yes/src/main.rs similarity index 100% rename from registry/native/crates/commands/yes/src/main.rs rename to software/coreutils/native/crates/cmd-yes/src/main.rs diff --git a/software/coreutils/native/crates/column/Cargo.toml b/software/coreutils/native/crates/column/Cargo.toml new file mode 100644 index 0000000000..3134b792fd --- /dev/null +++ b/software/coreutils/native/crates/column/Cargo.toml @@ -0,0 +1,9 @@ +[package] +workspace = "../../../../../toolchain" +name = "secureexec-column" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "column implementation for secure-exec standalone binaries" + +[dependencies] diff --git a/registry/native/crates/libs/column/src/lib.rs b/software/coreutils/native/crates/column/src/lib.rs similarity index 100% rename from registry/native/crates/libs/column/src/lib.rs rename to software/coreutils/native/crates/column/src/lib.rs diff --git a/software/coreutils/native/crates/du/Cargo.toml b/software/coreutils/native/crates/du/Cargo.toml new file mode 100644 index 0000000000..f167b14657 --- /dev/null +++ b/software/coreutils/native/crates/du/Cargo.toml @@ -0,0 +1,9 @@ +[package] +workspace = "../../../../../toolchain" +name = "secureexec-du" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "du implementation for secure-exec standalone binaries" + +[dependencies] diff --git a/registry/native/crates/libs/du/src/lib.rs b/software/coreutils/native/crates/du/src/lib.rs similarity index 100% rename from registry/native/crates/libs/du/src/lib.rs rename to software/coreutils/native/crates/du/src/lib.rs diff --git a/software/coreutils/native/crates/expr/Cargo.toml b/software/coreutils/native/crates/expr/Cargo.toml new file mode 100644 index 0000000000..e8f413d5c6 --- /dev/null +++ b/software/coreutils/native/crates/expr/Cargo.toml @@ -0,0 +1,10 @@ +[package] +workspace = "../../../../../toolchain" +name = "secureexec-expr" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "expr implementation for secure-exec standalone binaries" + +[dependencies] +regex = "1" diff --git a/registry/native/crates/libs/expr/src/lib.rs b/software/coreutils/native/crates/expr/src/lib.rs similarity index 100% rename from registry/native/crates/libs/expr/src/lib.rs rename to software/coreutils/native/crates/expr/src/lib.rs diff --git a/software/coreutils/native/crates/rev/Cargo.toml b/software/coreutils/native/crates/rev/Cargo.toml new file mode 100644 index 0000000000..3c8a175e92 --- /dev/null +++ b/software/coreutils/native/crates/rev/Cargo.toml @@ -0,0 +1,9 @@ +[package] +workspace = "../../../../../toolchain" +name = "secureexec-rev" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "rev implementation for secure-exec standalone binaries" + +[dependencies] diff --git a/registry/native/crates/libs/rev/src/lib.rs b/software/coreutils/native/crates/rev/src/lib.rs similarity index 100% rename from registry/native/crates/libs/rev/src/lib.rs rename to software/coreutils/native/crates/rev/src/lib.rs diff --git a/software/coreutils/native/crates/strings-cmd/Cargo.toml b/software/coreutils/native/crates/strings-cmd/Cargo.toml new file mode 100644 index 0000000000..7c3c023a54 --- /dev/null +++ b/software/coreutils/native/crates/strings-cmd/Cargo.toml @@ -0,0 +1,9 @@ +[package] +workspace = "../../../../../toolchain" +name = "secureexec-strings-cmd" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "strings command implementation for secure-exec standalone binaries" + +[dependencies] diff --git a/registry/native/crates/libs/strings-cmd/src/lib.rs b/software/coreutils/native/crates/strings-cmd/src/lib.rs similarity index 100% rename from registry/native/crates/libs/strings-cmd/src/lib.rs rename to software/coreutils/native/crates/strings-cmd/src/lib.rs diff --git a/software/coreutils/package.json b/software/coreutils/package.json new file mode 100644 index 0000000000..d17440e7a4 --- /dev/null +++ b/software/coreutils/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/coreutils", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "GNU coreutils for secure-exec VMs (sh, cat, ls, cp, sort, and 80+ commands)", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/agent/pi/src/index.ts b/software/coreutils/src/index.ts similarity index 100% rename from registry/agent/pi/src/index.ts rename to software/coreutils/src/index.ts diff --git a/software/coreutils/test/kill.test.ts b/software/coreutils/test/kill.test.ts new file mode 100644 index 0000000000..6c0da22810 --- /dev/null +++ b/software/coreutils/test/kill.test.ts @@ -0,0 +1,87 @@ +import { spawn } from "node:child_process"; +import { afterEach, describe, expect, it } from "vitest"; +import { + COMMANDS_DIR, + createInMemoryFileSystem, + createKernel, + createWasmVmRuntime, + describeIf, + hasWasmBinaries, + type Kernel, +} from "@agentos/test-harness"; + +const signalExitCode: Record = { + SIGTERM: 143, +}; + +function runNative(command: string, args: string[] = []): Promise<{ + exitCode: number; + stdout: string; + stderr: string; +}> { + return new Promise((resolve) => { + const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] }); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (chunk: Buffer) => { stdout += chunk.toString(); }); + child.stderr.on("data", (chunk: Buffer) => { stderr += chunk.toString(); }); + child.on("close", (code, signal) => resolve({ + exitCode: code ?? (signal ? signalExitCode[signal] ?? 1 : 1), + stdout, + stderr, + })); + }); +} + +describeIf(hasWasmBinaries, "upstream kill", () => { + let kernel: Kernel | undefined; + + afterEach(async () => { + await kernel?.dispose(); + kernel = undefined; + }); + + async function boot(): Promise { + const filesystem = createInMemoryFileSystem(); + kernel = createKernel({ filesystem }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + return kernel; + } + + it("matches native signal names, numbers, and -- parsing", async () => { + const vm = await boot(); + for (const args of [["-l", "TERM"], ["-l", "15"]]) { + const native = await runNative("kill", args); + const wasm = await vm.exec(`kill ${args.join(" ")}`); + expect(wasm.exitCode).toBe(native.exitCode); + expect(wasm.stdout).toBe(native.stdout); + } + + const nativeProbe = await runNative("sh", ["-c", "env kill -0 -- $$"]); + const wasmProbe = await vm.exec("sh -c 'env kill -0 -- $$'"); + expect(wasmProbe.exitCode).toBe(nativeProbe.exitCode); + expect(wasmProbe.exitCode).toBe(0); + }, 20_000); + + it.each(["TERM", "15"])("delivers %s like native", async (signal) => { + const vm = await boot(); + const script = `env kill -${signal} $$; exit 99`; + const native = await runNative("sh", ["-c", script]); + const wasm = await vm.exec(`sh -c '${script}'`); + expect(wasm.exitCode).toBe(native.exitCode); + expect(wasm.exitCode).toBe(143); + }, 20_000); + + it("reports invalid signals and missing processes", async () => { + const vm = await boot(); + const invalidSignal = await vm.exec("kill -s NOT_A_SIGNAL 2147483647"); + expect(invalidSignal.exitCode).not.toBe(0); + expect(invalidSignal.stderr).not.toBe(""); + + const nativeMissing = await runNative("kill", ["-0", "--", "2147483647"]); + const wasmMissing = await vm.exec("kill -0 -- 2147483647"); + expect(wasmMissing.exitCode).toBe(nativeMissing.exitCode); + expect(wasmMissing.exitCode).not.toBe(0); + expect(wasmMissing.stderr.toLowerCase()).toContain("no such process"); + }, 20_000); +}); diff --git a/software/coreutils/test/shell-redirect.test.ts b/software/coreutils/test/shell-redirect.test.ts new file mode 100644 index 0000000000..2ffcbeb039 --- /dev/null +++ b/software/coreutils/test/shell-redirect.test.ts @@ -0,0 +1,190 @@ +import { describe, it, expect, afterEach } from "vitest"; +import { spawnSync } from "node:child_process"; +import { chmodSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { + createInMemoryFileSystem, + createKernel, + createWasmVmRuntime, + COMMANDS_DIR, + describeIf, + hasWasmBinaries, + type Kernel, +} from '@agentos/test-harness'; + +function shellQuote(value: string): string { + return `'${value.replaceAll("'", `'\\''`)}'`; +} + +describeIf(hasWasmBinaries, "wasmvm shell redirects", () => { + let kernel: Kernel | undefined; + + afterEach(async () => { + await kernel?.dispose(); + kernel = undefined; + }); + + it("creates a redirected file relative to the changed cwd", async () => { + const vfs = createInMemoryFileSystem(); + await (vfs as any).chmod("/", 0o1777); + await vfs.mkdir("/tmp", { recursive: true }); + await (vfs as any).chmod("/tmp", 0o1777); + kernel = createKernel({ filesystem: vfs, syncFilesystemOnDispose: false }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec( + 'sh -c "mkdir -p /tmp/r && cd /tmp/r && echo hi > a.txt && cat a.txt"', + ); + + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout).toBe("hi\n"); + expect(await vfs.exists("/tmp/r/a.txt")).toBe(true); + }, 15_000); + + it("keeps Rust path resolution after guest fd 3 is reused", async () => { + const vfs = createInMemoryFileSystem(); + await (vfs as any).chmod("/", 0o1777); + await vfs.mkdir("/tmp", { recursive: true }); + await (vfs as any).chmod("/tmp", 0o1777); + kernel = createKernel({ filesystem: vfs, syncFilesystemOnDispose: false }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec( + "sh -c 'exec 3>/tmp/fd3-owned; mkdir -p /tmp/rust-path && cd /tmp/rust-path && echo payload > file && cat file; printf descriptor >&3'", + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout).toBe("payload\n"); + expect(Buffer.from(await kernel.readFile("/tmp/rust-path/file")).toString("utf8")).toBe("payload\n"); + expect(Buffer.from(await kernel.readFile("/tmp/fd3-owned")).toString("utf8")).toBe("descriptor"); + }, 15_000); + + it("preserves bytes written before stdout is closed", async () => { + const vfs = createInMemoryFileSystem(); + kernel = createKernel({ filesystem: vfs, syncFilesystemOnDispose: false }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + const script = "printf 'before-close\\n'; exec 1>&-"; + + const native = spawnSync("/bin/sh", ["-c", script], { encoding: "utf8" }); + const wasm = await kernel.exec(`sh -c ${shellQuote(script)}`); + + expect(wasm.exitCode).toBe(native.status); + expect(wasm.stdout).toBe(native.stdout); + expect(wasm.stderr).toBe(native.stderr); + }, 15_000); + + it("matches native exec PATH lookup, argv, environment, and replacement", async () => { + const vfs = createInMemoryFileSystem(); + kernel = createKernel({ filesystem: vfs, syncFilesystemOnDispose: false }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + const script = + `AGENTOS_EXEC_MARK=from-exec exec sh -c ` + + `'printf "%s|%s|%s\\n" "$0" "$1" "$AGENTOS_EXEC_MARK"' ` + + `custom-zero custom-one; printf 'continued\\n'`; + + const native = spawnSync("/bin/sh", ["-c", script], { encoding: "utf8" }); + const wasm = await kernel.exec(`sh -c ${shellQuote(script)}`); + + expect(wasm.exitCode).toBe(native.status); + expect(wasm.stdout).toBe(native.stdout); + expect(wasm.stderr).toBe(native.stderr); + expect(wasm.stdout).toBe("custom-zero|custom-one|from-exec\n"); + }, 30_000); + + it("matches native exec redirections and inherited descriptors", async () => { + const vfs = createInMemoryFileSystem(); + await (vfs as any).chmod("/", 0o1777); + await vfs.mkdir("/tmp", { recursive: true }); + await (vfs as any).chmod("/tmp", 0o1777); + kernel = createKernel({ filesystem: vfs, syncFilesystemOnDispose: false }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + const stem = `/tmp/agentos-exec-redir-${process.pid}`; + const stdoutPath = `${stem}.stdout`; + const fd3Path = `${stem}.fd3`; + const fd4Path = `${stem}.fd4`; + const script = + `exec sh -c 'printf "stdout\\n"; printf "fd3\\n" >&3; printf "fd4\\n" >&4' ` + + `> ${stdoutPath} 3> ${fd3Path} 4> ${fd4Path}`; + rmSync(stdoutPath, { force: true }); + rmSync(fd3Path, { force: true }); + rmSync(fd4Path, { force: true }); + + try { + const native = spawnSync("/bin/sh", ["-c", script], { encoding: "utf8" }); + const nativeStdoutFile = readFileSync(stdoutPath, "utf8"); + const nativeFd3File = readFileSync(fd3Path, "utf8"); + const nativeFd4File = readFileSync(fd4Path, "utf8"); + const wasm = await kernel.exec(`sh -c ${shellQuote(script)}`); + + expect(wasm.exitCode, wasm.stderr).toBe(native.status); + expect(wasm.stdout).toBe(native.stdout); + expect(wasm.stderr).toBe(native.stderr); + expect(Buffer.from(await kernel.readFile(stdoutPath)).toString("utf8")).toBe(nativeStdoutFile); + expect(Buffer.from(await kernel.readFile(fd3Path)).toString("utf8")).toBe(nativeFd3File); + expect(Buffer.from(await kernel.readFile(fd4Path)).toString("utf8")).toBe(nativeFd4File); + expect(nativeStdoutFile).toBe("stdout\n"); + expect(nativeFd3File).toBe("fd3\n"); + expect(nativeFd4File).toBe("fd4\n"); + } finally { + rmSync(stdoutPath, { force: true }); + rmSync(fd3Path, { force: true }); + rmSync(fd4Path, { force: true }); + } + }, 30_000); + + it("matches native exec without a command applying redirections", async () => { + const vfs = createInMemoryFileSystem(); + await (vfs as any).chmod("/", 0o1777); + await vfs.mkdir("/tmp", { recursive: true }); + await (vfs as any).chmod("/tmp", 0o1777); + kernel = createKernel({ filesystem: vfs, syncFilesystemOnDispose: false }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + const outputPath = `/tmp/agentos-exec-no-command-${process.pid}`; + const script = `exec > ${outputPath}; printf 'after\\n'`; + rmSync(outputPath, { force: true }); + + try { + const native = spawnSync("/bin/sh", ["-c", script], { encoding: "utf8" }); + const nativeFile = readFileSync(outputPath, "utf8"); + const wasm = await kernel.exec(`sh -c ${shellQuote(script)}`); + + expect(wasm.exitCode).toBe(native.status); + expect(wasm.stdout).toBe(native.stdout); + expect(wasm.stderr).toBe(native.stderr); + expect(Buffer.from(await kernel.readFile(outputPath)).toString("utf8")).toBe(nativeFile); + expect(nativeFile).toBe("after\n"); + } finally { + rmSync(outputPath, { force: true }); + } + }, 30_000); + + it("matches native exec failure statuses", async () => { + const vfs = createInMemoryFileSystem(); + await (vfs as any).chmod("/", 0o1777); + await vfs.mkdir("/tmp", { recursive: true }); + await (vfs as any).chmod("/tmp", 0o1777); + kernel = createKernel({ filesystem: vfs, syncFilesystemOnDispose: false }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const missingScript = "exec agentos-command-that-does-not-exist"; + const nativeMissing = spawnSync("/bin/sh", ["-c", missingScript], { encoding: "utf8" }); + const wasmMissing = await kernel.exec(`sh -c ${shellQuote(missingScript)}`); + expect(wasmMissing.exitCode).toBe(nativeMissing.status); + expect(wasmMissing.exitCode).toBe(127); + + const noexecPath = `/tmp/agentos-exec-noexec-${process.pid}`; + const noexecScript = "#!/bin/sh\nprintf 'must-not-run\\n'\n"; + writeFileSync(noexecPath, noexecScript, { mode: 0o644 }); + chmodSync(noexecPath, 0o644); + await kernel.writeFile(noexecPath, noexecScript); + await (vfs as any).chmod(noexecPath, 0o644); + try { + const command = `exec ${noexecPath}`; + const nativeNoexec = spawnSync("/bin/sh", ["-c", command], { encoding: "utf8" }); + const wasmNoexec = await kernel.exec(`sh -c ${shellQuote(command)}`); + expect(wasmNoexec.exitCode).toBe(nativeNoexec.status); + expect(wasmNoexec.exitCode).toBe(126); + expect(wasmNoexec.stdout).toBe(""); + } finally { + rmSync(noexecPath, { force: true }); + } + }, 30_000); +}); diff --git a/registry/tests/wasmvm/shell-terminal.test.ts b/software/coreutils/test/shell-terminal.test.ts similarity index 98% rename from registry/tests/wasmvm/shell-terminal.test.ts rename to software/coreutils/test/shell-terminal.test.ts index 7efaffbb69..ca99bb54eb 100644 --- a/registry/tests/wasmvm/shell-terminal.test.ts +++ b/software/coreutils/test/shell-terminal.test.ts @@ -7,10 +7,10 @@ */ import { describe, it, expect, afterEach } from "vitest"; -import { TerminalHarness } from './terminal-harness.js'; -import { createWasmVmRuntime } from '../helpers.js'; -import { COMMANDS_DIR, createKernel, describeIf, hasWasmBinaries } from '../helpers.js'; -import type { Kernel } from "../helpers.js"; +import { TerminalHarness } from '@agentos/test-harness'; +import { createWasmVmRuntime } from '@agentos/test-harness'; +import { COMMANDS_DIR, createKernel, describeIf, hasWasmBinaries } from '@agentos/test-harness'; +import type { Kernel } from '@agentos/test-harness'; /** brush-shell interactive prompt (captured empirically). */ const PROMPT = "sh-0.4$ "; @@ -169,7 +169,7 @@ describeIf(hasWasmBinaries, "wasmvm-shell-terminal", () => { expect(harness.screenshotTrimmed()).toBe( [`${PROMPT}echo hello`, "hello", PROMPT].join("\n"), ); - }); + }, 15_000); it("ls / shows listing — directory entries include /bin from command registration", async () => { const { kernel } = await createShellKernel(); diff --git a/software/coreutils/tsconfig.json b/software/coreutils/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/coreutils/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/curl/agentos-package.json b/software/curl/agentos-package.json new file mode 100644 index 0000000000..98a4c83f33 --- /dev/null +++ b/software/curl/agentos-package.json @@ -0,0 +1,12 @@ +{ + "commands": [ + "curl" + ], + "registry": { + "title": "curl", + "description": "HTTP(S) client for fetching URLs and APIs.", + "priority": 50, + "image": "/images/registry/curl.svg", + "category": "networking" + } +} diff --git a/registry/native/c/curl-upstream-overlay/lib/curl_setup.h b/software/curl/native/c/overlay/lib/curl_setup.h similarity index 99% rename from registry/native/c/curl-upstream-overlay/lib/curl_setup.h rename to software/curl/native/c/overlay/lib/curl_setup.h index 8edcf809eb..50b4479f23 100644 --- a/registry/native/c/curl-upstream-overlay/lib/curl_setup.h +++ b/software/curl/native/c/overlay/lib/curl_setup.h @@ -742,7 +742,7 @@ #if defined(USE_GNUTLS) || defined(USE_OPENSSL) || defined(USE_MBEDTLS) || \ defined(USE_WOLFSSL) || defined(USE_SCHANNEL) || defined(USE_SECTRANSP) || \ - defined(USE_BEARSSL) || defined(USE_RUSTLS) || defined(USE_WASI_TLS) + defined(USE_BEARSSL) || defined(USE_RUSTLS) #define USE_SSL /* SSL support has been enabled */ #endif diff --git a/registry/native/c/curl-upstream-overlay/lib/vtls/vtls.c b/software/curl/native/c/overlay/lib/vtls/vtls.c similarity index 99% rename from registry/native/c/curl-upstream-overlay/lib/vtls/vtls.c rename to software/curl/native/c/overlay/lib/vtls/vtls.c index 21b758f2e8..61b407ab22 100644 --- a/registry/native/c/curl-upstream-overlay/lib/vtls/vtls.c +++ b/software/curl/native/c/overlay/lib/vtls/vtls.c @@ -64,7 +64,6 @@ #include "mbedtls.h" /* mbedTLS versions */ #include "bearssl.h" /* BearSSL versions */ #include "rustls.h" /* Rustls versions */ -#include "wasi_tls.h" /* WASI host TLS */ #include "slist.h" #include "sendf.h" @@ -1380,8 +1379,6 @@ static const struct Curl_ssl Curl_ssl_multi = { const struct Curl_ssl *Curl_ssl = #if defined(CURL_WITH_MULTI_SSL) &Curl_ssl_multi; -#elif defined(USE_WASI_TLS) - &Curl_ssl_wasi_tls; #elif defined(USE_WOLFSSL) &Curl_ssl_wolfssl; #elif defined(USE_GNUTLS) @@ -1426,9 +1423,6 @@ static const struct Curl_ssl *available_backends[] = { #endif #if defined(USE_RUSTLS) &Curl_ssl_rustls, -#endif -#if defined(USE_WASI_TLS) - &Curl_ssl_wasi_tls, #endif NULL }; diff --git a/software/curl/native/crates/cmd-curl/Cargo.toml b/software/curl/native/crates/cmd-curl/Cargo.toml new file mode 100644 index 0000000000..5a65154842 --- /dev/null +++ b/software/curl/native/crates/cmd-curl/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-curl" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Minimal curl-compatible HTTP client for secure-exec" + +[[bin]] +name = "curl" +path = "src/main.rs" + +[dependencies] +wasi-http = { package = "secureexec-wasi-http", path = "../../../../../toolchain/crates/libs/wasi-http" } diff --git a/registry/native/crates/commands/curl/src/main.rs b/software/curl/native/crates/cmd-curl/src/main.rs similarity index 100% rename from registry/native/crates/commands/curl/src/main.rs rename to software/curl/native/crates/cmd-curl/src/main.rs diff --git a/software/curl/package.json b/software/curl/package.json new file mode 100644 index 0000000000..9a0ce7d95e --- /dev/null +++ b/software/curl/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/curl", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "curl HTTP client for secure-exec VMs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/browserbase/src/index.ts b/software/curl/src/index.ts similarity index 100% rename from registry/software/browserbase/src/index.ts rename to software/curl/src/index.ts diff --git a/software/curl/test/curl.test.ts b/software/curl/test/curl.test.ts new file mode 100644 index 0000000000..d79395baf1 --- /dev/null +++ b/software/curl/test/curl.test.ts @@ -0,0 +1,918 @@ +/** + * Integration tests for curl and the C socket layer (host_socket.c). + * + * Tests the WASM socket implementation that powers curl: + * - DNS resolution (getaddrinfo) + * - TCP socket creation and connection + * - Non-blocking socket mode (fcntl O_NONBLOCK) + * - Socket options (getsockopt SO_ERROR, setsockopt TCP_NODELAY) + * - Poll for readability/writability + * - HTTP send/recv over raw sockets + * - Remote endpoint connectivity + */ + +import { describe, it, expect, afterEach, beforeAll, afterAll } from 'vitest'; +import { createWasmVmRuntime } from '@agentos/test-harness'; +import { + allowAll, + C_BUILD_DIR, + COMMANDS_DIR, + createInMemoryFileSystem, + createKernel, + describeIf, + hasCWasmBinaries, + hasWasmBinaries, + itIf, +} from '@agentos/test-harness'; +import type { Kernel } from '@agentos/test-harness'; +import { + createServer as createHttpServer, + type IncomingMessage, + type Server as HttpServer, + type ServerResponse, +} from 'node:http'; +import { createServer as createHttpsServer, type Server as HttpsServer } from 'node:https'; +import { + createConnection, + createServer as createTcpServer, + type Server as TcpServer, +} from 'node:net'; +import { execSync } from 'node:child_process'; +import { + existsSync, + mkdtempSync, + readFileSync, + rmSync, + unlinkSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; +import { brotliCompressSync, gzipSync, zstdCompressSync } from 'node:zlib'; + +// The upstream curl parity assertions below only hold for the C-built curl +// artifact; the Rust fallback in COMMANDS_DIR intentionally supports a smaller +// flag surface and should not be used for these cases. +const hasHttpGetTest = hasWasmBinaries && existsSync(resolve(COMMANDS_DIR, 'http_get_test')); +const hasCurl = hasCWasmBinaries('curl'); +const runExternalNetwork = process.env.AGENTOS_E2E_NETWORK === '1'; +const EXTERNAL_HOST = 'example.com'; +const EXTERNAL_TCP_PORT = 80; +const EXTERNAL_HTTP_URL = `http://${EXTERNAL_HOST}/`; +const EXTERNAL_HTTPS_URL = `https://${EXTERNAL_HOST}/`; +const EXTERNAL_EXPECTED_BODY = 'Example Domain'; +const EXTERNAL_RETRY_ATTEMPTS = 3; +const EXTERNAL_RETRY_DELAY_MS = 1_000; +const EXTERNAL_PROBE_TIMEOUT_MS = 8_000; +let hasOpenssl = false; + +try { + execSync('openssl version', { stdio: 'pipe' }); + hasOpenssl = true; +} catch { + hasOpenssl = false; +} + +function sleep(ms: number): Promise { + return new Promise((resolveSleep) => setTimeout(resolveSleep, ms)); +} + +function formatError(error: unknown): string { + if (error instanceof Error) return error.message; + return String(error); +} + +async function retryExternal(run: () => Promise, attempts = EXTERNAL_RETRY_ATTEMPTS): Promise { + let lastError: unknown; + for (let attempt = 1; attempt <= attempts; attempt += 1) { + try { + return await run(); + } catch (error) { + lastError = error; + if (attempt < attempts) { + await sleep(EXTERNAL_RETRY_DELAY_MS); + } + } + } + + throw lastError ?? new Error('external network probe failed'); +} + +async function probeExternalTcp(): Promise { + await new Promise((resolveConnect, rejectConnect) => { + const socket = createConnection({ + host: EXTERNAL_HOST, + port: EXTERNAL_TCP_PORT, + }); + let settled = false; + + const finish = (callback: () => void) => { + if (settled) return; + settled = true; + callback(); + }; + + socket.setTimeout(EXTERNAL_PROBE_TIMEOUT_MS); + socket.once('connect', () => { + finish(() => { + socket.end(); + resolveConnect(); + }); + }); + socket.once('timeout', () => { + finish(() => { + socket.destroy(); + rejectConnect(new Error(`timed out connecting to ${EXTERNAL_HOST}:${EXTERNAL_TCP_PORT}`)); + }); + }); + socket.once('error', (error) => { + finish(() => { + socket.destroy(); + rejectConnect(error); + }); + }); + }); +} + +async function probeExternalHttps(): Promise { + const response = await fetch(EXTERNAL_HTTPS_URL, { + signal: AbortSignal.timeout(EXTERNAL_PROBE_TIMEOUT_MS), + }); + if (!response.ok) { + throw new Error(`host probe failed with HTTP ${response.status}`); + } + await response.arrayBuffer(); +} + +const externalNetworkSkipReason = runExternalNetwork + ? await (async () => { + try { + await retryExternal(async () => { + await probeExternalTcp(); + await probeExternalHttps(); + }); + return false as const; + } catch (error) { + return `external network unavailable: ${formatError(error)}`; + } + })() + : 'set AGENTOS_E2E_NETWORK=1 to enable external-network coverage'; + +// A known, highly compressible payload used by the --compressed round-trip +// tests. Long + repetitive so every codec produces a body distinct from the +// plaintext (proving curl actually inflated it, not just passed it through). +const COMPRESSION_PAYLOAD = + 'agentos-compression-parity ' + 'the quick brown fox jumps over the lazy dog. '.repeat(64); + +// Build a real CA and a leaf server certificate signed by it, with a SAN that +// covers the 127.0.0.1 loopback endpoint the tests connect to. This lets curl's +// mbedTLS backend perform genuine chain + hostname verification, exactly like +// Linux curl against a private CA. +function makeCaSignedCert(caCommonName: string): { + caPem: string; + serverKey: string; + serverCert: string; +} { + const dir = mkdtempSync(join(tmpdir(), 'curl-ca-')); + try { + execSync(`openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "${dir}/ca.key" 2>/dev/null`); + execSync( + `openssl req -x509 -new -key "${dir}/ca.key" -days 3650 -subj "/CN=${caCommonName}" -out "${dir}/ca.crt" 2>/dev/null`, + ); + execSync(`openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "${dir}/srv.key" 2>/dev/null`); + execSync(`openssl req -new -key "${dir}/srv.key" -subj "/CN=localhost" -out "${dir}/srv.csr" 2>/dev/null`); + writeFileSync(`${dir}/ext.cnf`, 'subjectAltName=DNS:localhost,IP:127.0.0.1\n'); + execSync( + `openssl x509 -req -in "${dir}/srv.csr" -CA "${dir}/ca.crt" -CAkey "${dir}/ca.key" ` + + `-CAcreateserial -days 3650 -extfile "${dir}/ext.cnf" -out "${dir}/srv.crt" 2>/dev/null`, + ); + return { + caPem: readFileSync(`${dir}/ca.crt`, 'utf8'), + serverKey: readFileSync(`${dir}/srv.key`, 'utf8'), + serverCert: readFileSync(`${dir}/srv.crt`, 'utf8'), + }; + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +function generateSelfSignedCert(): { key: string; cert: string } { + const keyPath = join(tmpdir(), `curl-test-key-${process.pid}-${Date.now()}.pem`); + try { + const key = execSync( + 'openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 2>/dev/null', + { encoding: 'utf8' }, + ); + writeFileSync(keyPath, key); + const cert = execSync( + `openssl req -new -x509 -key "${keyPath}" -days 1 -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" 2>/dev/null`, + { encoding: 'utf8' }, + ); + return { key, cert }; + } finally { + try { + unlinkSync(keyPath); + } catch { + // Best effort cleanup for test temp files. + } + } +} + +describeIf(hasCurl || hasHttpGetTest, 'curl and socket layer', () => { + let kernel: Kernel; + let httpServer: HttpServer; + let httpsServer: HttpsServer; + let validHttpsServer: HttpsServer; + let caHttpsServer: HttpsServer; + let keepAliveServer: TcpServer; + let httpPort: number; + let httpsPort: number; + let validHttpsPort: number; + let caHttpsPort: number; + let keepAlivePort: number; + // CA (PEM) trusted by the seeded /etc/ssl/certs/ca-certificates.crt bundle — + // it signs validHttpsServer's leaf. caOnlyPem signs caHttpsServer's leaf and + // is deliberately NOT in the bundle, so it verifies only via --cacert. + let seededCaPem = ''; + let caOnlyPem = ''; + let flakyRequestCount = 0; + + beforeAll(async () => { + httpServer = createHttpServer((req: IncomingMessage, res: ServerResponse) => { + const url = req.url ?? '/'; + + if (url === '/json') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true, path: url })); + return; + } + + if (url === '/redirect') { + res.writeHead(302, { Location: `http://127.0.0.1:${httpPort}/final` }); + res.end(); + return; + } + + if (url === '/final') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('followed redirect'); + return; + } + + if (url === '/one') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('first-response\n'); + return; + } + + if (url === '/two') { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('second-response\n'); + return; + } + + if (url === '/echo' && req.method === 'POST') { + let body = ''; + req.on('data', (chunk) => { + body += chunk; + }); + req.on('end', () => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + method: req.method, + body, + header: req.headers['x-test'] ?? null, + })); + }); + return; + } + + if (url === '/json-post' && req.method === 'POST') { + let body = ''; + req.on('data', (chunk) => { + body += chunk; + }); + req.on('end', () => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ + method: req.method, + contentType: req.headers['content-type'] ?? null, + accept: req.headers.accept ?? null, + body, + })); + }); + return; + } + + if (url === '/head-test') { + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'X-Test-Header': 'present', + }); + if (req.method === 'HEAD') { + res.end(); + } else { + res.end('body should not appear in HEAD output'); + } + return; + } + + if (url === '/auth-required') { + const auth = req.headers.authorization; + if (!auth || !auth.startsWith('Basic ')) { + res.writeHead(401, { 'Content-Type': 'text/plain' }); + res.end('unauthorized'); + return; + } + + const decoded = Buffer.from(auth.slice(6), 'base64').toString(); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end(`authenticated: ${decoded}`); + return; + } + + if (url === '/upload' && req.method === 'POST') { + const contentType = req.headers['content-type'] ?? ''; + const chunks: Buffer[] = []; + req.on('data', (chunk) => { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + }); + req.on('end', () => { + const body = Buffer.concat(chunks).toString(); + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end( + `multipart: ${contentType.startsWith('multipart/form-data')}\n` + + `body-contains-file: ${body.includes('upload.txt')}`, + ); + }); + return; + } + + if (url === '/binary') { + const payload = Buffer.alloc(256); + for (let i = 0; i < payload.length; i++) payload[i] = i & 0xff; + res.writeHead(200, { + 'Content-Type': 'application/octet-stream', + 'Content-Length': String(payload.length), + }); + res.end(payload); + return; + } + + if (url === '/named.txt') { + const body = 'downloaded-by-remote-name\n'; + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Content-Length': String(Buffer.byteLength(body)), + }); + res.end(body); + return; + } + + if (url === '/flaky') { + flakyRequestCount += 1; + if (flakyRequestCount === 1) { + res.writeHead(503, { 'Content-Type': 'text/plain' }); + res.end('retry please'); + return; + } + + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('retry succeeded'); + return; + } + + if (url === '/status') { + res.writeHead(201, { 'Content-Type': 'text/plain' }); + res.end('created'); + return; + } + + if (url === '/gzip' || url === '/brotli' || url === '/zstd') { + const raw = Buffer.from(COMPRESSION_PAYLOAD); + let encoding: string; + let body: Buffer; + if (url === '/gzip') { + encoding = 'gzip'; + body = gzipSync(raw); + } else if (url === '/brotli') { + encoding = 'br'; + body = brotliCompressSync(raw); + } else { + encoding = 'zstd'; + body = zstdCompressSync(raw); + } + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Content-Encoding': encoding, + 'Content-Length': String(body.length), + }); + res.end(body); + return; + } + + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('not found'); + }); + + await new Promise((resolveListen) => { + httpServer.listen(0, '127.0.0.1', resolveListen); + }); + httpPort = (httpServer.address() as import('node:net').AddressInfo).port; + + if (hasOpenssl) { + const tlsCert = generateSelfSignedCert(); + httpsServer = createHttpsServer({ key: tlsCert.key, cert: tlsCert.cert }, (req, res) => { + if (req.url === '/json') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ secure: true, path: req.url })); + return; + } + + if (req.url === '/keepalive') { + const body = 'hello from tls keepalive'; + res.writeHead(200, { + 'Content-Type': 'text/plain', + 'Content-Length': String(Buffer.byteLength(body)), + Connection: 'keep-alive', + 'Keep-Alive': 'timeout=60', + }); + res.end(body); + return; + } + + res.writeHead(404, { 'Content-Type': 'text/plain' }); + res.end('not found'); + }); + httpsServer.keepAliveTimeout = 60000; + + await new Promise((resolveListen) => { + httpsServer.listen(0, '127.0.0.1', resolveListen); + }); + httpsPort = (httpsServer.address() as import('node:net').AddressInfo).port; + + // HTTPS server whose leaf chains to a CA seeded into the guest's + // /etc/ssl/certs/ca-certificates.crt — verified with NO -k / --cacert. + const trusted = makeCaSignedCert('AgentOS Test Root CA'); + seededCaPem = trusted.caPem; + validHttpsServer = createHttpsServer( + { key: trusted.serverKey, cert: trusted.serverCert }, + (req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ verified: true, path: req.url })); + }, + ); + await new Promise((resolveListen) => { + validHttpsServer.listen(0, '127.0.0.1', resolveListen); + }); + validHttpsPort = (validHttpsServer.address() as import('node:net').AddressInfo).port; + + // HTTPS server whose CA is provided ONLY via --cacert (not in the bundle). + const caOnly = makeCaSignedCert('AgentOS Cacert-Only CA'); + caOnlyPem = caOnly.caPem; + caHttpsServer = createHttpsServer( + { key: caOnly.serverKey, cert: caOnly.serverCert }, + (req, res) => { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ cacert: true, path: req.url })); + }, + ); + await new Promise((resolveListen) => { + caHttpsServer.listen(0, '127.0.0.1', resolveListen); + }); + caHttpsPort = (caHttpsServer.address() as import('node:net').AddressInfo).port; + } + + keepAliveServer = createTcpServer((socket) => { + socket.once('data', () => { + const body = 'hello from keepalive'; + socket.write( + 'HTTP/1.1 200 OK\r\n' + + 'Content-Type: text/plain\r\n' + + `Content-Length: ${Buffer.byteLength(body)}\r\n` + + 'Connection: keep-alive\r\n' + + 'Keep-Alive: timeout=60\r\n' + + '\r\n' + + body, + ); + // Intentionally keep the socket open to exercise curl shutdown logic. + }); + }); + + await new Promise((resolveListen) => { + keepAliveServer.listen(0, '127.0.0.1', resolveListen); + }); + keepAlivePort = (keepAliveServer.address() as import('node:net').AddressInfo).port; + }); + + afterAll(async () => { + if (httpServer) { + await new Promise((resolveClose) => httpServer.close(() => resolveClose())); + } + if (httpsServer) { + await new Promise((resolveClose) => httpsServer.close(() => resolveClose())); + } + if (validHttpsServer) { + await new Promise((resolveClose) => validHttpsServer.close(() => resolveClose())); + } + if (caHttpsServer) { + await new Promise((resolveClose) => caHttpsServer.close(() => resolveClose())); + } + if (keepAliveServer) { + await new Promise((resolveClose) => keepAliveServer.close(() => resolveClose())); + } + }); + + async function createKernelWithNet() { + flakyRequestCount = 0; + const filesystem = createInMemoryFileSystem(); + await (filesystem as any).chmod('/', 0o1777); + await filesystem.mkdir('/tmp', { recursive: true }); + await (filesystem as any).chmod('/tmp', 0o1777); + + kernel = createKernel({ + filesystem, + permissions: allowAll, + loopbackExemptPorts: [ + httpPort, + httpsPort, + validHttpsPort, + caHttpsPort, + keepAlivePort, + ], + }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); + + // Seed the Debian-shaped trust store the way the native VM bootstrap does, + // so curl's compile-time default CA bundle resolves in-guest. Only the + // "trusted" CA is placed here; the cacert-only CA is intentionally absent. + if (seededCaPem) { + await filesystem.mkdir('/etc/ssl/certs', { recursive: true }); + await kernel.writeFile('/etc/ssl/certs/ca-certificates.crt', seededCaPem); + } + return kernel; + } + + async function execWithRetry(command: string) { + let lastResult: Awaited> | undefined; + for (let attempt = 1; attempt <= EXTERNAL_RETRY_ATTEMPTS; attempt += 1) { + lastResult = await kernel.exec(command); + if (lastResult.exitCode === 0) return lastResult; + if (attempt < EXTERNAL_RETRY_ATTEMPTS) { + await sleep(EXTERNAL_RETRY_DELAY_MS); + } + } + + return lastResult!; + } + + afterEach(async () => { + await kernel?.dispose(); + }); + + itIf(hasHttpGetTest, 'http_get_test reaches a local HTTP server', async () => { + await createKernelWithNet(); + const result = await kernel.exec(`http_get_test 127.0.0.1 ${httpPort} /json`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('HTTP/1.1 200'); + expect(result.stdout).toContain('"ok":true'); + }, 15000); + + itIf(hasHttpGetTest, 'http_get_test preserves non-blocking connect diagnostics', async () => { + await createKernelWithNet(); + const result = await kernel.exec(`http_get_test 127.0.0.1 ${httpPort} /json`); + expect(result.exitCode).toBe(0); + expect(result.stderr).toContain('fcntl F_SETFL(NONBLOCK)=0'); + expect(result.stderr).toMatch(/connect=(0|-1 errno=\d+)/); + expect(result.stderr).toContain('getsockopt(SO_ERROR)=0 value=0'); + expect(result.stderr).toContain('poll(POLLOUT)=1'); + }, 15000); + + itIf(hasCurl, 'curl GET returns JSON from a local server', async () => { + await createKernelWithNet(); + const result = await kernel.exec(`curl -s http://127.0.0.1:${httpPort}/json`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('"ok":true'); + }, 15000); + + itIf(hasCurl, 'curl --version reports the upstream tool version', async () => { + await createKernelWithNet(); + const result = await kernel.exec('curl --version'); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('curl 8.11.1'); + expect(result.stdout).toMatch(/Protocols:/); + }, 15000); + + itIf(hasCurl, 'curl -L follows redirects', async () => { + await createKernelWithNet(); + const result = await kernel.exec(`curl -s -L http://127.0.0.1:${httpPort}/redirect`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe('followed redirect'); + }, 15000); + + itIf(hasCurl, 'curl POST sends body and headers', async () => { + await createKernelWithNet(); + const result = await kernel.exec( + `curl -s -X POST -H 'X-Test: edge-case' -d 'payload-data' http://127.0.0.1:${httpPort}/echo`, + ); + expect(result.exitCode).toBe(0); + const body = JSON.parse(result.stdout); + expect(body.method).toBe('POST'); + expect(body.body).toBe('payload-data'); + expect(body.header).toBe('edge-case'); + }, 15000); + + itIf(hasCurl, 'curl --json sends JSON with the expected headers', async () => { + await createKernelWithNet(); + const result = await kernel.exec( + `curl -s --json '{\"hello\":\"world\"}' http://127.0.0.1:${httpPort}/json-post`, + ); + expect(result.exitCode).toBe(0); + const body = JSON.parse(result.stdout); + expect(body.method).toBe('POST'); + expect(body.body).toBe('{"hello":"world"}'); + expect(body.contentType).toBe('application/json'); + expect(body.accept).toBe('application/json'); + }, 15000); + + itIf(hasCurl, 'curl -I returns response headers without the body', async () => { + await createKernelWithNet(); + const result = await kernel.exec(`curl -s -I http://127.0.0.1:${httpPort}/head-test`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('HTTP/'); + expect(result.stdout).toMatch(/X-Test-Header/i); + expect(result.stdout).not.toContain('body should not appear'); + }, 15000); + + itIf(hasCurl, 'curl -u sends HTTP Basic authentication', async () => { + await createKernelWithNet(); + const result = await kernel.exec(`curl -s -u user:pass http://127.0.0.1:${httpPort}/auth-required`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe('authenticated: user:pass'); + }, 15000); + + itIf(hasCurl, 'curl -F uploads multipart form data', async () => { + await createKernelWithNet(); + await kernel.writeFile('/tmp/upload.txt', 'file payload\n'); + const result = await kernel.exec(`curl -s -F file=@/tmp/upload.txt http://127.0.0.1:${httpPort}/upload`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('multipart: true'); + expect(result.stdout).toContain('body-contains-file: true'); + }, 15000); + + itIf(hasCurl, 'curl -K reads options from a config file', async () => { + await createKernelWithNet(); + await kernel.writeFile( + '/tmp/curlrc', + `silent\nurl = "http://127.0.0.1:${httpPort}/json"\n`, + ); + const result = await kernel.exec('curl -K /tmp/curlrc'); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('"ok":true'); + }, 15000); + + itIf(hasCurl, 'curl -o writes text output to a file', async () => { + await createKernelWithNet(); + const result = await kernel.exec(`curl -s -o /tmp/out.json http://127.0.0.1:${httpPort}/json`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe(''); + const file = new TextDecoder().decode(await kernel.readFile('/tmp/out.json')); + expect(file).toContain('"ok":true'); + }, 15000); + + itIf(hasCurl, 'curl -o respects the current working directory for relative output paths', async () => { + await createKernelWithNet(); + const result = await kernel.exec( + `mkdir -p /tmp/curl-cwd && cd /tmp/curl-cwd && ` + + `curl -s -o local.txt http://127.0.0.1:${httpPort}/named.txt && cat /tmp/curl-cwd/local.txt`, + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe('downloaded-by-remote-name\n'); + }, 15000); + + itIf(hasCurl, 'curl -o writes binary output without truncation', async () => { + await createKernelWithNet(); + const result = await kernel.exec(`curl -s -o /tmp/out.bin http://127.0.0.1:${httpPort}/binary`); + expect(result.exitCode).toBe(0); + const file = await kernel.readFile('/tmp/out.bin'); + expect(file).toHaveLength(256); + expect(Array.from(file.slice(0, 8))).toEqual([0, 1, 2, 3, 4, 5, 6, 7]); + expect(Array.from(file.slice(-4))).toEqual([252, 253, 254, 255]); + }, 15000); + + itIf(hasCurl, 'curl -D and -o split headers and body into separate files', async () => { + await createKernelWithNet(); + const result = await kernel.exec( + `curl -s -D /tmp/headers.txt -o /tmp/body.txt http://127.0.0.1:${httpPort}/named.txt`, + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe(''); + + const headers = new TextDecoder().decode(await kernel.readFile('/tmp/headers.txt')); + const body = new TextDecoder().decode(await kernel.readFile('/tmp/body.txt')); + expect(headers).toContain('HTTP/1.1 200 OK'); + expect(headers).toMatch(/Content-Type: text\/plain/i); + expect(body).toBe('downloaded-by-remote-name\n'); + }, 15000); + + itIf(hasCurl, 'curl -O writes to the remote filename', async () => { + await createKernelWithNet(); + const result = await kernel.exec( + `mkdir -p /tmp/remote-name && cd /tmp/remote-name && ` + + `curl -s -O http://127.0.0.1:${httpPort}/named.txt && cat /tmp/remote-name/named.txt`, + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe('downloaded-by-remote-name\n'); + }, 15000); + + itIf(hasCurl, 'curl -w writes the HTTP status code', async () => { + await createKernelWithNet(); + const result = await kernel.exec(`curl -s -w '%{http_code}' http://127.0.0.1:${httpPort}/status`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('created'); + expect(result.stdout).toContain('201'); + }, 15000); + + itIf(hasCurl, 'curl -f reports HTTP errors with a non-zero exit code', async () => { + await createKernelWithNet(); + const result = await kernel.exec(`curl -fsS http://127.0.0.1:${httpPort}/missing`); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toMatch(/404|not found|error/i); + }, 15000); + + itIf(hasCurl, 'curl --fail-with-body preserves the response body on HTTP errors', async () => { + await createKernelWithNet(); + const result = await kernel.exec(`curl -sS --fail-with-body http://127.0.0.1:${httpPort}/missing`); + expect(result.exitCode).not.toBe(0); + expect(result.stdout).toBe('not found'); + expect(result.stderr).toMatch(/404|error/i); + }, 15000); + + itIf(hasCurl, 'curl reports refused connections without hanging', async () => { + await createKernelWithNet(); + + const probe = createTcpServer(); + await new Promise((resolveListen) => probe.listen(0, '127.0.0.1', resolveListen)); + const unusedPort = (probe.address() as import('node:net').AddressInfo).port; + await new Promise((resolveClose) => probe.close(() => resolveClose())); + + const startedAt = Date.now(); + const result = await kernel.exec(`curl -sS http://127.0.0.1:${unusedPort}/`); + expect(result.exitCode).not.toBe(0); + expect(Date.now() - startedAt).toBeLessThan(8000); + expect(result.stderr).toMatch(/connect|refused|failed/i); + }, 15000); + + itIf(hasCurl, 'curl reports DNS failures cleanly', async () => { + await createKernelWithNet(); + const result = await kernel.exec('curl -sS http://does-not-exist.invalid/'); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toMatch(/resolve|host|dns/i); + }, 15000); + + itIf(hasCurl, 'curl handles multiple URLs in one invocation', async () => { + await createKernelWithNet(); + const result = await kernel.exec( + `curl -s http://127.0.0.1:${httpPort}/one http://127.0.0.1:${httpPort}/two`, + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe('first-response\nsecond-response\n'); + }, 15000); + + itIf(hasCurl, 'curl --retry retries transient HTTP failures', async () => { + await createKernelWithNet(); + const result = await kernel.exec( + `curl -fsS --retry 2 --retry-delay 0 http://127.0.0.1:${httpPort}/flaky`, + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe('retry succeeded'); + expect(flakyRequestCount).toBeGreaterThanOrEqual(2); + }, 15000); + + itIf(hasCurl, 'curl exits promptly after a keep-alive response', async () => { + await createKernelWithNet(); + const startedAt = Date.now(); + const result = await kernel.exec(`curl -s http://127.0.0.1:${keepAlivePort}/`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe('hello from keepalive'); + expect(Date.now() - startedAt).toBeLessThan(8000); + }, 15000); + + itIf(hasCurl, 'curl --version reports the mbedTLS backend', async () => { + await createKernelWithNet(); + const result = await kernel.exec('curl --version'); + expect(result.exitCode).toBe(0); + // Real in-guest TLS: the SSL version line must name mbedTLS, and the + // retired host shim string must be gone. + expect(result.stdout).toMatch(/mbedTLS/i); + expect(result.stdout).not.toMatch(/WASI-TLS|wasi-tls/i); + expect(result.stdout).toMatch(/^Features:.*\bSSL\b/m); + expect(result.stdout).toMatch(/^Features:.*\bbrotli\b/m); + expect(result.stdout).toMatch(/^Features:.*\bzstd\b/m); + }, 15000); + + itIf(hasCurl && hasOpenssl, 'curl verifies a CA-signed cert against the seeded CA bundle', async () => { + await createKernelWithNet(); + // No -k, no --cacert: trust comes solely from the seeded + // /etc/ssl/certs/ca-certificates.crt, exactly like Debian curl. + const result = await kernel.exec(`curl -sS https://127.0.0.1:${validHttpsPort}/json`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('"verified":true'); + }, 15000); + + itIf(hasCurl && hasOpenssl, 'curl fails with exit 60 and a real verify message on an untrusted cert', async () => { + await createKernelWithNet(); + const result = await kernel.exec(`curl -sS https://127.0.0.1:${httpsPort}/json`); + // CURLE_PEER_FAILED_VERIFICATION == 60, the native Linux taxonomy. NOT 35 + // (SSL connect error, the old host-shim behavior). + expect(result.exitCode).toBe(60); + expect(result.stderr).toMatch(/certificate|verify|self[- ]?signed|CA/i); + expect(result.stderr).not.toMatch(/WASI TLS|wasi-tls/i); + expect(result.stdout).toBe(''); + }, 15000); + + itIf(hasCurl && hasOpenssl, 'curl -k skips verification and succeeds on an untrusted cert', async () => { + await createKernelWithNet(); + const result = await kernel.exec(`curl -ks https://127.0.0.1:${httpsPort}/json`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('"secure":true'); + }, 15000); + + itIf(hasCurl && hasOpenssl, 'curl --cacert accepts a server signed by that CA', async () => { + await createKernelWithNet(); + // caHttpsServer's CA is NOT in the seeded bundle, so this only passes if + // --cacert is honored (real file read + chain build in-guest). + await kernel.writeFile('/tmp/cacert-only.pem', caOnlyPem); + const result = await kernel.exec( + `curl -sS --cacert /tmp/cacert-only.pem https://127.0.0.1:${caHttpsPort}/json`, + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain('"cacert":true'); + }, 15000); + + itIf(hasCurl && hasOpenssl, 'curl --cacert with the wrong CA still fails verification (exit 60)', async () => { + await createKernelWithNet(); + // Point --cacert at the seeded CA, which did NOT sign caHttpsServer's leaf. + await kernel.writeFile('/tmp/wrong-ca.pem', seededCaPem); + const result = await kernel.exec( + `curl -sS --cacert /tmp/wrong-ca.pem https://127.0.0.1:${caHttpsPort}/json`, + ); + expect(result.exitCode).toBe(60); + expect(result.stderr).toMatch(/certificate|verify|CA/i); + }, 15000); + + itIf(hasCurl && hasOpenssl, 'curl -k exits promptly after an HTTPS keep-alive response', async () => { + await createKernelWithNet(); + const startedAt = Date.now(); + const result = await kernel.exec(`curl -ks https://127.0.0.1:${httpsPort}/keepalive`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe('hello from tls keepalive'); + expect(Date.now() - startedAt).toBeLessThan(8000); + }, 15000); + + itIf(hasCurl, 'curl --compressed round-trips a gzip response body', async () => { + await createKernelWithNet(); + const result = await kernel.exec(`curl -s --compressed http://127.0.0.1:${httpPort}/gzip`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe(COMPRESSION_PAYLOAD); + }, 15000); + + itIf(hasCurl, 'curl --compressed round-trips a brotli response body', async () => { + await createKernelWithNet(); + const result = await kernel.exec(`curl -s --compressed http://127.0.0.1:${httpPort}/brotli`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe(COMPRESSION_PAYLOAD); + }, 15000); + + itIf(hasCurl, 'curl --compressed round-trips a zstd response body', async () => { + await createKernelWithNet(); + const result = await kernel.exec(`curl -s --compressed http://127.0.0.1:${httpPort}/zstd`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toBe(COMPRESSION_PAYLOAD); + }, 15000); + + itIf(hasHttpGetTest && !externalNetworkSkipReason, 'http_get_test reaches an external host over real TCP', async () => { + await createKernelWithNet(); + const result = await execWithRetry(`http_get_test ${EXTERNAL_HOST} ${EXTERNAL_TCP_PORT} /`); + expect(result.exitCode).toBe(0); + expect(result.stdout).toMatch(/HTTP\/1\.[01] (200|301|302)/); + }, 30000); + + itIf(hasCurl && !externalNetworkSkipReason, 'curl reaches a real external HTTP endpoint', async () => { + await createKernelWithNet(); + const result = await execWithRetry( + `curl -fsSL --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 10 --max-time 30 ${EXTERNAL_HTTP_URL}`, + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain(EXTERNAL_EXPECTED_BODY); + }, 30000); + + itIf(hasCurl && !externalNetworkSkipReason, 'curl reaches a real external HTTPS endpoint', async () => { + await createKernelWithNet(); + const result = await execWithRetry( + `curl -fsSL --retry 2 --retry-delay 1 --retry-all-errors --connect-timeout 10 --max-time 30 ${EXTERNAL_HTTPS_URL}`, + ); + expect(result.exitCode).toBe(0); + expect(result.stdout).toContain(EXTERNAL_EXPECTED_BODY); + }, 30000); +}); diff --git a/software/curl/tsconfig.json b/software/curl/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/curl/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/diffutils/agentos-package.json b/software/diffutils/agentos-package.json new file mode 100644 index 0000000000..c9460d579b --- /dev/null +++ b/software/diffutils/agentos-package.json @@ -0,0 +1,12 @@ +{ + "commands": [ + "diff" + ], + "registry": { + "title": "diffutils", + "description": "GNU diff for comparing files.", + "priority": 6, + "image": "/images/registry/diffutils.svg", + "category": "core" + } +} diff --git a/software/diffutils/native/crates/cmd-diff/Cargo.toml b/software/diffutils/native/crates/cmd-diff/Cargo.toml new file mode 100644 index 0000000000..8cdc14cb7d --- /dev/null +++ b/software/diffutils/native/crates/cmd-diff/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-diff" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "diff standalone binary for secure-exec VM" + +[[bin]] +name = "diff" +path = "src/main.rs" + +[dependencies] +secureexec-diff = { path = "../diff" } diff --git a/registry/native/crates/commands/diff/src/main.rs b/software/diffutils/native/crates/cmd-diff/src/main.rs similarity index 100% rename from registry/native/crates/commands/diff/src/main.rs rename to software/diffutils/native/crates/cmd-diff/src/main.rs diff --git a/software/diffutils/native/crates/diff/Cargo.toml b/software/diffutils/native/crates/diff/Cargo.toml new file mode 100644 index 0000000000..587bcf5968 --- /dev/null +++ b/software/diffutils/native/crates/diff/Cargo.toml @@ -0,0 +1,10 @@ +[package] +workspace = "../../../../../toolchain" +name = "secureexec-diff" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "diff implementation for secure-exec standalone binaries" + +[dependencies] +similar = "2" diff --git a/software/diffutils/native/crates/diff/src/lib.rs b/software/diffutils/native/crates/diff/src/lib.rs new file mode 100644 index 0000000000..77e385a00e --- /dev/null +++ b/software/diffutils/native/crates/diff/src/lib.rs @@ -0,0 +1,476 @@ +//! diff -- compare files line by line using the `similar` crate + +use std::collections::HashSet; +use std::ffi::OsString; +use std::fs; +use std::io::{self, Write}; +use std::path::Path; + +use similar::{ChangeTag, TextDiff}; + +struct Options { + unified: bool, + context_fmt: bool, + context_lines: usize, + recursive: bool, + brief: bool, + ignore_case: bool, + ignore_whitespace: bool, + ignore_blank_lines: bool, +} + +impl Default for Options { + fn default() -> Self { + Self { + unified: false, + context_fmt: false, + context_lines: 3, + recursive: false, + brief: false, + ignore_case: false, + ignore_whitespace: false, + ignore_blank_lines: false, + } + } +} + +pub fn main(args: Vec) -> i32 { + let args: Vec = args + .iter() + .map(|a| a.to_string_lossy().to_string()) + .collect(); + let mut opts = Options::default(); + let mut files: Vec = Vec::new(); + let mut i = 1; + + while i < args.len() { + let arg = &args[i]; + match arg.as_str() { + "-u" | "--unified" => opts.unified = true, + "-c" | "--context" => opts.context_fmt = true, + "-r" | "-R" | "--recursive" => opts.recursive = true, + "-q" | "--brief" => opts.brief = true, + "-i" | "--ignore-case" => opts.ignore_case = true, + "-w" | "--ignore-all-space" => opts.ignore_whitespace = true, + "-B" | "--ignore-blank-lines" => opts.ignore_blank_lines = true, + s if s.starts_with("-U") => { + if let Ok(n) = s[2..].parse::() { + opts.unified = true; + opts.context_lines = n; + } + } + s if s.starts_with("-C") => { + if let Ok(n) = s[2..].parse::() { + opts.context_fmt = true; + opts.context_lines = n; + } + } + _ if !arg.starts_with('-') || arg == "-" => { + files.push(arg.clone()); + } + _ => { + eprintln!("diff: unknown option: {}", arg); + return 2; + } + } + i += 1; + } + + if files.len() != 2 { + eprintln!("diff: requires exactly two file or directory arguments"); + return 2; + } + + let path_a = Path::new(&files[0]); + let path_b = Path::new(&files[1]); + + let mut visited_dirs = HashSet::new(); + match diff_paths(path_a, path_b, &opts, &mut visited_dirs) { + Ok(has_diff) => { + if has_diff { + 1 + } else { + 0 + } + } + Err(e) => { + eprintln!("diff: {}", e); + 2 + } + } +} + +fn diff_paths( + path_a: &Path, + path_b: &Path, + opts: &Options, + visited_dirs: &mut HashSet<(std::path::PathBuf, std::path::PathBuf)>, +) -> Result { + let a_is_dir = path_a.is_dir(); + let b_is_dir = path_b.is_dir(); + + if a_is_dir && b_is_dir { + if opts.recursive { + diff_dirs(path_a, path_b, opts, visited_dirs) + } else { + Err(format!("{} is a directory", path_a.display())) + } + } else if a_is_dir || b_is_dir { + if a_is_dir { + let name = path_b.file_name().ok_or("invalid filename")?; + diff_files(&path_a.join(name), path_b, opts) + } else { + let name = path_a.file_name().ok_or("invalid filename")?; + diff_files(path_a, &path_b.join(name), opts) + } + } else { + diff_files(path_a, path_b, opts) + } +} + +fn diff_dirs( + dir_a: &Path, + dir_b: &Path, + opts: &Options, + visited_dirs: &mut HashSet<(std::path::PathBuf, std::path::PathBuf)>, +) -> Result { + let key = ( + fs::canonicalize(dir_a).map_err(|e| format!("{}: {}", dir_a.display(), e))?, + fs::canonicalize(dir_b).map_err(|e| format!("{}: {}", dir_b.display(), e))?, + ); + if !visited_dirs.insert(key) { + return Ok(false); + } + + let mut entries_a = list_dir(dir_a)?; + let mut entries_b = list_dir(dir_b)?; + entries_a.sort(); + entries_b.sort(); + + let mut all_names: Vec = Vec::new(); + for name in &entries_a { + all_names.push(name.clone()); + } + for name in &entries_b { + if !entries_a.contains(name) { + all_names.push(name.clone()); + } + } + all_names.sort(); + + let mut has_diff = false; + + for name in &all_names { + let pa = dir_a.join(name); + let pb = dir_b.join(name); + let a_exists = entries_a.contains(name); + let b_exists = entries_b.contains(name); + + if a_exists && !b_exists { + print_stdout_line(format_args!("Only in {}: {}", dir_a.display(), name))?; + has_diff = true; + } else if !a_exists && b_exists { + print_stdout_line(format_args!("Only in {}: {}", dir_b.display(), name))?; + has_diff = true; + } else { + match diff_paths(&pa, &pb, opts, visited_dirs) { + Ok(d) => { + if d { + has_diff = true; + } + } + Err(e) => { + return Err(e); + } + } + } + } + + Ok(has_diff) +} + +fn list_dir(dir: &Path) -> Result, String> { + let entries = fs::read_dir(dir).map_err(|e| format!("{}: {}", dir.display(), e))?; + let mut names = Vec::new(); + for entry in entries { + let entry = entry.map_err(|e| format!("{}: {}", dir.display(), e))?; + if let Some(name) = entry.file_name().to_str() { + names.push(name.to_string()); + } + } + Ok(names) +} + +fn preprocess(text: &str, opts: &Options) -> String { + let mut s = text.to_string(); + if opts.ignore_blank_lines { + let trailing = s.ends_with('\n'); + s = s + .lines() + .filter(|line| !line.trim().is_empty()) + .collect::>() + .join("\n"); + if trailing { + s.push('\n'); + } + } + if opts.ignore_case { + s = s.to_lowercase(); + } + if opts.ignore_whitespace { + let trailing = s.ends_with('\n'); + s = s + .lines() + .map(|line| { + let mut result = String::new(); + let mut in_ws = false; + for ch in line.chars() { + if ch.is_whitespace() { + if !in_ws { + result.push(' '); + in_ws = true; + } + } else { + result.push(ch); + in_ws = false; + } + } + let trimmed = result.trim(); + trimmed.to_string() + }) + .collect::>() + .join("\n"); + if trailing { + s.push('\n'); + } + } + s +} + +fn diff_files(path_a: &Path, path_b: &Path, opts: &Options) -> Result { + let text_a = read_file(path_a)?; + let text_b = read_file(path_b)?; + + // Check for differences (possibly with preprocessing) + let needs_pp = opts.ignore_case || opts.ignore_whitespace || opts.ignore_blank_lines; + let has_changes = if needs_pp { + preprocess(&text_a, opts) != preprocess(&text_b, opts) + } else { + text_a != text_b + }; + + if !has_changes { + return Ok(false); + } + + if opts.brief { + print_stdout_line(format_args!( + "Files {} and {} differ", + path_a.display(), + path_b.display() + ))?; + return Ok(true); + } + + if opts.recursive { + print_stdout_line(format_args!( + "diff -r {} {}", + path_a.display(), + path_b.display() + ))?; + } + + // Diff the original text for output (all output logic inline to avoid lifetime issues) + let diff = TextDiff::from_lines(&text_a, &text_b); + let label_a = format!("{}", path_a.display()); + let label_b = format!("{}", path_b.display()); + let stdout = io::stdout(); + let mut out = stdout.lock(); + + if opts.unified { + writeln!(out, "--- {}", label_a).map_err(|e| format!("failed to write output: {e}"))?; + writeln!(out, "+++ {}", label_b).map_err(|e| format!("failed to write output: {e}"))?; + for hunk in diff + .unified_diff() + .context_radius(opts.context_lines) + .iter_hunks() + { + write!(out, "{}", hunk).map_err(|e| format!("failed to write output: {e}"))?; + } + } else if opts.context_fmt { + writeln!(out, "*** {}", label_a).map_err(|e| format!("failed to write output: {e}"))?; + writeln!(out, "--- {}", label_b).map_err(|e| format!("failed to write output: {e}"))?; + for hunk in diff + .unified_diff() + .context_radius(opts.context_lines) + .iter_hunks() + { + let mut old_lines: Vec<(ChangeTag, String)> = Vec::new(); + let mut new_lines: Vec<(ChangeTag, String)> = Vec::new(); + let mut old_start = 0usize; + let mut new_start = 0usize; + let mut first = true; + for change in hunk.iter_changes() { + if first { + old_start = change.old_index().unwrap_or(0) + 1; + new_start = change.new_index().unwrap_or(0) + 1; + first = false; + } + match change.tag() { + ChangeTag::Equal => { + old_lines.push((ChangeTag::Equal, change.value().to_string())); + new_lines.push((ChangeTag::Equal, change.value().to_string())); + } + ChangeTag::Delete => { + old_lines.push((ChangeTag::Delete, change.value().to_string())); + } + ChangeTag::Insert => { + new_lines.push((ChangeTag::Insert, change.value().to_string())); + } + } + } + let old_end = old_start + old_lines.len().saturating_sub(1); + let new_end = new_start + new_lines.len().saturating_sub(1); + writeln!(out, "***************").map_err(|e| format!("failed to write output: {e}"))?; + writeln!(out, "*** {},{} ****", old_start, old_end) + .map_err(|e| format!("failed to write output: {e}"))?; + for (tag, line) in &old_lines { + let prefix = match tag { + ChangeTag::Delete => "- ", + ChangeTag::Equal => " ", + _ => continue, + }; + write!(out, "{}{}", prefix, line) + .map_err(|e| format!("failed to write output: {e}"))?; + if !line.ends_with('\n') { + writeln!(out).map_err(|e| format!("failed to write output: {e}"))?; + } + } + writeln!(out, "--- {},{} ----", new_start, new_end) + .map_err(|e| format!("failed to write output: {e}"))?; + for (tag, line) in &new_lines { + let prefix = match tag { + ChangeTag::Insert => "+ ", + ChangeTag::Equal => " ", + _ => continue, + }; + write!(out, "{}{}", prefix, line) + .map_err(|e| format!("failed to write output: {e}"))?; + if !line.ends_with('\n') { + writeln!(out).map_err(|e| format!("failed to write output: {e}"))?; + } + } + } + } else { + // Normal diff format + let old_text: String = diff.old_slices().concat(); + let new_text: String = diff.new_slices().concat(); + let old_lines: Vec<&str> = old_text.lines().collect(); + let new_lines: Vec<&str> = new_text.lines().collect(); + + for op in diff.ops() { + match op { + similar::DiffOp::Equal { .. } => {} + similar::DiffOp::Delete { + old_index, + old_len, + new_index, + } => { + writeln!( + out, + "{}d{}", + format_range(*old_index + 1, *old_len), + new_index + ) + .map_err(|e| format!("failed to write output: {e}"))?; + for i in *old_index..*old_index + old_len { + if i < old_lines.len() { + writeln!(out, "< {}", old_lines[i]) + .map_err(|e| format!("failed to write output: {e}"))?; + } + } + } + similar::DiffOp::Insert { + old_index, + new_index, + new_len, + } => { + writeln!( + out, + "{}a{}", + old_index, + format_range(*new_index + 1, *new_len) + ) + .map_err(|e| format!("failed to write output: {e}"))?; + for i in *new_index..*new_index + new_len { + if i < new_lines.len() { + writeln!(out, "> {}", new_lines[i]) + .map_err(|e| format!("failed to write output: {e}"))?; + } + } + } + similar::DiffOp::Replace { + old_index, + old_len, + new_index, + new_len, + } => { + writeln!( + out, + "{}c{}", + format_range(*old_index + 1, *old_len), + format_range(*new_index + 1, *new_len) + ) + .map_err(|e| format!("failed to write output: {e}"))?; + for i in *old_index..*old_index + old_len { + if i < old_lines.len() { + writeln!(out, "< {}", old_lines[i]) + .map_err(|e| format!("failed to write output: {e}"))?; + } + } + writeln!(out, "---").map_err(|e| format!("failed to write output: {e}"))?; + for i in *new_index..*new_index + new_len { + if i < new_lines.len() { + writeln!(out, "> {}", new_lines[i]) + .map_err(|e| format!("failed to write output: {e}"))?; + } + } + } + } + } + } + + out.flush() + .map_err(|e| format!("failed to write output: {e}"))?; + + Ok(true) +} + +fn print_stdout_line(args: std::fmt::Arguments<'_>) -> Result<(), String> { + let stdout = io::stdout(); + let mut out = stdout.lock(); + writeln!(out, "{args}").map_err(|e| format!("failed to write output: {e}"))?; + out.flush() + .map_err(|e| format!("failed to write output: {e}")) +} + +fn read_file(path: &Path) -> Result { + if path.to_str() == Some("-") { + use std::io::Read; + let mut buf = String::new(); + io::stdin() + .read_to_string(&mut buf) + .map_err(|e| format!("stdin: {}", e))?; + Ok(buf) + } else { + fs::read_to_string(path).map_err(|e| format!("{}: {}", path.display(), e)) + } +} + +fn format_range(start: usize, len: usize) -> String { + if len == 1 { + format!("{}", start) + } else { + format!("{},{}", start, start + len - 1) + } +} diff --git a/software/diffutils/package.json b/software/diffutils/package.json new file mode 100644 index 0000000000..7943248120 --- /dev/null +++ b/software/diffutils/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/diffutils", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "GNU diffutils for secure-exec VMs (diff)", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/codex-cli/src/index.ts b/software/diffutils/src/index.ts similarity index 100% rename from registry/software/codex-cli/src/index.ts rename to software/diffutils/src/index.ts diff --git a/software/diffutils/test/diff.test.ts b/software/diffutils/test/diff.test.ts new file mode 100644 index 0000000000..e3f175b502 --- /dev/null +++ b/software/diffutils/test/diff.test.ts @@ -0,0 +1,145 @@ +import { existsSync } from "node:fs"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + NodeFileSystem, + createKernel, + createWasmVmRuntime, + describeIf, +} from "@agentos/test-harness"; +import type { Kernel } from "@agentos/test-harness"; +import { afterEach, expect, it } from "vitest"; + +const DIFF_COMMAND_DIR = fileURLToPath(new URL("../bin", import.meta.url)); +const hasDiffPackageBinary = existsSync(join(DIFF_COMMAND_DIR, "diff")); + +let tempRoot: string | undefined; + +async function writeFixture(path: string, contents: string): Promise { + if (!tempRoot) throw new Error("fixture root not initialized"); + const hostPath = join(tempRoot, path.replace(/^\/+/, "")); + await mkdir(dirname(hostPath), { recursive: true }); + await writeFile(hostPath, contents); +} + +async function createTestVFS(): Promise { + tempRoot = await mkdtemp(join(tmpdir(), "agentos-diff-")); + await writeFixture("/project/a.txt", "alpha\nbeta\ngamma\n"); + await writeFixture("/project/b.txt", "alpha\ndelta\ngamma\n"); + await writeFixture("/project/same.txt", "alpha\nbeta\ngamma\n"); + await writeFixture("/project/ws-a.txt", "Alpha\n\nbeta value\n"); + await writeFixture("/project/ws-b.txt", "alpha\n BETA VALUE\n"); + await writeFixture("/project/left/common.txt", "one\ntwo\nthree\n"); + await writeFixture("/project/right/common.txt", "one\n2\nthree\n"); + await writeFixture("/project/right/extra.txt", "only on right\n"); + return new NodeFileSystem({ root: tempRoot }); +} + +describeIf(hasDiffPackageBinary, "diff command", { timeout: 10_000 }, () => { + let kernel: Kernel | undefined; + + afterEach(async () => { + await kernel?.dispose(); + kernel = undefined; + if (tempRoot) { + await rm(tempRoot, { recursive: true, force: true }); + tempRoot = undefined; + } + }); + + async function mountFixture(): Promise { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [DIFF_COMMAND_DIR] })); + } + + async function runDiff(args: string[]) { + if (!kernel) throw new Error("kernel not mounted"); + let stdout = ""; + let stderr = ""; + const proc = kernel.spawn("diff", args, { + onStdout: (chunk) => { + stdout += Buffer.from(chunk).toString("utf8"); + }, + onStderr: (chunk) => { + stderr += Buffer.from(chunk).toString("utf8"); + }, + }); + const exitCode = await proc.wait(); + await new Promise((resolve) => setTimeout(resolve, 0)); + return { stdout, stderr, exitCode }; + } + + it("returns zero for identical files", async () => { + await mountFixture(); + + const result = await runDiff(["/project/a.txt", "/project/same.txt"]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stdout).toBe(""); + }); + + it("prints normal diffs for changed files", async () => { + await mountFixture(); + + const result = await runDiff(["/project/a.txt", "/project/b.txt"]); + expect(result.exitCode).toBe(1); + expect(result.stdout).toContain("2c2"); + expect(result.stdout).toContain("< beta"); + expect(result.stdout).toContain("> delta"); + }); + + it("prints unified diffs", async () => { + await mountFixture(); + + const result = await runDiff(["-u", "/project/a.txt", "/project/b.txt"]); + expect(result.exitCode).toBe(1); + expect(result.stdout).toContain("--- /project/a.txt"); + expect(result.stdout).toContain("+++ /project/b.txt"); + expect(result.stdout).toContain("-beta"); + expect(result.stdout).toContain("+delta"); + }); + + it("prints brief output with -q", async () => { + await mountFixture(); + + const result = await runDiff(["-q", "/project/a.txt", "/project/b.txt"]); + expect(result.exitCode).toBe(1); + expect(result.stdout.trim()).toBe( + "Files /project/a.txt and /project/b.txt differ", + ); + }); + + it("honors ignore-case, whitespace, and blank-line flags", async () => { + await mountFixture(); + + const result = await runDiff([ + "-i", + "-w", + "-B", + "/project/ws-a.txt", + "/project/ws-b.txt", + ]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stdout).toBe(""); + }); + + it("compares directories recursively", async () => { + await mountFixture(); + + const result = await runDiff(["-r", "/project/left", "/project/right"]); + expect(result.exitCode).toBe(1); + expect(result.stdout).toContain("Only in /project/right: extra.txt"); + expect(result.stdout).toContain("/project/left/common.txt"); + expect(result.stdout).toContain("/project/right/common.txt"); + }); + + it("returns an error for missing inputs", async () => { + await mountFixture(); + + const result = await runDiff(["/project/a.txt", "/project/missing.txt"]); + expect(result.exitCode).toBe(2); + expect(result.stderr).toContain("/project/missing.txt"); + }); +}); diff --git a/software/diffutils/test/manifest.test.ts b/software/diffutils/test/manifest.test.ts new file mode 100644 index 0000000000..89139d33b8 --- /dev/null +++ b/software/diffutils/test/manifest.test.ts @@ -0,0 +1,19 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const packageDir = new URL("..", import.meta.url).pathname; + +describe("package manifest", () => { + it("declares command binaries", () => { + const manifest = JSON.parse( + readFileSync(join(packageDir, "agentos-package.json"), "utf8"), + ); + + expect(manifest.commands?.length ?? 0).toBeGreaterThan(0); + for (const command of manifest.commands) { + expect(typeof command).toBe("string"); + expect(command.length).toBeGreaterThan(0); + } + }); +}); diff --git a/software/diffutils/tsconfig.json b/software/diffutils/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/diffutils/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/duckdb/agentos-package.json b/software/duckdb/agentos-package.json new file mode 100644 index 0000000000..bb48072275 --- /dev/null +++ b/software/duckdb/agentos-package.json @@ -0,0 +1,12 @@ +{ + "commands": [ + "duckdb" + ], + "registry": { + "title": "DuckDB", + "description": "In-process analytics database CLI.", + "priority": 70, + "image": "/images/registry/duckdb.svg", + "category": "data" + } +} diff --git a/registry/native/c/patches/duckdb/0001-skip-http-object-when-extension-loading-is-disabled.patch b/software/duckdb/native/c/patches/0001-skip-http-object-when-extension-loading-is-disabled.patch similarity index 100% rename from registry/native/c/patches/duckdb/0001-skip-http-object-when-extension-loading-is-disabled.patch rename to software/duckdb/native/c/patches/0001-skip-http-object-when-extension-loading-is-disabled.patch diff --git a/registry/native/c/patches/duckdb/0002-disable-linenoise-on-wasi.patch b/software/duckdb/native/c/patches/0002-disable-linenoise-on-wasi.patch similarity index 100% rename from registry/native/c/patches/duckdb/0002-disable-linenoise-on-wasi.patch rename to software/duckdb/native/c/patches/0002-disable-linenoise-on-wasi.patch diff --git a/registry/native/c/patches/duckdb/0003-disable-shell-system-command-on-wasi.patch b/software/duckdb/native/c/patches/0003-disable-shell-system-command-on-wasi.patch similarity index 100% rename from registry/native/c/patches/duckdb/0003-disable-shell-system-command-on-wasi.patch rename to software/duckdb/native/c/patches/0003-disable-shell-system-command-on-wasi.patch diff --git a/registry/native/c/patches/duckdb/0004-skip-http-util-init-when-extension-loading-is-disabled.patch b/software/duckdb/native/c/patches/0004-skip-http-util-init-when-extension-loading-is-disabled.patch similarity index 100% rename from registry/native/c/patches/duckdb/0004-skip-http-util-init-when-extension-loading-is-disabled.patch rename to software/duckdb/native/c/patches/0004-skip-http-util-init-when-extension-loading-is-disabled.patch diff --git a/registry/native/c/patches/duckdb/0005-disable-shell-pager-popen-on-wasi.patch b/software/duckdb/native/c/patches/0005-disable-shell-pager-popen-on-wasi.patch similarity index 100% rename from registry/native/c/patches/duckdb/0005-disable-shell-pager-popen-on-wasi.patch rename to software/duckdb/native/c/patches/0005-disable-shell-pager-popen-on-wasi.patch diff --git a/software/duckdb/package.json b/software/duckdb/package.json new file mode 100644 index 0000000000..0c892487f9 --- /dev/null +++ b/software/duckdb/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/duckdb", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "DuckDB CLI for secure-exec VMs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/coreutils/src/index.ts b/software/duckdb/src/index.ts similarity index 100% rename from registry/software/coreutils/src/index.ts rename to software/duckdb/src/index.ts diff --git a/registry/tests/wasmvm/duckdb.test.ts b/software/duckdb/test/duckdb.test.ts similarity index 92% rename from registry/tests/wasmvm/duckdb.test.ts rename to software/duckdb/test/duckdb.test.ts index f1dddc9d46..06349c73fb 100644 --- a/registry/tests/wasmvm/duckdb.test.ts +++ b/software/duckdb/test/duckdb.test.ts @@ -23,8 +23,8 @@ import { createWasmVmRuntime, describeIf, itIf, -} from '../helpers.js'; -import type { Kernel } from '../helpers.js'; +} from '@agentos/test-harness'; +import type { Kernel } from '@agentos/test-harness'; import { createServer, type IncomingMessage, type Server, type ServerResponse } from 'node:http'; import { existsSync } from 'node:fs'; import { resolve } from 'node:path'; @@ -33,7 +33,6 @@ import { setTimeout as sleep } from 'node:timers/promises'; const hasWasmDuckDB = existsSync(resolve(C_BUILD_DIR, 'duckdb')); const hasWasmCurl = (existsSync(resolve(COMMANDS_DIR, 'curl')) || existsSync(resolve(C_BUILD_DIR, 'curl'))); -const hasWasmHttpGet = existsSync(resolve(C_BUILD_DIR, 'http_get')); async function mountKernel( filesystem: ReturnType, @@ -52,7 +51,7 @@ async function mountKernel( commandDirs, permissions: { duckdb: 'read-write', - http_get: 'full', + curl: 'full', }, }) ); @@ -82,8 +81,8 @@ async function waitForFilesystemPath( } } -// TODO(P6): requires duckdb WASM artifact, intentionally excluded from the fast registry-build gate. -describe.skip('duckdb command', { timeout: 120_000 }, () => { +// TODO(P6): requires duckdb WASM artifact, intentionally excluded from the fast software-build gate. +describeIf(hasWasmDuckDB, 'duckdb command', { timeout: 120_000 }, () => { let kernel: Kernel | undefined; afterEach(async () => { @@ -215,7 +214,7 @@ describe.skip('duckdb command', { timeout: 120_000 }, () => { }); itIf( - hasWasmCurl || hasWasmHttpGet, + hasWasmCurl, 'queries data fetched over the network through the shared VFS', async () => { const filesystem = createInMemoryFileSystem(); @@ -243,18 +242,10 @@ describe.skip('duckdb command', { timeout: 120_000 }, () => { loopbackExemptPorts: [address.port], }); - let result; - if (hasWasmHttpGet) { - result = await kernel.exec( - `http_get ${address.port} /remote.csv /tmp/remote.csv` - ); - expect(result.exitCode).toBe(0); - } else { - result = await kernel.exec( - `curl -fsS -o /tmp/remote.csv http://127.0.0.1:${address.port}/remote.csv` - ); - expect(result.exitCode).toBe(0); - } + let result = await kernel.exec( + `curl -fsS -o /tmp/remote.csv http://127.0.0.1:${address.port}/remote.csv` + ); + expect(result.exitCode).toBe(0); expect(await filesystem.readTextFile('/tmp/remote.csv')).toContain('city,value'); diff --git a/software/duckdb/tsconfig.json b/software/duckdb/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/duckdb/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/envsubst/agentos-package.json b/software/envsubst/agentos-package.json new file mode 100644 index 0000000000..4d990285ff --- /dev/null +++ b/software/envsubst/agentos-package.json @@ -0,0 +1,11 @@ +{ + "commands": [ + "envsubst" + ], + "registry": { + "title": "envsubst", + "description": "Substitute environment variables in text streams.", + "priority": 10, + "category": "core" + } +} diff --git a/registry/native/c/programs/envsubst.c b/software/envsubst/native/c/envsubst.c similarity index 100% rename from registry/native/c/programs/envsubst.c rename to software/envsubst/native/c/envsubst.c diff --git a/software/envsubst/package.json b/software/envsubst/package.json new file mode 100644 index 0000000000..397f9dc2c2 --- /dev/null +++ b/software/envsubst/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/envsubst", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "envsubst environment variable substitution command for AgentOS VMs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@agentos/test-harness": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/curl/src/index.ts b/software/envsubst/src/index.ts similarity index 100% rename from registry/software/curl/src/index.ts rename to software/envsubst/src/index.ts diff --git a/registry/tests/wasmvm/envsubst.test.ts b/software/envsubst/test/envsubst.test.ts similarity index 95% rename from registry/tests/wasmvm/envsubst.test.ts rename to software/envsubst/test/envsubst.test.ts index 3415284a6d..4897d8b700 100644 --- a/registry/tests/wasmvm/envsubst.test.ts +++ b/software/envsubst/test/envsubst.test.ts @@ -10,9 +10,9 @@ */ import { describe, it, expect, afterEach } from 'vitest'; -import { createWasmVmRuntime } from '../helpers.js'; -import { C_BUILD_DIR, COMMANDS_DIR, createKernel } from '../helpers.js'; -import type { Kernel } from '../helpers.js'; +import { createWasmVmRuntime } from '@agentos/test-harness'; +import { C_BUILD_DIR, COMMANDS_DIR, createKernel, describeIf, hasCWasmBinaries } from '@agentos/test-harness'; +import type { Kernel } from '@agentos/test-harness'; // Minimal in-memory VFS for kernel tests class SimpleVFS { @@ -102,7 +102,7 @@ class SimpleVFS { } } -describe('envsubst command', () => { +describeIf(hasCWasmBinaries('envsubst'), 'envsubst command', () => { let kernel: Kernel; afterEach(async () => { diff --git a/software/envsubst/tsconfig.json b/software/envsubst/tsconfig.json new file mode 100644 index 0000000000..73c85c8fee --- /dev/null +++ b/software/envsubst/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "composite": true, + "tsBuildInfoFile": "dist/.tsbuildinfo" + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/software/everything/agentos-package.json b/software/everything/agentos-package.json new file mode 100644 index 0000000000..6dc26907f3 --- /dev/null +++ b/software/everything/agentos-package.json @@ -0,0 +1,8 @@ +{ + "registry": { + "title": "Everything", + "description": "Meta-package: all available WASM command packages.", + "priority": 110, + "category": "meta" + } +} diff --git a/software/everything/package.json b/software/everything/package.json new file mode 100644 index 0000000000..95ec6ff1ea --- /dev/null +++ b/software/everything/package.json @@ -0,0 +1,58 @@ +{ + "name": "@agentos-software/everything", + "version": "0.3.0-rc.2", + "type": "module", + "license": "Apache-2.0", + "description": "All available WASM command packages for secure-exec VMs in a single bundle", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "tsc", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "dependencies": { + "@agentos-software/coreutils": "workspace:*", + "@agentos-software/sed": "workspace:*", + "@agentos-software/grep": "workspace:*", + "@agentos-software/gawk": "workspace:*", + "@agentos-software/findutils": "workspace:*", + "@agentos-software/diffutils": "workspace:*", + "@agentos-software/tar": "workspace:*", + "@agentos-software/gzip": "workspace:*", + "@agentos-software/curl": "workspace:*", + "@agentos-software/wget": "workspace:*", + "@agentos-software/duckdb": "workspace:*", + "@agentos-software/envsubst": "workspace:*", + "@agentos-software/git": "workspace:*", + "@agentos-software/sqlite3": "workspace:*", + "@agentos-software/vim": "workspace:*", + "@agentos-software/zip": "workspace:*", + "@agentos-software/unzip": "workspace:*", + "@agentos-software/jq": "workspace:*", + "@agentos-software/ripgrep": "workspace:*", + "@agentos-software/fd": "workspace:*", + "@agentos-software/tree": "workspace:*", + "@agentos-software/file": "workspace:*", + "@agentos-software/yq": "workspace:*", + "@agentos-software/codex-cli": "workspace:*" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/software/everything/src/index.ts b/software/everything/src/index.ts new file mode 100644 index 0000000000..c45ab24679 --- /dev/null +++ b/software/everything/src/index.ts @@ -0,0 +1,79 @@ +import coreutils from "@agentos-software/coreutils"; +import sed from "@agentos-software/sed"; +import grep from "@agentos-software/grep"; +import gawk from "@agentos-software/gawk"; +import findutils from "@agentos-software/findutils"; +import diffutils from "@agentos-software/diffutils"; +import tar from "@agentos-software/tar"; +import gzip from "@agentos-software/gzip"; +import curl from "@agentos-software/curl"; +import wget from "@agentos-software/wget"; +import duckdb from "@agentos-software/duckdb"; +import envsubst from "@agentos-software/envsubst"; +import git from "@agentos-software/git"; +import sqlite3 from "@agentos-software/sqlite3"; +import vim from "@agentos-software/vim"; +import zip from "@agentos-software/zip"; +import unzip from "@agentos-software/unzip"; +import jq from "@agentos-software/jq"; +import ripgrep from "@agentos-software/ripgrep"; +import fd from "@agentos-software/fd"; +import tree from "@agentos-software/tree"; +import file from "@agentos-software/file"; +import yq from "@agentos-software/yq"; +import codex from "@agentos-software/codex-cli"; + +const everything = [ + coreutils, + sed, + grep, + gawk, + findutils, + diffutils, + tar, + gzip, + curl, + wget, + duckdb, + envsubst, + git, + sqlite3, + vim, + zip, + unzip, + jq, + ripgrep, + fd, + tree, + file, + yq, + codex, +]; + +export default everything; +export { + coreutils, + sed, + grep, + gawk, + findutils, + diffutils, + tar, + gzip, + curl, + wget, + duckdb, + envsubst, + git, + sqlite3, + vim, + zip, + unzip, + jq, + ripgrep, + fd, + tree, + file, + yq, + codex, +}; diff --git a/software/everything/test/everything.test.ts b/software/everything/test/everything.test.ts new file mode 100644 index 0000000000..f9d1ea24af --- /dev/null +++ b/software/everything/test/everything.test.ts @@ -0,0 +1,81 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import everything, { + codex, + coreutils, + curl, + diffutils, + duckdb, + envsubst, + fd, + file, + findutils, + gawk, + git, + grep, + gzip, + jq, + ripgrep, + sed, + sqlite3, + tar, + tree, + unzip, + vim, + wget, + yq, + zip, +} from "../src/index.js"; + +const packageDir = new URL("..", import.meta.url).pathname; + +const expectedMembers = [ + coreutils, + sed, + grep, + gawk, + findutils, + diffutils, + tar, + gzip, + curl, + wget, + duckdb, + envsubst, + git, + sqlite3, + vim, + zip, + unzip, + jq, + ripgrep, + fd, + tree, + file, + yq, + codex, +]; + +describe("everything meta-package", () => { + it("has a registry manifest", () => { + const manifest = JSON.parse( + readFileSync(join(packageDir, "agentos-package.json"), "utf8"), + ); + + expect(manifest.registry).toMatchObject({ + title: "Everything", + category: "meta", + }); + }); + + it("exports every command package descriptor once", () => { + expect(everything).toEqual(expectedMembers); + expect(new Set(everything).size).toBe(everything.length); + for (const descriptor of everything) { + expect(descriptor).toEqual({ + packagePath: expect.stringMatching(/\/package\.aospkg$/), + }); + } + }); +}); diff --git a/software/everything/tsconfig.json b/software/everything/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/everything/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/fd/agentos-package.json b/software/fd/agentos-package.json new file mode 100644 index 0000000000..636140edab --- /dev/null +++ b/software/fd/agentos-package.json @@ -0,0 +1,11 @@ +{ + "commands": [ + "fd" + ], + "registry": { + "title": "fd", + "description": "Fast file finder.", + "priority": 30, + "category": "developer-tools" + } +} diff --git a/software/fd/native/crates/cmd-fd/Cargo.toml b/software/fd/native/crates/cmd-fd/Cargo.toml new file mode 100644 index 0000000000..ef6223d123 --- /dev/null +++ b/software/fd/native/crates/cmd-fd/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-fd" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "fd standalone binary for secure-exec VM" +publish = false + +[dependencies] +# fd-find is a bin-only crate. `toolchain/Makefile` builds its upstream `fd` +# binary directly; this local package exists only so `cmd/fd` is discoverable +# and the upstream package is present in Cargo's resolved/vendor set. +fd-find = { version = "10.4.2", default-features = false } diff --git a/software/fd/native/crates/cmd-fd/src/lib.rs b/software/fd/native/crates/cmd-fd/src/lib.rs new file mode 100644 index 0000000000..a372ecb5e7 --- /dev/null +++ b/software/fd/native/crates/cmd-fd/src/lib.rs @@ -0,0 +1 @@ +//! Build trigger for the upstream `fd-find` command package. diff --git a/software/fd/package.json b/software/fd/package.json new file mode 100644 index 0000000000..014cc77cc2 --- /dev/null +++ b/software/fd/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/fd", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "fd fast file finder for secure-exec VMs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/diffutils/src/index.ts b/software/fd/src/index.ts similarity index 100% rename from registry/software/diffutils/src/index.ts rename to software/fd/src/index.ts diff --git a/software/fd/test/fd.test.ts b/software/fd/test/fd.test.ts new file mode 100644 index 0000000000..0b76049d2f --- /dev/null +++ b/software/fd/test/fd.test.ts @@ -0,0 +1,188 @@ +/** + * Integration tests for fd (fd-find) command. + * + * Verifies file finding with regex patterns, extension filters, type filters, + * hidden file skipping, and empty directory handling via kernel.exec() with + * real WASM binaries. + * + * Note: kernel.exec() wraps commands in sh -c. Brush-shell currently returns + * exit code 17 for all child commands (benign "could not retrieve pid" issue). + * Tests verify stdout correctness rather than exit code. + */ + +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { describe, it, expect, afterEach } from 'vitest'; +import { createWasmVmRuntime } from '@agentos/test-harness'; +import { COMMANDS_DIR, createKernel, describeIf, hasWasmBinaries, NodeFileSystem } from '@agentos/test-harness'; +import type { Kernel } from '@agentos/test-harness'; + +let tempRoot: string | undefined; + +/** Create a VFS pre-populated with a test directory structure */ +async function createTestVFS(): Promise { + tempRoot = await mkdtemp(join(tmpdir(), 'agentos-fd-')); + + // /project/ + // src/ + // main.js + // utils.js + // helpers.ts + // lib/ + // parser.js + // docs/ + // readme.md + // .hidden/ + // secret.txt + // .gitignore + // config.json + await writeFixture('/project/src/main.js', 'console.log("main")'); + await writeFixture('/project/src/utils.js', 'export {}'); + await writeFixture('/project/src/helpers.ts', 'export {}'); + await writeFixture('/project/lib/parser.js', 'module.exports = {}'); + await writeFixture('/project/docs/readme.md', '# Readme'); + await writeFixture('/project/.hidden/secret.txt', 'secret'); + await writeFixture('/project/.gitignore', 'node_modules'); + await writeFixture('/project/config.json', '{}'); + // /empty/ — empty directory + await mkdir(join(tempRoot, 'empty'), { recursive: true }); + return new NodeFileSystem({ root: tempRoot }); +} + +async function writeFixture(path: string, contents: string): Promise { + if (!tempRoot) throw new Error('fixture root not initialized'); + const hostPath = join(tempRoot, path.replace(/^\/+/, '')); + await mkdir(dirname(hostPath), { recursive: true }); + await writeFile(hostPath, contents); +} + +/** Parse fd output lines, sorted for deterministic comparison */ +function parseLines(stdout: string): string[] { + return stdout.split('\n').filter(l => l.length > 0).sort(); +} + +describeIf(hasWasmBinaries, 'fd-find command', { timeout: 10_000 }, () => { + let kernel: Kernel; + + afterEach(async () => { + await kernel?.dispose(); + if (tempRoot) { + await rm(tempRoot, { recursive: true, force: true }); + tempRoot = undefined; + } + }); + + it('reports the upstream fd-find version', async () => { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec('fd --version', {}); + expect(result.stdout.trim()).toBe('fd 10.4.2'); + }); + + it('finds files matching regex pattern in current directory', async () => { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec('fd main /project', {}); + const lines = parseLines(result.stdout); + expect(lines).toContain('/project/src/main.js'); + }); + + it('finds all .js files with -e js', async () => { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec('fd -e js . /project', {}); + const lines = parseLines(result.stdout); + expect(lines).toContain('/project/src/main.js'); + expect(lines).toContain('/project/src/utils.js'); + expect(lines).toContain('/project/lib/parser.js'); + // .ts files should NOT match + expect(lines).not.toContain('/project/src/helpers.ts'); + }); + + it('finds only files with -t f', async () => { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec('fd -t f . /project', {}); + const lines = parseLines(result.stdout); + // All entries should be files, not directories + for (const line of lines) { + const stat = await vfs.stat(line); + expect(stat.isDirectory).toBe(false); + } + // Should include known files + expect(lines).toContain('/project/src/main.js'); + expect(lines).toContain('/project/config.json'); + }); + + it('finds only directories with -t d', async () => { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec('fd -t d . /project', {}); + const lines = parseLines(result.stdout); + // All entries should be directories + for (const line of lines) { + const stat = await vfs.stat(line); + expect(stat.isDirectory).toBe(true); + } + // Should include known directories (hidden skipped by default) + expect(lines).toContain('/project/src/'); + expect(lines).toContain('/project/lib/'); + expect(lines).toContain('/project/docs/'); + }); + + it('returns no results for empty directory', async () => { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec('fd . /empty', {}); + expect(result.stdout.trim()).toBe(''); + }); + + it('returns empty output when no files match pattern', async () => { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec('fd zzzznonexistent /project', {}); + expect(result.stdout.trim()).toBe(''); + }); + + it('skips hidden files and directories by default', async () => { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec('fd . /project', {}); + const lines = parseLines(result.stdout); + // Hidden files/dirs should NOT appear + const hiddenEntries = lines.filter(l => { + const parts = l.split('/'); + return parts.some(p => p.startsWith('.') && p.length > 1); + }); + expect(hiddenEntries).toEqual([]); + }); + + it('includes hidden files with -H flag', async () => { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec('fd -H . /project', {}); + const lines = parseLines(result.stdout); + // Hidden items should now appear + expect(lines).toContain('/project/.gitignore'); + expect(lines).toContain('/project/.hidden/'); + }); +}); diff --git a/software/fd/tsconfig.json b/software/fd/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/fd/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/file/agentos-package.json b/software/file/agentos-package.json new file mode 100644 index 0000000000..ef6f5acc1c --- /dev/null +++ b/software/file/agentos-package.json @@ -0,0 +1,11 @@ +{ + "commands": [ + "file" + ], + "registry": { + "title": "file", + "description": "Detect file types.", + "priority": 2, + "category": "core" + } +} diff --git a/software/file/native/crates/cmd-file/Cargo.toml b/software/file/native/crates/cmd-file/Cargo.toml new file mode 100644 index 0000000000..65e6ee9f90 --- /dev/null +++ b/software/file/native/crates/cmd-file/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-file" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "file standalone binary for secure-exec VM" + +[[bin]] +name = "file" +path = "src/main.rs" + +[dependencies] +secureexec-file-cmd = { path = "../file-cmd" } diff --git a/registry/native/crates/commands/file/src/main.rs b/software/file/native/crates/cmd-file/src/main.rs similarity index 100% rename from registry/native/crates/commands/file/src/main.rs rename to software/file/native/crates/cmd-file/src/main.rs diff --git a/software/file/native/crates/file-cmd/Cargo.toml b/software/file/native/crates/file-cmd/Cargo.toml new file mode 100644 index 0000000000..203113931a --- /dev/null +++ b/software/file/native/crates/file-cmd/Cargo.toml @@ -0,0 +1,10 @@ +[package] +workspace = "../../../../../toolchain" +name = "secureexec-file-cmd" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "file command implementation for secure-exec standalone binaries" + +[dependencies] +infer = "0.16" diff --git a/software/file/native/crates/file-cmd/src/lib.rs b/software/file/native/crates/file-cmd/src/lib.rs new file mode 100644 index 0000000000..38f507dd73 --- /dev/null +++ b/software/file/native/crates/file-cmd/src/lib.rs @@ -0,0 +1,336 @@ +//! file -- determine file type +//! +//! Magic byte detection using the `infer` crate, plus text/binary heuristic. +//! Supports -b (brief, no filename prefix), -i (output MIME type). + +use std::ffi::OsString; +use std::fs; +use std::io::{self, Read, Write}; + +const DETECTION_BYTES: usize = 8192; + +pub fn main(args: Vec) -> i32 { + let str_args: Vec = args + .iter() + .skip(1) + .map(|a| a.to_string_lossy().to_string()) + .collect(); + + let mut brief = false; + let mut mime = false; + let mut filenames: Vec = Vec::new(); + + let mut i = 0; + while i < str_args.len() { + match str_args[i].as_str() { + "-b" | "--brief" => brief = true, + "-i" | "--mime-type" | "--mime" => mime = true, + // Combined short flags + s if s.starts_with('-') && s.len() > 1 && !s.starts_with("--") => { + for ch in s[1..].chars() { + match ch { + 'b' => brief = true, + 'i' => mime = true, + _ => { + eprintln!("file: unknown option '{}'", s); + return 1; + } + } + } + } + "-" => { + // Read from stdin + filenames.push("-".to_string()); + } + s if s.starts_with('-') && s.len() > 1 => { + eprintln!("file: unknown option '{}'", s); + return 1; + } + _ => filenames.push(str_args[i].clone()), + } + i += 1; + } + + if filenames.is_empty() { + eprintln!("Usage: file [-bi] FILE..."); + return 1; + } + + let stdout = io::stdout(); + let mut out = stdout.lock(); + let mut exit_code = 0; + + for filename in &filenames { + let data = if filename == "-" { + match read_head_from_reader(io::stdin().lock(), DETECTION_BYTES) { + Ok(data) => data, + Err(e) => { + eprintln!("file: stdin: {}", e); + exit_code = 1; + continue; + } + } + } else { + // Check if path exists and what kind it is + match fs::metadata(filename) { + Ok(meta) => { + if meta.is_dir() { + if let Err(error) = print_result( + &mut out, + filename, + brief, + "directory", + if mime { "inode/directory" } else { "" }, + mime, + ) { + return output_error(error); + } + continue; + } + if meta.is_symlink() { + if let Err(error) = print_result( + &mut out, + filename, + brief, + "symbolic link", + if mime { "inode/symlink" } else { "" }, + mime, + ) { + return output_error(error); + } + continue; + } + if meta.len() == 0 { + if let Err(error) = print_result( + &mut out, + filename, + brief, + "empty", + if mime { "inode/x-empty" } else { "" }, + mime, + ) { + return output_error(error); + } + continue; + } + // Read file data (up to 8KB for detection) + match read_head(filename, DETECTION_BYTES) { + Ok(data) => data, + Err(e) => { + eprintln!("file: {}: {}", filename, e); + exit_code = 1; + continue; + } + } + } + Err(e) => { + eprintln!("file: {}: {}", filename, e); + exit_code = 1; + continue; + } + } + }; + + let (desc, mime_type) = identify(&data); + + if mime { + if let Err(error) = print_result(&mut out, filename, brief, &desc, &mime_type, true) { + return output_error(error); + } + } else { + if let Err(error) = print_result(&mut out, filename, brief, &desc, &mime_type, false) { + return output_error(error); + } + } + } + + if let Err(error) = out.flush() { + return output_error(error); + } + + exit_code +} + +fn output_error(error: io::Error) -> i32 { + eprintln!("file: failed to write output: {error}"); + 1 +} + +fn read_head(path: &str, max: usize) -> io::Result> { + let mut f = fs::File::open(path)?; + read_head_from_reader(&mut f, max) +} + +fn read_head_from_reader(mut reader: impl Read, max: usize) -> io::Result> { + let mut buf = vec![0u8; max]; + let n = reader.read(&mut buf)?; + buf.truncate(n); + Ok(buf) +} + +fn print_result( + out: &mut W, + filename: &str, + brief: bool, + desc: &str, + mime_type: &str, + use_mime: bool, +) -> io::Result<()> { + if use_mime && !mime_type.is_empty() { + if brief { + writeln!(out, "{}", mime_type) + } else { + writeln!(out, "{}: {}", filename, mime_type) + } + } else if brief { + writeln!(out, "{}", desc) + } else { + writeln!(out, "{}: {}", filename, desc) + } +} + +fn identify(data: &[u8]) -> (String, String) { + if data.is_empty() { + return ("empty".to_string(), "inode/x-empty".to_string()); + } + + // Check for shebang scripts + if data.len() >= 2 && data[0] == b'#' && data[1] == b'!' { + let first_line = data + .iter() + .take(128) + .take_while(|&&b| b != b'\n') + .copied() + .collect::>(); + if let Ok(line) = std::str::from_utf8(&first_line) { + let interp = line.trim_start_matches("#!"); + let interp = interp.trim(); + // Extract interpreter name + let name = interp.split_whitespace().next().unwrap_or(interp); + let basename = match name.rfind('/') { + Some(pos) => &name[pos + 1..], + None => name, + }; + // Handle "env " pattern + if basename == "env" { + let prog = interp.split_whitespace().nth(1).unwrap_or("script"); + return ( + format!("{} script, ASCII text executable", prog), + "text/x-script".to_string(), + ); + } + return ( + format!("{} script, ASCII text executable", basename), + "text/x-script".to_string(), + ); + } + } + + // Try infer crate for magic byte detection + if let Some(kind) = infer::get(data) { + let desc = match kind.mime_type() { + "image/png" => "PNG image data", + "image/jpeg" => "JPEG image data", + "image/gif" => "GIF image data", + "image/webp" => "WebP image data", + "image/bmp" => "BMP image data", + "image/tiff" => "TIFF image data", + "image/svg+xml" => "SVG image data", + "image/x-icon" => "ICO image data", + "application/pdf" => "PDF document", + "application/zip" => "Zip archive data", + "application/gzip" => "gzip compressed data", + "application/x-bzip2" => "bzip2 compressed data", + "application/x-xz" => "XZ compressed data", + "application/x-tar" => "POSIX tar archive", + "application/x-rar-compressed" => "RAR archive data", + "application/x-7z-compressed" => "7-zip archive data", + "application/x-executable" | "application/x-elf" => "ELF executable", + "application/wasm" => "WebAssembly (wasm) binary module", + "application/x-mach-binary" => "Mach-O binary", + "audio/mpeg" => "MPEG audio", + "audio/ogg" => "Ogg audio", + "audio/flac" => "FLAC audio", + "audio/wav" | "audio/x-wav" => "RIFF WAVE audio", + "video/mp4" => "MPEG-4 video", + "video/webm" => "WebM video", + "video/x-matroska" => "Matroska video", + "application/x-sqlite3" => "SQLite 3.x database", + "font/woff" => "Web Open Font Format", + "font/woff2" => "Web Open Font Format 2", + _ => kind.mime_type(), + }; + return (desc.to_string(), kind.mime_type().to_string()); + } + + // Check for known text patterns + if is_json(data) { + return ("JSON text data".to_string(), "application/json".to_string()); + } + if is_xml(data) { + return ("XML document".to_string(), "text/xml".to_string()); + } + if is_html(data) { + return ("HTML document".to_string(), "text/html".to_string()); + } + + // Text vs binary heuristic + if is_text(data) { + return ("ASCII text".to_string(), "text/plain".to_string()); + } + + ("data".to_string(), "application/octet-stream".to_string()) +} + +fn is_text(data: &[u8]) -> bool { + // Check first 8KB for non-text bytes + let check_len = data.len().min(8192); + let mut null_count = 0; + for &b in &data[..check_len] { + if b == 0 { + null_count += 1; + if null_count > 0 { + return false; + } + } + // Allow common control chars: tab, newline, carriage return, form feed, backspace, escape + if b < 0x08 || (b > 0x0D && b < 0x20 && b != 0x1B) { + if b != 0x00 { + // already counted nulls + return false; + } + } + } + true +} + +fn is_json(data: &[u8]) -> bool { + // Quick check: starts with { or [, ignoring leading whitespace + let trimmed = skip_whitespace(data); + !trimmed.is_empty() && (trimmed[0] == b'{' || trimmed[0] == b'[') +} + +fn is_xml(data: &[u8]) -> bool { + let trimmed = skip_whitespace(data); + trimmed.starts_with(b" bool { + let trimmed = skip_whitespace(data); + let lower: Vec = trimmed + .iter() + .take(64) + .map(|b| b.to_ascii_lowercase()) + .collect(); + lower.starts_with(b" &[u8] { + let mut i = 0; + while i < data.len() + && (data[i] == b' ' || data[i] == b'\t' || data[i] == b'\n' || data[i] == b'\r') + { + i += 1; + } + &data[i..] +} diff --git a/software/file/package.json b/software/file/package.json new file mode 100644 index 0000000000..424733c085 --- /dev/null +++ b/software/file/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/file", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "file type detection for secure-exec VMs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/duckdb/src/index.ts b/software/file/src/index.ts similarity index 100% rename from registry/software/duckdb/src/index.ts rename to software/file/src/index.ts diff --git a/software/file/test/file.test.ts b/software/file/test/file.test.ts new file mode 100644 index 0000000000..70657e05b8 --- /dev/null +++ b/software/file/test/file.test.ts @@ -0,0 +1,152 @@ +import { existsSync } from "node:fs"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + NodeFileSystem, + createKernel, + createWasmVmRuntime, + describeIf, +} from "@agentos/test-harness"; +import type { Kernel } from "@agentos/test-harness"; +import { afterEach, expect, it } from "vitest"; + +const FILE_COMMAND_DIR = fileURLToPath(new URL("../bin", import.meta.url)); +const hasFilePackageBinary = existsSync(join(FILE_COMMAND_DIR, "file")); + +let tempRoot: string | undefined; + +async function writeFixture(path: string, contents: string | Buffer): Promise { + if (!tempRoot) throw new Error("fixture root not initialized"); + const hostPath = join(tempRoot, path.replace(/^\/+/, "")); + await mkdir(dirname(hostPath), { recursive: true }); + await writeFile(hostPath, contents); +} + +async function createTestVFS(): Promise { + tempRoot = await mkdtemp(join(tmpdir(), "agentos-file-")); + await writeFixture("/project/text.txt", "hello from AgentOS\n"); + await writeFixture("/project/data.json", '{ "ok": true }\n'); + await writeFixture("/project/script.sh", "#!/usr/bin/env bash\necho hello\n"); + await writeFixture( + "/project/image.png", + Buffer.from([ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, + 0x0d, + ]), + ); + await writeFixture("/project/empty", ""); + await mkdir(join(tempRoot, "project/dir"), { recursive: true }); + return new NodeFileSystem({ root: tempRoot }); +} + +describeIf(hasFilePackageBinary, "file command", { timeout: 10_000 }, () => { + let kernel: Kernel | undefined; + + afterEach(async () => { + await kernel?.dispose(); + kernel = undefined; + if (tempRoot) { + await rm(tempRoot, { recursive: true, force: true }); + tempRoot = undefined; + } + }); + + async function mountFixture(): Promise { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [FILE_COMMAND_DIR] })); + } + + async function runFile(args: string[], stdin?: string) { + if (!kernel) throw new Error("kernel not mounted"); + let stdout = ""; + let stderr = ""; + const proc = kernel.spawn("file", args, { + streamStdin: stdin !== undefined, + onStdout: (chunk) => { + stdout += Buffer.from(chunk).toString("utf8"); + }, + onStderr: (chunk) => { + stderr += Buffer.from(chunk).toString("utf8"); + }, + }); + if (stdin !== undefined) { + proc.writeStdin(stdin); + proc.closeStdin(); + } + const exitCode = await proc.wait(); + await new Promise((resolve) => setTimeout(resolve, 0)); + return { stdout, stderr, exitCode }; + } + + it("identifies text, JSON, scripts, images, empty files, and directories", async () => { + await mountFixture(); + + const result = await runFile([ + "/project/text.txt", + "/project/data.json", + "/project/script.sh", + "/project/image.png", + "/project/empty", + "/project/dir", + ]); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stdout).toContain("/project/text.txt: ASCII text"); + expect(result.stdout).toContain("/project/data.json: JSON text data"); + expect(result.stdout).toContain( + "/project/script.sh: bash script, ASCII text executable", + ); + expect(result.stdout).toContain("/project/image.png: PNG image data"); + expect(result.stdout).toContain("/project/empty: empty"); + expect(result.stdout).toContain("/project/dir: directory"); + }); + + it("prints brief descriptions with -b", async () => { + await mountFixture(); + + const result = await runFile(["-b", "/project/data.json"]); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stdout.trim()).toBe("JSON text data"); + }); + + it("prints MIME types with -i", async () => { + await mountFixture(); + + const result = await runFile([ + "-i", + "/project/text.txt", + "/project/image.png", + "/project/dir", + ]); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stdout).toContain("/project/text.txt: text/plain"); + expect(result.stdout).toContain("/project/image.png: image/png"); + expect(result.stdout).toContain("/project/dir: inode/directory"); + }); + + it("reads stdin when the operand is -", async () => { + await mountFixture(); + + const result = await runFile( + ["-b", "-"], + "#!/usr/bin/env node\nconsole.log('hello')\n", + ); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stdout.trim()).toBe("node script, ASCII text executable"); + }); + + it("returns an error for missing inputs", async () => { + await mountFixture(); + + const result = await runFile(["/project/missing.txt"]); + + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain("/project/missing.txt"); + }); +}); diff --git a/software/file/test/manifest.test.ts b/software/file/test/manifest.test.ts new file mode 100644 index 0000000000..89139d33b8 --- /dev/null +++ b/software/file/test/manifest.test.ts @@ -0,0 +1,19 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const packageDir = new URL("..", import.meta.url).pathname; + +describe("package manifest", () => { + it("declares command binaries", () => { + const manifest = JSON.parse( + readFileSync(join(packageDir, "agentos-package.json"), "utf8"), + ); + + expect(manifest.commands?.length ?? 0).toBeGreaterThan(0); + for (const command of manifest.commands) { + expect(typeof command).toBe("string"); + expect(command.length).toBeGreaterThan(0); + } + }); +}); diff --git a/software/file/tsconfig.json b/software/file/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/file/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/findutils/agentos-package.json b/software/findutils/agentos-package.json new file mode 100644 index 0000000000..2415eb8b79 --- /dev/null +++ b/software/findutils/agentos-package.json @@ -0,0 +1,13 @@ +{ + "commands": [ + "find", + "xargs" + ], + "registry": { + "title": "findutils", + "description": "GNU find and xargs for file searching and batch execution.", + "priority": 15, + "image": "/images/registry/findutils.svg", + "category": "core" + } +} diff --git a/software/findutils/native/crates/cmd-find/Cargo.toml b/software/findutils/native/crates/cmd-find/Cargo.toml new file mode 100644 index 0000000000..19cc8f38f4 --- /dev/null +++ b/software/findutils/native/crates/cmd-find/Cargo.toml @@ -0,0 +1,15 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-find" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "find standalone binary for secure-exec VM" + +[[bin]] +name = "find" +path = "src/main.rs" + +[dependencies] +findutils = { version = "0.9.0", default-features = false } +uucore = { version = "0.9.0", default-features = false } diff --git a/software/findutils/native/crates/cmd-find/src/main.rs b/software/findutils/native/crates/cmd-find/src/main.rs new file mode 100644 index 0000000000..8990e43353 --- /dev/null +++ b/software/findutils/native/crates/cmd-find/src/main.rs @@ -0,0 +1,11 @@ +fn main() { + uucore::panic::mute_sigpipe_panic(); + + let args = std::env::args().collect::>(); + let args = args + .iter() + .map(std::convert::AsRef::as_ref) + .collect::>(); + let deps = findutils::find::StandardDependencies::new(); + std::process::exit(findutils::find::find_main(&args, &deps)); +} diff --git a/software/findutils/native/crates/cmd-xargs/Cargo.toml b/software/findutils/native/crates/cmd-xargs/Cargo.toml new file mode 100644 index 0000000000..1037c80a58 --- /dev/null +++ b/software/findutils/native/crates/cmd-xargs/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-xargs" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "xargs standalone binary for secure-exec VM" + +[[bin]] +name = "xargs" +path = "src/main.rs" + +[dependencies] +findutils = { version = "0.9.0", default-features = false } diff --git a/software/findutils/native/crates/cmd-xargs/src/main.rs b/software/findutils/native/crates/cmd-xargs/src/main.rs new file mode 100644 index 0000000000..85586163c6 --- /dev/null +++ b/software/findutils/native/crates/cmd-xargs/src/main.rs @@ -0,0 +1,8 @@ +fn main() { + let args = std::env::args().collect::>(); + let args = args + .iter() + .map(std::convert::AsRef::as_ref) + .collect::>(); + std::process::exit(findutils::xargs::xargs_main(&args)); +} diff --git a/software/findutils/package.json b/software/findutils/package.json new file mode 100644 index 0000000000..dd3cdd04c3 --- /dev/null +++ b/software/findutils/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/findutils", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "GNU findutils for secure-exec VMs (find, xargs)", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/fd/src/index.ts b/software/findutils/src/index.ts similarity index 100% rename from registry/software/fd/src/index.ts rename to software/findutils/src/index.ts diff --git a/software/findutils/test/findutils.test.ts b/software/findutils/test/findutils.test.ts new file mode 100644 index 0000000000..f343e46864 --- /dev/null +++ b/software/findutils/test/findutils.test.ts @@ -0,0 +1,105 @@ +/** + * Package-owned integration tests for GNU findutils commands. + */ + +import { afterEach, expect, it } from "vitest"; +import { + COMMANDS_DIR, + createInMemoryFileSystem, + createKernel, + createWasmVmRuntime, + describeIf, + hasWasmBinaries, + type Kernel, +} from "@agentos/test-harness"; + +function parseLines(stdout: string): string[] { + return stdout + .split("\n") + .map((line) => line.trim()) + .filter(Boolean) + .sort(); +} + +describeIf(hasWasmBinaries, "findutils commands", { timeout: 10_000 }, () => { + let kernel: Kernel; + + afterEach(async () => { + await kernel?.dispose(); + }); + + it("find matches files by name", async () => { + const vfs = createInMemoryFileSystem(); + await vfs.mkdir("/project/src", { recursive: true }); + await vfs.mkdir("/project/docs", { recursive: true }); + await vfs.writeFile("/project/src/main.js", "console.log('main')\n"); + await vfs.writeFile("/project/src/helper.ts", "export {}\n"); + await vfs.writeFile("/project/docs/readme.md", "# Readme\n"); + + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec("find /project -name '*.js'"); + expect(parseLines(result.stdout)).toEqual(["/project/src/main.js"]); + }); + + it("find filters directories", async () => { + const vfs = createInMemoryFileSystem(); + await vfs.mkdir("/project/src", { recursive: true }); + await vfs.mkdir("/project/docs", { recursive: true }); + await vfs.writeFile("/project/src/main.js", "console.log('main')\n"); + + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec("find /project -type d"); + expect(parseLines(result.stdout)).toEqual([ + "/project", + "/project/docs", + "/project/src", + ]); + }); + + it("find supports depth limits", async () => { + const vfs = createInMemoryFileSystem(); + await vfs.mkdir("/project/src/nested", { recursive: true }); + await vfs.mkdir("/project/docs", { recursive: true }); + await vfs.writeFile("/project/src/main.js", "console.log('main')\n"); + await vfs.writeFile("/project/src/nested/helper.ts", "export {}\n"); + await vfs.writeFile("/project/docs/readme.md", "# Readme\n"); + + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec( + "find /project -mindepth 2 -maxdepth 2 -type f -name '*.js'", + ); + expect(parseLines(result.stdout)).toEqual(["/project/src/main.js"]); + }); + + it("xargs passes stdin arguments to a command", async () => { + const vfs = createInMemoryFileSystem(); + await vfs.writeFile("/args.txt", "alpha\nbeta\n"); + + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec("xargs echo < /args.txt"); + expect(result.stdout.trim()).toBe("alpha beta"); + }); + + it("xargs batches arguments across spawned commands", async () => { + const vfs = createInMemoryFileSystem(); + await vfs.writeFile("/args.txt", "one two three four five\n"); + + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + + const result = await kernel.exec("xargs -n 2 echo < /args.txt"); + expect(result.stdout.trim().split("\n")).toEqual([ + "one two", + "three four", + "five", + ]); + }); +}); diff --git a/software/findutils/tsconfig.json b/software/findutils/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/findutils/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/gawk/agentos-package.json b/software/gawk/agentos-package.json new file mode 100644 index 0000000000..5b103e0e2b --- /dev/null +++ b/software/gawk/agentos-package.json @@ -0,0 +1,12 @@ +{ + "commands": [ + "awk" + ], + "registry": { + "title": "gawk", + "description": "GNU awk text processing and data extraction.", + "priority": 20, + "image": "/images/registry/gawk.svg", + "category": "core" + } +} diff --git a/software/gawk/native/crates/awk/Cargo.toml b/software/gawk/native/crates/awk/Cargo.toml new file mode 100644 index 0000000000..c630320b93 --- /dev/null +++ b/software/gawk/native/crates/awk/Cargo.toml @@ -0,0 +1,10 @@ +[package] +workspace = "../../../../../toolchain" +name = "secureexec-awk" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "awk implementation for secure-exec standalone binaries" + +[dependencies] +awk-rs = "0.1.0" diff --git a/registry/native/crates/libs/awk/src/lib.rs b/software/gawk/native/crates/awk/src/lib.rs similarity index 100% rename from registry/native/crates/libs/awk/src/lib.rs rename to software/gawk/native/crates/awk/src/lib.rs diff --git a/software/gawk/native/crates/cmd-awk/Cargo.toml b/software/gawk/native/crates/cmd-awk/Cargo.toml new file mode 100644 index 0000000000..af4cd03274 --- /dev/null +++ b/software/gawk/native/crates/cmd-awk/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-awk" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "awk standalone binary for secure-exec VM" + +[[bin]] +name = "awk" +path = "src/main.rs" + +[dependencies] +secureexec-awk = { path = "../awk" } diff --git a/registry/native/crates/commands/awk/src/main.rs b/software/gawk/native/crates/cmd-awk/src/main.rs similarity index 100% rename from registry/native/crates/commands/awk/src/main.rs rename to software/gawk/native/crates/cmd-awk/src/main.rs diff --git a/software/gawk/package.json b/software/gawk/package.json new file mode 100644 index 0000000000..e2d3831013 --- /dev/null +++ b/software/gawk/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/gawk", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "GNU awk text processing for secure-exec VMs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/file/src/index.ts b/software/gawk/src/index.ts similarity index 100% rename from registry/software/file/src/index.ts rename to software/gawk/src/index.ts diff --git a/software/gawk/test/gawk.test.ts b/software/gawk/test/gawk.test.ts new file mode 100644 index 0000000000..c900d622d1 --- /dev/null +++ b/software/gawk/test/gawk.test.ts @@ -0,0 +1,130 @@ +import { existsSync } from "node:fs"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + NodeFileSystem, + createKernel, + createWasmVmRuntime, + describeIf, +} from "@agentos/test-harness"; +import type { Kernel } from "@agentos/test-harness"; +import { afterEach, expect, it } from "vitest"; + +const AWK_COMMAND_DIR = fileURLToPath(new URL("../bin", import.meta.url)); +const hasAwkPackageBinary = existsSync(join(AWK_COMMAND_DIR, "awk")); + +let tempRoot: string | undefined; + +async function writeFixture(path: string, contents: string): Promise { + if (!tempRoot) throw new Error("fixture root not initialized"); + const hostPath = join(tempRoot, path.replace(/^\/+/, "")); + await mkdir(dirname(hostPath), { recursive: true }); + await writeFile(hostPath, contents); +} + +async function createTestVFS(): Promise { + tempRoot = await mkdtemp(join(tmpdir(), "agentos-gawk-")); + await writeFixture( + "/project/scores.txt", + [ + "Ada runtime 7", + "Grace docs 5", + "Linus runtime 3", + "Barbara infra 9", + ].join("\n") + "\n", + ); + await writeFixture( + "/project/colon.txt", + ["alpha:build:4", "beta:test:6", "gamma:deploy:2"].join("\n") + "\n", + ); + await writeFixture( + "/project/runtime.awk", + '/runtime/ { print $1 ":" $3 }\nEND { print "done" }\n', + ); + return new NodeFileSystem({ root: tempRoot }); +} + +function lines(stdout: string): string[] { + return stdout.split("\n").filter((line) => line.length > 0); +} + +describeIf(hasAwkPackageBinary, "awk command", { timeout: 10_000 }, () => { + let kernel: Kernel | undefined; + + afterEach(async () => { + await kernel?.dispose(); + kernel = undefined; + if (tempRoot) { + await rm(tempRoot, { recursive: true, force: true }); + tempRoot = undefined; + } + }); + + async function mountFixture(): Promise { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [AWK_COMMAND_DIR] })); + } + + async function runAwk(args: string[]) { + if (!kernel) throw new Error("kernel not mounted"); + let stdout = ""; + let stderr = ""; + const proc = kernel.spawn("awk", args, { + onStdout: (chunk) => { + stdout += Buffer.from(chunk).toString("utf8"); + }, + onStderr: (chunk) => { + stderr += Buffer.from(chunk).toString("utf8"); + }, + }); + const exitCode = await proc.wait(); + await new Promise((resolve) => setTimeout(resolve, 0)); + return { stdout, stderr, exitCode }; + } + + it("extracts fields from a file", async () => { + await mountFixture(); + + const result = await runAwk(["{print $1}", "/project/scores.txt"]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(lines(result.stdout)).toEqual(["Ada", "Grace", "Linus", "Barbara"]); + }); + + it("uses explicit field separators", async () => { + await mountFixture(); + + const result = await runAwk(["-F:", "{print $2}", "/project/colon.txt"]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(lines(result.stdout)).toEqual(["build", "test", "deploy"]); + }); + + it("aggregates numeric columns", async () => { + await mountFixture(); + + const result = await runAwk([ + "{sum += $3} END {print sum}", + "/project/scores.txt", + ]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stdout.trim()).toBe("24"); + }); + + it("runs awk programs from files", async () => { + await mountFixture(); + + const result = await runAwk(["-f", "/project/runtime.awk", "/project/scores.txt"]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(lines(result.stdout)).toEqual(["Ada:7", "Linus:3", "done"]); + }); + + it("fails when an input file is missing", async () => { + await mountFixture(); + + const result = await runAwk(["{print $1}", "/project/missing.txt"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("/project/missing.txt"); + }); +}); diff --git a/software/gawk/test/manifest.test.ts b/software/gawk/test/manifest.test.ts new file mode 100644 index 0000000000..89139d33b8 --- /dev/null +++ b/software/gawk/test/manifest.test.ts @@ -0,0 +1,19 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const packageDir = new URL("..", import.meta.url).pathname; + +describe("package manifest", () => { + it("declares command binaries", () => { + const manifest = JSON.parse( + readFileSync(join(packageDir, "agentos-package.json"), "utf8"), + ); + + expect(manifest.commands?.length ?? 0).toBeGreaterThan(0); + for (const command of manifest.commands) { + expect(typeof command).toBe("string"); + expect(command.length).toBeGreaterThan(0); + } + }); +}); diff --git a/software/gawk/tsconfig.json b/software/gawk/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/gawk/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/git/agentos-package.json b/software/git/agentos-package.json new file mode 100644 index 0000000000..b1c4f9c536 --- /dev/null +++ b/software/git/agentos-package.json @@ -0,0 +1,19 @@ +{ + "commands": [ + "git", + "git-remote-http" + ], + "aliases": { + "git-remote-https": "git-remote-http", + "git-upload-pack": "git", + "git-receive-pack": "git", + "git-upload-archive": "git" + }, + "registry": { + "title": "git", + "description": "Upstream Git version control for AgentOS VMs.", + "priority": 90, + "image": "/images/registry/git.svg", + "category": "developer-tools" + } +} diff --git a/software/git/package.json b/software/git/package.json new file mode 100644 index 0000000000..c37a93b30f --- /dev/null +++ b/software/git/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/git", + "version": "0.3.3", + "type": "module", + "license": "GPL-2.0-only", + "description": "Upstream Git version control for AgentOS VMs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/findutils/src/index.ts b/software/git/src/index.ts similarity index 100% rename from registry/software/findutils/src/index.ts rename to software/git/src/index.ts diff --git a/software/git/test/git.test.ts b/software/git/test/git.test.ts new file mode 100644 index 0000000000..caef866685 --- /dev/null +++ b/software/git/test/git.test.ts @@ -0,0 +1,965 @@ +/** + * Integration tests for git command. + * + * Verifies init, add, commit, branch, checkout (with DWIM), plus local and + * smart-HTTP remote clone via kernel.exec() with real WASM binaries. + */ + +import { describe, it, expect, afterEach, beforeAll, afterAll, vi } from 'vitest'; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { resolve, join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { createServer as createHttpsServer, type Server as HttpsServer } from 'node:https'; +import { spawn, spawnSync, execSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { createWasmVmRuntime } from '@agentos/test-harness'; +import { + allowAll, + COMMANDS_DIR, + createInMemoryFileSystem, + createKernel, + describeIf, + hasWasmBinaries, +} from '@agentos/test-harness'; +import type { Kernel } from '@agentos/test-harness'; + +vi.setConfig({ testTimeout: 30_000 }); + +/** Check git binary exists in addition to base WASM binaries */ +const hasGit = hasWasmBinaries && existsSync(resolve(COMMANDS_DIR, 'git')); +const hasHostGit = spawnSync('git', ['--version'], { stdio: 'ignore' }).status === 0; +// Smart HTTP needs Git's libcurl-backed remote helper. It is now a real second +// WASM binary (git-remote-http links the overlaid mbedTLS libcurl in-process); +// git-remote-https aliases to it. +const hasGitHttpHelper = + hasGit && existsSync(resolve(COMMANDS_DIR, 'git-remote-http')); +// The real OpenSSH client (software/ssh) lights up git-over-ssh; its presence +// changes how ssh:// remotes fail when unreachable. +const hasSshClient = existsSync(resolve(COMMANDS_DIR, 'ssh')); + +const gitConfig = [ + '-c safe.directory=*', + '-c init.defaultBranch=main', + '-c user.name=agentos', + '-c user.email=agentos@example.invalid', +].join(' '); + +function git(args: string) { + return `git ${gitConfig} ${args}`; +} + +/** Create a kernel with a world-writable in-memory filesystem */ +async function createGitKernel() { + const vfs = createInMemoryFileSystem(); + // Make root and /tmp writable by all users (WASM processes run as non-root) + await (vfs as any).chmod('/', 0o1777); + await vfs.mkdir('/tmp', { recursive: true }); + await (vfs as any).chmod('/tmp', 0o1777); + const kernel = createKernel({ filesystem: vfs, syncFilesystemOnDispose: false }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + return { kernel, vfs, dispose: () => kernel.dispose() }; +} + +async function createGitKernelWithNet(loopbackExemptPorts: number[], seededCaPem?: string) { + const vfs = createInMemoryFileSystem(); + await (vfs as any).chmod('/', 0o1777); + await vfs.mkdir('/tmp', { recursive: true }); + await (vfs as any).chmod('/tmp', 0o1777); + const kernel = createKernel({ + filesystem: vfs, + permissions: allowAll, + loopbackExemptPorts, + syncFilesystemOnDispose: false, + }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + // Seed the Debian-shaped trust store the way the native VM bootstrap does, so + // libcurl's compile-time default CA bundle (/etc/ssl/certs/ca-certificates.crt) + // resolves in-guest for git-remote-http's mbedTLS backend. + if (seededCaPem) { + await vfs.mkdir('/etc/ssl/certs', { recursive: true }); + await kernel.writeFile('/etc/ssl/certs/ca-certificates.crt', seededCaPem); + } + return { kernel, vfs, dispose: () => kernel.dispose() }; +} + +// Build a real CA and a leaf server certificate signed by it, with a SAN that +// covers the 127.0.0.1 loopback endpoint the VM connects to. This lets +// git-remote-http's mbedTLS backend perform genuine chain + hostname +// verification, exactly like Linux git against a private CA. +function makeCaSignedCert(caCommonName: string): { + caPem: string; + serverKey: string; + serverCert: string; +} { + const dir = mkdtempSync(join(tmpdir(), 'git-ca-')); + try { + execSync(`openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "${dir}/ca.key" 2>/dev/null`); + execSync( + `openssl req -x509 -new -key "${dir}/ca.key" -days 3650 -subj "/CN=${caCommonName}" -out "${dir}/ca.crt" 2>/dev/null`, + ); + execSync(`openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "${dir}/srv.key" 2>/dev/null`); + execSync(`openssl req -new -key "${dir}/srv.key" -subj "/CN=localhost" -out "${dir}/srv.csr" 2>/dev/null`); + writeFileSync(`${dir}/ext.cnf`, 'subjectAltName=DNS:localhost,IP:127.0.0.1\n'); + execSync( + `openssl x509 -req -in "${dir}/srv.csr" -CA "${dir}/ca.crt" -CAkey "${dir}/ca.key" ` + + `-CAcreateserial -days 3650 -extfile "${dir}/ext.cnf" -out "${dir}/srv.crt" 2>/dev/null`, + ); + return { + caPem: readFileSync(`${dir}/ca.crt`, 'utf8'), + serverKey: readFileSync(`${dir}/srv.key`, 'utf8'), + serverCert: readFileSync(`${dir}/srv.crt`, 'utf8'), + }; + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +function runHostGit(args: string[], cwd?: string) { + const result = spawnSync('git', args, { + cwd, + encoding: 'utf8', + }); + if (result.status !== 0) { + throw new Error( + `host git failed: git ${args.join(' ')}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, + ); + } +} + +async function runHostGitResult(args: string[], cwd?: string) { + return await new Promise<{ status: number | null; stdout: string; stderr: string }>((resolveResult, reject) => { + const child = spawn('git', args, { cwd }); + const stdout: Buffer[] = []; + const stderr: Buffer[] = []; + child.stdout.on('data', (chunk) => stdout.push(Buffer.from(chunk))); + child.stderr.on('data', (chunk) => stderr.push(Buffer.from(chunk))); + child.on('error', reject); + child.on('close', (status) => { + resolveResult({ + status, + stdout: Buffer.concat(stdout).toString('utf8'), + stderr: Buffer.concat(stderr).toString('utf8'), + }); + }); + }); +} + +/** Helper: run command and assert success */ +async function run(kernel: Kernel, cmd: string): Promise<{ stdout: string; stderr: string; exitCode: number }> { + const r = await kernel.exec(cmd); + if (r.exitCode !== 0) { + throw new Error(`Command failed (exit ${r.exitCode}): ${cmd}\nstdout: ${r.stdout}\nstderr: ${r.stderr}`); + } + return r; +} + +async function expectGitRef(kernel: Kernel, repo: string, ref: string) { + const result = await run(kernel, git(`-C ${repo} rev-parse --verify ${ref}`)); + expect(result.stdout.trim()).toMatch(/^[0-9a-f]{40,64}$/); +} + +function sidebandPacket(band: 1 | 2 | 3, payload: Uint8Array): Uint8Array { + const packetLength = payload.length + 5; + const header = new TextEncoder().encode(packetLength.toString(16).padStart(4, '0')); + const packet = new Uint8Array(packetLength); + packet.set(header, 0); + packet[4] = band; + packet.set(payload, 5); + return packet; +} + +function concatBytes(chunks: Uint8Array[]): Uint8Array { + const output = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0)); + let offset = 0; + for (const chunk of chunks) { + output.set(chunk, offset); + offset += chunk.length; + } + return output; +} + +// TODO(P6): requires git WASM artifact, intentionally excluded from the fast software-build gate. +describeIf(hasGit, 'git command', () => { + let kernel: Kernel; + let vfs: any; + let dispose: () => Promise; + + afterEach(async () => { + await dispose?.(); + }); + + it('init creates .git directory structure', async () => { + ({ kernel, vfs, dispose } = await createGitKernel()); + + const result = await run(kernel, git('init /repo')); + expect(result.stdout).toContain('Initialized empty Git repository'); + + expect(await vfs.exists('/repo/.git/HEAD')).toBe(true); + expect(await vfs.exists('/repo/.git/objects')).toBe(true); + expect(await vfs.exists('/repo/.git/refs/heads')).toBe(true); + + const head = new TextDecoder().decode(await vfs.readFile('/repo/.git/HEAD')); + expect(head.trim()).toBe('ref: refs/heads/main'); + }); + + it('add + commit creates objects and updates ref', async () => { + ({ kernel, vfs, dispose } = await createGitKernel()); + + await run(kernel, git('init /repo')); + await kernel.writeFile('/repo/hello.txt', 'hello world\n'); + await run(kernel, git('-C /repo add hello.txt')); + await run(kernel, git("-C /repo commit -m 'first commit'")); + + expect(await vfs.exists('/repo/.git/refs/heads/main')).toBe(true); + }); + + it('hidden sideband helper repeatedly streams band 1 and forwards progress without spooling', async () => { + ({ kernel, dispose } = await createGitKernel()); + + const encoder = new TextEncoder(); + const payloadChunks = Array.from({ length: 128 }, (_, index) => + encoder.encode(`pack-${index.toString().padStart(3, '0')}:${'x'.repeat(4096)}\n`)); + const stream: Uint8Array[] = []; + for (const [index, payload] of payloadChunks.entries()) { + stream.push(sidebandPacket(1, payload)); + if (index % 16 === 0) + stream.push(sidebandPacket(2, encoder.encode(`progress ${index}\n`))); + } + stream.push(encoder.encode('0000')); + + const result = await kernel.exec('git sideband--helper demux parity-test 1', { + stdin: concatBytes(stream), + }); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout).toBe(new TextDecoder().decode(concatBytes(payloadChunks))); + expect(result.stderr).toContain('progress 0'); + expect(result.stderr).toContain('progress 112'); + + const coloredProgress = concatBytes([ + sidebandPacket(2, encoder.encode('\x1b[31mred\x1b[0m\n')), + encoder.encode('0000'), + ]); + const sanitized = await kernel.exec('git sideband--helper demux parity-test 0', { + stdin: coloredProgress, + }); + expect(sanitized.exitCode, sanitized.stderr).toBe(0); + expect(sanitized.stderr).toContain('^[[31mred^[[0m'); + + const ansiAllowed = await kernel.exec('git sideband--helper demux parity-test 1', { + stdin: coloredProgress, + }); + expect(ansiAllowed.exitCode, ansiAllowed.stderr).toBe(0); + expect(ansiAllowed.stderr).toContain('\x1b[31mred\x1b[0m'); + + const invalidPolicy = await kernel.exec('git sideband--helper demux parity-test 16', { + stdin: coloredProgress, + }); + expect(invalidPolicy.exitCode).not.toBe(0); + expect(invalidPolicy.stderr).toContain('invalid sideband control-character policy'); + + const muxed = await kernel.exec('git sideband--helper mux 65520 0 5', { + stdin: 'receive progress\n', + }); + expect(muxed.exitCode, muxed.stderr).toBe(0); + expect(muxed.stdout).toMatch(/^[0-9a-f]{4}\u0002receive progress\n$/); + + const help = await kernel.exec('git help -a'); + expect(help.stdout).not.toContain('sideband--helper'); + + const patchText = readFileSync( + resolve(import.meta.dirname, '../../../toolchain/c/patches/git/0002-wasi-synchronous-sideband-demux.patch'), + 'utf8', + ); + const addedImplementation = patchText + .split('\n') + .filter((line) => line.startsWith('+') && !line.startsWith('+++')) + .join('\n'); + expect(addedImplementation).not.toContain('tmp_sideband_'); + expect(addedImplementation).not.toMatch(/odb_mkstemp|spool_fd|spool_tempfile/); + }); + + it('branch lists branches with current marked', async () => { + ({ kernel, vfs, dispose } = await createGitKernel()); + + await run(kernel, git('init /repo')); + await kernel.writeFile('/repo/file.txt', 'content\n'); + await run(kernel, git('-C /repo add file.txt')); + await run(kernel, git("-C /repo commit -m 'init'")); + + const result = await run(kernel, git('-C /repo branch')); + expect(result.stdout.trim()).toBe('* main'); + }); + + it('checkout -b creates a new branch', async () => { + ({ kernel, vfs, dispose } = await createGitKernel()); + + await run(kernel, git('init /repo')); + await kernel.writeFile('/repo/file.txt', 'content\n'); + await run(kernel, git('-C /repo add file.txt')); + await run(kernel, git("-C /repo commit -m 'init'")); + + await run(kernel, git('-C /repo checkout -b feature')); + + const result = await run(kernel, git('-C /repo branch')); + const lines = result.stdout.trim().split('\n').map((l: string) => l.trim()); + expect(lines).toContain('* feature'); + expect(lines).toContain('main'); + }); + + it('full quickstart scenario: init, commit, branch, clone, checkout', async () => { + ({ kernel, vfs, dispose } = await createGitKernel()); + + // Create origin repo + await run(kernel, git('init /tmp/origin')); + await kernel.writeFile('/tmp/origin/README.md', '# demo repo\n'); + await run(kernel, git('-C /tmp/origin add README.md')); + await run(kernel, git("-C /tmp/origin commit -m 'initial commit'")); + + // Check default branch + let r = await run(kernel, git('-C /tmp/origin branch')); + expect(r.stdout.trim()).toBe('* main'); + + // Create feature branch with a new file + await run(kernel, git('-C /tmp/origin checkout -b feature')); + await kernel.writeFile('/tmp/origin/feature.txt', 'checked out from feature\n'); + await run(kernel, git('-C /tmp/origin add feature.txt')); + await run(kernel, git("-C /tmp/origin commit -m 'add feature file'")); + + // Switch back to main + await run(kernel, git('-C /tmp/origin checkout main')); + + // Clone + await run(kernel, git('clone /tmp/origin /tmp/clone')); + + // Clone should only show main branch initially + r = await run(kernel, git('-C /tmp/clone branch')); + expect(r.stdout.trim()).toBe('* main'); + + // Checkout feature (DWIM from remote tracking) + await run(kernel, git('-C /tmp/clone checkout feature')); + + // Now both branches should be listed + r = await run(kernel, git('-C /tmp/clone branch')); + const branches = r.stdout.trim().split('\n').map((l: string) => l.trim()); + expect(branches).toContain('* feature'); + expect(branches).toContain('main'); + + // Verify feature file exists in clone + const featureContent = new TextDecoder().decode(await vfs.readFile('/tmp/clone/feature.txt')); + expect(featureContent).toBe('checked out from feature\n'); + + // Verify README exists too + const readmeContent = new TextDecoder().decode(await vfs.readFile('/tmp/clone/README.md')); + expect(readmeContent).toBe('# demo repo\n'); + }); + + it('clone without an explicit destination uses the source basename', async () => { + ({ kernel, vfs, dispose } = await createGitKernel()); + + await run(kernel, git('init /tmp/origin')); + await kernel.writeFile('/tmp/origin/README.md', 'default destination\n'); + await run(kernel, git('-C /tmp/origin add README.md')); + await run(kernel, git("-C /tmp/origin commit -m 'seed'")); + + await run(kernel, 'mkdir -p /work'); + await run(kernel, git('-C /work clone /tmp/origin')); + + expect(await vfs.exists('/work/origin/.git/HEAD')).toBe(true); + const readmeContent = new TextDecoder().decode(await vfs.readFile('/work/origin/README.md')); + expect(readmeContent).toBe('default destination\n'); + }); + + it('clone without an explicit destination strips a trailing .git suffix', async () => { + ({ kernel, vfs, dispose } = await createGitKernel()); + + await run(kernel, git('init /tmp/origin.git')); + await kernel.writeFile('/tmp/origin.git/README.md', 'suffix destination\n'); + await run(kernel, git('-C /tmp/origin.git add README.md')); + await run(kernel, git("-C /tmp/origin.git commit -m 'seed'")); + + await run(kernel, 'mkdir -p /work'); + await run(kernel, git('-C /work clone /tmp/origin.git')); + + expect(await vfs.exists('/work/origin/.git/HEAD')).toBe(true); + const readmeContent = new TextDecoder().decode(await vfs.readFile('/work/origin/README.md')); + expect(readmeContent).toBe('suffix destination\n'); + }); + + it('clone into an existing empty destination directory succeeds', async () => { + ({ kernel, vfs, dispose } = await createGitKernel()); + + await run(kernel, git('init /tmp/origin')); + await kernel.writeFile('/tmp/origin/README.md', 'empty destination\n'); + await run(kernel, git('-C /tmp/origin add README.md')); + await run(kernel, git("-C /tmp/origin commit -m 'seed'")); + + await run(kernel, 'mkdir -p /tmp/clone'); + await run(kernel, git('clone /tmp/origin /tmp/clone')); + + expect(await vfs.exists('/tmp/clone/.git/HEAD')).toBe(true); + const readmeContent = new TextDecoder().decode(await vfs.readFile('/tmp/clone/README.md')); + expect(readmeContent).toBe('empty destination\n'); + }); + + it('clone rejects a non-empty destination directory', async () => { + ({ kernel, vfs, dispose } = await createGitKernel()); + + await run(kernel, git('init /tmp/origin')); + await kernel.writeFile('/tmp/origin/README.md', 'origin\n'); + await run(kernel, git('-C /tmp/origin add README.md')); + await run(kernel, git("-C /tmp/origin commit -m 'seed'")); + + await run(kernel, 'mkdir -p /tmp/clone'); + await kernel.writeFile('/tmp/clone/existing.txt', 'keep me\n'); + + const result = await kernel.exec(git('clone /tmp/origin /tmp/clone')); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toMatch(/already exists|not an empty directory|destination/i); + + const existing = new TextDecoder().decode(await vfs.readFile('/tmp/clone/existing.txt')); + expect(existing).toBe('keep me\n'); + expect(await vfs.exists('/tmp/clone/.git')).toBe(false); + }); + + it('clone of a missing repository fails without leaving a partial destination', async () => { + ({ kernel, vfs, dispose } = await createGitKernel()); + + const result = await kernel.exec(git('clone /tmp/missing /tmp/clone')); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toMatch(/not a git repository|missing|no such file|fatal/i); + expect(await vfs.exists('/tmp/clone')).toBe(false); + }); + + it('clone of an empty repository succeeds and leaves an empty worktree', async () => { + ({ kernel, vfs, dispose } = await createGitKernel()); + + await run(kernel, git('init /tmp/origin')); + await run(kernel, git('clone /tmp/origin /tmp/clone')); + + const head = new TextDecoder().decode(await vfs.readFile('/tmp/clone/.git/HEAD')); + expect(head.trim()).toBe('ref: refs/heads/main'); + expect(await vfs.exists('/tmp/clone/.git/config')).toBe(true); + expect(await vfs.exists('/tmp/clone/.git/refs/heads/main')).toBe(false); + expect(await vfs.exists('/tmp/clone/README.md')).toBe(false); + }); + + it('clone preserves nested directory trees', async () => { + ({ kernel, vfs, dispose } = await createGitKernel()); + + await run(kernel, git('init /tmp/origin')); + await run(kernel, 'mkdir -p /tmp/origin/src/nested'); + await kernel.writeFile('/tmp/origin/src/nested/file.txt', 'nested payload\n'); + await kernel.writeFile('/tmp/origin/src/root.txt', 'root payload\n'); + await run(kernel, git('-C /tmp/origin add src/nested/file.txt src/root.txt')); + await run(kernel, git("-C /tmp/origin commit -m 'nested tree'")); + + await run(kernel, git('clone /tmp/origin /tmp/clone')); + + const nested = new TextDecoder().decode(await vfs.readFile('/tmp/clone/src/nested/file.txt')); + const root = new TextDecoder().decode(await vfs.readFile('/tmp/clone/src/root.txt')); + expect(nested).toBe('nested payload\n'); + expect(root).toBe('root payload\n'); + }); + + it('clone honors the source default branch when HEAD is not main', async () => { + ({ kernel, vfs, dispose } = await createGitKernel()); + + await run(kernel, git('init /tmp/origin')); + await kernel.writeFile('/tmp/origin/README.md', 'main branch\n'); + await run(kernel, git('-C /tmp/origin add README.md')); + await run(kernel, git("-C /tmp/origin commit -m 'main'")); + + await run(kernel, git('-C /tmp/origin checkout -b trunk')); + await kernel.writeFile('/tmp/origin/trunk.txt', 'trunk branch\n'); + await run(kernel, git('-C /tmp/origin add trunk.txt')); + await run(kernel, git("-C /tmp/origin commit -m 'trunk'")); + + await run(kernel, git('clone /tmp/origin /tmp/clone')); + + const head = new TextDecoder().decode(await vfs.readFile('/tmp/clone/.git/HEAD')); + expect(head.trim()).toBe('ref: refs/heads/trunk'); + expect(await vfs.exists('/tmp/clone/.git/refs/heads/trunk')).toBe(true); + const trunk = new TextDecoder().decode(await vfs.readFile('/tmp/clone/trunk.txt')); + expect(trunk).toBe('trunk branch\n'); + }); + + it('clone copies nested branch refs and checkout DWIM works for branch names with slashes', async () => { + ({ kernel, vfs, dispose } = await createGitKernel()); + + await run(kernel, git('init /tmp/origin')); + await kernel.writeFile('/tmp/origin/README.md', '# demo repo\n'); + await run(kernel, git('-C /tmp/origin add README.md')); + await run(kernel, git("-C /tmp/origin commit -m 'initial commit'")); + + await run(kernel, git('-C /tmp/origin checkout -b feature/deep')); + await kernel.writeFile('/tmp/origin/feature.txt', 'nested branch payload\n'); + await run(kernel, git('-C /tmp/origin add feature.txt')); + await run(kernel, git("-C /tmp/origin commit -m 'nested branch'")); + await run(kernel, git('-C /tmp/origin checkout main')); + + await run(kernel, git('clone /tmp/origin /tmp/clone')); + + await expectGitRef(kernel, '/tmp/clone', 'refs/remotes/origin/feature/deep'); + + await run(kernel, git('-C /tmp/clone checkout feature/deep')); + const featureContent = new TextDecoder().decode(await vfs.readFile('/tmp/clone/feature.txt')); + expect(featureContent).toBe('nested branch payload\n'); + const head = new TextDecoder().decode(await vfs.readFile('/tmp/clone/.git/HEAD')); + expect(head.trim()).toBe('ref: refs/heads/feature/deep'); + }); + + it('clone works with relative source and destination paths', async () => { + ({ kernel, vfs, dispose } = await createGitKernel()); + + await run(kernel, 'mkdir -p /tmp/work'); + await run(kernel, git('init /tmp/work/origin')); + await kernel.writeFile('/tmp/work/origin/README.md', 'relative clone\n'); + await run(kernel, git('-C /tmp/work/origin add README.md')); + await run(kernel, git("-C /tmp/work/origin commit -m 'seed'")); + + await run(kernel, git('-C /tmp/work clone ./origin ./clone')); + + expect(await vfs.exists('/tmp/work/clone/.git/HEAD')).toBe(true); + const readmeContent = new TextDecoder().decode(await vfs.readFile('/tmp/work/clone/README.md')); + expect(readmeContent).toBe('relative clone\n'); + }); + + it('push fails with a real Git remote/ref error', async () => { + ({ kernel, dispose } = await createGitKernel()); + + await run(kernel, git('init /tmp/repo')); + + const result = await kernel.exec(git('-C /tmp/repo push origin main')); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toMatch(/fatal|error|origin|refspec|repository/i); + expect(result.stderr).not.toContain('GitSubcommandUnsupported'); + }); + + it('local push streams through the shipped receive-pack sideband mux', async () => { + ({ kernel, dispose } = await createGitKernel()); + + await run(kernel, git('init --bare /tmp/origin.git')); + await run(kernel, git('init /tmp/work')); + await kernel.writeFile('/tmp/work/pushed.txt', 'receive-pack streaming\n'); + await kernel.writeFile('/tmp/git-system-config', '[safe]\n\tdirectory = *\n'); + await run(kernel, git('-C /tmp/work add pushed.txt')); + await run(kernel, git("-C /tmp/work commit -m 'stream to receive-pack'")); + + const pushed = await kernel.exec( + git('-C /tmp/work push /tmp/origin.git main:refs/heads/main'), + { env: { GIT_CONFIG_SYSTEM: '/tmp/git-system-config', GIT_TRACE: '1' } }, + ); + expect(pushed.exitCode, pushed.stderr).toBe(0); + expect(pushed.stderr).toContain('sideband--helper'); + + const ref = await run(kernel, git('-C /tmp/origin.git rev-parse --verify refs/heads/main')); + expect(ref.stdout.trim()).toMatch(/^[0-9a-f]{40,64}$/); + }); + + it('clone over ssh:// reaches the real ssh client and surfaces its transport error', async () => { + ({ kernel, dispose } = await createGitKernel()); + + // Port 1 on loopback is never exempted for this kernel, so the real + // OpenSSH client (now on PATH — git connect.c execs `ssh`) fails with a + // genuine connection error instead of a spawn failure. Full git-over-ssh + // success coverage lives in software/ssh/test/ssh.test.ts. + const result = await kernel.exec(git('clone ssh://git@127.0.0.1:1/repo.git /tmp/clone')); + expect(result.exitCode).not.toBe(0); + if (hasSshClient) { + // The error must come from ssh's transport, proving git spawned it. + expect(result.stderr).toMatch(/ssh:|Connection closed|Could not read from remote repository/i); + expect(result.stderr).not.toMatch(/cannot run ssh|unable to fork/i); + } else { + expect(result.stderr).toMatch(/cannot run ssh|unable to fork|ssh|fatal/i); + } + expect(result.stderr).not.toContain('GitSubcommandUnsupported'); + }); + + // Real smart-HTTP over TLS: git-remote-http (libcurl + in-guest mbedTLS) + // clones/fetches/pushes against `git http-backend` behind a Node HTTPS + // endpoint. Certificate trust comes from a private CA seeded into the guest's + // /etc/ssl/certs bundle (the "trusted" server) — exactly the Debian trust + // path — while a second server signed by a CA absent from the bundle exercises + // verify-fail, http.sslVerify=false, GIT_SSL_NO_VERIFY, and http.sslCAInfo. + describeIf(hasHostGit && hasGitHttpHelper, 'smart-HTTP clone/fetch/push over TLS', () => { + let repoRoot: string; + let trustedServer: HttpsServer; + let untrustedServer: HttpsServer; + let trustedPort: number; + let untrustedPort: number; + let trustedCaPem = ''; + let untrustedCaPem = ''; + let sawChunkedReceivePack = false; + let receivePackBodyBytes = 0; + let receivePackBodyEnded = false; + let receivePackOffset = -1; + let receivePackObjectCount = -1; + let receivePackTrailerValid = false; + + // A CGI bridge to `git http-backend`. receive-pack is enabled on the origin + // (below) so pushes are accepted; GIT_HTTP_EXPORT_ALL allows anonymous read. + function makeBackendHandler() { + return (req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => { + const url = new URL(req.url ?? '/', 'https://127.0.0.1'); + const isReceivePack = + req.method === 'POST' && + url.pathname.endsWith('/git-receive-pack'); + if ( + isReceivePack && + String(req.headers['transfer-encoding'] ?? '').split(/\s*,\s*/).includes('chunked') + ) { + sawChunkedReceivePack = true; + } + const bodyChunks: Buffer[] = []; + req.on('data', (chunk) => { + bodyChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + }); + req.on('end', () => { + const requestBody = Buffer.concat(bodyChunks); + if (isReceivePack) { + receivePackBodyEnded = true; + receivePackBodyBytes = requestBody.length; + receivePackOffset = requestBody.indexOf(Buffer.from('PACK')); + if (receivePackOffset >= 0 && requestBody.length >= receivePackOffset + 32) { + receivePackObjectCount = requestBody.readUInt32BE(receivePackOffset + 8); + const pack = requestBody.subarray(receivePackOffset); + const expectedTrailer = pack.subarray(pack.length - 20); + const actualTrailer = createHash('sha1').update(pack.subarray(0, -20)).digest(); + receivePackTrailerValid = actualTrailer.equals(expectedTrailer); + } + } + const gitProtocol = req.headers['git-protocol']; + const env = { + ...process.env, + GIT_HTTP_EXPORT_ALL: '1', + GIT_PROJECT_ROOT: repoRoot, + PATH_INFO: url.pathname, + QUERY_STRING: url.search.startsWith('?') ? url.search.slice(1) : url.search, + REQUEST_METHOD: req.method ?? 'GET', + CONTENT_TYPE: String(req.headers['content-type'] ?? ''), + CONTENT_LENGTH: String(requestBody.length), + REMOTE_ADDR: '127.0.0.1', + GIT_PROTOCOL: typeof gitProtocol === 'string' ? gitProtocol : '', + HTTP_GIT_PROTOCOL: typeof gitProtocol === 'string' ? gitProtocol : '', + }; + + const child = spawn('git', ['http-backend'], { env }); + const stdout: Buffer[] = []; + const stderr: Buffer[] = []; + child.stdout.on('data', (chunk) => { + stdout.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + }); + child.stderr.on('data', (chunk) => { + stderr.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + }); + child.on('error', (error) => { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end(String(error)); + }); + child.on('close', (code) => { + const output = Buffer.concat(stdout); + const headerSep = output.indexOf(Buffer.from('\r\n\r\n')); + const altSep = output.indexOf(Buffer.from('\n\n')); + const sepIndex = headerSep >= 0 ? headerSep : altSep; + const sepLen = headerSep >= 0 ? 4 : altSep >= 0 ? 2 : 0; + if (code !== 0 && sepIndex === -1) { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end(Buffer.concat(stderr)); + return; + } + if (sepIndex === -1) { + res.writeHead(500, { 'Content-Type': 'text/plain' }); + res.end(output); + return; + } + const headerText = output.subarray(0, sepIndex).toString('utf8'); + const responseBody = output.subarray(sepIndex + sepLen); + let status = 200; + const headers: Record = {}; + for (const line of headerText.split(/\r?\n/)) { + if (!line) continue; + const colon = line.indexOf(':'); + if (colon === -1) continue; + const name = line.slice(0, colon); + const value = line.slice(colon + 1).trim(); + if (name.toLowerCase() === 'status') { + status = Number.parseInt(value, 10) || 200; + } else { + headers[name] = value; + } + } + res.writeHead(status, headers); + res.end(responseBody); + }); + child.stdin.end(requestBody); + }); + }; + } + + async function listen(server: HttpsServer): Promise { + await new Promise((r) => server.listen(0, '127.0.0.1', r)); + return (server.address() as import('node:net').AddressInfo).port; + } + + beforeAll(async () => { + repoRoot = mkdtempSync(join(tmpdir(), 'agentos-git-https-')); + const worktree = join(repoRoot, 'worktree'); + const origin = join(repoRoot, 'origin.git'); + + runHostGit(['-c', 'init.defaultBranch=main', 'init', worktree]); + writeFileSync(join(worktree, 'README.md'), 'remote smart clone\n'); + runHostGit(['-C', worktree, 'add', 'README.md']); + runHostGit([ + '-C', worktree, + '-c', 'user.name=secure-exec', '-c', 'user.email=agent@example.com', + 'commit', '-m', 'seed', + ]); + runHostGit(['-C', worktree, 'checkout', '-b', 'feature/deep']); + writeFileSync(join(worktree, 'feature.txt'), 'remote branch payload\n'); + runHostGit(['-C', worktree, 'add', 'feature.txt']); + runHostGit([ + '-C', worktree, + '-c', 'user.name=secure-exec', '-c', 'user.email=agent@example.com', + 'commit', '-m', 'feature branch', + ]); + runHostGit(['-C', worktree, 'checkout', 'main']); + runHostGit(['clone', '--bare', worktree, origin]); + runHostGit(['-C', origin, 'repack', '-a', '-d', '-f', '--depth=50', '--window=50']); + // Accept anonymous pushes over smart HTTP. + runHostGit(['-C', origin, 'config', 'http.receivepack', 'true']); + + const trusted = makeCaSignedCert('AgentOS Git Test Root CA'); + trustedCaPem = trusted.caPem; + trustedServer = createHttpsServer( + { key: trusted.serverKey, cert: trusted.serverCert }, + makeBackendHandler(), + ); + trustedPort = await listen(trustedServer); + + const untrusted = makeCaSignedCert('AgentOS Git Untrusted CA'); + untrustedCaPem = untrusted.caPem; + untrustedServer = createHttpsServer( + { key: untrusted.serverKey, cert: untrusted.serverCert }, + makeBackendHandler(), + ); + untrustedPort = await listen(untrustedServer); + }); + + afterAll(async () => { + if (trustedServer) await new Promise((r) => trustedServer.close(() => r())); + if (untrustedServer) await new Promise((r) => untrustedServer.close(() => r())); + rmSync(repoRoot, { recursive: true, force: true }); + }); + + const trustedUrl = () => `https://127.0.0.1:${trustedPort}/origin.git`; + const untrustedUrl = () => `https://127.0.0.1:${untrustedPort}/origin.git`; + + it('clone fetches refs and worktree contents over HTTPS with a trusted CA', async () => { + ({ kernel, vfs, dispose } = await createGitKernelWithNet([trustedPort], trustedCaPem)); + + const res = await kernel.exec(git(`clone ${trustedUrl()} /tmp/clone`), { + env: { GIT_CURL_VERBOSE: '1' }, + }); + expect(res.exitCode, res.stderr).toBe(0); + // Proof a real TLS handshake happened in-guest (mbedTLS via libcurl). + expect(res.stderr).toMatch(/SSL connection|TLS|SSL certificate|CAfile/i); + + const head = new TextDecoder().decode(await kernel.readFile('/tmp/clone/.git/HEAD')); + expect(head.trim()).toBe('ref: refs/heads/main'); + const readme = new TextDecoder().decode(await kernel.readFile('/tmp/clone/README.md')); + expect(readme).toBe('remote smart clone\n'); + await expectGitRef(kernel, '/tmp/clone', 'refs/remotes/origin/feature/deep'); + + await run(kernel, git('-C /tmp/clone checkout feature/deep')); + const feature = new TextDecoder().decode(await kernel.readFile('/tmp/clone/feature.txt')); + expect(feature).toBe('remote branch payload\n'); + }); + + it('fetch picks up a new remote branch over HTTPS', async () => { + ({ kernel, vfs, dispose } = await createGitKernelWithNet([trustedPort], trustedCaPem)); + await run(kernel, git(`clone ${trustedUrl()} /tmp/clone`)); + + // Add a new branch on the origin (host side), then fetch it in-guest. + const bareBranch = 'fetched-branch'; + runHostGit(['-C', join(repoRoot, 'origin.git'), 'branch', bareBranch, 'main']); + + await run(kernel, git('-C /tmp/clone fetch origin')); + await expectGitRef(kernel, '/tmp/clone', `refs/remotes/origin/${bareBranch}`); + }); + + it('repeated fetches keep streaming refs without sideband temp packs', async () => { + ({ kernel, vfs, dispose } = await createGitKernelWithNet([trustedPort], trustedCaPem)); + await run(kernel, git(`clone ${trustedUrl()} /tmp/clone`)); + + for (let index = 0; index < 3; index++) { + const branch = `stream-round-${index}`; + runHostGit(['-C', join(repoRoot, 'origin.git'), 'branch', branch, 'main']); + await run(kernel, git('-C /tmp/clone fetch origin')); + await expectGitRef(kernel, '/tmp/clone', `refs/remotes/origin/${branch}`); + } + + const fsck = await kernel.exec(git('-C /tmp/clone fsck --full')); + expect(fsck.exitCode, fsck.stderr).toBe(0); + }); + + it('push sends a small commit over HTTPS smart-HTTP', async () => { + ({ kernel, vfs, dispose } = await createGitKernelWithNet([trustedPort], trustedCaPem)); + await run(kernel, git(`clone ${trustedUrl()} /tmp/clone`)); + + await kernel.writeFile('/tmp/clone/pushed.txt', 'pushed over https\n'); + await run(kernel, git('-C /tmp/clone add pushed.txt')); + await run(kernel, git("-C /tmp/clone commit -m 'push small'")); + const pushed = await kernel.exec(git('-C /tmp/clone push origin HEAD:refs/heads/small-push')); + expect(pushed.exitCode, pushed.stderr).toBe(0); + + // Verify the ref really landed in the origin bare repo (host side). + const originRef = spawnSync( + 'git', + ['-C', join(repoRoot, 'origin.git'), 'rev-parse', '--verify', 'refs/heads/small-push'], + { encoding: 'utf8' }, + ); + expect(originRef.status).toBe(0); + expect(originRef.stdout.trim()).toMatch(/^[0-9a-f]{40,64}$/); + }); + + it('push streams an unknown-size pack over HTTPS with chunked POST', async () => { + ({ kernel, vfs, dispose } = await createGitKernelWithNet([trustedPort], trustedCaPem)); + await run(kernel, git(`clone ${trustedUrl()} /tmp/clone`)); + + // Match upstream Git's smart-HTTP chunking coverage: lower the supported + // postBuffer knob and exceed it with incompressible data. Anonymous Git + // deliberately suppresses Expect: 100-continue, so chunked transfer is + // the Linux behavior under test here. + const { randomBytes } = await import('node:crypto'); + const big = randomBytes(Number(process.env.AGENTOS_GIT_PUSH_BYTES ?? 128 * 1024)); + await kernel.writeFile('/tmp/clone/big.bin', big); + await run(kernel, git('-C /tmp/clone add big.bin')); + await run(kernel, git("-C /tmp/clone commit -m 'push large'")); + const guestObjectSize = await run( + kernel, + git('-C /tmp/clone cat-file -s HEAD:big.bin'), + ); + expect(Number(guestObjectSize.stdout.trim())).toBe(big.length); + sawChunkedReceivePack = false; + receivePackBodyBytes = 0; + receivePackBodyEnded = false; + receivePackOffset = -1; + receivePackObjectCount = -1; + receivePackTrailerValid = false; + const pushed = await kernel.exec( + git('-c http.postBuffer=65536 -C /tmp/clone push origin HEAD:refs/heads/large-push'), + { env: { GIT_TRACE: '1' } }, + ); + expect(pushed.exitCode, pushed.stderr).toBe(0); + expect(sawChunkedReceivePack).toBe(true); + expect(receivePackBodyEnded).toBe(true); + expect(receivePackBodyBytes).toBeGreaterThan(big.length); + expect(receivePackOffset).toBeGreaterThanOrEqual(0); + expect(receivePackObjectCount).toBeGreaterThan(0); + expect(receivePackTrailerValid).toBe(true); + const demuxStart = pushed.stderr.indexOf('sideband--helper demux send-pack'); + const packStart = pushed.stderr.indexOf('pack-objects'); + expect(demuxStart, pushed.stderr).toBeGreaterThanOrEqual(0); + expect(packStart, pushed.stderr).toBeGreaterThan(demuxStart); + + const originRef = spawnSync( + 'git', + ['-C', join(repoRoot, 'origin.git'), 'rev-parse', '--verify', 'refs/heads/large-push'], + { encoding: 'utf8' }, + ); + expect(originRef.status).toBe(0); + // Confirm the large object is actually present in the origin object store. + const cat = spawnSync( + 'git', + ['-C', join(repoRoot, 'origin.git'), 'cat-file', '-s', `${originRef.stdout.trim()}:big.bin`], + { encoding: 'utf8' }, + ); + expect(cat.status).toBe(0); + expect(Number(cat.stdout.trim())).toBe(big.length); + }, Number(process.env.AGENTOS_GIT_PUSH_TIMEOUT_MS ?? 60_000)); + + it('pack-objects failure reports the same smart-HTTP transport failure as native Git', async () => { + const trustedCaPath = join(repoRoot, 'trusted-ca.pem'); + writeFileSync(trustedCaPath, trustedCaPem); + const nativeFailure = await runHostGitResult([ + '-C', join(repoRoot, 'worktree'), + '-c', `http.sslCAInfo=${trustedCaPath}`, + '-c', 'pack.windowMemory=bogus', + 'push', trustedUrl(), + 'main:refs/heads/native-pack-failure', + ]); + expect(nativeFailure.status).not.toBe(0); + expect(nativeFailure.stderr).toMatch(/bad numeric config value/); + + ({ kernel, vfs, dispose } = await createGitKernelWithNet([trustedPort], trustedCaPem)); + await run(kernel, git(`clone ${trustedUrl()} /tmp/clone`)); + await kernel.writeFile('/tmp/clone/failing-pack.txt', 'must not reach origin\n'); + await run(kernel, git('-C /tmp/clone add failing-pack.txt')); + await run(kernel, git("-C /tmp/clone commit -m 'pack failure'")); + + const wasmFailure = await kernel.exec( + git('-C /tmp/clone -c pack.windowMemory=bogus push origin HEAD:refs/heads/wasm-pack-failure'), + ); + expect(wasmFailure.exitCode).not.toBe(0); + expect(wasmFailure.stderr).toMatch(/bad numeric config value/); + const transportFailure = /remote unpack failed: eof before pack header|remote end hung up unexpectedly/i; + expect(nativeFailure.stderr).toMatch(transportFailure); + expect(wasmFailure.stderr).toMatch(transportFailure); + expect(wasmFailure.stderr.match(transportFailure)?.[0].toLowerCase()).toBe( + nativeFailure.stderr.match(transportFailure)?.[0].toLowerCase(), + ); + + const absent = spawnSync( + 'git', + ['-C', join(repoRoot, 'origin.git'), 'rev-parse', '--verify', 'refs/heads/wasm-pack-failure'], + { encoding: 'utf8' }, + ); + expect(absent.status).not.toBe(0); + }); + + it('clone fails with a real certificate-verification error on an untrusted CA', async () => { + // Only the trusted CA is seeded; the untrusted server's CA is absent. + ({ kernel, vfs, dispose } = await createGitKernelWithNet([untrustedPort], trustedCaPem)); + + const res = await kernel.exec(git(`clone ${untrustedUrl()} /tmp/clone`)); + expect(res.exitCode).not.toBe(0); + expect(res.stderr).toMatch(/certificate|SSL|TLS|verify|CAfile|unable to (access|get local)/i); + expect(res.stderr).not.toContain('GitSubcommandUnsupported'); + expect(await vfs.exists('/tmp/clone/.git')).toBe(false); + }); + + it('http.sslVerify=false bypasses verification for an untrusted CA', async () => { + ({ kernel, vfs, dispose } = await createGitKernelWithNet([untrustedPort], trustedCaPem)); + + const res = await kernel.exec(git(`-c http.sslVerify=false clone ${untrustedUrl()} /tmp/clone`)); + expect(res.exitCode).toBe(0); + const readme = new TextDecoder().decode(await kernel.readFile('/tmp/clone/README.md')); + expect(readme).toBe('remote smart clone\n'); + }); + + it('GIT_SSL_NO_VERIFY bypasses verification for an untrusted CA', async () => { + ({ kernel, vfs, dispose } = await createGitKernelWithNet([untrustedPort], trustedCaPem)); + + const res = await kernel.exec(git(`clone ${untrustedUrl()} /tmp/clone`), { + env: { GIT_SSL_NO_VERIFY: '1' }, + }); + expect(res.exitCode).toBe(0); + expect(await vfs.exists('/tmp/clone/.git/HEAD')).toBe(true); + }); + + it('http.sslCAInfo trusts an explicitly supplied CA bundle', async () => { + // Seed only the trusted CA in the default bundle; supply the untrusted + // server's CA via a VFS file referenced with http.sslCAInfo. + ({ kernel, vfs, dispose } = await createGitKernelWithNet([untrustedPort], trustedCaPem)); + await vfs.mkdir('/tmp/ca', { recursive: true }); + await kernel.writeFile('/tmp/ca/untrusted.pem', untrustedCaPem); + + const res = await kernel.exec( + git(`-c http.sslCAInfo=/tmp/ca/untrusted.pem clone ${untrustedUrl()} /tmp/clone`), + ); + expect(res.exitCode).toBe(0); + const readme = new TextDecoder().decode(await kernel.readFile('/tmp/clone/README.md')); + expect(readme).toBe('remote smart clone\n'); + }); + }); +}); diff --git a/software/git/tsconfig.json b/software/git/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/git/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/grep/agentos-package.json b/software/grep/agentos-package.json new file mode 100644 index 0000000000..2b14ba7c37 --- /dev/null +++ b/software/grep/agentos-package.json @@ -0,0 +1,14 @@ +{ + "commands": [ + "grep", + "egrep", + "fgrep" + ], + "registry": { + "title": "grep", + "description": "GNU grep pattern matching (grep, egrep, fgrep).", + "priority": 40, + "image": "/images/registry/grep.svg", + "category": "core" + } +} diff --git a/software/grep/native/crates/cmd-egrep/Cargo.toml b/software/grep/native/crates/cmd-egrep/Cargo.toml new file mode 100644 index 0000000000..b3eb4c9222 --- /dev/null +++ b/software/grep/native/crates/cmd-egrep/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-egrep" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "egrep launcher for GNU grep" + +[[bin]] +name = "egrep" +path = "src/main.rs" + +[dependencies] +shims = { package = "secureexec-shims", path = "../../../../../toolchain/crates/libs/shims" } diff --git a/software/grep/native/crates/cmd-egrep/src/main.rs b/software/grep/native/crates/cmd-egrep/src/main.rs new file mode 100644 index 0000000000..b6671b6c38 --- /dev/null +++ b/software/grep/native/crates/cmd-egrep/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + let args: Vec = std::env::args_os().collect(); + std::process::exit(shims::grep_alias::egrep(args)); +} diff --git a/software/grep/native/crates/cmd-fgrep/Cargo.toml b/software/grep/native/crates/cmd-fgrep/Cargo.toml new file mode 100644 index 0000000000..573b6b100e --- /dev/null +++ b/software/grep/native/crates/cmd-fgrep/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-fgrep" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "fgrep launcher for GNU grep" + +[[bin]] +name = "fgrep" +path = "src/main.rs" + +[dependencies] +shims = { package = "secureexec-shims", path = "../../../../../toolchain/crates/libs/shims" } diff --git a/software/grep/native/crates/cmd-fgrep/src/main.rs b/software/grep/native/crates/cmd-fgrep/src/main.rs new file mode 100644 index 0000000000..3f81c7a4d5 --- /dev/null +++ b/software/grep/native/crates/cmd-fgrep/src/main.rs @@ -0,0 +1,4 @@ +fn main() { + let args: Vec = std::env::args_os().collect(); + std::process::exit(shims::grep_alias::fgrep(args)); +} diff --git a/software/grep/package.json b/software/grep/package.json new file mode 100644 index 0000000000..ed30841797 --- /dev/null +++ b/software/grep/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/grep", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "GNU grep pattern matching for secure-exec VMs (grep, egrep, fgrep)", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/gawk/src/index.ts b/software/grep/src/index.ts similarity index 100% rename from registry/software/gawk/src/index.ts rename to software/grep/src/index.ts diff --git a/software/grep/test/grep.test.ts b/software/grep/test/grep.test.ts new file mode 100644 index 0000000000..1816f9cbcb --- /dev/null +++ b/software/grep/test/grep.test.ts @@ -0,0 +1,125 @@ +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { createWasmVmRuntime } from "@agentos/test-harness"; +import { + C_BUILD_DIR, + COMMANDS_DIR, + NodeFileSystem, + createKernel, + describeIf, + hasCWasmBinaries, + hasWasmBinaries, +} from "@agentos/test-harness"; +import type { Kernel } from "@agentos/test-harness"; +import { afterEach, describe, expect, it } from "vitest"; + +let tempRoot: string | undefined; + +async function createTestVFS(): Promise { + tempRoot = await mkdtemp(join(tmpdir(), "agentos-grep-")); + await writeFixture( + "/project/notes.txt", + ["Alpha", "beta", "alphabet", "delta"].join("\n") + "\n", + ); + await writeFixture( + "/project/other.txt", + ["gamma", "Beta blocker", "literal a+b"].join("\n") + "\n", + ); + return new NodeFileSystem({ root: tempRoot }); +} + +async function writeFixture(path: string, contents: string): Promise { + if (!tempRoot) throw new Error("fixture root not initialized"); + const hostPath = join(tempRoot, path.replace(/^\/+/, "")); + await mkdir(dirname(hostPath), { recursive: true }); + await writeFile(hostPath, contents); +} + +describeIf( + hasWasmBinaries && hasCWasmBinaries("grep"), + "GNU grep command", + { timeout: 10_000 }, + () => { + let kernel: Kernel; + + afterEach(async () => { + await kernel?.dispose(); + if (tempRoot) { + await rm(tempRoot, { recursive: true, force: true }); + tempRoot = undefined; + } + }); + + async function mountFixture(): Promise { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount( + createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), + ); + return vfs; + } + + it("reports the upstream GNU grep version", async () => { + await mountFixture(); + + const result = await kernel.exec("grep --version", {}); + expect(result.stdout).toContain("GNU grep 3.12"); + }); + + it("prints matching lines from a file", async () => { + await mountFixture(); + + const result = await kernel.exec("grep alpha /project/notes.txt", {}); + expect(result.stdout).toBe("alphabet\n"); + }); + + it("supports case-insensitive search", async () => { + await mountFixture(); + + const result = await kernel.exec("grep -i beta /project/notes.txt", {}); + expect(result.stdout).toBe("beta\n"); + }); + + it("supports inverted matches", async () => { + await mountFixture(); + + const result = await kernel.exec("grep -v Alpha /project/notes.txt", {}); + expect(result.stdout).toBe("beta\nalphabet\ndelta\n"); + }); + + it("counts matches", async () => { + await mountFixture(); + + const result = await kernel.exec("grep -c a /project/notes.txt", {}); + expect(result.stdout).toBe("4\n"); + }); + + it("prints file names with matches", async () => { + await mountFixture(); + + const result = await kernel.exec( + "grep -l gamma /project/notes.txt /project/other.txt", + {}, + ); + expect(result.stdout).toBe("/project/other.txt\n"); + }); + + it("supports egrep extended regex alias", async () => { + await mountFixture(); + + const result = await kernel.exec( + "egrep 'Alpha|delta' /project/notes.txt", + {}, + ); + expect(result.stdout).toBe("Alpha\ndelta\n"); + }); + + it("supports fgrep fixed-string alias", async () => { + await mountFixture(); + + const result = await kernel.exec("fgrep 'a+b' /project/other.txt", {}); + expect(result.stdout).toBe("literal a+b\n"); + }); + }, +); diff --git a/software/grep/test/manifest.test.ts b/software/grep/test/manifest.test.ts new file mode 100644 index 0000000000..89139d33b8 --- /dev/null +++ b/software/grep/test/manifest.test.ts @@ -0,0 +1,19 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const packageDir = new URL("..", import.meta.url).pathname; + +describe("package manifest", () => { + it("declares command binaries", () => { + const manifest = JSON.parse( + readFileSync(join(packageDir, "agentos-package.json"), "utf8"), + ); + + expect(manifest.commands?.length ?? 0).toBeGreaterThan(0); + for (const command of manifest.commands) { + expect(typeof command).toBe("string"); + expect(command.length).toBeGreaterThan(0); + } + }); +}); diff --git a/software/grep/tsconfig.json b/software/grep/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/grep/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/gzip/agentos-package.json b/software/gzip/agentos-package.json new file mode 100644 index 0000000000..33ceeb2f6e --- /dev/null +++ b/software/gzip/agentos-package.json @@ -0,0 +1,16 @@ +{ + "commands": [ + "gzip" + ], + "aliases": { + "gunzip": "gzip", + "zcat": "gzip" + }, + "registry": { + "title": "gzip", + "description": "GNU gzip compression (gzip, gunzip, zcat).", + "priority": 8, + "image": "/images/registry/gzip.svg", + "category": "core" + } +} diff --git a/software/gzip/native/crates/cmd-gzip/Cargo.toml b/software/gzip/native/crates/cmd-gzip/Cargo.toml new file mode 100644 index 0000000000..dafe689b6d --- /dev/null +++ b/software/gzip/native/crates/cmd-gzip/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-gzip" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "gzip standalone binary for secure-exec VM" + +[[bin]] +name = "gzip" +path = "src/main.rs" + +[dependencies] +secureexec-gzip = { path = "../gzip" } diff --git a/registry/native/crates/commands/gzip/src/main.rs b/software/gzip/native/crates/cmd-gzip/src/main.rs similarity index 100% rename from registry/native/crates/commands/gzip/src/main.rs rename to software/gzip/native/crates/cmd-gzip/src/main.rs diff --git a/software/gzip/native/crates/gzip/Cargo.toml b/software/gzip/native/crates/gzip/Cargo.toml new file mode 100644 index 0000000000..897731b6d4 --- /dev/null +++ b/software/gzip/native/crates/gzip/Cargo.toml @@ -0,0 +1,10 @@ +[package] +workspace = "../../../../../toolchain" +name = "secureexec-gzip" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "gzip/gunzip/zcat implementations for secure-exec standalone binaries" + +[dependencies] +flate2 = { version = "1.0", default-features = false, features = ["rust_backend"] } diff --git a/registry/native/crates/libs/gzip/src/lib.rs b/software/gzip/native/crates/gzip/src/lib.rs similarity index 100% rename from registry/native/crates/libs/gzip/src/lib.rs rename to software/gzip/native/crates/gzip/src/lib.rs diff --git a/software/gzip/package.json b/software/gzip/package.json new file mode 100644 index 0000000000..f35d2a16a1 --- /dev/null +++ b/software/gzip/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/gzip", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "GNU gzip compression for secure-exec VMs (gzip, gunzip, zcat)", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/git/src/index.ts b/software/gzip/src/index.ts similarity index 100% rename from registry/software/git/src/index.ts rename to software/gzip/src/index.ts diff --git a/software/gzip/test/gzip.test.ts b/software/gzip/test/gzip.test.ts new file mode 100644 index 0000000000..477499a35a --- /dev/null +++ b/software/gzip/test/gzip.test.ts @@ -0,0 +1,138 @@ +import { existsSync } from "node:fs"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + NodeFileSystem, + createKernel, + createWasmVmRuntime, + describeIf, +} from "@agentos/test-harness"; +import type { Kernel } from "@agentos/test-harness"; +import { afterEach, expect, it } from "vitest"; + +const GZIP_COMMAND_DIR = fileURLToPath(new URL("../bin", import.meta.url)); +const hasGzipPackageBinary = existsSync(join(GZIP_COMMAND_DIR, "gzip")); + +let tempRoot: string | undefined; + +async function writeFixture(path: string, contents: string): Promise { + if (!tempRoot) throw new Error("fixture root not initialized"); + const hostPath = join(tempRoot, path.replace(/^\/+/, "")); + await mkdir(dirname(hostPath), { recursive: true }); + await writeFile(hostPath, contents); +} + +async function createTestVFS(): Promise { + tempRoot = await mkdtemp(join(tmpdir(), "agentos-gzip-")); + await writeFixture("/project/report.txt", "alpha\nbeta\ngamma\n"); + await writeFixture("/project/remove.txt", "temporary payload\n"); + return new NodeFileSystem({ root: tempRoot }); +} + +const textDecoder = new TextDecoder(); + +describeIf(hasGzipPackageBinary, "gzip command", { timeout: 10_000 }, () => { + let kernel: Kernel | undefined; + + afterEach(async () => { + await kernel?.dispose(); + kernel = undefined; + if (tempRoot) { + await rm(tempRoot, { recursive: true, force: true }); + tempRoot = undefined; + } + }); + + async function mountFixture(): Promise { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [GZIP_COMMAND_DIR] })); + } + + async function runCommand(command: string, args: string[]) { + if (!kernel) throw new Error("kernel not mounted"); + let stdout = ""; + let stderr = ""; + const proc = kernel.spawn(command, args, { + onStdout: (chunk) => { + stdout += Buffer.from(chunk).toString("utf8"); + }, + onStderr: (chunk) => { + stderr += Buffer.from(chunk).toString("utf8"); + }, + }); + const exitCode = await proc.wait(); + await new Promise((resolve) => setTimeout(resolve, 0)); + return { stdout, stderr, exitCode }; + } + + async function readGuestText(path: string): Promise { + if (!kernel) throw new Error("kernel not mounted"); + return textDecoder.decode(await kernel.readFile(path)); + } + + it("compresses files while keeping originals with -k", async () => { + await mountFixture(); + + const result = await runCommand("gzip", ["-k", "/project/report.txt"]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + await expect(readGuestText("/project/report.txt")).resolves.toBe( + "alpha\nbeta\ngamma\n", + ); + + const decompressed = await runCommand("gunzip", [ + "-c", + "/project/report.txt.gz", + ]); + expect(decompressed.exitCode, decompressed.stderr || decompressed.stdout).toBe(0); + expect(decompressed.stdout).toBe("alpha\nbeta\ngamma\n"); + }); + + it("removes the source file unless -k is set", async () => { + await mountFixture(); + + const result = await runCommand("gzip", ["/project/remove.txt"]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + await expect(readGuestText("/project/remove.txt")).rejects.toThrow(); + await expect(readGuestText("/project/remove.txt.gz")).resolves.toBeTruthy(); + }); + + it("decompresses files with gunzip -k", async () => { + await mountFixture(); + const compressed = await runCommand("gzip", ["-k", "/project/report.txt"]); + expect(compressed.exitCode, compressed.stderr || compressed.stdout).toBe(0); + await writeFixture("/project/report.txt", "replacement\n"); + + const result = await runCommand("gunzip", [ + "-fk", + "/project/report.txt.gz", + ]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + await expect(readGuestText("/project/report.txt")).resolves.toBe( + "alpha\nbeta\ngamma\n", + ); + await expect(readGuestText("/project/report.txt.gz")).resolves.toBeTruthy(); + }); + + it("streams decompressed content with zcat", async () => { + await mountFixture(); + const compressed = await runCommand("gzip", ["-k", "/project/report.txt"]); + expect(compressed.exitCode, compressed.stderr || compressed.stdout).toBe(0); + + const result = await runCommand("zcat", ["/project/report.txt.gz"]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stdout).toBe("alpha\nbeta\ngamma\n"); + }); + + it("fails instead of overwriting compressed outputs without -f", async () => { + await mountFixture(); + const first = await runCommand("gzip", ["-k", "/project/report.txt"]); + expect(first.exitCode, first.stderr || first.stdout).toBe(0); + + const second = await runCommand("gzip", ["-k", "/project/report.txt"]); + expect(second.exitCode).not.toBe(0); + expect(second.stderr).toContain("already exists"); + }); +}); diff --git a/software/gzip/test/manifest.test.ts b/software/gzip/test/manifest.test.ts new file mode 100644 index 0000000000..89139d33b8 --- /dev/null +++ b/software/gzip/test/manifest.test.ts @@ -0,0 +1,19 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const packageDir = new URL("..", import.meta.url).pathname; + +describe("package manifest", () => { + it("declares command binaries", () => { + const manifest = JSON.parse( + readFileSync(join(packageDir, "agentos-package.json"), "utf8"), + ); + + expect(manifest.commands?.length ?? 0).toBeGreaterThan(0); + for (const command of manifest.commands) { + expect(typeof command).toBe("string"); + expect(command.length).toBeGreaterThan(0); + } + }); +}); diff --git a/software/gzip/tsconfig.json b/software/gzip/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/gzip/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/jq/agentos-package.json b/software/jq/agentos-package.json new file mode 100644 index 0000000000..43e0a6f3c8 --- /dev/null +++ b/software/jq/agentos-package.json @@ -0,0 +1,12 @@ +{ + "commands": [ + "jq" + ], + "registry": { + "title": "jq", + "description": "Lightweight JSON processor.", + "priority": 80, + "image": "/images/registry/jq.svg", + "category": "data" + } +} diff --git a/software/jq/native/crates/cmd-jq/Cargo.toml b/software/jq/native/crates/cmd-jq/Cargo.toml new file mode 100644 index 0000000000..bdc904ce10 --- /dev/null +++ b/software/jq/native/crates/cmd-jq/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-jq" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "jq standalone binary for secure-exec VM" + +[[bin]] +name = "jq" +path = "src/main.rs" + +[dependencies] +secureexec-jq = { path = "../jq" } diff --git a/registry/native/crates/commands/jq/src/main.rs b/software/jq/native/crates/cmd-jq/src/main.rs similarity index 100% rename from registry/native/crates/commands/jq/src/main.rs rename to software/jq/native/crates/cmd-jq/src/main.rs diff --git a/software/jq/native/crates/jq/Cargo.toml b/software/jq/native/crates/jq/Cargo.toml new file mode 100644 index 0000000000..6a86e30e47 --- /dev/null +++ b/software/jq/native/crates/jq/Cargo.toml @@ -0,0 +1,13 @@ +[package] +workspace = "../../../../../toolchain" +name = "secureexec-jq" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "jq implementation for secure-exec standalone binaries" + +[dependencies] +jaq-core = "2.2" +jaq-std = "2.1" +jaq-json = { version = "1.1", features = ["serde_json"] } +serde_json = "1" diff --git a/software/jq/native/crates/jq/src/lib.rs b/software/jq/native/crates/jq/src/lib.rs new file mode 100644 index 0000000000..66fc2544cc --- /dev/null +++ b/software/jq/native/crates/jq/src/lib.rs @@ -0,0 +1,349 @@ +//! jq implementation using the jaq crate (pure Rust, jq-compatible). +//! +//! Wraps jaq-core/jaq-std/jaq-json to provide a standard jq CLI interface. + +use std::ffi::OsString; +use std::fs::File as FsFile; +use std::io::{self, Read, Write}; + +use jaq_core::load::{Arena, File, Loader}; +use jaq_core::{Compiler, Ctx, RcIter}; +use jaq_json::Val; + +const MAX_INPUT_BYTES: usize = 16 * 1024 * 1024; +const MAX_INPUT_VALUES: usize = 100_000; + +/// Entry point for jq command. +pub fn main(args: Vec) -> i32 { + let str_args: Vec = args + .iter() + .skip(1) // skip argv[0] + .map(|a| a.to_string_lossy().to_string()) + .collect(); + + match run_jq(&str_args) { + Ok(code) => code, + Err(msg) => { + eprintln!("jq: {}", msg); + 2 + } + } +} + +struct JqOptions { + filter: String, + raw_output: bool, + raw_input: bool, + slurp: bool, + compact: bool, + null_input: bool, + exit_status: bool, + join_output: bool, + args: Vec<(String, String)>, + jsonargs: Vec<(String, Val)>, + input_paths: Vec, +} + +fn parse_args(args: &[String]) -> Result { + let mut opts = JqOptions { + filter: String::new(), + raw_output: false, + raw_input: false, + slurp: false, + compact: false, + null_input: false, + exit_status: false, + join_output: false, + args: Vec::new(), + jsonargs: Vec::new(), + input_paths: Vec::new(), + }; + + let mut filter_set = false; + let mut i = 0; + + while i < args.len() { + let arg = &args[i]; + + if arg == "--" { + let remaining = &args[i + 1..]; + if !filter_set { + let Some(filter) = remaining.first() else { + return Err("no filter provided".to_string()); + }; + opts.filter = filter.clone(); + filter_set = true; + opts.input_paths.extend(remaining.iter().skip(1).cloned()); + } else { + opts.input_paths.extend(remaining.iter().cloned()); + } + break; + } + + if arg.starts_with('-') && arg.len() > 1 && !arg.starts_with("--") { + // Handle combined short flags like -rn, -crS, etc. + let flags = &arg[1..]; + for c in flags.chars() { + match c { + 'r' => opts.raw_output = true, + 'R' => opts.raw_input = true, + 's' => opts.slurp = true, + 'c' => opts.compact = true, + 'n' => opts.null_input = true, + 'e' => opts.exit_status = true, + 'j' => opts.join_output = true, + 'S' => {} // sort keys - jaq sorts by default + _ => return Err(format!("unknown option: -{}", c)), + } + } + } else if arg == "--raw-output" || arg == "--raw-output0" { + opts.raw_output = true; + } else if arg == "--raw-input" { + opts.raw_input = true; + } else if arg == "--slurp" { + opts.slurp = true; + } else if arg == "--compact-output" { + opts.compact = true; + } else if arg == "--null-input" { + opts.null_input = true; + } else if arg == "--exit-status" { + opts.exit_status = true; + } else if arg == "--join-output" { + opts.join_output = true; + } else if arg == "--arg" { + if i + 2 >= args.len() { + return Err("--arg requires name and value".to_string()); + } + let name = args[i + 1].clone(); + let value = args[i + 2].clone(); + opts.args.push((name, value)); + i += 2; + } else if arg == "--argjson" { + if i + 2 >= args.len() { + return Err("--argjson requires name and value".to_string()); + } + let name = args[i + 1].clone(); + let json_str = &args[i + 2]; + let value: serde_json::Value = serde_json::from_str(json_str) + .map_err(|e| format!("invalid JSON for --argjson: {}", e))?; + opts.jsonargs.push((name, Val::from(value))); + i += 2; + } else if !filter_set { + opts.filter = arg.clone(); + filter_set = true; + } else { + opts.input_paths.push(arg.clone()); + } + + i += 1; + } + + if !filter_set { + return Err("no filter provided".to_string()); + } + + Ok(opts) +} + +fn read_sources(opts: &JqOptions) -> Result, String> { + if opts.input_paths.is_empty() { + let mut stdin_data = String::new(); + io::stdin() + .take((MAX_INPUT_BYTES + 1) as u64) + .read_to_string(&mut stdin_data) + .map_err(|e| format!("failed to read stdin: {}", e))?; + if stdin_data.len() > MAX_INPUT_BYTES { + return Err("stdin exceeds size limit".to_string()); + } + return Ok(vec![stdin_data]); + } + + let mut total = 0usize; + let mut sources = Vec::new(); + for path in &opts.input_paths { + let mut data = String::new(); + if path == "-" { + io::stdin() + .take((MAX_INPUT_BYTES.saturating_sub(total) + 1) as u64) + .read_to_string(&mut data) + .map_err(|e| format!("failed to read stdin: {}", e))?; + } else { + FsFile::open(path) + .map_err(|e| format!("failed to open {}: {}", path, e))? + .take((MAX_INPUT_BYTES.saturating_sub(total) + 1) as u64) + .read_to_string(&mut data) + .map_err(|e| format!("failed to read {}: {}", path, e))?; + } + total = total + .checked_add(data.len()) + .ok_or_else(|| "input exceeds size limit".to_string())?; + if total > MAX_INPUT_BYTES { + return Err("input exceeds size limit".to_string()); + } + sources.push(data); + } + Ok(sources) +} + +fn read_inputs(opts: &JqOptions) -> Result, String> { + if opts.null_input { + return Ok(vec![Val::from(serde_json::Value::Null)]); + } + + let sources = read_sources(opts)?; + + if opts.raw_input { + let raw_data = sources.concat(); + if opts.slurp { + Ok(vec![Val::from(serde_json::Value::String(raw_data))]) + } else { + let mut lines = Vec::new(); + for line in raw_data.lines() { + push_input_value( + &mut lines, + Val::from(serde_json::Value::String(line.to_string())), + )?; + } + Ok(lines) + } + } else { + let mut values = Vec::new(); + for source in &sources { + let trimmed = source.trim(); + if trimmed.is_empty() { + continue; + } + + let decoder = + serde_json::Deserializer::from_str(trimmed).into_iter::(); + for result in decoder { + let value = result.map_err(|e| format!("parse error: {}", e))?; + push_input_value(&mut values, value)?; + } + } + + if values.is_empty() && opts.input_paths.is_empty() { + values.push(serde_json::Value::Null); + } + + if opts.slurp { + Ok(vec![Val::from(serde_json::Value::Array(values))]) + } else { + Ok(values.into_iter().map(Val::from).collect()) + } + } +} + +fn push_input_value(values: &mut Vec, value: T) -> Result<(), String> { + if values.len() >= MAX_INPUT_VALUES { + return Err("too many input values".to_string()); + } + values.push(value); + Ok(()) +} + +/// Format a jaq Val as a string for output. +fn format_output(val: &Val, opts: &JqOptions) -> Result { + let compact_str = format!("{}", val); + + // For raw output, unquote strings + if opts.raw_output { + if compact_str.starts_with('"') && compact_str.ends_with('"') && compact_str.len() >= 2 { + if let Ok(unescaped) = serde_json::from_str::(&compact_str) { + return Ok(unescaped); + } + } + } + + if opts.compact { + Ok(compact_str) + } else { + // Pretty print via serde_json + if let Ok(v) = serde_json::from_str::(&compact_str) { + serde_json::to_string_pretty(&v).map_err(|e| format!("output format error: {}", e)) + } else { + Ok(compact_str) + } + } +} + +fn run_jq(args: &[String]) -> Result { + if matches!(args, [arg] if arg == "--version" || arg == "-V") { + println!("jq-jaq-{}", env!("CARGO_PKG_VERSION")); + return Ok(0); + } + + let opts = parse_args(args)?; + let inputs = read_inputs(&opts)?; + + // Set up variable bindings for --arg and --argjson + let mut var_vals: Vec = Vec::new(); + + for (_name, value) in &opts.args { + var_vals.push(Val::from(serde_json::Value::String(value.clone()))); + } + for (_name, value) in &opts.jsonargs { + var_vals.push(value.clone()); + } + + // Load and compile filter + let loader = Loader::new(jaq_std::defs().chain(jaq_json::defs())); + let arena = Arena::default(); + let program = File { + code: opts.filter.as_str(), + path: (), + }; + let modules = loader + .load(&arena, program) + .map_err(|errs| format!("parse error: {:?}", errs))?; + + let filter = Compiler::default() + .with_funs(jaq_std::funs().chain(jaq_json::funs())) + .compile(modules) + .map_err(|errs| format!("compile error: {:?}", errs))?; + + let empty_inputs = RcIter::new(core::iter::empty()); + let stdout = io::stdout(); + let mut out = stdout.lock(); + let mut had_false_or_null = false; + + for input in inputs { + let ctx = Ctx::new(var_vals.iter().cloned(), &empty_inputs); + let results = filter.run((ctx, input)); + + for result in results { + match result { + Ok(val) => { + let s = format_output(&val, &opts)?; + + // Track for --exit-status + let compact = format!("{}", val); + if compact == "null" || compact == "false" { + had_false_or_null = true; + } + + if opts.join_output { + write!(out, "{}", s) + .map_err(|e| format!("failed to write stdout: {}", e))?; + } else { + writeln!(out, "{}", s) + .map_err(|e| format!("failed to write stdout: {}", e))?; + } + } + Err(e) => { + eprintln!("jq: error: {}", e); + return Ok(5); + } + } + } + } + + if opts.exit_status && had_false_or_null { + return Ok(1); + } + + out.flush() + .map_err(|e| format!("failed to flush stdout: {}", e))?; + + Ok(0) +} diff --git a/software/jq/package.json b/software/jq/package.json new file mode 100644 index 0000000000..a7fa98ecf1 --- /dev/null +++ b/software/jq/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/jq", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "jq JSON processor for secure-exec VMs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/grep/src/index.ts b/software/jq/src/index.ts similarity index 100% rename from registry/software/grep/src/index.ts rename to software/jq/src/index.ts diff --git a/software/jq/test/jq.test.ts b/software/jq/test/jq.test.ts new file mode 100644 index 0000000000..5d15eaebc3 --- /dev/null +++ b/software/jq/test/jq.test.ts @@ -0,0 +1,153 @@ +import { existsSync } from "node:fs"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + NodeFileSystem, + createKernel, + createWasmVmRuntime, + describeIf, +} from "@agentos/test-harness"; +import type { Kernel } from "@agentos/test-harness"; +import { afterEach, expect, it } from "vitest"; + +const JQ_COMMAND_DIR = fileURLToPath(new URL("../bin", import.meta.url)); +const hasJqPackageBinary = existsSync(join(JQ_COMMAND_DIR, "jq")); + +let tempRoot: string | undefined; + +async function writeFixture(path: string, contents: string): Promise { + if (!tempRoot) throw new Error("fixture root not initialized"); + const hostPath = join(tempRoot, path.replace(/^\/+/, "")); + await mkdir(dirname(hostPath), { recursive: true }); + await writeFile(hostPath, contents); +} + +async function createTestVFS(): Promise { + tempRoot = await mkdtemp(join(tmpdir(), "agentos-jq-")); + await writeFixture( + "/project/users.json", + `${JSON.stringify( + { + users: [ + { name: "Ada", active: true, team: "runtime", score: 7 }, + { name: "Grace", active: false, team: "docs", score: 5 }, + { name: "Linus", active: true, team: "runtime", score: 3 }, + ], + }, + null, + 2, + )}\n`, + ); + await writeFixture( + "/project/events.ndjson", + [ + JSON.stringify({ type: "build", value: 4 }), + JSON.stringify({ type: "test", value: 6 }), + JSON.stringify({ type: "deploy", value: 2 }), + ].join("\n") + "\n", + ); + await writeFixture("/project/broken.json", '{"users": ['); + return new NodeFileSystem({ root: tempRoot }); +} + +function lines(stdout: string): string[] { + return stdout.split("\n").filter((line) => line.length > 0); +} + +describeIf(hasJqPackageBinary, "jq command", { timeout: 10_000 }, () => { + let kernel: Kernel | undefined; + + afterEach(async () => { + await kernel?.dispose(); + kernel = undefined; + if (tempRoot) { + await rm(tempRoot, { recursive: true, force: true }); + tempRoot = undefined; + } + }); + + async function mountFixture(): Promise { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [JQ_COMMAND_DIR] })); + } + + async function runJq(args: string[]) { + if (!kernel) throw new Error("kernel not mounted"); + let stdout = ""; + let stderr = ""; + const proc = kernel.spawn("jq", args, { + onStdout: (chunk) => { + stdout += Buffer.from(chunk).toString("utf8"); + }, + onStderr: (chunk) => { + stderr += Buffer.from(chunk).toString("utf8"); + }, + }); + const exitCode = await proc.wait(); + await new Promise((resolve) => setTimeout(resolve, 0)); + return { stdout, stderr, exitCode }; + } + + it("reports a jq-compatible version", async () => { + await mountFixture(); + + const result = await runJq(["--version"]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stdout).toMatch(/^jq-/); + }); + + it("filters arrays and emits raw strings", async () => { + await mountFixture(); + + const result = await runJq([ + "-r", + '.users[] | select(.active) | "\\(.name):\\(.score)"', + "/project/users.json", + ]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(lines(result.stdout)).toEqual(["Ada:7", "Linus:3"]); + }); + + it("builds aggregate JSON objects", async () => { + await mountFixture(); + + const result = await runJq([ + "-c", + '{activeNames: [.users[] | select(.active) | .name], runtimeTotal: ([.users[] | select(.team == "runtime") | .score] | add)}', + "/project/users.json", + ]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(JSON.parse(result.stdout.trim())).toEqual({ + activeNames: ["Ada", "Linus"], + runtimeTotal: 10, + }); + }); + + it("slurps newline-delimited JSON records", async () => { + await mountFixture(); + + const result = await runJq([ + "-s", + "-c", + "{count: length, total: (map(.value) | add), types: map(.type)}", + "/project/events.ndjson", + ]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(JSON.parse(result.stdout.trim())).toEqual({ + count: 3, + total: 12, + types: ["build", "test", "deploy"], + }); + }); + + it("fails with a parse error for invalid JSON", async () => { + await mountFixture(); + + const result = await runJq([".", "/project/broken.json"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("parse error"); + }); +}); diff --git a/software/jq/test/manifest.test.ts b/software/jq/test/manifest.test.ts new file mode 100644 index 0000000000..89139d33b8 --- /dev/null +++ b/software/jq/test/manifest.test.ts @@ -0,0 +1,19 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const packageDir = new URL("..", import.meta.url).pathname; + +describe("package manifest", () => { + it("declares command binaries", () => { + const manifest = JSON.parse( + readFileSync(join(packageDir, "agentos-package.json"), "utf8"), + ); + + expect(manifest.commands?.length ?? 0).toBeGreaterThan(0); + for (const command of manifest.commands) { + expect(typeof command).toBe("string"); + expect(command.length).toBeGreaterThan(0); + } + }); +}); diff --git a/software/jq/tsconfig.json b/software/jq/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/jq/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/opencode/agentos-package.json b/software/opencode/agentos-package.json new file mode 100644 index 0000000000..c32dead093 --- /dev/null +++ b/software/opencode/agentos-package.json @@ -0,0 +1,19 @@ +{ + "name": "opencode", + "agent": { + "acpEntrypoint": "agentos-opencode-acp", + "env": { + "OPENCODE_DISABLE_CONFIG_DEP_INSTALL": "1", + "OPENCODE_DISABLE_EMBEDDED_WEB_UI": "1" + } + }, + "registry": { + "title": "OpenCode", + "description": "Run OpenCode, an open-source coding agent, inside agentOS.", + "docsHref": "/docs/agents/opencode", + "image": "/images/registry/opencode.svg", + "priority": 70, + "category": "agents" + }, + "kind": "agent" +} diff --git a/software/opencode/package.json b/software/opencode/package.json new file mode 100644 index 0000000000..b0d1546147 --- /dev/null +++ b/software/opencode/package.json @@ -0,0 +1,38 @@ +{ + "name": "@agentos-software/opencode", + "version": "0.2.1", + "type": "module", + "license": "Apache-2.0", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "agentos-opencode-acp": "./dist/adapter.js" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "!dist/package", + "!dist/package.tar", + "agentos-package.json" + ], + "scripts": { + "build": "node ./scripts/build-opencode-acp.mjs && tsc && rm -rf dist/package dist/package.tar && agentos-toolchain pack . --out dist/package --agent agentos-opencode-acp", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "bun": "1.3.11", + "typescript": "^5.7.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/agent/opencode/scripts/build-opencode-acp.mjs b/software/opencode/scripts/build-opencode-acp.mjs similarity index 100% rename from registry/agent/opencode/scripts/build-opencode-acp.mjs rename to software/opencode/scripts/build-opencode-acp.mjs diff --git a/registry/agent/opencode/src/adapter.ts b/software/opencode/src/adapter.ts similarity index 100% rename from registry/agent/opencode/src/adapter.ts rename to software/opencode/src/adapter.ts diff --git a/registry/software/gzip/src/index.ts b/software/opencode/src/index.ts similarity index 100% rename from registry/software/gzip/src/index.ts rename to software/opencode/src/index.ts diff --git a/registry/agent/opencode/src/opencode-acp.mjs.d.ts b/software/opencode/src/opencode-acp.mjs.d.ts similarity index 100% rename from registry/agent/opencode/src/opencode-acp.mjs.d.ts rename to software/opencode/src/opencode-acp.mjs.d.ts diff --git a/software/opencode/tsconfig.json b/software/opencode/tsconfig.json new file mode 100644 index 0000000000..73a06ddbdb --- /dev/null +++ b/software/opencode/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/registry/agent/opencode/upstream/opencode-v1.3.13.patch b/software/opencode/upstream/opencode-v1.3.13.patch similarity index 100% rename from registry/agent/opencode/upstream/opencode-v1.3.13.patch rename to software/opencode/upstream/opencode-v1.3.13.patch diff --git a/software/pi-cli/agentos-package.json b/software/pi-cli/agentos-package.json new file mode 100644 index 0000000000..9d25dd6f54 --- /dev/null +++ b/software/pi-cli/agentos-package.json @@ -0,0 +1,10 @@ +{ + "name": "pi-cli", + "agent": { + "acpEntrypoint": "pi-acp", + "env": { + "PI_ACP_PI_COMMAND": "pi" + } + }, + "kind": "agent" +} diff --git a/software/pi-cli/package.json b/software/pi-cli/package.json new file mode 100644 index 0000000000..a6c9463342 --- /dev/null +++ b/software/pi-cli/package.json @@ -0,0 +1,42 @@ +{ + "name": "@agentos-software/pi-cli", + "version": "0.2.1", + "type": "module", + "license": "Apache-2.0", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "pi-acp": "./node_modules/pi-acp/dist/index.js", + "pi": "./node_modules/@mariozechner/pi-coding-agent/dist/cli.js" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "!dist/package", + "!dist/package.tar", + "agentos-package.json" + ], + "scripts": { + "build": "tsc && rm -rf dist/package dist/package.tar && agentos-toolchain pack . --out dist/package --agent pi-acp --prune-native", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "dependencies": { + "@mariozechner/pi-coding-agent": "^0.60.0", + "pi-acp": "^0.0.23" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.7.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/http-get/src/index.ts b/software/pi-cli/src/index.ts similarity index 100% rename from registry/software/http-get/src/index.ts rename to software/pi-cli/src/index.ts diff --git a/software/pi-cli/tsconfig.json b/software/pi-cli/tsconfig.json new file mode 100644 index 0000000000..73a06ddbdb --- /dev/null +++ b/software/pi-cli/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/software/pi/adapter-browser-entry.mjs b/software/pi/adapter-browser-entry.mjs new file mode 100644 index 0000000000..2bb013fee2 --- /dev/null +++ b/software/pi/adapter-browser-entry.mjs @@ -0,0 +1,38 @@ +// Browser-bundle entry for the pi ACP adapter. The adapter normally loads its SDK via +// computed dynamic import() from a VM-mounted node_modules; in the browser converged +// executor there is no such mount, so we statically import the SDK submodules (which +// esbuild bundles into a single self-contained file) and hand them to the adapter via +// the `__piSdkModules` override (see loadPiSdkRuntime). The adapter is otherwise +// unchanged — same ACP behavior, just bundled instead of VFS-resolved. +// +// Imports use relative node_modules file paths (not bare package subpaths) so esbuild +// resolves the dist files directly, bypassing the packages' restrictive `exports`. +import * as agentCore from "./node_modules/@mariozechner/pi-agent-core/dist/index.js"; +import * as authStorage from "./node_modules/@mariozechner/pi-coding-agent/dist/core/auth-storage.js"; +import * as config from "./node_modules/@mariozechner/pi-coding-agent/dist/config.js"; +import * as defaults from "./node_modules/@mariozechner/pi-coding-agent/dist/core/defaults.js"; +import * as messages from "./node_modules/@mariozechner/pi-coding-agent/dist/core/messages.js"; +import * as modelRegistry from "./node_modules/@mariozechner/pi-coding-agent/dist/core/model-registry.js"; +import * as resourceLoader from "./node_modules/@mariozechner/pi-coding-agent/dist/core/resource-loader.js"; +import * as sdk from "./node_modules/@mariozechner/pi-coding-agent/dist/core/sdk.js"; +import * as sessionManager from "./node_modules/@mariozechner/pi-coding-agent/dist/core/session-manager.js"; +import * as settingsManager from "./node_modules/@mariozechner/pi-coding-agent/dist/core/settings-manager.js"; +import * as tools from "./node_modules/@mariozechner/pi-coding-agent/dist/core/tools/index.js"; + +// loadPiSdkRuntime reads this lazily (at session/new), so setting it after the adapter +// import (ESM-hoisted) is fine. +globalThis.__piSdkModules = { + agentCore, + authStorage, + config, + defaults, + messages, + modelRegistry, + resourceLoader, + sdk, + sessionManager, + settingsManager, + tools, +}; + +import "./dist/adapter.js"; diff --git a/software/pi/agentos-package.json b/software/pi/agentos-package.json new file mode 100644 index 0000000000..07ebc84234 --- /dev/null +++ b/software/pi/agentos-package.json @@ -0,0 +1,15 @@ +{ + "name": "pi", + "agent": { + "acpEntrypoint": "pi-sdk-acp", + "snapshot": true + }, + "registry": { + "title": "PI", + "description": "Run the PI coding agent with lightweight, fast execution.", + "image": "/images/registry/pi.svg", + "priority": 100, + "category": "agents" + }, + "kind": "agent" +} diff --git a/software/pi/package.json b/software/pi/package.json new file mode 100644 index 0000000000..c32c77b09b --- /dev/null +++ b/software/pi/package.json @@ -0,0 +1,44 @@ +{ + "name": "@agentos-software/pi", + "version": "0.2.1", + "type": "module", + "license": "Apache-2.0", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "bin": { + "pi": "./node_modules/@mariozechner/pi-coding-agent/dist/cli.js", + "pi-sdk-acp": "./dist/adapter.js" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "files": [ + "dist", + "!dist/package", + "!dist/package.tar", + "agentos-package.json" + ], + "scripts": { + "build": "tsc && node scripts/build-snapshot-bundle.mjs && rm -rf dist/package dist/package.tar && agentos-toolchain pack . --out dist/package --agent pi-sdk-acp --prune-native && node scripts/copy-snapshot-into-package.mjs", + "build:snapshot": "node scripts/build-snapshot-bundle.mjs", + "check-types": "tsc --noEmit", + "test": "pnpm build && node --test --test-force-exit tests/*.test.mjs" + }, + "dependencies": { + "@agentclientprotocol/sdk": "^0.16.1", + "@mariozechner/pi-coding-agent": "0.60.0", + "@mariozechner/pi-ai": "0.60.0" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "esbuild": "^0.27.4", + "typescript": "^5.7.2", + "@agentos/test-harness": "workspace:*" + } +} diff --git a/registry/agent/pi/scripts/build-snapshot-bundle.mjs b/software/pi/scripts/build-snapshot-bundle.mjs similarity index 100% rename from registry/agent/pi/scripts/build-snapshot-bundle.mjs rename to software/pi/scripts/build-snapshot-bundle.mjs diff --git a/registry/agent/pi/scripts/copy-snapshot-into-package.mjs b/software/pi/scripts/copy-snapshot-into-package.mjs similarity index 100% rename from registry/agent/pi/scripts/copy-snapshot-into-package.mjs rename to software/pi/scripts/copy-snapshot-into-package.mjs diff --git a/registry/agent/pi/src/adapter.ts b/software/pi/src/adapter.ts similarity index 97% rename from registry/agent/pi/src/adapter.ts rename to software/pi/src/adapter.ts index 1eea462bdb..6fb8a3bf6d 100644 --- a/registry/agent/pi/src/adapter.ts +++ b/software/pi/src/adapter.ts @@ -103,32 +103,8 @@ Object.defineProperty(process, "stdin", { type SessionManagerLike = { inMemory(cwd?: string): unknown; - continueRecent(cwd: string, sessionDir: string): unknown; }; -/** - * Choose the Pi `SessionManager` for a new session. - * - * By default sessions are in-memory (nothing is written to disk), so a - * conversation is lost when the adapter process restarts. When the embedder - * provides a session directory via the `PI_SESSION_DIR` env var, use pi's - * `continueRecent` instead: it persists the session's `.jsonl` under that - * directory and resumes the most recent one, so conversations survive an adapter - * restart. No behavior change unless `PI_SESSION_DIR` is set. - * - * Exported for unit testing (the real `newSession` path needs the Pi SDK). - */ -export function resolveSessionManager( - SessionManager: SessionManagerLike, - cwd: string, - env: Record = process.env, -): unknown { - const sessionDir = env.PI_SESSION_DIR?.trim(); - return sessionDir - ? SessionManager.continueRecent(cwd, sessionDir) - : SessionManager.inMemory(cwd); -} - type ModelLike = { id: string; provider: string; @@ -897,7 +873,7 @@ export class PiSdkAgent implements Agent { const { session } = await __trace.span("createAgentSession", () => createAgentSession({ cwd: params.cwd, - sessionManager: resolveSessionManager(SessionManager, params.cwd), + sessionManager: SessionManager.inMemory(params.cwd), resourceLoader, tools: this.wrapTools( createCodingTools(params.cwd, { diff --git a/registry/software/jq/src/index.ts b/software/pi/src/index.ts similarity index 100% rename from registry/software/jq/src/index.ts rename to software/pi/src/index.ts diff --git a/registry/agent/pi/src/snapshot-entry.ts b/software/pi/src/snapshot-entry.ts similarity index 100% rename from registry/agent/pi/src/snapshot-entry.ts rename to software/pi/src/snapshot-entry.ts diff --git a/registry/agent/pi/tests/adapter.test.mjs b/software/pi/tests/adapter.test.mjs similarity index 100% rename from registry/agent/pi/tests/adapter.test.mjs rename to software/pi/tests/adapter.test.mjs diff --git a/software/pi/tsconfig.json b/software/pi/tsconfig.json new file mode 100644 index 0000000000..3ecc60a8e9 --- /dev/null +++ b/software/pi/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src/snapshot-entry.ts"] +} diff --git a/software/ripgrep/agentos-package.json b/software/ripgrep/agentos-package.json new file mode 100644 index 0000000000..dbc08da455 --- /dev/null +++ b/software/ripgrep/agentos-package.json @@ -0,0 +1,11 @@ +{ + "commands": [ + "rg" + ], + "registry": { + "title": "ripgrep", + "description": "Fast recursive search (rg).", + "priority": 85, + "category": "developer-tools" + } +} diff --git a/software/ripgrep/native/crates/cmd-rg/Cargo.toml b/software/ripgrep/native/crates/cmd-rg/Cargo.toml new file mode 100644 index 0000000000..f6184892c7 --- /dev/null +++ b/software/ripgrep/native/crates/cmd-rg/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-rg" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "rg (ripgrep) standalone binary for secure-exec VM" +publish = false + +[dependencies] +# ripgrep is a bin-only crate. `toolchain/Makefile` builds its upstream `rg` +# binary directly; this local package exists only so `cmd/rg` is discoverable +# and the upstream package is present in Cargo's resolved/vendor set. +ripgrep = { version = "15.1.0", default-features = false } diff --git a/software/ripgrep/native/crates/cmd-rg/src/lib.rs b/software/ripgrep/native/crates/cmd-rg/src/lib.rs new file mode 100644 index 0000000000..1bd8717252 --- /dev/null +++ b/software/ripgrep/native/crates/cmd-rg/src/lib.rs @@ -0,0 +1 @@ +//! Build trigger for the upstream `ripgrep` command package. diff --git a/software/ripgrep/package.json b/software/ripgrep/package.json new file mode 100644 index 0000000000..7241515e70 --- /dev/null +++ b/software/ripgrep/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/ripgrep", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "ripgrep fast search for secure-exec VMs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/ripgrep/src/index.ts b/software/ripgrep/src/index.ts similarity index 100% rename from registry/software/ripgrep/src/index.ts rename to software/ripgrep/src/index.ts diff --git a/software/ripgrep/test/manifest.test.ts b/software/ripgrep/test/manifest.test.ts new file mode 100644 index 0000000000..89139d33b8 --- /dev/null +++ b/software/ripgrep/test/manifest.test.ts @@ -0,0 +1,19 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const packageDir = new URL("..", import.meta.url).pathname; + +describe("package manifest", () => { + it("declares command binaries", () => { + const manifest = JSON.parse( + readFileSync(join(packageDir, "agentos-package.json"), "utf8"), + ); + + expect(manifest.commands?.length ?? 0).toBeGreaterThan(0); + for (const command of manifest.commands) { + expect(typeof command).toBe("string"); + expect(command.length).toBeGreaterThan(0); + } + }); +}); diff --git a/software/ripgrep/test/ripgrep.test.ts b/software/ripgrep/test/ripgrep.test.ts new file mode 100644 index 0000000000..af8d77885e --- /dev/null +++ b/software/ripgrep/test/ripgrep.test.ts @@ -0,0 +1,128 @@ +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { createWasmVmRuntime } from "@agentos/test-harness"; +import { + COMMANDS_DIR, + NodeFileSystem, + createKernel, + describeIf, + hasWasmBinaries, +} from "@agentos/test-harness"; +import type { Kernel } from "@agentos/test-harness"; +import { afterEach, describe, expect, it } from "vitest"; + +let tempRoot: string | undefined; + +async function createTestVFS(): Promise { + tempRoot = await mkdtemp(join(tmpdir(), "agentos-ripgrep-")); + + await writeFixture( + "/project/src/main.rs", + ["fn main() {", ' println!("needle");', "}"].join("\n") + "\n", + ); + await writeFixture( + "/project/src/lib.rs", + ["pub fn helper() {", " // Needle in a comment", "}"].join("\n") + "\n", + ); + await writeFixture("/project/docs/readme.md", "needle in docs\n"); + await writeFixture("/project/vendor/generated.rs", "needle in vendor\n"); + await writeFixture("/project/.hidden.txt", "needle hidden\n"); + await writeFixture("/project/.gitignore", "vendor/\n"); + await writeFixture("/project/.git/HEAD", "ref: refs/heads/main\n"); + + return new NodeFileSystem({ root: tempRoot }); +} + +async function writeFixture(path: string, contents: string): Promise { + if (!tempRoot) throw new Error("fixture root not initialized"); + const hostPath = join(tempRoot, path.replace(/^\/+/, "")); + await mkdir(dirname(hostPath), { recursive: true }); + await writeFile(hostPath, contents); +} + +function lines(stdout: string): string[] { + return stdout + .split("\n") + .filter((line) => line.length > 0) + .sort(); +} + +describeIf(hasWasmBinaries, "ripgrep command", { timeout: 10_000 }, () => { + let kernel: Kernel; + + afterEach(async () => { + await kernel?.dispose(); + if (tempRoot) { + await rm(tempRoot, { recursive: true, force: true }); + tempRoot = undefined; + } + }); + + async function mountFixture(): Promise { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [COMMANDS_DIR] })); + } + + it("reports the upstream ripgrep version", async () => { + await mountFixture(); + + const result = await kernel.exec("rg --version", {}); + expect(result.stdout).toContain("ripgrep 15.1.0"); + expect(result.stdout).not.toContain("secure-exec"); + }); + + it("searches recursively and respects .gitignore by default", async () => { + await mountFixture(); + + const result = await kernel.exec("rg needle /project", {}); + const output = lines(result.stdout); + + expect(output).toContain('/project/src/main.rs: println!("needle");'); + expect(output).toContain("/project/docs/readme.md:needle in docs"); + expect(output).not.toContain("/project/vendor/generated.rs:needle in vendor"); + expect(output).not.toContain("/project/.hidden.txt:needle hidden"); + }); + + it("supports case-insensitive search", async () => { + await mountFixture(); + + const result = await kernel.exec("rg -i needle /project/src", {}); + expect(lines(result.stdout)).toContain("/project/src/lib.rs: // Needle in a comment"); + }); + + it("supports fixed-string search", async () => { + await mountFixture(); + + const result = await kernel.exec("rg -F 'println!(\"needle\")' /project/src", {}); + expect(result.stdout.trim()).toBe('/project/src/main.rs: println!("needle");'); + }); + + it("supports glob filtering", async () => { + await mountFixture(); + + const result = await kernel.exec("rg needle /project -g '*.md'", {}); + expect(result.stdout.trim()).toBe("/project/docs/readme.md:needle in docs"); + }); + + it("can include hidden and ignored files when requested", async () => { + await mountFixture(); + + const result = await kernel.exec("rg -uu needle /project", {}); + const output = lines(result.stdout); + + expect(output).toContain("/project/.hidden.txt:needle hidden"); + expect(output).toContain("/project/vendor/generated.rs:needle in vendor"); + }); + + it("emits JSON search records", async () => { + await mountFixture(); + + const result = await kernel.exec("rg --json needle /project/docs", {}); + const records = lines(result.stdout).map((line) => JSON.parse(line)); + + expect(records.some((record) => record.type === "match")).toBe(true); + expect(records.some((record) => record.type === "summary")).toBe(true); + }); +}); diff --git a/software/ripgrep/tsconfig.json b/software/ripgrep/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/ripgrep/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/sed/agentos-package.json b/software/sed/agentos-package.json new file mode 100644 index 0000000000..7f66a6f0ed --- /dev/null +++ b/software/sed/agentos-package.json @@ -0,0 +1,12 @@ +{ + "commands": [ + "sed" + ], + "registry": { + "title": "sed", + "description": "GNU stream editor for text transformation.", + "priority": 35, + "image": "/images/registry/sed.svg", + "category": "core" + } +} diff --git a/software/sed/native/crates/cmd-sed/Cargo.toml b/software/sed/native/crates/cmd-sed/Cargo.toml new file mode 100644 index 0000000000..990de688db --- /dev/null +++ b/software/sed/native/crates/cmd-sed/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-sed" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "sed standalone binary for secure-exec VM" + +[[bin]] +name = "sed" +path = "src/main.rs" + +[dependencies] +sed = "0.1.1" diff --git a/registry/native/crates/commands/sed/src/main.rs b/software/sed/native/crates/cmd-sed/src/main.rs similarity index 100% rename from registry/native/crates/commands/sed/src/main.rs rename to software/sed/native/crates/cmd-sed/src/main.rs diff --git a/software/sed/package.json b/software/sed/package.json new file mode 100644 index 0000000000..24609bb457 --- /dev/null +++ b/software/sed/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/sed", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "GNU sed stream editor for secure-exec VMs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/sed/src/index.ts b/software/sed/src/index.ts similarity index 100% rename from registry/software/sed/src/index.ts rename to software/sed/src/index.ts diff --git a/software/sed/test/manifest.test.ts b/software/sed/test/manifest.test.ts new file mode 100644 index 0000000000..89139d33b8 --- /dev/null +++ b/software/sed/test/manifest.test.ts @@ -0,0 +1,19 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const packageDir = new URL("..", import.meta.url).pathname; + +describe("package manifest", () => { + it("declares command binaries", () => { + const manifest = JSON.parse( + readFileSync(join(packageDir, "agentos-package.json"), "utf8"), + ); + + expect(manifest.commands?.length ?? 0).toBeGreaterThan(0); + for (const command of manifest.commands) { + expect(typeof command).toBe("string"); + expect(command.length).toBeGreaterThan(0); + } + }); +}); diff --git a/software/sed/test/sed.test.ts b/software/sed/test/sed.test.ts new file mode 100644 index 0000000000..c6ccf420a2 --- /dev/null +++ b/software/sed/test/sed.test.ts @@ -0,0 +1,142 @@ +import { existsSync } from "node:fs"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + NodeFileSystem, + createKernel, + createWasmVmRuntime, + describeIf, +} from "@agentos/test-harness"; +import type { Kernel } from "@agentos/test-harness"; +import { afterEach, expect, it } from "vitest"; + +const SED_COMMAND_DIR = fileURLToPath(new URL("../bin", import.meta.url)); +const hasSedPackageBinary = existsSync(join(SED_COMMAND_DIR, "sed")); + +let tempRoot: string | undefined; + +async function writeFixture(path: string, contents: string): Promise { + if (!tempRoot) throw new Error("fixture root not initialized"); + const hostPath = join(tempRoot, path.replace(/^\/+/, "")); + await mkdir(dirname(hostPath), { recursive: true }); + await writeFile(hostPath, contents); +} + +async function createTestVFS(): Promise { + tempRoot = await mkdtemp(join(tmpdir(), "agentos-sed-")); + await writeFixture( + "/project/log.txt", + [ + "ERROR: disk full", + "INFO: retrying", + "ERROR: timeout", + "DEBUG: ignored", + ].join("\n") + "\n", + ); + await writeFixture( + "/project/records.txt", + ["alpha:build:4", "beta:test:6", "gamma:deploy:2"].join("\n") + "\n", + ); + return new NodeFileSystem({ root: tempRoot }); +} + +function lines(stdout: string): string[] { + return stdout.split("\n").filter((line) => line.length > 0); +} + +describeIf(hasSedPackageBinary, "sed command", { timeout: 10_000 }, () => { + let kernel: Kernel | undefined; + + afterEach(async () => { + await kernel?.dispose(); + kernel = undefined; + if (tempRoot) { + await rm(tempRoot, { recursive: true, force: true }); + tempRoot = undefined; + } + }); + + async function mountFixture(): Promise { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [SED_COMMAND_DIR] })); + } + + async function runSed(args: string[]) { + if (!kernel) throw new Error("kernel not mounted"); + let stdout = ""; + let stderr = ""; + const proc = kernel.spawn("sed", args, { + onStdout: (chunk) => { + stdout += Buffer.from(chunk).toString("utf8"); + }, + onStderr: (chunk) => { + stderr += Buffer.from(chunk).toString("utf8"); + }, + }); + const exitCode = await proc.wait(); + await new Promise((resolve) => setTimeout(resolve, 0)); + return { stdout, stderr, exitCode }; + } + + it("substitutes text in file operands", async () => { + await mountFixture(); + + const result = await runSed(["s/ERROR/WARN/", "/project/log.txt"]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(lines(result.stdout)).toEqual([ + "WARN: disk full", + "INFO: retrying", + "WARN: timeout", + "DEBUG: ignored", + ]); + }); + + it("prints addressed matches with -n", async () => { + await mountFixture(); + + const result = await runSed(["-n", "/ERROR/p", "/project/log.txt"]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(lines(result.stdout)).toEqual(["ERROR: disk full", "ERROR: timeout"]); + }); + + it("deletes addressed records", async () => { + await mountFixture(); + + const result = await runSed(["/DEBUG/d", "/project/log.txt"]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(lines(result.stdout)).toEqual([ + "ERROR: disk full", + "INFO: retrying", + "ERROR: timeout", + ]); + }); + + it("applies multiple expressions in order", async () => { + await mountFixture(); + + const result = await runSed([ + "-e", + "s/:/ /g", + "-e", + "s/^/row /", + "/project/records.txt", + ]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(lines(result.stdout)).toEqual([ + "row alpha build 4", + "row beta test 6", + "row gamma deploy 2", + ]); + }); + + it("fails when an input file is missing", async () => { + await mountFixture(); + + const result = await runSed(["s/a/b/", "/project/missing.txt"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("/project/missing.txt"); + }); +}); diff --git a/software/sed/tsconfig.json b/software/sed/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/sed/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/sqlite3/agentos-package.json b/software/sqlite3/agentos-package.json new file mode 100644 index 0000000000..8722328cdf --- /dev/null +++ b/software/sqlite3/agentos-package.json @@ -0,0 +1,12 @@ +{ + "commands": [ + "sqlite3" + ], + "registry": { + "title": "SQLite3", + "description": "SQLite database command-line interface.", + "priority": 75, + "image": "/images/registry/sqlite3.svg", + "category": "data" + } +} diff --git a/software/sqlite3/package.json b/software/sqlite3/package.json new file mode 100644 index 0000000000..11434344f6 --- /dev/null +++ b/software/sqlite3/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/sqlite3", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "SQLite3 CLI for secure-exec VMs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/sqlite3/src/index.ts b/software/sqlite3/src/index.ts similarity index 100% rename from registry/software/sqlite3/src/index.ts rename to software/sqlite3/src/index.ts diff --git a/software/sqlite3/test/sqlite3.test.ts b/software/sqlite3/test/sqlite3.test.ts new file mode 100644 index 0000000000..2b7144304e --- /dev/null +++ b/software/sqlite3/test/sqlite3.test.ts @@ -0,0 +1,356 @@ +/** + * Integration tests for sqlite3 C command. + * + * Verifies SQLite CLI operations via kernel.exec() with real WASM binaries: + * - In-memory databases (:memory:) + * - Stdin pipe mode for simple queries + * - SQL from command line arguments for multi-statement operations + * - Meta-commands (.dump, .schema, .tables) + * + * The command is the official SQLite shell compiled against the AgentOS C + * sysroot, not the former local sqlite3_cli.c reimplementation. + */ + +import { describe, it, expect, afterEach } from 'vitest'; +import { existsSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { createWasmVmRuntime } from '@agentos/test-harness'; +import { + C_BUILD_DIR, + COMMANDS_DIR, + createKernel, + describeIf, +} from '@agentos/test-harness'; +import type { Kernel } from '@agentos/test-harness'; + +const SQLITE3_COMMAND_DIRS = [C_BUILD_DIR, COMMANDS_DIR].filter((dir) => + existsSync(dir) +); +const hasSqlite3Binary = SQLITE3_COMMAND_DIRS.some((dir) => + existsSync(resolve(dir, 'sqlite3')) +); + +// Minimal in-memory VFS for kernel tests +class SimpleVFS { + private files = new Map(); + private dirs = new Set(['/']); + + async readFile(path: string): Promise { + const data = this.files.get(path); + if (!data) throw new Error(`ENOENT: ${path}`); + return data; + } + async readTextFile(path: string): Promise { + return new TextDecoder().decode(await this.readFile(path)); + } + async readDir(path: string): Promise { + const prefix = path === '/' ? '/' : path + '/'; + const entries: string[] = []; + for (const p of [...this.files.keys(), ...this.dirs]) { + if (p !== path && p.startsWith(prefix)) { + const rest = p.slice(prefix.length); + if (!rest.includes('/')) entries.push(rest); + } + } + return entries; + } + async readDirWithTypes(path: string) { + return (await this.readDir(path)).map(name => ({ + name, + isDirectory: this.dirs.has(path === '/' ? `/${name}` : `${path}/${name}`), + })); + } + async writeFile(path: string, content: string | Uint8Array): Promise { + const data = typeof content === 'string' ? new TextEncoder().encode(content) : content; + this.files.set(path, new Uint8Array(data)); + const parts = path.split('/').filter(Boolean); + for (let i = 1; i < parts.length; i++) { + this.dirs.add('/' + parts.slice(0, i).join('/')); + } + } + async createDir(path: string) { this.dirs.add(path); } + async mkdir(path: string, _options?: { recursive?: boolean }) { + this.dirs.add(path); + const parts = path.split('/').filter(Boolean); + for (let i = 1; i < parts.length; i++) { + this.dirs.add('/' + parts.slice(0, i).join('/')); + } + } + async exists(path: string): Promise { + return this.files.has(path) || this.dirs.has(path); + } + async stat(path: string) { + const isDir = this.dirs.has(path); + const data = this.files.get(path); + if (!isDir && !data) throw new Error(`ENOENT: ${path}`); + return { + mode: isDir ? 0o40755 : 0o100644, + size: data?.length ?? 0, + isDirectory: isDir, + isSymbolicLink: false, + atimeMs: Date.now(), + mtimeMs: Date.now(), + ctimeMs: Date.now(), + birthtimeMs: Date.now(), + ino: 0, + nlink: 1, + uid: 1000, + gid: 1000, + }; + } + async chmod(_path: string, _mode: number) {} + async lstat(path: string) { return this.stat(path); } + async removeFile(path: string) { this.files.delete(path); } + async removeDir(path: string) { this.dirs.delete(path); } + async rename(oldPath: string, newPath: string) { + const data = this.files.get(oldPath); + if (data) { + this.files.set(newPath, data); + this.files.delete(oldPath); + } + } + async pread(path: string, buffer: Uint8Array, offset: number, length: number, position: number): Promise { + const data = this.files.get(path); + if (!data) throw new Error(`ENOENT: ${path}`); + const available = Math.min(length, data.length - position); + if (available <= 0) return 0; + buffer.set(data.subarray(position, position + available), offset); + return available; + } + async pwrite(path: string, offset: number, data: Uint8Array): Promise { + const current = this.files.get(path); + if (!current) throw new Error(`ENOENT: ${path}`); + const next = new Uint8Array(Math.max(current.length, offset + data.length)); + next.set(current); + next.set(data, offset); + this.files.set(path, next); + } +} + +describeIf(hasSqlite3Binary, 'sqlite3 command', () => { + let kernel: Kernel; + + afterEach(async () => { + await kernel?.dispose(); + }); + + it('executes SQL from stdin pipe on in-memory database', async () => { + const vfs = new SimpleVFS(); + kernel = createKernel({ filesystem: vfs as any }); + await kernel.mount(createWasmVmRuntime({ commandDirs: SQLITE3_COMMAND_DIRS })); + + const result = await kernel.exec('sqlite3 :memory:', { + stdin: 'SELECT 1+1 AS result;\n', + }); + expect(result.stdout.trim()).toBe('2'); + }); + + it('executes multi-statement SQL as command argument', async () => { + const vfs = new SimpleVFS(); + kernel = createKernel({ filesystem: vfs as any }); + await kernel.mount(createWasmVmRuntime({ commandDirs: SQLITE3_COMMAND_DIRS })); + + // Multi-statement SQL passed as command argument (more reliable than stdin in WASM) + const sql = 'CREATE TABLE t(x INTEGER); INSERT INTO t VALUES(10); INSERT INTO t VALUES(20); INSERT INTO t VALUES(30); SELECT * FROM t ORDER BY x;'; + const result = await kernel.exec(`sqlite3 :memory: "${sql}"`); + expect(result.stdout.trim()).toBe('10\n20\n30'); + }); + + it('supports .tables meta-command', async () => { + const vfs = new SimpleVFS(); + await vfs.mkdir('/tmp', { recursive: true }); + kernel = createKernel({ filesystem: vfs as any }); + await kernel.mount(createWasmVmRuntime({ commandDirs: SQLITE3_COMMAND_DIRS })); + + const createResult = await kernel.exec( + 'sqlite3 /tmp/meta.db "CREATE TABLE alpha(x); CREATE TABLE beta(y);"' + ); + expect(createResult.stderr).toBe(''); + + const result = await kernel.exec('sqlite3 /tmp/meta.db ".tables"'); + expect(result.stdout).toContain('alpha'); + expect(result.stdout).toContain('beta'); + }); + + it('supports .schema meta-command', async () => { + const vfs = new SimpleVFS(); + await vfs.mkdir('/tmp', { recursive: true }); + kernel = createKernel({ filesystem: vfs as any }); + await kernel.mount(createWasmVmRuntime({ commandDirs: SQLITE3_COMMAND_DIRS })); + + const createResult = await kernel.exec( + 'sqlite3 /tmp/schema.db "CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT NOT NULL);"' + ); + expect(createResult.stderr).toBe(''); + + const result = await kernel.exec('sqlite3 /tmp/schema.db ".schema users"'); + expect(result.stdout).toContain('CREATE TABLE users'); + expect(result.stdout).toContain('id INTEGER PRIMARY KEY'); + }); + + it('supports .dump meta-command', async () => { + const vfs = new SimpleVFS(); + await vfs.mkdir('/tmp', { recursive: true }); + kernel = createKernel({ filesystem: vfs as any }); + await kernel.mount(createWasmVmRuntime({ commandDirs: SQLITE3_COMMAND_DIRS })); + + const createResult = await kernel.exec( + `sqlite3 /tmp/dump.db "CREATE TABLE t(x INTEGER, y TEXT); INSERT INTO t VALUES(1,'hello');"` + ); + expect(createResult.stderr).toBe(''); + + const result = await kernel.exec('sqlite3 /tmp/dump.db ".dump"'); + const output = result.stdout; + expect(output).toContain('BEGIN TRANSACTION'); + expect(output).toContain('CREATE TABLE t'); + expect(output).toContain("INSERT INTO t VALUES(1,'hello')"); + expect(output).toContain('COMMIT'); + }); + + it('handles SELECT with multiple columns', async () => { + const vfs = new SimpleVFS(); + kernel = createKernel({ filesystem: vfs as any }); + await kernel.mount(createWasmVmRuntime({ commandDirs: SQLITE3_COMMAND_DIRS })); + + const result = await kernel.exec('sqlite3 :memory:', { + stdin: "SELECT 'hello' AS greeting, 42 AS number, 3.14 AS pi;\n", + }); + expect(result.stdout.trim()).toBe('hello|42|3.14'); + }); + + it('handles NULL values in output', async () => { + const vfs = new SimpleVFS(); + kernel = createKernel({ filesystem: vfs as any }); + await kernel.mount(createWasmVmRuntime({ commandDirs: SQLITE3_COMMAND_DIRS })); + + const result = await kernel.exec('sqlite3 :memory:', { + stdin: 'SELECT NULL;\n', + }); + // SQLite CLI outputs empty string for NULL + expect(result.stdout.trim()).toBe(''); + }); + + it('reports SQL errors on stderr', async () => { + const vfs = new SimpleVFS(); + kernel = createKernel({ filesystem: vfs as any }); + await kernel.mount(createWasmVmRuntime({ commandDirs: SQLITE3_COMMAND_DIRS })); + + const result = await kernel.exec(`sqlite3 :memory: "SELECT * FROM nonexistent_table;"`); + expect(result.stderr).toContain('no such table'); + }); + + it('defaults to :memory: when no database specified', async () => { + const vfs = new SimpleVFS(); + kernel = createKernel({ filesystem: vfs as any }); + await kernel.mount(createWasmVmRuntime({ commandDirs: SQLITE3_COMMAND_DIRS })); + + const result = await kernel.exec('sqlite3', { + stdin: 'SELECT 99;\n', + }); + expect(result.stdout.trim()).toBe('99'); + }); + + it('CREATE TABLE, INSERT, SELECT roundtrip via piped SQL', async () => { + const vfs = new SimpleVFS(); + kernel = createKernel({ filesystem: vfs as any }); + await kernel.mount(createWasmVmRuntime({ commandDirs: SQLITE3_COMMAND_DIRS })); + + // Multi-statement SQL via command arg (stdin multi-statement has fgetc buffering issues in WASM) + const sql = "CREATE TABLE items(id INTEGER PRIMARY KEY, name TEXT); INSERT INTO items VALUES(1,'apple'); INSERT INTO items VALUES(2,'banana'); SELECT id, name FROM items ORDER BY id;"; + const result = await kernel.exec(`sqlite3 :memory: "${sql}"`); + expect(result.stdout.trim()).toBe('1|apple\n2|banana'); + }); + + it('.tables meta-command lists created tables', async () => { + const vfs = new SimpleVFS(); + await vfs.mkdir('/tmp', { recursive: true }); + kernel = createKernel({ filesystem: vfs as any }); + await kernel.mount(createWasmVmRuntime({ commandDirs: SQLITE3_COMMAND_DIRS })); + + const createResult = await kernel.exec( + 'sqlite3 /tmp/tables.db "CREATE TABLE alpha(x); CREATE TABLE beta(y);"' + ); + expect(createResult.stderr).toBe(''); + + const result = await kernel.exec('sqlite3 /tmp/tables.db ".tables"'); + expect(result.stdout).toContain('alpha'); + expect(result.stdout).toContain('beta'); + }); + + it('.schema meta-command shows CREATE TABLE statements', async () => { + const vfs = new SimpleVFS(); + await vfs.mkdir('/tmp', { recursive: true }); + kernel = createKernel({ filesystem: vfs as any }); + await kernel.mount(createWasmVmRuntime({ commandDirs: SQLITE3_COMMAND_DIRS })); + + const createResult = await kernel.exec( + 'sqlite3 /tmp/schema-meta.db "CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT NOT NULL);"' + ); + expect(createResult.stderr).toBe(''); + + const result = await kernel.exec('sqlite3 /tmp/schema-meta.db ".schema users"'); + expect(result.stdout).toContain('CREATE TABLE users'); + expect(result.stdout).toContain('id INTEGER PRIMARY KEY'); + }); + + it('.dump meta-command outputs INSERT statements for data', async () => { + const vfs = new SimpleVFS(); + await vfs.mkdir('/tmp', { recursive: true }); + kernel = createKernel({ filesystem: vfs as any }); + await kernel.mount(createWasmVmRuntime({ commandDirs: SQLITE3_COMMAND_DIRS })); + + const createResult = await kernel.exec( + `sqlite3 /tmp/dump-meta.db "CREATE TABLE t(x INTEGER, y TEXT); INSERT INTO t VALUES(1,'hello'); INSERT INTO t VALUES(2,'world');"` + ); + expect(createResult.stderr).toBe(''); + + const result = await kernel.exec('sqlite3 /tmp/dump-meta.db ".dump"'); + const output = result.stdout; + expect(output).toContain('BEGIN TRANSACTION'); + expect(output).toContain('CREATE TABLE t'); + expect(output).toContain("INSERT INTO t VALUES(1,'hello')"); + expect(output).toContain("INSERT INTO t VALUES(2,'world')"); + expect(output).toContain('COMMIT'); + }); + + it('file-based DB persists data across separate exec calls', async () => { + const vfs = new SimpleVFS(); + await vfs.mkdir('/tmp', { recursive: true }); + kernel = createKernel({ filesystem: vfs as any }); + await kernel.mount(createWasmVmRuntime({ commandDirs: SQLITE3_COMMAND_DIRS })); + + // File-backed SQLite must persist through the kernel VFS across exec calls. + const createSql = "CREATE TABLE t(x INTEGER); INSERT INTO t VALUES(42); INSERT INTO t VALUES(99);"; + const createResult = await kernel.exec(`sqlite3 /tmp/test.db "${createSql}"`); + expect(createResult.stderr).toBe(''); + + // Verify file was created in VFS + const dbData = await vfs.readFile('/tmp/test.db'); + expect(dbData.length).toBeGreaterThan(0); + + // Second exec: reopen and query persisted data. + const result = await kernel.exec('sqlite3 /tmp/test.db "SELECT * FROM t ORDER BY x;"'); + expect(result.stdout.trim()).toBe('42\n99'); + }); + + it('multi-statement input separated by semicolons', async () => { + const vfs = new SimpleVFS(); + kernel = createKernel({ filesystem: vfs as any }); + await kernel.mount(createWasmVmRuntime({ commandDirs: SQLITE3_COMMAND_DIRS })); + + // Multi-statement SQL via command arg (semicolons separate statements) + const sql = "CREATE TABLE nums(v); INSERT INTO nums VALUES(10); INSERT INTO nums VALUES(20); SELECT v FROM nums ORDER BY v;"; + const result = await kernel.exec(`sqlite3 :memory: "${sql}"`); + expect(result.stdout.trim()).toBe('10\n20'); + }); + + it('SQL syntax error produces error on stderr with non-zero exit', async () => { + const vfs = new SimpleVFS(); + kernel = createKernel({ filesystem: vfs as any }); + await kernel.mount(createWasmVmRuntime({ commandDirs: SQLITE3_COMMAND_DIRS })); + + // Syntax error via command arg (reliable error output) + const result = await kernel.exec('sqlite3 :memory: "SELEC INVALID SYNTAX;"'); + expect(result.stderr).toContain('Error'); + }); +}); diff --git a/software/sqlite3/tsconfig.json b/software/sqlite3/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/sqlite3/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/ssh/agentos-package.json b/software/ssh/agentos-package.json new file mode 100644 index 0000000000..d3da5876b0 --- /dev/null +++ b/software/ssh/agentos-package.json @@ -0,0 +1,14 @@ +{ + "commands": [ + "ssh", + "ssh-keysign", + "ssh-sk-helper" + ], + "registry": { + "title": "ssh", + "description": "OpenSSH client for remote commands and git-over-ssh, with enforced known_hosts verification and RSA, ECDSA, Ed25519, DH, ECDH, ChaCha20, and AES crypto.", + "priority": 55, + "image": "/images/registry/ssh.svg", + "category": "networking" + } +} diff --git a/software/ssh/package.json b/software/ssh/package.json new file mode 100644 index 0000000000..bad5ce0988 --- /dev/null +++ b/software/ssh/package.json @@ -0,0 +1,35 @@ +{ + "name": "@agentos-software/ssh", + "version": "0.0.1", + "type": "module", + "license": "Apache-2.0", + "description": "OpenSSH client for AgentOS VMs (batch/key-based; also powers git-over-ssh)", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "@types/ssh2": "^1.15.1", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "ssh2": "^1.16.0", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/tar/src/index.ts b/software/ssh/src/index.ts similarity index 100% rename from registry/software/tar/src/index.ts rename to software/ssh/src/index.ts diff --git a/software/ssh/test/ssh.test.ts b/software/ssh/test/ssh.test.ts new file mode 100644 index 0000000000..5743c71356 --- /dev/null +++ b/software/ssh/test/ssh.test.ts @@ -0,0 +1,716 @@ +/** + * Integration tests for the real OpenSSH ssh client (10.4p1, linked with a + * hermetic OpenSSL libcrypto build for standard software algorithm coverage). + * + * Mirrors the git HTTPS suite's loopback-server harness: an in-test SSH + * server (the pure-JS `ssh2` package) listens on a host loopback port that + * the kernel exempts, and the WASM ssh client connects out through the + * kernel's host_net path. Covers batch/key-based exec, host-key + * verification (known_hosts, StrictHostKeyChecking=accept-new), publickey + * auth failure, and git-over-ssh (clone + push against host + * git-upload-pack/git-receive-pack). + */ + +import { describe, it, expect, afterEach, beforeAll, afterAll, vi } from 'vitest'; +import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { resolve, join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { spawn, spawnSync } from 'node:child_process'; +import { Server as SshServer, utils as sshUtils } from 'ssh2'; +import type { Connection } from 'ssh2'; +import { createWasmVmRuntime } from '@agentos/test-harness'; +import { + allowAll, + C_BUILD_DIR, + COMMANDS_DIR, + createInMemoryFileSystem, + createKernel, + describeIf, + hasWasmBinaries, + hasCWasmBinaries, +} from '@agentos/test-harness'; +import type { Kernel } from '@agentos/test-harness'; + +vi.setConfig({ testTimeout: 60_000, hookTimeout: 60_000 }); + +const hasSsh = hasWasmBinaries && existsSync(resolve(COMMANDS_DIR, 'ssh')); +const hasGit = hasWasmBinaries && existsSync(resolve(COMMANDS_DIR, 'git')); +const hasHostGit = spawnSync('git', ['--version'], { stdio: 'ignore' }).status === 0; +const hasProxyHelper = hasCWasmBinaries('ssh_proxy_helper'); + +const SSH_USER = 'agentos'; + +interface TestKeys { + hostKey: ReturnType; + clientKey: ReturnType; + /** A second client keypair the server does NOT authorize. */ + wrongClientKey: ReturnType; + /** A second host key used to simulate a changed/unknown server identity. */ + otherHostKey: ReturnType; +} + +function generateKeys(): TestKeys { + return { + hostKey: sshUtils.generateKeyPairSync('ed25519'), + clientKey: sshUtils.generateKeyPairSync('ed25519'), + wrongClientKey: sshUtils.generateKeyPairSync('ed25519'), + otherHostKey: sshUtils.generateKeyPairSync('ed25519'), + }; +} + +/** Standard ssh2 publickey-auth handler restricted to one authorized key. */ +function installAuthHandler(client: Connection, authorizedPublicKey: string) { + const allowed = sshUtils.parseKey(authorizedPublicKey); + if (allowed instanceof Error) throw allowed; + client.on('authentication', (ctx) => { + if (ctx.method !== 'publickey') { + return ctx.reject(['publickey']); + } + const matches = + ctx.key.algo === allowed.type && + ctx.key.data.equals(allowed.getPublicSSH()); + if (!matches) { + return ctx.reject(['publickey']); + } + if (ctx.signature && ctx.blob) { + if (allowed.verify(ctx.blob, ctx.signature, ctx.hashAlgo) === true) { + return ctx.accept(); + } + return ctx.reject(['publickey']); + } + // pk-check phase (no signature yet): tell the client the key is OK. + return ctx.accept(); + }); +} + +/** exec handler: `echo hello`-style canned command execution. */ +function installEchoExecHandler(client: Connection) { + client.on('ready', () => { + client.on('session', (acceptSession) => { + const session = acceptSession(); + session.on('exec', (acceptExec, _reject, info) => { + const stream = acceptExec(); + if (info.command === 'echo hello') { + stream.write('hello\n'); + stream.exit(0); + } else { + stream.stderr.write(`unknown test command: ${info.command}\n`); + stream.exit(127); + } + stream.end(); + }); + }); + }); +} + +/** + * exec handler bridging `git-upload-pack '/x.git'` / `git-receive-pack ...` + * to the host git against a bare repo root — an in-test stand-in for a real + * SSH git host (what git-shell does on a server). + */ +function installGitExecHandler(client: Connection, repoRoot: string) { + client.on('ready', () => { + client.on('session', (acceptSession) => { + const session = acceptSession(); + session.on('exec', (acceptExec, reject, info) => { + const match = /^(git-upload-pack|git-receive-pack|git-upload-archive) '(.*)'$/.exec( + info.command, + ); + if (!match) { + const stream = acceptExec(); + stream.stderr.write(`unsupported command: ${info.command}\n`); + stream.exit(128); + stream.end(); + return; + } + const [, service, requestedPath] = match; + const repoPath = join(repoRoot, requestedPath.replace(/^\/+/, '')); + const stream = acceptExec(); + const child = spawn('git', [service.replace(/^git-/, ''), repoPath]); + stream.pipe(child.stdin); + child.stdout.pipe(stream, { end: false }); + child.stderr.pipe(stream.stderr, { end: false }); + child.on('close', (code) => { + stream.exit(code ?? 1); + stream.end(); + }); + child.on('error', () => { + stream.exit(127); + stream.end(); + }); + }); + }); + }); +} + +async function listen(server: SshServer): Promise { + await new Promise((r) => server.listen(0, '127.0.0.1', r)); + return (server.address() as import('node:net').AddressInfo).port; +} + +async function createSshKernel(loopbackExemptPorts: number[]) { + const vfs = createInMemoryFileSystem(); + await (vfs as any).chmod('/', 0o1777); + await vfs.mkdir('/tmp', { recursive: true }); + await (vfs as any).chmod('/tmp', 0o1777); + const kernel = createKernel({ + filesystem: vfs, + permissions: allowAll, + loopbackExemptPorts, + syncFilesystemOnDispose: false, + }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] })); + return { kernel, vfs, dispose: () => kernel.dispose() }; +} + +async function run( + kernel: Kernel, + cmd: string, +): Promise<{ stdout: string; stderr: string; exitCode: number }> { + const r = await kernel.exec(cmd); + if (r.exitCode !== 0) { + throw new Error( + `Command failed (exit ${r.exitCode}): ${cmd}\nstdout: ${r.stdout}\nstderr: ${r.stderr}`, + ); + } + return r; +} + +/** + * Resolve the guest user's home directory (ssh resolves `~` through + * getpwuid(getuid())->pw_dir, which the runtime keeps aligned with $HOME). + */ +async function guestHome(kernel: Kernel): Promise { + const r = await run(kernel, "sh -c 'echo $HOME'"); + const home = r.stdout.trim(); + expect(home).toMatch(/^\//); + return home; +} + +/** Seed ~/.ssh with an identity and (optionally) a known_hosts line. */ +async function seedSshDir( + kernel: Kernel, + vfs: any, + home: string, + privateKey: string, + knownHostsLine?: string, +): Promise { + const sshDir = `${home}/.ssh`; + await vfs.mkdir(sshDir, { recursive: true }); + await vfs.chmod(sshDir, 0o700); + await kernel.writeFile(`${sshDir}/id_ed25519`, `${privateKey}\n`); + await vfs.chmod(`${sshDir}/id_ed25519`, 0o600); + if (knownHostsLine !== undefined) { + await kernel.writeFile(`${sshDir}/known_hosts`, `${knownHostsLine}\n`); + await vfs.chmod(`${sshDir}/known_hosts`, 0o600); + } + return sshDir; +} + +function knownHostsEntry(port: number, hostPublicKey: string): string { + // `[host]:port` hashing syntax from sshd(8) AUTHORIZED_KEYS/known_hosts + // format; non-default ports always use the bracketed form. + return `[127.0.0.1]:${port} ${hostPublicKey}`; +} + +// TODO(P6): requires the ssh WASM artifact, intentionally excluded from the +// fast software-build gate (same as git). +describeIf(hasSsh, 'ssh command', () => { + let kernel: Kernel; + let vfs: any; + let dispose: (() => Promise) | undefined; + + afterEach(async () => { + await dispose?.(); + dispose = undefined; + }); + + it('ssh -V reports the real OpenSSH and OpenSSL versions', async () => { + ({ kernel, vfs, dispose } = await createSshKernel([])); + const r = await kernel.exec('ssh -V'); + expect(r.exitCode).toBe(0); + const banner = `${r.stdout}${r.stderr}`; + expect(banner).toMatch(/OpenSSH_10\.4/); + expect(banner).toMatch(/OpenSSL 3\.5\.7/); + }); + + it.each([ + ['key', ['ssh-ed25519', 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'sk-ssh-ed25519@openssh.com', 'sk-ecdsa-sha2-nistp256@openssh.com']], + ['key-sig', ['ssh-ed25519', 'ssh-rsa', 'rsa-sha2-256', 'rsa-sha2-512', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'sk-ssh-ed25519@openssh.com', 'sk-ecdsa-sha2-nistp256@openssh.com']], + ['cipher', ['chacha20-poly1305@openssh.com', 'aes128-gcm@openssh.com', 'aes256-gcm@openssh.com', 'aes128-cbc', 'aes256-cbc', '3des-cbc']], + ])('ssh -Q %s includes the standard software crypto families', async (query, expected) => { + ({ kernel, vfs, dispose } = await createSshKernel([])); + const r = await kernel.exec(`ssh -Q ${query}`); + expect(r.exitCode, `stdout:\n${r.stdout}\nstderr:\n${r.stderr}`).toBe(0); + const algorithms = new Set(r.stdout.trim().split(/\s+/)); + for (const algorithm of expected) { + expect(algorithms.has(algorithm), `missing ${query} algorithm ${algorithm}`).toBe(true); + } + }); + + it('Match exec uses the spawned command exit status like native OpenSSH', async () => { + ({ kernel, vfs, dispose } = await createSshKernel([])); + await kernel.writeFile( + '/tmp/ssh-match.conf', + [ + 'Match originalhost parity exec "true"', + ' HostName matched.invalid', + 'Host parity', + ' HostName unmatched.invalid', + '', + ].join('\n'), + ); + const r = await kernel.exec('ssh -G -F /tmp/ssh-match.conf parity'); + expect(r.exitCode, r.stderr).toBe(0); + expect(r.stdout).toMatch(/^hostname matched\.invalid$/m); + }); + + it('ships the client helpers and reaches their real wire-protocol parsers', async () => { + ({ kernel, vfs, dispose } = await createSshKernel([])); + await vfs.mkdir('/etc/ssh', { recursive: true }); + const hostKey = sshUtils.generateKeyPairSync('ed25519'); + await kernel.writeFile('/etc/ssh/ssh_host_ed25519_key', `${hostKey.private}\n`); + await vfs.chmod('/etc/ssh/ssh_host_ed25519_key', 0o600); + await kernel.writeFile('/etc/ssh/ssh_config', 'EnableSSHKeysign yes\n'); + + const keysign = await kernel.exec('ssh-keysign'); + expect(keysign.exitCode).not.toBe(127); + expect(keysign.stderr).toMatch(/ssh_msg_recv failed|incomplete message/i); + expect(keysign.stderr).not.toMatch(/not found|ENOENT/i); + + const sk = await kernel.exec('ssh-sk-helper'); + expect(sk.exitCode).not.toBe(127); + // Like native OpenSSH, ssh-sk-helper sends its pre-protocol failure to + // syslog by default and may therefore have empty stderr on EOF. + expect(sk.stderr).not.toMatch(/not found|ENOENT/i); + + const framed = await kernel.exec('ssh_sk_helper_contract'); + expect(framed.exitCode, framed.stderr).toBe(0); + expect(framed.stdout).toBe('ssh_sk_helper_framed_provider_error=yes\n'); + }); + + it('uses ssh-keysign for a signed hostbased authentication request', async () => { + const serverHostKey = sshUtils.generateKeyPairSync('ed25519'); + const clientHostKey = sshUtils.generateKeyPairSync('ed25519'); + const parsedClientHostKey = sshUtils.parseKey(clientHostKey.public); + if (parsedClientHostKey instanceof Error) throw parsedClientHostKey; + let sawSignedHostbased = false; + const server = new SshServer({ hostKeys: [serverHostKey.private] }, (client) => { + client.on('authentication', (context) => { + const ctx = context as any; + if (ctx.method !== 'hostbased') return ctx.reject(['hostbased']); + sawSignedHostbased = Boolean( + ctx.signature && + ctx.blob && + parsedClientHostKey.verify(ctx.blob, ctx.signature, ctx.hashAlgo), + ); + return sawSignedHostbased ? ctx.accept() : ctx.reject(['hostbased']); + }); + installEchoExecHandler(client); + }); + const port = await listen(server); + try { + ({ kernel, vfs, dispose } = await createSshKernel([port])); + const home = await guestHome(kernel); + await seedSshDir( + kernel, + vfs, + home, + clientHostKey.private, + knownHostsEntry(port, serverHostKey.public), + ); + await vfs.mkdir('/etc/ssh', { recursive: true }); + await kernel.writeFile('/etc/ssh/ssh_host_ed25519_key', `${clientHostKey.private}\n`); + await vfs.chmod('/etc/ssh/ssh_host_ed25519_key', 0o600); + await kernel.writeFile('/etc/ssh/ssh_host_ed25519_key.pub', `${clientHostKey.public}\n`); + await kernel.writeFile('/etc/ssh/ssh_config', 'Host *\n EnableSSHKeysign yes\n'); + + const result = await kernel.exec( + `ssh -T -o BatchMode=yes -o HostbasedAuthentication=yes ` + + `-o PreferredAuthentications=hostbased -o EnableSSHKeysign=yes ` + + `-o HostbasedKeyTypes=ssh-ed25519 -p ${port} ${SSH_USER}@127.0.0.1 echo hello`, + ); + expect(result.exitCode, result.stderr).toBe(0); + expect(result.stdout).toBe('hello\n'); + expect(sawSignedHostbased).toBe(true); + } finally { + await new Promise((resolveClose) => server.close(() => resolveClose())); + } + }); + + describe('against an in-test ssh2 server', () => { + let keys: TestKeys; + let server: SshServer; + let port: number; + let sessionRequests: number; + + beforeAll(async () => { + keys = generateKeys(); + sessionRequests = 0; + server = new SshServer({ hostKeys: [keys.hostKey.private] }, (client) => { + installAuthHandler(client, keys.clientKey.public); + client.on('ready', () => { + client.on('session', () => { + sessionRequests++; + }); + }); + installEchoExecHandler(client); + }); + port = await listen(server); + }); + + afterAll(async () => { + await new Promise((r) => server.close(() => r())); + }); + + const sshCmd = (extra: string) => + `ssh -T -o BatchMode=yes ${extra} -p ${port} ${SSH_USER}@127.0.0.1 echo hello`; + + it('runs a remote command with ed25519 publickey auth and known_hosts', async () => { + ({ kernel, vfs, dispose } = await createSshKernel([port])); + const home = await guestHome(kernel); + await seedSshDir( + kernel, + vfs, + home, + keys.clientKey.private, + knownHostsEntry(port, keys.hostKey.public), + ); + + const r = await kernel.exec(sshCmd('')); + expect(r.stderr).not.toMatch(/setsockopt/i); + expect(r.stdout).toBe('hello\n'); + expect(r.exitCode, `stdout:\n${r.stdout}\nstderr:\n${r.stderr}`).toBe(0); + }); + + it('runs LocalCommand through the same POSIX spawn path', async () => { + ({ kernel, vfs, dispose } = await createSshKernel([port])); + const home = await guestHome(kernel); + await seedSshDir( + kernel, + vfs, + home, + keys.clientKey.private, + knownHostsEntry(port, keys.hostKey.public), + ); + + const r = await kernel.exec( + sshCmd("-o PermitLocalCommand=yes -o LocalCommand='echo local-command'"), + ); + expect(r.exitCode, r.stderr).toBe(0); + expect(r.stdout).toContain('local-command'); + expect(r.stdout).toContain('hello\n'); + }); + + it('uses KnownHostsCommand output for host-key verification', async () => { + ({ kernel, vfs, dispose } = await createSshKernel([port])); + const home = await guestHome(kernel); + await seedSshDir(kernel, vfs, home, keys.clientKey.private); + await kernel.writeFile( + '/tmp/known-hosts-command', + `#!/bin/sh\necho '${knownHostsEntry(port, keys.hostKey.public)}'\n`, + ); + await vfs.chmod('/tmp/known-hosts-command', 0o700); + + const r = await kernel.exec( + sshCmd('-o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null -o KnownHostsCommand=/tmp/known-hosts-command'), + ); + expect(r.exitCode, r.stderr).toBe(0); + expect(r.stdout).toBe('hello\n'); + }); + + it('fails explicitly when ssh -f would need a live authenticated snapshot', async () => { + ({ kernel, vfs, dispose } = await createSshKernel([port])); + const home = await guestHome(kernel); + await seedSshDir( + kernel, + vfs, + home, + keys.clientKey.private, + knownHostsEntry(port, keys.hostKey.public), + ); + + const r = await kernel.exec( + `ssh -f -T -o BatchMode=yes -p ${port} ${SSH_USER}@127.0.0.1 echo hello`, + ); + expect(r.exitCode).not.toBe(0); + expect(r.stderr).toMatch(/cannot snapshot|live authenticated process state/i); + }); + + it('rejects multiplexed ssh -f before asking the master to open a session', async () => { + ({ kernel, vfs, dispose } = await createSshKernel([port])); + const home = await guestHome(kernel); + await seedSshDir( + kernel, + vfs, + home, + keys.clientKey.private, + knownHostsEntry(port, keys.hostKey.public), + ); + + const controlPath = '/tmp/agentos-ssh-control'; + const target = `-p ${port} ${SSH_USER}@127.0.0.1`; + let masterResult: Awaited> | undefined; + const master = kernel.exec( + `ssh -M -N -n -T -o BatchMode=yes -o ControlMaster=yes ` + + `-o ControlPersist=no -S ${controlPath} ${target}`, + ); + void master.then((result) => { + masterResult = result; + }); + + try { + const deadline = Date.now() + 10_000; + for (;;) { + try { + await vfs.stat(controlPath); + break; + } catch { + if (masterResult !== undefined) { + throw new Error( + `SSH control master exited early (${masterResult.exitCode}): ${masterResult.stderr}`, + ); + } + if (Date.now() >= deadline) { + throw new Error('timed out waiting for the SSH control socket'); + } + await new Promise((resolveWait) => setTimeout(resolveWait, 10)); + } + } + + const before = sessionRequests; + const r = await kernel.exec( + `ssh -f -T -o BatchMode=yes -S ${controlPath} ${target} echo hello`, + ); + expect(r.exitCode).not.toBe(0); + expect(r.stderr).toMatch(/cannot snapshot|live authenticated process state/i); + expect(sessionRequests).toBe(before); + + const check = await kernel.exec(`ssh -S ${controlPath} -O check ${target}`); + expect(check.exitCode, check.stderr).toBe(0); + } finally { + await kernel.exec(`ssh -S ${controlPath} -O exit ${target}`); + await master; + } + }); + + it.runIf(hasProxyHelper)('ProxyCommand preserves full-duplex SSH transport through a spawned helper', async () => { + ({ kernel, vfs, dispose } = await createSshKernel([port])); + const home = await guestHome(kernel); + await seedSshDir( + kernel, + vfs, + home, + keys.clientKey.private, + knownHostsEntry(port, keys.hostKey.public), + ); + + const r = await kernel.exec( + sshCmd("-o ProxyCommand='ssh_proxy_helper stdio %h %p'"), + ); + expect(r.exitCode, `stdout:\n${r.stdout}\nstderr:\n${r.stderr}`).toBe(0); + expect(r.stdout).toBe('hello\n'); + }); + + it.runIf(hasProxyHelper)('ProxyUseFdpass transfers a connected TCP socket from the spawned helper', async () => { + ({ kernel, vfs, dispose } = await createSshKernel([port])); + const home = await guestHome(kernel); + await seedSshDir( + kernel, + vfs, + home, + keys.clientKey.private, + knownHostsEntry(port, keys.hostKey.public), + ); + + const r = await kernel.exec( + sshCmd("-o ProxyUseFdpass=yes -o ProxyCommand='ssh_proxy_helper fdpass %h %p'"), + ); + expect(r.exitCode, `stdout:\n${r.stdout}\nstderr:\n${r.stderr}`).toBe(0); + expect(r.stdout).toBe('hello\n'); + }); + + it('propagates the remote exit status', async () => { + ({ kernel, vfs, dispose } = await createSshKernel([port])); + const home = await guestHome(kernel); + await seedSshDir( + kernel, + vfs, + home, + keys.clientKey.private, + knownHostsEntry(port, keys.hostKey.public), + ); + + const r = await kernel.exec( + `ssh -T -o BatchMode=yes -p ${port} ${SSH_USER}@127.0.0.1 false-command`, + ); + expect(r.exitCode).toBe(127); + expect(r.stderr).toContain('unknown test command'); + }); + + it('fails publickey auth with an unauthorized client key', async () => { + ({ kernel, vfs, dispose } = await createSshKernel([port])); + const home = await guestHome(kernel); + await seedSshDir( + kernel, + vfs, + home, + keys.wrongClientKey.private, + knownHostsEntry(port, keys.hostKey.public), + ); + + const r = await kernel.exec(sshCmd('')); + expect(r.exitCode).not.toBe(0); + expect(r.stderr).toMatch(/Permission denied \(publickey\)/i); + expect(r.stdout).not.toContain('hello'); + }); + + it('fails host key verification when known_hosts pins a different key', async () => { + ({ kernel, vfs, dispose } = await createSshKernel([port])); + const home = await guestHome(kernel); + await seedSshDir( + kernel, + vfs, + home, + keys.clientKey.private, + knownHostsEntry(port, keys.otherHostKey.public), + ); + + const r = await kernel.exec(sshCmd('')); + expect(r.exitCode).not.toBe(0); + expect(r.stderr).toMatch( + /REMOTE HOST IDENTIFICATION HAS CHANGED|Host key verification failed/i, + ); + expect(r.stdout).not.toContain('hello'); + }); + + it('fails closed in BatchMode when the host key is unknown', async () => { + ({ kernel, vfs, dispose } = await createSshKernel([port])); + const home = await guestHome(kernel); + await seedSshDir(kernel, vfs, home, keys.clientKey.private); + + const r = await kernel.exec(sshCmd('')); + expect(r.exitCode).not.toBe(0); + expect(r.stderr).toMatch(/Host key verification failed/i); + }); + + it('StrictHostKeyChecking=accept-new succeeds and records the host key', async () => { + ({ kernel, vfs, dispose } = await createSshKernel([port])); + const home = await guestHome(kernel); + const sshDir = await seedSshDir(kernel, vfs, home, keys.clientKey.private); + + const r = await kernel.exec(sshCmd('-o StrictHostKeyChecking=accept-new')); + expect(r.stdout).toBe('hello\n'); + expect(r.exitCode).toBe(0); + expect(r.stderr).toMatch(/Permanently added/i); + + const knownHosts = new TextDecoder().decode( + await kernel.readFile(`${sshDir}/known_hosts`), + ); + const hostKeyBlob = keys.hostKey.public.split(/\s+/)[1]; + expect(knownHosts).toContain(`[127.0.0.1]:${port}`); + expect(knownHosts).toContain(hostKeyBlob); + }); + }); + + // git-over-ssh: the WASM git execs the WASM ssh from PATH (git connect.c), + // which tunnels git-upload-pack / git-receive-pack to the host-side bare + // repo behind the ssh2 server. + // + // This also regresses mixed polling in the runtime: ssh polls a dup'd stdin + // pipe alongside its host-net socket while Git waits for the remote helper. + describeIf(hasGit && hasHostGit, 'git-over-ssh clone/push', () => { + let keys: TestKeys; + let server: SshServer; + let port: number; + let repoRoot: string; + + const gitConfig = [ + '-c safe.directory=*', + '-c init.defaultBranch=main', + '-c user.name=agentos', + '-c user.email=agentos@example.invalid', + ].join(' '); + const git = (args: string) => `git ${gitConfig} ${args}`; + + function runHostGit(args: string[], cwd?: string) { + const result = spawnSync('git', args, { cwd, encoding: 'utf8' }); + if (result.status !== 0) { + throw new Error( + `host git failed: git ${args.join(' ')}\nstdout: ${result.stdout}\nstderr: ${result.stderr}`, + ); + } + } + + beforeAll(async () => { + keys = generateKeys(); + repoRoot = mkdtempSync(join(tmpdir(), 'agentos-git-ssh-')); + const worktree = join(repoRoot, 'worktree'); + const origin = join(repoRoot, 'origin.git'); + + runHostGit(['-c', 'init.defaultBranch=main', 'init', worktree]); + writeFileSync(join(worktree, 'README.md'), 'remote ssh clone\n'); + runHostGit(['-C', worktree, 'add', 'README.md']); + runHostGit([ + '-C', worktree, + '-c', 'user.name=agentos', '-c', 'user.email=agentos@example.invalid', + 'commit', '-m', 'seed', + ]); + runHostGit(['clone', '--bare', worktree, origin]); + + server = new SshServer({ hostKeys: [keys.hostKey.private] }, (client) => { + installAuthHandler(client, keys.clientKey.public); + installGitExecHandler(client, repoRoot); + }); + port = await listen(server); + }); + + afterAll(async () => { + if (server) await new Promise((r) => server.close(() => r())); + rmSync(repoRoot, { recursive: true, force: true }); + }); + + it('clones and pushes over ssh://', async () => { + ({ kernel, vfs, dispose } = await createSshKernel([port])); + const home = await guestHome(kernel); + await seedSshDir( + kernel, + vfs, + home, + keys.clientKey.private, + knownHostsEntry(port, keys.hostKey.public), + ); + + const url = `ssh://${SSH_USER}@127.0.0.1:${port}/origin.git`; + + const cloned = await kernel.exec(git(`clone ${url} /tmp/clone`)); + expect(cloned.exitCode, cloned.stderr).toBe(0); + const readme = new TextDecoder().decode( + await kernel.readFile('/tmp/clone/README.md'), + ); + expect(readme).toBe('remote ssh clone\n'); + const head = new TextDecoder().decode( + await kernel.readFile('/tmp/clone/.git/HEAD'), + ); + expect(head.trim()).toBe('ref: refs/heads/main'); + + // Push a new commit back over the same transport. + await kernel.writeFile('/tmp/clone/pushed.txt', 'pushed over ssh\n'); + await run(kernel, git('-C /tmp/clone add pushed.txt')); + await run(kernel, git("-C /tmp/clone commit -m 'push over ssh'")); + const pushed = await kernel.exec( + git('-C /tmp/clone push origin HEAD:refs/heads/ssh-push'), + ); + expect(pushed.exitCode, pushed.stderr).toBe(0); + + // Verify the ref really landed in the host-side bare repo. + const originRef = spawnSync( + 'git', + ['-C', join(repoRoot, 'origin.git'), 'rev-parse', '--verify', 'refs/heads/ssh-push'], + { encoding: 'utf8' }, + ); + expect(originRef.status).toBe(0); + expect(originRef.stdout.trim()).toMatch(/^[0-9a-f]{40,64}$/); + }); + }); +}); diff --git a/software/ssh/tsconfig.json b/software/ssh/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/ssh/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/tar/agentos-package.json b/software/tar/agentos-package.json new file mode 100644 index 0000000000..94501679ac --- /dev/null +++ b/software/tar/agentos-package.json @@ -0,0 +1,12 @@ +{ + "commands": [ + "tar" + ], + "registry": { + "title": "tar", + "description": "GNU tar archiver.", + "priority": 60, + "image": "/images/registry/tar.svg", + "category": "core" + } +} diff --git a/software/tar/native/crates/cmd-tar/Cargo.toml b/software/tar/native/crates/cmd-tar/Cargo.toml new file mode 100644 index 0000000000..a5ca3d0b4b --- /dev/null +++ b/software/tar/native/crates/cmd-tar/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-tar" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "tar standalone binary for secure-exec VM" + +[[bin]] +name = "tar" +path = "src/main.rs" + +[dependencies] +secureexec-tar = { path = "../tar" } diff --git a/registry/native/crates/commands/tar/src/main.rs b/software/tar/native/crates/cmd-tar/src/main.rs similarity index 100% rename from registry/native/crates/commands/tar/src/main.rs rename to software/tar/native/crates/cmd-tar/src/main.rs diff --git a/software/tar/native/crates/tar/Cargo.toml b/software/tar/native/crates/tar/Cargo.toml new file mode 100644 index 0000000000..f10f711e29 --- /dev/null +++ b/software/tar/native/crates/tar/Cargo.toml @@ -0,0 +1,11 @@ +[package] +workspace = "../../../../../toolchain" +name = "secureexec-tar" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "tar implementation for secure-exec standalone binaries" + +[dependencies] +flate2 = { version = "1.0", default-features = false, features = ["rust_backend"] } +tar = "0.4" diff --git a/software/tar/native/crates/tar/src/lib.rs b/software/tar/native/crates/tar/src/lib.rs new file mode 100644 index 0000000000..db039cff45 --- /dev/null +++ b/software/tar/native/crates/tar/src/lib.rs @@ -0,0 +1,753 @@ +//! tar implementation — create, extract, and list tape archives. +//! +//! Supports -c create, -x extract, -t list. +//! Options: -f archive, -z gzip, -v verbose, -C directory, --strip-components=N. + +use std::collections::HashSet; +use std::ffi::OsString; +use std::fs::{self, File, OpenOptions}; +use std::io::{self, Read, Write}; +use std::path::{Component, Path, PathBuf}; + +use flate2::read::GzDecoder; +use flate2::write::GzEncoder; +use flate2::Compression; + +const MAX_ARCHIVE_ENTRIES: usize = 100_000; +const MAX_CREATE_DEPTH: usize = 256; +const MAX_DIRECTORY_ENTRIES: usize = 100_000; + +#[derive(PartialEq)] +enum Mode { + None, + Create, + Extract, + List, +} + +/// Unified tar entry point. +pub fn main(args: Vec) -> i32 { + let str_args: Vec = args + .iter() + .skip(1) + .map(|a| a.to_string_lossy().to_string()) + .collect(); + + if str_args.is_empty() { + eprintln!("tar: must specify one of -c, -x, -t"); + return 1; + } + + let mut mode = Mode::None; + let mut archive_file: Option = None; + let mut gzip = false; + let mut verbose = false; + let mut directory: Option = None; + let mut strip_components: usize = 0; + let mut paths: Vec = Vec::new(); + + let mut i = 0; + let mut first_arg = true; + + while i < str_args.len() { + let arg = &str_args[i]; + + if arg.starts_with("--strip-components=") { + if let Ok(n) = arg["--strip-components=".len()..].parse() { + strip_components = n; + } + first_arg = false; + } else if arg == "--strip-components" { + i += 1; + if i < str_args.len() { + strip_components = str_args[i].parse().unwrap_or(0); + } + first_arg = false; + } else if arg == "-C" || arg == "--directory" { + i += 1; + if i < str_args.len() { + directory = Some(str_args[i].clone()); + } + first_arg = false; + } else if arg == "--help" { + print_usage(); + return 0; + } else if arg.starts_with('-') || first_arg { + // tar's first argument can omit the leading dash (e.g., `tar czf`) + let flags = if arg.starts_with('-') { + &arg[1..] + } else { + &arg[..] + }; + let mut chars = flags.chars().peekable(); + while let Some(ch) = chars.next() { + match ch { + 'c' => mode = Mode::Create, + 'x' => mode = Mode::Extract, + 't' => mode = Mode::List, + 'z' => gzip = true, + 'v' => verbose = true, + 'f' => { + let rest: String = chars.collect(); + if !rest.is_empty() { + archive_file = Some(rest); + } else { + i += 1; + if i < str_args.len() { + archive_file = Some(str_args[i].clone()); + } + } + break; + } + 'C' => { + let rest: String = chars.collect(); + if !rest.is_empty() { + directory = Some(rest); + } else { + i += 1; + if i < str_args.len() { + directory = Some(str_args[i].clone()); + } + } + break; + } + _ => { + eprintln!("tar: unknown option: {}", ch); + return 1; + } + } + } + first_arg = false; + } else { + paths.push(arg.clone()); + first_arg = false; + } + + i += 1; + } + + // Auto-detect gzip from filename + if !gzip { + if let Some(ref f) = archive_file { + if f.ends_with(".tar.gz") || f.ends_with(".tgz") { + gzip = true; + } + } + } + + let result = match mode { + Mode::Create => do_create( + archive_file.as_deref(), + gzip, + verbose, + directory.as_deref(), + &paths, + ), + Mode::Extract => do_extract( + archive_file.as_deref(), + gzip, + verbose, + directory.as_deref(), + strip_components, + ), + Mode::List => do_list(archive_file.as_deref(), gzip, verbose), + Mode::None => { + eprintln!("tar: must specify one of -c, -x, -t"); + return 1; + } + }; + + match result { + Ok(()) => 0, + Err(e) => { + eprintln!("tar: {}", e); + 1 + } + } +} + +fn do_create( + archive_file: Option<&str>, + gzip: bool, + verbose: bool, + directory: Option<&str>, + paths: &[String], +) -> io::Result<()> { + if paths.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "cowardly refusing to create an empty archive", + )); + } + + match archive_file { + Some("-") | None => create_to_writer(io::stdout(), gzip, verbose, directory, paths), + Some(path) => { + let mut archive = BoundedVecWriter::new(512 * 1024 * 1024); + create_to_writer(&mut archive, gzip, verbose, directory, paths)?; + fs::write(path, archive.into_inner()) + } + } +} + +struct BoundedVecWriter { + bytes: Vec, + capacity: usize, +} + +impl BoundedVecWriter { + fn new(capacity: usize) -> Self { + Self { + bytes: Vec::new(), + capacity, + } + } + + fn into_inner(self) -> Vec { + self.bytes + } +} + +impl Write for BoundedVecWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + let Some(new_len) = self.bytes.len().checked_add(buf.len()) else { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "archive is too large to buffer", + )); + }; + if new_len > self.capacity { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "archive is too large to buffer", + )); + } + self.bytes.extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +fn create_to_writer( + writer: W, + gzip: bool, + verbose: bool, + directory: Option<&str>, + paths: &[String], +) -> io::Result<()> { + if gzip { + let encoder = GzEncoder::new(writer, Compression::default()); + let mut builder = tar::Builder::new(encoder); + append_paths(&mut builder, directory, paths, verbose)?; + let encoder = builder.into_inner()?; + let mut writer = encoder.finish()?; + writer.flush() + } else { + let mut builder = tar::Builder::new(writer); + append_paths(&mut builder, directory, paths, verbose)?; + let mut writer = builder.into_inner()?; + writer.flush() + } +} + +fn append_paths( + builder: &mut tar::Builder, + directory: Option<&str>, + paths: &[String], + verbose: bool, +) -> io::Result<()> { + let mut entry_count = 0; + for path in paths { + append_path( + builder, + resolve_disk_path(directory, Path::new(path)), + Path::new(path), + verbose, + 0, + &mut entry_count, + )?; + } + Ok(()) +} + +fn append_path( + builder: &mut tar::Builder, + disk_path: PathBuf, + archive_path: &Path, + verbose: bool, + depth: usize, + entry_count: &mut usize, +) -> io::Result<()> { + if depth > MAX_CREATE_DEPTH { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "maximum directory depth exceeded at {}", + disk_path.display() + ), + )); + } + increment_entry_count(entry_count)?; + + let meta = fs::symlink_metadata(&disk_path).map_err(|e| { + io::Error::new(e.kind(), format!("metadata {}: {}", disk_path.display(), e)) + })?; + + if meta.is_dir() { + append_dir( + builder, + &disk_path, + archive_path, + verbose, + depth, + entry_count, + )?; + } else if meta.is_file() { + if verbose { + eprintln!("{}", archive_path.display()); + } + let mut header = tar::Header::new_gnu(); + header.set_entry_type(tar::EntryType::Regular); + header.set_size(meta.len()); + header.set_mode(0o644); + header.set_cksum(); + let mut file = File::open(&disk_path)?; + builder.append_data(&mut header, archive_path, &mut file)?; + } else if meta.file_type().is_symlink() { + if verbose { + eprintln!("{}", archive_path.display()); + } + let target = fs::read_link(&disk_path)?; + let mut header = tar::Header::new_gnu(); + header.set_entry_type(tar::EntryType::Symlink); + header.set_size(0); + header.set_mode(0o777); + header.set_cksum(); + builder.append_link(&mut header, archive_path, &target)?; + } + + Ok(()) +} + +fn append_dir( + builder: &mut tar::Builder, + disk_dir: &Path, + archive_dir: &Path, + verbose: bool, + depth: usize, + entry_count: &mut usize, +) -> io::Result<()> { + if verbose { + eprintln!("{}/", archive_dir.display()); + } + + let mut header = tar::Header::new_gnu(); + header.set_entry_type(tar::EntryType::Directory); + header.set_size(0); + header.set_mode(0o755); + header.set_cksum(); + builder.append_data(&mut header, archive_dir, io::empty())?; + + let mut dir_entries = 0; + for entry_result in fs::read_dir(disk_dir)? { + let entry = entry_result?; + dir_entries += 1; + if dir_entries > MAX_DIRECTORY_ENTRIES { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("too many entries in {}", disk_dir.display()), + )); + } + let archive_child = archive_dir.join(entry.file_name()); + append_path( + builder, + entry.path(), + &archive_child, + verbose, + depth + 1, + entry_count, + )?; + } + + Ok(()) +} + +fn do_extract( + archive_file: Option<&str>, + gzip: bool, + verbose: bool, + directory: Option<&str>, + strip_components: usize, +) -> io::Result<()> { + let reader = open_read(archive_file, gzip)?; + let mut archive = tar::Archive::new(reader); + let mut known_dirs = HashSet::new(); + if let Some(base) = directory { + validate_extract_base(Path::new(base))?; + known_dirs.insert(PathBuf::from(base)); + } + + let mut entry_count = 0; + for entry_result in archive.entries()? { + increment_entry_count(&mut entry_count)?; + let mut entry = entry_result?; + let orig_path = entry.path()?.into_owned(); + validate_archive_input_path(&orig_path)?; + + let relative_dest = match strip_path_components(&orig_path, strip_components) { + Some(p) if !p.as_os_str().is_empty() => p, + _ => continue, + }; + validate_relative_output_path(&relative_dest)?; + validate_extract_depth(&relative_dest)?; + let dest = resolve_output_path(directory, &relative_dest); + + if verbose { + eprintln!("{}", orig_path.display()); + } + + match entry.header().entry_type() { + tar::EntryType::Directory => { + ensure_relative_dir_exists(directory, &relative_dest, &mut known_dirs)?; + } + tar::EntryType::Regular | tar::EntryType::GNUSparse => { + if let Some(parent) = dest.parent() { + if !parent.as_os_str().is_empty() { + let relative_parent = + relative_dest.parent().unwrap_or_else(|| Path::new("")); + ensure_relative_dir_exists(directory, relative_parent, &mut known_dirs)?; + } + } + reject_existing_symlink(&dest)?; + let mut output = OpenOptions::new() + .create(true) + .write(true) + .truncate(true) + .open(&dest) + .map_err(|e| { + io::Error::new(e.kind(), format!("open {}: {}", dest.display(), e)) + })?; + io::copy(&mut entry, &mut output).map_err(|e| { + io::Error::new(e.kind(), format!("write {}: {}", dest.display(), e)) + })?; + output.flush().map_err(|e| { + io::Error::new(e.kind(), format!("write {}: {}", dest.display(), e)) + })?; + } + tar::EntryType::Symlink => { + if let Some(target) = entry.link_name()? { + validate_symlink_target(target.as_ref())?; + if let Some(parent) = dest.parent() { + if !parent.as_os_str().is_empty() { + let relative_parent = + relative_dest.parent().unwrap_or_else(|| Path::new("")); + ensure_relative_dir_exists( + directory, + relative_parent, + &mut known_dirs, + )?; + } + } + reject_existing_symlink(&dest)?; + #[allow(deprecated)] + std::fs::soft_link(target.as_ref(), &dest).map_err(|e| { + io::Error::new(e.kind(), format!("symlink {}: {}", dest.display(), e)) + })?; + } + } + _ => { + // Skip hard links, char/block devices, etc. + } + } + } + + Ok(()) +} + +fn resolve_disk_path(directory: Option<&str>, path: &Path) -> PathBuf { + match directory { + Some(base) if !path.is_absolute() => Path::new(base).join(path), + _ => path.to_path_buf(), + } +} + +fn resolve_output_path(directory: Option<&str>, path: &Path) -> PathBuf { + match directory { + Some(base) if path.is_relative() => Path::new(base).join(path), + _ => path.to_path_buf(), + } +} + +fn ensure_relative_dir_exists( + directory: Option<&str>, + relative_path: &Path, + known_dirs: &mut HashSet, +) -> io::Result<()> { + let mut current = match directory { + Some(base) => PathBuf::from(base), + None => PathBuf::new(), + }; + + for component in relative_path.components() { + match component { + Component::CurDir => {} + Component::Normal(part) => { + current.push(part); + if known_dirs.contains(¤t) { + continue; + } + match fs::create_dir(¤t) { + Ok(()) => { + known_dirs.insert(current.clone()); + } + Err(err) if err.kind() == io::ErrorKind::AlreadyExists => { + let metadata = fs::symlink_metadata(¤t).map_err(|metadata_err| { + io::Error::new( + metadata_err.kind(), + format!("metadata {}: {}", current.display(), metadata_err), + ) + })?; + if metadata.file_type().is_symlink() || !metadata.is_dir() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("refusing to extract through {}", current.display()), + )); + } + known_dirs.insert(current.clone()); + } + Err(err) => { + return Err(io::Error::new( + err.kind(), + format!("create_dir {}: {}", current.display(), err), + )); + } + } + } + Component::Prefix(_) | Component::RootDir | Component::ParentDir => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("unsupported path component in {}", relative_path.display()), + )); + } + } + } + + Ok(()) +} + +fn do_list(archive_file: Option<&str>, gzip: bool, verbose: bool) -> io::Result<()> { + let reader = open_read(archive_file, gzip)?; + let mut archive = tar::Archive::new(reader); + let stdout = io::stdout(); + let mut out = stdout.lock(); + + let mut entry_count = 0; + for entry_result in archive.entries()? { + increment_entry_count(&mut entry_count)?; + let entry = entry_result?; + let path = entry.path()?; + + let entry_type = entry.header().entry_type(); + if verbose { + let h = entry.header(); + let size = h.size().unwrap_or(0); + let mode = h.mode().unwrap_or(0o644); + let type_ch = match entry_type { + tar::EntryType::Directory => 'd', + tar::EntryType::Symlink => 'l', + _ => '-', + }; + writeln!( + out, + "{}{} {:>8} {}", + type_ch, + format_mode(mode), + size, + path.display() + )?; + } else if entry_type == tar::EntryType::Directory { + writeln!(out, "{}/", path.display())?; + } else { + writeln!(out, "{}", path.display())?; + } + } + + out.flush() +} + +fn open_read(archive_file: Option<&str>, gzip: bool) -> io::Result> { + let reader: Box = match archive_file { + Some("-") | None => Box::new(io::stdin()), + Some(path) => Box::new(File::open(path)?), + }; + + if gzip { + Ok(Box::new(GzDecoder::new(reader))) + } else { + Ok(reader) + } +} + +fn strip_path_components(path: &Path, n: usize) -> Option { + let mut remaining = n; + let mut stripped = PathBuf::new(); + + for component in path.components() { + match component { + Component::Prefix(_) | Component::RootDir | Component::CurDir => {} + Component::ParentDir => { + if remaining == 0 { + stripped.push(component.as_os_str()); + } + } + Component::Normal(part) => { + if remaining > 0 { + remaining -= 1; + } else { + stripped.push(part); + } + } + } + } + + if stripped.as_os_str().is_empty() { + None + } else { + Some(stripped) + } +} + +fn increment_entry_count(count: &mut usize) -> io::Result<()> { + *count += 1; + if *count > MAX_ARCHIVE_ENTRIES { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "too many archive entries", + )); + } + Ok(()) +} + +fn validate_relative_output_path(path: &Path) -> io::Result<()> { + for component in path.components() { + match component { + Component::Normal(_) | Component::CurDir => {} + Component::Prefix(_) | Component::RootDir | Component::ParentDir => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("refusing to extract unsafe path {}", path.display()), + )); + } + } + } + Ok(()) +} + +fn validate_archive_input_path(path: &Path) -> io::Result<()> { + for component in path.components() { + match component { + Component::Normal(_) | Component::CurDir => {} + Component::Prefix(_) | Component::RootDir | Component::ParentDir => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("refusing to extract unsafe path {}", path.display()), + )); + } + } + } + Ok(()) +} + +fn validate_extract_depth(path: &Path) -> io::Result<()> { + let depth = path + .components() + .filter(|component| matches!(component, Component::Normal(_))) + .count(); + if depth > MAX_CREATE_DEPTH { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("maximum extraction depth exceeded at {}", path.display()), + )); + } + Ok(()) +} + +fn validate_extract_base(path: &Path) -> io::Result<()> { + let metadata = fs::symlink_metadata(path).map_err(|err| { + io::Error::new(err.kind(), format!("metadata {}: {}", path.display(), err)) + })?; + if metadata.file_type().is_symlink() || !metadata.is_dir() { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("refusing to extract through {}", path.display()), + )); + } + Ok(()) +} + +fn validate_symlink_target(target: &Path) -> io::Result<()> { + for component in target.components() { + match component { + Component::Normal(_) | Component::CurDir => {} + Component::Prefix(_) | Component::RootDir | Component::ParentDir => { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!( + "refusing to extract unsafe symlink target {}", + target.display() + ), + )); + } + } + } + Ok(()) +} + +fn reject_existing_symlink(path: &Path) -> io::Result<()> { + match fs::symlink_metadata(path) { + Ok(metadata) if metadata.file_type().is_symlink() => Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("refusing to overwrite symlink {}", path.display()), + )), + Ok(_) => Ok(()), + Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()), + Err(err) => Err(io::Error::new( + err.kind(), + format!("metadata {}: {}", path.display(), err), + )), + } +} + +fn format_mode(mode: u32) -> String { + let mut s = String::with_capacity(9); + for &(bit, ch) in &[ + (0o400, 'r'), + (0o200, 'w'), + (0o100, 'x'), + (0o040, 'r'), + (0o020, 'w'), + (0o010, 'x'), + (0o004, 'r'), + (0o002, 'w'), + (0o001, 'x'), + ] { + s.push(if mode & bit != 0 { ch } else { '-' }); + } + s +} + +fn print_usage() { + eprintln!("Usage: tar [options] [files...]"); + eprintln!(" -c create archive"); + eprintln!(" -x extract archive"); + eprintln!(" -t list archive contents"); + eprintln!(" -f FILE archive filename (- for stdin/stdout)"); + eprintln!(" -z gzip compress/decompress"); + eprintln!(" -v verbose"); + eprintln!(" -C DIR change directory"); + eprintln!(" --strip-components=N"); + eprintln!(" strip N leading components on extract"); +} diff --git a/software/tar/package.json b/software/tar/package.json new file mode 100644 index 0000000000..97f1e92da1 --- /dev/null +++ b/software/tar/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/tar", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "GNU tar archiver for secure-exec VMs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/tree/src/index.ts b/software/tar/src/index.ts similarity index 100% rename from registry/software/tree/src/index.ts rename to software/tar/src/index.ts diff --git a/software/tar/test/manifest.test.ts b/software/tar/test/manifest.test.ts new file mode 100644 index 0000000000..89139d33b8 --- /dev/null +++ b/software/tar/test/manifest.test.ts @@ -0,0 +1,19 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const packageDir = new URL("..", import.meta.url).pathname; + +describe("package manifest", () => { + it("declares command binaries", () => { + const manifest = JSON.parse( + readFileSync(join(packageDir, "agentos-package.json"), "utf8"), + ); + + expect(manifest.commands?.length ?? 0).toBeGreaterThan(0); + for (const command of manifest.commands) { + expect(typeof command).toBe("string"); + expect(command.length).toBeGreaterThan(0); + } + }); +}); diff --git a/software/tar/test/tar.test.ts b/software/tar/test/tar.test.ts new file mode 100644 index 0000000000..adebae14c4 --- /dev/null +++ b/software/tar/test/tar.test.ts @@ -0,0 +1,165 @@ +import { existsSync } from "node:fs"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + NodeFileSystem, + createKernel, + createWasmVmRuntime, + describeIf, +} from "@agentos/test-harness"; +import type { Kernel } from "@agentos/test-harness"; +import { afterEach, expect, it } from "vitest"; + +const TAR_COMMAND_DIR = fileURLToPath(new URL("../bin", import.meta.url)); +const hasTarPackageBinary = existsSync(join(TAR_COMMAND_DIR, "tar")); + +let tempRoot: string | undefined; + +function hostPath(path: string): string { + if (!tempRoot) throw new Error("fixture root not initialized"); + return join(tempRoot, path.replace(/^\/+/, "")); +} + +async function writeFixture(path: string, contents: string): Promise { + const target = hostPath(path); + await mkdir(dirname(target), { recursive: true }); + await writeFile(target, contents); +} + +async function createTestVFS(): Promise { + tempRoot = await mkdtemp(join(tmpdir(), "agentos-tar-")); + await writeFixture("/project/src/docs/readme.txt", "hello from docs\n"); + await writeFixture("/project/src/data/values.csv", "name,score\nAda,7\nLinus,3\n"); + await mkdir(hostPath("/project/out"), { recursive: true }); + await mkdir(hostPath("/project/strip"), { recursive: true }); + await mkdir(hostPath("/project/gzip-out"), { recursive: true }); + return new NodeFileSystem({ root: tempRoot }); +} + +function lines(stdout: string): string[] { + return stdout.split("\n").filter((line) => line.length > 0); +} + +const textDecoder = new TextDecoder(); + +describeIf(hasTarPackageBinary, "tar command", { timeout: 10_000 }, () => { + let kernel: Kernel | undefined; + + afterEach(async () => { + await kernel?.dispose(); + kernel = undefined; + if (tempRoot) { + await rm(tempRoot, { recursive: true, force: true }); + tempRoot = undefined; + } + }); + + async function mountFixture(): Promise { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [TAR_COMMAND_DIR] })); + } + + async function runTar(args: string[]) { + if (!kernel) throw new Error("kernel not mounted"); + let stdout = ""; + let stderr = ""; + const proc = kernel.spawn("tar", args, { + onStdout: (chunk) => { + stdout += Buffer.from(chunk).toString("utf8"); + }, + onStderr: (chunk) => { + stderr += Buffer.from(chunk).toString("utf8"); + }, + }); + const exitCode = await proc.wait(); + await new Promise((resolve) => setTimeout(resolve, 0)); + return { stdout, stderr, exitCode }; + } + + async function createArchive(path = "/project/archive.tar") { + const result = await runTar(["-cf", path, "-C", "/project", "src"]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + } + + async function readGuestText(path: string): Promise { + if (!kernel) throw new Error("kernel not mounted"); + return textDecoder.decode(await kernel.readFile(path)); + } + + it("creates and lists file-backed archives", async () => { + await mountFixture(); + await createArchive(); + + const result = await runTar(["-tf", "/project/archive.tar"]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(lines(result.stdout)).toEqual( + expect.arrayContaining([ + "src/", + "src/docs/", + "src/docs/readme.txt", + "src/data/", + "src/data/values.csv", + ]), + ); + }); + + it("extracts archives into target directories", async () => { + await mountFixture(); + await createArchive(); + + const result = await runTar(["-xf", "/project/archive.tar", "-C", "/project/out"]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + await expect(readGuestText("/project/out/src/docs/readme.txt")).resolves.toBe( + "hello from docs\n", + ); + await expect(readGuestText("/project/out/src/data/values.csv")).resolves.toBe( + "name,score\nAda,7\nLinus,3\n", + ); + }); + + it("auto-detects gzip archives by extension", async () => { + await mountFixture(); + + const create = await runTar(["-czf", "/project/archive.tgz", "-C", "/project", "src"]); + expect(create.exitCode, create.stderr || create.stdout).toBe(0); + + const list = await runTar(["-tf", "/project/archive.tgz"]); + expect(list.exitCode, list.stderr || list.stdout).toBe(0); + expect(lines(list.stdout)).toContain("src/docs/readme.txt"); + + const extract = await runTar(["-xf", "/project/archive.tgz", "-C", "/project/gzip-out"]); + expect(extract.exitCode, extract.stderr || extract.stdout).toBe(0); + await expect(readGuestText("/project/gzip-out/src/docs/readme.txt")).resolves.toBe( + "hello from docs\n", + ); + }); + + it("strips path components on extraction", async () => { + await mountFixture(); + await createArchive(); + + const result = await runTar([ + "-xf", + "/project/archive.tar", + "-C", + "/project/strip", + "--strip-components=1", + ]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + await expect(readGuestText("/project/strip/docs/readme.txt")).resolves.toBe( + "hello from docs\n", + ); + await expect(readGuestText("/project/strip/src/docs/readme.txt")).rejects.toThrow(); + }); + + it("fails when a create input is missing", async () => { + await mountFixture(); + + const result = await runTar(["-cf", "/project/missing.tar", "-C", "/project", "nope"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("nope"); + }); +}); diff --git a/software/tar/tsconfig.json b/software/tar/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/tar/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/tree/agentos-package.json b/software/tree/agentos-package.json new file mode 100644 index 0000000000..9a3df6fcee --- /dev/null +++ b/software/tree/agentos-package.json @@ -0,0 +1,11 @@ +{ + "commands": [ + "tree" + ], + "registry": { + "title": "tree", + "description": "Display directory structure as a tree.", + "priority": 25, + "category": "core" + } +} diff --git a/software/tree/package.json b/software/tree/package.json new file mode 100644 index 0000000000..45d2aee6d2 --- /dev/null +++ b/software/tree/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/tree", + "version": "0.3.3", + "type": "module", + "license": "GPL-2.0-or-later", + "description": "tree directory listing for secure-exec VMs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/unzip/src/index.ts b/software/tree/src/index.ts similarity index 100% rename from registry/software/unzip/src/index.ts rename to software/tree/src/index.ts diff --git a/registry/tests/kernel/tree-test.test.ts b/software/tree/test/tree-test.test.ts similarity index 96% rename from registry/tests/kernel/tree-test.test.ts rename to software/tree/test/tree-test.test.ts index 850b5e371a..0c505f5ff0 100644 --- a/registry/tests/kernel/tree-test.test.ts +++ b/software/tree/test/tree-test.test.ts @@ -10,8 +10,8 @@ import { describeIf, createIntegrationKernel, skipUnlessWasmBuilt, -} from './helpers.ts'; -import type { IntegrationKernelResult } from './helpers.ts'; +} from '@agentos/test-harness'; +import type { IntegrationKernelResult } from '@agentos/test-harness'; const wasmSkip = skipUnlessWasmBuilt(); const TREE_COMMAND_TIMEOUT_MS = 20_000; @@ -65,8 +65,8 @@ describeIf(!wasmSkip, 'tree command behavior', () => { expect(result.stdout).toContain('types.ts'); expect(result.stdout).toContain('index.ts'); expect(result.stdout).toContain('README.md'); - // Should show 2 directories (src, lib) and 4 files - expect(result.stdout).toMatch(/2 director/); + // Upstream tree reports the displayed root plus src/lib. + expect(result.stdout).toMatch(/3 director/); expect(result.stdout).toMatch(/4 file/); }, TREE_TEST_TIMEOUT_MS); diff --git a/software/tree/tsconfig.json b/software/tree/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/tree/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/registry/tsconfig.base.json b/software/tsconfig.base.json similarity index 100% rename from registry/tsconfig.base.json rename to software/tsconfig.base.json diff --git a/software/unzip/agentos-package.json b/software/unzip/agentos-package.json new file mode 100644 index 0000000000..2260eedf19 --- /dev/null +++ b/software/unzip/agentos-package.json @@ -0,0 +1,11 @@ +{ + "commands": [ + "unzip" + ], + "registry": { + "title": "unzip", + "description": "Extract zip archives.", + "priority": 10, + "category": "core" + } +} diff --git a/software/unzip/package.json b/software/unzip/package.json new file mode 100644 index 0000000000..9fcf15667a --- /dev/null +++ b/software/unzip/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/unzip", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "unzip archive extraction for secure-exec VMs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/vim/src/index.ts b/software/unzip/src/index.ts similarity index 100% rename from registry/software/vim/src/index.ts rename to software/unzip/src/index.ts diff --git a/software/unzip/test/unzip.test.ts b/software/unzip/test/unzip.test.ts new file mode 100644 index 0000000000..b6081dc2e2 --- /dev/null +++ b/software/unzip/test/unzip.test.ts @@ -0,0 +1,218 @@ +/** + * Package-owned integration tests for the unzip command. + */ + +import { afterEach, expect, it } from "vitest"; +import { + C_BUILD_DIR, + COMMANDS_DIR, + createInMemoryFileSystem, + createKernel, + createWasmVmRuntime, + describeIf, + hasCWasmBinaries, + type Kernel, +} from "@agentos/test-harness"; + +interface HostileEntry { + name: string; + method: number; + compressedSize: number; + uncompressedSize: number; + localOffset: number; +} + +/** Builds a ZIP whose EOCD cd-size field is corrupt so unzip's raw central-directory fallback parser is exercised. */ +function buildFallbackArchive( + prefix: Uint8Array, + entries: HostileEntry[], +): Uint8Array { + const enc = new TextEncoder(); + const cdParts: Uint8Array[] = []; + for (const entry of entries) { + const nameBytes = enc.encode(entry.name); + const cd = new Uint8Array(46 + nameBytes.length); + const dv = new DataView(cd.buffer); + dv.setUint32(0, 0x02014b50, true); + dv.setUint16(4, 20, true); + dv.setUint16(6, 20, true); + dv.setUint16(10, entry.method, true); + dv.setUint32(20, entry.compressedSize, true); + dv.setUint32(24, entry.uncompressedSize, true); + dv.setUint16(28, nameBytes.length, true); + dv.setUint32(42, entry.localOffset, true); + cd.set(nameBytes, 46); + cdParts.push(cd); + } + const cdOffset = prefix.length; + const cdLen = cdParts.reduce((n, part) => n + part.length, 0); + const eocd = new Uint8Array(22); + const dv = new DataView(eocd.buffer); + dv.setUint32(0, 0x06054b50, true); + dv.setUint16(8, entries.length, true); + dv.setUint16(10, entries.length, true); + dv.setUint32(12, 0xffffffff, true); + dv.setUint32(16, cdOffset, true); + const out = new Uint8Array(prefix.length + cdLen + 22); + out.set(prefix, 0); + let offset = cdOffset; + for (const part of cdParts) { + out.set(part, offset); + offset += part.length; + } + out.set(eocd, offset); + return out; +} + +describeIf( + hasCWasmBinaries("zip", "unzip"), + "unzip command", + { timeout: 10_000 }, + () => { + let kernel: Kernel; + + afterEach(async () => { + await kernel?.dispose(); + }); + + it("extracts a zip archive into a target directory", async () => { + const vfs = createInMemoryFileSystem(); + await vfs.writeFile("/hello.txt", "Hello, World!\n"); + + kernel = createKernel({ filesystem: vfs }); + await kernel.mount( + createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), + ); + + const zipResult = await kernel.exec("zip /archive.zip /hello.txt"); + expect(zipResult.exitCode, zipResult.stderr).toBe(0); + + const unzipResult = await kernel.exec("unzip -d /extracted /archive.zip"); + expect(unzipResult.exitCode, unzipResult.stderr).toBe(0); + expect(await vfs.readTextFile("/extracted/hello.txt")).toBe( + "Hello, World!\n", + ); + }); + + it("lists archive contents with sizes", async () => { + const vfs = createInMemoryFileSystem(); + await vfs.writeFile("/data.txt", "some data content\n"); + + kernel = createKernel({ filesystem: vfs }); + await kernel.mount( + createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), + ); + + const zipResult = await kernel.exec("zip /list-test.zip /data.txt"); + expect(zipResult.exitCode, zipResult.stderr).toBe(0); + + const listResult = await kernel.exec("unzip -l /list-test.zip"); + expect(listResult.exitCode, listResult.stderr).toBe(0); + expect(listResult.stdout).toContain("data.txt"); + expect(listResult.stdout).toContain("18"); + expect(listResult.stdout).toMatch(/1 file/); + }); + + it("extracts binary file contents exactly", async () => { + const vfs = createInMemoryFileSystem(); + const content = new Uint8Array(256); + for (let i = 0; i < 256; i++) content[i] = i; + await vfs.writeFile("/binary.bin", content); + + kernel = createKernel({ filesystem: vfs }); + await kernel.mount( + createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), + ); + + const zipResult = await kernel.exec("zip /roundtrip.zip /binary.bin"); + expect(zipResult.exitCode, zipResult.stderr).toBe(0); + + const unzipResult = await kernel.exec("unzip -d /rt-out /roundtrip.zip"); + expect(unzipResult.exitCode, unzipResult.stderr).toBe(0); + + const extracted = await vfs.readFile("/rt-out/binary.bin"); + expect(extracted.length).toBe(256); + for (let i = 0; i < 256; i++) { + expect(extracted[i]).toBe(i); + } + }); + + it("rejects an entry with a wrapping local offset", async () => { + const vfs = createInMemoryFileSystem(); + const bytes = buildFallbackArchive(new Uint8Array(0), [ + { + name: "evil.txt", + method: 0, + compressedSize: 4, + uncompressedSize: 4, + localOffset: 0xfffffff0, + }, + ]); + await vfs.writeFile("/evil.zip", bytes); + + kernel = createKernel({ filesystem: vfs }); + await kernel.mount( + createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), + ); + + const result = await kernel.exec("unzip -d /out /evil.zip"); + expect(result.exitCode, result.stderr).not.toBe(0); + expect(result.stderr).toMatch(/error/); + expect(await vfs.exists("/out/evil.txt")).toBe(false); + }); + + it("rejects an entry whose normalized name is empty", async () => { + const vfs = createInMemoryFileSystem(); + const bytes = buildFallbackArchive(new Uint8Array(0), [ + { + name: "/", + method: 0, + compressedSize: 0, + uncompressedSize: 0, + localOffset: 0, + }, + ]); + await vfs.writeFile("/empty-name.zip", bytes); + + kernel = createKernel({ filesystem: vfs }); + await kernel.mount( + createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), + ); + + const result = await kernel.exec("unzip /empty-name.zip"); + expect(result.exitCode, result.stderr).not.toBe(0); + expect(result.stderr).toMatch(/error/); + }); + + it("rejects hostile uncompressed sizes before extracting", async () => { + const vfs = createInMemoryFileSystem(); + const prefix = new Uint8Array(31); + const pdv = new DataView(prefix.buffer); + pdv.setUint32(0, 0x04034b50, true); + pdv.setUint16(4, 20, true); + pdv.setUint16(26, 0, true); + pdv.setUint16(28, 0, true); + prefix[30] = 0x41; + const bytes = buildFallbackArchive(prefix, [ + { + name: "big.bin", + method: 0, + compressedSize: 1, + uncompressedSize: 0xffffffff, + localOffset: 0, + }, + ]); + await vfs.writeFile("/big.zip", bytes); + + kernel = createKernel({ filesystem: vfs }); + await kernel.mount( + createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), + ); + + const result = await kernel.exec("unzip -d /cap-out /big.zip"); + expect(result.exitCode, result.stderr).not.toBe(0); + expect(result.stderr).toMatch(/error/); + expect(await vfs.exists("/cap-out/big.bin")).toBe(false); + }); + }, +); diff --git a/software/unzip/tsconfig.json b/software/unzip/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/unzip/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/vim/agentos-package.json b/software/vim/agentos-package.json new file mode 100644 index 0000000000..801b9d8a53 --- /dev/null +++ b/software/vim/agentos-package.json @@ -0,0 +1,25 @@ +{ + "name": "vim", + "commands": [ + "vim" + ], + "provides": { + "env": { + "VIM": "/usr/local/share/vim", + "VIMRUNTIME": "/usr/local/share/vim/vim92" + }, + "files": [ + { + "source": "share/vim/vim92", + "target": "/usr/local/share/vim/vim92" + } + ] + }, + "registry": { + "title": "vim", + "description": "Vim text editor with bundled runtime.", + "priority": 65, + "image": "/images/registry/vim.svg", + "category": "editors" + } +} diff --git a/registry/native/c/vim/compat.h b/software/vim/native/c/vim-bridge/compat.h similarity index 100% rename from registry/native/c/vim/compat.h rename to software/vim/native/c/vim-bridge/compat.h diff --git a/registry/native/c/vim/grp.h b/software/vim/native/c/vim-bridge/grp.h similarity index 100% rename from registry/native/c/vim/grp.h rename to software/vim/native/c/vim-bridge/grp.h diff --git a/software/vim/native/c/vim-bridge/posix_stubs.c b/software/vim/native/c/vim-bridge/posix_stubs.c new file mode 100644 index 0000000000..2428b7beca --- /dev/null +++ b/software/vim/native/c/vim-bridge/posix_stubs.c @@ -0,0 +1,11 @@ +/* posix_stubs.c — stubs for full-OS libc gaps vim references but that the VM + * does not implement yet. Keep this narrow; the patched sysroot owns libc. */ +#include +#include +struct group *getgrent(void) { return NULL; } +void setgrent(void) {} +void endgrent(void) {} + +/* --- process/signal stubs (not used by core editing) --- */ +void __SIG_DFL(int s) { (void)s; } +int sigpending(void *set) { (void)set; return 0; } diff --git a/registry/native/c/vim/termcap_stub.c b/software/vim/native/c/vim-bridge/termcap_stub.c similarity index 100% rename from registry/native/c/vim/termcap_stub.c rename to software/vim/native/c/vim-bridge/termcap_stub.c diff --git a/registry/native/c/vim/termios.h b/software/vim/native/c/vim-bridge/termios.h similarity index 100% rename from registry/native/c/vim/termios.h rename to software/vim/native/c/vim-bridge/termios.h diff --git a/registry/native/c/vim/termios_bridge.c b/software/vim/native/c/vim-bridge/termios_bridge.c similarity index 100% rename from registry/native/c/vim/termios_bridge.c rename to software/vim/native/c/vim-bridge/termios_bridge.c diff --git a/software/vim/package.json b/software/vim/package.json new file mode 100644 index 0000000000..ae33f6fc96 --- /dev/null +++ b/software/vim/package.json @@ -0,0 +1,34 @@ +{ + "name": "@agentos-software/vim", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "vim for secure-exec VMs (wasm build + bundled runtime via provides)", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && node ./scripts/stage-runtime.mjs && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/vim/scripts/stage-runtime.mjs b/software/vim/scripts/stage-runtime.mjs similarity index 100% rename from registry/software/vim/scripts/stage-runtime.mjs rename to software/vim/scripts/stage-runtime.mjs diff --git a/registry/software/vim/share/vim/vim92/autoload/RstFold.vim b/software/vim/share/vim/vim92/autoload/RstFold.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/RstFold.vim rename to software/vim/share/vim/vim92/autoload/RstFold.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/ada.vim b/software/vim/share/vim/vim92/autoload/ada.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/ada.vim rename to software/vim/share/vim/vim92/autoload/ada.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/adacomplete.vim b/software/vim/share/vim/vim92/autoload/adacomplete.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/adacomplete.vim rename to software/vim/share/vim/vim92/autoload/adacomplete.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/bitbake.vim b/software/vim/share/vim/vim92/autoload/bitbake.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/bitbake.vim rename to software/vim/share/vim/vim92/autoload/bitbake.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/ccomplete.vim b/software/vim/share/vim/vim92/autoload/ccomplete.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/ccomplete.vim rename to software/vim/share/vim/vim92/autoload/ccomplete.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/clojurecomplete.vim b/software/vim/share/vim/vim92/autoload/clojurecomplete.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/clojurecomplete.vim rename to software/vim/share/vim/vim92/autoload/clojurecomplete.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/context.vim b/software/vim/share/vim/vim92/autoload/context.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/context.vim rename to software/vim/share/vim/vim92/autoload/context.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/contextcomplete.vim b/software/vim/share/vim/vim92/autoload/contextcomplete.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/contextcomplete.vim rename to software/vim/share/vim/vim92/autoload/contextcomplete.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/csscomplete.vim b/software/vim/share/vim/vim92/autoload/csscomplete.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/csscomplete.vim rename to software/vim/share/vim/vim92/autoload/csscomplete.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/decada.vim b/software/vim/share/vim/vim92/autoload/decada.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/decada.vim rename to software/vim/share/vim/vim92/autoload/decada.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/freebasic.vim b/software/vim/share/vim/vim92/autoload/freebasic.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/freebasic.vim rename to software/vim/share/vim/vim92/autoload/freebasic.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/getscript.vim b/software/vim/share/vim/vim92/autoload/getscript.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/getscript.vim rename to software/vim/share/vim/vim92/autoload/getscript.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/gnat.vim b/software/vim/share/vim/vim92/autoload/gnat.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/gnat.vim rename to software/vim/share/vim/vim92/autoload/gnat.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/gzip.vim b/software/vim/share/vim/vim92/autoload/gzip.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/gzip.vim rename to software/vim/share/vim/vim92/autoload/gzip.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/haskellcomplete.vim b/software/vim/share/vim/vim92/autoload/haskellcomplete.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/haskellcomplete.vim rename to software/vim/share/vim/vim92/autoload/haskellcomplete.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/htmlcomplete.vim b/software/vim/share/vim/vim92/autoload/htmlcomplete.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/htmlcomplete.vim rename to software/vim/share/vim/vim92/autoload/htmlcomplete.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/javascriptcomplete.vim b/software/vim/share/vim/vim92/autoload/javascriptcomplete.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/javascriptcomplete.vim rename to software/vim/share/vim/vim92/autoload/javascriptcomplete.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/netrw.vim b/software/vim/share/vim/vim92/autoload/netrw.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/netrw.vim rename to software/vim/share/vim/vim92/autoload/netrw.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/netrwFileHandlers.vim b/software/vim/share/vim/vim92/autoload/netrwFileHandlers.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/netrwFileHandlers.vim rename to software/vim/share/vim/vim92/autoload/netrwFileHandlers.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/netrwSettings.vim b/software/vim/share/vim/vim92/autoload/netrwSettings.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/netrwSettings.vim rename to software/vim/share/vim/vim92/autoload/netrwSettings.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/netrw_gitignore.vim b/software/vim/share/vim/vim92/autoload/netrw_gitignore.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/netrw_gitignore.vim rename to software/vim/share/vim/vim92/autoload/netrw_gitignore.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/paste.vim b/software/vim/share/vim/vim92/autoload/paste.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/paste.vim rename to software/vim/share/vim/vim92/autoload/paste.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/phpcomplete.vim b/software/vim/share/vim/vim92/autoload/phpcomplete.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/phpcomplete.vim rename to software/vim/share/vim/vim92/autoload/phpcomplete.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/python.vim b/software/vim/share/vim/vim92/autoload/python.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/python.vim rename to software/vim/share/vim/vim92/autoload/python.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/python3complete.vim b/software/vim/share/vim/vim92/autoload/python3complete.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/python3complete.vim rename to software/vim/share/vim/vim92/autoload/python3complete.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/pythoncomplete.vim b/software/vim/share/vim/vim92/autoload/pythoncomplete.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/pythoncomplete.vim rename to software/vim/share/vim/vim92/autoload/pythoncomplete.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/rubycomplete.vim b/software/vim/share/vim/vim92/autoload/rubycomplete.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/rubycomplete.vim rename to software/vim/share/vim/vim92/autoload/rubycomplete.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/rust.vim b/software/vim/share/vim/vim92/autoload/rust.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/rust.vim rename to software/vim/share/vim/vim92/autoload/rust.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/rustfmt.vim b/software/vim/share/vim/vim92/autoload/rustfmt.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/rustfmt.vim rename to software/vim/share/vim/vim92/autoload/rustfmt.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/spellfile.vim b/software/vim/share/vim/vim92/autoload/spellfile.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/spellfile.vim rename to software/vim/share/vim/vim92/autoload/spellfile.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/sqlcomplete.vim b/software/vim/share/vim/vim92/autoload/sqlcomplete.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/sqlcomplete.vim rename to software/vim/share/vim/vim92/autoload/sqlcomplete.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/syntaxcomplete.vim b/software/vim/share/vim/vim92/autoload/syntaxcomplete.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/syntaxcomplete.vim rename to software/vim/share/vim/vim92/autoload/syntaxcomplete.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/tar.vim b/software/vim/share/vim/vim92/autoload/tar.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/tar.vim rename to software/vim/share/vim/vim92/autoload/tar.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/tohtml.vim b/software/vim/share/vim/vim92/autoload/tohtml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/tohtml.vim rename to software/vim/share/vim/vim92/autoload/tohtml.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/typeset.vim b/software/vim/share/vim/vim92/autoload/typeset.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/typeset.vim rename to software/vim/share/vim/vim92/autoload/typeset.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/vimball.vim b/software/vim/share/vim/vim92/autoload/vimball.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/vimball.vim rename to software/vim/share/vim/vim92/autoload/vimball.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/xml/html32.vim b/software/vim/share/vim/vim92/autoload/xml/html32.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/xml/html32.vim rename to software/vim/share/vim/vim92/autoload/xml/html32.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/xml/html401f.vim b/software/vim/share/vim/vim92/autoload/xml/html401f.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/xml/html401f.vim rename to software/vim/share/vim/vim92/autoload/xml/html401f.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/xml/html401s.vim b/software/vim/share/vim/vim92/autoload/xml/html401s.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/xml/html401s.vim rename to software/vim/share/vim/vim92/autoload/xml/html401s.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/xml/html401t.vim b/software/vim/share/vim/vim92/autoload/xml/html401t.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/xml/html401t.vim rename to software/vim/share/vim/vim92/autoload/xml/html401t.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/xml/html40f.vim b/software/vim/share/vim/vim92/autoload/xml/html40f.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/xml/html40f.vim rename to software/vim/share/vim/vim92/autoload/xml/html40f.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/xml/html40s.vim b/software/vim/share/vim/vim92/autoload/xml/html40s.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/xml/html40s.vim rename to software/vim/share/vim/vim92/autoload/xml/html40s.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/xml/html40t.vim b/software/vim/share/vim/vim92/autoload/xml/html40t.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/xml/html40t.vim rename to software/vim/share/vim/vim92/autoload/xml/html40t.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/xml/xhtml10f.vim b/software/vim/share/vim/vim92/autoload/xml/xhtml10f.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/xml/xhtml10f.vim rename to software/vim/share/vim/vim92/autoload/xml/xhtml10f.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/xml/xhtml10s.vim b/software/vim/share/vim/vim92/autoload/xml/xhtml10s.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/xml/xhtml10s.vim rename to software/vim/share/vim/vim92/autoload/xml/xhtml10s.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/xml/xhtml10t.vim b/software/vim/share/vim/vim92/autoload/xml/xhtml10t.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/xml/xhtml10t.vim rename to software/vim/share/vim/vim92/autoload/xml/xhtml10t.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/xml/xhtml11.vim b/software/vim/share/vim/vim92/autoload/xml/xhtml11.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/xml/xhtml11.vim rename to software/vim/share/vim/vim92/autoload/xml/xhtml11.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/xml/xsd.vim b/software/vim/share/vim/vim92/autoload/xml/xsd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/xml/xsd.vim rename to software/vim/share/vim/vim92/autoload/xml/xsd.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/xml/xsl.vim b/software/vim/share/vim/vim92/autoload/xml/xsl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/xml/xsl.vim rename to software/vim/share/vim/vim92/autoload/xml/xsl.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/xmlcomplete.vim b/software/vim/share/vim/vim92/autoload/xmlcomplete.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/xmlcomplete.vim rename to software/vim/share/vim/vim92/autoload/xmlcomplete.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/xmlformat.vim b/software/vim/share/vim/vim92/autoload/xmlformat.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/xmlformat.vim rename to software/vim/share/vim/vim92/autoload/xmlformat.vim diff --git a/registry/software/vim/share/vim/vim92/autoload/zip.vim b/software/vim/share/vim/vim92/autoload/zip.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/autoload/zip.vim rename to software/vim/share/vim/vim92/autoload/zip.vim diff --git a/registry/software/vim/share/vim/vim92/bugreport.vim b/software/vim/share/vim/vim92/bugreport.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/bugreport.vim rename to software/vim/share/vim/vim92/bugreport.vim diff --git a/registry/software/vim/share/vim/vim92/colors/blue.vim b/software/vim/share/vim/vim92/colors/blue.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/blue.vim rename to software/vim/share/vim/vim92/colors/blue.vim diff --git a/registry/software/vim/share/vim/vim92/colors/darkblue.vim b/software/vim/share/vim/vim92/colors/darkblue.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/darkblue.vim rename to software/vim/share/vim/vim92/colors/darkblue.vim diff --git a/registry/software/vim/share/vim/vim92/colors/default.vim b/software/vim/share/vim/vim92/colors/default.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/default.vim rename to software/vim/share/vim/vim92/colors/default.vim diff --git a/registry/software/vim/share/vim/vim92/colors/delek.vim b/software/vim/share/vim/vim92/colors/delek.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/delek.vim rename to software/vim/share/vim/vim92/colors/delek.vim diff --git a/registry/software/vim/share/vim/vim92/colors/desert.vim b/software/vim/share/vim/vim92/colors/desert.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/desert.vim rename to software/vim/share/vim/vim92/colors/desert.vim diff --git a/registry/software/vim/share/vim/vim92/colors/elflord.vim b/software/vim/share/vim/vim92/colors/elflord.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/elflord.vim rename to software/vim/share/vim/vim92/colors/elflord.vim diff --git a/registry/software/vim/share/vim/vim92/colors/evening.vim b/software/vim/share/vim/vim92/colors/evening.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/evening.vim rename to software/vim/share/vim/vim92/colors/evening.vim diff --git a/registry/software/vim/share/vim/vim92/colors/habamax.vim b/software/vim/share/vim/vim92/colors/habamax.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/habamax.vim rename to software/vim/share/vim/vim92/colors/habamax.vim diff --git a/registry/software/vim/share/vim/vim92/colors/industry.vim b/software/vim/share/vim/vim92/colors/industry.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/industry.vim rename to software/vim/share/vim/vim92/colors/industry.vim diff --git a/registry/software/vim/share/vim/vim92/colors/koehler.vim b/software/vim/share/vim/vim92/colors/koehler.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/koehler.vim rename to software/vim/share/vim/vim92/colors/koehler.vim diff --git a/registry/software/vim/share/vim/vim92/colors/lists/csscolors.vim b/software/vim/share/vim/vim92/colors/lists/csscolors.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/lists/csscolors.vim rename to software/vim/share/vim/vim92/colors/lists/csscolors.vim diff --git a/registry/software/vim/share/vim/vim92/colors/lists/default.vim b/software/vim/share/vim/vim92/colors/lists/default.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/lists/default.vim rename to software/vim/share/vim/vim92/colors/lists/default.vim diff --git a/registry/software/vim/share/vim/vim92/colors/lunaperche.vim b/software/vim/share/vim/vim92/colors/lunaperche.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/lunaperche.vim rename to software/vim/share/vim/vim92/colors/lunaperche.vim diff --git a/registry/software/vim/share/vim/vim92/colors/morning.vim b/software/vim/share/vim/vim92/colors/morning.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/morning.vim rename to software/vim/share/vim/vim92/colors/morning.vim diff --git a/registry/software/vim/share/vim/vim92/colors/murphy.vim b/software/vim/share/vim/vim92/colors/murphy.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/murphy.vim rename to software/vim/share/vim/vim92/colors/murphy.vim diff --git a/registry/software/vim/share/vim/vim92/colors/pablo.vim b/software/vim/share/vim/vim92/colors/pablo.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/pablo.vim rename to software/vim/share/vim/vim92/colors/pablo.vim diff --git a/registry/software/vim/share/vim/vim92/colors/peachpuff.vim b/software/vim/share/vim/vim92/colors/peachpuff.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/peachpuff.vim rename to software/vim/share/vim/vim92/colors/peachpuff.vim diff --git a/registry/software/vim/share/vim/vim92/colors/quiet.vim b/software/vim/share/vim/vim92/colors/quiet.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/quiet.vim rename to software/vim/share/vim/vim92/colors/quiet.vim diff --git a/registry/software/vim/share/vim/vim92/colors/ron.vim b/software/vim/share/vim/vim92/colors/ron.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/ron.vim rename to software/vim/share/vim/vim92/colors/ron.vim diff --git a/registry/software/vim/share/vim/vim92/colors/shine.vim b/software/vim/share/vim/vim92/colors/shine.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/shine.vim rename to software/vim/share/vim/vim92/colors/shine.vim diff --git a/registry/software/vim/share/vim/vim92/colors/slate.vim b/software/vim/share/vim/vim92/colors/slate.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/slate.vim rename to software/vim/share/vim/vim92/colors/slate.vim diff --git a/registry/software/vim/share/vim/vim92/colors/tools/check_colors.vim b/software/vim/share/vim/vim92/colors/tools/check_colors.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/tools/check_colors.vim rename to software/vim/share/vim/vim92/colors/tools/check_colors.vim diff --git a/registry/software/vim/share/vim/vim92/colors/torte.vim b/software/vim/share/vim/vim92/colors/torte.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/torte.vim rename to software/vim/share/vim/vim92/colors/torte.vim diff --git a/registry/software/vim/share/vim/vim92/colors/zellner.vim b/software/vim/share/vim/vim92/colors/zellner.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/colors/zellner.vim rename to software/vim/share/vim/vim92/colors/zellner.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/ant.vim b/software/vim/share/vim/vim92/compiler/ant.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/ant.vim rename to software/vim/share/vim/vim92/compiler/ant.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/bcc.vim b/software/vim/share/vim/vim92/compiler/bcc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/bcc.vim rename to software/vim/share/vim/vim92/compiler/bcc.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/bdf.vim b/software/vim/share/vim/vim92/compiler/bdf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/bdf.vim rename to software/vim/share/vim/vim92/compiler/bdf.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/cargo.vim b/software/vim/share/vim/vim92/compiler/cargo.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/cargo.vim rename to software/vim/share/vim/vim92/compiler/cargo.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/checkstyle.vim b/software/vim/share/vim/vim92/compiler/checkstyle.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/checkstyle.vim rename to software/vim/share/vim/vim92/compiler/checkstyle.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/cm3.vim b/software/vim/share/vim/vim92/compiler/cm3.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/cm3.vim rename to software/vim/share/vim/vim92/compiler/cm3.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/context.vim b/software/vim/share/vim/vim92/compiler/context.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/context.vim rename to software/vim/share/vim/vim92/compiler/context.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/cs.vim b/software/vim/share/vim/vim92/compiler/cs.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/cs.vim rename to software/vim/share/vim/vim92/compiler/cs.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/csslint.vim b/software/vim/share/vim/vim92/compiler/csslint.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/csslint.vim rename to software/vim/share/vim/vim92/compiler/csslint.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/cucumber.vim b/software/vim/share/vim/vim92/compiler/cucumber.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/cucumber.vim rename to software/vim/share/vim/vim92/compiler/cucumber.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/dart.vim b/software/vim/share/vim/vim92/compiler/dart.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/dart.vim rename to software/vim/share/vim/vim92/compiler/dart.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/dart2js.vim b/software/vim/share/vim/vim92/compiler/dart2js.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/dart2js.vim rename to software/vim/share/vim/vim92/compiler/dart2js.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/dart2native.vim b/software/vim/share/vim/vim92/compiler/dart2native.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/dart2native.vim rename to software/vim/share/vim/vim92/compiler/dart2native.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/dartanalyser.vim b/software/vim/share/vim/vim92/compiler/dartanalyser.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/dartanalyser.vim rename to software/vim/share/vim/vim92/compiler/dartanalyser.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/dartdevc.vim b/software/vim/share/vim/vim92/compiler/dartdevc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/dartdevc.vim rename to software/vim/share/vim/vim92/compiler/dartdevc.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/dartdoc.vim b/software/vim/share/vim/vim92/compiler/dartdoc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/dartdoc.vim rename to software/vim/share/vim/vim92/compiler/dartdoc.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/dartfmt.vim b/software/vim/share/vim/vim92/compiler/dartfmt.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/dartfmt.vim rename to software/vim/share/vim/vim92/compiler/dartfmt.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/decada.vim b/software/vim/share/vim/vim92/compiler/decada.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/decada.vim rename to software/vim/share/vim/vim92/compiler/decada.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/dot.vim b/software/vim/share/vim/vim92/compiler/dot.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/dot.vim rename to software/vim/share/vim/vim92/compiler/dot.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/dotnet.vim b/software/vim/share/vim/vim92/compiler/dotnet.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/dotnet.vim rename to software/vim/share/vim/vim92/compiler/dotnet.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/erlang.vim b/software/vim/share/vim/vim92/compiler/erlang.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/erlang.vim rename to software/vim/share/vim/vim92/compiler/erlang.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/eruby.vim b/software/vim/share/vim/vim92/compiler/eruby.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/eruby.vim rename to software/vim/share/vim/vim92/compiler/eruby.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/eslint.vim b/software/vim/share/vim/vim92/compiler/eslint.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/eslint.vim rename to software/vim/share/vim/vim92/compiler/eslint.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/fbc.vim b/software/vim/share/vim/vim92/compiler/fbc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/fbc.vim rename to software/vim/share/vim/vim92/compiler/fbc.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/fortran_F.vim b/software/vim/share/vim/vim92/compiler/fortran_F.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/fortran_F.vim rename to software/vim/share/vim/vim92/compiler/fortran_F.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/fortran_cv.vim b/software/vim/share/vim/vim92/compiler/fortran_cv.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/fortran_cv.vim rename to software/vim/share/vim/vim92/compiler/fortran_cv.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/fortran_elf90.vim b/software/vim/share/vim/vim92/compiler/fortran_elf90.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/fortran_elf90.vim rename to software/vim/share/vim/vim92/compiler/fortran_elf90.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/fortran_g77.vim b/software/vim/share/vim/vim92/compiler/fortran_g77.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/fortran_g77.vim rename to software/vim/share/vim/vim92/compiler/fortran_g77.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/fortran_lf95.vim b/software/vim/share/vim/vim92/compiler/fortran_lf95.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/fortran_lf95.vim rename to software/vim/share/vim/vim92/compiler/fortran_lf95.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/fpc.vim b/software/vim/share/vim/vim92/compiler/fpc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/fpc.vim rename to software/vim/share/vim/vim92/compiler/fpc.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/g95.vim b/software/vim/share/vim/vim92/compiler/g95.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/g95.vim rename to software/vim/share/vim/vim92/compiler/g95.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/gawk.vim b/software/vim/share/vim/vim92/compiler/gawk.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/gawk.vim rename to software/vim/share/vim/vim92/compiler/gawk.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/gcc.vim b/software/vim/share/vim/vim92/compiler/gcc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/gcc.vim rename to software/vim/share/vim/vim92/compiler/gcc.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/gfortran.vim b/software/vim/share/vim/vim92/compiler/gfortran.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/gfortran.vim rename to software/vim/share/vim/vim92/compiler/gfortran.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/ghc.vim b/software/vim/share/vim/vim92/compiler/ghc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/ghc.vim rename to software/vim/share/vim/vim92/compiler/ghc.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/gjs.vim b/software/vim/share/vim/vim92/compiler/gjs.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/gjs.vim rename to software/vim/share/vim/vim92/compiler/gjs.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/gnat.vim b/software/vim/share/vim/vim92/compiler/gnat.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/gnat.vim rename to software/vim/share/vim/vim92/compiler/gnat.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/go.vim b/software/vim/share/vim/vim92/compiler/go.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/go.vim rename to software/vim/share/vim/vim92/compiler/go.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/haml.vim b/software/vim/share/vim/vim92/compiler/haml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/haml.vim rename to software/vim/share/vim/vim92/compiler/haml.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/hare.vim b/software/vim/share/vim/vim92/compiler/hare.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/hare.vim rename to software/vim/share/vim/vim92/compiler/hare.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/hp_acc.vim b/software/vim/share/vim/vim92/compiler/hp_acc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/hp_acc.vim rename to software/vim/share/vim/vim92/compiler/hp_acc.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/icc.vim b/software/vim/share/vim/vim92/compiler/icc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/icc.vim rename to software/vim/share/vim/vim92/compiler/icc.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/icon.vim b/software/vim/share/vim/vim92/compiler/icon.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/icon.vim rename to software/vim/share/vim/vim92/compiler/icon.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/ifort.vim b/software/vim/share/vim/vim92/compiler/ifort.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/ifort.vim rename to software/vim/share/vim/vim92/compiler/ifort.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/intel.vim b/software/vim/share/vim/vim92/compiler/intel.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/intel.vim rename to software/vim/share/vim/vim92/compiler/intel.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/irix5_c.vim b/software/vim/share/vim/vim92/compiler/irix5_c.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/irix5_c.vim rename to software/vim/share/vim/vim92/compiler/irix5_c.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/irix5_cpp.vim b/software/vim/share/vim/vim92/compiler/irix5_cpp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/irix5_cpp.vim rename to software/vim/share/vim/vim92/compiler/irix5_cpp.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/javac.vim b/software/vim/share/vim/vim92/compiler/javac.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/javac.vim rename to software/vim/share/vim/vim92/compiler/javac.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/jest.vim b/software/vim/share/vim/vim92/compiler/jest.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/jest.vim rename to software/vim/share/vim/vim92/compiler/jest.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/jikes.vim b/software/vim/share/vim/vim92/compiler/jikes.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/jikes.vim rename to software/vim/share/vim/vim92/compiler/jikes.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/jjs.vim b/software/vim/share/vim/vim92/compiler/jjs.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/jjs.vim rename to software/vim/share/vim/vim92/compiler/jjs.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/jshint.vim b/software/vim/share/vim/vim92/compiler/jshint.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/jshint.vim rename to software/vim/share/vim/vim92/compiler/jshint.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/jsonlint.vim b/software/vim/share/vim/vim92/compiler/jsonlint.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/jsonlint.vim rename to software/vim/share/vim/vim92/compiler/jsonlint.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/mcs.vim b/software/vim/share/vim/vim92/compiler/mcs.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/mcs.vim rename to software/vim/share/vim/vim92/compiler/mcs.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/mips_c.vim b/software/vim/share/vim/vim92/compiler/mips_c.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/mips_c.vim rename to software/vim/share/vim/vim92/compiler/mips_c.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/mipspro_c89.vim b/software/vim/share/vim/vim92/compiler/mipspro_c89.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/mipspro_c89.vim rename to software/vim/share/vim/vim92/compiler/mipspro_c89.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/mipspro_cpp.vim b/software/vim/share/vim/vim92/compiler/mipspro_cpp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/mipspro_cpp.vim rename to software/vim/share/vim/vim92/compiler/mipspro_cpp.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/modelsim_vcom.vim b/software/vim/share/vim/vim92/compiler/modelsim_vcom.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/modelsim_vcom.vim rename to software/vim/share/vim/vim92/compiler/modelsim_vcom.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/msbuild.vim b/software/vim/share/vim/vim92/compiler/msbuild.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/msbuild.vim rename to software/vim/share/vim/vim92/compiler/msbuild.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/msvc.vim b/software/vim/share/vim/vim92/compiler/msvc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/msvc.vim rename to software/vim/share/vim/vim92/compiler/msvc.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/neato.vim b/software/vim/share/vim/vim92/compiler/neato.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/neato.vim rename to software/vim/share/vim/vim92/compiler/neato.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/ocaml.vim b/software/vim/share/vim/vim92/compiler/ocaml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/ocaml.vim rename to software/vim/share/vim/vim92/compiler/ocaml.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/onsgmls.vim b/software/vim/share/vim/vim92/compiler/onsgmls.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/onsgmls.vim rename to software/vim/share/vim/vim92/compiler/onsgmls.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/pbx.vim b/software/vim/share/vim/vim92/compiler/pbx.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/pbx.vim rename to software/vim/share/vim/vim92/compiler/pbx.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/perl.vim b/software/vim/share/vim/vim92/compiler/perl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/perl.vim rename to software/vim/share/vim/vim92/compiler/perl.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/perlcritic.vim b/software/vim/share/vim/vim92/compiler/perlcritic.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/perlcritic.vim rename to software/vim/share/vim/vim92/compiler/perlcritic.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/php.vim b/software/vim/share/vim/vim92/compiler/php.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/php.vim rename to software/vim/share/vim/vim92/compiler/php.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/podchecker.vim b/software/vim/share/vim/vim92/compiler/podchecker.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/podchecker.vim rename to software/vim/share/vim/vim92/compiler/podchecker.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/powershell.vim b/software/vim/share/vim/vim92/compiler/powershell.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/powershell.vim rename to software/vim/share/vim/vim92/compiler/powershell.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/pylint.vim b/software/vim/share/vim/vim92/compiler/pylint.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/pylint.vim rename to software/vim/share/vim/vim92/compiler/pylint.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/pyunit.vim b/software/vim/share/vim/vim92/compiler/pyunit.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/pyunit.vim rename to software/vim/share/vim/vim92/compiler/pyunit.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/raco.vim b/software/vim/share/vim/vim92/compiler/raco.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/raco.vim rename to software/vim/share/vim/vim92/compiler/raco.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/racomake.vim b/software/vim/share/vim/vim92/compiler/racomake.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/racomake.vim rename to software/vim/share/vim/vim92/compiler/racomake.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/racosetup.vim b/software/vim/share/vim/vim92/compiler/racosetup.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/racosetup.vim rename to software/vim/share/vim/vim92/compiler/racosetup.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/racotest.vim b/software/vim/share/vim/vim92/compiler/racotest.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/racotest.vim rename to software/vim/share/vim/vim92/compiler/racotest.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/rake.vim b/software/vim/share/vim/vim92/compiler/rake.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/rake.vim rename to software/vim/share/vim/vim92/compiler/rake.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/rhino.vim b/software/vim/share/vim/vim92/compiler/rhino.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/rhino.vim rename to software/vim/share/vim/vim92/compiler/rhino.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/rspec.vim b/software/vim/share/vim/vim92/compiler/rspec.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/rspec.vim rename to software/vim/share/vim/vim92/compiler/rspec.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/rst.vim b/software/vim/share/vim/vim92/compiler/rst.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/rst.vim rename to software/vim/share/vim/vim92/compiler/rst.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/rubocop.vim b/software/vim/share/vim/vim92/compiler/rubocop.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/rubocop.vim rename to software/vim/share/vim/vim92/compiler/rubocop.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/ruby.vim b/software/vim/share/vim/vim92/compiler/ruby.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/ruby.vim rename to software/vim/share/vim/vim92/compiler/ruby.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/rubyunit.vim b/software/vim/share/vim/vim92/compiler/rubyunit.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/rubyunit.vim rename to software/vim/share/vim/vim92/compiler/rubyunit.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/rustc.vim b/software/vim/share/vim/vim92/compiler/rustc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/rustc.vim rename to software/vim/share/vim/vim92/compiler/rustc.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/sass.vim b/software/vim/share/vim/vim92/compiler/sass.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/sass.vim rename to software/vim/share/vim/vim92/compiler/sass.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/scdoc.vim b/software/vim/share/vim/vim92/compiler/scdoc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/scdoc.vim rename to software/vim/share/vim/vim92/compiler/scdoc.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/se.vim b/software/vim/share/vim/vim92/compiler/se.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/se.vim rename to software/vim/share/vim/vim92/compiler/se.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/shellcheck.vim b/software/vim/share/vim/vim92/compiler/shellcheck.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/shellcheck.vim rename to software/vim/share/vim/vim92/compiler/shellcheck.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/sml.vim b/software/vim/share/vim/vim92/compiler/sml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/sml.vim rename to software/vim/share/vim/vim92/compiler/sml.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/spectral.vim b/software/vim/share/vim/vim92/compiler/spectral.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/spectral.vim rename to software/vim/share/vim/vim92/compiler/spectral.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/splint.vim b/software/vim/share/vim/vim92/compiler/splint.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/splint.vim rename to software/vim/share/vim/vim92/compiler/splint.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/stack.vim b/software/vim/share/vim/vim92/compiler/stack.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/stack.vim rename to software/vim/share/vim/vim92/compiler/stack.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/standard.vim b/software/vim/share/vim/vim92/compiler/standard.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/standard.vim rename to software/vim/share/vim/vim92/compiler/standard.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/stylelint.vim b/software/vim/share/vim/vim92/compiler/stylelint.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/stylelint.vim rename to software/vim/share/vim/vim92/compiler/stylelint.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/tcl.vim b/software/vim/share/vim/vim92/compiler/tcl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/tcl.vim rename to software/vim/share/vim/vim92/compiler/tcl.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/tex.vim b/software/vim/share/vim/vim92/compiler/tex.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/tex.vim rename to software/vim/share/vim/vim92/compiler/tex.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/tidy.vim b/software/vim/share/vim/vim92/compiler/tidy.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/tidy.vim rename to software/vim/share/vim/vim92/compiler/tidy.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/ts-node.vim b/software/vim/share/vim/vim92/compiler/ts-node.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/ts-node.vim rename to software/vim/share/vim/vim92/compiler/ts-node.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/tsc.vim b/software/vim/share/vim/vim92/compiler/tsc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/tsc.vim rename to software/vim/share/vim/vim92/compiler/tsc.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/typedoc.vim b/software/vim/share/vim/vim92/compiler/typedoc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/typedoc.vim rename to software/vim/share/vim/vim92/compiler/typedoc.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/xbuild.vim b/software/vim/share/vim/vim92/compiler/xbuild.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/xbuild.vim rename to software/vim/share/vim/vim92/compiler/xbuild.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/xmllint.vim b/software/vim/share/vim/vim92/compiler/xmllint.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/xmllint.vim rename to software/vim/share/vim/vim92/compiler/xmllint.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/xmlwf.vim b/software/vim/share/vim/vim92/compiler/xmlwf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/xmlwf.vim rename to software/vim/share/vim/vim92/compiler/xmlwf.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/xo.vim b/software/vim/share/vim/vim92/compiler/xo.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/xo.vim rename to software/vim/share/vim/vim92/compiler/xo.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/yamllint.vim b/software/vim/share/vim/vim92/compiler/yamllint.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/yamllint.vim rename to software/vim/share/vim/vim92/compiler/yamllint.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/zig.vim b/software/vim/share/vim/vim92/compiler/zig.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/zig.vim rename to software/vim/share/vim/vim92/compiler/zig.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/zig_build.vim b/software/vim/share/vim/vim92/compiler/zig_build.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/zig_build.vim rename to software/vim/share/vim/vim92/compiler/zig_build.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/zig_build_exe.vim b/software/vim/share/vim/vim92/compiler/zig_build_exe.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/zig_build_exe.vim rename to software/vim/share/vim/vim92/compiler/zig_build_exe.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/zig_test.vim b/software/vim/share/vim/vim92/compiler/zig_test.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/zig_test.vim rename to software/vim/share/vim/vim92/compiler/zig_test.vim diff --git a/registry/software/vim/share/vim/vim92/compiler/zsh.vim b/software/vim/share/vim/vim92/compiler/zsh.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/compiler/zsh.vim rename to software/vim/share/vim/vim92/compiler/zsh.vim diff --git a/registry/software/vim/share/vim/vim92/debian.vim b/software/vim/share/vim/vim92/debian.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/debian.vim rename to software/vim/share/vim/vim92/debian.vim diff --git a/registry/software/vim/share/vim/vim92/defaults.vim b/software/vim/share/vim/vim92/defaults.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/defaults.vim rename to software/vim/share/vim/vim92/defaults.vim diff --git a/registry/software/vim/share/vim/vim92/delmenu.vim b/software/vim/share/vim/vim92/delmenu.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/delmenu.vim rename to software/vim/share/vim/vim92/delmenu.vim diff --git a/registry/software/vim/share/vim/vim92/evim.vim b/software/vim/share/vim/vim92/evim.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/evim.vim rename to software/vim/share/vim/vim92/evim.vim diff --git a/registry/software/vim/share/vim/vim92/filetype.vim b/software/vim/share/vim/vim92/filetype.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/filetype.vim rename to software/vim/share/vim/vim92/filetype.vim diff --git a/registry/software/vim/share/vim/vim92/ftoff.vim b/software/vim/share/vim/vim92/ftoff.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftoff.vim rename to software/vim/share/vim/vim92/ftoff.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin.vim b/software/vim/share/vim/vim92/ftplugin.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin.vim rename to software/vim/share/vim/vim92/ftplugin.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/8th.vim b/software/vim/share/vim/vim92/ftplugin/8th.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/8th.vim rename to software/vim/share/vim/vim92/ftplugin/8th.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/a2ps.vim b/software/vim/share/vim/vim92/ftplugin/a2ps.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/a2ps.vim rename to software/vim/share/vim/vim92/ftplugin/a2ps.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/aap.vim b/software/vim/share/vim/vim92/ftplugin/aap.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/aap.vim rename to software/vim/share/vim/vim92/ftplugin/aap.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/abap.vim b/software/vim/share/vim/vim92/ftplugin/abap.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/abap.vim rename to software/vim/share/vim/vim92/ftplugin/abap.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/abaqus.vim b/software/vim/share/vim/vim92/ftplugin/abaqus.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/abaqus.vim rename to software/vim/share/vim/vim92/ftplugin/abaqus.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/ada.vim b/software/vim/share/vim/vim92/ftplugin/ada.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/ada.vim rename to software/vim/share/vim/vim92/ftplugin/ada.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/alsaconf.vim b/software/vim/share/vim/vim92/ftplugin/alsaconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/alsaconf.vim rename to software/vim/share/vim/vim92/ftplugin/alsaconf.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/ant.vim b/software/vim/share/vim/vim92/ftplugin/ant.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/ant.vim rename to software/vim/share/vim/vim92/ftplugin/ant.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/apache.vim b/software/vim/share/vim/vim92/ftplugin/apache.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/apache.vim rename to software/vim/share/vim/vim92/ftplugin/apache.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/arch.vim b/software/vim/share/vim/vim92/ftplugin/arch.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/arch.vim rename to software/vim/share/vim/vim92/ftplugin/arch.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/art.vim b/software/vim/share/vim/vim92/ftplugin/art.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/art.vim rename to software/vim/share/vim/vim92/ftplugin/art.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/asm.vim b/software/vim/share/vim/vim92/ftplugin/asm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/asm.vim rename to software/vim/share/vim/vim92/ftplugin/asm.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/aspvbs.vim b/software/vim/share/vim/vim92/ftplugin/aspvbs.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/aspvbs.vim rename to software/vim/share/vim/vim92/ftplugin/aspvbs.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/automake.vim b/software/vim/share/vim/vim92/ftplugin/automake.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/automake.vim rename to software/vim/share/vim/vim92/ftplugin/automake.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/awk.vim b/software/vim/share/vim/vim92/ftplugin/awk.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/awk.vim rename to software/vim/share/vim/vim92/ftplugin/awk.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/bash.vim b/software/vim/share/vim/vim92/ftplugin/bash.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/bash.vim rename to software/vim/share/vim/vim92/ftplugin/bash.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/basic.vim b/software/vim/share/vim/vim92/ftplugin/basic.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/basic.vim rename to software/vim/share/vim/vim92/ftplugin/basic.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/bdf.vim b/software/vim/share/vim/vim92/ftplugin/bdf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/bdf.vim rename to software/vim/share/vim/vim92/ftplugin/bdf.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/bitbake.vim b/software/vim/share/vim/vim92/ftplugin/bitbake.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/bitbake.vim rename to software/vim/share/vim/vim92/ftplugin/bitbake.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/bst.vim b/software/vim/share/vim/vim92/ftplugin/bst.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/bst.vim rename to software/vim/share/vim/vim92/ftplugin/bst.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/btm.vim b/software/vim/share/vim/vim92/ftplugin/btm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/btm.vim rename to software/vim/share/vim/vim92/ftplugin/btm.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/bzl.vim b/software/vim/share/vim/vim92/ftplugin/bzl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/bzl.vim rename to software/vim/share/vim/vim92/ftplugin/bzl.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/c.vim b/software/vim/share/vim/vim92/ftplugin/c.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/c.vim rename to software/vim/share/vim/vim92/ftplugin/c.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/calendar.vim b/software/vim/share/vim/vim92/ftplugin/calendar.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/calendar.vim rename to software/vim/share/vim/vim92/ftplugin/calendar.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/cdrdaoconf.vim b/software/vim/share/vim/vim92/ftplugin/cdrdaoconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/cdrdaoconf.vim rename to software/vim/share/vim/vim92/ftplugin/cdrdaoconf.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/cfg.vim b/software/vim/share/vim/vim92/ftplugin/cfg.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/cfg.vim rename to software/vim/share/vim/vim92/ftplugin/cfg.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/ch.vim b/software/vim/share/vim/vim92/ftplugin/ch.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/ch.vim rename to software/vim/share/vim/vim92/ftplugin/ch.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/changelog.vim b/software/vim/share/vim/vim92/ftplugin/changelog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/changelog.vim rename to software/vim/share/vim/vim92/ftplugin/changelog.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/chatito.vim b/software/vim/share/vim/vim92/ftplugin/chatito.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/chatito.vim rename to software/vim/share/vim/vim92/ftplugin/chatito.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/chicken.vim b/software/vim/share/vim/vim92/ftplugin/chicken.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/chicken.vim rename to software/vim/share/vim/vim92/ftplugin/chicken.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/clojure.vim b/software/vim/share/vim/vim92/ftplugin/clojure.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/clojure.vim rename to software/vim/share/vim/vim92/ftplugin/clojure.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/cmake.vim b/software/vim/share/vim/vim92/ftplugin/cmake.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/cmake.vim rename to software/vim/share/vim/vim92/ftplugin/cmake.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/cobol.vim b/software/vim/share/vim/vim92/ftplugin/cobol.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/cobol.vim rename to software/vim/share/vim/vim92/ftplugin/cobol.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/conf.vim b/software/vim/share/vim/vim92/ftplugin/conf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/conf.vim rename to software/vim/share/vim/vim92/ftplugin/conf.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/config.vim b/software/vim/share/vim/vim92/ftplugin/config.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/config.vim rename to software/vim/share/vim/vim92/ftplugin/config.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/confini.vim b/software/vim/share/vim/vim92/ftplugin/confini.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/confini.vim rename to software/vim/share/vim/vim92/ftplugin/confini.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/context.vim b/software/vim/share/vim/vim92/ftplugin/context.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/context.vim rename to software/vim/share/vim/vim92/ftplugin/context.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/cpp.vim b/software/vim/share/vim/vim92/ftplugin/cpp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/cpp.vim rename to software/vim/share/vim/vim92/ftplugin/cpp.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/crm.vim b/software/vim/share/vim/vim92/ftplugin/crm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/crm.vim rename to software/vim/share/vim/vim92/ftplugin/crm.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/crontab.vim b/software/vim/share/vim/vim92/ftplugin/crontab.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/crontab.vim rename to software/vim/share/vim/vim92/ftplugin/crontab.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/cs.vim b/software/vim/share/vim/vim92/ftplugin/cs.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/cs.vim rename to software/vim/share/vim/vim92/ftplugin/cs.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/csc.vim b/software/vim/share/vim/vim92/ftplugin/csc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/csc.vim rename to software/vim/share/vim/vim92/ftplugin/csc.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/csh.vim b/software/vim/share/vim/vim92/ftplugin/csh.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/csh.vim rename to software/vim/share/vim/vim92/ftplugin/csh.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/css.vim b/software/vim/share/vim/vim92/ftplugin/css.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/css.vim rename to software/vim/share/vim/vim92/ftplugin/css.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/cucumber.vim b/software/vim/share/vim/vim92/ftplugin/cucumber.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/cucumber.vim rename to software/vim/share/vim/vim92/ftplugin/cucumber.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/cvsrc.vim b/software/vim/share/vim/vim92/ftplugin/cvsrc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/cvsrc.vim rename to software/vim/share/vim/vim92/ftplugin/cvsrc.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/debchangelog.vim b/software/vim/share/vim/vim92/ftplugin/debchangelog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/debchangelog.vim rename to software/vim/share/vim/vim92/ftplugin/debchangelog.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/debcontrol.vim b/software/vim/share/vim/vim92/ftplugin/debcontrol.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/debcontrol.vim rename to software/vim/share/vim/vim92/ftplugin/debcontrol.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/denyhosts.vim b/software/vim/share/vim/vim92/ftplugin/denyhosts.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/denyhosts.vim rename to software/vim/share/vim/vim92/ftplugin/denyhosts.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/desktop.vim b/software/vim/share/vim/vim92/ftplugin/desktop.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/desktop.vim rename to software/vim/share/vim/vim92/ftplugin/desktop.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/dictconf.vim b/software/vim/share/vim/vim92/ftplugin/dictconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/dictconf.vim rename to software/vim/share/vim/vim92/ftplugin/dictconf.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/dictdconf.vim b/software/vim/share/vim/vim92/ftplugin/dictdconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/dictdconf.vim rename to software/vim/share/vim/vim92/ftplugin/dictdconf.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/diff.vim b/software/vim/share/vim/vim92/ftplugin/diff.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/diff.vim rename to software/vim/share/vim/vim92/ftplugin/diff.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/dircolors.vim b/software/vim/share/vim/vim92/ftplugin/dircolors.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/dircolors.vim rename to software/vim/share/vim/vim92/ftplugin/dircolors.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/docbk.vim b/software/vim/share/vim/vim92/ftplugin/docbk.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/docbk.vim rename to software/vim/share/vim/vim92/ftplugin/docbk.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/dockerfile.vim b/software/vim/share/vim/vim92/ftplugin/dockerfile.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/dockerfile.vim rename to software/vim/share/vim/vim92/ftplugin/dockerfile.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/dosbatch.vim b/software/vim/share/vim/vim92/ftplugin/dosbatch.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/dosbatch.vim rename to software/vim/share/vim/vim92/ftplugin/dosbatch.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/dosini.vim b/software/vim/share/vim/vim92/ftplugin/dosini.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/dosini.vim rename to software/vim/share/vim/vim92/ftplugin/dosini.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/dtd.vim b/software/vim/share/vim/vim92/ftplugin/dtd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/dtd.vim rename to software/vim/share/vim/vim92/ftplugin/dtd.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/dtrace.vim b/software/vim/share/vim/vim92/ftplugin/dtrace.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/dtrace.vim rename to software/vim/share/vim/vim92/ftplugin/dtrace.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/dune.vim b/software/vim/share/vim/vim92/ftplugin/dune.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/dune.vim rename to software/vim/share/vim/vim92/ftplugin/dune.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/eiffel.vim b/software/vim/share/vim/vim92/ftplugin/eiffel.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/eiffel.vim rename to software/vim/share/vim/vim92/ftplugin/eiffel.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/elinks.vim b/software/vim/share/vim/vim92/ftplugin/elinks.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/elinks.vim rename to software/vim/share/vim/vim92/ftplugin/elinks.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/elixir.vim b/software/vim/share/vim/vim92/ftplugin/elixir.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/elixir.vim rename to software/vim/share/vim/vim92/ftplugin/elixir.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/elm.vim b/software/vim/share/vim/vim92/ftplugin/elm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/elm.vim rename to software/vim/share/vim/vim92/ftplugin/elm.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/erlang.vim b/software/vim/share/vim/vim92/ftplugin/erlang.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/erlang.vim rename to software/vim/share/vim/vim92/ftplugin/erlang.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/eruby.vim b/software/vim/share/vim/vim92/ftplugin/eruby.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/eruby.vim rename to software/vim/share/vim/vim92/ftplugin/eruby.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/eterm.vim b/software/vim/share/vim/vim92/ftplugin/eterm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/eterm.vim rename to software/vim/share/vim/vim92/ftplugin/eterm.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/expect.vim b/software/vim/share/vim/vim92/ftplugin/expect.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/expect.vim rename to software/vim/share/vim/vim92/ftplugin/expect.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/falcon.vim b/software/vim/share/vim/vim92/ftplugin/falcon.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/falcon.vim rename to software/vim/share/vim/vim92/ftplugin/falcon.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/fennel.vim b/software/vim/share/vim/vim92/ftplugin/fennel.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/fennel.vim rename to software/vim/share/vim/vim92/ftplugin/fennel.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/fetchmail.vim b/software/vim/share/vim/vim92/ftplugin/fetchmail.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/fetchmail.vim rename to software/vim/share/vim/vim92/ftplugin/fetchmail.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/fish.vim b/software/vim/share/vim/vim92/ftplugin/fish.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/fish.vim rename to software/vim/share/vim/vim92/ftplugin/fish.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/flexwiki.vim b/software/vim/share/vim/vim92/ftplugin/flexwiki.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/flexwiki.vim rename to software/vim/share/vim/vim92/ftplugin/flexwiki.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/fortran.vim b/software/vim/share/vim/vim92/ftplugin/fortran.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/fortran.vim rename to software/vim/share/vim/vim92/ftplugin/fortran.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/fpcmake.vim b/software/vim/share/vim/vim92/ftplugin/fpcmake.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/fpcmake.vim rename to software/vim/share/vim/vim92/ftplugin/fpcmake.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/framescript.vim b/software/vim/share/vim/vim92/ftplugin/framescript.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/framescript.vim rename to software/vim/share/vim/vim92/ftplugin/framescript.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/freebasic.vim b/software/vim/share/vim/vim92/ftplugin/freebasic.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/freebasic.vim rename to software/vim/share/vim/vim92/ftplugin/freebasic.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/fstab.vim b/software/vim/share/vim/vim92/ftplugin/fstab.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/fstab.vim rename to software/vim/share/vim/vim92/ftplugin/fstab.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/fvwm.vim b/software/vim/share/vim/vim92/ftplugin/fvwm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/fvwm.vim rename to software/vim/share/vim/vim92/ftplugin/fvwm.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/gdb.vim b/software/vim/share/vim/vim92/ftplugin/gdb.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/gdb.vim rename to software/vim/share/vim/vim92/ftplugin/gdb.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/gdscript.vim b/software/vim/share/vim/vim92/ftplugin/gdscript.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/gdscript.vim rename to software/vim/share/vim/vim92/ftplugin/gdscript.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/gdshader.vim b/software/vim/share/vim/vim92/ftplugin/gdshader.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/gdshader.vim rename to software/vim/share/vim/vim92/ftplugin/gdshader.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/gitattributes.vim b/software/vim/share/vim/vim92/ftplugin/gitattributes.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/gitattributes.vim rename to software/vim/share/vim/vim92/ftplugin/gitattributes.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/gitcommit.vim b/software/vim/share/vim/vim92/ftplugin/gitcommit.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/gitcommit.vim rename to software/vim/share/vim/vim92/ftplugin/gitcommit.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/gitconfig.vim b/software/vim/share/vim/vim92/ftplugin/gitconfig.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/gitconfig.vim rename to software/vim/share/vim/vim92/ftplugin/gitconfig.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/gitignore.vim b/software/vim/share/vim/vim92/ftplugin/gitignore.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/gitignore.vim rename to software/vim/share/vim/vim92/ftplugin/gitignore.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/gitrebase.vim b/software/vim/share/vim/vim92/ftplugin/gitrebase.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/gitrebase.vim rename to software/vim/share/vim/vim92/ftplugin/gitrebase.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/gitsendemail.vim b/software/vim/share/vim/vim92/ftplugin/gitsendemail.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/gitsendemail.vim rename to software/vim/share/vim/vim92/ftplugin/gitsendemail.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/go.vim b/software/vim/share/vim/vim92/ftplugin/go.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/go.vim rename to software/vim/share/vim/vim92/ftplugin/go.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/gpg.vim b/software/vim/share/vim/vim92/ftplugin/gpg.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/gpg.vim rename to software/vim/share/vim/vim92/ftplugin/gpg.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/gprof.vim b/software/vim/share/vim/vim92/ftplugin/gprof.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/gprof.vim rename to software/vim/share/vim/vim92/ftplugin/gprof.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/groovy.vim b/software/vim/share/vim/vim92/ftplugin/groovy.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/groovy.vim rename to software/vim/share/vim/vim92/ftplugin/groovy.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/group.vim b/software/vim/share/vim/vim92/ftplugin/group.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/group.vim rename to software/vim/share/vim/vim92/ftplugin/group.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/grub.vim b/software/vim/share/vim/vim92/ftplugin/grub.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/grub.vim rename to software/vim/share/vim/vim92/ftplugin/grub.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/gyp.vim b/software/vim/share/vim/vim92/ftplugin/gyp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/gyp.vim rename to software/vim/share/vim/vim92/ftplugin/gyp.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/haml.vim b/software/vim/share/vim/vim92/ftplugin/haml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/haml.vim rename to software/vim/share/vim/vim92/ftplugin/haml.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/hamster.vim b/software/vim/share/vim/vim92/ftplugin/hamster.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/hamster.vim rename to software/vim/share/vim/vim92/ftplugin/hamster.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/hare.vim b/software/vim/share/vim/vim92/ftplugin/hare.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/hare.vim rename to software/vim/share/vim/vim92/ftplugin/hare.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/haskell.vim b/software/vim/share/vim/vim92/ftplugin/haskell.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/haskell.vim rename to software/vim/share/vim/vim92/ftplugin/haskell.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/heex.vim b/software/vim/share/vim/vim92/ftplugin/heex.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/heex.vim rename to software/vim/share/vim/vim92/ftplugin/heex.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/help.vim b/software/vim/share/vim/vim92/ftplugin/help.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/help.vim rename to software/vim/share/vim/vim92/ftplugin/help.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/hgcommit.vim b/software/vim/share/vim/vim92/ftplugin/hgcommit.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/hgcommit.vim rename to software/vim/share/vim/vim92/ftplugin/hgcommit.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/hog.vim b/software/vim/share/vim/vim92/ftplugin/hog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/hog.vim rename to software/vim/share/vim/vim92/ftplugin/hog.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/hostconf.vim b/software/vim/share/vim/vim92/ftplugin/hostconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/hostconf.vim rename to software/vim/share/vim/vim92/ftplugin/hostconf.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/hostsaccess.vim b/software/vim/share/vim/vim92/ftplugin/hostsaccess.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/hostsaccess.vim rename to software/vim/share/vim/vim92/ftplugin/hostsaccess.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/html.vim b/software/vim/share/vim/vim92/ftplugin/html.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/html.vim rename to software/vim/share/vim/vim92/ftplugin/html.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/htmldjango.vim b/software/vim/share/vim/vim92/ftplugin/htmldjango.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/htmldjango.vim rename to software/vim/share/vim/vim92/ftplugin/htmldjango.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/i3config.vim b/software/vim/share/vim/vim92/ftplugin/i3config.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/i3config.vim rename to software/vim/share/vim/vim92/ftplugin/i3config.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/icon.vim b/software/vim/share/vim/vim92/ftplugin/icon.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/icon.vim rename to software/vim/share/vim/vim92/ftplugin/icon.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/indent.vim b/software/vim/share/vim/vim92/ftplugin/indent.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/indent.vim rename to software/vim/share/vim/vim92/ftplugin/indent.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/initex.vim b/software/vim/share/vim/vim92/ftplugin/initex.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/initex.vim rename to software/vim/share/vim/vim92/ftplugin/initex.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/ishd.vim b/software/vim/share/vim/vim92/ftplugin/ishd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/ishd.vim rename to software/vim/share/vim/vim92/ftplugin/ishd.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/j.vim b/software/vim/share/vim/vim92/ftplugin/j.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/j.vim rename to software/vim/share/vim/vim92/ftplugin/j.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/java.vim b/software/vim/share/vim/vim92/ftplugin/java.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/java.vim rename to software/vim/share/vim/vim92/ftplugin/java.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/javascript.vim b/software/vim/share/vim/vim92/ftplugin/javascript.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/javascript.vim rename to software/vim/share/vim/vim92/ftplugin/javascript.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/javascriptreact.vim b/software/vim/share/vim/vim92/ftplugin/javascriptreact.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/javascriptreact.vim rename to software/vim/share/vim/vim92/ftplugin/javascriptreact.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/jproperties.vim b/software/vim/share/vim/vim92/ftplugin/jproperties.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/jproperties.vim rename to software/vim/share/vim/vim92/ftplugin/jproperties.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/json.vim b/software/vim/share/vim/vim92/ftplugin/json.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/json.vim rename to software/vim/share/vim/vim92/ftplugin/json.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/jsonc.vim b/software/vim/share/vim/vim92/ftplugin/jsonc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/jsonc.vim rename to software/vim/share/vim/vim92/ftplugin/jsonc.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/jsonnet.vim b/software/vim/share/vim/vim92/ftplugin/jsonnet.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/jsonnet.vim rename to software/vim/share/vim/vim92/ftplugin/jsonnet.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/jsp.vim b/software/vim/share/vim/vim92/ftplugin/jsp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/jsp.vim rename to software/vim/share/vim/vim92/ftplugin/jsp.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/julia.vim b/software/vim/share/vim/vim92/ftplugin/julia.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/julia.vim rename to software/vim/share/vim/vim92/ftplugin/julia.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/kconfig.vim b/software/vim/share/vim/vim92/ftplugin/kconfig.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/kconfig.vim rename to software/vim/share/vim/vim92/ftplugin/kconfig.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/kwt.vim b/software/vim/share/vim/vim92/ftplugin/kwt.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/kwt.vim rename to software/vim/share/vim/vim92/ftplugin/kwt.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/ld.vim b/software/vim/share/vim/vim92/ftplugin/ld.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/ld.vim rename to software/vim/share/vim/vim92/ftplugin/ld.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/less.vim b/software/vim/share/vim/vim92/ftplugin/less.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/less.vim rename to software/vim/share/vim/vim92/ftplugin/less.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/lftp.vim b/software/vim/share/vim/vim92/ftplugin/lftp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/lftp.vim rename to software/vim/share/vim/vim92/ftplugin/lftp.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/libao.vim b/software/vim/share/vim/vim92/ftplugin/libao.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/libao.vim rename to software/vim/share/vim/vim92/ftplugin/libao.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/limits.vim b/software/vim/share/vim/vim92/ftplugin/limits.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/limits.vim rename to software/vim/share/vim/vim92/ftplugin/limits.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/liquid.vim b/software/vim/share/vim/vim92/ftplugin/liquid.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/liquid.vim rename to software/vim/share/vim/vim92/ftplugin/liquid.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/lisp.vim b/software/vim/share/vim/vim92/ftplugin/lisp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/lisp.vim rename to software/vim/share/vim/vim92/ftplugin/lisp.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/logcheck.vim b/software/vim/share/vim/vim92/ftplugin/logcheck.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/logcheck.vim rename to software/vim/share/vim/vim92/ftplugin/logcheck.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/loginaccess.vim b/software/vim/share/vim/vim92/ftplugin/loginaccess.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/loginaccess.vim rename to software/vim/share/vim/vim92/ftplugin/loginaccess.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/logindefs.vim b/software/vim/share/vim/vim92/ftplugin/logindefs.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/logindefs.vim rename to software/vim/share/vim/vim92/ftplugin/logindefs.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/logtalk.dict b/software/vim/share/vim/vim92/ftplugin/logtalk.dict similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/logtalk.dict rename to software/vim/share/vim/vim92/ftplugin/logtalk.dict diff --git a/registry/software/vim/share/vim/vim92/ftplugin/logtalk.vim b/software/vim/share/vim/vim92/ftplugin/logtalk.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/logtalk.vim rename to software/vim/share/vim/vim92/ftplugin/logtalk.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/lprolog.vim b/software/vim/share/vim/vim92/ftplugin/lprolog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/lprolog.vim rename to software/vim/share/vim/vim92/ftplugin/lprolog.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/lua.vim b/software/vim/share/vim/vim92/ftplugin/lua.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/lua.vim rename to software/vim/share/vim/vim92/ftplugin/lua.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/lynx.vim b/software/vim/share/vim/vim92/ftplugin/lynx.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/lynx.vim rename to software/vim/share/vim/vim92/ftplugin/lynx.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/m3build.vim b/software/vim/share/vim/vim92/ftplugin/m3build.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/m3build.vim rename to software/vim/share/vim/vim92/ftplugin/m3build.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/m3quake.vim b/software/vim/share/vim/vim92/ftplugin/m3quake.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/m3quake.vim rename to software/vim/share/vim/vim92/ftplugin/m3quake.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/m4.vim b/software/vim/share/vim/vim92/ftplugin/m4.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/m4.vim rename to software/vim/share/vim/vim92/ftplugin/m4.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/mail.vim b/software/vim/share/vim/vim92/ftplugin/mail.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/mail.vim rename to software/vim/share/vim/vim92/ftplugin/mail.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/mailaliases.vim b/software/vim/share/vim/vim92/ftplugin/mailaliases.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/mailaliases.vim rename to software/vim/share/vim/vim92/ftplugin/mailaliases.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/mailcap.vim b/software/vim/share/vim/vim92/ftplugin/mailcap.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/mailcap.vim rename to software/vim/share/vim/vim92/ftplugin/mailcap.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/make.vim b/software/vim/share/vim/vim92/ftplugin/make.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/make.vim rename to software/vim/share/vim/vim92/ftplugin/make.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/man.vim b/software/vim/share/vim/vim92/ftplugin/man.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/man.vim rename to software/vim/share/vim/vim92/ftplugin/man.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/manconf.vim b/software/vim/share/vim/vim92/ftplugin/manconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/manconf.vim rename to software/vim/share/vim/vim92/ftplugin/manconf.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/markdown.vim b/software/vim/share/vim/vim92/ftplugin/markdown.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/markdown.vim rename to software/vim/share/vim/vim92/ftplugin/markdown.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/masm.vim b/software/vim/share/vim/vim92/ftplugin/masm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/masm.vim rename to software/vim/share/vim/vim92/ftplugin/masm.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/matlab.vim b/software/vim/share/vim/vim92/ftplugin/matlab.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/matlab.vim rename to software/vim/share/vim/vim92/ftplugin/matlab.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/mermaid.vim b/software/vim/share/vim/vim92/ftplugin/mermaid.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/mermaid.vim rename to software/vim/share/vim/vim92/ftplugin/mermaid.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/meson.vim b/software/vim/share/vim/vim92/ftplugin/meson.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/meson.vim rename to software/vim/share/vim/vim92/ftplugin/meson.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/mf.vim b/software/vim/share/vim/vim92/ftplugin/mf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/mf.vim rename to software/vim/share/vim/vim92/ftplugin/mf.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/mma.vim b/software/vim/share/vim/vim92/ftplugin/mma.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/mma.vim rename to software/vim/share/vim/vim92/ftplugin/mma.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/modconf.vim b/software/vim/share/vim/vim92/ftplugin/modconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/modconf.vim rename to software/vim/share/vim/vim92/ftplugin/modconf.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/modula2.vim b/software/vim/share/vim/vim92/ftplugin/modula2.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/modula2.vim rename to software/vim/share/vim/vim92/ftplugin/modula2.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/modula3.vim b/software/vim/share/vim/vim92/ftplugin/modula3.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/modula3.vim rename to software/vim/share/vim/vim92/ftplugin/modula3.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/mp.vim b/software/vim/share/vim/vim92/ftplugin/mp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/mp.vim rename to software/vim/share/vim/vim92/ftplugin/mp.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/mplayerconf.vim b/software/vim/share/vim/vim92/ftplugin/mplayerconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/mplayerconf.vim rename to software/vim/share/vim/vim92/ftplugin/mplayerconf.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/mrxvtrc.vim b/software/vim/share/vim/vim92/ftplugin/mrxvtrc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/mrxvtrc.vim rename to software/vim/share/vim/vim92/ftplugin/mrxvtrc.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/msmessages.vim b/software/vim/share/vim/vim92/ftplugin/msmessages.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/msmessages.vim rename to software/vim/share/vim/vim92/ftplugin/msmessages.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/muttrc.vim b/software/vim/share/vim/vim92/ftplugin/muttrc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/muttrc.vim rename to software/vim/share/vim/vim92/ftplugin/muttrc.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/nanorc.vim b/software/vim/share/vim/vim92/ftplugin/nanorc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/nanorc.vim rename to software/vim/share/vim/vim92/ftplugin/nanorc.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/neomuttrc.vim b/software/vim/share/vim/vim92/ftplugin/neomuttrc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/neomuttrc.vim rename to software/vim/share/vim/vim92/ftplugin/neomuttrc.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/netrc.vim b/software/vim/share/vim/vim92/ftplugin/netrc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/netrc.vim rename to software/vim/share/vim/vim92/ftplugin/netrc.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/nginx.vim b/software/vim/share/vim/vim92/ftplugin/nginx.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/nginx.vim rename to software/vim/share/vim/vim92/ftplugin/nginx.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/nroff.vim b/software/vim/share/vim/vim92/ftplugin/nroff.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/nroff.vim rename to software/vim/share/vim/vim92/ftplugin/nroff.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/nsis.vim b/software/vim/share/vim/vim92/ftplugin/nsis.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/nsis.vim rename to software/vim/share/vim/vim92/ftplugin/nsis.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/objc.vim b/software/vim/share/vim/vim92/ftplugin/objc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/objc.vim rename to software/vim/share/vim/vim92/ftplugin/objc.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/obse.vim b/software/vim/share/vim/vim92/ftplugin/obse.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/obse.vim rename to software/vim/share/vim/vim92/ftplugin/obse.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/ocaml.vim b/software/vim/share/vim/vim92/ftplugin/ocaml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/ocaml.vim rename to software/vim/share/vim/vim92/ftplugin/ocaml.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/occam.vim b/software/vim/share/vim/vim92/ftplugin/occam.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/occam.vim rename to software/vim/share/vim/vim92/ftplugin/occam.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/octave.vim b/software/vim/share/vim/vim92/ftplugin/octave.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/octave.vim rename to software/vim/share/vim/vim92/ftplugin/octave.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/openvpn.vim b/software/vim/share/vim/vim92/ftplugin/openvpn.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/openvpn.vim rename to software/vim/share/vim/vim92/ftplugin/openvpn.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/pamconf.vim b/software/vim/share/vim/vim92/ftplugin/pamconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/pamconf.vim rename to software/vim/share/vim/vim92/ftplugin/pamconf.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/pascal.vim b/software/vim/share/vim/vim92/ftplugin/pascal.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/pascal.vim rename to software/vim/share/vim/vim92/ftplugin/pascal.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/passwd.vim b/software/vim/share/vim/vim92/ftplugin/passwd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/passwd.vim rename to software/vim/share/vim/vim92/ftplugin/passwd.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/pbtxt.vim b/software/vim/share/vim/vim92/ftplugin/pbtxt.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/pbtxt.vim rename to software/vim/share/vim/vim92/ftplugin/pbtxt.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/pdf.vim b/software/vim/share/vim/vim92/ftplugin/pdf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/pdf.vim rename to software/vim/share/vim/vim92/ftplugin/pdf.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/perl.vim b/software/vim/share/vim/vim92/ftplugin/perl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/perl.vim rename to software/vim/share/vim/vim92/ftplugin/perl.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/php.vim b/software/vim/share/vim/vim92/ftplugin/php.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/php.vim rename to software/vim/share/vim/vim92/ftplugin/php.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/pinfo.vim b/software/vim/share/vim/vim92/ftplugin/pinfo.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/pinfo.vim rename to software/vim/share/vim/vim92/ftplugin/pinfo.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/plaintex.vim b/software/vim/share/vim/vim92/ftplugin/plaintex.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/plaintex.vim rename to software/vim/share/vim/vim92/ftplugin/plaintex.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/pod.vim b/software/vim/share/vim/vim92/ftplugin/pod.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/pod.vim rename to software/vim/share/vim/vim92/ftplugin/pod.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/poefilter.vim b/software/vim/share/vim/vim92/ftplugin/poefilter.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/poefilter.vim rename to software/vim/share/vim/vim92/ftplugin/poefilter.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/poke.vim b/software/vim/share/vim/vim92/ftplugin/poke.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/poke.vim rename to software/vim/share/vim/vim92/ftplugin/poke.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/postscr.vim b/software/vim/share/vim/vim92/ftplugin/postscr.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/postscr.vim rename to software/vim/share/vim/vim92/ftplugin/postscr.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/procmail.vim b/software/vim/share/vim/vim92/ftplugin/procmail.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/procmail.vim rename to software/vim/share/vim/vim92/ftplugin/procmail.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/prolog.vim b/software/vim/share/vim/vim92/ftplugin/prolog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/prolog.vim rename to software/vim/share/vim/vim92/ftplugin/prolog.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/protocols.vim b/software/vim/share/vim/vim92/ftplugin/protocols.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/protocols.vim rename to software/vim/share/vim/vim92/ftplugin/protocols.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/ps1.vim b/software/vim/share/vim/vim92/ftplugin/ps1.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/ps1.vim rename to software/vim/share/vim/vim92/ftplugin/ps1.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/ps1xml.vim b/software/vim/share/vim/vim92/ftplugin/ps1xml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/ps1xml.vim rename to software/vim/share/vim/vim92/ftplugin/ps1xml.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/pyrex.vim b/software/vim/share/vim/vim92/ftplugin/pyrex.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/pyrex.vim rename to software/vim/share/vim/vim92/ftplugin/pyrex.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/python.vim b/software/vim/share/vim/vim92/ftplugin/python.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/python.vim rename to software/vim/share/vim/vim92/ftplugin/python.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/qb64.vim b/software/vim/share/vim/vim92/ftplugin/qb64.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/qb64.vim rename to software/vim/share/vim/vim92/ftplugin/qb64.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/qf.vim b/software/vim/share/vim/vim92/ftplugin/qf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/qf.vim rename to software/vim/share/vim/vim92/ftplugin/qf.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/quake.vim b/software/vim/share/vim/vim92/ftplugin/quake.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/quake.vim rename to software/vim/share/vim/vim92/ftplugin/quake.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/quarto.vim b/software/vim/share/vim/vim92/ftplugin/quarto.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/quarto.vim rename to software/vim/share/vim/vim92/ftplugin/quarto.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/r.vim b/software/vim/share/vim/vim92/ftplugin/r.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/r.vim rename to software/vim/share/vim/vim92/ftplugin/r.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/racc.vim b/software/vim/share/vim/vim92/ftplugin/racc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/racc.vim rename to software/vim/share/vim/vim92/ftplugin/racc.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/racket.vim b/software/vim/share/vim/vim92/ftplugin/racket.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/racket.vim rename to software/vim/share/vim/vim92/ftplugin/racket.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/raku.vim b/software/vim/share/vim/vim92/ftplugin/raku.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/raku.vim rename to software/vim/share/vim/vim92/ftplugin/raku.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/readline.vim b/software/vim/share/vim/vim92/ftplugin/readline.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/readline.vim rename to software/vim/share/vim/vim92/ftplugin/readline.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/registry.vim b/software/vim/share/vim/vim92/ftplugin/registry.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/registry.vim rename to software/vim/share/vim/vim92/ftplugin/registry.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/reva.vim b/software/vim/share/vim/vim92/ftplugin/reva.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/reva.vim rename to software/vim/share/vim/vim92/ftplugin/reva.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/rhelp.vim b/software/vim/share/vim/vim92/ftplugin/rhelp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/rhelp.vim rename to software/vim/share/vim/vim92/ftplugin/rhelp.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/rmd.vim b/software/vim/share/vim/vim92/ftplugin/rmd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/rmd.vim rename to software/vim/share/vim/vim92/ftplugin/rmd.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/rnc.vim b/software/vim/share/vim/vim92/ftplugin/rnc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/rnc.vim rename to software/vim/share/vim/vim92/ftplugin/rnc.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/rnoweb.vim b/software/vim/share/vim/vim92/ftplugin/rnoweb.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/rnoweb.vim rename to software/vim/share/vim/vim92/ftplugin/rnoweb.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/routeros.vim b/software/vim/share/vim/vim92/ftplugin/routeros.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/routeros.vim rename to software/vim/share/vim/vim92/ftplugin/routeros.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/rpl.vim b/software/vim/share/vim/vim92/ftplugin/rpl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/rpl.vim rename to software/vim/share/vim/vim92/ftplugin/rpl.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/rrst.vim b/software/vim/share/vim/vim92/ftplugin/rrst.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/rrst.vim rename to software/vim/share/vim/vim92/ftplugin/rrst.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/rst.vim b/software/vim/share/vim/vim92/ftplugin/rst.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/rst.vim rename to software/vim/share/vim/vim92/ftplugin/rst.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/ruby.vim b/software/vim/share/vim/vim92/ftplugin/ruby.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/ruby.vim rename to software/vim/share/vim/vim92/ftplugin/ruby.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/rust.vim b/software/vim/share/vim/vim92/ftplugin/rust.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/rust.vim rename to software/vim/share/vim/vim92/ftplugin/rust.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/sass.vim b/software/vim/share/vim/vim92/ftplugin/sass.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/sass.vim rename to software/vim/share/vim/vim92/ftplugin/sass.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/sbt.vim b/software/vim/share/vim/vim92/ftplugin/sbt.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/sbt.vim rename to software/vim/share/vim/vim92/ftplugin/sbt.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/scala.vim b/software/vim/share/vim/vim92/ftplugin/scala.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/scala.vim rename to software/vim/share/vim/vim92/ftplugin/scala.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/scdoc.vim b/software/vim/share/vim/vim92/ftplugin/scdoc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/scdoc.vim rename to software/vim/share/vim/vim92/ftplugin/scdoc.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/scheme.vim b/software/vim/share/vim/vim92/ftplugin/scheme.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/scheme.vim rename to software/vim/share/vim/vim92/ftplugin/scheme.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/screen.vim b/software/vim/share/vim/vim92/ftplugin/screen.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/screen.vim rename to software/vim/share/vim/vim92/ftplugin/screen.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/scss.vim b/software/vim/share/vim/vim92/ftplugin/scss.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/scss.vim rename to software/vim/share/vim/vim92/ftplugin/scss.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/sensors.vim b/software/vim/share/vim/vim92/ftplugin/sensors.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/sensors.vim rename to software/vim/share/vim/vim92/ftplugin/sensors.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/services.vim b/software/vim/share/vim/vim92/ftplugin/services.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/services.vim rename to software/vim/share/vim/vim92/ftplugin/services.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/setserial.vim b/software/vim/share/vim/vim92/ftplugin/setserial.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/setserial.vim rename to software/vim/share/vim/vim92/ftplugin/setserial.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/sexplib.vim b/software/vim/share/vim/vim92/ftplugin/sexplib.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/sexplib.vim rename to software/vim/share/vim/vim92/ftplugin/sexplib.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/sgml.vim b/software/vim/share/vim/vim92/ftplugin/sgml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/sgml.vim rename to software/vim/share/vim/vim92/ftplugin/sgml.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/sh.vim b/software/vim/share/vim/vim92/ftplugin/sh.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/sh.vim rename to software/vim/share/vim/vim92/ftplugin/sh.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/sieve.vim b/software/vim/share/vim/vim92/ftplugin/sieve.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/sieve.vim rename to software/vim/share/vim/vim92/ftplugin/sieve.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/slpconf.vim b/software/vim/share/vim/vim92/ftplugin/slpconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/slpconf.vim rename to software/vim/share/vim/vim92/ftplugin/slpconf.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/slpreg.vim b/software/vim/share/vim/vim92/ftplugin/slpreg.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/slpreg.vim rename to software/vim/share/vim/vim92/ftplugin/slpreg.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/slpspi.vim b/software/vim/share/vim/vim92/ftplugin/slpspi.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/slpspi.vim rename to software/vim/share/vim/vim92/ftplugin/slpspi.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/solution.vim b/software/vim/share/vim/vim92/ftplugin/solution.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/solution.vim rename to software/vim/share/vim/vim92/ftplugin/solution.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/spec.vim b/software/vim/share/vim/vim92/ftplugin/spec.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/spec.vim rename to software/vim/share/vim/vim92/ftplugin/spec.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/sql.vim b/software/vim/share/vim/vim92/ftplugin/sql.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/sql.vim rename to software/vim/share/vim/vim92/ftplugin/sql.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/ssa.vim b/software/vim/share/vim/vim92/ftplugin/ssa.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/ssa.vim rename to software/vim/share/vim/vim92/ftplugin/ssa.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/sshconfig.vim b/software/vim/share/vim/vim92/ftplugin/sshconfig.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/sshconfig.vim rename to software/vim/share/vim/vim92/ftplugin/sshconfig.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/sudoers.vim b/software/vim/share/vim/vim92/ftplugin/sudoers.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/sudoers.vim rename to software/vim/share/vim/vim92/ftplugin/sudoers.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/svg.vim b/software/vim/share/vim/vim92/ftplugin/svg.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/svg.vim rename to software/vim/share/vim/vim92/ftplugin/svg.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/swayconfig.vim b/software/vim/share/vim/vim92/ftplugin/swayconfig.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/swayconfig.vim rename to software/vim/share/vim/vim92/ftplugin/swayconfig.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/swift.vim b/software/vim/share/vim/vim92/ftplugin/swift.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/swift.vim rename to software/vim/share/vim/vim92/ftplugin/swift.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/swiftgyb.vim b/software/vim/share/vim/vim92/ftplugin/swiftgyb.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/swiftgyb.vim rename to software/vim/share/vim/vim92/ftplugin/swiftgyb.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/sysctl.vim b/software/vim/share/vim/vim92/ftplugin/sysctl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/sysctl.vim rename to software/vim/share/vim/vim92/ftplugin/sysctl.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/systemd.vim b/software/vim/share/vim/vim92/ftplugin/systemd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/systemd.vim rename to software/vim/share/vim/vim92/ftplugin/systemd.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/systemverilog.vim b/software/vim/share/vim/vim92/ftplugin/systemverilog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/systemverilog.vim rename to software/vim/share/vim/vim92/ftplugin/systemverilog.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/tap.vim b/software/vim/share/vim/vim92/ftplugin/tap.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/tap.vim rename to software/vim/share/vim/vim92/ftplugin/tap.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/tcl.vim b/software/vim/share/vim/vim92/ftplugin/tcl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/tcl.vim rename to software/vim/share/vim/vim92/ftplugin/tcl.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/tcsh.vim b/software/vim/share/vim/vim92/ftplugin/tcsh.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/tcsh.vim rename to software/vim/share/vim/vim92/ftplugin/tcsh.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/terminfo.vim b/software/vim/share/vim/vim92/ftplugin/terminfo.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/terminfo.vim rename to software/vim/share/vim/vim92/ftplugin/terminfo.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/tex.vim b/software/vim/share/vim/vim92/ftplugin/tex.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/tex.vim rename to software/vim/share/vim/vim92/ftplugin/tex.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/text.vim b/software/vim/share/vim/vim92/ftplugin/text.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/text.vim rename to software/vim/share/vim/vim92/ftplugin/text.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/tidy.vim b/software/vim/share/vim/vim92/ftplugin/tidy.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/tidy.vim rename to software/vim/share/vim/vim92/ftplugin/tidy.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/tmux.vim b/software/vim/share/vim/vim92/ftplugin/tmux.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/tmux.vim rename to software/vim/share/vim/vim92/ftplugin/tmux.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/toml.vim b/software/vim/share/vim/vim92/ftplugin/toml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/toml.vim rename to software/vim/share/vim/vim92/ftplugin/toml.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/treetop.vim b/software/vim/share/vim/vim92/ftplugin/treetop.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/treetop.vim rename to software/vim/share/vim/vim92/ftplugin/treetop.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/tt2html.vim b/software/vim/share/vim/vim92/ftplugin/tt2html.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/tt2html.vim rename to software/vim/share/vim/vim92/ftplugin/tt2html.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/typescript.vim b/software/vim/share/vim/vim92/ftplugin/typescript.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/typescript.vim rename to software/vim/share/vim/vim92/ftplugin/typescript.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/typescriptreact.vim b/software/vim/share/vim/vim92/ftplugin/typescriptreact.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/typescriptreact.vim rename to software/vim/share/vim/vim92/ftplugin/typescriptreact.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/udevconf.vim b/software/vim/share/vim/vim92/ftplugin/udevconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/udevconf.vim rename to software/vim/share/vim/vim92/ftplugin/udevconf.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/udevperm.vim b/software/vim/share/vim/vim92/ftplugin/udevperm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/udevperm.vim rename to software/vim/share/vim/vim92/ftplugin/udevperm.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/udevrules.vim b/software/vim/share/vim/vim92/ftplugin/udevrules.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/udevrules.vim rename to software/vim/share/vim/vim92/ftplugin/udevrules.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/updatedb.vim b/software/vim/share/vim/vim92/ftplugin/updatedb.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/updatedb.vim rename to software/vim/share/vim/vim92/ftplugin/updatedb.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/vb.vim b/software/vim/share/vim/vim92/ftplugin/vb.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/vb.vim rename to software/vim/share/vim/vim92/ftplugin/vb.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/vdf.vim b/software/vim/share/vim/vim92/ftplugin/vdf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/vdf.vim rename to software/vim/share/vim/vim92/ftplugin/vdf.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/verilog.vim b/software/vim/share/vim/vim92/ftplugin/verilog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/verilog.vim rename to software/vim/share/vim/vim92/ftplugin/verilog.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/vhdl.vim b/software/vim/share/vim/vim92/ftplugin/vhdl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/vhdl.vim rename to software/vim/share/vim/vim92/ftplugin/vhdl.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/vim.vim b/software/vim/share/vim/vim92/ftplugin/vim.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/vim.vim rename to software/vim/share/vim/vim92/ftplugin/vim.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/vroom.vim b/software/vim/share/vim/vim92/ftplugin/vroom.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/vroom.vim rename to software/vim/share/vim/vim92/ftplugin/vroom.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/vue.vim b/software/vim/share/vim/vim92/ftplugin/vue.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/vue.vim rename to software/vim/share/vim/vim92/ftplugin/vue.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/wast.vim b/software/vim/share/vim/vim92/ftplugin/wast.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/wast.vim rename to software/vim/share/vim/vim92/ftplugin/wast.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/wget.vim b/software/vim/share/vim/vim92/ftplugin/wget.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/wget.vim rename to software/vim/share/vim/vim92/ftplugin/wget.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/wget2.vim b/software/vim/share/vim/vim92/ftplugin/wget2.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/wget2.vim rename to software/vim/share/vim/vim92/ftplugin/wget2.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/xdefaults.vim b/software/vim/share/vim/vim92/ftplugin/xdefaults.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/xdefaults.vim rename to software/vim/share/vim/vim92/ftplugin/xdefaults.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/xf86conf.vim b/software/vim/share/vim/vim92/ftplugin/xf86conf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/xf86conf.vim rename to software/vim/share/vim/vim92/ftplugin/xf86conf.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/xhtml.vim b/software/vim/share/vim/vim92/ftplugin/xhtml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/xhtml.vim rename to software/vim/share/vim/vim92/ftplugin/xhtml.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/xinetd.vim b/software/vim/share/vim/vim92/ftplugin/xinetd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/xinetd.vim rename to software/vim/share/vim/vim92/ftplugin/xinetd.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/xml.vim b/software/vim/share/vim/vim92/ftplugin/xml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/xml.vim rename to software/vim/share/vim/vim92/ftplugin/xml.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/xmodmap.vim b/software/vim/share/vim/vim92/ftplugin/xmodmap.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/xmodmap.vim rename to software/vim/share/vim/vim92/ftplugin/xmodmap.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/xs.vim b/software/vim/share/vim/vim92/ftplugin/xs.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/xs.vim rename to software/vim/share/vim/vim92/ftplugin/xs.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/xsd.vim b/software/vim/share/vim/vim92/ftplugin/xsd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/xsd.vim rename to software/vim/share/vim/vim92/ftplugin/xsd.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/xslt.vim b/software/vim/share/vim/vim92/ftplugin/xslt.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/xslt.vim rename to software/vim/share/vim/vim92/ftplugin/xslt.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/yaml.vim b/software/vim/share/vim/vim92/ftplugin/yaml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/yaml.vim rename to software/vim/share/vim/vim92/ftplugin/yaml.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/zig.vim b/software/vim/share/vim/vim92/ftplugin/zig.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/zig.vim rename to software/vim/share/vim/vim92/ftplugin/zig.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/zimbu.vim b/software/vim/share/vim/vim92/ftplugin/zimbu.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/zimbu.vim rename to software/vim/share/vim/vim92/ftplugin/zimbu.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugin/zsh.vim b/software/vim/share/vim/vim92/ftplugin/zsh.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugin/zsh.vim rename to software/vim/share/vim/vim92/ftplugin/zsh.vim diff --git a/registry/software/vim/share/vim/vim92/ftplugof.vim b/software/vim/share/vim/vim92/ftplugof.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/ftplugof.vim rename to software/vim/share/vim/vim92/ftplugof.vim diff --git a/registry/software/vim/share/vim/vim92/gvimrc_example.vim b/software/vim/share/vim/vim92/gvimrc_example.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/gvimrc_example.vim rename to software/vim/share/vim/vim92/gvimrc_example.vim diff --git a/registry/software/vim/share/vim/vim92/indent.vim b/software/vim/share/vim/vim92/indent.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent.vim rename to software/vim/share/vim/vim92/indent.vim diff --git a/registry/software/vim/share/vim/vim92/indent/aap.vim b/software/vim/share/vim/vim92/indent/aap.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/aap.vim rename to software/vim/share/vim/vim92/indent/aap.vim diff --git a/registry/software/vim/share/vim/vim92/indent/ada.vim b/software/vim/share/vim/vim92/indent/ada.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/ada.vim rename to software/vim/share/vim/vim92/indent/ada.vim diff --git a/registry/software/vim/share/vim/vim92/indent/ant.vim b/software/vim/share/vim/vim92/indent/ant.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/ant.vim rename to software/vim/share/vim/vim92/indent/ant.vim diff --git a/registry/software/vim/share/vim/vim92/indent/automake.vim b/software/vim/share/vim/vim92/indent/automake.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/automake.vim rename to software/vim/share/vim/vim92/indent/automake.vim diff --git a/registry/software/vim/share/vim/vim92/indent/awk.vim b/software/vim/share/vim/vim92/indent/awk.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/awk.vim rename to software/vim/share/vim/vim92/indent/awk.vim diff --git a/registry/software/vim/share/vim/vim92/indent/bash.vim b/software/vim/share/vim/vim92/indent/bash.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/bash.vim rename to software/vim/share/vim/vim92/indent/bash.vim diff --git a/registry/software/vim/share/vim/vim92/indent/basic.vim b/software/vim/share/vim/vim92/indent/basic.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/basic.vim rename to software/vim/share/vim/vim92/indent/basic.vim diff --git a/registry/software/vim/share/vim/vim92/indent/bib.vim b/software/vim/share/vim/vim92/indent/bib.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/bib.vim rename to software/vim/share/vim/vim92/indent/bib.vim diff --git a/registry/software/vim/share/vim/vim92/indent/bitbake.vim b/software/vim/share/vim/vim92/indent/bitbake.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/bitbake.vim rename to software/vim/share/vim/vim92/indent/bitbake.vim diff --git a/registry/software/vim/share/vim/vim92/indent/bst.vim b/software/vim/share/vim/vim92/indent/bst.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/bst.vim rename to software/vim/share/vim/vim92/indent/bst.vim diff --git a/registry/software/vim/share/vim/vim92/indent/bzl.vim b/software/vim/share/vim/vim92/indent/bzl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/bzl.vim rename to software/vim/share/vim/vim92/indent/bzl.vim diff --git a/registry/software/vim/share/vim/vim92/indent/c.vim b/software/vim/share/vim/vim92/indent/c.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/c.vim rename to software/vim/share/vim/vim92/indent/c.vim diff --git a/registry/software/vim/share/vim/vim92/indent/cdl.vim b/software/vim/share/vim/vim92/indent/cdl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/cdl.vim rename to software/vim/share/vim/vim92/indent/cdl.vim diff --git a/registry/software/vim/share/vim/vim92/indent/ch.vim b/software/vim/share/vim/vim92/indent/ch.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/ch.vim rename to software/vim/share/vim/vim92/indent/ch.vim diff --git a/registry/software/vim/share/vim/vim92/indent/chaiscript.vim b/software/vim/share/vim/vim92/indent/chaiscript.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/chaiscript.vim rename to software/vim/share/vim/vim92/indent/chaiscript.vim diff --git a/registry/software/vim/share/vim/vim92/indent/changelog.vim b/software/vim/share/vim/vim92/indent/changelog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/changelog.vim rename to software/vim/share/vim/vim92/indent/changelog.vim diff --git a/registry/software/vim/share/vim/vim92/indent/chatito.vim b/software/vim/share/vim/vim92/indent/chatito.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/chatito.vim rename to software/vim/share/vim/vim92/indent/chatito.vim diff --git a/registry/software/vim/share/vim/vim92/indent/clojure.vim b/software/vim/share/vim/vim92/indent/clojure.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/clojure.vim rename to software/vim/share/vim/vim92/indent/clojure.vim diff --git a/registry/software/vim/share/vim/vim92/indent/cmake.vim b/software/vim/share/vim/vim92/indent/cmake.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/cmake.vim rename to software/vim/share/vim/vim92/indent/cmake.vim diff --git a/registry/software/vim/share/vim/vim92/indent/cobol.vim b/software/vim/share/vim/vim92/indent/cobol.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/cobol.vim rename to software/vim/share/vim/vim92/indent/cobol.vim diff --git a/registry/software/vim/share/vim/vim92/indent/config.vim b/software/vim/share/vim/vim92/indent/config.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/config.vim rename to software/vim/share/vim/vim92/indent/config.vim diff --git a/registry/software/vim/share/vim/vim92/indent/context.vim b/software/vim/share/vim/vim92/indent/context.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/context.vim rename to software/vim/share/vim/vim92/indent/context.vim diff --git a/registry/software/vim/share/vim/vim92/indent/cpp.vim b/software/vim/share/vim/vim92/indent/cpp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/cpp.vim rename to software/vim/share/vim/vim92/indent/cpp.vim diff --git a/registry/software/vim/share/vim/vim92/indent/cs.vim b/software/vim/share/vim/vim92/indent/cs.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/cs.vim rename to software/vim/share/vim/vim92/indent/cs.vim diff --git a/registry/software/vim/share/vim/vim92/indent/css.vim b/software/vim/share/vim/vim92/indent/css.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/css.vim rename to software/vim/share/vim/vim92/indent/css.vim diff --git a/registry/software/vim/share/vim/vim92/indent/cucumber.vim b/software/vim/share/vim/vim92/indent/cucumber.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/cucumber.vim rename to software/vim/share/vim/vim92/indent/cucumber.vim diff --git a/registry/software/vim/share/vim/vim92/indent/cuda.vim b/software/vim/share/vim/vim92/indent/cuda.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/cuda.vim rename to software/vim/share/vim/vim92/indent/cuda.vim diff --git a/registry/software/vim/share/vim/vim92/indent/d.vim b/software/vim/share/vim/vim92/indent/d.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/d.vim rename to software/vim/share/vim/vim92/indent/d.vim diff --git a/registry/software/vim/share/vim/vim92/indent/dictconf.vim b/software/vim/share/vim/vim92/indent/dictconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/dictconf.vim rename to software/vim/share/vim/vim92/indent/dictconf.vim diff --git a/registry/software/vim/share/vim/vim92/indent/dictdconf.vim b/software/vim/share/vim/vim92/indent/dictdconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/dictdconf.vim rename to software/vim/share/vim/vim92/indent/dictdconf.vim diff --git a/registry/software/vim/share/vim/vim92/indent/docbk.vim b/software/vim/share/vim/vim92/indent/docbk.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/docbk.vim rename to software/vim/share/vim/vim92/indent/docbk.vim diff --git a/registry/software/vim/share/vim/vim92/indent/dosbatch.vim b/software/vim/share/vim/vim92/indent/dosbatch.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/dosbatch.vim rename to software/vim/share/vim/vim92/indent/dosbatch.vim diff --git a/registry/software/vim/share/vim/vim92/indent/dtd.vim b/software/vim/share/vim/vim92/indent/dtd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/dtd.vim rename to software/vim/share/vim/vim92/indent/dtd.vim diff --git a/registry/software/vim/share/vim/vim92/indent/dtrace.vim b/software/vim/share/vim/vim92/indent/dtrace.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/dtrace.vim rename to software/vim/share/vim/vim92/indent/dtrace.vim diff --git a/registry/software/vim/share/vim/vim92/indent/dune.vim b/software/vim/share/vim/vim92/indent/dune.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/dune.vim rename to software/vim/share/vim/vim92/indent/dune.vim diff --git a/registry/software/vim/share/vim/vim92/indent/dylan.vim b/software/vim/share/vim/vim92/indent/dylan.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/dylan.vim rename to software/vim/share/vim/vim92/indent/dylan.vim diff --git a/registry/software/vim/share/vim/vim92/indent/eiffel.vim b/software/vim/share/vim/vim92/indent/eiffel.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/eiffel.vim rename to software/vim/share/vim/vim92/indent/eiffel.vim diff --git a/registry/software/vim/share/vim/vim92/indent/elm.vim b/software/vim/share/vim/vim92/indent/elm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/elm.vim rename to software/vim/share/vim/vim92/indent/elm.vim diff --git a/registry/software/vim/share/vim/vim92/indent/erlang.vim b/software/vim/share/vim/vim92/indent/erlang.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/erlang.vim rename to software/vim/share/vim/vim92/indent/erlang.vim diff --git a/registry/software/vim/share/vim/vim92/indent/eruby.vim b/software/vim/share/vim/vim92/indent/eruby.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/eruby.vim rename to software/vim/share/vim/vim92/indent/eruby.vim diff --git a/registry/software/vim/share/vim/vim92/indent/eterm.vim b/software/vim/share/vim/vim92/indent/eterm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/eterm.vim rename to software/vim/share/vim/vim92/indent/eterm.vim diff --git a/registry/software/vim/share/vim/vim92/indent/expect.vim b/software/vim/share/vim/vim92/indent/expect.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/expect.vim rename to software/vim/share/vim/vim92/indent/expect.vim diff --git a/registry/software/vim/share/vim/vim92/indent/falcon.vim b/software/vim/share/vim/vim92/indent/falcon.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/falcon.vim rename to software/vim/share/vim/vim92/indent/falcon.vim diff --git a/registry/software/vim/share/vim/vim92/indent/fennel.vim b/software/vim/share/vim/vim92/indent/fennel.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/fennel.vim rename to software/vim/share/vim/vim92/indent/fennel.vim diff --git a/registry/software/vim/share/vim/vim92/indent/fish.vim b/software/vim/share/vim/vim92/indent/fish.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/fish.vim rename to software/vim/share/vim/vim92/indent/fish.vim diff --git a/registry/software/vim/share/vim/vim92/indent/fortran.vim b/software/vim/share/vim/vim92/indent/fortran.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/fortran.vim rename to software/vim/share/vim/vim92/indent/fortran.vim diff --git a/registry/software/vim/share/vim/vim92/indent/framescript.vim b/software/vim/share/vim/vim92/indent/framescript.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/framescript.vim rename to software/vim/share/vim/vim92/indent/framescript.vim diff --git a/registry/software/vim/share/vim/vim92/indent/freebasic.vim b/software/vim/share/vim/vim92/indent/freebasic.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/freebasic.vim rename to software/vim/share/vim/vim92/indent/freebasic.vim diff --git a/registry/software/vim/share/vim/vim92/indent/gdscript.vim b/software/vim/share/vim/vim92/indent/gdscript.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/gdscript.vim rename to software/vim/share/vim/vim92/indent/gdscript.vim diff --git a/registry/software/vim/share/vim/vim92/indent/gitconfig.vim b/software/vim/share/vim/vim92/indent/gitconfig.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/gitconfig.vim rename to software/vim/share/vim/vim92/indent/gitconfig.vim diff --git a/registry/software/vim/share/vim/vim92/indent/gitolite.vim b/software/vim/share/vim/vim92/indent/gitolite.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/gitolite.vim rename to software/vim/share/vim/vim92/indent/gitolite.vim diff --git a/registry/software/vim/share/vim/vim92/indent/go.vim b/software/vim/share/vim/vim92/indent/go.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/go.vim rename to software/vim/share/vim/vim92/indent/go.vim diff --git a/registry/software/vim/share/vim/vim92/indent/gyp.vim b/software/vim/share/vim/vim92/indent/gyp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/gyp.vim rename to software/vim/share/vim/vim92/indent/gyp.vim diff --git a/registry/software/vim/share/vim/vim92/indent/haml.vim b/software/vim/share/vim/vim92/indent/haml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/haml.vim rename to software/vim/share/vim/vim92/indent/haml.vim diff --git a/registry/software/vim/share/vim/vim92/indent/hamster.vim b/software/vim/share/vim/vim92/indent/hamster.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/hamster.vim rename to software/vim/share/vim/vim92/indent/hamster.vim diff --git a/registry/software/vim/share/vim/vim92/indent/hare.vim b/software/vim/share/vim/vim92/indent/hare.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/hare.vim rename to software/vim/share/vim/vim92/indent/hare.vim diff --git a/registry/software/vim/share/vim/vim92/indent/hog.vim b/software/vim/share/vim/vim92/indent/hog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/hog.vim rename to software/vim/share/vim/vim92/indent/hog.vim diff --git a/registry/software/vim/share/vim/vim92/indent/html.vim b/software/vim/share/vim/vim92/indent/html.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/html.vim rename to software/vim/share/vim/vim92/indent/html.vim diff --git a/registry/software/vim/share/vim/vim92/indent/htmldjango.vim b/software/vim/share/vim/vim92/indent/htmldjango.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/htmldjango.vim rename to software/vim/share/vim/vim92/indent/htmldjango.vim diff --git a/registry/software/vim/share/vim/vim92/indent/idlang.vim b/software/vim/share/vim/vim92/indent/idlang.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/idlang.vim rename to software/vim/share/vim/vim92/indent/idlang.vim diff --git a/registry/software/vim/share/vim/vim92/indent/ishd.vim b/software/vim/share/vim/vim92/indent/ishd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/ishd.vim rename to software/vim/share/vim/vim92/indent/ishd.vim diff --git a/registry/software/vim/share/vim/vim92/indent/j.vim b/software/vim/share/vim/vim92/indent/j.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/j.vim rename to software/vim/share/vim/vim92/indent/j.vim diff --git a/registry/software/vim/share/vim/vim92/indent/java.vim b/software/vim/share/vim/vim92/indent/java.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/java.vim rename to software/vim/share/vim/vim92/indent/java.vim diff --git a/registry/software/vim/share/vim/vim92/indent/javascript.vim b/software/vim/share/vim/vim92/indent/javascript.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/javascript.vim rename to software/vim/share/vim/vim92/indent/javascript.vim diff --git a/registry/software/vim/share/vim/vim92/indent/javascriptreact.vim b/software/vim/share/vim/vim92/indent/javascriptreact.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/javascriptreact.vim rename to software/vim/share/vim/vim92/indent/javascriptreact.vim diff --git a/registry/software/vim/share/vim/vim92/indent/json.vim b/software/vim/share/vim/vim92/indent/json.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/json.vim rename to software/vim/share/vim/vim92/indent/json.vim diff --git a/registry/software/vim/share/vim/vim92/indent/jsonc.vim b/software/vim/share/vim/vim92/indent/jsonc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/jsonc.vim rename to software/vim/share/vim/vim92/indent/jsonc.vim diff --git a/registry/software/vim/share/vim/vim92/indent/jsp.vim b/software/vim/share/vim/vim92/indent/jsp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/jsp.vim rename to software/vim/share/vim/vim92/indent/jsp.vim diff --git a/registry/software/vim/share/vim/vim92/indent/julia.vim b/software/vim/share/vim/vim92/indent/julia.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/julia.vim rename to software/vim/share/vim/vim92/indent/julia.vim diff --git a/registry/software/vim/share/vim/vim92/indent/krl.vim b/software/vim/share/vim/vim92/indent/krl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/krl.vim rename to software/vim/share/vim/vim92/indent/krl.vim diff --git a/registry/software/vim/share/vim/vim92/indent/ld.vim b/software/vim/share/vim/vim92/indent/ld.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/ld.vim rename to software/vim/share/vim/vim92/indent/ld.vim diff --git a/registry/software/vim/share/vim/vim92/indent/less.vim b/software/vim/share/vim/vim92/indent/less.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/less.vim rename to software/vim/share/vim/vim92/indent/less.vim diff --git a/registry/software/vim/share/vim/vim92/indent/lifelines.vim b/software/vim/share/vim/vim92/indent/lifelines.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/lifelines.vim rename to software/vim/share/vim/vim92/indent/lifelines.vim diff --git a/registry/software/vim/share/vim/vim92/indent/liquid.vim b/software/vim/share/vim/vim92/indent/liquid.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/liquid.vim rename to software/vim/share/vim/vim92/indent/liquid.vim diff --git a/registry/software/vim/share/vim/vim92/indent/lisp.vim b/software/vim/share/vim/vim92/indent/lisp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/lisp.vim rename to software/vim/share/vim/vim92/indent/lisp.vim diff --git a/registry/software/vim/share/vim/vim92/indent/logtalk.vim b/software/vim/share/vim/vim92/indent/logtalk.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/logtalk.vim rename to software/vim/share/vim/vim92/indent/logtalk.vim diff --git a/registry/software/vim/share/vim/vim92/indent/lua.vim b/software/vim/share/vim/vim92/indent/lua.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/lua.vim rename to software/vim/share/vim/vim92/indent/lua.vim diff --git a/registry/software/vim/share/vim/vim92/indent/mail.vim b/software/vim/share/vim/vim92/indent/mail.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/mail.vim rename to software/vim/share/vim/vim92/indent/mail.vim diff --git a/registry/software/vim/share/vim/vim92/indent/make.vim b/software/vim/share/vim/vim92/indent/make.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/make.vim rename to software/vim/share/vim/vim92/indent/make.vim diff --git a/registry/software/vim/share/vim/vim92/indent/matlab.vim b/software/vim/share/vim/vim92/indent/matlab.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/matlab.vim rename to software/vim/share/vim/vim92/indent/matlab.vim diff --git a/registry/software/vim/share/vim/vim92/indent/meson.vim b/software/vim/share/vim/vim92/indent/meson.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/meson.vim rename to software/vim/share/vim/vim92/indent/meson.vim diff --git a/registry/software/vim/share/vim/vim92/indent/mf.vim b/software/vim/share/vim/vim92/indent/mf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/mf.vim rename to software/vim/share/vim/vim92/indent/mf.vim diff --git a/registry/software/vim/share/vim/vim92/indent/mma.vim b/software/vim/share/vim/vim92/indent/mma.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/mma.vim rename to software/vim/share/vim/vim92/indent/mma.vim diff --git a/registry/software/vim/share/vim/vim92/indent/mp.vim b/software/vim/share/vim/vim92/indent/mp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/mp.vim rename to software/vim/share/vim/vim92/indent/mp.vim diff --git a/registry/software/vim/share/vim/vim92/indent/nginx.vim b/software/vim/share/vim/vim92/indent/nginx.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/nginx.vim rename to software/vim/share/vim/vim92/indent/nginx.vim diff --git a/registry/software/vim/share/vim/vim92/indent/nsis.vim b/software/vim/share/vim/vim92/indent/nsis.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/nsis.vim rename to software/vim/share/vim/vim92/indent/nsis.vim diff --git a/registry/software/vim/share/vim/vim92/indent/objc.vim b/software/vim/share/vim/vim92/indent/objc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/objc.vim rename to software/vim/share/vim/vim92/indent/objc.vim diff --git a/registry/software/vim/share/vim/vim92/indent/obse.vim b/software/vim/share/vim/vim92/indent/obse.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/obse.vim rename to software/vim/share/vim/vim92/indent/obse.vim diff --git a/registry/software/vim/share/vim/vim92/indent/ocaml.vim b/software/vim/share/vim/vim92/indent/ocaml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/ocaml.vim rename to software/vim/share/vim/vim92/indent/ocaml.vim diff --git a/registry/software/vim/share/vim/vim92/indent/occam.vim b/software/vim/share/vim/vim92/indent/occam.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/occam.vim rename to software/vim/share/vim/vim92/indent/occam.vim diff --git a/registry/software/vim/share/vim/vim92/indent/pascal.vim b/software/vim/share/vim/vim92/indent/pascal.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/pascal.vim rename to software/vim/share/vim/vim92/indent/pascal.vim diff --git a/registry/software/vim/share/vim/vim92/indent/perl.vim b/software/vim/share/vim/vim92/indent/perl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/perl.vim rename to software/vim/share/vim/vim92/indent/perl.vim diff --git a/registry/software/vim/share/vim/vim92/indent/php.vim b/software/vim/share/vim/vim92/indent/php.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/php.vim rename to software/vim/share/vim/vim92/indent/php.vim diff --git a/registry/software/vim/share/vim/vim92/indent/postscr.vim b/software/vim/share/vim/vim92/indent/postscr.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/postscr.vim rename to software/vim/share/vim/vim92/indent/postscr.vim diff --git a/registry/software/vim/share/vim/vim92/indent/pov.vim b/software/vim/share/vim/vim92/indent/pov.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/pov.vim rename to software/vim/share/vim/vim92/indent/pov.vim diff --git a/registry/software/vim/share/vim/vim92/indent/prolog.vim b/software/vim/share/vim/vim92/indent/prolog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/prolog.vim rename to software/vim/share/vim/vim92/indent/prolog.vim diff --git a/registry/software/vim/share/vim/vim92/indent/ps1.vim b/software/vim/share/vim/vim92/indent/ps1.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/ps1.vim rename to software/vim/share/vim/vim92/indent/ps1.vim diff --git a/registry/software/vim/share/vim/vim92/indent/pyrex.vim b/software/vim/share/vim/vim92/indent/pyrex.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/pyrex.vim rename to software/vim/share/vim/vim92/indent/pyrex.vim diff --git a/registry/software/vim/share/vim/vim92/indent/python.vim b/software/vim/share/vim/vim92/indent/python.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/python.vim rename to software/vim/share/vim/vim92/indent/python.vim diff --git a/registry/software/vim/share/vim/vim92/indent/qb64.vim b/software/vim/share/vim/vim92/indent/qb64.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/qb64.vim rename to software/vim/share/vim/vim92/indent/qb64.vim diff --git a/registry/software/vim/share/vim/vim92/indent/quarto.vim b/software/vim/share/vim/vim92/indent/quarto.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/quarto.vim rename to software/vim/share/vim/vim92/indent/quarto.vim diff --git a/registry/software/vim/share/vim/vim92/indent/r.vim b/software/vim/share/vim/vim92/indent/r.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/r.vim rename to software/vim/share/vim/vim92/indent/r.vim diff --git a/registry/software/vim/share/vim/vim92/indent/racket.vim b/software/vim/share/vim/vim92/indent/racket.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/racket.vim rename to software/vim/share/vim/vim92/indent/racket.vim diff --git a/registry/software/vim/share/vim/vim92/indent/raku.vim b/software/vim/share/vim/vim92/indent/raku.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/raku.vim rename to software/vim/share/vim/vim92/indent/raku.vim diff --git a/registry/software/vim/share/vim/vim92/indent/raml.vim b/software/vim/share/vim/vim92/indent/raml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/raml.vim rename to software/vim/share/vim/vim92/indent/raml.vim diff --git a/registry/software/vim/share/vim/vim92/indent/readline.vim b/software/vim/share/vim/vim92/indent/readline.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/readline.vim rename to software/vim/share/vim/vim92/indent/readline.vim diff --git a/registry/software/vim/share/vim/vim92/indent/rhelp.vim b/software/vim/share/vim/vim92/indent/rhelp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/rhelp.vim rename to software/vim/share/vim/vim92/indent/rhelp.vim diff --git a/registry/software/vim/share/vim/vim92/indent/rmd.vim b/software/vim/share/vim/vim92/indent/rmd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/rmd.vim rename to software/vim/share/vim/vim92/indent/rmd.vim diff --git a/registry/software/vim/share/vim/vim92/indent/rnoweb.vim b/software/vim/share/vim/vim92/indent/rnoweb.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/rnoweb.vim rename to software/vim/share/vim/vim92/indent/rnoweb.vim diff --git a/registry/software/vim/share/vim/vim92/indent/rpl.vim b/software/vim/share/vim/vim92/indent/rpl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/rpl.vim rename to software/vim/share/vim/vim92/indent/rpl.vim diff --git a/registry/software/vim/share/vim/vim92/indent/rrst.vim b/software/vim/share/vim/vim92/indent/rrst.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/rrst.vim rename to software/vim/share/vim/vim92/indent/rrst.vim diff --git a/registry/software/vim/share/vim/vim92/indent/rst.vim b/software/vim/share/vim/vim92/indent/rst.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/rst.vim rename to software/vim/share/vim/vim92/indent/rst.vim diff --git a/registry/software/vim/share/vim/vim92/indent/ruby.vim b/software/vim/share/vim/vim92/indent/ruby.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/ruby.vim rename to software/vim/share/vim/vim92/indent/ruby.vim diff --git a/registry/software/vim/share/vim/vim92/indent/rust.vim b/software/vim/share/vim/vim92/indent/rust.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/rust.vim rename to software/vim/share/vim/vim92/indent/rust.vim diff --git a/registry/software/vim/share/vim/vim92/indent/sas.vim b/software/vim/share/vim/vim92/indent/sas.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/sas.vim rename to software/vim/share/vim/vim92/indent/sas.vim diff --git a/registry/software/vim/share/vim/vim92/indent/sass.vim b/software/vim/share/vim/vim92/indent/sass.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/sass.vim rename to software/vim/share/vim/vim92/indent/sass.vim diff --git a/registry/software/vim/share/vim/vim92/indent/scala.vim b/software/vim/share/vim/vim92/indent/scala.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/scala.vim rename to software/vim/share/vim/vim92/indent/scala.vim diff --git a/registry/software/vim/share/vim/vim92/indent/scheme.vim b/software/vim/share/vim/vim92/indent/scheme.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/scheme.vim rename to software/vim/share/vim/vim92/indent/scheme.vim diff --git a/registry/software/vim/share/vim/vim92/indent/scss.vim b/software/vim/share/vim/vim92/indent/scss.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/scss.vim rename to software/vim/share/vim/vim92/indent/scss.vim diff --git a/registry/software/vim/share/vim/vim92/indent/sdl.vim b/software/vim/share/vim/vim92/indent/sdl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/sdl.vim rename to software/vim/share/vim/vim92/indent/sdl.vim diff --git a/registry/software/vim/share/vim/vim92/indent/sh.vim b/software/vim/share/vim/vim92/indent/sh.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/sh.vim rename to software/vim/share/vim/vim92/indent/sh.vim diff --git a/registry/software/vim/share/vim/vim92/indent/sml.vim b/software/vim/share/vim/vim92/indent/sml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/sml.vim rename to software/vim/share/vim/vim92/indent/sml.vim diff --git a/registry/software/vim/share/vim/vim92/indent/solidity.vim b/software/vim/share/vim/vim92/indent/solidity.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/solidity.vim rename to software/vim/share/vim/vim92/indent/solidity.vim diff --git a/registry/software/vim/share/vim/vim92/indent/sql.vim b/software/vim/share/vim/vim92/indent/sql.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/sql.vim rename to software/vim/share/vim/vim92/indent/sql.vim diff --git a/registry/software/vim/share/vim/vim92/indent/sqlanywhere.vim b/software/vim/share/vim/vim92/indent/sqlanywhere.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/sqlanywhere.vim rename to software/vim/share/vim/vim92/indent/sqlanywhere.vim diff --git a/registry/software/vim/share/vim/vim92/indent/sshconfig.vim b/software/vim/share/vim/vim92/indent/sshconfig.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/sshconfig.vim rename to software/vim/share/vim/vim92/indent/sshconfig.vim diff --git a/registry/software/vim/share/vim/vim92/indent/systemverilog.vim b/software/vim/share/vim/vim92/indent/systemverilog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/systemverilog.vim rename to software/vim/share/vim/vim92/indent/systemverilog.vim diff --git a/registry/software/vim/share/vim/vim92/indent/tcl.vim b/software/vim/share/vim/vim92/indent/tcl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/tcl.vim rename to software/vim/share/vim/vim92/indent/tcl.vim diff --git a/registry/software/vim/share/vim/vim92/indent/tcsh.vim b/software/vim/share/vim/vim92/indent/tcsh.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/tcsh.vim rename to software/vim/share/vim/vim92/indent/tcsh.vim diff --git a/registry/software/vim/share/vim/vim92/indent/teraterm.vim b/software/vim/share/vim/vim92/indent/teraterm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/teraterm.vim rename to software/vim/share/vim/vim92/indent/teraterm.vim diff --git a/registry/software/vim/share/vim/vim92/indent/tex.vim b/software/vim/share/vim/vim92/indent/tex.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/tex.vim rename to software/vim/share/vim/vim92/indent/tex.vim diff --git a/registry/software/vim/share/vim/vim92/indent/tf.vim b/software/vim/share/vim/vim92/indent/tf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/tf.vim rename to software/vim/share/vim/vim92/indent/tf.vim diff --git a/registry/software/vim/share/vim/vim92/indent/tilde.vim b/software/vim/share/vim/vim92/indent/tilde.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/tilde.vim rename to software/vim/share/vim/vim92/indent/tilde.vim diff --git a/registry/software/vim/share/vim/vim92/indent/treetop.vim b/software/vim/share/vim/vim92/indent/treetop.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/treetop.vim rename to software/vim/share/vim/vim92/indent/treetop.vim diff --git a/registry/software/vim/share/vim/vim92/indent/typescript.vim b/software/vim/share/vim/vim92/indent/typescript.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/typescript.vim rename to software/vim/share/vim/vim92/indent/typescript.vim diff --git a/registry/software/vim/share/vim/vim92/indent/vb.vim b/software/vim/share/vim/vim92/indent/vb.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/vb.vim rename to software/vim/share/vim/vim92/indent/vb.vim diff --git a/registry/software/vim/share/vim/vim92/indent/verilog.vim b/software/vim/share/vim/vim92/indent/verilog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/verilog.vim rename to software/vim/share/vim/vim92/indent/verilog.vim diff --git a/registry/software/vim/share/vim/vim92/indent/vhdl.vim b/software/vim/share/vim/vim92/indent/vhdl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/vhdl.vim rename to software/vim/share/vim/vim92/indent/vhdl.vim diff --git a/registry/software/vim/share/vim/vim92/indent/vim.vim b/software/vim/share/vim/vim92/indent/vim.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/vim.vim rename to software/vim/share/vim/vim92/indent/vim.vim diff --git a/registry/software/vim/share/vim/vim92/indent/vroom.vim b/software/vim/share/vim/vim92/indent/vroom.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/vroom.vim rename to software/vim/share/vim/vim92/indent/vroom.vim diff --git a/registry/software/vim/share/vim/vim92/indent/vue.vim b/software/vim/share/vim/vim92/indent/vue.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/vue.vim rename to software/vim/share/vim/vim92/indent/vue.vim diff --git a/registry/software/vim/share/vim/vim92/indent/wast.vim b/software/vim/share/vim/vim92/indent/wast.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/wast.vim rename to software/vim/share/vim/vim92/indent/wast.vim diff --git a/registry/software/vim/share/vim/vim92/indent/xf86conf.vim b/software/vim/share/vim/vim92/indent/xf86conf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/xf86conf.vim rename to software/vim/share/vim/vim92/indent/xf86conf.vim diff --git a/registry/software/vim/share/vim/vim92/indent/xhtml.vim b/software/vim/share/vim/vim92/indent/xhtml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/xhtml.vim rename to software/vim/share/vim/vim92/indent/xhtml.vim diff --git a/registry/software/vim/share/vim/vim92/indent/xinetd.vim b/software/vim/share/vim/vim92/indent/xinetd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/xinetd.vim rename to software/vim/share/vim/vim92/indent/xinetd.vim diff --git a/registry/software/vim/share/vim/vim92/indent/xml.vim b/software/vim/share/vim/vim92/indent/xml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/xml.vim rename to software/vim/share/vim/vim92/indent/xml.vim diff --git a/registry/software/vim/share/vim/vim92/indent/xsd.vim b/software/vim/share/vim/vim92/indent/xsd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/xsd.vim rename to software/vim/share/vim/vim92/indent/xsd.vim diff --git a/registry/software/vim/share/vim/vim92/indent/xslt.vim b/software/vim/share/vim/vim92/indent/xslt.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/xslt.vim rename to software/vim/share/vim/vim92/indent/xslt.vim diff --git a/registry/software/vim/share/vim/vim92/indent/yacc.vim b/software/vim/share/vim/vim92/indent/yacc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/yacc.vim rename to software/vim/share/vim/vim92/indent/yacc.vim diff --git a/registry/software/vim/share/vim/vim92/indent/yaml.vim b/software/vim/share/vim/vim92/indent/yaml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/yaml.vim rename to software/vim/share/vim/vim92/indent/yaml.vim diff --git a/registry/software/vim/share/vim/vim92/indent/zig.vim b/software/vim/share/vim/vim92/indent/zig.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/zig.vim rename to software/vim/share/vim/vim92/indent/zig.vim diff --git a/registry/software/vim/share/vim/vim92/indent/zimbu.vim b/software/vim/share/vim/vim92/indent/zimbu.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/zimbu.vim rename to software/vim/share/vim/vim92/indent/zimbu.vim diff --git a/registry/software/vim/share/vim/vim92/indent/zsh.vim b/software/vim/share/vim/vim92/indent/zsh.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indent/zsh.vim rename to software/vim/share/vim/vim92/indent/zsh.vim diff --git a/registry/software/vim/share/vim/vim92/indoff.vim b/software/vim/share/vim/vim92/indoff.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/indoff.vim rename to software/vim/share/vim/vim92/indoff.vim diff --git a/registry/software/vim/share/vim/vim92/macros/editexisting.vim b/software/vim/share/vim/vim92/macros/editexisting.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/editexisting.vim rename to software/vim/share/vim/vim92/macros/editexisting.vim diff --git a/registry/software/vim/share/vim/vim92/macros/hanoi/click.me b/software/vim/share/vim/vim92/macros/hanoi/click.me similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/hanoi/click.me rename to software/vim/share/vim/vim92/macros/hanoi/click.me diff --git a/registry/software/vim/share/vim/vim92/macros/hanoi/hanoi.vim b/software/vim/share/vim/vim92/macros/hanoi/hanoi.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/hanoi/hanoi.vim rename to software/vim/share/vim/vim92/macros/hanoi/hanoi.vim diff --git a/registry/software/vim/share/vim/vim92/macros/hanoi/poster b/software/vim/share/vim/vim92/macros/hanoi/poster similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/hanoi/poster rename to software/vim/share/vim/vim92/macros/hanoi/poster diff --git a/registry/software/vim/share/vim/vim92/macros/justify.vim b/software/vim/share/vim/vim92/macros/justify.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/justify.vim rename to software/vim/share/vim/vim92/macros/justify.vim diff --git a/registry/software/vim/share/vim/vim92/macros/less.bat b/software/vim/share/vim/vim92/macros/less.bat similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/less.bat rename to software/vim/share/vim/vim92/macros/less.bat diff --git a/registry/software/vim/share/vim/vim92/macros/less.sh b/software/vim/share/vim/vim92/macros/less.sh similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/less.sh rename to software/vim/share/vim/vim92/macros/less.sh diff --git a/registry/software/vim/share/vim/vim92/macros/less.vim b/software/vim/share/vim/vim92/macros/less.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/less.vim rename to software/vim/share/vim/vim92/macros/less.vim diff --git a/registry/software/vim/share/vim/vim92/macros/life/click.me b/software/vim/share/vim/vim92/macros/life/click.me similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/life/click.me rename to software/vim/share/vim/vim92/macros/life/click.me diff --git a/registry/software/vim/share/vim/vim92/macros/life/life.vim b/software/vim/share/vim/vim92/macros/life/life.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/life/life.vim rename to software/vim/share/vim/vim92/macros/life/life.vim diff --git a/registry/software/vim/share/vim/vim92/macros/matchit.vim b/software/vim/share/vim/vim92/macros/matchit.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/matchit.vim rename to software/vim/share/vim/vim92/macros/matchit.vim diff --git a/registry/software/vim/share/vim/vim92/macros/maze/Makefile b/software/vim/share/vim/vim92/macros/maze/Makefile similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/maze/Makefile rename to software/vim/share/vim/vim92/macros/maze/Makefile diff --git a/registry/software/vim/share/vim/vim92/macros/maze/maze.c b/software/vim/share/vim/vim92/macros/maze/maze.c similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/maze/maze.c rename to software/vim/share/vim/vim92/macros/maze/maze.c diff --git a/registry/software/vim/share/vim/vim92/macros/maze/maze_5.78 b/software/vim/share/vim/vim92/macros/maze/maze_5.78 similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/maze/maze_5.78 rename to software/vim/share/vim/vim92/macros/maze/maze_5.78 diff --git a/registry/software/vim/share/vim/vim92/macros/maze/maze_mac b/software/vim/share/vim/vim92/macros/maze/maze_mac similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/maze/maze_mac rename to software/vim/share/vim/vim92/macros/maze/maze_mac diff --git a/registry/software/vim/share/vim/vim92/macros/maze/mazeansi.c b/software/vim/share/vim/vim92/macros/maze/mazeansi.c similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/maze/mazeansi.c rename to software/vim/share/vim/vim92/macros/maze/mazeansi.c diff --git a/registry/software/vim/share/vim/vim92/macros/maze/mazeclean.c b/software/vim/share/vim/vim92/macros/maze/mazeclean.c similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/maze/mazeclean.c rename to software/vim/share/vim/vim92/macros/maze/mazeclean.c diff --git a/registry/software/vim/share/vim/vim92/macros/maze/poster b/software/vim/share/vim/vim92/macros/maze/poster similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/maze/poster rename to software/vim/share/vim/vim92/macros/maze/poster diff --git a/registry/software/vim/share/vim/vim92/macros/shellmenu.vim b/software/vim/share/vim/vim92/macros/shellmenu.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/shellmenu.vim rename to software/vim/share/vim/vim92/macros/shellmenu.vim diff --git a/registry/software/vim/share/vim/vim92/macros/swapmous.vim b/software/vim/share/vim/vim92/macros/swapmous.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/swapmous.vim rename to software/vim/share/vim/vim92/macros/swapmous.vim diff --git a/registry/software/vim/share/vim/vim92/macros/urm/examples b/software/vim/share/vim/vim92/macros/urm/examples similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/urm/examples rename to software/vim/share/vim/vim92/macros/urm/examples diff --git a/registry/software/vim/share/vim/vim92/macros/urm/urm b/software/vim/share/vim/vim92/macros/urm/urm similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/urm/urm rename to software/vim/share/vim/vim92/macros/urm/urm diff --git a/registry/software/vim/share/vim/vim92/macros/urm/urm.vim b/software/vim/share/vim/vim92/macros/urm/urm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/macros/urm/urm.vim rename to software/vim/share/vim/vim92/macros/urm/urm.vim diff --git a/registry/software/vim/share/vim/vim92/menu.vim b/software/vim/share/vim/vim92/menu.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/menu.vim rename to software/vim/share/vim/vim92/menu.vim diff --git a/registry/software/vim/share/vim/vim92/mswin.vim b/software/vim/share/vim/vim92/mswin.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/mswin.vim rename to software/vim/share/vim/vim92/mswin.vim diff --git a/registry/software/vim/share/vim/vim92/optwin.vim b/software/vim/share/vim/vim92/optwin.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/optwin.vim rename to software/vim/share/vim/vim92/optwin.vim diff --git a/registry/software/vim/share/vim/vim92/plugin/getscriptPlugin.vim b/software/vim/share/vim/vim92/plugin/getscriptPlugin.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/plugin/getscriptPlugin.vim rename to software/vim/share/vim/vim92/plugin/getscriptPlugin.vim diff --git a/registry/software/vim/share/vim/vim92/plugin/gzip.vim b/software/vim/share/vim/vim92/plugin/gzip.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/plugin/gzip.vim rename to software/vim/share/vim/vim92/plugin/gzip.vim diff --git a/registry/software/vim/share/vim/vim92/plugin/logiPat.vim b/software/vim/share/vim/vim92/plugin/logiPat.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/plugin/logiPat.vim rename to software/vim/share/vim/vim92/plugin/logiPat.vim diff --git a/registry/software/vim/share/vim/vim92/plugin/manpager.vim b/software/vim/share/vim/vim92/plugin/manpager.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/plugin/manpager.vim rename to software/vim/share/vim/vim92/plugin/manpager.vim diff --git a/registry/software/vim/share/vim/vim92/plugin/matchparen.vim b/software/vim/share/vim/vim92/plugin/matchparen.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/plugin/matchparen.vim rename to software/vim/share/vim/vim92/plugin/matchparen.vim diff --git a/registry/software/vim/share/vim/vim92/plugin/netrwPlugin.vim b/software/vim/share/vim/vim92/plugin/netrwPlugin.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/plugin/netrwPlugin.vim rename to software/vim/share/vim/vim92/plugin/netrwPlugin.vim diff --git a/registry/software/vim/share/vim/vim92/plugin/rrhelper.vim b/software/vim/share/vim/vim92/plugin/rrhelper.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/plugin/rrhelper.vim rename to software/vim/share/vim/vim92/plugin/rrhelper.vim diff --git a/registry/software/vim/share/vim/vim92/plugin/spellfile.vim b/software/vim/share/vim/vim92/plugin/spellfile.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/plugin/spellfile.vim rename to software/vim/share/vim/vim92/plugin/spellfile.vim diff --git a/registry/software/vim/share/vim/vim92/plugin/tarPlugin.vim b/software/vim/share/vim/vim92/plugin/tarPlugin.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/plugin/tarPlugin.vim rename to software/vim/share/vim/vim92/plugin/tarPlugin.vim diff --git a/registry/software/vim/share/vim/vim92/plugin/tohtml.vim b/software/vim/share/vim/vim92/plugin/tohtml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/plugin/tohtml.vim rename to software/vim/share/vim/vim92/plugin/tohtml.vim diff --git a/registry/software/vim/share/vim/vim92/plugin/vimballPlugin.vim b/software/vim/share/vim/vim92/plugin/vimballPlugin.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/plugin/vimballPlugin.vim rename to software/vim/share/vim/vim92/plugin/vimballPlugin.vim diff --git a/registry/software/vim/share/vim/vim92/plugin/zipPlugin.vim b/software/vim/share/vim/vim92/plugin/zipPlugin.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/plugin/zipPlugin.vim rename to software/vim/share/vim/vim92/plugin/zipPlugin.vim diff --git a/registry/software/vim/share/vim/vim92/scripts.vim b/software/vim/share/vim/vim92/scripts.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/scripts.vim rename to software/vim/share/vim/vim92/scripts.vim diff --git a/registry/software/vim/share/vim/vim92/synmenu.vim b/software/vim/share/vim/vim92/synmenu.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/synmenu.vim rename to software/vim/share/vim/vim92/synmenu.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/2html.vim b/software/vim/share/vim/vim92/syntax/2html.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/2html.vim rename to software/vim/share/vim/vim92/syntax/2html.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/8th.vim b/software/vim/share/vim/vim92/syntax/8th.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/8th.vim rename to software/vim/share/vim/vim92/syntax/8th.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/a2ps.vim b/software/vim/share/vim/vim92/syntax/a2ps.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/a2ps.vim rename to software/vim/share/vim/vim92/syntax/a2ps.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/a65.vim b/software/vim/share/vim/vim92/syntax/a65.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/a65.vim rename to software/vim/share/vim/vim92/syntax/a65.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/aap.vim b/software/vim/share/vim/vim92/syntax/aap.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/aap.vim rename to software/vim/share/vim/vim92/syntax/aap.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/abap.vim b/software/vim/share/vim/vim92/syntax/abap.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/abap.vim rename to software/vim/share/vim/vim92/syntax/abap.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/abaqus.vim b/software/vim/share/vim/vim92/syntax/abaqus.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/abaqus.vim rename to software/vim/share/vim/vim92/syntax/abaqus.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/abc.vim b/software/vim/share/vim/vim92/syntax/abc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/abc.vim rename to software/vim/share/vim/vim92/syntax/abc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/abel.vim b/software/vim/share/vim/vim92/syntax/abel.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/abel.vim rename to software/vim/share/vim/vim92/syntax/abel.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/acedb.vim b/software/vim/share/vim/vim92/syntax/acedb.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/acedb.vim rename to software/vim/share/vim/vim92/syntax/acedb.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ada.vim b/software/vim/share/vim/vim92/syntax/ada.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ada.vim rename to software/vim/share/vim/vim92/syntax/ada.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/aflex.vim b/software/vim/share/vim/vim92/syntax/aflex.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/aflex.vim rename to software/vim/share/vim/vim92/syntax/aflex.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ahdl.vim b/software/vim/share/vim/vim92/syntax/ahdl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ahdl.vim rename to software/vim/share/vim/vim92/syntax/ahdl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/aidl.vim b/software/vim/share/vim/vim92/syntax/aidl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/aidl.vim rename to software/vim/share/vim/vim92/syntax/aidl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/alsaconf.vim b/software/vim/share/vim/vim92/syntax/alsaconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/alsaconf.vim rename to software/vim/share/vim/vim92/syntax/alsaconf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/amiga.vim b/software/vim/share/vim/vim92/syntax/amiga.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/amiga.vim rename to software/vim/share/vim/vim92/syntax/amiga.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/aml.vim b/software/vim/share/vim/vim92/syntax/aml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/aml.vim rename to software/vim/share/vim/vim92/syntax/aml.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ampl.vim b/software/vim/share/vim/vim92/syntax/ampl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ampl.vim rename to software/vim/share/vim/vim92/syntax/ampl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ant.vim b/software/vim/share/vim/vim92/syntax/ant.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ant.vim rename to software/vim/share/vim/vim92/syntax/ant.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/antlr.vim b/software/vim/share/vim/vim92/syntax/antlr.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/antlr.vim rename to software/vim/share/vim/vim92/syntax/antlr.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/apache.vim b/software/vim/share/vim/vim92/syntax/apache.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/apache.vim rename to software/vim/share/vim/vim92/syntax/apache.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/apachestyle.vim b/software/vim/share/vim/vim92/syntax/apachestyle.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/apachestyle.vim rename to software/vim/share/vim/vim92/syntax/apachestyle.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/aptconf.vim b/software/vim/share/vim/vim92/syntax/aptconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/aptconf.vim rename to software/vim/share/vim/vim92/syntax/aptconf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/arch.vim b/software/vim/share/vim/vim92/syntax/arch.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/arch.vim rename to software/vim/share/vim/vim92/syntax/arch.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/arduino.vim b/software/vim/share/vim/vim92/syntax/arduino.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/arduino.vim rename to software/vim/share/vim/vim92/syntax/arduino.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/art.vim b/software/vim/share/vim/vim92/syntax/art.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/art.vim rename to software/vim/share/vim/vim92/syntax/art.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/asciidoc.vim b/software/vim/share/vim/vim92/syntax/asciidoc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/asciidoc.vim rename to software/vim/share/vim/vim92/syntax/asciidoc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/asm.vim b/software/vim/share/vim/vim92/syntax/asm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/asm.vim rename to software/vim/share/vim/vim92/syntax/asm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/asm68k.vim b/software/vim/share/vim/vim92/syntax/asm68k.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/asm68k.vim rename to software/vim/share/vim/vim92/syntax/asm68k.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/asmh8300.vim b/software/vim/share/vim/vim92/syntax/asmh8300.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/asmh8300.vim rename to software/vim/share/vim/vim92/syntax/asmh8300.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/asn.vim b/software/vim/share/vim/vim92/syntax/asn.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/asn.vim rename to software/vim/share/vim/vim92/syntax/asn.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/aspperl.vim b/software/vim/share/vim/vim92/syntax/aspperl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/aspperl.vim rename to software/vim/share/vim/vim92/syntax/aspperl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/aspvbs.vim b/software/vim/share/vim/vim92/syntax/aspvbs.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/aspvbs.vim rename to software/vim/share/vim/vim92/syntax/aspvbs.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/asterisk.vim b/software/vim/share/vim/vim92/syntax/asterisk.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/asterisk.vim rename to software/vim/share/vim/vim92/syntax/asterisk.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/asteriskvm.vim b/software/vim/share/vim/vim92/syntax/asteriskvm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/asteriskvm.vim rename to software/vim/share/vim/vim92/syntax/asteriskvm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/atlas.vim b/software/vim/share/vim/vim92/syntax/atlas.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/atlas.vim rename to software/vim/share/vim/vim92/syntax/atlas.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/autodoc.vim b/software/vim/share/vim/vim92/syntax/autodoc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/autodoc.vim rename to software/vim/share/vim/vim92/syntax/autodoc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/autohotkey.vim b/software/vim/share/vim/vim92/syntax/autohotkey.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/autohotkey.vim rename to software/vim/share/vim/vim92/syntax/autohotkey.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/autoit.vim b/software/vim/share/vim/vim92/syntax/autoit.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/autoit.vim rename to software/vim/share/vim/vim92/syntax/autoit.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/automake.vim b/software/vim/share/vim/vim92/syntax/automake.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/automake.vim rename to software/vim/share/vim/vim92/syntax/automake.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ave.vim b/software/vim/share/vim/vim92/syntax/ave.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ave.vim rename to software/vim/share/vim/vim92/syntax/ave.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/avra.vim b/software/vim/share/vim/vim92/syntax/avra.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/avra.vim rename to software/vim/share/vim/vim92/syntax/avra.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/awk.vim b/software/vim/share/vim/vim92/syntax/awk.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/awk.vim rename to software/vim/share/vim/vim92/syntax/awk.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ayacc.vim b/software/vim/share/vim/vim92/syntax/ayacc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ayacc.vim rename to software/vim/share/vim/vim92/syntax/ayacc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/b.vim b/software/vim/share/vim/vim92/syntax/b.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/b.vim rename to software/vim/share/vim/vim92/syntax/b.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/baan.vim b/software/vim/share/vim/vim92/syntax/baan.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/baan.vim rename to software/vim/share/vim/vim92/syntax/baan.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/bash.vim b/software/vim/share/vim/vim92/syntax/bash.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/bash.vim rename to software/vim/share/vim/vim92/syntax/bash.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/basic.vim b/software/vim/share/vim/vim92/syntax/basic.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/basic.vim rename to software/vim/share/vim/vim92/syntax/basic.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/bc.vim b/software/vim/share/vim/vim92/syntax/bc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/bc.vim rename to software/vim/share/vim/vim92/syntax/bc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/bdf.vim b/software/vim/share/vim/vim92/syntax/bdf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/bdf.vim rename to software/vim/share/vim/vim92/syntax/bdf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/bib.vim b/software/vim/share/vim/vim92/syntax/bib.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/bib.vim rename to software/vim/share/vim/vim92/syntax/bib.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/bindzone.vim b/software/vim/share/vim/vim92/syntax/bindzone.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/bindzone.vim rename to software/vim/share/vim/vim92/syntax/bindzone.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/bitbake.vim b/software/vim/share/vim/vim92/syntax/bitbake.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/bitbake.vim rename to software/vim/share/vim/vim92/syntax/bitbake.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/blank.vim b/software/vim/share/vim/vim92/syntax/blank.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/blank.vim rename to software/vim/share/vim/vim92/syntax/blank.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/bsdl.vim b/software/vim/share/vim/vim92/syntax/bsdl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/bsdl.vim rename to software/vim/share/vim/vim92/syntax/bsdl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/bst.vim b/software/vim/share/vim/vim92/syntax/bst.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/bst.vim rename to software/vim/share/vim/vim92/syntax/bst.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/btm.vim b/software/vim/share/vim/vim92/syntax/btm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/btm.vim rename to software/vim/share/vim/vim92/syntax/btm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/bzl.vim b/software/vim/share/vim/vim92/syntax/bzl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/bzl.vim rename to software/vim/share/vim/vim92/syntax/bzl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/bzr.vim b/software/vim/share/vim/vim92/syntax/bzr.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/bzr.vim rename to software/vim/share/vim/vim92/syntax/bzr.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/c.vim b/software/vim/share/vim/vim92/syntax/c.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/c.vim rename to software/vim/share/vim/vim92/syntax/c.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cabal.vim b/software/vim/share/vim/vim92/syntax/cabal.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cabal.vim rename to software/vim/share/vim/vim92/syntax/cabal.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cabalconfig.vim b/software/vim/share/vim/vim92/syntax/cabalconfig.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cabalconfig.vim rename to software/vim/share/vim/vim92/syntax/cabalconfig.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cabalproject.vim b/software/vim/share/vim/vim92/syntax/cabalproject.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cabalproject.vim rename to software/vim/share/vim/vim92/syntax/cabalproject.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/calendar.vim b/software/vim/share/vim/vim92/syntax/calendar.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/calendar.vim rename to software/vim/share/vim/vim92/syntax/calendar.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/catalog.vim b/software/vim/share/vim/vim92/syntax/catalog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/catalog.vim rename to software/vim/share/vim/vim92/syntax/catalog.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cdl.vim b/software/vim/share/vim/vim92/syntax/cdl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cdl.vim rename to software/vim/share/vim/vim92/syntax/cdl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cdrdaoconf.vim b/software/vim/share/vim/vim92/syntax/cdrdaoconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cdrdaoconf.vim rename to software/vim/share/vim/vim92/syntax/cdrdaoconf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cdrtoc.vim b/software/vim/share/vim/vim92/syntax/cdrtoc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cdrtoc.vim rename to software/vim/share/vim/vim92/syntax/cdrtoc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cf.vim b/software/vim/share/vim/vim92/syntax/cf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cf.vim rename to software/vim/share/vim/vim92/syntax/cf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cfg.vim b/software/vim/share/vim/vim92/syntax/cfg.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cfg.vim rename to software/vim/share/vim/vim92/syntax/cfg.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ch.vim b/software/vim/share/vim/vim92/syntax/ch.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ch.vim rename to software/vim/share/vim/vim92/syntax/ch.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/chaiscript.vim b/software/vim/share/vim/vim92/syntax/chaiscript.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/chaiscript.vim rename to software/vim/share/vim/vim92/syntax/chaiscript.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/change.vim b/software/vim/share/vim/vim92/syntax/change.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/change.vim rename to software/vim/share/vim/vim92/syntax/change.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/changelog.vim b/software/vim/share/vim/vim92/syntax/changelog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/changelog.vim rename to software/vim/share/vim/vim92/syntax/changelog.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/chaskell.vim b/software/vim/share/vim/vim92/syntax/chaskell.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/chaskell.vim rename to software/vim/share/vim/vim92/syntax/chaskell.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/chatito.vim b/software/vim/share/vim/vim92/syntax/chatito.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/chatito.vim rename to software/vim/share/vim/vim92/syntax/chatito.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cheetah.vim b/software/vim/share/vim/vim92/syntax/cheetah.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cheetah.vim rename to software/vim/share/vim/vim92/syntax/cheetah.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/chicken.vim b/software/vim/share/vim/vim92/syntax/chicken.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/chicken.vim rename to software/vim/share/vim/vim92/syntax/chicken.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/chill.vim b/software/vim/share/vim/vim92/syntax/chill.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/chill.vim rename to software/vim/share/vim/vim92/syntax/chill.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/chordpro.vim b/software/vim/share/vim/vim92/syntax/chordpro.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/chordpro.vim rename to software/vim/share/vim/vim92/syntax/chordpro.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cl.vim b/software/vim/share/vim/vim92/syntax/cl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cl.vim rename to software/vim/share/vim/vim92/syntax/cl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/clean.vim b/software/vim/share/vim/vim92/syntax/clean.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/clean.vim rename to software/vim/share/vim/vim92/syntax/clean.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/clipper.vim b/software/vim/share/vim/vim92/syntax/clipper.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/clipper.vim rename to software/vim/share/vim/vim92/syntax/clipper.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/clojure.vim b/software/vim/share/vim/vim92/syntax/clojure.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/clojure.vim rename to software/vim/share/vim/vim92/syntax/clojure.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cmake.vim b/software/vim/share/vim/vim92/syntax/cmake.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cmake.vim rename to software/vim/share/vim/vim92/syntax/cmake.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cmod.vim b/software/vim/share/vim/vim92/syntax/cmod.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cmod.vim rename to software/vim/share/vim/vim92/syntax/cmod.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cmusrc.vim b/software/vim/share/vim/vim92/syntax/cmusrc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cmusrc.vim rename to software/vim/share/vim/vim92/syntax/cmusrc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cobol.vim b/software/vim/share/vim/vim92/syntax/cobol.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cobol.vim rename to software/vim/share/vim/vim92/syntax/cobol.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/coco.vim b/software/vim/share/vim/vim92/syntax/coco.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/coco.vim rename to software/vim/share/vim/vim92/syntax/coco.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/colortest.vim b/software/vim/share/vim/vim92/syntax/colortest.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/colortest.vim rename to software/vim/share/vim/vim92/syntax/colortest.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/conaryrecipe.vim b/software/vim/share/vim/vim92/syntax/conaryrecipe.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/conaryrecipe.vim rename to software/vim/share/vim/vim92/syntax/conaryrecipe.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/conf.vim b/software/vim/share/vim/vim92/syntax/conf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/conf.vim rename to software/vim/share/vim/vim92/syntax/conf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/config.vim b/software/vim/share/vim/vim92/syntax/config.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/config.vim rename to software/vim/share/vim/vim92/syntax/config.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/confini.vim b/software/vim/share/vim/vim92/syntax/confini.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/confini.vim rename to software/vim/share/vim/vim92/syntax/confini.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/context.vim b/software/vim/share/vim/vim92/syntax/context.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/context.vim rename to software/vim/share/vim/vim92/syntax/context.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cpp.vim b/software/vim/share/vim/vim92/syntax/cpp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cpp.vim rename to software/vim/share/vim/vim92/syntax/cpp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/crm.vim b/software/vim/share/vim/vim92/syntax/crm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/crm.vim rename to software/vim/share/vim/vim92/syntax/crm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/crontab.vim b/software/vim/share/vim/vim92/syntax/crontab.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/crontab.vim rename to software/vim/share/vim/vim92/syntax/crontab.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cs.vim b/software/vim/share/vim/vim92/syntax/cs.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cs.vim rename to software/vim/share/vim/vim92/syntax/cs.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/csc.vim b/software/vim/share/vim/vim92/syntax/csc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/csc.vim rename to software/vim/share/vim/vim92/syntax/csc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/csdl.vim b/software/vim/share/vim/vim92/syntax/csdl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/csdl.vim rename to software/vim/share/vim/vim92/syntax/csdl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/csh.vim b/software/vim/share/vim/vim92/syntax/csh.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/csh.vim rename to software/vim/share/vim/vim92/syntax/csh.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/csp.vim b/software/vim/share/vim/vim92/syntax/csp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/csp.vim rename to software/vim/share/vim/vim92/syntax/csp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/css.vim b/software/vim/share/vim/vim92/syntax/css.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/css.vim rename to software/vim/share/vim/vim92/syntax/css.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cterm.vim b/software/vim/share/vim/vim92/syntax/cterm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cterm.vim rename to software/vim/share/vim/vim92/syntax/cterm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ctrlh.vim b/software/vim/share/vim/vim92/syntax/ctrlh.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ctrlh.vim rename to software/vim/share/vim/vim92/syntax/ctrlh.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cucumber.vim b/software/vim/share/vim/vim92/syntax/cucumber.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cucumber.vim rename to software/vim/share/vim/vim92/syntax/cucumber.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cuda.vim b/software/vim/share/vim/vim92/syntax/cuda.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cuda.vim rename to software/vim/share/vim/vim92/syntax/cuda.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cupl.vim b/software/vim/share/vim/vim92/syntax/cupl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cupl.vim rename to software/vim/share/vim/vim92/syntax/cupl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cuplsim.vim b/software/vim/share/vim/vim92/syntax/cuplsim.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cuplsim.vim rename to software/vim/share/vim/vim92/syntax/cuplsim.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cvs.vim b/software/vim/share/vim/vim92/syntax/cvs.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cvs.vim rename to software/vim/share/vim/vim92/syntax/cvs.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cvsrc.vim b/software/vim/share/vim/vim92/syntax/cvsrc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cvsrc.vim rename to software/vim/share/vim/vim92/syntax/cvsrc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cweb.vim b/software/vim/share/vim/vim92/syntax/cweb.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cweb.vim rename to software/vim/share/vim/vim92/syntax/cweb.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cynlib.vim b/software/vim/share/vim/vim92/syntax/cynlib.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cynlib.vim rename to software/vim/share/vim/vim92/syntax/cynlib.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/cynpp.vim b/software/vim/share/vim/vim92/syntax/cynpp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/cynpp.vim rename to software/vim/share/vim/vim92/syntax/cynpp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/d.vim b/software/vim/share/vim/vim92/syntax/d.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/d.vim rename to software/vim/share/vim/vim92/syntax/d.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dart.vim b/software/vim/share/vim/vim92/syntax/dart.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dart.vim rename to software/vim/share/vim/vim92/syntax/dart.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/datascript.vim b/software/vim/share/vim/vim92/syntax/datascript.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/datascript.vim rename to software/vim/share/vim/vim92/syntax/datascript.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dcd.vim b/software/vim/share/vim/vim92/syntax/dcd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dcd.vim rename to software/vim/share/vim/vim92/syntax/dcd.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dcl.vim b/software/vim/share/vim/vim92/syntax/dcl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dcl.vim rename to software/vim/share/vim/vim92/syntax/dcl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/debchangelog.vim b/software/vim/share/vim/vim92/syntax/debchangelog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/debchangelog.vim rename to software/vim/share/vim/vim92/syntax/debchangelog.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/debcontrol.vim b/software/vim/share/vim/vim92/syntax/debcontrol.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/debcontrol.vim rename to software/vim/share/vim/vim92/syntax/debcontrol.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/debcopyright.vim b/software/vim/share/vim/vim92/syntax/debcopyright.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/debcopyright.vim rename to software/vim/share/vim/vim92/syntax/debcopyright.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/debsources.vim b/software/vim/share/vim/vim92/syntax/debsources.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/debsources.vim rename to software/vim/share/vim/vim92/syntax/debsources.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/def.vim b/software/vim/share/vim/vim92/syntax/def.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/def.vim rename to software/vim/share/vim/vim92/syntax/def.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/denyhosts.vim b/software/vim/share/vim/vim92/syntax/denyhosts.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/denyhosts.vim rename to software/vim/share/vim/vim92/syntax/denyhosts.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dep3patch.vim b/software/vim/share/vim/vim92/syntax/dep3patch.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dep3patch.vim rename to software/vim/share/vim/vim92/syntax/dep3patch.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/desc.vim b/software/vim/share/vim/vim92/syntax/desc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/desc.vim rename to software/vim/share/vim/vim92/syntax/desc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/desktop.vim b/software/vim/share/vim/vim92/syntax/desktop.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/desktop.vim rename to software/vim/share/vim/vim92/syntax/desktop.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dictconf.vim b/software/vim/share/vim/vim92/syntax/dictconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dictconf.vim rename to software/vim/share/vim/vim92/syntax/dictconf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dictdconf.vim b/software/vim/share/vim/vim92/syntax/dictdconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dictdconf.vim rename to software/vim/share/vim/vim92/syntax/dictdconf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/diff.vim b/software/vim/share/vim/vim92/syntax/diff.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/diff.vim rename to software/vim/share/vim/vim92/syntax/diff.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dircolors.vim b/software/vim/share/vim/vim92/syntax/dircolors.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dircolors.vim rename to software/vim/share/vim/vim92/syntax/dircolors.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dirpager.vim b/software/vim/share/vim/vim92/syntax/dirpager.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dirpager.vim rename to software/vim/share/vim/vim92/syntax/dirpager.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/diva.vim b/software/vim/share/vim/vim92/syntax/diva.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/diva.vim rename to software/vim/share/vim/vim92/syntax/diva.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/django.vim b/software/vim/share/vim/vim92/syntax/django.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/django.vim rename to software/vim/share/vim/vim92/syntax/django.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dns.vim b/software/vim/share/vim/vim92/syntax/dns.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dns.vim rename to software/vim/share/vim/vim92/syntax/dns.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dnsmasq.vim b/software/vim/share/vim/vim92/syntax/dnsmasq.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dnsmasq.vim rename to software/vim/share/vim/vim92/syntax/dnsmasq.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/docbk.vim b/software/vim/share/vim/vim92/syntax/docbk.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/docbk.vim rename to software/vim/share/vim/vim92/syntax/docbk.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/docbksgml.vim b/software/vim/share/vim/vim92/syntax/docbksgml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/docbksgml.vim rename to software/vim/share/vim/vim92/syntax/docbksgml.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/docbkxml.vim b/software/vim/share/vim/vim92/syntax/docbkxml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/docbkxml.vim rename to software/vim/share/vim/vim92/syntax/docbkxml.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dockerfile.vim b/software/vim/share/vim/vim92/syntax/dockerfile.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dockerfile.vim rename to software/vim/share/vim/vim92/syntax/dockerfile.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dosbatch.vim b/software/vim/share/vim/vim92/syntax/dosbatch.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dosbatch.vim rename to software/vim/share/vim/vim92/syntax/dosbatch.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dosini.vim b/software/vim/share/vim/vim92/syntax/dosini.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dosini.vim rename to software/vim/share/vim/vim92/syntax/dosini.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dot.vim b/software/vim/share/vim/vim92/syntax/dot.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dot.vim rename to software/vim/share/vim/vim92/syntax/dot.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/doxygen.vim b/software/vim/share/vim/vim92/syntax/doxygen.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/doxygen.vim rename to software/vim/share/vim/vim92/syntax/doxygen.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dracula.vim b/software/vim/share/vim/vim92/syntax/dracula.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dracula.vim rename to software/vim/share/vim/vim92/syntax/dracula.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dsl.vim b/software/vim/share/vim/vim92/syntax/dsl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dsl.vim rename to software/vim/share/vim/vim92/syntax/dsl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dtd.vim b/software/vim/share/vim/vim92/syntax/dtd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dtd.vim rename to software/vim/share/vim/vim92/syntax/dtd.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dtml.vim b/software/vim/share/vim/vim92/syntax/dtml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dtml.vim rename to software/vim/share/vim/vim92/syntax/dtml.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dtrace.vim b/software/vim/share/vim/vim92/syntax/dtrace.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dtrace.vim rename to software/vim/share/vim/vim92/syntax/dtrace.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dts.vim b/software/vim/share/vim/vim92/syntax/dts.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dts.vim rename to software/vim/share/vim/vim92/syntax/dts.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dune.vim b/software/vim/share/vim/vim92/syntax/dune.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dune.vim rename to software/vim/share/vim/vim92/syntax/dune.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dylan.vim b/software/vim/share/vim/vim92/syntax/dylan.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dylan.vim rename to software/vim/share/vim/vim92/syntax/dylan.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dylanintr.vim b/software/vim/share/vim/vim92/syntax/dylanintr.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dylanintr.vim rename to software/vim/share/vim/vim92/syntax/dylanintr.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/dylanlid.vim b/software/vim/share/vim/vim92/syntax/dylanlid.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/dylanlid.vim rename to software/vim/share/vim/vim92/syntax/dylanlid.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ecd.vim b/software/vim/share/vim/vim92/syntax/ecd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ecd.vim rename to software/vim/share/vim/vim92/syntax/ecd.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/edif.vim b/software/vim/share/vim/vim92/syntax/edif.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/edif.vim rename to software/vim/share/vim/vim92/syntax/edif.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/editorconfig.vim b/software/vim/share/vim/vim92/syntax/editorconfig.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/editorconfig.vim rename to software/vim/share/vim/vim92/syntax/editorconfig.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/eiffel.vim b/software/vim/share/vim/vim92/syntax/eiffel.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/eiffel.vim rename to software/vim/share/vim/vim92/syntax/eiffel.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/elf.vim b/software/vim/share/vim/vim92/syntax/elf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/elf.vim rename to software/vim/share/vim/vim92/syntax/elf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/elinks.vim b/software/vim/share/vim/vim92/syntax/elinks.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/elinks.vim rename to software/vim/share/vim/vim92/syntax/elinks.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/elm.vim b/software/vim/share/vim/vim92/syntax/elm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/elm.vim rename to software/vim/share/vim/vim92/syntax/elm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/elmfilt.vim b/software/vim/share/vim/vim92/syntax/elmfilt.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/elmfilt.vim rename to software/vim/share/vim/vim92/syntax/elmfilt.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/erlang.vim b/software/vim/share/vim/vim92/syntax/erlang.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/erlang.vim rename to software/vim/share/vim/vim92/syntax/erlang.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/eruby.vim b/software/vim/share/vim/vim92/syntax/eruby.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/eruby.vim rename to software/vim/share/vim/vim92/syntax/eruby.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/esmtprc.vim b/software/vim/share/vim/vim92/syntax/esmtprc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/esmtprc.vim rename to software/vim/share/vim/vim92/syntax/esmtprc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/esqlc.vim b/software/vim/share/vim/vim92/syntax/esqlc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/esqlc.vim rename to software/vim/share/vim/vim92/syntax/esqlc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/esterel.vim b/software/vim/share/vim/vim92/syntax/esterel.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/esterel.vim rename to software/vim/share/vim/vim92/syntax/esterel.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/eterm.vim b/software/vim/share/vim/vim92/syntax/eterm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/eterm.vim rename to software/vim/share/vim/vim92/syntax/eterm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/euphoria3.vim b/software/vim/share/vim/vim92/syntax/euphoria3.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/euphoria3.vim rename to software/vim/share/vim/vim92/syntax/euphoria3.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/euphoria4.vim b/software/vim/share/vim/vim92/syntax/euphoria4.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/euphoria4.vim rename to software/vim/share/vim/vim92/syntax/euphoria4.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/eviews.vim b/software/vim/share/vim/vim92/syntax/eviews.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/eviews.vim rename to software/vim/share/vim/vim92/syntax/eviews.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/exim.vim b/software/vim/share/vim/vim92/syntax/exim.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/exim.vim rename to software/vim/share/vim/vim92/syntax/exim.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/expect.vim b/software/vim/share/vim/vim92/syntax/expect.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/expect.vim rename to software/vim/share/vim/vim92/syntax/expect.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/exports.vim b/software/vim/share/vim/vim92/syntax/exports.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/exports.vim rename to software/vim/share/vim/vim92/syntax/exports.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/falcon.vim b/software/vim/share/vim/vim92/syntax/falcon.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/falcon.vim rename to software/vim/share/vim/vim92/syntax/falcon.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/fan.vim b/software/vim/share/vim/vim92/syntax/fan.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/fan.vim rename to software/vim/share/vim/vim92/syntax/fan.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/fasm.vim b/software/vim/share/vim/vim92/syntax/fasm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/fasm.vim rename to software/vim/share/vim/vim92/syntax/fasm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/fdcc.vim b/software/vim/share/vim/vim92/syntax/fdcc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/fdcc.vim rename to software/vim/share/vim/vim92/syntax/fdcc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/fetchmail.vim b/software/vim/share/vim/vim92/syntax/fetchmail.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/fetchmail.vim rename to software/vim/share/vim/vim92/syntax/fetchmail.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/fgl.vim b/software/vim/share/vim/vim92/syntax/fgl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/fgl.vim rename to software/vim/share/vim/vim92/syntax/fgl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/fish.vim b/software/vim/share/vim/vim92/syntax/fish.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/fish.vim rename to software/vim/share/vim/vim92/syntax/fish.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/flexwiki.vim b/software/vim/share/vim/vim92/syntax/flexwiki.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/flexwiki.vim rename to software/vim/share/vim/vim92/syntax/flexwiki.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/focexec.vim b/software/vim/share/vim/vim92/syntax/focexec.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/focexec.vim rename to software/vim/share/vim/vim92/syntax/focexec.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/form.vim b/software/vim/share/vim/vim92/syntax/form.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/form.vim rename to software/vim/share/vim/vim92/syntax/form.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/forth.vim b/software/vim/share/vim/vim92/syntax/forth.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/forth.vim rename to software/vim/share/vim/vim92/syntax/forth.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/fortran.vim b/software/vim/share/vim/vim92/syntax/fortran.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/fortran.vim rename to software/vim/share/vim/vim92/syntax/fortran.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/foxpro.vim b/software/vim/share/vim/vim92/syntax/foxpro.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/foxpro.vim rename to software/vim/share/vim/vim92/syntax/foxpro.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/fpcmake.vim b/software/vim/share/vim/vim92/syntax/fpcmake.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/fpcmake.vim rename to software/vim/share/vim/vim92/syntax/fpcmake.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/framescript.vim b/software/vim/share/vim/vim92/syntax/framescript.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/framescript.vim rename to software/vim/share/vim/vim92/syntax/framescript.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/freebasic.vim b/software/vim/share/vim/vim92/syntax/freebasic.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/freebasic.vim rename to software/vim/share/vim/vim92/syntax/freebasic.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/fstab.vim b/software/vim/share/vim/vim92/syntax/fstab.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/fstab.vim rename to software/vim/share/vim/vim92/syntax/fstab.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/fvwm.vim b/software/vim/share/vim/vim92/syntax/fvwm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/fvwm.vim rename to software/vim/share/vim/vim92/syntax/fvwm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/fvwm2m4.vim b/software/vim/share/vim/vim92/syntax/fvwm2m4.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/fvwm2m4.vim rename to software/vim/share/vim/vim92/syntax/fvwm2m4.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gdb.vim b/software/vim/share/vim/vim92/syntax/gdb.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gdb.vim rename to software/vim/share/vim/vim92/syntax/gdb.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gdmo.vim b/software/vim/share/vim/vim92/syntax/gdmo.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gdmo.vim rename to software/vim/share/vim/vim92/syntax/gdmo.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gdresource.vim b/software/vim/share/vim/vim92/syntax/gdresource.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gdresource.vim rename to software/vim/share/vim/vim92/syntax/gdresource.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gdscript.vim b/software/vim/share/vim/vim92/syntax/gdscript.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gdscript.vim rename to software/vim/share/vim/vim92/syntax/gdscript.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gdshader.vim b/software/vim/share/vim/vim92/syntax/gdshader.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gdshader.vim rename to software/vim/share/vim/vim92/syntax/gdshader.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gedcom.vim b/software/vim/share/vim/vim92/syntax/gedcom.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gedcom.vim rename to software/vim/share/vim/vim92/syntax/gedcom.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gemtext.vim b/software/vim/share/vim/vim92/syntax/gemtext.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gemtext.vim rename to software/vim/share/vim/vim92/syntax/gemtext.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gift.vim b/software/vim/share/vim/vim92/syntax/gift.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gift.vim rename to software/vim/share/vim/vim92/syntax/gift.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/git.vim b/software/vim/share/vim/vim92/syntax/git.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/git.vim rename to software/vim/share/vim/vim92/syntax/git.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gitattributes.vim b/software/vim/share/vim/vim92/syntax/gitattributes.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gitattributes.vim rename to software/vim/share/vim/vim92/syntax/gitattributes.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gitcommit.vim b/software/vim/share/vim/vim92/syntax/gitcommit.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gitcommit.vim rename to software/vim/share/vim/vim92/syntax/gitcommit.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gitconfig.vim b/software/vim/share/vim/vim92/syntax/gitconfig.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gitconfig.vim rename to software/vim/share/vim/vim92/syntax/gitconfig.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gitignore.vim b/software/vim/share/vim/vim92/syntax/gitignore.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gitignore.vim rename to software/vim/share/vim/vim92/syntax/gitignore.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gitolite.vim b/software/vim/share/vim/vim92/syntax/gitolite.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gitolite.vim rename to software/vim/share/vim/vim92/syntax/gitolite.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gitrebase.vim b/software/vim/share/vim/vim92/syntax/gitrebase.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gitrebase.vim rename to software/vim/share/vim/vim92/syntax/gitrebase.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gitsendemail.vim b/software/vim/share/vim/vim92/syntax/gitsendemail.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gitsendemail.vim rename to software/vim/share/vim/vim92/syntax/gitsendemail.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gkrellmrc.vim b/software/vim/share/vim/vim92/syntax/gkrellmrc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gkrellmrc.vim rename to software/vim/share/vim/vim92/syntax/gkrellmrc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gnash.vim b/software/vim/share/vim/vim92/syntax/gnash.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gnash.vim rename to software/vim/share/vim/vim92/syntax/gnash.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gnuplot.vim b/software/vim/share/vim/vim92/syntax/gnuplot.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gnuplot.vim rename to software/vim/share/vim/vim92/syntax/gnuplot.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/go.vim b/software/vim/share/vim/vim92/syntax/go.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/go.vim rename to software/vim/share/vim/vim92/syntax/go.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/godoc.vim b/software/vim/share/vim/vim92/syntax/godoc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/godoc.vim rename to software/vim/share/vim/vim92/syntax/godoc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gp.vim b/software/vim/share/vim/vim92/syntax/gp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gp.vim rename to software/vim/share/vim/vim92/syntax/gp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gpg.vim b/software/vim/share/vim/vim92/syntax/gpg.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gpg.vim rename to software/vim/share/vim/vim92/syntax/gpg.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gprof.vim b/software/vim/share/vim/vim92/syntax/gprof.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gprof.vim rename to software/vim/share/vim/vim92/syntax/gprof.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/grads.vim b/software/vim/share/vim/vim92/syntax/grads.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/grads.vim rename to software/vim/share/vim/vim92/syntax/grads.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gretl.vim b/software/vim/share/vim/vim92/syntax/gretl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gretl.vim rename to software/vim/share/vim/vim92/syntax/gretl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/groff.vim b/software/vim/share/vim/vim92/syntax/groff.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/groff.vim rename to software/vim/share/vim/vim92/syntax/groff.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/groovy.vim b/software/vim/share/vim/vim92/syntax/groovy.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/groovy.vim rename to software/vim/share/vim/vim92/syntax/groovy.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/group.vim b/software/vim/share/vim/vim92/syntax/group.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/group.vim rename to software/vim/share/vim/vim92/syntax/group.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/grub.vim b/software/vim/share/vim/vim92/syntax/grub.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/grub.vim rename to software/vim/share/vim/vim92/syntax/grub.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gsp.vim b/software/vim/share/vim/vim92/syntax/gsp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gsp.vim rename to software/vim/share/vim/vim92/syntax/gsp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gtkrc.vim b/software/vim/share/vim/vim92/syntax/gtkrc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gtkrc.vim rename to software/vim/share/vim/vim92/syntax/gtkrc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gvpr.vim b/software/vim/share/vim/vim92/syntax/gvpr.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gvpr.vim rename to software/vim/share/vim/vim92/syntax/gvpr.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/gyp.vim b/software/vim/share/vim/vim92/syntax/gyp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/gyp.vim rename to software/vim/share/vim/vim92/syntax/gyp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/haml.vim b/software/vim/share/vim/vim92/syntax/haml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/haml.vim rename to software/vim/share/vim/vim92/syntax/haml.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/hamster.vim b/software/vim/share/vim/vim92/syntax/hamster.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/hamster.vim rename to software/vim/share/vim/vim92/syntax/hamster.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/hare.vim b/software/vim/share/vim/vim92/syntax/hare.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/hare.vim rename to software/vim/share/vim/vim92/syntax/hare.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/haskell.vim b/software/vim/share/vim/vim92/syntax/haskell.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/haskell.vim rename to software/vim/share/vim/vim92/syntax/haskell.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/haste.vim b/software/vim/share/vim/vim92/syntax/haste.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/haste.vim rename to software/vim/share/vim/vim92/syntax/haste.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/hastepreproc.vim b/software/vim/share/vim/vim92/syntax/hastepreproc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/hastepreproc.vim rename to software/vim/share/vim/vim92/syntax/hastepreproc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/hb.vim b/software/vim/share/vim/vim92/syntax/hb.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/hb.vim rename to software/vim/share/vim/vim92/syntax/hb.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/help.vim b/software/vim/share/vim/vim92/syntax/help.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/help.vim rename to software/vim/share/vim/vim92/syntax/help.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/help_ru.vim b/software/vim/share/vim/vim92/syntax/help_ru.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/help_ru.vim rename to software/vim/share/vim/vim92/syntax/help_ru.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/hercules.vim b/software/vim/share/vim/vim92/syntax/hercules.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/hercules.vim rename to software/vim/share/vim/vim92/syntax/hercules.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/hex.vim b/software/vim/share/vim/vim92/syntax/hex.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/hex.vim rename to software/vim/share/vim/vim92/syntax/hex.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/hgcommit.vim b/software/vim/share/vim/vim92/syntax/hgcommit.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/hgcommit.vim rename to software/vim/share/vim/vim92/syntax/hgcommit.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/hitest.vim b/software/vim/share/vim/vim92/syntax/hitest.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/hitest.vim rename to software/vim/share/vim/vim92/syntax/hitest.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/hlsplaylist.vim b/software/vim/share/vim/vim92/syntax/hlsplaylist.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/hlsplaylist.vim rename to software/vim/share/vim/vim92/syntax/hlsplaylist.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/hog.vim b/software/vim/share/vim/vim92/syntax/hog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/hog.vim rename to software/vim/share/vim/vim92/syntax/hog.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/hollywood.vim b/software/vim/share/vim/vim92/syntax/hollywood.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/hollywood.vim rename to software/vim/share/vim/vim92/syntax/hollywood.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/hostconf.vim b/software/vim/share/vim/vim92/syntax/hostconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/hostconf.vim rename to software/vim/share/vim/vim92/syntax/hostconf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/hostsaccess.vim b/software/vim/share/vim/vim92/syntax/hostsaccess.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/hostsaccess.vim rename to software/vim/share/vim/vim92/syntax/hostsaccess.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/html.vim b/software/vim/share/vim/vim92/syntax/html.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/html.vim rename to software/vim/share/vim/vim92/syntax/html.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/htmlcheetah.vim b/software/vim/share/vim/vim92/syntax/htmlcheetah.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/htmlcheetah.vim rename to software/vim/share/vim/vim92/syntax/htmlcheetah.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/htmldjango.vim b/software/vim/share/vim/vim92/syntax/htmldjango.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/htmldjango.vim rename to software/vim/share/vim/vim92/syntax/htmldjango.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/htmlm4.vim b/software/vim/share/vim/vim92/syntax/htmlm4.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/htmlm4.vim rename to software/vim/share/vim/vim92/syntax/htmlm4.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/htmlos.vim b/software/vim/share/vim/vim92/syntax/htmlos.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/htmlos.vim rename to software/vim/share/vim/vim92/syntax/htmlos.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/i3config.vim b/software/vim/share/vim/vim92/syntax/i3config.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/i3config.vim rename to software/vim/share/vim/vim92/syntax/i3config.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ia64.vim b/software/vim/share/vim/vim92/syntax/ia64.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ia64.vim rename to software/vim/share/vim/vim92/syntax/ia64.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ibasic.vim b/software/vim/share/vim/vim92/syntax/ibasic.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ibasic.vim rename to software/vim/share/vim/vim92/syntax/ibasic.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/icemenu.vim b/software/vim/share/vim/vim92/syntax/icemenu.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/icemenu.vim rename to software/vim/share/vim/vim92/syntax/icemenu.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/icon.vim b/software/vim/share/vim/vim92/syntax/icon.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/icon.vim rename to software/vim/share/vim/vim92/syntax/icon.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/idl.vim b/software/vim/share/vim/vim92/syntax/idl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/idl.vim rename to software/vim/share/vim/vim92/syntax/idl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/idlang.vim b/software/vim/share/vim/vim92/syntax/idlang.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/idlang.vim rename to software/vim/share/vim/vim92/syntax/idlang.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/indent.vim b/software/vim/share/vim/vim92/syntax/indent.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/indent.vim rename to software/vim/share/vim/vim92/syntax/indent.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/inform.vim b/software/vim/share/vim/vim92/syntax/inform.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/inform.vim rename to software/vim/share/vim/vim92/syntax/inform.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/initex.vim b/software/vim/share/vim/vim92/syntax/initex.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/initex.vim rename to software/vim/share/vim/vim92/syntax/initex.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/initng.vim b/software/vim/share/vim/vim92/syntax/initng.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/initng.vim rename to software/vim/share/vim/vim92/syntax/initng.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/inittab.vim b/software/vim/share/vim/vim92/syntax/inittab.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/inittab.vim rename to software/vim/share/vim/vim92/syntax/inittab.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ipfilter.vim b/software/vim/share/vim/vim92/syntax/ipfilter.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ipfilter.vim rename to software/vim/share/vim/vim92/syntax/ipfilter.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ishd.vim b/software/vim/share/vim/vim92/syntax/ishd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ishd.vim rename to software/vim/share/vim/vim92/syntax/ishd.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/iss.vim b/software/vim/share/vim/vim92/syntax/iss.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/iss.vim rename to software/vim/share/vim/vim92/syntax/iss.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ist.vim b/software/vim/share/vim/vim92/syntax/ist.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ist.vim rename to software/vim/share/vim/vim92/syntax/ist.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/j.vim b/software/vim/share/vim/vim92/syntax/j.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/j.vim rename to software/vim/share/vim/vim92/syntax/j.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/jal.vim b/software/vim/share/vim/vim92/syntax/jal.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/jal.vim rename to software/vim/share/vim/vim92/syntax/jal.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/jam.vim b/software/vim/share/vim/vim92/syntax/jam.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/jam.vim rename to software/vim/share/vim/vim92/syntax/jam.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/jargon.vim b/software/vim/share/vim/vim92/syntax/jargon.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/jargon.vim rename to software/vim/share/vim/vim92/syntax/jargon.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/java.vim b/software/vim/share/vim/vim92/syntax/java.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/java.vim rename to software/vim/share/vim/vim92/syntax/java.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/javacc.vim b/software/vim/share/vim/vim92/syntax/javacc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/javacc.vim rename to software/vim/share/vim/vim92/syntax/javacc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/javascript.vim b/software/vim/share/vim/vim92/syntax/javascript.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/javascript.vim rename to software/vim/share/vim/vim92/syntax/javascript.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/javascriptreact.vim b/software/vim/share/vim/vim92/syntax/javascriptreact.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/javascriptreact.vim rename to software/vim/share/vim/vim92/syntax/javascriptreact.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/jess.vim b/software/vim/share/vim/vim92/syntax/jess.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/jess.vim rename to software/vim/share/vim/vim92/syntax/jess.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/jgraph.vim b/software/vim/share/vim/vim92/syntax/jgraph.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/jgraph.vim rename to software/vim/share/vim/vim92/syntax/jgraph.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/jovial.vim b/software/vim/share/vim/vim92/syntax/jovial.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/jovial.vim rename to software/vim/share/vim/vim92/syntax/jovial.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/jproperties.vim b/software/vim/share/vim/vim92/syntax/jproperties.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/jproperties.vim rename to software/vim/share/vim/vim92/syntax/jproperties.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/json.vim b/software/vim/share/vim/vim92/syntax/json.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/json.vim rename to software/vim/share/vim/vim92/syntax/json.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/jsonc.vim b/software/vim/share/vim/vim92/syntax/jsonc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/jsonc.vim rename to software/vim/share/vim/vim92/syntax/jsonc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/jsp.vim b/software/vim/share/vim/vim92/syntax/jsp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/jsp.vim rename to software/vim/share/vim/vim92/syntax/jsp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/julia.vim b/software/vim/share/vim/vim92/syntax/julia.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/julia.vim rename to software/vim/share/vim/vim92/syntax/julia.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/kconfig.vim b/software/vim/share/vim/vim92/syntax/kconfig.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/kconfig.vim rename to software/vim/share/vim/vim92/syntax/kconfig.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/kivy.vim b/software/vim/share/vim/vim92/syntax/kivy.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/kivy.vim rename to software/vim/share/vim/vim92/syntax/kivy.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/kix.vim b/software/vim/share/vim/vim92/syntax/kix.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/kix.vim rename to software/vim/share/vim/vim92/syntax/kix.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/krl.vim b/software/vim/share/vim/vim92/syntax/krl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/krl.vim rename to software/vim/share/vim/vim92/syntax/krl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/kscript.vim b/software/vim/share/vim/vim92/syntax/kscript.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/kscript.vim rename to software/vim/share/vim/vim92/syntax/kscript.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/kwt.vim b/software/vim/share/vim/vim92/syntax/kwt.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/kwt.vim rename to software/vim/share/vim/vim92/syntax/kwt.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lace.vim b/software/vim/share/vim/vim92/syntax/lace.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lace.vim rename to software/vim/share/vim/vim92/syntax/lace.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/latte.vim b/software/vim/share/vim/vim92/syntax/latte.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/latte.vim rename to software/vim/share/vim/vim92/syntax/latte.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lc.vim b/software/vim/share/vim/vim92/syntax/lc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lc.vim rename to software/vim/share/vim/vim92/syntax/lc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ld.vim b/software/vim/share/vim/vim92/syntax/ld.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ld.vim rename to software/vim/share/vim/vim92/syntax/ld.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ldapconf.vim b/software/vim/share/vim/vim92/syntax/ldapconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ldapconf.vim rename to software/vim/share/vim/vim92/syntax/ldapconf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ldif.vim b/software/vim/share/vim/vim92/syntax/ldif.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ldif.vim rename to software/vim/share/vim/vim92/syntax/ldif.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/less.vim b/software/vim/share/vim/vim92/syntax/less.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/less.vim rename to software/vim/share/vim/vim92/syntax/less.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lex.vim b/software/vim/share/vim/vim92/syntax/lex.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lex.vim rename to software/vim/share/vim/vim92/syntax/lex.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lftp.vim b/software/vim/share/vim/vim92/syntax/lftp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lftp.vim rename to software/vim/share/vim/vim92/syntax/lftp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lhaskell.vim b/software/vim/share/vim/vim92/syntax/lhaskell.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lhaskell.vim rename to software/vim/share/vim/vim92/syntax/lhaskell.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/libao.vim b/software/vim/share/vim/vim92/syntax/libao.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/libao.vim rename to software/vim/share/vim/vim92/syntax/libao.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lifelines.vim b/software/vim/share/vim/vim92/syntax/lifelines.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lifelines.vim rename to software/vim/share/vim/vim92/syntax/lifelines.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lilo.vim b/software/vim/share/vim/vim92/syntax/lilo.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lilo.vim rename to software/vim/share/vim/vim92/syntax/lilo.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/limits.vim b/software/vim/share/vim/vim92/syntax/limits.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/limits.vim rename to software/vim/share/vim/vim92/syntax/limits.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/liquid.vim b/software/vim/share/vim/vim92/syntax/liquid.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/liquid.vim rename to software/vim/share/vim/vim92/syntax/liquid.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lisp.vim b/software/vim/share/vim/vim92/syntax/lisp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lisp.vim rename to software/vim/share/vim/vim92/syntax/lisp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lite.vim b/software/vim/share/vim/vim92/syntax/lite.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lite.vim rename to software/vim/share/vim/vim92/syntax/lite.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/litestep.vim b/software/vim/share/vim/vim92/syntax/litestep.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/litestep.vim rename to software/vim/share/vim/vim92/syntax/litestep.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/loginaccess.vim b/software/vim/share/vim/vim92/syntax/loginaccess.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/loginaccess.vim rename to software/vim/share/vim/vim92/syntax/loginaccess.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/logindefs.vim b/software/vim/share/vim/vim92/syntax/logindefs.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/logindefs.vim rename to software/vim/share/vim/vim92/syntax/logindefs.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/logtalk.vim b/software/vim/share/vim/vim92/syntax/logtalk.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/logtalk.vim rename to software/vim/share/vim/vim92/syntax/logtalk.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lotos.vim b/software/vim/share/vim/vim92/syntax/lotos.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lotos.vim rename to software/vim/share/vim/vim92/syntax/lotos.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lout.vim b/software/vim/share/vim/vim92/syntax/lout.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lout.vim rename to software/vim/share/vim/vim92/syntax/lout.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lpc.vim b/software/vim/share/vim/vim92/syntax/lpc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lpc.vim rename to software/vim/share/vim/vim92/syntax/lpc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lprolog.vim b/software/vim/share/vim/vim92/syntax/lprolog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lprolog.vim rename to software/vim/share/vim/vim92/syntax/lprolog.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lscript.vim b/software/vim/share/vim/vim92/syntax/lscript.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lscript.vim rename to software/vim/share/vim/vim92/syntax/lscript.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lsl.vim b/software/vim/share/vim/vim92/syntax/lsl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lsl.vim rename to software/vim/share/vim/vim92/syntax/lsl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lss.vim b/software/vim/share/vim/vim92/syntax/lss.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lss.vim rename to software/vim/share/vim/vim92/syntax/lss.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lua.vim b/software/vim/share/vim/vim92/syntax/lua.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lua.vim rename to software/vim/share/vim/vim92/syntax/lua.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lynx.vim b/software/vim/share/vim/vim92/syntax/lynx.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lynx.vim rename to software/vim/share/vim/vim92/syntax/lynx.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/lyrics.vim b/software/vim/share/vim/vim92/syntax/lyrics.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/lyrics.vim rename to software/vim/share/vim/vim92/syntax/lyrics.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/m3build.vim b/software/vim/share/vim/vim92/syntax/m3build.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/m3build.vim rename to software/vim/share/vim/vim92/syntax/m3build.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/m3quake.vim b/software/vim/share/vim/vim92/syntax/m3quake.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/m3quake.vim rename to software/vim/share/vim/vim92/syntax/m3quake.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/m4.vim b/software/vim/share/vim/vim92/syntax/m4.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/m4.vim rename to software/vim/share/vim/vim92/syntax/m4.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mail.vim b/software/vim/share/vim/vim92/syntax/mail.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mail.vim rename to software/vim/share/vim/vim92/syntax/mail.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mailaliases.vim b/software/vim/share/vim/vim92/syntax/mailaliases.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mailaliases.vim rename to software/vim/share/vim/vim92/syntax/mailaliases.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mailcap.vim b/software/vim/share/vim/vim92/syntax/mailcap.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mailcap.vim rename to software/vim/share/vim/vim92/syntax/mailcap.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/make.vim b/software/vim/share/vim/vim92/syntax/make.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/make.vim rename to software/vim/share/vim/vim92/syntax/make.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mallard.vim b/software/vim/share/vim/vim92/syntax/mallard.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mallard.vim rename to software/vim/share/vim/vim92/syntax/mallard.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/man.vim b/software/vim/share/vim/vim92/syntax/man.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/man.vim rename to software/vim/share/vim/vim92/syntax/man.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/manconf.vim b/software/vim/share/vim/vim92/syntax/manconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/manconf.vim rename to software/vim/share/vim/vim92/syntax/manconf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/manual.vim b/software/vim/share/vim/vim92/syntax/manual.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/manual.vim rename to software/vim/share/vim/vim92/syntax/manual.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/maple.vim b/software/vim/share/vim/vim92/syntax/maple.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/maple.vim rename to software/vim/share/vim/vim92/syntax/maple.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/markdown.vim b/software/vim/share/vim/vim92/syntax/markdown.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/markdown.vim rename to software/vim/share/vim/vim92/syntax/markdown.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/masm.vim b/software/vim/share/vim/vim92/syntax/masm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/masm.vim rename to software/vim/share/vim/vim92/syntax/masm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mason.vim b/software/vim/share/vim/vim92/syntax/mason.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mason.vim rename to software/vim/share/vim/vim92/syntax/mason.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/master.vim b/software/vim/share/vim/vim92/syntax/master.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/master.vim rename to software/vim/share/vim/vim92/syntax/master.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/matlab.vim b/software/vim/share/vim/vim92/syntax/matlab.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/matlab.vim rename to software/vim/share/vim/vim92/syntax/matlab.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/maxima.vim b/software/vim/share/vim/vim92/syntax/maxima.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/maxima.vim rename to software/vim/share/vim/vim92/syntax/maxima.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mel.vim b/software/vim/share/vim/vim92/syntax/mel.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mel.vim rename to software/vim/share/vim/vim92/syntax/mel.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mermaid.vim b/software/vim/share/vim/vim92/syntax/mermaid.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mermaid.vim rename to software/vim/share/vim/vim92/syntax/mermaid.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/meson.vim b/software/vim/share/vim/vim92/syntax/meson.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/meson.vim rename to software/vim/share/vim/vim92/syntax/meson.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/messages.vim b/software/vim/share/vim/vim92/syntax/messages.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/messages.vim rename to software/vim/share/vim/vim92/syntax/messages.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mf.vim b/software/vim/share/vim/vim92/syntax/mf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mf.vim rename to software/vim/share/vim/vim92/syntax/mf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mgl.vim b/software/vim/share/vim/vim92/syntax/mgl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mgl.vim rename to software/vim/share/vim/vim92/syntax/mgl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mgp.vim b/software/vim/share/vim/vim92/syntax/mgp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mgp.vim rename to software/vim/share/vim/vim92/syntax/mgp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mib.vim b/software/vim/share/vim/vim92/syntax/mib.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mib.vim rename to software/vim/share/vim/vim92/syntax/mib.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mix.vim b/software/vim/share/vim/vim92/syntax/mix.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mix.vim rename to software/vim/share/vim/vim92/syntax/mix.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mma.vim b/software/vim/share/vim/vim92/syntax/mma.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mma.vim rename to software/vim/share/vim/vim92/syntax/mma.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mmix.vim b/software/vim/share/vim/vim92/syntax/mmix.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mmix.vim rename to software/vim/share/vim/vim92/syntax/mmix.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mmp.vim b/software/vim/share/vim/vim92/syntax/mmp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mmp.vim rename to software/vim/share/vim/vim92/syntax/mmp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/modconf.vim b/software/vim/share/vim/vim92/syntax/modconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/modconf.vim rename to software/vim/share/vim/vim92/syntax/modconf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/model.vim b/software/vim/share/vim/vim92/syntax/model.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/model.vim rename to software/vim/share/vim/vim92/syntax/model.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/modsim3.vim b/software/vim/share/vim/vim92/syntax/modsim3.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/modsim3.vim rename to software/vim/share/vim/vim92/syntax/modsim3.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/modula2.vim b/software/vim/share/vim/vim92/syntax/modula2.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/modula2.vim rename to software/vim/share/vim/vim92/syntax/modula2.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/modula3.vim b/software/vim/share/vim/vim92/syntax/modula3.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/modula3.vim rename to software/vim/share/vim/vim92/syntax/modula3.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/monk.vim b/software/vim/share/vim/vim92/syntax/monk.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/monk.vim rename to software/vim/share/vim/vim92/syntax/monk.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/moo.vim b/software/vim/share/vim/vim92/syntax/moo.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/moo.vim rename to software/vim/share/vim/vim92/syntax/moo.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mp.vim b/software/vim/share/vim/vim92/syntax/mp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mp.vim rename to software/vim/share/vim/vim92/syntax/mp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mplayerconf.vim b/software/vim/share/vim/vim92/syntax/mplayerconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mplayerconf.vim rename to software/vim/share/vim/vim92/syntax/mplayerconf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mrxvtrc.vim b/software/vim/share/vim/vim92/syntax/mrxvtrc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mrxvtrc.vim rename to software/vim/share/vim/vim92/syntax/mrxvtrc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/msidl.vim b/software/vim/share/vim/vim92/syntax/msidl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/msidl.vim rename to software/vim/share/vim/vim92/syntax/msidl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/msmessages.vim b/software/vim/share/vim/vim92/syntax/msmessages.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/msmessages.vim rename to software/vim/share/vim/vim92/syntax/msmessages.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/msql.vim b/software/vim/share/vim/vim92/syntax/msql.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/msql.vim rename to software/vim/share/vim/vim92/syntax/msql.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mupad.vim b/software/vim/share/vim/vim92/syntax/mupad.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mupad.vim rename to software/vim/share/vim/vim92/syntax/mupad.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/murphi.vim b/software/vim/share/vim/vim92/syntax/murphi.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/murphi.vim rename to software/vim/share/vim/vim92/syntax/murphi.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mush.vim b/software/vim/share/vim/vim92/syntax/mush.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mush.vim rename to software/vim/share/vim/vim92/syntax/mush.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/muttrc.vim b/software/vim/share/vim/vim92/syntax/muttrc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/muttrc.vim rename to software/vim/share/vim/vim92/syntax/muttrc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/mysql.vim b/software/vim/share/vim/vim92/syntax/mysql.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/mysql.vim rename to software/vim/share/vim/vim92/syntax/mysql.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/n1ql.vim b/software/vim/share/vim/vim92/syntax/n1ql.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/n1ql.vim rename to software/vim/share/vim/vim92/syntax/n1ql.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/named.vim b/software/vim/share/vim/vim92/syntax/named.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/named.vim rename to software/vim/share/vim/vim92/syntax/named.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/nanorc.vim b/software/vim/share/vim/vim92/syntax/nanorc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/nanorc.vim rename to software/vim/share/vim/vim92/syntax/nanorc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/nasm.vim b/software/vim/share/vim/vim92/syntax/nasm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/nasm.vim rename to software/vim/share/vim/vim92/syntax/nasm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/nastran.vim b/software/vim/share/vim/vim92/syntax/nastran.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/nastran.vim rename to software/vim/share/vim/vim92/syntax/nastran.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/natural.vim b/software/vim/share/vim/vim92/syntax/natural.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/natural.vim rename to software/vim/share/vim/vim92/syntax/natural.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ncf.vim b/software/vim/share/vim/vim92/syntax/ncf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ncf.vim rename to software/vim/share/vim/vim92/syntax/ncf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/neomuttrc.vim b/software/vim/share/vim/vim92/syntax/neomuttrc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/neomuttrc.vim rename to software/vim/share/vim/vim92/syntax/neomuttrc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/netrc.vim b/software/vim/share/vim/vim92/syntax/netrc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/netrc.vim rename to software/vim/share/vim/vim92/syntax/netrc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/netrw.vim b/software/vim/share/vim/vim92/syntax/netrw.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/netrw.vim rename to software/vim/share/vim/vim92/syntax/netrw.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/nginx.vim b/software/vim/share/vim/vim92/syntax/nginx.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/nginx.vim rename to software/vim/share/vim/vim92/syntax/nginx.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ninja.vim b/software/vim/share/vim/vim92/syntax/ninja.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ninja.vim rename to software/vim/share/vim/vim92/syntax/ninja.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/nix.vim b/software/vim/share/vim/vim92/syntax/nix.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/nix.vim rename to software/vim/share/vim/vim92/syntax/nix.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/nosyntax.vim b/software/vim/share/vim/vim92/syntax/nosyntax.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/nosyntax.vim rename to software/vim/share/vim/vim92/syntax/nosyntax.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/nqc.vim b/software/vim/share/vim/vim92/syntax/nqc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/nqc.vim rename to software/vim/share/vim/vim92/syntax/nqc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/nroff.vim b/software/vim/share/vim/vim92/syntax/nroff.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/nroff.vim rename to software/vim/share/vim/vim92/syntax/nroff.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/nsis.vim b/software/vim/share/vim/vim92/syntax/nsis.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/nsis.vim rename to software/vim/share/vim/vim92/syntax/nsis.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/obj.vim b/software/vim/share/vim/vim92/syntax/obj.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/obj.vim rename to software/vim/share/vim/vim92/syntax/obj.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/objc.vim b/software/vim/share/vim/vim92/syntax/objc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/objc.vim rename to software/vim/share/vim/vim92/syntax/objc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/objcpp.vim b/software/vim/share/vim/vim92/syntax/objcpp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/objcpp.vim rename to software/vim/share/vim/vim92/syntax/objcpp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/obse.vim b/software/vim/share/vim/vim92/syntax/obse.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/obse.vim rename to software/vim/share/vim/vim92/syntax/obse.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ocaml.vim b/software/vim/share/vim/vim92/syntax/ocaml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ocaml.vim rename to software/vim/share/vim/vim92/syntax/ocaml.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/occam.vim b/software/vim/share/vim/vim92/syntax/occam.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/occam.vim rename to software/vim/share/vim/vim92/syntax/occam.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/omnimark.vim b/software/vim/share/vim/vim92/syntax/omnimark.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/omnimark.vim rename to software/vim/share/vim/vim92/syntax/omnimark.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/opam.vim b/software/vim/share/vim/vim92/syntax/opam.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/opam.vim rename to software/vim/share/vim/vim92/syntax/opam.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/openroad.vim b/software/vim/share/vim/vim92/syntax/openroad.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/openroad.vim rename to software/vim/share/vim/vim92/syntax/openroad.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/openscad.vim b/software/vim/share/vim/vim92/syntax/openscad.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/openscad.vim rename to software/vim/share/vim/vim92/syntax/openscad.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/openvpn.vim b/software/vim/share/vim/vim92/syntax/openvpn.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/openvpn.vim rename to software/vim/share/vim/vim92/syntax/openvpn.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/opl.vim b/software/vim/share/vim/vim92/syntax/opl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/opl.vim rename to software/vim/share/vim/vim92/syntax/opl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ora.vim b/software/vim/share/vim/vim92/syntax/ora.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ora.vim rename to software/vim/share/vim/vim92/syntax/ora.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/pamconf.vim b/software/vim/share/vim/vim92/syntax/pamconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/pamconf.vim rename to software/vim/share/vim/vim92/syntax/pamconf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/pamenv.vim b/software/vim/share/vim/vim92/syntax/pamenv.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/pamenv.vim rename to software/vim/share/vim/vim92/syntax/pamenv.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/papp.vim b/software/vim/share/vim/vim92/syntax/papp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/papp.vim rename to software/vim/share/vim/vim92/syntax/papp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/pascal.vim b/software/vim/share/vim/vim92/syntax/pascal.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/pascal.vim rename to software/vim/share/vim/vim92/syntax/pascal.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/passwd.vim b/software/vim/share/vim/vim92/syntax/passwd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/passwd.vim rename to software/vim/share/vim/vim92/syntax/passwd.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/pbtxt.vim b/software/vim/share/vim/vim92/syntax/pbtxt.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/pbtxt.vim rename to software/vim/share/vim/vim92/syntax/pbtxt.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/pcap.vim b/software/vim/share/vim/vim92/syntax/pcap.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/pcap.vim rename to software/vim/share/vim/vim92/syntax/pcap.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/pccts.vim b/software/vim/share/vim/vim92/syntax/pccts.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/pccts.vim rename to software/vim/share/vim/vim92/syntax/pccts.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/pdf.vim b/software/vim/share/vim/vim92/syntax/pdf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/pdf.vim rename to software/vim/share/vim/vim92/syntax/pdf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/perl.vim b/software/vim/share/vim/vim92/syntax/perl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/perl.vim rename to software/vim/share/vim/vim92/syntax/perl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/pf.vim b/software/vim/share/vim/vim92/syntax/pf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/pf.vim rename to software/vim/share/vim/vim92/syntax/pf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/pfmain.vim b/software/vim/share/vim/vim92/syntax/pfmain.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/pfmain.vim rename to software/vim/share/vim/vim92/syntax/pfmain.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/php.vim b/software/vim/share/vim/vim92/syntax/php.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/php.vim rename to software/vim/share/vim/vim92/syntax/php.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/phtml.vim b/software/vim/share/vim/vim92/syntax/phtml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/phtml.vim rename to software/vim/share/vim/vim92/syntax/phtml.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/pic.vim b/software/vim/share/vim/vim92/syntax/pic.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/pic.vim rename to software/vim/share/vim/vim92/syntax/pic.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/pike.vim b/software/vim/share/vim/vim92/syntax/pike.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/pike.vim rename to software/vim/share/vim/vim92/syntax/pike.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/pilrc.vim b/software/vim/share/vim/vim92/syntax/pilrc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/pilrc.vim rename to software/vim/share/vim/vim92/syntax/pilrc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/pine.vim b/software/vim/share/vim/vim92/syntax/pine.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/pine.vim rename to software/vim/share/vim/vim92/syntax/pine.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/pinfo.vim b/software/vim/share/vim/vim92/syntax/pinfo.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/pinfo.vim rename to software/vim/share/vim/vim92/syntax/pinfo.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/plaintex.vim b/software/vim/share/vim/vim92/syntax/plaintex.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/plaintex.vim rename to software/vim/share/vim/vim92/syntax/plaintex.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/pli.vim b/software/vim/share/vim/vim92/syntax/pli.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/pli.vim rename to software/vim/share/vim/vim92/syntax/pli.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/plm.vim b/software/vim/share/vim/vim92/syntax/plm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/plm.vim rename to software/vim/share/vim/vim92/syntax/plm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/plp.vim b/software/vim/share/vim/vim92/syntax/plp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/plp.vim rename to software/vim/share/vim/vim92/syntax/plp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/plsql.vim b/software/vim/share/vim/vim92/syntax/plsql.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/plsql.vim rename to software/vim/share/vim/vim92/syntax/plsql.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/po.vim b/software/vim/share/vim/vim92/syntax/po.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/po.vim rename to software/vim/share/vim/vim92/syntax/po.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/pod.vim b/software/vim/share/vim/vim92/syntax/pod.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/pod.vim rename to software/vim/share/vim/vim92/syntax/pod.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/poefilter.vim b/software/vim/share/vim/vim92/syntax/poefilter.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/poefilter.vim rename to software/vim/share/vim/vim92/syntax/poefilter.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/poke.vim b/software/vim/share/vim/vim92/syntax/poke.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/poke.vim rename to software/vim/share/vim/vim92/syntax/poke.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/postscr.vim b/software/vim/share/vim/vim92/syntax/postscr.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/postscr.vim rename to software/vim/share/vim/vim92/syntax/postscr.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/pov.vim b/software/vim/share/vim/vim92/syntax/pov.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/pov.vim rename to software/vim/share/vim/vim92/syntax/pov.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/povini.vim b/software/vim/share/vim/vim92/syntax/povini.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/povini.vim rename to software/vim/share/vim/vim92/syntax/povini.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ppd.vim b/software/vim/share/vim/vim92/syntax/ppd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ppd.vim rename to software/vim/share/vim/vim92/syntax/ppd.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ppwiz.vim b/software/vim/share/vim/vim92/syntax/ppwiz.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ppwiz.vim rename to software/vim/share/vim/vim92/syntax/ppwiz.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/prescribe.vim b/software/vim/share/vim/vim92/syntax/prescribe.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/prescribe.vim rename to software/vim/share/vim/vim92/syntax/prescribe.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/privoxy.vim b/software/vim/share/vim/vim92/syntax/privoxy.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/privoxy.vim rename to software/vim/share/vim/vim92/syntax/privoxy.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/procmail.vim b/software/vim/share/vim/vim92/syntax/procmail.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/procmail.vim rename to software/vim/share/vim/vim92/syntax/procmail.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/progress.vim b/software/vim/share/vim/vim92/syntax/progress.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/progress.vim rename to software/vim/share/vim/vim92/syntax/progress.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/prolog.vim b/software/vim/share/vim/vim92/syntax/prolog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/prolog.vim rename to software/vim/share/vim/vim92/syntax/prolog.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/promela.vim b/software/vim/share/vim/vim92/syntax/promela.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/promela.vim rename to software/vim/share/vim/vim92/syntax/promela.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/proto.vim b/software/vim/share/vim/vim92/syntax/proto.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/proto.vim rename to software/vim/share/vim/vim92/syntax/proto.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/protocols.vim b/software/vim/share/vim/vim92/syntax/protocols.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/protocols.vim rename to software/vim/share/vim/vim92/syntax/protocols.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ps1.vim b/software/vim/share/vim/vim92/syntax/ps1.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ps1.vim rename to software/vim/share/vim/vim92/syntax/ps1.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ps1xml.vim b/software/vim/share/vim/vim92/syntax/ps1xml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ps1xml.vim rename to software/vim/share/vim/vim92/syntax/ps1xml.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/psf.vim b/software/vim/share/vim/vim92/syntax/psf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/psf.vim rename to software/vim/share/vim/vim92/syntax/psf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/psl.vim b/software/vim/share/vim/vim92/syntax/psl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/psl.vim rename to software/vim/share/vim/vim92/syntax/psl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ptcap.vim b/software/vim/share/vim/vim92/syntax/ptcap.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ptcap.vim rename to software/vim/share/vim/vim92/syntax/ptcap.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/purifylog.vim b/software/vim/share/vim/vim92/syntax/purifylog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/purifylog.vim rename to software/vim/share/vim/vim92/syntax/purifylog.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/pyrex.vim b/software/vim/share/vim/vim92/syntax/pyrex.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/pyrex.vim rename to software/vim/share/vim/vim92/syntax/pyrex.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/python.vim b/software/vim/share/vim/vim92/syntax/python.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/python.vim rename to software/vim/share/vim/vim92/syntax/python.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/python2.vim b/software/vim/share/vim/vim92/syntax/python2.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/python2.vim rename to software/vim/share/vim/vim92/syntax/python2.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/qb64.vim b/software/vim/share/vim/vim92/syntax/qb64.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/qb64.vim rename to software/vim/share/vim/vim92/syntax/qb64.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/qf.vim b/software/vim/share/vim/vim92/syntax/qf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/qf.vim rename to software/vim/share/vim/vim92/syntax/qf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/quake.vim b/software/vim/share/vim/vim92/syntax/quake.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/quake.vim rename to software/vim/share/vim/vim92/syntax/quake.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/quarto.vim b/software/vim/share/vim/vim92/syntax/quarto.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/quarto.vim rename to software/vim/share/vim/vim92/syntax/quarto.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/r.vim b/software/vim/share/vim/vim92/syntax/r.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/r.vim rename to software/vim/share/vim/vim92/syntax/r.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/racc.vim b/software/vim/share/vim/vim92/syntax/racc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/racc.vim rename to software/vim/share/vim/vim92/syntax/racc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/racket.vim b/software/vim/share/vim/vim92/syntax/racket.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/racket.vim rename to software/vim/share/vim/vim92/syntax/racket.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/radiance.vim b/software/vim/share/vim/vim92/syntax/radiance.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/radiance.vim rename to software/vim/share/vim/vim92/syntax/radiance.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/raku.vim b/software/vim/share/vim/vim92/syntax/raku.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/raku.vim rename to software/vim/share/vim/vim92/syntax/raku.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/raml.vim b/software/vim/share/vim/vim92/syntax/raml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/raml.vim rename to software/vim/share/vim/vim92/syntax/raml.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ratpoison.vim b/software/vim/share/vim/vim92/syntax/ratpoison.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ratpoison.vim rename to software/vim/share/vim/vim92/syntax/ratpoison.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/rc.vim b/software/vim/share/vim/vim92/syntax/rc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/rc.vim rename to software/vim/share/vim/vim92/syntax/rc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/rcs.vim b/software/vim/share/vim/vim92/syntax/rcs.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/rcs.vim rename to software/vim/share/vim/vim92/syntax/rcs.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/rcslog.vim b/software/vim/share/vim/vim92/syntax/rcslog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/rcslog.vim rename to software/vim/share/vim/vim92/syntax/rcslog.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/readline.vim b/software/vim/share/vim/vim92/syntax/readline.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/readline.vim rename to software/vim/share/vim/vim92/syntax/readline.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/rebol.vim b/software/vim/share/vim/vim92/syntax/rebol.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/rebol.vim rename to software/vim/share/vim/vim92/syntax/rebol.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/redif.vim b/software/vim/share/vim/vim92/syntax/redif.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/redif.vim rename to software/vim/share/vim/vim92/syntax/redif.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/registry.vim b/software/vim/share/vim/vim92/syntax/registry.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/registry.vim rename to software/vim/share/vim/vim92/syntax/registry.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/rego.vim b/software/vim/share/vim/vim92/syntax/rego.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/rego.vim rename to software/vim/share/vim/vim92/syntax/rego.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/remind.vim b/software/vim/share/vim/vim92/syntax/remind.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/remind.vim rename to software/vim/share/vim/vim92/syntax/remind.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/resolv.vim b/software/vim/share/vim/vim92/syntax/resolv.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/resolv.vim rename to software/vim/share/vim/vim92/syntax/resolv.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/reva.vim b/software/vim/share/vim/vim92/syntax/reva.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/reva.vim rename to software/vim/share/vim/vim92/syntax/reva.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/rexx.vim b/software/vim/share/vim/vim92/syntax/rexx.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/rexx.vim rename to software/vim/share/vim/vim92/syntax/rexx.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/rhelp.vim b/software/vim/share/vim/vim92/syntax/rhelp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/rhelp.vim rename to software/vim/share/vim/vim92/syntax/rhelp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/rib.vim b/software/vim/share/vim/vim92/syntax/rib.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/rib.vim rename to software/vim/share/vim/vim92/syntax/rib.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/rmd.vim b/software/vim/share/vim/vim92/syntax/rmd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/rmd.vim rename to software/vim/share/vim/vim92/syntax/rmd.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/rnc.vim b/software/vim/share/vim/vim92/syntax/rnc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/rnc.vim rename to software/vim/share/vim/vim92/syntax/rnc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/rng.vim b/software/vim/share/vim/vim92/syntax/rng.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/rng.vim rename to software/vim/share/vim/vim92/syntax/rng.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/rnoweb.vim b/software/vim/share/vim/vim92/syntax/rnoweb.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/rnoweb.vim rename to software/vim/share/vim/vim92/syntax/rnoweb.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/robots.vim b/software/vim/share/vim/vim92/syntax/robots.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/robots.vim rename to software/vim/share/vim/vim92/syntax/robots.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/routeros.vim b/software/vim/share/vim/vim92/syntax/routeros.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/routeros.vim rename to software/vim/share/vim/vim92/syntax/routeros.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/rpcgen.vim b/software/vim/share/vim/vim92/syntax/rpcgen.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/rpcgen.vim rename to software/vim/share/vim/vim92/syntax/rpcgen.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/rpl.vim b/software/vim/share/vim/vim92/syntax/rpl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/rpl.vim rename to software/vim/share/vim/vim92/syntax/rpl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/rrst.vim b/software/vim/share/vim/vim92/syntax/rrst.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/rrst.vim rename to software/vim/share/vim/vim92/syntax/rrst.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/rst.vim b/software/vim/share/vim/vim92/syntax/rst.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/rst.vim rename to software/vim/share/vim/vim92/syntax/rst.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/rtf.vim b/software/vim/share/vim/vim92/syntax/rtf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/rtf.vim rename to software/vim/share/vim/vim92/syntax/rtf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ruby.vim b/software/vim/share/vim/vim92/syntax/ruby.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ruby.vim rename to software/vim/share/vim/vim92/syntax/ruby.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/rust.vim b/software/vim/share/vim/vim92/syntax/rust.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/rust.vim rename to software/vim/share/vim/vim92/syntax/rust.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/samba.vim b/software/vim/share/vim/vim92/syntax/samba.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/samba.vim rename to software/vim/share/vim/vim92/syntax/samba.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sas.vim b/software/vim/share/vim/vim92/syntax/sas.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sas.vim rename to software/vim/share/vim/vim92/syntax/sas.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sass.vim b/software/vim/share/vim/vim92/syntax/sass.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sass.vim rename to software/vim/share/vim/vim92/syntax/sass.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sather.vim b/software/vim/share/vim/vim92/syntax/sather.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sather.vim rename to software/vim/share/vim/vim92/syntax/sather.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sbt.vim b/software/vim/share/vim/vim92/syntax/sbt.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sbt.vim rename to software/vim/share/vim/vim92/syntax/sbt.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/scala.vim b/software/vim/share/vim/vim92/syntax/scala.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/scala.vim rename to software/vim/share/vim/vim92/syntax/scala.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/scdoc.vim b/software/vim/share/vim/vim92/syntax/scdoc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/scdoc.vim rename to software/vim/share/vim/vim92/syntax/scdoc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/scheme.vim b/software/vim/share/vim/vim92/syntax/scheme.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/scheme.vim rename to software/vim/share/vim/vim92/syntax/scheme.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/scilab.vim b/software/vim/share/vim/vim92/syntax/scilab.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/scilab.vim rename to software/vim/share/vim/vim92/syntax/scilab.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/screen.vim b/software/vim/share/vim/vim92/syntax/screen.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/screen.vim rename to software/vim/share/vim/vim92/syntax/screen.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/scss.vim b/software/vim/share/vim/vim92/syntax/scss.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/scss.vim rename to software/vim/share/vim/vim92/syntax/scss.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sd.vim b/software/vim/share/vim/vim92/syntax/sd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sd.vim rename to software/vim/share/vim/vim92/syntax/sd.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sdc.vim b/software/vim/share/vim/vim92/syntax/sdc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sdc.vim rename to software/vim/share/vim/vim92/syntax/sdc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sdl.vim b/software/vim/share/vim/vim92/syntax/sdl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sdl.vim rename to software/vim/share/vim/vim92/syntax/sdl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sed.vim b/software/vim/share/vim/vim92/syntax/sed.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sed.vim rename to software/vim/share/vim/vim92/syntax/sed.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sendpr.vim b/software/vim/share/vim/vim92/syntax/sendpr.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sendpr.vim rename to software/vim/share/vim/vim92/syntax/sendpr.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sensors.vim b/software/vim/share/vim/vim92/syntax/sensors.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sensors.vim rename to software/vim/share/vim/vim92/syntax/sensors.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/services.vim b/software/vim/share/vim/vim92/syntax/services.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/services.vim rename to software/vim/share/vim/vim92/syntax/services.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/setserial.vim b/software/vim/share/vim/vim92/syntax/setserial.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/setserial.vim rename to software/vim/share/vim/vim92/syntax/setserial.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sexplib.vim b/software/vim/share/vim/vim92/syntax/sexplib.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sexplib.vim rename to software/vim/share/vim/vim92/syntax/sexplib.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sgml.vim b/software/vim/share/vim/vim92/syntax/sgml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sgml.vim rename to software/vim/share/vim/vim92/syntax/sgml.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sgmldecl.vim b/software/vim/share/vim/vim92/syntax/sgmldecl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sgmldecl.vim rename to software/vim/share/vim/vim92/syntax/sgmldecl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sgmllnx.vim b/software/vim/share/vim/vim92/syntax/sgmllnx.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sgmllnx.vim rename to software/vim/share/vim/vim92/syntax/sgmllnx.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sh.vim b/software/vim/share/vim/vim92/syntax/sh.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sh.vim rename to software/vim/share/vim/vim92/syntax/sh.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/shared/context-data-context.vim b/software/vim/share/vim/vim92/syntax/shared/context-data-context.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/shared/context-data-context.vim rename to software/vim/share/vim/vim92/syntax/shared/context-data-context.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/shared/context-data-interfaces.vim b/software/vim/share/vim/vim92/syntax/shared/context-data-interfaces.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/shared/context-data-interfaces.vim rename to software/vim/share/vim/vim92/syntax/shared/context-data-interfaces.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/shared/context-data-metafun.vim b/software/vim/share/vim/vim92/syntax/shared/context-data-metafun.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/shared/context-data-metafun.vim rename to software/vim/share/vim/vim92/syntax/shared/context-data-metafun.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/shared/context-data-tex.vim b/software/vim/share/vim/vim92/syntax/shared/context-data-tex.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/shared/context-data-tex.vim rename to software/vim/share/vim/vim92/syntax/shared/context-data-tex.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/shared/hgcommitDiff.vim b/software/vim/share/vim/vim92/syntax/shared/hgcommitDiff.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/shared/hgcommitDiff.vim rename to software/vim/share/vim/vim92/syntax/shared/hgcommitDiff.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/shared/typescriptcommon.vim b/software/vim/share/vim/vim92/syntax/shared/typescriptcommon.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/shared/typescriptcommon.vim rename to software/vim/share/vim/vim92/syntax/shared/typescriptcommon.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sicad.vim b/software/vim/share/vim/vim92/syntax/sicad.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sicad.vim rename to software/vim/share/vim/vim92/syntax/sicad.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sieve.vim b/software/vim/share/vim/vim92/syntax/sieve.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sieve.vim rename to software/vim/share/vim/vim92/syntax/sieve.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sil.vim b/software/vim/share/vim/vim92/syntax/sil.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sil.vim rename to software/vim/share/vim/vim92/syntax/sil.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/simula.vim b/software/vim/share/vim/vim92/syntax/simula.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/simula.vim rename to software/vim/share/vim/vim92/syntax/simula.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sinda.vim b/software/vim/share/vim/vim92/syntax/sinda.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sinda.vim rename to software/vim/share/vim/vim92/syntax/sinda.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sindacmp.vim b/software/vim/share/vim/vim92/syntax/sindacmp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sindacmp.vim rename to software/vim/share/vim/vim92/syntax/sindacmp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sindaout.vim b/software/vim/share/vim/vim92/syntax/sindaout.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sindaout.vim rename to software/vim/share/vim/vim92/syntax/sindaout.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sisu.vim b/software/vim/share/vim/vim92/syntax/sisu.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sisu.vim rename to software/vim/share/vim/vim92/syntax/sisu.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/skill.vim b/software/vim/share/vim/vim92/syntax/skill.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/skill.vim rename to software/vim/share/vim/vim92/syntax/skill.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sl.vim b/software/vim/share/vim/vim92/syntax/sl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sl.vim rename to software/vim/share/vim/vim92/syntax/sl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/slang.vim b/software/vim/share/vim/vim92/syntax/slang.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/slang.vim rename to software/vim/share/vim/vim92/syntax/slang.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/slice.vim b/software/vim/share/vim/vim92/syntax/slice.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/slice.vim rename to software/vim/share/vim/vim92/syntax/slice.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/slpconf.vim b/software/vim/share/vim/vim92/syntax/slpconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/slpconf.vim rename to software/vim/share/vim/vim92/syntax/slpconf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/slpreg.vim b/software/vim/share/vim/vim92/syntax/slpreg.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/slpreg.vim rename to software/vim/share/vim/vim92/syntax/slpreg.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/slpspi.vim b/software/vim/share/vim/vim92/syntax/slpspi.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/slpspi.vim rename to software/vim/share/vim/vim92/syntax/slpspi.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/slrnrc.vim b/software/vim/share/vim/vim92/syntax/slrnrc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/slrnrc.vim rename to software/vim/share/vim/vim92/syntax/slrnrc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/slrnsc.vim b/software/vim/share/vim/vim92/syntax/slrnsc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/slrnsc.vim rename to software/vim/share/vim/vim92/syntax/slrnsc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sm.vim b/software/vim/share/vim/vim92/syntax/sm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sm.vim rename to software/vim/share/vim/vim92/syntax/sm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/smarty.vim b/software/vim/share/vim/vim92/syntax/smarty.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/smarty.vim rename to software/vim/share/vim/vim92/syntax/smarty.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/smcl.vim b/software/vim/share/vim/vim92/syntax/smcl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/smcl.vim rename to software/vim/share/vim/vim92/syntax/smcl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/smil.vim b/software/vim/share/vim/vim92/syntax/smil.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/smil.vim rename to software/vim/share/vim/vim92/syntax/smil.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/smith.vim b/software/vim/share/vim/vim92/syntax/smith.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/smith.vim rename to software/vim/share/vim/vim92/syntax/smith.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sml.vim b/software/vim/share/vim/vim92/syntax/sml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sml.vim rename to software/vim/share/vim/vim92/syntax/sml.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/snnsnet.vim b/software/vim/share/vim/vim92/syntax/snnsnet.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/snnsnet.vim rename to software/vim/share/vim/vim92/syntax/snnsnet.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/snnspat.vim b/software/vim/share/vim/vim92/syntax/snnspat.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/snnspat.vim rename to software/vim/share/vim/vim92/syntax/snnspat.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/snnsres.vim b/software/vim/share/vim/vim92/syntax/snnsres.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/snnsres.vim rename to software/vim/share/vim/vim92/syntax/snnsres.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/snobol4.vim b/software/vim/share/vim/vim92/syntax/snobol4.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/snobol4.vim rename to software/vim/share/vim/vim92/syntax/snobol4.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/solidity.vim b/software/vim/share/vim/vim92/syntax/solidity.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/solidity.vim rename to software/vim/share/vim/vim92/syntax/solidity.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/spec.vim b/software/vim/share/vim/vim92/syntax/spec.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/spec.vim rename to software/vim/share/vim/vim92/syntax/spec.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/specman.vim b/software/vim/share/vim/vim92/syntax/specman.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/specman.vim rename to software/vim/share/vim/vim92/syntax/specman.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/spice.vim b/software/vim/share/vim/vim92/syntax/spice.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/spice.vim rename to software/vim/share/vim/vim92/syntax/spice.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/splint.vim b/software/vim/share/vim/vim92/syntax/splint.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/splint.vim rename to software/vim/share/vim/vim92/syntax/splint.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/spup.vim b/software/vim/share/vim/vim92/syntax/spup.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/spup.vim rename to software/vim/share/vim/vim92/syntax/spup.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/spyce.vim b/software/vim/share/vim/vim92/syntax/spyce.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/spyce.vim rename to software/vim/share/vim/vim92/syntax/spyce.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sql.vim b/software/vim/share/vim/vim92/syntax/sql.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sql.vim rename to software/vim/share/vim/vim92/syntax/sql.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sqlanywhere.vim b/software/vim/share/vim/vim92/syntax/sqlanywhere.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sqlanywhere.vim rename to software/vim/share/vim/vim92/syntax/sqlanywhere.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sqlforms.vim b/software/vim/share/vim/vim92/syntax/sqlforms.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sqlforms.vim rename to software/vim/share/vim/vim92/syntax/sqlforms.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sqlhana.vim b/software/vim/share/vim/vim92/syntax/sqlhana.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sqlhana.vim rename to software/vim/share/vim/vim92/syntax/sqlhana.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sqlinformix.vim b/software/vim/share/vim/vim92/syntax/sqlinformix.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sqlinformix.vim rename to software/vim/share/vim/vim92/syntax/sqlinformix.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sqlj.vim b/software/vim/share/vim/vim92/syntax/sqlj.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sqlj.vim rename to software/vim/share/vim/vim92/syntax/sqlj.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sqloracle.vim b/software/vim/share/vim/vim92/syntax/sqloracle.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sqloracle.vim rename to software/vim/share/vim/vim92/syntax/sqloracle.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sqr.vim b/software/vim/share/vim/vim92/syntax/sqr.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sqr.vim rename to software/vim/share/vim/vim92/syntax/sqr.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/squid.vim b/software/vim/share/vim/vim92/syntax/squid.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/squid.vim rename to software/vim/share/vim/vim92/syntax/squid.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/squirrel.vim b/software/vim/share/vim/vim92/syntax/squirrel.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/squirrel.vim rename to software/vim/share/vim/vim92/syntax/squirrel.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/srec.vim b/software/vim/share/vim/vim92/syntax/srec.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/srec.vim rename to software/vim/share/vim/vim92/syntax/srec.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/srt.vim b/software/vim/share/vim/vim92/syntax/srt.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/srt.vim rename to software/vim/share/vim/vim92/syntax/srt.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/ssa.vim b/software/vim/share/vim/vim92/syntax/ssa.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/ssa.vim rename to software/vim/share/vim/vim92/syntax/ssa.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sshconfig.vim b/software/vim/share/vim/vim92/syntax/sshconfig.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sshconfig.vim rename to software/vim/share/vim/vim92/syntax/sshconfig.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sshdconfig.vim b/software/vim/share/vim/vim92/syntax/sshdconfig.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sshdconfig.vim rename to software/vim/share/vim/vim92/syntax/sshdconfig.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/st.vim b/software/vim/share/vim/vim92/syntax/st.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/st.vim rename to software/vim/share/vim/vim92/syntax/st.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/stata.vim b/software/vim/share/vim/vim92/syntax/stata.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/stata.vim rename to software/vim/share/vim/vim92/syntax/stata.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/stp.vim b/software/vim/share/vim/vim92/syntax/stp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/stp.vim rename to software/vim/share/vim/vim92/syntax/stp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/strace.vim b/software/vim/share/vim/vim92/syntax/strace.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/strace.vim rename to software/vim/share/vim/vim92/syntax/strace.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/structurizr.vim b/software/vim/share/vim/vim92/syntax/structurizr.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/structurizr.vim rename to software/vim/share/vim/vim92/syntax/structurizr.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sudoers.vim b/software/vim/share/vim/vim92/syntax/sudoers.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sudoers.vim rename to software/vim/share/vim/vim92/syntax/sudoers.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/svg.vim b/software/vim/share/vim/vim92/syntax/svg.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/svg.vim rename to software/vim/share/vim/vim92/syntax/svg.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/svn.vim b/software/vim/share/vim/vim92/syntax/svn.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/svn.vim rename to software/vim/share/vim/vim92/syntax/svn.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/swayconfig.vim b/software/vim/share/vim/vim92/syntax/swayconfig.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/swayconfig.vim rename to software/vim/share/vim/vim92/syntax/swayconfig.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/swift.vim b/software/vim/share/vim/vim92/syntax/swift.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/swift.vim rename to software/vim/share/vim/vim92/syntax/swift.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/swiftgyb.vim b/software/vim/share/vim/vim92/syntax/swiftgyb.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/swiftgyb.vim rename to software/vim/share/vim/vim92/syntax/swiftgyb.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/syncolor.vim b/software/vim/share/vim/vim92/syntax/syncolor.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/syncolor.vim rename to software/vim/share/vim/vim92/syntax/syncolor.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/synload.vim b/software/vim/share/vim/vim92/syntax/synload.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/synload.vim rename to software/vim/share/vim/vim92/syntax/synload.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/syntax.vim b/software/vim/share/vim/vim92/syntax/syntax.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/syntax.vim rename to software/vim/share/vim/vim92/syntax/syntax.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/sysctl.vim b/software/vim/share/vim/vim92/syntax/sysctl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/sysctl.vim rename to software/vim/share/vim/vim92/syntax/sysctl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/systemd.vim b/software/vim/share/vim/vim92/syntax/systemd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/systemd.vim rename to software/vim/share/vim/vim92/syntax/systemd.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/systemverilog.vim b/software/vim/share/vim/vim92/syntax/systemverilog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/systemverilog.vim rename to software/vim/share/vim/vim92/syntax/systemverilog.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tads.vim b/software/vim/share/vim/vim92/syntax/tads.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tads.vim rename to software/vim/share/vim/vim92/syntax/tads.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tags.vim b/software/vim/share/vim/vim92/syntax/tags.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tags.vim rename to software/vim/share/vim/vim92/syntax/tags.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tak.vim b/software/vim/share/vim/vim92/syntax/tak.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tak.vim rename to software/vim/share/vim/vim92/syntax/tak.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/takcmp.vim b/software/vim/share/vim/vim92/syntax/takcmp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/takcmp.vim rename to software/vim/share/vim/vim92/syntax/takcmp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/takout.vim b/software/vim/share/vim/vim92/syntax/takout.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/takout.vim rename to software/vim/share/vim/vim92/syntax/takout.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tap.vim b/software/vim/share/vim/vim92/syntax/tap.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tap.vim rename to software/vim/share/vim/vim92/syntax/tap.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tar.vim b/software/vim/share/vim/vim92/syntax/tar.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tar.vim rename to software/vim/share/vim/vim92/syntax/tar.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/taskdata.vim b/software/vim/share/vim/vim92/syntax/taskdata.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/taskdata.vim rename to software/vim/share/vim/vim92/syntax/taskdata.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/taskedit.vim b/software/vim/share/vim/vim92/syntax/taskedit.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/taskedit.vim rename to software/vim/share/vim/vim92/syntax/taskedit.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tasm.vim b/software/vim/share/vim/vim92/syntax/tasm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tasm.vim rename to software/vim/share/vim/vim92/syntax/tasm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tcl.vim b/software/vim/share/vim/vim92/syntax/tcl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tcl.vim rename to software/vim/share/vim/vim92/syntax/tcl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tcsh.vim b/software/vim/share/vim/vim92/syntax/tcsh.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tcsh.vim rename to software/vim/share/vim/vim92/syntax/tcsh.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/template.vim b/software/vim/share/vim/vim92/syntax/template.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/template.vim rename to software/vim/share/vim/vim92/syntax/template.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/teraterm.vim b/software/vim/share/vim/vim92/syntax/teraterm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/teraterm.vim rename to software/vim/share/vim/vim92/syntax/teraterm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/terminfo.vim b/software/vim/share/vim/vim92/syntax/terminfo.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/terminfo.vim rename to software/vim/share/vim/vim92/syntax/terminfo.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tex.vim b/software/vim/share/vim/vim92/syntax/tex.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tex.vim rename to software/vim/share/vim/vim92/syntax/tex.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/texinfo.vim b/software/vim/share/vim/vim92/syntax/texinfo.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/texinfo.vim rename to software/vim/share/vim/vim92/syntax/texinfo.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/texmf.vim b/software/vim/share/vim/vim92/syntax/texmf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/texmf.vim rename to software/vim/share/vim/vim92/syntax/texmf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tf.vim b/software/vim/share/vim/vim92/syntax/tf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tf.vim rename to software/vim/share/vim/vim92/syntax/tf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tidy.vim b/software/vim/share/vim/vim92/syntax/tidy.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tidy.vim rename to software/vim/share/vim/vim92/syntax/tidy.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tilde.vim b/software/vim/share/vim/vim92/syntax/tilde.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tilde.vim rename to software/vim/share/vim/vim92/syntax/tilde.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tli.vim b/software/vim/share/vim/vim92/syntax/tli.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tli.vim rename to software/vim/share/vim/vim92/syntax/tli.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tmux.vim b/software/vim/share/vim/vim92/syntax/tmux.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tmux.vim rename to software/vim/share/vim/vim92/syntax/tmux.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/toml.vim b/software/vim/share/vim/vim92/syntax/toml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/toml.vim rename to software/vim/share/vim/vim92/syntax/toml.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tpp.vim b/software/vim/share/vim/vim92/syntax/tpp.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tpp.vim rename to software/vim/share/vim/vim92/syntax/tpp.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/trasys.vim b/software/vim/share/vim/vim92/syntax/trasys.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/trasys.vim rename to software/vim/share/vim/vim92/syntax/trasys.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/treetop.vim b/software/vim/share/vim/vim92/syntax/treetop.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/treetop.vim rename to software/vim/share/vim/vim92/syntax/treetop.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/trustees.vim b/software/vim/share/vim/vim92/syntax/trustees.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/trustees.vim rename to software/vim/share/vim/vim92/syntax/trustees.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tsalt.vim b/software/vim/share/vim/vim92/syntax/tsalt.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tsalt.vim rename to software/vim/share/vim/vim92/syntax/tsalt.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tsscl.vim b/software/vim/share/vim/vim92/syntax/tsscl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tsscl.vim rename to software/vim/share/vim/vim92/syntax/tsscl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tssgm.vim b/software/vim/share/vim/vim92/syntax/tssgm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tssgm.vim rename to software/vim/share/vim/vim92/syntax/tssgm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tssop.vim b/software/vim/share/vim/vim92/syntax/tssop.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tssop.vim rename to software/vim/share/vim/vim92/syntax/tssop.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tt2.vim b/software/vim/share/vim/vim92/syntax/tt2.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tt2.vim rename to software/vim/share/vim/vim92/syntax/tt2.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tt2html.vim b/software/vim/share/vim/vim92/syntax/tt2html.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tt2html.vim rename to software/vim/share/vim/vim92/syntax/tt2html.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/tt2js.vim b/software/vim/share/vim/vim92/syntax/tt2js.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/tt2js.vim rename to software/vim/share/vim/vim92/syntax/tt2js.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/typescript.vim b/software/vim/share/vim/vim92/syntax/typescript.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/typescript.vim rename to software/vim/share/vim/vim92/syntax/typescript.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/typescriptreact.vim b/software/vim/share/vim/vim92/syntax/typescriptreact.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/typescriptreact.vim rename to software/vim/share/vim/vim92/syntax/typescriptreact.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/uc.vim b/software/vim/share/vim/vim92/syntax/uc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/uc.vim rename to software/vim/share/vim/vim92/syntax/uc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/udevconf.vim b/software/vim/share/vim/vim92/syntax/udevconf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/udevconf.vim rename to software/vim/share/vim/vim92/syntax/udevconf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/udevperm.vim b/software/vim/share/vim/vim92/syntax/udevperm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/udevperm.vim rename to software/vim/share/vim/vim92/syntax/udevperm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/udevrules.vim b/software/vim/share/vim/vim92/syntax/udevrules.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/udevrules.vim rename to software/vim/share/vim/vim92/syntax/udevrules.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/uil.vim b/software/vim/share/vim/vim92/syntax/uil.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/uil.vim rename to software/vim/share/vim/vim92/syntax/uil.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/updatedb.vim b/software/vim/share/vim/vim92/syntax/updatedb.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/updatedb.vim rename to software/vim/share/vim/vim92/syntax/updatedb.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/upstart.vim b/software/vim/share/vim/vim92/syntax/upstart.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/upstart.vim rename to software/vim/share/vim/vim92/syntax/upstart.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/upstreamdat.vim b/software/vim/share/vim/vim92/syntax/upstreamdat.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/upstreamdat.vim rename to software/vim/share/vim/vim92/syntax/upstreamdat.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/upstreaminstalllog.vim b/software/vim/share/vim/vim92/syntax/upstreaminstalllog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/upstreaminstalllog.vim rename to software/vim/share/vim/vim92/syntax/upstreaminstalllog.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/upstreamlog.vim b/software/vim/share/vim/vim92/syntax/upstreamlog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/upstreamlog.vim rename to software/vim/share/vim/vim92/syntax/upstreamlog.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/upstreamrpt.vim b/software/vim/share/vim/vim92/syntax/upstreamrpt.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/upstreamrpt.vim rename to software/vim/share/vim/vim92/syntax/upstreamrpt.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/usserverlog.vim b/software/vim/share/vim/vim92/syntax/usserverlog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/usserverlog.vim rename to software/vim/share/vim/vim92/syntax/usserverlog.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/usw2kagtlog.vim b/software/vim/share/vim/vim92/syntax/usw2kagtlog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/usw2kagtlog.vim rename to software/vim/share/vim/vim92/syntax/usw2kagtlog.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/valgrind.vim b/software/vim/share/vim/vim92/syntax/valgrind.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/valgrind.vim rename to software/vim/share/vim/vim92/syntax/valgrind.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/vb.vim b/software/vim/share/vim/vim92/syntax/vb.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/vb.vim rename to software/vim/share/vim/vim92/syntax/vb.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/vdf.vim b/software/vim/share/vim/vim92/syntax/vdf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/vdf.vim rename to software/vim/share/vim/vim92/syntax/vdf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/vera.vim b/software/vim/share/vim/vim92/syntax/vera.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/vera.vim rename to software/vim/share/vim/vim92/syntax/vera.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/verilog.vim b/software/vim/share/vim/vim92/syntax/verilog.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/verilog.vim rename to software/vim/share/vim/vim92/syntax/verilog.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/verilogams.vim b/software/vim/share/vim/vim92/syntax/verilogams.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/verilogams.vim rename to software/vim/share/vim/vim92/syntax/verilogams.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/vgrindefs.vim b/software/vim/share/vim/vim92/syntax/vgrindefs.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/vgrindefs.vim rename to software/vim/share/vim/vim92/syntax/vgrindefs.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/vhdl.vim b/software/vim/share/vim/vim92/syntax/vhdl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/vhdl.vim rename to software/vim/share/vim/vim92/syntax/vhdl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/vim.vim b/software/vim/share/vim/vim92/syntax/vim.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/vim.vim rename to software/vim/share/vim/vim92/syntax/vim.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/viminfo.vim b/software/vim/share/vim/vim92/syntax/viminfo.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/viminfo.vim rename to software/vim/share/vim/vim92/syntax/viminfo.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/virata.vim b/software/vim/share/vim/vim92/syntax/virata.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/virata.vim rename to software/vim/share/vim/vim92/syntax/virata.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/vmasm.vim b/software/vim/share/vim/vim92/syntax/vmasm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/vmasm.vim rename to software/vim/share/vim/vim92/syntax/vmasm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/voscm.vim b/software/vim/share/vim/vim92/syntax/voscm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/voscm.vim rename to software/vim/share/vim/vim92/syntax/voscm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/vrml.vim b/software/vim/share/vim/vim92/syntax/vrml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/vrml.vim rename to software/vim/share/vim/vim92/syntax/vrml.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/vroom.vim b/software/vim/share/vim/vim92/syntax/vroom.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/vroom.vim rename to software/vim/share/vim/vim92/syntax/vroom.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/vsejcl.vim b/software/vim/share/vim/vim92/syntax/vsejcl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/vsejcl.vim rename to software/vim/share/vim/vim92/syntax/vsejcl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/vue.vim b/software/vim/share/vim/vim92/syntax/vue.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/vue.vim rename to software/vim/share/vim/vim92/syntax/vue.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/wast.vim b/software/vim/share/vim/vim92/syntax/wast.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/wast.vim rename to software/vim/share/vim/vim92/syntax/wast.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/wdiff.vim b/software/vim/share/vim/vim92/syntax/wdiff.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/wdiff.vim rename to software/vim/share/vim/vim92/syntax/wdiff.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/wdl.vim b/software/vim/share/vim/vim92/syntax/wdl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/wdl.vim rename to software/vim/share/vim/vim92/syntax/wdl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/web.vim b/software/vim/share/vim/vim92/syntax/web.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/web.vim rename to software/vim/share/vim/vim92/syntax/web.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/webmacro.vim b/software/vim/share/vim/vim92/syntax/webmacro.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/webmacro.vim rename to software/vim/share/vim/vim92/syntax/webmacro.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/wget.vim b/software/vim/share/vim/vim92/syntax/wget.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/wget.vim rename to software/vim/share/vim/vim92/syntax/wget.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/wget2.vim b/software/vim/share/vim/vim92/syntax/wget2.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/wget2.vim rename to software/vim/share/vim/vim92/syntax/wget2.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/whitespace.vim b/software/vim/share/vim/vim92/syntax/whitespace.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/whitespace.vim rename to software/vim/share/vim/vim92/syntax/whitespace.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/winbatch.vim b/software/vim/share/vim/vim92/syntax/winbatch.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/winbatch.vim rename to software/vim/share/vim/vim92/syntax/winbatch.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/wml.vim b/software/vim/share/vim/vim92/syntax/wml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/wml.vim rename to software/vim/share/vim/vim92/syntax/wml.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/wsh.vim b/software/vim/share/vim/vim92/syntax/wsh.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/wsh.vim rename to software/vim/share/vim/vim92/syntax/wsh.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/wsml.vim b/software/vim/share/vim/vim92/syntax/wsml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/wsml.vim rename to software/vim/share/vim/vim92/syntax/wsml.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/wvdial.vim b/software/vim/share/vim/vim92/syntax/wvdial.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/wvdial.vim rename to software/vim/share/vim/vim92/syntax/wvdial.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/xbl.vim b/software/vim/share/vim/vim92/syntax/xbl.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/xbl.vim rename to software/vim/share/vim/vim92/syntax/xbl.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/xdefaults.vim b/software/vim/share/vim/vim92/syntax/xdefaults.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/xdefaults.vim rename to software/vim/share/vim/vim92/syntax/xdefaults.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/xf86conf.vim b/software/vim/share/vim/vim92/syntax/xf86conf.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/xf86conf.vim rename to software/vim/share/vim/vim92/syntax/xf86conf.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/xhtml.vim b/software/vim/share/vim/vim92/syntax/xhtml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/xhtml.vim rename to software/vim/share/vim/vim92/syntax/xhtml.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/xinetd.vim b/software/vim/share/vim/vim92/syntax/xinetd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/xinetd.vim rename to software/vim/share/vim/vim92/syntax/xinetd.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/xkb.vim b/software/vim/share/vim/vim92/syntax/xkb.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/xkb.vim rename to software/vim/share/vim/vim92/syntax/xkb.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/xmath.vim b/software/vim/share/vim/vim92/syntax/xmath.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/xmath.vim rename to software/vim/share/vim/vim92/syntax/xmath.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/xml.vim b/software/vim/share/vim/vim92/syntax/xml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/xml.vim rename to software/vim/share/vim/vim92/syntax/xml.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/xmodmap.vim b/software/vim/share/vim/vim92/syntax/xmodmap.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/xmodmap.vim rename to software/vim/share/vim/vim92/syntax/xmodmap.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/xpm.vim b/software/vim/share/vim/vim92/syntax/xpm.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/xpm.vim rename to software/vim/share/vim/vim92/syntax/xpm.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/xpm2.vim b/software/vim/share/vim/vim92/syntax/xpm2.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/xpm2.vim rename to software/vim/share/vim/vim92/syntax/xpm2.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/xquery.vim b/software/vim/share/vim/vim92/syntax/xquery.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/xquery.vim rename to software/vim/share/vim/vim92/syntax/xquery.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/xs.vim b/software/vim/share/vim/vim92/syntax/xs.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/xs.vim rename to software/vim/share/vim/vim92/syntax/xs.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/xsd.vim b/software/vim/share/vim/vim92/syntax/xsd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/xsd.vim rename to software/vim/share/vim/vim92/syntax/xsd.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/xslt.vim b/software/vim/share/vim/vim92/syntax/xslt.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/xslt.vim rename to software/vim/share/vim/vim92/syntax/xslt.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/xxd.vim b/software/vim/share/vim/vim92/syntax/xxd.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/xxd.vim rename to software/vim/share/vim/vim92/syntax/xxd.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/yacc.vim b/software/vim/share/vim/vim92/syntax/yacc.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/yacc.vim rename to software/vim/share/vim/vim92/syntax/yacc.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/yaml.vim b/software/vim/share/vim/vim92/syntax/yaml.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/yaml.vim rename to software/vim/share/vim/vim92/syntax/yaml.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/z8a.vim b/software/vim/share/vim/vim92/syntax/z8a.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/z8a.vim rename to software/vim/share/vim/vim92/syntax/z8a.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/zig.vim b/software/vim/share/vim/vim92/syntax/zig.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/zig.vim rename to software/vim/share/vim/vim92/syntax/zig.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/zimbu.vim b/software/vim/share/vim/vim92/syntax/zimbu.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/zimbu.vim rename to software/vim/share/vim/vim92/syntax/zimbu.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/zir.vim b/software/vim/share/vim/vim92/syntax/zir.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/zir.vim rename to software/vim/share/vim/vim92/syntax/zir.vim diff --git a/registry/software/vim/share/vim/vim92/syntax/zsh.vim b/software/vim/share/vim/vim92/syntax/zsh.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/syntax/zsh.vim rename to software/vim/share/vim/vim92/syntax/zsh.vim diff --git a/registry/software/vim/share/vim/vim92/vimrc_example.vim b/software/vim/share/vim/vim92/vimrc_example.vim similarity index 100% rename from registry/software/vim/share/vim/vim92/vimrc_example.vim rename to software/vim/share/vim/vim92/vimrc_example.vim diff --git a/registry/software/vix/src/index.ts b/software/vim/src/index.ts similarity index 100% rename from registry/software/vix/src/index.ts rename to software/vim/src/index.ts diff --git a/software/vim/test/manifest.test.ts b/software/vim/test/manifest.test.ts new file mode 100644 index 0000000000..89139d33b8 --- /dev/null +++ b/software/vim/test/manifest.test.ts @@ -0,0 +1,19 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const packageDir = new URL("..", import.meta.url).pathname; + +describe("package manifest", () => { + it("declares command binaries", () => { + const manifest = JSON.parse( + readFileSync(join(packageDir, "agentos-package.json"), "utf8"), + ); + + expect(manifest.commands?.length ?? 0).toBeGreaterThan(0); + for (const command of manifest.commands) { + expect(typeof command).toBe("string"); + expect(command.length).toBeGreaterThan(0); + } + }); +}); diff --git a/software/vim/test/vim.test.ts b/software/vim/test/vim.test.ts new file mode 100644 index 0000000000..0fb5ab7724 --- /dev/null +++ b/software/vim/test/vim.test.ts @@ -0,0 +1,115 @@ +import { existsSync } from "node:fs"; +import { cp, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + NodeFileSystem, + createKernel, + createWasmVmRuntime, + describeIf, +} from "@agentos/test-harness"; +import type { Kernel } from "@agentos/test-harness"; +import { afterEach, expect, it } from "vitest"; + +const VIM_COMMAND_DIR = fileURLToPath(new URL("../bin", import.meta.url)); +const VIM_RUNTIME_DIR = fileURLToPath( + new URL("../dist/package/share/vim/vim92", import.meta.url), +); +const hasVimPackage = existsSync(join(VIM_COMMAND_DIR, "vim")) && + existsSync(join(VIM_RUNTIME_DIR, "defaults.vim")); + +let tempRoot: string | undefined; + +async function writeFixture(path: string, contents: string): Promise { + if (!tempRoot) throw new Error("fixture root not initialized"); + const hostPath = join(tempRoot, path.replace(/^\/+/, "")); + await mkdir(dirname(hostPath), { recursive: true }); + await writeFile(hostPath, contents); +} + +async function createTestVFS(): Promise { + tempRoot = await mkdtemp(join(tmpdir(), "agentos-vim-")); + await writeFixture("/project/input.txt", "alpha\nbeta\ngamma\n"); + await writeFixture( + "/project/edit.vim", + "set nomore\nedit /project/input.txt\n%s/beta/delta/\nwrite\nquitall!\n", + ); + await cp(VIM_RUNTIME_DIR, join(tempRoot, "usr/local/share/vim/vim92"), { + recursive: true, + }); + return new NodeFileSystem({ root: tempRoot }); +} + +describeIf(hasVimPackage, "vim command", { timeout: 60_000 }, () => { + let kernel: Kernel | undefined; + + afterEach(async () => { + await kernel?.dispose(); + kernel = undefined; + if (tempRoot) { + await rm(tempRoot, { recursive: true, force: true }); + tempRoot = undefined; + } + }); + + async function mountFixture(): Promise { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [VIM_COMMAND_DIR] })); + } + + async function runVim(args: string[]) { + if (!kernel) throw new Error("kernel not mounted"); + let stdout = ""; + let stderr = ""; + const proc = kernel.spawn("vim", args, { + streamStdin: true, + env: { + TERM: "xterm", + VIM: "/usr/local/share/vim", + VIMRUNTIME: "/usr/local/share/vim/vim92", + }, + onStdout: (chunk) => { + stdout += Buffer.from(chunk).toString("utf8"); + }, + onStderr: (chunk) => { + stderr += Buffer.from(chunk).toString("utf8"); + }, + }); + proc.closeStdin(); + const exitCode = await proc.wait(); + await new Promise((resolve) => setTimeout(resolve, 0)); + return { stdout, stderr, exitCode }; + } + + it("starts the packaged binary and reports Vim features", async () => { + await mountFixture(); + + const result = await runVim(["--version"]); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stdout).toContain("VIM - Vi IMproved"); + expect(result.stdout).toContain("-libcall"); + }); + + it("edits and writes a file in Ex mode", async () => { + await mountFixture(); + + const result = await runVim([ + "-Nu", + "NONE", + "-n", + "-es", + "-S", + "/project/edit.vim", + ]); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + if (!kernel) throw new Error("kernel not mounted"); + const edited = Buffer.from(await kernel.readFile("/project/input.txt")).toString( + "utf8", + ); + expect(edited).toBe("alpha\ndelta\ngamma\n"); + }); +}); diff --git a/software/vim/tsconfig.json b/software/vim/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/vim/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/wget/agentos-package.json b/software/wget/agentos-package.json new file mode 100644 index 0000000000..e8b8178957 --- /dev/null +++ b/software/wget/agentos-package.json @@ -0,0 +1,12 @@ +{ + "commands": [ + "wget" + ], + "registry": { + "title": "wget", + "description": "GNU Wget file downloader.", + "priority": 55, + "image": "/images/registry/wget.svg", + "category": "networking" + } +} diff --git a/software/wget/native/c/overlay/src/wasi_ssl.c b/software/wget/native/c/overlay/src/wasi_ssl.c new file mode 100644 index 0000000000..fe74f12ab2 --- /dev/null +++ b/software/wget/native/c/overlay/src/wasi_ssl.c @@ -0,0 +1,1063 @@ +/* In-guest TLS backend for GNU Wget on wasm32-wasip1, built on mbedTLS. + + GNU Wget has no upstream mbedTLS backend; its TLS abstraction is the four + functions declared in src/ssl.h (ssl_init, ssl_cleanup, ssl_connect_wget, + ssl_check_certificate). This file implements those four against mbedTLS, + performing a real TLS handshake and X.509 chain + hostname verification + entirely inside the guest -- the sidecar is a dumb ciphertext pipe. + + The already-connected TCP file descriptor handed to ssl_connect_wget is a + real socket carried by the patched wasi-libc sysroot (host_net imports), so + the mbedTLS BIO callbacks simply read()/write() that fd. On success the fd is + registered with Wget's transport layer (fd_register_transport) so that all + subsequent fd_read/fd_write/fd_peek calls flow through the TLS session. + + Trust configuration mirrors Linux Wget: certificates are verified against + /etc/ssl/certs/ca-certificates.crt by default, --ca-certificate + (opt.ca_cert) and --ca-directory (opt.ca_directory) override the trust + anchors, --crl-file (opt.crl_file) adds a revocation list, and + --no-check-certificate (opt.check_cert != CHECK_CERT_ON) downgrades a + verification failure to a warning. A failed handshake or a rejected + certificate makes the relevant function return false, so http.c reports + CONSSLERR / VERIFCERTERR exactly as with the GnuTLS/OpenSSL backends. */ + +#include "wget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "connect.h" +#include "log.h" +#include "ssl.h" +#include "url.h" +#include "utils.h" + +/* Debian-shaped default trust store, seeded into the VM by the native + bootstrap. Matches curl's compile-time CA bundle default and OpenSSL's + OPENSSLDIR resolution on Debian. */ +#ifndef WASI_TLS_DEFAULT_CA_BUNDLE +# define WASI_TLS_DEFAULT_CA_BUNDLE "/etc/ssl/certs/ca-certificates.crt" +#endif + +/* The TLS transport singleton, defined after the I/O callbacks below. */ +static struct transport_implementation wasi_tls_transport; + +struct wasi_ssl_context +{ + mbedtls_ssl_context ssl; + mbedtls_ssl_config conf; + mbedtls_ssl_session session; + bool have_session; + mbedtls_x509_crt cacert; + mbedtls_x509_crl crl; + bool have_crl; + mbedtls_x509_crt client_cert; + mbedtls_pk_context client_key; + int *ciphersuites; + mbedtls_ctr_drbg_context ctr_drbg; + mbedtls_entropy_context entropy; + int fd; + int last_err; /* last mbedTLS error, for errstr */ + unsigned char *peekbuf; /* buffered-but-unconsumed plaintext */ + int peeklen; + int peekcap; +}; + +/* mbedTLS BIO send/recv over the raw, already-connected socket fd. TLS sockets + are made nonblocking before the handshake so every WANT_READ/WANT_WRITE can + be driven by readiness with Wget's Linux timeout semantics. */ + +static int +wasi_bio_send (void *arg, const unsigned char *buf, size_t len) +{ + struct wasi_ssl_context *ctx = arg; + ssize_t n; + + do + n = write (ctx->fd, buf, len); + while (n < 0 && errno == EINTR); + + if (n >= 0) + return (int) n; + if (errno == EAGAIN || errno == EWOULDBLOCK) + return MBEDTLS_ERR_SSL_WANT_WRITE; + return MBEDTLS_ERR_NET_SEND_FAILED; +} + +static int +wasi_bio_recv (void *arg, unsigned char *buf, size_t len) +{ + struct wasi_ssl_context *ctx = arg; + ssize_t n; + + do + n = read (ctx->fd, buf, len); + while (n < 0 && errno == EINTR); + + if (n >= 0) + return (int) n; + if (errno == EAGAIN || errno == EWOULDBLOCK) + return MBEDTLS_ERR_SSL_WANT_READ; + return MBEDTLS_ERR_NET_RECV_FAILED; +} + +static double +wasi_monotonic_seconds (void) +{ + struct timespec now; + + if (clock_gettime (CLOCK_MONOTONIC, &now) < 0) + return -1; + return (double) now.tv_sec + (double) now.tv_nsec / 1000000000.0; +} + +/* Wait for the readiness mbedTLS requested. TIMEOUT == 0 means no timeout, + matching Wget's command-line timeout semantics (select_fd itself uses zero + as an immediate poll). START makes repeated WANT_* retries share one budget. */ +static int +wasi_wait_fd (int fd, double timeout, double start, int wait_for) +{ + if (timeout == 0) + { + struct pollfd pfd; + int ret; + + pfd.fd = fd; + pfd.events = 0; + pfd.revents = 0; + if (wait_for & WAIT_FOR_READ) + pfd.events |= POLLIN; + if (wait_for & WAIT_FOR_WRITE) + pfd.events |= POLLOUT; + + do + ret = poll (&pfd, 1, -1); + while (ret < 0 && errno == EINTR); + return ret; + } + else + { + double now = wasi_monotonic_seconds (); + double remaining; + int ret; + + if (now < 0) + return -1; + remaining = timeout - (now - start); + if (remaining <= 0) + { + errno = ETIMEDOUT; + return -1; + } + + ret = select_fd (fd, remaining, wait_for); + if (ret == 0) + { + errno = ETIMEDOUT; + return -1; + } + return ret; + } +} + +/* Global init. mbedTLS keeps no process-wide state we must set up, so this is + a no-op that simply reports readiness, like the GnuTLS backend. */ +bool +ssl_init (void) +{ + return true; +} + +void +ssl_cleanup (void) +{ +} + +/* Map --secure-protocol onto the mbedTLS min/max TLS version knobs. mbedTLS + 3.6 only speaks TLS 1.2 and 1.3, so the legacy SSLv3/TLS1.0/1.1 selectors + pin the floor at TLS 1.2 (the lowest still supported), matching how a modern + Wget build behaves. */ + +static void +wasi_cipher_canonical_name (const char *input, char *output, size_t output_size) +{ + const char *p = input; + size_t written = 0; + + if (0 == strncasecmp (p, "TLS", 3)) + { + p += 3; + /* mbedTLS spells TLS 1.3 suites TLS1-3-..., while OpenSSL uses the + standard TLS_AES_... form. Ignore both prefixes for comparison. */ + if (p[0] == '1' && (p[1] == '-' || p[1] == '_') && p[2] == '3') + p += 3; + } + + while (*p && written + 1 < output_size) + { + if (0 == strncasecmp (p, "WITH", 4)) + { + p += 4; + continue; + } + if (isalnum ((unsigned char) *p)) + output[written++] = toupper ((unsigned char) *p); + ++p; + } + output[written] = '\0'; +} + +static bool +wasi_cipher_name_matches (const char *name, const char *selector) +{ + char name_key[160]; + char selector_key[160]; + char rsa_selector_key[164]; + + wasi_cipher_canonical_name (name, name_key, sizeof name_key); + wasi_cipher_canonical_name (selector, selector_key, sizeof selector_key); + if (0 == strcmp (name_key, selector_key)) + return true; + + /* OpenSSL omits the RSA key-exchange prefix in names such as + AES128-GCM-SHA256; mbedTLS uses TLS-RSA-WITH-AES-128-GCM-SHA256. */ + if (strlen (selector_key) + 4 < sizeof rsa_selector_key) + { + strcpy (rsa_selector_key, "RSA"); + strcat (rsa_selector_key, selector_key); + if (0 == strcmp (name_key, rsa_selector_key)) + return true; + } + return false; +} + +static bool +wasi_cipher_is_tls13 (int id) +{ + const char *name = mbedtls_ssl_get_ciphersuite_name (id); + return name && 0 == strncmp (name, "TLS1-3-", 7); +} + +static bool +wasi_cipher_matches_class (int id, const char *selector) +{ + const char *name = mbedtls_ssl_get_ciphersuite_name (id); + const mbedtls_ssl_ciphersuite_t *info = + mbedtls_ssl_ciphersuite_from_id (id); + + if (!name || !info) + return false; + if (0 == strcasecmp (selector, "ALL") + || 0 == strcasecmp (selector, "DEFAULT")) + return true; + if (0 == strcasecmp (selector, "HIGH")) + return mbedtls_ssl_ciphersuite_get_cipher_key_bitlen (info) >= 128 + && !strstr (name, "-NULL-"); + if (0 == strcasecmp (selector, "aNULL")) + return strstr (name, "-ANON-") != NULL; + if (0 == strcasecmp (selector, "eNULL") + || 0 == strcasecmp (selector, "NULL")) + return strstr (name, "-NULL-") != NULL; + if (0 == strcasecmp (selector, "kRSA")) + return 0 == strncmp (name, "TLS-RSA-WITH-", 13); + if (0 == strcasecmp (selector, "aRSA") + || 0 == strcasecmp (selector, "RSA")) + return strstr (name, "-RSA-") != NULL; + if (0 == strcasecmp (selector, "PSK")) + return strstr (name, "-PSK-") != NULL; + if (0 == strcasecmp (selector, "SRP")) + return strstr (name, "-SRP-") != NULL; + if (0 == strcasecmp (selector, "RC4")) + return strstr (name, "-RC4-") != NULL; + if (0 == strcasecmp (selector, "MD5")) + return strstr (name, "-MD5") != NULL; + if (0 == strcasecmp (selector, "3DES")) + return strstr (name, "-3DES-") != NULL; + if (0 == strcasecmp (selector, "DES")) + return strstr (name, "-DES-") != NULL + || strstr (name, "-3DES-") != NULL; + if (0 == strcasecmp (selector, "AES")) + return strstr (name, "-AES-") != NULL; + if (0 == strcasecmp (selector, "AESGCM")) + return strstr (name, "-AES-") != NULL + && strstr (name, "-GCM-") != NULL; + if (0 == strcasecmp (selector, "CHACHA20")) + return strstr (name, "-CHACHA20-") != NULL; + return wasi_cipher_name_matches (name, selector); +} + +static bool +wasi_configure_cipher_policy (struct wasi_ssl_context *ctx, + const char *policy) +{ + const int *available = mbedtls_ssl_list_ciphersuites (); + size_t available_count = 0; + size_t selected_count = 0; + bool *disabled; + char *copy; + char *saveptr = NULL; + char *token; + + while (available[available_count] != 0) + ++available_count; + ctx->ciphersuites = xnmalloc (available_count + 1, + sizeof *ctx->ciphersuites); + disabled = xnmalloc (available_count, sizeof *disabled); + memset (disabled, 0, available_count * sizeof *disabled); + copy = xstrdup (policy); + + for (token = strtok_r (copy, ":, ", &saveptr); + token; + token = strtok_r (NULL, ":, ", &saveptr)) + { + char operation = 0; + bool matched = false; + + if (*token == '!' || *token == '-' || *token == '+') + operation = *token++; + if (*token == '\0' || *token == '@') + goto unsupported; + + if (operation == '+') + { + /* OpenSSL's +selector moves already-enabled suites to the end. */ + size_t keep = 0; + size_t moved = 0; + int *move = xnmalloc (selected_count, sizeof *move); + for (size_t i = 0; i < selected_count; ++i) + if (wasi_cipher_matches_class (ctx->ciphersuites[i], token)) + { + move[moved++] = ctx->ciphersuites[i]; + matched = true; + } + else + ctx->ciphersuites[keep++] = ctx->ciphersuites[i]; + memcpy (ctx->ciphersuites + keep, move, moved * sizeof *move); + selected_count = keep + moved; + xfree (move); + continue; + } + + for (size_t i = 0; i < available_count; ++i) + { + bool already_selected = false; + /* Native OpenSSL Wget applies --ciphers with + SSL_CTX_set_cipher_list(), which intentionally controls only + TLS 1.2-and-older suites. TLS 1.3 suites use a separate OpenSSL + API and remain at their defaults. Preserve that split instead of + allowing a TLS 1.2 policy to disable TLS 1.3 accidentally. */ + if (wasi_cipher_is_tls13 (available[i])) + continue; + if (!wasi_cipher_matches_class (available[i], token)) + continue; + matched = true; + + if (operation == '!' || operation == '-') + { + size_t out = 0; + if (operation == '!') + disabled[i] = true; + for (size_t j = 0; j < selected_count; ++j) + if (ctx->ciphersuites[j] != available[i]) + ctx->ciphersuites[out++] = ctx->ciphersuites[j]; + selected_count = out; + continue; + } + + if (disabled[i]) + continue; + for (size_t j = 0; j < selected_count; ++j) + if (ctx->ciphersuites[j] == available[i]) + already_selected = true; + if (!already_selected) + ctx->ciphersuites[selected_count++] = available[i]; + } + + /* OpenSSL accepts exclusions for algorithms absent from the compiled + backend (for example !RC4 on a modern build). Unknown positive + selectors are errors because otherwise the policy would broaden. */ + if (!matched && operation != '!' && operation != '-') + goto unsupported; + } + + xfree (copy); + xfree (disabled); + if (selected_count == 0) + { + logprintf (LOG_NOTQUIET, + _("Cipher policy %s selects no mbedTLS ciphersuites.\n"), + quote (policy)); + return false; + } + + /* mbedTLS has one ciphersuite configuration API for every TLS version. + Append its default TLS 1.3 suites after validating the OpenSSL-style + pre-TLS-1.3 policy above, matching SSL_CTX_set_cipher_list semantics. */ + for (size_t i = 0; i < available_count; ++i) + if (wasi_cipher_is_tls13 (available[i])) + ctx->ciphersuites[selected_count++] = available[i]; + ctx->ciphersuites[selected_count] = 0; + mbedtls_ssl_conf_ciphersuites (&ctx->conf, ctx->ciphersuites); + return true; + + unsupported: + logprintf (LOG_NOTQUIET, + _("Unsupported mbedTLS cipher policy token '%s' in %s.\n"), + token, quote (policy)); + xfree (copy); + xfree (disabled); + return false; +} + +static bool +wasi_apply_secure_protocol (struct wasi_ssl_context *ctx) +{ + mbedtls_ssl_config *conf = &ctx->conf; + bool cipher_policy_configured = opt.tls_ciphers_string != NULL; + + if (cipher_policy_configured + && !wasi_configure_cipher_policy (ctx, opt.tls_ciphers_string)) + return false; + + switch (opt.secure_protocol) + { + case secure_protocol_tlsv1_3: + mbedtls_ssl_conf_min_tls_version (conf, MBEDTLS_SSL_VERSION_TLS1_3); + mbedtls_ssl_conf_max_tls_version (conf, MBEDTLS_SSL_VERSION_TLS1_3); + return true; + case secure_protocol_tlsv1_2: + /* GNU Wget treats TLSv1_2 as a minimum, not an exact version: a + TLS-1.3-only endpoint must remain reachable. */ + mbedtls_ssl_conf_min_tls_version (conf, MBEDTLS_SSL_VERSION_TLS1_2); + return true; + case secure_protocol_tlsv1: + case secure_protocol_tlsv1_1: + /* Not supported by mbedTLS 3.6; fall back to the lowest available. */ + mbedtls_ssl_conf_min_tls_version (conf, MBEDTLS_SSL_VERSION_TLS1_2); + return true; + case secure_protocol_sslv2: + case secure_protocol_sslv3: + /* A native modern Wget build rejects these unavailable protocols. Do + not silently upgrade an explicitly requested SSLv2/SSLv3 connection + to TLS, which could make a command succeed with different policy. */ + logprintf (LOG_NOTQUIET, + _("mbedTLS does not support requested protocol %s.\n"), + opt.secure_protocol_name); + return false; + case secure_protocol_auto: + default: + /* Library defaults: TLS 1.2 .. 1.3. */ + return true; + case secure_protocol_pfs: + { + const int *available = mbedtls_ssl_list_ciphersuites (); + size_t count = 0; + size_t selected = 0; + + /* Like native OpenSSL Wget, an explicit --ciphers policy overrides + the PFS preset rather than being intersected with it. */ + if (cipher_policy_configured) + return true; + + while (available[count] != 0) + ++count; + ctx->ciphersuites = xnmalloc (count + 1, sizeof *ctx->ciphersuites); + + for (size_t i = 0; i < count; ++i) + { + const char *name = + mbedtls_ssl_get_ciphersuite_name (available[i]); + /* TLS 1.3 always uses (EC)DHE for certificate-authenticated + handshakes. For TLS 1.2, retain only explicitly ephemeral + DHE/ECDHE suites, excluding static RSA/ECDH and plain PSK. */ + if (name + && (0 == strncmp (name, "TLS1-3-", 7) + || strstr (name, "-ECDHE-") + || strstr (name, "-DHE-"))) + ctx->ciphersuites[selected++] = available[i]; + } + ctx->ciphersuites[selected] = 0; + if (selected == 0) + { + logprintf (LOG_NOTQUIET, + _("mbedTLS has no forward-secret ciphersuites enabled.\n")); + return false; + } + mbedtls_ssl_conf_ciphersuites (conf, ctx->ciphersuites); + return true; + } + } +} + +/* Configure the client certificate/key pair accepted by native Wget's + --certificate and --private-key options. If only one path is provided, + native OpenSSL Wget treats it as a combined PEM containing both objects. */ +static int +wasi_load_client_credentials (struct wasi_ssl_context *ctx) +{ + const char *cert_path; + const char *key_path; + int ret; + + if (!opt.cert_file && !opt.private_key) + return 0; + + cert_path = opt.cert_file ? opt.cert_file : opt.private_key; + key_path = opt.private_key ? opt.private_key : opt.cert_file; + + ret = mbedtls_x509_crt_parse_file (&ctx->client_cert, cert_path); + if (ret < 0) + return ret; + + ret = mbedtls_pk_parse_keyfile (&ctx->client_key, key_path, NULL, + mbedtls_ctr_drbg_random, &ctx->ctr_drbg); + if (ret == MBEDTLS_ERR_PK_PASSWORD_REQUIRED) + { + char *password = getpass (_("Enter PEM pass phrase: ")); + if (!password) + return ret; + mbedtls_pk_free (&ctx->client_key); + mbedtls_pk_init (&ctx->client_key); + ret = mbedtls_pk_parse_keyfile (&ctx->client_key, key_path, password, + mbedtls_ctr_drbg_random, &ctx->ctr_drbg); + } + if (ret < 0) + return ret; + + ret = mbedtls_ssl_conf_own_cert (&ctx->conf, &ctx->client_cert, + &ctx->client_key); + return ret; +} + +/* Load trust anchors in the same order as Linux Wget's GnuTLS backend: use the + system trust store unless --ca-directory replaces it, then add the bundle + supplied through --ca-certificate. Returns the number of sources parsed + (>= 0) or a negative mbedTLS error. */ +static int +wasi_load_trust (struct wasi_ssl_context *ctx) +{ + int loaded = 0; + int ret; + + if (opt.ca_directory && opt.ca_directory[0] != '\0') + { + ret = mbedtls_x509_crt_parse_path (&ctx->cacert, opt.ca_directory); + /* parse_path returns the number of files that failed to parse as a + positive value; only a negative value is a hard error. */ + if (ret < 0) + return ret; + loaded += 1; + } + else + { + ret = mbedtls_x509_crt_parse_file (&ctx->cacert, + WASI_TLS_DEFAULT_CA_BUNDLE); + if (ret < 0) + return ret; + loaded += 1; + } + + if (opt.ca_cert) + { + ret = mbedtls_x509_crt_parse_file (&ctx->cacert, opt.ca_cert); + if (ret < 0) + return ret; + loaded += 1; + } + + return loaded; +} + +/* Perform the TLS handshake on FD and, on success, register the TLS transport + so Wget's fd_* helpers use it. CONTINUE_SESSION requests the native Wget + FTPS behavior: resume the control-channel session on a protected data + connection. Returns true on success. */ +bool +ssl_connect_wget (int fd, const char *hostname, int *continue_session) +{ + struct wasi_ssl_context *ctx; + int original_flags = -1; + bool nonblocking = false; + double handshake_start; + int ret; + + DEBUGP (("Initiating SSL handshake (mbedTLS).\n")); + + if (!hostname || hostname[0] == '\0') + { + errno = EINVAL; + logprintf (LOG_NOTQUIET, + _("SSL handshake requires a non-empty server hostname.\n")); + return false; + } + + ctx = xnew0 (struct wasi_ssl_context); + ctx->fd = fd; + + mbedtls_ssl_init (&ctx->ssl); + mbedtls_ssl_config_init (&ctx->conf); + mbedtls_ssl_session_init (&ctx->session); + mbedtls_x509_crt_init (&ctx->cacert); + mbedtls_x509_crl_init (&ctx->crl); + mbedtls_x509_crt_init (&ctx->client_cert); + mbedtls_pk_init (&ctx->client_key); + mbedtls_ctr_drbg_init (&ctx->ctr_drbg); + mbedtls_entropy_init (&ctx->entropy); + + ret = mbedtls_ctr_drbg_seed (&ctx->ctr_drbg, mbedtls_entropy_func, + &ctx->entropy, + (const unsigned char *) "agentos-wget-tls", 16); + if (ret != 0) + goto error; + + /* Load trust anchors. A failure to read the bundle is only fatal when the + user asked us to verify; with --no-check-certificate we proceed with an + empty trust store (verification is skipped in ssl_check_certificate). */ + ret = wasi_load_trust (ctx); + if (ret < 0 && opt.check_cert == CHECK_CERT_ON) + { + char errbuf[128]; + mbedtls_strerror (ret, errbuf, sizeof errbuf); + logprintf (LOG_NOTQUIET, + _("Could not load CA certificates: %s\n"), errbuf); + goto error; + } + + if (opt.crl_file) + { + ret = mbedtls_x509_crl_parse_file (&ctx->crl, opt.crl_file); + if (ret != 0 && opt.check_cert == CHECK_CERT_ON) + { + char errbuf[128]; + mbedtls_strerror (ret, errbuf, sizeof errbuf); + logprintf (LOG_NOTQUIET, + _("Could not load CRL from %s: %s\n"), + opt.crl_file, errbuf); + goto error; + } + if (ret == 0) + ctx->have_crl = true; + } + + ret = mbedtls_ssl_config_defaults (&ctx->conf, MBEDTLS_SSL_IS_CLIENT, + MBEDTLS_SSL_TRANSPORT_STREAM, + MBEDTLS_SSL_PRESET_DEFAULT); + if (ret != 0) + goto error; + + if (!wasi_apply_secure_protocol (ctx)) + goto error; + + ret = wasi_load_client_credentials (ctx); + if (ret != 0) + { + char errbuf[128]; + mbedtls_strerror (ret, errbuf, sizeof errbuf); + logprintf (LOG_NOTQUIET, + _("Could not load TLS client certificate/key: %s\n"), + errbuf); + goto error; + } + + /* Verify optionally: the handshake always completes and records the chain + + hostname result, which ssl_check_certificate reads and turns into a + pass/fail decision honoring opt.check_cert -- the same split the OpenSSL + backend uses (SSL_VERIFY_NONE at handshake, manual check afterwards). */ + mbedtls_ssl_conf_authmode (&ctx->conf, MBEDTLS_SSL_VERIFY_OPTIONAL); + mbedtls_ssl_conf_ca_chain (&ctx->conf, &ctx->cacert, + ctx->have_crl ? &ctx->crl : NULL); + mbedtls_ssl_conf_rng (&ctx->conf, mbedtls_ctr_drbg_random, &ctx->ctr_drbg); + + ret = mbedtls_ssl_setup (&ctx->ssl, &ctx->conf); + if (ret != 0) + goto error; + + if (continue_session) + { + struct wasi_ssl_context *previous = + fd_transport_context (*continue_session); + if (previous && !previous->have_session) + { + /* Export lazily after Wget has finished using the control channel + for commands. Exporting a TLS 1.3 session immediately after the + handshake can precede the post-handshake NewSessionTicket and + disturb application-record processing in mbedTLS 3.6. */ + mbedtls_ssl_session_free (&previous->session); + mbedtls_ssl_session_init (&previous->session); + if (mbedtls_ssl_get_session (&previous->ssl, + &previous->session) == 0) + previous->have_session = true; + } + if (!previous || !previous->have_session + || mbedtls_ssl_set_session (&ctx->ssl, &previous->session) != 0) + { + logprintf (LOG_NOTQUIET, + _("Could not resume TLS session for socket %d.\n"), fd); + goto error; + } + } + + /* SNI + the name checked during verification. Covers DNS and IP-address + SANs (mbedTLS matches an IP literal against iPAddress SAN entries). */ + ret = mbedtls_ssl_set_hostname (&ctx->ssl, hostname); + if (ret != 0) + goto error; + + mbedtls_ssl_set_bio (&ctx->ssl, ctx, wasi_bio_send, wasi_bio_recv, NULL); + + original_flags = fcntl (fd, F_GETFL); + if (original_flags < 0 || fcntl (fd, F_SETFL, original_flags | O_NONBLOCK) < 0) + goto error; + nonblocking = true; + + handshake_start = wasi_monotonic_seconds (); + if (handshake_start < 0) + goto error; + + do + { + ret = mbedtls_ssl_handshake (&ctx->ssl); + if (ret == MBEDTLS_ERR_SSL_WANT_READ) + { + if (wasi_wait_fd (fd, opt.read_timeout, handshake_start, + WAIT_FOR_READ) <= 0) + { + ret = MBEDTLS_ERR_SSL_TIMEOUT; + break; + } + } + else if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) + { + if (wasi_wait_fd (fd, opt.read_timeout, handshake_start, + WAIT_FOR_WRITE) <= 0) + { + ret = MBEDTLS_ERR_SSL_TIMEOUT; + break; + } + } + } + while (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE); + + if (ret != 0) + { + char errbuf[128]; + mbedtls_strerror (ret, errbuf, sizeof errbuf); + DEBUGP (("SSL handshake failed: %s\n", errbuf)); + logprintf (LOG_NOTQUIET, _("SSL handshake failed: %s\n"), errbuf); + goto error; + } + + /* Register FD with Wget's transport layer so fd_read/fd_write/fd_peek use + our TLS callbacks from here on. */ + fd_register_transport (fd, &wasi_tls_transport, ctx); + DEBUGP (("Handshake successful; TLS registered on socket %d\n", fd)); + + return true; + + error: + if (nonblocking) + { + int saved_errno = errno; + if (fcntl (fd, F_SETFL, original_flags) < 0) + logprintf (LOG_NOTQUIET, + _("Failed to restore socket flags after TLS handshake failure: %s\n"), + strerror (errno)); + errno = saved_errno; + } + mbedtls_ssl_free (&ctx->ssl); + mbedtls_ssl_config_free (&ctx->conf); + mbedtls_ssl_session_free (&ctx->session); + mbedtls_x509_crt_free (&ctx->cacert); + mbedtls_x509_crl_free (&ctx->crl); + mbedtls_x509_crt_free (&ctx->client_cert); + mbedtls_pk_free (&ctx->client_key); + mbedtls_ctr_drbg_free (&ctx->ctr_drbg); + mbedtls_entropy_free (&ctx->entropy); + xfree (ctx->ciphersuites); + xfree (ctx); + return false; +} + +/* --- Wget transport implementation over the TLS session --- */ + +static int +wasi_tls_read (int fd, char *buf, int bufsize, void *arg, double timeout) +{ + struct wasi_ssl_context *ctx = arg; + double start; + int ret; + + /* Serve any peeked-but-unconsumed plaintext first. */ + if (ctx->peeklen > 0) + { + int n = ctx->peeklen < bufsize ? ctx->peeklen : bufsize; + memcpy (buf, ctx->peekbuf, n); + if (n < ctx->peeklen) + memmove (ctx->peekbuf, ctx->peekbuf + n, ctx->peeklen - n); + ctx->peeklen -= n; + return n; + } + + if (timeout == -1) + timeout = opt.read_timeout; + start = wasi_monotonic_seconds (); + if (start < 0) + return -1; + + do + { + ret = mbedtls_ssl_read (&ctx->ssl, (unsigned char *) buf, bufsize); + if (ret == MBEDTLS_ERR_SSL_WANT_READ + && wasi_wait_fd (fd, timeout, start, WAIT_FOR_READ) <= 0) + return -1; + if (ret == MBEDTLS_ERR_SSL_WANT_WRITE + && wasi_wait_fd (fd, timeout, start, WAIT_FOR_WRITE) <= 0) + return -1; + } + while (ret == MBEDTLS_ERR_SSL_WANT_READ || ret == MBEDTLS_ERR_SSL_WANT_WRITE); + + if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) + return 0; + if (ret < 0) + { + ctx->last_err = ret; + return -1; + } + return ret; +} + +static int +wasi_tls_write (int fd, char *buf, int bufsize, void *arg) +{ + struct wasi_ssl_context *ctx = arg; + int written = 0; + + while (written < bufsize) + { + int ret = mbedtls_ssl_write (&ctx->ssl, + (const unsigned char *) buf + written, + bufsize - written); + if (ret == MBEDTLS_ERR_SSL_WANT_READ) + { + /* Linux Wget's OpenSSL/GnuTLS write callbacks block without applying + the read timeout. Keep the socket nonblocking internally, but wait + indefinitely for the readiness mbedTLS requested. */ + if (wasi_wait_fd (fd, 0, 0, WAIT_FOR_READ) <= 0) + { + ctx->last_err = MBEDTLS_ERR_SSL_TIMEOUT; + return -1; + } + continue; + } + if (ret == MBEDTLS_ERR_SSL_WANT_WRITE) + { + if (wasi_wait_fd (fd, 0, 0, WAIT_FOR_WRITE) <= 0) + { + ctx->last_err = MBEDTLS_ERR_SSL_TIMEOUT; + return -1; + } + continue; + } + if (ret < 0) + { + ctx->last_err = ret; + return -1; + } + if (ret == 0) + { + errno = EIO; + return -1; + } + written += ret; + } + return written; +} + +static int +wasi_tls_poll (int fd, double timeout, int wait_for, void *arg) +{ + struct wasi_ssl_context *ctx = arg; + + if ((wait_for & WAIT_FOR_READ) + && (ctx->peeklen > 0 || mbedtls_ssl_get_bytes_avail (&ctx->ssl) > 0)) + return 1; + if (timeout == -1) + timeout = opt.read_timeout; + return select_fd (fd, timeout, wait_for); +} + +static int +wasi_tls_peek (int fd, char *buf, int bufsize, void *arg, double timeout) +{ + struct wasi_ssl_context *ctx = arg; + double start; + int n; + + /* Mirror recv(MSG_PEEK)/SSL_peek semantics: preview data without consuming + it, returning as soon as *some* data is available -- one TLS record's + worth. Never try to fill BUFSIZE (that would block on a keep-alive + connection once the whole response fits in one record). Wget's + fd_read_hunk consumes each preview and re-peeks for the next record, so + returning one chunk at a time is sufficient and cannot deadlock. + + Data that mbedtls_ssl_read pulls off the wire here is retained in peekbuf + so wasi_tls_read drains it first -- i.e. the "peek" does not lose it. */ + if (ctx->peeklen == 0) + { + int ret; + + if (ctx->peekcap < bufsize) + { + ctx->peekbuf = xrealloc (ctx->peekbuf, bufsize); + ctx->peekcap = bufsize; + } + + if (timeout == -1) + timeout = opt.read_timeout; + start = wasi_monotonic_seconds (); + if (start < 0) + return -1; + + do + { + ret = mbedtls_ssl_read (&ctx->ssl, ctx->peekbuf, bufsize); + if (ret == MBEDTLS_ERR_SSL_WANT_READ + && wasi_wait_fd (fd, timeout, start, WAIT_FOR_READ) <= 0) + return -1; + if (ret == MBEDTLS_ERR_SSL_WANT_WRITE + && wasi_wait_fd (fd, timeout, start, WAIT_FOR_WRITE) <= 0) + return -1; + } + while (ret == MBEDTLS_ERR_SSL_WANT_READ + || ret == MBEDTLS_ERR_SSL_WANT_WRITE); + + if (ret == MBEDTLS_ERR_SSL_PEER_CLOSE_NOTIFY) + return 0; + if (ret < 0) + { + ctx->last_err = ret; + return -1; + } + ctx->peeklen = ret; + } + + n = ctx->peeklen < bufsize ? ctx->peeklen : bufsize; + if (n > 0) + memcpy (buf, ctx->peekbuf, n); + return n; +} + +static const char * +wasi_tls_errstr (int fd _GL_UNUSED, void *arg) +{ + struct wasi_ssl_context *ctx = arg; + static char errbuf[160]; + + if (ctx->last_err == 0) + return NULL; + mbedtls_strerror (ctx->last_err, errbuf, sizeof errbuf); + return errbuf; +} + +static void +wasi_tls_close (int fd, void *arg) +{ + struct wasi_ssl_context *ctx = arg; + + mbedtls_ssl_close_notify (&ctx->ssl); + mbedtls_ssl_free (&ctx->ssl); + mbedtls_ssl_config_free (&ctx->conf); + mbedtls_ssl_session_free (&ctx->session); + mbedtls_x509_crt_free (&ctx->cacert); + mbedtls_x509_crl_free (&ctx->crl); + mbedtls_x509_crt_free (&ctx->client_cert); + mbedtls_pk_free (&ctx->client_key); + mbedtls_ctr_drbg_free (&ctx->ctr_drbg); + mbedtls_entropy_free (&ctx->entropy); + xfree (ctx->ciphersuites); + xfree (ctx->peekbuf); + xfree (ctx); + + close (fd); + DEBUGP (("Closed %d/SSL (mbedTLS)\n", fd)); +} + +static struct transport_implementation wasi_tls_transport = { + wasi_tls_read, wasi_tls_write, wasi_tls_poll, + wasi_tls_peek, wasi_tls_errstr, wasi_tls_close +}; + +/* Verify the peer certificate against the configured trust anchors and check + that it matches HOST. Reads the verify result recorded during the handshake. + Returns false only when verification failed AND the user requested checking + (opt.check_cert == CHECK_CERT_ON); otherwise it warns and returns true, + matching Wget's --no-check-certificate semantics. */ +bool +ssl_check_certificate (int fd, const char *host) +{ + struct wasi_ssl_context *ctx = fd_transport_context (fd); + uint32_t flags; + bool success; + const char *severity = opt.check_cert ? _("ERROR") : _("WARNING"); + + if (!host || host[0] == '\0') + { + logprintf (LOG_NOTQUIET, + _("ERROR: cannot verify a certificate without a server hostname.\n")); + return false; + } + + if (!ctx) + return opt.check_cert != CHECK_CERT_ON; + + flags = mbedtls_ssl_get_verify_result (&ctx->ssl); + success = (flags == 0); + + if (!success) + { + char vbuf[512]; + int n = mbedtls_x509_crt_verify_info (vbuf, sizeof vbuf, " ", flags); + if (n < 0) + { + vbuf[0] = '\0'; + n = 0; + } + + logprintf (LOG_NOTQUIET, + _("%s: cannot verify %s's certificate:\n"), + severity, quote (host)); + if (n > 0) + logprintf (LOG_NOTQUIET, "%s", vbuf); + + if (opt.check_cert == CHECK_CERT_ON) + logprintf (LOG_NOTQUIET, + _("To connect to %s insecurely, use `--no-check-certificate'.\n"), + quote (host)); + } + else + DEBUGP (("X509 certificate successfully verified and matches host %s\n", + quote (host))); + + return opt.check_cert == CHECK_CERT_ON ? success : true; +} + +/* + * vim: tabstop=2 shiftwidth=2 softtabstop=2 + */ diff --git a/software/wget/package.json b/software/wget/package.json new file mode 100644 index 0000000000..8147e9e893 --- /dev/null +++ b/software/wget/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/wget", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "GNU Wget HTTP client for AgentOS VMs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@agentos/test-harness": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/wget/src/index.ts b/software/wget/src/index.ts similarity index 100% rename from registry/software/wget/src/index.ts rename to software/wget/src/index.ts diff --git a/software/wget/test/wget.test.ts b/software/wget/test/wget.test.ts new file mode 100644 index 0000000000..1d3acbaf64 --- /dev/null +++ b/software/wget/test/wget.test.ts @@ -0,0 +1,813 @@ +import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; +import { execSync } from "node:child_process"; +import { randomBytes } from "node:crypto"; +import { + existsSync, + mkdtempSync, + readFileSync, + rmSync, + unlinkSync, + writeFileSync, +} from "node:fs"; +import { + createServer, + type IncomingMessage, + type Server, + type ServerResponse, +} from "node:http"; +import { + createServer as createHttpsServer, + type Server as HttpsServer, +} from "node:https"; +import { + createServer as createTlsServer, + type Server as TlsServer, +} from "node:tls"; +import { + createServer as createNetServer, + type Server as NetServer, +} from "node:net"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { gzipSync } from "node:zlib"; +import { createWasmVmRuntime } from "@agentos/test-harness"; +import { + allowAll, + C_BUILD_DIR, + COMMANDS_DIR, + createInMemoryFileSystem, + createKernel, + describeIf, + itIf, +} from "@agentos/test-harness"; +import type { Kernel } from "@agentos/test-harness"; + +const WGET_COMMAND_DIRS = [C_BUILD_DIR, COMMANDS_DIR].filter((dir) => + existsSync(dir), +); +const hasWgetBinary = WGET_COMMAND_DIRS.some((dir) => + existsSync(resolve(dir, "wget")), +); +const WGET_EXEC_TIMEOUT_MS = 10_000; + +let hasOpenssl = false; +try { + execSync("openssl version", { stdio: "pipe" }); + hasOpenssl = true; +} catch { + hasOpenssl = false; +} + +// A long, highly compressible payload so the gzipped body is clearly distinct +// from the plaintext (proving wget actually inflated it via zlib). +const COMPRESSION_PAYLOAD = + "agentos-wget-compression " + + "the quick brown fox jumps over the lazy dog. ".repeat(64); + +// Build a real CA and a leaf cert signed by it, with a SAN covering the +// 127.0.0.1 loopback endpoint the tests connect to. This lets wget's mbedTLS +// backend perform genuine chain + hostname verification, exactly like Linux +// wget against a private CA. +function makeCaSignedCert(caCommonName: string): { + caPem: string; + serverKey: string; + serverCert: string; +} { + const dir = mkdtempSync(join(tmpdir(), "wget-ca-")); + try { + execSync( + `openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "${dir}/ca.key" 2>/dev/null`, + ); + execSync( + `openssl req -x509 -new -key "${dir}/ca.key" -days 3650 -subj "/CN=${caCommonName}" -out "${dir}/ca.crt" 2>/dev/null`, + ); + execSync( + `openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "${dir}/srv.key" 2>/dev/null`, + ); + execSync( + `openssl req -new -key "${dir}/srv.key" -subj "/CN=localhost" -out "${dir}/srv.csr" 2>/dev/null`, + ); + writeFileSync(`${dir}/ext.cnf`, "subjectAltName=DNS:localhost,IP:127.0.0.1\n"); + execSync( + `openssl x509 -req -in "${dir}/srv.csr" -CA "${dir}/ca.crt" -CAkey "${dir}/ca.key" ` + + `-CAcreateserial -days 3650 -extfile "${dir}/ext.cnf" -out "${dir}/srv.crt" 2>/dev/null`, + ); + return { + caPem: readFileSync(`${dir}/ca.crt`, "utf8"), + serverKey: readFileSync(`${dir}/srv.key`, "utf8"), + serverCert: readFileSync(`${dir}/srv.crt`, "utf8"), + }; + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +function makeMutualTlsCerts(caCommonName: string): { + caPem: string; + serverKey: string; + serverCert: string; + clientKey: string; + clientCert: string; +} { + const dir = mkdtempSync(join(tmpdir(), "wget-mtls-")); + try { + execSync( + `openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "${dir}/ca.key" 2>/dev/null`, + ); + execSync( + `openssl req -x509 -new -key "${dir}/ca.key" -days 3650 -subj "/CN=${caCommonName}" -out "${dir}/ca.crt" 2>/dev/null`, + ); + for (const [name, commonName, extensions] of [ + ["server", "localhost", "subjectAltName=DNS:localhost,IP:127.0.0.1\nextendedKeyUsage=serverAuth\n"], + ["client", "wget-client", "extendedKeyUsage=clientAuth\n"], + ] as const) { + execSync( + `openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out "${dir}/${name}.key" 2>/dev/null`, + ); + execSync( + `openssl req -new -key "${dir}/${name}.key" -subj "/CN=${commonName}" -out "${dir}/${name}.csr" 2>/dev/null`, + ); + writeFileSync(`${dir}/${name}.cnf`, extensions); + execSync( + `openssl x509 -req -in "${dir}/${name}.csr" -CA "${dir}/ca.crt" -CAkey "${dir}/ca.key" ` + + `-CAcreateserial -days 3650 -extfile "${dir}/${name}.cnf" -out "${dir}/${name}.crt" 2>/dev/null`, + ); + } + return { + caPem: readFileSync(`${dir}/ca.crt`, "utf8"), + serverKey: readFileSync(`${dir}/server.key`, "utf8"), + serverCert: readFileSync(`${dir}/server.crt`, "utf8"), + clientKey: readFileSync(`${dir}/client.key`, "utf8"), + clientCert: readFileSync(`${dir}/client.crt`, "utf8"), + }; + } finally { + rmSync(dir, { recursive: true, force: true }); + } +} + +function generateSelfSignedCert(): { key: string; cert: string } { + const keyPath = join(tmpdir(), `wget-test-key-${process.pid}-${Date.now()}.pem`); + try { + const key = execSync( + "openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 2>/dev/null", + { encoding: "utf8" }, + ); + writeFileSync(keyPath, key); + const cert = execSync( + `openssl req -new -x509 -key "${keyPath}" -days 1 -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" 2>/dev/null`, + { encoding: "utf8" }, + ); + return { key, cert }; + } finally { + try { + unlinkSync(keyPath); + } catch { + // Best effort cleanup for test temp files. + } + } +} + +describeIf(hasWgetBinary, "wget command", () => { + let kernel: Kernel; + let server: Server; + let selfSignedServer: HttpsServer; + let validHttpsServer: HttpsServer; + let caHttpsServer: HttpsServer; + let mutualTlsServer: HttpsServer; + let handshakeStallServer: NetServer; + let readStallServer: TlsServer; + let ftpsControlServer: TlsServer; + let ftpsDataServer: TlsServer; + let port: number; + let selfSignedPort: number; + let validHttpsPort: number; + let caHttpsPort: number; + let mutualTlsPort: number; + let handshakeStallPort: number; + let readStallPort: number; + let ftpsControlPort: number; + let ftpsDataPort: number; + // CA (PEM) trusted by the seeded /etc/ssl/certs/ca-certificates.crt bundle; + // it signs validHttpsServer's leaf. caOnlyPem signs caHttpsServer's leaf and + // is deliberately NOT in the bundle, so it verifies only via + // --ca-certificate. + let seededCaPem = ""; + let caOnlyPem = ""; + let clientKeyPem = ""; + let clientCertPem = ""; + let mutualCaPem = ""; + let ftpsDataSessionReused = false; + + beforeAll(async () => { + server = createServer((req: IncomingMessage, res: ServerResponse) => { + const url = req.url ?? "/"; + + if (url === "/file.txt") { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("downloaded content"); + return; + } + + if (url === "/data.json") { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "ok" })); + return; + } + + if (url === "/gzip") { + const body = gzipSync(Buffer.from(COMPRESSION_PAYLOAD)); + res.writeHead(200, { + "Content-Type": "text/plain", + "Content-Encoding": "gzip", + "Content-Length": String(body.length), + }); + res.end(body); + return; + } + + if (url === "/redirect") { + res.writeHead(302, { + Location: `http://127.0.0.1:${port}/redirected`, + }); + res.end(); + return; + } + + if (url === "/redirected") { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("arrived after redirect"); + return; + } + + res.writeHead(404, { "Content-Type": "text/plain" }); + res.end("not found"); + }); + + await new Promise((resolveListen) => + server.listen(0, "127.0.0.1", resolveListen), + ); + port = (server.address() as import("node:net").AddressInfo).port; + + // Accept TCP but never send a TLS ServerHello. Native Wget applies its + // read timeout while handshaking; the mbedTLS backend must do the same. + handshakeStallServer = createNetServer((socket) => { + setTimeout(() => socket.destroy(), 3_000); + }); + await new Promise((resolveListen) => + handshakeStallServer.listen(0, "127.0.0.1", resolveListen), + ); + handshakeStallPort = ( + handshakeStallServer.address() as import("node:net").AddressInfo + ).port; + + if (hasOpenssl) { + // Self-signed leaf: no chain to any trusted CA -> must fail verify. + const selfSigned = generateSelfSignedCert(); + selfSignedServer = createHttpsServer( + { key: selfSigned.key, cert: selfSigned.cert }, + (req, res) => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("self-signed secure content"); + }, + ); + await new Promise((resolveListen) => + selfSignedServer.listen(0, "127.0.0.1", resolveListen), + ); + selfSignedPort = ( + selfSignedServer.address() as import("node:net").AddressInfo + ).port; + + // Leaf chaining to a CA seeded into the guest's bundle -> verifies + // with no --no-check-certificate / --ca-certificate. + const trusted = makeCaSignedCert("AgentOS Wget Test Root CA"); + seededCaPem = trusted.caPem; + validHttpsServer = createHttpsServer( + { + key: trusted.serverKey, + cert: trusted.serverCert, + minVersion: "TLSv1.3", + }, + (req, res) => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("verified https content"); + }, + ); + await new Promise((resolveListen) => + validHttpsServer.listen(0, "127.0.0.1", resolveListen), + ); + validHttpsPort = ( + validHttpsServer.address() as import("node:net").AddressInfo + ).port; + + // Complete TLS and send a deliberately incomplete fixed-length body. + // A read timeout is an error, not a clean EOF/truncated success. + readStallServer = createTlsServer( + { key: trusted.serverKey, cert: trusted.serverCert }, + (socket) => { + socket.write( + "HTTP/1.1 200 OK\r\nContent-Length: 64\r\nConnection: close\r\n\r\npartial", + ); + setTimeout(() => socket.destroy(), 3_000); + }, + ); + await new Promise((resolveListen) => + readStallServer.listen(0, "127.0.0.1", resolveListen), + ); + readStallPort = ( + readStallServer.address() as import("node:net").AddressInfo + ).port; + + // Leaf whose CA is provided ONLY via --ca-certificate (not in bundle). + const caOnly = makeCaSignedCert("AgentOS Wget Cacert-Only CA"); + caOnlyPem = caOnly.caPem; + caHttpsServer = createHttpsServer( + { + key: caOnly.serverKey, + cert: caOnly.serverCert, + maxVersion: "TLSv1.2", + }, + (req, res) => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("cacert https content"); + }, + ); + await new Promise((resolveListen) => + caHttpsServer.listen(0, "127.0.0.1", resolveListen), + ); + caHttpsPort = ( + caHttpsServer.address() as import("node:net").AddressInfo + ).port; + + const mutual = makeMutualTlsCerts("AgentOS Wget Mutual TLS CA"); + mutualCaPem = mutual.caPem; + clientKeyPem = mutual.clientKey; + clientCertPem = mutual.clientCert; + mutualTlsServer = createHttpsServer( + { + key: mutual.serverKey, + cert: mutual.serverCert, + ca: mutual.caPem, + requestCert: true, + rejectUnauthorized: true, + }, + (_req, res) => { + res.writeHead(200, { "Content-Type": "text/plain" }); + res.end("mutual tls content"); + }, + ); + await new Promise((resolveListen) => + mutualTlsServer.listen(0, "127.0.0.1", resolveListen), + ); + mutualTlsPort = ( + mutualTlsServer.address() as import("node:net").AddressInfo + ).port; + + // Minimal implicit-FTPS endpoint. Control and data listeners share TLS + // ticket keys; the data listener records whether Wget resumed the + // control session, as native Wget does for protected data channels. + const ticketKeys = randomBytes(48); + let releaseData: (() => void) | undefined; + const dataMayFlow = new Promise((resolveData) => { + releaseData = resolveData; + }); + const ftpsPayload = "resumed ftps content\n"; + const ftpsTlsOptions = { + key: trusted.serverKey, + cert: trusted.serverCert, + minVersion: "TLSv1.2" as const, + maxVersion: "TLSv1.2" as const, + ticketKeys, + }; + ftpsDataServer = createTlsServer(ftpsTlsOptions, async (socket) => { + ftpsDataSessionReused = socket.isSessionReused(); + await dataMayFlow; + socket.end(ftpsPayload); + }); + await new Promise((resolveListen) => + ftpsDataServer.listen(0, "127.0.0.1", resolveListen), + ); + ftpsDataPort = ( + ftpsDataServer.address() as import("node:net").AddressInfo + ).port; + + ftpsControlServer = createTlsServer(ftpsTlsOptions, (socket) => { + let buffered = ""; + socket.write("220 AgentOS FTPS ready\r\n"); + socket.on("data", (chunk) => { + buffered += chunk.toString("utf8"); + for (;;) { + const newline = buffered.indexOf("\n"); + if (newline < 0) break; + const line = buffered.slice(0, newline).trim(); + buffered = buffered.slice(newline + 1); + const [command = "", ...args] = line.split(/\s+/); + const argument = args.join(" "); + switch (command.toUpperCase()) { + case "USER": socket.write("331 Password required\r\n"); break; + case "PASS": socket.write("230 Logged in\r\n"); break; + case "SYST": socket.write("215 UNIX Type: L8\r\n"); break; + case "PWD": socket.write('257 "/" is current directory\r\n'); break; + case "TYPE": + case "PBSZ": + case "PROT": + case "OPTS": socket.write("200 OK\r\n"); break; + case "SIZE": socket.write(`213 ${Buffer.byteLength(ftpsPayload)}\r\n`); break; + case "MDTM": socket.write("213 20260714000000\r\n"); break; + case "EPSV": socket.write(`229 Entering Extended Passive Mode (|||${ftpsDataPort}|)\r\n`); break; + case "PASV": socket.write(`227 Entering Passive Mode (127,0,0,1,${Math.floor(ftpsDataPort / 256)},${ftpsDataPort % 256})\r\n`); break; + case "RETR": + socket.write(`150 Opening data connection for ${argument}\r\n`); + releaseData?.(); + setTimeout(() => socket.write("226 Transfer complete\r\n"), 25); + break; + case "QUIT": socket.end("221 Goodbye\r\n"); break; + default: socket.write("200 OK\r\n"); break; + } + } + }); + }); + await new Promise((resolveListen) => + ftpsControlServer.listen(0, "127.0.0.1", resolveListen), + ); + ftpsControlPort = ( + ftpsControlServer.address() as import("node:net").AddressInfo + ).port; + } + }); + + afterAll(async () => { + for (const s of [ + server, + selfSignedServer, + validHttpsServer, + caHttpsServer, + mutualTlsServer, + handshakeStallServer, + readStallServer, + ftpsControlServer, + ftpsDataServer, + ]) { + if (s) { + await new Promise((resolveClose) => s.close(() => resolveClose())); + } + } + }); + + afterEach(async () => { + await kernel?.dispose(); + }); + + async function mountKernel() { + const filesystem = createInMemoryFileSystem(); + kernel = createKernel({ + filesystem, + permissions: allowAll, + loopbackExemptPorts: [ + port, + selfSignedPort, + validHttpsPort, + caHttpsPort, + mutualTlsPort, + handshakeStallPort, + readStallPort, + ftpsControlPort, + ftpsDataPort, + ].filter((p) => typeof p === "number"), + }); + await kernel.mount(createWasmVmRuntime({ commandDirs: WGET_COMMAND_DIRS })); + + // Seed the Debian-shaped trust store the way the native VM bootstrap + // does, so wget's default CA bundle resolves in-guest. Only the + // "trusted" CA is placed here; the cacert-only CA is intentionally + // absent. + if (seededCaPem) { + await filesystem.mkdir("/etc/ssl/certs", { recursive: true }); + await kernel.writeFile("/etc/ssl/certs/ca-certificates.crt", seededCaPem); + } + return filesystem; + } + + it("downloads a file using the URL basename", async () => { + const filesystem = await mountKernel(); + + const result = await kernel.exec(`wget http://127.0.0.1:${port}/file.txt`, { + timeout: WGET_EXEC_TIMEOUT_MS, + }); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(await filesystem.readTextFile("/workspace/file.txt")).toBe( + "downloaded content", + ); + }, 15_000); + + it("-O saves to the requested output path", async () => { + const filesystem = await mountKernel(); + + const result = await kernel.exec( + `wget -O /output.txt http://127.0.0.1:${port}/data.json`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(await filesystem.readTextFile("/output.txt")).toContain( + '"status":"ok"', + ); + }, 15_000); + + it("-q suppresses progress output", async () => { + const filesystem = await mountKernel(); + + const result = await kernel.exec( + `wget -q -O /quiet.txt http://127.0.0.1:${port}/file.txt`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stderr).toBe(""); + expect(await filesystem.readTextFile("/quiet.txt")).toBe( + "downloaded content", + ); + }, 15_000); + + it("reports failure for a 404 URL", async () => { + await mountKernel(); + + const result = await kernel.exec( + `wget http://127.0.0.1:${port}/missing.txt`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toMatch(/404|not found|error/i); + }, 15_000); + + it("follows redirects by default", async () => { + const filesystem = await mountKernel(); + + const result = await kernel.exec( + `wget -O /redirected.txt http://127.0.0.1:${port}/redirect`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(await filesystem.readTextFile("/redirected.txt")).toBe( + "arrived after redirect", + ); + }, 15_000); + + it("--version reports the mbedTLS HTTPS backend", async () => { + await mountKernel(); + + const result = await kernel.exec("wget --version", { + timeout: WGET_EXEC_TIMEOUT_MS, + }); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(result.stdout).toContain("GNU Wget 1.24.5"); + // Real in-guest TLS: HTTPS is compiled in and the backend is mbedTLS. + expect(result.stdout).toMatch(/\+https/); + expect(result.stdout).toMatch(/ssl\/mbedtls/i); + }, 15_000); + + it("--compression=auto inflates a gzip response body", async () => { + const filesystem = await mountKernel(); + + const result = await kernel.exec( + `wget --compression=auto -O /gz.txt http://127.0.0.1:${port}/gzip`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(await filesystem.readTextFile("/gz.txt")).toBe(COMPRESSION_PAYLOAD); + }, 15_000); + + it("times out a stalled TLS handshake instead of hanging", async () => { + await mountKernel(); + + const result = await kernel.exec( + `wget --tries=1 --connect-timeout=1 --read-timeout=1 --no-check-certificate -O /handshake-timeout.txt https://127.0.0.1:${handshakeStallPort}/`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toMatch(/timed out|timeout/i); + }, 15_000); + + itIf( + hasOpenssl, + "treats a stalled HTTPS response body as a timeout, not clean EOF", + async () => { + await mountKernel(); + + const result = await kernel.exec( + `wget --tries=1 --read-timeout=1 -O /truncated.txt https://127.0.0.1:${readStallPort}/`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toMatch(/timed out|timeout/i); + }, + 15_000, + ); + + itIf(hasOpenssl, "downloads over HTTPS verifying against the seeded CA bundle", async () => { + const filesystem = await mountKernel(); + + // No --no-check-certificate, no --ca-certificate: trust comes solely + // from the seeded /etc/ssl/certs/ca-certificates.crt, like Debian wget. + const result = await kernel.exec( + `wget -O /secure.txt https://127.0.0.1:${validHttpsPort}/file`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(await filesystem.readTextFile("/secure.txt")).toBe( + "verified https content", + ); + }, 15_000); + + itIf(hasOpenssl, "fails with a real cert error on an untrusted (self-signed) server", async () => { + await mountKernel(); + + const result = await kernel.exec( + `wget -O /nope.txt https://127.0.0.1:${selfSignedPort}/file`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + // VERIFCERTERR -> WGET_EXIT_SSL_AUTH_FAIL == 5, the native taxonomy. + expect(result.exitCode).toBe(5); + expect(result.stderr).toMatch(/cannot verify|certificate|not trusted/i); + }, 15_000); + + itIf(hasOpenssl, "--no-check-certificate accepts a self-signed server", async () => { + const filesystem = await mountKernel(); + + const result = await kernel.exec( + `wget --no-check-certificate -O /insecure.txt https://127.0.0.1:${selfSignedPort}/file`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(await filesystem.readTextFile("/insecure.txt")).toBe( + "self-signed secure content", + ); + }, 15_000); + + itIf(hasOpenssl, "--ca-certificate trusts a server signed by that CA", async () => { + const filesystem = await mountKernel(); + + // caHttpsServer's CA is NOT in the seeded bundle, so this only passes if + // --ca-certificate is honored (real file read + chain build in-guest). + await kernel.writeFile("/tmp/cacert-only.pem", caOnlyPem); + const result = await kernel.exec( + `wget --ca-certificate=/tmp/cacert-only.pem -O /cacert.txt https://127.0.0.1:${caHttpsPort}/file`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(await filesystem.readTextFile("/cacert.txt")).toBe( + "cacert https content", + ); + }, 15_000); + + itIf(hasOpenssl, "--ca-certificate augments rather than replaces system trust", async () => { + const filesystem = await mountKernel(); + + // Linux Wget loads system trust first, then adds --ca-certificate. An + // unrelated supplemental CA must not make an otherwise trusted server + // fail verification. + await kernel.writeFile("/tmp/additional-ca.pem", caOnlyPem); + const result = await kernel.exec( + `wget --ca-certificate=/tmp/additional-ca.pem -O /system-trust.txt https://127.0.0.1:${validHttpsPort}/file`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(await filesystem.readTextFile("/system-trust.txt")).toBe( + "verified https content", + ); + }, 15_000); + + itIf(hasOpenssl, "--ca-certificate with the wrong CA still fails verification", async () => { + await mountKernel(); + + // Point --ca-certificate at the seeded CA, which did NOT sign + // caHttpsServer's leaf. + await kernel.writeFile("/tmp/wrong-ca.pem", seededCaPem); + const result = await kernel.exec( + `wget --ca-certificate=/tmp/wrong-ca.pem -O /wrong.txt https://127.0.0.1:${caHttpsPort}/file`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode).toBe(5); + expect(result.stderr).toMatch(/cannot verify|certificate|not trusted/i); + }, 15_000); + + itIf(hasOpenssl, "--secure-protocol=TLSv1_2 remains a minimum and permits TLS 1.3", async () => { + const filesystem = await mountKernel(); + const result = await kernel.exec( + `wget --secure-protocol=TLSv1_2 -O /tls13.txt https://127.0.0.1:${validHttpsPort}/file`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(await filesystem.readTextFile("/tls13.txt")).toBe( + "verified https content", + ); + }, 15_000); + + itIf(hasOpenssl, "rejects unavailable SSLv3 instead of silently upgrading to TLS", async () => { + await mountKernel(); + const result = await kernel.exec( + `wget --secure-protocol=SSLv3 -O /old.txt https://127.0.0.1:${validHttpsPort}/file`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toMatch(/does not support requested protocol|SSLv3/i); + }, 15_000); + + itIf(hasOpenssl, "honors common OpenSSL HIGH/exclusion cipher policy syntax", async () => { + const filesystem = await mountKernel(); + const result = await kernel.exec( + `wget --ciphers='HIGH:!aNULL:!RC4:!MD5:!SRP:!PSK' -O /cipher.txt https://127.0.0.1:${validHttpsPort}/file`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(await filesystem.readTextFile("/cipher.txt")).toBe( + "verified https content", + ); + }, 15_000); + + itIf(hasOpenssl, "translates an explicit OpenSSL TLS 1.2 cipher name", async () => { + const filesystem = await mountKernel(); + await kernel.writeFile("/tmp/cipher-ca.pem", caOnlyPem); + const result = await kernel.exec( + `wget --ciphers=ECDHE-RSA-AES128-GCM-SHA256 --ca-certificate=/tmp/cipher-ca.pem ` + + `-O /explicit-cipher.txt https://127.0.0.1:${caHttpsPort}/file`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(await filesystem.readTextFile("/explicit-cipher.txt")).toBe( + "cacert https content", + ); + }, 15_000); + + itIf(hasOpenssl, "leaves TLS 1.3 enabled when --ciphers names a TLS 1.2 suite", async () => { + const filesystem = await mountKernel(); + const result = await kernel.exec( + `wget --ciphers=ECDHE-RSA-AES128-GCM-SHA256 -O /tls13-cipher.txt ` + + `https://127.0.0.1:${validHttpsPort}/file`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(await filesystem.readTextFile("/tls13-cipher.txt")).toBe( + "verified https content", + ); + }, 15_000); + + itIf(hasOpenssl, "fails explicitly for an unsupported cipher policy token", async () => { + await mountKernel(); + const result = await kernel.exec( + `wget --ciphers=NOT-A-CIPHER -O /bad-cipher.txt https://127.0.0.1:${validHttpsPort}/file`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toMatch(/unsupported.*cipher policy token|NOT-A-CIPHER/i); + }, 15_000); + + itIf(hasOpenssl, "presents --certificate and --private-key to a mutual-TLS server", async () => { + const filesystem = await mountKernel(); + await kernel.writeFile("/tmp/mutual-ca.pem", mutualCaPem); + await kernel.writeFile("/tmp/client.crt", clientCertPem); + await kernel.writeFile("/tmp/client.key", clientKeyPem); + const result = await kernel.exec( + `wget --ca-certificate=/tmp/mutual-ca.pem --certificate=/tmp/client.crt ` + + `--private-key=/tmp/client.key -O /mutual.txt https://127.0.0.1:${mutualTlsPort}/file`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(await filesystem.readTextFile("/mutual.txt")).toBe( + "mutual tls content", + ); + }, 15_000); + + itIf(hasOpenssl, "resumes the FTPS control session on the protected data channel", async () => { + const filesystem = await mountKernel(); + const result = await kernel.exec( + `wget --ftps-implicit -O /ftps.txt ftps://127.0.0.1:${ftpsControlPort}/file.txt`, + { timeout: WGET_EXEC_TIMEOUT_MS }, + ); + + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(await filesystem.readTextFile("/ftps.txt")).toBe( + "resumed ftps content\n", + ); + expect(ftpsDataSessionReused).toBe(true); + }, 20_000); +}); diff --git a/software/wget/tsconfig.json b/software/wget/tsconfig.json new file mode 100644 index 0000000000..db523bef56 --- /dev/null +++ b/software/wget/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/yq/agentos-package.json b/software/yq/agentos-package.json new file mode 100644 index 0000000000..d6de19566d --- /dev/null +++ b/software/yq/agentos-package.json @@ -0,0 +1,11 @@ +{ + "commands": [ + "yq" + ], + "registry": { + "title": "yq", + "description": "YAML/JSON processor.", + "priority": 4, + "category": "data" + } +} diff --git a/software/yq/native/crates/cmd-yq/Cargo.toml b/software/yq/native/crates/cmd-yq/Cargo.toml new file mode 100644 index 0000000000..dfa19879f9 --- /dev/null +++ b/software/yq/native/crates/cmd-yq/Cargo.toml @@ -0,0 +1,14 @@ +[package] +workspace = "../../../../../toolchain" +name = "cmd-yq" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "yq standalone binary for secure-exec VM" + +[[bin]] +name = "yq" +path = "src/main.rs" + +[dependencies] +secureexec-yq = { path = "../yq" } diff --git a/registry/native/crates/commands/yq/src/main.rs b/software/yq/native/crates/cmd-yq/src/main.rs similarity index 100% rename from registry/native/crates/commands/yq/src/main.rs rename to software/yq/native/crates/cmd-yq/src/main.rs diff --git a/software/yq/native/crates/yq/Cargo.toml b/software/yq/native/crates/yq/Cargo.toml new file mode 100644 index 0000000000..5712402a21 --- /dev/null +++ b/software/yq/native/crates/yq/Cargo.toml @@ -0,0 +1,16 @@ +[package] +workspace = "../../../../../toolchain" +name = "secureexec-yq" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "yq (YAML/XML/TOML/JSON processor) implementation for secure-exec standalone binaries" + +[dependencies] +jaq-core = "2.2" +jaq-std = "2.1" +jaq-json = { version = "1.1", features = ["serde_json"] } +serde_json = "1" +serde_yaml = "0.9" +toml = "0.8" +quick-xml = "0.37" diff --git a/software/yq/native/crates/yq/src/lib.rs b/software/yq/native/crates/yq/src/lib.rs new file mode 100644 index 0000000000..e5332476bd --- /dev/null +++ b/software/yq/native/crates/yq/src/lib.rs @@ -0,0 +1,1049 @@ +//! yq — YAML/XML/TOML/JSON processor using jaq filter engine. +//! +//! Converts input to JSON, runs jaq filter, converts output to requested format. +//! Reuses jaq-core/jaq-std/jaq-json (same engine as jq command). + +use std::ffi::OsString; +use std::fmt; +use std::fs::File as FsFile; +use std::io::{self, Read, Write}; + +use jaq_core::load::{Arena, File, Loader}; +use jaq_core::{Compiler, Ctx, RcIter}; +use jaq_json::Val; + +const MAX_INPUT_BYTES: usize = 16 * 1024 * 1024; +const MAX_FORMATTED_OUTPUT_BYTES: usize = 16 * 1024 * 1024; +const MAX_OUTPUT_VALUES: usize = 100_000; +const MAX_XML_DEPTH: usize = 256; +const MAX_XML_NODES: usize = 100_000; +const MAX_XML_ATTRIBUTES_PER_ELEMENT: usize = 4096; +const MAX_XML_TEXT_BYTES: usize = 16 * 1024 * 1024; + +#[derive(Clone, Copy, PartialEq)] +enum Format { + Yaml, + Json, + Toml, + Xml, +} + +struct YqOptions { + filter: String, + input_format: Option, + output_format: Option, + raw_output: bool, + compact: bool, + null_input: bool, + slurp: bool, + input_paths: Vec, +} + +/// Entry point for yq command. +pub fn main(args: Vec) -> i32 { + let str_args: Vec = args + .iter() + .skip(1) + .map(|a| a.to_string_lossy().to_string()) + .collect(); + + match run_yq(&str_args) { + Ok(code) => code, + Err(msg) => { + eprintln!("yq: {}", msg); + 2 + } + } +} + +fn parse_format(s: &str) -> Result { + match s { + "yaml" | "y" => Ok(Format::Yaml), + "json" | "j" => Ok(Format::Json), + "toml" | "t" => Ok(Format::Toml), + "xml" | "x" => Ok(Format::Xml), + _ => Err(format!( + "unknown format: {} (expected yaml, json, toml, xml)", + s + )), + } +} + +fn parse_args(args: &[String]) -> Result { + let mut opts = YqOptions { + filter: String::new(), + input_format: None, + output_format: None, + raw_output: false, + compact: false, + null_input: false, + slurp: false, + input_paths: Vec::new(), + }; + + let mut filter_set = false; + let mut i = 0; + + while i < args.len() { + let arg = &args[i]; + + if arg == "--" { + let remaining = &args[i + 1..]; + if !filter_set { + if let Some(filter) = remaining.first() { + opts.filter = filter.clone(); + filter_set = true; + opts.input_paths.extend(remaining.iter().skip(1).cloned()); + } + } else { + opts.input_paths.extend(remaining.iter().cloned()); + } + break; + } + + if arg == "-p" || arg == "--input-format" { + i += 1; + if i >= args.len() { + return Err("-p requires a format argument".to_string()); + } + opts.input_format = Some(parse_format(&args[i])?); + } else if arg == "-o" || arg == "--output-format" { + i += 1; + if i >= args.len() { + return Err("-o requires a format argument".to_string()); + } + opts.output_format = Some(parse_format(&args[i])?); + } else if arg == "-r" || arg == "--raw-output" { + opts.raw_output = true; + } else if arg == "-c" || arg == "--compact-output" { + opts.compact = true; + } else if arg == "-n" || arg == "--null-input" { + opts.null_input = true; + } else if arg == "-s" || arg == "--slurp" { + opts.slurp = true; + } else if arg.starts_with('-') && arg.len() > 1 && !arg.starts_with("--") { + // Combined short flags like -rc + let flags = &arg[1..]; + let mut chars = flags.chars().peekable(); + while let Some(c) = chars.next() { + match c { + 'r' => opts.raw_output = true, + 'c' => opts.compact = true, + 'n' => opts.null_input = true, + 's' => opts.slurp = true, + 'p' => { + let rest: String = chars.collect(); + if !rest.is_empty() { + opts.input_format = Some(parse_format(&rest)?); + } else { + i += 1; + if i >= args.len() { + return Err("-p requires a format argument".to_string()); + } + opts.input_format = Some(parse_format(&args[i])?); + } + break; + } + 'o' => { + let rest: String = chars.collect(); + if !rest.is_empty() { + opts.output_format = Some(parse_format(&rest)?); + } else { + i += 1; + if i >= args.len() { + return Err("-o requires a format argument".to_string()); + } + opts.output_format = Some(parse_format(&args[i])?); + } + break; + } + _ => return Err(format!("unknown option: -{}", c)), + } + } + } else if !filter_set { + opts.filter = arg.clone(); + filter_set = true; + } else { + opts.input_paths.push(arg.clone()); + } + + i += 1; + } + + if !filter_set { + opts.filter = ".".to_string(); + } + + Ok(opts) +} + +fn detect_format(input: &str) -> Format { + let trimmed = input.trim_start(); + + // JSON: starts with { or [ + if trimmed.starts_with('{') || trimmed.starts_with('[') { + if serde_json::from_str::(trimmed).is_ok() { + return Format::Json; + } + } + + // XML: starts with < (including (trimmed).is_ok() + { + return Format::Toml; + } + } + + // Default: YAML + Format::Yaml +} + +fn parse_input(input: &str, format: Format) -> Result { + match format { + Format::Json => serde_json::from_str(input).map_err(|e| format!("invalid JSON: {}", e)), + Format::Yaml => serde_yaml::from_str(input).map_err(|e| format!("invalid YAML: {}", e)), + Format::Toml => { + let toml_val: toml::Value = + toml::from_str(input).map_err(|e| format!("invalid TOML: {}", e))?; + toml_to_json(toml_val) + } + Format::Xml => xml_to_json(input), + } +} + +fn toml_to_json(val: toml::Value) -> Result { + Ok(match val { + toml::Value::String(s) => serde_json::Value::String(s), + toml::Value::Integer(i) => serde_json::Value::Number(serde_json::Number::from(i)), + toml::Value::Float(f) => serde_json::Number::from_f64(f) + .map(serde_json::Value::Number) + .unwrap_or(serde_json::Value::Null), + toml::Value::Boolean(b) => serde_json::Value::Bool(b), + toml::Value::Datetime(dt) => serde_json::Value::String(dt.to_string()), + toml::Value::Array(arr) => { + let items: Result, _> = arr.into_iter().map(toml_to_json).collect(); + serde_json::Value::Array(items?) + } + toml::Value::Table(table) => { + let mut map = serde_json::Map::new(); + for (k, v) in table { + map.insert(k, toml_to_json(v)?); + } + serde_json::Value::Object(map) + } + }) +} + +fn json_to_toml(val: &serde_json::Value) -> Result { + Ok(match val { + serde_json::Value::Null => return Err("TOML does not support null values".to_string()), + serde_json::Value::Bool(b) => toml::Value::Boolean(*b), + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() { + toml::Value::Integer(i) + } else if let Some(f) = n.as_f64() { + toml::Value::Float(f) + } else { + return Err("unsupported number for TOML".to_string()); + } + } + serde_json::Value::String(s) => toml::Value::String(s.clone()), + serde_json::Value::Array(arr) => { + let items: Result, _> = arr.iter().map(json_to_toml).collect(); + toml::Value::Array(items?) + } + serde_json::Value::Object(map) => { + let mut table = toml::map::Map::new(); + for (k, v) in map { + table.insert(k.clone(), json_to_toml(v)?); + } + toml::Value::Table(table) + } + }) +} + +// --- XML parsing --- + +fn xml_to_json(input: &str) -> Result { + use quick_xml::events::Event; + use quick_xml::Reader; + + struct StackEntry { + name: String, + children: serde_json::Map, + text: String, + } + + let mut reader = Reader::from_str(input); + let mut stack: Vec = Vec::new(); + let mut root = serde_json::Map::new(); + let mut nodes = 0usize; + + loop { + match reader.read_event() { + Ok(Event::Start(ref e)) => { + count_xml_node(&mut nodes)?; + if stack.len() >= MAX_XML_DEPTH { + return Err("XML exceeds maximum nesting depth".to_string()); + } + let name = String::from_utf8_lossy(e.name().as_ref()).to_string(); + let mut children = serde_json::Map::new(); + let mut attr_count = 0usize; + for attr in e.attributes() { + let attr = attr.map_err(|e| format!("invalid XML attribute: {}", e))?; + attr_count += 1; + if attr_count > MAX_XML_ATTRIBUTES_PER_ELEMENT { + return Err("XML element has too many attributes".to_string()); + } + let key = format!("@{}", String::from_utf8_lossy(attr.key.as_ref())); + let val = String::from_utf8_lossy(&attr.value).to_string(); + children.insert(key, serde_json::Value::String(val)); + } + stack.push(StackEntry { + name, + children, + text: String::new(), + }); + } + Ok(Event::End(_)) => { + let entry = stack.pop().ok_or("unexpected closing tag")?; + let text = entry.text.trim().to_string(); + + let value = if entry.children.is_empty() && text.is_empty() { + serde_json::Value::Null + } else if entry.children.is_empty() { + serde_json::Value::String(text) + } else { + let mut obj = entry.children; + if !text.is_empty() { + obj.insert("#text".to_string(), serde_json::Value::String(text)); + } + serde_json::Value::Object(obj) + }; + + let target = if let Some(parent) = stack.last_mut() { + &mut parent.children + } else { + &mut root + }; + + insert_or_array(target, entry.name, value); + } + Ok(Event::Empty(ref e)) => { + count_xml_node(&mut nodes)?; + if stack.len() >= MAX_XML_DEPTH { + return Err("XML exceeds maximum nesting depth".to_string()); + } + let name = String::from_utf8_lossy(e.name().as_ref()).to_string(); + let mut attrs = serde_json::Map::new(); + let mut attr_count = 0usize; + for attr in e.attributes() { + let attr = attr.map_err(|e| format!("invalid XML attribute: {}", e))?; + attr_count += 1; + if attr_count > MAX_XML_ATTRIBUTES_PER_ELEMENT { + return Err("XML element has too many attributes".to_string()); + } + let key = format!("@{}", String::from_utf8_lossy(attr.key.as_ref())); + let val = String::from_utf8_lossy(&attr.value).to_string(); + attrs.insert(key, serde_json::Value::String(val)); + } + + let value = if attrs.is_empty() { + serde_json::Value::Null + } else { + serde_json::Value::Object(attrs) + }; + + let target = if let Some(parent) = stack.last_mut() { + &mut parent.children + } else { + &mut root + }; + + insert_or_array(target, name, value); + } + Ok(Event::Text(ref e)) => { + if let Some(entry) = stack.last_mut() { + let text = e + .unescape() + .map_err(|e| format!("invalid XML text: {}", e))?; + let next_len = entry + .text + .len() + .checked_add(text.len()) + .ok_or("XML text length overflowed")?; + if next_len > MAX_XML_TEXT_BYTES { + return Err("XML text exceeds size limit".to_string()); + } + entry.text.push_str(&text); + } + } + Ok(Event::Eof) => break, + Ok(_) => {} // Skip PI, Comment, Decl, DocType, CData + Err(e) => return Err(format!("invalid XML: {}", e)), + } + } + + if !stack.is_empty() { + return Err("unexpected end of XML input".to_string()); + } + + Ok(serde_json::Value::Object(root)) +} + +fn count_xml_node(nodes: &mut usize) -> Result<(), String> { + *nodes = nodes.checked_add(1).ok_or("XML node count overflowed")?; + if *nodes > MAX_XML_NODES { + return Err("XML contains too many nodes".to_string()); + } + Ok(()) +} + +fn record_output_value(output_count: &mut usize) -> Result<(), String> { + *output_count = output_count + .checked_add(1) + .ok_or("output count overflowed")?; + if *output_count > MAX_OUTPUT_VALUES { + return Err("too many output values".to_string()); + } + Ok(()) +} + +fn read_limited_string(reader: R) -> Result { + read_limited_source(reader, "stdin", MAX_INPUT_BYTES) +} + +fn read_limited_source(reader: R, label: &str, limit: usize) -> Result { + let mut input = String::new(); + reader + .take((limit + 1) as u64) + .read_to_string(&mut input) + .map_err(|e| format!("failed to read {}: {}", label, e))?; + if input.len() > limit { + return Err(format!("{} exceeds size limit", label)); + } + Ok(input) +} + +fn read_sources(opts: &YqOptions) -> Result, String> { + if opts.input_paths.is_empty() { + return Ok(vec![read_limited_string(io::stdin())?]); + } + + let mut total = 0usize; + let mut sources = Vec::new(); + for path in &opts.input_paths { + let remaining = MAX_INPUT_BYTES.saturating_sub(total); + let data = if path == "-" { + read_limited_source(io::stdin(), "stdin", remaining)? + } else { + let file = FsFile::open(path).map_err(|e| format!("failed to open {}: {}", path, e))?; + read_limited_source(file, path, remaining)? + }; + total = total + .checked_add(data.len()) + .ok_or_else(|| "input exceeds size limit".to_string())?; + if total > MAX_INPUT_BYTES { + return Err("input exceeds size limit".to_string()); + } + sources.push(data); + } + Ok(sources) +} + +fn insert_or_array( + map: &mut serde_json::Map, + key: String, + value: serde_json::Value, +) { + if let Some(existing) = map.get_mut(&key) { + match existing { + serde_json::Value::Array(arr) => arr.push(value), + _ => { + let prev = existing.clone(); + *existing = serde_json::Value::Array(vec![prev, value]); + } + } + } else { + map.insert(key, value); + } +} + +// --- XML output --- + +fn json_to_xml(val: &serde_json::Value) -> Result { + use quick_xml::Writer; + + let mut writer = Writer::new(LimitedBytes::new(MAX_FORMATTED_OUTPUT_BYTES)); + + match val { + serde_json::Value::Object(map) => { + for (key, value) in map { + write_xml_element(&mut writer, key, value) + .map_err(|e| format!("XML write error: {}", e))?; + } + } + _ => { + write_xml_element(&mut writer, "root", val) + .map_err(|e| format!("XML write error: {}", e))?; + } + } + + writer + .into_inner() + .into_string() + .map_err(|e| format!("XML encoding error: {}", e)) +} + +fn write_xml_element( + writer: &mut quick_xml::Writer, + name: &str, + val: &serde_json::Value, +) -> Result<(), quick_xml::Error> { + use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event}; + + match val { + serde_json::Value::Object(map) => { + let mut elem = BytesStart::new(name); + for (k, v) in map { + if let Some(attr_name) = k.strip_prefix('@') { + if let serde_json::Value::String(s) = v { + elem.push_attribute((attr_name, s.as_str())); + } + } + } + writer.write_event(Event::Start(elem))?; + + if let Some(serde_json::Value::String(text)) = map.get("#text") { + writer.write_event(Event::Text(BytesText::new(text)))?; + } + + for (k, v) in map { + if k.starts_with('@') || k == "#text" { + continue; + } + match v { + serde_json::Value::Array(arr) => { + for item in arr { + write_xml_element(writer, k, item)?; + } + } + _ => write_xml_element(writer, k, v)?, + } + } + + writer.write_event(Event::End(BytesEnd::new(name)))?; + } + serde_json::Value::Array(arr) => { + for item in arr { + write_xml_element(writer, name, item)?; + } + } + serde_json::Value::String(s) => { + writer.write_event(Event::Start(BytesStart::new(name)))?; + writer.write_event(Event::Text(BytesText::new(s)))?; + writer.write_event(Event::End(BytesEnd::new(name)))?; + } + serde_json::Value::Number(n) => { + let s = n.to_string(); + writer.write_event(Event::Start(BytesStart::new(name)))?; + writer.write_event(Event::Text(BytesText::new(&s)))?; + writer.write_event(Event::End(BytesEnd::new(name)))?; + } + serde_json::Value::Bool(b) => { + let s = if *b { "true" } else { "false" }; + writer.write_event(Event::Start(BytesStart::new(name)))?; + writer.write_event(Event::Text(BytesText::new(s)))?; + writer.write_event(Event::End(BytesEnd::new(name)))?; + } + serde_json::Value::Null => { + writer.write_event(Event::Empty(BytesStart::new(name)))?; + } + } + Ok(()) +} + +// --- Output formatting --- + +fn format_val_output(val: &Val, opts: &YqOptions, out_format: Format) -> Result { + let mut compact = LimitedString::new(MAX_FORMATTED_OUTPUT_BYTES); + fmt::write(&mut compact, format_args!("{}", val)) + .map_err(|_| "formatted output exceeds size limit".to_string())?; + let compact_str = compact.into_string(); + + // Raw output: unquote strings + if opts.raw_output { + if compact_str.starts_with('"') && compact_str.ends_with('"') && compact_str.len() >= 2 { + if let Ok(unescaped) = serde_json::from_str::(&compact_str) { + ensure_formatted_output_limit(unescaped.len())?; + return Ok(unescaped); + } + } + } + + let json_val: serde_json::Value = + serde_json::from_str(&compact_str).unwrap_or(serde_json::Value::String(compact_str)); + + let output = format_json_as(out_format, &json_val, opts.compact)?; + ensure_formatted_output_limit(output.len())?; + Ok(output) +} + +fn ensure_formatted_output_limit(len: usize) -> Result<(), String> { + if len > MAX_FORMATTED_OUTPUT_BYTES { + return Err("formatted output exceeds size limit".to_string()); + } + Ok(()) +} + +struct LimitedString { + inner: String, + limit: usize, +} + +impl LimitedString { + fn new(limit: usize) -> Self { + Self { + inner: String::new(), + limit, + } + } + + fn into_string(self) -> String { + self.inner + } + + fn write_str(&mut self, s: &str) -> Result<(), String> { + let next_len = self + .inner + .len() + .checked_add(s.len()) + .ok_or("formatted output length overflowed")?; + if next_len > self.limit { + return Err("formatted output exceeds size limit".to_string()); + } + self.inner.push_str(s); + Ok(()) + } + + fn write_char(&mut self, ch: char) -> Result<(), String> { + let mut buf = [0u8; 4]; + self.write_str(ch.encode_utf8(&mut buf)) + } +} + +impl fmt::Write for LimitedString { + fn write_str(&mut self, s: &str) -> fmt::Result { + LimitedString::write_str(self, s).map_err(|_| fmt::Error) + } +} + +struct LimitedBytes { + inner: Vec, + limit: usize, +} + +impl LimitedBytes { + fn new(limit: usize) -> Self { + Self { + inner: Vec::new(), + limit, + } + } + + fn into_string(self) -> Result { + String::from_utf8(self.inner) + } +} + +impl io::Write for LimitedBytes { + fn write(&mut self, buf: &[u8]) -> io::Result { + let next_len = self + .inner + .len() + .checked_add(buf.len()) + .ok_or_else(|| io::Error::other("formatted output length overflowed"))?; + if next_len > self.limit { + return Err(io::Error::other("formatted output exceeds size limit")); + } + self.inner.extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} + +fn format_json_as( + format: Format, + val: &serde_json::Value, + compact: bool, +) -> Result { + match format { + Format::Json => { + let mut out = LimitedBytes::new(MAX_FORMATTED_OUTPUT_BYTES); + if compact { + serde_json::to_writer(&mut out, val) + .map_err(|e| format!("JSON output error: {}", e))?; + } else { + serde_json::to_writer_pretty(&mut out, val) + .map_err(|e| format!("JSON output error: {}", e))?; + } + out.into_string() + .map_err(|e| format!("JSON encoding error: {}", e)) + } + Format::Yaml => { + let mut out = LimitedBytes::new(MAX_FORMATTED_OUTPUT_BYTES); + serde_yaml::to_writer(&mut out, val) + .map_err(|e| format!("YAML output error: {}", e))?; + let s = out + .into_string() + .map_err(|e| format!("YAML encoding error: {}", e))?; + // Strip leading "---\n" and trailing newline for cleaner output + let s = s.strip_prefix("---\n").unwrap_or(&s); + let s = s.strip_suffix('\n').unwrap_or(s); + Ok(s.to_string()) + } + Format::Toml => json_to_toml_bounded(val), + Format::Xml => json_to_xml(val), + } +} + +fn json_to_toml_bounded(val: &serde_json::Value) -> Result { + let toml_val = json_to_toml(val)?; + let mut out = LimitedString::new(MAX_FORMATTED_OUTPUT_BYTES); + write_toml_document(&mut out, &toml_val)?; + let s = out.into_string(); + Ok(s.strip_suffix('\n').unwrap_or(&s).to_string()) +} + +fn write_toml_document(out: &mut LimitedString, val: &toml::Value) -> Result<(), String> { + match val { + toml::Value::Table(table) => write_toml_table(out, &mut Vec::new(), table), + other => write_toml_inline(out, other), + } +} + +fn write_toml_table( + out: &mut LimitedString, + path: &mut Vec, + table: &toml::map::Map, +) -> Result<(), String> { + for (key, value) in table { + if matches!(value, toml::Value::Table(_)) { + continue; + } + write_toml_key(out, key)?; + out.write_str(" = ")?; + write_toml_inline(out, value)?; + out.write_char('\n')?; + } + + for (key, value) in table { + let toml::Value::Table(child) = value else { + continue; + }; + if !path.is_empty() || table_has_scalar_entries(child) { + out.write_char('\n')?; + path.push(key.clone()); + out.write_char('[')?; + write_toml_path(out, path)?; + out.write_str("]\n")?; + write_toml_table(out, path, child)?; + path.pop(); + } else { + path.push(key.clone()); + write_toml_table(out, path, child)?; + path.pop(); + } + } + + Ok(()) +} + +fn table_has_scalar_entries(table: &toml::map::Map) -> bool { + table + .values() + .any(|value| !matches!(value, toml::Value::Table(_))) +} + +fn write_toml_path(out: &mut LimitedString, path: &[String]) -> Result<(), String> { + for (i, key) in path.iter().enumerate() { + if i > 0 { + out.write_char('.')?; + } + write_toml_key(out, key)?; + } + Ok(()) +} + +fn write_toml_key(out: &mut LimitedString, key: &str) -> Result<(), String> { + if !key.is_empty() + && key + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-') + { + out.write_str(key)?; + } else { + write_toml_string(out, key)?; + } + Ok(()) +} + +fn write_toml_inline(out: &mut LimitedString, val: &toml::Value) -> Result<(), String> { + match val { + toml::Value::String(s) => write_toml_string(out, s), + toml::Value::Integer(i) => out.write_str(&i.to_string()), + toml::Value::Float(f) => out.write_str(&f.to_string()), + toml::Value::Boolean(b) => out.write_str(if *b { "true" } else { "false" }), + toml::Value::Datetime(dt) => out.write_str(&dt.to_string()), + toml::Value::Array(arr) => { + out.write_char('[')?; + for (i, item) in arr.iter().enumerate() { + if i > 0 { + out.write_str(", ")?; + } + write_toml_inline(out, item)?; + } + out.write_char(']') + } + toml::Value::Table(table) => { + out.write_str("{ ")?; + for (i, (key, value)) in table.iter().enumerate() { + if i > 0 { + out.write_str(", ")?; + } + write_toml_key(out, key)?; + out.write_str(" = ")?; + write_toml_inline(out, value)?; + } + out.write_str(" }") + } + } +} + +fn write_toml_string(out: &mut LimitedString, s: &str) -> Result<(), String> { + out.write_char('"')?; + for ch in s.chars() { + match ch { + '"' => out.write_str("\\\"")?, + '\\' => out.write_str("\\\\")?, + '\n' => out.write_str("\\n")?, + '\r' => out.write_str("\\r")?, + '\t' => out.write_str("\\t")?, + '\u{08}' => out.write_str("\\b")?, + '\u{0c}' => out.write_str("\\f")?, + ch if ch.is_control() => out.write_str(&format!("\\u{:04X}", ch as u32))?, + ch => out.write_char(ch)?, + } + } + out.write_char('"') +} + +// --- Main logic --- + +fn run_yq(args: &[String]) -> Result { + let opts = parse_args(args)?; + + let sources = if opts.null_input { + Vec::new() + } else { + read_sources(&opts)? + }; + + let input_formats: Vec = if opts.null_input { + vec![opts.input_format.unwrap_or(Format::Yaml)] + } else { + sources + .iter() + .map(|source| opts.input_format.unwrap_or_else(|| detect_format(source))) + .collect() + }; + let default_input_format = input_formats.first().copied().unwrap_or(Format::Yaml); + + // Default output format: YAML for YAML input, otherwise matches input + let out_format = opts.output_format.unwrap_or(default_input_format); + + // Parse input to JSON, then convert to jaq Val + let inputs = if opts.null_input { + vec![Val::from(serde_json::Value::Null)] + } else { + let json_values: Result, _> = sources + .iter() + .zip(input_formats.iter().copied()) + .map(|(source, format)| parse_input(source, format)) + .collect(); + let mut json_values = json_values?; + if opts.slurp { + vec![Val::from(serde_json::Value::Array(json_values))] + } else { + json_values.drain(..).map(Val::from).collect() + } + }; + + // Compile jaq filter (same engine as jq) + let loader = Loader::new(jaq_std::defs().chain(jaq_json::defs())); + let arena = Arena::default(); + let program = File { + code: opts.filter.as_str(), + path: (), + }; + let modules = loader + .load(&arena, program) + .map_err(|errs| format!("parse error: {:?}", errs))?; + + let filter = Compiler::default() + .with_funs(jaq_std::funs().chain(jaq_json::funs())) + .compile(modules) + .map_err(|errs| format!("compile error: {:?}", errs))?; + + // Execute filter and output + let empty_inputs = RcIter::new(core::iter::empty()); + let stdout = io::stdout(); + let mut out = stdout.lock(); + let mut output_count = 0usize; + + for input in inputs { + let ctx = Ctx::new(core::iter::empty(), &empty_inputs); + let results = filter.run((ctx, input)); + + for result in results { + match result { + Ok(val) => { + record_output_value(&mut output_count)?; + let s = format_val_output(&val, &opts, out_format)?; + writeln!(out, "{}", s).map_err(|e| format!("failed to write stdout: {}", e))?; + } + Err(e) => { + eprintln!("yq: error: {}", e); + return Ok(5); + } + } + } + } + + out.flush() + .map_err(|e| format!("failed to flush stdout: {}", e))?; + + Ok(0) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn xml_depth_limit_rejects_deep_input() { + let mut input = String::new(); + for i in 0..=MAX_XML_DEPTH { + input.push_str(&format!("")); + } + for i in (0..=MAX_XML_DEPTH).rev() { + input.push_str(&format!("")); + } + + let err = xml_to_json(&input).unwrap_err(); + + assert!(err.contains("nesting depth")); + } + + #[test] + fn xml_depth_limit_rejects_deep_empty_input() { + let mut input = String::new(); + for i in 0..MAX_XML_DEPTH { + input.push_str(&format!("")); + } + input.push_str(""); + for i in (0..MAX_XML_DEPTH).rev() { + input.push_str(&format!("")); + } + + let err = xml_to_json(&input).unwrap_err(); + + assert!(err.contains("nesting depth")); + } + + #[test] + fn xml_node_limit_rejects_many_elements() { + let mut nodes = MAX_XML_NODES; + + let err = count_xml_node(&mut nodes).unwrap_err(); + + assert!(err.contains("too many nodes")); + } + + #[test] + fn xml_text_limit_rejects_large_text() { + let input = format!("{}", "x".repeat(MAX_XML_TEXT_BYTES + 1)); + + let err = xml_to_json(&input).unwrap_err(); + + assert!(err.contains("text exceeds")); + } + + #[test] + fn output_limit_rejects_too_many_values() { + let mut count = MAX_OUTPUT_VALUES; + + let err = record_output_value(&mut count).unwrap_err(); + + assert!(err.contains("too many output values")); + } + + #[test] + fn formatted_output_limit_rejects_large_output() { + let err = ensure_formatted_output_limit(MAX_FORMATTED_OUTPUT_BYTES + 1).unwrap_err(); + + assert!(err.contains("formatted output")); + } + + #[test] + fn limited_bytes_rejects_large_serializer_write() { + let mut output = LimitedBytes::new(4); + + let err = output.write_all(b"hello").unwrap_err(); + + assert!(err.to_string().contains("formatted output")); + } + + #[test] + fn limited_toml_writer_rejects_large_value() { + let mut output = LimitedString::new(4); + let value = toml::Value::String("hello".to_string()); + + let err = write_toml_inline(&mut output, &value).unwrap_err(); + + assert!(err.contains("formatted output")); + } + + #[test] + fn input_limit_rejects_oversized_reader() { + let input = vec![b'x'; MAX_INPUT_BYTES + 1]; + + let err = read_limited_string(&input[..]).unwrap_err(); + + assert!(err.contains("stdin exceeds")); + } + + #[test] + fn xml_rejects_unclosed_elements() { + let err = xml_to_json("").unwrap_err(); + + assert!(err.contains("unexpected end")); + } + + #[test] + fn xml_rejects_invalid_text_escape() { + let err = xml_to_json("&bogus;").unwrap_err(); + + assert!(err.contains("invalid XML text")); + } +} diff --git a/software/yq/package.json b/software/yq/package.json new file mode 100644 index 0000000000..bce99150e3 --- /dev/null +++ b/software/yq/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/yq", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "yq YAML/JSON processor for secure-exec VMs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/yq/src/index.ts b/software/yq/src/index.ts similarity index 100% rename from registry/software/yq/src/index.ts rename to software/yq/src/index.ts diff --git a/software/yq/test/manifest.test.ts b/software/yq/test/manifest.test.ts new file mode 100644 index 0000000000..89139d33b8 --- /dev/null +++ b/software/yq/test/manifest.test.ts @@ -0,0 +1,19 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const packageDir = new URL("..", import.meta.url).pathname; + +describe("package manifest", () => { + it("declares command binaries", () => { + const manifest = JSON.parse( + readFileSync(join(packageDir, "agentos-package.json"), "utf8"), + ); + + expect(manifest.commands?.length ?? 0).toBeGreaterThan(0); + for (const command of manifest.commands) { + expect(typeof command).toBe("string"); + expect(command.length).toBeGreaterThan(0); + } + }); +}); diff --git a/software/yq/test/yq.test.ts b/software/yq/test/yq.test.ts new file mode 100644 index 0000000000..c74fe1b1d7 --- /dev/null +++ b/software/yq/test/yq.test.ts @@ -0,0 +1,181 @@ +import { existsSync } from "node:fs"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + NodeFileSystem, + createKernel, + createWasmVmRuntime, + describeIf, +} from "@agentos/test-harness"; +import type { Kernel } from "@agentos/test-harness"; +import { afterEach, expect, it } from "vitest"; + +const YQ_COMMAND_DIR = fileURLToPath(new URL("../bin", import.meta.url)); +const hasYqPackageBinary = existsSync(join(YQ_COMMAND_DIR, "yq")); + +let tempRoot: string | undefined; + +async function writeFixture(path: string, contents: string): Promise { + if (!tempRoot) throw new Error("fixture root not initialized"); + const hostPath = join(tempRoot, path.replace(/^\/+/, "")); + await mkdir(dirname(hostPath), { recursive: true }); + await writeFile(hostPath, contents); +} + +async function createTestVFS(): Promise { + tempRoot = await mkdtemp(join(tmpdir(), "agentos-yq-")); + await writeFixture( + "/project/services.yaml", + [ + "services:", + " - name: api", + " enabled: true", + " port: 8080", + " - name: worker", + " enabled: false", + " port: 9090", + ].join("\n") + "\n", + ); + await writeFixture( + "/project/services.json", + JSON.stringify({ services: [{ name: "api" }, { name: "worker" }] }) + "\n", + ); + await writeFixture( + "/project/config.toml", + ["[server]", 'name = "agentos"', "port = 7331", "enabled = true"].join("\n") + + "\n", + ); + await writeFixture( + "/project/inventory.xml", + 'hammernail\n', + ); + await writeFixture("/project/broken.yaml", "services:\n - name: ok\n bad"); + return new NodeFileSystem({ root: tempRoot }); +} + +function lines(stdout: string): string[] { + return stdout.split("\n").filter((line) => line.length > 0); +} + +describeIf(hasYqPackageBinary, "yq command", { timeout: 10_000 }, () => { + let kernel: Kernel | undefined; + + afterEach(async () => { + await kernel?.dispose(); + kernel = undefined; + if (tempRoot) { + await rm(tempRoot, { recursive: true, force: true }); + tempRoot = undefined; + } + }); + + async function mountFixture(): Promise { + const vfs = await createTestVFS(); + kernel = createKernel({ filesystem: vfs }); + await kernel.mount(createWasmVmRuntime({ commandDirs: [YQ_COMMAND_DIR] })); + } + + async function runYq(args: string[]) { + if (!kernel) throw new Error("kernel not mounted"); + let stdout = ""; + let stderr = ""; + const proc = kernel.spawn("yq", args, { + onStdout: (chunk) => { + stdout += Buffer.from(chunk).toString("utf8"); + }, + onStderr: (chunk) => { + stderr += Buffer.from(chunk).toString("utf8"); + }, + }); + const exitCode = await proc.wait(); + await new Promise((resolve) => setTimeout(resolve, 0)); + return { stdout, stderr, exitCode }; + } + + it("filters YAML files and emits raw strings", async () => { + await mountFixture(); + + const result = await runYq([ + "-r", + ".services[] | select(.enabled) | .name", + "/project/services.yaml", + ]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(lines(result.stdout)).toEqual(["api"]); + }); + + it("converts YAML query results to compact JSON", async () => { + await mountFixture(); + + const result = await runYq([ + "-o", + "json", + "-c", + "{names: [.services[].name], ports: [.services[].port]}", + "/project/services.yaml", + ]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(JSON.parse(result.stdout.trim())).toEqual({ + names: ["api", "worker"], + ports: [8080, 9090], + }); + }); + + it("reads JSON files explicitly", async () => { + await mountFixture(); + + const result = await runYq([ + "-p", + "json", + "-r", + ".services[].name", + "/project/services.json", + ]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(lines(result.stdout)).toEqual(["api", "worker"]); + }); + + it("reads TOML files explicitly", async () => { + await mountFixture(); + + const result = await runYq([ + "-p", + "toml", + "-o", + "json", + "-c", + ".server", + "/project/config.toml", + ]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(JSON.parse(result.stdout.trim())).toEqual({ + name: "agentos", + port: 7331, + enabled: true, + }); + }); + + it("reads XML files explicitly", async () => { + await mountFixture(); + + const result = await runYq([ + "-p", + "xml", + "-r", + '.inventory.item[]["#text"]', + "/project/inventory.xml", + ]); + expect(result.exitCode, result.stderr || result.stdout).toBe(0); + expect(lines(result.stdout)).toEqual(["hammer", "nail"]); + }); + + it("fails with a parse error for invalid YAML", async () => { + await mountFixture(); + + const result = await runYq([".", "/project/broken.yaml"]); + expect(result.exitCode).not.toBe(0); + expect(result.stderr).toContain("invalid YAML"); + }); +}); diff --git a/software/yq/tsconfig.json b/software/yq/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/yq/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/software/zip/agentos-package.json b/software/zip/agentos-package.json new file mode 100644 index 0000000000..b2f8cb34ab --- /dev/null +++ b/software/zip/agentos-package.json @@ -0,0 +1,11 @@ +{ + "commands": [ + "zip" + ], + "registry": { + "title": "zip", + "description": "Create zip archives.", + "priority": 12, + "category": "core" + } +} diff --git a/software/zip/package.json b/software/zip/package.json new file mode 100644 index 0000000000..c638954a3c --- /dev/null +++ b/software/zip/package.json @@ -0,0 +1,33 @@ +{ + "name": "@agentos-software/zip", + "version": "0.3.3", + "type": "module", + "license": "Apache-2.0", + "description": "zip archive creation for secure-exec VMs", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist", + "!dist/package", + "!dist/package.tar" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "scripts": { + "build": "agentos-toolchain stage --commands-dir ../../toolchain/target/wasm32-wasip1/release/commands --if-missing skip && tsc && agentos-toolchain build", + "check-types": "tsc --noEmit", + "test": "vitest run test/ --passWithNoTests" + }, + "devDependencies": { + "@agentos-software/manifest": "workspace:*", + "@rivet-dev/agentos-toolchain": "workspace:*", + "@types/node": "^22.10.2", + "typescript": "^5.9.2", + "@agentos/test-harness": "workspace:*", + "vitest": "^2.1.9" + } +} diff --git a/registry/software/zip/src/index.ts b/software/zip/src/index.ts similarity index 100% rename from registry/software/zip/src/index.ts rename to software/zip/src/index.ts diff --git a/software/zip/test/zip.test.ts b/software/zip/test/zip.test.ts new file mode 100644 index 0000000000..0318e5b088 --- /dev/null +++ b/software/zip/test/zip.test.ts @@ -0,0 +1,56 @@ +/** + * Package-owned integration tests for the zip command. + */ + +import { afterEach, expect, it } from "vitest"; +import { + C_BUILD_DIR, + COMMANDS_DIR, + createInMemoryFileSystem, + createKernel, + createWasmVmRuntime, + describeIf, + hasCWasmBinaries, + type Kernel, +} from "@agentos/test-harness"; + +describeIf(hasCWasmBinaries("zip"), "zip command", { timeout: 10_000 }, () => { + let kernel: Kernel; + + afterEach(async () => { + await kernel?.dispose(); + }); + + it("creates a zip archive for a single file", async () => { + const vfs = createInMemoryFileSystem(); + await vfs.writeFile("/hello.txt", "Hello, World!\n"); + + kernel = createKernel({ filesystem: vfs }); + await kernel.mount( + createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), + ); + + const result = await kernel.exec("zip /archive.zip /hello.txt"); + expect(result.exitCode, result.stderr).toBe(0); + + const archive = await vfs.readFile("/archive.zip"); + expect(archive.length).toBeGreaterThan(0); + expect(Array.from(archive.slice(0, 2))).toEqual([0x50, 0x4b]); + }); + + it("creates a zip archive recursively", async () => { + const vfs = createInMemoryFileSystem(); + await vfs.mkdir("/mydir"); + await vfs.writeFile("/mydir/a.txt", "file a\n"); + await vfs.writeFile("/mydir/b.txt", "file b\n"); + + kernel = createKernel({ filesystem: vfs }); + await kernel.mount( + createWasmVmRuntime({ commandDirs: [C_BUILD_DIR, COMMANDS_DIR] }), + ); + + const result = await kernel.exec("zip -r /dir.zip /mydir"); + expect(result.exitCode, result.stderr).toBe(0); + expect(await vfs.exists("/dir.zip")).toBe(true); + }); +}); diff --git a/software/zip/tsconfig.json b/software/zip/tsconfig.json new file mode 100644 index 0000000000..03ce790ab7 --- /dev/null +++ b/software/zip/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"] +} diff --git a/test-harness/package.json b/test-harness/package.json new file mode 100644 index 0000000000..9bef79bc5a --- /dev/null +++ b/test-harness/package.json @@ -0,0 +1,21 @@ +{ + "name": "@agentos/test-harness", + "version": "0.0.0", + "private": true, + "type": "module", + "license": "Apache-2.0", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "scripts": { + "check-types": "tsc --noEmit" + }, + "dependencies": { + "@rivet-dev/agentos-vm-test-harness": "workspace:*" + }, + "devDependencies": { + "typescript": "^5.9.2" + } +} diff --git a/test-harness/src/index.ts b/test-harness/src/index.ts new file mode 100644 index 0000000000..cb139a8665 --- /dev/null +++ b/test-harness/src/index.ts @@ -0,0 +1 @@ +export * from "@rivet-dev/agentos-vm-test-harness"; diff --git a/test-harness/tsconfig.json b/test-harness/tsconfig.json new file mode 100644 index 0000000000..d47274867f --- /dev/null +++ b/test-harness/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../software/tsconfig.base.json", + "compilerOptions": { + "noEmit": true + }, + "include": ["src/**/*"] +} diff --git a/toolchain/.gitignore b/toolchain/.gitignore new file mode 100644 index 0000000000..819906a59b --- /dev/null +++ b/toolchain/.gitignore @@ -0,0 +1,6 @@ +# Reproducible codex WASI build scratch: the shallow fork clone, its cargo +# vendor tree, and its wasm32-wasip1 target/ all live here. Never snapshot it. +.codex-build/ + +# cargo vendor output for `make wasm` (the command build) and any codex vendor. +vendor/ diff --git a/toolchain/Cargo.lock b/toolchain/Cargo.lock new file mode 100644 index 0000000000..3c261424d0 --- /dev/null +++ b/toolchain/Cargo.lock @@ -0,0 +1,6880 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi-width" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219e3ce6f2611d83b51ec2098a12702112c29e57203a6b0a0929b2cddb486608" +dependencies = [ + "unicode-width 0.1.14", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse 0.2.7", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse 1.0.0", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "archery" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e0a5f99dfebb87bb342d0f53bb92c81842e100bbb915223e38349580e5441d" +dependencies = [ + "triomphe", +] + +[[package]] +name = "argmax" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0144c58b55af0133ec3963ce5e4d07aad866e3bbcfdcddbf4590dbd7ad6ff557" +dependencies = [ + "libc", + "nix 0.30.1", + "once_cell", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "awk-rs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6041a9be45690d0003e8d2ea488cb0a578104e591944398d96d9b7d9a0fbf9" +dependencies = [ + "regex", + "thiserror 1.0.69", +] + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link 0.2.1", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "bigdecimal" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6867f1565b3aad85681f1015055b087fcfd840d6aeee6eee7f2da317603695" +dependencies = [ + "autocfg", + "libm", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "binary-heap-plus" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4551d8382e911ecc0d0f0ffb602777988669be09447d536ff4388d1def11296" +dependencies = [ + "compare", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bon" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47dbe92550676ee653353c310dfb9cf6ba17ee70396e1f7cf0a2020ad49b2fe" +dependencies = [ + "bon-macros", + "rustversion", +] + +[[package]] +name = "bon-macros" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "519bd3116aeeb42d5372c29d982d16d0170d3d4a5ed85fc7dd91642ffff3c67c" +dependencies = [ + "darling 0.23.0", + "ident_case", + "prettyplease", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "brush-builtins" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "454219f460ee4308ae5b8befc47936b4ef7ac3a94347052270e59b6c6cfe14ec" +dependencies = [ + "brush-core", + "brush-parser", + "cfg-if", + "chrono", + "clap", + "fancy-regex 0.16.2", + "futures", + "itertools 0.14.0", + "nix 0.30.1", + "procfs", + "rlimit", + "strum 0.27.2", + "strum_macros 0.27.2", + "thiserror 2.0.18", + "tokio", + "tracing", + "uucore 0.4.0", + "wasi-ext", +] + +[[package]] +name = "brush-core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a3ad2f2d4eb45ef7f11e74bc1a0816c8f3dce298072352c6b3b41a2c4bfdb74" +dependencies = [ + "async-recursion", + "async-trait", + "bon", + "brush-parser", + "cached", + "cfg-if", + "chrono", + "clap", + "command-fds", + "fancy-regex 0.16.2", + "futures", + "getrandom 0.3.4", + "homedir", + "hostname", + "indexmap", + "itertools 0.14.0", + "nix 0.30.1", + "normalize-path", + "rand 0.9.2", + "rpds", + "strum 0.27.2", + "strum_macros 0.27.2", + "terminfo", + "thiserror 2.0.18", + "tokio", + "tracing", + "uuid", + "uzers", + "whoami", +] + +[[package]] +name = "brush-interactive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d971b1d01934fa815b061451d183f6f25e248f491a0e181fa186e3d78a1ad51" +dependencies = [ + "brush-core", + "brush-parser", + "crossterm 0.29.0", + "getrandom 0.3.4", + "indexmap", + "nix 0.30.1", + "nu-ansi-term", + "reedline", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "brush-parser" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7367124d4f38fdcd65f4b815bda7caeb3de377b9cd95ffa1b23627989c93718" +dependencies = [ + "bon", + "cached", + "indenter", + "peg", + "thiserror 2.0.18", + "tracing", + "utf8-chars", +] + +[[package]] +name = "brush-shell" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0d5917155a7043b86d27d4832feea614b4e1349735a18ab65c551949bc15d5" +dependencies = [ + "brush-builtins", + "brush-core", + "brush-interactive", + "clap", + "color-print", + "const_format", + "crossterm 0.29.0", + "getrandom 0.3.4", + "git-version", + "human-panic", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cached" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "801927ee168e17809ab8901d9f01f700cd7d8d6a6527997fee44e4b0327a253c" +dependencies = [ + "ahash", + "cached_proc_macro", + "cached_proc_macro_types", + "hashbrown 0.15.5", + "once_cell", + "thiserror 2.0.18", + "web-time", +] + +[[package]] +name = "cached_proc_macro" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9225bdcf4e4a9a4c08bf16607908eb2fbf746828d5e0b5e019726dbf6571f201" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "cached_proc_macro_types" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" + +[[package]] +name = "calendrical_calculations" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7" +dependencies = [ + "core_maths", + "displaydoc", +] + +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream 1.0.0", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "cmd-arch" +version = "0.1.0" +dependencies = [ + "uu_arch", +] + +[[package]] +name = "cmd-awk" +version = "0.1.0" +dependencies = [ + "secureexec-awk", +] + +[[package]] +name = "cmd-b2sum" +version = "0.1.0" +dependencies = [ + "uu_b2sum", +] + +[[package]] +name = "cmd-base32" +version = "0.1.0" +dependencies = [ + "uu_base32", +] + +[[package]] +name = "cmd-base64" +version = "0.1.0" +dependencies = [ + "uu_base64", +] + +[[package]] +name = "cmd-basename" +version = "0.1.0" +dependencies = [ + "uu_basename", +] + +[[package]] +name = "cmd-basenc" +version = "0.1.0" +dependencies = [ + "uu_basenc", +] + +[[package]] +name = "cmd-cat" +version = "0.1.0" +dependencies = [ + "uu_cat", + "uucore 0.7.0", +] + +[[package]] +name = "cmd-chmod" +version = "0.1.0" +dependencies = [ + "uu_chmod", +] + +[[package]] +name = "cmd-cksum" +version = "0.1.0" +dependencies = [ + "uu_cksum", +] + +[[package]] +name = "cmd-codex" +version = "0.1.0" +dependencies = [ + "codex-network-proxy", + "codex-otel", + "ratatui", + "secureexec-wasi-http", + "secureexec-wasi-spawn", +] + +[[package]] +name = "cmd-codex-exec" +version = "0.1.0" +dependencies = [ + "codex-network-proxy", + "codex-otel", + "secureexec-wasi-http", +] + +[[package]] +name = "cmd-column" +version = "0.1.0" +dependencies = [ + "secureexec-column", +] + +[[package]] +name = "cmd-comm" +version = "0.1.0" +dependencies = [ + "uu_comm", +] + +[[package]] +name = "cmd-cp" +version = "0.1.0" +dependencies = [ + "uu_cp", +] + +[[package]] +name = "cmd-curl" +version = "0.1.0" +dependencies = [ + "secureexec-wasi-http", +] + +[[package]] +name = "cmd-cut" +version = "0.1.0" +dependencies = [ + "uu_cut", +] + +[[package]] +name = "cmd-date" +version = "0.1.0" +dependencies = [ + "uu_date", +] + +[[package]] +name = "cmd-dd" +version = "0.1.0" +dependencies = [ + "uu_dd", +] + +[[package]] +name = "cmd-diff" +version = "0.1.0" +dependencies = [ + "secureexec-diff", +] + +[[package]] +name = "cmd-dircolors" +version = "0.1.0" +dependencies = [ + "uu_dircolors", +] + +[[package]] +name = "cmd-dirname" +version = "0.1.0" +dependencies = [ + "uu_dirname", +] + +[[package]] +name = "cmd-du" +version = "0.1.0" +dependencies = [ + "secureexec-du", +] + +[[package]] +name = "cmd-echo" +version = "0.1.0" +dependencies = [ + "uu_echo", +] + +[[package]] +name = "cmd-egrep" +version = "0.1.0" +dependencies = [ + "secureexec-shims", +] + +[[package]] +name = "cmd-env" +version = "0.1.0" +dependencies = [ + "secureexec-shims", +] + +[[package]] +name = "cmd-expand" +version = "0.1.0" +dependencies = [ + "uu_expand", +] + +[[package]] +name = "cmd-expr" +version = "0.1.0" +dependencies = [ + "secureexec-expr", +] + +[[package]] +name = "cmd-factor" +version = "0.1.0" +dependencies = [ + "uu_factor", +] + +[[package]] +name = "cmd-false" +version = "0.1.0" +dependencies = [ + "uu_false", +] + +[[package]] +name = "cmd-fd" +version = "0.1.0" +dependencies = [ + "fd-find", +] + +[[package]] +name = "cmd-fgrep" +version = "0.1.0" +dependencies = [ + "secureexec-shims", +] + +[[package]] +name = "cmd-file" +version = "0.1.0" +dependencies = [ + "secureexec-file-cmd", +] + +[[package]] +name = "cmd-find" +version = "0.1.0" +dependencies = [ + "findutils", + "uucore 0.9.0", +] + +[[package]] +name = "cmd-fmt" +version = "0.1.0" +dependencies = [ + "uu_fmt", +] + +[[package]] +name = "cmd-fold" +version = "0.1.0" +dependencies = [ + "uu_fold", +] + +[[package]] +name = "cmd-gzip" +version = "0.1.0" +dependencies = [ + "secureexec-gzip", +] + +[[package]] +name = "cmd-head" +version = "0.1.0" +dependencies = [ + "uu_head", +] + +[[package]] +name = "cmd-http-test" +version = "0.1.0" +dependencies = [ + "secureexec-wasi-http", +] + +[[package]] +name = "cmd-join" +version = "0.1.0" +dependencies = [ + "uu_join", +] + +[[package]] +name = "cmd-jq" +version = "0.1.0" +dependencies = [ + "secureexec-jq", +] + +[[package]] +name = "cmd-kill" +version = "0.1.0" +dependencies = [ + "uu_kill", +] + +[[package]] +name = "cmd-link" +version = "0.1.0" +dependencies = [ + "uu_link", +] + +[[package]] +name = "cmd-ln" +version = "0.1.0" +dependencies = [ + "uu_ln", +] + +[[package]] +name = "cmd-logname" +version = "0.1.0" +dependencies = [ + "uu_logname", +] + +[[package]] +name = "cmd-ls" +version = "0.1.0" +dependencies = [ + "uu_ls", +] + +[[package]] +name = "cmd-md5sum" +version = "0.1.0" +dependencies = [ + "uu_md5sum", +] + +[[package]] +name = "cmd-mkdir" +version = "0.1.0" +dependencies = [ + "uu_mkdir", +] + +[[package]] +name = "cmd-mktemp" +version = "0.1.0" +dependencies = [ + "uu_mktemp", +] + +[[package]] +name = "cmd-mv" +version = "0.1.0" +dependencies = [ + "uu_mv", +] + +[[package]] +name = "cmd-nice" +version = "0.1.0" +dependencies = [ + "secureexec-shims", +] + +[[package]] +name = "cmd-nl" +version = "0.1.0" +dependencies = [ + "uu_nl", +] + +[[package]] +name = "cmd-nohup" +version = "0.1.0" +dependencies = [ + "secureexec-shims", +] + +[[package]] +name = "cmd-nproc" +version = "0.1.0" +dependencies = [ + "uu_nproc", +] + +[[package]] +name = "cmd-numfmt" +version = "0.1.0" +dependencies = [ + "uu_numfmt", +] + +[[package]] +name = "cmd-od" +version = "0.1.0" +dependencies = [ + "uu_od", +] + +[[package]] +name = "cmd-paste" +version = "0.1.0" +dependencies = [ + "uu_paste", +] + +[[package]] +name = "cmd-pathchk" +version = "0.1.0" +dependencies = [ + "uu_pathchk", +] + +[[package]] +name = "cmd-printenv" +version = "0.1.0" +dependencies = [ + "uu_printenv", +] + +[[package]] +name = "cmd-printf" +version = "0.1.0" +dependencies = [ + "uu_printf", +] + +[[package]] +name = "cmd-ptx" +version = "0.1.0" +dependencies = [ + "uu_ptx", +] + +[[package]] +name = "cmd-pwd" +version = "0.1.0" +dependencies = [ + "uu_pwd", +] + +[[package]] +name = "cmd-readlink" +version = "0.1.0" +dependencies = [ + "uu_readlink", +] + +[[package]] +name = "cmd-realpath" +version = "0.1.0" +dependencies = [ + "uu_realpath", +] + +[[package]] +name = "cmd-rev" +version = "0.1.0" +dependencies = [ + "secureexec-rev", +] + +[[package]] +name = "cmd-rg" +version = "0.1.0" +dependencies = [ + "ripgrep", +] + +[[package]] +name = "cmd-rm" +version = "0.1.0" +dependencies = [ + "uu_rm", +] + +[[package]] +name = "cmd-rmdir" +version = "0.1.0" +dependencies = [ + "uu_rmdir", +] + +[[package]] +name = "cmd-sed" +version = "0.1.0" +dependencies = [ + "sed", +] + +[[package]] +name = "cmd-seq" +version = "0.1.0" +dependencies = [ + "uu_seq", +] + +[[package]] +name = "cmd-sh" +version = "0.1.0" +dependencies = [ + "brush-shell", +] + +[[package]] +name = "cmd-sha1sum" +version = "0.1.0" +dependencies = [ + "uu_sha1sum", +] + +[[package]] +name = "cmd-sha224sum" +version = "0.1.0" +dependencies = [ + "uu_sha224sum", +] + +[[package]] +name = "cmd-sha256sum" +version = "0.1.0" +dependencies = [ + "uu_sha256sum", +] + +[[package]] +name = "cmd-sha384sum" +version = "0.1.0" +dependencies = [ + "uu_sha384sum", +] + +[[package]] +name = "cmd-sha512sum" +version = "0.1.0" +dependencies = [ + "uu_sha512sum", +] + +[[package]] +name = "cmd-shred" +version = "0.1.0" +dependencies = [ + "uu_shred", +] + +[[package]] +name = "cmd-shuf" +version = "0.1.0" +dependencies = [ + "uu_shuf", +] + +[[package]] +name = "cmd-sleep" +version = "0.1.0" +dependencies = [ + "secureexec-builtins", +] + +[[package]] +name = "cmd-sort" +version = "0.1.0" +dependencies = [ + "uu_sort", +] + +[[package]] +name = "cmd-spawn-test-host" +version = "0.1.0" +dependencies = [ + "secureexec-wasi-spawn", +] + +[[package]] +name = "cmd-split" +version = "0.1.0" +dependencies = [ + "uu_split", +] + +[[package]] +name = "cmd-stat" +version = "0.1.0" +dependencies = [ + "uu_stat", +] + +[[package]] +name = "cmd-stdbuf" +version = "0.1.0" +dependencies = [ + "secureexec-shims", +] + +[[package]] +name = "cmd-strings" +version = "0.1.0" +dependencies = [ + "secureexec-strings-cmd", +] + +[[package]] +name = "cmd-stubs" +version = "0.1.0" +dependencies = [ + "secureexec-stubs", +] + +[[package]] +name = "cmd-sum" +version = "0.1.0" +dependencies = [ + "uu_sum", +] + +[[package]] +name = "cmd-tac" +version = "0.1.0" +dependencies = [ + "uu_tac", +] + +[[package]] +name = "cmd-tail" +version = "0.1.0" +dependencies = [ + "uu_tail", +] + +[[package]] +name = "cmd-tar" +version = "0.1.0" +dependencies = [ + "secureexec-tar", +] + +[[package]] +name = "cmd-tee" +version = "0.1.0" +dependencies = [ + "uu_tee", +] + +[[package]] +name = "cmd-test" +version = "0.1.0" +dependencies = [ + "secureexec-builtins", +] + +[[package]] +name = "cmd-timeout" +version = "0.1.0" +dependencies = [ + "secureexec-shims", +] + +[[package]] +name = "cmd-touch" +version = "0.1.0" +dependencies = [ + "uu_touch", +] + +[[package]] +name = "cmd-tr" +version = "0.1.0" +dependencies = [ + "uu_tr", +] + +[[package]] +name = "cmd-true" +version = "0.1.0" +dependencies = [ + "uu_true", +] + +[[package]] +name = "cmd-truncate" +version = "0.1.0" +dependencies = [ + "uu_truncate", +] + +[[package]] +name = "cmd-tsort" +version = "0.1.0" +dependencies = [ + "uu_tsort", +] + +[[package]] +name = "cmd-uname" +version = "0.1.0" +dependencies = [ + "uu_uname", +] + +[[package]] +name = "cmd-unexpand" +version = "0.1.0" +dependencies = [ + "uu_unexpand", +] + +[[package]] +name = "cmd-uniq" +version = "0.1.0" +dependencies = [ + "uu_uniq", +] + +[[package]] +name = "cmd-unlink" +version = "0.1.0" +dependencies = [ + "uu_unlink", +] + +[[package]] +name = "cmd-wc" +version = "0.1.0" +dependencies = [ + "uu_wc", +] + +[[package]] +name = "cmd-which" +version = "0.1.0" +dependencies = [ + "secureexec-shims", +] + +[[package]] +name = "cmd-whoami" +version = "0.1.0" +dependencies = [ + "secureexec-builtins", +] + +[[package]] +name = "cmd-xargs" +version = "0.1.0" +dependencies = [ + "findutils", +] + +[[package]] +name = "cmd-xu" +version = "0.1.0" + +[[package]] +name = "cmd-yes" +version = "0.1.0" +dependencies = [ + "uu_yes", +] + +[[package]] +name = "cmd-yq" +version = "0.1.0" +dependencies = [ + "secureexec-yq", +] + +[[package]] +name = "codex-network-proxy" +version = "0.0.0" + +[[package]] +name = "codex-otel" +version = "0.0.0" + +[[package]] +name = "color-print" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aa954171903797d5623e047d9ab69d91b493657917bdfb8c2c80ecaf9cdb6f4" +dependencies = [ + "color-print-proc-macro", +] + +[[package]] +name = "color-print-proc-macro" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692186b5ebe54007e45a59aea47ece9eb4108e141326c304cdc91699a7118a22" +dependencies = [ + "nom 7.1.3", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "command-fds" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f849b92c694fe237ecd8fafd1ba0df7ae0d45c1df6daeb7f68ed4220d51640bd" +dependencies = [ + "nix 0.30.1", + "thiserror 2.0.18", +] + +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "compare" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120133d4db2ec47efe2e26502ee984747630c67f51974fca0b6c1340cf2368d3" + +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width 0.2.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc-fast" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" +dependencies = [ + "digest", + "spin", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.11.0", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.44", + "serde", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.11.0", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.1.4", + "serde", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctrlc" +version = "3.5.2" + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "data-encoding-macro" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8142a83c17aa9461d637e649271eae18bf2edd00e91f2e105df36c3c16355bdb" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" +dependencies = [ + "data-encoding", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de48cc4d1c1d97a20fd819def54b890cadde72ed3ad0c614822a0a433361be96" +dependencies = [ + "cfg-if", + "windows-sys 0.61.2", +] + +[[package]] +name = "faccess" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ae66425802d6a903e268ae1a08b8c38ba143520f227a205edf4e9c7e3e26d5" +dependencies = [ + "bitflags 1.3.2", + "libc", + "winapi", +] + +[[package]] +name = "fancy-regex" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fancy-regex" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72cf461f865c862bb7dc573f643dd6a2b6842f7c30b07882b56bd148cc2761b8" +dependencies = [ + "bit-set", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fd-find" +version = "10.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95ed7d1f53e0446a7d47715801f6bee95f816c4aa33e25b5d89a2734ab00436" +dependencies = [ + "aho-corasick", + "anyhow", + "argmax", + "clap", + "crossbeam-channel", + "ctrlc", + "etcetera", + "faccess", + "globset", + "ignore", + "jiff", + "libc", + "lscolors", + "nix 0.31.3", + "normpath", + "nu-ansi-term", + "regex", + "regex-syntax", +] + +[[package]] +name = "fd-lock" +version = "4.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" +dependencies = [ + "cfg-if", + "rustix 1.1.4", + "windows-sys 0.59.0", +] + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "findutils" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719cc0af0060da07d610f4faa20d2d46dca46ff6bdf778a4b4feec3a7c8af7ea" +dependencies = [ + "argmax", + "chrono", + "clap", + "faccess", + "itertools 0.14.0", + "nix 0.31.3", + "onig", + "regex", + "thiserror 2.0.18", + "uucore 0.9.0", + "walkdir", +] + +[[package]] +name = "fixed_decimal" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35eabf480f94d69182677e37571d3be065822acfafd12f2f085db44fbbcc8e57" +dependencies = [ + "displaydoc", + "smallvec", + "writeable", +] + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fluent" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8137a6d5a2c50d6b0ebfcb9aaa91a28154e0a70605f112d30cb0cd4a78670477" +dependencies = [ + "fluent-bundle", + "unic-langid", +] + +[[package]] +name = "fluent-bundle" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01203cb8918f5711e73891b347816d932046f95f54207710bda99beaeb423bf4" +dependencies = [ + "fluent-langneg", + "fluent-syntax", + "intl-memoizer", + "intl_pluralrules", + "rustc-hash", + "self_cell", + "smallvec", + "unic-langid", +] + +[[package]] +name = "fluent-langneg" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "fluent-syntax" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54f0d287c53ffd184d04d8677f590f4ac5379785529e5e08b1c8083acdd5c198" +dependencies = [ + "memchr", + "thiserror 2.0.18", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gcd" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "git-version" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad568aa3db0fcbc81f2f116137f263d7304f512a1209b35b85150d3ef88ad19" +dependencies = [ + "git-version-macro", +] + +[[package]] +name = "git-version-macro" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "globset" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +dependencies = [ + "aho-corasick", + "bstr", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "grep" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "309217bc53e2c691c314389c7fa91f9cd1a998cda19e25544ea47d94103880c3" +dependencies = [ + "grep-cli", + "grep-matcher", + "grep-printer", + "grep-regex", + "grep-searcher", +] + +[[package]] +name = "grep-cli" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf32d263c5d5cc2a23ce587097f5ddafdb188492ba2e6fb638eaccdc22453631" +dependencies = [ + "bstr", + "globset", + "libc", + "log", + "termcolor", + "winapi-util", +] + +[[package]] +name = "grep-matcher" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36d7b71093325ab22d780b40d7df3066ae4aebb518ba719d38c697a8228a8023" +dependencies = [ + "memchr", +] + +[[package]] +name = "grep-printer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd76035e87871f51c1ee5b793e32122b3ccf9c692662d9622ef1686ff5321acb" +dependencies = [ + "bstr", + "grep-matcher", + "grep-searcher", + "log", + "serde", + "serde_json", + "termcolor", +] + +[[package]] +name = "grep-regex" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce0c256c3ad82bcc07b812c15a45ec1d398122e8e15124f96695234db7112ef" +dependencies = [ + "bstr", + "grep-matcher", + "log", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "grep-searcher" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac63295322dc48ebb20a25348147905d816318888e64f531bfc2a2bc0577dc34" +dependencies = [ + "bstr", + "encoding_rs", + "encoding_rs_io", + "grep-matcher", + "log", + "memchr", + "memmap2", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hifijson" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a7763b98ba8a24f59e698bf9ab197e7676c640d6455d1580b4ce7dc560f0f0d" + +[[package]] +name = "homedir" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bdbbd5bc8c5749697ccaa352fa45aff8730cf21c68029c0eef1ffed7c3d6ba2" +dependencies = [ + "cfg-if", + "nix 0.29.0", + "widestring", + "windows 0.57.0", +] + +[[package]] +name = "hostname" +version = "0.4.2" + +[[package]] +name = "human-panic" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075e8747af11abcff07d55d98297c9c6c70eb5d6365b25e7b12f02e484935191" +dependencies = [ + "anstream 0.6.21", + "anstyle", + "backtrace", + "serde", + "serde_derive", + "sysinfo", + "toml 0.9.12+spec-1.1.0", + "uuid", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_calendar" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e" +dependencies = [ + "calendrical_calculations", + "displaydoc", + "icu_calendar_data", + "icu_locale", + "icu_locale_core", + "icu_provider", + "ixdtf", + "serde", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_calendar_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d" + +[[package]] +name = "icu_collator" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32eed11a5572f1088b63fa21dc2e70d4a865e5739fc2d10abc05be93bae97019" +dependencies = [ + "icu_collator_data", + "icu_collections", + "icu_locale", + "icu_locale_core", + "icu_normalizer", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "zerovec", +] + +[[package]] +name = "icu_collator_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab06f0e83a613efddba3e4913e00e43ed4001fae651cb7d40fc7e66b83b6fb9" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_datetime" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9d49f41ded8e63761b6b4c3120dfdc289415a1ed10107db6198eb311057ca5" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_calendar", + "icu_datetime_data", + "icu_decimal", + "icu_locale", + "icu_locale_core", + "icu_pattern", + "icu_plurals", + "icu_provider", + "icu_time", + "potential_utf", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_datetime_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46597233625417b7c8052a63d916e4fdc73df21614ac0b679492a5d6e3b01aeb" + +[[package]] +name = "icu_decimal" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38c52231bc348f9b982c1868a2af3195199623007ba2c7650f432038f5b3e8e" +dependencies = [ + "fixed_decimal", + "icu_decimal_data", + "icu_locale", + "icu_locale_core", + "icu_provider", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_decimal_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2905b4044eab2dd848fe84199f9195567b63ab3a93094711501363f63546fef7" + +[[package]] +name = "icu_locale" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "532b11722e350ab6bf916ba6eb0efe3ee54b932666afec989465f9243fe6dd60" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_locale_data", + "icu_provider", + "potential_utf", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c5f1d16b4c3a2642d3a719f18f6b06070ab0aef246a6418130c955ae08aa831" + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_pattern" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a7ff8c0ff6f61cdce299dcb54f557b0a251adbc78f6f0c35a21332c452b4a1b" +dependencies = [ + "displaydoc", + "either", + "serde", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_plurals" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9cfe49f5b1d1163cc58db451562339916a9ca5cbcaae83924d41a0bf839474" +dependencies = [ + "fixed_decimal", + "icu_locale", + "icu_plurals_data", + "icu_provider", + "zerovec", +] + +[[package]] +name = "icu_plurals_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f018a98dccf7f0eb02ba06ac0ff67d102d8ded80734724305e924de304e12ff0" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "serde", + "stable_deref_trait", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_time" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8242b00da3b3b6678f731437a11c8833a43c821ae081eca60ba1b7579d45b6d8" +dependencies = [ + "calendrical_calculations", + "displaydoc", + "icu_calendar", + "icu_locale_core", + "icu_provider", + "icu_time_data", + "ixdtf", + "serde", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_time_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e10b0e5e87a2c84bd5fa407705732052edebe69291d347d0c3033785470edbf" + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "ignore" +version = "0.4.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b915661dd01db3f05050265b2477bcc6527b3792388e2749b41623cc592be67d" +dependencies = [ + "crossbeam-deque", + "globset", + "log", + "memchr", + "regex-automata", + "same-file", + "walkdir", + "winapi-util", +] + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "indicatif" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb" +dependencies = [ + "console", + "portable-atomic", + "unicode-width 0.2.0", + "unit-prefix", + "web-time", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "infer" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc150e5ce2330295b8616ce0e3f53250e53af31759a9dbedad1621ba29151847" +dependencies = [ + "cfb", +] + +[[package]] +name = "inotify" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" +dependencies = [ + "bitflags 2.11.0", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "instability" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +dependencies = [ + "darling 0.23.0", + "indoc", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "intl-memoizer" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972" +dependencies = [ + "unic-langid", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "ixdtf" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992" + +[[package]] +name = "jaq-core" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77526a72eb79412c29fd141767a6549bbfcb1cb40e00556fe16532d5e878e098" +dependencies = [ + "dyn-clone", + "once_cell", + "typed-arena", +] + +[[package]] +name = "jaq-json" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01dbdbd07b076e8403abac68ce7744d93e2ecd953bbc44bf77bf00e1e81172bc" +dependencies = [ + "foldhash 0.1.5", + "hifijson", + "indexmap", + "jaq-core", + "jaq-std", + "serde_json", +] + +[[package]] +name = "jaq-std" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c264fe397c981705976c71f1bfe020382b9eda52ae950e57fe885e147bdd67d" +dependencies = [ + "aho-corasick", + "base64", + "chrono", + "jaq-core", + "libm", + "log", + "regex-lite", + "urlencoding", +] + +[[package]] +name = "jiff" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +dependencies = [ + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-sys 0.61.2", +] + +[[package]] +name = "jiff-icu" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e67c2beaae8b10a82d849b9aabb698a43a682f32b17bcdc035d5ecadb44d646" +dependencies = [ + "icu_calendar", + "icu_time", + "jiff", +] + +[[package]] +name = "jiff-static" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lexopt" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "803ec87c9cfb29b9d2633f20cba1f488db3fd53f2158b1024cbefb47ba05d413" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "bitflags 2.11.0", + "libc", + "plain", + "redox_syscall 0.7.3", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "lru" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "lscolors" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d60e266dfb1426eb2d24792602e041131fdc0236bb7007abc0e589acafd60929" +dependencies = [ + "aho-corasick", + "nu-ansi-term", +] + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "normalize-path" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5438dd2b2ff4c6df6e1ce22d825ed2fa93ee2922235cc45186991717f0a892d" + +[[package]] +name = "normpath" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9985ef7269fa99f3b12437bb698381da2428743ab90f20393f399fa14cab21a" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.11.0", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-prime" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b285c575532a33ef6fdd3a57640d0b1c70e6ca48644d6df7bbd4b7a0cfbbb12d" +dependencies = [ + "bitvec", + "either", + "lru 0.16.3", + "num-bigint", + "num-integer", + "num-modular", + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "objc2-io-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" +dependencies = [ + "libc", + "objc2-core-foundation", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "onig" +version = "6.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc3cbf698f9438986c11a880c90a6d04b9de27575afd28bbf45b154b6c709e2" +dependencies = [ + "bitflags 2.11.0", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e68317604e77e53b85896388e1a803c1d21b74c899ec9e5e1112db90735edd7" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "os_display" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5fd71b79026fb918650dde6d125000a233764f1c2f1659a1c71118e33ea08f" +dependencies = [ + "unicode-width 0.2.0", +] + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "parse_datetime" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413775a7eac2261d2211a79d10ef275e5b6f7b527eec42ad09adce2ffa92b6e5" +dependencies = [ + "jiff", + "num-traits", + "winnow", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "peg" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9928cfca101b36ec5163e70049ee5368a8a1c3c6efc9ca9c5f9cc2f816152477" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6298ab04c202fa5b5d52ba03269fb7b74550b150323038878fe6c372d8280f71" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "132dca9b868d927b35b5dd728167b2dee150eb1ad686008fc71ccb298b776fca" + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "platform-info" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7539aeb3fdd8cb4f6a331307cf71a1039cee75e94e8a71725b9484f4a0d9451a" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "serde_core", + "writeable", + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "procfs" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7" +dependencies = [ + "bitflags 2.11.0", + "chrono", + "flate2", + "procfs-core", + "rustix 1.1.4", +] + +[[package]] +name = "procfs-core" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405" +dependencies = [ + "bitflags 2.11.0", + "chrono", + "hex", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags 2.11.0", + "cassowary", + "compact_str", + "crossterm 0.28.1", + "indoc", + "instability", + "itertools 0.13.0", + "lru 0.12.5", + "paste", + "strum 0.26.3", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "reedline" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fcf9f5225169f49622af4027b6d775cf9b6313f9eba16ba9e5ebd9d319a1f0" +dependencies = [ + "chrono", + "crossterm 0.28.1", + "fd-lock", + "itertools 0.13.0", + "nu-ansi-term", + "serde", + "strip-ansi-escapes", + "strum 0.26.3", + "strum_macros 0.26.4", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-width 0.2.0", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "ripgrep" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f388c4955f85477c28a8667355819844a06614b083c23517f0e86bd1d6d82b73" +dependencies = [ + "anyhow", + "bstr", + "grep", + "ignore", + "lexopt", + "log", + "serde_json", + "termcolor", + "textwrap", + "tikv-jemallocator", +] + +[[package]] +name = "rlimit" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7043b63bd0cd1aaa628e476b80e6d4023a3b50eb32789f2728908107bd0c793a" +dependencies = [ + "libc", +] + +[[package]] +name = "rpds" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e75f485e819d4d3015e6c0d55d02a4fd3db47c1993d9e603e0361fba2bffb34" +dependencies = [ + "archery", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secureexec-awk" +version = "0.1.0" +dependencies = [ + "awk-rs", +] + +[[package]] +name = "secureexec-builtins" +version = "0.1.0" +dependencies = [ + "libc", + "wasi-ext", +] + +[[package]] +name = "secureexec-column" +version = "0.1.0" + +[[package]] +name = "secureexec-diff" +version = "0.1.0" +dependencies = [ + "similar", +] + +[[package]] +name = "secureexec-du" +version = "0.1.0" + +[[package]] +name = "secureexec-expr" +version = "0.1.0" +dependencies = [ + "regex", +] + +[[package]] +name = "secureexec-file-cmd" +version = "0.1.0" +dependencies = [ + "infer", +] + +[[package]] +name = "secureexec-gzip" +version = "0.1.0" +dependencies = [ + "flate2", +] + +[[package]] +name = "secureexec-jq" +version = "0.1.0" +dependencies = [ + "jaq-core", + "jaq-json", + "jaq-std", + "serde_json", +] + +[[package]] +name = "secureexec-rev" +version = "0.1.0" + +[[package]] +name = "secureexec-shims" +version = "0.1.0" +dependencies = [ + "wasi-ext", +] + +[[package]] +name = "secureexec-strings-cmd" +version = "0.1.0" + +[[package]] +name = "secureexec-stubs" +version = "0.1.0" + +[[package]] +name = "secureexec-tar" +version = "0.1.0" +dependencies = [ + "flate2", + "tar", +] + +[[package]] +name = "secureexec-wasi-http" +version = "0.1.0" +dependencies = [ + "wasi-ext", +] + +[[package]] +name = "secureexec-wasi-pty" +version = "0.1.0" +dependencies = [ + "secureexec-wasi-spawn", + "wasi-ext", +] + +[[package]] +name = "secureexec-wasi-spawn" +version = "0.1.0" +dependencies = [ + "wasi", + "wasi-ext", +] + +[[package]] +name = "secureexec-yq" +version = "0.1.0" +dependencies = [ + "jaq-core", + "jaq-json", + "jaq-std", + "quick-xml", + "serde_json", + "serde_yaml", + "toml 0.8.23", +] + +[[package]] +name = "sed" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5a00cdbffacbccf02decb7577da5e298c091bdee492a2a1b87a3d8b0cc5b7c5" +dependencies = [ + "clap", + "fancy-regex 0.17.0", + "memchr", + "memmap2", + "once_cell", + "phf 0.13.1", + "phf_codegen 0.13.1", + "regex", + "tempfile", + "uucore 0.7.0", +] + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "sm3" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebb9a3b702d0a7e33bc4d85a14456633d2b165c2ad839c5fd9a8417c1ab15860" +dependencies = [ + "digest", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "string-interner" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23de088478b31c349c9ba67816fa55d9355232d63c3afea8bf513e31f0f1d2c0" +dependencies = [ + "hashbrown 0.15.5", + "serde", +] + +[[package]] +name = "strip-ansi-escapes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a8f8038e7e7969abb3f1b7c2a811225e9296da208539e0f79c5251d6cac0025" +dependencies = [ + "vte", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sysinfo" +version = "0.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" +dependencies = [ + "libc", + "memchr", + "ntapi", + "objc2-core-foundation", + "objc2-io-kit", + "windows 0.61.3", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix 1.1.4", + "windows-sys 0.60.2", +] + +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom 7.1.3", + "phf 0.11.3", + "phf_codegen 0.11.3", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tikv-jemalloc-sys" +version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd8aa5b2ab86a2cefa406d889139c162cbb230092f7d1d7cbc1716405d852a3b" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "tikv-jemallocator" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0359b4327f954e0567e69fb191cf1436617748813819c94b8cd4a431422d053a" +dependencies = [ + "libc", + "tikv-jemalloc-sys", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_writer", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "triomphe" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39" + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash", +] + +[[package]] +name = "typed-arena" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unic-langid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05" +dependencies = [ + "unic-langid-impl", +] + +[[package]] +name = "unic-langid-impl" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658" +dependencies = [ + "tinystr", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.1.14", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8-chars" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe49e006d6df172d7f14794568a90fe41e05a1fa9e03dc276fa6da4bb747ec3" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uu_arch" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f789358d2df8eb5933daa0d6ae53b79334269360d33cdd7079032d554a7e982c" +dependencies = [ + "clap", + "fluent", + "platform-info", + "uucore 0.7.0", +] + +[[package]] +name = "uu_b2sum" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "272b257659f523df2788d94866f95ea196188975a8b267a9ca0d566292a3a772" +dependencies = [ + "clap", + "fluent", + "uu_checksum_common", + "uucore 0.7.0", +] + +[[package]] +name = "uu_base32" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db368d94af37dcd4d9b91d0f57f8ad2088af770be12b0114829b230762c4274" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_base64" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4eef6f62da9204fd7be919be88ef0c8271e22483415bf284a5efc0f173366" +dependencies = [ + "clap", + "fluent", + "uu_base32", + "uucore 0.7.0", +] + +[[package]] +name = "uu_basename" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdedbc10b8ef9c97415bfa80b1f25f3775420080dd59341bd39505fbbe797c48" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_basenc" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06a5c56ce70e8783820401ed2ec4468a2a235f58fb2d90f5c163f14e7a94eacc" +dependencies = [ + "clap", + "fluent", + "uu_base32", + "uucore 0.7.0", +] + +[[package]] +name = "uu_cat" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043b7b9873244298be72a26057d1d85a0129497817c304ff7d1a2a1a0b27b335" +dependencies = [ + "clap", + "fluent", + "memchr", + "nix 0.30.1", + "thiserror 2.0.18", + "uucore 0.7.0", + "winapi-util", + "windows-sys 0.61.2", +] + +[[package]] +name = "uu_checksum_common" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "485e68c04134bfda144c9c79073e190586a105713b45971692be83775a9ef5e8" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_chmod" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3a5d93a175bf8993f2e0437228e7b736658f10d642dd7476397259cc6ed6af3" +dependencies = [ + "clap", + "fluent", + "thiserror 2.0.18", + "uucore 0.7.0", +] + +[[package]] +name = "uu_cksum" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ecef1133f3970c22aad791c34d6fd70d24f10d25082ff8fa4493d4158ae1a6" +dependencies = [ + "clap", + "fluent", + "uu_checksum_common", + "uucore 0.7.0", +] + +[[package]] +name = "uu_comm" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf70eb4d7bc82aa7ce6be80087e85d9dfa2d431e1cad2ba3080dd362d2bf717" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_cp" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd199017f3fcfc2d266fb9d76385d510665393c400163b7c9cfd6f622e313b6" +dependencies = [ + "clap", + "filetime", + "fluent", + "indicatif", + "libc", + "nix 0.30.1", + "thiserror 2.0.18", + "uucore 0.7.0", + "walkdir", +] + +[[package]] +name = "uu_cut" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba96ac3631f81c4beef269d8eaba79b9bf2f72d97bdbd6ddadbf0e787327c63a" +dependencies = [ + "bstr", + "clap", + "fluent", + "memchr", + "uucore 0.7.0", +] + +[[package]] +name = "uu_date" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f41472a2321637ae96a126708210fc4c3603b1684e810af244df4a736f86515" +dependencies = [ + "clap", + "fluent", + "icu_calendar", + "icu_locale", + "jiff", + "jiff-icu", + "nix 0.30.1", + "parse_datetime", + "regex", + "uucore 0.7.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "uu_dd" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e14a3ee96607b62ca4d65807be7ec1c2b5782b36d22fe7998307a0d08b85d94" +dependencies = [ + "clap", + "fluent", + "gcd", + "libc", + "nix 0.30.1", + "thiserror 2.0.18", + "uucore 0.7.0", +] + +[[package]] +name = "uu_dircolors" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391283cfef2dd209cffd699ba5ea7bd84cdba817105acd66bb020262ca59660a" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_dirname" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8954430cc6fc6d426905eea23c4aa13af3eb5260311ff712d698e208546b831c" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_echo" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77f4d0beb31487fe8f4b9202ffc118b9561dda1341e3dd6d3bb52991bfbd58d" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_expand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca8a49c55b0396ae0a5326704130e9706fc09acd6180d795bff1154b60db708" +dependencies = [ + "clap", + "fluent", + "thiserror 2.0.18", + "unicode-width 0.2.0", + "uucore 0.7.0", +] + +[[package]] +name = "uu_factor" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520da1c9c94bb354ac1ee753eb8fb4f81d0512ec8c2405d21a6ff6f07c2fe1b3" +dependencies = [ + "clap", + "fluent", + "num-bigint", + "num-prime", + "num-traits", + "uucore 0.7.0", +] + +[[package]] +name = "uu_false" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a963cd94c29de3678273df72ca60857f6ffe6af5a3ede672cb4bc79a11ef22" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_fmt" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e133acad1273c60086d190ac76c4caaf6f694b4e2b85276124307d9e639b442c" +dependencies = [ + "clap", + "fluent", + "thiserror 2.0.18", + "unicode-width 0.2.0", + "uucore 0.7.0", +] + +[[package]] +name = "uu_fold" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8098e158f38287fe1144c3e7337856c6ac6e6f45c9924e75592d45e78dc467e6" +dependencies = [ + "clap", + "fluent", + "unicode-width 0.2.0", + "uucore 0.7.0", +] + +[[package]] +name = "uu_head" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d886e39ddc1a462479b0cd6fcdc7b1b2d2d086db53d65149c1cfe69b74b6baa" +dependencies = [ + "clap", + "fluent", + "memchr", + "thiserror 2.0.18", + "uucore 0.7.0", +] + +[[package]] +name = "uu_join" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ae9afe4a6a602d1fb3752f63a55bea0957403b0fc3c244184ba6f9c1a65932" +dependencies = [ + "clap", + "fluent", + "memchr", + "thiserror 2.0.18", + "uucore 0.7.0", +] + +[[package]] +name = "uu_kill" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0b4beed0ffa73d1433cc678eb49bce51a69f5576b21f7945af0c8b3f2fa3da5" +dependencies = [ + "clap", + "fluent", + "nix 0.30.1", + "uucore 0.7.0", + "wasi-ext", +] + +[[package]] +name = "uu_link" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a7c36d0be50e550760b5d6d10a4735d8f5ca94fc8a32f5e0ca33af3fbc7e3a" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_ln" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99cb878e1b6b498aeb55704caa2f764ac0dd61347fd3efc6eff0bda6bf0b9536" +dependencies = [ + "clap", + "fluent", + "thiserror 2.0.18", + "uucore 0.7.0", +] + +[[package]] +name = "uu_logname" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be8dd72043a0b5032741234a0854bf1ac01b8f7a47d7a20cae6e9649d803d408" +dependencies = [ + "clap", + "fluent", + "libc", + "uucore 0.7.0", +] + +[[package]] +name = "uu_ls" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dc54d6f54ed818aca41490fa92eaf094b5e6ff613257245f8d9c6fe1b0a2224" +dependencies = [ + "ansi-width", + "clap", + "fluent", + "glob", + "hostname", + "lscolors", + "rustc-hash", + "terminal_size", + "thiserror 2.0.18", + "uucore 0.7.0", + "uutils_term_grid", +] + +[[package]] +name = "uu_md5sum" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8003075b73ee23011de0618d2f84da765c37bd0be34e301aeb70dd7f0996bd" +dependencies = [ + "clap", + "fluent", + "uu_checksum_common", + "uucore 0.7.0", +] + +[[package]] +name = "uu_mkdir" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8cdba0fed9c1c12d0a0c954e447cc75a50b466b001f1a814921ee702fafff7a" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_mktemp" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b7af3550fa4c926d8fde66932421d904cbe1102a7031947b456ad0f90f81569" +dependencies = [ + "clap", + "fluent", + "rand 0.9.2", + "tempfile", + "thiserror 2.0.18", + "uucore 0.7.0", +] + +[[package]] +name = "uu_mv" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678a9ce279116c4d1aa3dc460be904069336adc63d9130acdb543568bee22a6" +dependencies = [ + "clap", + "fluent", + "fs_extra", + "indicatif", + "libc", + "rustc-hash", + "thiserror 2.0.18", + "uucore 0.7.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "uu_nl" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "823a2102789b4fc5227ae76c15f8b57df0534a9aff1ac19bf1961303bf663d48" +dependencies = [ + "clap", + "fluent", + "itoa", + "regex", + "uucore 0.7.0", +] + +[[package]] +name = "uu_nproc" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cceeca12036b18857d4c773373419185ffa057b32b0c87a447a1861a06c5ddc3" +dependencies = [ + "clap", + "fluent", + "libc", + "uucore 0.7.0", +] + +[[package]] +name = "uu_numfmt" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac350590aecae7aa234270b48380013188786969fe0c17824b542d3ce904baf" +dependencies = [ + "clap", + "fluent", + "memchr", + "thiserror 2.0.18", + "uucore 0.7.0", +] + +[[package]] +name = "uu_od" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b9c0c5345041b6734f89e7642995f479c1740c7c91075f0602e042ec55b268" +dependencies = [ + "byteorder", + "clap", + "fluent", + "half", + "libc", + "uucore 0.7.0", +] + +[[package]] +name = "uu_paste" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc4e66bb50e347e68b76aa3703064ba1953af2ca21a67bcee15d5a6e1f9aa4c" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_pathchk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "013e2313eafe2d854e271c00b2a7822042e03f8d52493dd7ea70260c1eca37c4" +dependencies = [ + "clap", + "fluent", + "libc", + "uucore 0.7.0", +] + +[[package]] +name = "uu_printenv" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c733e8c3c4ff851d86733b2c79968a9b30ae1b2453d904b357d0a0588f4c5c68" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_printf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079ccf2689495f99ffd0aafdcaf81753d73b710fbd80884487f71715635bf53" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_ptx" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "268dc2ca3624228b9929fa60c843d54affa9024586e7f553a1484d029f6e9d99" +dependencies = [ + "clap", + "fluent", + "regex", + "thiserror 2.0.18", + "uucore 0.7.0", +] + +[[package]] +name = "uu_pwd" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b6e26cf64171f8825fccbade68bf03de15924955cff36c5d9a2574a6ceecd3" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_readlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695c0287310f05d6dfedae9a0f62139742ad19b3f6d57e2d21ab93fe56ca374d" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_realpath" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65bb98033c204f1b6a985564b4d58eaa35b0e5855c996642823c21b6f1448c9" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_rm" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb65631fc1a8e8834bd30ac4ad8ee337c6d5a46c15254dfdbdfaea96f7290fa1" +dependencies = [ + "clap", + "fluent", + "indicatif", + "libc", + "thiserror 2.0.18", + "uucore 0.7.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "uu_rmdir" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7462a7b03e28881b5c8129dc576524fe42fcb7e8de9a30e16bf90f25b64a095f" +dependencies = [ + "clap", + "fluent", + "libc", + "uucore 0.7.0", +] + +[[package]] +name = "uu_seq" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "689227681b5be62ece851a5b3d28b8d175feb18624a4b24d1bc4c4ce9c760ea4" +dependencies = [ + "bigdecimal", + "clap", + "fluent", + "num-bigint", + "num-traits", + "thiserror 2.0.18", + "uucore 0.7.0", +] + +[[package]] +name = "uu_sha1sum" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "426f471bcdf58779e0125a5231421330982533d354ac13291677a8a3a5d9b765" +dependencies = [ + "clap", + "fluent", + "uu_checksum_common", + "uucore 0.7.0", +] + +[[package]] +name = "uu_sha224sum" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b353936ed3b67b8654169161a2bc84ba29c72bedbd95dc504a23f69016fa9ea1" +dependencies = [ + "clap", + "fluent", + "uu_checksum_common", + "uucore 0.7.0", +] + +[[package]] +name = "uu_sha256sum" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071ec05bd12da342a70e1d094b5b210453e5e028041058722db6769d06d5719a" +dependencies = [ + "clap", + "fluent", + "uu_checksum_common", + "uucore 0.7.0", +] + +[[package]] +name = "uu_sha384sum" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfc23178584a12c7cd7266c77c9a187ebfff4fd761738d9c838203bb17020f1f" +dependencies = [ + "clap", + "fluent", + "uu_checksum_common", + "uucore 0.7.0", +] + +[[package]] +name = "uu_sha512sum" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c7719ee5dfdd58be739b87add1dfe8026114303b83b3ae24fb099824ae5fda" +dependencies = [ + "clap", + "fluent", + "uu_checksum_common", + "uucore 0.7.0", +] + +[[package]] +name = "uu_shred" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33090888bfc315a756f028ba16251ed34dca810e708435eae54a5064e3d9f3fb" +dependencies = [ + "clap", + "fluent", + "libc", + "rand 0.9.2", + "uucore 0.7.0", +] + +[[package]] +name = "uu_shuf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c2e5663694fde672961884c94c8afd3cd5859969878594eda6a25059621cd97" +dependencies = [ + "clap", + "fluent", + "itoa", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rustc-hash", + "sha3", + "uucore 0.7.0", +] + +[[package]] +name = "uu_sort" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb61d4619d85c291b9fa5c4cf057c8da764efe0541f4ff888c8f2e603294a965" +dependencies = [ + "bigdecimal", + "binary-heap-plus", + "clap", + "compare", + "ctrlc", + "fluent", + "foldhash 0.2.0", + "itertools 0.14.0", + "memchr", + "nix 0.30.1", + "rand 0.9.2", + "rayon", + "self_cell", + "tempfile", + "thiserror 2.0.18", + "uucore 0.7.0", +] + +[[package]] +name = "uu_split" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7514e1430433a581b7317685a90858d34fdbdc726d778ae00a0a9541123eda76" +dependencies = [ + "clap", + "fluent", + "memchr", + "thiserror 2.0.18", + "uucore 0.7.0", +] + +[[package]] +name = "uu_stat" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef4ee0943facf79c267e2ff7fe8869f15b76d9ce4709d2c8117db788bc851c6" +dependencies = [ + "clap", + "fluent", + "thiserror 2.0.18", + "uucore 0.7.0", +] + +[[package]] +name = "uu_sum" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31a443c458befede734a5e1cd76768bef727d568d4290c651a82c6fc8217f201" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_tac" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0075181c425cfdf7b2e3400cdfa90d419aa9f4723163210b94d85e2ea47d25" +dependencies = [ + "clap", + "fluent", + "libc", + "memchr", + "memmap2", + "regex", + "tempfile", + "thiserror 2.0.18", + "uucore 0.7.0", +] + +[[package]] +name = "uu_tail" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88cb366f24aecb51aa9b03c2ae8b40538aa5081351822a6dfec883f95cc744d3" +dependencies = [ + "clap", + "fluent", + "libc", + "memchr", + "nix 0.30.1", + "notify", + "same-file", + "uucore 0.7.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "uu_tee" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a8d33dd843a8e5fb37018390edf904678767a995308514bd3f64fe7d01bb4f" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_touch" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f1f61215638f2b85f03008f94bdb26b5071579ee8b4ab1d0ff74c3e12e3acd" +dependencies = [ + "clap", + "filetime", + "fluent", + "jiff", + "nix 0.30.1", + "parse_datetime", + "thiserror 2.0.18", + "uucore 0.7.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "uu_tr" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3028c44f9e14787a07e96d7365f645d18ab060126515a67857150f57bb96c45" +dependencies = [ + "bytecount", + "clap", + "fluent", + "nom 8.0.0", + "uucore 0.7.0", +] + +[[package]] +name = "uu_true" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfec486c633bf98e354373f7d0ec26910e51376ddfeea29b2fd8a2d2f3d96736" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_truncate" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5790fe8bc07089c2e38e2965afa16c0beca87c6df7c655251e15a5050c980eb" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_tsort" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29fc5c3773a819f847867c5c964ddfa7fd48b372b00490f611aa0629722d7ad2" +dependencies = [ + "clap", + "fluent", + "nix 0.30.1", + "rustc-hash", + "string-interner", + "thiserror 2.0.18", + "uucore 0.7.0", +] + +[[package]] +name = "uu_uname" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8650f019d2addbb56c316bd53058d5a0165e573e2ae46636df9f36e1719b3280" +dependencies = [ + "clap", + "fluent", + "platform-info", + "uucore 0.7.0", +] + +[[package]] +name = "uu_unexpand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24f324b11a5f7d27f0f0cceb255fc755748b83dba844bd0ff4c6ddbbbc93c3d0" +dependencies = [ + "clap", + "fluent", + "thiserror 2.0.18", + "uucore 0.7.0", +] + +[[package]] +name = "uu_uniq" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6c3864b9912a51bab19445351f8def44fb30ccf1a87be02d68ae185c264b217" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_unlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05bd002b6736800fd985546043667a22264781526dcbd56a623a237f9391a91a" +dependencies = [ + "clap", + "fluent", + "uucore 0.7.0", +] + +[[package]] +name = "uu_wc" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d606da8f0184e8450502430bb3434419b2b66d66d315d5d58cda3d2190af1764" +dependencies = [ + "bytecount", + "clap", + "fluent", + "libc", + "nix 0.30.1", + "thiserror 2.0.18", + "unicode-width 0.2.0", + "uucore 0.7.0", +] + +[[package]] +name = "uu_yes" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21740c358b89a807706ddb273570ac7853a25e59bf175d3d6f7e77f67d1efdc1" +dependencies = [ + "clap", + "fluent", + "itertools 0.14.0", + "uucore 0.7.0", +] + +[[package]] +name = "uucore" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2003164a38a7f39da1de103a70fa66b745f572f0045ec261481539516c0a8a0e" +dependencies = [ + "bigdecimal", + "bstr", + "clap", + "fluent", + "fluent-bundle", + "fluent-syntax", + "glob", + "icu_locale", + "itertools 0.14.0", + "nix 0.30.1", + "num-traits", + "number_prefix", + "os_display", + "phf 0.13.1", + "procfs", + "thiserror 2.0.18", + "unic-langid", + "uucore_procs 0.4.0", + "wild", +] + +[[package]] +name = "uucore" +version = "0.7.0" +dependencies = [ + "base64-simd", + "bigdecimal", + "blake2b_simd", + "blake3", + "bstr", + "clap", + "crc-fast", + "data-encoding", + "data-encoding-macro", + "digest", + "dunce", + "fluent", + "fluent-bundle", + "fluent-syntax", + "glob", + "hex", + "icu_calendar", + "icu_collator", + "icu_datetime", + "icu_decimal", + "icu_locale", + "icu_provider", + "itertools 0.14.0", + "jiff", + "jiff-icu", + "libc", + "md-5", + "memchr", + "nix 0.30.1", + "num-traits", + "os_display", + "procfs", + "rustc-hash", + "sha1", + "sha2", + "sha3", + "sm3", + "thiserror 2.0.18", + "unic-langid", + "unit-prefix", + "uucore_procs 0.7.0", + "walkdir", + "wild", + "winapi-util", + "windows-sys 0.61.2", + "xattr", + "z85", +] + +[[package]] +name = "uucore" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "069b34217c27f611e1589f540f58118dbf226a9e407d38ab472052ff075a1dc2" +dependencies = [ + "bstr", + "clap", + "dunce", + "fluent", + "fluent-syntax", + "libc", + "nix 0.31.3", + "os_display", + "rustc-hash", + "rustix 1.1.4", + "thiserror 2.0.18", + "unic-langid", + "uucore_procs 0.9.0", + "wild", + "winapi-util", + "windows-sys 0.61.2", +] + +[[package]] +name = "uucore_procs" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c76f0308f7810d915246a39748e7f5d64e43e6bb9d6c8107224f9d741aefc375" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "uucore_procs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f63e2d5083ff0983193a33e2d57fd271c7e3e3e7df8e46e8f471865647b2cbc" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "uucore_procs" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34337da211e7abfff7189b794afb3b5018fe356fb36474b73645458fc1201350" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "uutils_term_grid" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcba141ce511bad08e80b43f02976571072e1ff4286f7d628943efbd277c6361" +dependencies = [ + "ansi-width", +] + +[[package]] +name = "uzers" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b8275fb1afee25b4111d2dc8b5c505dbbc4afd0b990cb96deb2d88bff8be18d" +dependencies = [ + "libc", + "log", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "vte" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "231fdcd7ef3037e8330d8e17e61011a2c244126acc0a982f4040ac3f9f0bc077" +dependencies = [ + "memchr", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi-ext" +version = "0.1.0" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", + "web-sys", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "wild" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3131afc8c575281e1e80f36ed6a092aa502c08b18ed7524e86fbbb12bb410e1" +dependencies = [ + "glob", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +dependencies = [ + "either", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.4", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "z85" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e61e59a957b7ccee15d2049f86e8bfd6f66968fcd88f018950662d9b86e675" + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/toolchain/Cargo.toml b/toolchain/Cargo.toml new file mode 100644 index 0000000000..320a5e7e43 --- /dev/null +++ b/toolchain/Cargo.toml @@ -0,0 +1,30 @@ +[workspace] +resolver = "2" +members = [ + "crates/wasi-ext", + "crates/libs/*", + "crates/commands/*", + "stubs/codex-otel", + "stubs/ctrlc", + "stubs/hostname", + "../software/*/native/crates/*", +] +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" + +[profile.release] +opt-level = "z" +lto = true +codegen-units = 1 +strip = true + +# WASM-compatible stubs for crates that don't support wasm32-wasip1 +[patch.crates-io] +ctrlc = { path = "stubs/ctrlc" } +hostname = { path = "stubs/hostname" } +uucore = { path = "stubs/uucore" } +codex-network-proxy = { path = "stubs/codex-network-proxy" } +codex-otel = { path = "stubs/codex-otel" } +wasi-ext = { path = "crates/wasi-ext" } diff --git a/toolchain/Makefile b/toolchain/Makefile new file mode 100644 index 0000000000..fe42b34368 --- /dev/null +++ b/toolchain/Makefile @@ -0,0 +1,453 @@ +WASM_TARGET := wasm32-wasip1 +RELEASE_DIR := target/$(WASM_TARGET)/release +WASI_C_SYSROOT := $(CURDIR)/c/sysroot +WASI_C_CC := $(CURDIR)/c/vendor/wasi-sdk/bin/clang +WASI_C_AR := $(CURDIR)/c/vendor/wasi-sdk/bin/llvm-ar +WASI_C_LIBDIR := $(WASI_C_SYSROOT)/lib/wasm32-wasi + +# Standalone binary output directory (configurable) +COMMANDS_DIR ?= $(RELEASE_DIR)/commands + +# Detect patched std: if std-patches/ has .patch files and patch-std.sh exists, use patched sysroot +PATCHES := $(wildcard std-patches/*.patch) +PATCH_SCRIPT := scripts/patch-std.sh + +# Disable rustc's stock self-contained WASI libraries and explicitly link the +# command CRT and libc from AgentOS's owned sysroot. Merely putting this libdir +# first in -L is insufficient: rustc selects its bundled libc internally. +# Include the libc digest in rustc's metadata so Cargo relinks Rust commands +# whenever the external sysroot is rebuilt; Cargo does not otherwise track +# archives passed as linker arguments. +WASI_C_LIBC_DIGEST = $(shell if command -v sha256sum >/dev/null 2>&1; then sha256sum "$(WASI_C_LIBDIR)/libc.a"; elif command -v shasum >/dev/null 2>&1; then shasum -a 256 "$(WASI_C_LIBDIR)/libc.a"; else cksum "$(WASI_C_LIBDIR)/libc.a"; fi 2>/dev/null | cut -d ' ' -f1 | cut -c1-16) +WASI_RUSTFLAGS = -C link-self-contained=no -C link-arg=$(WASI_C_LIBDIR)/crt1-command.o -C link-arg=$(WASI_C_LIBDIR)/libc.a -C link-arg=$(WASI_C_LIBDIR)/libwasi-emulated-pthread.a -C link-arg=-L$(WASI_C_LIBDIR) -C metadata=agentos-libc-$(WASI_C_LIBC_DIGEST) --cfg tokio_unstable + +# Discover command binary names from colocated package crates plus toolchain +# helper crates. Keep known slow/heavy commands out of the default software +# gate; they can still be built explicitly +# with `make cmd/`. +# Cargo package names are normally `cmd-`. `_stubs` is the only +# workspace package whose historical name differs from the installed binary. +# The `cmd-fd` and `cmd-rg` packages pull their bin-only upstream crates into +# vendor/; their real binaries are built by manifest path below because a +# crates.io dependency is not selectable with `cargo -p`. +cargo_package_for_command = $(if $(filter _stubs,$(1)),cmd-stubs,cmd-$(1)) + +SOFTWARE_COMMAND_CRATES := $(wildcard ../software/*/native/crates/cmd-*) +TOOLCHAIN_COMMAND_CRATES := $(wildcard crates/commands/*) +SKIPPED_BULK_COMMANDS := git codex codex-exec +COMMAND_NAMES := $(filter-out $(SKIPPED_BULK_COMMANDS),$(patsubst cmd-%,%,$(notdir $(SOFTWARE_COMMAND_CRATES))) $(notdir $(TOOLCHAIN_COMMAND_CRATES))) +COMMAND_PACKAGES := $(foreach command,$(COMMAND_NAMES),-p $(call cargo_package_for_command,$(command))) + +# Alias symlinks: link_name:target_binary +ALIAS_SYMLINKS := \ + gunzip:gzip zcat:gzip \ + bash:sh \ + dir:ls vdir:ls \ + more:cat \ + [:test + +# Stub commands: each gets a symlink to _stubs +STUB_COMMANDS := \ + chcon runcon \ + chgrp chown chroot \ + df \ + groups id \ + hostname hostid \ + install \ + mkfifo mknod \ + pinky who users uptime \ + stty sync tty + +.PHONY: all commands wasm wasm-opt-check host test clean patch-std patch-check vendor patch-vendor vendor-patch-check size-report install c-wasm codex fd-mapping-contract-wasm fd-mapping-contract-native + +all: wasm + +# Build the fast command set that lands in COMMANDS_DIR: Rust commands except +# known heavy/external ones, then the opted-in fast C commands. Heavy commands +# (git, duckdb, vim) and the external codex build are intentionally excluded +# from the default software-build gate; `_stubs` is a required Rust input for +# the generated stub aliases and is built with the default command set. +commands: wasm + $(MAKE) -C c programs install + +# Build the real wasm32-wasip1 `codex-exec` agent engine and install it where +# @agentos-software/codex-cli stages commands from. +# +# Two modes: +# - default (CODEX_REPO unset): REPRODUCIBLE build. clone-and-build-codex-wasi.sh +# shallow-clones the fork pinned in toolchain/codex-ref, rewrites its +# [patch.crates-io] to the toolchain stubs + vendored/patched tokio, and builds. +# No local checkout needed; deterministic from the pin. +# - CODEX_REPO=/path/to/codex-rs/codex-rs: DEV override. Builds that existing +# checkout in place via scripts/build-codex-wasi.sh (fast local iteration). +AGENTOS_ROOT := $(abspath $(CURDIR)/..) +CODEX_REF := $(shell cat $(CURDIR)/codex-ref 2>/dev/null) +CODEX_REPO ?= +CODEX_WASI_SDK_DIR := $(abspath $(CURDIR)/c/vendor/wasi-sdk) +codex: c/vendor/wasi-sdk/bin/clang +ifeq ($(strip $(CODEX_REPO)),) + @echo "codex: reproducible build from pin $(CODEX_REF)" + @WASI_SDK_DIR="$(CODEX_WASI_SDK_DIR)" \ + "$(CURDIR)/scripts/clone-and-build-codex-wasi.sh" +else + @if [ -x "$(CODEX_REPO)/scripts/build-wasi-codex-exec.sh" ]; then \ + echo "codex: CODEX_REPO override -> building $(CODEX_REPO) in place"; \ + CODEX_REPO="$(CODEX_REPO)" \ + WASI_SDK_DIR="$(CODEX_WASI_SDK_DIR)" \ + "$(CURDIR)/scripts/build-codex-wasi.sh"; \ + else \ + echo "codex: CODEX_REPO=$(CODEX_REPO) has no scripts/build-wasi-codex-exec.sh"; \ + echo " Unset CODEX_REPO to build reproducibly from the pin instead."; \ + exit 1; \ + fi +endif + +c/vendor/wasi-sdk/bin/clang: + $(MAKE) -C c wasi-sdk + +c/sysroot/lib/wasm32-wasi/libc.a: + $(MAKE) -C c sysroot + +# Strict variant for environments that require the codex artifact. With CODEX_REPO +# unset it builds reproducibly from the pin; with CODEX_REPO set it fails hard if +# that checkout lacks the fork build script. +.PHONY: codex-required +codex-required: +ifeq ($(strip $(CODEX_REPO)),) + @$(MAKE) codex +else + @test -x "$(CODEX_REPO)/scripts/build-wasi-codex-exec.sh" || { \ + echo "ERROR: codex fork build script not found at $(CODEX_REPO); unset CODEX_REPO to build from the pin"; exit 1; } + @$(MAKE) codex +endif + +# Apply std patches if they exist +patch-std: +ifneq ($(PATCHES),) + @if [ -x "$(PATCH_SCRIPT)" ]; then \ + echo "Applying std patches..."; \ + ./$(PATCH_SCRIPT); \ + else \ + echo "Warning: patches found but $(PATCH_SCRIPT) not executable or missing"; \ + fi +else + @echo "No patches found, using stock std" +endif + +# Resolve Rust std source for vendoring build-std deps +RUST_STD_SRC := $(shell rustc --print sysroot)/lib/rustlib/src/rust + +# Vendor dependencies from crates.io into vendor/ (includes std deps for -Z build-std) +vendor: + @if [ ! -d vendor ]; then \ + echo "Vendoring dependencies (including std library deps)..."; \ + mkdir -p .cargo; \ + cargo vendor \ + --sync "$(RUST_STD_SRC)/library/std/Cargo.toml" \ + --sync "$(RUST_STD_SRC)/library/test/Cargo.toml" \ + vendor > .cargo/config.toml; \ + else \ + echo "vendor/ exists — skipping (run 'make clean-vendor' to re-vendor)"; \ + fi + @# Source-replacement config so cargo builds from the patched vendor/ tree + @# instead of the crates.io registry cache. This file is gitignored, so a + @# fresh checkout (CI) has none; without it every std-patches/crates/*.patch + @# wasi fix is silently ignored and crates like uu_tac fail to compile for + @# wasm32-wasip1. `cargo vendor` prints the exact config (including the + @# synced std-dep sources) on stdout, so capture it above. + @test -s .cargo/config.toml || { \ + echo "ERROR: .cargo/config.toml missing or empty after vendor"; \ + echo "Run 'make clean-vendor && make vendor' to regenerate."; \ + exit 1; \ + } + +# Apply crate-level patches to vendored sources +patch-vendor: vendor + @if [ -x scripts/patch-vendor.sh ]; then \ + ./scripts/patch-vendor.sh; \ + else \ + echo "Warning: scripts/patch-vendor.sh not found or not executable"; \ + fi + +# Dry-run verification of crate patches +vendor-patch-check: vendor + @if [ -x scripts/patch-vendor.sh ]; then \ + ./scripts/patch-vendor.sh --check; \ + else \ + echo "Warning: scripts/patch-vendor.sh not found or not executable"; \ + fi + +# Ensure wasm-opt is installed (needed for post-build optimization) +wasm-opt-check: + @if ! command -v wasm-opt >/dev/null 2>&1; then \ + echo "wasm-opt not found — installing via cargo..."; \ + cargo install wasm-opt; \ + else \ + echo "wasm-opt found: $$(wasm-opt --version)"; \ + fi + +# Build all standalone command binaries, optimize, strip .wasm extension, create symlinks +wasm: c/sysroot/lib/wasm32-wasi/libc.a vendor patch-vendor patch-std wasm-opt-check + # --cfg tokio_unstable lifts tokio's wasm `compile_error!` so the patched + # tokio process/net modules (std-patches/crates/tokio) compile on wasm32-wasip1. + # Needed by codex (tokio::process/net). Harmless for commands not using tokio. + CC_wasm32_wasip1="$(WASI_C_CC)" \ + AR_wasm32_wasip1="$(WASI_C_AR)" \ + CFLAGS_wasm32_wasip1="--sysroot=$(WASI_C_SYSROOT)" \ + RUSTFLAGS="$(WASI_RUSTFLAGS)" \ + cargo build --target $(WASM_TARGET) \ + -Z build-std=std,panic_abort \ + --release \ + $(COMMAND_PACKAGES) + @set -e; \ + for spec in "fd:vendor/fd-find/Cargo.toml" "rg:vendor/ripgrep/Cargo.toml"; do \ + bin=$${spec%%:*}; manifest=$${spec#*:}; \ + rm -f "$${manifest%/*}/Cargo.lock"; \ + CC_wasm32_wasip1="$(WASI_C_CC)" \ + AR_wasm32_wasip1="$(WASI_C_AR)" \ + CFLAGS_wasm32_wasip1="--sysroot=$(WASI_C_SYSROOT)" \ + RUSTFLAGS="$(WASI_RUSTFLAGS)" \ + cargo build --manifest-path "$$manifest" \ + --target-dir target \ + --target $(WASM_TARGET) \ + -Z build-std=std,panic_abort \ + --release \ + --no-default-features \ + --bin "$$bin"; \ + done + @mkdir -p $(COMMANDS_DIR) + @TOTAL=0; SKIPPED=0; \ + HAS_OPT=$$(command -v wasm-opt >/dev/null 2>&1 && echo 1 || echo 0); \ + for cmd in $(COMMAND_NAMES); do \ + wasm="$(RELEASE_DIR)/$$cmd.wasm"; \ + if [ -f "$$wasm" ]; then \ + TOTAL=$$((TOTAL + 1)); \ + if [ "$$HAS_OPT" = "1" ]; then \ + wasm-opt -O3 --strip-debug --all-features "$$wasm" -o "$(COMMANDS_DIR)/$$cmd"; \ + else \ + cp "$$wasm" "$(COMMANDS_DIR)/$$cmd"; \ + fi; \ + else \ + SKIPPED=$$((SKIPPED + 1)); \ + echo "Warning: $$wasm not found — skipping $$cmd"; \ + fi; \ + done; \ + echo ""; \ + echo "=== Standalone Binary Build Report ==="; \ + echo "Binaries: $$TOTAL built"; \ + if [ $$SKIPPED -gt 0 ]; then \ + echo "Skipped: $$SKIPPED (binary not found)"; \ + fi; \ + if [ "$$HAS_OPT" = "1" ]; then \ + echo "Optimized: wasm-opt -O3 --strip-debug"; \ + else \ + echo "Optimized: none (wasm-opt not found)"; \ + fi + @# Create alias symlinks (fall back to copy on platforms without symlink support) + @cd $(COMMANDS_DIR) && \ + for pair in $(ALIAS_SYMLINKS); do \ + link=$${pair%%:*}; target=$${pair##*:}; \ + if [ -f "$$target" ]; then \ + ln -sf "$$target" "$$link" 2>/dev/null || cp "$$target" "$$link"; \ + fi; \ + done + @# Create stub command symlinks + @cd $(COMMANDS_DIR) && \ + for stub in $(STUB_COMMANDS); do \ + if [ -f _stubs ]; then \ + ln -sf _stubs "$$stub" 2>/dev/null || cp _stubs "$$stub"; \ + fi; \ + done + @TOTAL_FILES=$$(ls -1 $(COMMANDS_DIR) 2>/dev/null | wc -l); \ + echo "Total: $$TOTAL_FILES entries in $(COMMANDS_DIR)"; \ + echo "=== Build complete ===" + +# --------------------------------------------------------------------------- +# Uniform per-binary entry point: `make cmd/` builds ONE command binary +# into $(COMMANDS_DIR), dispatching to the toolchain that owns it: +# Rust software/*/native/crates/cmd- or crates/commands/ +# (cargo package cmd-, via wasm-cmd) +# C the C opt-in set below (toolchain/c, patched sysroot) +# codex the codex fork build (codex, codex-exec) +# `just toolchain-cmd ` calls this. `make wasm` builds the fast +# Rust set; `make -C c programs install` builds/installs the fast C set. +C_COMMANDS := zip unzip envsubst sqlite3 curl wget grep git duckdb vim ssh ssh-keysign ssh-sk-helper + +.PHONY: cmd/% +cmd/%: + @name="$*"; \ + crate_dir=$$(find ../software -path "*/native/crates/cmd-$$name" -type d -print -quit); \ + if [ -n "$$crate_dir" ] || [ -d "crates/commands/$$name" ]; then \ + $(MAKE) wasm-cmd CMD=$$name; \ + elif echo " $(C_COMMANDS) " | grep -q " $$name "; then \ + src=$$name; install_commands=$$name; \ + [ "$$name" = "sqlite3" ] && src=sqlite3_cli; \ + [ "$$name" = "git" ] && install_commands="git git-remote-http"; \ + [ "$$name" = "ssh" ] && install_commands="ssh ssh-keysign ssh-sk-helper"; \ + $(MAKE) -C c sysroot "build/$$src" && \ + $(MAKE) -C c install COMMANDS="$$install_commands"; \ + elif [ "$$name" = "codex" ] || [ "$$name" = "codex-exec" ]; then \ + $(MAKE) codex-required; \ + else \ + echo "ERROR: unknown command '$$name' (no software/*/native/crates/cmd-$$name or crates/commands/$$name; not in C_COMMANDS or codex)"; \ + exit 1; \ + fi + +# Build ONE command binary (cargo package cmd-$(CMD) -> $(COMMANDS_DIR)/$(CMD)). +# Usage: make wasm-cmd CMD=sh +.PHONY: wasm-cmd +wasm-cmd: c/sysroot/lib/wasm32-wasi/libc.a vendor patch-vendor patch-std wasm-opt-check + @test -n "$(CMD)" || { echo "ERROR: pass CMD= (a dir under crates/commands/)"; exit 1; } + @crate_dir=$$(find ../software -path "*/native/crates/cmd-$(CMD)" -type d -print -quit); \ + test -n "$$crate_dir" || test -d "crates/commands/$(CMD)" || { \ + echo "ERROR: no software/*/native/crates/cmd-$(CMD) or crates/commands/$(CMD)"; exit 1; } + @cargo_pkg="$(call cargo_package_for_command,$(CMD))"; cargo_bin="$(CMD)"; \ + manifest=""; \ + [ "$(CMD)" = "fd" ] && manifest="vendor/fd-find/Cargo.toml"; \ + [ "$(CMD)" = "rg" ] && manifest="vendor/ripgrep/Cargo.toml"; \ + if [ -n "$$manifest" ]; then \ + rm -f "$${manifest%/*}/Cargo.lock"; \ + CC_wasm32_wasip1="$(WASI_C_CC)" \ + AR_wasm32_wasip1="$(WASI_C_AR)" \ + CFLAGS_wasm32_wasip1="--sysroot=$(WASI_C_SYSROOT)" \ + RUSTFLAGS="$(WASI_RUSTFLAGS)" \ + cargo build --manifest-path "$$manifest" \ + --target-dir target \ + --target $(WASM_TARGET) \ + -Z build-std=std,panic_abort \ + --release \ + --no-default-features \ + --bin $$cargo_bin; \ + else \ + CC_wasm32_wasip1="$(WASI_C_CC)" \ + AR_wasm32_wasip1="$(WASI_C_AR)" \ + CFLAGS_wasm32_wasip1="--sysroot=$(WASI_C_SYSROOT)" \ + RUSTFLAGS="$(WASI_RUSTFLAGS)" \ + cargo build --target $(WASM_TARGET) \ + -Z build-std=std,panic_abort \ + --release \ + -p $$cargo_pkg \ + --bin $$cargo_bin; \ + fi + @mkdir -p $(COMMANDS_DIR) + @if command -v wasm-opt >/dev/null 2>&1; then \ + wasm-opt -O3 --strip-debug --all-features "$(RELEASE_DIR)/$(CMD).wasm" -o "$(COMMANDS_DIR)/$(CMD)"; \ + else \ + cp "$(RELEASE_DIR)/$(CMD).wasm" "$(COMMANDS_DIR)/$(CMD)"; \ + fi + @echo "Built $(COMMANDS_DIR)/$(CMD)" + +.PHONY: command-dispatch-check +command-dispatch-check: + @test "$(call cargo_package_for_command,_stubs)" = "cmd-stubs" + @test "$(call cargo_package_for_command,fd)" = "cmd-fd" + @test "$(call cargo_package_for_command,rg)" = "cmd-rg" + @test "$(call cargo_package_for_command,sh)" = "cmd-sh" + @test -n "$(filter _stubs,$(COMMAND_NAMES))" + @test -n "$(filter cmd-stubs,$(COMMAND_PACKAGES))" + @test -n "$(filter cmd-fd,$(COMMAND_PACKAGES))" + @test -n "$(filter cmd-rg,$(COMMAND_PACKAGES))" + @echo "Command package dispatch mappings verified" + +# Rust fixture for exact parent->child fd-map parity. It lives outside the +# production workspace so it cannot become a shipped command accidentally; +# the C conformance build invokes these targets and mounts the resulting binary +# from c/build alongside the other native/WASM fixtures. +FD_MAPPING_CONTRACT := test-programs/fd-mapping-contract/Cargo.toml +FD_MAPPING_CONTRACT_TARGET := target/fd-mapping-contract + +fd-mapping-contract-wasm: c/sysroot/lib/wasm32-wasi/libc.a patch-std + CC_wasm32_wasip1="$(WASI_C_CC)" \ + AR_wasm32_wasip1="$(WASI_C_AR)" \ + CFLAGS_wasm32_wasip1="--sysroot=$(WASI_C_SYSROOT)" \ + RUSTFLAGS="$(WASI_RUSTFLAGS)" \ + cargo build --manifest-path $(FD_MAPPING_CONTRACT) \ + --target-dir $(FD_MAPPING_CONTRACT_TARGET) \ + --target $(WASM_TARGET) \ + -Z build-std=std,panic_abort \ + --release + @mkdir -p c/build + @cp $(FD_MAPPING_CONTRACT_TARGET)/$(WASM_TARGET)/release/fd-mapping-contract.wasm \ + c/build/fd_mapping_contract + +fd-mapping-contract-native: + cargo build --manifest-path $(FD_MAPPING_CONTRACT) \ + --target-dir $(FD_MAPPING_CONTRACT_TARGET)/native \ + --release + @mkdir -p c/build/native + @cp $(FD_MAPPING_CONTRACT_TARGET)/native/release/fd-mapping-contract \ + c/build/native/fd_mapping_contract + +# Show per-command binary sizes in COMMANDS_DIR +size-report: + @if [ ! -d "$(COMMANDS_DIR)" ]; then \ + echo "Error: $(COMMANDS_DIR) not found. Run 'make wasm' first."; \ + exit 1; \ + fi + @echo "=== Standalone Binary Size Report ===" + @echo "" + @printf "%-10s %s\n" "SIZE" "COMMAND" + @printf "%-10s %s\n" "----" "-------" + @ls -lhS $(COMMANDS_DIR)/* 2>/dev/null | \ + grep -v "^total" | \ + while read line; do \ + size=$$(echo "$$line" | awk '{print $$5}'); \ + name=$$(echo "$$line" | awk '{print $$NF}'); \ + base=$$(basename "$$name"); \ + link=""; \ + if [ -L "$$name" ]; then \ + target=$$(readlink "$$name"); \ + link=" -> $$target"; \ + fi; \ + printf "%-10s %s%s\n" "$$size" "$$base" "$$link"; \ + done + @echo "" + @TOTAL_SIZE=$$(du -sh $(COMMANDS_DIR) 2>/dev/null | awk '{print $$1}'); \ + TOTAL_FILES=$$(ls -1 $(COMMANDS_DIR) 2>/dev/null | wc -l); \ + REAL_FILES=$$(find $(COMMANDS_DIR) -maxdepth 1 -type f 2>/dev/null | wc -l); \ + SYMLINKS=$$(find $(COMMANDS_DIR) -maxdepth 1 -type l 2>/dev/null | wc -l); \ + echo "Total: $$TOTAL_SIZE ($$TOTAL_FILES entries: $$REAL_FILES binaries, $$SYMLINKS symlinks)" + @echo "" + @echo "=== Per-Crate Size Contribution (rlib, top 30) ===" + @echo "Note: rlib sizes are pre-LTO; actual contribution after LTO/DCE may differ." + @echo "" + @printf "%-10s %s\n" "SIZE" "CRATE" + @printf "%-10s %s\n" "----" "-----" + @ls -lhS $(RELEASE_DIR)/deps/*.rlib 2>/dev/null | \ + awk '{split($$NF, a, "/"); name=a[length(a)]; gsub(/^lib/, "", name); gsub(/-[0-9a-f]+\.rlib$$/, "", name); printf "%-10s %s\n", $$5, name}' | \ + awk '!seen[$$2]++' | \ + head -30 + @echo "" + @echo "=== Dependency Count ===" + @TOTAL=$$(ls $(RELEASE_DIR)/deps/*.rlib 2>/dev/null | \ + sed 's/.*\/lib//' | sed 's/-[0-9a-f]*\.rlib//' | sort -u | wc -l); \ + echo "Unique crates: $$TOTAL" + +# Install standalone binaries to a custom directory + +# Build C programs to WASM (downloads wasi-sdk if needed) + + +test: + cargo test --workspace + +clean: + cargo clean + rm -rf host/dist + +clean-vendor: + rm -rf vendor .cargo/config.toml + +patch-check: +ifneq ($(PATCHES),) + @if [ -x scripts/test-patch-std-check.sh ]; then \ + ./scripts/test-patch-std-check.sh; \ + fi + @if [ -x "$(PATCH_SCRIPT)" ]; then \ + ./$(PATCH_SCRIPT) --check; \ + else \ + echo "Warning: $(PATCH_SCRIPT) not found or not executable"; \ + fi +else + @echo "No patches found" +endif diff --git a/toolchain/c/Makefile b/toolchain/c/Makefile new file mode 100644 index 0000000000..ae6123c169 --- /dev/null +++ b/toolchain/c/Makefile @@ -0,0 +1,1030 @@ +# wasmvm/c/Makefile — Build C programs to standalone WASM binaries +# +# Targets: +# wasi-sdk Download and cache wasi-sdk toolchain +# programs Compile package C commands and C test programs to WASM binaries +# sysroot Build patched wasi-libc sysroot (invokes patch-wasi-libc.sh) +# install Copy opted-in real commands to COMMANDS_DIR +# native Compile all .c files to native binaries (for parity testing) +# clean Remove build output +# +# The vanilla (unpatched) wasi-sdk sysroot is used by default. +# After US-025 adds patch-wasi-libc.sh, `make sysroot` builds a patched one. + +# wasi-sdk version and platform detection +WASI_SDK_VERSION := 25 +WASI_SDK_MAJOR := 25.0 + +UNAME_S := $(shell uname -s) +UNAME_M := $(shell uname -m) + +ifeq ($(UNAME_S),Linux) + WASI_SDK_PLATFORM := linux +else ifeq ($(UNAME_S),Darwin) + WASI_SDK_PLATFORM := macos +else + WASI_SDK_PLATFORM := linux +endif + +ifeq ($(UNAME_M),arm64) + WASI_SDK_ARCH := arm64 +else ifeq ($(UNAME_M),aarch64) + WASI_SDK_ARCH := arm64 +else + WASI_SDK_ARCH := x86_64 +endif + +WASI_SDK_TARBALL := wasi-sdk-$(WASI_SDK_MAJOR)-$(WASI_SDK_ARCH)-$(WASI_SDK_PLATFORM).tar.gz +WASI_SDK_URL := https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-$(WASI_SDK_VERSION)/$(WASI_SDK_TARBALL) + +# Directories +VENDOR_DIR := vendor +WASI_SDK_DIR := $(VENDOR_DIR)/wasi-sdk +BUILD_DIR := build +NATIVE_DIR := build/native +SOFTWARE_DIR := ../../software +TEST_PROGRAMS_DIR := ../test-programs +OVERLAY_HEADERS := $(shell find include -type f -name '*.h' -print 2>/dev/null | LC_ALL=C sort) +C_SOURCE_DIRS := $(TEST_PROGRAMS_DIR) $(wildcard $(SOFTWARE_DIR)/*/native/c) +c_source = $(firstword $(foreach dir,$(C_SOURCE_DIRS),$(wildcard $(dir)/$(1).c))) + +# Toolchain +CC := $(WASI_SDK_DIR)/bin/clang +NATIVE_CC := $(shell command -v cc 2>/dev/null || command -v gcc 2>/dev/null || command -v clang 2>/dev/null) + +# Sysroot: use patched sysroot if built, otherwise use wasi-sdk's vanilla sysroot +PATCHED_SYSROOT := sysroot +ifeq ($(wildcard $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a),) + SYSROOT := $(WASI_SDK_DIR)/share/wasi-sysroot +else + SYSROOT := $(PATCHED_SYSROOT) +endif + +# Compile flags +WASM_CFLAGS := --target=wasm32-wasip1 --sysroot=$(SYSROOT) -O2 -flto -I include/ +NATIVE_CFLAGS := -O0 -g -D_LARGEFILE64_SOURCE -I include/ + +# COMMANDS_DIR for install target (configurable, matches Rust binary output) +COMMANDS_DIR ?= ../target/wasm32-wasip1/release/commands + +# Fast real commands installed by the default software gate. Slow/heavy commands +# (duckdb, vim) remain available through explicit parent `make cmd/`. +COMMANDS := zip unzip envsubst sqlite3 curl wget grep git git-remote-http tree ssh ssh-keysign ssh-sk-helper + +# Programs requiring patched sysroot (Tier 2+ custom host imports) +PATCHED_PROGRAMS := http_get_test isatty_test getpid_test getppid_test getppid_verify userinfo pipe_test dup_test closefrom_test spawn_child spawn_contract spawn_exit_code exec_edge exec_variants pipeline itimer_contract kill_child waitpid_return waitpid_edge syscall_coverage getpwuid_test libc_compat_contract libc_bounds_contract chown_contract signal_tests sigaction_self sigaction_behavior delayed_tcp_echo delayed_kill pipe_edge select_edge socket_flags socketpair_rights ssh_proxy_helper ssh_sk_helper_contract tcp_accept_spawn tcp_echo tcp_server http_server udp_echo unix_socket signal_handler dns_lookup getaddrinfo_connect getnameinfo_contract ppoll_contract open_flags record_lock mlock_contract sqlite3 sqlite3_mem curl wget grep tree zip unzip fs_probe + +# Discover all package command and test-program C source files. +ALL_SOURCES := $(foreach dir,$(C_SOURCE_DIRS),$(wildcard $(dir)/*.c)) +SKIPPED_BULK_PROGRAMS := + +# Exclude patched-sysroot programs when only vanilla sysroot is available +ifeq ($(wildcard $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a),) + EXCLUDED := $(foreach program,$(PATCHED_PROGRAMS),$(call c_source,$(program))) + SOURCES := $(filter-out $(EXCLUDED) $(foreach program,$(SKIPPED_BULK_PROGRAMS),$(call c_source,$(program))),$(ALL_SOURCES)) +else + SOURCES := $(filter-out $(foreach program,$(SKIPPED_BULK_PROGRAMS),$(call c_source,$(program))),$(ALL_SOURCES)) +endif + +# Program names (without .c extension) +ifeq ($(wildcard $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a),) + CUSTOM_WASM_PROG_NAMES := +else + CUSTOM_WASM_PROG_NAMES := curl wget grep sqlite3 tree zip unzip +endif + +WASM_PROG_NAMES := $(sort $(basename $(notdir $(SOURCES))) $(CUSTOM_WASM_PROG_NAMES)) +NATIVE_PROG_NAMES := $(basename $(notdir $(SOURCES))) +EXTRA_WASM_OUTPUTS := $(addprefix $(BUILD_DIR)/,ssh ssh-keysign ssh-sk-helper) +PROGRAM_REPORT_NAMES := $(WASM_PROG_NAMES) $(notdir $(EXTRA_WASM_OUTPUTS)) fd_mapping_contract +NATIVE_REPORT_NAMES := $(NATIVE_PROG_NAMES) fd_mapping_contract + +# Default WASM output targets +WASM_OUTPUTS := $(addprefix $(BUILD_DIR)/,$(WASM_PROG_NAMES)) $(EXTRA_WASM_OUTPUTS) $(BUILD_DIR)/fd_mapping_contract +# Native output targets +NATIVE_OUTPUTS := $(addprefix $(NATIVE_DIR)/,$(NATIVE_PROG_NAMES)) $(NATIVE_DIR)/fd_mapping_contract + +.SECONDEXPANSION: + +.PHONY: all wasi-sdk programs sysroot install native clean wasm-opt-check os-test os-test-native + +HAS_WASM_OPT := $(shell command -v wasm-opt >/dev/null 2>&1 && echo 1 || echo 0) + +all: programs + +# --- wasi-sdk download and cache --- + +wasi-sdk: $(WASI_SDK_DIR)/bin/clang + +$(WASI_SDK_DIR)/bin/clang: + @echo "=== Downloading wasi-sdk $(WASI_SDK_MAJOR) ===" + @mkdir -p $(VENDOR_DIR) + @if command -v curl >/dev/null 2>&1; then \ + curl -fSL "$(WASI_SDK_URL)" -o "$(VENDOR_DIR)/$(WASI_SDK_TARBALL)"; \ + elif command -v wget >/dev/null 2>&1; then \ + wget -q "$(WASI_SDK_URL)" -O "$(VENDOR_DIR)/$(WASI_SDK_TARBALL)"; \ + else \ + echo "Error: neither curl nor wget found"; exit 1; \ + fi + @echo "Extracting..." + @tar -xzf "$(VENDOR_DIR)/$(WASI_SDK_TARBALL)" -C "$(VENDOR_DIR)" + @mv "$(VENDOR_DIR)/wasi-sdk-$(WASI_SDK_MAJOR)-$(WASI_SDK_ARCH)-$(WASI_SDK_PLATFORM)" "$(WASI_SDK_DIR)" + @rm -f "$(VENDOR_DIR)/$(WASI_SDK_TARBALL)" + @echo "wasi-sdk $(WASI_SDK_MAJOR) installed to $(WASI_SDK_DIR)" + +# --- C library dependencies (downloaded at build time) --- +# Unmodified upstream libs are fetched from their official sources. +# Modified libs (curl with WASI patches) are fetched from rivet-dev forks. +# All downloads cached in libs/ — add libs/ to .gitignore. + +SQLITE3_URL := https://www.sqlite.org/2024/sqlite-amalgamation-3470200.zip +CJSON_URL := https://github.com/DaveGamble/cJSON/archive/refs/tags/v1.7.18.zip +CURL_COMMIT := main +CURL_FORK_REPO_PREFIX := secure +CURL_FORK_REPO_SUFFIX := exec-curl +CURL_FORK_REPO := $(CURL_FORK_REPO_PREFIX)-$(CURL_FORK_REPO_SUFFIX) +CURL_URL := https://github.com/rivet-dev/$(CURL_FORK_REPO)/archive/refs/heads/$(CURL_COMMIT).zip +CURL_TOOL_VERSION := 8_11_1 +CURL_TOOL_URL := https://github.com/curl/curl/archive/refs/tags/curl-$(CURL_TOOL_VERSION).tar.gz +CURL_RELEASE_VERSION := 8.11.1 +CURL_RELEASE_TAG := 8_11_1 +CURL_RELEASE_URL := https://github.com/curl/curl/releases/download/curl-$(CURL_RELEASE_TAG)/curl-$(CURL_RELEASE_VERSION).tar.xz +CURL_UPSTREAM_BUILD_DIR := $(BUILD_DIR)/curl-upstream +# Reusable libcurl artifact (headers + libcurl.a) installed by the curl build so +# git's git-remote-http links the same overlaid, mbedTLS-linked libcurl. +CURL_LIBCURL_PREFIX := $(CURL_UPSTREAM_BUILD_DIR)/install +CURL_LIBCURL_ARTIFACT := $(CURL_LIBCURL_PREFIX)/lib/libcurl.a +CURL_UPSTREAM_OVERLAY_DIR := ../../software/curl/native/c/overlay +CURL_UPSTREAM_OVERLAY_FILES := $(wildcard $(CURL_UPSTREAM_OVERLAY_DIR)/lib/*.c $(CURL_UPSTREAM_OVERLAY_DIR)/lib/*.h $(CURL_UPSTREAM_OVERLAY_DIR)/lib/vtls/*.c $(CURL_UPSTREAM_OVERLAY_DIR)/lib/vtls/*.h) +ZLIB_VERSION := 1.3.1 +ZLIB_URL := https://zlib.net/zlib-$(ZLIB_VERSION).tar.gz +ZLIB_BUILD_DIR := $(BUILD_DIR)/zlib +# mbedTLS 3.6 LTS — in-guest TLS for curl/wget/git (see +# docs-internal/networking-parity-spec.md). Use the prepared release tarball +# (`mbedtls-.tar.bz2`), which bundles the generated sources and the +# `framework` submodule; the bare GitHub source archive does not. Everything +# ships under library/ in the 3.6 LTS line (crypto is not yet split into a +# separate tf-psa-crypto tree as on 4.x/master), so `make lib` needs no +# submodule fetch. We cross-compile every library/*.c against the wasi-sdk +# sysroot, mirroring the zlib rule, plus a getentropy() entropy shim. +MBEDTLS_VERSION := 3.6.2 +MBEDTLS_URL := https://github.com/Mbed-TLS/mbedtls/releases/download/mbedtls-$(MBEDTLS_VERSION)/mbedtls-$(MBEDTLS_VERSION).tar.bz2 +# Deferred `=`: LIBS_CACHE is defined further down, so expand it lazily. +MBEDTLS_SRC_DIR = $(LIBS_CACHE)/mbedtls-$(MBEDTLS_VERSION) +MBEDTLS_BUILD_DIR := $(BUILD_DIR)/mbedtls +MBEDTLS_CONFIG_DIR := mbedtls +MBEDTLS_LIBS := $(MBEDTLS_BUILD_DIR)/libmbedtls.a $(MBEDTLS_BUILD_DIR)/libmbedx509.a $(MBEDTLS_BUILD_DIR)/libmbedcrypto.a +WGET_VERSION := 1.24.5 +WGET_URL := https://ftp.gnu.org/gnu/wget/wget-$(WGET_VERSION).tar.gz +WGET_UPSTREAM_BUILD_DIR := $(BUILD_DIR)/wget-upstream +WGET_UPSTREAM_OVERLAY_INCLUDE_DIR := overlays/wget/include +# mbedTLS backend overlay (mirrors the curl overlay layout): src/wasi_ssl.c +# is copied into the wget source tree at configure time. +WGET_UPSTREAM_OVERLAY_DIR := ../../software/wget/native/c/overlay +WGET_UPSTREAM_OVERLAY_FILES := $(wildcard $(WGET_UPSTREAM_OVERLAY_INCLUDE_DIR)/*.h) \ + $(shell find $(WGET_UPSTREAM_OVERLAY_DIR) -type f 2>/dev/null) +GIT_VERSION := 2.55.0 +GIT_URL := https://www.kernel.org/pub/software/scm/git/git-$(GIT_VERSION).tar.xz +GIT_UPSTREAM_BUILD_DIR := $(BUILD_DIR)/git-upstream +GIT_PATCH_DIR := patches/git +# OpenSSH portable — real upstream ssh client (batch/key-based), linked with a +# hermetic libcrypto build for the same RSA/ECDSA/DH/AES families as Linux. +OPENSSH_VERSION := 10.4p1 +OPENSSH_URL := https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-$(OPENSSH_VERSION).tar.gz +OPENSSH_SHA256 := ef6026dd2aea8d56059638d5d3262902c892ceba9f88395835e0d06d3fb63238 +OPENSSH_UPSTREAM_BUILD_DIR := $(BUILD_DIR)/ssh-upstream +OPENSSH_PATCH_DIR := patches/openssh +OPENSSL_VERSION := 3.5.7 +OPENSSL_URL := https://github.com/openssl/openssl/releases/download/openssl-$(OPENSSL_VERSION)/openssl-$(OPENSSL_VERSION).tar.gz +OPENSSL_SHA256 := a8c0d28a529ca480f9f36cf5792e2cd21984552a3c8e4aa11a24aa31aeac98e8 +OPENSSL_UPSTREAM_BUILD_DIR := $(BUILD_DIR)/openssl-upstream +OPENSSL_PREFIX := $(BUILD_DIR)/openssl +OPENSSL_LIBCRYPTO := $(OPENSSL_PREFIX)/lib/libcrypto.a +OPENSSL_HEADER := $(OPENSSL_PREFIX)/include/openssl/opensslv.h +GREP_VERSION := 3.12 +GREP_URL := https://ftp.gnu.org/gnu/grep/grep-$(GREP_VERSION).tar.xz +GREP_UPSTREAM_BUILD_DIR := $(BUILD_DIR)/grep-upstream +INFOZIP_ZIP_VERSION := 30 +INFOZIP_ZIP_URL := https://downloads.sourceforge.net/infozip/zip$(INFOZIP_ZIP_VERSION).tar.gz +INFOZIP_ZIP_BUILD_DIR := $(BUILD_DIR)/infozip-zip-upstream +INFOZIP_UNZIP_VERSION := 60 +INFOZIP_UNZIP_URL := https://downloads.sourceforge.net/infozip/unzip$(INFOZIP_UNZIP_VERSION).tar.gz +INFOZIP_UNZIP_BUILD_DIR := $(BUILD_DIR)/infozip-unzip-upstream +TREE_VERSION := 2.3.2 +TREE_URL := https://gitlab.com/OldManProgrammer/unix-tree/-/archive/$(TREE_VERSION)/unix-tree-$(TREE_VERSION).tar.gz +# duckdb-wasm currently documents DuckDB v1.5.0 in its README. We build the +# matching upstream DuckDB tag ourselves with our patched WASI/POSIX sysroot. +DUCKDB_VERSION := v1.5.0 +DUCKDB_GIT_DESCRIBE := $(DUCKDB_VERSION)-0-g0123456789 +DUCKDB_URL := https://github.com/duckdb/duckdb/archive/refs/tags/$(DUCKDB_VERSION).tar.gz + +LIBS_DIR := libs +LIBS_CACHE := .cache/libs + +TREE_SOURCES := color.c file.c filter.c hash.c html.c info.c json.c list.c tree.c unix.c util.c xml.c strverscmp.c +TREE_FILES := $(addprefix $(LIBS_DIR)/tree/,$(TREE_SOURCES) tree.h) +TREE_WASM_FLAGS := -DLARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 + +libs/sqlite3/sqlite3.c libs/sqlite3/sqlite3.h libs/sqlite3/shell.c: + @echo "Fetching sqlite3..." + @mkdir -p $(LIBS_CACHE) $(LIBS_DIR)/sqlite3 + @curl -fSL "$(SQLITE3_URL)" -o "$(LIBS_CACHE)/sqlite3.zip" + @cd $(LIBS_CACHE) && unzip -qo sqlite3.zip + @cp $(LIBS_CACHE)/sqlite-amalgamation-*/sqlite3.c \ + $(LIBS_CACHE)/sqlite-amalgamation-*/sqlite3.h \ + $(LIBS_CACHE)/sqlite-amalgamation-*/shell.c \ + $(LIBS_DIR)/sqlite3/ + +$(TREE_FILES): $(LIBS_DIR)/tree/.fetched + +$(LIBS_DIR)/tree/.fetched: + @echo "Fetching upstream tree $(TREE_VERSION)..." + @mkdir -p $(LIBS_CACHE) $(LIBS_DIR)/tree + @curl -fSL "$(TREE_URL)" -o "$(LIBS_CACHE)/tree-$(TREE_VERSION).tar.gz" + @rm -rf "$(LIBS_CACHE)/unix-tree-$(TREE_VERSION)" + @tar -xzf "$(LIBS_CACHE)/tree-$(TREE_VERSION).tar.gz" -C "$(LIBS_CACHE)" + @cp $(addprefix $(LIBS_CACHE)/unix-tree-$(TREE_VERSION)/,$(TREE_SOURCES) tree.h) $(LIBS_DIR)/tree/ + @touch $@ + +libs/cjson/cJSON.c: + @echo "Fetching cJSON..." + @mkdir -p $(LIBS_CACHE) $(LIBS_DIR)/cjson + @curl -fSL "$(CJSON_URL)" -o "$(LIBS_CACHE)/cjson.zip" + @cd $(LIBS_CACHE) && unzip -qo cjson.zip + @cp $(LIBS_CACHE)/cJSON-*/cJSON.c $(LIBS_CACHE)/cJSON-*/cJSON.h $(LIBS_DIR)/cjson/ + +libs/curl/lib/easy.c: + @echo "Fetching curl (rivet-dev/$(CURL_FORK_REPO))..." + @mkdir -p $(LIBS_CACHE) + @curl -fSL "$(CURL_URL)" -o "$(LIBS_CACHE)/curl.zip" + @cd $(LIBS_CACHE) && unzip -qo curl.zip + @rm -rf $(LIBS_DIR)/curl + @mv $(LIBS_CACHE)/$(CURL_FORK_REPO)-* $(LIBS_DIR)/curl + +libs/duckdb/CMakeLists.txt: + @echo "Fetching DuckDB ($(DUCKDB_VERSION))..." + @mkdir -p $(LIBS_CACHE) + @curl -fSL "$(DUCKDB_URL)" -o "$(LIBS_CACHE)/duckdb.tar.gz" + @rm -rf $(LIBS_DIR)/duckdb + @mkdir -p $(LIBS_DIR)/duckdb + @tar -xzf "$(LIBS_CACHE)/duckdb.tar.gz" --strip-components=1 -C $(LIBS_DIR)/duckdb + +.PHONY: fetch-libs fetch-fast-libs clean-libs +fetch-libs: libs/sqlite3/sqlite3.c libs/cjson/cJSON.c libs/curl/lib/easy.c libs/duckdb/CMakeLists.txt +fetch-fast-libs: libs/sqlite3/sqlite3.c libs/cjson/cJSON.c libs/curl/lib/easy.c + +clean-libs: + rm -rf $(LIBS_DIR) $(LIBS_CACHE) + +# --- os-test POSIX conformance suite (downloaded at build time) --- +# ISC-licensed test suite from the Sortix project (https://sortix.org/os-test/) + +OS_TEST_VERSION := main +OS_TEST_URL := https://gitlab.com/sortix/os-test/-/archive/$(OS_TEST_VERSION)/os-test-$(OS_TEST_VERSION).tar.gz +OS_TEST_DIR := os-test + +.PHONY: fetch-os-test clean-os-test + +fetch-os-test: $(OS_TEST_DIR)/include +$(OS_TEST_DIR)/include: + @echo "Fetching os-test $(OS_TEST_VERSION)..." + @mkdir -p $(LIBS_CACHE) + @curl -fSL "$(OS_TEST_URL)" -o "$(LIBS_CACHE)/os-test.tar.gz" + @mkdir -p $(OS_TEST_DIR) + @tar -xzf "$(LIBS_CACHE)/os-test.tar.gz" --strip-components=1 -C $(OS_TEST_DIR) + @echo "os-test $(OS_TEST_VERSION) extracted to $(OS_TEST_DIR)/" + @echo " include/ : $$(find $(OS_TEST_DIR)/include -name '*.h' 2>/dev/null | wc -l) headers" + @echo " src/ : $$(find $(OS_TEST_DIR)/src -name '*.c' 2>/dev/null | wc -l) test programs" + +clean-os-test: + rm -rf $(OS_TEST_DIR) $(LIBS_CACHE)/os-test.tar.gz + +# --- os-test POSIX conformance build targets --- +# Compile every os-test C file to WASM and native binaries. +# Individual compile failures are expected (missing headers, unsupported features) +# and do not abort the build. + +OS_TEST_BUILD := $(BUILD_DIR)/os-test +OS_TEST_NATIVE_BUILD := $(NATIVE_DIR)/os-test +OS_TEST_WASM_CFLAGS := --target=wasm32-wasip1 --sysroot=$(SYSROOT) -O0 \ + -D_GNU_SOURCE -D_BSD_SOURCE -D_ALL_SOURCE -D_DEFAULT_SOURCE +OS_TEST_WASM_LDFLAGS := -lc-printscan-long-double +# Stub main() for namespace/ tests (compile-only header conformance checks) +OS_TEST_NS_MAIN := os-test-overrides/namespace_main.c +OS_TEST_NATIVE_CFLAGS := -O0 -g \ + -D_GNU_SOURCE -D_BSD_SOURCE -D_ALL_SOURCE -D_DEFAULT_SOURCE +OS_TEST_NATIVE_LDFLAGS := -lm +ifeq ($(UNAME_S),Linux) + OS_TEST_NATIVE_LDFLAGS += -lpthread -lrt +endif +ifeq ($(UNAME_S),Darwin) + OS_TEST_NATIVE_LDFLAGS += -lpthread +endif + +.PHONY: os-test os-test-native + +os-test: wasi-sdk fetch-os-test + @total=0; pass=0; fail=0; \ + for src in $$(find $(OS_TEST_DIR) -name '*.c' -not -path '*/.expect/*' -not -path '*/misc/*' | sort); do \ + rel=$${src#$(OS_TEST_DIR)/}; \ + name=$${rel%.c}; \ + out=$(OS_TEST_BUILD)/$$name; \ + mkdir -p "$$(dirname "$$out")"; \ + total=$$((total + 1)); \ + extras=""; \ + case "$$rel" in namespace/*) extras="$(OS_TEST_NS_MAIN)";; esac; \ + if $(CC) $(OS_TEST_WASM_CFLAGS) -o "$$out" "$$src" $$extras $(OS_TEST_WASM_LDFLAGS) 2>/dev/null; then \ + pass=$$((pass + 1)); \ + else \ + echo "COMPILE FAILED: $$name"; \ + rm -f "$$out"; \ + fail=$$((fail + 1)); \ + fi; \ + done; \ + echo ""; \ + echo "=== os-test WASM Build Report ==="; \ + echo "Total: $$total"; \ + echo "Compiled: $$pass"; \ + echo "Failed: $$fail"; \ + echo "Output: $(OS_TEST_BUILD)/"; \ + echo "=== Build complete ==="; \ + if [ $$fail -gt 0 ]; then echo "ERROR: $$fail tests failed to compile" >&2; exit 1; fi + +os-test-native: fetch-os-test + @total=0; pass=0; fail=0; \ + for src in $$(find $(OS_TEST_DIR) -name '*.c' -not -path '*/.expect/*' -not -path '*/misc/*' | sort); do \ + rel=$${src#$(OS_TEST_DIR)/}; \ + out=$(OS_TEST_NATIVE_BUILD)/$${rel%.c}; \ + mkdir -p "$$(dirname "$$out")"; \ + total=$$((total + 1)); \ + extras=""; \ + case "$$rel" in namespace/*) extras="$(OS_TEST_NS_MAIN)";; esac; \ + if $(NATIVE_CC) $(OS_TEST_NATIVE_CFLAGS) -o "$$out" "$$src" $$extras $(OS_TEST_NATIVE_LDFLAGS) 2>/dev/null; then \ + pass=$$((pass + 1)); \ + else \ + rm -f "$$out"; \ + fail=$$((fail + 1)); \ + fi; \ + done; \ + echo ""; \ + echo "=== os-test Native Build Report ==="; \ + echo "Total: $$total"; \ + echo "Compiled: $$pass"; \ + echo "Failed: $$fail"; \ + echo "Output: $(OS_TEST_NATIVE_BUILD)/"; \ + echo "=== Build complete ===" + +# --- musl libc-test conformance suite (downloaded at build time) --- +# MIT-licensed test suite from musl (Bytecode Alliance mirror) + +LIBC_TEST_VERSION := master +LIBC_TEST_URL := https://github.com/bytecodealliance/libc-test/archive/refs/heads/$(LIBC_TEST_VERSION).tar.gz +LIBC_TEST_DIR := libc-test + +.PHONY: fetch-libc-test clean-libc-test + +fetch-libc-test: $(LIBC_TEST_DIR)/src +$(LIBC_TEST_DIR)/src: + @echo "Fetching musl libc-test $(LIBC_TEST_VERSION)..." + @mkdir -p $(LIBS_CACHE) + @curl -fSL "$(LIBC_TEST_URL)" -o "$(LIBS_CACHE)/libc-test.tar.gz" + @mkdir -p $(LIBC_TEST_DIR) + @tar -xzf "$(LIBS_CACHE)/libc-test.tar.gz" --strip-components=1 -C $(LIBC_TEST_DIR) + @echo "libc-test $(LIBC_TEST_VERSION) extracted to $(LIBC_TEST_DIR)/" + @echo " src/ : $$(find $(LIBC_TEST_DIR)/src -name '*.c' 2>/dev/null | wc -l) test programs" + +clean-libc-test: + rm -rf $(LIBC_TEST_DIR) $(LIBS_CACHE)/libc-test.tar.gz + +# --- musl libc-test build targets --- +# Compile functional/ and regression/ tests to WASM and native binaries. +# Tests requiring fork/exec/signals/pthreads will fail to compile for WASM — that's expected. +# The test.h framework (src/common/) is compiled into a static library first. + +LIBC_TEST_BUILD := $(BUILD_DIR)/libc-test +LIBC_TEST_NATIVE_BUILD := $(NATIVE_DIR)/libc-test +LIBC_TEST_WASM_CFLAGS := --target=wasm32-wasip1 --sysroot=$(SYSROOT) -O0 \ + -D_GNU_SOURCE -D_BSD_SOURCE -D_ALL_SOURCE -D_DEFAULT_SOURCE \ + -I$(LIBC_TEST_DIR)/src/common +LIBC_TEST_WASM_LDFLAGS := -lc-printscan-long-double -lm +LIBC_TEST_NATIVE_CFLAGS := -O0 -g \ + -D_GNU_SOURCE -D_BSD_SOURCE -D_ALL_SOURCE -D_DEFAULT_SOURCE \ + -I$(LIBC_TEST_DIR)/src/common +LIBC_TEST_NATIVE_LDFLAGS := -lm +ifeq ($(UNAME_S),Linux) + LIBC_TEST_NATIVE_LDFLAGS += -lpthread -lrt -lcrypt -lresolv -lutil -ldl +endif +ifeq ($(UNAME_S),Darwin) + LIBC_TEST_NATIVE_LDFLAGS += -lpthread +endif + +# Test framework sources compiled alongside each test +LIBC_TEST_COMMON_SRCS := $(LIBC_TEST_DIR)/src/common/print.c + +.PHONY: libc-test libc-test-native + +libc-test: wasi-sdk fetch-libc-test + @total=0; pass=0; fail=0; \ + for src in $$(find $(LIBC_TEST_DIR)/src/functional $(LIBC_TEST_DIR)/src/regression -name '*.c' 2>/dev/null | sort); do \ + rel=$${src#$(LIBC_TEST_DIR)/src/}; \ + name=$${rel%.c}; \ + out=$(LIBC_TEST_BUILD)/$$name; \ + mkdir -p "$$(dirname "$$out")"; \ + total=$$((total + 1)); \ + if $(CC) $(LIBC_TEST_WASM_CFLAGS) -o "$$out" "$$src" $(LIBC_TEST_COMMON_SRCS) $(LIBC_TEST_WASM_LDFLAGS) 2>/dev/null; then \ + pass=$$((pass + 1)); \ + else \ + rm -f "$$out"; \ + fail=$$((fail + 1)); \ + fi; \ + done; \ + echo ""; \ + echo "=== libc-test WASM Build Report ==="; \ + echo "Total: $$total"; \ + echo "Compiled: $$pass"; \ + echo "Failed: $$fail"; \ + echo "Output: $(LIBC_TEST_BUILD)/"; \ + echo "=== Build complete ===" + +libc-test-native: fetch-libc-test + @total=0; pass=0; fail=0; \ + for src in $$(find $(LIBC_TEST_DIR)/src/functional $(LIBC_TEST_DIR)/src/regression -name '*.c' 2>/dev/null | sort); do \ + rel=$${src#$(LIBC_TEST_DIR)/src/}; \ + out=$(LIBC_TEST_NATIVE_BUILD)/$${rel%.c}; \ + mkdir -p "$$(dirname "$$out")"; \ + total=$$((total + 1)); \ + if $(NATIVE_CC) $(LIBC_TEST_NATIVE_CFLAGS) -o "$$out" "$$src" $(LIBC_TEST_COMMON_SRCS) $(LIBC_TEST_NATIVE_LDFLAGS) 2>/dev/null; then \ + pass=$$((pass + 1)); \ + else \ + rm -f "$$out"; \ + fail=$$((fail + 1)); \ + fi; \ + done; \ + echo ""; \ + echo "=== libc-test Native Build Report ==="; \ + echo "Total: $$total"; \ + echo "Compiled: $$pass"; \ + echo "Failed: $$fail"; \ + echo "Output: $(LIBC_TEST_NATIVE_BUILD)/"; \ + echo "=== Build complete ===" + +# --- Patched sysroot (delegates to patch-wasi-libc.sh) --- + +WASI_LIBC_PATCHES := $(wildcard ../std-patches/wasi-libc/*.patch) +WASI_LIBC_OVERRIDES := $(wildcard ../std-patches/wasi-libc-overrides/*.c) +LLVM_RUNTIME_PATCHES := $(wildcard patches/llvm-project/*.patch) + +$(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a: $(WASI_SDK_DIR)/bin/clang ../scripts/patch-wasi-libc.sh scripts/build-llvm-runtimes.sh $(WASI_LIBC_PATCHES) $(WASI_LIBC_OVERRIDES) $(LLVM_RUNTIME_PATCHES) + ../scripts/patch-wasi-libc.sh + +sysroot: $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a + +# Patched fixtures must relink whenever the owned libc changes. Without this +# edge, make can leave an older host-import ABI embedded in an otherwise +# up-to-date test binary after a sysroot rebuild. +PATCHED_WASM_OUTPUTS := $(addprefix $(BUILD_DIR)/,$(PATCHED_PROGRAMS)) +$(PATCHED_WASM_OUTPUTS): $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a +$(PATCHED_WASM_OUTPUTS): WASM_CFLAGS := --target=wasm32-wasip1 --sysroot=$(PATCHED_SYSROOT) -O2 -flto -I include/ +$(BUILD_DIR)/select_edge: WASM_CFLAGS += -DFD_SETSIZE=8192 + +# --- wasm-opt check --- + +wasm-opt-check: + @if [ "$(HAS_WASM_OPT)" = "1" ]; then \ + echo "wasm-opt found: $$(wasm-opt --version | head -1)"; \ + else \ + echo "Warning: wasm-opt (binaryen) is not installed; skipping WASM optimization."; \ + fi + +# --- Compile all C programs to WASM --- + +programs: wasi-sdk fetch-fast-libs wasm-opt-check $(WASM_OUTPUTS) + @echo "" + @echo "=== C WASM Build Report ===" + @echo "Programs: $(words $(PROGRAM_REPORT_NAMES)) compiled" + @echo "Sysroot: $(SYSROOT)" + @echo "Output: $(BUILD_DIR)/" + @echo "=== Build complete ===" + +# Default rule: single-file programs +$(BUILD_DIR)/%: $$(call c_source,$$*) $(WASI_SDK_DIR)/bin/clang + @mkdir -p $(BUILD_DIR) + $(CC) $(WASM_CFLAGS) -o $@.wasm $< + @if [ "$(HAS_WASM_OPT)" = "1" ]; then \ + wasm-opt -O3 --strip-debug $@.wasm -o $@; \ + else \ + cp $@.wasm $@; \ + fi + @rm -f $@.wasm + +$(BUILD_DIR)/fd_mapping_contract: $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a + $(MAKE) -C .. fd-mapping-contract-wasm + +# --- Per-program override rules for multi-file compilation --- +# Add overrides here for programs that link vendored libraries. + +# json_parse: links cJSON library +$(BUILD_DIR)/json_parse: $(call c_source,json_parse) libs/cjson/cJSON.c $(WASI_SDK_DIR)/bin/clang + @mkdir -p $(BUILD_DIR) + $(CC) $(WASM_CFLAGS) -Ilibs/cjson -o $@.wasm $(call c_source,json_parse) libs/cjson/cJSON.c + @if [ "$(HAS_WASM_OPT)" = "1" ]; then \ + wasm-opt -O3 --strip-debug $@.wasm -o $@; \ + else \ + cp $@.wasm $@; \ + fi + @rm -f $@.wasm + +$(NATIVE_DIR)/json_parse: $(call c_source,json_parse) libs/cjson/cJSON.c + @mkdir -p $(NATIVE_DIR) + $(NATIVE_CC) $(NATIVE_CFLAGS) -Ilibs/cjson -o $@ $(call c_source,json_parse) libs/cjson/cJSON.c + +# sqlite3_mem: links SQLite amalgamation +SQLITE_COMMON := -DSQLITE_OMIT_WAL -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_THREADSAFE=0 \ + -DSQLITE_OMIT_LOCALTIME +SQLITE_WASM := -DSQLITE_OS_OTHER $(SQLITE_COMMON) -lwasi-emulated-mman +SQLITE_NATIVE := $(SQLITE_COMMON) +SQLITE_SHELL_COMMON := -DSQLITE_OMIT_WAL -DSQLITE_OMIT_LOAD_EXTENSION -DSQLITE_THREADSAFE=0 \ + -DSQLITE_OMIT_LOCALTIME -DSQLITE_NOHAVE_SYSTEM -DHAVE_READLINE=0 +SQLITE_SHELL_WASM := $(SQLITE_SHELL_COMMON) -D_WASI_EMULATED_PROCESS_CLOCKS \ + -lwasi-emulated-mman -lwasi-emulated-process-clocks +SQLITE_SHELL_NATIVE := $(SQLITE_SHELL_COMMON) + +$(BUILD_DIR)/sqlite3_mem: $(call c_source,sqlite3_mem) libs/sqlite3/sqlite3.c $(WASI_SDK_DIR)/bin/clang + @mkdir -p $(BUILD_DIR) + $(CC) --target=wasm32-wasip1 --sysroot=$(PATCHED_SYSROOT) -Os -I include/ \ + $(SQLITE_WASM) -Ilibs/sqlite3 -Wl,--initial-memory=16777216 -o $@ \ + $(call c_source,sqlite3_mem) libs/sqlite3/sqlite3.c + +$(NATIVE_DIR)/sqlite3_mem: $(call c_source,sqlite3_mem) libs/sqlite3/sqlite3.c + @mkdir -p $(NATIVE_DIR) + $(NATIVE_CC) $(NATIVE_CFLAGS) $(SQLITE_NATIVE) -Ilibs/sqlite3 -o $@ \ + $(call c_source,sqlite3_mem) libs/sqlite3/sqlite3.c -lm -lpthread + +# sqlite3: full official CLI, links SQLite amalgamation. +# Uses -Os (not -O2) + no wasm-opt — higher optimization breaks SQLite's +# internal function pointer tables in WASM indirect call table. +# Uses --initial-memory=16777216 (16MB) for SQLite's malloc requirements. +$(BUILD_DIR)/sqlite3: libs/sqlite3/shell.c libs/sqlite3/sqlite3.c $(WASI_SDK_DIR)/bin/clang + @mkdir -p $(BUILD_DIR) + $(CC) --target=wasm32-wasip1 --sysroot=$(PATCHED_SYSROOT) -Os -I include/ \ + $(SQLITE_SHELL_WASM) -Ilibs/sqlite3 -Wl,--initial-memory=16777216 \ + -o $@ libs/sqlite3/shell.c libs/sqlite3/sqlite3.c + +$(BUILD_DIR)/sqlite3_cli: $(BUILD_DIR)/sqlite3 + cp $< $@ + +$(NATIVE_DIR)/sqlite3: libs/sqlite3/shell.c libs/sqlite3/sqlite3.c + @mkdir -p $(NATIVE_DIR) + $(NATIVE_CC) $(NATIVE_CFLAGS) $(SQLITE_SHELL_NATIVE) -Ilibs/sqlite3 -o $@ \ + libs/sqlite3/shell.c libs/sqlite3/sqlite3.c -lm -lpthread + +$(LIBS_CACHE)/zlib-$(ZLIB_VERSION)/zlib.h: + @echo "Fetching zlib $(ZLIB_VERSION)..." + @mkdir -p $(LIBS_CACHE) + @curl -fSL "$(ZLIB_URL)" -o "$(LIBS_CACHE)/zlib-$(ZLIB_VERSION).tar.gz" + @rm -rf "$(LIBS_CACHE)/zlib-$(ZLIB_VERSION)" + @tar -xzf "$(LIBS_CACHE)/zlib-$(ZLIB_VERSION).tar.gz" -C "$(LIBS_CACHE)" + +$(ZLIB_BUILD_DIR)/libz.a: $(LIBS_CACHE)/zlib-$(ZLIB_VERSION)/zlib.h $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a $(WASI_SDK_DIR)/bin/clang + @mkdir -p $(ZLIB_BUILD_DIR) + @for src in adler32.c crc32.c deflate.c infback.c inffast.c inflate.c inftrees.c trees.c zutil.c compress.c uncompr.c gzclose.c gzlib.c gzread.c gzwrite.c; do \ + $(CC) --target=wasm32-wasip1 --sysroot=$(PATCHED_SYSROOT) -O2 -DHAVE_UNISTD_H \ + -I$(LIBS_CACHE)/zlib-$(ZLIB_VERSION) \ + -c "$(LIBS_CACHE)/zlib-$(ZLIB_VERSION)/$$src" \ + -o "$(ZLIB_BUILD_DIR)/$${src%.c}.o"; \ + done + $(WASI_SDK_DIR)/bin/llvm-ar rcs $@ $(ZLIB_BUILD_DIR)/*.o + +# --- mbedTLS 3.6 LTS for wasm32-wasip1 --- +# Fetch/extract the prepared release tarball, then cross-compile every +# library/*.c (plus the getentropy entropy shim) against the wasi-sdk sysroot, +# splitting objects into the three canonical archives by filename the same way +# mbedTLS's own Makefile does (crypto / x509 / tls). Single-threaded; entropy +# via getentropy() (MBEDTLS_ENTROPY_HARDWARE_ALT); TLS 1.3 on (default in 3.6). +$(MBEDTLS_SRC_DIR)/library/ssl_tls.c: + @echo "Fetching mbedTLS $(MBEDTLS_VERSION)..." + @mkdir -p $(LIBS_CACHE) + @curl -fSL "$(MBEDTLS_URL)" -o "$(LIBS_CACHE)/mbedtls-$(MBEDTLS_VERSION).tar.bz2" + @rm -rf "$(MBEDTLS_SRC_DIR)" + @tar -xjf "$(LIBS_CACHE)/mbedtls-$(MBEDTLS_VERSION).tar.bz2" -C "$(LIBS_CACHE)" + +.PHONY: mbedtls +mbedtls: $(MBEDTLS_LIBS) + +$(MBEDTLS_LIBS) &: $(MBEDTLS_SRC_DIR)/library/ssl_tls.c \ + $(MBEDTLS_CONFIG_DIR)/wasi_user_config.h \ + $(MBEDTLS_CONFIG_DIR)/mbedtls_wasi_entropy.c \ + $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a \ + $(WASI_SDK_DIR)/bin/clang + @echo "=== Building mbedTLS $(MBEDTLS_VERSION) for wasm32-wasip1 ===" + @mkdir -p $(MBEDTLS_BUILD_DIR)/crypto $(MBEDTLS_BUILD_DIR)/x509 $(MBEDTLS_BUILD_DIR)/tls + @for src in $(MBEDTLS_SRC_DIR)/library/*.c $(MBEDTLS_CONFIG_DIR)/mbedtls_wasi_entropy.c; do \ + name=$$(basename "$${src%.c}"); \ + case "$$name" in \ + x509*|pkcs7) grp=x509 ;; \ + ssl_*|ssl|mps_*|debug|net_sockets) grp=tls ;; \ + *) grp=crypto ;; \ + esac; \ + $(CC) --target=wasm32-wasip1 --sysroot=$(PATCHED_SYSROOT) -O2 \ + -I$(MBEDTLS_SRC_DIR)/include -I$(MBEDTLS_CONFIG_DIR) \ + -DMBEDTLS_USER_CONFIG_FILE='"wasi_user_config.h"' \ + -c "$$src" -o "$(MBEDTLS_BUILD_DIR)/$$grp/$$name.o" || exit 1; \ + done + @rm -f $(MBEDTLS_LIBS) + $(WASI_SDK_DIR)/bin/llvm-ar rcs $(MBEDTLS_BUILD_DIR)/libmbedcrypto.a $(MBEDTLS_BUILD_DIR)/crypto/*.o + $(WASI_SDK_DIR)/bin/llvm-ar rcs $(MBEDTLS_BUILD_DIR)/libmbedx509.a $(MBEDTLS_BUILD_DIR)/x509/*.o + $(WASI_SDK_DIR)/bin/llvm-ar rcs $(MBEDTLS_BUILD_DIR)/libmbedtls.a $(MBEDTLS_BUILD_DIR)/tls/*.o + @echo "=== mbedTLS libs built ===" + @ls -l $(MBEDTLS_LIBS) + +# --- brotli 1.1.0 (decoder) for wasm32-wasip1 --- +# curl's --compressed brotli decode. Only the dependency-free portable C +# `common` + `dec` sources are needed (no encoder). Mirrors the zlib rule: +# cross-compile each .c against the wasi-sdk sysroot and archive into the two +# canonical libs (libbrotlicommon.a, libbrotlidec.a; dec depends on common). +BROTLI_VERSION := 1.1.0 +BROTLI_URL := https://github.com/google/brotli/archive/refs/tags/v$(BROTLI_VERSION).tar.gz +BROTLI_SRC_DIR = $(LIBS_CACHE)/brotli-$(BROTLI_VERSION) +BROTLI_BUILD_DIR := $(BUILD_DIR)/brotli +BROTLI_INCLUDE_DIR = $(BROTLI_SRC_DIR)/c/include +BROTLI_COMMON_SRCS := constants.c context.c dictionary.c platform.c shared_dictionary.c transform.c +BROTLI_DEC_SRCS := bit_reader.c decode.c huffman.c state.c +BROTLI_LIBS := $(BROTLI_BUILD_DIR)/libbrotlidec.a $(BROTLI_BUILD_DIR)/libbrotlicommon.a + +$(BROTLI_SRC_DIR)/c/dec/decode.c: + @echo "Fetching brotli $(BROTLI_VERSION)..." + @mkdir -p $(LIBS_CACHE) + @curl -fSL "$(BROTLI_URL)" -o "$(LIBS_CACHE)/brotli-$(BROTLI_VERSION).tar.gz" + @rm -rf "$(BROTLI_SRC_DIR)" + @tar -xzf "$(LIBS_CACHE)/brotli-$(BROTLI_VERSION).tar.gz" -C "$(LIBS_CACHE)" + +.PHONY: brotli +brotli: $(BROTLI_LIBS) + +$(BROTLI_LIBS) &: $(BROTLI_SRC_DIR)/c/dec/decode.c \ + $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a \ + $(WASI_SDK_DIR)/bin/clang + @echo "=== Building brotli $(BROTLI_VERSION) (dec) for wasm32-wasip1 ===" + @mkdir -p $(BROTLI_BUILD_DIR)/common $(BROTLI_BUILD_DIR)/dec + @for src in $(BROTLI_COMMON_SRCS); do \ + $(CC) --target=wasm32-wasip1 --sysroot=$(PATCHED_SYSROOT) -O2 \ + -I$(BROTLI_INCLUDE_DIR) \ + -c "$(BROTLI_SRC_DIR)/c/common/$$src" \ + -o "$(BROTLI_BUILD_DIR)/common/$${src%.c}.o" || exit 1; \ + done + @for src in $(BROTLI_DEC_SRCS); do \ + $(CC) --target=wasm32-wasip1 --sysroot=$(PATCHED_SYSROOT) -O2 \ + -I$(BROTLI_INCLUDE_DIR) \ + -c "$(BROTLI_SRC_DIR)/c/dec/$$src" \ + -o "$(BROTLI_BUILD_DIR)/dec/$${src%.c}.o" || exit 1; \ + done + @rm -f $(BROTLI_LIBS) + $(WASI_SDK_DIR)/bin/llvm-ar rcs $(BROTLI_BUILD_DIR)/libbrotlicommon.a $(BROTLI_BUILD_DIR)/common/*.o + $(WASI_SDK_DIR)/bin/llvm-ar rcs $(BROTLI_BUILD_DIR)/libbrotlidec.a $(BROTLI_BUILD_DIR)/dec/*.o + @echo "=== brotli libs built ===" + @ls -l $(BROTLI_LIBS) + +# --- zstd 1.5.6 (decoder) for wasm32-wasip1 --- +# curl's --compressed zstd decode. Single-threaded (ZSTD_MULTITHREAD off), +# ZSTD_DISABLE_ASM=1 so the portable C huffman path is used instead of the +# amd64 .S. Only lib/common + lib/decompress are needed for ZSTD_*DStream. +ZSTD_VERSION := 1.5.6 +ZSTD_URL := https://github.com/facebook/zstd/releases/download/v$(ZSTD_VERSION)/zstd-$(ZSTD_VERSION).tar.gz +ZSTD_SRC_DIR = $(LIBS_CACHE)/zstd-$(ZSTD_VERSION) +ZSTD_BUILD_DIR := $(BUILD_DIR)/zstd +ZSTD_INCLUDE_DIR = $(ZSTD_SRC_DIR)/lib +ZSTD_LIB := $(ZSTD_BUILD_DIR)/libzstd.a +# NB: leave ZSTD_MULTITHREAD *undefined* (not =0). zstd's threading.h keys the +# pthread path off `#if defined(ZSTD_MULTITHREAD)`, so even =0 pulls in +# pthread_create/join, which wasi-libc static-asserts away without threads. +ZSTD_CFLAGS := -O2 -DZSTD_DISABLE_ASM=1 \ + -I$(ZSTD_SRC_DIR)/lib -I$(ZSTD_SRC_DIR)/lib/common + +$(ZSTD_SRC_DIR)/lib/zstd.h: + @echo "Fetching zstd $(ZSTD_VERSION)..." + @mkdir -p $(LIBS_CACHE) + @curl -fSL "$(ZSTD_URL)" -o "$(LIBS_CACHE)/zstd-$(ZSTD_VERSION).tar.gz" + @rm -rf "$(ZSTD_SRC_DIR)" + @tar -xzf "$(LIBS_CACHE)/zstd-$(ZSTD_VERSION).tar.gz" -C "$(LIBS_CACHE)" + +.PHONY: zstd +zstd: $(ZSTD_LIB) + +$(ZSTD_LIB): $(ZSTD_SRC_DIR)/lib/zstd.h \ + $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a \ + $(WASI_SDK_DIR)/bin/clang + @echo "=== Building zstd $(ZSTD_VERSION) (decompress) for wasm32-wasip1 ===" + @mkdir -p $(ZSTD_BUILD_DIR)/obj + @for src in $(ZSTD_SRC_DIR)/lib/common/*.c $(ZSTD_SRC_DIR)/lib/decompress/*.c; do \ + name=$$(basename "$${src%.c}"); \ + $(CC) --target=wasm32-wasip1 --sysroot=$(PATCHED_SYSROOT) $(ZSTD_CFLAGS) \ + -c "$$src" -o "$(ZSTD_BUILD_DIR)/obj/$$name.o" || exit 1; \ + done + @rm -f $(ZSTD_LIB) + $(WASI_SDK_DIR)/bin/llvm-ar rcs $(ZSTD_LIB) $(ZSTD_BUILD_DIR)/obj/*.o + @echo "=== zstd lib built ===" + @ls -l $(ZSTD_LIB) + +$(NATIVE_DIR)/sqlite3_cli: $(NATIVE_DIR)/sqlite3 + cp $< $@ + +# tree: upstream Steve Baker tree compiled from pinned C source. +$(BUILD_DIR)/tree: $(TREE_FILES) $(WASI_SDK_DIR)/bin/clang $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a + @mkdir -p $(BUILD_DIR) + $(CC) --target=wasm32-wasip1 --sysroot=$(PATCHED_SYSROOT) -O2 -I include/ \ + $(TREE_WASM_FLAGS) -I$(LIBS_DIR)/tree -o $@ \ + $(addprefix $(LIBS_DIR)/tree/,$(TREE_SOURCES)) + +$(NATIVE_DIR)/tree: $(TREE_FILES) + @mkdir -p $(NATIVE_DIR) + $(NATIVE_CC) $(NATIVE_CFLAGS) $(TREE_WASM_FLAGS) -I$(LIBS_DIR)/tree -o $@ \ + $(addprefix $(LIBS_DIR)/tree/,$(TREE_SOURCES)) + +# zip/unzip: upstream Info-ZIP releases built against the patched C sysroot. +$(BUILD_DIR)/zip: scripts/build-infozip-upstream.sh $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a $(WASI_SDK_DIR)/bin/clang + @mkdir -p $(BUILD_DIR) + bash scripts/build-infozip-upstream.sh \ + --tool zip \ + --version "$(INFOZIP_ZIP_VERSION)" \ + --url "$(INFOZIP_ZIP_URL)" \ + --cache-dir "$(abspath $(LIBS_CACHE))" \ + --build-dir "$(abspath $(INFOZIP_ZIP_BUILD_DIR))" \ + --cc "$(abspath $(CC)) --target=wasm32-wasip1 --sysroot=$(abspath $(PATCHED_SYSROOT))" \ + --output "$(abspath $@)" + +$(BUILD_DIR)/unzip: scripts/build-infozip-upstream.sh $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a $(WASI_SDK_DIR)/bin/clang + @mkdir -p $(BUILD_DIR) + bash scripts/build-infozip-upstream.sh \ + --tool unzip \ + --version "$(INFOZIP_UNZIP_VERSION)" \ + --url "$(INFOZIP_UNZIP_URL)" \ + --cache-dir "$(abspath $(LIBS_CACHE))" \ + --build-dir "$(abspath $(INFOZIP_UNZIP_BUILD_DIR))" \ + --cc "$(abspath $(CC)) --target=wasm32-wasip1 --sysroot=$(abspath $(PATCHED_SYSROOT))" \ + --output "$(abspath $@)" + +$(BUILD_DIR)/git: scripts/build-git-upstream.sh $(wildcard $(GIT_PATCH_DIR)/*.patch) $(CURL_LIBCURL_ARTIFACT) $(MBEDTLS_LIBS) $(BROTLI_LIBS) $(ZSTD_LIB) $(ZLIB_BUILD_DIR)/libz.a $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a $(WASI_SDK_DIR)/bin/clang + @mkdir -p $(BUILD_DIR) + bash scripts/build-git-upstream.sh \ + --version "$(GIT_VERSION)" \ + --url "$(GIT_URL)" \ + --cache-dir "$(abspath $(LIBS_CACHE))" \ + --build-dir "$(abspath $(GIT_UPSTREAM_BUILD_DIR))" \ + --patch-dir "$(abspath $(GIT_PATCH_DIR))" \ + --zlib-dir "$(abspath $(LIBS_CACHE)/zlib-$(ZLIB_VERSION))" \ + --zlib-build-dir "$(abspath $(ZLIB_BUILD_DIR))" \ + --curl-prefix "$(abspath $(CURL_LIBCURL_PREFIX))" \ + --mbedtls-libdir "$(abspath $(MBEDTLS_BUILD_DIR))" \ + --brotli-libdir "$(abspath $(BROTLI_BUILD_DIR))" \ + --zstd-libdir "$(abspath $(ZSTD_BUILD_DIR))" \ + --cc "$(abspath $(CC))" \ + --ar "$(abspath $(WASI_SDK_DIR)/bin/llvm-ar)" \ + --ranlib "$(abspath $(WASI_SDK_DIR)/bin/llvm-ranlib)" \ + --sysroot "$(abspath $(PATCHED_SYSROOT))" \ + --remote-http-output "$(abspath $(BUILD_DIR)/git-remote-http)" \ + --output "$(abspath $@)" + +# git-remote-http is produced by the same git build invocation. +$(BUILD_DIR)/git-remote-http: $(BUILD_DIR)/git + @test -f $@ || { echo "Error: git-remote-http missing at $@ (git build did not produce it)"; exit 1; } + +# OpenSSL is private to OpenSSH. curl, wget, and git continue to use mbedTLS. +$(OPENSSL_LIBCRYPTO) $(OPENSSL_HEADER) &: scripts/build-openssl-upstream.sh $(OVERLAY_HEADERS) $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a $(WASI_SDK_DIR)/bin/clang + @mkdir -p $(BUILD_DIR) + bash scripts/build-openssl-upstream.sh \ + --version "$(OPENSSL_VERSION)" \ + --url "$(OPENSSL_URL)" \ + --sha256 "$(OPENSSL_SHA256)" \ + --cache-dir "$(abspath $(LIBS_CACHE))" \ + --build-dir "$(abspath $(OPENSSL_UPSTREAM_BUILD_DIR))" \ + --prefix "$(abspath $(OPENSSL_PREFIX))" \ + --overlay-include-dir "$(abspath include)" \ + --cc "$(abspath $(CC)) --target=wasm32-wasip1 --sysroot=$(abspath $(PATCHED_SYSROOT))" \ + --ar "$(abspath $(WASI_SDK_DIR)/bin/llvm-ar)" \ + --ranlib "$(abspath $(WASI_SDK_DIR)/bin/llvm-ranlib)" + +# ssh and its client-side helpers: upstream OpenSSH linked with libcrypto against the patched +# sysroot (closefrom/socketpair implementations live in the owned libc/kernel +# layers). Also lights up git-over-ssh: git's connect.c execs `ssh` from PATH. +$(BUILD_DIR)/ssh $(BUILD_DIR)/ssh-keysign $(BUILD_DIR)/ssh-sk-helper &: scripts/build-ssh-upstream.sh $(wildcard $(OPENSSH_PATCH_DIR)/*.patch) $(OVERLAY_HEADERS) $(OPENSSL_LIBCRYPTO) $(OPENSSL_HEADER) $(ZLIB_BUILD_DIR)/libz.a $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a $(WASI_SDK_DIR)/bin/clang + @mkdir -p $(BUILD_DIR) + bash scripts/build-ssh-upstream.sh \ + --version "$(OPENSSH_VERSION)" \ + --url "$(OPENSSH_URL)" \ + --sha256 "$(OPENSSH_SHA256)" \ + --cache-dir "$(abspath $(LIBS_CACHE))" \ + --build-dir "$(abspath $(OPENSSH_UPSTREAM_BUILD_DIR))" \ + --patch-dir "$(abspath $(OPENSSH_PATCH_DIR))" \ + --zlib-include "$(abspath $(LIBS_CACHE)/zlib-$(ZLIB_VERSION))" \ + --zlib-libdir "$(abspath $(ZLIB_BUILD_DIR))" \ + --openssl-prefix "$(abspath $(OPENSSL_PREFIX))" \ + --overlay-include-dir "$(abspath include)" \ + --cc "$(abspath $(CC)) --target=wasm32-wasip1 --sysroot=$(abspath $(PATCHED_SYSROOT))" \ + --ar "$(abspath $(WASI_SDK_DIR)/bin/llvm-ar)" \ + --ranlib "$(abspath $(WASI_SDK_DIR)/bin/llvm-ranlib)" \ + --output "$(abspath $(BUILD_DIR)/ssh)" \ + --keysign-output "$(abspath $(BUILD_DIR)/ssh-keysign)" \ + --sk-helper-output "$(abspath $(BUILD_DIR)/ssh-sk-helper)" + +# curl_test: links libcurl (HTTP/HTTPS build for WASM via host_net + host_tls) +CURL_SRCS := $(wildcard libs/curl/lib/*.c) $(wildcard libs/curl/lib/vauth/*.c) \ + $(wildcard libs/curl/lib/vtls/*.c) $(wildcard libs/curl/lib/vquic/*.c) \ + $(wildcard libs/curl/lib/vssh/*.c) +CURL_INCLUDES := -Ilibs/curl/include -Ilibs/curl/lib -include libs/curl/lib/curl_setup.h -include libs/curl/lib/curl_printf.h +CURL_LIB_DEFS := -DHAVE_CONFIG_H -DBUILDING_LIBCURL -DHAVE_BASENAME -DHAVE_LIBGEN_H + +$(BUILD_DIR)/curl $(CURL_LIBCURL_ARTIFACT) &: scripts/build-curl-upstream.sh $(CURL_UPSTREAM_OVERLAY_FILES) $(MBEDTLS_LIBS) $(ZLIB_BUILD_DIR)/libz.a $(BROTLI_LIBS) $(ZSTD_LIB) $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a $(WASI_SDK_DIR)/bin/clang + @mkdir -p $(BUILD_DIR) + bash scripts/build-curl-upstream.sh \ + --version "$(CURL_RELEASE_VERSION)" \ + --tag "$(CURL_RELEASE_TAG)" \ + --url "$(CURL_RELEASE_URL)" \ + --cache-dir "$(abspath $(LIBS_CACHE))" \ + --build-dir "$(abspath $(CURL_UPSTREAM_BUILD_DIR))" \ + --overlay-dir "$(abspath $(CURL_UPSTREAM_OVERLAY_DIR))" \ + --cc "$(abspath $(CC)) --target=wasm32-wasip1 --sysroot=$(abspath $(PATCHED_SYSROOT))" \ + --ar "$(abspath $(WASI_SDK_DIR)/bin/llvm-ar)" \ + --ranlib "$(abspath $(WASI_SDK_DIR)/bin/llvm-ranlib)" \ + --mbedtls-include "$(abspath $(MBEDTLS_SRC_DIR)/include)" \ + --mbedtls-libdir "$(abspath $(MBEDTLS_BUILD_DIR))" \ + --zlib-include "$(abspath $(LIBS_CACHE)/zlib-$(ZLIB_VERSION))" \ + --zlib-libdir "$(abspath $(ZLIB_BUILD_DIR))" \ + --brotli-include "$(abspath $(BROTLI_INCLUDE_DIR))" \ + --brotli-libdir "$(abspath $(BROTLI_BUILD_DIR))" \ + --zstd-include "$(abspath $(ZSTD_INCLUDE_DIR))" \ + --zstd-libdir "$(abspath $(ZSTD_BUILD_DIR))" \ + --ca-bundle "/etc/ssl/certs/ca-certificates.crt" \ + --install-prefix "$(abspath $(CURL_LIBCURL_PREFIX))" \ + --output "$(abspath $(BUILD_DIR)/curl)" + +$(BUILD_DIR)/wget: scripts/build-wget-upstream.sh $(WGET_UPSTREAM_OVERLAY_FILES) $(MBEDTLS_LIBS) $(ZLIB_BUILD_DIR)/libz.a $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a $(WASI_SDK_DIR)/bin/clang + @mkdir -p $(BUILD_DIR) + bash scripts/build-wget-upstream.sh \ + --version "$(WGET_VERSION)" \ + --url "$(WGET_URL)" \ + --cache-dir "$(abspath $(LIBS_CACHE))" \ + --build-dir "$(abspath $(WGET_UPSTREAM_BUILD_DIR))" \ + --overlay-include-dir "$(abspath $(WGET_UPSTREAM_OVERLAY_INCLUDE_DIR))" \ + --overlay-dir "$(abspath $(WGET_UPSTREAM_OVERLAY_DIR))" \ + --mbedtls-include "$(abspath $(MBEDTLS_SRC_DIR)/include)" \ + --mbedtls-libdir "$(abspath $(MBEDTLS_BUILD_DIR))" \ + --zlib-include "$(abspath $(LIBS_CACHE)/zlib-$(ZLIB_VERSION))" \ + --zlib-libdir "$(abspath $(ZLIB_BUILD_DIR))" \ + --cc "$(abspath $(CC)) --target=wasm32-wasip1 --sysroot=$(abspath $(PATCHED_SYSROOT))" \ + --ar "$(abspath $(WASI_SDK_DIR)/bin/llvm-ar)" \ + --ranlib "$(abspath $(WASI_SDK_DIR)/bin/llvm-ranlib)" \ + --output "$(abspath $@)" + +$(BUILD_DIR)/grep: scripts/build-grep-upstream.sh $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a $(WASI_SDK_DIR)/bin/clang + @mkdir -p $(BUILD_DIR) + bash scripts/build-grep-upstream.sh \ + --version "$(GREP_VERSION)" \ + --url "$(GREP_URL)" \ + --cache-dir "$(abspath $(LIBS_CACHE))" \ + --build-dir "$(abspath $(GREP_UPSTREAM_BUILD_DIR))" \ + --cc "$(abspath $(CC)) --target=wasm32-wasip1 --sysroot=$(abspath $(PATCHED_SYSROOT))" \ + --ar "$(abspath $(WASI_SDK_DIR)/bin/llvm-ar)" \ + --ranlib "$(abspath $(WASI_SDK_DIR)/bin/llvm-ranlib)" \ + --output "$(abspath $@)" + +# duckdb: upstream DuckDB CLI built from source with our patched WASI/POSIX sysroot +$(BUILD_DIR)/duckdb: libs/duckdb/CMakeLists.txt $(PATCHED_SYSROOT)/lib/wasm32-wasi/libc.a $(WASI_SDK_DIR)/bin/clang $(WASI_SDK_DIR)/bin/clang++ cmake/FindThreads.cmake scripts/build-duckdb.sh include/fcntl.h include/ifaddrs.h include/net/if.h include/sys/ioctl.h + @mkdir -p $(BUILD_DIR) + DUCKDB_SRC_DIR="$(abspath $(LIBS_DIR)/duckdb)" \ + DUCKDB_BUILD_DIR="$(abspath $(BUILD_DIR)/duckdb-cmake)" \ + DUCKDB_OUTPUT="$(abspath $(BUILD_DIR)/duckdb)" \ + WASI_SDK_DIR="$(abspath $(WASI_SDK_DIR))" \ + SYSROOT_DIR="$(abspath $(PATCHED_SYSROOT))" \ + MODULE_PATH="$(abspath cmake)" \ + OVERLAY_INCLUDE_DIR="$(abspath include)" \ + DUCKDB_GIT_DESCRIBE="$(DUCKDB_GIT_DESCRIBE)" \ + PATCH_DIR="$(abspath ../../software/duckdb/native/c/patches)" \ + bash scripts/build-duckdb.sh + +# --- Compile all C programs to native binaries (for parity testing) --- + +native: $(NATIVE_OUTPUTS) + @echo "" + @echo "=== Native Build Report ===" + @echo "Programs: $(words $(NATIVE_REPORT_NAMES)) compiled" + @echo "Output: $(NATIVE_DIR)/" + @echo "=== Build complete ===" + +$(NATIVE_DIR)/%: $$(call c_source,$$*) + @mkdir -p $(NATIVE_DIR) + $(NATIVE_CC) $(NATIVE_CFLAGS) -o $@ $< + +$(NATIVE_DIR)/fd_mapping_contract: + $(MAKE) -C .. fd-mapping-contract-native + +# --- Install opted-in commands to COMMANDS_DIR --- + +install: + @if [ -z "$(COMMANDS)" ]; then \ + echo "No C commands opted in for install (COMMANDS is empty)"; \ + exit 0; \ + fi + @if [ ! -d "$(BUILD_DIR)" ]; then \ + echo "Error: $(BUILD_DIR) not found. Run 'make programs' first."; \ + exit 1; \ + fi + @mkdir -p $(COMMANDS_DIR) + @INSTALLED=0; \ + for cmd in $(COMMANDS); do \ + src="$$cmd"; \ + if [ -f "$(BUILD_DIR)/$$src" ]; then \ + cp "$(BUILD_DIR)/$$src" "$(COMMANDS_DIR)/$$cmd"; \ + INSTALLED=$$((INSTALLED + 1)); \ + else \ + echo "Warning: $(BUILD_DIR)/$$src not found — skipping"; \ + fi; \ + done; \ + echo "Installed $$INSTALLED C command(s) to $(COMMANDS_DIR)" + @# Git helper entry points. upload-pack/receive-pack/upload-archive ARE git + @# builtins, so they alias the git binary. The HTTPS transport lives in the + @# real git-remote-http binary (libcurl in-process); git-remote-https is an + @# alias to it (same binary, ftp/ftps schemes unsupported and unused). + @if [ -f "$(COMMANDS_DIR)/git" ]; then \ + for alias in git-upload-pack git-receive-pack git-upload-archive; do \ + ln -sf git "$(COMMANDS_DIR)/$$alias"; \ + done; \ + fi + @if [ -f "$(COMMANDS_DIR)/git-remote-http" ]; then \ + ln -sf git-remote-http "$(COMMANDS_DIR)/git-remote-https"; \ + fi + +# --- Clean --- + +clean: + rm -rf $(BUILD_DIR) + +# --- vim: real vim 9.2 -> wasm32-wasip1 against the full-OS libc --- +# +# vim compiles as a normal terminal program against the PATCHED sysroot plus a +# small bridge library (vim/): a termcap stub (builtin termcaps), and POSIX +# stubs the full-OS libc still lacks. Termios/process spawning live in sysroot. +# Recipe recovered from the original hand build (see vim/ headers). The result +# lands in $(BUILD_DIR)/vim; `make -C .. cmd/vim` installs it into the shared +# commands dir. NOTE: run as `vim -n ` docs-wise is no longer needed — +# swap files work; kept here for archaeology. +VIM_VERSION ?= v9.2.0780 +VIM_SRC := libs/vim +VIM_BRIDGE_SRC := ../../software/vim/native/c/vim-bridge +VIM_BRIDGE_BUILD := $(BUILD_DIR)/vim-bridge +VIM_BRIDGE_OBJS := $(VIM_BRIDGE_BUILD)/termcap_stub.o $(VIM_BRIDGE_BUILD)/posix_stubs.o +VIM_WCC := $(VIM_BRIDGE_BUILD)/wcc + +.PHONY: fetch-vim +fetch-vim: $(VIM_SRC)/src/vim.h +$(VIM_SRC)/src/vim.h: + git clone --depth 1 --branch $(VIM_VERSION) https://github.com/vim/vim $(VIM_SRC) + +$(VIM_BRIDGE_BUILD)/%.o: $(VIM_BRIDGE_SRC)/%.c $(WASI_SDK_DIR)/bin/clang sysroot + @mkdir -p $(VIM_BRIDGE_BUILD) + $(CC) --target=wasm32-wasip1 --sysroot=$(PATCHED_SYSROOT) -O2 -I $(VIM_BRIDGE_SRC) -c -o $@ $< + +$(VIM_BRIDGE_BUILD)/libtermcap.a: $(VIM_BRIDGE_OBJS) + rm -f $@ + $(WASI_SDK_DIR)/bin/llvm-ar rcs $@ $(VIM_BRIDGE_OBJS) + +# Wrapper compiler: every vim TU gets the sysroot, the bridge headers, and the +# forced compat include, so vim's own Makefile needs no flag surgery. +$(VIM_WCC): $(WASI_SDK_DIR)/bin/clang + @mkdir -p $(VIM_BRIDGE_BUILD) + @printf '#!/bin/sh\nexec %s --target=wasm32-wasip1 --sysroot=%s -O2 -I %s -include %s "$$@"\n' \ + "$(abspath $(WASI_SDK_DIR)/bin/clang)" \ + "$(abspath $(PATCHED_SYSROOT))" \ + "$(abspath $(VIM_BRIDGE_SRC))" \ + "$(abspath $(VIM_BRIDGE_SRC)/compat.h)" > $@ + @chmod +x $@ + +$(BUILD_DIR)/vim: fetch-vim $(VIM_BRIDGE_BUILD)/libtermcap.a $(VIM_WCC) wasm-opt-check + rm -f $(VIM_SRC)/src/auto/config.cache + cd $(VIM_SRC)/src && \ +CC="$(abspath $(VIM_WCC))" \ +LDFLAGS="-L$(abspath $(VIM_BRIDGE_BUILD))" \ + vim_cv_toupper_broken=no \ + vim_cv_terminfo=no \ + vim_cv_tgetent=zero \ + vim_cv_getcwd_broken=no \ + vim_cv_stat_ignores_slash=no \ + vim_cv_memmove_handles_overlap=yes \ +vim_cv_bcopy_handles_overlap=yes \ +vim_cv_memcpy_handles_overlap=yes \ +vim_cv_tty_group=world \ +vim_cv_tty_mode=0620 \ +ac_cv_header_dlfcn_h=no \ +./configure --host=wasm32-wasip1 --build=$$(cc -dumpmachine 2>/dev/null || echo x86_64-pc-linux-gnu) \ + --with-features=normal --with-tlib=termcap \ + --disable-gui --without-x --disable-nls --disable-channel \ + --without-wayland \ + --disable-netbeans --disable-selinux --disable-gpm --disable-sysmouse \ + --disable-icon-cache-update --disable-desktop-database-update + $(MAKE) -C $(VIM_SRC)/src vim + @mkdir -p $(BUILD_DIR) + @if [ "$(HAS_WASM_OPT)" = "1" ]; then \ + wasm-opt -O3 --strip-debug --all-features $(VIM_SRC)/src/vim -o $(BUILD_DIR)/vim; \ + else \ + cp $(VIM_SRC)/src/vim $(BUILD_DIR)/vim; \ + fi + @echo "Built $(BUILD_DIR)/vim" diff --git a/registry/native/c/cmake/FindThreads.cmake b/toolchain/c/cmake/FindThreads.cmake similarity index 100% rename from registry/native/c/cmake/FindThreads.cmake rename to toolchain/c/cmake/FindThreads.cmake diff --git a/registry/native/c/include/fcntl.h b/toolchain/c/include/fcntl.h similarity index 100% rename from registry/native/c/include/fcntl.h rename to toolchain/c/include/fcntl.h diff --git a/registry/native/c/include/ifaddrs.h b/toolchain/c/include/ifaddrs.h similarity index 100% rename from registry/native/c/include/ifaddrs.h rename to toolchain/c/include/ifaddrs.h diff --git a/toolchain/c/include/net/if.h b/toolchain/c/include/net/if.h new file mode 100644 index 0000000000..d754f546c8 --- /dev/null +++ b/toolchain/c/include/net/if.h @@ -0,0 +1,36 @@ +#ifndef REGISTRY_NATIVE_C_INCLUDE_NET_IF_H +#define REGISTRY_NATIVE_C_INCLUDE_NET_IF_H + +#ifndef IFNAMSIZ +#define IFNAMSIZ 16 +#endif + +#ifndef IF_NAMESIZE +#define IF_NAMESIZE IFNAMSIZ +#endif + +struct if_nameindex { + unsigned int if_index; + char *if_name; +}; + +unsigned int if_nametoindex(const char *name); +char *if_indextoname(unsigned int index, char *name); +struct if_nameindex *if_nameindex(void); +void if_freenameindex(struct if_nameindex *indexes); + +/* Interface flag bits, values as in Linux (netdevice(7)) and musl. + * The runtime's getifaddrs() (see ifaddrs.h in this overlay) reports a + * loopback-only interface set; ported tools (e.g. OpenSSH's BindInterface + * handling in sshconnect.c and Match-address handling in readconf.c) test + * IFF_UP / IFF_LOOPBACK on those entries. */ +#ifndef IFF_UP +#define IFF_UP 0x1 +#define IFF_BROADCAST 0x2 +#define IFF_LOOPBACK 0x8 +#define IFF_POINTOPOINT 0x10 +#define IFF_RUNNING 0x40 +#define IFF_MULTICAST 0x1000 +#endif + +#endif diff --git a/toolchain/c/include/posix_spawn_compat.h b/toolchain/c/include/posix_spawn_compat.h new file mode 100644 index 0000000000..7a1ba586c4 --- /dev/null +++ b/toolchain/c/include/posix_spawn_compat.h @@ -0,0 +1,9 @@ +/* posix_spawn_compat.h -- shared POSIX process declarations. */ +#ifndef POSIX_SPAWN_COMPAT_H +#define POSIX_SPAWN_COMPAT_H + +#include +#include +#include + +#endif /* POSIX_SPAWN_COMPAT_H */ diff --git a/registry/native/c/include/sched.h b/toolchain/c/include/sched.h similarity index 100% rename from registry/native/c/include/sched.h rename to toolchain/c/include/sched.h diff --git a/registry/native/c/include/sys/ioctl.h b/toolchain/c/include/sys/ioctl.h similarity index 100% rename from registry/native/c/include/sys/ioctl.h rename to toolchain/c/include/sys/ioctl.h diff --git a/toolchain/c/mbedtls/mbedtls_wasi_entropy.c b/toolchain/c/mbedtls/mbedtls_wasi_entropy.c new file mode 100644 index 0000000000..ac76210218 --- /dev/null +++ b/toolchain/c/mbedtls/mbedtls_wasi_entropy.c @@ -0,0 +1,60 @@ +/* + * Hardware entropy shim for the wasm32-wasip1 (WASI) mbedTLS build. + * + * The WASI build sets MBEDTLS_NO_PLATFORM_ENTROPY (no /dev/urandom fopen path) + * and MBEDTLS_ENTROPY_HARDWARE_ALT, which routes the entropy module through + * this mbedtls_hardware_poll() implementation. We satisfy it with getentropy(), + * the WASI random primitive (backed by the kernel's random device layer) that + * the repo's host-side wasi_tls.c already uses. getentropy() fills at most 256 + * bytes per call, so we loop for larger requests. + */ + +#include +#include /* clock_gettime(), CLOCK_MONOTONIC */ +#include /* getentropy() — declared by wasi-libc */ + +#include "mbedtls/build_info.h" +#include "mbedtls/platform_util.h" /* mbedtls_ms_time_t */ + +/* + * WASI mbedtls_ms_time() over clock_gettime(CLOCK_MONOTONIC). Enabled by + * MBEDTLS_PLATFORM_MS_TIME_ALT in the WASI user config (see wasi_user_config.h), + * which suppresses mbedTLS's own platform-detected definition. + */ +#if defined(MBEDTLS_PLATFORM_MS_TIME_ALT) +mbedtls_ms_time_t mbedtls_ms_time(void) +{ + struct timespec tv; + if (clock_gettime(CLOCK_MONOTONIC, &tv) != 0) { + return (mbedtls_ms_time_t) time(NULL) * 1000; + } + return (mbedtls_ms_time_t) tv.tv_sec * 1000 + + (mbedtls_ms_time_t) (tv.tv_nsec / 1000000); +} +#endif /* MBEDTLS_PLATFORM_MS_TIME_ALT */ + +int mbedtls_hardware_poll(void *data, unsigned char *output, size_t len, + size_t *olen) +{ + (void)data; + + size_t filled = 0; + while (filled < len) { + size_t chunk = len - filled; + if (chunk > 256) { + chunk = 256; /* getentropy() rejects requests larger than 256. */ + } + if (getentropy(output + filled, chunk) != 0) { + if (olen != NULL) { + *olen = filled; + } + return -1; /* MBEDTLS_ERR_ENTROPY_SOURCE_FAILED at the call site. */ + } + filled += chunk; + } + + if (olen != NULL) { + *olen = len; + } + return 0; +} diff --git a/toolchain/c/mbedtls/wasi_user_config.h b/toolchain/c/mbedtls/wasi_user_config.h new file mode 100644 index 0000000000..88b22cb823 --- /dev/null +++ b/toolchain/c/mbedtls/wasi_user_config.h @@ -0,0 +1,44 @@ +/* + * mbedTLS user-config overrides for the wasm32-wasip1 (WASI) build. + * + * This file is included via -DMBEDTLS_USER_CONFIG_FILE after mbedTLS's own + * default `mbedtls_config.h`, so it only needs to describe the deltas required + * to cross-compile cleanly against the wasi-sdk sysroot. The default 3.6 config + * already enables TLS 1.2 + TLS 1.3, X.509, PK, PSA crypto, and MBEDTLS_FS_IO + * (needed for curl/wget's mbedtls_x509_crt_parse_file on /etc/ssl/certs). + * + * Deltas: + * - Single-threaded: MBEDTLS_THREADING_C stays off (default) — no pthreads. + * - Entropy: WASI has no /dev/urandom fopen path, so disable platform entropy + * and route the entropy module through mbedtls_hardware_poll(), which we + * implement over getentropy() (see mbedtls_wasi_entropy.c). getentropy() is + * the same primitive the repo's host-side wasi_tls.c already relies on. + * - Networking: MBEDTLS_NET_C pulls in BSD select()/socket() + * that wasi-libc does not fully provide. curl/wget own the transport and + * only need the TLS record + X.509 layers, so drop mbedTLS's own sockets. + * - Timing: MBEDTLS_TIMING_C uses gettimeofday()/select() hardware timers that + * are unnecessary here (no DTLS retransmission timers in the curl path). + */ + +#ifndef MBEDTLS_WASI_USER_CONFIG_H +#define MBEDTLS_WASI_USER_CONFIG_H + +/* Entropy: replace the POSIX /dev/urandom source with a getentropy() poll. */ +#undef MBEDTLS_PLATFORM_ENTROPY +#define MBEDTLS_NO_PLATFORM_ENTROPY +#define MBEDTLS_ENTROPY_HARDWARE_ALT + +/* + * mbedtls_ms_time(): mbedTLS's built-in POSIX branch only compiles when it can + * see _POSIX_VERSION, but it guards the include behind unix/__unix + * macros that clang does not define for wasm32, so the platform detection falls + * through to `#error "No mbedtls_ms_time available"`. Provide it ourselves over + * clock_gettime(CLOCK_MONOTONIC) (available in wasi-libc) via the ALT hook. + */ +#define MBEDTLS_PLATFORM_MS_TIME_ALT + +/* Drop mbedTLS's own BSD-sockets and hardware-timing modules for WASI. */ +#undef MBEDTLS_NET_C +#undef MBEDTLS_TIMING_C + +#endif /* MBEDTLS_WASI_USER_CONFIG_H */ diff --git a/toolchain/c/overlays/wget/include/unistd.h b/toolchain/c/overlays/wget/include/unistd.h new file mode 100644 index 0000000000..7ff50df4d5 --- /dev/null +++ b/toolchain/c/overlays/wget/include/unistd.h @@ -0,0 +1,14 @@ +#ifndef AGENTOS_WGET_UNISTD_OVERLAY_H +#define AGENTOS_WGET_UNISTD_OVERLAY_H + +#include_next + +/* + * GNU Wget's ptimer fallback only needs to know that Linux-style POSIX timer + * IDs are not compile-time portable here. The underlying sysroot still exposes + * WASI clocks for libc++ and other consumers that use the WASI clockid_t ABI. + */ +#undef _POSIX_TIMERS +#define _POSIX_TIMERS 0 + +#endif diff --git a/toolchain/c/patches/git/0001-wasi-posix-spawn-run-command.patch b/toolchain/c/patches/git/0001-wasi-posix-spawn-run-command.patch new file mode 100644 index 0000000000..aa954de840 --- /dev/null +++ b/toolchain/c/patches/git/0001-wasi-posix-spawn-run-command.patch @@ -0,0 +1,201 @@ +Use posix_spawn for Git child processes on WASI. + +AgentOS provides POSIX process creation through the sysroot's posix_spawn +implementation, backed by the host_process broker. WASI cannot provide fork() +semantics, so route Git's central child-process path through posix_spawn while +leaving the rest of upstream Git's command machinery intact. + +POSIX spawn does not execute Git's fork-child close calls. Add explicit, +deduplicated close actions after the descriptor mappings so children cannot +retain unused pipe ends and suppress EOF. + +--- a/run-command.c ++++ b/run-command.c +@@ -18,6 +18,10 @@ + #include "packfile.h" + #include "compat/nonblock.h" + ++#ifdef __wasi__ ++#include ++#endif ++ + void child_process_init(struct child_process *child) + { + struct child_process blank = CHILD_PROCESS_INIT; +@@ -747,12 +751,14 @@ + + #ifndef GIT_WINDOWS_NATIVE + { ++#ifndef __wasi__ + int notify_pipe[2]; ++ struct child_err cerr; ++ struct atfork_state as; ++#endif + int null_fd = -1; + char **childenv; + struct strvec argv = STRVEC_INIT; +- struct child_err cerr; +- struct atfork_state as; + + if (prepare_cmd(&argv, cmd) < 0) { + failed_errno = errno; +@@ -764,8 +770,10 @@ + + trace_argv_printf(&argv.v[1], "trace: start_command:"); + ++#ifndef __wasi__ + if (pipe(notify_pipe)) + notify_pipe[0] = notify_pipe[1] = -1; ++#endif + + if (cmd->no_stdin || cmd->no_stdout || cmd->no_stderr) { + null_fd = xopen("/dev/null", O_RDWR | O_CLOEXEC); +@@ -773,6 +781,140 @@ + } + + childenv = prep_childenv(cmd->env.v); ++#ifdef __wasi__ ++ { ++ posix_spawn_file_actions_t actions; ++ int spawn_err; ++ ++ spawn_err = posix_spawn_file_actions_init(&actions); ++ if (spawn_err) { ++ failed_errno = spawn_err; ++ cmd->pid = -1; ++ errno = spawn_err; ++ goto wasi_spawn_done; ++ } ++ ++ if (cmd->no_stdin) ++ spawn_err = posix_spawn_file_actions_adddup2(&actions, null_fd, 0); ++ else if (need_in) ++ spawn_err = posix_spawn_file_actions_adddup2(&actions, fdin[0], 0); ++ else if (cmd->in) ++ spawn_err = posix_spawn_file_actions_adddup2(&actions, cmd->in, 0); ++ if (spawn_err) ++ goto wasi_spawn_failed_action; ++ ++ if (cmd->no_stderr) ++ spawn_err = posix_spawn_file_actions_adddup2(&actions, null_fd, 2); ++ else if (need_err) ++ spawn_err = posix_spawn_file_actions_adddup2(&actions, fderr[1], 2); ++ else if (cmd->err > 1) ++ spawn_err = posix_spawn_file_actions_adddup2(&actions, cmd->err, 2); ++ if (spawn_err) ++ goto wasi_spawn_failed_action; ++ ++ if (cmd->no_stdout) ++ spawn_err = posix_spawn_file_actions_adddup2(&actions, null_fd, 1); ++ else if (cmd->stdout_to_stderr) ++ spawn_err = posix_spawn_file_actions_adddup2(&actions, 2, 1); ++ else if (need_out) ++ spawn_err = posix_spawn_file_actions_adddup2(&actions, fdout[1], 1); ++ else if (cmd->out > 1) ++ spawn_err = posix_spawn_file_actions_adddup2(&actions, cmd->out, 1); ++ if (spawn_err) ++ goto wasi_spawn_failed_action; ++ ++ if (cmd->dir) { ++ spawn_err = posix_spawn_file_actions_addchdir(&actions, cmd->dir); ++ if (spawn_err) ++ goto wasi_spawn_failed_action; ++ } ++ ++ /* ++ * Unlike fork(), posix_spawn() does not run Git's child_close() ++ * calls. Close every non-stdio pipe end and caller-supplied source ++ * after the dup2 actions, otherwise a child can retain the last ++ * writer and prevent its sibling from ever observing EOF. ++ */ ++ { ++ int close_fds[8]; ++ size_t close_nr = 0; ++ ++#define ADD_SPAWN_CLOSE(fd) do { \ ++ int close_fd = (fd); \ ++ int already_added = 0; \ ++ for (size_t close_i = 0; close_i < close_nr; close_i++) \ ++ if (close_fds[close_i] == close_fd) \ ++ already_added = 1; \ ++ if (close_fd > STDERR_FILENO && !already_added) \ ++ close_fds[close_nr++] = close_fd; \ ++} while (0) ++ ++ if (need_in) { ++ ADD_SPAWN_CLOSE(fdin[0]); ++ ADD_SPAWN_CLOSE(fdin[1]); ++ } else { ++ ADD_SPAWN_CLOSE(cmd->in); ++ } ++ if (need_out) { ++ ADD_SPAWN_CLOSE(fdout[0]); ++ ADD_SPAWN_CLOSE(fdout[1]); ++ } else { ++ ADD_SPAWN_CLOSE(cmd->out); ++ } ++ if (need_err) { ++ ADD_SPAWN_CLOSE(fderr[0]); ++ ADD_SPAWN_CLOSE(fderr[1]); ++ } else { ++ ADD_SPAWN_CLOSE(cmd->err); ++ } ++ ADD_SPAWN_CLOSE(null_fd); ++#undef ADD_SPAWN_CLOSE ++ ++ for (size_t close_i = 0; close_i < close_nr; close_i++) { ++ spawn_err = posix_spawn_file_actions_addclose( ++ &actions, close_fds[close_i]); ++ if (spawn_err) ++ goto wasi_spawn_failed_action; ++ } ++ } ++ ++ /* ++ * Match the fork path's close_fd_above_stderr contract. Helpers ++ * that communicate exclusively through stdio must not inherit an ++ * unrelated pipe end and keep a sibling from observing EOF. ++ */ ++ if (cmd->close_fd_above_stderr) { ++ spawn_err = posix_spawn_file_actions_addclosefrom_np( ++ &actions, STDERR_FILENO + 1); ++ if (spawn_err) ++ goto wasi_spawn_failed_action; ++ } ++ ++ spawn_err = posix_spawn(&cmd->pid, argv.v[1], &actions, NULL, ++ (char *const *)argv.v + 1, childenv); ++ if (spawn_err == ENOEXEC) ++ spawn_err = posix_spawn(&cmd->pid, argv.v[0], &actions, NULL, ++ (char *const *)argv.v, childenv); ++ ++wasi_spawn_failed_action: ++ posix_spawn_file_actions_destroy(&actions); ++ if (spawn_err) { ++ failed_errno = spawn_err; ++ cmd->pid = -1; ++ errno = spawn_err; ++ if (!cmd->silent_exec_failure || errno != ENOENT) ++ error_errno("cannot spawn %s", cmd->args.v[0]); ++ } else if (cmd->clean_on_exit) { ++ mark_child_for_cleanup(cmd->pid, cmd); ++ } ++ ++wasi_spawn_done: ++ if (null_fd >= 0) ++ close(null_fd); ++ strvec_clear(&argv); ++ free(childenv); ++ } ++#else + atfork_prepare(&as); + + /* +@@ -905,6 +1047,7 @@ + close(null_fd); + strvec_clear(&argv); + free(childenv); ++#endif + } + end_of_spawn: + diff --git a/toolchain/c/patches/git/0002-wasi-synchronous-sideband-demux.patch b/toolchain/c/patches/git/0002-wasi-synchronous-sideband-demux.patch new file mode 100644 index 0000000000..a27a7d5454 --- /dev/null +++ b/toolchain/c/patches/git/0002-wasi-synchronous-sideband-demux.patch @@ -0,0 +1,213 @@ +Stream Git sidebands through a spawned multicall helper on WASI. + +Native Git runs recv_sideband() in an async worker while index-pack or +pack-objects runs in parallel. The old WASI workaround ran recv_sideband() +synchronously: fetch packs and push status reports were first written to +objects/pack/tmp_sideband_* and only consumed after the network phase. +Besides doubling storage for fetched packs, that changed full-duplex push +semantics and could deadlock when a server needed its response drained while +the client was still uploading. + +The AgentOS POSIX layer supports posix_spawn(), so preserve Git's native +process topology with a hidden multicall applet. The applet performs the +same recv_sideband() call as the native async worker. fetch-pack pipes its +band-1 output directly into index-pack/unpack-objects; send-pack starts it +before pack-objects and reads the remote status from its bounded pipe. +Band-2 progress and band-3 errors continue to go directly to stderr. The +pack failure path also drains and reports whatever remote status arrives, +matching native Git. + +--- a/builtin.h ++++ b/builtin.h +@@ -252,6 +252,7 @@ + int cmd_revert(int argc, const char **argv, const char *prefix, struct repository *repo); + int cmd_rm(int argc, const char **argv, const char *prefix, struct repository *repo); + int cmd_send_pack(int argc, const char **argv, const char *prefix, struct repository *repo); ++int cmd_sideband__helper(int argc, const char **argv, const char *prefix, struct repository *repo); + int cmd_shortlog(int argc, const char **argv, const char *prefix, struct repository *repo); + int cmd_show(int argc, const char **argv, const char *prefix, struct repository *repo); + int cmd_show_branch(int argc, const char **argv, const char *prefix, struct repository *repo); +--- a/git.c ++++ b/git.c +@@ -649,6 +649,7 @@ + { "revert", cmd_revert, RUN_SETUP | NEED_WORK_TREE }, + { "rm", cmd_rm, RUN_SETUP }, + { "send-pack", cmd_send_pack, RUN_SETUP }, ++ { "sideband--helper", cmd_sideband__helper, NO_PARSEOPT }, + { "shortlog", cmd_shortlog, RUN_SETUP_GENTLY | USE_PAGER }, + { "show", cmd_show, RUN_SETUP }, + { "show-branch", cmd_show_branch, RUN_SETUP }, +--- a/fetch-pack.c ++++ b/fetch-pack.c +@@ -892,6 +892,7 @@ + return retval; + } + ++#ifndef __wasi__ + static int sideband_demux(int in UNUSED, int out, void *data) + { + int *xd = data; +@@ -901,6 +902,7 @@ + close(out); + return ret; + } ++#endif + + static void create_promisor_file(const char *keep_name, + struct ref **sought, int nr_sought) +@@ -969,9 +971,13 @@ + struct child_process cmd = CHILD_PROCESS_INIT; + int fsck_objects = 0; + int ret; ++#ifdef __wasi__ ++ struct child_process demux_process = CHILD_PROCESS_INIT; ++#endif + + memset(&demux, 0, sizeof(demux)); + if (use_sideband) { ++#ifndef __wasi__ + /* xd[] is talking with upload-pack; subprocess reads from + * xd[0], spits out band#2 to stderr, and feeds us band#1 + * through demux->out. +@@ -982,6 +988,28 @@ + demux.isolate_sigpipe = 1; + if (start_async(&demux)) + die(_("fetch-pack: unable to fork off sideband demultiplexer")); ++#else ++ /* ++ * The WASI runtime cannot execute an in-process async function, ++ * but it can spawn another Git multicall applet. Keep the native ++ * streaming topology: the helper drains the remote concurrently ++ * and pipes band#1 directly to index-pack/unpack-objects. ++ */ ++ strvec_pushl(&demux_process.args, ++ "sideband--helper", "demux", "fetch-pack", NULL); ++ strvec_pushf(&demux_process.args, "%d", ++ sideband_get_allow_control_characters()); ++ demux_process.git_cmd = 1; ++ demux_process.clean_on_exit = 1; ++ demux_process.in = dup(xd[0]); ++ if (demux_process.in < 0) ++ die_errno(_("fetch-pack: unable to duplicate sideband input")); ++ demux_process.out = -1; ++ demux_process.close_fd_above_stderr = 1; ++ if (start_command(&demux_process)) ++ die(_("fetch-pack: unable to spawn sideband demultiplexer")); ++ demux.out = demux_process.out; ++#endif + } + else + demux.out = xd[0]; +@@ -1098,8 +1126,13 @@ + ret == 0; + else + die(_("%s failed"), cmd_name); ++#ifndef __wasi__ + if (use_sideband && finish_async(&demux)) + die(_("error in sideband demultiplexer")); ++#else ++ if (use_sideband && finish_command(&demux_process)) ++ die(_("error in sideband demultiplexer")); ++#endif + + sigchain_pop(SIGPIPE); + +--- a/send-pack.c ++++ b/send-pack.c +@@ -281,6 +281,7 @@ + return ret; + } + ++#ifndef __wasi__ + static int sideband_demux(int in UNUSED, int out, void *data) + { + int *fd = data, ret; +@@ -290,6 +291,7 @@ + close(out); + return ret; + } ++#endif + + static int advertise_shallow_grafts_cb(const struct commit_graft *graft, void *cb) + { +@@ -538,6 +540,9 @@ + char *push_cert_nonce = NULL; + struct packet_reader reader; + int use_bitmaps; ++#ifdef __wasi__ ++ struct child_process demux_process = CHILD_PROCESS_INIT; ++#endif + + if (!remote_refs) { + fprintf(stderr, "No refs in common and none specified; doing nothing.\n" +@@ -729,6 +734,7 @@ + + if (use_sideband && cmds_sent) { + memset(&demux, 0, sizeof(demux)); ++#ifndef __wasi__ + demux.proc = sideband_demux; + demux.data = fd; + demux.out = -1; +@@ -736,6 +742,28 @@ + if (start_async(&demux)) + die("send-pack: unable to fork off sideband demultiplexer"); + in = demux.out; ++#endif ++ /* ++ * Spawn the hidden Git helper on WASI so the response is drained ++ * while pack-objects is still writing the request, matching the ++ * native async worker's full-duplex process topology. ++ */ ++#ifdef __wasi__ ++ strvec_pushl(&demux_process.args, ++ "sideband--helper", "demux", "send-pack", NULL); ++ strvec_pushf(&demux_process.args, "%d", ++ sideband_get_allow_control_characters()); ++ demux_process.git_cmd = 1; ++ demux_process.clean_on_exit = 1; ++ demux_process.in = dup(fd[0]); ++ if (demux_process.in < 0) ++ die_errno("send-pack: unable to duplicate sideband input"); ++ demux_process.out = -1; ++ demux_process.close_fd_above_stderr = 1; ++ if (start_command(&demux_process)) ++ die("send-pack: unable to spawn sideband demultiplexer"); ++ in = demux_process.out; ++#endif + } + + packet_reader_init(&reader, in, NULL, 0, +@@ -759,8 +787,12 @@ + receive_status(r, &reader, remote_refs); + + if (use_sideband) { +- close(demux.out); ++ close(in); ++#ifndef __wasi__ + finish_async(&demux); ++#else ++ finish_command(&demux_process); ++#endif + } + fd[1] = -1; + +@@ -782,11 +814,19 @@ + packet_flush(out); + + if (use_sideband && cmds_sent) { ++#ifndef __wasi__ + close(demux.out); + if (finish_async(&demux)) { + error("error in sideband demultiplexer"); + ret = -1; + } ++#else ++ close(in); ++ if (finish_command(&demux_process)) { ++ error("error in sideband demultiplexer"); ++ ret = -1; ++ } ++#endif + } + + if (ret < 0) diff --git a/toolchain/c/patches/git/0003-wasi-stream-receive-sideband.patch b/toolchain/c/patches/git/0003-wasi-stream-receive-sideband.patch new file mode 100644 index 0000000000..f633f2e90f --- /dev/null +++ b/toolchain/c/patches/git/0003-wasi-stream-receive-sideband.patch @@ -0,0 +1,290 @@ +Stream receive-pack sideband muxers through a spawned helper on WASI. + +receive-pack uses the same in-process async facility for hook stderr, +connectivity progress, and index-pack progress. On WASI, async startup falls +back to unavailable fork(); most callers then lose progress and the unpack +path incorrectly returns success without consuming the pack. + +Reuse the hidden sideband multicall helper as a streaming mux process. The +helper reads bounded 128-byte chunks exactly like upstream copy_to_sideband(), +including keepalives, while its stdin pipe provides backpressure. Native builds +retain the existing async implementation. Treat mux startup or exit failure as +an unpack failure instead of silently skipping the pack. + +--- a/builtin/receive-pack.c ++++ b/builtin/receive-pack.c +@@ -560,35 +560,103 @@ + return 0; + } + ++int cmd_sideband__helper(int argc, ++ const char **argv, ++ const char *prefix UNUSED, ++ struct repository *repo UNUSED) ++{ ++ if (argc == 3 && !strcmp(argv[1], "demux")) ++ return recv_sideband(argv[2], STDIN_FILENO, STDOUT_FILENO); ++ ++ if (argc == 5 && !strcmp(argv[1], "mux")) { ++ use_sideband = atoi(argv[2]); ++ use_keepalive = atoi(argv[3]); ++ keepalive_in_sec = atoi(argv[4]); ++ if (use_sideband <= 0 || use_keepalive < KEEPALIVE_NEVER || ++ use_keepalive > KEEPALIVE_ALWAYS) ++ return error("invalid sideband mux parameters"); ++ return copy_to_sideband(STDIN_FILENO, STDOUT_FILENO, NULL); ++ } ++ ++ return error("usage: git sideband--helper demux