diff --git a/.gitignore b/.gitignore index 8ecdc765ff..42c889c3b0 100644 --- a/.gitignore +++ b/.gitignore @@ -90,3 +90,6 @@ scripts/ralph/codex-streams/ # Agent working files now live user-scoped in ~/.agents (or $AGENTS_DIR). # This entry is a safety net so a stray in-repo .agent/ never gets committed. .agent/ + +# Local rivet-engine binary placed for engine-cli getEnginePath resolution +rivetkit-typescript/packages/engine-cli/rivet-engine diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000..d67f374883 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +node-linker=hoisted diff --git a/CLAUDE.md b/CLAUDE.md index 30d98141df..2575794fa8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -133,6 +133,19 @@ docker-compose up -d - When `rivetkit` needs ergonomic helpers on a `rivetkit-core` type it re-exports, prefer an extension trait plus `prelude` re-export instead of wrapping and replacing the core type. - `engine/sdks/*/api-*` are auto-generated SDK outputs; update the source API schema and regenerate them instead of editing them by hand. +### Agent OS dependency (local dev vs preview publish) + +rivet consumes agent-os two ways: the **npm `@rivet-dev/agent-os-*`** packages (`core`, `sidecar`, `pi`, `common`, `sandbox`) and the Rust **`agent-os-client`** crate (path-depended by `rivetkit-agent-os` and `rivetkit-napi`). Both are switched together by `scripts/agent-os-dep.mjs`, mirroring how agent-os switches its own secure-exec dependency (`agent-os/scripts/secure-exec-dep.mjs`). Same two modes: + +- **`local`** (hacking on agent-os) — every agent-os dependency is redirected at the sibling `../agent-os` checkout: npm via `link:` and the cargo crate via `path = ".../agent-os/crates/client"`. This is the local dev loop: edit agent-os, rebuild rivet, no publish. `just agent-os-local` (then `pnpm install` + a cargo/napi build). +- **`pinned`** (CI/release default) — npm resolves the published `@rivet-dev/agent-os-*` version; the cargo crate resolves a pinned git rev. CI needs no sibling checkout. `just agent-os-pinned`. + +Rules: +- **Never hand-edit the agent-os dep paths/versions** in individual `Cargo.toml`/`package.json` files. Always go through `scripts/agent-os-dep.mjs` (`just agent-os-local` / `agent-os-pinned` / `agent-os-set-version ` / `agent-os-status`) so all consumers stay consistent and paths are sibling-relative (portable), not absolute. +- **agent-os ↔ rivet ship in same-version lockstep** (the protocol has no back-compat). When agent-os changes the ACP wire protocol or the `agent-os-client` API, rebuild rivet against it in `local` mode; for a release, agent-os preview-publishes first, then bump rivet with `agent-os-set-version ` and re-pin. +- **`agent-os-common`** is renamed from `agent-os/registry/software/common` at publish time; `local` mode links it only when that source dir exists, otherwise it stays at the pinned version (harmless). +- The Rust crate is not on crates.io, so `pinned` cargo mode needs a git rev (`PINNED_GIT.rev` in the script). Until that's set, `pinned` leaves the cargo path dep unchanged and warns — local dev is unaffected. + ### RivetKit Test Fixtures - Core tests that touch the `_RIVET_TEST_INSPECTOR_TOKEN` env override must share a process-wide lock with startup tests that assert inspector-token initialization side effects; otherwise parallel `cargo test` runs can flip `init_inspector_token(...)` between the env-override no-op path and the KV-backed path. diff --git a/Cargo.lock b/Cargo.lock index 84e63204dd..12587504c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -21,6 +21,12 @@ dependencies = [ "gimli 0.31.1", ] +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "adler2" version = "2.0.1" @@ -33,6 +39,43 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[package]] +name = "agent-os-client" +version = "0.2.0-rc.3" +dependencies = [ + "agent-os-protocol", + "anyhow", + "async-trait", + "base64 0.22.1", + "bytes", + "chrono", + "futures", + "http 1.3.1", + "once_cell", + "parking_lot", + "scc 2.4.0", + "secure-exec-bridge", + "secure-exec-client", + "secure-exec-vm-config", + "serde", + "serde_bare", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tokio-util", + "tracing", + "uuid", +] + +[[package]] +name = "agent-os-protocol" +version = "0.2.0-rc.3" +dependencies = [ + "rivet-vbare-compiler", + "serde", + "serde_bare", +] + [[package]] name = "ahash" version = "0.8.12" @@ -270,6 +313,48 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-config" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c478f5b10ce55c9a33f87ca3404ca92768b144fc1bfdede7c0121214a8283a25" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http 0.62.6", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.3.1", + "ring 0.17.14", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26bbf46abc608f2dc61fd6cb3b7b0665497cc259a21520151ed98f8b37d2c79" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + [[package]] name = "aws-lc-rs" version = "1.16.3" @@ -292,6 +377,392 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "aws-runtime" +version = "1.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c034a1bc1d70e16e7f4e4caf7e9f7693e4c9c24cd91cf17c2a0b21abaebc7c8b" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http 0.62.6", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.103.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af040a86ae4378b7ed2f62c83b36be1848709bbbf5757ec850d0e08596a26be9" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http 0.62.6", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex", + "hmac", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "lru", + "percent-encoding", + "regex-lite", + "sha2", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.82.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b069e4973dc25875bbd54e4c6658bdb4086a846ee9ed50f328d4d4c33ebf9857" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http 0.62.6", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.83.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b49e8fe57ff100a2f717abfa65bdd94e39702fa5ab3f60cddc6ac7784010c68" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http 0.62.6", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.84.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91abcdbfb48c38a0419eb75e0eac772a4783a96750392680e4f3c25a8a0535b9" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http 0.62.6", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68f6ae9b71597dc5fd115d52849d7a5556ad9265885ad3492ea8d73b93bbc46e" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http 0.63.4", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint 0.5.5", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.3.1", + "p256", + "percent-encoding", + "ring 0.17.14", + "sha2", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cba48474f1d6807384d06fec085b909f5807e16653c5af5c45dfe89539f0b70" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.63.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23374b9170cbbcc6f5df8dc5ebb9b6c5c28a3c8f599f0e8b8b10eb6f4a5c6e74" +dependencies = [ + "aws-smithy-http 0.62.6", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 0.2.12", + "http-body 0.4.6", + "md-5", + "pin-project-lite", + "sha1", + "sha2", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c0b3e587fbaa5d7f7e870544508af8ce82ea47cd30376e69e1e37c4ac746f79" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.62.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826141069295752372f8203c17f28e30c464d22899a43a0c9fd9c458d469c88b" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http" +version = "0.63.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4a8a5fe3e4ac7ee871237c340bbce13e982d37543b65700f4419e039f5d78e" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f108f1ca850f3feef3009bdcc977be201bca9a91058864d9de0684e64514bee0" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.11", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.6.0", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.7", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.40", + "rustls-native-certs 0.8.3", + "rustls-pki-types", + "tokio", + "tower 0.5.2", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.61.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fa1213db31ac95288d981476f78d05d9cbb0353d22cdf3472cc05bb02f6551" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f616c3f2260612fe44cede278bafa18e73e6479c4e393e2c4518cf2a9a228a" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f76a580e3d8f8961e5d48763214025a2af65c2fa4cd1fb7f270a0e107a71b0" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e107ce0783019dbff59b3a244aa0c114e4a8c9d93498af9162608cd5474e796" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http 0.62.6", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "http-body 1.0.1", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c55e0837e9b8526f49e0b9bfa9ee18ddee70e853f5bc09c5d11ebceddcb0fec" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.3.1", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "576b0d6991c9c32bc14fc340582ef148311f924d41815f641a308b5d11e8e7cd" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa 1.0.15", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c50f3cdf47caa8d01f2be4a6663ea02418e892f9bbfd82c7b9a3a37eaccdd3a" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + [[package]] name = "axum" version = "0.7.9" @@ -472,12 +943,18 @@ dependencies = [ "addr2line", "cfg-if", "libc", - "miniz_oxide", + "miniz_oxide 0.8.9", "object 0.36.7", "rustc-demangle", "windows-targets 0.52.6", ] +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + [[package]] name = "base64" version = "0.13.1" @@ -496,6 +973,16 @@ 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 = "base64ct" version = "1.8.0" @@ -514,6 +1001,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bindgen" +version = "0.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" +dependencies = [ + "bitflags 2.10.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.117", +] + [[package]] name = "bindgen" version = "0.72.0" @@ -527,7 +1034,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "rustc-hash", + "rustc-hash 2.1.1", "shlex", "syn 2.0.117", ] @@ -586,14 +1093,14 @@ dependencies = [ "http-body-util", "hyper 1.6.0", "hyper-named-pipe", - "hyper-rustls", + "hyper-rustls 0.27.7", "hyper-util", "hyperlocal", "log", "pin-project-lite", - "rustls", - "rustls-native-certs", - "rustls-pemfile", + "rustls 0.23.40", + "rustls-native-certs 0.8.3", + "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", "serde_derive", @@ -661,6 +1168,16 @@ dependencies = [ "serde", ] +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + [[package]] name = "bytesize" version = "2.0.1" @@ -741,6 +1258,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.41" @@ -1108,6 +1636,42 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crc-fast" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d" +dependencies = [ + "crc", + "digest", + "rustversion", + "spin 0.10.0", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -1117,6 +1681,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "croner" version = "2.2.0" @@ -1156,6 +1726,28 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -1183,7 +1775,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "digest", "fiat-crypto", @@ -1381,7 +1973,7 @@ dependencies = [ "rivet-test-deps", "rivet-util", "rusqlite", - "scc", + "scc 3.6.12", "serde", "serde_bare", "serde_json", @@ -1409,6 +2001,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "der" version = "0.7.10" @@ -1435,12 +2037,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -1623,13 +2225,25 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der 0.6.1", + "elliptic-curve", + "rfc6979", + "signature 1.6.4", +] + [[package]] name = "ed25519" version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "signature", + "signature 2.2.0", ] [[package]] @@ -1641,7 +2255,7 @@ dependencies = [ "curve25519-dalek", "ed25519", "sha2", - "signature", + "signature 2.2.0", "subtle", ] @@ -1651,6 +2265,26 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct", + "crypto-bigint 0.4.9", + "der 0.6.1", + "digest", + "ff", + "generic-array", + "group", + "pkcs8 0.9.0", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -1761,7 +2395,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -1868,10 +2502,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" dependencies = [ "cfg-if", - "rustix", + "rustix 1.0.8", "windows-sys 0.59.0", ] +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -1914,7 +2558,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", - "miniz_oxide", + "miniz_oxide 0.8.9", ] [[package]] @@ -1935,6 +2579,21 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -1974,6 +2633,16 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" +[[package]] +name = "fslock" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "futures" version = "0.3.31" @@ -2204,6 +2873,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -2249,6 +2919,45 @@ dependencies = [ "spinning_top", ] +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "gzip-header" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86848f4fd157d91041a62c78046fb7b248bcc2dce78376d436a1756e9a038577" +dependencies = [ + "crc32fast", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "h2" version = "0.4.11" @@ -2307,6 +3016,8 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ + "allocator-api2", + "equivalent", "foldhash 0.1.5", ] @@ -2382,6 +3093,76 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hickory-net" +version = "0.26.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbafe58dd6a1bfa058c9c3dd3372c54665a1935e504a25783cdcf9bf14b21d6" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "futures-channel", + "futures-io", + "futures-util", + "hickory-proto", + "idna", + "ipnet", + "jni", + "rand 0.10.1", + "thiserror 2.0.12", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-proto" +version = "0.26.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ddac4552e5be0deead6df196824a5964b0797302569ef4686b75d32efad052" +dependencies = [ + "data-encoding", + "idna", + "ipnet", + "jni", + "once_cell", + "prefix-trie", + "rand 0.10.1", + "ring 0.17.14", + "thiserror 2.0.12", + "tinyvec", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.26.0-beta.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a751e330e7cdf445892d6ce47cb4666a8b127834d2e42cee4db15713b9a27780" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-net", + "hickory-proto", + "ipconfig", + "ipnet", + "jni", + "moka", + "ndk-context", + "once_cell", + "parking_lot", + "rand 0.10.1", + "resolv-conf", + "smallvec", + "system-configuration 0.7.0", + "thiserror 2.0.12", + "tokio", + "tracing", +] + [[package]] name = "higher-kinded-types" version = "0.2.1" @@ -2506,6 +3287,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", + "h2 0.3.27", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -2528,7 +3310,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "httparse", @@ -2555,6 +3337,22 @@ dependencies = [ "winapi", ] +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "tokio", + "tokio-rustls 0.24.1", +] + [[package]] name = "hyper-rustls" version = "0.27.7" @@ -2564,11 +3362,11 @@ dependencies = [ "http 1.3.1", "hyper 1.6.0", "hyper-util", - "rustls", - "rustls-native-certs", + "rustls 0.23.40", + "rustls-native-certs 0.8.3", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tower-service", "webpki-roots 1.0.2", ] @@ -2620,11 +3418,11 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2 0.6.0", - "system-configuration", + "system-configuration 0.6.1", "tokio", "tower-service", "tracing", - "windows-registry", + "windows-registry 0.5.3", ] [[package]] @@ -2853,6 +3651,19 @@ dependencies = [ "libc", ] +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2 0.6.0", + "widestring", + "windows-registry 0.6.1", + "windows-result 0.4.1", + "windows-sys 0.61.2", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2989,6 +3800,20 @@ dependencies = [ "serde", ] +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.7", + "pem", + "ring 0.16.20", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -3038,7 +3863,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.2", + "windows-targets 0.52.6", ] [[package]] @@ -3064,7 +3889,7 @@ version = "0.17.3+10.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cef2a00ee60fe526157c9023edab23943fae1ce2ab6f4abb2a807c1746835de9" dependencies = [ - "bindgen", + "bindgen 0.72.0", "bzip2-sys", "cc", "libc", @@ -3095,6 +3920,12 @@ dependencies = [ "vcpkg", ] +[[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.9.4" @@ -3136,6 +3967,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.4", +] + [[package]] name = "lru-slab" version = "0.1.2" @@ -3278,6 +4118,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -3421,6 +4270,12 @@ dependencies = [ "libloading", ] +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + [[package]] name = "never-say-never" version = "6.6.666" @@ -3565,9 +4420,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-derive" @@ -3673,6 +4528,10 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "once_cell_polyfill" @@ -3680,12 +4539,55 @@ version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +[[package]] +name = "openssl" +version = "0.10.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + [[package]] name = "openssl-probe" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "openssl-sys" +version = "0.9.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "opentelemetry" version = "0.28.0" @@ -3805,6 +4707,23 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sha2", +] + [[package]] name = "papaya" version = "0.2.3" @@ -3882,6 +4801,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -3894,6 +4824,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pe-unwind-info" version = "0.6.0" @@ -3943,7 +4883,7 @@ dependencies = [ "rivet-util", "rivetkit-shared-types", "rusqlite", - "scc", + "scc 3.6.12", "scopeguard", "serde", "serde_bare", @@ -3995,7 +4935,7 @@ dependencies = [ "rivet-runtime", "rivet-types", "rusqlite", - "scc", + "scc 3.6.12", "serde", "serde_bare", "serde_json", @@ -4033,7 +4973,7 @@ dependencies = [ "rivet-metrics", "rivet-runner-protocol", "rivet-util", - "scc", + "scc 3.6.12", "serde", "serde_json", "thiserror 1.0.69", @@ -4068,7 +5008,7 @@ dependencies = [ "rivet-guard-core", "rivet-metrics", "rivet-util", - "scc", + "scc 3.6.12", "serde", "serde_json", "thiserror 1.0.69", @@ -4131,7 +5071,7 @@ dependencies = [ "rivet-runner-protocol", "rivet-runtime", "rivet-types", - "scc", + "scc 3.6.12", "serde", "serde_bare", "serde_json", @@ -4144,6 +5084,15 @@ dependencies = [ "vbare", ] +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -4254,14 +5203,24 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki 0.6.0", +] + [[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", ] [[package]] @@ -4362,6 +5321,17 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "prefix-trie" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7" +dependencies = [ + "either", + "ipnet", + "num-traits", +] + [[package]] name = "pretty_assertions" version = "1.4.1" @@ -4525,7 +5495,7 @@ dependencies = [ "reqwest 0.13.4", "serde_json", "smallvec", - "spin", + "spin 0.12.0", "symbolic-demangle", "tempfile", "thiserror 2.0.12", @@ -4568,8 +5538,8 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", - "rustls", + "rustc-hash 2.1.1", + "rustls 0.23.40", "socket2 0.6.0", "thiserror 2.0.12", "tokio", @@ -4588,9 +5558,9 @@ dependencies = [ "getrandom 0.3.3", "lru-slab", "rand 0.9.2", - "ring", - "rustc-hash", - "rustls", + "ring 0.17.14", + "rustc-hash 2.1.1", + "rustls 0.23.40", "rustls-pki-types", "slab", "thiserror 2.0.12", @@ -4610,7 +5580,7 @@ dependencies = [ "once_cell", "socket2 0.6.0", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -4661,8 +5631,19 @@ 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.3", + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", ] [[package]] @@ -4703,6 +5684,12 @@ dependencies = [ "getrandom 0.3.3", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "rand_distr" version = "0.4.3" @@ -4830,12 +5817,12 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "http-body-util", "hyper 1.6.0", - "hyper-rustls", + "hyper-rustls 0.27.7", "hyper-util", "js-sys", "log", @@ -4844,15 +5831,15 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls", - "rustls-native-certs", + "rustls 0.23.40", + "rustls-native-certs 0.8.3", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-util", "tower 0.5.2", "tower-http", @@ -4880,21 +5867,21 @@ dependencies = [ "http-body 1.0.1", "http-body-util", "hyper 1.6.0", - "hyper-rustls", + "hyper-rustls 0.27.7", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", "quinn", - "rustls", + "rustls 0.23.40", "rustls-pki-types", "rustls-platform-verifier", "serde", "serde_urlencoded", "sync_wrapper", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tower 0.5.2", "tower-http", "tower-service", @@ -4929,6 +5916,38 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac", + "zeroize", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + [[package]] name = "ring" version = "0.17.14" @@ -4939,7 +5958,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.16", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -5122,8 +6141,8 @@ dependencies = [ "portable-atomic", "rand 0.8.5", "regex", - "ring", - "rustls-native-certs", + "ring 0.17.14", + "rustls-native-certs 0.8.3", "rustls-pki-types", "rustls-webpki 0.102.8", "serde", @@ -5133,7 +6152,7 @@ dependencies = [ "thiserror 1.0.69", "time", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-stream", "tokio-util", "tokio-websockets", @@ -5179,7 +6198,7 @@ dependencies = [ "rivet-metrics", "rivet-pools", "rivet-util", - "scc", + "scc 3.6.12", "serde", "serde_json", "thiserror 1.0.69", @@ -5276,7 +6295,7 @@ dependencies = [ "rivet-error", "rivet-pools", "rivet-test-deps", - "scc", + "scc 3.6.12", "sha2", "tempfile", "tokio", @@ -5403,8 +6422,8 @@ dependencies = [ "rivet-envoy-protocol", "rivet-metrics", "rivet-util-serde", - "rustls", - "scc", + "rustls 0.23.40", + "scc 3.6.12", "serde", "serde_bare", "serde_json", @@ -5503,8 +6522,8 @@ dependencies = [ "rivet-runner-protocol", "rivet-runtime", "rivet-types", - "rustls", - "rustls-pemfile", + "rustls 0.23.40", + "rustls-pemfile 2.2.0", "serde", "serde_json", "subtle", @@ -5531,7 +6550,7 @@ dependencies = [ "http-body 1.0.1", "http-body-util", "hyper 1.6.0", - "hyper-rustls", + "hyper-rustls 0.27.7", "hyper-tungstenite", "hyper-util", "indoc", @@ -5550,12 +6569,12 @@ dependencies = [ "rivet-runner-protocol", "rivet-runtime", "rivet-util", - "rustls", - "rustls-pemfile", + "rustls 0.23.40", + "rustls-pemfile 2.2.0", "serde", "serde_json", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-stream", "tokio-tungstenite", "tracing", @@ -5624,7 +6643,7 @@ dependencies = [ "divan", "futures-util", "governor", - "hyper-rustls", + "hyper-rustls 0.27.7", "hyper-util", "lazy_static", "reqwest 0.12.22", @@ -5632,7 +6651,7 @@ dependencies = [ "rivet-config", "rivet-metrics", "rivet-util", - "rustls", + "rustls 0.23.40", "serde", "tempfile", "thiserror 1.0.69", @@ -5652,8 +6671,8 @@ name = "rivet-postgres-util" version = "2.3.0-rc.12" dependencies = [ "anyhow", - "rustls", - "rustls-pemfile", + "rustls 0.23.40", + "rustls-pemfile 2.2.0", "tracing", "webpki-roots 0.26.11", ] @@ -5988,6 +7007,7 @@ dependencies = [ "anyhow", "async-trait", "axum 0.8.4", + "base64 0.22.1", "bytes", "ciborium", "futures", @@ -5999,6 +7019,7 @@ dependencies = [ "rivetkit-client-protocol", "rivetkit-core", "serde", + "serde_bytes", "serde_json", "tokio", "tokio-util", @@ -6019,6 +7040,36 @@ dependencies = [ "vbare", ] +[[package]] +name = "rivetkit-agent-os" +version = "2.3.0-rc.12" +dependencies = [ + "agent-os-client", + "anyhow", + "base64 0.22.1", + "bytes", + "ciborium", + "depot", + "depot-client-embedded", + "futures", + "gasoline", + "http 1.3.1", + "rivet-depot-client", + "rivet-error", + "rivet-pools", + "rivetkit", + "rivetkit-core", + "rusqlite", + "serde", + "serde_bytes", + "serde_json", + "tempfile", + "tokio", + "tracing", + "universaldb", + "uuid", +] + [[package]] name = "rivetkit-client" version = "2.3.0-rc.12" @@ -6033,7 +7084,7 @@ dependencies = [ "portpicker", "reqwest 0.12.22", "rivetkit-client-protocol", - "scc", + "scc 3.6.12", "serde", "serde_bare", "serde_cbor", @@ -6088,7 +7139,7 @@ dependencies = [ "rivetkit-client-protocol", "rivetkit-inspector-protocol", "rivetkit-shared-types", - "scc", + "scc 3.6.12", "serde", "serde_bare", "serde_bytes", @@ -6123,8 +7174,11 @@ dependencies = [ name = "rivetkit-napi" version = "2.3.0-rc.12" dependencies = [ + "agent-os-client", "anyhow", "async-trait", + "base64 0.22.1", + "ciborium", "hex", "http 1.3.1", "napi", @@ -6134,9 +7188,11 @@ dependencies = [ "rivet-depot-client", "rivet-error", "rivetkit-actor-persist", + "rivetkit-agent-os", "rivetkit-core", - "scc", + "scc 3.6.12", "serde", + "serde_bytes", "serde_json", "tokio", "tokio-util", @@ -6164,7 +7220,7 @@ dependencies = [ "js-sys", "rivet-error", "rivetkit-core", - "scc", + "scc 3.6.12", "serde", "serde-wasm-bindgen", "serde_json", @@ -6225,6 +7281,12 @@ version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.1" @@ -6240,6 +7302,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.0.8" @@ -6249,36 +7324,69 @@ dependencies = [ "bitflags 2.10.0", "errno", "libc", - "linux-raw-sys", - "windows-sys 0.60.2", + "linux-raw-sys 0.9.4", + "windows-sys 0.59.0", ] [[package]] name = "rustls" -version = "0.23.29" +version = "0.21.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring 0.17.14", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "log", "once_cell", - "ring", + "ring 0.17.14", "rustls-pki-types", - "rustls-webpki 0.103.4", + "rustls-webpki 0.103.13", "subtle", "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe 0.1.6", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework 2.11.1", +] + [[package]] name = "rustls-native-certs" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" dependencies = [ - "openssl-probe", + "openssl-probe 0.2.1", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.6.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", ] [[package]] @@ -6311,11 +7419,11 @@ dependencies = [ "jni", "log", "once_cell", - "rustls", - "rustls-native-certs", + "rustls 0.23.40", + "rustls-native-certs 0.8.3", "rustls-platform-verifier-android", - "rustls-webpki 0.103.4", - "security-framework", + "rustls-webpki 0.103.13", + "security-framework 3.6.0", "security-framework-sys", "webpki-root-certs", "windows-sys 0.61.2", @@ -6327,6 +7435,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + [[package]] name = "rustls-webpki" version = "0.102.8" @@ -6334,19 +7452,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] name = "rustls-webpki" -version = "0.103.4" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", - "ring", + "ring 0.17.14", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -6407,6 +7525,15 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -6416,6 +7543,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "scc" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" +dependencies = [ + "sdd 3.0.10", +] + [[package]] name = "scc" version = "3.6.12" @@ -6423,7 +7559,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f448f7d881535036452f0cac656a41463807f095eda504890764ca7d11e2a2ea" dependencies = [ "saa", - "sdd", + "sdd 4.7.5", ] [[package]] @@ -6446,63 +7582,236 @@ dependencies = [ "serde", "serde_json", "url", - "uuid", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.117", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "password-hash", + "pbkdf2", + "salsa20", + "sha2", +] + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "sdd" +version = "3.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" + +[[package]] +name = "sdd" +version = "4.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ca0e33fc1ae39e36b2d1fdfc8ee470b26397b642ff87572a59a36ff4f2340b" + +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct", + "der 0.6.1", + "generic-array", + "pkcs8 0.9.0", + "subtle", + "zeroize", +] + +[[package]] +name = "secure-exec-bridge" +version = "0.3.0-rc.1" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "secure-exec-client" +version = "0.3.0-rc.1" +dependencies = [ + "futures", + "parking_lot", + "scc 2.4.0", + "secure-exec-sidecar", + "thiserror 2.0.12", + "tokio", + "tracing", +] + +[[package]] +name = "secure-exec-execution" +version = "0.3.0-rc.1" +dependencies = [ + "base64 0.22.1", + "ciborium", + "getrandom 0.2.16", + "nix 0.29.0", + "secure-exec-bridge", + "secure-exec-v8-runtime", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "secure-exec-kernel" +version = "0.3.0-rc.1" +dependencies = [ + "base64 0.22.1", + "getrandom 0.2.16", + "hickory-resolver", + "secure-exec-bridge", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "secure-exec-sidecar" +version = "0.3.0-rc.1" +dependencies = [ + "aws-config", + "aws-credential-types", + "aws-sdk-s3", + "base64 0.22.1", + "bytes", + "filetime", + "h2 0.4.11", + "hickory-resolver", + "hmac", + "http 1.3.1", + "jsonwebtoken", + "log", + "md-5", + "nix 0.29.0", + "openssl", + "pbkdf2", + "rivet-vbare-compiler", + "rusqlite", + "rustls 0.23.40", + "rustls-native-certs 0.8.3", + "rustls-pemfile 2.2.0", + "scrypt", + "secure-exec-bridge", + "secure-exec-execution", + "secure-exec-kernel", + "secure-exec-vm-config", + "serde", + "serde_bare", + "serde_json", + "sha1", + "sha2", + "socket2 0.6.0", + "tokio", + "tokio-rustls 0.26.2", + "tracing", + "tracing-subscriber", + "ureq 2.12.1", + "url", + "vbare", ] [[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +name = "secure-exec-v8-runtime" +version = "0.3.0-rc.1" dependencies = [ - "dyn-clone", - "ref-cast", + "ciborium", + "crossbeam-channel", + "libc", + "openssl", + "secure-exec-bridge", "serde", - "serde_json", + "signal-hook", + "v8", ] [[package]] -name = "schemars" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" +name = "secure-exec-vm-config" +version = "0.3.0-rc.1" dependencies = [ - "dyn-clone", - "ref-cast", "serde", "serde_json", + "ts-rs", ] [[package]] -name = "schemars_derive" -version = "0.8.22" +name = "security-framework" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 2.0.117", + "bitflags 2.10.0", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", ] -[[package]] -name = "scoped-tls" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sdd" -version = "4.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ca0e33fc1ae39e36b2d1fdfc8ee470b26397b642ff87572a59a36ff4f2340b" - [[package]] name = "security-framework" version = "3.6.0" @@ -6554,7 +7863,7 @@ checksum = "48b85e25e8a1fc13928885e8bf13abe8a09e15c46993aed05d6405f7755d6e20" dependencies = [ "httpdate", "reqwest 0.12.22", - "rustls", + "rustls 0.23.40", "sentry-anyhow", "sentry-backtrace", "sentry-contexts", @@ -6563,7 +7872,7 @@ dependencies = [ "sentry-panic", "sentry-tracing", "tokio", - "ureq", + "ureq 3.1.2", ] [[package]] @@ -6874,7 +8183,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -6885,7 +8194,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -6904,6 +8213,16 @@ 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-registry" version = "1.4.5" @@ -6919,12 +8238,22 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1e303f8205714074f6068773f0e29527e0453937fe837c9717d066635b65f31" dependencies = [ - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", - "signature", + "signature 2.2.0", "zeroize", ] +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "signature" version = "2.2.0" @@ -6964,6 +8293,18 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" +[[package]] +name = "simple_asn1" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d585997b0ac10be3c5ee635f1bab02d512760d14b7c468801ac8a01d9ae5f1d" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.12", + "time", +] + [[package]] name = "siphasher" version = "1.0.1" @@ -7033,6 +8374,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + [[package]] name = "spin" version = "0.12.0" @@ -7051,6 +8404,16 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] + [[package]] name = "spki" version = "0.7.3" @@ -7058,7 +8421,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", ] [[package]] @@ -7242,6 +8605,17 @@ dependencies = [ "system-configuration-sys", ] +[[package]] +name = "system-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.10.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + [[package]] name = "system-configuration-sys" version = "0.6.0" @@ -7304,7 +8678,7 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix", + "rustix 1.0.8", "windows-sys 0.59.0", ] @@ -7344,7 +8718,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed" dependencies = [ - "rustix", + "rustix 1.0.8", "windows-sys 0.59.0", ] @@ -7461,9 +8835,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa 1.0.15", @@ -7471,22 +8845,22 @@ dependencies = [ "num-conv", "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" dependencies = [ "num-conv", "time-core", @@ -7618,21 +8992,31 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27d684bad428a0f2481f42241f821db42c54e2dc81d8c00db8536c506b0a0144" dependencies = [ "const-oid", - "ring", - "rustls", + "ring 0.17.14", + "rustls 0.23.40", "tokio", "tokio-postgres", - "tokio-rustls", + "tokio-rustls 0.26.2", "x509-cert", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls", + "rustls 0.23.40", "tokio", ] @@ -7681,11 +9065,11 @@ checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" dependencies = [ "futures-util", "log", - "rustls", - "rustls-native-certs", + "rustls 0.23.40", + "rustls-native-certs 0.8.3", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tungstenite", "webpki-roots 0.26.11", ] @@ -7716,10 +9100,10 @@ dependencies = [ "http 1.3.1", "httparse", "rand 0.8.5", - "ring", + "ring 0.17.14", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-util", "webpki-roots 0.26.11", ] @@ -7774,7 +9158,7 @@ dependencies = [ "axum 0.7.9", "base64 0.22.1", "bytes", - "h2", + "h2 0.4.11", "http 1.3.1", "http-body 1.0.1", "http-body-util", @@ -8023,6 +9407,29 @@ dependencies = [ "tokio", ] +[[package]] +name = "ts-rs" +version = "10.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e640d9b0964e9d39df633548591090ab92f7a4567bc31d3891af23471a3365c6" +dependencies = [ + "lazy_static", + "thiserror 2.0.12", + "ts-rs-macros", +] + +[[package]] +name = "ts-rs-macros" +version = "10.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9d8656589772eeec2cf7a8264d9cda40fb28b9bc53118ceb9e8c07f8f38730" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "termcolor", +] + [[package]] name = "tungstenite" version = "0.26.2" @@ -8035,7 +9442,7 @@ dependencies = [ "httparse", "log", "rand 0.9.2", - "rustls", + "rustls 0.23.40", "rustls-pki-types", "sha1", "thiserror 2.0.12", @@ -8173,7 +9580,7 @@ dependencies = [ "rivet-test-deps-docker", "rivet-ups-protocol", "rivet-util", - "scc", + "scc 3.6.12", "serde", "serde_json", "sha2", @@ -8195,12 +9602,36 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "once_cell", + "rustls 0.23.40", + "rustls-pki-types", + "serde", + "serde_json", + "url", + "webpki-roots 0.26.11", +] + [[package]] name = "ureq" version = "3.1.2" @@ -8210,8 +9641,8 @@ dependencies = [ "base64 0.22.1", "log", "percent-encoding", - "rustls", - "rustls-pemfile", + "rustls 0.23.40", + "rustls-pemfile 2.2.0", "rustls-pki-types", "ureq-proto", "utf-8", @@ -8302,6 +9733,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "v8" +version = "130.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a511192602f7b435b0a241c1947aa743eb7717f20a9195f4b5e8ed1952e01db1" +dependencies = [ + "bindgen 0.70.1", + "bitflags 2.10.0", + "fslock", + "gzip-header", + "home", + "miniz_oxide 0.7.4", + "once_cell", + "paste", + "which", +] + [[package]] name = "valuable" version = "0.1.1" @@ -8370,6 +9818,12 @@ 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.10.1" @@ -8614,6 +10068,18 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "which" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +dependencies = [ + "either", + "home", + "rustix 0.38.44", + "winsafe", +] + [[package]] name = "whoami" version = "1.6.0" @@ -8635,6 +10101,12 @@ dependencies = [ "safe_arch", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -8812,6 +10284,17 @@ dependencies = [ "windows-strings 0.4.2", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -8830,6 +10313,15 @@ 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.1.0" @@ -8849,6 +10341,15 @@ 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.48.0" @@ -8876,15 +10377,6 @@ 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.2", -] - [[package]] name = "windows-sys" version = "0.61.2" @@ -8918,29 +10410,13 @@ 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_gnullvm", "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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" -dependencies = [ - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", -] - [[package]] name = "windows-threading" version = "0.1.0" @@ -8962,12 +10438,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -8980,12 +10450,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -8998,24 +10462,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - [[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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -9028,12 +10480,6 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -9046,12 +10492,6 @@ 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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -9064,12 +10504,6 @@ 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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -9082,18 +10516,18 @@ 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.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" - [[package]] name = "winnow" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "wit-bindgen" version = "0.51.0" @@ -9210,8 +10644,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1301e935010a701ae5f8655edc0ad17c44bad3ac5ce8c39185f75453b720ae94" dependencies = [ "const-oid", - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", "tls_codec", ] @@ -9222,9 +10656,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" dependencies = [ "libc", - "rustix", + "rustix 1.0.8", ] +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "xxhash-rust" version = "0.8.15" diff --git a/Cargo.toml b/Cargo.toml index a4d2c4d9df..3889339209 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,7 @@ members = [ "rivetkit-rust/packages/actor-persist", "rivetkit-rust/packages/client", "rivetkit-rust/packages/rivetkit", + "rivetkit-rust/packages/rivetkit-agent-os", "rivetkit-rust/packages/rivetkit-core", "rivetkit-rust/packages/shared-types", "rivetkit-typescript/packages/rivetkit-napi", diff --git a/PLAN2.md b/PLAN2.md new file mode 100644 index 0000000000..7dd73a671c --- /dev/null +++ b/PLAN2.md @@ -0,0 +1,276 @@ +# Plan 2 — agent-os integration + +NAPI-only Rust-backed agent-os actor for rivetkit. Written after a first attempt overcomplicated the architecture; reviewed by 5 subagents (architecture, feasibility, test strategy, risks, adversarial) and corrected based on findings. + +--- + +## Scope + +**In:** NAPI-only agent-os actor. JS users on the native runtime call `agentOs(config)` and get a working Rust-backed actor. + +**Out:** wasm runtime (agent-os-client uses `tokio::process`, native-only). Rust-direct registration (no caller). Future actor kinds beyond agent-os. + +--- + +## Architectural principles + +1. **One NAPI class, one register method.** No separate `NapiAgentOsDefinition`. agent-os produces a `NapiActorFactory` via a static constructor and registers through the existing `register(name, &NapiActorFactory)` path. +2. **Discriminator via marker field, not class hierarchy.** `ActorDefinition` grows an optional `nativeFactoryBuilder?: (runtime: CoreRuntime) => ActorFactoryHandle` field. Two call sites (registry loop, engine driver ladder) check that one field. +3. **Validation drives ordering.** Get one test through the driver suite (the real end-to-end gate) before building anything else. No structural scaffolding before signal. +4. **No premature abstractions.** No `HostToolInvoker` trait. No `entry_fn` + `actor_config` + `build_core_factory` three-way API — just `build_core_factory(config) -> CoreActorFactory`. No event broadcast constants without feeders. Wire DTOs land in the same PR as their first feeder action, never alone. +5. **User config is load-bearing from Day 1 for the serializable subset.** `software`, `loopbackExemptPorts`, `allowedNodeBuiltins`, `moduleAccessCwd`, `additionalInstructions`, `permissions`, `rootFilesystem`, `sidecar.Shared`, plain `mounts` flow through. Non-serializable fields (`mounts[].driver` callbacks, `scheduleDriver`, `sidecar.Explicit`, `onBeforeConnect`/`onSessionEvent`/`onPermissionRequest` callbacks) **fail loud** with a clear "not yet supported" error. Never silently drop user config. +6. **TypeScript is the source of truth for the wire protocol.** Byte payloads use the rivetkit convention `["$Uint8Array", base64]` because that's what TS emits and expects. The Rust framework must mirror it on both encode and decode sides (see RIVETKIT_RUST_FIX.md — this is a framework fix, not agent-os work). + +--- + +## Architecture + +### Rust crate `rivetkit-agent-os` (native-only) + +``` +src/ + lib.rs — pub fn build_core_factory(config: AgentOsActorConfig) + -> CoreActorFactory + actor.rs — AgentOsActor marker (impl Actor) + config.rs — AgentOsActorConfig (closure factory for AgentOsConfig + so we can rebuild it across sleep/wake cycles) + run.rs — event loop: ensure_vm, shutdown_vm, dispatch + actions/ + mod.rs — pub async fn dispatch(ctx, vm, action) + filesystem.rs — read_file, write_file, ... + session.rs — create_session, send_prompt, ... + process.rs — exec, spawn, ... + shell.rs — open_shell, ... + cron.rs — schedule_cron, ... + network.rs — vm_fetch + preview.rs — create_signed_preview_url, expire_signed_preview_url + events.rs — Subscriptions: per-VM cron broadcast + per-entity + session/process/shell broadcasts wired from + dispatcher arms when entities are created + persistence.rs — MIGRATION_SQL + migrate(sql) + preview_http.rs — handle(ctx, vm, http) for /fetch/{token}/path + state.rs — RunState counters (populated by dispatcher arms) +``` + +One public function: `build_core_factory(config) -> CoreActorFactory`. No separate `AgentOsActorDefinition` struct, no `entry_fn`/`actor_config` accessors. + +### NAPI binding `rivetkit-napi` + +Add a static constructor to the existing `NapiActorFactory`: + +```rust +#[napi] +impl NapiActorFactory { + /// Existing constructor for JS-callback actors. + #[napi(constructor)] + pub fn constructor(callbacks: JsObject, config: Option) + -> napi::Result { ... } + + /// New: build a NapiActorFactory backed by rivetkit-agent-os. + /// Walks `tool_callbacks` JsObject to extract execute fns, + /// builds AgentOsActorConfig from `options`, calls + /// `rivetkit_agent_os::build_core_factory(config)`. + #[napi(factory)] + pub fn from_agent_os( + options: NapiAgentOsOptions, + tool_callbacks: Option, + ) -> napi::Result { ... } +} +``` + +Zero changes to the existing `register` method. + +### TypeScript `rivetkit` + +Three small changes: + +**Change 1: `AnyActorDefinition` and `ActorDefinition` grow an optional field.** +```ts +export interface AnyActorDefinition { + readonly config: any; + readonly nativeFactoryBuilder?: (runtime: CoreRuntime) => ActorFactoryHandle; +} +``` + +The `ActorDefinition` class gets the field as a defaultable instance property so `instanceof ActorDefinition` still works downstream. + +**Change 2: `agentOs(config)` returns a real `ActorDefinition` instance with `nativeFactoryBuilder` set.** + +Lazy — `agentOs()` runs at module load time before the runtime is selected. The builder runs at registration time when the runtime is known. Mirrors the Cloudflare Workers global-scope rule. + +```ts +// src/agent-os/actor/index.ts +export function agentOs(config): ActorDefinition<...> { + const parsed = agentOsActorConfigSchema.parse(config); + const nativeFactoryBuilder = (runtime: CoreRuntime): ActorFactoryHandle => { + if (runtime.kind !== "napi") { + throw new Error( + "agentOs() requires the NAPI runtime; the wasm runtime does not support agent-os actors", + ); + } + const options = toNapiAgentOsOptions(parsed); + const toolCallbacks = extractToolCallbacks(parsed); + return runtime.createAgentOsFactory!(options, toolCallbacks); + }; + const definition = new ActorDefinition({} as any); + (definition as any).nativeFactoryBuilder = nativeFactoryBuilder; + return definition; +} +``` + +**Change 3: Dispatch lives inside `runtime.registerActor`. The registry loop is a one-liner.** + +```ts +// registry/native.ts::buildConfiguredRegistry +for (const [name, definition] of Object.entries(config.use)) { + runtime.registerActor(registry, name, definition, config); +} +``` + +```ts +// registry/napi-runtime.ts::NapiCoreRuntime +registerActor( + registry: RegistryHandle, + name: string, + definition: AnyActorDefinition, + registryConfig: RegistryConfig, +): void { + const factory = (definition as any).nativeFactoryBuilder + ? (definition as any).nativeFactoryBuilder(this) + : buildNativeFactory(this, registryConfig, definition); + asNativeRegistry(registry).register(name, asNativeFactory(factory)); +} +``` + +`registerActor`'s signature widens to take an `AnyActorDefinition` + the `RegistryConfig`. Wasm-runtime mirrors the same dispatch with its own fallback (or throws for the agent-os case). + +**Engine driver ladder** (`drivers/engine/actor-driver.ts`) — between dynamic and static branches: +```ts +} else if ((definition as any).nativeFactoryBuilder) { + // Rust-backed actor; CoreActorFactory handles everything. + // handler.actor stays undefined. +} else if (isStaticActorDefinition(definition)) { + ... +} +``` + +`handler.actor` undefined is safe because the only blocking accessor (`#loadActorHandler`) is called from hibernating-WS code that agent-os doesn't use. + +**Keep on `CoreRuntime`:** +- `createAgentOsFactory?(options, toolCallbacks): ActorFactoryHandle` — named capability seam per `rivetkit-typescript/CLAUDE.md` "Runtime Boundary" rule. Wasm throws "not supported." + +**Delete:** +- Legacy `agentOs()` body (the `actor({...})` callback bag). +- `src/agent-os/actor/{cron,db,filesystem,network,preview,process,session,shell}.ts` (the buildXActions modules). + +--- + +## Implementation order + +Each phase has its own e2e gate. See TODOLIST.md for the concrete checklist. + +**Phase 0** — prerequisites + smoke test target. + +**RIVETKIT_RUST_FIX** — precondition. Framework byte-encoding fix (see RIVETKIT_RUST_FIX.md). + +**Phase 1a** — Rust crate skeleton + ONE action (`readFile`) + dispatcher e2e against real sidecar. No NAPI, no engine, no JS. + +**Phase 1b** — NAPI `NapiActorFactory::from_agent_os` + focused Vitest. 1a still passes. + +**Phase 1c** — JS shim + ladder branches + first driver-suite cell green (bare encoding). 1a + 1b still pass. *This is the "architecture is real" milestone.* + +**Phase 2** — cross-encoding parity (cbor + json work without per-action fixes thanks to RIVETKIT_RUST_FIX). Test one structured-object action across all three encodings. + +**Phase 3** — action surface buildout, category by category. Driver suite no-bail at each category. + +**Phase 4** — persistence + preview HTTP handler. + +**Phase 5** — toolkit callbacks (highest risk). + +**Phase 6** — cleanup + docs. + +--- + +## Tests + +| Layer | Test file | Purpose | +|---|---|---| +| Unit | `rivetkit-agent-os/tests/persistence.rs` | MIGRATION_SQL valid via rusqlite | +| Unit | `rivetkit-agent-os/tests/preview_http.rs` | path parser + token generator | +| Helper-e2e | `rivetkit-agent-os/tests/end_to_end.rs` | helpers against real sidecar (gated) | +| Dispatcher-e2e | `rivetkit-agent-os/tests/dispatcher_e2e.rs` | dispatch arms against real sidecar (gated) | +| Cutover | `rivetkit-typescript/.../tests/agent-os-cutover.test.ts` | `agentOs()` returns expected shape | +| End-to-end | `rivetkit-typescript/.../tests/driver/actor-agent-os.test.ts` | full chain via engine + sidecar | +| Sleep/wake | `rivetkit-typescript/.../tests/driver/actor-agent-os-sleep.test.ts` | VM recreates with user config on wake; catches the "default VM after wake" regression | + +**Inner loop:** `dispatcher_e2e.rs` is the per-save gate. ~2-second feedback against a real sidecar. No engine. + +**Boundary gate:** the driver suite at phase boundaries. **No `--bail`** at boundaries — the full suite must be green. `--bail=1` is for local fix-and-retry only. + +--- + +## Review checklist (every PR) + +**No premature scaffolding:** +- [ ] Every wire DTO receives a real value from a real caller in this PR. No DTOs added "for the next action." +- [ ] Every event broadcast name constant is fed by an actual `Subscriptions::spawn_*` call. +- [ ] Every `RunState` counter is incremented in the dispatcher arm that creates the entity AND decremented in the arm that destroys it. +- [ ] No abstraction (trait, generic wrapper) with one concrete impl. +- [ ] No orphan methods left after refactors (grep the workspace). + +**Full data flow, no stubs:** +- [ ] User config flows from `agentOs(config)` → `NapiAgentOsOptions` → `AgentOsActorConfig` → `AgentOsConfig` for every plain-data field, OR an explicit fail-loud error is raised for non-serializable fields. + +**Wire format hygiene:** +- [ ] Byte payloads (top-level and nested) go through the RIVETKIT_RUST_FIX `Action::ok` wrapping. Never raw `serde_bytes::ByteBuf` or `Vec` to client. + +**Cleanup discipline:** +- [ ] No legacy code paths left as "fallback" — if the new path is the path, the old path is deleted in the same PR. +- [ ] No diagnostic `console.error` / `tracing::debug!` added during debugging that survives commit. + +**Verified, not just compiled:** +- [ ] No commit message says "verified" without naming the exact test (file + name) that was run and passed. +- [ ] `cargo check` and `pnpm tsc --noEmit` are baselines, not gates. The actual gate is the e2e test from the relevant sub-phase. + +**Sub-phase regression check:** +- [ ] All prior sub-phase e2e tests still pass after each subsequent change. + +**Build hygiene:** +- [ ] If Rust touched, NAPI artifact rebuilt (`pnpm --filter @rivetkit/rivetkit-napi build:force`). +- [ ] Driver suite ran at phase boundaries without `--bail`. + +Note on `pnpm build` for `rivetkit/`: not required for src edits — `vitest.config.ts` uses `vite-tsconfig-paths` so `rivetkit/agent-os` resolves to `src/agent-os/index.ts` during test runs. `dist/` build matters only for external consumers. + +--- + +## Resolved questions + +1. **`nativeFactoryBuilder` typing:** `ActorFactoryHandle` opaque. The engine driver passes it through to `runtime.registerActor` and never inspects. +2. **Construction timing:** lazy builder. `agentOs(config)` runs at module load before runtime selection. The registration loop calls the builder once the runtime is known. +3. **Wire DTOs location:** inline in each action module. +4. **`skip.agentOs` on wasm:** `createWasmDriverTestConfig` defaults it to `true`. + +## Open question to resolve at Phase 1 start + +**Fail-loud strategy for non-serializable config fields.** Decide which fields throw vs. silently drop with a warning. The conservative call is fail-loud for everything that can't round-trip cleanly. Decide before Phase 1b's `NapiAgentOsOptions` shape lands. + +--- + +## Slip risk + +| Phase | Slip risk | +|---|---| +| 0 | Low — clean state | +| RIVETKIT_RUST_FIX | Medium — custom serializer adapter; well-defined | +| 1a | Low — `dispatcher_e2e.rs` is a focused gate | +| 1b | Low — `#[napi(factory)]` is established | +| 1c | Low if discovered; Medium if a new layer-mismatch surfaces | +| 2 | Low — Parts 1+2 of the fix handle byte encoding | +| 3 | Low — pattern repeats per arm | +| 4 | Low | +| 5 | **High** — TSF lifetime + cancellation token bridging across `napi_actor_events.rs` is a known minefield | +| 6 | Low | + +**Rollback for high-risk phases:** +- RIVETKIT_RUST_FIX: ship Phase 3 with `bare`-encoding-only and document. +- Phase 5: ship Phases 0–4 without host-tool support, document, merge. The other 50+ actions are usable without tools. diff --git a/RIVETKIT_RUST_FIX.md b/RIVETKIT_RUST_FIX.md new file mode 100644 index 0000000000..ce9436dce8 --- /dev/null +++ b/RIVETKIT_RUST_FIX.md @@ -0,0 +1,226 @@ +# Rivetkit-rust fix: `$Uint8Array` encoding parity with TS + +A narrow framework fix to bring rivetkit-rust to parity with rivetkit-typescript on **`Uint8Array` byte payloads only**. Precondition for the agent-os integration (PLAN2.md). + +**Explicitly scope-limited:** the only TS convention this implements is `JSON_COMPAT_UINT8_ARRAY`. Other JSON-compat types (`$BigInt`, `$ArrayBuffer`, `$Undefined`, `$Set`, `Date`, `RegExp`, `Error`, `Map`, etc.) are **not** in scope. They can be added when a real consumer needs them. agent-os only returns `Uint8Array`-shaped bytes (`readFile`, `vmFetch.body`, batch-read `content`); nothing else. + +**TS is the source of truth.** TS sits on at least one end of every action call. The wire convention `["$Uint8Array", base64]` is what TS emits and what TS expects. The Rust framework mirrors it on both encode and decode sides. + +--- + +## The gap + +TS rivetkit handles byte payloads transparently: + +```ts +readFile: async (c, path) => agentOs.readFile(path), // returns Uint8Array +``` + +The user returns a `Uint8Array`, the framework wraps as `["$Uint8Array", base64]`, the receiving client revives. Works across bare/cbor/json. + +Rust rivetkit has no equivalent: + +```rust +action.ok(&bytes); // Vec +``` + +Bytes round-trip cleanly on bare. On cbor and json they get mangled into a number array because the engine decodes through `serde_json::Value` (no byte variant) at `rivetkit-core/src/registry/inspector.rs::decode_cbor_json_or_null`. + +This is a framework-feature-parity miss. Every Rust-defined actor that returns bytes is silently broken on non-bare encodings. + +--- + +## TS reference + +`rivetkit-typescript/packages/rivetkit/src/common/encoding.ts:14`: + +```ts +const JSON_COMPAT_UINT8_ARRAY = "$Uint8Array"; // capital U +``` + +Encode (`encodeJsonCompatValue`): +```ts +if (input instanceof Uint8Array) { + return [JSON_COMPAT_UINT8_ARRAY, base64EncodeUint8Array(input)]; +} +``` + +Decode (`reviveJsonCompatValue`): +```ts +if (input[0] === JSON_COMPAT_UINT8_ARRAY) { + return base64DecodeToUint8Array(input[1]); +} +``` + +Applied recursively to nested byte fields. + +--- + +## Proposed fix + +Three parts. Encode + decode are essential for full parity; the engine cleanup is a follow-up. + +### Part 1 — Encode side: auto-wrap in `Action::ok` + +**Goal:** mirror TS's `Uint8Array` wrapping. When a user-returned value contains byte payloads, wrap them as `["$Uint8Array", base64]` before CBOR-encoding. Recurse into nested fields. + +```rust +// rivetkit-rust/packages/rivetkit/src/encoding.rs (new) +const JSON_COMPAT_UINT8_ARRAY: &str = "$Uint8Array"; + +pub(crate) fn encode_json_compat(value: &T, writer: &mut W) -> anyhow::Result<()> +where + T: Serialize, + W: std::io::Write, +{ + let mut adapter = JsonCompatAdapter::new(writer); + value.serialize(&mut adapter)?; + Ok(()) +} +``` + +The adapter intercepts `serialize_bytes` calls (`serde_bytes::ByteBuf`, `serde_bytes::Bytes`, `&[u8]` via `serde_bytes`) and emits the 2-element array shape. Plain `Vec` keeps default behavior (CBOR array) — users opting into `Uint8Array` semantics annotate `#[serde(with = "serde_bytes")]`. Matches TS's explicit `Uint8Array` vs other-typed-array distinction. + +All other serde calls pass through to ciborium unchanged. No `$BigInt`, `$ArrayBuffer`, `$Set`, `$Undefined` handling. Other types encode as ciborium's default — same as today. + +Swap `Action::ok` (and `WfHistory::reply`, `WfReplay::reply`) to use `encode_json_compat` instead of raw `ciborium::into_writer`. State serialization (`SerializeState::save`) and queue payloads stay raw — consumed by Rust, not JS. + +### Part 2 — Decode side: auto-unwrap in `rivetkit-client` + +**Goal:** mirror TS's `Uint8Array` revival. When a Rust client receives an action response containing `["$Uint8Array", base64]`, hand the caller bytes instead of the literal array. + +Where: `rivetkit-rust/packages/client/`. + +```rust +// rivetkit-rust/packages/client/src/encoding.rs (new) +pub(crate) fn revive_json_compat(value: serde_json::Value) -> serde_json::Value { + match value { + serde_json::Value::Array(items) if is_uint8_array_tag(&items) => { + // ["$Uint8Array", base64] → bytes (whatever shape this crate uses) + } + serde_json::Value::Array(items) => serde_json::Value::Array( + items.into_iter().map(revive_json_compat).collect(), + ), + serde_json::Value::Object(_) => /* recurse */ value, + other => other, + } +} + +fn is_uint8_array_tag(items: &[serde_json::Value]) -> bool { + items.len() == 2 + && items[0].as_str() == Some("$Uint8Array") + && items[1].is_string() +} +``` + +Other tagged arrays (`["$BigInt", ...]`, `["$Set", ...]`) and non-tagged arrays pass through unchanged. + +Hook into the action-response decode site. Confirm `rivetkit-client`'s response shape during implementation. + +### Part 3 — Engine routing cleanup (follow-up) + +**Goal:** `rivetkit-core` shouldn't lossy-decode action responses through `serde_json::Value` in the first place. + +Audit callers of `decode_cbor_json_or_null` (`rivetkit-core/src/registry/inspector.rs:598-609`). Split: +- Inspector display path: keep `serde_json::Value` intermediate (browser tab shows bytes as base64 or hex). +- Action-response forward path: sibling that forwards encoded bytes opaquely. + +Can land any time. Parts 1+2's wrapping convention survives the lossy decode anyway — `["$Uint8Array", base64]` is JSON-native. + +--- + +## Tests + +### Part 1 — Rust encode (`rivetkit-rust/packages/rivetkit/tests/encoding.rs`) + +```rust +#[test] +fn byte_buf_wraps_as_json_compat_uint8_array() { ... } + +#[test] +fn nested_byte_field_in_struct_wraps() { + #[derive(Serialize)] + struct Reply { status: u16, body: serde_bytes::ByteBuf } + // assert intermediate["body"][0] == "$Uint8Array" + // assert intermediate["body"][1] == base64 +} + +#[test] +fn plain_vec_u8_stays_as_array() { ... } + +#[test] +fn non_byte_types_pass_through_unchanged() { ... } +``` + +### Part 2 — Rust decode (`rivetkit-rust/packages/client/tests/encoding.rs`) + +```rust +#[test] +fn json_compat_uint8_array_revives_to_bytes() { ... } + +#[test] +fn nested_byte_field_revives_inside_struct() { ... } + +#[test] +fn non_byte_arrays_pass_through() { ... } + +#[test] +fn unrelated_tagged_arrays_pass_through() { ... } +``` + +### Round-trip parity + +```rust +#[test] +fn encode_then_decode_round_trips_bytes() { + let original = b"round-trip data".to_vec(); + let value = serde_bytes::ByteBuf::from(original.clone()); + let encoded = encode_json_compat_to_vec(&value).unwrap(); + let intermediate: serde_json::Value = + ciborium::from_reader(&encoded[..]).unwrap(); + let revived = revive_json_compat(intermediate); + assert_eq!(revived_as_bytes(&revived).unwrap(), original); +} +``` + +### TS parity (cross-language) + +A Rust test in `rivetkit-rust/packages/rivetkit/tests/encoding_fixtures.rs` writes Rust-encoded output to a fixture file. A Vitest test in `rivetkit-typescript/packages/rivetkit/tests/byte-encoding-parity.test.ts` reads the fixture and asserts: +- TS `encodeJsonCompatValue` produces the same shape on the same input. +- TS `reviveJsonCompatValue` revives Rust-encoded bytes correctly. + +Keeps both sides honest. + +### End-to-end (after Parts 1+2 land) + +The agent-os driver suite cell `writeFile and readFile round-trip` should pass for cbor and json without any agent-os-specific changes. That's the all-the-way-through validation. + +--- + +## Scope + +### Part 1 — Encode (rivetkit-rust) + +- `rivetkit-rust/packages/rivetkit/src/encoding.rs` (new, ~150 lines). +- `rivetkit-rust/packages/rivetkit/src/event.rs` (~5 line change in `Action::ok`, `WfHistory::reply`, `WfReplay::reply`). +- `rivetkit-rust/packages/rivetkit/src/lib.rs` (add `pub mod encoding`). +- `rivetkit-rust/packages/rivetkit/tests/encoding.rs` (new). + +### Part 2 — Decode (rivetkit-client) + +- `rivetkit-rust/packages/client/src/encoding.rs` (new, ~120 lines). +- The action-response decode site (find via grep). +- `rivetkit-rust/packages/client/tests/encoding.rs` (new). + +### Part 3 — Engine cleanup (rivetkit-core, follow-up) + +- `rivetkit-core/src/registry/inspector.rs` (split function), plus every caller. + +--- + +## Ordering + +1. Land Parts 1 + 2 in `rivetkit-rust`. +2. Verify via the focused Rust unit tests, the round-trip test, and the cross-language TS-parity test. +3. Then proceed with PLAN2's Phase 1 (agent-os bones). +4. Part 3 (engine routing cleanup) can land any time, before or after agent-os ships. diff --git a/TODOLIST.md b/TODOLIST.md new file mode 100644 index 0000000000..8703772865 --- /dev/null +++ b/TODOLIST.md @@ -0,0 +1,415 @@ +# TODOLIST for PLAN2 + +Phase-by-phase task list. Each phase ends with an **e2e gate** that must pass before moving on, and a **STOP — discuss** marker. + +**E2E maintained at every STOP.** A regression in an earlier phase blocks progress in the current phase. **Feature debt is OK; e2e debt is not.** + +Reference: PLAN2.md (design), RIVETKIT_RUST_FIX.md (framework precondition). + +--- + +## Phase 0 — Prerequisites + smoke test target + +### Tasks + +- [ ] Verify prereqs all build: + - [ ] `cargo check -p rivetkit` + - [ ] `cargo check -p rivetkit-core` + - [ ] `cargo build -p agent-os-sidecar` + - [ ] `cargo build -p rivet-engine` + - [ ] `pnpm install` at root + - [ ] `pnpm -r --filter @rivetkit/workflow-engine --filter @rivetkit/virtual-websocket --filter @rivetkit/engine-envoy-protocol build` +- [ ] Confirm test fixture `rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/agent-os.ts` exists (it's just `agentOs({ options: { software: [common] } })` against legacy code). +- [ ] Confirm `createWasmDriverTestConfig` defaults `skip.agentOs = true` (sensible default; add if missing). +- [ ] Write the driver-suite smoke test target as a placeholder in `rivetkit-typescript/packages/rivetkit/tests/driver/actor-agent-os.test.ts`: + ```ts + test("writeFile and readFile round-trip", async (c) => { + const { client } = await setupDriverTest(c, { + ...driverTestConfig, + useRealTimers: true, + }); + const actor = client.agentOsTestActor.getOrCreate([crypto.randomUUID()]); + await actor.writeFile("/home/user/hello.txt", "hello world"); + const data = await actor.readFile("/home/user/hello.txt"); + expect(new TextDecoder().decode(data)).toBe("hello world"); + }, 60_000); + ``` + +### E2E gate + +The smoke test fails because no implementation exists yet — that's expected. The test framework must be operational (failure mode is "actor not registered" or "no agentOs implementation," not "test runner broken"). + +### STOP — discuss + +Confirm prereqs all build. Confirm the smoke test target is runnable (even if failing). Decide if anything in the prereq gate failed and how to handle. + +--- + +## RIVETKIT_RUST_FIX — precondition + +Land before Phase 1. Three parts; Parts 1+2 essential, Part 3 follow-up. + +### Tasks — Part 1 (encode) + +- [ ] Create `rivetkit-rust/packages/rivetkit/src/encoding.rs` with `JsonCompatAdapter` serializer that intercepts `serialize_bytes` and emits `["$Uint8Array", base64]`. Only the Uint8Array case. +- [ ] Add `JSON_COMPAT_UINT8_ARRAY = "$Uint8Array"` const (capital U). +- [ ] Add `pub mod encoding;` to `rivetkit-rust/packages/rivetkit/src/lib.rs`. +- [ ] Swap `encode_cbor` for `encode_json_compat` in `Action::ok` (`event.rs:196`). +- [ ] Audit other callers of `encode_cbor` in `event.rs` (`WfHistory::reply`, `WfReplay::reply`). Swap if they forward to JS clients. +- [ ] Add doc-comment to `lib.rs` referencing the TS source-of-truth convention. + +### Tasks — Part 2 (decode) + +- [ ] Create `rivetkit-rust/packages/client/src/encoding.rs` with `revive_json_compat` walker. Detect `["$Uint8Array", base64]` tagged arrays; recurse; pass through everything else. +- [ ] Hook into the action-response decode site. Find via grep for action result deserialization in `rivetkit-rust/packages/client/`. + +### Tasks — Tests + +- [ ] `rivetkit-rust/packages/rivetkit/tests/encoding.rs`: + - [ ] `byte_buf_wraps_as_json_compat_uint8_array` + - [ ] `nested_byte_field_in_struct_wraps` + - [ ] `plain_vec_u8_stays_as_array` + - [ ] `non_byte_types_pass_through_unchanged` +- [ ] `rivetkit-rust/packages/client/tests/encoding.rs`: + - [ ] `json_compat_uint8_array_revives_to_bytes` + - [ ] `nested_byte_field_revives_inside_struct` + - [ ] `non_byte_arrays_pass_through` + - [ ] `unrelated_tagged_arrays_pass_through` +- [ ] Round-trip test (encode then decode) asserting original bytes recovered. +- [ ] Cross-language parity test: + - [ ] Rust test writes fixture file. + - [ ] Vitest test reads fixture, asserts TS `encodeJsonCompatValue` matches and TS `reviveJsonCompatValue` revives correctly. + +### E2E gate + +- [ ] All Rust encoding tests pass: `cargo test -p rivetkit --test encoding`. +- [ ] All Rust client decoding tests pass: `cargo test -p rivetkit-client --test encoding`. +- [ ] Cross-language fixture parity test passes. + +### STOP — discuss + +Framework fix is solid. Confirm before adding agent-os on top. + +--- + +## Phase 1a — Rust crate + one action, dispatcher-level e2e + +### Tasks + +- [ ] Create new crate at `rivetkit-rust/packages/rivetkit-agent-os/`: + - [ ] `Cargo.toml` with deps on `rivetkit`, `rivetkit-core`, `agent-os-client`, `anyhow`, `tokio`, `tracing`, `serde`, `serde_bytes`, `ciborium`, `futures`. + - [ ] Add to root workspace members in `Cargo.toml`. + - [ ] May need to re-pin hickory-resolver to `0.26.0-beta.3` via `cargo update --precise` if the lockfile resolves to a newer beta. +- [ ] `src/actor.rs`: `AgentOsActor` marker struct implementing `Actor` with `Input=()`, `ConnParams=serde_json::Value`, `ConnState=()`, `Action=Raw`. +- [ ] `src/config.rs`: `AgentOsActorConfig` with `build_options: Arc AgentOsConfig + Send + Sync>` closure factory. Carries `SoftwareInput` with `command_dir: Option` field on the Rust DTO. +- [ ] `src/lib.rs`: `pub fn build_core_factory(config: AgentOsActorConfig) -> CoreActorFactory`. ONE public function. +- [ ] `src/run.rs`: event loop with `ensure_vm` (lazy bring-up, broadcasts `vmBooted`) and `shutdown_vm` (on Sleep/Destroy, broadcasts `vmShutdown`). +- [ ] `src/actions/mod.rs`: `pub async fn dispatch(ctx, vm, action)` with ONE arm: `readFile`. Uses `action.ok(&bytes)` which auto-wraps via the RIVETKIT_RUST_FIX adapter. + +### E2E gate — `rivetkit-agent-os/tests/dispatcher_e2e.rs` + +Gated on `AGENT_OS_SIDECAR_BIN` env var (skip if missing). + +- [ ] Build VM via `AgentOs::create(AgentOsConfig::default())`. +- [ ] Seed a known file via `vm.write_file(...)` directly (bypass dispatcher). +- [ ] Build synthetic `Action` event for `"readFile"`. +- [ ] Call `actions::dispatch(ctx, vm, action)`. +- [ ] Decode reply, assert bytes match what was written. + +Run with: +```sh +cargo build -p agent-os-sidecar +AGENT_OS_SIDECAR_BIN=$(pwd)/target/debug/agent-os-sidecar \ + cargo test -p rivetkit-agent-os --test dispatcher_e2e +``` + +Must pass. + +### STOP — discuss + +The Rust crate works against a real sidecar at the dispatcher layer. Confirm before adding NAPI. + +--- + +## Phase 1b — NAPI binding, NAPI-level e2e + +### Tasks + +- [ ] `NapiAgentOsOptions` `#[napi(object)]` in `rivetkit-typescript/packages/rivetkit-napi/src/actor_factory.rs` (or new module) carrying plain-data fields. JSON-envelope fields for nested shapes: + - `software_json: Option` (JSON-encoded `SoftwareInput[]`) + - `loopback_exempt_ports: Option>` + - `allowed_node_builtins: Option>` + - `module_access_cwd: Option` + - `additional_instructions: Option` + - `permissions_json: Option` + - `mounts_json: Option` (plain-data subset only) + - `root_filesystem_json: Option` + - `sidecar_json: Option` +- [ ] `NapiActorFactory::from_agent_os(options, tool_callbacks)` static `#[napi(factory)]` method: + - Walk `tool_callbacks` JsObject if provided to wrap `execute` fns as TSFs (or stub for now — Phase 5 fully wires this). + - Build `AgentOsActorConfig` from options. + - Call `rivetkit_agent_os::build_core_factory(config)`. + - Store `Arc` on `inner`. +- [ ] Fail-loud detection: if `options` contains markers for non-serializable fields (`mounts[].driver`, `scheduleDriver`, `sidecar.Explicit`, the three user callbacks), throw a clear "not yet supported on the Rust path" error. +- [ ] Rebuild NAPI: `pnpm --filter @rivetkit/rivetkit-napi build:force`. Verify `from_agent_os` appears in `index.d.ts` as a static method. + +### E2E gate — `rivetkit-typescript/packages/rivetkit-napi/tests/agent-os-factory.test.ts` + +- [ ] Import `NapiActorFactory` from `@rivetkit/rivetkit-napi`. +- [ ] Construct via `NapiActorFactory.fromAgentOs({ software_json: JSON.stringify([{ package: "@rivet-dev/agent-os-common", commandDir: "/path" }]) }, undefined)`. Assert non-null handle. +- [ ] Construct with deliberately bad config (non-data driver field). Assert throws fail-loud error. + +Plus: **Phase 1a's `dispatcher_e2e.rs` must still pass.** No regressions. + +### STOP — discuss + +NAPI binding works. The Rust crate is reachable from JS. Confirm before adding JS shim layer. + +--- + +## Phase 1c — JS shim + engine driver branches, full driver-suite e2e + +### Tasks + +- [ ] Add optional `nativeFactoryBuilder?: (runtime: CoreRuntime) => ActorFactoryHandle` to `AnyActorDefinition` interface in `actor/definition.ts`, and to `ActorDefinition` class as defaultable instance property. +- [ ] Rewrite `agentOs(config)` in `src/agent-os/actor/index.ts`: + - Parse config via existing `agentOsActorConfigSchema`. + - Build `nativeFactoryBuilder` lazy closure: + - Reject if `runtime.kind !== "napi"` with clear error. + - Build `NapiAgentOsOptions` from parsed config (thread `software` with `commandDir`). + - Extract tool callbacks from `parsed.options.toolKits` (stub for Phase 1c — pass `undefined`). + - Call `runtime.createAgentOsFactory(options, toolCallbacks)`, cast to `ActorFactoryHandle`. + - Return `new ActorDefinition({} as any)` with `nativeFactoryBuilder` set. +- [ ] Delete the legacy `agentOs()` body and the `buildXActions` imports in the same file. +- [ ] Delete `src/agent-os/actor/{cron,db,filesystem,network,preview,process,session,shell}.ts`. +- [ ] Update `src/agent-os/index.ts` barrel to drop the deleted exports. +- [ ] Add `createAgentOsFactory?(options, toolCallbacks): ActorFactoryHandle` to `CoreRuntime` interface in `registry/runtime.ts`. +- [ ] `napi-runtime.ts::NapiCoreRuntime`: + - [ ] Implement `createAgentOsFactory(options, toolCallbacks)` calling `NapiActorFactory.fromAgentOs(...)` and returning the handle. + - [ ] **Widen `registerActor` signature** to take `definition: AnyActorDefinition` + `registryConfig: RegistryConfig`. Inside, do the dispatch: `if (definition.nativeFactoryBuilder) → call it; else → buildNativeFactory`. +- [ ] `wasm-runtime.ts::WasmCoreRuntime`: + - [ ] Mirror the widened `registerActor` signature. For agent-os actors (`nativeFactoryBuilder` set), throw "not supported." +- [ ] `registry/native.ts::buildConfiguredRegistry`: loop becomes `runtime.registerActor(registry, name, definition, config)`. +- [ ] `drivers/engine/actor-driver.ts`: add branch between dynamic and static: + ```ts + } else if ((definition as any).nativeFactoryBuilder) { + // Rust-backed; CoreActorFactory handles everything. + logger().debug({ msg: "engine actor started (rust-native)", actorId, name, key }); + } + ``` +- [ ] Add `writeFile` arm to `actions::dispatch` in the Rust crate (smoke test needs both). + +### E2E gate — Phase 0's smoke test passes for bare encoding + +```sh +AGENT_OS_SIDECAR_BIN=$(pwd)/target/debug/agent-os-sidecar \ +RIVET_ENGINE_BINARY=$(pwd)/target/debug/rivet-engine \ + pnpm vitest run tests/driver/actor-agent-os.test.ts \ + --bail=1 -t "writeFile and readFile round-trip" +``` + +Bare encoding cell must pass. + +Plus: **Phase 1a tests AND Phase 1b test must still pass.** + +### STOP — discuss + +End-to-end works. JS → engine → NAPI → Rust → sidecar → VM → bytes back. **This is the milestone that means the architecture is real.** Confirm before expanding. + +--- + +## Phase 2 — Cross-encoding parity + +### Tasks + +- [ ] Verify Phase 0's smoke test passes for cbor encoding (should "just work" because RIVETKIT_RUST_FIX wraps bytes at the framework layer). +- [ ] Verify the same for json encoding. +- [ ] If either fails, narrow: was RIVETKIT_RUST_FIX applied to every relevant `encode_cbor` call? Did the dispatcher arm use `action.ok(&bytes)` correctly? +- [ ] Add one structured-object action (e.g. `stat`) to validate non-byte shapes round-trip across all three encodings. + +### E2E gate + +- [ ] `writeFile and readFile round-trip` passes for bare + cbor + json. +- [ ] `stat returns file metadata` passes for bare + cbor + json. +- [ ] All Phase 1 e2e gates still pass. + +### STOP — discuss + +Cross-encoding works. Confirm before bulk action buildout. + +--- + +## Phase 3 — Action surface buildout + +Build out the remaining ~49 actions. After each category, run the full driver suite **no-bail** to catch independent failures. + +### Filesystem + +- [ ] Add arms: `mkdir`, `readdir`, `stat`, `exists`, `move`, `deleteFile`, `readFiles`, `writeFiles`, `readdirRecursive`. +- [ ] Wire DTOs land with their first feeder arm. +- [ ] **Driver-suite gate for filesystem category (no `--bail`).** + +### Process + +- [ ] Add arms: `exec`, `spawn`, `waitProcess`, `killProcess`, `stopProcess`, `listProcesses`, `allProcesses`, `processTree`, `getProcess`, `writeProcessStdin`, `closeProcessStdin`. +- [ ] Wire `processOutput` + `processExit` broadcasts inside `spawn` / `exec` arms. +- [ ] Increment `RunState.active_processes` on spawn, decrement on exit. +- [ ] **Driver-suite gate for process category.** + +### Shell + +- [ ] Add arms: `openShell`, `writeShell`, `resizeShell`, `closeShell`. +- [ ] Wire `shellData` broadcast inside `openShell` arm. +- [ ] Increment/decrement `RunState.active_shells`. +- [ ] **Driver-suite gate for shell category.** + +### Session + +- [ ] Add arms: `createSession`, `sendPrompt`, `cancelPrompt`, `respondPermission`, `closeSession`, `destroySession`, `resumeSession`, `listSessions`, `getSession`, `setMode`, `getModes`, `setModel`, `setThoughtLevel`, `getConfigOptions`, `getEvents`, `getSequencedEvents`, `rawSend`, `listAgents`. +- [ ] Wire `sessionEvent` + `permissionRequest` broadcasts inside `createSession`. +- [ ] Increment/decrement `RunState.active_sessions`. +- [ ] **Driver-suite gate for session category.** + +### Cron + +- [ ] Add arms: `scheduleCron`, `listCronJobs`, `cancelCronJob`. +- [ ] Wire `cronEvent` broadcast (always-on per VM). +- [ ] **Driver-suite gate for cron category.** + +### Network + +- [ ] Add arm: `vmFetch`. +- [ ] **Driver-suite gate for network category.** + +### Misc session bookkeeping (persistence-backed) + +- [ ] `listPersistedSessions` querying `agent_os_sessions`. +- [ ] `getSessionEvents` querying `agent_os_session_events`. +- [ ] **Driver-suite gate.** + +### E2E gate (phase boundary) + +Full driver suite no-bail. All categories green except preview (Phase 4) and tool actions (Phase 5). + +Plus all earlier e2e gates still pass. + +### STOP — discuss + +Action surface built out and validated. Confirm before preview + toolkits. + +--- + +## Phase 4 — Persistence + preview + +### Tasks + +- [ ] `src/persistence.rs`: `MIGRATION_SQL` const with 4 agent-os tables + indexes (port from TS `actor/db.ts`). `pub const MIGRATION_SQL: &str`. +- [ ] `pub async fn migrate_actor(ctx: &Ctx) -> Result<()>` — calls `sql.exec(MIGRATION_SQL)` if SQLite enabled, idempotent. +- [ ] Call `migrate_actor` at the top of `run::run` before the event loop. +- [ ] `tests/persistence.rs` rusqlite unit tests covering migration validity. +- [ ] `src/actions/preview.rs`: `generate_token` + `create_signed_preview_url` + `expire_signed_preview_url`. SQLite-backed via `ctx.sql()`. +- [ ] `src/preview_http.rs`: `parse_fetch_path` + `handle(ctx, vm, http)` for `/fetch/{token}/path` URLs. **Forward all request headers** to the VM fetch (don't drop them). +- [ ] Wire preview handler into `run::run`'s `Event::Http` arm. +- [ ] `tests/preview_http.rs` unit tests for path parser + token generator. + +### E2E gate + +- [ ] `tests/persistence.rs` passes. +- [ ] `tests/preview_http.rs` passes. +- [ ] Driver-suite preview tests pass (`createSignedPreviewUrl`, token round-trip via `/fetch/{token}/path`, `expireSignedPreviewUrl`). +- [ ] All earlier e2e gates still pass. + +### STOP — discuss + +Persistence + preview works. Confirm before toolkit callbacks. + +--- + +## Phase 5 — Toolkit callbacks + +### Tasks + +- [ ] In `NapiActorFactory::from_agent_os`, walk `tool_callbacks` JsObject. Extract `execute: JsFunction` for each `":"` key. Wrap as `ThreadsafeFunction`. +- [ ] Plug TSF callbacks into `AgentOsConfig::tool_kits` via a `ToolCallback` Arc closure that dispatches through the TSF. +- [ ] In JS `agentOs()` shim's `nativeFactoryBuilder`, walk `parsed.options.toolKits[*].tools[*]`, build the `{":": executeFn}` JsObject, pass to `runtime.createAgentOsFactory`. +- [ ] Driver-suite test: register an actor with a host tool, send a prompt that triggers the tool, assert JS `execute` ran and result reached the session. + +### Risk callout + +Highest-risk phase. TSF lifetime + cancellation token bridging across `napi_actor_events.rs` is a known minefield. If lifetime bugs surface, the rollback is to ship Phases 0–4 without host-tool support and merge. + +### E2E gate + +- [ ] Driver-suite host-tool round-trip test passes. +- [ ] All earlier e2e gates still pass. + +### STOP — discuss + +Toolkits work end-to-end. Confirm before cleanup. + +--- + +## Phase 6 — Cleanup + docs + +### Tasks — final cleanup pass + +- [ ] Grep for any orphan code (unused helpers, dead constants, unused wire DTOs from earlier phases). +- [ ] Grep for diagnostic logs: `grep -rE "console\.error|tracing::debug" rivetkit-typescript/packages/rivetkit/src/agent-os rivetkit-rust/packages/rivetkit-agent-os/src` — clean up. +- [ ] Confirm `RunState` counters are wired throughout (no unincremented counter sets). +- [ ] Confirm every wire DTO has a feeder. +- [ ] Confirm every event broadcast constant has a `Subscriptions::spawn_*` feeder. + +### Tasks — docs + +- [ ] Add `rivetkit-rust/packages/rivetkit-agent-os/CLAUDE.md`. +- [ ] Add `AGENTS.md` symlink (`ln -s CLAUDE.md AGENTS.md` from the package dir). +- [ ] Add bullet to root `CLAUDE.md` under "RivetKit Layer Architecture" describing the new crate. +- [ ] Update website docs for the new agent-os actor surface. + +### Tasks — lint + format + +- [ ] `cargo clippy -p rivetkit-agent-os -- -W warnings` clean. +- [ ] `pnpm biome check` clean on changed files. +- [ ] `pnpm tsc --noEmit` baseline clean (modulo pre-existing workspace issues). + +### Tasks — final regression + +- [ ] Full driver suite no-bail. Every cell green (modulo intentional skips). +- [ ] `cargo test -p rivetkit-agent-os` clean. +- [ ] Rebuild NAPI: `pnpm --filter @rivetkit/rivetkit-napi build:force`. +- [ ] Quickstart smoke (`ext/agent-os/examples/quickstart/`) runs against the new path. + +### E2E gate + +Final driver suite no-bail run. All cells green. Bytes round-trip on all three encodings. + +### STOP — discuss + +Ready to land. Confirm before merging. + +--- + +## Review checklist (apply at every PR within these phases) + +Pulled from PLAN2.md "Review checklist." Applies to **every PR**, not just phase boundaries: + +- [ ] No DTO added without a feeder in the same PR. +- [ ] No event broadcast constant without a `Subscriptions::spawn_*` feeder. +- [ ] No `RunState` counter introduced without dispatcher-arm increment + decrement. +- [ ] No abstraction (trait, generic wrapper) with one impl. +- [ ] No orphan method (grep the workspace before merge). +- [ ] User config flows or fails loud — never silently dropped. +- [ ] Byte payloads go through `Action::ok` wrapping (post-RIVETKIT_RUST_FIX). +- [ ] No legacy paths kept as fallback after a cutover. +- [ ] No diagnostic logs left in. +- [ ] Commit messages don't say "verified" without naming the test that ran. +- [ ] All prior sub-phase e2e tests still pass. +- [ ] NAPI artifact rebuilt if Rust touched. + +--- + +## Notes + +- **E2E maintained:** at every STOP, all prior phase e2e tests must still pass. +- **Stop and discuss:** don't move past a STOP without confirming the gate. +- **Feature debt is OK; e2e debt is not.** A missing action gets added in Phase 3. A failing driver-suite cell means the architecture has a leak that more code will only obscure. diff --git a/engine/artifacts/errors/actor.not_found.json b/engine/artifacts/errors/actor.not_found.json index 2e30c938f6..e99880acf2 100644 --- a/engine/artifacts/errors/actor.not_found.json +++ b/engine/artifacts/errors/actor.not_found.json @@ -1,5 +1,5 @@ { "code": "not_found", "group": "actor", - "message": "The actor does not exist or was destroyed." + "message": "Actor resource was not found." } \ No newline at end of file diff --git a/examples/agent-os-e2e/.gitignore b/examples/agent-os-e2e/.gitignore new file mode 100644 index 0000000000..499a30e545 --- /dev/null +++ b/examples/agent-os-e2e/.gitignore @@ -0,0 +1 @@ +.agent-modules/ diff --git a/examples/agent-os-e2e/README.md b/examples/agent-os-e2e/README.md index 99fe1492d2..3ce12d30cf 100644 --- a/examples/agent-os-e2e/README.md +++ b/examples/agent-os-e2e/README.md @@ -1,10 +1,10 @@ # Agent OS E2E Smoke Test -End-to-end smoke test for agentOS via the rivetkit actor wrapper. Boots a VM with WASM coreutils and the Pi coding agent, then exercises filesystem operations, subprocess execution, and an LLM-driven agent session. +End-to-end smoke test for agentOS via the rivetkit actor wrapper. Boots a VM with WASM coreutils and the Pi coding agent, then exercises filesystem operations, subprocess execution, preview URLs, and an llmock-backed agent session resume. ## Prerequisites -- `ANTHROPIC_API_KEY` environment variable +- No real model API key is required. The client starts `@copilotkit/llmock` on `E2E_LLMOCK_PORT` (default `41235`), and the server exempts that loopback port for the VM. ## Getting Started @@ -31,8 +31,8 @@ npx tsx src/client.ts - Filesystem round-trip: write, read, mkdir, readdir, exists - Subprocess execution: echo, pipes, grep, cat - Preview URL: spawn HTTP server in VM, create signed preview URL, fetch through proxy -- Pi agent session with streaming events -- Host-side verification of agent-created files +- Pi agent session with streaming events through host llmock +- Session resume after a forced actor sleep/wake ## Implementation diff --git a/examples/agent-os-e2e/package.json b/examples/agent-os-e2e/package.json index 6796ebb01f..b90893980e 100644 --- a/examples/agent-os-e2e/package.json +++ b/examples/agent-os-e2e/package.json @@ -4,16 +4,21 @@ "private": true, "type": "module", "scripts": { - "test:server": "tsx src/server.ts", + "prepare-agent-modules": "node scripts/prepare-agent-modules.mjs", + "test:server": "npm run prepare-agent-modules && tsx src/server.ts", + "test:opencode-resume-server": "tsx src/opencode-resume-server.ts", "test": "tsx src/client.ts", + "test:opencode-resume": "tsx src/opencode-resume-client.ts", "check-types": "tsc --noEmit" }, "dependencies": { "rivetkit": "*", - "@rivet-dev/agent-os-common": "*", - "@rivet-dev/agent-os-pi": "^0.1.1" + "@rivet-dev/agent-os-common": "0.0.260331072558", + "@rivet-dev/agent-os-opencode": "link:../../../agent-os/registry/agent/opencode", + "@rivet-dev/agent-os-pi": "link:../../../agent-os/registry/agent/pi" }, "devDependencies": { + "@copilotkit/llmock": "^1.6.0", "@types/node": "^22.13.9", "tsx": "^3.12.7", "typescript": "^5.5.2" diff --git a/examples/agent-os-e2e/scripts/prepare-agent-modules.mjs b/examples/agent-os-e2e/scripts/prepare-agent-modules.mjs new file mode 100644 index 0000000000..cb87df664d --- /dev/null +++ b/examples/agent-os-e2e/scripts/prepare-agent-modules.mjs @@ -0,0 +1,55 @@ +// Builds a small, self-contained dependency closure for the Pi agent at +// `.agent-modules/`, which the example mounts as the VM's `/root/node_modules` +// via `nodeModulesMount(".agent-modules/node_modules")` in src/server.ts. +// +// Why a dedicated dir instead of the example's own node_modules: in this pnpm +// monorepo the example's deps are symlinks that resolve out to the workspace +// root store, and the VM resolver (correctly) refuses symlinks that escape the +// mounted root. Mounting the workspace root itself would expose the entire +// ~4.5 GB monorepo to the VM. A flat `npm install` here gives the agent exactly +// its own closure and nothing else. +import { execFileSync } from "node:child_process"; +import { existsSync, mkdirSync, writeFileSync, readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; + +const here = dirname(fileURLToPath(import.meta.url)); +const root = join(here, ".."); +const dir = join(root, ".agent-modules"); +const pi = JSON.parse(readFileSync(join(root, "package.json"), "utf8")); +let PI_VER = pi.dependencies["@rivet-dev/agent-os-pi"]; +// In agent-os local-dev mode (`agent-os-dep local`) the example's dep is rewritten +// to a `link:`/`file:`/`workspace:` value that plain `npm install` cannot consume, +// and the in-VM agent JS closure is not where the resume logic lives anyway (that +// is the Rust sidecar binary + napi). So for the closure fall back to a real +// published version. Override with AGENT_OS_CLOSURE_VERSION if needed. +if (/^(link:|file:|workspace:)/.test(PI_VER)) { + PI_VER = process.env.AGENT_OS_CLOSURE_VERSION ?? "0.0.0-main.8794200"; + console.log( + `[prepare-agent-modules] local link dep detected; using published closure version ${PI_VER}`, + ); +} +// Keep agent + SDK versions in sync with the example's pinned agent-os version. +const deps = { + "@rivet-dev/agent-os-pi": PI_VER, + "@rivet-dev/agent-os-core": PI_VER, + "@mariozechner/pi-coding-agent": "0.60.0", +}; + +const stamp = join(dir, ".deps.json"); +const nodeModulesDir = join(dir, "node_modules"); +const want = JSON.stringify(deps); +if ( + existsSync(stamp) && + existsSync(nodeModulesDir) && + readFileSync(stamp, "utf8") === want +) { + console.log("[prepare-agent-modules] up to date"); + process.exit(0); +} +mkdirSync(dir, { recursive: true }); +writeFileSync(join(dir, "package.json"), JSON.stringify({ name: "agent-modules", private: true, dependencies: deps }, null, 2)); +console.log("[prepare-agent-modules] installing agent closure into .agent-modules ..."); +execFileSync("npm", ["install", "--no-audit", "--no-fund", "--loglevel=error"], { cwd: dir, stdio: "inherit" }); +writeFileSync(stamp, want); +console.log("[prepare-agent-modules] done"); diff --git a/examples/agent-os-e2e/src/client.ts b/examples/agent-os-e2e/src/client.ts index 128daf9d93..13e4048929 100644 --- a/examples/agent-os-e2e/src/client.ts +++ b/examples/agent-os-e2e/src/client.ts @@ -6,19 +6,29 @@ // 1. Start the server: npx tsx src/server.ts // 2. Run the client: npx tsx src/client.ts // -// Requires ANTHROPIC_API_KEY environment variable. - +import { LLMock } from "@copilotkit/llmock"; import { createClient } from "rivetkit/client"; import type { registry } from "./server.ts"; -const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY; -if (!ANTHROPIC_API_KEY) { - console.error("ANTHROPIC_API_KEY is required"); - process.exit(1); -} +const LLMOCK_PORT = Number(process.env.E2E_LLMOCK_PORT ?? "41235"); +const LLMOCK_READY = "E2E_LLMOCK_OK"; -const client = createClient("http://localhost:6420"); -const agent = client.vm.getOrCreate(["e2e-test"]); +const client = createClient( + process.env.RIVET_ENDPOINT ?? "http://localhost:6420", +); +const ACTOR_KEY = process.env.E2E_ACTOR_KEY ?? "e2e-test"; +const agent = client.vm.getOrCreate([ACTOR_KEY]); + +const llmock = new LLMock({ port: LLMOCK_PORT, logLevel: "silent" }); +const SECRET = "BANANA-7731"; +llmock.addFixtures([ + { + match: { predicate: () => true }, + response: { content: LLMOCK_READY }, + }, +]); +const llmockUrl = await llmock.start(); +console.log(`llmock: ${llmockUrl}`); // --- Step 1: Filesystem basics --- console.log("=== Step 1: Filesystem ==="); @@ -92,9 +102,6 @@ const serverProc = (await agent.spawn("node", ["/tmp/server.mjs"])) as { }; console.log(`Spawned preview server: pid ${serverProc.pid}`); -// Give the server a moment to bind -await new Promise((r) => setTimeout(r, 1000)); - // Create a signed preview URL for port 8080 const preview = (await agent.createSignedPreviewUrl(8080, 60)) as { path: string; @@ -105,12 +112,25 @@ const preview = (await agent.createSignedPreviewUrl(8080, 60)) as { console.log(`Preview path: ${preview.path}`); console.log(`Preview token: ${preview.token}`); -// Fetch through the preview proxy +// Fetch through the preview proxy. getGatewayUrl() returns a routing URL that +// already carries a query string (rvt-namespace/rvt-method/rvt-crash-policy/...), +// so the preview path must be inserted into the pathname before the query rather +// than naively appended (which would land inside the rvt-crash-policy value). const gatewayUrl = await agent.getGatewayUrl(); -const previewUrl = `${gatewayUrl}${preview.path}`; +const previewUrlObj = new URL(gatewayUrl); +previewUrlObj.pathname = + previewUrlObj.pathname.replace(/\/$/, "") + preview.path; +const previewUrl = previewUrlObj.toString(); console.log(`Fetching preview URL: ${previewUrl}`); -const previewResponse = await fetch(previewUrl); -const previewBody = await previewResponse.text(); +let previewResponse = new Response("", { status: 503 }); +let previewBody = ""; +const previewDeadline = Date.now() + 10_000; +while (Date.now() < previewDeadline) { + previewResponse = await fetch(previewUrl); + previewBody = await previewResponse.text(); + if (previewResponse.status === 200) break; + await new Promise((r) => setTimeout(r, 250)); +} console.log(`Preview response: ${previewResponse.status} "${previewBody}"`); assert(previewResponse.status === 200, "preview status 200"); assert(previewBody === "preview ok", "preview body matches"); @@ -118,52 +138,314 @@ assert(previewBody === "preview ok", "preview body matches"); // Clean up the server process await agent.killProcess(serverProc.pid); -// --- Step 4: Agent session (Pi + Anthropic) --- +// --- Step 4: Agent session (Pi + llmock) --- console.log("\n=== Step 4: Agent session ==="); console.log("Creating Pi agent session..."); +await agent.mkdir("/home/user/.pi/agent"); +await agent.writeFile( + "/home/user/.pi/agent/models.json", + JSON.stringify( + { + providers: { + anthropic: { + baseUrl: llmockUrl, + apiKey: "mock-key", + }, + }, + }, + null, + 2, + ), +); +await agent.writeFile( + "/home/user/.pi/agent/auth.json", + JSON.stringify( + { + anthropic: { + type: "api_key", + key: "mock-key", + }, + }, + null, + 2, + ), +); +await agent.mkdir("/home/user/workspace"); const session = (await agent.createSession("pi", { - env: { ANTHROPIC_API_KEY }, + cwd: "/home/user/workspace", + env: { + HOME: "/home/user", + ANTHROPIC_API_KEY: "mock-key", + ANTHROPIC_BASE_URL: llmockUrl, + PI_CODING_AGENT_DIR: "/home/user/.pi/agent", + PI_SKIP_VERSION_CHECK: "1", + }, })) as { sessionId: string }; console.log(`Session created: ${session.sessionId}`); +const transcriptPath = `/root/.agentos/threads/${session.sessionId}.md`; +llmock.prependFixture({ + match: { + predicate: (req: any) => + req.messages?.at?.(-1)?.role === "tool" && + JSON.stringify(req.messages.at(-1)).includes(SECRET), + }, + response: { content: SECRET }, +}); +llmock.prependFixture({ + match: { + predicate: (req: any) => { + const body = JSON.stringify(req).toLowerCase(); + return ( + body.includes("after wake") && + body.includes(transcriptPath.toLowerCase()) && + !body.includes(SECRET.toLowerCase()) + ); + }, + }, + response: { + toolCalls: [ + { + id: "call_read_resume_transcript", + name: "read", + arguments: JSON.stringify({ path: transcriptPath }), + }, + ], + }, +}); // Subscribe to streaming events via WebSocket connection const conn = agent.connect(); +let initialStream = ""; conn.on("sessionEvent", (data: any) => { const event = data?.event ?? data; const params = event?.params; if (params?.update?.sessionUpdate === "agent_message_chunk") { - process.stdout.write(params.update.content?.text ?? ""); + const text = params.update.content?.text ?? ""; + initialStream += text; + process.stdout.write(text); } }); +// Track VM lifecycle so we can prove the actor actually slept (VM torn down) +// before the resume prompt, and that a fresh VM was booted to serve it. +let vmShutdownCount = 0; +let vmBootedCount = 0; +let lastShutdownReason: string | undefined; +conn.on("vmShutdown", (data: any) => { + const payload = data?.payload ?? data; + lastShutdownReason = payload?.reason ?? lastShutdownReason; + vmShutdownCount++; + console.log(`\n[lifecycle] vmShutdown (reason=${lastShutdownReason})`); +}); +conn.on("vmBooted", () => { + vmBootedCount++; + console.log(`[lifecycle] vmBooted`); +}); + // Wait for WebSocket to establish await new Promise((r) => setTimeout(r, 500)); console.log("\nSending prompt..."); const response = (await agent.sendPrompt( session.sessionId, - 'Write the text "E2E test passed!" to /tmp/e2e-result.txt using the write tool. Then use the bash tool to run `cat /tmp/e2e-result.txt` and tell me what it says.', -)) as { stopReason?: string }; + `Reply with the exact text ${LLMOCK_READY}.`, +)) as { stopReason?: string; text?: string }; console.log(`\n\nPrompt completed: ${response?.stopReason ?? "done"}`); +assert( + (initialStream || response?.text || "").includes(LLMOCK_READY), + "Pi session reached host llmock", +); + +// --- Step 5: Session resume across a real actor sleep/wake --- +// Plant a memorable fact, force the actor to sleep (VM torn down, live_sessions +// cleared), then resume the SAME session with a second prompt. The actor must +// reconstruct enough session state from agent_os_session_events for the prompt +// to continue after wake. +console.log("\n=== Step 5: Session resume across sleep/wake ==="); + +console.log(`Planting secret "${SECRET}" in session ${session.sessionId}...`); +console.log("\nResume prompt 1 (plant secret)..."); +const plant = (await agent.sendPrompt( + session.sessionId, + `Remember this secret code for later: ${SECRET}. Just acknowledge with "ok".`, +)) as { stopReason?: string }; +console.log(`\n\nPlant prompt completed: ${plant?.stopReason ?? "done"}`); -// --- Step 5: Verify agent wrote the file --- -console.log("\n=== Step 5: Verify agent output ==="); -try { - const agentData = (await agent.readFile( - "/tmp/e2e-result.txt", - )) as Uint8Array; - const agentText = new TextDecoder().decode(agentData); - console.log(`Agent wrote: "${agentText.trim()}"`); - assert(agentText.includes("E2E test passed!"), "agent file content"); -} catch (err: any) { - console.error(`Failed to read agent output: ${err.message}`); - process.exit(1); +// Force a real sleep deterministically via the engine admin endpoint +// (POST /actors/{id}/sleep). An open WebSocket keeps the actor awake +// (CanSleep::ActiveConnections), so we first DISPOSE the conn, then signal sleep, +// then poll the actor record until `sleep_ts` is set -- which means the actor +// workflow tore down the VM (clearing Vars.live_sessions). This is more +// deterministic than waiting out the idle timeout. +const shutdownsBefore = vmShutdownCount; +const ENGINE = process.env.RIVET_ENDPOINT ?? "http://localhost:6420"; +const NS = "default"; +const ACTOR_NAME = "vm"; + +async function resolveActor(): Promise { + const url = `${ENGINE}/actors?namespace=${NS}&name=${ACTOR_NAME}&key=${encodeURIComponent(ACTOR_KEY)}`; + const res = await fetch(url); + if (!res.ok) throw new Error(`list actors failed: ${res.status}`); + const body = (await res.json()) as { actors: any[] }; + const a = + body.actors?.find((x) => x.destroy_ts == null) ?? body.actors?.[0]; + if (!a) throw new Error("actor not found by key"); + return a; } -// --- Cleanup --- +async function forceActorToSleep(label: string) { + const actor = await resolveActor(); + console.log(`Resolved actor_id=${actor.actor_id} for ${label}`); + + console.log( + `Signaling actor sleep via engine admin endpoint (${label})...`, + ); + const sleepRes = await fetch( + `${ENGINE}/actors/${actor.actor_id}/sleep?namespace=${NS}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "{}", + }, + ); + console.log(` sleep endpoint -> HTTP ${sleepRes.status}`); + assert(sleepRes.ok, `${label}: engine admin sleep endpoint accepted`); + + const sleepDeadline = Date.now() + 30_000; + while (Date.now() < sleepDeadline) { + const a = await resolveActor(); + if (a.sleep_ts != null) { + console.log( + ` actor asleep (sleep_ts=${a.sleep_ts}) -> VM torn down`, + ); + return; + } + await new Promise((r) => setTimeout(r, 500)); + } + assert(false, `${label}: actor actually slept (sleep_ts set)`); +} + +console.log( + "\nDisconnecting WebSocket so the actor can sleep (no active connection)...", +); await conn.dispose(); +await new Promise((r) => setTimeout(r, 500)); + +const actor = await resolveActor(); +console.log(`Resolved actor_id=${actor.actor_id}`); + +console.log("Signaling actor sleep via engine admin endpoint..."); +const sleepRes = await fetch( + `${ENGINE}/actors/${actor.actor_id}/sleep?namespace=${NS}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "{}", + }, +); +console.log(` sleep endpoint -> HTTP ${sleepRes.status}`); +assert( + sleepRes.ok, + "engine admin sleep endpoint accepted (VM teardown signaled)", +); + +// Poll until the actor record shows sleep_ts (asleep -> VM torn down). +const sleepDeadline = Date.now() + 30_000; +let slept = false; +while (Date.now() < sleepDeadline) { + const a = await resolveActor(); + if (a.sleep_ts != null) { + slept = true; + console.log(` actor asleep (sleep_ts=${a.sleep_ts}) -> VM torn down`); + break; + } + await new Promise((r) => setTimeout(r, 500)); +} +assert( + slept, + "actor actually slept (sleep_ts set -> VM destroyed, live_sessions cleared)", +); +if (vmShutdownCount > shutdownsBefore) { + console.log(` also observed vmShutdown reason: ${lastShutdownReason}`); +} + +// Re-open a connection and subscribe to lifecycle BEFORE the resume prompt, so we +// catch the fresh vmBooted that the wake triggers. +const bootsBeforeResume = vmBootedCount; +const conn2 = agent.connect(); +let resumeBooted = false; +let recallStream = ""; +conn2.on("vmBooted", () => { + resumeBooted = true; + console.log(`[lifecycle] vmBooted (resume wake)`); +}); +conn2.on("sessionEvent", (data: any) => { + const event = data?.event ?? data; + const params = event?.params; + if (params?.update?.sessionUpdate === "agent_message_chunk") { + const t = params.update.content?.text ?? ""; + recallStream += t; + process.stdout.write(t); + } +}); +await new Promise((r) => setTimeout(r, 500)); + +console.log("\nResume prompt 2 (post-wake prompt -- triggers lazy resume)..."); +const recall = (await agent.sendPrompt( + session.sessionId, + "After wake, what secret code did I ask you to remember? Use prior session context if needed and reply with only the code.", +)) as { stopReason?: string; text?: string }; +console.log(`\n\nRecall prompt completed: ${recall?.stopReason ?? "done"}`); + +// A fresh VM boots to serve the resumed session. The vmBooted broadcast is a +// best-effort client-side signal (it can race the WS attach), so it is logged +// but NOT asserted: the hard teardown proof is the `slept` assertion above +// (sleep_ts -> actor Terminated), and the hard resume proof below is that the +// same session event log contains both the pre-sleep prompt and the completed +// post-wake prompt after the VM was destroyed. +if (resumeBooted || vmBootedCount > bootsBeforeResume) { + console.log( + " fresh VM booted to serve resumed session (vmBooted observed)", + ); +} else { + console.log( + " (vmBooted broadcast not observed on client conn -- non-fatal; teardown proven by sleep_ts, resume proven by persisted post-wake prompt)", + ); +} + +// Verify both prompts are persisted in the same session event log. Combined +// with the completed post-wake `sendPrompt`, this proves lazy resume continued +// the same persisted session after VM teardown. +async function persistedPromptText(): Promise { + const events = (await agent.getSessionEvents(session.sessionId)) as any[]; + return events + .filter((ev) => ev?.method === "user_prompt") + .map((ev) => ev?.params?.text ?? "") + .join("\n"); +} + +const prompts = await persistedPromptText(); +assert( + prompts.includes(SECRET), + "pre-sleep prompt persisted in session event log", +); +assert( + prompts.toLowerCase().includes("after wake"), + "post-wake prompt persisted in same session event log", +); +assert(recall != null, "post-wake prompt completed"); +const recallText = recallStream || recall?.text || ""; +assert( + recallText.includes(SECRET), + "post-wake agent recalled prior-turn secret via resumed context", +); + +await conn2.dispose(); await agent.closeSession(session.sessionId); +// --- Cleanup --- +await llmock.stop(); + console.log("\n=== Results ==="); console.log("All checks passed!"); diff --git a/examples/agent-os-e2e/src/opencode-resume-client.ts b/examples/agent-os-e2e/src/opencode-resume-client.ts new file mode 100644 index 0000000000..df4a9aebea --- /dev/null +++ b/examples/agent-os-e2e/src/opencode-resume-client.ts @@ -0,0 +1,276 @@ +import { LLMock } from "@copilotkit/llmock"; +import { createClient } from "rivetkit/client"; +import type { registry } from "./opencode-resume-server.ts"; + +type LlmockMessage = { + role?: string; + content?: string | null; +}; + +const LLMOCK_PORT = Number(process.env.E2E_LLMOCK_PORT ?? "41235"); +const client = createClient( + process.env.RIVET_ENDPOINT ?? "http://localhost:6420", +); +const ACTOR_KEY = process.env.E2E_ACTOR_KEY ?? `opencode-resume-${Date.now()}`; +const agent = client.vm.getOrCreate([ACTOR_KEY]); +const llmock = new LLMock({ port: LLMOCK_PORT, logLevel: "silent" }); +const llmockUrl = await llmock.start(); + +const ENGINE = process.env.RIVET_ENDPOINT ?? "http://localhost:6420"; +const NS = "default"; +const ACTOR_NAME = "vm"; + +console.log(`llmock: ${llmockUrl}`); +console.log(`actor key: ${ACTOR_KEY}`); + +try { + await runNativeResume(); + await runMissingStoreFallback(); + console.log("\n=== Results ==="); + console.log("OpenCode resume checks passed!"); +} finally { + await llmock.stop(); +} + +async function runNativeResume() { + console.log("\n=== OpenCode native resume across sleep/wake ==="); + const token = "ORCHID-2718"; + const firstPrompt = `Remember the native OpenCode token: ${token}.`; + const secondPrompt = "What native OpenCode token did I give you earlier?"; + + llmock.prependFixture({ + match: { + predicate: (req: unknown) => + hasUserMessageContaining(req, secondPrompt), + }, + response: { content: `The token was ${token}.` }, + }); + llmock.prependFixture({ + match: { + predicate: (req: unknown) => + hasUserMessageContaining(req, firstPrompt), + }, + response: { content: `I will remember ${token}.` }, + }); + + const home = `/tmp/opencode-native-${crypto.randomUUID()}`; + const workspace = `/tmp/opencode-native-workspace-${crypto.randomUUID()}`; + await createOpenCodeHome(home); + await mkdirp(workspace); + + const { sessionId } = (await agent.createSession("opencode", { + cwd: workspace, + env: openCodeEnv(home), + })) as { sessionId: string }; + console.log(`session: ${sessionId}`); + + await agent.sendPrompt(sessionId, firstPrompt); + await forceActorToSleep("OpenCode native resume"); + + const recall = (await agent.sendPrompt(sessionId, secondPrompt)) as { + text?: string; + }; + const secondRequest = llmock + .getRequests() + .find((req: unknown) => hasUserMessageContaining(req, secondPrompt)); + assert( + secondRequest != null, + "second OpenCode native LLM request observed", + ); + assert( + hasUserMessageContaining(secondRequest, firstPrompt), + "native session/load preserved prior prompt context after real sleep/wake", + ); + assert( + !hasUserMessageContaining( + secondRequest, + "You are continuing an earlier session", + ), + "native session/load did not inject fallback transcript preamble", + ); + assert( + (recall.text ?? "").includes(token), + "native post-wake response used preserved context", + ); + await agent.closeSession(sessionId); +} + +async function runMissingStoreFallback() { + console.log("\n=== OpenCode missing-store fallback across sleep/wake ==="); + const token = "FALLBACK-4930"; + const firstPrompt = `Remember the fallback OpenCode token: ${token}.`; + const secondPrompt = + "After missing-store wake, continue using the transcript. What fallback OpenCode token did I give you?"; + + llmock.prependFixture({ + match: { + predicate: (req: unknown) => + hasUserMessageContaining(req, secondPrompt), + }, + response: { content: `The token was ${token}.` }, + }); + llmock.prependFixture({ + match: { + predicate: (req: unknown) => + hasUserMessageContaining(req, firstPrompt), + }, + response: { content: `I will remember ${token}.` }, + }); + + const home = `/tmp/opencode-fallback-${crypto.randomUUID()}`; + const workspace = `/tmp/opencode-fallback-workspace-${crypto.randomUUID()}`; + await createOpenCodeHome(home); + await mkdirp(workspace); + + const { sessionId } = (await agent.createSession("opencode", { + cwd: workspace, + env: openCodeEnv(home), + })) as { sessionId: string }; + console.log(`session: ${sessionId}`); + + await agent.sendPrompt(sessionId, firstPrompt); + await agent.deleteFile(`${home}/.local/share/opencode`, { + recursive: true, + }); + await forceActorToSleep("OpenCode missing-store fallback"); + + const recall = (await agent.sendPrompt(sessionId, secondPrompt)) as { + text?: string; + }; + const transcriptPath = `/root/.agentos/threads/${sessionId}.md`; + const secondRequest = llmock + .getRequests() + .find((req: unknown) => hasUserMessageContaining(req, secondPrompt)); + assert( + secondRequest != null, + "second OpenCode fallback LLM request observed", + ); + assert( + hasUserMessageContaining( + secondRequest, + "You are continuing an earlier session", + ), + "missing-store resume injected fallback transcript preamble", + ); + assert( + hasUserMessageContaining(secondRequest, transcriptPath), + "fallback preamble pointed at the stable transcript path", + ); + assert( + (recall.text ?? "").includes(token), + "fallback post-wake response completed after transcript fallback", + ); + await agent.closeSession(sessionId); +} + +async function resolveActor(): Promise { + const url = `${ENGINE}/actors?namespace=${NS}&name=${ACTOR_NAME}&key=${encodeURIComponent(ACTOR_KEY)}`; + const res = await fetch(url); + if (!res.ok) throw new Error(`list actors failed: ${res.status}`); + const body = (await res.json()) as { actors: any[] }; + const actor = + body.actors?.find((candidate) => candidate.destroy_ts == null) ?? + body.actors?.[0]; + if (!actor) throw new Error("actor not found by key"); + return actor; +} + +async function forceActorToSleep(label: string) { + const actor = await resolveActor(); + console.log(`Resolved actor_id=${actor.actor_id} for ${label}`); + + const sleepRes = await fetch( + `${ENGINE}/actors/${actor.actor_id}/sleep?namespace=${NS}`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: "{}", + }, + ); + console.log(` sleep endpoint -> HTTP ${sleepRes.status}`); + assert(sleepRes.ok, `${label}: engine admin sleep endpoint accepted`); + + const sleepDeadline = Date.now() + 30_000; + while (Date.now() < sleepDeadline) { + const sleptActor = await resolveActor(); + if (sleptActor.sleep_ts != null) { + console.log( + ` actor asleep (sleep_ts=${sleptActor.sleep_ts}) -> VM torn down`, + ); + return; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + assert(false, `${label}: actor actually slept (sleep_ts set)`); +} + +async function mkdirp(path: string) { + const parts = path.split("/").filter(Boolean); + let current = ""; + for (const part of parts) { + current += `/${part}`; + try { + await agent.mkdir(current); + } catch { + // Directory already exists. + } + } +} + +async function createOpenCodeHome(homeDir: string) { + await mkdirp(`${homeDir}/.config/opencode`); + await agent.writeFile( + `${homeDir}/.config/opencode/opencode.json`, + JSON.stringify( + { + $schema: "https://opencode.ai/config.json", + autoupdate: false, + share: "disabled", + snapshot: false, + model: "anthropic/claude-sonnet-4-20250514", + provider: { + anthropic: { + options: { + baseURL: `${llmockUrl}/v1`, + }, + }, + }, + }, + null, + 2, + ), + ); +} + +function openCodeEnv(homeDir: string) { + return { + HOME: homeDir, + ANTHROPIC_API_KEY: "mock-key", + }; +} + +function getLlmockMessages(req: unknown): LlmockMessage[] { + const directMessages = (req as { messages?: LlmockMessage[] }).messages; + if (Array.isArray(directMessages)) return directMessages; + + const bodyMessages = (req as { body?: { messages?: LlmockMessage[] } }).body + ?.messages; + return Array.isArray(bodyMessages) ? bodyMessages : []; +} + +function hasUserMessageContaining(req: unknown, expected: string): boolean { + return getLlmockMessages(req).some( + (message) => + message.role === "user" && + typeof message.content === "string" && + message.content.includes(expected), + ); +} + +function assert(condition: boolean, label: string) { + if (!condition) { + console.error(`FAILED: ${label}`); + process.exit(1); + } + console.log(` OK: ${label}`); +} diff --git a/examples/agent-os-e2e/src/opencode-resume-server.ts b/examples/agent-os-e2e/src/opencode-resume-server.ts new file mode 100644 index 0000000000..bb97b28a0c --- /dev/null +++ b/examples/agent-os-e2e/src/opencode-resume-server.ts @@ -0,0 +1,27 @@ +import common from "@rivet-dev/agent-os-common"; +import opencode from "@rivet-dev/agent-os-opencode"; +import { setup } from "rivetkit"; +import { agentOs } from "rivetkit/agent-os"; + +const llmockPort = Number(process.env.E2E_LLMOCK_PORT ?? "41235"); +const opencodePackageDir = (opencode as { packageDir: string }).packageDir; + +const vm = agentOs({ + options: { + software: [common, opencode], + mounts: [ + { + path: "/root/node_modules/@rivet-dev/agent-os-opencode", + plugin: { + id: "host_dir", + config: { hostPath: opencodePackageDir, readOnly: true }, + }, + readOnly: true, + }, + ], + loopbackExemptPorts: [llmockPort], + }, +}); + +export const registry = setup({ use: { vm } }); +registry.start(); diff --git a/examples/agent-os-e2e/src/server.ts b/examples/agent-os-e2e/src/server.ts index 7522c60853..63fb9f5405 100644 --- a/examples/agent-os-e2e/src/server.ts +++ b/examples/agent-os-e2e/src/server.ts @@ -1,9 +1,30 @@ +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; import common from "@rivet-dev/agent-os-common"; import pi from "@rivet-dev/agent-os-pi"; import { setup } from "rivetkit"; -import { agentOs } from "rivetkit/agent-os"; +import { agentOs, nodeModulesMount } from "rivetkit/agent-os"; -const vm = agentOs({ options: { software: [common, pi] } }); +// The Pi agent closure is pre-installed (flat npm tree) into `.agent-modules/` +// by `scripts/prepare-agent-modules.mjs`. Mount its `node_modules` at +// `/root/node_modules` so the VM module resolver can read the agent SDK + its +// transitive deps through the kernel VFS. +const here = dirname(fileURLToPath(import.meta.url)); +const agentModules = join(here, "..", ".agent-modules", "node_modules"); +const llmockPort = Number(process.env.E2E_LLMOCK_PORT ?? "41235"); + +// The client exercises session resume by forcing the actor to sleep (engine admin +// POST /actors/{id}/sleep) between two prompts in the same session. Sleep tears +// down the VM and clears the actor's ephemeral `live_sessions` map, so the second +// prompt lazily reconstructs the session transcript from `agent_os_session_events` +// and resumes -- proving resume survives a real sleep/wake. +const vm = agentOs({ + options: { + software: [common, pi], + mounts: [nodeModulesMount(agentModules)], + loopbackExemptPorts: [llmockPort], + }, +}); export const registry = setup({ use: { vm } }); registry.start(); diff --git a/examples/agent-os/package.json b/examples/agent-os/package.json index 7823fd392b..88fdd0ffdf 100644 --- a/examples/agent-os/package.json +++ b/examples/agent-os/package.json @@ -25,7 +25,13 @@ "check-types": "tsc --noEmit" }, "dependencies": { + "@agent-os-pkgs/common": "0.0.0-split-runtime-preview.5d46b14", + "@rivet-dev/agent-os-core": "link:../../../agent-os/packages/core", + "@rivet-dev/agent-os-pi": "link:../../../agent-os/registry/agent/pi", + "@rivet-dev/agent-os-sandbox": "link:../../../agent-os/packages/agent-os-sandbox", + "@secure-exec/s3": "0.0.0-split-runtime-preview.5d46b14", "rivetkit": "*", + "sandbox-agent": "^0.4.2", "zod": "^3.25.0" }, "devDependencies": { @@ -34,8 +40,13 @@ "typescript": "^5.5.2" }, "template": { - "technologies": ["typescript"], - "tags": ["agent-os", "vm"], + "technologies": [ + "typescript" + ], + "tags": [ + "agent-os", + "vm" + ], "noFrontend": true, "skipVercel": true }, diff --git a/examples/agent-os/src/agent-session/server.ts b/examples/agent-os/src/agent-session/server.ts index 7522c60853..35ca8b12a8 100644 --- a/examples/agent-os/src/agent-session/server.ts +++ b/examples/agent-os/src/agent-session/server.ts @@ -1,4 +1,4 @@ -import common from "@rivet-dev/agent-os-common"; +import common from "@agent-os-pkgs/common"; import pi from "@rivet-dev/agent-os-pi"; import { setup } from "rivetkit"; import { agentOs } from "rivetkit/agent-os"; diff --git a/examples/agent-os/src/cron/server.ts b/examples/agent-os/src/cron/server.ts index b4e759e13a..ab1649d41f 100644 --- a/examples/agent-os/src/cron/server.ts +++ b/examples/agent-os/src/cron/server.ts @@ -1,4 +1,4 @@ -import common from "@rivet-dev/agent-os-common"; +import common from "@agent-os-pkgs/common"; import { setup } from "rivetkit"; import { agentOs } from "rivetkit/agent-os"; diff --git a/examples/agent-os/src/filesystem/server.ts b/examples/agent-os/src/filesystem/server.ts index 84304fd150..ad6af4a21f 100644 --- a/examples/agent-os/src/filesystem/server.ts +++ b/examples/agent-os/src/filesystem/server.ts @@ -1,11 +1,11 @@ -import common from "@rivet-dev/agent-os-common"; +import common from "@agent-os-pkgs/common"; import { setup } from "rivetkit"; import { agentOs } from "rivetkit/agent-os"; // The default agentOS actor mounts an in-memory filesystem at /home/user. // You can add custom mounts for S3, host directories, or other backends: // -// import { S3BlockStore } from "@rivet-dev/agent-os-s3"; +// import { S3BlockStore } from "@secure-exec/s3"; // const vm = agentOs({ // options: { // software: [common], diff --git a/examples/agent-os/src/git/server.ts b/examples/agent-os/src/git/server.ts index 0833c35031..77c5beadac 100644 --- a/examples/agent-os/src/git/server.ts +++ b/examples/agent-os/src/git/server.ts @@ -1,4 +1,4 @@ -import common from "@rivet-dev/agent-os-common"; +import common from "@agent-os-pkgs/common"; import git from "@rivet-dev/agent-os-git"; import { setup } from "rivetkit"; import { agentOs } from "rivetkit/agent-os"; diff --git a/examples/agent-os/src/hello-world/server.ts b/examples/agent-os/src/hello-world/server.ts index b4e759e13a..ab1649d41f 100644 --- a/examples/agent-os/src/hello-world/server.ts +++ b/examples/agent-os/src/hello-world/server.ts @@ -1,4 +1,4 @@ -import common from "@rivet-dev/agent-os-common"; +import common from "@agent-os-pkgs/common"; import { setup } from "rivetkit"; import { agentOs } from "rivetkit/agent-os"; diff --git a/examples/agent-os/src/network/server.ts b/examples/agent-os/src/network/server.ts index b4e759e13a..ab1649d41f 100644 --- a/examples/agent-os/src/network/server.ts +++ b/examples/agent-os/src/network/server.ts @@ -1,4 +1,4 @@ -import common from "@rivet-dev/agent-os-common"; +import common from "@agent-os-pkgs/common"; import { setup } from "rivetkit"; import { agentOs } from "rivetkit/agent-os"; diff --git a/examples/agent-os/src/processes/server.ts b/examples/agent-os/src/processes/server.ts index b4e759e13a..ab1649d41f 100644 --- a/examples/agent-os/src/processes/server.ts +++ b/examples/agent-os/src/processes/server.ts @@ -1,4 +1,4 @@ -import common from "@rivet-dev/agent-os-common"; +import common from "@agent-os-pkgs/common"; import { setup } from "rivetkit"; import { agentOs } from "rivetkit/agent-os"; diff --git a/examples/agent-os/src/sandbox/server.ts b/examples/agent-os/src/sandbox/server.ts index b489bbd863..8ae3e33f62 100644 --- a/examples/agent-os/src/sandbox/server.ts +++ b/examples/agent-os/src/sandbox/server.ts @@ -4,7 +4,7 @@ // container lifecycle. The sandbox filesystem is mounted at /sandbox and // the toolkit exposes process management as CLI commands. -import common from "@rivet-dev/agent-os-common"; +import common from "@agent-os-pkgs/common"; import { createSandboxFs, createSandboxToolkit, diff --git a/examples/agent-os/src/tools/server.ts b/examples/agent-os/src/tools/server.ts index 761bf16dc3..0a1c919fba 100644 --- a/examples/agent-os/src/tools/server.ts +++ b/examples/agent-os/src/tools/server.ts @@ -1,4 +1,4 @@ -import common from "@rivet-dev/agent-os-common"; +import common from "@agent-os-pkgs/common"; import { hostTool, toolKit } from "@rivet-dev/agent-os-core"; import pi from "@rivet-dev/agent-os-pi"; import { setup } from "rivetkit"; diff --git a/justfile b/justfile index fe03d162c3..e2ac5cff62 100644 --- a/justfile +++ b/justfile @@ -6,6 +6,27 @@ release *ARGS: preview-publish REF: gh workflow run .github/workflows/publish.yaml --ref "{{ REF }}" +# Point rivet at the sibling ../agent-os checkout for local hacking on agent-os +# (npm `link:`, cargo `path`). The local dev loop: edit agent-os, rebuild here. +[group('agent-os')] +agent-os-local: + node scripts/agent-os-dep.mjs local + +# Point rivet at PUBLISHED agent-os versions (CI/release default). +[group('agent-os')] +agent-os-pinned: + node scripts/agent-os-dep.mjs pinned + +# Bump the pinned @rivet-dev/agent-os-* npm version (after an agent-os preview publish). +[group('agent-os')] +agent-os-set-version VERSION: + node scripts/agent-os-dep.mjs set-version "{{ VERSION }}" + +# Show the current agent-os dependency mode + pinned versions. +[group('agent-os')] +agent-os-status: + node scripts/agent-os-dep.mjs status + [group('docker')] docker-build: docker build -f engine/docker/universal/Dockerfile --target engine-full -t rivetdev/engine:local --platform linux/x86_64 . diff --git a/package.json b/package.json index 0c715eaa54..37da6628eb 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "devDependencies": { "@bare-ts/tools": "0.15.0", "@biomejs/biome": "^2.3", + "@rivetkit/engine-cli-linux-x64-musl": "2.3.0-rc.12", "commander": "^12.1.0", "lefthook": "^1.12.4", "semver": "^7.7.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b599fc08b..5a42f4e806 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,9 @@ importers: '@biomejs/biome': specifier: ^2.3 version: 2.3.11 + '@rivetkit/engine-cli-linux-x64-musl': + specifier: 2.3.0-rc.12 + version: 2.3.0-rc.12 commander: specifier: ^12.1.0 version: 12.1.0 @@ -223,9 +226,27 @@ importers: examples/agent-os: dependencies: + '@agent-os-pkgs/common': + specifier: 0.0.0-split-runtime-preview.5d46b14 + version: 0.0.0-split-runtime-preview.5d46b14 + '@rivet-dev/agent-os-core': + specifier: link:../../../agent-os/packages/core + version: link:../../../agent-os/packages/core + '@rivet-dev/agent-os-pi': + specifier: link:../../../agent-os/registry/agent/pi + version: link:../../../agent-os/registry/agent/pi + '@rivet-dev/agent-os-sandbox': + specifier: link:../../../agent-os/packages/agent-os-sandbox + version: link:../../../agent-os/packages/agent-os-sandbox + '@secure-exec/s3': + specifier: 0.0.0-split-runtime-preview.5d46b14 + version: 0.0.0-split-runtime-preview.5d46b14 rivetkit: specifier: workspace:* version: link:../../rivetkit-typescript/packages/rivetkit + sandbox-agent: + specifier: ^0.4.2 + version: 0.4.2(get-port@7.1.0)(zod@3.25.76) zod: specifier: ^3.25.0 version: 3.25.76 @@ -243,15 +264,21 @@ importers: examples/agent-os-e2e: dependencies: '@rivet-dev/agent-os-common': - specifier: '*' + specifier: 0.0.260331072558 version: 0.0.260331072558 + '@rivet-dev/agent-os-opencode': + specifier: link:../../../agent-os/registry/agent/opencode + version: link:../../../agent-os/registry/agent/opencode '@rivet-dev/agent-os-pi': - specifier: ^0.1.1 - version: 0.1.1(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.1)(zod@3.25.76) + specifier: link:../../../agent-os/registry/agent/pi + version: link:../../../agent-os/registry/agent/pi rivetkit: specifier: workspace:* version: link:../../rivetkit-typescript/packages/rivetkit devDependencies: + '@copilotkit/llmock': + specifier: ^1.6.0 + version: 1.7.1 '@types/node': specifier: ^22.13.9 version: 22.19.15 @@ -3049,8 +3076,11 @@ importers: specifier: ^1.1.5 version: 1.1.5(hono@4.11.9)(zod@4.1.13) '@rivet-dev/agent-os-core': - specifier: ^0.1.1 - version: 0.1.1(pyodide@0.28.3) + specifier: link:../../../../agent-os/packages/core + version: link:../../../../agent-os/packages/core + '@rivet-dev/agent-os-sidecar': + specifier: link:../../../../agent-os/packages/sidecar-binary + version: link:../../../../agent-os/packages/sidecar-binary '@rivetkit/bare-ts': specifier: ^0.6.2 version: 0.6.2 @@ -3119,11 +3149,11 @@ importers: specifier: ^1.6.0 version: 1.7.1 '@rivet-dev/agent-os-common': - specifier: '*' + specifier: 0.0.260331072558 version: 0.0.260331072558 '@rivet-dev/agent-os-pi': - specifier: ^0.1.1 - version: 0.1.1(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13))(pyodide@0.28.3)(ws@8.19.0)(zod@4.1.13) + specifier: link:../../../../agent-os/registry/agent/pi + version: link:../../../../agent-os/registry/agent/pi '@standard-schema/spec': specifier: ^1.0.0 version: 1.0.0 @@ -3648,6 +3678,33 @@ packages: '@adobe/css-tools@4.3.3': resolution: {integrity: sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==} + '@agent-os-pkgs/common@0.0.0-split-runtime-preview.5d46b14': + resolution: {integrity: sha512-6TzgaSZzgDdgQzY1Zgb4zAlzvb49NYuXK1gNDjWAfQeivz2NcWyfaN1qPRATHXOtpGUGm05Z65OwjLF05tTbNg==} + + '@agent-os-pkgs/coreutils@0.0.0-split-runtime-preview.5d46b14': + resolution: {integrity: sha512-AOOiAYcPfFvTDzwpV2U6n0/lxJXuUhbq67Tt7HRiIyG76e4zHTSzAu55RoW/24myRsKVjRw5jPA0hB413TuEQA==} + + '@agent-os-pkgs/diffutils@0.0.0-split-runtime-preview.5d46b14': + resolution: {integrity: sha512-i38lpZ24rm8nt4ImGuZ2KQBjI/ohADkponspwbHWiqR6t3jBKggg4PaSq3xcxY8O8rZ/05Igq1YBlrDh33Y4hQ==} + + '@agent-os-pkgs/findutils@0.0.0-split-runtime-preview.5d46b14': + resolution: {integrity: sha512-9IwymPoxtJveSB+HppgAUII05JBYGNhP9QSys5NoPSiaEOlIhcAVoKZHq6XFGyr33MhWmHyy3KEyrXFnkan+9Q==} + + '@agent-os-pkgs/gawk@0.0.0-split-runtime-preview.5d46b14': + resolution: {integrity: sha512-zjuyTyjXCz6LgZZqdA14vcgmyzjR5Ipeb3hlL+Tn3sCfAGp+Y7GSbTO9yBtTv8fkWNGOLq8il5zQJkJlHHMMfg==} + + '@agent-os-pkgs/grep@0.0.0-split-runtime-preview.5d46b14': + resolution: {integrity: sha512-xworE1A81YCKzXY2uW+2oK6BiB7YzEvcxU/RVxI7Psveylpfdlr/GJOVGfjyHTPi30hhDik3TEPSowS1WJqAfg==} + + '@agent-os-pkgs/gzip@0.0.0-split-runtime-preview.5d46b14': + resolution: {integrity: sha512-j8WENZbxekTBuMzLQCgqaA9vkkGGDn57mg/H4VaIIoiOWnUkKJMYbZ26twP0s6RbKDCe0vNWiAKDukhxe5jezQ==} + + '@agent-os-pkgs/sed@0.0.0-split-runtime-preview.5d46b14': + resolution: {integrity: sha512-uPEuPNIve/IOLYb9kJDFv8LDisg0jjcvdwLqdm9kacvOuc19wjhUZG2xPB8cjzKCgNtDN91jN+uo8F9no+oQBA==} + + '@agent-os-pkgs/tar@0.0.0-split-runtime-preview.5d46b14': + resolution: {integrity: sha512-mkh3mZtalQJ3n7M4769q30hvYSbI8wEIUF3LS9IzMQ9XfHW4NmIbGjDDrJxTuv8DepsN6SMS4lkNXjbY5yh4DQ==} + '@agentclientprotocol/sdk@0.16.1': resolution: {integrity: sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw==} peerDependencies: @@ -3748,15 +3805,6 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} - '@anthropic-ai/sdk@0.73.0': - resolution: {integrity: sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==} - hasBin: true - peerDependencies: - zod: ^3.25.0 || ^4.0.0 - peerDependenciesMeta: - zod: - optional: true - '@arethetypeswrong/cli@0.18.3': resolution: {integrity: sha512-GeAlc+lUD4gKHD/LDQNvQY30FfQ+xAXg2inbQKUjFZgTOdI5ygEweaOnGHGBPSKXSLGQC7VLhpXu9zMnYk/4sQ==} engines: {node: '>=20'} @@ -3812,147 +3860,6 @@ packages: resolution: {integrity: sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==} engines: {node: 18.20.8 || ^20.3.0 || >=22.0.0} - '@aws-crypto/crc32@5.2.0': - resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} - engines: {node: '>=16.0.0'} - - '@aws-crypto/sha256-browser@5.2.0': - resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} - - '@aws-crypto/sha256-js@5.2.0': - resolution: {integrity: sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==} - engines: {node: '>=16.0.0'} - - '@aws-crypto/supports-web-crypto@5.2.0': - resolution: {integrity: sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==} - - '@aws-crypto/util@5.2.0': - resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - - '@aws-sdk/client-bedrock-runtime@3.1024.0': - resolution: {integrity: sha512-nIhsn0/eYrL2fTh4kMO7Hpfmhv+AkkXl0KGNpD6+fdmotGvRBWcDv9/PmP/+sT6gvrKTYyzH3vu4efpTPzzP0Q==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/core@3.973.26': - resolution: {integrity: sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-env@3.972.24': - resolution: {integrity: sha512-FWg8uFmT6vQM7VuzELzwVo5bzExGaKHdubn0StjgrcU5FvuLExUe+k06kn/40uKv59rYzhez8eFNM4yYE/Yb/w==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-http@3.972.26': - resolution: {integrity: sha512-CY4ppZ+qHYqcXqBVi//sdHST1QK3KzOEiLtpLsc9W2k2vfZPKExGaQIsOwcyvjpjUEolotitmd3mUNY56IwDEA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-ini@3.972.28': - resolution: {integrity: sha512-wXYvq3+uQcZV7k+bE4yDXCTBdzWTU9x/nMiKBfzInmv6yYK1veMK0AKvRfRBd72nGWYKcL6AxwiPg9z/pYlgpw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-login@3.972.28': - resolution: {integrity: sha512-ZSTfO6jqUTCysbdBPtEX5OUR//3rbD0lN7jO3sQeS2Gjr/Y+DT6SbIJ0oT2cemNw3UzKu97sNONd1CwNMthuZQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-node@3.972.29': - resolution: {integrity: sha512-clSzDcvndpFJAggLDnDb36sPdlZYyEs5Zm6zgZjjUhwsJgSWiWKwFIXUVBcbruidNyBdbpOv2tNDL9sX8y3/0g==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-process@3.972.24': - resolution: {integrity: sha512-Q2k/XLrFXhEztPHqj4SLCNID3hEPdlhh1CDLBpNnM+1L8fq7P+yON9/9M1IGN/dA5W45v44ylERfXtDAlmMNmw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-sso@3.972.28': - resolution: {integrity: sha512-IoUlmKMLEITFn1SiCTjPfR6KrE799FBo5baWyk/5Ppar2yXZoUdaRqZzJzK6TcJxx450M8m8DbpddRVYlp5R/A==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-web-identity@3.972.28': - resolution: {integrity: sha512-d+6h0SD8GGERzKe27v5rOzNGKOl0D+l0bWJdqrxH8WSQzHzjsQFIAPgIeOTUwBHVsKKwtSxc91K/SWax6XgswQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/eventstream-handler-node@3.972.12': - resolution: {integrity: sha512-ruyc/MNR6e+cUrGCth7fLQ12RXBZDy/bV06tgqB9Z5n/0SN/C0m6bsQEV8FF9zPI6VSAOaRd0rNgmpYVnGawrQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-eventstream@3.972.8': - resolution: {integrity: sha512-r+oP+tbCxgqXVC3pu3MUVePgSY0ILMjA+aEwOosS77m3/DRbtvHrHwqvMcw+cjANMeGzJ+i0ar+n77KXpRA8RQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-host-header@3.972.8': - resolution: {integrity: sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-logger@3.972.8': - resolution: {integrity: sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-recursion-detection@3.972.9': - resolution: {integrity: sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-user-agent@3.972.28': - resolution: {integrity: sha512-cfWZFlVh7Va9lRay4PN2A9ARFzaBYcA097InT5M2CdRS05ECF5yaz86jET8Wsl2WcyKYEvVr/QNmKtYtafUHtQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/middleware-websocket@3.972.14': - resolution: {integrity: sha512-qnfDlIHjm6DrTYNvWOUbnZdVKgtoKbO/Qzj+C0Wp5Y7VUrsvBRQtGKxD+hc+mRTS4N0kBJ6iZ3+zxm4N1OSyjg==} - engines: {node: '>= 14.0.0'} - - '@aws-sdk/nested-clients@3.996.18': - resolution: {integrity: sha512-c7ZSIXrESxHKx2Mcopgd8AlzZgoXMr20fkx5ViPWPOLBvmyhw9VwJx/Govg8Ef/IhEon5R9l53Z8fdYSEmp6VA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/region-config-resolver@3.972.10': - resolution: {integrity: sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/token-providers@3.1021.0': - resolution: {integrity: sha512-TKY6h9spUk3OLs5v1oAgW9mAeBE3LAGNBwJokLy96wwmd4W2v/tYlXseProyed9ValDj2u1jK/4Rg1T+1NXyJA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/token-providers@3.1024.0': - resolution: {integrity: sha512-eoyTMgd6OzoE1dq50um5Y53NrosEkWsjH0W6pswi7vrv1W9hY/7hR43jDcPevqqj+OQksf/5lc++FTqRlb8Y1Q==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/types@3.973.5': - resolution: {integrity: sha512-hl7BGwDCWsjH8NkZfx+HgS7H2LyM2lTMAI7ba9c8O0KqdBLTdNJivsHpqjg9rNlAlPyREb6DeDRXUl0s8uFdmQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/types@3.973.6': - resolution: {integrity: sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/util-endpoints@3.996.5': - resolution: {integrity: sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/util-format-url@3.972.8': - resolution: {integrity: sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/util-locate-window@3.965.5': - resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/util-user-agent-browser@3.972.8': - resolution: {integrity: sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==} - - '@aws-sdk/util-user-agent-node@3.973.14': - resolution: {integrity: sha512-vNSB/DYaPOyujVZBg/zUznH9QC142MaTHVmaFlF7uzzfg3CgT9f/l4C0Yi+vU/tbBhxVcXVB90Oohk5+o+ZbWw==} - engines: {node: '>=20.0.0'} - peerDependencies: - aws-crt: '>=1.0.0' - peerDependenciesMeta: - aws-crt: - optional: true - - '@aws-sdk/xml-builder@3.972.16': - resolution: {integrity: sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==} - engines: {node: '>=20.0.0'} - - '@aws/lambda-invoke-store@0.2.3': - resolution: {integrity: sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==} - engines: {node: '>=18.0.0'} - '@babel/code-frame@7.10.4': resolution: {integrity: sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==} @@ -4623,61 +4530,31 @@ packages: cpu: [arm64] os: [darwin] - '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': - resolution: {integrity: sha512-ZKZ/F8US7JR92J4DMct6cLW/Y66o2K576+zjlEN/MevH70bFIsB10wkZEQPLzl2oNh2SMGy55xpJ9JoBRl5DOA==} - cpu: [arm64] - os: [darwin] - '@cbor-extract/cbor-extract-darwin-x64@2.2.0': resolution: {integrity: sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==} cpu: [x64] os: [darwin] - '@cbor-extract/cbor-extract-darwin-x64@2.2.2': - resolution: {integrity: sha512-32b1mgc+P61Js+KW9VZv/c+xRw5EfmOcPx990JbCBSkYJFY0l25VinvyyWfl+3KjibQmAcYwmyzKF9J4DyKP/Q==} - cpu: [x64] - os: [darwin] - '@cbor-extract/cbor-extract-linux-arm64@2.2.0': resolution: {integrity: sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==} cpu: [arm64] os: [linux] - '@cbor-extract/cbor-extract-linux-arm64@2.2.2': - resolution: {integrity: sha512-wfqgzqCAy/Vn8i6WVIh7qZd0DdBFaWBjPdB6ma+Wihcjv0gHqD/mw3ouVv7kbbUNrab6dKEx/w3xQZEdeXIlzg==} - cpu: [arm64] - os: [linux] - '@cbor-extract/cbor-extract-linux-arm@2.2.0': resolution: {integrity: sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==} cpu: [arm] os: [linux] - '@cbor-extract/cbor-extract-linux-arm@2.2.2': - resolution: {integrity: sha512-tNg0za41TpQfkhWjptD+0gSD2fggMiDCSacuIeELyb2xZhr7PrhPe5h66Jc67B/5dmpIhI2QOUtv4SBsricyYQ==} - cpu: [arm] - os: [linux] - '@cbor-extract/cbor-extract-linux-x64@2.2.0': resolution: {integrity: sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==} cpu: [x64] os: [linux] - '@cbor-extract/cbor-extract-linux-x64@2.2.2': - resolution: {integrity: sha512-rpiLnVEsqtPJ+mXTdx1rfz4RtUGYIUg2rUAZgd1KjiC1SehYUSkJN7Yh+aVfSjvCGtVP0/bfkQkXpPXKbmSUaA==} - cpu: [x64] - os: [linux] - '@cbor-extract/cbor-extract-win32-x64@2.2.0': resolution: {integrity: sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==} cpu: [x64] os: [win32] - '@cbor-extract/cbor-extract-win32-x64@2.2.2': - resolution: {integrity: sha512-dI+9P7cfWxkTQ+oE+7Aa6onEn92PHgfWXZivjNheCRmTBDBf2fx6RyTi0cmgpYLnD1KLZK9ZYrMxaPZ4oiXhGA==} - cpu: [x64] - os: [win32] - '@chevrotain/cst-dts-gen@11.0.3': resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} @@ -4739,7 +4616,6 @@ packages: '@copilotkit/aimock@1.7.0': resolution: {integrity: sha512-X6B2z0MgGTg8N/geRg6zRVVgEp3krP+gYapwXCt2w3JU7BSf2q0laa4iHC+BZqPXf29iVDVwDM7BxB5LqhjcAg==} engines: {node: '>=20.15.0'} - deprecated: This package has moved to @copilotkit/aimock hasBin: true '@copilotkit/llmock@1.7.1': @@ -5799,15 +5675,6 @@ packages: '@fortawesome/fontawesome-svg-core': ~6 || ~7 react: 19.1.0 - '@google/genai@1.48.0': - resolution: {integrity: sha512-plonYK4ML2PrxsRD9SeqmFt76eREWkQdPCglOA6aYDzL1AAbE+7PUnT54SvpWGfws13L0AZEqGSpL7+1IPnTxQ==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@modelcontextprotocol/sdk': ^1.25.2 - peerDependenciesMeta: - '@modelcontextprotocol/sdk': - optional: true - '@headlessui/react@2.2.9': resolution: {integrity: sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==} engines: {node: '>=10'} @@ -6336,95 +6203,6 @@ packages: '@marijn/find-cluster-break@1.0.2': resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} - '@mariozechner/clipboard-darwin-arm64@0.3.2': - resolution: {integrity: sha512-uBf6K7Je1ihsgvmWxA8UCGCeI+nbRVRXoarZdLjl6slz94Zs1tNKFZqx7aCI5O1i3e0B6ja82zZ06BWrl0MCVw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - - '@mariozechner/clipboard-darwin-universal@0.3.2': - resolution: {integrity: sha512-mxSheKTW2U9LsBdXy0SdmdCAE5HqNS9QUmpNHLnfJ+SsbFKALjEZc5oRrVMXxGQSirDvYf5bjmRyT0QYYonnlg==} - engines: {node: '>= 10'} - os: [darwin] - - '@mariozechner/clipboard-darwin-x64@0.3.2': - resolution: {integrity: sha512-U1BcVEoidvwIp95+HJswSW+xr28EQiHR7rZjH6pn8Sja5yO4Yoe3yCN0Zm8Lo72BbSOK/fTSq0je7CJpaPCspg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - - '@mariozechner/clipboard-linux-arm64-gnu@0.3.2': - resolution: {integrity: sha512-BsinwG3yWTIjdgNCxsFlip7LkfwPk+ruw/aFCXHUg/fb5XC/Ksp+YMQ7u0LUtiKzIv/7LMXgZInJQH6gxbAaqQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@mariozechner/clipboard-linux-arm64-musl@0.3.2': - resolution: {integrity: sha512-0/Gi5Xq2V6goXBop19ePoHvXsmJD9SzFlO3S+d6+T2b+BlPcpOu3Oa0wTjl+cZrLAAEzA86aPNBI+VVAFDFPKw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - - '@mariozechner/clipboard-linux-riscv64-gnu@0.3.2': - resolution: {integrity: sha512-2AFFiXB24qf0zOZsxI1GJGb9wQGlOJyN6UwoXqmKS3dpQi/l6ix30IzDDA4c4ZcCcx4D+9HLYXhC1w7Sov8pXA==} - engines: {node: '>= 10'} - cpu: [riscv64] - os: [linux] - - '@mariozechner/clipboard-linux-x64-gnu@0.3.2': - resolution: {integrity: sha512-v6fVnsn7WMGg73Dab8QMwyFce7tzGfgEixKgzLP8f1GJqkJZi5zO4k4FOHzSgUufgLil63gnxvMpjWkgfeQN7A==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@mariozechner/clipboard-linux-x64-musl@0.3.2': - resolution: {integrity: sha512-xVUtnoMQ8v2JVyfJLKKXACA6avdnchdbBkTsZs8BgJQo29qwCp5NIHAUO8gbJ40iaEGToW5RlmVk2M9V0HsHEw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - - '@mariozechner/clipboard-win32-arm64-msvc@0.3.2': - resolution: {integrity: sha512-AEgg95TNi8TGgak2wSXZkXKCvAUTjWoU1Pqb0ON7JHrX78p616XUFNTJohtIon3e0w6k0pYPZeCuqRCza/Tqeg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - - '@mariozechner/clipboard-win32-x64-msvc@0.3.2': - resolution: {integrity: sha512-tGRuYpZwDOD7HBrCpyRuhGnHHSCknELvqwKKUG4JSfSB7JIU7LKRh6zx6fMUOQd8uISK35TjFg5UcNih+vJhFA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - - '@mariozechner/clipboard@0.3.2': - resolution: {integrity: sha512-IHQpksNjo7EAtGuHFU+tbWDp5LarH3HU/8WiB9O70ZEoBPHOg0/6afwSLK0QyNMMmx4Bpi/zl6+DcBXe95nWYA==} - engines: {node: '>= 10'} - - '@mariozechner/jiti@2.6.5': - resolution: {integrity: sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==} - hasBin: true - - '@mariozechner/pi-agent-core@0.60.0': - resolution: {integrity: sha512-1zQcfFp8r0iwZCxCBQ9/ccFJoagns68cndLPTJJXl1ZqkYirzSld1zBOPxLAgeAKWIz3OX8dB2WQwTJFhmEojQ==} - engines: {node: '>=20.0.0'} - deprecated: please use @earendil-works/pi-agent-core instead going forward - - '@mariozechner/pi-ai@0.60.0': - resolution: {integrity: sha512-OiMuXQturnEDPmA+ho7eLe4G8plO2z21yjNMs9niQREauoblWOz7Glv58I66KPzczLED4aZTlQLTRdU6t1rz8A==} - engines: {node: '>=20.0.0'} - deprecated: please use @earendil-works/pi-ai instead going forward - hasBin: true - - '@mariozechner/pi-coding-agent@0.60.0': - resolution: {integrity: sha512-IOv7cTU4nbznFNUE5ofi13k2dmSG39coBoGWIBQTVw3iVyl0HxuHbg0NiTx3ktrPIDNtkii+y7tWXzWqwoo4lw==} - engines: {node: '>=20.6.0'} - deprecated: please use @earendil-works/pi-coding-agent instead going forward - hasBin: true - - '@mariozechner/pi-tui@0.60.0': - resolution: {integrity: sha512-ZAK5gxYhGmfJqMjfWcRBjB8glITltDbTrYJXvcDtfengbKTZN0p39p5uO5pvUB8/PiAWKTRS06yaNMhf/LG26g==} - engines: {node: '>=20.0.0'} - deprecated: please use @earendil-works/pi-tui instead going forward - '@marsidev/react-turnstile@1.5.0': resolution: {integrity: sha512-Ph6mcj8u9WBDsBO7s9jKPsyRDz1sBPBJwrk+Ngx09vFInvKsQ6U6kW5amEcGq4dHOreB6DgFrOJk7/fy318YlQ==} peerDependencies: @@ -6472,9 +6250,6 @@ packages: '@microsoft/tsdoc@0.15.1': resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} - '@mistralai/mistralai@1.14.1': - resolution: {integrity: sha512-IiLmmZFCCTReQgPAT33r7KQ1nYo5JPdvGkrkZqA8qQ2qB1GHgs5LoP5K2ICyrjnpw2n8oSxMM/VP+liiKcGNlQ==} - '@modelcontextprotocol/inspector-cli@0.14.3': resolution: {integrity: sha512-cAjCfwJUfN1WHc/sGgY/yAQ7K02WOKIso+LzVoKzEr50Nf4R+WKEuq6lhnLfG3f61sU823V8TxRscc8NTYTgww==} hasBin: true @@ -6949,36 +6724,6 @@ packages: peerDependencies: '@opentelemetry/api': ^1.8 - '@protobufjs/aspromise@1.1.2': - resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} - - '@protobufjs/base64@1.1.2': - resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} - - '@protobufjs/codegen@2.0.4': - resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} - - '@protobufjs/eventemitter@1.1.0': - resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} - - '@protobufjs/fetch@1.1.0': - resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} - - '@protobufjs/float@1.0.2': - resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - - '@protobufjs/inquire@1.1.0': - resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} - - '@protobufjs/path@1.1.2': - resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} - - '@protobufjs/pool@1.1.0': - resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} - - '@protobufjs/utf8@1.1.0': - resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} - '@publint/pack@0.1.4': resolution: {integrity: sha512-HDVTWq3H0uTXiU0eeSQntcVUTPP3GamzeXI41+x7uU9J65JgWQh3qWZHblR1i0npXfFtF+mxBiU2nJH8znxWnQ==} engines: {node: '>=18'} @@ -7725,9 +7470,6 @@ packages: '@rivet-dev/agent-os-common@0.0.260331072558': resolution: {integrity: sha512-bkMU6yqLxNLoXYA2/f2qRf0JrlqbIiBRKRpP3d+tbmF+VV2wrAY+iLJDHN6Jw+Q8q3CkT6jB5KTnCZkcYEF3RQ==} - '@rivet-dev/agent-os-core@0.1.1': - resolution: {integrity: sha512-Uw5jr+gUXDY7TDUFqlypjGe1BD2KL9kTHPNo/f1iNS1R+l9IWuvT+FF/MXsLOEkc3fB06OPVu2ZvUuNwp9MpLQ==} - '@rivet-dev/agent-os-coreutils@0.0.260331072558': resolution: {integrity: sha512-vI/J2MJnpsJQZ7F5DU/udGqGMtliqhTg28lE6XZ/qEGyjUvmEQ9AiV8CaAcX5HrImAUWOofBbTv6/XGKjOhT1w==} @@ -7746,21 +7488,6 @@ packages: '@rivet-dev/agent-os-gzip@0.0.260331072558': resolution: {integrity: sha512-id9iOrZFAuaXaFWIIiQEo70Ze/poI7hodSqWfo9ro0sajV0xI7kSp0BbXgS4PvGc9O2+VWcOE2qzqTUnKNlBzQ==} - '@rivet-dev/agent-os-pi@0.1.1': - resolution: {integrity: sha512-+PWE8Kubl6ZfXAPETFXhPRBoPKJbsnbXKK3gvrgGqvBa7kJvzPCYM5muUipSsSPwXurLOtIN+vnCiu8bU/Q7kA==} - hasBin: true - - '@rivet-dev/agent-os-posix@0.1.0': - resolution: {integrity: sha512-NIrI7cCb9x6jdmzRPPx7dAeXoTF/YCqf93ydEzYFA2zshIelLW9Rp5KtgP/2hM6fP0ly4+vVnOeavxJW0wYtcA==} - - '@rivet-dev/agent-os-python@0.1.0': - resolution: {integrity: sha512-1tH1beMf1ceSpicQKwN/a6h+NmJrmfuT4GStiRDZmvN/UWfZhkxuy7HR5VPTQpE/feUZJ01FdtBS3Em/Qoxb2Q==} - peerDependencies: - pyodide: '>=0.28.0' - peerDependenciesMeta: - pyodide: - optional: true - '@rivet-dev/agent-os-sed@0.0.260331072558': resolution: {integrity: sha512-i/6ifWCcGE2TEfPWR5ig5tMVKUc0qL0ng59htgS+sqt6zdMaPIllwVztOgXNxMEdFM1TF646e/OkMfMzmYZDCA==} @@ -7778,6 +7505,12 @@ packages: resolution: {integrity: sha512-3qndQUQXLdwafMEqfhz24hUtDPcsf1Bu3q52Kb8MqeH8JUh3h6R4HYW3ZJXiQsLcyYyFM68PuIwlLRlg1xDEpg==} engines: {node: ^14.18.0 || >=16.0.0} + '@rivetkit/engine-cli-linux-x64-musl@2.3.0-rc.12': + resolution: {integrity: sha512-z+mycjLHItRGj5hpc2I4gBYAidXWFZvYZy92Ch48g7XJsLUiEpleZJN4b+Afgj1XSwRrzeR9k1cV8cswZjTc+g==} + engines: {node: '>= 20.0.0'} + cpu: [x64] + os: [linux] + '@rivetkit/on-change@6.0.1': resolution: {integrity: sha512-QBN/KRBXLJdCgN4gBTL3XAc/zKm58atSnieXWMOyFSPmo6F1/yIVV/LTRdvAktfCttrGx7W6c32i/lwqCHWnsQ==} engines: {node: '>=20'} @@ -7974,37 +7707,56 @@ packages: '@rushstack/ts-command-line@5.1.2': resolution: {integrity: sha512-jn0EnSefYrkZDrBGd6KGuecL84LI06DgzL4hVQ46AUijNBt2nRU/ST4HhrfII/w91siCd1J/Okvxq/BS75Me/A==} - '@sec-ant/readable-stream@0.4.1': - resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - - '@secure-exec/core@0.2.1': - resolution: {integrity: sha512-HsnUv6gClpMA1BBRmX86j30TKTZtgJC/fO1tVavr7IpM2zNKbHU8LgSlBd7mv2SNy02ImTmU/GnQ3aYB4NSbEg==} - - '@secure-exec/nodejs@0.2.1': - resolution: {integrity: sha512-UJMJqVFxexlHJV0Q9nWURvrz6GElj8673DDOOFln6FHR6JS+9SaSU3eISrN158DuNC3SFi4rgjb/scKnK4YOYQ==} - - '@secure-exec/v8-darwin-arm64@0.2.1': - resolution: {integrity: sha512-gEWhMHzUpLwzuBNAD0lVkZXE8wFlWMLp4IOZ+56FYwOW/C+m07cYxuW4TjHyPqZ+vPm3IkoaMqqH5yT9VhjX/Q==} + '@sandbox-agent/cli-darwin-arm64@0.4.2': + resolution: {integrity: sha512-+L1O8SI7k/LLhyB4dG0ghmz1cJHa0WtVjuRTrEE2gw/5EbGLWopPBsCVCmQ7snrQ4fPwtaiZDhfExcEj1VI7aw==} cpu: [arm64] os: [darwin] - '@secure-exec/v8-darwin-x64@0.2.1': - resolution: {integrity: sha512-H2Z5K+Cq+fn/kxjGvhJzepnNFWG6qNdyhZybVWGr5bAAZoSz/Qkad4WnXcurWU+880tKDtnf19LHBXrg7zewNQ==} + '@sandbox-agent/cli-darwin-x64@0.4.2': + resolution: {integrity: sha512-dDg/EwWsdgVVbJiiCX1scSNRRA48u77SsC7Tuqrfzx4fIJMLuLiIcmEtXQyCBWysSyQNV2Cr+PYXXQfCb3xg8g==} cpu: [x64] os: [darwin] - '@secure-exec/v8-linux-arm64-gnu@0.2.1': - resolution: {integrity: sha512-14subGhVV/gW35mYYm7Gv1Keeex7PxIgQfoKji/JH7wYyDuarP6kgaES0nJw+JXVkxEVud52c+kbcIjIggqCEw==} + '@sandbox-agent/cli-linux-arm64@0.4.2': + resolution: {integrity: sha512-TGmTUexMoubmWQyTeaOJu0rDVl2h0Ifh1pZ0ceZy7u/6Eoqs2n46CbfQtasUxZJf10uxPgRyzEDhcdDrTYVQUA==} cpu: [arm64] os: [linux] - '@secure-exec/v8-linux-x64-gnu@0.2.1': - resolution: {integrity: sha512-Az4s+vUf+78vWtsC7rTn/jQc6WKJafAdt2YpEjB4Gnu+sX+FFTIst1hRV4gJonbRyJdy6SW+OQ6DZatmwczorQ==} + '@sandbox-agent/cli-linux-x64@0.4.2': + resolution: {integrity: sha512-H9Rbqq0DRkCHvakzefJUDrDa2y+vJjlYd5/tefzKbQ34locE13TGNygRLxdEVXpBECjK9wVdBwTVEphQNsOcjw==} + cpu: [x64] + os: [linux] + + '@sandbox-agent/cli-shared@0.4.2': + resolution: {integrity: sha512-sjZXRkKeFXCSKR6hHzF2Af8CCRO3F3WFwVQJ22+sLTXJ2xskV8lkUE4egknQU9B5BC1Zumts/YiNCFQWG85awQ==} + + '@sandbox-agent/cli-win32-x64@0.4.2': + resolution: {integrity: sha512-lZNfHWPwQe/VH51Yvrl/ATCUvBZ3a+c8mwovojhQcmZlv4QuUQPkuvxhPqHRh9AyBx78L5J/ha46es2doa34nQ==} + cpu: [x64] + os: [win32] + + '@sandbox-agent/cli@0.4.2': + resolution: {integrity: sha512-trO//ypJBSt5xkewuol9LOykvDgHwUXq8R+yQVS+0CmpN3lYUtewHkb+At9RVGRhDMmJZY2oasaXDnhfurQ33w==} + hasBin: true + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@secure-exec/core@0.0.0-split-runtime-preview.5d46b14': + resolution: {integrity: sha512-U7oWReKG4K+7o0x+nP2qx0CUYGoO2RvRbUZJXhy2iG2ce+Axe/+o5j/NaondRu1C37Tx2EjVwjP1hNhmD0DTNA==} + + '@secure-exec/s3@0.0.0-split-runtime-preview.5d46b14': + resolution: {integrity: sha512-X7stPV4f5/J+t3qBd/8YQFty9iNFNCD7ipAp0rwVDVNdhjA1xyAMKvQXfG11rbe2Y5o1P43k5cT6Z10pZtFevw==} + + '@secure-exec/sidecar-linux-x64-gnu@0.0.0-split-runtime-preview.5d46b14': + resolution: {integrity: sha512-litPZ/HgzA1VjjOGaBuP5uLj6PXiTGJJF7q12NXM7xN+nqFrSDRrwgByPde4TxX2kbk1zP+hYM08S+qqdrAwxQ==} + engines: {node: '>=20'} cpu: [x64] os: [linux] - '@secure-exec/v8@0.2.1': - resolution: {integrity: sha512-ye/seCqzvyMGnvyP+AO7RkVMR/lE3x9m0D2PfmiAXA457R78ZmOFmZ6v+JlJG2vv3LM30KsSXTUhwpG+Teh0hw==} + '@secure-exec/sidecar@0.0.0-split-runtime-preview.5d46b14': + resolution: {integrity: sha512-Zfj6i7UNikc4SXtyBYRcMydHFEsrmUT0VnG8lt8hTz6Ux6YgBmIXmzMZntyJnDfa+ayJoYbNnfcjEiy4AvTr8Q==} + engines: {node: '>=20'} '@sentry-internal/browser-utils@10.42.0': resolution: {integrity: sha512-HCEICKvepxN4/6NYfnMMMlppcSwIEwtS66X6d1/mwaHdi2ivw0uGl52p7Nfhda/lIJArbrkWprxl0WcjZajhQA==} @@ -8283,9 +8035,6 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} - '@silvia-odwyer/photon-node@0.3.4': - resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} - '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -8318,198 +8067,6 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} - '@smithy/config-resolver@4.4.13': - resolution: {integrity: sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==} - engines: {node: '>=18.0.0'} - - '@smithy/core@3.23.13': - resolution: {integrity: sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q==} - engines: {node: '>=18.0.0'} - - '@smithy/credential-provider-imds@4.2.12': - resolution: {integrity: sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-codec@4.2.12': - resolution: {integrity: sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-serde-browser@4.2.12': - resolution: {integrity: sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-serde-config-resolver@4.3.12': - resolution: {integrity: sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-serde-node@4.2.12': - resolution: {integrity: sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-serde-universal@4.2.12': - resolution: {integrity: sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==} - engines: {node: '>=18.0.0'} - - '@smithy/fetch-http-handler@5.3.15': - resolution: {integrity: sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==} - engines: {node: '>=18.0.0'} - - '@smithy/hash-node@4.2.12': - resolution: {integrity: sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==} - engines: {node: '>=18.0.0'} - - '@smithy/invalid-dependency@4.2.12': - resolution: {integrity: sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==} - engines: {node: '>=18.0.0'} - - '@smithy/is-array-buffer@2.2.0': - resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} - engines: {node: '>=14.0.0'} - - '@smithy/is-array-buffer@4.2.2': - resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-content-length@4.2.12': - resolution: {integrity: sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-endpoint@4.4.28': - resolution: {integrity: sha512-p1gfYpi91CHcs5cBq982UlGlDrxoYUX6XdHSo91cQ2KFuz6QloHosO7Jc60pJiVmkWrKOV8kFYlGFFbQ2WUKKQ==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-retry@4.4.46': - resolution: {integrity: sha512-SpvWNNOPOrKQGUqZbEPO+es+FRXMWvIyzUKUOYdDgdlA6BdZj/R58p4umoQ76c2oJC44PiM7mKizyyex1IJzow==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-serde@4.2.16': - resolution: {integrity: sha512-beqfV+RZ9RSv+sQqor3xroUUYgRFCGRw6niGstPG8zO9LgTl0B0MCucxjmrH/2WwksQN7UUgI7KNANoZv+KALA==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-stack@4.2.12': - resolution: {integrity: sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==} - engines: {node: '>=18.0.0'} - - '@smithy/node-config-provider@4.3.12': - resolution: {integrity: sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==} - engines: {node: '>=18.0.0'} - - '@smithy/node-http-handler@4.5.1': - resolution: {integrity: sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw==} - engines: {node: '>=18.0.0'} - - '@smithy/property-provider@4.2.12': - resolution: {integrity: sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==} - engines: {node: '>=18.0.0'} - - '@smithy/protocol-http@5.3.12': - resolution: {integrity: sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==} - engines: {node: '>=18.0.0'} - - '@smithy/querystring-builder@4.2.12': - resolution: {integrity: sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==} - engines: {node: '>=18.0.0'} - - '@smithy/querystring-parser@4.2.12': - resolution: {integrity: sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==} - engines: {node: '>=18.0.0'} - - '@smithy/service-error-classification@4.2.12': - resolution: {integrity: sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==} - engines: {node: '>=18.0.0'} - - '@smithy/shared-ini-file-loader@4.4.7': - resolution: {integrity: sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==} - engines: {node: '>=18.0.0'} - - '@smithy/signature-v4@5.3.12': - resolution: {integrity: sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==} - engines: {node: '>=18.0.0'} - - '@smithy/smithy-client@4.12.8': - resolution: {integrity: sha512-aJaAX7vHe5i66smoSSID7t4rKY08PbD8EBU7DOloixvhOozfYWdcSYE4l6/tjkZ0vBZhGjheWzB2mh31sLgCMA==} - engines: {node: '>=18.0.0'} - - '@smithy/types@4.13.0': - resolution: {integrity: sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==} - engines: {node: '>=18.0.0'} - - '@smithy/types@4.13.1': - resolution: {integrity: sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==} - engines: {node: '>=18.0.0'} - - '@smithy/url-parser@4.2.12': - resolution: {integrity: sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==} - engines: {node: '>=18.0.0'} - - '@smithy/util-base64@4.3.2': - resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-body-length-browser@4.2.2': - resolution: {integrity: sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-body-length-node@4.2.3': - resolution: {integrity: sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==} - engines: {node: '>=18.0.0'} - - '@smithy/util-buffer-from@2.2.0': - resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} - engines: {node: '>=14.0.0'} - - '@smithy/util-buffer-from@4.2.2': - resolution: {integrity: sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==} - engines: {node: '>=18.0.0'} - - '@smithy/util-config-provider@4.2.2': - resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-defaults-mode-browser@4.3.44': - resolution: {integrity: sha512-eZg6XzaCbVr2S5cAErU5eGBDaOVTuTo1I65i4tQcHENRcZ8rMWhQy1DaIYUSLyZjsfXvmCqZrstSMYyGFocvHA==} - engines: {node: '>=18.0.0'} - - '@smithy/util-defaults-mode-node@4.2.48': - resolution: {integrity: sha512-FqOKTlqSaoV3nzO55pMs5NBnZX8EhoI0DGmn9kbYeXWppgHD6dchyuj2HLqp4INJDJbSrj6OFYJkAh/WhSzZPg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-endpoints@3.3.3': - resolution: {integrity: sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==} - engines: {node: '>=18.0.0'} - - '@smithy/util-hex-encoding@4.2.2': - resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-middleware@4.2.12': - resolution: {integrity: sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-retry@4.2.13': - resolution: {integrity: sha512-qQQsIvL0MGIbUjeSrg0/VlQ3jGNKyM3/2iU3FPNgy01z+Sp4OvcaxbgIoFOTvB61ZoohtutuOvOcgmhbD0katQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-stream@4.5.21': - resolution: {integrity: sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q==} - engines: {node: '>=18.0.0'} - - '@smithy/util-uri-escape@4.2.2': - resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==} - engines: {node: '>=18.0.0'} - - '@smithy/util-utf8@2.3.0': - resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} - engines: {node: '>=14.0.0'} - - '@smithy/util-utf8@4.2.2': - resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} - engines: {node: '>=18.0.0'} - - '@smithy/uuid@1.1.2': - resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} - engines: {node: '>=18.0.0'} - '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -8912,9 +8469,6 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} - '@tootallnate/quickjs-emscripten@0.23.0': - resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} - '@trpc/client@11.6.0': resolution: {integrity: sha512-DyWbYk2hd50BaVrXWVkaUnaSwgAF5g/lfBkXtkF1Aqlk6BtSzGUo3owPkgqQO2I5LwWy1+ra9TsSfBBvIZpTwg==} peerDependencies: @@ -9163,9 +8717,6 @@ packages: '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} - '@types/mime-types@2.1.4': - resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} - '@types/mime@4.0.0': resolution: {integrity: sha512-5eEkJZ/BLvTE3vXGKkWlyTSUVZuzj23Wj8PoyOq2lt5I3CYbiLBOPb3XmCW6QcuOibIUE6emHXHt9E/F/rCa6w==} deprecated: This is a stub types definition. mime provides its own type definitions, so you do not need this installed. @@ -9241,9 +8792,6 @@ packages: '@types/reconnectingwebsocket@1.0.10': resolution: {integrity: sha512-30Pq4D3o8BKcdY53dzr0elGFyB/ChYpGrHiRH/GuaZKXXGWq/CsD1QBEu1b8IgdHReOKpo9tjk80UaxSbuXoTQ==} - '@types/retry@0.12.0': - resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} - '@types/retry@0.12.2': resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==} @@ -9301,9 +8849,6 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@types/yauzl@2.10.3': - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@uiw/codemirror-extensions-basic-setup@4.25.1': resolution: {integrity: sha512-zxgA2QkvP3ZDKxTBc9UltNFTrSeFezGXcZtZj6qcsBxiMzowoEMP5mVwXcKjpzldpZVRuY+JCC+RsekEgid4vg==} peerDependencies: @@ -9719,6 +9264,9 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acp-http-client@0.4.2: + resolution: {integrity: sha512-3wtPieF08YIU4vNXaoL5up/1D0if4i9IX3Ye5q/bwbcwg1BKsazIK/VNNfvN4ldbPjWul69IqIOpGRS3I0qo3Q==} + actor-core@0.6.3: resolution: {integrity: sha512-cdYf0GX3m3jvlubbdujOcnPn93r1fP9F0mEBso72ofMTI0+EeGMS34BNrmaGmk5Pb3iD45KQl3u5ZY5Mzv4DNg==} hasBin: true @@ -9899,10 +9447,6 @@ packages: resolution: {integrity: sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw==} engines: {node: '>=20.19.0'} - ast-types@0.13.4: - resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} - engines: {node: '>=4'} - ast-types@0.16.1: resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} engines: {node: '>=4'} @@ -10048,11 +9592,6 @@ packages: resolution: {integrity: sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==} hasBin: true - basic-ftp@5.2.0: - resolution: {integrity: sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==} - engines: {node: '>=10.0.0'} - deprecated: Security vulnerability fixed in 5.2.1, please upgrade - bcp-47-match@2.0.3: resolution: {integrity: sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==} @@ -10141,9 +9680,6 @@ packages: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} engines: {node: '>=0.6'} - bignumber.js@9.3.1: - resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} - binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -10167,9 +9703,6 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} - bowser@2.14.1: - resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} - boxen@7.0.0: resolution: {integrity: sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==} engines: {node: '>=14.16'} @@ -10237,12 +9770,6 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} - buffer-crc32@0.2.13: - resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - - buffer-equal-constant-time@1.0.1: - resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -10339,16 +9866,9 @@ packages: resolution: {integrity: sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==} hasBin: true - cbor-extract@2.2.2: - resolution: {integrity: sha512-hlSxxI9XO2yQfe9g6msd3g4xCfDqK5T5P0fRMLuaLHhxn4ViPrm+a+MUfhrvH2W962RGxcBwEGzLQyjbDG1gng==} - hasBin: true - cbor-x@1.6.0: resolution: {integrity: sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg==} - cbor-x@1.6.4: - resolution: {integrity: sha512-UGKHjp6RHC6QuZ2yy5LCKm7MojM4716DwoSaqwQpaH4DvZvbBTGcoDNTiG9Y2lByXZYFEs9WRkS5tLl96IrF1Q==} - ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -10749,10 +10269,6 @@ packages: crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} - croner@10.0.1: - resolution: {integrity: sha512-ixNtAJndqh173VQ4KodSdJEI6nuioBWI0V1ITNKhZZsO0pEMoDxz539T4FTTbSZ/xIOSuDnzxLVRqBVSvPNE2g==} - engines: {node: '>=18.0'} - cross-fetch@4.1.0: resolution: {integrity: sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==} @@ -10965,14 +10481,6 @@ packages: dagre-d3-es@7.0.13: resolution: {integrity: sha512-efEhnxpSuwpYOKRm/L5KbqoZmNNukHa/Flty4Wp62JRvgH2ojwVgPgdYyr4twpieZnyRDdIH7PY2mopX26+j2Q==} - data-uri-to-buffer@4.0.1: - resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} - engines: {node: '>= 12'} - - data-uri-to-buffer@6.0.2: - resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} - engines: {node: '>= 14'} - date-fns@4.1.0: resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} @@ -11088,10 +10596,6 @@ packages: defu@6.1.4: resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} - degenerator@5.0.1: - resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} - engines: {node: '>= 14'} - delaunator@5.0.1: resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} @@ -11427,9 +10931,6 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - ecdsa-sig-formatter@1.0.11: - resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} - ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -11594,11 +11095,6 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - escodegen@2.1.0: - resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} - engines: {node: '>=6.0'} - hasBin: true - eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -11860,11 +11356,6 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - extract-zip@2.0.1: - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} - engines: {node: '>= 10.17.0'} - hasBin: true - fast-check@4.8.0: resolution: {integrity: sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==} engines: {node: '>=12.17.0'} @@ -11914,13 +11405,6 @@ packages: fast-wrap-ansi@0.2.0: resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} - fast-xml-builder@1.1.4: - resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} - - fast-xml-parser@5.5.8: - resolution: {integrity: sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==} - hasBin: true - fastq@1.20.1: resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} @@ -11953,9 +11437,6 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} - fd-slicer@1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} - fdb-tuple@1.0.0: resolution: {integrity: sha512-8jSvKPCYCgTpi9Pt87qlfTk6griyMx4Gk3Xv31Dp72Qp8b6XgIyFsMm8KzPmFJ9iJ8K4pGvRxvOS8D0XGnrkjw==} @@ -11972,10 +11453,6 @@ packages: resolution: {integrity: sha512-qGNhgYygnefSkAHHrNHqC7p3R8J0/xQDS/cYUud8er/qD9EFGWyCdUDfULHTJQN1d3H3WprzVwMc9MfB4J50Wg==} engines: {node: '>=20', pnpm: '>=10'} - fetch-blob@3.2.0: - resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} - engines: {node: ^12.20 || >= 14.13} - fflate@0.4.8: resolution: {integrity: sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==} @@ -12096,10 +11573,6 @@ packages: resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} engines: {node: '>= 12.20'} - formdata-polyfill@4.0.10: - resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} - engines: {node: '>=12.20.0'} - forwarded-parse@2.1.2: resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} @@ -12189,14 +11662,6 @@ packages: resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==} engines: {node: '>=10'} - gaxios@7.1.4: - resolution: {integrity: sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==} - engines: {node: '>=18'} - - gcp-metadata@8.1.2: - resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} - engines: {node: '>=18'} - generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -12236,10 +11701,6 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -12255,10 +11716,6 @@ packages: get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} - get-uri@6.0.5: - resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} - engines: {node: '>= 14'} - getenv@1.0.0: resolution: {integrity: sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==} engines: {node: '>=6'} @@ -12327,14 +11784,6 @@ packages: peerDependencies: csstype: ^3.0.10 - google-auth-library@10.6.2: - resolution: {integrity: sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==} - engines: {node: '>=18'} - - google-logging-utils@1.1.3: - resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} - engines: {node: '>=14'} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -12517,10 +11966,6 @@ packages: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} - hosted-git-info@9.0.2: - resolution: {integrity: sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==} - engines: {node: ^20.17.0 || >=22.9.0} - html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} @@ -12548,10 +11993,6 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} - http-proxy-agent@7.0.2: - resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} - engines: {node: '>= 14'} - https-browserify@1.0.0: resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} @@ -12677,10 +12118,6 @@ packages: resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==} engines: {node: '>=12.22.0'} - ip-address@10.1.0: - resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} - engines: {node: '>= 12'} - ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -12985,19 +12422,12 @@ packages: engines: {node: '>=6'} hasBin: true - json-bigint@1.0.0: - resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} - json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - json-schema-to-ts@3.1.1: - resolution: {integrity: sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==} - engines: {node: '>=16'} - json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} @@ -13032,12 +12462,6 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} - jwa@2.0.1: - resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} - - jws@4.0.1: - resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} - katex@0.16.27: resolution: {integrity: sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==} hasBin: true @@ -13067,9 +12491,6 @@ packages: resolution: {integrity: sha512-zPPuIt+ku1iCpFBRwseMcPYQ1cJL8l60rSmKeOuGfOXyE6YnTBmf2aEFNL2HQGrD0cPcLO/t+v9RTgC+fwEh/g==} engines: {node: ^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4} - koffi@2.15.4: - resolution: {integrity: sha512-6l7xxt8heHWQ63WyGd8ofne4TrzhqeKHhvSlI3GnxMIHp3PlDrOPyZbW5YNINXNma1qrKkpM/PGLY8U0V8Hxbw==} - kolorist@1.8.0: resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} @@ -13320,12 +12741,6 @@ packages: resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} engines: {node: '>= 0.6.0'} - long-timeout@0.1.1: - resolution: {integrity: sha512-BFRuQUqc7x2NWxfJBCyUrN8iYUYznzL9JROmRz1gZ6KlOIgmoD+njPVbb+VNn2nGMKggMsK79iUNErillsrx7w==} - - long@5.3.2: - resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} - longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -13360,10 +12775,6 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - lucide-react@0.344.0: resolution: {integrity: sha512-6YyBnn91GB45VuVT96bYCOKElbJzUHqp65vX8cDcu55MQL9T969v4dhGClpljamuI/+KMO9P6w9Acq1CVQGvIQ==} peerDependencies: @@ -13428,11 +12839,6 @@ packages: engines: {node: '>= 18'} hasBin: true - marked@15.0.12: - resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} - engines: {node: '>= 18'} - hasBin: true - marked@16.4.2: resolution: {integrity: sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==} engines: {node: '>= 20'} @@ -14026,10 +13432,6 @@ packages: nested-error-stacks@2.0.1: resolution: {integrity: sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==} - netmask@2.0.2: - resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} - engines: {node: '>= 0.4.0'} - next@16.1.1: resolution: {integrity: sha512-QI+T7xrxt1pF6SQ/JYFz95ro/mg/1Znk5vBebsWwbpejj1T0A23hO7GYEaVac9QUOT2BIMiuzm0L99ooq7k0/w==} engines: {node: '>=20.9.0'} @@ -14088,10 +13490,6 @@ packages: encoding: optional: true - node-fetch@3.3.2: - resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} - engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-forge@1.3.3: resolution: {integrity: sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==} engines: {node: '>= 6.13.0'} @@ -14275,18 +13673,6 @@ packages: zod: optional: true - openai@6.26.0: - resolution: {integrity: sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==} - hasBin: true - peerDependencies: - ws: ^8.18.0 - zod: ^3.25 || ^4.0 - peerDependenciesMeta: - ws: - optional: true - zod: - optional: true - openapi-types@12.1.3: resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} @@ -14343,10 +13729,6 @@ packages: resolution: {integrity: sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==} engines: {node: '>=18'} - p-retry@4.6.2: - resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} - engines: {node: '>=8'} - p-retry@6.2.1: resolution: {integrity: sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==} engines: {node: '>=16.17'} @@ -14359,14 +13741,6 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - pac-proxy-agent@7.2.0: - resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} - engines: {node: '>= 14'} - - pac-resolver@7.0.1: - resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} - engines: {node: '>= 14'} - package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -14425,9 +13799,6 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} - partial-json@0.1.7: - resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} - pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} @@ -14444,10 +13815,6 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-expression-matcher@1.2.1: - resolution: {integrity: sha512-d7gQQmLvAKXKXE2GeP9apIGbMYKz88zWdsn/BN2HRWVQsDFdUY36WSLTY0Jvd4HWi7Fb30gQ62oAOzdgJA6fZw==} - engines: {node: '>=14.0.0'} - path-is-absolute@1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} @@ -14508,9 +13875,6 @@ packages: resolution: {integrity: sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==} engines: {node: '>= 0.10'} - pend@1.2.0: - resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} - pg-cloudflare@1.3.0: resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} @@ -14898,24 +14262,13 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - proper-lockfile@4.1.2: - resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} - property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} - protobufjs@7.5.4: - resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} - engines: {node: '>=12.0.0'} - proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} - proxy-agent@6.5.0: - resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} - engines: {node: '>= 14'} - proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} @@ -15448,10 +14801,6 @@ packages: retext@9.0.0: resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} - retry@0.12.0: - resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} - engines: {node: '>= 4'} - retry@0.13.1: resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} engines: {node: '>= 4'} @@ -15524,6 +14873,38 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sandbox-agent@0.4.2: + resolution: {integrity: sha512-fH6WDQEaIrgiu93LxZcy+4Dx+t+/cslu+hzXImDyUlsaL6jV2jIv4fdxELkALlo7uzyEDVK9lmqs9qy65RHwBQ==} + peerDependencies: + '@cloudflare/sandbox': '>=0.1.0' + '@daytonaio/sdk': '>=0.12.0' + '@e2b/code-interpreter': '>=1.0.0' + '@fly/sprites': '>=0.0.1' + '@vercel/sandbox': '>=0.1.0' + computesdk: '>=0.1.0' + dockerode: '>=4.0.0' + get-port: '>=7.0.0' + modal: '>=0.1.0' + peerDependenciesMeta: + '@cloudflare/sandbox': + optional: true + '@daytonaio/sdk': + optional: true + '@e2b/code-interpreter': + optional: true + '@fly/sprites': + optional: true + '@vercel/sandbox': + optional: true + computesdk: + optional: true + dockerode: + optional: true + get-port: + optional: true + modal: + optional: true + sass@1.93.2: resolution: {integrity: sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==} engines: {node: '>=14.0.0'} @@ -15547,9 +14928,6 @@ packages: resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} engines: {node: '>= 10.13.0'} - secure-exec@0.2.1: - resolution: {integrity: sha512-oaQDzTPDSCOckYC8G0PimIqzEVxY6sYEvcx0fMGsRR/Wl4wkFVHaZgQ3kc2DHWysV6WHWt5g1AXc/6seafO2XQ==} - secure-exec@https://pkg.pr.new/rivet-dev/secure-exec@7659aba: resolution: {tarball: https://pkg.pr.new/rivet-dev/secure-exec@7659aba} version: 0.1.0 @@ -15736,10 +15114,6 @@ packages: resolution: {integrity: sha512-HVk9X1E0gz3mSpoi60h/saazLKXKaZThMLU3u/aNwoYn8/xQyX2MGxL0ui2eaokkD7tF+Zo+cKTHUbe1mmmGzA==} engines: {node: '>=8.0.0'} - smart-buffer@4.2.0: - resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} - engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - smol-toml@1.6.0: resolution: {integrity: sha512-4zemZi0HvTnYwLfrpk/CF9LOd9Lt87kAt50GnqhMpyF9U3poDAP2+iukq2bZsO/ufegbYehBkqINbsWxj4l4cw==} engines: {node: '>= 18'} @@ -15747,14 +15121,6 @@ packages: snake-case@3.0.4: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} - socks-proxy-agent@8.0.5: - resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} - engines: {node: '>= 14'} - - socks@2.8.7: - resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} - engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - sonic-boom@4.2.0: resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} @@ -15940,9 +15306,6 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - strnum@2.2.0: - resolution: {integrity: sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==} - strtok3@10.3.4: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} engines: {node: '>=18'} @@ -16237,9 +15600,6 @@ packages: trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - ts-algebra@2.0.0: - resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} - ts-dedent@2.2.0: resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} engines: {node: '>=6.10'} @@ -16267,6 +15627,7 @@ packages: tsconfck@3.1.6: resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} engines: {node: ^18 || >=20} + deprecated: unmaintained hasBin: true peerDependencies: typescript: ^5.0.0 @@ -16462,10 +15823,6 @@ packages: resolution: {integrity: sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==} engines: {node: '>=18.17'} - undici@7.24.7: - resolution: {integrity: sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==} - engines: {node: '>=20.18.1'} - undici@8.3.0: resolution: {integrity: sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==} engines: {node: '>=22.19.0'} @@ -17142,18 +16499,10 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} - web-streams-polyfill@3.3.3: - resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} - engines: {node: '>= 8'} - web-streams-polyfill@4.0.0-beta.3: resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} engines: {node: '>= 14'} - web-streams-polyfill@4.2.0: - resolution: {integrity: sha512-0rYDzGOh9EZpig92umN5g5D/9A1Kff7k0/mzPSSCY8jEQeYkgRMoY7LhbXtUCWzLCMX0TUE9aoHkjFNB7D9pfA==} - engines: {node: '>= 8'} - web-vitals@4.2.4: resolution: {integrity: sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==} @@ -17400,9 +16749,6 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - yjs@13.6.29: resolution: {integrity: sha512-kHqDPdltoXH+X4w1lVmMtddE3Oeqq48nM40FD5ojTd8xYhQpzIDcfE2keMSU5bAgRPJBe225WTUdyUgj1DtbiQ==} engines: {node: '>=16.0.0', npm: '>=8.0.0'} @@ -17506,13 +16852,36 @@ snapshots: '@adobe/css-tools@4.3.3': optional: true - '@agentclientprotocol/sdk@0.16.1(zod@3.25.76)': + '@agent-os-pkgs/common@0.0.0-split-runtime-preview.5d46b14': dependencies: - zod: 3.25.76 + '@agent-os-pkgs/coreutils': 0.0.0-split-runtime-preview.5d46b14 + '@agent-os-pkgs/diffutils': 0.0.0-split-runtime-preview.5d46b14 + '@agent-os-pkgs/findutils': 0.0.0-split-runtime-preview.5d46b14 + '@agent-os-pkgs/gawk': 0.0.0-split-runtime-preview.5d46b14 + '@agent-os-pkgs/grep': 0.0.0-split-runtime-preview.5d46b14 + '@agent-os-pkgs/gzip': 0.0.0-split-runtime-preview.5d46b14 + '@agent-os-pkgs/sed': 0.0.0-split-runtime-preview.5d46b14 + '@agent-os-pkgs/tar': 0.0.0-split-runtime-preview.5d46b14 + + '@agent-os-pkgs/coreutils@0.0.0-split-runtime-preview.5d46b14': {} + + '@agent-os-pkgs/diffutils@0.0.0-split-runtime-preview.5d46b14': {} + + '@agent-os-pkgs/findutils@0.0.0-split-runtime-preview.5d46b14': {} + + '@agent-os-pkgs/gawk@0.0.0-split-runtime-preview.5d46b14': {} + + '@agent-os-pkgs/grep@0.0.0-split-runtime-preview.5d46b14': {} - '@agentclientprotocol/sdk@0.16.1(zod@4.1.13)': + '@agent-os-pkgs/gzip@0.0.0-split-runtime-preview.5d46b14': {} + + '@agent-os-pkgs/sed@0.0.0-split-runtime-preview.5d46b14': {} + + '@agent-os-pkgs/tar@0.0.0-split-runtime-preview.5d46b14': {} + + '@agentclientprotocol/sdk@0.16.1(zod@3.25.76)': dependencies: - zod: 4.1.13 + zod: 3.25.76 '@ai-sdk/anthropic@1.2.12(zod@4.1.13)': dependencies: @@ -17677,18 +17046,6 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.0.2 - '@anthropic-ai/sdk@0.73.0(zod@3.25.76)': - dependencies: - json-schema-to-ts: 3.1.1 - optionalDependencies: - zod: 3.25.76 - - '@anthropic-ai/sdk@0.73.0(zod@4.1.13)': - dependencies: - json-schema-to-ts: 3.1.1 - optionalDependencies: - zod: 4.1.13 - '@arethetypeswrong/cli@0.18.3': dependencies: '@arethetypeswrong/core': 0.18.3 @@ -17819,399 +17176,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@aws-crypto/crc32@5.2.0': - dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.5 - tslib: 2.8.1 - - '@aws-crypto/sha256-browser@5.2.0': - dependencies: - '@aws-crypto/sha256-js': 5.2.0 - '@aws-crypto/supports-web-crypto': 5.2.0 - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.5 - '@aws-sdk/util-locate-window': 3.965.5 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - - '@aws-crypto/sha256-js@5.2.0': - dependencies: - '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.5 - tslib: 2.8.1 - - '@aws-crypto/supports-web-crypto@5.2.0': - dependencies: - tslib: 2.8.1 - - '@aws-crypto/util@5.2.0': - dependencies: - '@aws-sdk/types': 3.973.5 - '@smithy/util-utf8': 2.3.0 - tslib: 2.8.1 - - '@aws-sdk/client-bedrock-runtime@3.1024.0': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.26 - '@aws-sdk/credential-provider-node': 3.972.29 - '@aws-sdk/eventstream-handler-node': 3.972.12 - '@aws-sdk/middleware-eventstream': 3.972.8 - '@aws-sdk/middleware-host-header': 3.972.8 - '@aws-sdk/middleware-logger': 3.972.8 - '@aws-sdk/middleware-recursion-detection': 3.972.9 - '@aws-sdk/middleware-user-agent': 3.972.28 - '@aws-sdk/middleware-websocket': 3.972.14 - '@aws-sdk/region-config-resolver': 3.972.10 - '@aws-sdk/token-providers': 3.1024.0 - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-endpoints': 3.996.5 - '@aws-sdk/util-user-agent-browser': 3.972.8 - '@aws-sdk/util-user-agent-node': 3.973.14 - '@smithy/config-resolver': 4.4.13 - '@smithy/core': 3.23.13 - '@smithy/eventstream-serde-browser': 4.2.12 - '@smithy/eventstream-serde-config-resolver': 4.3.12 - '@smithy/eventstream-serde-node': 4.2.12 - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/hash-node': 4.2.12 - '@smithy/invalid-dependency': 4.2.12 - '@smithy/middleware-content-length': 4.2.12 - '@smithy/middleware-endpoint': 4.4.28 - '@smithy/middleware-retry': 4.4.46 - '@smithy/middleware-serde': 4.2.16 - '@smithy/middleware-stack': 4.2.12 - '@smithy/node-config-provider': 4.3.12 - '@smithy/node-http-handler': 4.5.1 - '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-base64': 4.3.2 - '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.44 - '@smithy/util-defaults-mode-node': 4.2.48 - '@smithy/util-endpoints': 3.3.3 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.13 - '@smithy/util-stream': 4.5.21 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/core@3.973.26': - dependencies: - '@aws-sdk/types': 3.973.6 - '@aws-sdk/xml-builder': 3.972.16 - '@smithy/core': 3.23.13 - '@smithy/node-config-provider': 4.3.12 - '@smithy/property-provider': 4.2.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/signature-v4': 5.3.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - '@smithy/util-base64': 4.3.2 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-env@3.972.24': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-http@3.972.26': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/types': 3.973.6 - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/node-http-handler': 4.5.1 - '@smithy/property-provider': 4.2.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - '@smithy/util-stream': 4.5.21 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-ini@3.972.28': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/credential-provider-env': 3.972.24 - '@aws-sdk/credential-provider-http': 3.972.26 - '@aws-sdk/credential-provider-login': 3.972.28 - '@aws-sdk/credential-provider-process': 3.972.24 - '@aws-sdk/credential-provider-sso': 3.972.28 - '@aws-sdk/credential-provider-web-identity': 3.972.28 - '@aws-sdk/nested-clients': 3.996.18 - '@aws-sdk/types': 3.973.6 - '@smithy/credential-provider-imds': 4.2.12 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-login@3.972.28': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/nested-clients': 3.996.18 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-node@3.972.29': - dependencies: - '@aws-sdk/credential-provider-env': 3.972.24 - '@aws-sdk/credential-provider-http': 3.972.26 - '@aws-sdk/credential-provider-ini': 3.972.28 - '@aws-sdk/credential-provider-process': 3.972.24 - '@aws-sdk/credential-provider-sso': 3.972.28 - '@aws-sdk/credential-provider-web-identity': 3.972.28 - '@aws-sdk/types': 3.973.6 - '@smithy/credential-provider-imds': 4.2.12 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-process@3.972.24': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-sso@3.972.28': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/nested-clients': 3.996.18 - '@aws-sdk/token-providers': 3.1021.0 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-web-identity@3.972.28': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/nested-clients': 3.996.18 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/eventstream-handler-node@3.972.12': - dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/eventstream-codec': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@aws-sdk/middleware-eventstream@3.972.8': - dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@aws-sdk/middleware-host-header@3.972.8': - dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@aws-sdk/middleware-logger@3.972.8': - dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@aws-sdk/middleware-recursion-detection@3.972.9': - dependencies: - '@aws-sdk/types': 3.973.6 - '@aws/lambda-invoke-store': 0.2.3 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@aws-sdk/middleware-user-agent@3.972.28': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-endpoints': 3.996.5 - '@smithy/core': 3.23.13 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-retry': 4.2.13 - tslib: 2.8.1 - - '@aws-sdk/middleware-websocket@3.972.14': - dependencies: - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-format-url': 3.972.8 - '@smithy/eventstream-codec': 4.2.12 - '@smithy/eventstream-serde-browser': 4.2.12 - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/protocol-http': 5.3.12 - '@smithy/signature-v4': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-base64': 4.3.2 - '@smithy/util-hex-encoding': 4.2.2 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@aws-sdk/nested-clients@3.996.18': - dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.26 - '@aws-sdk/middleware-host-header': 3.972.8 - '@aws-sdk/middleware-logger': 3.972.8 - '@aws-sdk/middleware-recursion-detection': 3.972.9 - '@aws-sdk/middleware-user-agent': 3.972.28 - '@aws-sdk/region-config-resolver': 3.972.10 - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-endpoints': 3.996.5 - '@aws-sdk/util-user-agent-browser': 3.972.8 - '@aws-sdk/util-user-agent-node': 3.973.14 - '@smithy/config-resolver': 4.4.13 - '@smithy/core': 3.23.13 - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/hash-node': 4.2.12 - '@smithy/invalid-dependency': 4.2.12 - '@smithy/middleware-content-length': 4.2.12 - '@smithy/middleware-endpoint': 4.4.28 - '@smithy/middleware-retry': 4.4.46 - '@smithy/middleware-serde': 4.2.16 - '@smithy/middleware-stack': 4.2.12 - '@smithy/node-config-provider': 4.3.12 - '@smithy/node-http-handler': 4.5.1 - '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-base64': 4.3.2 - '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.44 - '@smithy/util-defaults-mode-node': 4.2.48 - '@smithy/util-endpoints': 3.3.3 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.13 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/region-config-resolver@3.972.10': - dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/config-resolver': 4.4.13 - '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@aws-sdk/token-providers@3.1021.0': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/nested-clients': 3.996.18 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/token-providers@3.1024.0': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/nested-clients': 3.996.18 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/types@3.973.5': - dependencies: - '@smithy/types': 4.13.0 - tslib: 2.8.1 - - '@aws-sdk/types@3.973.6': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@aws-sdk/util-endpoints@3.996.5': - dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-endpoints': 3.3.3 - tslib: 2.8.1 - - '@aws-sdk/util-format-url@3.972.8': - dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/querystring-builder': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@aws-sdk/util-locate-window@3.965.5': - dependencies: - tslib: 2.8.1 - - '@aws-sdk/util-user-agent-browser@3.972.8': - dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 - bowser: 2.14.1 - tslib: 2.8.1 - - '@aws-sdk/util-user-agent-node@3.973.14': - dependencies: - '@aws-sdk/middleware-user-agent': 3.972.28 - '@aws-sdk/types': 3.973.6 - '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-config-provider': 4.2.2 - tslib: 2.8.1 - - '@aws-sdk/xml-builder@3.972.16': - dependencies: - '@smithy/types': 4.13.1 - fast-xml-parser: 5.5.8 - tslib: 2.8.1 - - '@aws/lambda-invoke-store@0.2.3': {} - '@babel/code-frame@7.10.4': dependencies: '@babel/highlight': 7.25.9 @@ -18937,39 +17901,21 @@ snapshots: '@cbor-extract/cbor-extract-darwin-arm64@2.2.0': optional: true - '@cbor-extract/cbor-extract-darwin-arm64@2.2.2': - optional: true - '@cbor-extract/cbor-extract-darwin-x64@2.2.0': optional: true - '@cbor-extract/cbor-extract-darwin-x64@2.2.2': - optional: true - '@cbor-extract/cbor-extract-linux-arm64@2.2.0': optional: true - '@cbor-extract/cbor-extract-linux-arm64@2.2.2': - optional: true - '@cbor-extract/cbor-extract-linux-arm@2.2.0': optional: true - '@cbor-extract/cbor-extract-linux-arm@2.2.2': - optional: true - '@cbor-extract/cbor-extract-linux-x64@2.2.0': optional: true - '@cbor-extract/cbor-extract-linux-x64@2.2.2': - optional: true - '@cbor-extract/cbor-extract-win32-x64@2.2.0': optional: true - '@cbor-extract/cbor-extract-win32-x64@2.2.2': - optional: true - '@chevrotain/cst-dts-gen@11.0.3': dependencies: '@chevrotain/gast': 11.0.3 @@ -20026,32 +18972,6 @@ snapshots: '@fortawesome/fontawesome-svg-core': 7.1.0 react: 19.1.0 - '@google/genai@1.48.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))': - dependencies: - google-auth-library: 10.6.2 - p-retry: 4.6.2 - protobufjs: 7.5.4 - ws: 8.19.0 - optionalDependencies: - '@modelcontextprotocol/sdk': 1.25.3(hono@4.11.9)(zod@3.25.76) - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@google/genai@1.48.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13))': - dependencies: - google-auth-library: 10.6.2 - p-retry: 4.6.2 - protobufjs: 7.5.4 - ws: 8.19.0 - optionalDependencies: - '@modelcontextprotocol/sdk': 1.25.3(hono@4.11.9)(zod@4.1.13) - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - '@headlessui/react@2.2.9(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: '@floating-ui/react': 0.26.28(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -20645,201 +19565,6 @@ snapshots: '@marijn/find-cluster-break@1.0.2': {} - '@mariozechner/clipboard-darwin-arm64@0.3.2': - optional: true - - '@mariozechner/clipboard-darwin-universal@0.3.2': - optional: true - - '@mariozechner/clipboard-darwin-x64@0.3.2': - optional: true - - '@mariozechner/clipboard-linux-arm64-gnu@0.3.2': - optional: true - - '@mariozechner/clipboard-linux-arm64-musl@0.3.2': - optional: true - - '@mariozechner/clipboard-linux-riscv64-gnu@0.3.2': - optional: true - - '@mariozechner/clipboard-linux-x64-gnu@0.3.2': - optional: true - - '@mariozechner/clipboard-linux-x64-musl@0.3.2': - optional: true - - '@mariozechner/clipboard-win32-arm64-msvc@0.3.2': - optional: true - - '@mariozechner/clipboard-win32-x64-msvc@0.3.2': - optional: true - - '@mariozechner/clipboard@0.3.2': - optionalDependencies: - '@mariozechner/clipboard-darwin-arm64': 0.3.2 - '@mariozechner/clipboard-darwin-universal': 0.3.2 - '@mariozechner/clipboard-darwin-x64': 0.3.2 - '@mariozechner/clipboard-linux-arm64-gnu': 0.3.2 - '@mariozechner/clipboard-linux-arm64-musl': 0.3.2 - '@mariozechner/clipboard-linux-riscv64-gnu': 0.3.2 - '@mariozechner/clipboard-linux-x64-gnu': 0.3.2 - '@mariozechner/clipboard-linux-x64-musl': 0.3.2 - '@mariozechner/clipboard-win32-arm64-msvc': 0.3.2 - '@mariozechner/clipboard-win32-x64-msvc': 0.3.2 - optional: true - - '@mariozechner/jiti@2.6.5': - dependencies: - std-env: 3.10.0 - yoctocolors: 2.1.2 - - '@mariozechner/pi-agent-core@0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.1)(zod@3.25.76)': - dependencies: - '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.1)(zod@3.25.76) - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - aws-crt - - bufferutil - - supports-color - - utf-8-validate - - ws - - zod - - '@mariozechner/pi-agent-core@0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13))(ws@8.19.0)(zod@4.1.13)': - dependencies: - '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13))(ws@8.19.0)(zod@4.1.13) - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - aws-crt - - bufferutil - - supports-color - - utf-8-validate - - ws - - zod - - '@mariozechner/pi-ai@0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.1)(zod@3.25.76)': - dependencies: - '@anthropic-ai/sdk': 0.73.0(zod@3.25.76) - '@aws-sdk/client-bedrock-runtime': 3.1024.0 - '@google/genai': 1.48.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76)) - '@mistralai/mistralai': 1.14.1 - '@sinclair/typebox': 0.34.41 - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) - chalk: 5.6.2 - openai: 6.26.0(ws@8.20.1)(zod@3.25.76) - partial-json: 0.1.7 - proxy-agent: 6.5.0 - undici: 7.24.7 - zod-to-json-schema: 3.25.1(zod@3.25.76) - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - aws-crt - - bufferutil - - supports-color - - utf-8-validate - - ws - - zod - - '@mariozechner/pi-ai@0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13))(ws@8.19.0)(zod@4.1.13)': - dependencies: - '@anthropic-ai/sdk': 0.73.0(zod@4.1.13) - '@aws-sdk/client-bedrock-runtime': 3.1024.0 - '@google/genai': 1.48.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13)) - '@mistralai/mistralai': 1.14.1 - '@sinclair/typebox': 0.34.41 - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) - chalk: 5.6.2 - openai: 6.26.0(ws@8.19.0)(zod@4.1.13) - partial-json: 0.1.7 - proxy-agent: 6.5.0 - undici: 7.24.7 - zod-to-json-schema: 3.25.1(zod@4.1.13) - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - aws-crt - - bufferutil - - supports-color - - utf-8-validate - - ws - - zod - - '@mariozechner/pi-coding-agent@0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.1)(zod@3.25.76)': - dependencies: - '@mariozechner/jiti': 2.6.5 - '@mariozechner/pi-agent-core': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.1)(zod@3.25.76) - '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.1)(zod@3.25.76) - '@mariozechner/pi-tui': 0.60.0 - '@silvia-odwyer/photon-node': 0.3.4 - chalk: 5.6.2 - cli-highlight: 2.1.11 - diff: 8.0.3 - extract-zip: 2.0.1 - file-type: 21.3.4 - glob: 13.0.6 - hosted-git-info: 9.0.2 - ignore: 7.0.5 - marked: 15.0.12 - minimatch: 10.2.5 - proper-lockfile: 4.1.2 - strip-ansi: 7.1.2 - undici: 7.24.7 - yaml: 2.8.2 - optionalDependencies: - '@mariozechner/clipboard': 0.3.2 - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - aws-crt - - bufferutil - - supports-color - - utf-8-validate - - ws - - zod - - '@mariozechner/pi-coding-agent@0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13))(ws@8.19.0)(zod@4.1.13)': - dependencies: - '@mariozechner/jiti': 2.6.5 - '@mariozechner/pi-agent-core': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13))(ws@8.19.0)(zod@4.1.13) - '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13))(ws@8.19.0)(zod@4.1.13) - '@mariozechner/pi-tui': 0.60.0 - '@silvia-odwyer/photon-node': 0.3.4 - chalk: 5.6.2 - cli-highlight: 2.1.11 - diff: 8.0.3 - extract-zip: 2.0.1 - file-type: 21.3.4 - glob: 13.0.6 - hosted-git-info: 9.0.2 - ignore: 7.0.5 - marked: 15.0.12 - minimatch: 10.2.5 - proper-lockfile: 4.1.2 - strip-ansi: 7.1.2 - undici: 7.24.7 - yaml: 2.8.2 - optionalDependencies: - '@mariozechner/clipboard': 0.3.2 - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - aws-crt - - bufferutil - - supports-color - - utf-8-validate - - ws - - zod - - '@mariozechner/pi-tui@0.60.0': - dependencies: - '@types/mime-types': 2.1.4 - chalk: 5.6.2 - get-east-asian-width: 1.4.0 - marked: 15.0.12 - mime-types: 3.0.2 - optionalDependencies: - koffi: 2.15.4 - '@marsidev/react-turnstile@1.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: react: 19.1.0 @@ -21045,15 +19770,6 @@ snapshots: '@microsoft/tsdoc@0.15.1': optional: true - '@mistralai/mistralai@1.14.1': - dependencies: - ws: 8.19.0 - zod: 3.25.76 - zod-to-json-schema: 3.25.1(zod@3.25.76) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - '@modelcontextprotocol/inspector-cli@0.14.3(hono@4.11.9)(zod@3.25.76)': dependencies: '@modelcontextprotocol/sdk': 1.25.3(hono@4.11.9)(zod@3.25.76) @@ -21162,29 +19878,6 @@ snapshots: - hono - supports-color - '@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13)': - dependencies: - '@hono/node-server': 1.19.13(hono@4.11.9) - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) - content-type: 1.0.5 - cors: 2.8.5 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.0.6 - express: 5.2.1 - express-rate-limit: 7.5.1(express@5.2.1) - jose: 6.1.3 - json-schema-typed: 8.0.2 - pkce-challenge: 5.0.1 - raw-body: 3.0.2 - zod: 4.1.13 - zod-to-json-schema: 3.25.1(zod@4.1.13) - transitivePeerDependencies: - - hono - - supports-color - optional: true - '@monaco-editor/loader@1.7.0': dependencies: state-local: 1.0.7 @@ -21630,29 +20323,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@protobufjs/aspromise@1.1.2': {} - - '@protobufjs/base64@1.1.2': {} - - '@protobufjs/codegen@2.0.4': {} - - '@protobufjs/eventemitter@1.1.0': {} - - '@protobufjs/fetch@1.1.0': - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/inquire': 1.1.0 - - '@protobufjs/float@1.0.2': {} - - '@protobufjs/inquire@1.1.0': {} - - '@protobufjs/path@1.1.2': {} - - '@protobufjs/pool@1.1.0': {} - - '@protobufjs/utf8@1.1.0': {} - '@publint/pack@0.1.4': {} '@radix-ui/number@1.1.1': {} @@ -22539,19 +21209,6 @@ snapshots: '@rivet-dev/agent-os-sed': 0.0.260331072558 '@rivet-dev/agent-os-tar': 0.0.260331072558 - '@rivet-dev/agent-os-core@0.1.1(pyodide@0.28.3)': - dependencies: - '@rivet-dev/agent-os-posix': 0.1.0 - '@rivet-dev/agent-os-python': 0.1.0(pyodide@0.28.3) - '@secure-exec/core': 0.2.1 - '@secure-exec/nodejs': 0.2.1 - '@secure-exec/v8': 0.2.1 - croner: 10.0.1 - long-timeout: 0.1.1 - secure-exec: 0.2.1 - transitivePeerDependencies: - - pyodide - '@rivet-dev/agent-os-coreutils@0.0.260331072558': {} '@rivet-dev/agent-os-diffutils@0.0.260331072558': {} @@ -22564,48 +21221,6 @@ snapshots: '@rivet-dev/agent-os-gzip@0.0.260331072558': {} - '@rivet-dev/agent-os-pi@0.1.1(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.1)(zod@3.25.76)': - dependencies: - '@agentclientprotocol/sdk': 0.16.1(zod@3.25.76) - '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.1)(zod@3.25.76) - '@mariozechner/pi-coding-agent': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(ws@8.20.1)(zod@3.25.76) - '@rivet-dev/agent-os-core': 0.1.1(pyodide@0.28.3) - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - aws-crt - - bufferutil - - pyodide - - supports-color - - utf-8-validate - - ws - - zod - - '@rivet-dev/agent-os-pi@0.1.1(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13))(pyodide@0.28.3)(ws@8.19.0)(zod@4.1.13)': - dependencies: - '@agentclientprotocol/sdk': 0.16.1(zod@4.1.13) - '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13))(ws@8.19.0)(zod@4.1.13) - '@mariozechner/pi-coding-agent': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13))(ws@8.19.0)(zod@4.1.13) - '@rivet-dev/agent-os-core': 0.1.1(pyodide@0.28.3) - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - aws-crt - - bufferutil - - pyodide - - supports-color - - utf-8-validate - - ws - - zod - - '@rivet-dev/agent-os-posix@0.1.0': - dependencies: - '@secure-exec/core': 0.2.1 - - '@rivet-dev/agent-os-python@0.1.0(pyodide@0.28.3)': - dependencies: - '@secure-exec/core': 0.2.1 - optionalDependencies: - pyodide: 0.28.3 - '@rivet-dev/agent-os-sed@0.0.260331072558': {} '@rivet-dev/agent-os-tar@0.0.260331072558': {} @@ -22636,6 +21251,8 @@ snapshots: '@rivetkit/bare-ts@0.6.2': {} + '@rivetkit/engine-cli-linux-x64-musl@2.3.0-rc.12': {} + '@rivetkit/on-change@6.0.1': {} '@rivetkit/sql-loader@2.2.1': {} @@ -22915,43 +21532,51 @@ snapshots: - '@types/node' optional: true - '@sec-ant/readable-stream@0.4.1': {} - - '@secure-exec/core@0.2.1': - dependencies: - better-sqlite3: 12.8.0 + '@sandbox-agent/cli-darwin-arm64@0.4.2': + optional: true - '@secure-exec/nodejs@0.2.1': - dependencies: - '@secure-exec/core': 0.2.1 - '@secure-exec/v8': 0.2.1 - cbor-x: 1.6.4 - cjs-module-lexer: 2.2.0 - es-module-lexer: 1.7.0 - esbuild: 0.27.3 - node-stdlib-browser: 1.3.1 - web-streams-polyfill: 4.2.0 + '@sandbox-agent/cli-darwin-x64@0.4.2': + optional: true - '@secure-exec/v8-darwin-arm64@0.2.1': + '@sandbox-agent/cli-linux-arm64@0.4.2': optional: true - '@secure-exec/v8-darwin-x64@0.2.1': + '@sandbox-agent/cli-linux-x64@0.4.2': optional: true - '@secure-exec/v8-linux-arm64-gnu@0.2.1': + '@sandbox-agent/cli-shared@0.4.2': {} + + '@sandbox-agent/cli-win32-x64@0.4.2': optional: true - '@secure-exec/v8-linux-x64-gnu@0.2.1': + '@sandbox-agent/cli@0.4.2': + dependencies: + '@sandbox-agent/cli-shared': 0.4.2 + optionalDependencies: + '@sandbox-agent/cli-darwin-arm64': 0.4.2 + '@sandbox-agent/cli-darwin-x64': 0.4.2 + '@sandbox-agent/cli-linux-arm64': 0.4.2 + '@sandbox-agent/cli-linux-x64': 0.4.2 + '@sandbox-agent/cli-win32-x64': 0.4.2 optional: true - '@secure-exec/v8@0.2.1': + '@sec-ant/readable-stream@0.4.1': {} + + '@secure-exec/core@0.0.0-split-runtime-preview.5d46b14': dependencies: - cbor-x: 1.6.4 + '@rivetkit/bare-ts': 0.6.2 + '@secure-exec/sidecar': 0.0.0-split-runtime-preview.5d46b14 + + '@secure-exec/s3@0.0.0-split-runtime-preview.5d46b14': + dependencies: + '@secure-exec/core': 0.0.0-split-runtime-preview.5d46b14 + + '@secure-exec/sidecar-linux-x64-gnu@0.0.0-split-runtime-preview.5d46b14': + optional: true + + '@secure-exec/sidecar@0.0.0-split-runtime-preview.5d46b14': optionalDependencies: - '@secure-exec/v8-darwin-arm64': 0.2.1 - '@secure-exec/v8-darwin-x64': 0.2.1 - '@secure-exec/v8-linux-arm64-gnu': 0.2.1 - '@secure-exec/v8-linux-x64-gnu': 0.2.1 + '@secure-exec/sidecar-linux-x64-gnu': 0.0.0-split-runtime-preview.5d46b14 '@sentry-internal/browser-utils@10.42.0': dependencies: @@ -23315,8 +21940,6 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} - '@silvia-odwyer/photon-node@0.3.4': {} - '@sinclair/typebox@0.27.8': {} '@sinclair/typebox@0.34.41': {} @@ -23342,309 +21965,6 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 - '@smithy/config-resolver@4.4.13': - dependencies: - '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-config-provider': 4.2.2 - '@smithy/util-endpoints': 3.3.3 - '@smithy/util-middleware': 4.2.12 - tslib: 2.8.1 - - '@smithy/core@3.23.13': - dependencies: - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-base64': 4.3.2 - '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-stream': 4.5.21 - '@smithy/util-utf8': 4.2.2 - '@smithy/uuid': 1.1.2 - tslib: 2.8.1 - - '@smithy/credential-provider-imds@4.2.12': - dependencies: - '@smithy/node-config-provider': 4.3.12 - '@smithy/property-provider': 4.2.12 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - tslib: 2.8.1 - - '@smithy/eventstream-codec@4.2.12': - dependencies: - '@aws-crypto/crc32': 5.2.0 - '@smithy/types': 4.13.1 - '@smithy/util-hex-encoding': 4.2.2 - tslib: 2.8.1 - - '@smithy/eventstream-serde-browser@4.2.12': - dependencies: - '@smithy/eventstream-serde-universal': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/eventstream-serde-config-resolver@4.3.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/eventstream-serde-node@4.2.12': - dependencies: - '@smithy/eventstream-serde-universal': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/eventstream-serde-universal@4.2.12': - dependencies: - '@smithy/eventstream-codec': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/fetch-http-handler@5.3.15': - dependencies: - '@smithy/protocol-http': 5.3.12 - '@smithy/querystring-builder': 4.2.12 - '@smithy/types': 4.13.1 - '@smithy/util-base64': 4.3.2 - tslib: 2.8.1 - - '@smithy/hash-node@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - '@smithy/util-buffer-from': 4.2.2 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@smithy/invalid-dependency@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/is-array-buffer@2.2.0': - dependencies: - tslib: 2.8.1 - - '@smithy/is-array-buffer@4.2.2': - dependencies: - tslib: 2.8.1 - - '@smithy/middleware-content-length@4.2.12': - dependencies: - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/middleware-endpoint@4.4.28': - dependencies: - '@smithy/core': 3.23.13 - '@smithy/middleware-serde': 4.2.16 - '@smithy/node-config-provider': 4.3.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-middleware': 4.2.12 - tslib: 2.8.1 - - '@smithy/middleware-retry@4.4.46': - dependencies: - '@smithy/node-config-provider': 4.3.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/service-error-classification': 4.2.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.13 - '@smithy/uuid': 1.1.2 - tslib: 2.8.1 - - '@smithy/middleware-serde@4.2.16': - dependencies: - '@smithy/core': 3.23.13 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/middleware-stack@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/node-config-provider@4.3.12': - dependencies: - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/node-http-handler@4.5.1': - dependencies: - '@smithy/protocol-http': 5.3.12 - '@smithy/querystring-builder': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/property-provider@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/protocol-http@5.3.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/querystring-builder@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - '@smithy/util-uri-escape': 4.2.2 - tslib: 2.8.1 - - '@smithy/querystring-parser@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/service-error-classification@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - - '@smithy/shared-ini-file-loader@4.4.7': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/signature-v4@5.3.12': - dependencies: - '@smithy/is-array-buffer': 4.2.2 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-hex-encoding': 4.2.2 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-uri-escape': 4.2.2 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@smithy/smithy-client@4.12.8': - dependencies: - '@smithy/core': 3.23.13 - '@smithy/middleware-endpoint': 4.4.28 - '@smithy/middleware-stack': 4.2.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-stream': 4.5.21 - tslib: 2.8.1 - - '@smithy/types@4.13.0': - dependencies: - tslib: 2.8.1 - - '@smithy/types@4.13.1': - dependencies: - tslib: 2.8.1 - - '@smithy/url-parser@4.2.12': - dependencies: - '@smithy/querystring-parser': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/util-base64@4.3.2': - dependencies: - '@smithy/util-buffer-from': 4.2.2 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@smithy/util-body-length-browser@4.2.2': - dependencies: - tslib: 2.8.1 - - '@smithy/util-body-length-node@4.2.3': - dependencies: - tslib: 2.8.1 - - '@smithy/util-buffer-from@2.2.0': - dependencies: - '@smithy/is-array-buffer': 2.2.0 - tslib: 2.8.1 - - '@smithy/util-buffer-from@4.2.2': - dependencies: - '@smithy/is-array-buffer': 4.2.2 - tslib: 2.8.1 - - '@smithy/util-config-provider@4.2.2': - dependencies: - tslib: 2.8.1 - - '@smithy/util-defaults-mode-browser@4.3.44': - dependencies: - '@smithy/property-provider': 4.2.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/util-defaults-mode-node@4.2.48': - dependencies: - '@smithy/config-resolver': 4.4.13 - '@smithy/credential-provider-imds': 4.2.12 - '@smithy/node-config-provider': 4.3.12 - '@smithy/property-provider': 4.2.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/util-endpoints@3.3.3': - dependencies: - '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/util-hex-encoding@4.2.2': - dependencies: - tslib: 2.8.1 - - '@smithy/util-middleware@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/util-retry@4.2.13': - dependencies: - '@smithy/service-error-classification': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/util-stream@4.5.21': - dependencies: - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/node-http-handler': 4.5.1 - '@smithy/types': 4.13.1 - '@smithy/util-base64': 4.3.2 - '@smithy/util-buffer-from': 4.2.2 - '@smithy/util-hex-encoding': 4.2.2 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@smithy/util-uri-escape@4.2.2': - dependencies: - tslib: 2.8.1 - - '@smithy/util-utf8@2.3.0': - dependencies: - '@smithy/util-buffer-from': 2.2.0 - tslib: 2.8.1 - - '@smithy/util-utf8@4.2.2': - dependencies: - '@smithy/util-buffer-from': 4.2.2 - tslib: 2.8.1 - - '@smithy/uuid@1.1.2': - dependencies: - tslib: 2.8.1 - '@standard-schema/spec@1.0.0': {} '@standard-schema/spec@1.1.0': {} @@ -24032,8 +22352,6 @@ snapshots: '@tokenizer/token@0.3.0': {} - '@tootallnate/quickjs-emscripten@0.23.0': {} - '@trpc/client@11.6.0(@trpc/server@11.6.0(typescript@5.9.3))(typescript@5.9.3)': dependencies: '@trpc/server': 11.6.0(typescript@5.9.3) @@ -24309,8 +22627,6 @@ snapshots: '@types/mdx@2.0.13': {} - '@types/mime-types@2.1.4': {} - '@types/mime@4.0.0': dependencies: mime: 4.0.7 @@ -24401,8 +22717,6 @@ snapshots: '@types/reconnectingwebsocket@1.0.10': {} - '@types/retry@0.12.0': {} - '@types/retry@0.12.2': {} '@types/sax@1.2.7': @@ -24465,11 +22779,6 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@types/yauzl@2.10.3': - dependencies: - '@types/node': 22.19.15 - optional: true - '@uiw/codemirror-extensions-basic-setup@4.25.1(@codemirror/autocomplete@6.18.7)(@codemirror/commands@6.8.1)(@codemirror/language@6.11.3)(@codemirror/lint@6.8.5)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.2)': dependencies: '@codemirror/autocomplete': 6.18.7 @@ -25186,6 +23495,12 @@ snapshots: acorn@8.16.0: {} + acp-http-client@0.4.2(zod@3.25.76): + dependencies: + '@agentclientprotocol/sdk': 0.16.1(zod@3.25.76) + transitivePeerDependencies: + - zod + actor-core@0.6.3(eventsource@3.0.7)(ws@8.20.1): dependencies: cbor-x: 1.6.0 @@ -25391,10 +23706,6 @@ snapshots: '@babel/parser': 7.29.0 pathe: 2.0.3 - ast-types@0.13.4: - dependencies: - tslib: 2.8.1 - ast-types@0.16.1: dependencies: tslib: 2.8.1 @@ -25702,8 +24013,6 @@ snapshots: baseline-browser-mapping@2.9.19: {} - basic-ftp@5.2.0: {} - bcp-47-match@2.0.3: {} bcryptjs@2.4.3: {} @@ -25757,22 +24066,23 @@ snapshots: dependencies: bindings: 1.5.0 prebuild-install: 7.1.3 + optional: true big-integer@1.6.52: {} - bignumber.js@9.3.1: {} - binary-extensions@2.3.0: {} bindings@1.5.0: dependencies: file-uri-to-path: 1.0.0 + optional: true bl@4.1.0: dependencies: buffer: 5.7.1 inherits: 2.0.4 readable-stream: 3.6.2 + optional: true bn.js@4.12.3: {} @@ -25794,8 +24104,6 @@ snapshots: boolbase@1.0.0: {} - bowser@2.14.1: {} - boxen@7.0.0: dependencies: ansi-align: 3.0.1 @@ -25909,10 +24217,6 @@ snapshots: dependencies: node-int64: 0.4.0 - buffer-crc32@0.2.13: {} - - buffer-equal-constant-time@1.0.1: {} - buffer-from@1.1.2: {} buffer-xor@1.0.3: {} @@ -26010,26 +24314,10 @@ snapshots: '@cbor-extract/cbor-extract-win32-x64': 2.2.0 optional: true - cbor-extract@2.2.2: - dependencies: - node-gyp-build-optional-packages: 5.1.1 - optionalDependencies: - '@cbor-extract/cbor-extract-darwin-arm64': 2.2.2 - '@cbor-extract/cbor-extract-darwin-x64': 2.2.2 - '@cbor-extract/cbor-extract-linux-arm': 2.2.2 - '@cbor-extract/cbor-extract-linux-arm64': 2.2.2 - '@cbor-extract/cbor-extract-linux-x64': 2.2.2 - '@cbor-extract/cbor-extract-win32-x64': 2.2.2 - optional: true - cbor-x@1.6.0: optionalDependencies: cbor-extract: 2.2.0 - cbor-x@1.6.4: - optionalDependencies: - cbor-extract: 2.2.2 - ccount@2.0.1: {} chai@4.5.0: @@ -26141,7 +24429,8 @@ snapshots: dependencies: readdirp: 4.1.2 - chownr@1.1.4: {} + chownr@1.1.4: + optional: true chownr@3.0.0: {} @@ -26467,8 +24756,6 @@ snapshots: crelt@1.0.6: {} - croner@10.0.1: {} - cross-fetch@4.1.0: dependencies: node-fetch: 2.7.0 @@ -26726,10 +25013,6 @@ snapshots: d3: 7.9.0 lodash-es: 4.17.22 - data-uri-to-buffer@4.0.1: {} - - data-uri-to-buffer@6.0.2: {} - date-fns@4.1.0: {} dateformat@4.6.3: {} @@ -26763,6 +25046,7 @@ snapshots: decompress-response@6.0.0: dependencies: mimic-response: 3.1.0 + optional: true dedent@1.7.0(babel-plugin-macros@3.1.0): optionalDependencies: @@ -26811,12 +25095,6 @@ snapshots: defu@6.1.4: {} - degenerator@5.0.1: - dependencies: - ast-types: 0.13.4 - escodegen: 2.1.0 - esprima: 4.0.1 - delaunator@5.0.1: dependencies: robust-predicates: 3.0.2 @@ -26974,10 +25252,6 @@ snapshots: eastasianwidth@0.2.0: {} - ecdsa-sig-formatter@1.0.11: - dependencies: - safe-buffer: 5.2.1 - ee-first@1.1.1: {} effect@4.0.0-beta.66: @@ -27249,14 +25523,6 @@ snapshots: escape-string-regexp@5.0.0: {} - escodegen@2.1.0: - dependencies: - esprima: 4.0.1 - estraverse: 5.3.0 - esutils: 2.0.3 - optionalDependencies: - source-map: 0.6.1 - eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 @@ -27446,7 +25712,8 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 - expand-template@2.0.3: {} + expand-template@2.0.3: + optional: true expect-type@1.2.2: {} @@ -27628,16 +25895,6 @@ snapshots: extend@3.0.2: {} - extract-zip@2.0.1: - dependencies: - debug: 4.4.3 - get-stream: 5.2.0 - yauzl: 2.10.0 - optionalDependencies: - '@types/yauzl': 2.10.3 - transitivePeerDependencies: - - supports-color - fast-check@4.8.0: dependencies: pure-rand: 8.4.0 @@ -27680,16 +25937,6 @@ snapshots: dependencies: fast-string-width: 3.0.2 - fast-xml-builder@1.1.4: - dependencies: - path-expression-matcher: 1.2.1 - - fast-xml-parser@5.5.8: - dependencies: - fast-xml-builder: 1.1.4 - path-expression-matcher: 1.2.1 - strnum: 2.2.0 - fastq@1.20.1: dependencies: reusify: 1.1.0 @@ -27717,10 +25964,6 @@ snapshots: dependencies: bser: 2.1.1 - fd-slicer@1.1.0: - dependencies: - pend: 1.2.0 - fdb-tuple@1.0.0: {} fdir@6.5.0(picomatch@4.0.3): @@ -27731,11 +25974,6 @@ snapshots: dependencies: xml-js: 1.6.11 - fetch-blob@3.2.0: - dependencies: - node-domexception: 1.0.0 - web-streams-polyfill: 3.3.3 - fflate@0.4.8: {} fflate@0.8.2: {} @@ -27761,7 +25999,8 @@ snapshots: transitivePeerDependencies: - supports-color - file-uri-to-path@1.0.0: {} + file-uri-to-path@1.0.0: + optional: true filesize@11.0.2: {} @@ -27863,10 +26102,6 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 4.0.0-beta.3 - formdata-polyfill@4.0.10: - dependencies: - fetch-blob: 3.2.0 - forwarded-parse@2.1.2: {} forwarded@0.2.0: {} @@ -27953,7 +26188,8 @@ snapshots: fresh@2.0.0: {} - fs-constants@1.0.0: {} + fs-constants@1.0.0: + optional: true fs-extra@11.3.4: dependencies: @@ -27980,22 +26216,6 @@ snapshots: fuse.js@7.1.0: {} - gaxios@7.1.4: - dependencies: - extend: 3.0.2 - https-proxy-agent: 7.0.6 - node-fetch: 3.3.2 - transitivePeerDependencies: - - supports-color - - gcp-metadata@8.1.2: - dependencies: - gaxios: 7.1.4 - google-logging-utils: 1.1.3 - json-bigint: 1.0.0 - transitivePeerDependencies: - - supports-color - generator-function@2.0.1: {} gensync@1.0.0-beta.2: {} @@ -28030,10 +26250,6 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-stream@5.2.0: - dependencies: - pump: 3.0.4 - get-stream@6.0.1: {} get-stream@8.0.1: {} @@ -28047,21 +26263,14 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 - get-uri@6.0.5: - dependencies: - basic-ftp: 5.2.0 - data-uri-to-buffer: 6.0.2 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - getenv@1.0.0: {} getenv@2.0.0: {} github-buttons@2.29.1: {} - github-from-package@0.0.0: {} + github-from-package@0.0.0: + optional: true github-slugger@2.0.0: {} @@ -28133,19 +26342,6 @@ snapshots: dependencies: csstype: 3.2.3 - google-auth-library@10.6.2: - dependencies: - base64-js: 1.5.1 - ecdsa-sig-formatter: 1.0.11 - gaxios: 7.1.4 - gcp-metadata: 8.1.2 - google-logging-utils: 1.1.3 - jws: 4.0.1 - transitivePeerDependencies: - - supports-color - - google-logging-utils@1.1.3: {} - gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -28482,10 +26678,6 @@ snapshots: dependencies: lru-cache: 10.4.3 - hosted-git-info@9.0.2: - dependencies: - lru-cache: 11.2.6 - html-escaper@2.0.2: {} html-escaper@3.0.3: {} @@ -28517,13 +26709,6 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - http-proxy-agent@7.0.2: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - https-browserify@1.0.0: {} https-proxy-agent@5.0.1: @@ -28640,8 +26825,6 @@ snapshots: transitivePeerDependencies: - supports-color - ip-address@10.1.0: {} - ipaddr.js@1.9.1: {} iron-webcrypto@1.2.1: {} @@ -28929,19 +27112,10 @@ snapshots: jsesc@3.1.0: {} - json-bigint@1.0.0: - dependencies: - bignumber.js: 9.3.1 - json-buffer@3.0.1: {} json-parse-even-better-errors@2.3.1: {} - json-schema-to-ts@3.1.1: - dependencies: - '@babel/runtime': 7.29.2 - ts-algebra: 2.0.0 - json-schema-traverse@0.4.1: {} json-schema-traverse@1.0.0: {} @@ -28978,17 +27152,6 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 - jwa@2.0.1: - dependencies: - buffer-equal-constant-time: 1.0.1 - ecdsa-sig-formatter: 1.0.11 - safe-buffer: 5.2.1 - - jws@4.0.1: - dependencies: - jwa: 2.0.1 - safe-buffer: 5.2.1 - katex@0.16.27: dependencies: commander: 8.3.0 @@ -29040,9 +27203,6 @@ snapshots: transitivePeerDependencies: - supports-color - koffi@2.15.4: - optional: true - kolorist@1.8.0: {} kubernetes-types@1.30.0: {} @@ -29259,10 +27419,6 @@ snapshots: loglevel@1.9.2: {} - long-timeout@0.1.1: {} - - long@5.3.2: {} - longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -29293,8 +27449,6 @@ snapshots: dependencies: yallist: 4.0.0 - lru-cache@7.18.3: {} - lucide-react@0.344.0(react@19.1.0): dependencies: react: 19.1.0 @@ -29364,8 +27518,6 @@ snapshots: marked@14.0.0: {} - marked@15.0.12: {} - marked@16.4.2: {} marked@9.1.6: {} @@ -30260,7 +28412,8 @@ snapshots: mimic-fn@4.0.0: {} - mimic-response@3.1.0: {} + mimic-response@3.1.0: + optional: true mini-svg-data-uri@1.4.4: {} @@ -30307,7 +28460,8 @@ snapshots: dependencies: minipass: 7.1.3 - mkdirp-classic@0.5.3: {} + mkdirp-classic@0.5.3: + optional: true mkdirp@1.0.4: {} @@ -30468,7 +28622,8 @@ snapshots: nanostores@1.2.0: {} - napi-build-utils@2.0.0: {} + napi-build-utils@2.0.0: + optional: true natural-compare@1.4.0: {} @@ -30490,8 +28645,6 @@ snapshots: nested-error-stacks@2.0.1: {} - netmask@2.0.2: {} - next@16.1.1(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(@playwright/test@1.57.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2): dependencies: '@next/env': 16.1.1 @@ -30532,6 +28685,7 @@ snapshots: node-abi@3.88.0: dependencies: semver: 7.7.4 + optional: true node-addon-api@6.1.0: {} @@ -30553,12 +28707,6 @@ snapshots: dependencies: whatwg-url: 5.0.0 - node-fetch@3.3.2: - dependencies: - data-uri-to-buffer: 4.0.1 - fetch-blob: 3.2.0 - formdata-polyfill: 4.0.10 - node-forge@1.3.3: {} node-gyp-build-optional-packages@5.1.1: @@ -30763,16 +28911,6 @@ snapshots: transitivePeerDependencies: - encoding - openai@6.26.0(ws@8.19.0)(zod@4.1.13): - optionalDependencies: - ws: 8.19.0 - zod: 4.1.13 - - openai@6.26.0(ws@8.20.1)(zod@3.25.76): - optionalDependencies: - ws: 8.20.1 - zod: 3.25.76 - openapi-types@12.1.3: {} openapi3-ts@4.5.0: @@ -30846,11 +28984,6 @@ snapshots: eventemitter3: 5.0.1 p-timeout: 6.1.4 - p-retry@4.6.2: - dependencies: - '@types/retry': 0.12.0 - retry: 0.13.1 - p-retry@6.2.1: dependencies: '@types/retry': 0.12.2 @@ -30861,24 +28994,6 @@ snapshots: p-try@2.2.0: {} - pac-proxy-agent@7.2.0: - dependencies: - '@tootallnate/quickjs-emscripten': 0.23.0 - agent-base: 7.1.4 - debug: 4.4.3 - get-uri: 6.0.5 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - pac-resolver: 7.0.1 - socks-proxy-agent: 8.0.5 - transitivePeerDependencies: - - supports-color - - pac-resolver@7.0.1: - dependencies: - degenerator: 5.0.1 - netmask: 2.0.2 - package-json-from-dist@1.0.1: {} package-manager-detector@1.6.0: {} @@ -30950,8 +29065,6 @@ snapshots: parseurl@1.3.3: {} - partial-json@0.1.7: {} - pascal-case@3.1.2: dependencies: no-case: 3.0.4 @@ -30968,8 +29081,6 @@ snapshots: path-exists@4.0.0: {} - path-expression-matcher@1.2.1: {} - path-is-absolute@1.0.1: {} path-is-inside@1.0.2: {} @@ -31017,8 +29128,6 @@ snapshots: sha.js: 2.4.12 to-buffer: 1.2.2 - pend@1.2.0: {} - pg-cloudflare@1.3.0: optional: true @@ -31296,6 +29405,7 @@ snapshots: simple-get: 4.0.1 tar-fs: 2.1.4 tunnel-agent: 0.6.0 + optional: true prelude-ls@1.2.1: {} @@ -31361,47 +29471,13 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 - proper-lockfile@4.1.2: - dependencies: - graceful-fs: 4.2.11 - retry: 0.12.0 - signal-exit: 3.0.7 - property-information@7.1.0: {} - protobufjs@7.5.4: - dependencies: - '@protobufjs/aspromise': 1.1.2 - '@protobufjs/base64': 1.1.2 - '@protobufjs/codegen': 2.0.4 - '@protobufjs/eventemitter': 1.1.0 - '@protobufjs/fetch': 1.1.0 - '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.0 - '@protobufjs/path': 1.1.2 - '@protobufjs/pool': 1.1.0 - '@protobufjs/utf8': 1.1.0 - '@types/node': 22.19.15 - long: 5.3.2 - proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 ipaddr.js: 1.9.1 - proxy-agent@6.5.0: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - lru-cache: 7.18.3 - pac-proxy-agent: 7.2.0 - proxy-from-env: 1.1.0 - socks-proxy-agent: 8.0.5 - transitivePeerDependencies: - - supports-color - proxy-from-env@1.1.0: {} prr@1.0.1: @@ -32112,8 +30188,6 @@ snapshots: retext-stringify: 4.0.0 unified: 11.0.5 - retry@0.12.0: {} - retry@0.13.1: {} rettime@0.11.11: {} @@ -32211,6 +30285,16 @@ snapshots: safer-buffer@2.1.2: {} + sandbox-agent@0.4.2(get-port@7.1.0)(zod@3.25.76): + dependencies: + '@sandbox-agent/cli-shared': 0.4.2 + acp-http-client: 0.4.2(zod@3.25.76) + optionalDependencies: + '@sandbox-agent/cli': 0.4.2 + get-port: 7.1.0 + transitivePeerDependencies: + - zod + sass@1.93.2: dependencies: chokidar: 4.0.3 @@ -32235,11 +30319,6 @@ snapshots: ajv-formats: 2.1.1(ajv@8.17.1) ajv-keywords: 5.1.0(ajv@8.17.1) - secure-exec@0.2.1: - dependencies: - '@secure-exec/core': 0.2.1 - '@secure-exec/nodejs': 0.2.1 - secure-exec@https://pkg.pr.new/rivet-dev/secure-exec@7659aba: dependencies: buffer: 6.0.3 @@ -32509,13 +30588,15 @@ snapshots: signal-exit@4.1.0: {} - simple-concat@1.0.1: {} + simple-concat@1.0.1: + optional: true simple-get@4.0.1: dependencies: decompress-response: 6.0.0 once: 1.4.0 simple-concat: 1.0.1 + optional: true simple-plist@1.3.1: dependencies: @@ -32546,8 +30627,6 @@ snapshots: slugify@1.6.8: {} - smart-buffer@4.2.0: {} - smol-toml@1.6.0: {} snake-case@3.0.4: @@ -32555,19 +30634,6 @@ snapshots: dot-case: 3.0.4 tslib: 2.8.1 - socks-proxy-agent@8.0.5: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - socks: 2.8.7 - transitivePeerDependencies: - - supports-color - - socks@2.8.7: - dependencies: - ip-address: 10.1.0 - smart-buffer: 4.2.0 - sonic-boom@4.2.0: dependencies: atomic-sleep: 1.0.0 @@ -32725,8 +30791,6 @@ snapshots: dependencies: js-tokens: 9.0.1 - strnum@2.2.0: {} - strtok3@10.3.4: dependencies: '@tokenizer/token': 0.3.0 @@ -32903,6 +30967,7 @@ snapshots: mkdirp-classic: 0.5.3 pump: 3.0.4 tar-stream: 2.2.0 + optional: true tar-stream@2.2.0: dependencies: @@ -32911,6 +30976,7 @@ snapshots: fs-constants: 1.0.0 inherits: 2.0.4 readable-stream: 3.6.2 + optional: true tar@7.5.11: dependencies: @@ -33059,8 +31125,6 @@ snapshots: trough@2.2.0: {} - ts-algebra@2.0.0: {} - ts-dedent@2.2.0: {} ts-interface-checker@0.1.13: {} @@ -33283,6 +31347,7 @@ snapshots: tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 + optional: true turbo-darwin-64@2.5.6: optional: true @@ -33411,8 +31476,6 @@ snapshots: undici@6.24.1: {} - undici@7.24.7: {} - undici@8.3.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -34450,12 +32513,8 @@ snapshots: web-namespaces@2.0.1: {} - web-streams-polyfill@3.3.3: {} - web-streams-polyfill@4.0.0-beta.3: {} - web-streams-polyfill@4.2.0: {} - web-vitals@4.2.4: {} webidl-conversions@3.0.1: {} @@ -34676,11 +32735,6 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yauzl@2.10.0: - dependencies: - buffer-crc32: 0.2.13 - fd-slicer: 1.1.0 - yjs@13.6.29: dependencies: lib0: 0.2.117 diff --git a/rivetkit-rust/Cargo.toml b/rivetkit-rust/Cargo.toml deleted file mode 100644 index f30335dd72..0000000000 --- a/rivetkit-rust/Cargo.toml +++ /dev/null @@ -1,9 +0,0 @@ -[workspace] -resolver = "2" -members = [ - "packages/client", -] - -[workspace.package] -edition = "2021" -license = "Apache-2.0" diff --git a/rivetkit-rust/packages/client/src/drivers/mod.rs b/rivetkit-rust/packages/client/src/drivers/mod.rs index bdfae84b1f..d237315d4e 100644 --- a/rivetkit-rust/packages/client/src/drivers/mod.rs +++ b/rivetkit-rust/packages/client/src/drivers/mod.rs @@ -1,9 +1,9 @@ use std::sync::Arc; use crate::{ + EncodingKind, TransportKind, protocol::{query, to_client, to_server}, remote_manager::RemoteManager, - EncodingKind, TransportKind, }; use anyhow::Result; use serde_json::Value; diff --git a/rivetkit-rust/packages/client/src/drivers/ws.rs b/rivetkit-rust/packages/client/src/drivers/ws.rs index 6129cdd82f..4ab1a862ca 100644 --- a/rivetkit-rust/packages/client/src/drivers/ws.rs +++ b/rivetkit-rust/packages/client/src/drivers/ws.rs @@ -6,8 +6,8 @@ use tokio_tungstenite::tungstenite::Message; use tracing::debug; use crate::{ - protocol::{codec, to_client, to_server}, EncodingKind, + protocol::{codec, to_client, to_server}, }; use super::{ diff --git a/rivetkit-rust/packages/client/src/encoding.rs b/rivetkit-rust/packages/client/src/encoding.rs new file mode 100644 index 0000000000..f80c1d5df7 --- /dev/null +++ b/rivetkit-rust/packages/client/src/encoding.rs @@ -0,0 +1,62 @@ +//! Byte-payload decoding parity with the rivetkit TypeScript framework. +//! +//! TS sits on the server end of every action call (when invoked through +//! `rivetkit-typescript`). Action responses that contain `Uint8Array` +//! payloads arrive wrapped as `["$Uint8Array", base64]` per the +//! convention defined at +//! `rivetkit-typescript/packages/rivetkit/src/common/encoding.ts:14`. +//! +//! This module strips that wrapper before handing the result to the +//! caller. +//! +//! **Scope-limited:** only `JSON_COMPAT_UINT8_ARRAY` is recognized. +//! Other JSON-compat tags from the TS side (`$BigInt`, `$ArrayBuffer`, +//! `$Set`, `$Undefined`, etc.) are not revived — add them when a real +//! consumer needs them. +//! +//! ## Caveat — `serde_json::Value` has no byte variant +//! +//! TS's `reviveJsonCompatValue` returns a real `Uint8Array`. The Rust +//! client surface uses `serde_json::Value`, which has no native byte +//! representation. The revival strips the `["$Uint8Array", ...]` tag +//! and leaves the **base64-encoded string** as the field's value. The +//! caller knows from action-shape context which fields are bytes and +//! can decode the base64 if raw bytes are needed. +//! +//! This is a known limitation. A future revision could change the +//! action-result type to one that carries bytes natively, but that's a +//! larger API change. + +use serde_json::Value; + +/// Tag string for the `Uint8Array` JSON-compat envelope. Matches the +/// TypeScript constant. +pub const JSON_COMPAT_UINT8_ARRAY: &str = "$Uint8Array"; + +/// Walk a `serde_json::Value` and strip `["$Uint8Array", base64]` +/// wrappers, leaving the base64 string in place. +/// +/// Recurses into arrays and objects so nested byte fields get unwrapped +/// too. Non-wrapper arrays and other types pass through unchanged. +pub fn revive_json_compat(value: Value) -> Value { + match value { + Value::Array(items) if is_uint8_array_tag(&items) => { + // ["$Uint8Array", ""] → "" + // Safe: is_uint8_array_tag guarantees items[1] is a string. + items.into_iter().nth(1).expect("tagged array has 2 items") + } + Value::Array(items) => Value::Array(items.into_iter().map(revive_json_compat).collect()), + Value::Object(map) => { + let mut revived = serde_json::Map::with_capacity(map.len()); + for (k, v) in map { + revived.insert(k, revive_json_compat(v)); + } + Value::Object(revived) + } + other => other, + } +} + +fn is_uint8_array_tag(items: &[Value]) -> bool { + items.len() == 2 && items[0].as_str() == Some(JSON_COMPAT_UINT8_ARRAY) && items[1].is_string() +} diff --git a/rivetkit-rust/packages/client/src/lib.rs b/rivetkit-rust/packages/client/src/lib.rs index adf5ece4a3..4de3028f45 100644 --- a/rivetkit-rust/packages/client/src/lib.rs +++ b/rivetkit-rust/packages/client/src/lib.rs @@ -10,6 +10,7 @@ pub mod client; mod common; pub mod connection; pub mod drivers; +pub mod encoding; pub mod handle; pub mod protocol; mod remote_manager; diff --git a/rivetkit-rust/packages/client/src/protocol/codec.rs b/rivetkit-rust/packages/client/src/protocol/codec.rs index 71f3fd44f1..23ad1d7474 100644 --- a/rivetkit-rust/packages/client/src/protocol/codec.rs +++ b/rivetkit-rust/packages/client/src/protocol/codec.rs @@ -1,7 +1,7 @@ -use anyhow::{anyhow, Context, Result}; +use anyhow::{Context, Result, anyhow}; use rivetkit_client_protocol as wire; use serde::Serialize; -use serde_json::{json, Value as JsonValue}; +use serde_json::{Value as JsonValue, json}; use vbare::OwnedVersionedData; use crate::EncodingKind; @@ -46,20 +46,20 @@ pub fn encode_http_action_request(encoding: EncodingKind, args: &[JsonValue]) -> } pub fn decode_http_action_response(encoding: EncodingKind, payload: &[u8]) -> Result { - match encoding { + let raw = match encoding { EncodingKind::Json => { let value: JsonValue = serde_json::from_slice(payload)?; value .get("output") .cloned() - .ok_or_else(|| anyhow!("action response missing output")) + .ok_or_else(|| anyhow!("action response missing output"))? } EncodingKind::Cbor => { let value: JsonValue = serde_cbor::from_slice(payload)?; value .get("output") .cloned() - .ok_or_else(|| anyhow!("action response missing output")) + .ok_or_else(|| anyhow!("action response missing output"))? } EncodingKind::Bare => { let response = @@ -67,9 +67,13 @@ pub fn decode_http_action_response(encoding: EncodingKind, payload: &[u8]) -> Re payload, ) .context("decode bare action response")?; - Ok(serde_cbor::from_slice(&response.output)?) + serde_cbor::from_slice(&response.output)? } - } + }; + // Strip rivetkit's `["$Uint8Array", base64]` byte-payload wrappers + // (mirrors TS `reviveJsonCompatValue`). See `crate::encoding` for + // the caveat about how byte payloads are represented in JsonValue. + Ok(crate::encoding::revive_json_compat(raw)) } pub fn encode_http_queue_request( diff --git a/rivetkit-rust/packages/client/tests/encoding.rs b/rivetkit-rust/packages/client/tests/encoding.rs new file mode 100644 index 0000000000..3cd7724dff --- /dev/null +++ b/rivetkit-rust/packages/client/tests/encoding.rs @@ -0,0 +1,82 @@ +//! Decode-side tests for the `JSON_COMPAT_UINT8_ARRAY` revival. +//! +//! Mirrors `rivetkit-typescript/.../common/encoding.ts::reviveJsonCompatValue` +//! for the `Uint8Array` case. + +use rivetkit_client::encoding::revive_json_compat; +use serde_json::json; + +#[test] +fn json_compat_uint8_array_revives_to_base64_string() { + // Note: with `serde_json::Value`, we can't represent raw bytes + // natively. The tag is stripped; the base64 string remains. + // Callers know the field shape and decode as needed. + let wrapped = json!(["$Uint8Array", "aGVsbG8="]); + let revived = revive_json_compat(wrapped); + assert_eq!(revived, json!("aGVsbG8=")); +} + +#[test] +fn nested_byte_field_revives_inside_struct() { + let wrapped = json!({ + "status": 200, + "body": ["$Uint8Array", "b2s="], + }); + let revived = revive_json_compat(wrapped); + assert_eq!(revived["status"], 200); + assert_eq!(revived["body"], json!("b2s=")); +} + +#[test] +fn deeply_nested_byte_field_revives() { + let wrapped = json!({ + "outer": { + "middle": { + "inner_bytes": ["$Uint8Array", "ZGVlcA=="] + } + } + }); + let revived = revive_json_compat(wrapped); + assert_eq!(revived["outer"]["middle"]["inner_bytes"], json!("ZGVlcA==")); +} + +#[test] +fn non_byte_arrays_pass_through() { + let value = json!([1, 2, 3]); + assert_eq!(revive_json_compat(value.clone()), value); +} + +#[test] +fn unrelated_tagged_arrays_pass_through() { + // `["$BigInt", "12345"]` is a different tag; we only handle Uint8Array. + let value = json!(["$BigInt", "12345"]); + assert_eq!(revive_json_compat(value.clone()), value); + + // Random 2-element arrays where the first element isn't a recognized + // tag should pass through unchanged. + let value = json!(["hello", "world"]); + assert_eq!(revive_json_compat(value.clone()), value); +} + +#[test] +fn three_element_arrays_starting_with_tag_pass_through() { + // Only 2-element arrays with the exact tag are recognized. + let value = json!(["$Uint8Array", "data", "extra"]); + assert_eq!(revive_json_compat(value.clone()), value); +} + +#[test] +fn array_of_byte_payloads_each_revives() { + let wrapped = json!([["$Uint8Array", "YQ=="], ["$Uint8Array", "YmM="],]); + let revived = revive_json_compat(wrapped); + assert_eq!(revived, json!(["YQ==", "YmM="])); +} + +#[test] +fn primitives_pass_through() { + assert_eq!(revive_json_compat(json!(null)), json!(null)); + assert_eq!(revive_json_compat(json!(true)), json!(true)); + assert_eq!(revive_json_compat(json!(42)), json!(42)); + assert_eq!(revive_json_compat(json!("string")), json!("string")); + assert_eq!(revive_json_compat(json!(3.14)), json!(3.14)); +} diff --git a/rivetkit-rust/packages/rivetkit-agent-os/Cargo.toml b/rivetkit-rust/packages/rivetkit-agent-os/Cargo.toml new file mode 100644 index 0000000000..f4736bde15 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "rivetkit-agent-os" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true + +[dependencies] +agent-os-client = { path = "../../../../agent-os/crates/client" } +anyhow.workspace = true +base64.workspace = true +bytes = "1" +ciborium.workspace = true +futures.workspace = true +http = "1" +rivet-error.workspace = true +rivetkit = { path = "../rivetkit" } +rivetkit-core = { path = "../rivetkit-core" } +serde.workspace = true +serde_bytes.workspace = true +serde_json.workspace = true +tokio.workspace = true +tracing.workspace = true +uuid.workspace = true + +[dev-dependencies] +base64.workspace = true +ciborium.workspace = true +rusqlite = { workspace = true } +serde_json.workspace = true +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +# L0 durable-sqlite harness: in-process, file-backed (RocksDB) depot store so a +# real persistent SQLite database can be driven from a unit/integration test +# without the engine binary. See tests/resume_sleep_wake.rs. +depot = { workspace = true } +depot-client = { workspace = true } +depot-client-embedded = { workspace = true } +universaldb = { workspace = true } +gas = { workspace = true } +rivet-pools = { workspace = true } +tempfile = { workspace = true } diff --git a/rivetkit-rust/packages/rivetkit-agent-os/src/actions/cron.rs b/rivetkit-rust/packages/rivetkit-agent-os/src/actions/cron.rs new file mode 100644 index 0000000000..ba52b312df --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/src/actions/cron.rs @@ -0,0 +1,93 @@ +//! Cron actions. The client's `CronJobOptions` / `CronAction` / +//! `CronJobInfo` are not serde types (they carry closures), so we define +//! serde DTOs here and map to/from the client types. + +use agent_os_client::{AgentOs, CronAction, CronJobOptions, CronOverlap}; +use anyhow::{Result, anyhow}; +use serde::{Deserialize, Serialize}; + +/// `{ type: "exec", command, args }` | `{ type: "session", agentType, prompt }`. +#[derive(Debug, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum CronActionDto { + Exec { + command: String, + #[serde(default)] + args: Vec, + }, + Session { + agent_type: String, + prompt: String, + }, +} + +/// Options object for `scheduleCron(...)`. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CronJobOptionsDto { + #[serde(default)] + pub id: Option, + pub schedule: String, + pub action: CronActionDto, + #[serde(default)] + pub overlap: Option, +} + +/// `{ id }` returned by `scheduleCron`. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ScheduledCronDto { + pub id: String, +} + +/// One entry returned by `listCronJobs`. `last_run` / `next_run` are +/// epoch-millis timestamps serialized as `f64` so they cross the napi +/// boundary as JS `number`s (not `BigInt`s), matching the core API. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CronJobInfoDto { + pub id: String, + pub schedule: String, + pub overlap: CronOverlap, + pub last_run: Option, + pub next_run: Option, +} + +fn to_action(dto: CronActionDto) -> CronAction { + match dto { + CronActionDto::Exec { command, args } => CronAction::Exec { command, args }, + CronActionDto::Session { agent_type, prompt } => CronAction::Session { + agent_type, + prompt, + options: None, + }, + } +} + +pub fn schedule_cron(vm: &AgentOs, dto: CronJobOptionsDto) -> Result { + let options = CronJobOptions { + id: dto.id, + schedule: dto.schedule, + action: to_action(dto.action), + overlap: dto.overlap, + }; + let handle = vm.schedule_cron(options).map_err(|e| anyhow!(e))?; + Ok(ScheduledCronDto { id: handle.id }) +} + +pub fn list_cron_jobs(vm: &AgentOs) -> Vec { + vm.list_cron_jobs() + .into_iter() + .map(|info| CronJobInfoDto { + id: info.id, + schedule: info.schedule, + overlap: info.overlap, + last_run: info.last_run.map(|t| t.timestamp_millis() as f64), + next_run: info.next_run.map(|t| t.timestamp_millis() as f64), + }) + .collect() +} + +pub fn cancel_cron_job(vm: &AgentOs, id: &str) { + vm.cancel_cron_job(id); +} diff --git a/rivetkit-rust/packages/rivetkit-agent-os/src/actions/filesystem.rs b/rivetkit-rust/packages/rivetkit-agent-os/src/actions/filesystem.rs new file mode 100644 index 0000000000..0ad1702505 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/src/actions/filesystem.rs @@ -0,0 +1,213 @@ +//! Filesystem actions. Each helper takes `&AgentOs` plus typed args +//! and delegates to the matching upstream `AgentOs::*` method. DTOs +//! used by batch operations live here too so the dispatcher arms can +//! deserialize/serialize directly without re-declaring shapes. + +use agent_os_client::{ + AgentOs, BatchReadResult, BatchWriteEntry, BatchWriteResult, DeleteOptions, DirEntry, + FileContent, MkdirOptions, ReaddirRecursiveOptions, VirtualStat, +}; +use anyhow::Result; +use serde::{Deserialize, Serialize}; + +/// `readFile(path)` — port of [`AgentOs::read_file`]. +pub async fn read_file(vm: &AgentOs, path: &str) -> Result> { + vm.read_file(path) + .await + .inspect_err(|error| tracing::error!(?error, path, "read_file failed")) +} + +/// `writeFile(path, contents)` — port of [`AgentOs::write_file`]. +pub async fn write_file(vm: &AgentOs, path: &str, contents: Vec) -> Result<()> { + vm.write_file(path, FileContent::Bytes(contents)) + .await + .inspect_err(|error| tracing::error!(?error, path, "write_file failed")) +} + +/// `stat(path)` — port of [`AgentOs::stat`]. Returns the [`VirtualStat`] +/// structure directly; the rivetkit encoder handles cross-encoding +/// translation (bare / cbor / json) at the framework layer. +pub async fn stat(vm: &AgentOs, path: &str) -> Result { + vm.stat(path).await +} + +/// `mkdir(path)` — port of [`AgentOs::mkdir`]. Always recursive so the +/// JS shim's "create parent dirs if needed" expectation holds; the +/// driver tests rely on this. +pub async fn mkdir(vm: &AgentOs, path: &str) -> Result<()> { + vm.mkdir(path, MkdirOptions { recursive: true }).await +} + +/// `readdir(path)` — port of [`AgentOs::readdir`]. Returns the +/// (unsorted) child names, including `.` and `..`. Sorting / filtering +/// is up to the caller. +pub async fn readdir(vm: &AgentOs, path: &str) -> Result> { + vm.readdir(path).await +} + +/// `exists(path)` — port of [`AgentOs::exists`]. +pub async fn exists(vm: &AgentOs, path: &str) -> Result { + vm.exists(path).await +} + +/// `move(from, to)` — port of [`AgentOs::move_path`]. Named `move_path` +/// in Rust because `move` is a keyword. +pub async fn move_path(vm: &AgentOs, from: &str, to: &str) -> Result<()> { + vm.move_path(from, to).await +} + +/// Options for `deleteFile`. TS sends `{ recursive?: boolean }`. +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct DeleteOptionsArg { + #[serde(default)] + pub recursive: bool, +} + +/// `deleteFile(path, options?)` — port of [`AgentOs::delete`]. Honors the +/// `recursive` option so directory deletes match JS semantics. +pub async fn delete_file(vm: &AgentOs, path: &str, recursive: bool) -> Result<()> { + vm.delete(path, DeleteOptions { recursive }).await +} + +/// `writeFiles(entries)` — port of [`AgentOs::write_files`]. Per-entry +/// failures are reported in the [`BatchWriteResultDto`]'s `success` / +/// `error` fields rather than as a top-level error. +pub async fn write_files( + vm: &AgentOs, + entries: Vec, +) -> Vec { + let entries: Vec = entries + .into_iter() + .map(|entry| BatchWriteEntry { + path: entry.path, + content: FileContent::Bytes(entry.content.into_bytes()), + }) + .collect(); + vm.write_files(entries) + .await + .into_iter() + .map(BatchWriteResultDto::from) + .collect() +} + +/// `readFiles(paths)` — port of [`AgentOs::read_files`]. Per-entry +/// failures are reported as `content: None` plus an error string. +pub async fn read_files(vm: &AgentOs, paths: Vec) -> Vec { + vm.read_files(paths) + .await + .into_iter() + .map(BatchReadResultDto::from) + .collect() +} + +/// `readdirRecursive(path)` — port of [`AgentOs::readdir_recursive`]. +/// Returns every reachable entry with its type and size. Unbounded +/// depth; the JS shim passes no max-depth in the driver tests so this +/// arm defaults to `ReaddirRecursiveOptions::default()`. +pub async fn readdir_recursive(vm: &AgentOs, path: &str) -> Result> { + vm.readdir_recursive(path, ReaddirRecursiveOptions::default()) + .await +} + +// --------------------------------------------------------------------------- +// Action argument / reply DTOs +// --------------------------------------------------------------------------- + +/// Accept either a CBOR text string, a CBOR byte string (via `ByteBuf`), or +/// the `["$Uint8Array", base64]` wrapper that TS encoders emit when the +/// outer codec is JSON-compatible. Used by `writeFile` and `writeFiles`. +#[derive(Deserialize)] +#[serde(untagged)] +pub enum WriteFileContent { + String(String), + Bytes(serde_bytes::ByteBuf), + Wrapped(JsonCompatUint8Array), +} + +impl WriteFileContent { + pub fn into_bytes(self) -> Vec { + match self { + Self::String(s) => s.into_bytes(), + Self::Bytes(b) => b.into_vec(), + Self::Wrapped(w) => w.bytes, + } + } +} + +/// Deserializer for the `["$Uint8Array", base64]` envelope. Part of +/// [`WriteFileContent`]'s untagged enum so the same arms accept wrapped +/// bytes from the JSON encoder path. +pub struct JsonCompatUint8Array { + bytes: Vec, +} + +impl<'de> Deserialize<'de> for JsonCompatUint8Array { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; + let (tag, base64): (String, String) = Deserialize::deserialize(deserializer)?; + if tag != "$Uint8Array" { + return Err(serde::de::Error::custom(format!( + "expected $Uint8Array wrapper, got {tag}" + ))); + } + let bytes = BASE64 + .decode(&base64) + .map_err(|error| serde::de::Error::custom(format!("base64 decode: {error}")))?; + Ok(Self { bytes }) + } +} + +/// Argument entry for `writeFiles`. TS sends `[{path, content}, ...]` +/// where `content` follows the same coercion rules as `writeFile`. +#[derive(Deserialize)] +pub struct WriteFilesEntryArg { + pub path: String, + pub content: WriteFileContent, +} + +/// Reply entry for `writeFiles`. Mirrors `BatchWriteResult` in a +/// serializable form. `error` is `None` on success. +#[derive(Serialize)] +pub struct BatchWriteResultDto { + pub path: String, + pub success: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +impl From for BatchWriteResultDto { + fn from(value: BatchWriteResult) -> Self { + Self { + path: value.path, + success: value.success, + error: value.error, + } + } +} + +/// Reply entry for `readFiles`. `content` is wrapped via `serde_bytes` +/// so the `JsonCompatAdapter` re-wraps it as `["$Uint8Array", base64]` +/// for JSON encoders. `None` content + `Some(error)` indicates that the +/// specific file failed without aborting the whole batch. +#[derive(Serialize)] +pub struct BatchReadResultDto { + pub path: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub content: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +impl From for BatchReadResultDto { + fn from(value: BatchReadResult) -> Self { + Self { + path: value.path, + content: value.content.map(serde_bytes::ByteBuf::from), + error: value.error, + } + } +} diff --git a/rivetkit-rust/packages/rivetkit-agent-os/src/actions/mod.rs b/rivetkit-rust/packages/rivetkit-agent-os/src/actions/mod.rs new file mode 100644 index 0000000000..60ab533fc6 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/src/actions/mod.rs @@ -0,0 +1,388 @@ +//! Action dispatcher entry point. +//! +//! Each arm decodes its positional args with `action.decode_as::<(...)>()` +//! (TS sends args as a CBOR array) and replies via [`Action::ok`] or +//! [`Action::err`]. Byte payloads auto-wrap via the rivetkit +//! `JSON_COMPAT_UINT8_ARRAY` convention thanks to `Action::ok` running +//! through `encode_json_compat`. + +pub mod cron; +pub mod filesystem; +pub mod network; +pub mod preview; +pub mod process; +pub mod session; + +use agent_os_client::AgentOs; +use anyhow::{Result, anyhow}; +use rivetkit::{ActionCall, Ctx}; + +use crate::actor::{AgentOsActor, Vars}; +use filesystem::{WriteFileContent, WriteFilesEntryArg}; + +/// Dispatch one action against a live VM. Each arm decodes its args, +/// calls the helper, and replies through `action.ok` / `action.err`. +/// +/// `ctx` provides the actor's SQLite database, used by the persistence-backed +/// arms (signed preview URLs and session metadata in `agent_os_*` tables). +pub async fn dispatch( + ctx: &Ctx, + vm: &AgentOs, + vars: &mut Vars, + action: ActionCall, +) { + let name = action.name().to_owned(); + match name.as_str() { + "readFile" => { + let args: Result<(String,)> = action.decode_as(); + match args { + Ok((path,)) => match filesystem::read_file(vm, &path).await { + Ok(bytes) => { + // Wrap as serde_bytes so it serializes as a byte + // string, which the rivetkit JsonCompatAdapter then + // re-wraps as `["$Uint8Array", base64]`. + action.ok(&serde_bytes::ByteBuf::from(bytes)); + } + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "writeFile" => { + // TS sends `contents` as either a `string` (CBOR text string), + // a `Uint8Array` / `Buffer` (CBOR byte string -> `ByteBuf`), or + // a `["$Uint8Array", base64]` wrapper. Accept any of those and + // coerce to raw bytes. + let args: Result<(String, WriteFileContent)> = action.decode_as(); + match args { + Ok((path, contents)) => { + match filesystem::write_file(vm, &path, contents.into_bytes()).await { + Ok(()) => action.ok(&()), + Err(error) => action.err(error), + } + } + Err(error) => action.err(error), + } + } + "stat" => { + let args: Result<(String,)> = action.decode_as(); + match args { + Ok((path,)) => match filesystem::stat(vm, &path).await { + Ok(vstat) => action.ok(&vstat), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "mkdir" => { + let args: Result<(String,)> = action.decode_as(); + match args { + Ok((path,)) => match filesystem::mkdir(vm, &path).await { + Ok(()) => action.ok(&()), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "readdir" => { + let args: Result<(String,)> = action.decode_as(); + match args { + Ok((path,)) => match filesystem::readdir(vm, &path).await { + Ok(entries) => action.ok(&entries), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "exists" => { + let args: Result<(String,)> = action.decode_as(); + match args { + Ok((path,)) => match filesystem::exists(vm, &path).await { + Ok(present) => action.ok(&present), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "move" => { + let args: Result<(String, String)> = action.decode_as(); + match args { + Ok((from, to)) => match filesystem::move_path(vm, &from, &to).await { + Ok(()) => action.ok(&()), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "deleteFile" => { + // TS may omit the trailing options object, so the CBOR array has + // length 1 or 2. Try the two-arg shape first, then fall back to + // the one-arg shape (ciborium rejects a short array for a fixed + // tuple, so a plain `Option` tuple is not enough). + let decoded = action + .decode_as::<(String, Option)>() + .map(|(path, options)| (path, options.unwrap_or_default().recursive)) + .or_else(|_| action.decode_as::<(String,)>().map(|(path,)| (path, false))); + match decoded { + Ok((path, recursive)) => { + match filesystem::delete_file(vm, &path, recursive).await { + Ok(()) => action.ok(&()), + Err(error) => action.err(error), + } + } + Err(error) => action.err(error), + } + } + "writeFiles" => { + let args: Result<(Vec,)> = action.decode_as(); + match args { + Ok((entries,)) => { + let results = filesystem::write_files(vm, entries).await; + action.ok(&results); + } + Err(error) => action.err(error), + } + } + "readFiles" => { + let args: Result<(Vec,)> = action.decode_as(); + match args { + Ok((paths,)) => { + let results = filesystem::read_files(vm, paths).await; + action.ok(&results); + } + Err(error) => action.err(error), + } + } + "readdirRecursive" => { + let args: Result<(String,)> = action.decode_as(); + match args { + Ok((path,)) => match filesystem::readdir_recursive(vm, &path).await { + Ok(entries) => action.ok(&entries), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "exec" => { + let args: Result<(String,)> = action.decode_as(); + match args { + Ok((command,)) => match process::exec(vm, &command).await { + Ok(result) => action.ok(&result), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "spawn" => { + let args: Result<(String, Vec)> = action.decode_as(); + match args { + Ok((command, spawn_args)) => match process::spawn(vm, &command, spawn_args) { + Ok(handle) => action.ok(&handle), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "waitProcess" => { + let args: Result<(u32,)> = action.decode_as(); + match args { + Ok((pid,)) => match process::wait_process(vm, pid).await { + Ok(code) => action.ok(&code), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "killProcess" => { + let args: Result<(u32,)> = action.decode_as(); + match args { + Ok((pid,)) => match process::kill_process(vm, pid) { + Ok(()) => action.ok(&()), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "stopProcess" => { + let args: Result<(u32,)> = action.decode_as(); + match args { + Ok((pid,)) => match process::stop_process(vm, pid) { + Ok(()) => action.ok(&()), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "listProcesses" => { + // No args. + let processes = process::list_processes(vm); + action.ok(&processes); + } + "allProcesses" => match process::all_processes(vm).await { + Ok(processes) => action.ok(&processes), + Err(error) => action.err(error), + }, + "processTree" => match process::process_tree(vm).await { + Ok(tree) => action.ok(&tree), + Err(error) => action.err(error), + }, + "getProcess" => { + let args: Result<(u32,)> = action.decode_as(); + match args { + Ok((pid,)) => match process::get_process(vm, pid) { + Ok(info) => action.ok(&info), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "writeProcessStdin" => { + let args: Result<(u32, WriteFileContent)> = action.decode_as(); + match args { + Ok((pid, data)) => match process::write_process_stdin(vm, pid, data) { + Ok(()) => action.ok(&()), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "closeProcessStdin" => { + let args: Result<(u32,)> = action.decode_as(); + match args { + Ok((pid,)) => match process::close_process_stdin(vm, pid) { + Ok(()) => action.ok(&()), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "vmFetch" => { + // Trailing options object is optional (length 2 or 3). + let decoded = action + .decode_as::<(u16, String, Option)>() + .map(|(port, url, options)| (port, url, options.unwrap_or_default())) + .or_else(|_| { + action + .decode_as::<(u16, String)>() + .map(|(port, url)| (port, url, network::FetchOptions::default())) + }); + match decoded { + Ok((port, url, options)) => match network::fetch(vm, port, &url, options).await { + Ok(response) => action.ok(&response), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "scheduleCron" => { + let args: Result<(cron::CronJobOptionsDto,)> = action.decode_as(); + match args { + Ok((options,)) => match cron::schedule_cron(vm, options) { + Ok(handle) => action.ok(&handle), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "listCronJobs" => action.ok(&cron::list_cron_jobs(vm)), + "cancelCronJob" => { + let args: Result<(String,)> = action.decode_as(); + match args { + Ok((id,)) => { + cron::cancel_cron_job(vm, &id); + action.ok(&()); + } + Err(error) => action.err(error), + } + } + "createSession" => { + // Trailing options object is optional (length 1 or 2). + let decoded = action + .decode_as::<(String, Option)>() + .map(|(agent_type, options)| (agent_type, options.unwrap_or_default())) + .or_else(|_| { + action.decode_as::<(String,)>().map(|(agent_type,)| { + (agent_type, session::CreateSessionOptionsDto::default()) + }) + }); + match decoded { + Ok((agent_type, options)) => { + match session::create_session(ctx, vm, vars, &agent_type, options).await { + Ok(id) => action.ok(&id), + Err(error) => { + tracing::error!(?error, agent_type, "create_session failed"); + action.err(error) + } + } + } + Err(error) => action.err(error), + } + } + "sendPrompt" => { + let args: Result<(String, String)> = action.decode_as(); + match args { + Ok((session_id, text)) => { + match session::send_prompt(ctx, vm, vars, &session_id, &text).await { + Ok(result) => action.ok(&result), + Err(error) => { + tracing::error!(?error, session_id, "send_prompt failed"); + action.err(error) + } + } + } + Err(error) => action.err(error), + } + } + "closeSession" => { + let args: Result<(String,)> = action.decode_as(); + match args { + Ok((session_id,)) => match session::close_session(ctx, vm, vars, &session_id).await + { + Ok(()) => action.ok(&()), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "listPersistedSessions" => match session::list_persisted_sessions(ctx).await { + Ok(sessions) => action.ok(&sessions), + Err(error) => action.err(error), + }, + "getSessionEvents" => { + let args: Result<(String,)> = action.decode_as(); + match args { + Ok((session_id,)) => match session::get_session_events(ctx, &session_id).await { + Ok(events) => action.ok(&events), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "createSignedPreviewUrl" => { + // TS calls `createSignedPreviewUrl(port, ttlSeconds)`. + let args: Result<(u16, u64)> = action.decode_as(); + match args { + Ok((port, ttl_seconds)) => match preview::create(ctx, port, ttl_seconds).await { + Ok(dto) => action.ok(&dto), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + "expireSignedPreviewUrl" => { + let args: Result<(String,)> = action.decode_as(); + match args { + Ok((token,)) => match preview::expire(ctx, &token).await { + Ok(()) => action.ok(&()), + Err(error) => action.err(error), + }, + Err(error) => action.err(error), + } + } + _ => action.err(not_implemented(&name)), + } +} + +fn not_implemented(name: &str) -> anyhow::Error { + anyhow!("agent-os action not implemented yet: {name}") +} diff --git a/rivetkit-rust/packages/rivetkit-agent-os/src/actions/network.rs b/rivetkit-rust/packages/rivetkit-agent-os/src/actions/network.rs new file mode 100644 index 0000000000..98407ac27d --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/src/actions/network.rs @@ -0,0 +1,70 @@ +//! Network actions: `vmFetch` routes an HTTP request to a service +//! listening on a guest loopback port via [`AgentOs::fetch`]. + +use std::collections::BTreeMap; + +use agent_os_client::AgentOs; +use anyhow::Result; +use bytes::Bytes; +use serde::{Deserialize, Serialize}; + +/// Optional request shape for `vmFetch(port, url, options?)`. +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FetchOptions { + #[serde(default)] + pub method: Option, + #[serde(default)] + pub headers: Option>, + #[serde(default)] + pub body: Option>, +} + +/// JSON-serializable response returned to the TS client. `body` is wrapped +/// via `serde_bytes` so the rivetkit `JsonCompatAdapter` re-encodes it as +/// `["$Uint8Array", base64]`, which the TS client decodes back to a +/// `Uint8Array` (the shape the example's `TextDecoder` expects). +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FetchResponseDto { + pub status: u16, + pub headers: BTreeMap, + pub body: serde_bytes::ByteBuf, +} + +/// `vmFetch(port, url, options?)` — port of [`AgentOs::fetch`]. +pub async fn fetch( + vm: &AgentOs, + port: u16, + url: &str, + options: FetchOptions, +) -> Result { + let method = options.method.as_deref().unwrap_or("GET"); + let mut builder = http::Request::builder().method(method).uri(url); + if let Some(headers) = &options.headers { + for (name, value) in headers { + builder = builder.header(name.as_str(), value.as_str()); + } + } + let body = Bytes::from(options.body.unwrap_or_default()); + let request = builder.body(body)?; + + let response = vm.fetch(port, request).await?; + let status = response.status().as_u16(); + let headers = response + .headers() + .iter() + .map(|(name, value)| { + ( + name.as_str().to_owned(), + value.to_str().unwrap_or_default().to_owned(), + ) + }) + .collect(); + let body = serde_bytes::ByteBuf::from(response.into_body().to_vec()); + Ok(FetchResponseDto { + status, + headers, + body, + }) +} diff --git a/rivetkit-rust/packages/rivetkit-agent-os/src/actions/preview.rs b/rivetkit-rust/packages/rivetkit-agent-os/src/actions/preview.rs new file mode 100644 index 0000000000..7ac5ca653e --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/src/actions/preview.rs @@ -0,0 +1,114 @@ +//! Preview URL actions. These are a rivetkit-actor-layer feature, not part +//! of the core `AgentOs` API: they issue a signed, time-limited token that +//! maps an external request path to a guest loopback port. The actor's HTTP +//! event handler (`crate::run`) proxies `/preview/{token}/...` requests to +//! that port via [`agent_os_client::AgentOs::fetch`]. +//! +//! Tokens are persisted to the actor's SQLite database (`agent_os_preview_tokens`) +//! via `ctx.db_*`, so issued previews survive actor sleep/wake. + +use std::time::{SystemTime, UNIX_EPOCH}; + +use anyhow::Result; +use rivetkit::Ctx; +use serde::Serialize; +use serde_json::json; +use uuid::Uuid; + +use crate::actor::AgentOsActor; +use crate::persistence::{query_rows, run_stmt}; + +/// Default lifetime of a signed preview URL: one hour. +const PREVIEW_TTL_MS: i64 = 60 * 60 * 1000; + +/// `{ path, token, port, expiresAt }` returned by `createSignedPreviewUrl`. +/// +/// `expires_at` is an epoch-millis timestamp serialized as `f64` so it +/// crosses the napi boundary as a JS `number` (not a `BigInt`), matching the +/// core API and the example's `new Date(expiresAt)` usage. Millisecond +/// timestamps are exactly representable in `f64` well past the year 10000. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SignedPreviewUrlDto { + pub path: String, + pub token: String, + pub port: u16, + pub expires_at: f64, +} + +fn now_ms() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as i64) + .unwrap_or(0) +} + +/// Issue a signed preview URL for `port`, valid for `ttl_seconds` (falling back +/// to [`PREVIEW_TTL_MS`] when the caller passes `0`). +pub async fn create( + ctx: &Ctx, + port: u16, + ttl_seconds: u64, +) -> Result { + let token = Uuid::new_v4().to_string(); + let created_at = now_ms(); + let ttl_ms = if ttl_seconds == 0 { + PREVIEW_TTL_MS + } else { + (ttl_seconds as i64).saturating_mul(1000) + }; + let expires_at = created_at + ttl_ms; + run_stmt( + ctx, + "INSERT INTO agent_os_preview_tokens (token, port, created_at, expires_at) \ + VALUES (?, ?, ?, ?)", + &[ + json!(token), + json!(port), + json!(created_at), + json!(expires_at), + ], + ) + .await?; + Ok(SignedPreviewUrlDto { + // The `/request` prefix routes through rivetkit's raw-actor-HTTP path + // (RegistryHttpRoute::UserRawRequest); the gateway strips `/request` + // before the actor sees it, so `proxy_preview` receives `/preview/`. + // Without this prefix the gateway classifies the path as NotFound (404). + path: format!("/request/preview/{token}"), + token, + port, + expires_at: expires_at as f64, + }) +} + +/// Revoke a previously issued preview token. Idempotent. +pub async fn expire(ctx: &Ctx, token: &str) -> Result<()> { + run_stmt( + ctx, + "DELETE FROM agent_os_preview_tokens WHERE token = ?", + &[json!(token)], + ) + .await +} + +/// Resolve `token` to its target port if it exists and has not expired. +/// Expired tokens are pruned as a side effect. +pub async fn resolve(ctx: &Ctx, token: &str) -> Result> { + let rows = query_rows( + ctx, + "SELECT port, expires_at FROM agent_os_preview_tokens WHERE token = ?", + &[json!(token)], + ) + .await?; + let Some(row) = rows.into_iter().next() else { + return Ok(None); + }; + let expires_at = row.get("expires_at").and_then(|v| v.as_i64()).unwrap_or(0); + let port = row.get("port").and_then(|v| v.as_i64()).unwrap_or(0) as u16; + if expires_at <= now_ms() { + expire(ctx, token).await?; + return Ok(None); + } + Ok(Some(port)) +} diff --git a/rivetkit-rust/packages/rivetkit-agent-os/src/actions/process.rs b/rivetkit-rust/packages/rivetkit-agent-os/src/actions/process.rs new file mode 100644 index 0000000000..a23e884b9a --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/src/actions/process.rs @@ -0,0 +1,110 @@ +//! Process actions. Each helper takes `&AgentOs` plus typed args and +//! delegates to the matching upstream `AgentOs::*` method. DTOs used +//! by `exec` and other arms that need camelCase serialization live +//! here so the dispatcher arms can reply directly. + +use agent_os_client::{ + AgentOs, ExecOptions, ExecResult, ProcessInfo, ProcessTreeNode, SpawnHandle, SpawnOptions, + SpawnedProcessInfo, +}; +use anyhow::Result; +use serde::Serialize; + +/// `exec(command)` — port of [`AgentOs::exec`] with default options. +/// Returns an [`ExecResultDto`] with camelCase `exitCode` for the JS side. +pub async fn exec(vm: &AgentOs, command: &str) -> Result { + vm.exec(command, ExecOptions::default()) + .await + .map(ExecResultDto::from) +} + +/// `spawn(command, args)` — port of [`AgentOs::spawn`]. Returns the +/// [`SpawnHandle`] `{ pid }` directly; the underlying type already +/// derives `Serialize`. +pub fn spawn(vm: &AgentOs, command: &str, args: Vec) -> Result { + vm.spawn(command, args, SpawnOptions::default()) +} + +/// `waitProcess(pid)` — port of [`AgentOs::wait_process`]. Returns the +/// exit code (`i32`). +pub async fn wait_process(vm: &AgentOs, pid: u32) -> Result { + vm.wait_process(pid).await.map_err(anyhow::Error::from) +} + +/// `killProcess(pid)` — port of [`AgentOs::kill_process`] (sync). +pub fn kill_process(vm: &AgentOs, pid: u32) -> Result<()> { + vm.kill_process(pid).map_err(anyhow::Error::from) +} + +/// `stopProcess(pid)` — port of [`AgentOs::stop_process`] (sync). +pub fn stop_process(vm: &AgentOs, pid: u32) -> Result<()> { + vm.stop_process(pid).map_err(anyhow::Error::from) +} + +/// `listProcesses()` — port of [`AgentOs::list_processes`]. Returns the +/// SDK-spawned processes (not kernel processes); already camelCase via +/// `#[serde(rename = "exitCode")]` on `SpawnedProcessInfo`. +pub fn list_processes(vm: &AgentOs) -> Vec { + vm.list_processes() +} + +/// `allProcesses()` — port of [`AgentOs::all_processes`]. Returns the +/// full kernel process snapshot. +pub async fn all_processes(vm: &AgentOs) -> Result> { + vm.all_processes().await +} + +/// `processTree()` — port of [`AgentOs::process_tree`]. Returns the +/// kernel process forest. +pub async fn process_tree(vm: &AgentOs) -> Result> { + vm.process_tree().await +} + +/// `getProcess(pid)` — port of [`AgentOs::get_process`] (sync). +pub fn get_process(vm: &AgentOs, pid: u32) -> Result { + vm.get_process(pid).map_err(anyhow::Error::from) +} + +/// `writeProcessStdin(pid, data)` — port of +/// [`AgentOs::write_process_stdin`]. Accepts string or bytes content +/// via the same coercion rules as `writeFile`. +pub fn write_process_stdin( + vm: &AgentOs, + pid: u32, + data: super::filesystem::WriteFileContent, +) -> Result<()> { + use agent_os_client::StdinInput; + let stdin = StdinInput::Bytes(data.into_bytes()); + vm.write_process_stdin(pid, stdin) + .map_err(anyhow::Error::from) +} + +/// `closeProcessStdin(pid)` — port of [`AgentOs::close_process_stdin`]. +pub fn close_process_stdin(vm: &AgentOs, pid: u32) -> Result<()> { + vm.close_process_stdin(pid).map_err(anyhow::Error::from) +} + +// --------------------------------------------------------------------------- +// Action reply DTOs +// --------------------------------------------------------------------------- + +/// Serializable mirror of [`ExecResult`] with camelCase `exitCode`. The +/// upstream type doesn't derive `Serialize`, and the field name is +/// `exit_code` (snake_case) which the JS test expects as `exitCode`. +#[derive(Serialize)] +pub struct ExecResultDto { + #[serde(rename = "exitCode")] + pub exit_code: i32, + pub stdout: String, + pub stderr: String, +} + +impl From for ExecResultDto { + fn from(value: ExecResult) -> Self { + Self { + exit_code: value.exit_code, + stdout: value.stdout, + stderr: value.stderr, + } + } +} diff --git a/rivetkit-rust/packages/rivetkit-agent-os/src/actions/session.rs b/rivetkit-rust/packages/rivetkit-agent-os/src/actions/session.rs new file mode 100644 index 0000000000..9eaae7cd56 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/src/actions/session.rs @@ -0,0 +1,435 @@ +//! Agent session actions: create an ACP agent session, send prompts, +//! and close it. Ports of [`AgentOs::create_session`] / `prompt` / +//! `close_session`. +//! +//! Session metadata is persisted to the actor's SQLite database +//! (`agent_os_sessions`, with streamed events in `agent_os_session_events`) +//! via `ctx.db_*`, so the set of sessions survives actor sleep/wake. The live +//! ACP session itself lives in the VM and is recreated on demand. + +use std::collections::BTreeMap; +use std::time::{SystemTime, UNIX_EPOCH}; + +use agent_os_client::{AgentOs, CreateSessionOptions, ResumeSessionOptions}; +use anyhow::{Result, anyhow}; +use futures::StreamExt; +use rivetkit::Ctx; +use serde::{Deserialize, Serialize}; +use serde_json::{Value as JsonValue, json}; + +use crate::actor::{AgentOsActor, Vars}; +use crate::persistence::{ + insert_session_event, query_rows, reconstruct_transcript_to_file, run_stmt, +}; + +/// Options object for `createSession(agentType, options?)`. +#[derive(Debug, Default, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateSessionOptionsDto { + #[serde(default)] + pub cwd: Option, + #[serde(default)] + pub env: BTreeMap, + #[serde(default)] + pub skip_os_instructions: bool, + #[serde(default)] + pub additional_instructions: Option, +} + +/// `{ sessionId }` returned by `createSession`. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionIdDto { + pub session_id: String, +} + +/// Result of `sendPrompt` exposed to the TS client. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PromptResultDto { + pub text: String, +} + +/// One row of `listPersistedSessions`. +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PersistedSessionDto { + pub session_id: String, + pub agent_type: String, + pub created_at: f64, +} + +fn now_ms() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as i64) + .unwrap_or(0) +} + +/// Subscribe to the live `session/update` stream for `live_session_id` and +/// spawn a task that persists each event under `external_session_id` (spec §5). +/// +/// The subscription is broadcast-backed, so aborting the spawned task — which +/// drops the stream — is the unsubscribe. The handle is tracked in +/// [`Vars::capture_tasks`] keyed by the live id so it can be cancelled on close +/// / sleep / destroy. Re-subscribing for the same live id first aborts any +/// existing pump so we never run two pumps for one session. +fn spawn_event_capture( + ctx: &Ctx, + vm: &AgentOs, + vars: &mut Vars, + external_session_id: &str, + live_session_id: &str, +) { + let (mut stream, subscription) = match vm.on_session_event(live_session_id) { + Ok(sub) => sub, + Err(error) => { + tracing::warn!(?error, live_session_id, "on_session_event subscribe failed"); + return; + } + }; + // Replace any existing pump for this live id. + if let Some(old) = vars.capture_tasks.remove(live_session_id) { + old.abort(); + } + let ctx = ctx.clone(); + let external = external_session_id.to_owned(); + let handle = tokio::spawn(async move { + // Keep the RAII guard alive for the lifetime of the pump; dropping the + // stream (on abort / channel close) is the unsubscribe. + let _subscription = subscription; + while let Some(notification) = stream.next().await { + let event_json = match serde_json::to_string(¬ification) { + Ok(json) => json, + Err(error) => { + tracing::warn!(?error, "failed to encode captured session event"); + continue; + } + }; + if let Err(error) = insert_session_event(&ctx, &external, &event_json).await { + tracing::warn!(?error, external, "failed to persist captured session event"); + } + } + }); + vars.capture_tasks + .insert(live_session_id.to_owned(), handle); +} + +pub async fn create_session( + ctx: &Ctx, + vm: &AgentOs, + vars: &mut Vars, + agent_type: &str, + dto: CreateSessionOptionsDto, +) -> Result { + // Capture cwd + env BEFORE they move into `options`, so the resume fallback + // `session/new` can rehydrate the same working dir + environment (spec §12b). + let persist_cwd = dto.cwd.clone(); + let persist_env = serde_json::to_string(&dto.env).ok(); + let options = CreateSessionOptions { + cwd: dto.cwd, + env: dto.env, + skip_os_instructions: dto.skip_os_instructions, + additional_instructions: dto.additional_instructions, + ..CreateSessionOptions::default() + }; + let session_id = vm.create_session(agent_type, options).await?.session_id; + // Persist session metadata so the set of sessions survives sleep/wake. Capture the REAL + // agent capabilities + info (not a `"{}"` placeholder) so the resume path can capability-gate + // the native `session/load` tier after a wake, when the live session is gone. See + // `resume_session` for how these are read back. + let capabilities = vm + .get_session_capabilities(&session_id) + .and_then(|caps| serde_json::to_string(&caps).ok()) + .unwrap_or_else(|| "{}".to_owned()); + let agent_info = vm + .get_session_agent_info(&session_id) + .and_then(|info| serde_json::to_string(&info).ok()); + run_stmt( + ctx, + "INSERT OR REPLACE INTO agent_os_sessions \ + (session_id, agent_type, capabilities, agent_info, created_at, cwd, env) \ + VALUES (?, ?, ?, ?, ?, ?, ?)", + &[ + json!(session_id), + json!(agent_type), + json!(capabilities), + agent_info.map(JsonValue::String).unwrap_or(JsonValue::Null), + json!(now_ms()), + persist_cwd + .map(JsonValue::String) + .unwrap_or(JsonValue::Null), + persist_env + .map(JsonValue::String) + .unwrap_or(JsonValue::Null), + ], + ) + .await?; + // At create time `external == live`; capture every `session/update` for this + // session under the external id (spec §3/§5). + spawn_event_capture(ctx, vm, vars, &session_id, &session_id); + Ok(SessionIdDto { session_id }) +} + +pub async fn send_prompt( + ctx: &Ctx, + vm: &AgentOs, + vars: &mut Vars, + session_id: &str, + text: &str, +) -> Result { + // Lazy-resume trigger (spec §8): a prompt for a session that is persisted in + // `agent_os_sessions` but absent from `Vars.live_sessions` means the VM was + // recreated since the session was last live — resume it before forwarding. + // `session_id` here is the client-facing `external_session_id`. + // + // Canonical resume state-machine documentation lives on the sidecar handler + // in `crates/agent-os-sidecar/src/acp_extension.rs` (spec §6); this is just + // the actor-side trigger that drives it. + if !vars.live_sessions.contains_key(session_id) && !is_session_live(vm, session_id) { + if session_is_persisted(ctx, session_id).await? { + resume_session(ctx, vm, vars, session_id).await?; + } + } + + // Record the outbound prompt text as a synthetic `user_prompt` event BEFORE + // the prompt streams, so the transcript turn ordering is correct (the prompt + // row precedes the agent `session/update` rows for this turn). Stored under + // the stable external id (spec §4/§5). + let prompt_event = json!({ + "method": "user_prompt", + "params": { "text": text }, + }); + if let Err(error) = insert_session_event(ctx, session_id, &prompt_event.to_string()).await { + tracing::warn!(?error, session_id, "failed to persist user_prompt event"); + } + + // Forward to the live id (== external for native/not-yet-resumed sessions). + let live_session_id = vars.live_id(session_id).to_owned(); + let result = vm.prompt(&live_session_id, text).await?; + Ok(PromptResultDto { text: result.text }) +} + +pub async fn close_session( + ctx: &Ctx, + vm: &AgentOs, + vars: &mut Vars, + session_id: &str, +) -> Result<()> { + // Stop event capture + drop the remap for this external session. + let live_session_id = vars.live_id(session_id).to_owned(); + if let Some(task) = vars.capture_tasks.remove(&live_session_id) { + task.abort(); + } + vars.live_sessions.remove(session_id); + let persisted = session_is_persisted(ctx, session_id).await?; + if is_session_live(vm, &live_session_id) { + vm.close_session(&live_session_id).map_err(|e| anyhow!(e))?; + } else if !persisted { + // Preserve the unknown-session error for callers that close something that + // is neither live nor durably persisted. + vm.close_session(&live_session_id).map_err(|e| anyhow!(e))?; + } + // Drop persisted metadata + events (explicit, since SQLite FK cascade is + // only enforced when `PRAGMA foreign_keys = ON`). + run_stmt( + ctx, + "DELETE FROM agent_os_session_events WHERE session_id = ?", + &[json!(session_id)], + ) + .await?; + run_stmt( + ctx, + "DELETE FROM agent_os_sessions WHERE session_id = ?", + &[json!(session_id)], + ) + .await?; + Ok(()) +} + +/// List the sessions persisted for this actor (`listPersistedSessions`). +pub async fn list_persisted_sessions(ctx: &Ctx) -> Result> { + let rows = query_rows( + ctx, + "SELECT session_id, agent_type, created_at FROM agent_os_sessions \ + ORDER BY created_at", + &[], + ) + .await?; + Ok(rows + .into_iter() + .map(|row| PersistedSessionDto { + session_id: row + .get("session_id") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_owned(), + agent_type: row + .get("agent_type") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_owned(), + created_at: row.get("created_at").and_then(|v| v.as_i64()).unwrap_or(0) as f64, + }) + .collect()) +} + +/// Return the persisted ACP events for a session, ordered by sequence +/// (`getSessionEvents`). Each event is the stored JSON-RPC notification. +pub async fn get_session_events( + ctx: &Ctx, + session_id: &str, +) -> Result> { + let rows = query_rows( + ctx, + "SELECT event FROM agent_os_session_events WHERE session_id = ? ORDER BY seq", + &[json!(session_id)], + ) + .await?; + Ok(rows + .into_iter() + .filter_map(|row| { + row.get("event") + .and_then(|v| v.as_str()) + .and_then(|raw| serde_json::from_str::(raw).ok()) + }) + .collect()) +} + +/// True when an ACP session with this id is currently live in the VM. +fn is_session_live(vm: &AgentOs, session_id: &str) -> bool { + vm.list_sessions() + .iter() + .any(|info| info.session_id == session_id) +} + +/// True when `external_session_id` has a persisted registry row in +/// `agent_os_sessions` (so it is resumable). +async fn session_is_persisted(ctx: &Ctx, external_session_id: &str) -> Result { + let rows = query_rows( + ctx, + "SELECT session_id FROM agent_os_sessions WHERE session_id = ? LIMIT 1", + &[json!(external_session_id)], + ) + .await?; + Ok(!rows.is_empty()) +} + +/// Persisted registry row needed to resume a session: agent type, parsed +/// capabilities (`{}` if absent), and the original create-time cwd + env. +struct SessionRegistryRow { + agent_type: String, + #[allow(dead_code)] + capabilities: JsonValue, + cwd: Option, + env: BTreeMap, +} + +/// Read the persisted registry row for a session (agent_type, capabilities, and +/// the create-time cwd + env) so resume can rehydrate it faithfully. +async fn read_session_registry( + ctx: &Ctx, + external_session_id: &str, +) -> Result { + let rows = query_rows( + ctx, + "SELECT agent_type, capabilities, cwd, env FROM agent_os_sessions WHERE session_id = ? LIMIT 1", + &[json!(external_session_id)], + ) + .await?; + let row = rows + .into_iter() + .next() + .ok_or_else(|| anyhow!("no persisted session {external_session_id} to resume"))?; + let agent_type = row + .get("agent_type") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_owned(); + let capabilities = row + .get("capabilities") + .and_then(|v| v.as_str()) + .and_then(|raw| serde_json::from_str::(raw).ok()) + .unwrap_or_else(|| json!({})); + let cwd = row + .get("cwd") + .and_then(|v| v.as_str()) + .map(|s| s.to_owned()); + let env = row + .get("env") + .and_then(|v| v.as_str()) + .and_then(|raw| serde_json::from_str::>(raw).ok()) + .unwrap_or_default(); + Ok(SessionRegistryRow { + agent_type, + capabilities, + cwd, + env, + }) +} + +/// Resume a persisted-but-not-live session in the freshly recreated VM +/// (spec §6/§8). Reads the registry caps, reconstructs the transcript file from +/// `agent_os_session_events`, calls the sidecar resume orchestration via the +/// client, records the `external -> live` remap, and starts event capture for +/// the live session. +/// +/// The canonical resume state machine (native `session/load`/`resume` tier with +/// the `unknown_session` fallthrough, then the universal `session/new` + +/// transcript-preamble fallback) lives on the sidecar handler in +/// `crates/agent-os-sidecar/src/acp_extension.rs` (spec §6). This actor function +/// only supplies the durable inputs (caps + transcript path) and records the +/// remap the sidecar returns. +pub async fn resume_session( + ctx: &Ctx, + vm: &AgentOs, + vars: &mut Vars, + external_session_id: &str, +) -> Result<()> { + let registry = read_session_registry(ctx, external_session_id).await?; + + // Disposable on-demand render of the canonical event log; handed to the + // sidecar so a fallback agent can read prior context with its file tools. + let transcript_path = reconstruct_transcript_to_file(ctx, external_session_id).await?; + + // Call the sidecar resume orchestration through the client. The contract is + // `AcpResumeSessionRequest { sessionId, agentType, transcriptPath?, cwd, env }` + // (spec §6); it returns the live session id (== external for the native tier, + // a new id for the `session/new` fallback) plus the tier that ran (`mode`). + // + // Rehydrate with the ORIGINAL create-time cwd + env (spec §12b) so the fallback + // `session/new` keeps the same working dir + environment instead of defaulting. + let result = vm + .resume_session( + external_session_id, + ®istry.agent_type, + ResumeSessionOptions { + transcript_path: Some(transcript_path), + cwd: registry.cwd, + env: registry.env, + }, + ) + .await?; + tracing::info!( + external_session_id, + live_session_id = %result.session_id, + mode = %result.mode, + "resumed persisted session" + ); + let live_session_id = result.session_id; + + // The remap lives SOLELY in the actor (spec §3): record external -> live so + // subsequent prompts route to the live session, and start event capture for the + // live session under the stable external id. + // + // NOTE on capture ordering (review SEV-3): we subscribe AFTER resume returns, on + // purpose. For the native `session/load` tier the agent replays prior history as + // `session/update` notifications DURING the load — those are already persisted in + // `agent_os_session_events` from the original session, so subscribing afterward + // correctly avoids RE-capturing (duplicating) the replay. For the `session/new` + // fallback there is no replay. New post-resume updates are captured normally. + vars.live_sessions + .insert(external_session_id.to_owned(), live_session_id.clone()); + spawn_event_capture(ctx, vm, vars, external_session_id, &live_session_id); + Ok(()) +} diff --git a/rivetkit-rust/packages/rivetkit-agent-os/src/actor.rs b/rivetkit-rust/packages/rivetkit-agent-os/src/actor.rs new file mode 100644 index 0000000000..93f61b0014 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/src/actor.rs @@ -0,0 +1,73 @@ +use std::collections::HashMap; + +use rivetkit::Actor; +use rivetkit::action::Raw; +use tokio::task::JoinHandle; + +/// Marker type implementing [`Actor`] for the agent-os actor. +/// +/// Actions are dispatched by name in [`crate::run::run`] using +/// `action.decode_as::<(...)>()` with per-arm tuple types matching the +/// underlying `AgentOs::*` method signatures (TS sends positional args +/// as a CBOR array which decodes cleanly into a Rust tuple). +#[derive(Debug)] +pub struct AgentOsActor; + +impl Actor for AgentOsActor { + type State = (); + type Input = (); + type Actions = (); + type Events = (); + type Queue = (); + type ConnParams = serde_json::Value; + type ConnState = (); + type Action = Raw; + + // Agent-os persists all of its state (filesystem, sessions, previews) to the + // actor's SQLite database via `ctx.sql()`, so the actor needs a database. + const HAS_DATABASE: bool = true; +} + +/// Ephemeral per-VM-lifetime actor state (session-resume, spec §3/§5/§8). +/// +/// Everything here is reconstructed on each wake from the durable SQLite tables +/// and the freshly created VM — it is intentionally NOT persisted: +/// +/// - `live_sessions` is the `external_session_id -> live_session_id` remap. It +/// lives SOLELY in the actor (the sidecar is stateless across VM lifetimes and +/// only ever knows live ids). For native `session/load` resume `external == +/// live`; for the fallback `session/new` tier the agent assigns a new id and +/// the actor records `external -> live` here. The client never sees `live`. +/// - `capture_tasks` holds the spawned `on_session_event` pump task per live +/// session, so capture can be cancelled on close / sleep / destroy. The +/// subscription is broadcast-backed, so aborting the task (which drops the +/// stream) is the unsubscribe. +#[derive(Default)] +pub struct Vars { + /// `external_session_id -> live_session_id`. + pub live_sessions: HashMap, + /// `live_session_id -> capture pump task`. + pub capture_tasks: HashMap>, +} + +impl Vars { + /// Resolve a client-facing `external_session_id` to the live ACP session id, + /// falling back to the external id itself (the native / not-yet-resumed case + /// where `external == live`). + pub fn live_id<'a>(&'a self, external_session_id: &'a str) -> &'a str { + self.live_sessions + .get(external_session_id) + .map(String::as_str) + .unwrap_or(external_session_id) + } + + /// Abort and clear all in-flight capture tasks. Called on VM teardown + /// (sleep / destroy / run-loop exit); the spawned task dropping its event + /// stream is the unsubscribe. + pub fn clear(&mut self) { + for (_, task) in self.capture_tasks.drain() { + task.abort(); + } + self.live_sessions.clear(); + } +} diff --git a/rivetkit-rust/packages/rivetkit-agent-os/src/config.rs b/rivetkit-rust/packages/rivetkit-agent-os/src/config.rs new file mode 100644 index 0000000000..7b44a18b5a --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/src/config.rs @@ -0,0 +1,40 @@ +use std::sync::Arc; + +use agent_os_client::AgentOsConfig; + +/// Configuration for the agent-os actor. +/// +/// `build_options` is a closure that yields a fresh [`AgentOsConfig`] +/// on every call. `AgentOsConfig` is non-`Clone` (it holds +/// `Arc` and other trait-object handles), and the +/// actor needs to rebuild it across sleep/wake cycles, so the config +/// is expressed as a factory rather than a value. +#[derive(Clone)] +pub struct AgentOsActorConfig { + build_options: Arc AgentOsConfig + Send + Sync>, +} + +impl AgentOsActorConfig { + /// Construct from a closure that builds a fresh [`AgentOsConfig`] + /// each time the actor needs to bring up a VM. + pub fn from_builder(builder: F) -> Self + where + F: Fn() -> AgentOsConfig + Send + Sync + 'static, + { + Self { + build_options: Arc::new(builder), + } + } + + /// Yield a fresh [`AgentOsConfig`] for VM bring-up. + pub fn build_options(&self) -> AgentOsConfig { + (self.build_options)() + } +} + +impl Default for AgentOsActorConfig { + /// Default config: every bring-up uses [`AgentOsConfig::default`]. + fn default() -> Self { + Self::from_builder(AgentOsConfig::default) + } +} diff --git a/rivetkit-rust/packages/rivetkit-agent-os/src/lib.rs b/rivetkit-rust/packages/rivetkit-agent-os/src/lib.rs new file mode 100644 index 0000000000..69af715c54 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/src/lib.rs @@ -0,0 +1,74 @@ +//! Rust-native actor wrapper around `agent-os-client`. +//! +//! Exposes a single `build_core_factory(config) -> CoreActorFactory` +//! consumed by the NAPI binding (`rivetkit-typescript/packages/rivetkit-napi`). +//! The factory's entry function drives the actor's event loop, brings up +//! an Agent OS VM lazily on first action, and tears it down on Sleep / +//! Destroy. + +pub mod actions; +pub mod actor; +pub mod config; +pub mod persistence; +pub mod run; + +use std::sync::Arc; +use std::time::Duration; + +use anyhow::Result; +use futures::future::BoxFuture; +use rivet_error::RivetError; +use rivetkit::start::wrap_start; +use rivetkit_core::{ActorConfig, ActorFactory as CoreActorFactory, ActorStart}; + +pub use actor::AgentOsActor; +pub use config::AgentOsActorConfig; + +/// Build a [`CoreActorFactory`] that runs the agent-os actor with the +/// given config. The factory's entry function captures the config via +/// `Arc` so multiple actor instances share the same builder. +pub fn build_core_factory(config: AgentOsActorConfig) -> CoreActorFactory { + let config = Arc::new(config); + let actor_config = ActorConfig { + has_database: true, + // Match the legacy TS actor's timeouts so long-running prompts + // and slow shutdowns don't get cut off prematurely. + sleep_grace_period: Duration::from_millis(900_000), + sleep_grace_period_overridden: true, + action_timeout: Duration::from_millis(900_000), + ..ActorConfig::default() + }; + CoreActorFactory::new_with_manual_startup_ready(actor_config, move |core_start: ActorStart| { + let config = config.clone(); + Box::pin(async move { + let mut core_start = core_start; + let startup_ready = core_start.startup_ready.take(); + match wrap_start::(core_start) { + Ok(start) => { + if let Some(reply) = startup_ready { + let _ = reply.send(Ok(())); + } + run::run(config, start).await + } + Err(error) => { + if let Some(reply) = startup_ready { + let startup_error = anyhow::Error::new(RivetError::extract(&error)); + let _ = reply.send(Err(startup_error)); + } + Err(error) + } + } + }) as BoxFuture<'static, Result<()>> + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn core_factory_enables_actor_database() { + let factory = build_core_factory(AgentOsActorConfig::default()); + assert!(factory.config().has_database); + } +} diff --git a/rivetkit-rust/packages/rivetkit-agent-os/src/persistence.rs b/rivetkit-rust/packages/rivetkit-agent-os/src/persistence.rs new file mode 100644 index 0000000000..b246218aea --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/src/persistence.rs @@ -0,0 +1,1224 @@ +//! Agent-os SQLite persistence: schema + idempotent migration. +//! +//! The agent-os actor persists all of its durable state — signed preview +//! tokens, sessions, session events, and (eventually) the filesystem — to its +//! per-actor SQLite database via `ctx.sql()` / `ctx.db_*`. There is no actor-KV +//! state for agent-os; SQLite is the single source of truth. The schema is +//! ported from the (deleted) TS `agent-os/actor/db.ts` `migrateAgentOsTables`. + +use std::io::Cursor; +use std::time::{SystemTime, UNIX_EPOCH}; + +use agent_os_client::SidecarJsBridgeCall; +use anyhow::{Result, anyhow, bail}; +use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; +use rivetkit::Ctx; +use serde_json::{Map as JsonMap, Value as JsonValue, json}; + +use crate::actor::AgentOsActor; + +/// All agent-os persistence tables + indexes. Every statement is +/// `IF NOT EXISTS`, so this is idempotent and safe to run on every actor start. +pub const MIGRATION_SQL: &str = "\ +CREATE TABLE IF NOT EXISTS agent_os_preview_tokens ( + token TEXT PRIMARY KEY, + port INTEGER NOT NULL, + created_at INTEGER NOT NULL, + expires_at INTEGER NOT NULL +); +CREATE INDEX IF NOT EXISTS idx_preview_tokens_expires_at + ON agent_os_preview_tokens(expires_at); +CREATE TABLE IF NOT EXISTS agent_os_fs_entries ( + path TEXT PRIMARY KEY, + is_directory INTEGER NOT NULL DEFAULT 0, + content BLOB, + mode INTEGER NOT NULL DEFAULT 33188, + uid INTEGER NOT NULL DEFAULT 0, + gid INTEGER NOT NULL DEFAULT 0, + size INTEGER NOT NULL DEFAULT 0, + atime_ms INTEGER NOT NULL, + mtime_ms INTEGER NOT NULL, + ctime_ms INTEGER NOT NULL, + birthtime_ms INTEGER NOT NULL, + symlink_target TEXT, + nlink INTEGER NOT NULL DEFAULT 1 +); +CREATE INDEX IF NOT EXISTS idx_fs_entries_parent + ON agent_os_fs_entries(path); +CREATE TABLE IF NOT EXISTS agent_os_sessions ( + session_id TEXT PRIMARY KEY, + agent_type TEXT NOT NULL, + capabilities TEXT NOT NULL, + agent_info TEXT, + created_at INTEGER NOT NULL, + -- Original create-time cwd + env (env as a JSON object), threaded into the + -- fallback `session/new` at resume so the rehydrated session keeps the same + -- working dir + environment instead of defaulting (spec §12b item 3). + cwd TEXT, + env TEXT +); +CREATE TABLE IF NOT EXISTS agent_os_session_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id TEXT NOT NULL, + seq INTEGER NOT NULL, + event TEXT NOT NULL, + created_at INTEGER NOT NULL, + FOREIGN KEY (session_id) REFERENCES agent_os_sessions(session_id) ON DELETE CASCADE +); +CREATE INDEX IF NOT EXISTS idx_session_events_session_seq + ON agent_os_session_events(session_id, seq); +"; + +/// Run the agent-os schema migration against the actor's SQLite database. +/// Idempotent; intended to be called once at the top of the actor run loop. +pub async fn migrate_actor(ctx: &Ctx) -> Result<()> { + ctx.db_exec(MIGRATION_SQL).await?; + Ok(()) +} + +// --------------------------------------------------------------------------- +// SQLite helpers shared by the persistence-backed actions (previews, sessions). +// +// The actor `ctx.db_*` API takes parameters as a CBOR-encoded JSON array and +// returns rows as CBOR-encoded JSON objects (column name -> value). +// --------------------------------------------------------------------------- + +/// Encode positional bind params as the CBOR JSON array the `db_*` API expects. +fn cbor_params(values: &[JsonValue]) -> Result> { + let mut buf = Vec::new(); + ciborium::into_writer(&JsonValue::Array(values.to_vec()), &mut buf)?; + Ok(buf) +} + +/// Decode a `db_query` CBOR result into object rows (column -> value). +fn decode_rows(bytes: &[u8]) -> Result>> { + if bytes.is_empty() { + return Ok(Vec::new()); + } + let value: JsonValue = ciborium::from_reader(Cursor::new(bytes))?; + Ok(match value { + JsonValue::Array(rows) => rows + .into_iter() + .filter_map(|row| match row { + JsonValue::Object(map) => Some(map), + _ => None, + }) + .collect(), + _ => Vec::new(), + }) +} + +/// Run a parameterized query and return the decoded object rows. +pub(crate) async fn query_rows( + ctx: &Ctx, + sql: &str, + params: &[JsonValue], +) -> Result>> { + let encoded = cbor_params(params)?; + let bytes = ctx.db_query(sql, Some(encoded.as_slice())).await?; + decode_rows(&bytes) +} + +/// Run a parameterized statement that returns no rows (INSERT/UPDATE/DELETE). +pub(crate) async fn run_stmt( + ctx: &Ctx, + sql: &str, + params: &[JsonValue], +) -> Result<()> { + let encoded = cbor_params(params)?; + ctx.db_run(sql, Some(encoded.as_slice())).await?; + Ok(()) +} + +const SQLITE_VFS_MOUNT_ID: &str = "rivetkit-agent-os-root"; +const DEFAULT_FILE_MODE: i64 = 0o100644; +const DEFAULT_DIR_MODE: i64 = 0o040755; +const DEFAULT_SYMLINK_MODE: i64 = 0o120777; + +// --------------------------------------------------------------------------- +// Session-event capture + transcript reconstruction (session-resume, spec §5/§7). +// +// `agent_os_session_events` is the canonical append-only conversation log, +// keyed by `external_session_id`. `seq` is INTERNAL ordering only: it is never +// surfaced to a client as a cursor/recovery state, so the "ACP session events +// are live-only" invariant holds — this is consumer-side durable persistence, +// not a replay buffer on the live `onSessionEvent` API. +// --------------------------------------------------------------------------- + +/// Append one captured event to `agent_os_session_events` under the stable +/// `external_session_id`, allocating the next per-session `seq` (`MAX(seq)+1`). +/// +/// `event_json` is the raw JSON text of either an inbound ACP `session/update` +/// notification (captured via `on_session_event`) or a synthetic outbound +/// `user_prompt` event recorded in `send_prompt` before the prompt streams. +pub async fn insert_session_event( + ctx: &Ctx, + external_session_id: &str, + event_json: &str, +) -> Result<()> { + // Allocate the next per-session `seq` ATOMICALLY inside the INSERT. The capture + // pump (a spawned tokio task) and the prompt action both call this and run + // concurrently on the runtime, so a separate `SELECT MAX(seq)` followed by an + // INSERT would race: both reads could observe the same max and write a duplicate + // `seq` (there is no UNIQUE(session_id, seq) constraint). SQLite serializes + // writers, so computing `MAX(seq)+1` in a sub-SELECT within the INSERT is atomic + // against other inserts and cannot duplicate. `MAX(seq)` over an empty set is + // SQL NULL → `COALESCE(..., 0)` gives the first event seq 0. `seq` is internal + // ordering only, never a client-facing cursor (live-only invariant). + run_stmt( + ctx, + "INSERT INTO agent_os_session_events (session_id, seq, event, created_at) \ + SELECT ?, \ + COALESCE((SELECT MAX(seq) + 1 FROM agent_os_session_events WHERE session_id = ?), 0), \ + ?, ?", + &[ + json!(external_session_id), + json!(external_session_id), + json!(event_json), + json!(now_ms()), + ], + ) + .await +} + +/// Render the persisted event log for `external_session_id` to a Markdown +/// transcript and write it to a guest-readable path via the VM filesystem +/// callback, returning that path (spec §7). +/// +/// The file is a disposable on-demand render of the canonical +/// `agent_os_session_events` rows: it is overwritten fresh each resume and is +/// **idempotent** (two reconstructs of the same rows produce identical bytes, +/// no append-duplication). The path is handed to the sidecar resume request so +/// a fallback agent can read prior context with its file tools. +pub async fn reconstruct_transcript_to_file( + ctx: &Ctx, + external_session_id: &str, +) -> Result { + let rows = query_rows( + ctx, + "SELECT event FROM agent_os_session_events WHERE session_id = ? ORDER BY seq", + &[json!(external_session_id)], + ) + .await?; + let events: Vec = rows + .into_iter() + .filter_map(|mut row| { + row.remove("event") + .and_then(|v| match v { + JsonValue::String(raw) => Some(raw), + _ => None, + }) + .and_then(|raw| serde_json::from_str::(&raw).ok()) + }) + .collect(); + + let markdown = render_transcript_markdown(external_session_id, &events); + + let path = format!("/root/.agentos/threads/{external_session_id}.md"); + // Ensure the parent directory chain exists (mkdir -p), then overwrite the + // transcript fresh. Both go through the same sqlite_vfs callback the sidecar + // uses, so the bytes are visible to the guest agent. + create_dir(ctx, "/root/.agentos/threads", DEFAULT_DIR_MODE, true).await?; + // The callback stores base64 file content (see `handle_sqlite_vfs_call`). + write_file(ctx, &path, BASE64.encode(markdown), DEFAULT_FILE_MODE).await?; + Ok(path) +} + +/// Render captured ACP events to a role-labeled Markdown transcript. Pure / +/// deterministic so reconstruction is idempotent. +fn render_transcript_markdown(external_session_id: &str, events: &[JsonValue]) -> String { + let mut out = String::new(); + out.push_str(&format!("# Session transcript: {external_session_id}\n")); + + for event in events { + // Synthetic outbound prompt event (recorded in `send_prompt`). + if event.get("method").and_then(JsonValue::as_str) == Some("user_prompt") { + if let Some(text) = event + .get("params") + .and_then(|p| p.get("text")) + .and_then(JsonValue::as_str) + { + out.push_str(&format!("\n## User\n\n{text}\n")); + } + continue; + } + + // Inbound `session/update` notifications: the conversation content a + // transcript needs (`update.sessionUpdate` discriminator). + let Some(update) = event.get("params").and_then(|p| p.get("update")) else { + continue; + }; + let kind = update + .get("sessionUpdate") + .and_then(JsonValue::as_str) + .unwrap_or(""); + match kind { + "agent_message_chunk" | "agent_thought_chunk" => { + if let Some(text) = update + .get("content") + .and_then(|c| c.get("text")) + .and_then(JsonValue::as_str) + { + if kind == "agent_thought_chunk" { + out.push_str(&format!("\n## Assistant (thinking)\n\n{text}\n")); + } else { + out.push_str(&format!("\n## Assistant\n\n{text}\n")); + } + } + } + "tool_call" | "tool_call_update" => { + let title = update + .get("title") + .and_then(JsonValue::as_str) + .or_else(|| update.get("kind").and_then(JsonValue::as_str)) + .unwrap_or("tool call"); + let status = update + .get("status") + .and_then(JsonValue::as_str) + .unwrap_or(""); + out.push_str(&format!("\n### Tool call: {title}")); + if !status.is_empty() { + out.push_str(&format!(" ({status})")); + } + out.push('\n'); + // Render any textual tool output content. + if let Some(content) = update.get("content").and_then(JsonValue::as_array) { + for item in content { + if let Some(text) = item + .get("content") + .and_then(|c| c.get("text")) + .and_then(JsonValue::as_str) + .or_else(|| item.get("text").and_then(JsonValue::as_str)) + { + out.push_str(&format!("\n```\n{text}\n```\n")); + } + } + } + } + _ => {} + } + } + + out +} + +/// Native sqlite_vfs callback used by the Agent OS sidecar when the root is +/// backed by Rivet actor SQLite. The sidecar speaks base64 at the bridge +/// boundary; this actor stores that base64 text in `agent_os_fs_entries.content` +/// because the current `ctx.db_*` binder cannot bind raw BLOB parameters. +pub async fn handle_sqlite_vfs_call( + ctx: &Ctx, + call: SidecarJsBridgeCall, +) -> std::result::Result, String> { + if call.mount_id != SQLITE_VFS_MOUNT_ID { + return Err(format!("ENOENT unknown sqlite_vfs mount {}", call.mount_id)); + } + + handle_sqlite_vfs_call_inner(ctx, call) + .await + .map_err(|error| error.to_string()) +} + +async fn handle_sqlite_vfs_call_inner( + ctx: &Ctx, + call: SidecarJsBridgeCall, +) -> Result> { + ensure_fs_root(ctx).await?; + let args = call.args; + match call.operation.as_str() { + "readFile" => Ok(Some(json!(read_file(ctx, required_path(&args)?).await?))), + "writeFile" => { + write_file( + ctx, + required_path(&args)?, + required_string(&args, "content")?, + optional_i64(&args, "mode").unwrap_or(DEFAULT_FILE_MODE), + ) + .await?; + Ok(None) + } + "createFileExclusive" => { + create_file_exclusive( + ctx, + required_path(&args)?, + required_string(&args, "content")?, + optional_i64(&args, "mode").unwrap_or(DEFAULT_FILE_MODE), + ) + .await?; + Ok(None) + } + "readDir" => Ok(Some(JsonValue::Array( + read_dir(ctx, required_path(&args)?) + .await? + .into_iter() + .map(JsonValue::String) + .collect(), + ))), + "readDirWithTypes" => Ok(Some(JsonValue::Array( + read_dir_entries(ctx, required_path(&args)?) + .await? + .into_iter() + .map(|entry| { + json!({ + "name": entry.name, + "isDirectory": entry.is_directory, + "isSymbolicLink": entry.symlink_target.is_some(), + }) + }) + .collect(), + ))), + "createDir" => { + create_dir( + ctx, + required_path(&args)?, + optional_i64(&args, "mode").unwrap_or(DEFAULT_DIR_MODE), + false, + ) + .await?; + Ok(None) + } + "mkdir" => { + let recursive = args + .get("recursive") + .and_then(JsonValue::as_bool) + .unwrap_or(false); + create_dir( + ctx, + required_path(&args)?, + optional_i64(&args, "mode").unwrap_or(DEFAULT_DIR_MODE), + recursive, + ) + .await?; + Ok(None) + } + "exists" => Ok(Some(json!( + lookup_entry(ctx, required_path(&args)?).await?.is_some() + ))), + "access" => { + lookup_entry_required(ctx, required_path(&args)?).await?; + Ok(None) + } + "stat" => Ok(Some(stat_json( + lookup_entry_required(ctx, required_path(&args)?).await?, + ))), + "lstat" => Ok(Some(stat_json( + lookup_entry_required(ctx, required_path(&args)?).await?, + ))), + "open" => Ok(Some(stat_json( + lookup_entry_required(ctx, required_path(&args)?).await?, + ))), + "removeFile" => { + remove_file(ctx, required_path(&args)?).await?; + Ok(None) + } + "removeDir" => { + remove_dir(ctx, required_path(&args)?).await?; + Ok(None) + } + "rename" => { + rename_entry( + ctx, + required_string(&args, "oldPath")?, + required_string(&args, "newPath")?, + ) + .await?; + Ok(None) + } + "realpath" => Ok(Some(json!(normalize_path(required_path(&args)?)?))), + "symlink" => { + symlink_entry( + ctx, + required_string(&args, "target")?, + required_string(&args, "path")?, + ) + .await?; + Ok(None) + } + "readLink" => Ok(Some(json!(read_link(ctx, required_path(&args)?).await?))), + "link" => { + link_entry( + ctx, + required_string(&args, "oldPath")?, + required_string(&args, "newPath")?, + ) + .await?; + Ok(None) + } + "chmod" => { + update_one_field( + ctx, + required_path(&args)?, + "mode", + json!(required_i64(&args, "mode")?), + ) + .await?; + Ok(None) + } + "chown" => { + update_owner( + ctx, + required_path(&args)?, + required_i64(&args, "uid")?, + required_i64(&args, "gid")?, + ) + .await?; + Ok(None) + } + "utimes" => { + update_times( + ctx, + required_path(&args)?, + required_i64(&args, "atimeMs")?, + required_i64(&args, "mtimeMs")?, + ) + .await?; + Ok(None) + } + "truncate" => { + truncate_file(ctx, required_path(&args)?, required_len(&args)?).await?; + Ok(None) + } + "pread" => Ok(Some(json!( + pread_file( + ctx, + required_path(&args)?, + required_i64(&args, "offset")?, + required_len(&args)?, + ) + .await? + ))), + operation => bail!("ENOSYS unsupported sqlite_vfs operation {operation}"), + } +} + +#[derive(Clone, Debug)] +struct FsEntry { + path: String, + name: String, + is_directory: bool, + content: Option, + mode: i64, + uid: i64, + gid: i64, + size: i64, + atime_ms: i64, + mtime_ms: i64, + ctime_ms: i64, + birthtime_ms: i64, + symlink_target: Option, + nlink: i64, +} + +impl FsEntry { + fn from_row(mut row: JsonMap) -> Result { + let path = string_col(&mut row, "path")?; + Ok(Self { + name: basename(&path), + path, + is_directory: int_col(&mut row, "is_directory")? != 0, + content: optional_content_col(&mut row, "content")?, + mode: int_col(&mut row, "mode")?, + uid: int_col(&mut row, "uid")?, + gid: int_col(&mut row, "gid")?, + size: int_col(&mut row, "size")?, + atime_ms: int_col(&mut row, "atime_ms")?, + mtime_ms: int_col(&mut row, "mtime_ms")?, + ctime_ms: int_col(&mut row, "ctime_ms")?, + birthtime_ms: int_col(&mut row, "birthtime_ms")?, + symlink_target: optional_string_col(&mut row, "symlink_target")?, + nlink: int_col(&mut row, "nlink")?, + }) + } +} + +fn now_ms() -> i64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64 +} + +async fn ensure_fs_root(ctx: &Ctx) -> Result<()> { + let now = now_ms(); + run_stmt( + ctx, + "INSERT OR IGNORE INTO agent_os_fs_entries + (path, is_directory, content, mode, uid, gid, size, atime_ms, mtime_ms, ctime_ms, birthtime_ms, symlink_target, nlink) + VALUES (?, 1, NULL, ?, 0, 0, 0, ?, ?, ?, ?, NULL, 2)", + &[ + json!("/"), + json!(DEFAULT_DIR_MODE), + json!(now), + json!(now), + json!(now), + json!(now), + ], + ) + .await +} + +async fn lookup_entry(ctx: &Ctx, path: &str) -> Result> { + let path = normalize_path(path)?; + let rows = query_rows( + ctx, + "SELECT path, is_directory, content, mode, uid, gid, size, atime_ms, mtime_ms, ctime_ms, birthtime_ms, symlink_target, nlink + FROM agent_os_fs_entries WHERE path = ?", + &[json!(path)], + ) + .await?; + rows.into_iter().next().map(FsEntry::from_row).transpose() +} + +async fn lookup_entry_required(ctx: &Ctx, path: &str) -> Result { + lookup_entry(ctx, path) + .await? + .ok_or_else(|| anyhow!("ENOENT no such file or directory: {}", path)) +} + +async fn ensure_parent_dir(ctx: &Ctx, path: &str) -> Result<()> { + let Some(parent) = parent_path(path) else { + return Ok(()); + }; + let parent = lookup_entry_required(ctx, &parent).await?; + if !parent.is_directory { + bail!("ENOTDIR parent is not a directory: {}", parent.path); + } + Ok(()) +} + +async fn read_file(ctx: &Ctx, path: &str) -> Result { + let entry = lookup_entry_required(ctx, path).await?; + if entry.is_directory { + bail!("EISDIR is a directory: {}", entry.path); + } + Ok(entry.content.unwrap_or_default()) +} + +async fn write_file(ctx: &Ctx, path: &str, content: String, mode: i64) -> Result<()> { + let path = normalize_path(path)?; + ensure_parent_dir(ctx, &path).await?; + let size = decoded_len(&content)?; + let now = now_ms(); + if let Some(existing) = lookup_entry(ctx, &path).await? { + if existing.is_directory { + bail!("EISDIR is a directory: {path}"); + } + run_stmt( + ctx, + "UPDATE agent_os_fs_entries + SET is_directory = 0, content = ?, mode = ?, size = ?, mtime_ms = ?, ctime_ms = ?, symlink_target = NULL, nlink = 1 + WHERE path = ?", + &[ + json!(content), + json!(mode), + json!(size), + json!(now), + json!(now), + json!(path), + ], + ) + .await?; + return Ok(()); + } + insert_entry(ctx, &path, false, Some(content), mode, size, None, 1, now).await +} + +async fn create_file_exclusive( + ctx: &Ctx, + path: &str, + content: String, + mode: i64, +) -> Result<()> { + let path = normalize_path(path)?; + if lookup_entry(ctx, &path).await?.is_some() { + bail!("EEXIST file exists: {path}"); + } + ensure_parent_dir(ctx, &path).await?; + let size = decoded_len(&content)?; + insert_entry( + ctx, + &path, + false, + Some(content), + mode, + size, + None, + 1, + now_ms(), + ) + .await +} + +async fn create_dir(ctx: &Ctx, path: &str, mode: i64, recursive: bool) -> Result<()> { + let path = normalize_path(path)?; + if path == "/" { + return Ok(()); + } + if let Some(existing) = lookup_entry(ctx, &path).await? { + if recursive && existing.is_directory { + return Ok(()); + } + bail!("EEXIST file exists: {path}"); + } + if recursive { + let mut parents = Vec::new(); + let mut cursor = parent_path(&path); + while let Some(parent) = cursor { + if parent == "/" { + break; + } + parents.push(parent.clone()); + cursor = parent_path(&parent); + } + parents.reverse(); + for parent in parents { + if let Some(existing) = lookup_entry(ctx, &parent).await? { + if !existing.is_directory { + bail!("ENOTDIR parent is not a directory: {}", existing.path); + } + continue; + } + insert_entry(ctx, &parent, true, None, mode, 0, None, 2, now_ms()).await?; + } + } else { + ensure_parent_dir(ctx, &path).await?; + } + insert_entry(ctx, &path, true, None, mode, 0, None, 2, now_ms()).await +} + +async fn read_dir(ctx: &Ctx, path: &str) -> Result> { + Ok(read_dir_entries(ctx, path) + .await? + .into_iter() + .map(|entry| entry.name) + .collect()) +} + +async fn read_dir_entries(ctx: &Ctx, path: &str) -> Result> { + let path = normalize_path(path)?; + let entry = lookup_entry_required(ctx, &path).await?; + if !entry.is_directory { + bail!("ENOTDIR not a directory: {path}"); + } + let prefix = if path == "/" { + "/".to_owned() + } else { + format!("{path}/") + }; + let rows = query_rows( + ctx, + "SELECT path, is_directory, content, mode, uid, gid, size, atime_ms, mtime_ms, ctime_ms, birthtime_ms, symlink_target, nlink + FROM agent_os_fs_entries WHERE path LIKE ? AND path != ? ORDER BY path", + &[json!(format!("{prefix}%")), json!(path)], + ) + .await?; + rows.into_iter() + .map(FsEntry::from_row) + .filter_map(|entry| match entry { + Ok(entry) if parent_path(&entry.path).as_deref() == Some(path.as_str()) => { + Some(Ok(entry)) + } + Ok(_) => None, + Err(error) => Some(Err(error)), + }) + .collect() +} + +async fn remove_file(ctx: &Ctx, path: &str) -> Result<()> { + let entry = lookup_entry_required(ctx, path).await?; + if entry.is_directory { + bail!("EISDIR is a directory: {}", entry.path); + } + run_stmt( + ctx, + "DELETE FROM agent_os_fs_entries WHERE path = ?", + &[json!(entry.path)], + ) + .await +} + +async fn remove_dir(ctx: &Ctx, path: &str) -> Result<()> { + let entry = lookup_entry_required(ctx, path).await?; + if !entry.is_directory { + bail!("ENOTDIR not a directory: {}", entry.path); + } + if entry.path == "/" { + bail!("EBUSY cannot remove root directory"); + } + if !read_dir_entries(ctx, &entry.path).await?.is_empty() { + bail!("ENOTEMPTY directory not empty: {}", entry.path); + } + run_stmt( + ctx, + "DELETE FROM agent_os_fs_entries WHERE path = ?", + &[json!(entry.path)], + ) + .await +} + +async fn rename_entry(ctx: &Ctx, old_path: String, new_path: String) -> Result<()> { + let old_path = normalize_path(&old_path)?; + let new_path = normalize_path(&new_path)?; + if old_path == "/" { + bail!("EBUSY cannot rename root directory"); + } + let entry = lookup_entry_required(ctx, &old_path).await?; + ensure_parent_dir(ctx, &new_path).await?; + if entry.is_directory && new_path.starts_with(&format!("{old_path}/")) { + bail!("EINVAL cannot move directory into itself"); + } + if let Some(existing) = lookup_entry(ctx, &new_path).await? { + if existing.is_directory && !read_dir_entries(ctx, &existing.path).await?.is_empty() { + bail!("ENOTEMPTY target directory not empty: {}", existing.path); + } + run_stmt( + ctx, + "DELETE FROM agent_os_fs_entries WHERE path = ?", + &[json!(existing.path)], + ) + .await?; + } + let old_prefix = format!("{old_path}/"); + let new_prefix = format!("{new_path}/"); + let rows = query_rows( + ctx, + "SELECT path FROM agent_os_fs_entries WHERE path = ? OR path LIKE ? ORDER BY path", + &[json!(old_path), json!(format!("{old_prefix}%"))], + ) + .await?; + for row in rows { + let path = row + .get("path") + .and_then(JsonValue::as_str) + .ok_or_else(|| anyhow!("sqlite_vfs rename row missing path"))?; + let next_path = if path == old_path { + new_path.clone() + } else { + format!("{new_prefix}{}", &path[old_prefix.len()..]) + }; + run_stmt( + ctx, + "UPDATE agent_os_fs_entries SET path = ?, ctime_ms = ? WHERE path = ?", + &[json!(next_path), json!(now_ms()), json!(path)], + ) + .await?; + } + Ok(()) +} + +async fn symlink_entry(ctx: &Ctx, target: String, path: String) -> Result<()> { + let path = normalize_path(&path)?; + if lookup_entry(ctx, &path).await?.is_some() { + bail!("EEXIST file exists: {path}"); + } + ensure_parent_dir(ctx, &path).await?; + insert_entry( + ctx, + &path, + false, + None, + DEFAULT_SYMLINK_MODE, + target.len() as i64, + Some(target), + 1, + now_ms(), + ) + .await +} + +async fn read_link(ctx: &Ctx, path: &str) -> Result { + let entry = lookup_entry_required(ctx, path).await?; + entry + .symlink_target + .ok_or_else(|| anyhow!("EINVAL not a symbolic link: {}", entry.path)) +} + +async fn link_entry(ctx: &Ctx, old_path: String, new_path: String) -> Result<()> { + let old_path = normalize_path(&old_path)?; + let new_path = normalize_path(&new_path)?; + if lookup_entry(ctx, &new_path).await?.is_some() { + bail!("EEXIST file exists: {new_path}"); + } + ensure_parent_dir(ctx, &new_path).await?; + let entry = lookup_entry_required(ctx, &old_path).await?; + if entry.is_directory { + bail!("EPERM cannot hard-link directory: {old_path}"); + } + insert_entry( + ctx, + &new_path, + false, + entry.content, + entry.mode, + entry.size, + entry.symlink_target, + 1, + now_ms(), + ) + .await?; + update_one_field(ctx, &old_path, "nlink", json!(entry.nlink + 1)).await +} + +async fn update_owner(ctx: &Ctx, path: &str, uid: i64, gid: i64) -> Result<()> { + let path = normalize_path(path)?; + lookup_entry_required(ctx, &path).await?; + run_stmt( + ctx, + "UPDATE agent_os_fs_entries SET uid = ?, gid = ?, ctime_ms = ? WHERE path = ?", + &[json!(uid), json!(gid), json!(now_ms()), json!(path)], + ) + .await +} + +async fn update_times( + ctx: &Ctx, + path: &str, + atime_ms: i64, + mtime_ms: i64, +) -> Result<()> { + let path = normalize_path(path)?; + lookup_entry_required(ctx, &path).await?; + run_stmt( + ctx, + "UPDATE agent_os_fs_entries SET atime_ms = ?, mtime_ms = ?, ctime_ms = ? WHERE path = ?", + &[ + json!(atime_ms), + json!(mtime_ms), + json!(now_ms()), + json!(path), + ], + ) + .await +} + +async fn truncate_file(ctx: &Ctx, path: &str, len: i64) -> Result<()> { + if len < 0 { + bail!("EINVAL negative truncate length"); + } + let entry = lookup_entry_required(ctx, path).await?; + if entry.is_directory { + bail!("EISDIR is a directory: {}", entry.path); + } + let mut bytes = decode_content(entry.content.as_deref().unwrap_or_default())?; + bytes.resize(len as usize, 0); + let content = BASE64.encode(bytes); + run_stmt( + ctx, + "UPDATE agent_os_fs_entries SET content = ?, size = ?, mtime_ms = ?, ctime_ms = ? WHERE path = ?", + &[ + json!(content), + json!(len), + json!(now_ms()), + json!(now_ms()), + json!(entry.path), + ], + ) + .await +} + +async fn pread_file(ctx: &Ctx, path: &str, offset: i64, len: i64) -> Result { + if offset < 0 || len < 0 { + bail!("EINVAL negative pread offset or length"); + } + let entry = lookup_entry_required(ctx, path).await?; + if entry.is_directory { + bail!("EISDIR is a directory: {}", entry.path); + } + let bytes = decode_content(entry.content.as_deref().unwrap_or_default())?; + let start = (offset as usize).min(bytes.len()); + let end = start.saturating_add(len as usize).min(bytes.len()); + Ok(BASE64.encode(&bytes[start..end])) +} + +async fn update_one_field( + ctx: &Ctx, + path: &str, + field: &str, + value: JsonValue, +) -> Result<()> { + let path = normalize_path(path)?; + lookup_entry_required(ctx, &path).await?; + let sql = match field { + "mode" => "UPDATE agent_os_fs_entries SET mode = ?, ctime_ms = ? WHERE path = ?", + "nlink" => "UPDATE agent_os_fs_entries SET nlink = ?, ctime_ms = ? WHERE path = ?", + _ => bail!("EINVAL unsupported update field {field}"), + }; + run_stmt(ctx, sql, &[value, json!(now_ms()), json!(path)]).await +} + +async fn insert_entry( + ctx: &Ctx, + path: &str, + is_directory: bool, + content: Option, + mode: i64, + size: i64, + symlink_target: Option, + nlink: i64, + now: i64, +) -> Result<()> { + run_stmt( + ctx, + "INSERT INTO agent_os_fs_entries + (path, is_directory, content, mode, uid, gid, size, atime_ms, mtime_ms, ctime_ms, birthtime_ms, symlink_target, nlink) + VALUES (?, ?, ?, ?, 0, 0, ?, ?, ?, ?, ?, ?, ?)", + &[ + json!(path), + json!(if is_directory { 1 } else { 0 }), + content.map_or(JsonValue::Null, JsonValue::String), + json!(mode), + json!(size), + json!(now), + json!(now), + json!(now), + json!(now), + symlink_target.map_or(JsonValue::Null, JsonValue::String), + json!(nlink), + ], + ) + .await +} + +fn stat_json(entry: FsEntry) -> JsonValue { + json!({ + "dev": 0, + "ino": stable_ino(&entry.path), + "mode": entry.mode, + "nlink": entry.nlink, + "uid": entry.uid, + "gid": entry.gid, + "rdev": 0, + "size": entry.size, + "blocks": if entry.size == 0 { 0 } else { (entry.size + 511) / 512 }, + "atimeMs": entry.atime_ms, + "mtimeMs": entry.mtime_ms, + "ctimeMs": entry.ctime_ms, + "birthtimeMs": entry.birthtime_ms, + "atimeNsec": (entry.atime_ms % 1000) * 1_000_000, + "mtimeNsec": (entry.mtime_ms % 1000) * 1_000_000, + "ctimeNsec": (entry.ctime_ms % 1000) * 1_000_000, + "birthtimeNsec": (entry.birthtime_ms % 1000) * 1_000_000, + "isDirectory": entry.is_directory, + "isSymbolicLink": entry.symlink_target.is_some(), + }) +} + +fn normalize_path(path: &str) -> Result { + if path.is_empty() { + bail!("ENOENT empty path"); + } + let mut parts = Vec::new(); + for part in path.split('/') { + if part.is_empty() || part == "." { + continue; + } + if part == ".." { + parts.pop(); + continue; + } + parts.push(part); + } + if parts.is_empty() { + Ok("/".to_owned()) + } else { + Ok(format!("/{}", parts.join("/"))) + } +} + +fn parent_path(path: &str) -> Option { + if path == "/" { + return None; + } + let path = path.trim_end_matches('/'); + let index = path.rfind('/')?; + if index == 0 { + Some("/".to_owned()) + } else { + Some(path[..index].to_owned()) + } +} + +fn basename(path: &str) -> String { + if path == "/" { + return "/".to_owned(); + } + path.rsplit('/').next().unwrap_or(path).to_owned() +} + +fn required_path(args: &JsonValue) -> Result<&str> { + required_string_ref(args, "path") +} + +fn required_string(args: &JsonValue, key: &str) -> Result { + Ok(required_string_ref(args, key)?.to_owned()) +} + +fn required_string_ref<'a>(args: &'a JsonValue, key: &str) -> Result<&'a str> { + args.get(key) + .and_then(JsonValue::as_str) + .ok_or_else(|| anyhow!("EINVAL missing string arg {key}")) +} + +fn optional_i64(args: &JsonValue, key: &str) -> Option { + args.get(key).and_then(JsonValue::as_i64) +} + +fn required_i64(args: &JsonValue, key: &str) -> Result { + optional_i64(args, key).ok_or_else(|| anyhow!("EINVAL missing integer arg {key}")) +} + +fn required_len(args: &JsonValue) -> Result { + optional_i64(args, "len") + .or_else(|| optional_i64(args, "length")) + .ok_or_else(|| anyhow!("EINVAL missing integer arg length")) +} + +fn decoded_len(content: &str) -> Result { + Ok(decode_content(content)?.len() as i64) +} + +fn decode_content(content: &str) -> Result> { + BASE64 + .decode(content) + .map_err(|error| anyhow!("EINVAL invalid base64 file content: {error}")) +} + +fn string_col(row: &mut JsonMap, key: &str) -> Result { + row.remove(key) + .and_then(|value| value.as_str().map(str::to_owned)) + .ok_or_else(|| anyhow!("sqlite_vfs row missing string column {key}")) +} + +fn optional_string_col(row: &mut JsonMap, key: &str) -> Result> { + match row.remove(key) { + Some(JsonValue::Null) | None => Ok(None), + Some(JsonValue::String(value)) => Ok(Some(value)), + Some(value) => bail!("sqlite_vfs row column {key} expected string/null, got {value:?}"), + } +} + +fn optional_content_col(row: &mut JsonMap, key: &str) -> Result> { + match row.remove(key) { + Some(JsonValue::Null) | None => Ok(None), + Some(JsonValue::String(value)) => Ok(Some(value)), + Some(JsonValue::Array(bytes)) => { + let raw = bytes + .into_iter() + .map(|value| { + value + .as_u64() + .and_then(|byte| u8::try_from(byte).ok()) + .ok_or_else(|| { + anyhow!("sqlite_vfs blob column {key} contains non-byte value") + }) + }) + .collect::>>()?; + Ok(Some(String::from_utf8(raw)?)) + } + Some(value) => { + bail!("sqlite_vfs row column {key} expected blob/string/null, got {value:?}") + } + } +} + +fn int_col(row: &mut JsonMap, key: &str) -> Result { + row.remove(key) + .and_then(|value| value.as_i64()) + .ok_or_else(|| anyhow!("sqlite_vfs row missing integer column {key}")) +} + +fn stable_ino(path: &str) -> u64 { + let mut hash = 0xcbf29ce484222325u64; + for byte in path.as_bytes() { + hash ^= u64::from(*byte); + hash = hash.wrapping_mul(0x100000001b3); + } + hash +} + +#[cfg(test)] +mod sqlite_vfs_callback_tests { + use super::*; + + #[test] + fn path_normalization_is_absolute_and_stays_within_root() { + assert_eq!(normalize_path("/a/./b").unwrap(), "/a/b"); + assert_eq!(normalize_path("a/../b").unwrap(), "/b"); + assert_eq!(normalize_path("/../../").unwrap(), "/"); + } + + #[test] + fn content_column_accepts_blob_rows_from_sqlite() { + let mut row = JsonMap::new(); + row.insert( + "content".to_owned(), + JsonValue::Array( + b"aGVsbG8=" + .iter() + .map(|byte| JsonValue::from(*byte)) + .collect(), + ), + ); + assert_eq!( + optional_content_col(&mut row, "content").unwrap(), + Some("aGVsbG8=".to_owned()) + ); + } + + #[test] + fn transcript_render_is_role_labeled_and_idempotent() { + let events = vec![ + json!({ "method": "user_prompt", "params": { "text": "hello there" } }), + json!({ + "method": "session/update", + "params": { "update": { + "sessionUpdate": "agent_message_chunk", + "content": { "type": "text", "text": "hi back" } + }} + }), + json!({ + "method": "session/update", + "params": { "update": { + "sessionUpdate": "tool_call", + "title": "read_file", + "status": "completed", + "content": [{ "content": { "type": "text", "text": "file body" } }] + }} + }), + ]; + let first = render_transcript_markdown("sess-1", &events); + let second = render_transcript_markdown("sess-1", &events); + // Idempotent: identical bytes on repeated render. + assert_eq!(first, second); + assert!(first.contains("# Session transcript: sess-1")); + assert!(first.contains("## User\n\nhello there")); + assert!(first.contains("## Assistant\n\nhi back")); + assert!(first.contains("### Tool call: read_file (completed)")); + assert!(first.contains("file body")); + } + + #[test] + fn stat_shape_uses_callback_camel_case_fields() { + let entry = FsEntry { + path: "/file".to_owned(), + name: "file".to_owned(), + is_directory: false, + content: Some(BASE64.encode("hello")), + mode: DEFAULT_FILE_MODE, + uid: 1, + gid: 2, + size: 5, + atime_ms: 1001, + mtime_ms: 2002, + ctime_ms: 3003, + birthtime_ms: 4004, + symlink_target: None, + nlink: 1, + }; + let value = stat_json(entry); + assert_eq!(value["isDirectory"], JsonValue::Bool(false)); + assert_eq!(value["isSymbolicLink"], JsonValue::Bool(false)); + assert_eq!(value["size"], JsonValue::from(5)); + assert_eq!(value["mtimeNsec"], JsonValue::from(2_000_000)); + } +} diff --git a/rivetkit-rust/packages/rivetkit-agent-os/src/run.rs b/rivetkit-rust/packages/rivetkit-agent-os/src/run.rs new file mode 100644 index 0000000000..eda0d15d36 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/src/run.rs @@ -0,0 +1,238 @@ +//! Actor run loop. Brings up an `AgentOs` VM lazily on the first action +//! that needs it, tears it down on `Sleep` / `Destroy`, dispatches +//! actions through [`actions::dispatch`]. + +use std::collections::HashMap; +use std::sync::Arc; + +use agent_os_client::{ + AgentOs, MountPlugin, RootFilesystemConfig, RootFilesystemKind, SidecarJsBridgeCallback, +}; +use anyhow::{Result, anyhow}; +use bytes::Bytes; +use rivetkit::{Ctx, HttpCall, Response, RuntimeEvent, Start}; +use serde::Serialize; +use serde_json::json; + +use crate::actions; +use crate::actions::preview; +use crate::actor::{AgentOsActor, Vars}; +use crate::config::AgentOsActorConfig; +use crate::persistence; + +/// Empty payload type for the `vmBooted` broadcast. +#[derive(Serialize)] +struct VmBootedPayload {} + +/// Payload for the `vmShutdown` broadcast. `reason` matches the TS actor: +/// `"sleep"`, `"destroy"`, or `"error"`. +#[derive(Serialize)] +struct VmShutdownPayload<'a> { + reason: &'a str, +} + +/// Run-loop entry function. Brings up the VM lazily on first event-driven +/// need and tears it down on `Sleep` / `Destroy`. +pub async fn run(config: Arc, mut start: Start) -> Result<()> { + let mut vm: Option = None; + // Ephemeral per-VM-lifetime state: the `external -> live` session remap and + // the live event-capture pump tasks. Reconstructed on each wake; cleared on + // teardown (see `Vars::clear`). + let mut vars = Vars::default(); + + // Ensure the agent-os SQLite persistence schema exists before handling any + // events. Bare unit-test contexts can omit SQLite; production actor contexts + // provide it and get the durable sqlite_vfs root below. + if start.ctx.sql().is_enabled() { + persistence::migrate_actor(&start.ctx).await?; + } + + while let Some(event) = start.events.recv().await { + match event { + RuntimeEvent::Action(action) => { + if let Err(error) = ensure_vm(&start.ctx, &config, &mut vm).await { + tracing::error!(?error, "ensure_vm failed"); + action.err(error); + continue; + } + let handle = vm.as_ref().expect("vm present after ensure_vm"); + actions::dispatch(&start.ctx, handle, &mut vars, action).await; + } + RuntimeEvent::Http(http) => proxy_preview(&start.ctx, vm.as_ref(), http).await, + RuntimeEvent::QueueSend(queue) => queue.err(anyhow!("queue send not supported")), + RuntimeEvent::WebSocketOpen(ws) => ws.reject(anyhow!("websocket not supported")), + RuntimeEvent::ConnOpen(conn) => conn.accept(()), + RuntimeEvent::ConnClosed(_) => {} + RuntimeEvent::Subscribe(subscribe) => subscribe.allow(), + RuntimeEvent::SerializeState(serialize) => serialize.skip(), + RuntimeEvent::Sleep(sleep) => { + // Cancel live event-capture pumps + drop the remap before the VM + // goes away; both are reconstructed on wake. + vars.clear(); + shutdown_vm(&start.ctx, &mut vm, "sleep").await; + sleep.ok(); + } + RuntimeEvent::Destroy(destroy) => { + vars.clear(); + shutdown_vm(&start.ctx, &mut vm, "destroy").await; + destroy.ok(); + } + } + } + + // Channel closed: best-effort cleanup if the run loop terminates while + // a VM is still up. + vars.clear(); + shutdown_vm(&start.ctx, &mut vm, "error").await; + + Ok(()) +} + +/// Proxy a `/preview/{token}/...` HTTP request to the guest port the token +/// was issued for. The first path segment after `/preview/` is the token; +/// the remainder is forwarded to the guest service via [`AgentOs::fetch`]. +/// An unmatched path, an unknown or expired token, or a VM that is not yet +/// up all reply `404`. +async fn proxy_preview(ctx: &Ctx, vm: Option<&AgentOs>, http: HttpCall) { + let path = http + .request() + .map(|request| request.uri().path().to_owned()) + .unwrap_or_default(); + let Some(rest) = path.strip_prefix("/preview/") else { + tracing::warn!(%path, "proxy_preview: path lacks /preview/ prefix"); + http.reply_status(404); + return; + }; + let (token, forward_path) = match rest.split_once('/') { + Some((token, tail)) => (token.to_owned(), format!("/{tail}")), + None => (rest.to_owned(), "/".to_owned()), + }; + + let port = match preview::resolve(ctx, &token).await { + Ok(Some(port)) => port, + Ok(None) => { + tracing::warn!(token, "proxy_preview: token not found in persistence"); + http.reply_status(404); + return; + } + Err(error) => { + tracing::warn!(?error, "preview token resolve failed"); + http.reply_status(404); + return; + } + }; + let Some(vm) = vm else { + http.reply_status(404); + return; + }; + + let (request, reply) = match http.into_request() { + Ok(pair) => pair, + Err(error) => { + tracing::warn!(?error, "preview request decode failed"); + return; + } + }; + let forward_uri: http::Uri = match forward_path.parse() { + Ok(uri) => uri, + Err(error) => { + reply.reply_err(anyhow!("invalid preview path: {error}")); + return; + } + }; + let (parts, body) = request.into_inner().into_parts(); + let mut forwarded = http::Request::new(Bytes::from(body)); + *forwarded.method_mut() = parts.method; + *forwarded.uri_mut() = forward_uri; + *forwarded.headers_mut() = parts.headers; + + match vm.fetch(port, forwarded).await { + Ok(response) => { + let status = response.status().as_u16(); + let mut headers: HashMap = HashMap::new(); + for (name, value) in response.headers().iter() { + headers.insert( + name.as_str().to_owned(), + String::from_utf8_lossy(value.as_bytes()).into_owned(), + ); + } + let body = response.into_body().to_vec(); + match Response::from_parts(status, headers, body) { + Ok(response) => reply.reply(response), + Err(error) => reply.reply_err(error), + } + } + Err(error) => reply.reply_err(error), + } +} + +/// Bring up the VM if not already running. Broadcasts `vmBooted` on +/// first success. +async fn ensure_vm( + ctx: &Ctx, + config: &Arc, + vm: &mut Option, +) -> Result<()> { + if vm.is_some() { + return Ok(()); + } + let mut options = config.build_options(); + configure_actor_db_root(ctx, &mut options); + let handle = AgentOs::create(options) + .await + .map_err(|error| anyhow!("agent-os vm bring-up failed: {error}"))?; + *vm = Some(handle); + ctx.broadcast("vmBooted", &VmBootedPayload {})?; + Ok(()) +} + +fn configure_actor_db_root(ctx: &Ctx, options: &mut agent_os_client::AgentOsConfig) { + if !ctx.sql().is_enabled() { + tracing::debug!("actor DB root disabled because ctx.sql is unavailable"); + return; + } + + if options.root_filesystem == RootFilesystemConfig::default() { + tracing::debug!("configuring actor DB sqlite_vfs root filesystem"); + options.root_filesystem = RootFilesystemConfig { + kind: RootFilesystemKind::Native, + native_plugin: Some(MountPlugin { + id: "sqlite_vfs".to_owned(), + config: Some(json!({ + "backend": "callback", + "mountId": "rivetkit-agent-os-root", + })), + }), + ..RootFilesystemConfig::default() + }; + } else { + tracing::debug!( + root_filesystem = ?options.root_filesystem, + "keeping configured agent-os root filesystem" + ); + } + + if options.sidecar_js_bridge_callback.is_none() { + let ctx = ctx.clone(); + let callback: SidecarJsBridgeCallback = Arc::new(move |call| { + let ctx = ctx.clone(); + Box::pin(async move { persistence::handle_sqlite_vfs_call(&ctx, call).await }) + }); + options.sidecar_js_bridge_callback = Some(callback); + } +} + +/// Tear down the VM if running. Broadcasts `vmShutdown` after +/// `AgentOs::shutdown` completes (best-effort: shutdown errors are +/// logged but don't suppress the broadcast). +async fn shutdown_vm(ctx: &Ctx, vm: &mut Option, reason: &str) { + let Some(handle) = vm.take() else { + return; + }; + if let Err(error) = handle.shutdown().await { + tracing::warn!(?error, reason, "agent-os vm shutdown error"); + } + if let Err(error) = ctx.broadcast("vmShutdown", &VmShutdownPayload { reason }) { + tracing::warn!(?error, reason, "vmShutdown broadcast failed"); + } +} diff --git a/rivetkit-rust/packages/rivetkit-agent-os/tests/dispatcher_e2e.rs b/rivetkit-rust/packages/rivetkit-agent-os/tests/dispatcher_e2e.rs new file mode 100644 index 0000000000..dd70d4e188 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/tests/dispatcher_e2e.rs @@ -0,0 +1,195 @@ +//! Phase 1a end-to-end gate. Drives `actions::dispatch` against a real +//! `agent-os-sidecar` binary. Skips when `AGENT_OS_SIDECAR_BIN` is unset +//! (CI/dev environments where the binary isn't built). +//! +//! To run for real: +//! ```sh +//! cargo build -p agent-os-sidecar +//! AGENT_OS_SIDECAR_BIN=$(pwd)/target/debug/agent-os-sidecar \ +//! cargo test -p rivetkit-agent-os --test dispatcher_e2e -- --nocapture +//! ``` + +use std::io::Cursor; +use std::path::PathBuf; + +use agent_os_client::{AgentOs, AgentOsConfig, FileContent}; +use rivetkit::RuntimeEvent; +use rivetkit::start::wrap_start; +use rivetkit_agent_os::AgentOsActor; +use rivetkit_core::{ActorContext, ActorEvent, ActorStart, Reply}; +use tokio::sync::{mpsc, oneshot}; + +fn sidecar_available() -> bool { + if std::env::var("AGENT_OS_SIDECAR_BIN").is_err() { + let candidate = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../../target/debug/agent-os-sidecar"); + if candidate.exists() { + // SAFETY: tests run single-process; env mutation here is fine. + unsafe { + std::env::set_var("AGENT_OS_SIDECAR_BIN", candidate); + } + } + } + std::env::var("AGENT_OS_SIDECAR_BIN") + .map(|path| PathBuf::from(path).exists()) + .unwrap_or(false) +} + +async fn new_vm() -> AgentOs { + AgentOs::create(AgentOsConfig::default()) + .await + .expect("create VM against real sidecar") +} + +fn encode_args(values: &[serde_json::Value]) -> Vec { + let mut buf = Vec::new(); + ciborium::into_writer(values, &mut buf).expect("encode CBOR args"); + buf +} + +/// Drive one action through the dispatcher and return the encoded reply +/// bytes (or stringified error). +async fn dispatch_one(vm: &AgentOs, name: &str, args_cbor: Vec) -> Result, String> { + // Synthesize an ActorEvent::Action and pipe it through a typed + // Start via wrap_start. This is the canonical + // canned-events pattern used by the rivetkit integration tests. + let (reply_tx, reply_rx) = oneshot::channel(); + let action_event = ActorEvent::Action { + name: name.to_owned(), + args: args_cbor, + conn: None, + reply: Reply::from(reply_tx), + }; + let (event_tx, event_rx) = mpsc::unbounded_channel::(); + event_tx.send(action_event).expect("queue action event"); + drop(event_tx); + + let start = wrap_start::(ActorStart { + ctx: ActorContext::new("dispatcher-e2e", "agent-os", Vec::new(), "local"), + input: None, + is_new: true, + snapshot: None, + hibernated: Vec::new(), + events: event_rx.into(), + startup_ready: None, + }) + .expect("wrap_start"); + let mut events = start.events; + + let event = events.recv().await.expect("recv typed event"); + let action = match event { + RuntimeEvent::Action(action) => action, + other => panic!("expected Action event, got {other:?}"), + }; + let mut vars = rivetkit_agent_os::actor::Vars::default(); + rivetkit_agent_os::actions::dispatch(&start.ctx, vm, &mut vars, action).await; + + match reply_rx.await.expect("await reply") { + Ok(bytes) => Ok(bytes), + Err(error) => Err(error.to_string()), + } +} + +#[tokio::test] +async fn dispatcher_round_trips_read_file_against_real_sidecar() { + if !sidecar_available() { + eprintln!("skipping: AGENT_OS_SIDECAR_BIN not present"); + return; + } + + let vm = new_vm().await; + + // Seed a known file via the raw client (bypass the dispatcher to + // avoid bootstrapping a writeFile arm we haven't added yet). + let path = "/home/user/dispatcher-e2e.txt"; + let payload = b"hello world".to_vec(); + vm.write_file(path, FileContent::Bytes(payload.clone())) + .await + .expect("seed file"); + + // readFile takes a single string arg; TS sends args as a CBOR array. + let args = encode_args(&[serde_json::json!(path)]); + let reply_bytes = dispatch_one(&vm, "readFile", args) + .await + .expect("dispatch readFile"); + + // The dispatcher replies via `action.ok(&ByteBuf)` which wraps the + // bytes per the rivetkit JSON_COMPAT_UINT8_ARRAY convention. + // Decode the wrapped intermediate and verify the structure. + let intermediate: serde_json::Value = + ciborium::from_reader(Cursor::new(reply_bytes)).expect("decode reply CBOR"); + assert!( + intermediate.is_array(), + "expected wrapped Uint8Array, got {intermediate:?}" + ); + assert_eq!(intermediate[0], "$Uint8Array"); + let base64 = intermediate[1].as_str().expect("base64 element"); + + use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; + let decoded = BASE64.decode(base64).expect("decode base64"); + assert_eq!(decoded, payload); + + let _ = vm.shutdown().await; +} + +#[tokio::test] +async fn dispatcher_round_trips_write_then_read_file() { + if !sidecar_available() { + eprintln!("skipping: AGENT_OS_SIDECAR_BIN not present"); + return; + } + + let vm = new_vm().await; + + // writeFile via the dispatcher (no direct vm.write_file seeding). + let path = "/home/user/dispatcher-roundtrip.txt"; + let payload = b"round-trip via writeFile arm".to_vec(); + let write_args = { + let mut buf = Vec::new(); + let tuple = (path.to_owned(), serde_bytes::ByteBuf::from(payload.clone())); + ciborium::into_writer(&tuple, &mut buf).expect("encode writeFile args"); + buf + }; + let write_reply = dispatch_one(&vm, "writeFile", write_args) + .await + .expect("dispatch writeFile"); + // writeFile replies with unit `()` — should encode as a single CBOR null. + let unit: Option = ciborium::from_reader(Cursor::new(write_reply)).ok(); + // We don't assert the exact unit encoding; just that it isn't an error + // envelope and the read below succeeds. + let _ = unit; + + // readFile via the dispatcher. + let read_args = encode_args(&[serde_json::json!(path)]); + let read_reply = dispatch_one(&vm, "readFile", read_args) + .await + .expect("dispatch readFile"); + + let intermediate: serde_json::Value = + ciborium::from_reader(Cursor::new(read_reply)).expect("decode readFile reply"); + assert_eq!(intermediate[0], "$Uint8Array"); + let base64 = intermediate[1].as_str().expect("base64 element"); + use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64}; + let decoded = BASE64.decode(base64).expect("decode base64"); + assert_eq!(decoded, payload); + + let _ = vm.shutdown().await; +} + +#[tokio::test] +async fn dispatcher_returns_not_implemented_for_unknown_action() { + if !sidecar_available() { + eprintln!("skipping: AGENT_OS_SIDECAR_BIN not present"); + return; + } + let vm = new_vm().await; + let args = encode_args(&[]); + let error = dispatch_one(&vm, "definitelyNotAnAction", args) + .await + .expect_err("unknown action should error"); + assert!( + error.contains("not implemented yet") || error.contains("definitelyNotAnAction"), + "expected not-implemented error, got: {error}" + ); + let _ = vm.shutdown().await; +} diff --git a/rivetkit-rust/packages/rivetkit-agent-os/tests/persistence.rs b/rivetkit-rust/packages/rivetkit-agent-os/tests/persistence.rs new file mode 100644 index 0000000000..92d8ecb041 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/tests/persistence.rs @@ -0,0 +1,195 @@ +//! Validates the agent-os SQLite schema (`MIGRATION_SQL`) is well-formed, +//! idempotent, and round-trips the persisted tables — using an in-memory +//! rusqlite database (the same SQL the actor runs via `ctx.db_exec`). + +use rivetkit_agent_os::persistence::MIGRATION_SQL; +use rusqlite::{Connection, params}; + +fn migrated_db() -> Connection { + let conn = Connection::open_in_memory().expect("open in-memory db"); + conn.execute_batch(MIGRATION_SQL).expect("apply migration"); + conn +} + +#[test] +fn migration_sql_is_valid_and_idempotent() { + let conn = Connection::open_in_memory().expect("open in-memory db"); + // Applying twice must succeed (every statement is IF NOT EXISTS). + conn.execute_batch(MIGRATION_SQL).expect("first migration"); + conn.execute_batch(MIGRATION_SQL) + .expect("second migration must be idempotent"); + + for table in [ + "agent_os_preview_tokens", + "agent_os_fs_entries", + "agent_os_sessions", + "agent_os_session_events", + ] { + let count: i64 = conn + .query_row( + "SELECT count(*) FROM sqlite_master WHERE type = 'table' AND name = ?1", + [table], + |row| row.get(0), + ) + .expect("query table presence"); + assert_eq!(count, 1, "table `{table}` should exist after migration"); + } + + for index in [ + "idx_preview_tokens_expires_at", + "idx_fs_entries_parent", + "idx_session_events_session_seq", + ] { + let count: i64 = conn + .query_row( + "SELECT count(*) FROM sqlite_master WHERE type = 'index' AND name = ?1", + [index], + |row| row.get(0), + ) + .expect("query index presence"); + assert_eq!(count, 1, "index `{index}` should exist after migration"); + } +} + +#[test] +fn preview_tokens_roundtrip() { + let conn = migrated_db(); + conn.execute( + "INSERT INTO agent_os_preview_tokens (token, port, created_at, expires_at) \ + VALUES (?1, ?2, ?3, ?4)", + params!["tok-1", 8080_i64, 1_000_i64, 2_000_i64], + ) + .expect("insert preview token"); + + let (port, expires): (i64, i64) = conn + .query_row( + "SELECT port, expires_at FROM agent_os_preview_tokens WHERE token = ?1", + ["tok-1"], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .expect("read preview token"); + assert_eq!(port, 8080); + assert_eq!(expires, 2_000); + + conn.execute( + "DELETE FROM agent_os_preview_tokens WHERE token = ?1", + ["tok-1"], + ) + .expect("delete preview token"); + let remaining: i64 = conn + .query_row("SELECT count(*) FROM agent_os_preview_tokens", [], |r| { + r.get(0) + }) + .unwrap(); + assert_eq!(remaining, 0); +} + +#[test] +fn sessions_and_events_roundtrip() { + let conn = migrated_db(); + conn.execute( + "INSERT INTO agent_os_sessions (session_id, agent_type, capabilities, agent_info, created_at) \ + VALUES (?1, ?2, ?3, NULL, ?4)", + params!["sess-1", "claude", "{}", 1_234_i64], + ) + .expect("insert session"); + conn.execute( + "INSERT INTO agent_os_session_events (session_id, seq, event, created_at) \ + VALUES (?1, ?2, ?3, ?4)", + params![ + "sess-1", + 0_i64, + "{\"method\":\"session/update\"}", + 1_235_i64 + ], + ) + .expect("insert session event"); + + let (agent_type, created_at): (String, i64) = conn + .query_row( + "SELECT agent_type, created_at FROM agent_os_sessions WHERE session_id = ?1", + ["sess-1"], + |row| Ok((row.get(0)?, row.get(1)?)), + ) + .expect("read session"); + assert_eq!(agent_type, "claude"); + assert_eq!(created_at, 1_234); + + let event: String = conn + .query_row( + "SELECT event FROM agent_os_session_events WHERE session_id = ?1 ORDER BY seq", + ["sess-1"], + |row| row.get(0), + ) + .expect("read session event"); + assert_eq!(event, "{\"method\":\"session/update\"}"); +} + +/// L1: exercises the EXACT atomic `seq` allocation SQL used by +/// `persistence::insert_session_event` (which can't be unit-tested directly +/// since it takes a live actor `Ctx`). Proves: the first event for a session +/// gets `seq` 0 (`MAX` over the empty set -> SQL NULL -> `COALESCE(..,0)`); +/// subsequent events increment; and per-session counters are independent. The +/// allocation is computed INSIDE the INSERT (a sub-SELECT), which is what makes +/// the concurrent capture-task-vs-prompt-action path race-safe — SQLite +/// serializes writers, so two inserts cannot read the same `MAX` and duplicate. +/// A duplicate or gap here is a regression. +#[test] +fn session_event_seq_allocation_is_atomic_and_per_session() { + let conn = migrated_db(); + for sid in ["a", "b"] { + conn.execute( + "INSERT INTO agent_os_sessions (session_id, agent_type, capabilities, created_at) \ + VALUES (?1, 'pi', '{}', 0)", + [sid], + ) + .expect("insert session"); + } + + // The exact statement from `insert_session_event` (session_id bound twice). + let insert = |sid: &str, event: &str| { + conn.execute( + "INSERT INTO agent_os_session_events (session_id, seq, event, created_at) \ + SELECT ?1, \ + COALESCE((SELECT MAX(seq) + 1 FROM agent_os_session_events WHERE session_id = ?1), 0), \ + ?2, 0", + params![sid, event], + ) + .expect("insert event"); + }; + + // Interleave the two sessions to mimic concurrent capture across sessions. + insert("a", "a0"); + insert("b", "b0"); + insert("a", "a1"); + insert("a", "a2"); + insert("b", "b1"); + + let seqs = |sid: &str| -> Vec { + let mut stmt = conn + .prepare("SELECT seq FROM agent_os_session_events WHERE session_id = ?1 ORDER BY seq") + .unwrap(); + stmt.query_map([sid], |row| row.get(0)) + .unwrap() + .map(|r| r.unwrap()) + .collect() + }; + + // Dense, gap-free, per-session sequences starting at 0. + assert_eq!(seqs("a"), vec![0, 1, 2], "session a seqs"); + assert_eq!(seqs("b"), vec![0, 1], "session b seqs"); + + // Insertion order preserved by seq ordering. + let a_events: Vec = { + let mut stmt = conn + .prepare( + "SELECT event FROM agent_os_session_events WHERE session_id = 'a' ORDER BY seq", + ) + .unwrap(); + stmt.query_map([], |row| row.get(0)) + .unwrap() + .map(|r| r.unwrap()) + .collect() + }; + assert_eq!(a_events, vec!["a0", "a1", "a2"]); +} diff --git a/rivetkit-rust/packages/rivetkit-agent-os/tests/resume_sleep_wake.rs b/rivetkit-rust/packages/rivetkit-agent-os/tests/resume_sleep_wake.rs new file mode 100644 index 0000000000..1c0f58280d --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/tests/resume_sleep_wake.rs @@ -0,0 +1,214 @@ +//! L0 durable-SQLite prerequisite for the session-resume tests (L1/L2/L3). +//! +//! Goal: prove that the agent-os persistence schema + an +//! `agent_os_session_events` row survive a VM teardown ("Sleep") and a +//! subsequent wake, against a REAL persistent SQLite backend — without the +//! engine binary and without the agent-os sidecar. +//! +//! ## Why this does not drive the real actor `run()` loop +//! +//! The production run loop (`src/run.rs`) persists through `ctx.db_*`, which is +//! backed by `rivetkit_core::SqliteDb`. In every *constructible-from-a-test* +//! `ActorContext` the SqliteDb is `SqliteDb::default()` → +//! `SqliteBackend::Unavailable` (`ctx.sql().is_enabled() == false`), so +//! `migrate_actor`/`configure_actor_db_root` are skipped. There is **no public +//! constructor** on `ActorContext` (`new` / `new_with_kv`) that injects a live +//! `SqliteDb`; the only injecting path is `ActorContext::build`, which is +//! `pub(crate)` to `rivetkit-core` and only called by the registry with a +//! real `EnvoyHandle`. And `EnvoyHandle::sqlite_get_pages`/`sqlite_commit` +//! send `ToEnvoyMessage::SqliteRequest` over the envoy channel, which only the +//! engine (or an in-process pump, see notes at bottom) services. +//! +//! So this harness exercises the SAME storage primitive the run loop relies on +//! — the depot-backed `NativeDatabaseHandle` that core's `SqliteBackend::Local` +//! wraps — directly, running the agent-os schema and session-event SQL verbatim. +//! Closing the SQLite handle models the VM teardown on `RuntimeEvent::Sleep`; +//! reopening a fresh handle over the *same* durable depot store models the wake. +//! The depot store (RocksDB on disk) is the durable layer; it outlives the +//! handle exactly as it outlives the actor generation in production. +//! +//! Unblocking the full `run()`-loop variant requires a small production change +//! in `rivetkit-core`: a public test seam that builds an `ActorContext` with a +//! caller-supplied `SqliteDb` (e.g. `ActorContext::new_with_sqlite(...)`), plus +//! enabling the `rivetkit/sqlite-local` feature for the agent-os test build (the +//! default `rivetkit/sqlite` feature only turns on `sqlite-remote`). That change +//! is intentionally NOT made here. + +use std::sync::Arc; + +use depot::conveyer::Db; +use depot_client::database::NativeDatabaseHandle; +use depot_client::types::{BindParam, ColumnValue}; +use depot_client_embedded::open_database_from_embedded_depot; +use gas::prelude::Id; +use rivet_pools::NodeId; +use rivetkit_agent_os::persistence::MIGRATION_SQL; +use tempfile::TempDir; + +/// The exact INSERT the actor runs in `persistence::insert_session_event` +/// (atomic `MAX(seq)+1` allocation). Kept verbatim so this harness exercises the +/// real statement, not a paraphrase. +const INSERT_SESSION_EVENT_SQL: &str = "INSERT INTO agent_os_session_events (session_id, seq, event, created_at) \ + SELECT ?, \ + COALESCE((SELECT MAX(seq) + 1 FROM agent_os_session_events WHERE session_id = ?), 0), \ + ?, ?"; + +/// A durable, in-process depot store backed by RocksDB in a temp dir. Holding +/// the `TempDir` keeps the on-disk store alive across handle close/reopen. +struct DurableDepot { + udb: Arc, + bucket_id: Id, + actor_id: String, + generation: u64, + _dir: TempDir, +} + +impl DurableDepot { + async fn new(actor_id: &str) -> anyhow::Result { + let dir = tempfile::Builder::new() + .prefix("agent-os-resume-l0-") + .tempdir()?; + let driver = + universaldb::driver::RocksDbDatabaseDriver::new(dir.path().to_path_buf()).await?; + let udb = Arc::new(universaldb::Database::new(Arc::new(driver))); + Ok(Self { + udb, + bucket_id: Id::new_v1(1), + actor_id: actor_id.to_owned(), + generation: 1, + _dir: dir, + }) + } + + /// Build a fresh `NativeDatabaseHandle` over the same durable depot store. + /// This is the same handle type `rivetkit_core::SqliteDb` opens for its + /// local backend (`open_database_from_transport`), so the bytes path is + /// identical to the actor's `ctx.db_*` writes. + async fn open_handle(&self) -> anyhow::Result { + let db = Arc::new(Db::new( + self.udb.clone(), + self.bucket_id, + self.actor_id.clone(), + NodeId::new(), + )); + let handle = open_database_from_embedded_depot( + db, + self.actor_id.clone(), + self.generation, + tokio::runtime::Handle::current(), + None, + ) + .await?; + Ok(handle) + } +} + +async fn count_session_events( + handle: &NativeDatabaseHandle, + session_id: &str, +) -> anyhow::Result { + let result = handle + .execute( + "SELECT COUNT(*) AS n FROM agent_os_session_events WHERE session_id = ?".to_owned(), + Some(vec![BindParam::Text(session_id.to_owned())]), + ) + .await?; + let n = match result.rows.first().and_then(|row| row.first()) { + Some(ColumnValue::Integer(n)) => *n, + other => anyhow::bail!("unexpected COUNT(*) result: {other:?}"), + }; + Ok(n) +} + +#[tokio::test(flavor = "multi_thread")] +async fn session_event_survives_sleep_then_wake() -> anyhow::Result<()> { + let session_id = "external-session-l0"; + let depot = DurableDepot::new("agent-os-actor-l0").await?; + + // --- Generation 1: migrate + write an agent_os_session_events row. --- + let handle = depot.open_handle().await?; + + // Same schema the actor runs at the top of run() via migrate_actor(). + handle.exec(MIGRATION_SQL.to_owned()).await?; + + // A session row first (FK target), then the event — same shape as the + // real persistence module. + handle + .execute( + "INSERT INTO agent_os_sessions (session_id, agent_type, capabilities, agent_info, created_at) \ + VALUES (?, ?, ?, NULL, ?)" + .to_owned(), + Some(vec![ + BindParam::Text(session_id.to_owned()), + BindParam::Text("claude".to_owned()), + BindParam::Text("{}".to_owned()), + BindParam::Integer(1_000), + ]), + ) + .await?; + + handle + .execute( + INSERT_SESSION_EVENT_SQL.to_owned(), + Some(vec![ + BindParam::Text(session_id.to_owned()), + BindParam::Text(session_id.to_owned()), + BindParam::Text(r#"{"method":"session/update"}"#.to_owned()), + BindParam::Integer(1_001), + ]), + ) + .await?; + + assert_eq!( + count_session_events(&handle, session_id).await?, + 1, + "row should be visible within generation 1" + ); + + // --- Sleep: tear the SQLite handle down (models VM teardown on Sleep). --- + handle.close().await?; + drop(handle); + + // --- Wake: reopen a fresh handle over the SAME durable depot store. --- + let woken = depot.open_handle().await?; + // migrate is idempotent; the actor reruns it on every start. + woken.exec(MIGRATION_SQL.to_owned()).await?; + + assert_eq!( + count_session_events(&woken, session_id).await?, + 1, + "agent_os_session_events row must survive Sleep + wake" + ); + + // The actual event payload must round-trip unchanged. + let event = woken + .execute( + "SELECT event FROM agent_os_session_events WHERE session_id = ? ORDER BY seq" + .to_owned(), + Some(vec![BindParam::Text(session_id.to_owned())]), + ) + .await?; + let event_text = match event.rows.first().and_then(|row| row.first()) { + Some(ColumnValue::Text(text)) => text.clone(), + other => anyhow::bail!("unexpected event column: {other:?}"), + }; + assert_eq!(event_text, r#"{"method":"session/update"}"#); + + // A second event after wake gets seq 1 (MAX(seq)+1), proving the atomic + // allocator reads the surviving row. + woken + .execute( + INSERT_SESSION_EVENT_SQL.to_owned(), + Some(vec![ + BindParam::Text(session_id.to_owned()), + BindParam::Text(session_id.to_owned()), + BindParam::Text(r#"{"method":"session/update","after":"wake"}"#.to_owned()), + BindParam::Integer(2_000), + ]), + ) + .await?; + assert_eq!(count_session_events(&woken, session_id).await?, 2); + + woken.close().await?; + Ok(()) +} diff --git a/rivetkit-rust/packages/rivetkit/Cargo.toml b/rivetkit-rust/packages/rivetkit/Cargo.toml index 584432442c..0587338ad4 100644 --- a/rivetkit-rust/packages/rivetkit/Cargo.toml +++ b/rivetkit-rust/packages/rivetkit/Cargo.toml @@ -17,6 +17,7 @@ sqlite-local = ["rivetkit-core/sqlite-local"] [dependencies] anyhow.workspace = true async-trait.workspace = true +base64.workspace = true ciborium.workspace = true futures.workspace = true http.workspace = true @@ -26,15 +27,20 @@ rivetkit-client.workspace = true parking_lot.workspace = true serde.workspace = true serde_json.workspace = true +serde_bytes.workspace = true tokio.workspace = true tokio-util.workspace = true tracing.workspace = true [dev-dependencies] axum = { workspace = true, features = ["ws"] } +base64.workspace = true bytes.workspace = true +ciborium.workspace = true rivet-envoy-client = { workspace = true, features = ["native-transport"] } +rivetkit-client = { path = "../client" } rivetkit-client-protocol.workspace = true +serde_bytes.workspace = true serde_json.workspace = true tracing-subscriber.workspace = true trybuild = "1.0.116" diff --git a/rivetkit-rust/packages/rivetkit/src/encoding.rs b/rivetkit-rust/packages/rivetkit/src/encoding.rs new file mode 100644 index 0000000000..2b03f2a053 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit/src/encoding.rs @@ -0,0 +1,423 @@ +//! Byte-payload encoding parity with the rivetkit TypeScript framework. +//! +//! TS sits on at least one end of every action call (usually the client end). +//! The wire convention `["$Uint8Array", base64]` is what TS emits and what +//! TS expects. This module mirrors it for action-response encoding so Rust +//! actors can return byte payloads that round-trip correctly across all +//! three wire encodings (bare, cbor, json). +//! +//! **Scope-limited:** only `JSON_COMPAT_UINT8_ARRAY` is implemented. Other +//! JSON-compat tags from the TS side (`$BigInt`, `$ArrayBuffer`, `$Set`, +//! `$Undefined`, etc.) are not mirrored — add them when a real consumer +//! needs them. +//! +//! Reference: `rivetkit-typescript/packages/rivetkit/src/common/encoding.ts` +//! (`JSON_COMPAT_UINT8_ARRAY`, `encodeJsonCompatValue`). +//! +//! ## Convention +//! +//! Byte payloads (anything that goes through `serialize_bytes`, including +//! `serde_bytes::ByteBuf`, `serde_bytes::Bytes`, and `&[u8]` annotated +//! `#[serde(with = "serde_bytes")]`) are wrapped as a 2-element tagged +//! array: +//! +//! ```ignore +//! ["$Uint8Array", ""] +//! ``` +//! +//! Plain `Vec` without `#[serde(with = "serde_bytes")]` is treated as +//! a CBOR array of integers — matching TS's distinction between +//! `Uint8Array` (wrapped) and other typed arrays (passed through). +//! +//! All other serde calls pass through to `ciborium` unchanged. + +use std::io::Write; + +use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; +use serde::Serialize; + +/// Tag string for the `Uint8Array` JSON-compat envelope. Matches the +/// TypeScript constant in `rivetkit-typescript/.../common/encoding.ts:14`. +/// Note the capital `U`. +pub const JSON_COMPAT_UINT8_ARRAY: &str = "$Uint8Array"; + +/// Encode `value` as CBOR with byte payloads wrapped per the TS convention. +/// +/// Use this in place of `ciborium::into_writer` at every site that +/// forwards a user value to a JS client (action replies, workflow +/// history, workflow replay). +pub fn encode_json_compat(value: &T, writer: W) -> anyhow::Result<()> +where + T: Serialize, + W: Write, +{ + let wrapped = JsonCompatWrap(value); + ciborium::into_writer(&wrapped, writer)?; + Ok(()) +} + +/// Convenience wrapper that encodes to a `Vec`. +pub fn encode_json_compat_to_vec(value: &T) -> anyhow::Result> { + let mut buf = Vec::new(); + encode_json_compat(value, &mut buf)?; + Ok(buf) +} + +/// Newtype that re-serializes any embedded `serialize_bytes` call as the +/// `["$Uint8Array", base64]` shape. Wraps any `Serialize` value so the +/// transformation applies recursively to nested fields. +struct JsonCompatWrap<'a, T: ?Sized>(&'a T); + +impl Serialize for JsonCompatWrap<'_, T> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(JsonCompatSerializer { inner: serializer }) + } +} + +/// Serializer adapter that intercepts `serialize_bytes` to emit the +/// rivetkit `Uint8Array` envelope. Every other method forwards to the +/// underlying serializer with the same `JsonCompatWrap` recursion so +/// nested byte fields get the same treatment. +struct JsonCompatSerializer { + inner: S, +} + +impl serde::Serializer for JsonCompatSerializer { + type Ok = S::Ok; + type Error = S::Error; + + type SerializeSeq = JsonCompatSerializeSeq; + type SerializeTuple = JsonCompatSerializeTuple; + type SerializeTupleStruct = JsonCompatSerializeTupleStruct; + type SerializeTupleVariant = JsonCompatSerializeTupleVariant; + type SerializeMap = JsonCompatSerializeMap; + type SerializeStruct = JsonCompatSerializeStruct; + type SerializeStructVariant = JsonCompatSerializeStructVariant; + + fn serialize_bool(self, v: bool) -> Result { + self.inner.serialize_bool(v) + } + + fn serialize_i8(self, v: i8) -> Result { + self.inner.serialize_i8(v) + } + + fn serialize_i16(self, v: i16) -> Result { + self.inner.serialize_i16(v) + } + + fn serialize_i32(self, v: i32) -> Result { + self.inner.serialize_i32(v) + } + + fn serialize_i64(self, v: i64) -> Result { + self.inner.serialize_i64(v) + } + + fn serialize_i128(self, v: i128) -> Result { + self.inner.serialize_i128(v) + } + + fn serialize_u8(self, v: u8) -> Result { + self.inner.serialize_u8(v) + } + + fn serialize_u16(self, v: u16) -> Result { + self.inner.serialize_u16(v) + } + + fn serialize_u32(self, v: u32) -> Result { + self.inner.serialize_u32(v) + } + + fn serialize_u64(self, v: u64) -> Result { + self.inner.serialize_u64(v) + } + + fn serialize_u128(self, v: u128) -> Result { + self.inner.serialize_u128(v) + } + + fn serialize_f32(self, v: f32) -> Result { + self.inner.serialize_f32(v) + } + + fn serialize_f64(self, v: f64) -> Result { + self.inner.serialize_f64(v) + } + + fn serialize_char(self, v: char) -> Result { + self.inner.serialize_char(v) + } + + fn serialize_str(self, v: &str) -> Result { + self.inner.serialize_str(v) + } + + /// The load-bearing override. Byte payloads (`serde_bytes::ByteBuf`, + /// `serde_bytes::Bytes`, `&[u8]` with `#[serde(with = "serde_bytes")]`) + /// all funnel through here. Emit the 2-element tagged array shape. + fn serialize_bytes(self, v: &[u8]) -> Result { + use serde::ser::SerializeTuple as _; + let base64 = BASE64_STANDARD.encode(v); + let mut tuple = self.inner.serialize_tuple(2)?; + tuple.serialize_element(JSON_COMPAT_UINT8_ARRAY)?; + tuple.serialize_element(&base64)?; + tuple.end() + } + + fn serialize_none(self) -> Result { + self.inner.serialize_none() + } + + fn serialize_some(self, value: &T) -> Result { + self.inner.serialize_some(&JsonCompatWrap(value)) + } + + fn serialize_unit(self) -> Result { + self.inner.serialize_unit() + } + + fn serialize_unit_struct(self, name: &'static str) -> Result { + self.inner.serialize_unit_struct(name) + } + + fn serialize_unit_variant( + self, + name: &'static str, + variant_index: u32, + variant: &'static str, + ) -> Result { + self.inner + .serialize_unit_variant(name, variant_index, variant) + } + + fn serialize_newtype_struct( + self, + name: &'static str, + value: &T, + ) -> Result { + self.inner + .serialize_newtype_struct(name, &JsonCompatWrap(value)) + } + + fn serialize_newtype_variant( + self, + name: &'static str, + variant_index: u32, + variant: &'static str, + value: &T, + ) -> Result { + self.inner + .serialize_newtype_variant(name, variant_index, variant, &JsonCompatWrap(value)) + } + + fn serialize_seq(self, len: Option) -> Result { + Ok(JsonCompatSerializeSeq { + inner: self.inner.serialize_seq(len)?, + }) + } + + fn serialize_tuple(self, len: usize) -> Result { + Ok(JsonCompatSerializeTuple { + inner: self.inner.serialize_tuple(len)?, + }) + } + + fn serialize_tuple_struct( + self, + name: &'static str, + len: usize, + ) -> Result { + Ok(JsonCompatSerializeTupleStruct { + inner: self.inner.serialize_tuple_struct(name, len)?, + }) + } + + fn serialize_tuple_variant( + self, + name: &'static str, + variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + Ok(JsonCompatSerializeTupleVariant { + inner: self + .inner + .serialize_tuple_variant(name, variant_index, variant, len)?, + }) + } + + fn serialize_map(self, len: Option) -> Result { + Ok(JsonCompatSerializeMap { + inner: self.inner.serialize_map(len)?, + }) + } + + fn serialize_struct( + self, + name: &'static str, + len: usize, + ) -> Result { + Ok(JsonCompatSerializeStruct { + inner: self.inner.serialize_struct(name, len)?, + }) + } + + fn serialize_struct_variant( + self, + name: &'static str, + variant_index: u32, + variant: &'static str, + len: usize, + ) -> Result { + Ok(JsonCompatSerializeStructVariant { + inner: self + .inner + .serialize_struct_variant(name, variant_index, variant, len)?, + }) + } +} + +// --- Compound serializer wrappers (each delegates element serialization +// through `JsonCompatWrap` so nested byte fields get wrapped too) --- + +struct JsonCompatSerializeSeq { + inner: S, +} + +impl serde::ser::SerializeSeq for JsonCompatSerializeSeq { + type Ok = S::Ok; + type Error = S::Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> { + self.inner.serialize_element(&JsonCompatWrap(value)) + } + + fn end(self) -> Result { + self.inner.end() + } +} + +struct JsonCompatSerializeTuple { + inner: S, +} + +impl serde::ser::SerializeTuple for JsonCompatSerializeTuple { + type Ok = S::Ok; + type Error = S::Error; + + fn serialize_element(&mut self, value: &T) -> Result<(), Self::Error> { + self.inner.serialize_element(&JsonCompatWrap(value)) + } + + fn end(self) -> Result { + self.inner.end() + } +} + +struct JsonCompatSerializeTupleStruct { + inner: S, +} + +impl serde::ser::SerializeTupleStruct + for JsonCompatSerializeTupleStruct +{ + type Ok = S::Ok; + type Error = S::Error; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> { + self.inner.serialize_field(&JsonCompatWrap(value)) + } + + fn end(self) -> Result { + self.inner.end() + } +} + +struct JsonCompatSerializeTupleVariant { + inner: S, +} + +impl serde::ser::SerializeTupleVariant + for JsonCompatSerializeTupleVariant +{ + type Ok = S::Ok; + type Error = S::Error; + + fn serialize_field(&mut self, value: &T) -> Result<(), Self::Error> { + self.inner.serialize_field(&JsonCompatWrap(value)) + } + + fn end(self) -> Result { + self.inner.end() + } +} + +struct JsonCompatSerializeMap { + inner: S, +} + +impl serde::ser::SerializeMap for JsonCompatSerializeMap { + type Ok = S::Ok; + type Error = S::Error; + + fn serialize_key(&mut self, key: &T) -> Result<(), Self::Error> { + self.inner.serialize_key(&JsonCompatWrap(key)) + } + + fn serialize_value(&mut self, value: &T) -> Result<(), Self::Error> { + self.inner.serialize_value(&JsonCompatWrap(value)) + } + + fn end(self) -> Result { + self.inner.end() + } +} + +struct JsonCompatSerializeStruct { + inner: S, +} + +impl serde::ser::SerializeStruct for JsonCompatSerializeStruct { + type Ok = S::Ok; + type Error = S::Error; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> { + self.inner.serialize_field(key, &JsonCompatWrap(value)) + } + + fn end(self) -> Result { + self.inner.end() + } +} + +struct JsonCompatSerializeStructVariant { + inner: S, +} + +impl serde::ser::SerializeStructVariant + for JsonCompatSerializeStructVariant +{ + type Ok = S::Ok; + type Error = S::Error; + + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), Self::Error> { + self.inner.serialize_field(key, &JsonCompatWrap(value)) + } + + fn end(self) -> Result { + self.inner.end() + } +} diff --git a/rivetkit-rust/packages/rivetkit/src/event.rs b/rivetkit-rust/packages/rivetkit/src/event.rs index 1c41d4852f..66970ec6b8 100644 --- a/rivetkit-rust/packages/rivetkit/src/event.rs +++ b/rivetkit-rust/packages/rivetkit/src/event.rs @@ -242,7 +242,8 @@ impl ActionCall { } pub fn ok(mut self, value: &T) { - let result = encode_cbor(value, "encode action response as cbor"); + let result = + crate::encoding::encode_json_compat_to_vec(value).context("encode action response"); if let Some(reply) = self.reply.take() { reply.send(result); } @@ -887,7 +888,6 @@ fn encode_cbor(value: &T, context: &'static str) -> AnyhowResult(value: Value, expected: &'static str) -> Result where T: TryFrom, @@ -1392,6 +1392,93 @@ impl Destroy { } } +#[derive(Debug)] +#[must_use = "reply to workflow history or dropping it sends actor/dropped_reply"] +pub struct WfHistory { + pub(crate) reply: Option>>>, +} + +impl Drop for WfHistory { + fn drop(&mut self) { + if self.reply.is_some() { + warn_dropped_event("WorkflowHistory", "history"); + } + } +} + +impl WfHistory { + pub fn reply(self, history: Option<&T>) { + match history { + Some(history) => match crate::encoding::encode_json_compat_to_vec(history) + .context("encode workflow history") + { + Ok(bytes) => self.reply_raw(Some(bytes)), + Err(error) => self.reply_err(error), + }, + None => self.reply_raw(None), + } + } + + pub fn reply_raw(mut self, bytes: Option>) { + if let Some(reply) = self.reply.take() { + reply.send(Ok(bytes)); + } + } + + pub fn reply_err(mut self, err: anyhow::Error) { + if let Some(reply) = self.reply.take() { + reply.send(Err(err)); + } + } +} + +#[derive(Debug)] +#[must_use = "reply to workflow replay or dropping it sends actor/dropped_reply"] +pub struct WfReplay { + pub(crate) entry_id: Option, + pub(crate) reply: Option>>>, +} + +impl Drop for WfReplay { + fn drop(&mut self) { + if self.reply.is_some() { + warn_dropped_event( + "WorkflowReplay", + self.entry_id.as_deref().unwrap_or(""), + ); + } + } +} + +impl WfReplay { + pub fn entry_id(&self) -> Option<&str> { + self.entry_id.as_deref() + } + + pub fn reply(self, value: Option<&T>) { + match value { + Some(value) => match crate::encoding::encode_json_compat_to_vec(value) + .context("encode workflow replay") + { + Ok(bytes) => self.reply_raw(Some(bytes)), + Err(error) => self.reply_err(error), + }, + None => self.reply_raw(None), + } + } + + pub fn reply_raw(mut self, bytes: Option>) { + if let Some(reply) = self.reply.take() { + reply.send(Ok(bytes)); + } + } + + pub fn reply_err(mut self, err: anyhow::Error) { + if let Some(reply) = self.reply.take() { + reply.send(Err(err)); + } + } +} fn warn_dropped_event(variant: &'static str, identifying: impl fmt::Display) { tracing::warn!( variant, diff --git a/rivetkit-rust/packages/rivetkit/src/lib.rs b/rivetkit-rust/packages/rivetkit/src/lib.rs index 792bfe1295..7455350941 100644 --- a/rivetkit-rust/packages/rivetkit/src/lib.rs +++ b/rivetkit-rust/packages/rivetkit/src/lib.rs @@ -1,6 +1,7 @@ pub mod action; pub mod actor; pub mod context; +pub mod encoding; pub mod event; pub mod persist; pub mod prelude; diff --git a/rivetkit-rust/packages/rivetkit/tests/encoding.rs b/rivetkit-rust/packages/rivetkit/tests/encoding.rs new file mode 100644 index 0000000000..b548e9de6c --- /dev/null +++ b/rivetkit-rust/packages/rivetkit/tests/encoding.rs @@ -0,0 +1,140 @@ +//! Encode-side tests for the `JSON_COMPAT_UINT8_ARRAY` byte-payload +//! wrapping convention. Mirrors what +//! `rivetkit-typescript/.../common/encoding.ts::encodeJsonCompatValue` +//! does for `Uint8Array` inputs. + +use std::io::Cursor; + +use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; +use rivetkit::encoding::encode_json_compat_to_vec; +use serde::Serialize; + +fn decode_intermediate(encoded: &[u8]) -> serde_json::Value { + ciborium::from_reader(Cursor::new(encoded)).expect("decode CBOR as JSON value") +} + +#[test] +fn byte_buf_wraps_as_json_compat_uint8_array() { + let bytes = serde_bytes::ByteBuf::from(b"hello".to_vec()); + let encoded = encode_json_compat_to_vec(&bytes).expect("encode"); + let intermediate = decode_intermediate(&encoded); + + assert!( + intermediate.is_array(), + "expected array, got {intermediate:?}" + ); + let arr = intermediate.as_array().unwrap(); + assert_eq!(arr.len(), 2); + assert_eq!(arr[0], "$Uint8Array"); + assert_eq!(arr[1], BASE64_STANDARD.encode(b"hello")); +} + +#[test] +fn nested_byte_field_in_struct_wraps() { + #[derive(Serialize)] + struct Reply { + status: u16, + body: serde_bytes::ByteBuf, + } + let value = Reply { + status: 200, + body: serde_bytes::ByteBuf::from(b"ok".to_vec()), + }; + let encoded = encode_json_compat_to_vec(&value).expect("encode"); + let intermediate = decode_intermediate(&encoded); + + assert_eq!(intermediate["status"], 200); + let body = &intermediate["body"]; + assert!( + body.is_array(), + "expected body to be wrapped array, got {body:?}" + ); + assert_eq!(body[0], "$Uint8Array"); + assert_eq!(body[1], BASE64_STANDARD.encode(b"ok")); +} + +#[test] +fn plain_vec_u8_stays_as_array() { + // Without #[serde(with = "serde_bytes")], Vec serializes via + // `serialize_seq` (one integer per element), not `serialize_bytes`. + // Matches TS's distinction between Uint8Array and other typed arrays. + let value: Vec = vec![1, 2, 3]; + let encoded = encode_json_compat_to_vec(&value).expect("encode"); + let intermediate = decode_intermediate(&encoded); + + assert!(intermediate.is_array()); + let arr = intermediate.as_array().unwrap(); + assert_eq!(arr.len(), 3); + assert_eq!(arr[0], 1); + assert_eq!(arr[1], 2); + assert_eq!(arr[2], 3); +} + +#[test] +fn non_byte_types_pass_through_unchanged() { + #[derive(Serialize)] + struct Reply { + msg: String, + count: u32, + enabled: bool, + ratio: f64, + } + let value = Reply { + msg: "hi".into(), + count: 7, + enabled: true, + ratio: 1.5, + }; + let encoded_compat = encode_json_compat_to_vec(&value).expect("encode via compat"); + let encoded_raw = { + let mut buf = Vec::new(); + ciborium::into_writer(&value, &mut buf).expect("encode via ciborium"); + buf + }; + // Compat path should be identical to raw ciborium when there are no + // byte payloads to wrap. + assert_eq!( + encoded_compat, encoded_raw, + "non-byte types should round-trip identically" + ); +} + +#[test] +fn nested_byte_field_inside_optional_wraps() { + #[derive(Serialize)] + struct Reply { + maybe_body: Option, + } + let value = Reply { + maybe_body: Some(serde_bytes::ByteBuf::from(b"present".to_vec())), + }; + let encoded = encode_json_compat_to_vec(&value).expect("encode"); + let intermediate = decode_intermediate(&encoded); + + let body = &intermediate["maybe_body"]; + assert!( + body.is_array(), + "expected Some(byte_buf) to wrap, got {body:?}" + ); + assert_eq!(body[0], "$Uint8Array"); + assert_eq!(body[1], BASE64_STANDARD.encode(b"present")); +} + +#[test] +fn byte_field_inside_seq_wraps_each_element() { + let values: Vec = vec![ + serde_bytes::ByteBuf::from(b"a".to_vec()), + serde_bytes::ByteBuf::from(b"bc".to_vec()), + ]; + let encoded = encode_json_compat_to_vec(&values).expect("encode"); + let intermediate = decode_intermediate(&encoded); + + let arr = intermediate.as_array().expect("outer should be array"); + assert_eq!(arr.len(), 2); + for (i, expected) in [b"a".as_ref(), b"bc".as_ref()].into_iter().enumerate() { + let item = &arr[i]; + assert!(item.is_array(), "item {i} should be wrapped array"); + assert_eq!(item[0], "$Uint8Array"); + assert_eq!(item[1], BASE64_STANDARD.encode(expected)); + } +} diff --git a/rivetkit-rust/packages/rivetkit/tests/encoding_fixtures.rs b/rivetkit-rust/packages/rivetkit/tests/encoding_fixtures.rs new file mode 100644 index 0000000000..7bffabbd4e --- /dev/null +++ b/rivetkit-rust/packages/rivetkit/tests/encoding_fixtures.rs @@ -0,0 +1,121 @@ +//! Generate cross-language parity fixtures. Writes Rust-encoded outputs +//! to JSON files that the TypeScript side reads to assert wire-format +//! parity (`tests/byte-encoding-parity.test.ts`). +//! +//! Run via `cargo test -p rivetkit --test encoding_fixtures`. The +//! fixtures land in `tests/fixtures/encoding/`. + +use std::io::Cursor; +use std::path::PathBuf; + +use rivetkit::encoding::encode_json_compat_to_vec; +use serde::Serialize; + +fn fixture_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join("encoding") +} + +fn write_fixture(name: &str, intermediate: &serde_json::Value) { + let dir = fixture_dir(); + std::fs::create_dir_all(&dir).expect("mkdir fixtures"); + let path = dir.join(format!("{name}.json")); + let serialized = serde_json::to_string_pretty(intermediate).expect("serialize fixture as JSON"); + std::fs::write(&path, serialized).expect("write fixture"); + println!("wrote fixture: {}", path.display()); +} + +fn encode_and_cbor_decode(value: &T) -> serde_json::Value { + let encoded = encode_json_compat_to_vec(value).expect("encode"); + ciborium::from_reader(Cursor::new(encoded)).expect("decode") +} + +#[test] +fn fixture_uint8array_hello() { + let bytes = serde_bytes::ByteBuf::from(b"hello".to_vec()); + let intermediate = encode_and_cbor_decode(&bytes); + write_fixture("uint8array_hello", &intermediate); +} + +#[test] +fn fixture_uint8array_1234() { + let bytes = serde_bytes::ByteBuf::from(vec![1u8, 2, 3, 4]); + let intermediate = encode_and_cbor_decode(&bytes); + write_fixture("uint8array_1234", &intermediate); +} + +#[test] +fn fixture_struct_with_byte_field() { + #[derive(Serialize)] + struct Reply { + status: u16, + body: serde_bytes::ByteBuf, + } + let value = Reply { + status: 200, + body: serde_bytes::ByteBuf::from(b"ok".to_vec()), + }; + let intermediate = encode_and_cbor_decode(&value); + write_fixture("struct_with_byte_field", &intermediate); +} + +/// Structured non-byte payload modeled after `agent_os_client::VirtualStat`. +/// Exercises bool, u32, u64, f64, and camelCase `#[serde(rename)]` fields +/// to catch encoder bugs that the byte-only fixtures would miss. Phase 2 +/// gate: this struct must round-trip losslessly across bare/cbor/json. +#[derive(Serialize)] +struct VirtualStatFixture { + mode: u32, + size: u64, + blocks: u64, + dev: u64, + rdev: u64, + #[serde(rename = "isDirectory")] + is_directory: bool, + #[serde(rename = "isSymbolicLink")] + is_symbolic_link: bool, + #[serde(rename = "atimeMs")] + atime_ms: f64, + #[serde(rename = "mtimeMs")] + mtime_ms: f64, + #[serde(rename = "ctimeMs")] + ctime_ms: f64, + #[serde(rename = "birthtimeMs")] + birthtime_ms: f64, + ino: u64, + nlink: u64, + uid: u32, + gid: u32, +} + +#[test] +fn fixture_virtual_stat_struct() { + let value = VirtualStatFixture { + mode: 0o100_644, + size: 7, + blocks: 1, + dev: 42, + rdev: 0, + is_directory: false, + is_symbolic_link: false, + atime_ms: 1_780_000_000_000.5, + mtime_ms: 1_780_000_001_000.25, + ctime_ms: 1_780_000_002_000.125, + birthtime_ms: 1_780_000_003_000.0625, + ino: 9_876_543_210, + nlink: 1, + uid: 1000, + gid: 1000, + }; + let intermediate = encode_and_cbor_decode(&value); + write_fixture("virtual_stat", &intermediate); +} + +#[test] +fn fixture_plain_string() { + let value = "hello world".to_string(); + let intermediate = encode_and_cbor_decode(&value); + write_fixture("plain_string", &intermediate); +} diff --git a/rivetkit-rust/packages/rivetkit/tests/encoding_roundtrip.rs b/rivetkit-rust/packages/rivetkit/tests/encoding_roundtrip.rs new file mode 100644 index 0000000000..f4b5d79a71 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit/tests/encoding_roundtrip.rs @@ -0,0 +1,144 @@ +//! Round-trip test: encode via `rivetkit::encoding`, decode via +//! `rivetkit_client::encoding::revive_json_compat` (through a +//! `serde_json::Value` intermediate to simulate the engine's lossy +//! decode path). + +use std::io::Cursor; + +use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64_STANDARD}; +use rivetkit::encoding::encode_json_compat_to_vec; +use rivetkit_client::encoding::revive_json_compat; +use serde::Serialize; + +fn ciborium_to_json(encoded: &[u8]) -> serde_json::Value { + ciborium::from_reader(Cursor::new(encoded)).expect("ciborium decode as JSON value") +} + +#[test] +fn encode_then_decode_round_trips_bytes() { + let original = b"round-trip data".to_vec(); + let value = serde_bytes::ByteBuf::from(original.clone()); + + let encoded = encode_json_compat_to_vec(&value).expect("encode"); + let intermediate = ciborium_to_json(&encoded); + let revived = revive_json_compat(intermediate); + + // After revival, the base64 string is what remains. + let base64 = revived.as_str().expect("revived to base64 string"); + let decoded = BASE64_STANDARD.decode(base64).expect("decode base64"); + assert_eq!(decoded, original); +} + +#[test] +fn encode_then_decode_round_trips_nested_struct() { + #[derive(Serialize)] + struct Reply { + status: u16, + body: serde_bytes::ByteBuf, + } + let value = Reply { + status: 200, + body: serde_bytes::ByteBuf::from(b"hello".to_vec()), + }; + + let encoded = encode_json_compat_to_vec(&value).expect("encode"); + let intermediate = ciborium_to_json(&encoded); + let revived = revive_json_compat(intermediate); + + assert_eq!(revived["status"], 200); + let base64 = revived["body"].as_str().expect("body revived to base64"); + let decoded = BASE64_STANDARD.decode(base64).expect("decode base64"); + assert_eq!(decoded, b"hello"); +} + +/// Encode/decode round-trip for a structured non-byte payload modeled +/// after `agent_os_client::VirtualStat`. Phase 2 gate: every field +/// (bool, u32, u64, f64, camelCase rename) survives the framework's +/// encode -> ciborium decode -> revive_json_compat path losslessly. +#[test] +fn encode_then_decode_round_trips_virtual_stat() { + #[derive(Serialize)] + struct VirtualStat { + mode: u32, + size: u64, + blocks: u64, + dev: u64, + rdev: u64, + #[serde(rename = "isDirectory")] + is_directory: bool, + #[serde(rename = "isSymbolicLink")] + is_symbolic_link: bool, + #[serde(rename = "atimeMs")] + atime_ms: f64, + #[serde(rename = "mtimeMs")] + mtime_ms: f64, + #[serde(rename = "ctimeMs")] + ctime_ms: f64, + #[serde(rename = "birthtimeMs")] + birthtime_ms: f64, + ino: u64, + nlink: u64, + uid: u32, + gid: u32, + } + let value = VirtualStat { + mode: 0o100_644, + size: 7, + blocks: 1, + dev: 42, + rdev: 0, + is_directory: false, + is_symbolic_link: true, + atime_ms: 1_780_000_000_000.5, + mtime_ms: 1_780_000_001_000.25, + ctime_ms: 1_780_000_002_000.125, + birthtime_ms: 1_780_000_003_000.0625, + ino: 9_876_543_210, + nlink: 1, + uid: 1000, + gid: 1000, + }; + + let encoded = encode_json_compat_to_vec(&value).expect("encode"); + let intermediate = ciborium_to_json(&encoded); + let revived = revive_json_compat(intermediate); + + // u32 / u16 / small integers come back as JSON numbers. + assert_eq!(revived["mode"], serde_json::json!(0o100_644u32)); + assert_eq!(revived["size"], serde_json::json!(7)); + assert_eq!(revived["blocks"], serde_json::json!(1)); + assert_eq!(revived["dev"], serde_json::json!(42)); + assert_eq!(revived["rdev"], serde_json::json!(0)); + + // Booleans. + assert_eq!(revived["isDirectory"], serde_json::json!(false)); + assert_eq!(revived["isSymbolicLink"], serde_json::json!(true)); + + // f64 timestamps must preserve fractional precision. + assert_eq!( + revived["atimeMs"].as_f64().expect("atimeMs f64"), + 1_780_000_000_000.5, + ); + assert_eq!( + revived["mtimeMs"].as_f64().expect("mtimeMs f64"), + 1_780_000_001_000.25, + ); + assert_eq!( + revived["ctimeMs"].as_f64().expect("ctimeMs f64"), + 1_780_000_002_000.125, + ); + assert_eq!( + revived["birthtimeMs"].as_f64().expect("birthtimeMs f64"), + 1_780_000_003_000.0625, + ); + + // Large u64 — must not silently downcast through f64. + assert_eq!(revived["ino"].as_u64().expect("ino u64"), 9_876_543_210u64,); + assert_eq!(revived["nlink"], serde_json::json!(1)); + assert_eq!(revived["uid"], serde_json::json!(1000)); + assert_eq!(revived["gid"], serde_json::json!(1000)); + + // camelCase renames must not leak the snake_case Rust names. + assert!(revived.get("is_directory").is_none()); + assert!(revived.get("atime_ms").is_none()); +} diff --git a/rivetkit-rust/packages/rivetkit/tests/fixtures/encoding/plain_string.json b/rivetkit-rust/packages/rivetkit/tests/fixtures/encoding/plain_string.json new file mode 100644 index 0000000000..cfcb15cf66 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit/tests/fixtures/encoding/plain_string.json @@ -0,0 +1 @@ +"hello world" \ No newline at end of file diff --git a/rivetkit-rust/packages/rivetkit/tests/fixtures/encoding/struct_with_byte_field.json b/rivetkit-rust/packages/rivetkit/tests/fixtures/encoding/struct_with_byte_field.json new file mode 100644 index 0000000000..a33c808934 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit/tests/fixtures/encoding/struct_with_byte_field.json @@ -0,0 +1,7 @@ +{ + "body": [ + "$Uint8Array", + "b2s=" + ], + "status": 200 +} \ No newline at end of file diff --git a/rivetkit-rust/packages/rivetkit/tests/fixtures/encoding/uint8array_1234.json b/rivetkit-rust/packages/rivetkit/tests/fixtures/encoding/uint8array_1234.json new file mode 100644 index 0000000000..0b09f4e30b --- /dev/null +++ b/rivetkit-rust/packages/rivetkit/tests/fixtures/encoding/uint8array_1234.json @@ -0,0 +1,4 @@ +[ + "$Uint8Array", + "AQIDBA==" +] \ No newline at end of file diff --git a/rivetkit-rust/packages/rivetkit/tests/fixtures/encoding/uint8array_hello.json b/rivetkit-rust/packages/rivetkit/tests/fixtures/encoding/uint8array_hello.json new file mode 100644 index 0000000000..f872ed793f --- /dev/null +++ b/rivetkit-rust/packages/rivetkit/tests/fixtures/encoding/uint8array_hello.json @@ -0,0 +1,4 @@ +[ + "$Uint8Array", + "aGVsbG8=" +] \ No newline at end of file diff --git a/rivetkit-rust/packages/rivetkit/tests/fixtures/encoding/virtual_stat.json b/rivetkit-rust/packages/rivetkit/tests/fixtures/encoding/virtual_stat.json new file mode 100644 index 0000000000..7620eeb9d8 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit/tests/fixtures/encoding/virtual_stat.json @@ -0,0 +1,17 @@ +{ + "atimeMs": 1780000000000.5, + "birthtimeMs": 1780000003000.0625, + "blocks": 1, + "ctimeMs": 1780000002000.125, + "dev": 42, + "gid": 1000, + "ino": 9876543210, + "isDirectory": false, + "isSymbolicLink": false, + "mode": 33188, + "mtimeMs": 1780000001000.25, + "nlink": 1, + "rdev": 0, + "size": 7, + "uid": 1000 +} \ No newline at end of file diff --git a/rivetkit-typescript/packages/engine-runner-protocol/test/exports-files-packaging.test.ts b/rivetkit-typescript/packages/engine-runner-protocol/test/exports-files-packaging.test.ts new file mode 100644 index 0000000000..190f77a835 --- /dev/null +++ b/rivetkit-typescript/packages/engine-runner-protocol/test/exports-files-packaging.test.ts @@ -0,0 +1,117 @@ +import { existsSync, readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { minimatch } from "minimatch"; +import { describe, expect, test } from "vitest"; + +/** + * Regression guard for rivetkit issue #3: "Missing CJS wrappers for declared exports". + * + * Root cause that remains: @rivetkit/engine-runner-protocol declares CJS export + * targets (require.default -> ./dist/index.cjs, require.types -> ./dist/index.d.cts) + * but its package.json `files` field is the restrictive glob + * ["dist/**\/*.js", "dist/**\/*.d.ts"], which excludes .cjs and .d.cts. As a result + * the published tarball does NOT contain the declared require target, so a CJS + * consumer doing `require("@rivetkit/engine-runner-protocol")` crashes with + * ERR_MODULE_NOT_FOUND / cannot find module ./dist/index.cjs. + * + * This is a purely STATIC check (no build / no network): it reads package.json, + * enumerates every leaf target path in `exports`, and asserts each one is included + * by the package's `files` field using npm's packing glob semantics. It encodes + * the CORRECT expected behavior, so it FAILS while the bug is present and will + * PASS once `files` is widened to ship the declared CJS artifacts. + */ + +const here = dirname(fileURLToPath(import.meta.url)); +const pkgDir = resolve(here, ".."); +const pkgJsonPath = join(pkgDir, "package.json"); + +interface PkgJson { + name: string; + exports?: unknown; + files?: string[]; +} + +const pkg: PkgJson = JSON.parse(readFileSync(pkgJsonPath, "utf8")); + +/** Recursively collect every leaf string value from an exports map. */ +function collectExportTargets(node: unknown, acc: Set): void { + if (typeof node === "string") { + // Only consider relative file targets (skip e.g. bare specifiers). + if (node.startsWith("./") || node.startsWith("dist/")) { + acc.add(node.replace(/^\.\//, "")); + } + return; + } + if (Array.isArray(node)) { + for (const child of node) collectExportTargets(child, acc); + return; + } + if (node && typeof node === "object") { + for (const value of Object.values(node as Record)) { + collectExportTargets(value, acc); + } + } +} + +/** + * Does the npm `files` field include `targetPath`? + * + * npm semantics: a bare path that names a directory (e.g. "dist") behaves like + * "dist/**" (the whole subtree is included). Otherwise the entry is treated as a + * glob and matched against the package-relative path. + */ +function filesIncludes(files: string[], targetPath: string): boolean { + for (const raw of files) { + const pattern = raw.replace(/^\.\//, "").replace(/\/$/, ""); + if (minimatch(targetPath, pattern)) return true; + // Bare directory name -> recursive include. + if (!pattern.includes("*")) { + if ( + targetPath === pattern || + targetPath.startsWith(`${pattern}/`) + ) { + return true; + } + } + } + return false; +} + +describe("engine-runner-protocol exports are shippable via files field", () => { + const targets = new Set(); + collectExportTargets(pkg.exports, targets); + const files = pkg.files ?? []; + + test("package declares exports and a files field", () => { + expect(targets.size).toBeGreaterThan(0); + expect(files.length).toBeGreaterThan(0); + }); + + for (const target of targets) { + test(`exports target "${target}" is included by the files field (so it is published)`, () => { + expect( + filesIncludes(files, target), + `package.json "files" (${JSON.stringify(files)}) does not include exports target "${target}". ` + + `A CJS/ESM consumer that resolves to "${target}" would crash with ERR_MODULE_NOT_FOUND ` + + `because the file is excluded from the published tarball (issue #3).`, + ).toBe(true); + }); + } + + test("declared exports targets exist on disk after build (when dist is present)", () => { + // Soft check: only assert disk presence if the package has been built. + const anyBuilt = [...targets].some((t) => existsSync(join(pkgDir, t))); + if (!anyBuilt) { + // Not built locally; the files-vs-exports static checks above are the + // authoritative guard for issue #3. + return; + } + for (const target of targets) { + expect( + existsSync(join(pkgDir, target)), + `exports target "${target}" is missing from dist after build`, + ).toBe(true); + } + }); +}); diff --git a/rivetkit-typescript/packages/rivetkit-napi/Cargo.toml b/rivetkit-typescript/packages/rivetkit-napi/Cargo.toml index 0dc83afd1a..8bde34d375 100644 --- a/rivetkit-typescript/packages/rivetkit-napi/Cargo.toml +++ b/rivetkit-typescript/packages/rivetkit-napi/Cargo.toml @@ -30,6 +30,8 @@ hex.workspace = true http.workspace = true rivet-error.workspace = true rivetkit-core = { workspace = true, features = ["sqlite"] } +rivetkit-agent-os = { path = "../../../rivetkit-rust/packages/rivetkit-agent-os" } +agent-os-client = { path = "../../../../agent-os/crates/client" } [build-dependencies] napi-build = "2" @@ -37,3 +39,6 @@ napi-build = "2" [dev-dependencies] rivetkit-actor-persist.workspace = true vbare.workspace = true +base64.workspace = true +ciborium.workspace = true +serde_bytes.workspace = true diff --git a/rivetkit-typescript/packages/rivetkit-napi/index.d.ts b/rivetkit-typescript/packages/rivetkit-napi/index.d.ts index abd33164cb..56a1ebaf36 100644 --- a/rivetkit-typescript/packages/rivetkit-napi/index.d.ts +++ b/rivetkit-typescript/packages/rivetkit-napi/index.d.ts @@ -100,6 +100,22 @@ export interface JsActorConfig { actions?: Array inspectorTabs?: Array } +export interface NapiAgentOsOptions { + /** + * JSON-encoded subset of `AgentOsConfig`. Fields that cannot be + * represented in JSON (e.g. `schedule_driver`, `MountConfig::driver`) + * are intentionally absent; passing them in the JSON envelope must + * fail loud (enforced by `deny_unknown_fields`). + */ + configJson?: string + /** + * Absolute path to the prebuilt `agent-os-sidecar` binary, resolved on + * the TypeScript side from the `@rivet-dev/agent-os-sidecar` npm package. + * Forwarded to the agent-os client via its `AGENT_OS_SIDECAR_BIN` env so + * the client spawns the bundled binary instead of relying on `PATH`. + */ + sidecarBinaryPath?: string +} export interface JsBindParam { kind: string intValue?: number @@ -272,6 +288,15 @@ export declare class ActorContext { } export declare class NapiActorFactory { constructor(callbacks: object, config?: JsActorConfig | undefined | null) + /** + * Static constructor that builds the agent-os actor factory. The + * factory wraps `rivetkit_agent_os::build_core_factory` and exposes + * no JS callbacks (the actor's run loop is owned by the Rust crate). + * + * `tool_callbacks` is accepted for forward compatibility with Phase 5 + * (toolkit dispatch). It is currently ignored. + */ + static fromAgentOs(options: NapiAgentOsOptions, toolCallbacks?: object | undefined | null): NapiActorFactory } export declare class CancellationToken { constructor() diff --git a/rivetkit-typescript/packages/rivetkit-napi/src/actor_factory.rs b/rivetkit-typescript/packages/rivetkit-napi/src/actor_factory.rs index c25e8c92ce..729c7eef40 100644 --- a/rivetkit-typescript/packages/rivetkit-napi/src/actor_factory.rs +++ b/rivetkit-typescript/packages/rivetkit-napi/src/actor_factory.rs @@ -342,6 +342,28 @@ impl NapiActorFactory { inner, }) } + + /// Static constructor that builds the agent-os actor factory. The + /// factory wraps `rivetkit_agent_os::build_core_factory` and exposes + /// no JS callbacks (the actor's run loop is owned by the Rust crate). + /// + /// `tool_callbacks` is accepted for forward compatibility with Phase 5 + /// (toolkit dispatch). It is currently ignored. + #[napi(factory)] + pub fn from_agent_os( + options: crate::agent_os::NapiAgentOsOptions, + _tool_callbacks: Option, + ) -> napi::Result { + crate::init_tracing(None); + let actor_config = crate::agent_os::parse_agent_os_options(options)?; + let inner = Arc::new(rivetkit_agent_os::build_core_factory(actor_config)); + let bindings = Arc::new(CallbackBindings::empty()); + tracing::debug!(class = "NapiActorFactory", "constructed via from_agent_os"); + Ok(Self { + _bindings: bindings, + inner, + }) + } } impl Drop for NapiActorFactory { @@ -375,6 +397,36 @@ impl AdapterConfig { } impl CallbackBindings { + /// Construct an empty `CallbackBindings` (no JS callbacks registered). + /// Used by foreign-runtime factories like agent-os where the actor's + /// event loop lives in Rust and never bridges back into JS. + pub(crate) fn empty() -> Self { + Self { + create_state: None, + on_create: None, + create_conn_state: None, + create_vars: None, + on_migrate: None, + on_wake: None, + on_before_actor_start: None, + on_sleep: None, + on_destroy: None, + on_before_connect: None, + on_connect: None, + on_disconnect_final: None, + on_before_subscribe: None, + actions: HashMap::new(), + on_before_action_response: None, + on_request: None, + on_queue_send: None, + on_websocket: None, + run: None, + get_workflow_history: None, + replay_workflow: None, + serialize_state: None, + } + } + fn from_js(callbacks: JsObject) -> napi::Result { let actions = if let Some(actions) = callbacks.get::<_, JsObject>("actions")? { let mut mapped = HashMap::new(); diff --git a/rivetkit-typescript/packages/rivetkit-napi/src/agent_os.rs b/rivetkit-typescript/packages/rivetkit-napi/src/agent_os.rs new file mode 100644 index 0000000000..0396a6a17d --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit-napi/src/agent_os.rs @@ -0,0 +1,154 @@ +//! Phase 1b: NAPI binding for the agent-os actor. +//! +//! Exposes `NapiAgentOsOptions` (`#[napi(object)]`) and the +//! `NapiActorFactory::from_agent_os` static constructor. The constructor +//! parses the JSON-envelope config with `serde(deny_unknown_fields)` so +//! non-serializable or unknown fields fail loud at construction time, +//! then builds a `CoreActorFactory` via `rivetkit_agent_os::build_core_factory`. + +use std::sync::Arc; + +use agent_os_client::{ + AgentOsConfig, AgentOsLimits, AgentOsSidecarConfig, MountConfig, MountPlugin, Permissions, + RootFilesystemConfig, SoftwareInput, +}; +use napi_derive::napi; +use rivetkit_agent_os::AgentOsActorConfig; + +use crate::NapiInvalidArgument; +use crate::napi_anyhow_error; + +#[napi(object)] +#[derive(Default)] +pub struct NapiAgentOsOptions { + /// JSON-encoded subset of `AgentOsConfig`. Fields that cannot be + /// represented in JSON (e.g. `schedule_driver`, `MountConfig::driver`) + /// are intentionally absent; passing them in the JSON envelope must + /// fail loud (enforced by `deny_unknown_fields`). + pub config_json: Option, + /// Absolute path to the prebuilt `agent-os-sidecar` binary, resolved on + /// the TypeScript side from the `@rivet-dev/agent-os-sidecar` npm package. + /// Forwarded to the agent-os client via its `AGENT_OS_SIDECAR_BIN` env so + /// the client spawns the bundled binary instead of relying on `PATH`. + pub sidecar_binary_path: Option, +} + +/// Serializable mirror of [`AgentOsConfig`] for the Phase 1b minimal scope. +/// `deny_unknown_fields` enforces fail-loud behavior when callers pass +/// fields outside this allow-list (including non-serializable fields like +/// `schedule_driver` or `driver` on mounts). +#[derive(serde::Deserialize, Default, Clone)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +struct AgentOsConfigJson { + #[serde(default)] + software: Vec, + #[serde(default)] + additional_instructions: Option, + #[serde(default)] + module_access_cwd: Option, + #[serde(default)] + loopback_exempt_ports: Vec, + #[serde(default)] + allowed_node_builtins: Option>, + #[serde(default)] + permissions: Option, + #[serde(default)] + mounts: Vec, + #[serde(default)] + root_filesystem: Option, + #[serde(default)] + limits: Option, + #[serde(default)] + sidecar: Option, +} + +#[derive(serde::Deserialize, Clone)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +struct NativeMountJson { + path: String, + plugin: MountPlugin, + #[serde(default)] + read_only: bool, +} + +#[derive(serde::Deserialize, Clone)] +#[serde(deny_unknown_fields, rename_all = "camelCase")] +struct SidecarJson { + #[serde(default)] + pool: Option, +} + +impl AgentOsConfigJson { + fn to_agent_os_config(&self) -> AgentOsConfig { + AgentOsConfig { + software: self.software.clone(), + loopback_exempt_ports: self.loopback_exempt_ports.clone(), + allowed_node_builtins: self.allowed_node_builtins.clone(), + module_access_cwd: self.module_access_cwd.clone(), + additional_instructions: self.additional_instructions.clone(), + permissions: self.permissions.clone(), + mounts: self + .mounts + .iter() + .map(|mount| MountConfig::Native { + path: mount.path.clone(), + plugin: mount.plugin.clone(), + read_only: mount.read_only, + }) + .collect(), + root_filesystem: self.root_filesystem.clone().unwrap_or_default(), + limits: self.limits.clone(), + sidecar: self + .sidecar + .as_ref() + .map(|sidecar| AgentOsSidecarConfig::Shared { + pool: sidecar.pool.clone(), + }), + ..AgentOsConfig::default() + } + } +} + +/// Parse `NapiAgentOsOptions` into an `AgentOsActorConfig` whose builder +/// closure produces a fresh `AgentOsConfig` per actor instance (because +/// `AgentOsConfig` is non-`Clone`). +pub(crate) fn parse_agent_os_options( + options: NapiAgentOsOptions, +) -> napi::Result { + // Forward the npm-resolved sidecar binary path to the agent-os client. The + // client reads `AGENT_OS_SIDECAR_BIN` when spawning the native sidecar, so + // setting it here makes the bundled binary authoritative for this process. + if let Some(path) = options.sidecar_binary_path.as_deref() { + if !path.is_empty() { + // SAFETY: runs once during factory construction at registry setup, + // before any VM (and thus any agent-os client thread that reads this + // var via `std::env::var`) is created. No other code reads + // `AGENT_OS_SIDECAR_BIN` concurrently with this write. + unsafe { + std::env::set_var("AGENT_OS_SIDECAR_BIN", path); + } + } + } + + let parsed: AgentOsConfigJson = match options.config_json.as_deref() { + Some(json) => serde_json::from_str(json).map_err(|error| { + napi_anyhow_error( + NapiInvalidArgument { + argument: "configJson".to_owned(), + reason: format!("agent-os config JSON parse error: {error}"), + } + .build(), + ) + })?, + None => AgentOsConfigJson::default(), + }; + let parsed = Arc::new(parsed); + Ok(AgentOsActorConfig::from_builder(move || { + parsed.to_agent_os_config() + })) +} + +// Test shim keeps moved tests in crate-root tests/ with private-module access. +#[cfg(test)] +#[path = "../tests/agent_os_factory.rs"] +mod tests; diff --git a/rivetkit-typescript/packages/rivetkit-napi/src/lib.rs b/rivetkit-typescript/packages/rivetkit-napi/src/lib.rs index 51e164eab6..ec40bfde52 100644 --- a/rivetkit-typescript/packages/rivetkit-napi/src/lib.rs +++ b/rivetkit-typescript/packages/rivetkit-napi/src/lib.rs @@ -1,5 +1,6 @@ pub mod actor_context; pub mod actor_factory; +pub mod agent_os; pub mod cancellation_token; pub mod connection; pub mod database; diff --git a/rivetkit-typescript/packages/rivetkit-napi/tests/agent_os_factory.rs b/rivetkit-typescript/packages/rivetkit-napi/tests/agent_os_factory.rs new file mode 100644 index 0000000000..8fc5770b4c --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit-napi/tests/agent_os_factory.rs @@ -0,0 +1,390 @@ +//! Phase 1b end-to-end gate. Constructs `NapiActorFactory` via the NAPI +//! `from_agent_os` factory method (the same path JS uses) and drives a +//! real action through the inner `CoreActorFactory` against a live +//! agent-os sidecar. Verifies: +//! +//! 1. `from_agent_os` builds a factory whose `start(...)` actually runs +//! the actor's event loop (not a no-op shell). +//! 2. A `writeFile` -> `readFile` round-trip dispatched through that loop +//! lands at `actions::dispatch` and replies with the wrapped +//! `["$Uint8Array", base64]` payload. +//! 3. The factory drains cleanly when the event channel closes. +//! +//! Sidecar-gated: skips when `AGENT_OS_SIDECAR_BIN` is unset. + +/// Pure parsing tests — no sidecar required. Phase 3 prep: verifies that +/// the JSON envelope sent by the TS shim actually round-trips through +/// `parse_agent_os_options` into an `AgentOsConfig` with the right fields. +mod parsing { + use crate::agent_os::{NapiAgentOsOptions, parse_agent_os_options}; + use agent_os_client::{ + AgentOsSidecarConfig, FsPermissions, MountConfig, PatternPermissions, RootFilesystemMode, + }; + + #[test] + fn parse_threads_software_through_to_agent_os_config() { + let options = NapiAgentOsOptions { + config_json: Some( + r#"{"software":[{"package":"node"},{"package":"python","version":"3.11"}]}"# + .to_owned(), + ), + sidecar_binary_path: None, + }; + let actor_config = parse_agent_os_options(options) + .expect("parse_agent_os_options ok with non-empty software"); + let agent_os_config = actor_config.build_options(); + assert_eq!( + agent_os_config.software.len(), + 2, + "software entries must be preserved across the bridge" + ); + assert_eq!(agent_os_config.software[0].package, "node"); + assert_eq!(agent_os_config.software[0].version, None); + assert_eq!(agent_os_config.software[1].package, "python"); + assert_eq!(agent_os_config.software[1].version.as_deref(), Some("3.11"),); + } + + #[test] + fn parse_preserves_all_supported_fields() { + let options = NapiAgentOsOptions { + config_json: Some( + r#"{ + "software": [{"package": "coreutils"}], + "additionalInstructions": "Be terse.", + "moduleAccessCwd": "/home/user/workspace", + "loopbackExemptPorts": [9000, 9001], + "allowedNodeBuiltins": ["fs", "path"], + "permissions": { + "fs": "deny", + "network": "allow" + }, + "mounts": [{ + "path": "/data", + "plugin": { + "id": "host_dir", + "config": { "hostPath": "/tmp/data" } + }, + "readOnly": true + }], + "rootFilesystem": { + "mode": "read-only", + "disableDefaultBaseLayer": true + }, + "limits": { + "resources": { "maxProcesses": 5 }, + "http": { "maxFetchResponseBytes": 1024 } + }, + "sidecar": { "pool": "zid" } + }"# + .to_owned(), + ), + sidecar_binary_path: None, + }; + let actor_config = parse_agent_os_options(options).expect("parse ok"); + let agent_os_config = actor_config.build_options(); + assert_eq!(agent_os_config.software.len(), 1); + assert_eq!( + agent_os_config.additional_instructions.as_deref(), + Some("Be terse."), + ); + assert_eq!( + agent_os_config.module_access_cwd.as_deref(), + Some("/home/user/workspace"), + ); + assert_eq!(agent_os_config.loopback_exempt_ports, vec![9000, 9001]); + assert_eq!( + agent_os_config.allowed_node_builtins.as_deref(), + Some(&["fs".to_owned(), "path".to_owned()][..]), + ); + assert!(matches!( + agent_os_config + .permissions + .as_ref() + .and_then(|p| p.fs.as_ref()), + Some(FsPermissions::Mode(agent_os_client::PermissionMode::Deny)) + )); + assert!(matches!( + agent_os_config + .permissions + .as_ref() + .and_then(|p| p.network.as_ref()), + Some(PatternPermissions::Mode( + agent_os_client::PermissionMode::Allow + )) + )); + assert_eq!(agent_os_config.mounts.len(), 1); + let MountConfig::Native { + path, + plugin, + read_only, + } = &agent_os_config.mounts[0] + else { + panic!("expected native mount"); + }; + assert_eq!(path, "/data"); + assert_eq!(plugin.id, "host_dir"); + assert_eq!( + plugin + .config + .as_ref() + .and_then(|config| config.get("hostPath")) + .and_then(|value| value.as_str()), + Some("/tmp/data"), + ); + assert!(*read_only); + assert_eq!( + agent_os_config.root_filesystem.mode, + Some(RootFilesystemMode::ReadOnly) + ); + assert!(agent_os_config.root_filesystem.disable_default_base_layer); + assert_eq!( + agent_os_config + .limits + .as_ref() + .and_then(|limits| limits.resources.as_ref()) + .and_then(|resources| resources.max_processes), + Some(5) + ); + assert!(matches!( + agent_os_config.sidecar, + Some(AgentOsSidecarConfig::Shared { + pool: Some(ref pool) + }) if pool == "zid" + )); + } + + #[test] + fn parse_builder_produces_fresh_config_each_call() { + // AgentOsConfig is non-Clone, so the builder must produce a fresh + // value per invocation. Each VM bring-up calls the builder again. + let options = NapiAgentOsOptions { + config_json: Some(r#"{"software":[{"package":"node"}]}"#.to_owned()), + sidecar_binary_path: None, + }; + let actor_config = parse_agent_os_options(options).expect("parse ok"); + let first = actor_config.build_options(); + let second = actor_config.build_options(); + assert_eq!(first.software.len(), 1); + assert_eq!(second.software.len(), 1); + assert_eq!(first.software[0].package, second.software[0].package); + } + + #[test] + fn empty_config_yields_empty_software_list() { + let options = NapiAgentOsOptions { + config_json: Some("{}".to_owned()), + sidecar_binary_path: None, + }; + let actor_config = parse_agent_os_options(options).expect("parse ok"); + let agent_os_config = actor_config.build_options(); + assert!(agent_os_config.software.is_empty()); + assert!(agent_os_config.additional_instructions.is_none()); + } +} + +mod e2e { + use std::io::Cursor; + use std::path::PathBuf; + + use base64::Engine as _; + use base64::engine::general_purpose::STANDARD as BASE64; + use rivetkit_core::{ActorContext, ActorEvent, ActorStart, Reply}; + use tokio::sync::{mpsc, oneshot}; + + use crate::actor_factory::NapiActorFactory; + use crate::agent_os::NapiAgentOsOptions; + + fn sidecar_available() -> bool { + if std::env::var("AGENT_OS_SIDECAR_BIN").is_err() { + let candidate = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../../target/debug/agent-os-sidecar"); + if candidate.exists() { + // SAFETY: tests run single-process; env mutation here is fine. + unsafe { + std::env::set_var("AGENT_OS_SIDECAR_BIN", candidate); + } + } + } + std::env::var("AGENT_OS_SIDECAR_BIN") + .map(|path| PathBuf::from(path).exists()) + .unwrap_or(false) + } + + fn encode_cbor(value: &T) -> Vec { + let mut buf = Vec::new(); + ciborium::into_writer(value, &mut buf).expect("encode CBOR"); + buf + } + + #[tokio::test] + async fn napi_factory_dispatches_write_then_read_file() { + if !sidecar_available() { + eprintln!("skipping: AGENT_OS_SIDECAR_BIN not present"); + return; + } + + // Build the factory the same way JS does: through the NAPI + // `from_agent_os` static. The underlying Rust fn is callable + // directly; `#[napi(factory)]` only adds the JS export. + let napi_factory = NapiActorFactory::from_agent_os( + NapiAgentOsOptions { + config_json: Some("{}".to_owned()), + sidecar_binary_path: None, + }, + None, + ) + .expect("from_agent_os ok"); + let core_factory = napi_factory.actor_factory(); + + // Queue two actions on the actor's event channel: + // 1. writeFile(path, bytes) + // 2. readFile(path) — must return the bytes from step 1. + let path = "/home/user/napi-factory-roundtrip.txt"; + let payload = b"NAPI factory e2e payload".to_vec(); + let write_args = + encode_cbor(&(path.to_owned(), serde_bytes::ByteBuf::from(payload.clone()))); + let read_args = encode_cbor(&(path.to_owned(),)); + + let (write_reply_tx, write_reply_rx) = oneshot::channel(); + let (read_reply_tx, read_reply_rx) = oneshot::channel(); + let (event_tx, event_rx) = mpsc::unbounded_channel::(); + event_tx + .send(ActorEvent::Action { + name: "writeFile".to_owned(), + args: write_args, + conn: None, + reply: Reply::from(write_reply_tx), + }) + .expect("queue writeFile"); + event_tx + .send(ActorEvent::Action { + name: "readFile".to_owned(), + args: read_args, + conn: None, + reply: Reply::from(read_reply_tx), + }) + .expect("queue readFile"); + + let (startup_tx, startup_rx) = oneshot::channel(); + let start = ActorStart { + ctx: ActorContext::new("napi-factory-e2e", "agent-os", Vec::new(), "local"), + is_new: true, + input: None, + snapshot: None, + hibernated: Vec::new(), + events: event_rx.into(), + startup_ready: Some(startup_tx), + }; + + // Spawn the factory's entry future. It drives ensure_vm on the + // first action, then dispatches both actions in order. + let factory = core_factory.clone(); + let join = tokio::spawn(async move { factory.start(start).await }); + + // Confirm startup before draining replies. + startup_rx + .await + .expect("recv startup signal") + .expect("startup ok"); + + // writeFile replies with `()` — CBOR-encoded as `null` (0xF6). + // We don't assert the exact bytes; just that the reply arrived + // without error (the round-trip is verified by readFile below). + let write_reply = write_reply_rx + .await + .expect("recv writeFile reply") + .expect("writeFile ok"); + assert!( + !write_reply.is_empty(), + "writeFile reply should encode at least the unit value" + ); + + // readFile reply: ciborium decode -> ["$Uint8Array", base64]. + let read_reply = read_reply_rx + .await + .expect("recv readFile reply") + .expect("readFile ok"); + let intermediate: serde_json::Value = + ciborium::from_reader(Cursor::new(&read_reply)).expect("decode readFile reply CBOR"); + assert!( + intermediate.is_array(), + "expected wrapped Uint8Array, got {intermediate:?}" + ); + assert_eq!(intermediate[0], "$Uint8Array"); + let base64 = intermediate[1].as_str().expect("base64 element"); + let decoded = BASE64.decode(base64).expect("decode base64"); + assert_eq!(decoded, payload, "readFile bytes match writeFile bytes"); + + // Drain the loop: closing the event channel triggers the loop's + // shutdown path (shutdown_vm with reason="error"). + drop(event_tx); + let _ = tokio::time::timeout(std::time::Duration::from_secs(30), join) + .await + .expect("factory task joins within 30s") + .expect("factory task didn't panic"); + } + + #[tokio::test] + async fn napi_factory_rejects_unknown_action_through_loop() { + if !sidecar_available() { + eprintln!("skipping: AGENT_OS_SIDECAR_BIN not present"); + return; + } + + let napi_factory = NapiActorFactory::from_agent_os( + NapiAgentOsOptions { + config_json: Some("{}".to_owned()), + sidecar_binary_path: None, + }, + None, + ) + .expect("from_agent_os ok"); + let core_factory = napi_factory.actor_factory(); + + let (reply_tx, reply_rx) = oneshot::channel(); + let (event_tx, event_rx) = mpsc::unbounded_channel::(); + let args = encode_cbor(&Vec::::new()); + event_tx + .send(ActorEvent::Action { + name: "totallyMadeUp".to_owned(), + args, + conn: None, + reply: Reply::from(reply_tx), + }) + .expect("queue action"); + + let (startup_tx, startup_rx) = oneshot::channel(); + let start = ActorStart { + ctx: ActorContext::new("napi-factory-unknown", "agent-os", Vec::new(), "local"), + is_new: true, + input: None, + snapshot: None, + hibernated: Vec::new(), + events: event_rx.into(), + startup_ready: Some(startup_tx), + }; + + let factory = core_factory.clone(); + let join = tokio::spawn(async move { factory.start(start).await }); + startup_rx + .await + .expect("recv startup signal") + .expect("startup ok"); + + let error = reply_rx + .await + .expect("recv reply") + .expect_err("unknown action should error"); + let msg = error.to_string(); + assert!( + msg.contains("not implemented yet") || msg.contains("totallyMadeUp"), + "expected not-implemented error, got: {msg}" + ); + + drop(event_tx); + let _ = tokio::time::timeout(std::time::Duration::from_secs(30), join) + .await + .expect("factory task joins within 30s") + .expect("factory task didn't panic"); + } +} diff --git a/rivetkit-typescript/packages/rivetkit/fixtures/agent-os-mock-modules/@rivet-dev/agent-os-opencode/mock-acp-adapter.mjs b/rivetkit-typescript/packages/rivetkit/fixtures/agent-os-mock-modules/@rivet-dev/agent-os-opencode/mock-acp-adapter.mjs new file mode 100644 index 0000000000..aa10a75e7b --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/fixtures/agent-os-mock-modules/@rivet-dev/agent-os-opencode/mock-acp-adapter.mjs @@ -0,0 +1,111 @@ +let buffer = ""; +let nextSession = 1; +let activeCwd = null; + +const scenario = process.env.MOCK_RESUME_SCENARIO || "native"; +const cwdEnvProbe = process.env.MOCK_CWD_ENV_PROBE || ""; + +const modes = { + currentModeId: "default", + availableModes: [{ id: "default", label: "Default" }], +}; + +function write(value) { + process.stdout.write(`${JSON.stringify(value)}\n`); +} + +function response(id, result) { + write({ jsonrpc: "2.0", id, result }); +} + +function error(id, code, message, data) { + write({ jsonrpc: "2.0", id, error: { code, message, data } }); +} + +function notification(method, params) { + write({ jsonrpc: "2.0", method, params }); +} + +function sessionId(prefix) { + return `${prefix}-${process.pid}-${nextSession++}`; +} + +process.stdin.resume(); +process.stdin.on("data", (chunk) => { + buffer += chunk instanceof Uint8Array ? new TextDecoder().decode(chunk) : String(chunk); + + while (true) { + const newline = buffer.indexOf("\n"); + if (newline === -1) break; + const line = buffer.slice(0, newline); + buffer = buffer.slice(newline + 1); + if (!line.trim()) continue; + + const msg = JSON.parse(line); + switch (msg.method) { + case "initialize": { + const agentCapabilities = { promptCapabilities: {} }; + if (scenario !== "no-loadsession") agentCapabilities.loadSession = true; + response(msg.id, { + protocolVersion: 1, + agentInfo: { name: "rivetkit-mock-opencode", version: "0.0.0-test" }, + agentCapabilities, + modes, + }); + break; + } + case "session/new": + activeCwd = msg.params?.cwd ?? null; + response(msg.id, { sessionId: sessionId("mock-live"), modes }); + break; + case "session/load": + activeCwd = msg.params?.cwd ?? null; + if (scenario === "native") { + response(msg.id, { modes }); + } else { + error(msg.id, -32603, "Internal error", { details: "NotFoundError" }); + } + break; + case "session/prompt": { + const sid = msg.params?.sessionId; + const blocks = Array.isArray(msg.params?.prompt) ? msg.params.prompt : []; + const outputBlocks = cwdEnvProbe + ? [ + ...blocks, + { + type: "probe", + text: JSON.stringify({ + cwd: activeCwd, + env: cwdEnvProbe, + }), + }, + ] + : blocks; + notification("session/update", { + sessionId: sid, + update: { + sessionUpdate: "tool_call", + title: "fixture/read", + status: "completed", + content: [{ type: "content", content: { type: "text", text: "tool-call-captured" } }], + }, + }); + notification("session/update", { + sessionId: sid, + update: { + sessionUpdate: "agent_message_chunk", + content: { type: "text", text: JSON.stringify(outputBlocks) }, + }, + }); + response(msg.id, { stopReason: "end_turn" }); + break; + } + case "session/cancel": + response(msg.id, {}); + break; + default: + error(msg.id, -32601, "Method not found", { method: msg.method }); + break; + } + } +}); diff --git a/rivetkit-typescript/packages/rivetkit/fixtures/agent-os-mock-modules/@rivet-dev/agent-os-opencode/package.json b/rivetkit-typescript/packages/rivetkit/fixtures/agent-os-mock-modules/@rivet-dev/agent-os-opencode/package.json new file mode 100644 index 0000000000..1713bff680 --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/fixtures/agent-os-mock-modules/@rivet-dev/agent-os-opencode/package.json @@ -0,0 +1,8 @@ +{ + "name": "@rivet-dev/agent-os-opencode", + "version": "0.0.0-test", + "type": "module", + "bin": { + "agent-os-opencode-mock": "./mock-acp-adapter.mjs" + } +} diff --git a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/agent-os.ts b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/agent-os.ts index 883965a50c..7775abe8e0 100644 --- a/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/agent-os.ts +++ b/rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/agent-os.ts @@ -1,4 +1,27 @@ +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; import common from "@rivet-dev/agent-os-common"; -import { agentOs } from "rivetkit/agent-os"; +import { agentOs, nodeModulesMount } from "rivetkit/agent-os"; -export const agentOsTestActor = agentOs({ options: { software: [common] } }); +const here = dirname(fileURLToPath(import.meta.url)); +const mockNodeModules = join(here, "..", "agent-os-mock-modules"); +const mockOpencodePackage = join( + mockNodeModules, + "@rivet-dev", + "agent-os-opencode", +); +const mockOpencode = { + packageDir: mockOpencodePackage, + agent: { + id: "opencode", + acpAdapter: "@rivet-dev/agent-os-opencode", + agentPackage: "@rivet-dev/agent-os-opencode", + }, +}; + +export const agentOsTestActor = agentOs({ + options: { + software: [common, mockOpencode], + mounts: [nodeModulesMount(mockNodeModules)], + }, +}); diff --git a/rivetkit-typescript/packages/rivetkit/package.json b/rivetkit-typescript/packages/rivetkit/package.json index 4b864ab335..896edc7d5b 100644 --- a/rivetkit-typescript/packages/rivetkit/package.json +++ b/rivetkit-typescript/packages/rivetkit/package.json @@ -191,7 +191,7 @@ "@hono/node-server": "^1.18.2", "@hono/node-ws": "^1.1.1", "@hono/zod-openapi": "^1.1.5", - "@rivet-dev/agent-os-core": "^0.1.1", + "@rivet-dev/agent-os-core": "link:../../../../agent-os/packages/core", "@rivetkit/bare-ts": "^0.6.2", "@rivetkit/engine-cli": "workspace:*", "@rivetkit/engine-envoy-protocol": "workspace:*", @@ -210,13 +210,14 @@ "pino": "^9.5.0", "uuid": "^12.0.0", "vbare": "^0.0.4", - "zod": "^4.1.0" + "zod": "^4.1.0", + "@rivet-dev/agent-os-sidecar": "link:../../../../agent-os/packages/sidecar-binary" }, "devDependencies": { "@biomejs/biome": "^2.3", "@copilotkit/llmock": "^1.6.0", - "@rivet-dev/agent-os-common": "*", - "@rivet-dev/agent-os-pi": "^0.1.1", + "@rivet-dev/agent-os-pi": "link:../../../../agent-os/registry/agent/pi", + "@rivet-dev/agent-os-common": "0.0.260331072558", "@standard-schema/spec": "^1.0.0", "@types/invariant": "^2", "@types/node": "^22.13.1", diff --git a/rivetkit-typescript/packages/rivetkit/src/actor/definition.ts b/rivetkit-typescript/packages/rivetkit/src/actor/definition.ts index b741b13dca..8f5ac86f04 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/definition.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/definition.ts @@ -1,5 +1,6 @@ import type { AnyDatabaseProvider } from "@/common/database/config"; import type { RegistryConfig } from "@/registry/config"; +import type { ActorFactoryHandle, CoreRuntime } from "@/registry/runtime"; import { type Actions, type ActorConfig, @@ -49,6 +50,16 @@ export interface BaseActorDefinition< export interface AnyActorDefinition { readonly config: any; + /** + * Marker for foreign-runtime factories (e.g. `agentOs(...)`). When set, + * the registry-build ladder calls this closure with the active + * `CoreRuntime` to obtain an `ActorFactoryHandle` directly, bypassing + * the normal JS-callbacks factory built from `actor(...)`. + * + * Set by the Rust-backed `agentOs()` definition; read by + * `CoreRuntime::registerActor` and by the engine actor-driver. + */ + nativeFactoryBuilder?: (runtime: CoreRuntime) => ActorFactoryHandle; } export type AnyStaticActorDefinition = ActorDefinition< @@ -85,6 +96,12 @@ export class ActorDefinition< > implements BaseActorDefinition { #config: ActorConfig; + /** + * Foreign-runtime factory marker. See [`AnyActorDefinition.nativeFactoryBuilder`]. + * Defaults to `undefined`; the Rust-backed `agentOs(...)` definition sets it + * via direct property assignment after construction. + */ + nativeFactoryBuilder?: (runtime: CoreRuntime) => ActorFactoryHandle; constructor(config: ActorConfig) { this.#config = config; diff --git a/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/cron.ts b/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/cron.ts deleted file mode 100644 index 053b850e40..0000000000 --- a/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/cron.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { CronAction, CronJobInfo } from "@rivet-dev/agent-os-core"; -import type { AgentOsActorConfig } from "../config"; -import type { - AgentOsActionContext, - SerializableCronAction, - SerializableCronJobInfo, - SerializableCronJobOptions, -} from "../types"; -import { ensureVm } from "./index"; - -function serializeCronAction(action: CronAction): SerializableCronAction { - switch (action.type) { - case "session": - return { - type: "session", - agentType: action.agentType, - prompt: action.prompt, - cwd: action.options?.cwd, - }; - case "exec": - return { - type: "exec", - command: action.command, - args: action.args, - }; - case "callback": - throw new TypeError("callback cron actions are not serializable"); - } -} - -function serializeCronJob(job: CronJobInfo): SerializableCronJobInfo { - return { - id: job.id, - schedule: job.schedule, - action: serializeCronAction(job.action), - overlap: job.overlap, - lastRun: job.lastRun?.toISOString(), - nextRun: job.nextRun?.toISOString(), - runCount: job.runCount, - running: job.running, - }; -} - -// Build cron scheduling actions for the actor factory. -export function buildCronActions( - config: AgentOsActorConfig, -) { - return { - scheduleCron: async ( - c: AgentOsActionContext, - options: SerializableCronJobOptions, - ): Promise<{ id: string }> => { - const agentOs = await ensureVm(c, config); - const job = agentOs.scheduleCron({ - id: options.id, - schedule: options.schedule, - action: options.action as CronAction, - overlap: options.overlap, - }); - c.log.info({ - msg: "agent-os cron job scheduled", - jobId: job.id, - schedule: options.schedule, - }); - return { id: job.id }; - }, - - listCronJobs: async ( - c: AgentOsActionContext, - ): Promise => { - const agentOs = await ensureVm(c, config); - return agentOs.listCronJobs().map(serializeCronJob); - }, - - cancelCronJob: async ( - c: AgentOsActionContext, - id: string, - ): Promise => { - const agentOs = await ensureVm(c, config); - agentOs.cancelCronJob(id); - c.log.info({ msg: "agent-os cron job cancelled", jobId: id }); - }, - }; -} diff --git a/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/db.ts b/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/db.ts deleted file mode 100644 index e1b1374f59..0000000000 --- a/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/db.ts +++ /dev/null @@ -1,54 +0,0 @@ -import type { RawAccess } from "@/common/database/config"; - -export async function migrateAgentOsTables(db: RawAccess): Promise { - await db.execute(` - CREATE TABLE IF NOT EXISTS agent_os_preview_tokens ( - token TEXT PRIMARY KEY, - port INTEGER NOT NULL, - created_at INTEGER NOT NULL, - expires_at INTEGER NOT NULL - ); - - CREATE INDEX IF NOT EXISTS idx_preview_tokens_expires_at - ON agent_os_preview_tokens(expires_at); - - CREATE TABLE IF NOT EXISTS agent_os_fs_entries ( - path TEXT PRIMARY KEY, - is_directory INTEGER NOT NULL DEFAULT 0, - content BLOB, - mode INTEGER NOT NULL DEFAULT 33188, - uid INTEGER NOT NULL DEFAULT 0, - gid INTEGER NOT NULL DEFAULT 0, - size INTEGER NOT NULL DEFAULT 0, - atime_ms INTEGER NOT NULL, - mtime_ms INTEGER NOT NULL, - ctime_ms INTEGER NOT NULL, - birthtime_ms INTEGER NOT NULL, - symlink_target TEXT, - nlink INTEGER NOT NULL DEFAULT 1 - ); - - CREATE INDEX IF NOT EXISTS idx_fs_entries_parent - ON agent_os_fs_entries(path); - - CREATE TABLE IF NOT EXISTS agent_os_sessions ( - session_id TEXT PRIMARY KEY, - agent_type TEXT NOT NULL, - capabilities TEXT NOT NULL, - agent_info TEXT, - created_at INTEGER NOT NULL - ); - - CREATE TABLE IF NOT EXISTS agent_os_session_events ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT NOT NULL, - seq INTEGER NOT NULL, - event TEXT NOT NULL, - created_at INTEGER NOT NULL, - FOREIGN KEY (session_id) REFERENCES agent_os_sessions(session_id) ON DELETE CASCADE - ); - - CREATE INDEX IF NOT EXISTS idx_session_events_session_seq - ON agent_os_session_events(session_id, seq); - `); -} diff --git a/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/filesystem.ts b/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/filesystem.ts deleted file mode 100644 index 2f17d14388..0000000000 --- a/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/filesystem.ts +++ /dev/null @@ -1,131 +0,0 @@ -import type { - AgentRegistryEntry, - BatchReadResult, - BatchWriteEntry, - BatchWriteResult, - DirEntry, - ReaddirRecursiveOptions, -} from "@rivet-dev/agent-os-core"; -import type { AgentOsActorConfig } from "../config"; -import type { AgentOsActionContext } from "../types"; -import { ensureVm } from "./index"; - -// Infer types from AgentOs methods since @secure-exec/core is not a direct dep. -type VirtualStat = Awaited< - ReturnType ->; -type DeleteOptions = Parameters< - import("@rivet-dev/agent-os-core").AgentOs["delete"] ->[1]; - -// Build filesystem and agent registry actions for the actor factory. -export function buildFilesystemActions( - config: AgentOsActorConfig, -) { - return { - readFile: async ( - c: AgentOsActionContext, - path: string, - ): Promise => { - const agentOs = await ensureVm(c, config); - return agentOs.readFile(path); - }, - - writeFile: async ( - c: AgentOsActionContext, - path: string, - content: string | Uint8Array, - ): Promise => { - const agentOs = await ensureVm(c, config); - await agentOs.writeFile(path, content); - }, - - readFiles: async ( - c: AgentOsActionContext, - paths: string[], - ): Promise => { - const agentOs = await ensureVm(c, config); - return agentOs.readFiles(paths); - }, - - writeFiles: async ( - c: AgentOsActionContext, - entries: BatchWriteEntry[], - ): Promise => { - const agentOs = await ensureVm(c, config); - return agentOs.writeFiles(entries); - }, - - mkdir: async ( - c: AgentOsActionContext, - path: string, - options?: { recursive?: boolean }, - ): Promise => { - const agentOs = await ensureVm(c, config); - await agentOs.mkdir(path, options); - }, - - readdir: async ( - c: AgentOsActionContext, - path: string, - ): Promise => { - const agentOs = await ensureVm(c, config); - return agentOs.readdir(path); - }, - - readdirRecursive: async ( - c: AgentOsActionContext, - path: string, - options?: ReaddirRecursiveOptions, - ): Promise => { - const agentOs = await ensureVm(c, config); - return agentOs.readdirRecursive(path, options); - }, - - stat: async ( - c: AgentOsActionContext, - path: string, - ): Promise => { - const agentOs = await ensureVm(c, config); - return agentOs.stat(path); - }, - - exists: async ( - c: AgentOsActionContext, - path: string, - ): Promise => { - const agentOs = await ensureVm(c, config); - return agentOs.exists(path); - }, - - move: async ( - c: AgentOsActionContext, - from: string, - to: string, - ): Promise => { - const agentOs = await ensureVm(c, config); - await agentOs.move(from, to); - }, - - deleteFile: async ( - c: AgentOsActionContext, - path: string, - options?: DeleteOptions, - ): Promise => { - const agentOs = await ensureVm(c, config); - await agentOs.delete(path, options); - }, - - // TODO: mountFs and unmountFs are not exposed as actor actions because - // filesystem drivers (VirtualFileSystem) are not serializable over the - // network. Mount filesystems via the `options.mounts` config in agentOs() - // instead. See: https://github.com/rivet-dev/rivet/issues/XXXX - - listAgents: async ( - c: AgentOsActionContext, - ): Promise => { - const agentOs = await ensureVm(c, config); - return agentOs.listAgents(); - }, - }; -} diff --git a/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/index.ts b/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/index.ts index 9facc5fd39..4a9e15bf10 100644 --- a/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/index.ts +++ b/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/index.ts @@ -1,285 +1,280 @@ -import type { AgentOsOptions, MountConfig } from "@rivet-dev/agent-os-core"; -import { AgentOs, createInMemoryFileSystem } from "@rivet-dev/agent-os-core"; -import { actor, event, type ActorDefinition } from "@/actor/mod"; +/** + * Rust-backed `agentOs(...)` definition (Phase 1c+). + * + * Produces an `ActorDefinition` whose `nativeFactoryBuilder` constructs a + * `CoreActorFactory` through `runtime.createAgentOsFactory(...)` (NAPI → + * `rivetkit_agent_os::build_core_factory`). All lifecycle, state, and + * action dispatch live in the Rust crate. The JS shim only validates + * configuration and hands it across the bridge. + */ + +import { getSidecarPath } from "@rivet-dev/agent-os-sidecar"; +import { actor, type ActorDefinition } from "@/actor/mod"; import type { DatabaseProvider, RawAccess } from "@/common/database/config"; -import { db } from "@/common/database/mod"; +import type { + ActorFactoryHandle, + CoreRuntime, + NapiAgentOsOptions, +} from "@/registry/runtime"; import { type AgentOsActorConfig, type AgentOsActorConfigInput, agentOsActorConfigSchema, } from "../config"; -import type { - AgentOsActionContext, - AgentOsActorState, - AgentOsActorVars, - CronEventPayload, - PermissionRequestPayload, - ProcessExitPayload, - ProcessOutputPayload, - SessionEventPayload, - ShellDataPayload, - VmBootedPayload, - VmShutdownPayload, -} from "../types"; -import { buildCronActions } from "./cron"; -import { migrateAgentOsTables } from "./db"; -import { buildFilesystemActions } from "./filesystem"; -import { buildNetworkActions } from "./network"; -import { buildOnRequestHandler, buildPreviewActions } from "./preview"; -import { buildProcessActions } from "./process"; -import { - buildConfigActions, - buildPromptActions, - buildSessionActions, - buildSessionPersistenceActions, -} from "./session"; -import { buildShellActions } from "./shell"; - -// --- VM lifecycle helpers --- - -async function ensureVm( - c: AgentOsActionContext, - config: AgentOsActorConfig, -): Promise { - if (c.vars.agentOs) { - return c.vars.agentOs; - } - - const start = Date.now(); - - // Build options with in-memory VFS as default working directory mount. - const options = buildVmOptions(config.options); - - const agentOs = await AgentOs.create(options); - c.vars.agentOs = agentOs; - - // Wire cron events to actor events. - agentOs.onCronEvent((cronEvent) => { - c.broadcast("cronEvent", { event: cronEvent }); - }); - - c.broadcast("vmBooted", {}); - c.log.info({ - msg: "agent-os vm booted", - bootDurationMs: Date.now() - start, - }); +import type { AgentOsActorState, AgentOsActorVars } from "../types"; - return agentOs; +/** + * Build the JSON envelope the Rust crate consumes. The Rust deserializer + * uses `deny_unknown_fields`, so the envelope must stay in lock-step + * with `agent_os.rs::AgentOsConfigJson`. + * + * Software threading: each software descriptor is flattened (meta packages + * such as `common` are arrays of descriptors) and mapped to the Rust + * `SoftwareInput { package, kind }`. The agent-os-client resolves an + * ABSOLUTE `package` directly (its `resolve_software` lets an absolute path + * bypass the `node_modules` prefix), so the descriptor's already-resolved + * `commandDir` (wasm commands) / `packageDir` (agents/tools) is forwarded as + * `package`. `build_command_mounts` then mounts each wasm dir at + * `/__agentos/commands/{N}/`, which is what makes `exec`/shell work. + */ +interface SoftwareDescriptorLike { + commandDir?: string; + packageDir?: string; + agent?: unknown; + hostTool?: unknown; + toolkit?: unknown; } -function buildVmOptions(userOptions?: AgentOsOptions): AgentOsOptions { - const userMounts = userOptions?.mounts ?? []; - - // Check if the user already provided a mount at /home/user. If so, respect - // their override and skip the default in-memory VFS mount. - const hasWorkdirMount = userMounts.some( - (m: MountConfig) => m.path === "/home/user", - ); - - if (hasWorkdirMount) { - return userOptions ?? {}; - } - - // TODO: Reimplement with persistent backend (actor KV-backed metadata + - // actor storage-backed blocks) so VM filesystem state survives sleep/wake. - const memMount: MountConfig = { - path: "/home/user", - driver: createInMemoryFileSystem(), +interface NativeMountLike { + path: string; + plugin: { + id: string; + config?: unknown; }; + readOnly?: boolean; +} +/** + * A native `host_dir` mount of a host `node_modules` directory at + * `/root/node_modules`, the serializable form `agentOs({ options: { mounts } })` + * accepts across the NAPI boundary. + */ +export interface NodeModulesMountConfig { + path: "/root/node_modules"; + plugin: { id: "host_dir"; config: { hostPath: string; readOnly: boolean } }; + readOnly: boolean; +} + +/** + * Mount a host `node_modules` directory into the VM at `/root/node_modules`. + * + * This is the explicit, mount-based replacement for the removed `moduleAccessCwd` + * / `AGENT_OS_MODULE_ACCESS_CWD` mechanism: the VM module resolver reads the + * mounted tree through the kernel VFS, so the caller supplies exactly the + * `node_modules` directory whose packages should resolve in the guest. + * + * @param hostNodeModulesDir Absolute host path to a `node_modules` directory. + * @param opts.readOnly Defaults to `true`; the mount is read-only. + */ +export function nodeModulesMount( + hostNodeModulesDir: string, + opts?: { readOnly?: boolean }, +): NodeModulesMountConfig { + const readOnly = opts?.readOnly ?? true; return { - ...userOptions, - mounts: [memMount, ...userMounts], + path: "/root/node_modules", + plugin: { + id: "host_dir", + config: { hostPath: hostNodeModulesDir, readOnly }, + }, + readOnly, }; } -// --- Prevent-sleep coordination --- +function flattenSoftware(input: unknown, out: SoftwareDescriptorLike[]): void { + if (input == null) return; + if (Array.isArray(input)) { + for (const item of input) flattenSoftware(item, out); + return; + } + if (typeof input === "object") out.push(input as SoftwareDescriptorLike); +} -function syncPreventSleep( - c: AgentOsActionContext, -): void { - const shouldPrevent = - c.vars.activeSessionIds.size > 0 || - c.vars.activeProcesses.size > 0 || - c.vars.activeHooks.size > 0 || - c.vars.activeShells.size > 0; +export function buildConfigJson( + parsed: AgentOsActorConfig, +): string { + const descriptors: SoftwareDescriptorLike[] = []; + flattenSoftware( + (parsed.options as { software?: unknown })?.software, + descriptors, + ); - c.setPreventSleep(shouldPrevent); + const software: Array<{ package: string; kind?: string }> = []; + for (const d of descriptors) { + if (typeof d.commandDir === "string") { + // Wasm command directory (kind defaults to WasmCommands on the Rust side). + software.push({ package: d.commandDir }); + } else if (typeof d.packageDir === "string") { + // Agent SDK / host-tool package: forwarded but not mounted as commands. + // `kind` matches the kebab-case serde tags of the Rust `SoftwareKind` + // enum (`wasm-commands` / `agent` / `tool`). + software.push({ + package: d.packageDir, + kind: d.hostTool || d.toolkit ? "tool" : "agent", + }); + } + } - c.log.info({ - msg: "agent-os prevent sleep sync", - preventSleep: shouldPrevent, - activeSessions: c.vars.activeSessionIds.size, - activeProcesses: c.vars.activeProcesses.size, - activeHooks: c.vars.activeHooks.size, - activeShells: c.vars.activeShells.size, + // `/root/node_modules` (agent SDK + transitive dep resolution) is now supplied + // explicitly by the client via `options.mounts` (see `nodeModulesMount(...)`), + // not derived from a host cwd. The VM module resolver reads the mounted tree + // through the kernel VFS. There is no `moduleAccessCwd` / `AGENT_OS_MODULE_ACCESS_CWD`. + const options = (parsed.options ?? {}) as Record; + const mounts = serializeNativeMounts(options.mounts); + const sidecar = serializeSidecar(options.sidecar); + return JSON.stringify({ + software, + additionalInstructions: options.additionalInstructions, + loopbackExemptPorts: options.loopbackExemptPorts, + allowedNodeBuiltins: options.allowedNodeBuiltins, + permissions: options.permissions, + rootFilesystem: options.rootFilesystem, + mounts, + limits: options.limits, + sidecar, }); } -// --- Hook tracking --- +function serializeNativeMounts(input: unknown): NativeMountLike[] | undefined { + if (input == null) return undefined; + if (!Array.isArray(input)) { + throw new Error("agentOs() options.mounts must be an array"); + } + return input.map((mount, index) => { + if (!mount || typeof mount !== "object") { + throw new Error( + `agentOs() options.mounts[${index}] must be an object`, + ); + } + const record = mount as Record; + if (record.driver !== undefined) { + throw new Error( + "agentOs() only supports Native mounts across the NAPI boundary; Plain mounts with driver callbacks are not serializable", + ); + } + if (record.filesystem !== undefined) { + throw new Error( + "agentOs() only supports Native mounts across the NAPI boundary; Overlay mounts are not serializable", + ); + } + const plugin = record.plugin; + if ( + typeof record.path !== "string" || + !plugin || + typeof plugin !== "object" || + typeof (plugin as Record).id !== "string" + ) { + throw new Error( + `agentOs() options.mounts[${index}] must be a Native mount with { path, plugin: { id, config? } }`, + ); + } + return { + path: record.path, + plugin: { + id: (plugin as Record).id as string, + config: (plugin as Record).config, + }, + readOnly: + typeof record.readOnly === "boolean" + ? record.readOnly + : undefined, + }; + }); +} -function runHook( - c: AgentOsActionContext, - name: string, - callback: () => void | Promise, -): void { - const promise = Promise.resolve(callback()) - .catch((error) => - c.log.error({ msg: "agent-os hook failed", hookName: name, error }), - ) - .finally(() => { - c.vars.activeHooks.delete(promise); - syncPreventSleep(c); - }); - c.vars.activeHooks.add(promise); - syncPreventSleep(c); - c.waitUntil(promise); +function serializeSidecar(input: unknown): { pool?: string } | undefined { + if (input == null) return undefined; + if (!input || typeof input !== "object") { + throw new Error("agentOs() options.sidecar must be an object"); + } + const record = input as Record; + if (record.kind === "explicit" || record.handle !== undefined) { + throw new Error( + "agentOs() only supports sidecar shared pool configuration across the NAPI boundary; explicit sidecar handles are not serializable", + ); + } + if (record.kind !== undefined && record.kind !== "shared") { + throw new Error('agentOs() options.sidecar.kind must be "shared"'); + } + return typeof record.pool === "string" ? { pool: record.pool } : {}; } -// --- Public API --- +function buildNativeFactoryBuilder( + parsed: AgentOsActorConfig, +): (runtime: CoreRuntime) => ActorFactoryHandle { + return (runtime) => { + if (runtime.kind !== "napi") { + throw new Error( + `agentOs() is only supported on the native NAPI runtime (current runtime kind: ${runtime.kind})`, + ); + } + if (!runtime.createAgentOsFactory) { + throw new Error( + "runtime.createAgentOsFactory is not implemented on the active CoreRuntime", + ); + } + const options: NapiAgentOsOptions = { + configJson: buildConfigJson(parsed), + // Resolve the prebuilt sidecar binary from the npm package and pass + // it through to the agent-os client so it spawns the bundled binary + // rather than relying on `agent-os-sidecar` being on PATH. + sidecarBinaryPath: getSidecarPath(), + }; + return runtime.createAgentOsFactory(options, undefined); + }; +} -export function agentOs( - config: AgentOsActorConfigInput, -): ActorDefinition< +/** + * Type alias for the `agentOs(...)` return type. Events are not typed at + * the TS surface because the Rust factory owns the broadcast set and the + * test/client surface uses `any` for actions. + */ +export type AgentOsActorDefinition = ActorDefinition< AgentOsActorState, TConnParams, undefined, AgentOsActorVars, undefined, DatabaseProvider, - { - sessionEvent: typeof sessionEventToken; - permissionRequest: typeof permissionRequestToken; - vmBooted: typeof vmBootedToken; - vmShutdown: typeof vmShutdownToken; - processOutput: typeof processOutputToken; - processExit: typeof processExitToken; - shellData: typeof shellDataToken; - cronEvent: typeof cronEventToken; - }, + Record, Record, any -> { - const parsedConfig = agentOsActorConfigSchema.parse( +>; + +export function agentOs( + config: AgentOsActorConfigInput, +): AgentOsActorDefinition { + const parsed = agentOsActorConfigSchema.parse( config, ) as AgentOsActorConfig; - const actions = { - ...buildSessionActions(parsedConfig), - ...buildPromptActions(parsedConfig), - ...buildConfigActions(parsedConfig), - ...buildSessionPersistenceActions(parsedConfig), - ...buildProcessActions(parsedConfig), - ...buildFilesystemActions(parsedConfig), - ...buildPreviewActions(parsedConfig), - ...buildShellActions(parsedConfig), - ...buildCronActions(parsedConfig), - ...buildNetworkActions(parsedConfig), - }; - return actor< - AgentOsActorState, - TConnParams, - undefined, - AgentOsActorVars, - undefined, - DatabaseProvider, - { - sessionEvent: typeof sessionEventToken; - permissionRequest: typeof permissionRequestToken; - vmBooted: typeof vmBootedToken; - vmShutdown: typeof vmShutdownToken; - processOutput: typeof processOutputToken; - processExit: typeof processExitToken; - shellData: typeof shellDataToken; - cronEvent: typeof cronEventToken; - }, - Record, - typeof actions - >({ - options: { - sleepGracePeriod: 900_000, - actionTimeout: 900_000, - }, - createState: async () => ({}), - createVars: () => ({ - agentOs: null, - activeSessionIds: new Set(), - activeProcesses: new Set(), - activeHooks: new Set>(), - activeShells: new Set(), - sessions: new Set(), - }), - db: db({ - onMigrate: migrateAgentOsTables, - }), - events: { - sessionEvent: sessionEventToken, - permissionRequest: permissionRequestToken, - vmBooted: vmBootedToken, - vmShutdown: vmShutdownToken, - processOutput: processOutputToken, - processExit: processExitToken, - shellData: shellDataToken, - cronEvent: cronEventToken, - }, - onBeforeConnect: parsedConfig.onBeforeConnect - ? async (ctx, params) => { - // Skip user auth for preview URL requests. The signed token - // in onRequest is the credential; browsers navigating preview - // URLs cannot supply actor connection params. - if (ctx.request) { - const url = new URL(ctx.request.url); - if (url.pathname.startsWith("/fetch/")) { - return; - } - } - await parsedConfig.onBeforeConnect?.(ctx, params); - } - : undefined, - onRequest: buildOnRequestHandler(parsedConfig), - onSleep: async (c) => { - c.log.info({ - msg: "agent-os vm shutdown for sleep", - activeSessions: c.vars.sessions.size, - activeProcesses: c.vars.activeProcesses.size, - activeShells: c.vars.activeShells.size, - }); - - if (c.vars.agentOs) { - await c.vars.agentOs.dispose(); - c.vars.agentOs = null; - } - - c.broadcast("vmShutdown", { reason: "sleep" as const }); - }, - onDestroy: async (c) => { - c.log.info({ - msg: "agent-os vm shutdown for destroy", - activeSessions: c.vars.sessions.size, - activeProcesses: c.vars.activeProcesses.size, - activeShells: c.vars.activeShells.size, - }); - - if (c.vars.agentOs) { - await c.vars.agentOs.dispose(); - c.vars.agentOs = null; - } - - c.broadcast("vmShutdown", { reason: "destroy" as const }); - }, - actions, - }); + // Construct a minimal definition through the existing actor() helper, + // then attach the Rust factory builder marker. The actions block stays + // empty because no JS-side action ever runs: the engine driver branches + // on `nativeFactoryBuilder` before reaching the JS dispatch path. + // + // `actorOptions` (e.g. `sleepTimeout`, `noSleep`) is forwarded as the + // actor `options` block so `buildActorConfig` threads it to the engine + // sleep timer; this is what lets a caller make the actor sleep quickly so + // the VM is torn down and sessions resume lazily on the next prompt. + const actorOptions = (parsed as { actorOptions?: Record }) + .actorOptions; + const definition = actor({ + actions: {}, + ...(actorOptions ? { options: actorOptions } : {}), + } as Parameters< + typeof actor + >[0]) as unknown as AgentOsActorDefinition; + definition.nativeFactoryBuilder = buildNativeFactoryBuilder(parsed); + return definition; } - -// Event type tokens. Declared at module level so they can be referenced in -// the actor generic type parameters. -const sessionEventToken = event(); -const permissionRequestToken = event(); -const vmBootedToken = event(); -const vmShutdownToken = event(); -const processOutputToken = event(); -const processExitToken = event(); -const shellDataToken = event(); -const cronEventToken = event(); - -export { ensureVm, syncPreventSleep, runHook }; diff --git a/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/network.ts b/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/network.ts deleted file mode 100644 index 8dbf418d1a..0000000000 --- a/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/network.ts +++ /dev/null @@ -1,58 +0,0 @@ -import type { AgentOsActorConfig } from "../config"; -import type { AgentOsActionContext } from "../types"; -import { ensureVm } from "./index"; - -// Serializable fetch options for the actor action boundary. -export interface VmFetchOptions { - method?: string; - headers?: Record; - body?: string | Uint8Array; -} - -// Serializable fetch result returned by the actor action. -export interface VmFetchResult { - status: number; - statusText: string; - headers: Record; - body: Uint8Array; -} - -// Build network actions for the actor factory. -export function buildNetworkActions( - config: AgentOsActorConfig, -) { - return { - vmFetch: async ( - c: AgentOsActionContext, - port: number, - url: string, - options?: VmFetchOptions, - ): Promise => { - const agentOs = await ensureVm(c, config); - - const headers = new Headers(options?.headers); - const request = new Request(url, { - method: options?.method ?? "GET", - headers, - body: options?.body ?? null, - }); - - const response = await agentOs.fetch(port, request); - - // Serialize response headers to a plain object. - const responseHeaders: Record = {}; - response.headers.forEach((value, key) => { - responseHeaders[key] = value; - }); - - const body = new Uint8Array(await response.arrayBuffer()); - - return { - status: response.status, - statusText: response.statusText, - headers: responseHeaders, - body, - }; - }, - }; -} diff --git a/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/preview.ts b/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/preview.ts deleted file mode 100644 index 3e5f9f7e96..0000000000 --- a/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/preview.ts +++ /dev/null @@ -1,190 +0,0 @@ -import crypto from "node:crypto"; -import type { RequestContext } from "@/actor/config"; -import type { DatabaseProvider, RawAccess } from "@/common/database/config"; -import type { AgentOsActorConfig } from "../config"; -import type { - AgentOsActionContext, - AgentOsActorState, - AgentOsActorVars, -} from "../types"; -import { ensureVm } from "./index"; - -// Generate a 32-character lowercase alphanumeric token (a-z0-9). -// 36^32 ~= 1.6e49 possible tokens, brute-force infeasible. -export function generateToken(): string { - const bytes = crypto.randomBytes(32); - const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789"; - let token = ""; - for (let i = 0; i < 32; i++) { - token += alphabet[bytes[i]! % alphabet.length]; - } - return token; -} - -// CORS headers added to all preview proxy responses. -const CORS_HEADERS: Record = { - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS", - "Access-Control-Allow-Headers": "*", -}; - -function addCorsHeaders(response: Response): Response { - const headers = new Headers(response.headers); - for (const [key, value] of Object.entries(CORS_HEADERS)) { - headers.set(key, value); - } - return new Response(response.body, { - status: response.status, - statusText: response.statusText, - headers, - }); -} - -type AgentOsRequestContext = RequestContext< - AgentOsActorState, - TConnParams, - undefined, - AgentOsActorVars, - undefined, - DatabaseProvider ->; - -export function buildOnRequestHandler( - config: AgentOsActorConfig, -) { - return async ( - c: AgentOsRequestContext, - request: Request, - ): Promise => { - const url = new URL(request.url); - const pathname = url.pathname; - - // Expect paths like /fetch/{token} or /fetch/{token}/remaining/path. - const match = pathname.match(/^\/fetch\/([a-z0-9]+)(\/.*)?$/); - if (!match) { - return new Response("Not Found", { status: 404 }); - } - - // Handle OPTIONS preflight before token validation. - if (request.method === "OPTIONS") { - return new Response(null, { status: 204, headers: CORS_HEADERS }); - } - - const token = match[1]!; - const remainingPath = match[2] ?? "/"; - - // Validate token from SQLite. - const now = Date.now(); - const rows: { port: number }[] = await c.db.execute( - `SELECT port FROM agent_os_preview_tokens WHERE token = ? AND expires_at > ?`, - token, - now, - ); - - if (rows.length === 0) { - c.log.warn({ msg: "agent-os preview auth failed", token }); - return addCorsHeaders(new Response("Forbidden", { status: 403 })); - } - - const port = rows[0]?.port; - - // Boot the VM if needed. - const agentOs = await ensureVm( - c as AgentOsActionContext, - config, - ); - - // Build the request to proxy through the VM's virtual network. - const vmUrl = `http://localhost:${port}${remainingPath}${url.search}`; - const vmRequest = new Request(vmUrl, { - method: request.method, - headers: request.headers, - body: request.body, - duplex: "half", - } as RequestInit); - - const vmResponse = await agentOs.fetch(port, vmRequest); - - c.log.info({ - msg: "agent-os preview request proxied", - port, - method: request.method, - path: remainingPath, - status: vmResponse.status, - }); - - return addCorsHeaders(vmResponse); - }; -} - -export function buildPreviewActions( - config: AgentOsActorConfig, -) { - return { - createSignedPreviewUrl: async ( - c: AgentOsActionContext, - port: number, - expiresInSeconds?: number, - ): Promise<{ - path: string; - token: string; - port: number; - expiresAt: number; - }> => { - await ensureVm(c, config); - - const effectiveExpires = - expiresInSeconds ?? config.preview.defaultExpiresInSeconds; - const maxExpires = config.preview.maxExpiresInSeconds; - - if (effectiveExpires < 1 || effectiveExpires > maxExpires) { - throw new Error( - `expiresInSeconds must be between 1 and ${maxExpires}`, - ); - } - - const token = generateToken(); - const now = Date.now(); - const expiresAt = now + effectiveExpires * 1000; - - // Insert token and lazy-delete expired tokens. - await c.db.execute( - `INSERT INTO agent_os_preview_tokens (token, port, created_at, expires_at) - VALUES (?, ?, ?, ?)`, - token, - port, - now, - expiresAt, - ); - await c.db.execute( - `DELETE FROM agent_os_preview_tokens WHERE expires_at <= ?`, - now, - ); - - // Path relative to the actor's gateway URL. Full URL is - // `${gatewayUrl}/request/fetch/${token}` where gatewayUrl - // comes from the client's getGatewayUrl(). - const path = `/request/fetch/${token}`; - - c.log.info({ - msg: "agent-os preview token created", - port, - expiresInSeconds: effectiveExpires, - }); - - return { path, token, port, expiresAt }; - }, - - expireSignedPreviewUrl: async ( - c: AgentOsActionContext, - token: string, - ): Promise => { - await c.db.execute( - `DELETE FROM agent_os_preview_tokens WHERE token = ?`, - token, - ); - - c.log.info({ msg: "agent-os preview token expired", token }); - }, - }; -} diff --git a/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/process.ts b/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/process.ts deleted file mode 100644 index a64f135b26..0000000000 --- a/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/process.ts +++ /dev/null @@ -1,177 +0,0 @@ -import type { - ProcessInfo, - ProcessTreeNode, - SpawnedProcessInfo, -} from "@rivet-dev/agent-os-core"; -import { isRivetErrorCode } from "@/actor/errors"; -import type { AgentOsActorConfig } from "../config"; -import type { AgentOsActionContext } from "../types"; -import { ensureVm, syncPreventSleep } from "./index"; - -// Infer types from AgentOs methods since @secure-exec/core is not a direct dep. -type ExecResult = Awaited< - ReturnType ->; -type ExecOptions = Parameters< - import("@rivet-dev/agent-os-core").AgentOs["exec"] ->[1]; -type SpawnOptions = Parameters< - import("@rivet-dev/agent-os-core").AgentOs["spawn"] ->[2]; - -function broadcastProcessEvent( - c: AgentOsActionContext, - name: "processOutput" | "processExit", - payload: unknown, -) { - try { - c.broadcast(name, payload); - } catch (error) { - if (isRivetErrorCode(error, "actor", "stopping")) { - return; - } - throw error; - } -} - -// Build process execution actions for the actor factory. -export function buildProcessActions( - config: AgentOsActorConfig, -) { - return { - exec: async ( - c: AgentOsActionContext, - command: string, - options?: ExecOptions, - ): Promise => { - const agentOs = await ensureVm(c, config); - return agentOs.exec(command, options); - }, - - spawn: async ( - c: AgentOsActionContext, - command: string, - args: string[], - options?: SpawnOptions, - ): Promise<{ pid: number }> => { - const agentOs = await ensureVm(c, config); - const { pid } = agentOs.spawn(command, args, { - ...options, - onStdout: (data: Uint8Array) => { - broadcastProcessEvent(c, "processOutput", { - pid, - stream: "stdout" as const, - data, - }); - options?.onStdout?.(data); - }, - onStderr: (data: Uint8Array) => { - broadcastProcessEvent(c, "processOutput", { - pid, - stream: "stderr" as const, - data, - }); - options?.onStderr?.(data); - }, - }); - - c.vars.activeProcesses.add(pid); - syncPreventSleep(c); - c.log.info({ - msg: "agent-os process spawned", - pid, - command, - }); - - agentOs - .waitProcess(pid) - .then((exitCode) => { - broadcastProcessEvent(c, "processExit", { pid, exitCode }); - c.log.info({ - msg: "agent-os process exited", - pid, - exitCode, - }); - }) - .catch(() => { - // Process killed during dispose. Silently clean up. - }) - .finally(() => { - c.vars.activeProcesses.delete(pid); - syncPreventSleep(c); - }); - - return { pid }; - }, - - writeProcessStdin: async ( - c: AgentOsActionContext, - pid: number, - data: string | Uint8Array, - ): Promise => { - const agentOs = await ensureVm(c, config); - agentOs.writeProcessStdin(pid, data); - }, - - closeProcessStdin: async ( - c: AgentOsActionContext, - pid: number, - ): Promise => { - const agentOs = await ensureVm(c, config); - agentOs.closeProcessStdin(pid); - }, - - waitProcess: async ( - c: AgentOsActionContext, - pid: number, - ): Promise => { - const agentOs = await ensureVm(c, config); - return agentOs.waitProcess(pid); - }, - - listProcesses: async ( - c: AgentOsActionContext, - ): Promise => { - const agentOs = await ensureVm(c, config); - return agentOs.listProcesses(); - }, - - allProcesses: async ( - c: AgentOsActionContext, - ): Promise => { - const agentOs = await ensureVm(c, config); - return agentOs.allProcesses(); - }, - - processTree: async ( - c: AgentOsActionContext, - ): Promise => { - const agentOs = await ensureVm(c, config); - return agentOs.processTree(); - }, - - getProcess: async ( - c: AgentOsActionContext, - pid: number, - ): Promise => { - const agentOs = await ensureVm(c, config); - return agentOs.getProcess(pid); - }, - - stopProcess: async ( - c: AgentOsActionContext, - pid: number, - ): Promise => { - const agentOs = await ensureVm(c, config); - agentOs.stopProcess(pid); - }, - - killProcess: async ( - c: AgentOsActionContext, - pid: number, - ): Promise => { - const agentOs = await ensureVm(c, config); - agentOs.killProcess(pid); - }, - }; -} diff --git a/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/session.ts b/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/session.ts deleted file mode 100644 index 136c33fbb8..0000000000 --- a/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/session.ts +++ /dev/null @@ -1,518 +0,0 @@ -import type { - AgentOs, - AgentType, - CreateSessionOptions, - GetEventsOptions, - JsonRpcNotification, - JsonRpcResponse, - PermissionReply, - SequencedEvent, - SessionConfigOption, - SessionInfo, - SessionModeState, -} from "@rivet-dev/agent-os-core"; -import type { AgentOsActorConfig } from "../config"; -import type { - AgentOsActionContext, - PersistedSessionEvent, - PersistedSessionRecord, - PromptResult, - SessionRecord, -} from "../types"; -import { ensureVm, runHook, syncPreventSleep } from "./index"; - -// Strip non-serializable values (functions) from agent-os-core responses so -// CBOR/BARE encoding doesn't fail. The JsonRpcResponse objects from -// secure-exec can contain function properties. -function stripFunctions(value: unknown): unknown { - if (value === null || value === undefined) return value; - if (typeof value === "function") return undefined; - if (typeof value !== "object") return value; - if (Array.isArray(value)) return value.map(stripFunctions); - const out: Record = {}; - for (const [k, v] of Object.entries(value as Record)) { - if (typeof v !== "function") { - out[k] = stripFunctions(v); - } - } - return out; -} - -// Helper to verify a session exists in the VM. Throws via AgentOs if not found. -function assertSessionExists( - c: AgentOsActionContext, - sessionId: string, -): void { - if (!c.vars.sessions.has(sessionId)) { - throw new Error(`session not found: ${sessionId}`); - } -} - -// Build a SessionRecord from AgentOs flat API. -function toSessionRecord( - agentOs: AgentOs, - sessionId: string, - agentType: string, -): SessionRecord { - return { - sessionId, - agentType, - capabilities: agentOs.getSessionCapabilities(sessionId) ?? {}, - agentInfo: agentOs.getSessionAgentInfo(sessionId), - }; -} - -// --- Session persistence helpers --- - -// Persist a session record to SQLite when it is created. -async function persistSession( - c: AgentOsActionContext, - agentOs: AgentOs, - sessionId: string, - agentType: string, -): Promise { - const now = Date.now(); - const capabilities = agentOs.getSessionCapabilities(sessionId) ?? {}; - const agentInfo = agentOs.getSessionAgentInfo(sessionId); - await c.db.execute( - `INSERT OR REPLACE INTO agent_os_sessions (session_id, agent_type, capabilities, agent_info, created_at) - VALUES (?, ?, ?, ?, ?)`, - sessionId, - agentType, - JSON.stringify(capabilities), - agentInfo ? JSON.stringify(agentInfo) : null, - now, - ); -} - -// Persist a session event to SQLite with an auto-incrementing sequence number. -async function persistSessionEvent( - c: AgentOsActionContext, - sessionId: string, - event: JsonRpcNotification, -): Promise { - const now = Date.now(); - - // Compute next sequence number for this session. - const rows: { max_seq: number | null }[] = await c.db.execute( - `SELECT MAX(seq) as max_seq FROM agent_os_session_events WHERE session_id = ?`, - sessionId, - ); - const nextSeq = (rows[0]?.max_seq ?? -1) + 1; - - await c.db.execute( - `INSERT INTO agent_os_session_events (session_id, seq, event, created_at) - VALUES (?, ?, ?, ?)`, - sessionId, - nextSeq, - JSON.stringify(event), - now, - ); -} - -// Remove a session and its events from SQLite. -async function deletePersistedSession( - c: AgentOsActionContext, - sessionId: string, -): Promise { - await c.db.execute( - `DELETE FROM agent_os_session_events WHERE session_id = ?`, - sessionId, - ); - await c.db.execute( - `DELETE FROM agent_os_sessions WHERE session_id = ?`, - sessionId, - ); -} - -// Subscribe to a session's event and permission streams via the flat AgentOs API, -// broadcasting events and running user-provided hooks. -export function subscribeToSession( - c: AgentOsActionContext, - agentOs: AgentOs, - sessionId: string, - parsedConfig: AgentOsActorConfig, -): void { - agentOs.onSessionEvent(sessionId, (event) => { - c.broadcast( - "sessionEvent", - JSON.parse(JSON.stringify({ sessionId, event })), - ); - - // Persist event to SQLite for sleep/wake recovery. - persistSessionEvent(c, sessionId, event).catch((error) => - c.log.error({ - msg: "agent-os failed to persist session event", - sessionId, - error, - }), - ); - - if (parsedConfig.onSessionEvent) { - runHook(c, "onSessionEvent", () => - parsedConfig.onSessionEvent?.(c, sessionId, event), - ); - } - }); - - agentOs.onPermissionRequest(sessionId, (request) => { - c.broadcast( - "permissionRequest", - JSON.parse(JSON.stringify({ sessionId, request })), - ); - - if (parsedConfig.onPermissionRequest) { - runHook(c, "onPermissionRequest", () => - parsedConfig.onPermissionRequest?.(c, sessionId, request), - ); - } - }); - - c.vars.sessions.add(sessionId); -} - -// Build session management actions for the actor factory. -export function buildSessionActions( - config: AgentOsActorConfig, -) { - return { - createSession: async ( - c: AgentOsActionContext, - agentType: AgentType, - options?: CreateSessionOptions, - ): Promise => { - const agentOs = await ensureVm(c, config); - const { sessionId } = await agentOs.createSession( - agentType, - options, - ); - subscribeToSession(c, agentOs, sessionId, config); - - // Persist session metadata to SQLite for sleep/wake recovery. - await persistSession(c, agentOs, sessionId, agentType); - - c.log.info({ - msg: "agent-os session created", - sessionId, - agentType, - }); - return toSessionRecord(agentOs, sessionId, agentType); - }, - - listSessions: async ( - c: AgentOsActionContext, - ): Promise => { - const agentOs = await ensureVm(c, config); - return agentOs.listSessions(); - }, - - getSession: async ( - c: AgentOsActionContext, - sessionId: string, - ): Promise => { - assertSessionExists(c, sessionId); - const agentOs = await ensureVm(c, config); - const info = agentOs - .listSessions() - .find((s) => s.sessionId === sessionId); - if (!info) { - throw new Error(`session not found: ${sessionId}`); - } - return toSessionRecord(agentOs, sessionId, info.agentType); - }, - - destroySession: async ( - c: AgentOsActionContext, - sessionId: string, - ): Promise => { - const agentOs = await ensureVm(c, config); - await agentOs.destroySession(sessionId); - c.vars.sessions.delete(sessionId); - c.vars.activeSessionIds.delete(sessionId); - syncPreventSleep(c); - - // Clean up persisted session and events from SQLite. - await deletePersistedSession(c, sessionId); - - c.log.info({ msg: "agent-os session destroyed", sessionId }); - }, - - resumeSession: async ( - c: AgentOsActionContext, - sessionId: string, - ): Promise<{ sessionId: string }> => { - const agentOs = await ensureVm(c, config); - return agentOs.resumeSession(sessionId); - }, - - closeSession: async ( - c: AgentOsActionContext, - sessionId: string, - ): Promise => { - const agentOs = await ensureVm(c, config); - agentOs.closeSession(sessionId); - c.vars.sessions.delete(sessionId); - c.vars.activeSessionIds.delete(sessionId); - syncPreventSleep(c); - - // Clean up persisted session and events from SQLite. - await deletePersistedSession(c, sessionId); - - c.log.info({ msg: "agent-os session closed", sessionId }); - }, - }; -} - -// Build prompt, cancel, and permission actions for the actor factory. -export function buildPromptActions( - _config: AgentOsActorConfig, -) { - return { - sendPrompt: async ( - c: AgentOsActionContext, - sessionId: string, - text: string, - ): Promise => { - if (c.aborted) { - throw new Error( - "actor is shutting down, cannot start new prompt", - ); - } - - assertSessionExists(c, sessionId); - const agentOs = c.vars.agentOs; - if (!agentOs) { - throw new Error("VM not initialized"); - } - - c.vars.activeSessionIds.add(sessionId); - syncPreventSleep(c); - c.log.info({ msg: "agent-os prompt turn started", sessionId }); - - const start = Date.now(); - try { - const result = await agentOs.prompt(sessionId, text); - return { - response: JSON.parse(JSON.stringify(result.response)), - text: result.text, - }; - } finally { - c.vars.activeSessionIds.delete(sessionId); - syncPreventSleep(c); - c.log.info({ - msg: "agent-os prompt turn ended", - sessionId, - durationMs: Date.now() - start, - }); - } - }, - - cancelPrompt: async ( - c: AgentOsActionContext, - sessionId: string, - ): Promise => { - assertSessionExists(c, sessionId); - const agentOs = c.vars.agentOs; - if (!agentOs) { - throw new Error("VM not initialized"); - } - return stripFunctions( - agentOs.cancelSession(sessionId), - ) as JsonRpcResponse; - }, - - respondPermission: async ( - c: AgentOsActionContext, - sessionId: string, - permissionId: string, - reply: PermissionReply, - ): Promise => { - assertSessionExists(c, sessionId); - const agentOs = c.vars.agentOs; - if (!agentOs) { - throw new Error("VM not initialized"); - } - return stripFunctions( - agentOs.respondPermission(sessionId, permissionId, reply), - ) as JsonRpcResponse; - }, - }; -} - -// Build session configuration proxy actions for the actor factory. -export function buildConfigActions( - _config: AgentOsActorConfig, -) { - return { - setMode: async ( - c: AgentOsActionContext, - sessionId: string, - modeId: string, - ): Promise => { - assertSessionExists(c, sessionId); - const agentOs = c.vars.agentOs; - if (!agentOs) { - throw new Error("VM not initialized"); - } - return stripFunctions( - agentOs.setSessionMode(sessionId, modeId), - ) as JsonRpcResponse; - }, - - getModes: async ( - c: AgentOsActionContext, - sessionId: string, - ): Promise => { - assertSessionExists(c, sessionId); - const agentOs = c.vars.agentOs; - if (!agentOs) { - throw new Error("VM not initialized"); - } - return agentOs.getSessionModes(sessionId); - }, - - setModel: async ( - c: AgentOsActionContext, - sessionId: string, - model: string, - ): Promise => { - assertSessionExists(c, sessionId); - const agentOs = c.vars.agentOs; - if (!agentOs) { - throw new Error("VM not initialized"); - } - return stripFunctions( - agentOs.setSessionModel(sessionId, model), - ) as JsonRpcResponse; - }, - - setThoughtLevel: async ( - c: AgentOsActionContext, - sessionId: string, - level: string, - ): Promise => { - assertSessionExists(c, sessionId); - const agentOs = c.vars.agentOs; - if (!agentOs) { - throw new Error("VM not initialized"); - } - return stripFunctions( - agentOs.setSessionThoughtLevel(sessionId, level), - ) as JsonRpcResponse; - }, - - getConfigOptions: async ( - c: AgentOsActionContext, - sessionId: string, - ): Promise => { - assertSessionExists(c, sessionId); - const agentOs = c.vars.agentOs; - if (!agentOs) { - throw new Error("VM not initialized"); - } - return agentOs.getSessionConfigOptions(sessionId); - }, - - getEvents: async ( - c: AgentOsActionContext, - sessionId: string, - options?: GetEventsOptions, - ): Promise => { - assertSessionExists(c, sessionId); - const agentOs = c.vars.agentOs; - if (!agentOs) { - throw new Error("VM not initialized"); - } - return agentOs - .getSessionEvents(sessionId, options) - .map((e) => e.notification); - }, - - getSequencedEvents: async ( - c: AgentOsActionContext, - sessionId: string, - options?: GetEventsOptions, - ): Promise => { - assertSessionExists(c, sessionId); - const agentOs = c.vars.agentOs; - if (!agentOs) { - throw new Error("VM not initialized"); - } - return agentOs.getSessionEvents(sessionId, options); - }, - - rawSend: async ( - c: AgentOsActionContext, - sessionId: string, - method: string, - params?: Record, - ): Promise => { - assertSessionExists(c, sessionId); - const agentOs = c.vars.agentOs; - if (!agentOs) { - throw new Error("VM not initialized"); - } - return stripFunctions( - agentOs.rawSessionSend(sessionId, method, params), - ) as JsonRpcResponse; - }, - }; -} - -// Build actions for querying persisted session data from SQLite. -// These work without a running VM and return data from prior sessions -// that survived sleep/wake cycles. -export function buildSessionPersistenceActions( - _config: AgentOsActorConfig, -) { - return { - listPersistedSessions: async ( - c: AgentOsActionContext, - ): Promise => { - const rows: { - session_id: string; - agent_type: string; - capabilities: string; - agent_info: string | null; - created_at: number; - }[] = await c.db.execute( - `SELECT session_id, agent_type, capabilities, agent_info, created_at - FROM agent_os_sessions - ORDER BY created_at ASC`, - ); - - return rows.map((row) => ({ - sessionId: row.session_id, - agentType: row.agent_type, - capabilities: JSON.parse(row.capabilities), - agentInfo: row.agent_info ? JSON.parse(row.agent_info) : null, - createdAt: row.created_at, - })); - }, - - getSessionEvents: async ( - c: AgentOsActionContext, - sessionId: string, - ): Promise => { - const rows: { - session_id: string; - seq: number; - event: string; - created_at: number; - }[] = await c.db.execute( - `SELECT session_id, seq, event, created_at - FROM agent_os_session_events - WHERE session_id = ? - ORDER BY seq ASC`, - sessionId, - ); - - return rows.map((row) => ({ - sessionId: row.session_id, - seq: row.seq, - event: JSON.parse(row.event), - createdAt: row.created_at, - })); - }, - }; -} diff --git a/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/shell.ts b/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/shell.ts deleted file mode 100644 index c44513ee45..0000000000 --- a/rivetkit-typescript/packages/rivetkit/src/agent-os/actor/shell.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { OpenShellOptions } from "@rivet-dev/agent-os-core"; -import type { AgentOsActorConfig } from "../config"; -import type { AgentOsActionContext } from "../types"; -import { ensureVm, syncPreventSleep } from "./index"; - -// Build shell actions for the actor factory. -export function buildShellActions( - config: AgentOsActorConfig, -) { - return { - openShell: async ( - c: AgentOsActionContext, - options?: OpenShellOptions, - ): Promise<{ shellId: string }> => { - const agentOs = await ensureVm(c, config); - const { shellId } = agentOs.openShell(options); - - // Wire shell data to actor events. - agentOs.onShellData(shellId, (data: Uint8Array) => { - c.broadcast("shellData", { shellId, data }); - }); - - c.vars.activeShells.add(shellId); - syncPreventSleep(c); - c.log.info({ msg: "agent-os shell opened", shellId }); - - return { shellId }; - }, - - writeShell: async ( - c: AgentOsActionContext, - shellId: string, - data: string | Uint8Array, - ): Promise => { - const agentOs = await ensureVm(c, config); - agentOs.writeShell(shellId, data); - }, - - resizeShell: async ( - c: AgentOsActionContext, - shellId: string, - cols: number, - rows: number, - ): Promise => { - const agentOs = await ensureVm(c, config); - agentOs.resizeShell(shellId, cols, rows); - }, - - closeShell: async ( - c: AgentOsActionContext, - shellId: string, - ): Promise => { - const agentOs = await ensureVm(c, config); - agentOs.closeShell(shellId); - c.vars.activeShells.delete(shellId); - syncPreventSleep(c); - c.log.info({ msg: "agent-os shell closed", shellId }); - }, - }; -} diff --git a/rivetkit-typescript/packages/rivetkit/src/agent-os/fs/database-vfs.ts b/rivetkit-typescript/packages/rivetkit/src/agent-os/fs/database-vfs.ts deleted file mode 100644 index 4ad96621fb..0000000000 --- a/rivetkit-typescript/packages/rivetkit/src/agent-os/fs/database-vfs.ts +++ /dev/null @@ -1,624 +0,0 @@ -/** - * SQLite-backed VirtualFileSystem implementation. - * - * Stores file content, metadata (mode, timestamps), and directory structure - * in SQLite tables managed by the RivetKit actor's database. This allows VM - * filesystem state to persist across sleep/wake cycles. - * - * We use SQLite instead of actor KV because SQLite handles bulk write - * optimizations (transactions, WAL mode, page caching) under the hood. With KV - * we would need to manually chunk writes to stay under batch size limits, - * implement our own indexing for directory listing queries, and handle - * consistency across multiple KV operations. SQLite gives us all of this for - * free. - * - * All paths are normalized to POSIX form (forward slashes, rooted at "/"). - */ - -import * as posixPath from "node:path/posix"; -import type { RawAccess } from "@/common/database/config"; - -// Infer VirtualFileSystem from PlainMountConfig.driver since -// @secure-exec/core is not a direct dependency of this package. -type VirtualFileSystem = - import("@rivet-dev/agent-os-core").PlainMountConfig["driver"]; - -// Infer VirtualStat from AgentOs.stat() return type. -type VirtualStat = Awaited< - ReturnType ->; - -// Infer VirtualDirEntry from readDirWithTypes. -// VirtualDirEntry has: name, isDirectory, isSymbolicLink?, ino? -interface VirtualDirEntry { - name: string; - isDirectory: boolean; - isSymbolicLink?: boolean; - ino?: number; -} - -// POSIX mode constants. -const S_IFDIR = 0o040000; -const S_IFREG = 0o100000; -const S_IFLNK = 0o120000; -const DEFAULT_FILE_MODE = S_IFREG | 0o644; -const DEFAULT_DIR_MODE = S_IFDIR | 0o755; - -interface FsRow extends Record { - path: string; - is_directory: number; - content: Uint8Array | null; - mode: number; - uid: number; - gid: number; - size: number; - atime_ms: number; - mtime_ms: number; - ctime_ms: number; - birthtime_ms: number; - symlink_target: string | null; - nlink: number; -} - -function normPath(p: string): string { - const normalized = posixPath.normalize(`/${p}`); - // Remove trailing slash unless it's the root. - if (normalized.length > 1 && normalized.endsWith("/")) { - return normalized.slice(0, -1); - } - return normalized; -} - -function parentPath(p: string): string { - const parent = posixPath.dirname(p); - return parent; -} - -function throwENOENT(path: string): never { - const err = new Error(`ENOENT: no such file or directory: ${path}`); - err.name = "ENOENT"; - throw err; -} - -function throwEEXIST(path: string): never { - const err = new Error(`EEXIST: file already exists: ${path}`); - err.name = "EEXIST"; - throw err; -} - -function throwENOTDIR(path: string): never { - const err = new Error(`ENOTDIR: not a directory: ${path}`); - err.name = "ENOTDIR"; - throw err; -} - -function throwEISDIR(path: string): never { - const err = new Error(`EISDIR: illegal operation on a directory: ${path}`); - err.name = "EISDIR"; - throw err; -} - -function throwENOTEMPTY(path: string): never { - const err = new Error(`ENOTEMPTY: directory not empty: ${path}`); - err.name = "ENOTEMPTY"; - throw err; -} - -function throwENOSYS(op: string): never { - const err = new Error(`ENOSYS: function not implemented: ${op}`); - err.name = "ENOSYS"; - throw err; -} - -function rowToStat(row: FsRow): VirtualStat { - return { - mode: row.mode, - size: row.size, - isDirectory: row.is_directory === 1, - isSymbolicLink: row.symlink_target !== null, - atimeMs: row.atime_ms, - mtimeMs: row.mtime_ms, - ctimeMs: row.ctime_ms, - birthtimeMs: row.birthtime_ms, - ino: 0, - nlink: row.nlink, - uid: row.uid, - gid: row.gid, - }; -} - -export interface DatabaseVfsOptions { - /** The RawAccess database handle from the actor's db provider. */ - db: RawAccess; -} - -/** - * Create a VirtualFileSystem backed by SQLite. - * - * The returned filesystem stores all content and metadata in the - * `agent_os_fs_entries` table. The table must be created beforehand - * via `migrateAgentOsTables()`. - */ -export function createDatabaseVfs( - options: DatabaseVfsOptions, -): VirtualFileSystem { - const { db } = options; - - async function getEntry(path: string): Promise { - const rows = await db.execute( - "SELECT * FROM agent_os_fs_entries WHERE path = ?", - path, - ); - return rows[0]; - } - - async function getEntryOrThrow(path: string): Promise { - const entry = await getEntry(path); - if (!entry) { - throwENOENT(path); - } - return entry; - } - - async function ensureParentExists(path: string): Promise { - const parent = parentPath(path); - if (parent === path) return; // root - const entry = await getEntry(parent); - if (!entry) { - throwENOENT(parent); - } - if (entry.is_directory !== 1) { - throwENOTDIR(parent); - } - } - - async function getChildEntries(dirPath: string): Promise { - // Find direct children by matching paths that are one level deeper. - // A direct child of "/foo" has path like "/foo/bar" but NOT "/foo/bar/baz". - const prefix = dirPath === "/" ? "/" : `${dirPath}/`; - const rows = await db.execute( - "SELECT * FROM agent_os_fs_entries WHERE path LIKE ? AND path != ?", - `${prefix}%`, - dirPath, - ); - // Filter to direct children only. - return rows.filter((row) => { - const relative = row.path.slice(prefix.length); - return relative.length > 0 && !relative.includes("/"); - }); - } - - // Ensure root directory exists. - const rootInit = (async () => { - const root = await getEntry("/"); - if (!root) { - const now = Date.now(); - await db.execute( - `INSERT OR IGNORE INTO agent_os_fs_entries (path, is_directory, content, mode, uid, gid, size, atime_ms, mtime_ms, ctime_ms, birthtime_ms, symlink_target, nlink) VALUES (?, 1, NULL, ?, 0, 0, 0, ?, ?, ?, ?, NULL, 2)`, - "/", - DEFAULT_DIR_MODE, - now, - now, - now, - now, - ); - } - })(); - - const backend: VirtualFileSystem = { - async readFile(p: string): Promise { - await rootInit; - const path = normPath(p); - const entry = await getEntryOrThrow(path); - if (entry.is_directory === 1) { - throwEISDIR(path); - } - return entry.content ?? new Uint8Array(0); - }, - - async readTextFile(p: string): Promise { - const data = await backend.readFile(p); - return new TextDecoder().decode(data); - }, - - async readDir(p: string): Promise { - await rootInit; - const path = normPath(p); - const entry = await getEntryOrThrow(path); - if (entry.is_directory !== 1) { - throwENOTDIR(path); - } - const children = await getChildEntries(path); - return children.map((child) => posixPath.basename(child.path)); - }, - - async readDirWithTypes(p: string): Promise { - await rootInit; - const path = normPath(p); - const entry = await getEntryOrThrow(path); - if (entry.is_directory !== 1) { - throwENOTDIR(path); - } - const children = await getChildEntries(path); - return children.map((child) => ({ - name: posixPath.basename(child.path), - isDirectory: child.is_directory === 1, - isSymbolicLink: child.symlink_target !== null, - ino: 0, - })); - }, - - async writeFile( - p: string, - content: string | Uint8Array, - ): Promise { - await rootInit; - const path = normPath(p); - await ensureParentExists(path); - - const existing = await getEntry(path); - if (existing && existing.is_directory === 1) { - throwEISDIR(path); - } - - const data = - typeof content === "string" - ? new TextEncoder().encode(content) - : content; - const now = Date.now(); - - if (existing) { - await db.execute( - `UPDATE agent_os_fs_entries SET content = ?, size = ?, mtime_ms = ?, ctime_ms = ?, atime_ms = ? WHERE path = ?`, - data, - data.byteLength, - now, - now, - now, - path, - ); - } else { - await db.execute( - `INSERT INTO agent_os_fs_entries (path, is_directory, content, mode, uid, gid, size, atime_ms, mtime_ms, ctime_ms, birthtime_ms, symlink_target, nlink) VALUES (?, 0, ?, ?, 0, 0, ?, ?, ?, ?, ?, NULL, 1)`, - path, - data, - DEFAULT_FILE_MODE, - data.byteLength, - now, - now, - now, - now, - ); - } - }, - - async createDir(p: string): Promise { - await rootInit; - const path = normPath(p); - await ensureParentExists(path); - - const existing = await getEntry(path); - if (existing) { - throwEEXIST(path); - } - - const now = Date.now(); - await db.execute( - `INSERT INTO agent_os_fs_entries (path, is_directory, content, mode, uid, gid, size, atime_ms, mtime_ms, ctime_ms, birthtime_ms, symlink_target, nlink) VALUES (?, 1, NULL, ?, 0, 0, 0, ?, ?, ?, ?, NULL, 2)`, - path, - DEFAULT_DIR_MODE, - now, - now, - now, - now, - ); - }, - - async mkdir( - p: string, - options?: { recursive?: boolean }, - ): Promise { - await rootInit; - const path = normPath(p); - - if (options?.recursive) { - const parts = path.split("/").filter(Boolean); - let current = ""; - for (const part of parts) { - current += `/${part}`; - const existing = await getEntry(current); - if (!existing) { - const now = Date.now(); - await db.execute( - `INSERT INTO agent_os_fs_entries (path, is_directory, content, mode, uid, gid, size, atime_ms, mtime_ms, ctime_ms, birthtime_ms, symlink_target, nlink) VALUES (?, 1, NULL, ?, 0, 0, 0, ?, ?, ?, ?, NULL, 2)`, - current, - DEFAULT_DIR_MODE, - now, - now, - now, - now, - ); - } else if (existing.is_directory !== 1) { - throwENOTDIR(current); - } - } - } else { - await backend.createDir(p); - } - }, - - async exists(p: string): Promise { - await rootInit; - const path = normPath(p); - const entry = await getEntry(path); - return entry !== undefined; - }, - - async stat(p: string): Promise { - await rootInit; - const path = normPath(p); - const entry = await getEntryOrThrow(path); - return rowToStat(entry); - }, - - async removeFile(p: string): Promise { - await rootInit; - const path = normPath(p); - const entry = await getEntryOrThrow(path); - if (entry.is_directory === 1) { - throwEISDIR(path); - } - await db.execute( - "DELETE FROM agent_os_fs_entries WHERE path = ?", - path, - ); - }, - - async removeDir(p: string): Promise { - await rootInit; - const path = normPath(p); - const entry = await getEntryOrThrow(path); - if (entry.is_directory !== 1) { - throwENOTDIR(path); - } - const children = await getChildEntries(path); - if (children.length > 0) { - throwENOTEMPTY(path); - } - await db.execute( - "DELETE FROM agent_os_fs_entries WHERE path = ?", - path, - ); - }, - - async rename(oldPath: string, newPath: string): Promise { - await rootInit; - const from = normPath(oldPath); - const to = normPath(newPath); - - const entry = await getEntryOrThrow(from); - await ensureParentExists(to); - - // Remove destination if it exists (overwrite semantics). - const destEntry = await getEntry(to); - if (destEntry) { - if (destEntry.is_directory === 1) { - const children = await getChildEntries(to); - if (children.length > 0) { - throwENOTEMPTY(to); - } - } - await db.execute( - "DELETE FROM agent_os_fs_entries WHERE path = ?", - to, - ); - } - - if (entry.is_directory === 1) { - // Move all descendants by updating path prefixes. - const prefix = from === "/" ? "/" : `${from}/`; - const newPrefix = to === "/" ? "/" : `${to}/`; - - // Get all descendants first, then update them. - const descendants = await db.execute( - "SELECT path FROM agent_os_fs_entries WHERE path LIKE ?", - `${prefix}%`, - ); - - for (const desc of descendants) { - const newDescPath = - newPrefix + desc.path.slice(prefix.length); - await db.execute( - "UPDATE agent_os_fs_entries SET path = ? WHERE path = ?", - newDescPath, - desc.path, - ); - } - } - - // Update the entry itself. - const now = Date.now(); - await db.execute( - "UPDATE agent_os_fs_entries SET path = ?, ctime_ms = ? WHERE path = ?", - to, - now, - from, - ); - }, - - async realpath(p: string): Promise { - await rootInit; - const path = normPath(p); - const entry = await getEntryOrThrow(path); - if (entry.symlink_target !== null) { - return normPath(entry.symlink_target); - } - return path; - }, - - async symlink(target: string, linkPath: string): Promise { - await rootInit; - const link = normPath(linkPath); - await ensureParentExists(link); - - const existing = await getEntry(link); - if (existing) { - throwEEXIST(link); - } - - const now = Date.now(); - await db.execute( - `INSERT INTO agent_os_fs_entries (path, is_directory, content, mode, uid, gid, size, atime_ms, mtime_ms, ctime_ms, birthtime_ms, symlink_target, nlink) VALUES (?, 0, NULL, ?, 0, 0, ?, ?, ?, ?, ?, ?, 1)`, - link, - S_IFLNK | 0o777, - target.length, - now, - now, - now, - now, - target, - ); - }, - - async readlink(p: string): Promise { - await rootInit; - const path = normPath(p); - const entry = await getEntryOrThrow(path); - if (entry.symlink_target === null) { - const err = new Error(`EINVAL: not a symlink: ${path}`); - err.name = "EINVAL"; - throw err; - } - return entry.symlink_target; - }, - - async lstat(p: string): Promise { - // lstat does not follow symlinks; same as stat for our storage model. - return backend.stat(p); - }, - - async link(_oldPath: string, _newPath: string): Promise { - throwENOSYS("link"); - }, - - async chmod(p: string, mode: number): Promise { - await rootInit; - const path = normPath(p); - await getEntryOrThrow(path); - const now = Date.now(); - await db.execute( - "UPDATE agent_os_fs_entries SET mode = ?, ctime_ms = ? WHERE path = ?", - mode, - now, - path, - ); - }, - - async chown(p: string, uid: number, gid: number): Promise { - await rootInit; - const path = normPath(p); - await getEntryOrThrow(path); - const now = Date.now(); - await db.execute( - "UPDATE agent_os_fs_entries SET uid = ?, gid = ?, ctime_ms = ? WHERE path = ?", - uid, - gid, - now, - path, - ); - }, - - async utimes(p: string, atime: number, mtime: number): Promise { - await rootInit; - const path = normPath(p); - await getEntryOrThrow(path); - const now = Date.now(); - await db.execute( - "UPDATE agent_os_fs_entries SET atime_ms = ?, mtime_ms = ?, ctime_ms = ? WHERE path = ?", - atime, - mtime, - now, - path, - ); - }, - - async truncate(p: string, length: number): Promise { - await rootInit; - const path = normPath(p); - const entry = await getEntryOrThrow(path); - if (entry.is_directory === 1) { - throwEISDIR(path); - } - - const existing = entry.content ?? new Uint8Array(0); - let newContent: Uint8Array; - if (length >= existing.byteLength) { - // Extend with zeros. - newContent = new Uint8Array(length); - newContent.set(existing); - } else { - // Truncate. - newContent = existing.slice(0, length); - } - - const now = Date.now(); - await db.execute( - "UPDATE agent_os_fs_entries SET content = ?, size = ?, mtime_ms = ?, ctime_ms = ? WHERE path = ?", - newContent, - length, - now, - now, - path, - ); - }, - - async pread( - p: string, - offset: number, - length: number, - ): Promise { - await rootInit; - const path = normPath(p); - const entry = await getEntryOrThrow(path); - if (entry.is_directory === 1) { - throwEISDIR(path); - } - const content = entry.content ?? new Uint8Array(0); - const end = Math.min(offset + length, content.byteLength); - if (offset >= content.byteLength) { - return new Uint8Array(0); - } - return content.slice(offset, end); - }, - - async pwrite( - p: string, - offset: number, - data: Uint8Array, - ): Promise { - await rootInit; - const path = normPath(p); - const entry = await getEntryOrThrow(path); - if (entry.is_directory === 1) { - throwEISDIR(path); - } - const content = entry.content ?? new Uint8Array(0); - const end = offset + data.byteLength; - const newSize = Math.max(content.byteLength, end); - const buf = new Uint8Array(newSize); - buf.set(content); - buf.set(data, offset); - const now = Date.now(); - await db.execute( - `UPDATE agent_os_fs_entries SET content = ?, size = ?, mtime_ms = ?, ctime_ms = ? WHERE path = ?`, - buf, - newSize, - now, - now, - path, - ); - }, - }; - - return backend; -} diff --git a/rivetkit-typescript/packages/rivetkit/src/agent-os/index.ts b/rivetkit-typescript/packages/rivetkit/src/agent-os/index.ts index 6e2023359b..a888661ed9 100644 --- a/rivetkit-typescript/packages/rivetkit/src/agent-os/index.ts +++ b/rivetkit-typescript/packages/rivetkit/src/agent-os/index.ts @@ -1,48 +1,24 @@ -// Database migration +// Rust-backed agent-os actor surface. +// +// Phase 1c: only the `agentOs()` definition function, the config schema, +// and the public domain types are re-exported. Legacy JS-port action +// builders (cron/db/filesystem/network/preview/process/session/shell) +// were removed along with the JS-port implementation files. Subsequent +// phases (3+) add new action arms to the Rust crate, not new TS modules. -// Cron actions -export { buildCronActions } from "./actor/cron"; -export { migrateAgentOsTables } from "./actor/db"; -// Filesystem actions -export { buildFilesystemActions } from "./actor/filesystem"; -// Actor factory and VM lifecycle helpers -export { agentOs, ensureVm, runHook, syncPreventSleep } from "./actor/index"; -// Network actions export { - buildNetworkActions, - type VmFetchOptions, - type VmFetchResult, -} from "./actor/network"; -// Preview actions -export { - buildOnRequestHandler, - buildPreviewActions, - generateToken, -} from "./actor/preview"; -// Process actions -export { buildProcessActions } from "./actor/process"; -// Session actions -export { - buildConfigActions, - buildPromptActions, - buildSessionActions, - buildSessionPersistenceActions, - subscribeToSession, -} from "./actor/session"; -// Shell actions -export { buildShellActions } from "./actor/shell"; -// Config schema and types + agentOs, + type AgentOsActorDefinition, + nodeModulesMount, + type NodeModulesMountConfig, +} from "./actor/index"; + export { type AgentOsActorConfig, type AgentOsActorConfigInput, agentOsActorConfigSchema, } from "./config"; -// Database-backed VFS -export { - createDatabaseVfs, - type DatabaseVfsOptions, -} from "./fs/database-vfs"; -// Domain types and event payloads + export type { AgentOsActionContext, AgentOsActorState, diff --git a/rivetkit-typescript/packages/rivetkit/src/client/actor-conn.ts b/rivetkit-typescript/packages/rivetkit/src/client/actor-conn.ts index adbb7b8af4..710b3eec8a 100644 --- a/rivetkit-typescript/packages/rivetkit/src/client/actor-conn.ts +++ b/rivetkit-typescript/packages/rivetkit/src/client/actor-conn.ts @@ -998,9 +998,21 @@ export class ActorConnRaw { const listeners = this.#eventSubscriptions.get(name); if (!listeners) return; + // Normalize the callback argument list. JS-side `c.broadcast(name, ...args)` + // delivers `args` as an array (spread into the listener). Native + // (Rust/NAPI) factory actors broadcast a SINGLE payload value via + // `ctx.broadcast(name, payload)`, so `args` arrives as one object (e.g. + // `{}` for vmBooted, `{ reason }` for vmShutdown, the JSON-RPC notification + // for sessionEvent). Spreading a non-iterable object throws + // "Spread syntax requires ...iterable", which would kill this handler — so + // wrap a non-array payload as a single positional argument. + const callbackArgs: unknown[] = Array.isArray(args) + ? (args as unknown[]) + : [args]; + // Create a new array to avoid issues with listeners being removed during iteration for (const listener of [...listeners]) { - listener.callback(...(args as unknown[])); + listener.callback(...callbackArgs); // Remove if this was a one-time listener if (listener.once) { diff --git a/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts b/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts index 298358ea73..f67cf46315 100644 --- a/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts +++ b/rivetkit-typescript/packages/rivetkit/src/drivers/engine/actor-driver.ts @@ -142,6 +142,18 @@ interface HibernatableRunnerWebSocketBinding { export type DriverContext = {}; +/** + * Placeholder `handler.actor` value used for Rust-backed actor definitions + * (those with `nativeFactoryBuilder`). The Rust `CoreActorFactory` owns + * lifecycle and request dispatch, so the JS-side actor instance is a stub + * that no-ops on the handful of methods the engine driver may invoke during + * stop/teardown paths. + */ +const NATIVE_NAPI_ACTOR_STUB = { + onStop: async (_reason: string) => {}, + debugForceCrash: async () => {}, +} as unknown as AnyActorInstance; + export class EngineActorDriver implements ActorDriver { #config: RegistryConfig; #inlineClient: Client; @@ -1640,6 +1652,22 @@ export class EngineActorDriver implements ActorDriver { error: stringifyError(error), }); } + } else if (definition.nativeFactoryBuilder) { + // Rust-backed actor (e.g. `agentOs(...)`). The + // `CoreActorFactory` registered for this name owns the + // actor's lifecycle, state, and request dispatch. The + // engine driver only needs to keep the handler alive so + // stop/load paths don't blow up. + handler.actor = NATIVE_NAPI_ACTOR_STUB; + handler.actorStartError = undefined; + handler.actorStartPromise?.resolve(); + handler.actorStartPromise = undefined; + logger().debug({ + msg: "engine actor started (rust-native)", + actorId, + name, + key, + }); } else if (isStaticActorDefinition(definition)) { const instantiateStart = performance.now(); const staticActor = diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/napi-runtime.ts b/rivetkit-typescript/packages/rivetkit/src/registry/napi-runtime.ts index 2165a420a2..b3e7045a1f 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/napi-runtime.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/napi-runtime.ts @@ -9,9 +9,11 @@ import type { import type { ActorContextHandle, ActorFactoryHandle, + AgentOsToolCallbacks, CancellationTokenHandle, ConnHandle, CoreRuntime, + NapiAgentOsOptions, RegistryHandle, RuntimeActorConfig, RuntimeBytes, @@ -191,6 +193,17 @@ export class NapiCoreRuntime implements CoreRuntime { asNativeRegistry(registry).register(name, asNativeFactory(factory)); } + createAgentOsFactory( + options: NapiAgentOsOptions, + toolCallbacks: AgentOsToolCallbacks, + ): ActorFactoryHandle { + const factory = this.#bindings.NapiActorFactory.fromAgentOs( + options, + toolCallbacks ?? undefined, + ); + return asActorFactoryHandle(factory); + } + async serveRegistry( registry: RegistryHandle, config: RuntimeServeConfig, diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/native.ts b/rivetkit-typescript/packages/rivetkit/src/registry/native.ts index fa2dddf8a9..0195fd46d9 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/native.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/native.ts @@ -4760,11 +4760,12 @@ export async function buildRegistryWithRuntime( const registry = runtime.createRegistry(); for (const [name, definition] of Object.entries(config.use)) { - runtime.registerActor( - registry, - name, - buildNativeFactory(runtime, config, definition), - ); + // Dispatch: foreign-runtime factories (Rust-backed `agentOs(...)`) + // bypass `buildNativeFactory` and own their entry loop directly. + const factory = definition.nativeFactoryBuilder + ? definition.nativeFactoryBuilder(runtime) + : buildNativeFactory(runtime, config, definition); + runtime.registerActor(registry, name, factory); } return { diff --git a/rivetkit-typescript/packages/rivetkit/src/registry/runtime.ts b/rivetkit-typescript/packages/rivetkit/src/registry/runtime.ts index 35f8e4748d..8f28f3a255 100644 --- a/rivetkit-typescript/packages/rivetkit/src/registry/runtime.ts +++ b/rivetkit-typescript/packages/rivetkit/src/registry/runtime.ts @@ -1,6 +1,28 @@ import type { SqliteNativeMetrics } from "@/common/database/config"; import type { RegistryConfig } from "./config"; +/** + * Opaque JSON-encoded config envelope for the Rust-backed agent-os actor. + * Built by the TS shim from `AgentOsActorConfig` and passed through to the + * Rust crate's `AgentOsConfigJson` deserializer. See `agent_os.rs` in + * rivetkit-napi. + */ +export interface NapiAgentOsOptions { + configJson?: string; + /** + * Absolute path to the prebuilt `agent-os-sidecar` binary, resolved from + * the `@rivet-dev/agent-os-sidecar` npm package. Forwarded to the agent-os + * client (via `AGENT_OS_SIDECAR_BIN`) so it spawns the bundled binary. + */ + sidecarBinaryPath?: string; +} + +/** + * Tool callback bag accepted by `createAgentOsFactory`. Reserved for Phase 5 + * (toolkit dispatch); currently always `undefined`. + */ +export type AgentOsToolCallbacks = Record | undefined | null; + declare const handleBrand: unique symbol; type OpaqueHandle = { @@ -323,6 +345,15 @@ export interface CoreRuntime { name: string, factory: ActorFactoryHandle, ): void; + /** + * Build a Rust-backed agent-os factory. Returns an `ActorFactoryHandle` + * suitable for `registerActor`. Optional: only the native NAPI runtime + * implements this; wasm runtimes throw. + */ + createAgentOsFactory?( + options: NapiAgentOsOptions, + toolCallbacks: AgentOsToolCallbacks, + ): ActorFactoryHandle; serveRegistry( registry: RegistryHandle, config: RuntimeServeConfig, diff --git a/rivetkit-typescript/packages/rivetkit/tests/agent-os-mounts.test.ts b/rivetkit-typescript/packages/rivetkit/tests/agent-os-mounts.test.ts new file mode 100644 index 0000000000..8c0316dc08 --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/tests/agent-os-mounts.test.ts @@ -0,0 +1,148 @@ +/** + * Regression test for rivetkit issue #6: + * "No hook to inject custom VFS mounts on VM creation (ensureVm)" + * + * The supported public surface for `agentOs(...)` lets callers pass native + * sidecar mounts via `options.mounts`. Plain/Overlay mounts carry live JS + * objects and are rejected until the NAPI callback channel exists. + * + * However, VM creation is now fully owned by the Rust crate. The only path + * from TS into VM creation is `buildConfigJson(parsed)` (src/agent-os/actor/ + * index.ts), whose JSON envelope is consumed by the Rust deserializer + * (AgentOsConfigJson, deny_unknown_fields). That envelope must preserve the + * native mount descriptors needed at VM creation. + * + * EXPECTED (correct/fixed) BEHAVIOR encoded below: a native `mounts` entry + * supplied via the public `options` should be forwarded into the config + * envelope, while non-serializable mount variants fail loudly. + */ + +import { describe, expect, test } from "vitest"; +import { buildConfigJson } from "@/agent-os/actor/index"; +import type { AgentOsActorConfig } from "@/agent-os/config"; + +describe("custom VFS mount injection at VM creation", () => { + test("buildConfigJson forwards the serializable agent-os config surface", () => { + const parsed = { + options: { + software: [], + additionalInstructions: "Be concise.", + loopbackExemptPorts: [3000, 5173], + allowedNodeBuiltins: ["fs", "path"], + permissions: { + fs: "deny", + network: "allow", + }, + rootFilesystem: { + mode: "read-only", + disableDefaultBaseLayer: true, + }, + limits: { + resources: { + maxProcesses: 4, + }, + }, + sidecar: { + kind: "shared", + pool: "zid", + }, + }, + preview: { + defaultExpiresInSeconds: 3600, + maxExpiresInSeconds: 86400, + }, + } as unknown as AgentOsActorConfig; + + const envelope = JSON.parse(buildConfigJson(parsed)); + + expect(envelope).toMatchObject({ + additionalInstructions: "Be concise.", + loopbackExemptPorts: [3000, 5173], + allowedNodeBuiltins: ["fs", "path"], + permissions: { + fs: "deny", + network: "allow", + }, + rootFilesystem: { + mode: "read-only", + disableDefaultBaseLayer: true, + }, + limits: { + resources: { + maxProcesses: 4, + }, + }, + sidecar: { + pool: "zid", + }, + }); + }); + + test("buildConfigJson forwards native options.mounts into the config envelope", () => { + const parsed = { + options: { + software: [], + mounts: [ + { + path: "/home/user/.pi/agent/sessions", + plugin: { + id: "host_dir", + config: { + hostPath: "/tmp/agent-sessions", + readOnly: false, + }, + }, + readOnly: false, + }, + ], + }, + preview: { + defaultExpiresInSeconds: 3600, + maxExpiresInSeconds: 86400, + }, + } as unknown as AgentOsActorConfig; + + const envelope = JSON.parse(buildConfigJson(parsed)); + + // The public `mounts` option must survive into the envelope that + // drives VM creation. + expect(envelope).toHaveProperty("mounts"); + expect(Array.isArray(envelope.mounts)).toBe(true); + expect(envelope.mounts).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + path: "/home/user/.pi/agent/sessions", + plugin: expect.objectContaining({ + id: "host_dir", + config: expect.objectContaining({ + hostPath: "/tmp/agent-sessions", + }), + }), + }), + ]), + ); + }); + + test("buildConfigJson rejects plain driver mounts because callbacks cannot cross JSON", () => { + const parsed = { + options: { + mounts: [ + { + path: "/home/user/.pi/agent/sessions", + driver: { + read: () => undefined, + }, + }, + ], + }, + preview: { + defaultExpiresInSeconds: 3600, + maxExpiresInSeconds: 86400, + }, + } as unknown as AgentOsActorConfig; + + expect(() => buildConfigJson(parsed)).toThrow( + /Plain mounts|not serializable/i, + ); + }); +}); diff --git a/rivetkit-typescript/packages/rivetkit/tests/agent-os-napi-factory.test.ts b/rivetkit-typescript/packages/rivetkit/tests/agent-os-napi-factory.test.ts new file mode 100644 index 0000000000..b3f5b740e4 --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/tests/agent-os-napi-factory.test.ts @@ -0,0 +1,63 @@ +/// Phase 1b E2E gate. Verifies that `NapiActorFactory.fromAgentOs` returns +/// a non-null handle for valid config and fails loud for unknown fields. +/// +/// This does NOT bring up an agent-os VM — that's Phase 1c's full-driver +/// gate. Phase 1b only proves the JS → NAPI factory construction path. + +import { describe, expect, test } from "vitest"; +import { NapiActorFactory } from "@rivetkit/rivetkit-napi"; + +describe("NapiActorFactory.fromAgentOs (Phase 1b)", () => { + test("returns a handle when given a valid empty config", () => { + const factory = NapiActorFactory.fromAgentOs( + { configJson: "{}" }, + undefined, + ); + expect(factory).toBeDefined(); + }); + + test("returns a handle when configJson is omitted", () => { + const factory = NapiActorFactory.fromAgentOs({}, undefined); + expect(factory).toBeDefined(); + }); + + test("returns a handle for a software-only config", () => { + const configJson = JSON.stringify({ + software: [{ package: "node" }], + }); + const factory = NapiActorFactory.fromAgentOs({ configJson }, undefined); + expect(factory).toBeDefined(); + }); + + test("fails loud on unknown top-level field (driver)", () => { + // `driver` is a non-serializable AgentOsConfig field that must + // never come in via JSON. `deny_unknown_fields` rejects it. + const configJson = JSON.stringify({ + software: [{ package: "node" }], + driver: "some-driver", + }); + expect(() => + NapiActorFactory.fromAgentOs({ configJson }, undefined), + ).toThrow(/configJson|driver|unknown field/i); + }); + + test("fails loud on malformed JSON", () => { + expect(() => + NapiActorFactory.fromAgentOs( + { configJson: "{not valid json" }, + undefined, + ), + ).toThrow(/configJson|parse|expected/i); + }); + + test("fails loud on non-serializable schedule_driver field", () => { + // schedule_driver is `Arc` — explicitly absent + // from the serializable subset. + const configJson = JSON.stringify({ + scheduleDriver: { kind: "timer" }, + }); + expect(() => + NapiActorFactory.fromAgentOs({ configJson }, undefined), + ).toThrow(/configJson|scheduleDriver|unknown field/i); + }); +}); diff --git a/rivetkit-typescript/packages/rivetkit/tests/byte-encoding-parity.test.ts b/rivetkit-typescript/packages/rivetkit/tests/byte-encoding-parity.test.ts new file mode 100644 index 0000000000..e217585e27 --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/tests/byte-encoding-parity.test.ts @@ -0,0 +1,132 @@ +// Cross-language parity tests for byte-payload encoding/decoding. +// +// Reads fixtures generated by Rust at +// `rivetkit-rust/packages/rivetkit/tests/fixtures/encoding/*.json` (run +// `cargo test -p rivetkit --test encoding_fixtures` to regenerate) and +// asserts that: +// 1. TS `encodeJsonCompatValue` produces the same wire shape on the same +// input (encode parity). +// 2. TS `reviveJsonCompatValue` revives Rust-encoded bytes to a real +// `Uint8Array` with the original contents (decode parity). +// +// If this test fails, the Rust framework and the TS framework have +// drifted apart on the wire convention — fix whichever side disagrees. + +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, test } from "vitest"; +import { + encodeJsonCompatValue, + reviveJsonCompatValue, +} from "@/common/encoding"; + +const FIXTURE_DIR = join( + __dirname, + "../../../../rivetkit-rust/packages/rivetkit/tests/fixtures/encoding", +); + +function loadFixture(name: string): unknown { + const raw = readFileSync(join(FIXTURE_DIR, `${name}.json`), "utf8"); + return JSON.parse(raw); +} + +describe("byte-encoding parity (Rust ⇄ TS)", () => { + test("uint8array_hello fixture matches TS encode output", () => { + const rustEncoded = loadFixture("uint8array_hello"); + const tsEncoded = encodeJsonCompatValue( + new Uint8Array([0x68, 0x65, 0x6c, 0x6c, 0x6f]), // "hello" + ); + expect(rustEncoded).toEqual(tsEncoded); + }); + + test("uint8array_1234 fixture matches TS encode output", () => { + const rustEncoded = loadFixture("uint8array_1234"); + const tsEncoded = encodeJsonCompatValue(new Uint8Array([1, 2, 3, 4])); + expect(rustEncoded).toEqual(tsEncoded); + }); + + test("struct_with_byte_field fixture matches TS encode output", () => { + const rustEncoded = loadFixture("struct_with_byte_field"); + const tsEncoded = encodeJsonCompatValue({ + status: 200, + body: new Uint8Array([0x6f, 0x6b]), // "ok" + }); + expect(rustEncoded).toEqual(tsEncoded); + }); + + test("plain string is unchanged across languages", () => { + const rustEncoded = loadFixture("plain_string"); + const tsEncoded = encodeJsonCompatValue("hello world"); + expect(rustEncoded).toEqual(tsEncoded); + }); + + test("TS reviveJsonCompatValue handles Rust-encoded uint8array_hello", () => { + const rustEncoded = loadFixture("uint8array_hello"); + const revived = reviveJsonCompatValue(rustEncoded); + expect(revived).toBeInstanceOf(Uint8Array); + expect(new TextDecoder().decode(revived as Uint8Array)).toBe("hello"); + }); + + test("TS reviveJsonCompatValue handles Rust-encoded uint8array_1234", () => { + const rustEncoded = loadFixture("uint8array_1234"); + const revived = reviveJsonCompatValue(rustEncoded); + expect(revived).toBeInstanceOf(Uint8Array); + expect([...revived]).toEqual([1, 2, 3, 4]); + }); + + test("TS reviveJsonCompatValue handles Rust-encoded nested byte field", () => { + const rustEncoded = loadFixture("struct_with_byte_field"); + const revived = reviveJsonCompatValue(rustEncoded) as { + status: number; + body: Uint8Array; + }; + expect(revived.status).toBe(200); + expect(revived.body).toBeInstanceOf(Uint8Array); + expect(new TextDecoder().decode(revived.body)).toBe("ok"); + }); + + // Structured non-byte payload (VirtualStat-shaped). Phase 2 gate: + // every field — bool, u32, u64, f64 with fractional precision, + // camelCase renames — must come through the Rust encode -> TS decode + // path losslessly. + test("virtual_stat fixture round-trips all field types from Rust to TS", () => { + const rustEncoded = loadFixture("virtual_stat") as Record< + string, + unknown + >; + + // Integer fields. + expect(rustEncoded.mode).toBe(0o100_644); + expect(rustEncoded.size).toBe(7); + expect(rustEncoded.blocks).toBe(1); + expect(rustEncoded.dev).toBe(42); + expect(rustEncoded.rdev).toBe(0); + expect(rustEncoded.nlink).toBe(1); + expect(rustEncoded.uid).toBe(1000); + expect(rustEncoded.gid).toBe(1000); + + // Booleans. + expect(rustEncoded.isDirectory).toBe(false); + expect(rustEncoded.isSymbolicLink).toBe(false); + + // f64 with sub-integer precision must survive intact. + expect(rustEncoded.atimeMs).toBe(1_780_000_000_000.5); + expect(rustEncoded.mtimeMs).toBe(1_780_000_001_000.25); + expect(rustEncoded.ctimeMs).toBe(1_780_000_002_000.125); + expect(rustEncoded.birthtimeMs).toBe(1_780_000_003_000.0625); + + // Large u64 — must not silently truncate to f53. + expect(rustEncoded.ino).toBe(9_876_543_210); + + // camelCase renames: snake_case Rust field names must not leak. + expect(rustEncoded).not.toHaveProperty("is_directory"); + expect(rustEncoded).not.toHaveProperty("is_symbolic_link"); + expect(rustEncoded).not.toHaveProperty("atime_ms"); + expect(rustEncoded).not.toHaveProperty("birthtime_ms"); + + // TS reviveJsonCompatValue on a structured (non-byte) payload + // must be a no-op: the object passes through unchanged. + const revived = reviveJsonCompatValue(rustEncoded); + expect(revived).toEqual(rustEncoded); + }); +}); diff --git a/rivetkit-typescript/packages/rivetkit/tests/cjs-no-import-meta.test.ts b/rivetkit-typescript/packages/rivetkit/tests/cjs-no-import-meta.test.ts new file mode 100644 index 0000000000..e6a49bf106 --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/tests/cjs-no-import-meta.test.ts @@ -0,0 +1,96 @@ +import { readdirSync, readFileSync, statSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, test } from "vitest"; + +// Regression guard for rivetkit issue #2: +// "import.meta.url in CJS chunks crashes ts-node/CJS loaders" +// +// Background: tsup emits CommonJS (.cjs) bundles for the `require` conditions +// of every package export. `import.meta` is an ESM-only syntactic form; when a +// raw `import.meta.url` leaks into a .cjs file, loading that file under a +// CommonJS loader (ts-node, plain `require()`, older bundlers) throws a +// SyntaxError ("Cannot use 'import.meta' outside a module"). The fix is +// `shims: true` in the shared tsup config (tsup.base.ts), which rewrites +// `import.meta.url` into a CJS-safe shim (e.g. `new URL(\`file:${__filename}\`).href`). +// +// This test statically scans every produced .cjs file in the built dist/tsup +// output and asserts that none of them contains the literal `import.meta.url`. +// It directly encodes the original failure mode: if `shims` regresses (or a new +// reachable usage slips past tree-shaking), the offending bundle will contain +// the raw token and this test will fail before it ever crashes a CJS consumer. +// +// Approach chosen: static build-output assertion (the lightest reliable check). +// It requires the package to be built. If dist/tsup has not been built yet we +// throw with an actionable message rather than silently passing, so a missing +// build is never mistaken for "no occurrences found". + +const TEST_DIR = dirname(fileURLToPath(import.meta.url)); +const PACKAGE_DIR = resolve(TEST_DIR, ".."); +const DIST_TSUP_DIR = resolve(PACKAGE_DIR, "dist", "tsup"); + +const FORBIDDEN_TOKEN = "import.meta.url"; + +function isDirectory(path: string): boolean { + try { + return statSync(path).isDirectory(); + } catch { + return false; + } +} + +function collectCjsFiles(root: string): string[] { + const results: string[] = []; + const stack: string[] = [root]; + while (stack.length > 0) { + const current = stack.pop(); + if (current === undefined) continue; + for (const entry of readdirSync(current, { withFileTypes: true })) { + const fullPath = resolve(current, entry.name); + if (entry.isDirectory()) { + stack.push(fullPath); + } else if (entry.isFile() && entry.name.endsWith(".cjs")) { + results.push(fullPath); + } + } + } + return results; +} + +describe("rivetkit CJS bundles are free of raw import.meta (issue #2)", () => { + test("no built .cjs file contains a raw import.meta.url token", () => { + if (!isDirectory(DIST_TSUP_DIR)) { + throw new Error( + `Expected built CJS output at ${DIST_TSUP_DIR} but it does not exist. ` + + `Run \`pnpm run build\` in ${PACKAGE_DIR} before running this regression guard.`, + ); + } + + const cjsFiles = collectCjsFiles(DIST_TSUP_DIR); + + // Sanity: the build must have produced at least one CJS bundle. If it + // produced none, the scan below would be vacuously green, which would + // silently mask a regression. + expect( + cjsFiles.length, + `Expected at least one .cjs bundle under ${DIST_TSUP_DIR}; found none. ` + + `The build may be incomplete.`, + ).toBeGreaterThan(0); + + const offenders: string[] = []; + for (const file of cjsFiles) { + const contents = readFileSync(file, "utf8"); + if (contents.includes(FORBIDDEN_TOKEN)) { + offenders.push(file.slice(PACKAGE_DIR.length + 1)); + } + } + + expect( + offenders, + `Found raw \`${FORBIDDEN_TOKEN}\` in ${offenders.length} built CJS file(s). ` + + `import.meta is ESM-only and crashes CommonJS/ts-node loaders. ` + + `Ensure \`shims: true\` remains set in tsup.base.ts. Offending files:\n` + + offenders.map((f) => ` - ${f}`).join("\n"), + ).toEqual([]); + }); +}); diff --git a/rivetkit-typescript/packages/rivetkit/tests/driver/actor-agent-os.test.ts b/rivetkit-typescript/packages/rivetkit/tests/driver/actor-agent-os.test.ts index 68d81e9d1e..ff679fce82 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/driver/actor-agent-os.test.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/driver/actor-agent-os.test.ts @@ -3,6 +3,7 @@ import { describe, expect, test } from "vitest"; import { describeDriverMatrix } from "./shared-matrix"; import { setupDriverTest } from "./shared-utils"; +const DRIVER_API_TOKEN = "dev"; const require = createRequire(import.meta.url); const hasAgentOsCore = (() => { try { @@ -13,6 +14,113 @@ const hasAgentOsCore = (() => { } })(); +async function forceActorSleep(input: { + endpoint: string; + namespace: string; + actorId: string; +}) { + const response = await fetch( + `${input.endpoint}/actors/${encodeURIComponent(input.actorId)}/sleep?namespace=${encodeURIComponent(input.namespace)}`, + { + method: "POST", + headers: { + Authorization: `Bearer ${DRIVER_API_TOKEN}`, + "Content-Type": "application/json", + }, + body: "{}", + }, + ); + if (!response.ok) { + throw new Error( + `failed to force actor sleep: ${response.status} ${await response.text()}`, + ); + } +} + +async function waitForActorSleep(input: { + endpoint: string; + namespace: string; + actorId: string; + timeoutMs: number; +}) { + const deadline = Date.now() + input.timeoutMs; + while (Date.now() < deadline) { + const response = await fetch( + `${input.endpoint}/actors?actor_ids=${encodeURIComponent(input.actorId)}&namespace=${encodeURIComponent(input.namespace)}`, + { + headers: { + Authorization: `Bearer ${DRIVER_API_TOKEN}`, + }, + }, + ); + expect(response.ok).toBe(true); + const body = (await response.json()) as { + actors: Array<{ sleep_ts?: number | null }>; + }; + if (body.actors[0]?.sleep_ts) { + return; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + throw new Error(`timed out waiting for actor ${input.actorId} to sleep`); +} + +async function waitForSessionEvents(input: { + actor: any; + sessionId: string; + predicate: (events: any[]) => boolean; + timeoutMs: number; +}): Promise { + const deadline = Date.now() + input.timeoutMs; + let lastEvents: any[] = []; + while (Date.now() < deadline) { + lastEvents = (await input.actor.getSessionEvents( + input.sessionId, + )) as any[]; + if (input.predicate(lastEvents)) { + return lastEvents; + } + await new Promise((resolve) => setTimeout(resolve, 250)); + } + throw new Error( + `timed out waiting for session events; last events=${JSON.stringify(lastEvents)}`, + ); +} + +function hasUpdate(events: any[], kind: string): boolean { + return events.some((event) => { + const update = event?.params?.update; + return update?.sessionUpdate === kind; + }); +} + +function expectPromptBeforeFollowingUpdate(events: any[], promptText: string) { + const promptIndex = events.findIndex( + (event) => + event?.method === "user_prompt" && + event?.params?.text === promptText, + ); + expect(promptIndex).toBeGreaterThanOrEqual(0); + + const updateIndex = events.findIndex( + (event, index) => + index > promptIndex && event?.method === "session/update", + ); + expect(updateIndex).toBeGreaterThan(promptIndex); +} + +function parsePromptBlocks( + text: string, +): Array<{ type: string; text: string }> { + return JSON.parse(text) as Array<{ type: string; text: string }>; +} + +function parseProbeBlock(blocks: Array<{ type: string; text: string }>) { + const probe = blocks.find((block) => block.type === "probe"); + expect(probe).toBeDefined(); + return JSON.parse(probe!.text) as { cwd?: string; env?: string }; +} + describeDriverMatrix("Actor Agent Os", (driverTestConfig) => { describe.skipIf(driverTestConfig.skip?.agentOs || !hasAgentOsCore)( "Actor agentOS Tests", @@ -33,6 +141,270 @@ describeDriverMatrix("Actor Agent Os", (driverTestConfig) => { expect(new TextDecoder().decode(data)).toBe("hello world"); }, 60_000); + test.skipIf(driverTestConfig.skip?.sleep)( + "filesystem survives sleep and wake", + async (c) => { + const { client, endpoint, namespace } = + await setupDriverTest(c, { + ...driverTestConfig, + useRealTimers: true, + }); + const actorKey = `fs-sleep-${crypto.randomUUID()}`; + const path = "/home/user/sleep-persist.txt"; + const actor = client.agentOsTestActor.getOrCreate([ + actorKey, + ]); + + await actor.writeFile(path, "durable hello"); + const actorId = await actor.resolve(); + await forceActorSleep({ endpoint, namespace, actorId }); + await waitForActorSleep({ + endpoint, + namespace, + actorId, + timeoutMs: 30_000, + }); + + const actorAfterWake = client.agentOsTestActor.getOrCreate([ + actorKey, + ]); + const data = await actorAfterWake.readFile(path); + expect(new TextDecoder().decode(data)).toBe( + "durable hello", + ); + }, + 90_000, + ); + + test("session capture persists tool calls and message chunks", async (c) => { + const { client } = await setupDriverTest(c, { + ...driverTestConfig, + useRealTimers: true, + }); + const actor = client.agentOsTestActor.getOrCreate([ + `session-capture-${crypto.randomUUID()}`, + ]); + + const { sessionId } = (await actor.createSession("opencode", { + env: { MOCK_RESUME_SCENARIO: "native" }, + })) as { sessionId: string }; + const result = (await actor.sendPrompt( + sessionId, + "capture both update kinds", + )) as { text: string }; + expect(parsePromptBlocks(result.text).at(-1)?.text).toBe( + "capture both update kinds", + ); + + const events = await waitForSessionEvents({ + actor, + sessionId, + timeoutMs: 10_000, + predicate: (events) => + hasUpdate(events, "tool_call") && + hasUpdate(events, "agent_message_chunk"), + }); + expect(hasUpdate(events, "tool_call")).toBe(true); + expect(hasUpdate(events, "agent_message_chunk")).toBe(true); + expectPromptBeforeFollowingUpdate( + events, + "capture both update kinds", + ); + }, 90_000); + + test.skipIf(driverTestConfig.skip?.sleep)( + "session fallback resume survives real sleep/wake with external id remap", + async (c) => { + const { client, endpoint, namespace } = + await setupDriverTest(c, { + ...driverTestConfig, + useRealTimers: true, + }); + const actorKey = `session-fallback-${crypto.randomUUID()}`; + const actor = client.agentOsTestActor.getOrCreate([ + actorKey, + ]); + const cwd = `/home/user/fallback-cwd-${crypto.randomUUID()}`; + const envProbe = `fallback-env-${crypto.randomUUID()}`; + await actor.mkdir(cwd); + + const { sessionId } = (await actor.createSession( + "opencode", + { + cwd, + env: { + MOCK_RESUME_SCENARIO: "fallthrough", + MOCK_CWD_ENV_PROBE: envProbe, + }, + }, + )) as { sessionId: string }; + await actor.sendPrompt(sessionId, "remember alpha"); + await waitForSessionEvents({ + actor, + sessionId, + timeoutMs: 10_000, + predicate: (events) => + hasUpdate(events, "tool_call") && + hasUpdate(events, "agent_message_chunk"), + }); + + const actorId = await actor.resolve(); + await forceActorSleep({ endpoint, namespace, actorId }); + await waitForActorSleep({ + endpoint, + namespace, + actorId, + timeoutMs: 30_000, + }); + + const actorAfterWake = client.agentOsTestActor.getOrCreate([ + actorKey, + ]); + const resumed = (await actorAfterWake.sendPrompt( + sessionId, + "continue after fallback", + )) as { text: string }; + const blocks = parsePromptBlocks(resumed.text); + expect(blocks).toHaveLength(3); + expect(blocks[0].text).toContain( + "You are continuing an earlier session", + ); + expect(blocks[0].text).toContain( + `/root/.agentos/threads/${sessionId}.md`, + ); + expect(blocks[1].text).toBe("continue after fallback"); + expect(parseProbeBlock(blocks)).toEqual({ + cwd, + env: envProbe, + }); + + const events = await waitForSessionEvents({ + actor: actorAfterWake, + sessionId, + timeoutMs: 10_000, + predicate: (events) => + events.filter( + (event) => event?.method === "user_prompt", + ).length >= 2 && + hasUpdate(events, "tool_call") && + hasUpdate(events, "agent_message_chunk"), + }); + expect( + events.some((event) => event?.method === "user_prompt"), + ).toBe(true); + expectPromptBeforeFollowingUpdate( + events, + "continue after fallback", + ); + }, + 120_000, + ); + + test.skipIf(driverTestConfig.skip?.sleep)( + "session native resume survives real sleep/wake without preamble", + async (c) => { + const { client, endpoint, namespace } = + await setupDriverTest(c, { + ...driverTestConfig, + useRealTimers: true, + }); + const actorKey = `session-native-${crypto.randomUUID()}`; + const actor = client.agentOsTestActor.getOrCreate([ + actorKey, + ]); + const cwd = `/home/user/native-cwd-${crypto.randomUUID()}`; + const envProbe = `native-env-${crypto.randomUUID()}`; + await actor.mkdir(cwd); + + const { sessionId } = (await actor.createSession( + "opencode", + { + cwd, + env: { + MOCK_RESUME_SCENARIO: "native", + MOCK_CWD_ENV_PROBE: envProbe, + }, + }, + )) as { sessionId: string }; + await actor.sendPrompt(sessionId, "before native sleep"); + + const actorId = await actor.resolve(); + await forceActorSleep({ endpoint, namespace, actorId }); + await waitForActorSleep({ + endpoint, + namespace, + actorId, + timeoutMs: 30_000, + }); + + const actorAfterWake = client.agentOsTestActor.getOrCreate([ + actorKey, + ]); + const resumed = (await actorAfterWake.sendPrompt( + sessionId, + "continue after native", + )) as { text: string }; + const blocks = parsePromptBlocks(resumed.text); + expect(blocks).toHaveLength(2); + expect(blocks[0].text).toBe("continue after native"); + expect(parseProbeBlock(blocks)).toEqual({ + cwd, + env: envProbe, + }); + }, + 120_000, + ); + + test.skipIf(driverTestConfig.skip?.sleep)( + "closeSession removes persisted session after sleep before lazy resume", + async (c) => { + const { client, endpoint, namespace } = + await setupDriverTest(c, { + ...driverTestConfig, + useRealTimers: true, + }); + const actorKey = `session-close-slept-${crypto.randomUUID()}`; + const actor = client.agentOsTestActor.getOrCreate([ + actorKey, + ]); + + const { sessionId } = (await actor.createSession( + "opencode", + { + env: { MOCK_RESUME_SCENARIO: "native" }, + }, + )) as { sessionId: string }; + await actor.sendPrompt(sessionId, "before close sleep"); + + const actorId = await actor.resolve(); + await forceActorSleep({ endpoint, namespace, actorId }); + await waitForActorSleep({ + endpoint, + namespace, + actorId, + timeoutMs: 30_000, + }); + + const actorAfterWake = client.agentOsTestActor.getOrCreate([ + actorKey, + ]); + await actorAfterWake.closeSession(sessionId); + const sessions = + (await actorAfterWake.listPersistedSessions()) as Array<{ + sessionId: string; + }>; + expect( + sessions.some( + (session) => session.sessionId === sessionId, + ), + ).toBe(false); + expect( + await actorAfterWake.getSessionEvents(sessionId), + ).toEqual([]); + }, + 120_000, + ); + test("mkdir and readdir", async (c) => { const { client } = await setupDriverTest(c, { ...driverTestConfig, @@ -139,6 +511,42 @@ describeDriverMatrix("Actor Agent Os", (driverTestConfig) => { ); }, 60_000); + // Partial-failure verification for the batch DTO mapping. + // `BatchReadResultDto` uses `Option` content and + // `Option` error, both `skip_serializing_if`. A bug + // where the partial shape doesn't make it across the encoding + // wire (e.g. None elided incorrectly, error string not + // surfaced) would be silent without this test. + test("readFiles surfaces per-entry error for missing paths", async (c) => { + const { client } = await setupDriverTest(c, { + ...driverTestConfig, + useRealTimers: true, + }); + const actor = client.agentOsTestActor.getOrCreate([ + `partial-${crypto.randomUUID()}`, + ]); + + await actor.writeFile("/home/user/exists.txt", "present"); + + const results = await actor.readFiles([ + "/home/user/exists.txt", + "/home/user/does-not-exist.txt", + ]); + + expect(results).toHaveLength(2); + // Successful entry: content present, no error field. + expect(results[0].path).toBe("/home/user/exists.txt"); + expect(new TextDecoder().decode(results[0].content)).toBe( + "present", + ); + expect(results[0].error).toBeUndefined(); + // Failed entry: no content, error string surfaced. + expect(results[1].path).toBe("/home/user/does-not-exist.txt"); + expect(results[1].content).toBeUndefined(); + expect(typeof results[1].error).toBe("string"); + expect(results[1].error?.length).toBeGreaterThan(0); + }, 60_000); + test("readdirRecursive lists nested files", async (c) => { const { client } = await setupDriverTest(c, { ...driverTestConfig, diff --git a/rivetkit-typescript/packages/rivetkit/tests/driver/shared-harness.ts b/rivetkit-typescript/packages/rivetkit/tests/driver/shared-harness.ts index 92f3610387..206b9a233d 100644 --- a/rivetkit-typescript/packages/rivetkit/tests/driver/shared-harness.ts +++ b/rivetkit-typescript/packages/rivetkit/tests/driver/shared-harness.ts @@ -435,7 +435,11 @@ export function createWasmDriverTestConfig( runtime: "wasm", sqliteBackend: "remote", encoding: options.encoding, - skip: options.skip, + // agent-os requires the NAPI runtime; rivetkit-agent-os depends + // on agent-os-client which uses tokio::process (native-only). + // Default-skip the agent-os suite on wasm; callers can override + // by passing `skip: { agentOs: false }` explicitly. + skip: { agentOs: true, ...options.skip }, features: { hibernatableWebSocketProtocol: false, ...options.features, diff --git a/scripts/agent-os-dep.mjs b/scripts/agent-os-dep.mjs new file mode 100644 index 0000000000..165b099e16 --- /dev/null +++ b/scripts/agent-os-dep.mjs @@ -0,0 +1,265 @@ +#!/usr/bin/env node +// ============================================================================= +// agent-os dependency manager (rivet / r6 -> agent-os) +// ============================================================================= +// +// Single tool to control how this repo (rivet / r6) consumes agent-os, mirroring +// agent-os's own `scripts/secure-exec-dep.mjs` (which controls how agent-os +// consumes secure-exec). Same two-mode model: +// +// pinned (default for CI/release) — every agent-os dependency resolves from +// its PUBLISHED artifact: npm `@rivet-dev/agent-os-*` from the registry +// at the pinned version, and the Rust `agent-os-client` crate from a +// vendored git rev. CI needs no sibling checkout. +// local (for hacking on agent-os) — every swappable dependency is redirected +// at the sibling ../agent-os checkout: npm via `link:` and the cargo +// `agent-os-client` crate via `path = ".../agent-os/crates/client"`. +// This is the local dev loop: edit agent-os, rebuild here, no publish. +// +// Commands: +// node scripts/agent-os-dep.mjs local +// node scripts/agent-os-dep.mjs pinned +// node scripts/agent-os-dep.mjs set-version # bump pinned npm version +// node scripts/agent-os-dep.mjs status +// +// After `local`/`pinned`/`set-version`, run `pnpm install` and a cargo build so +// the lockfiles pick up the new resolution. +// +// See the rivet CLAUDE.md "Agent OS dependency (local dev vs preview publish)" +// section for the full workflow. +// ============================================================================= + +import { readFileSync, writeFileSync, existsSync, readdirSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const AGENT_OS_REL = "../agent-os"; // sibling checkout, same convention as agent-os -> ../secure-exec +const AGENT_OS_ABS = path.resolve(ROOT, AGENT_OS_REL); + +// npm `@rivet-dev/agent-os-*` package -> its source dir under the agent-os repo. +// (Published name -> repo subpath. `common` is renamed from registry/software at +// publish time; we link its source dir when present.) +const NPM_PKGS = { + "@rivet-dev/agent-os-core": "packages/core", + "@rivet-dev/agent-os-sidecar": "packages/sidecar-binary", + "@rivet-dev/agent-os-sandbox": "packages/agent-os-sandbox", + "@rivet-dev/agent-os-pi": "registry/agent/pi", + "@rivet-dev/agent-os-common": "registry/software/common", +}; + +// Rust crate -> its source dir under the agent-os repo. +const CRATES = { + "agent-os-client": "crates/client", +}; + +// Pinned (published) versions, used by `pinned` mode. `set-version` rewrites +// these in place. agent-os publishes core/sidecar/pi/sandbox on one cadence and +// the renamed software packages (common) on another, hence two seeds. +const SEED_VERSION = "0.0.0-main.8794200"; +const SEED_SOFTWARE_VERSION = "0.0.260331072558"; +// Pinned git rev for the Rust crate (no crates.io publish). Empty => `pinned` +// leaves the cargo dep as-is and warns; set it once agent-os has a tagged rev. +const PINNED_GIT = { repo: "https://github.com/rivet-dev/agent-os.git", rev: "" }; + +const STATE_FILE = path.join(ROOT, "scripts", ".agent-os-dep.json"); + +// --------------------------------------------------------------------------- +const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + +function loadState() { + if (existsSync(STATE_FILE)) { + try { + return JSON.parse(readFileSync(STATE_FILE, "utf8")); + } catch { + /* fall through to seed */ + } + } + return { version: SEED_VERSION, softwareVersion: SEED_SOFTWARE_VERSION }; +} +function saveState(state) { + writeFileSync(STATE_FILE, `${JSON.stringify(state, null, 2)}\n`); +} +function pinnedVersionFor(name, state) { + return name === "@rivet-dev/agent-os-common" + ? state.softwareVersion + : state.version; +} + +// --------------------------------------------------------------------------- +// consumer discovery +// --------------------------------------------------------------------------- +// Every package.json under these groups is a potential npm consumer; every +// Cargo.toml under the workspace is a potential cargo consumer. +function packageManifests() { + const out = []; + const groups = ["examples", "rivetkit-typescript/packages"]; + for (const group of groups) { + const base = path.join(ROOT, group); + if (!existsSync(base)) continue; + for (const entry of readdirSync(base, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const p = path.join(base, entry.name, "package.json"); + if (existsSync(p)) out.push(p); + } + } + return out; +} +function cargoManifests() { + // Only the two crates that path-depend on agent-os today; discovered by grep + // to stay correct if more are added. + const out = []; + const candidates = [ + "rivetkit-rust/packages/rivetkit-agent-os/Cargo.toml", + "rivetkit-typescript/packages/rivetkit-napi/Cargo.toml", + ]; + for (const rel of candidates) { + const p = path.join(ROOT, rel); + if (existsSync(p)) out.push(p); + } + return out; +} + +// Relative path from a manifest's dir to an agent-os subpath. +function relTo(manifestPath, subpath) { + const target = path.join(AGENT_OS_ABS, subpath); + let rel = path.relative(path.dirname(manifestPath), target); + if (!rel.startsWith(".")) rel = `./${rel}`; + return rel; +} + +// --------------------------------------------------------------------------- +// npm: rewrite consumer dep values +// --------------------------------------------------------------------------- +function rewriteNpm(mode, state) { + let changed = 0; + for (const manifest of packageManifests()) { + let text = readFileSync(manifest, "utf8"); + const before = text; + for (const [name, subpath] of Object.entries(NPM_PKGS)) { + if (!new RegExp(`"${escapeRe(name)}"\\s*:`).test(text)) continue; + let value; + if (mode === "local") { + const target = path.join(AGENT_OS_ABS, subpath); + if (!existsSync(target)) continue; // skip packages with no local source + value = `link:${relTo(manifest, subpath)}`; + } else { + value = pinnedVersionFor(name, state); + } + const re = new RegExp(`("${escapeRe(name)}"\\s*:\\s*)"[^"]*"`, "g"); + text = text.replace(re, `$1"${value}"`); + } + if (text !== before) { + writeFileSync(manifest, text); + changed++; + } + } + return changed; +} + +// --------------------------------------------------------------------------- +// cargo: rewrite agent-os-client path/git dep +// --------------------------------------------------------------------------- +function rewriteCargo(mode) { + let changed = 0; + for (const manifest of cargoManifests()) { + const lines = readFileSync(manifest, "utf8").split("\n"); + let touched = false; + const out = lines.map((line) => { + const m = line.match(/^(\s*)([A-Za-z0-9_-]+)\s*=\s*\{(.*)\}\s*$/); + if (!m) return line; + const [, indent, key, body] = m; + const pkg = (body.match(/package\s*=\s*"([^"]+)"/) || [])[1] || key; + const subpath = CRATES[pkg]; + if (!subpath) return line; + touched = true; + const parts = []; + if (body.includes("package =")) parts.push(`package = "${pkg}"`); + if (mode === "local") { + parts.push(`path = "${relTo(manifest, subpath)}"`); + } else if (PINNED_GIT.rev) { + parts.push(`git = "${PINNED_GIT.repo}"`, `rev = "${PINNED_GIT.rev}"`); + } else { + // No published crate + no pinned rev: leave the line unchanged and warn. + console.warn( + ` [warn] ${path.relative(ROOT, manifest)}: '${pkg}' has no pinned git rev; left unchanged. Set PINNED_GIT.rev to enable pinned cargo mode.`, + ); + return line; + } + return `${indent}${key} = { ${parts.join(", ")} }`; + }); + if (touched) { + writeFileSync(manifest, out.join("\n")); + changed++; + } + } + return changed; +} + +// --------------------------------------------------------------------------- +// status detection +// --------------------------------------------------------------------------- +function npmMode() { + for (const manifest of packageManifests()) { + const text = readFileSync(manifest, "utf8"); + for (const name of Object.keys(NPM_PKGS)) { + if (new RegExp(`"${escapeRe(name)}"\\s*:\\s*"link:`).test(text)) return "local"; + } + } + return "pinned"; +} +function cargoMode() { + for (const manifest of cargoManifests()) { + const text = readFileSync(manifest, "utf8"); + if (/agent-os-client\s*=\s*\{[^}]*\bpath\s*=/.test(text)) return "local"; + } + return "pinned"; +} +function currentMode() { + const n = npmMode(); + const c = cargoMode(); + return n === c ? n : `hybrid(npm=${n},cargo=${c})`; +} + +// --------------------------------------------------------------------------- +const [cmd, arg] = process.argv.slice(2); +switch (cmd) { + case "local": { + const n = rewriteNpm("local", loadState()); + const c = rewriteCargo("local"); + console.log(`agent-os deps -> LOCAL (../agent-os via link:/path). npm:${n} cargo:${c} manifests.`); + console.log("Run: pnpm install (and a cargo build) to refresh lockfiles."); + break; + } + case "pinned": { + const n = rewriteNpm("pinned", loadState()); + const c = rewriteCargo("pinned"); + console.log(`agent-os deps -> PINNED (published versions). npm:${n} cargo:${c} manifests.`); + console.log("Run: pnpm install to refresh the lockfile."); + break; + } + case "set-version": { + if (!arg) { + console.error("usage: set-version "); + process.exit(1); + } + const state = loadState(); + state.version = arg; + saveState(state); + if (npmMode() === "pinned") rewriteNpm("pinned", state); + console.log(`pinned @rivet-dev/agent-os-* version set to ${arg}.`); + console.log("Run: pnpm install to refresh the lockfile."); + break; + } + case "status": { + const state = loadState(); + console.log(`mode: ${currentMode()}`); + console.log(`sibling: ${AGENT_OS_ABS} (${existsSync(AGENT_OS_ABS) ? "present" : "MISSING"})`); + console.log(`pinned npm version: ${state.version} (software: ${state.softwareVersion})`); + console.log(`pinned cargo rev: ${PINNED_GIT.rev || "(none — cargo stays local until set)"}`); + break; + } + default: + console.error("usage: agent-os-dep.mjs |status>"); + process.exit(1); +} diff --git a/website/src/content/docs/agent-os/system-prompt.mdx b/website/src/content/docs/agent-os/system-prompt.mdx index 68d8adc49e..dd91d54b0f 100644 --- a/website/src/content/docs/agent-os/system-prompt.mdx +++ b/website/src/content/docs/agent-os/system-prompt.mdx @@ -6,7 +6,7 @@ skill: true agentOS automatically injects a system prompt into every agent session that describes the VM environment and available tools. The prompt is additive and never replaces the agent's own instructions (CLAUDE.md, AGENTS.md, etc.). -The base prompt lives at `/etc/agentos/instructions.md` inside the VM. +The prompt is assembled and injected at the start of each session rather than baked into the VM image, so it always reflects the tools currently exposed to that session. ## Customization