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/Cargo.lock b/Cargo.lock index 84e63204dd..8e274244a2 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.3.0-rc.1" +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.3.0-rc.1" +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,16 +2255,36 @@ dependencies = [ "curve25519-dalek", "ed25519", "sha2", - "signature", + "signature 2.2.0", + "subtle", +] + +[[package]] +name = "either" +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 = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - [[package]] name = "encode_unicode" version = "1.0.0" @@ -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]] @@ -4665,6 +5635,17 @@ dependencies = [ "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]] name = "rand_chacha" version = "0.3.1" @@ -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,29 @@ dependencies = [ "vbare", ] +[[package]] +name = "rivetkit-agent-os" +version = "2.3.0-rc.12" +dependencies = [ + "agent-os-client", + "anyhow", + "base64 0.22.1", + "bytes", + "ciborium", + "futures", + "http 1.3.1", + "rivet-error", + "rivetkit", + "rivetkit-core", + "rusqlite", + "serde", + "serde_bytes", + "serde_json", + "tokio", + "tracing", + "uuid", +] + [[package]] name = "rivetkit-client" version = "2.3.0-rc.12" @@ -6033,7 +7077,7 @@ dependencies = [ "portpicker", "reqwest 0.12.22", "rivetkit-client-protocol", - "scc", + "scc 3.6.12", "serde", "serde_bare", "serde_cbor", @@ -6088,7 +7132,7 @@ dependencies = [ "rivetkit-client-protocol", "rivetkit-inspector-protocol", "rivetkit-shared-types", - "scc", + "scc 3.6.12", "serde", "serde_bare", "serde_bytes", @@ -6123,8 +7167,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 +7181,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 +7213,7 @@ dependencies = [ "js-sys", "rivet-error", "rivetkit-core", - "scc", + "scc 3.6.12", "serde", "serde-wasm-bindgen", "serde_json", @@ -6225,6 +7274,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 +7295,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 +7317,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 +7412,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 +7428,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 +7445,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 +7518,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 +7536,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 +7552,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f448f7d881535036452f0cac656a41463807f095eda504890764ca7d11e2a2ea" dependencies = [ "saa", - "sdd", + "sdd 4.7.5", ] [[package]] @@ -6446,63 +7575,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 +7856,7 @@ checksum = "48b85e25e8a1fc13928885e8bf13abe8a09e15c46993aed05d6405f7755d6e20" dependencies = [ "httpdate", "reqwest 0.12.22", - "rustls", + "rustls 0.23.40", "sentry-anyhow", "sentry-backtrace", "sentry-contexts", @@ -6563,7 +7865,7 @@ dependencies = [ "sentry-panic", "sentry-tracing", "tokio", - "ureq", + "ureq 3.1.2", ] [[package]] @@ -6874,7 +8176,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -6885,7 +8187,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -6904,6 +8206,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 +8231,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 +8286,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 +8367,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 +8397,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 +8414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", ] [[package]] @@ -7242,6 +8598,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 +8671,7 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix", + "rustix 1.0.8", "windows-sys 0.59.0", ] @@ -7344,7 +8711,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 +8828,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 +8838,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 +8985,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 +9058,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 +9093,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 +9151,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 +9400,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 +9435,7 @@ dependencies = [ "httparse", "log", "rand 0.9.2", - "rustls", + "rustls 0.23.40", "rustls-pki-types", "sha1", "thiserror 2.0.12", @@ -8173,7 +9573,7 @@ dependencies = [ "rivet-test-deps-docker", "rivet-ups-protocol", "rivet-util", - "scc", + "scc 3.6.12", "serde", "serde_json", "sha2", @@ -8195,12 +9595,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 +9634,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 +9726,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 +9811,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 +10061,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 +10094,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 +10277,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 +10306,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 +10334,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 +10370,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 +10403,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 +10431,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 +10443,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 +10455,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 +10473,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 +10485,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 +10497,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 +10509,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 +10637,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 +10649,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/package.json b/examples/agent-os-e2e/package.json index 6796ebb01f..b68d732cad 100644 --- a/examples/agent-os-e2e/package.json +++ b/examples/agent-os-e2e/package.json @@ -4,14 +4,15 @@ "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": "tsx src/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-pi": "0.0.0-main.8794200" }, "devDependencies": { "@types/node": "^22.13.9", 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..4b137e1a81 --- /dev/null +++ b/examples/agent-os-e2e/scripts/prepare-agent-modules.mjs @@ -0,0 +1,39 @@ +// 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")); +const PI_VER = pi.dependencies["@rivet-dev/agent-os-pi"]; +// 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 want = JSON.stringify(deps); +if (existsSync(stamp) && 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..880fb97c13 100644 --- a/examples/agent-os-e2e/src/client.ts +++ b/examples/agent-os-e2e/src/client.ts @@ -105,9 +105,14 @@ 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(); diff --git a/examples/agent-os-e2e/src/server.ts b/examples/agent-os-e2e/src/server.ts index 7522c60853..f1d5d6efd4 100644 --- a/examples/agent-os-e2e/src/server.ts +++ b/examples/agent-os-e2e/src/server.ts @@ -1,9 +1,23 @@ +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 vm = agentOs({ + options: { + software: [common, pi], + mounts: [nodeModulesMount(agentModules)], + }, +}); export const registry = setup({ use: { vm } }); registry.start(); diff --git a/examples/agent-os/package.json b/examples/agent-os/package.json index 7823fd392b..67a7bf8ed9 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": "0.0.0-main.8794200", + "@rivet-dev/agent-os-pi": "0.0.0-main.8794200", + "@rivet-dev/agent-os-sandbox": "0.0.0-main.8794200", + "@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/pnpm-lock.yaml b/pnpm-lock.yaml index 9b599fc08b..2c3789ec22 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -223,9 +223,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: 0.0.0-main.8794200 + version: 0.0.0-main.8794200 + '@rivet-dev/agent-os-pi': + specifier: 0.0.0-main.8794200 + version: 0.0.0-main.8794200(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(zod@3.25.76) + '@rivet-dev/agent-os-sandbox': + specifier: 0.0.0-main.8794200 + version: 0.0.0-main.8794200(get-port@7.1.0) + '@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,11 +261,11 @@ importers: examples/agent-os-e2e: dependencies: '@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@3.25.76))(ws@8.20.1)(zod@3.25.76) + specifier: 0.0.0-main.8794200 + version: 0.0.0-main.8794200(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.3.6))(zod@4.3.6) rivetkit: specifier: workspace:* version: link:../../rivetkit-typescript/packages/rivetkit @@ -2090,7 +2108,7 @@ importers: version: 5.2.2(react-hook-form@7.62.0(react@19.1.0)) '@ladle/react': specifier: ^5.1.1 - version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) + version: 5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) '@marsidev/react-turnstile': specifier: ^1.5.0 version: 1.5.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -2339,7 +2357,7 @@ importers: version: 2.4.3 better-auth: specifier: ^1.5.6 - version: 1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(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))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(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))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) canvas-confetti: specifier: ^1.9.3 version: 1.9.3 @@ -2459,7 +2477,7 @@ importers: version: 5.2.0(ts-node@10.9.2(@swc/core@1.15.11(@swc/helpers@0.5.17))(@types/node@20.19.13)(typescript@5.9.3))(typescript@5.9.3) unplugin-macros: specifier: ^0.18.3 - version: 0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + version: 0.18.3(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) usehooks-ts: specifier: ^3.1.1 version: 3.1.1(react@19.1.0) @@ -2481,7 +2499,7 @@ importers: version: 2.14.4(@types/node@20.19.13)(typescript@5.9.3) vitest: specifier: ^4.0.18 - version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) frontend/packages/components: dependencies: @@ -2796,6 +2814,28 @@ importers: specifier: ^5.7.3 version: 5.9.3 + rivetkit-typescript/packages/agentos: + dependencies: + '@rivetkit/react': + specifier: workspace:* + version: link:../react + react: + specifier: 19.1.0 + version: 19.1.0 + react-dom: + specifier: 19.1.0 + version: 19.1.0(react@19.1.0) + rivetkit: + specifier: workspace:* + version: link:../rivetkit + devDependencies: + tsup: + specifier: ^8.4.0 + version: 8.5.1(@microsoft/api-extractor@7.53.2(@types/node@25.0.7))(@swc/core@1.15.11(@swc/helpers@0.5.17))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0) + typescript: + specifier: ^5.7.3 + version: 5.9.3 + rivetkit-typescript/packages/devtools: dependencies: '@floating-ui/react': @@ -3049,8 +3089,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: 0.0.0-main.8794200 + version: 0.0.0-main.8794200 + '@rivet-dev/agent-os-sidecar': + specifier: 0.0.0-main.8794200 + version: 0.0.0-main.8794200 '@rivetkit/bare-ts': specifier: ^0.6.2 version: 0.6.2 @@ -3119,11 +3162,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: 0.0.0-main.8794200 + version: 0.0.0-main.8794200(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13))(ws@8.19.0)(zod@4.1.13) '@standard-schema/spec': specifier: ^1.0.0 version: 1.0.0 @@ -3147,7 +3190,7 @@ importers: version: 5.9.3 vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + version: 5.1.4(typescript@5.9.3)(vite@5.4.21(@types/node@22.19.10)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)) vitest: specifier: ^3.1.1 version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@22.19.10)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) @@ -3648,6 +3691,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: @@ -3816,6 +3886,12 @@ packages: resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} + '@aws-crypto/crc32c@5.2.0': + resolution: {integrity: sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==} + + '@aws-crypto/sha1-browser@5.2.0': + resolution: {integrity: sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==} + '@aws-crypto/sha256-browser@5.2.0': resolution: {integrity: sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==} @@ -3829,44 +3905,52 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + '@aws-sdk/checksums@3.1000.7': + resolution: {integrity: sha512-qh0fG/RtrFztst4+vn1HZehAvAhr5Jlq/WMP7e5KvvfF16oNVBc9CDNVdxdm19vzOY2x0qiDMFCRjhxQAusGWQ==} + engines: {node: '>=20.0.0'} + '@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==} + '@aws-sdk/client-s3@3.1071.0': + resolution: {integrity: sha512-BqsqkaU/FztbQnq5Aw0BK6/weQgwnC3n2w19M7CjEjRHscr5dZU8+ihi7PIY6UMW9RkJrzUUEmaoHQrIVScFYQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-env@3.972.24': - resolution: {integrity: sha512-FWg8uFmT6vQM7VuzELzwVo5bzExGaKHdubn0StjgrcU5FvuLExUe+k06kn/40uKv59rYzhez8eFNM4yYE/Yb/w==} + '@aws-sdk/core@3.974.22': + resolution: {integrity: sha512-YofH63shc6YRdXjz80BJkpJW+Bkn0Cuu2dn4Rv7s9G2Idt58tgtzQEWxrR2xVljlVfIBeUjPuULnSVYLke3sUQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-http@3.972.26': - resolution: {integrity: sha512-CY4ppZ+qHYqcXqBVi//sdHST1QK3KzOEiLtpLsc9W2k2vfZPKExGaQIsOwcyvjpjUEolotitmd3mUNY56IwDEA==} + '@aws-sdk/credential-provider-env@3.972.48': + resolution: {integrity: sha512-h6FEC95fbexUd6zxm4PdgS82bTcI2PRtUb2ZwMipb/Xr8bPwtf0G8rBo2jp7NA24Mbx2JA8/WingiYpA9RCCyw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-ini@3.972.28': - resolution: {integrity: sha512-wXYvq3+uQcZV7k+bE4yDXCTBdzWTU9x/nMiKBfzInmv6yYK1veMK0AKvRfRBd72nGWYKcL6AxwiPg9z/pYlgpw==} + '@aws-sdk/credential-provider-http@3.972.50': + resolution: {integrity: sha512-lJO3OLpjvz5m/RSBQmsG/CEUGsvCy5ruxKwPQaOCqxqCMuyYT2BZwQUTDZVVwqQ9LrZKuK24JSa6r31hL/tvkg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-login@3.972.28': - resolution: {integrity: sha512-ZSTfO6jqUTCysbdBPtEX5OUR//3rbD0lN7jO3sQeS2Gjr/Y+DT6SbIJ0oT2cemNw3UzKu97sNONd1CwNMthuZQ==} + '@aws-sdk/credential-provider-ini@3.972.55': + resolution: {integrity: sha512-TBoF4buBGYhXjdZAryayY2TrkQj2B2KfE/msG4V53XCt+w0EhEwM2JRjx8p2grJ2C6gtH5++SAwEvGMRdi0yyw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.972.29': - resolution: {integrity: sha512-clSzDcvndpFJAggLDnDb36sPdlZYyEs5Zm6zgZjjUhwsJgSWiWKwFIXUVBcbruidNyBdbpOv2tNDL9sX8y3/0g==} + '@aws-sdk/credential-provider-login@3.972.54': + resolution: {integrity: sha512-hBWI3wZTdTGiuMfmPts6AWbAjFfRniOQnqx68tc2cQvRKWawFbN9wkLOVPWM1FAOyowZU73mC6Fi+rHSHNyLFw==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.972.24': - resolution: {integrity: sha512-Q2k/XLrFXhEztPHqj4SLCNID3hEPdlhh1CDLBpNnM+1L8fq7P+yON9/9M1IGN/dA5W45v44ylERfXtDAlmMNmw==} + '@aws-sdk/credential-provider-node@3.972.57': + resolution: {integrity: sha512-u6dClpzNdWf1HGWz4wwhdXi1wiOofCLniM9S4BQQGlLAN9TW7VB+ld5V533GdKrYMaFeBGFqKnj0JCYvynLqwQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.972.28': - resolution: {integrity: sha512-IoUlmKMLEITFn1SiCTjPfR6KrE799FBo5baWyk/5Ppar2yXZoUdaRqZzJzK6TcJxx450M8m8DbpddRVYlp5R/A==} + '@aws-sdk/credential-provider-process@3.972.48': + resolution: {integrity: sha512-w6VZwojPt12WnEkAUy6Nu4K6sWCbBmR7QX390b0nE6vRvkXbrYr9Lq9VySGkfjiMjpUA87op+J4EgvRmtWIDoQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.972.28': - resolution: {integrity: sha512-d+6h0SD8GGERzKe27v5rOzNGKOl0D+l0bWJdqrxH8WSQzHzjsQFIAPgIeOTUwBHVsKKwtSxc91K/SWax6XgswQ==} + '@aws-sdk/credential-provider-sso@3.972.54': + resolution: {integrity: sha512-23uZpIpF2SIFDCa1fcWa202tK4gGeyvX6GIIAjiB8WBsvsVRBMnJ/7dCxHzxf7eZT7GToJg837LDIBnZsl/VUg==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/credential-provider-web-identity@3.972.54': + resolution: {integrity: sha512-0Iv5QttS6wcATlodYKgvQj6B9Db51rx7NU9fqu0PoLeS4BIgdYMc/QK4smwLwpm5RFrs02V/eLyEFp3FklvlNQ==} engines: {node: '>=20.0.0'} '@aws-sdk/eventstream-handler-node@3.972.12': @@ -3877,6 +3961,10 @@ packages: resolution: {integrity: sha512-r+oP+tbCxgqXVC3pu3MUVePgSY0ILMjA+aEwOosS77m3/DRbtvHrHwqvMcw+cjANMeGzJ+i0ar+n77KXpRA8RQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-flexible-checksums@3.974.32': + resolution: {integrity: sha512-KhuzFMzUbb3oEj43CdPDbEJ/RG/RkErkmXk3J/LE8OPFNvkCn8PYPMpjOLgzAzvxBacsSyytdWf+R50q0alJ4w==} + 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'} @@ -3889,6 +3977,10 @@ packages: resolution: {integrity: sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-sdk-s3@3.972.53': + resolution: {integrity: sha512-keWp6Z5cEIJzPwoCf/WRm0ceAeephPDDivhRsK/xXs2ZYXyypJ2/DL9G1IR0bz/s+iZC0EgzmFV4r7rlvLlxQQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-user-agent@3.972.28': resolution: {integrity: sha512-cfWZFlVh7Va9lRay4PN2A9ARFzaBYcA097InT5M2CdRS05ECF5yaz86jET8Wsl2WcyKYEvVr/QNmKtYtafUHtQ==} engines: {node: '>=20.0.0'} @@ -3897,28 +3989,28 @@ packages: 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==} + '@aws-sdk/nested-clients@3.997.22': + resolution: {integrity: sha512-4IwtcYSxEIVw5hcp8ogq0CMbFNZFw7jJUetpfFUhFFeqsa1K8j2Ihg2hnxLyOp3stMZnXda6VzOmPi1AFZQXcg==} 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==} + '@aws-sdk/signature-v4-multi-region@3.996.35': + resolution: {integrity: sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg==} 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==} + '@aws-sdk/token-providers@3.1071.0': + resolution: {integrity: sha512-4LDW2Qob6LoLFuqYSYZq2AyTE9koSE9+i+n5UZcm10GpmQOK0zRD9L4uYlzItiTKksIWgC/qMFChAi3RvKYtMg==} engines: {node: '>=20.0.0'} - '@aws-sdk/types@3.973.6': - resolution: {integrity: sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==} + '@aws-sdk/types@3.973.13': + resolution: {integrity: sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg==} engines: {node: '>=20.0.0'} '@aws-sdk/util-endpoints@3.996.5': @@ -3945,8 +4037,8 @@ packages: aws-crt: optional: true - '@aws-sdk/xml-builder@3.972.16': - resolution: {integrity: sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==} + '@aws-sdk/xml-builder@3.972.30': + resolution: {integrity: sha512-StElZPEoBquWwNqw1AcfpzEyZqJvFxouG+mpDNYlcH6ZOrqd2CuIryv+8LV8gNHZUOyKyJF3Dq9vxaXEmDR9TQ==} engines: {node: '>=20.0.0'} '@aws/lambda-invoke-store@0.2.3': @@ -4623,61 +4715,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==} @@ -6613,6 +6675,9 @@ packages: resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} engines: {node: '>= 20.19.0'} + '@nodable/entities@2.2.0': + resolution: {integrity: sha512-9uGyhaQavEUMC8AIddIjau4NsnsXhou+j5sBAGojCM1oxmQpVKTWR/9JxABD6UAv12vpIms55fPZKFQEhG6uBg==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -7725,8 +7790,8 @@ 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-core@0.0.0-main.8794200': + resolution: {integrity: sha512-Z6cedhN+UaV6+ML2+2kDQ+jeR5pZZXW5ltnL1GeLTyZF+Ni6oSKrnlUeJiufARBBDjvmaXslFx2K/WEm93xrsA==} '@rivet-dev/agent-os-coreutils@0.0.260331072558': resolution: {integrity: sha512-vI/J2MJnpsJQZ7F5DU/udGqGMtliqhTg28lE6XZ/qEGyjUvmEQ9AiV8CaAcX5HrImAUWOofBbTv6/XGKjOhT1w==} @@ -7746,24 +7811,26 @@ 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==} + '@rivet-dev/agent-os-pi@0.0.0-main.8794200': + resolution: {integrity: sha512-zskoPhFRNhTMzkhQ1PeUW7f8edHgj1CDDZ7Fosqs/zJiRK1xczKwo9frc1yykQq8jZSZqoolPMhLih+dUe3r2Q==} 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-sandbox@0.0.0-main.8794200': + resolution: {integrity: sha512-AJ8rNi7PHSivNicOq96p+Rs1j02xx9I2O6NbguTR5HHMpYJ5s6UW9qE1GVnNeuBWjvYXtKv2NfcPkVkKDJ4V+A==} '@rivet-dev/agent-os-sed@0.0.260331072558': resolution: {integrity: sha512-i/6ifWCcGE2TEfPWR5ig5tMVKUc0qL0ng59htgS+sqt6zdMaPIllwVztOgXNxMEdFM1TF646e/OkMfMzmYZDCA==} + '@rivet-dev/agent-os-sidecar-linux-x64-gnu@0.0.0-main.8794200': + resolution: {integrity: sha512-DAOrUTvAH+D8diMXOPy4MTm1W90cMkdMN8pL0ClblkqQfGgWx3nDG5sWQwsbzVM1ulNPMi+xVTUdsXA/LBPRBQ==} + engines: {node: '>=20'} + cpu: [x64] + os: [linux] + + '@rivet-dev/agent-os-sidecar@0.0.0-main.8794200': + resolution: {integrity: sha512-brNKTrpQTVTgWYXF+wJkoX+tc1rb4T4cHmJX7onQBNGB2vqFjktafuoJcajVsZPU32ZZ+crd5DK+dqYYLev75Q==} + engines: {node: '>=20'} + '@rivet-dev/agent-os-tar@0.0.260331072558': resolution: {integrity: sha512-LAa8fm4jombNBQRR42O/57WxY6VA13Uea8JFChh6yPN4VtFP84KI5Kg4/Zzne3YDronGZlgJivznNUH3dtTMzA==} @@ -7974,37 +8041,59 @@ 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] - '@secure-exec/v8@0.2.1': - resolution: {integrity: sha512-ye/seCqzvyMGnvyP+AO7RkVMR/lE3x9m0D2PfmiAXA457R78ZmOFmZ6v+JlJG2vv3LM30KsSXTUhwpG+Teh0hw==} + '@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/sandbox@0.0.0-split-runtime-preview.5d46b14': + resolution: {integrity: sha512-/CS91LGj03r0ylUeb+5uf+dKt9w/gcBXo11dvtfs6ZNSf0TRSayvHxHEwagN7ABarOGJT3uc1DEhp63/XsGGUQ==} + + '@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/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==} @@ -8322,12 +8411,12 @@ packages: resolution: {integrity: sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==} engines: {node: '>=18.0.0'} - '@smithy/core@3.23.13': - resolution: {integrity: sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q==} + '@smithy/core@3.25.1': + resolution: {integrity: sha512-zpDbpXBCBsxfLtG2GEUyfgvHvSFrw5CwDZSNzL0v52gx/c3oPlPbm+7W7num8xs6vyiUBn+bvYPHcQDOXZynCQ==} engines: {node: '>=18.0.0'} - '@smithy/credential-provider-imds@4.2.12': - resolution: {integrity: sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==} + '@smithy/credential-provider-imds@4.4.1': + resolution: {integrity: sha512-TSAF5NHgxEsllbErYWbK8aLnl5L601NGc5VYJlSPsKnf3YlkhdoBN+geGcaU00oiw2OK3QO5LA3QNXiiWhCidQ==} engines: {node: '>=18.0.0'} '@smithy/eventstream-codec@4.2.12': @@ -8350,8 +8439,8 @@ packages: resolution: {integrity: sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==} engines: {node: '>=18.0.0'} - '@smithy/fetch-http-handler@5.3.15': - resolution: {integrity: sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==} + '@smithy/fetch-http-handler@5.5.1': + resolution: {integrity: sha512-96JrD1q71anokymx9Iblb+zKmNQYNstlV/25A9ZYIJ2A0rp1r7/GZAIm0bDWSmVvz3DpNOCZuabzsiL+w0UHhw==} engines: {node: '>=18.0.0'} '@smithy/hash-node@4.2.12': @@ -8394,8 +8483,8 @@ packages: resolution: {integrity: sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==} engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.5.1': - resolution: {integrity: sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw==} + '@smithy/node-http-handler@4.8.1': + resolution: {integrity: sha512-emtXvoky671puri18ETf64AFIQUGIEA093F2drXpBgB0OGnBLjcwNR3CA2mYu62IAqNsS56xa5lnTxAgPq7cjw==} engines: {node: '>=18.0.0'} '@smithy/property-provider@4.2.12': @@ -8422,20 +8511,16 @@ packages: 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==} + '@smithy/signature-v4@5.5.1': + resolution: {integrity: sha512-X9rVls3En0z3NtrmguTmpRM0/NqtWUxBjal6fcAkwtsub+gOdLZ6kD+V7xhUgFMGdG14bHbZ7M5QjaRI1+DatQ==} 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==} + '@smithy/types@4.15.0': + resolution: {integrity: sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==} engines: {node: '>=18.0.0'} '@smithy/url-parser@4.2.12': @@ -9659,6 +9744,9 @@ packages: engines: {node: '>=10.0.0'} deprecated: this version has critical issues, please update to the latest version + '@xterm/headless@6.0.0': + resolution: {integrity: sha512-5Yj1QINYCyzrZtf8OFIHi47iQtI+0qYFPHmouEfG8dHNxbZ9Tb9YGSuLcsEwj9Z+OL75GJqPyJbyoFer80a2Hw==} + '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -9719,6 +9807,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 @@ -9853,6 +9944,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + anynum@1.0.1: + resolution: {integrity: sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==} + arch@2.2.0: resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} @@ -10339,16 +10433,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==} @@ -11914,11 +12001,11 @@ 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-builder@1.2.0: + resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} - fast-xml-parser@5.5.8: - resolution: {integrity: sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==} + fast-xml-parser@5.7.3: + resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} hasBin: true fastq@1.20.1: @@ -12189,10 +12276,18 @@ packages: resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==} engines: {node: '>=10'} + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + gaxios@7.1.4: resolution: {integrity: sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==} engines: {node: '>=18'} + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + gcp-metadata@8.1.2: resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} engines: {node: '>=18'} @@ -12331,10 +12426,26 @@ packages: resolution: {integrity: sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==} engines: {node: '>=18'} + google-auth-library@9.15.1: + resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} + engines: {node: '>=14'} + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + google-logging-utils@1.1.3: resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} engines: {node: '>=14'} + googleapis-common@7.2.0: + resolution: {integrity: sha512-/fhDZEJZvOV3X5jmD+fKxMqma5q2Q9nZNSF3kn1F18tpxmA86BcTxAGBQdM0N89Z3bEaIs+HVznSmFJEAmMTjA==} + engines: {node: '>=14.0.0'} + + googleapis@144.0.0: + resolution: {integrity: sha512-ELcWOXtJxjPX4vsKMh+7V+jZvgPwYMlEhQFiu2sa9Qmt5veX8nwXPksOWGGN6Zk4xCiLygUyaz7xGtcMO+Onxw==} + engines: {node: '>=14.0.0'} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -12346,6 +12457,10 @@ packages: resolution: {integrity: sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + h3@1.15.4: resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} @@ -14444,8 +14559,8 @@ packages: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} - path-expression-matcher@1.2.1: - resolution: {integrity: sha512-d7gQQmLvAKXKXE2GeP9apIGbMYKz88zWdsn/BN2HRWVQsDFdUY36WSLTY0Jvd4HWi7Fb30gQ62oAOzdgJA6fZw==} + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} engines: {node: '>=14.0.0'} path-is-absolute@1.0.1: @@ -15524,6 +15639,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 +15694,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 @@ -15940,8 +16084,8 @@ 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==} + strnum@2.4.1: + resolution: {integrity: sha512-M9eUSMT2dCB2cTNPG7UYj6KuK7RJR2SN2+yCV/fTW3xzTCS6EaGZ5pSMgDIjB7r8zSfTGk+dvvn9rTjpVS9Mwg==} strtok3@10.3.4: resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} @@ -16267,6 +16411,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 @@ -16663,6 +16808,9 @@ packages: url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + url-template@2.0.8: + resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==} + url@0.11.4: resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} engines: {node: '>= 0.4'} @@ -16730,6 +16878,11 @@ packages: deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). hasBin: true + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -17150,10 +17303,6 @@ packages: 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==} @@ -17323,6 +17472,10 @@ packages: resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} hasBin: true + xml-naming@0.1.0: + resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} + engines: {node: '>=16.0.0'} + xml2js@0.6.0: resolution: {integrity: sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==} engines: {node: '>=4.0.0'} @@ -17506,6 +17659,33 @@ snapshots: '@adobe/css-tools@4.3.3': optional: true + '@agent-os-pkgs/common@0.0.0-split-runtime-preview.5d46b14': + dependencies: + '@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': {} + + '@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: 3.25.76 @@ -17514,6 +17694,10 @@ snapshots: dependencies: zod: 4.1.13 + '@agentclientprotocol/sdk@0.16.1(zod@4.3.6)': + dependencies: + zod: 4.3.6 + '@ai-sdk/anthropic@1.2.12(zod@4.1.13)': dependencies: '@ai-sdk/provider': 1.1.3 @@ -17689,6 +17873,12 @@ snapshots: optionalDependencies: zod: 4.1.13 + '@anthropic-ai/sdk@0.73.0(zod@4.3.6)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.3.6 + '@arethetypeswrong/cli@0.18.3': dependencies: '@arethetypeswrong/core': 0.18.3 @@ -17822,7 +18012,22 @@ snapshots: '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.5 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/crc32c@5.2.0': + dependencies: + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + tslib: 2.8.1 + + '@aws-crypto/sha1-browser@5.2.0': + dependencies: + '@aws-crypto/supports-web-crypto': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/types': 3.973.13 + '@aws-sdk/util-locate-window': 3.965.5 + '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 '@aws-crypto/sha256-browser@5.2.0': @@ -17830,7 +18035,7 @@ snapshots: '@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/types': 3.973.13 '@aws-sdk/util-locate-window': 3.965.5 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -17838,7 +18043,7 @@ snapshots: '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.5 + '@aws-sdk/types': 3.973.13 tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': @@ -17847,16 +18052,27 @@ snapshots: '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.973.5 + '@aws-sdk/types': 3.973.13 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 + '@aws-sdk/checksums@3.1000.7': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/crc32c': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.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/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 '@aws-sdk/eventstream-handler-node': 3.972.12 '@aws-sdk/middleware-eventstream': 3.972.8 '@aws-sdk/middleware-host-header': 3.972.8 @@ -17866,16 +18082,16 @@ snapshots: '@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/types': 3.973.13 '@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/core': 3.25.1 '@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/fetch-http-handler': 5.5.1 '@smithy/hash-node': 4.2.12 '@smithy/invalid-dependency': 4.2.12 '@smithy/middleware-content-length': 4.2.12 @@ -17884,10 +18100,10 @@ snapshots: '@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/node-http-handler': 4.8.1 '@smithy/protocol-http': 5.3.12 '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 '@smithy/url-parser': 4.2.12 '@smithy/util-base64': 4.3.2 '@smithy/util-body-length-browser': 4.2.2 @@ -17903,285 +18119,258 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/core@3.973.26': + '@aws-sdk/client-s3@3.1071.0': 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 + '@aws-crypto/sha1-browser': 5.2.0 + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-node': 3.972.57 + '@aws-sdk/middleware-flexible-checksums': 3.974.32 + '@aws-sdk/middleware-sdk-s3': 3.972.53 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.972.24': + '@aws-sdk/core@3.974.22': dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.13 + '@aws-sdk/xml-builder': 3.972.30 + '@aws/lambda-invoke-store': 0.2.3 + '@smithy/core': 3.25.1 + '@smithy/signature-v4': 5.5.1 + '@smithy/types': 4.15.0 + bowser: 2.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.972.26': + '@aws-sdk/credential-provider-env@3.972.48': 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 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 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 + '@aws-sdk/credential-provider-http@3.972.50': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 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 + '@aws-sdk/credential-provider-ini@3.972.55': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/credential-provider-env': 3.972.48 + '@aws-sdk/credential-provider-http': 3.972.50 + '@aws-sdk/credential-provider-login': 3.972.54 + '@aws-sdk/credential-provider-process': 3.972.48 + '@aws-sdk/credential-provider-sso': 3.972.54 + '@aws-sdk/credential-provider-web-identity': 3.972.54 + '@aws-sdk/nested-clients': 3.997.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/credential-provider-imds': 4.4.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/credential-provider-node@3.972.29': + '@aws-sdk/credential-provider-login@3.972.54': 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 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/nested-clients': 3.997.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/credential-provider-node@3.972.57': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.48 + '@aws-sdk/credential-provider-http': 3.972.50 + '@aws-sdk/credential-provider-ini': 3.972.55 + '@aws-sdk/credential-provider-process': 3.972.48 + '@aws-sdk/credential-provider-sso': 3.972.54 + '@aws-sdk/credential-provider-web-identity': 3.972.54 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/credential-provider-imds': 4.4.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/credential-provider-process@3.972.24': + '@aws-sdk/credential-provider-process@3.972.48': 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 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.972.28': + '@aws-sdk/credential-provider-sso@3.972.54': 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 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/nested-clients': 3.997.22 + '@aws-sdk/token-providers': 3.1071.0 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/credential-provider-web-identity@3.972.28': + '@aws-sdk/credential-provider-web-identity@3.972.54': 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 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/nested-clients': 3.997.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt '@aws-sdk/eventstream-handler-node@3.972.12': dependencies: - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.13 '@smithy/eventstream-codec': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@aws-sdk/middleware-eventstream@3.972.8': dependencies: - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.13 '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-flexible-checksums@3.974.32': + dependencies: + '@aws-sdk/checksums': 3.1000.7 tslib: 2.8.1 '@aws-sdk/middleware-host-header@3.972.8': dependencies: - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.13 '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@aws-sdk/middleware-logger@3.972.8': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.13 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@aws-sdk/middleware-recursion-detection@3.972.9': dependencies: - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.13 '@aws/lambda-invoke-store': 0.2.3 '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 + tslib: 2.8.1 + + '@aws-sdk/middleware-sdk-s3@3.972.53': + dependencies: + '@aws-sdk/core': 3.974.22 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 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/core': 3.974.22 + '@aws-sdk/types': 3.973.13 '@aws-sdk/util-endpoints': 3.996.5 - '@smithy/core': 3.23.13 + '@smithy/core': 3.25.1 '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 '@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/types': 3.973.13 '@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/fetch-http-handler': 5.5.1 '@smithy/protocol-http': 5.3.12 - '@smithy/signature-v4': 5.3.12 - '@smithy/types': 4.13.1 + '@smithy/signature-v4': 5.5.1 + '@smithy/types': 4.15.0 '@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': + '@aws-sdk/nested-clients@3.997.22': 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 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/signature-v4-multi-region': 3.996.35 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt '@aws-sdk/region-config-resolver@3.972.10': dependencies: - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.13 '@smithy/config-resolver': 4.4.13 '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@aws-sdk/token-providers@3.1021.0': + '@aws-sdk/signature-v4-multi-region@3.996.35': 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 + '@aws-sdk/types': 3.973.13 + '@smithy/signature-v4': 5.5.1 + '@smithy/types': 4.15.0 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 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/nested-clients': 3.997.22 + '@aws-sdk/types': 3.973.13 '@smithy/property-provider': 4.2.12 '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/types@3.973.5': + '@aws-sdk/token-providers@3.1071.0': dependencies: - '@smithy/types': 4.13.0 + '@aws-sdk/core': 3.974.22 + '@aws-sdk/nested-clients': 3.997.22 + '@aws-sdk/types': 3.973.13 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@aws-sdk/types@3.973.6': + '@aws-sdk/types@3.973.13': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@aws-sdk/util-endpoints@3.996.5': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.13 + '@smithy/types': 4.15.0 '@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 + '@aws-sdk/types': 3.973.13 '@smithy/querystring-builder': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@aws-sdk/util-locate-window@3.965.5': @@ -18190,24 +18379,24 @@ snapshots: '@aws-sdk/util-user-agent-browser@3.972.8': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.13 + '@smithy/types': 4.15.0 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 + '@aws-sdk/types': 3.973.13 '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 '@smithy/util-config-provider': 4.2.2 tslib: 2.8.1 - '@aws-sdk/xml-builder@3.972.16': + '@aws-sdk/xml-builder@3.972.30': dependencies: - '@smithy/types': 4.13.1 - fast-xml-parser: 5.5.8 + '@smithy/types': 4.15.0 + fast-xml-parser: 5.7.3 tslib: 2.8.1 '@aws/lambda-invoke-store@0.2.3': {} @@ -18937,39 +19126,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 @@ -20031,7 +20202,7 @@ snapshots: google-auth-library: 10.6.2 p-retry: 4.6.2 protobufjs: 7.5.4 - ws: 8.19.0 + ws: 8.20.1 optionalDependencies: '@modelcontextprotocol/sdk': 1.25.3(hono@4.11.9)(zod@3.25.76) transitivePeerDependencies: @@ -20044,7 +20215,7 @@ snapshots: google-auth-library: 10.6.2 p-retry: 4.6.2 protobufjs: 7.5.4 - ws: 8.19.0 + ws: 8.20.1 optionalDependencies: '@modelcontextprotocol/sdk': 1.25.3(hono@4.11.9)(zod@4.1.13) transitivePeerDependencies: @@ -20052,6 +20223,19 @@ snapshots: - supports-color - utf-8-validate + '@google/genai@1.48.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.3.6))': + dependencies: + google-auth-library: 10.6.2 + p-retry: 4.6.2 + protobufjs: 7.5.4 + ws: 8.20.1 + optionalDependencies: + '@modelcontextprotocol/sdk': 1.25.3(hono@4.11.9)(zod@4.3.6) + 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) @@ -20537,7 +20721,7 @@ snapshots: react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - '@ladle/react@5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)': + '@ladle/react@5.1.1(@swc/helpers@0.5.17)(@types/node@20.19.13)(@types/react@19.2.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.9.0)': dependencies: '@babel/code-frame': 7.29.0 '@babel/core': 7.29.0 @@ -20549,8 +20733,8 @@ snapshots: '@ladle/react-context': 1.0.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0) '@mdx-js/mdx': 3.1.1 '@mdx-js/react': 3.1.1(@types/react@19.2.13)(react@19.1.0) - '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) - '@vitejs/plugin-react-swc': 3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + '@vitejs/plugin-react-swc': 3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) axe-core: 4.11.1 boxen: 8.0.1 chokidar: 4.0.3 @@ -20577,8 +20761,8 @@ snapshots: remark-gfm: 4.0.1 source-map: 0.7.6 vfile: 6.0.3 - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) - vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - '@swc/helpers' - '@types/node' @@ -20694,9 +20878,9 @@ snapshots: 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)': + '@mariozechner/pi-agent-core@0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(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) + '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(zod@3.25.76) transitivePeerDependencies: - '@modelcontextprotocol/sdk' - aws-crt @@ -20718,7 +20902,19 @@ snapshots: - 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)': + '@mariozechner/pi-agent-core@0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.3.6))(zod@4.3.6)': + dependencies: + '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.3.6))(zod@4.3.6) + 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))(zod@3.25.76)': dependencies: '@anthropic-ai/sdk': 0.73.0(zod@3.25.76) '@aws-sdk/client-bedrock-runtime': 3.1024.0 @@ -20728,7 +20924,7 @@ snapshots: 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) + openai: 6.26.0(zod@3.25.76) partial-json: 0.1.7 proxy-agent: 6.5.0 undici: 7.24.7 @@ -20766,11 +20962,35 @@ snapshots: - 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)': + '@mariozechner/pi-ai@0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.3.6))(zod@4.3.6)': + dependencies: + '@anthropic-ai/sdk': 0.73.0(zod@4.3.6) + '@aws-sdk/client-bedrock-runtime': 3.1024.0 + '@google/genai': 1.48.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.3.6)) + '@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(zod@4.3.6) + partial-json: 0.1.7 + proxy-agent: 6.5.0 + undici: 7.24.7 + zod-to-json-schema: 3.25.1(zod@4.3.6) + 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))(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-agent-core': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(zod@3.25.76) + '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(zod@3.25.76) '@mariozechner/pi-tui': 0.60.0 '@silvia-odwyer/photon-node': 0.3.4 chalk: 5.6.2 @@ -20786,7 +21006,7 @@ snapshots: proper-lockfile: 4.1.2 strip-ansi: 7.1.2 undici: 7.24.7 - yaml: 2.8.2 + yaml: 2.9.0 optionalDependencies: '@mariozechner/clipboard': 0.3.2 transitivePeerDependencies: @@ -20818,7 +21038,39 @@ snapshots: proper-lockfile: 4.1.2 strip-ansi: 7.1.2 undici: 7.24.7 - yaml: 2.8.2 + yaml: 2.9.0 + 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.3.6))(zod@4.3.6)': + dependencies: + '@mariozechner/jiti': 2.6.5 + '@mariozechner/pi-agent-core': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.3.6))(zod@4.3.6) + '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.3.6))(zod@4.3.6) + '@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.9.0 optionalDependencies: '@mariozechner/clipboard': 0.3.2 transitivePeerDependencies: @@ -21047,7 +21299,7 @@ snapshots: '@mistralai/mistralai@1.14.1': dependencies: - ws: 8.19.0 + ws: 8.20.1 zod: 3.25.76 zod-to-json-schema: 3.25.1(zod@3.25.76) transitivePeerDependencies: @@ -21185,6 +21437,29 @@ snapshots: - supports-color optional: true + '@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.3.6)': + 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.3.6 + zod-to-json-schema: 3.25.1(zod@4.3.6) + transitivePeerDependencies: + - hono + - supports-color + optional: true + '@monaco-editor/loader@1.7.0': dependencies: state-local: 1.0.7 @@ -21257,6 +21532,8 @@ snapshots: '@noble/hashes@2.0.1': {} + '@nodable/entities@2.2.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -22539,18 +22816,22 @@ 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)': + '@rivet-dev/agent-os-core@0.0.0-main.8794200': 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 + '@aws-sdk/client-s3': 3.1071.0 + '@rivet-dev/agent-os-sidecar': 0.0.0-main.8794200 + '@rivetkit/bare-ts': 0.6.2 + '@secure-exec/core': 0.0.0-split-runtime-preview.5d46b14 + '@xterm/headless': 6.0.0 + better-sqlite3: 12.8.0 croner: 10.0.1 + googleapis: 144.0.0 + isolated-vm: 6.1.2 long-timeout: 0.1.1 - secure-exec: 0.2.1 + minimatch: 10.2.5 transitivePeerDependencies: - - pyodide + - encoding + - supports-color '@rivet-dev/agent-os-coreutils@0.0.260331072558': {} @@ -22564,50 +22845,82 @@ 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)': + '@rivet-dev/agent-os-pi@0.0.0-main.8794200(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(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) + '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(zod@3.25.76) + '@mariozechner/pi-coding-agent': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@3.25.76))(zod@3.25.76) + '@rivet-dev/agent-os-core': 0.0.0-main.8794200 transitivePeerDependencies: - '@modelcontextprotocol/sdk' - aws-crt - bufferutil - - pyodide + - encoding - 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)': + '@rivet-dev/agent-os-pi@0.0.0-main.8794200(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.1.13))(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) + '@rivet-dev/agent-os-core': 0.0.0-main.8794200 transitivePeerDependencies: - '@modelcontextprotocol/sdk' - aws-crt - bufferutil - - pyodide + - encoding - supports-color - utf-8-validate - ws - zod - '@rivet-dev/agent-os-posix@0.1.0': + '@rivet-dev/agent-os-pi@0.0.0-main.8794200(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.3.6))(zod@4.3.6)': dependencies: - '@secure-exec/core': 0.2.1 + '@agentclientprotocol/sdk': 0.16.1(zod@4.3.6) + '@mariozechner/pi-ai': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.3.6))(zod@4.3.6) + '@mariozechner/pi-coding-agent': 0.60.0(@modelcontextprotocol/sdk@1.25.3(hono@4.11.9)(zod@4.3.6))(zod@4.3.6) + '@rivet-dev/agent-os-core': 0.0.0-main.8794200 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - aws-crt + - bufferutil + - encoding + - supports-color + - utf-8-validate + - ws + - zod - '@rivet-dev/agent-os-python@0.1.0(pyodide@0.28.3)': + '@rivet-dev/agent-os-sandbox@0.0.0-main.8794200(get-port@7.1.0)': dependencies: - '@secure-exec/core': 0.2.1 - optionalDependencies: - pyodide: 0.28.3 + '@rivet-dev/agent-os-core': 0.0.0-main.8794200 + '@secure-exec/sandbox': 0.0.0-split-runtime-preview.5d46b14(get-port@7.1.0)(zod@4.3.6) + sandbox-agent: 0.4.2(get-port@7.1.0)(zod@4.3.6) + zod: 4.3.6 + transitivePeerDependencies: + - '@cloudflare/sandbox' + - '@daytonaio/sdk' + - '@e2b/code-interpreter' + - '@fly/sprites' + - '@vercel/sandbox' + - computesdk + - dockerode + - encoding + - get-port + - modal + - supports-color '@rivet-dev/agent-os-sed@0.0.260331072558': {} + '@rivet-dev/agent-os-sidecar-linux-x64-gnu@0.0.0-main.8794200': + optional: true + + '@rivet-dev/agent-os-sidecar@0.0.0-main.8794200': + optionalDependencies: + '@rivet-dev/agent-os-sidecar-linux-x64-gnu': 0.0.0-main.8794200 + '@rivet-dev/agent-os-tar@0.0.260331072558': {} '@rivet-gg/api@25.5.3': @@ -22915,43 +23228,67 @@ 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: + '@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: - cbor-x: 1.6.4 + '@secure-exec/core': 0.0.0-split-runtime-preview.5d46b14 + + '@secure-exec/sandbox@0.0.0-split-runtime-preview.5d46b14(get-port@7.1.0)(zod@4.3.6)': + dependencies: + '@secure-exec/core': 0.0.0-split-runtime-preview.5d46b14 + sandbox-agent: 0.4.2(get-port@7.1.0)(zod@4.3.6) + transitivePeerDependencies: + - '@cloudflare/sandbox' + - '@daytonaio/sdk' + - '@e2b/code-interpreter' + - '@fly/sprites' + - '@vercel/sandbox' + - computesdk + - dockerode + - get-port + - modal + - zod + + '@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: @@ -23345,81 +23682,70 @@ snapshots: '@smithy/config-resolver@4.4.13': dependencies: '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 '@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': + '@smithy/core@3.25.1': 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 + '@aws-crypto/crc32': 5.2.0 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@smithy/credential-provider-imds@4.2.12': + '@smithy/credential-provider-imds@4.4.1': 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 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@smithy/eventstream-codec@4.2.12': dependencies: '@aws-crypto/crc32': 5.2.0 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 '@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 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@smithy/eventstream-serde-config-resolver@4.3.12': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@smithy/eventstream-serde-node@4.2.12': dependencies: '@smithy/eventstream-serde-universal': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@smithy/eventstream-serde-universal@4.2.12': dependencies: '@smithy/eventstream-codec': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@smithy/fetch-http-handler@5.3.15': + '@smithy/fetch-http-handler@5.5.1': dependencies: - '@smithy/protocol-http': 5.3.12 - '@smithy/querystring-builder': 4.2.12 - '@smithy/types': 4.13.1 - '@smithy/util-base64': 4.3.2 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@smithy/hash-node@4.2.12': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 '@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 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@smithy/is-array-buffer@2.2.0': @@ -23433,16 +23759,16 @@ snapshots: '@smithy/middleware-content-length@4.2.12': dependencies: '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@smithy/middleware-endpoint@4.4.28': dependencies: - '@smithy/core': 3.23.13 + '@smithy/core': 3.25.1 '@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/types': 4.15.0 '@smithy/url-parser': 4.2.12 '@smithy/util-middleware': 4.2.12 tslib: 2.8.1 @@ -23453,7 +23779,7 @@ snapshots: '@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/types': 4.15.0 '@smithy/util-middleware': 4.2.12 '@smithy/util-retry': 4.2.13 '@smithy/uuid': 1.1.2 @@ -23461,93 +23787,83 @@ snapshots: '@smithy/middleware-serde@4.2.16': dependencies: - '@smithy/core': 3.23.13 + '@smithy/core': 3.25.1 '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@smithy/middleware-stack@4.2.12': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 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 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@smithy/node-http-handler@4.5.1': + '@smithy/node-http-handler@4.8.1': dependencies: - '@smithy/protocol-http': 5.3.12 - '@smithy/querystring-builder': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@smithy/property-provider@4.2.12': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@smithy/protocol-http@5.3.12': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@smithy/querystring-builder@4.2.12': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 '@smithy/util-uri-escape': 4.2.2 tslib: 2.8.1 '@smithy/querystring-parser@4.2.12': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@smithy/service-error-classification@4.2.12': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 '@smithy/shared-ini-file-loader@4.4.7': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 - '@smithy/signature-v4@5.3.12': + '@smithy/signature-v4@5.5.1': 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 + '@smithy/core': 3.25.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@smithy/smithy-client@4.12.8': dependencies: - '@smithy/core': 3.23.13 + '@smithy/core': 3.25.1 '@smithy/middleware-endpoint': 4.4.28 '@smithy/middleware-stack': 4.2.12 '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 '@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': + '@smithy/types@4.15.0': dependencies: tslib: 2.8.1 '@smithy/url-parser@4.2.12': dependencies: '@smithy/querystring-parser': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@smithy/util-base64@4.3.2': @@ -23582,23 +23898,23 @@ snapshots: dependencies: '@smithy/property-provider': 4.2.12 '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 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/credential-provider-imds': 4.4.1 '@smithy/node-config-provider': 4.3.12 '@smithy/property-provider': 4.2.12 '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@smithy/util-endpoints@3.3.3': dependencies: '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@smithy/util-hex-encoding@4.2.2': @@ -23607,20 +23923,20 @@ snapshots: '@smithy/util-middleware@4.2.12': dependencies: - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 tslib: 2.8.1 '@smithy/util-retry@4.2.13': dependencies: '@smithy/service-error-classification': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.15.0 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/fetch-http-handler': 5.5.1 + '@smithy/node-http-handler': 4.8.1 + '@smithy/types': 4.15.0 '@smithy/util-base64': 4.3.2 '@smithy/util-buffer-from': 4.2.2 '@smithy/util-hex-encoding': 4.2.2 @@ -24674,11 +24990,11 @@ snapshots: d3-time-format: 4.1.0 internmap: 2.0.3 - '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': + '@vitejs/plugin-react-swc@3.11.0(@swc/helpers@0.5.17)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@rolldown/pluginutils': 1.0.0-beta.27 '@swc/core': 1.15.11(@swc/helpers@0.5.17) - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - '@swc/helpers' @@ -24718,7 +25034,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -24726,7 +25042,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -24843,14 +25159,14 @@ snapshots: msw: 2.14.4(@types/node@22.19.15)(typescript@5.9.3) vite: 5.4.21(@types/node@22.19.15)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) - '@vitest/mocker@4.0.18(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': + '@vitest/mocker@4.0.18(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 4.0.18 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.14.4(@types/node@20.19.13)(typescript@5.9.3) - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) '@vitest/mocker@4.1.7(msw@2.14.4(@types/node@22.19.15)(typescript@5.9.3))(vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0))': dependencies: @@ -25120,6 +25436,8 @@ snapshots: '@xmldom/xmldom@0.8.11': {} + '@xterm/headless@6.0.0': {} + '@xtuc/ieee754@1.2.0': optional: true @@ -25186,6 +25504,18 @@ 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 + + acp-http-client@0.4.2(zod@4.3.6): + dependencies: + '@agentclientprotocol/sdk': 0.16.1(zod@4.3.6) + transitivePeerDependencies: + - zod + actor-core@0.6.3(eventsource@3.0.7)(ws@8.20.1): dependencies: cbor-x: 1.6.0 @@ -25346,6 +25676,8 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + anynum@1.0.1: {} + arch@2.2.0: {} arg@4.1.3: {} @@ -25708,7 +26040,7 @@ snapshots: bcryptjs@2.4.3: {} - better-auth@1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(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))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)): + better-auth@1.5.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-sqlite3@12.8.0)(drizzle-kit@0.31.5)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0))(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))(pg@8.17.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)): dependencies: '@better-auth/core': 1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0) '@better-auth/drizzle-adapter': 1.5.6(@better-auth/core@1.5.6(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(better-call@1.3.2(zod@3.25.76))(jose@6.1.3)(kysely@0.28.15)(nanostores@1.2.0))(@better-auth/utils@0.3.1)(drizzle-orm@0.44.6(@cloudflare/workers-types@4.20251014.0)(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.8.0)(bun-types@1.3.11)(kysely@0.28.15)(pg@8.17.2)(sql.js@1.13.0)) @@ -25735,7 +26067,7 @@ snapshots: pg: 8.17.2 react: 19.1.0 react-dom: 19.1.0(react@19.1.0) - vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - '@cloudflare/workers-types' - '@opentelemetry/api' @@ -26010,26 +26342,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: @@ -27680,15 +27996,17 @@ snapshots: dependencies: fast-string-width: 3.0.2 - fast-xml-builder@1.1.4: + fast-xml-builder@1.2.0: dependencies: - path-expression-matcher: 1.2.1 + path-expression-matcher: 1.5.0 + xml-naming: 0.1.0 - fast-xml-parser@5.5.8: + fast-xml-parser@5.7.3: dependencies: - fast-xml-builder: 1.1.4 - path-expression-matcher: 1.2.1 - strnum: 2.2.0 + '@nodable/entities': 2.2.0 + fast-xml-builder: 1.2.0 + path-expression-matcher: 1.5.0 + strnum: 2.4.1 fastq@1.20.1: dependencies: @@ -27980,6 +28298,17 @@ snapshots: fuse.js@7.1.0: {} + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + gaxios@7.1.4: dependencies: extend: 3.0.2 @@ -27988,6 +28317,15 @@ snapshots: transitivePeerDependencies: - supports-color + gcp-metadata@6.1.1: + dependencies: + gaxios: 6.7.1 + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + gcp-metadata@8.1.2: dependencies: gaxios: 7.1.4 @@ -28144,14 +28482,56 @@ snapshots: transitivePeerDependencies: - supports-color + google-auth-library@9.15.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1 + gcp-metadata: 6.1.1 + gtoken: 7.1.0 + jws: 4.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + google-logging-utils@0.0.2: {} + google-logging-utils@1.1.3: {} + googleapis-common@7.2.0: + dependencies: + extend: 3.0.2 + gaxios: 6.7.1 + google-auth-library: 9.15.1 + qs: 6.14.1 + url-template: 2.0.8 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + googleapis@144.0.0: + dependencies: + google-auth-library: 9.15.1 + googleapis-common: 7.2.0 + transitivePeerDependencies: + - encoding + - supports-color + gopd@1.2.0: {} graceful-fs@4.2.11: {} graphql@16.14.0: {} + gtoken@7.1.0: + dependencies: + gaxios: 6.7.1 + jws: 4.0.1 + transitivePeerDependencies: + - encoding + - supports-color + h3@1.15.4: dependencies: cookie-es: 1.2.2 @@ -30768,11 +31148,14 @@ snapshots: ws: 8.19.0 zod: 4.1.13 - openai@6.26.0(ws@8.20.1)(zod@3.25.76): + openai@6.26.0(zod@3.25.76): optionalDependencies: - ws: 8.20.1 zod: 3.25.76 + openai@6.26.0(zod@4.3.6): + optionalDependencies: + zod: 4.3.6 + openapi-types@12.1.3: {} openapi3-ts@4.5.0: @@ -30968,7 +31351,7 @@ snapshots: path-exists@4.0.0: {} - path-expression-matcher@1.2.1: {} + path-expression-matcher@1.5.0: {} path-is-absolute@1.0.1: {} @@ -32211,6 +32594,26 @@ 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 + + sandbox-agent@0.4.2(get-port@7.1.0)(zod@4.3.6): + dependencies: + '@sandbox-agent/cli-shared': 0.4.2 + acp-http-client: 0.4.2(zod@4.3.6) + 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 +32638,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 @@ -32725,7 +33123,9 @@ snapshots: dependencies: js-tokens: 9.0.1 - strnum@2.2.0: {} + strnum@2.4.1: + dependencies: + anynum: 1.0.1 strtok3@10.3.4: dependencies: @@ -33520,13 +33920,13 @@ snapshots: unpipe@1.0.0: {} - unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): + unplugin-macros@0.18.3(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: ast-kit: 2.2.0 magic-string-ast: 1.0.3 unplugin: 2.3.10 - vite: 7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) - vite-node: 5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite-node: 5.2.0(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -33613,6 +34013,8 @@ snapshots: querystringify: 2.2.0 requires-port: 1.0.0 + url-template@2.0.8: {} + url@0.11.4: dependencies: punycode: 1.4.1 @@ -33666,6 +34068,8 @@ snapshots: uuid@7.0.3: {} + uuid@9.0.1: {} + v8-compile-cache-lib@3.0.1: {} validate-npm-package-name@5.0.1: {} @@ -33816,13 +34220,13 @@ snapshots: - supports-color - terser - vite-node@5.2.0(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): + vite-node@5.2.0(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: cac: 6.7.14 es-module-lexer: 1.7.0 obug: 2.0.0(ms@2.1.3) pathe: 2.0.3 - vite: 7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -33895,24 +34299,24 @@ snapshots: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@5.4.21(@types/node@22.19.10)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 5.4.21(@types/node@22.19.10)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0) transitivePeerDependencies: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 7.3.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color - typescript @@ -33959,7 +34363,7 @@ snapshots: stylus: 0.62.0 terser: 5.46.0 - vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): + vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -33970,7 +34374,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.13 fsevents: 2.3.3 - jiti: 1.21.7 + jiti: 2.6.1 less: 4.4.1 lightningcss: 1.32.0 sass: 1.93.2 @@ -34019,7 +34423,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vite@7.3.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): + vite@7.3.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -34030,26 +34434,6 @@ snapshots: optionalDependencies: '@types/node': 20.19.13 fsevents: 2.3.3 - jiti: 1.21.7 - less: 4.4.1 - lightningcss: 1.32.0 - sass: 1.93.2 - stylus: 0.62.0 - terser: 5.46.0 - tsx: 4.21.0 - yaml: 2.9.0 - - vite@7.3.1(@types/node@22.19.10)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): - dependencies: - esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.57.1 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 22.19.10 - fsevents: 2.3.3 jiti: 2.6.1 less: 4.4.1 lightningcss: 1.32.0 @@ -34058,7 +34442,6 @@ snapshots: terser: 5.46.0 tsx: 4.21.0 yaml: 2.9.0 - optional: true vite@7.3.1(@types/node@22.19.15)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: @@ -34324,10 +34707,10 @@ snapshots: - supports-color - terser - vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): + vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0): dependencies: '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/mocker': 4.0.18(msw@2.14.4(@types/node@20.19.13)(typescript@5.9.3))(vite@6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0)) '@vitest/pretty-format': 4.0.18 '@vitest/runner': 4.0.18 '@vitest/snapshot': 4.0.18 @@ -34344,7 +34727,7 @@ snapshots: tinyexec: 1.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 6.4.1(@types/node@20.19.13)(jiti@1.21.7)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) + vite: 6.4.1(@types/node@20.19.13)(jiti@2.6.1)(less@4.4.1)(lightningcss@1.32.0)(sass@1.93.2)(stylus@0.62.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.0 @@ -34454,8 +34837,6 @@ snapshots: web-streams-polyfill@4.0.0-beta.3: {} - web-streams-polyfill@4.2.0: {} - web-vitals@4.2.4: {} webidl-conversions@3.0.1: {} @@ -34613,6 +34994,8 @@ snapshots: dependencies: sax: 1.4.4 + xml-naming@0.1.0: {} + xml2js@0.6.0: dependencies: sax: 1.5.0 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/encoding.rs b/rivetkit-rust/packages/client/src/encoding.rs new file mode 100644 index 0000000000..9c50e51e2c --- /dev/null +++ b/rivetkit-rust/packages/client/src/encoding.rs @@ -0,0 +1,66 @@ +//! 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..14a6b08372 100644 --- a/rivetkit-rust/packages/client/src/protocol/codec.rs +++ b/rivetkit-rust/packages/client/src/protocol/codec.rs @@ -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..15f06c8237 --- /dev/null +++ b/rivetkit-rust/packages/client/tests/encoding.rs @@ -0,0 +1,85 @@ +//! 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..5eec0a41d3 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "rivetkit-agent-os" +version.workspace = true +authors.workspace = true +license.workspace = true +edition.workspace = true + +[dependencies] +agent-os-client = { path = "/home/nathan/agent-os-zid-patches/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"] } 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..04409910df --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/src/actions/mod.rs @@ -0,0 +1,384 @@ +//! 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) => 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..8547242ec9 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/src/actions/session.rs @@ -0,0 +1,382 @@ +//! 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}; +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 { + 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) \ + VALUES (?, ?, ?, ?, ?)", + &[ + json!(session_id), + json!(agent_type), + json!(capabilities), + agent_info.map(JsonValue::String).unwrap_or(JsonValue::Null), + json!(now_ms()), + ], + ) + .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); + 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()) +} + +/// Read the persisted `(agent_type, capabilities)` for a session from the +/// registry, returning the parsed capabilities JSON (`{}` if absent/unparsable). +async fn read_session_registry( + ctx: &Ctx, + external_session_id: &str, +) -> Result<(String, JsonValue)> { + let rows = query_rows( + ctx, + "SELECT agent_type, capabilities 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!({})); + Ok((agent_type, capabilities)) +} + +/// 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 (agent_type, _capabilities) = 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). The actor records the remap. + // + // TODO(session-resume): the `agent_os_client::AgentOs::resume_session` method + // is being implemented in parallel against the same spec §6 contract and is + // not present in the pinned client yet. Once it lands, replace the error + // below with the real call + remap: + // + // let live_session_id = vm + // .resume_session(external_session_id, &agent_type, Some(&transcript_path)) + // .await? + // .session_id; + // // The remap lives SOLELY in the actor (spec §3): record external -> live + // // and capture the live session's events under the stable external id. + // 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); + // return Ok(()); + let _ = (&agent_type, vm, vars); + Err(anyhow!( + "resume_session: client `resume_session` not yet available \ + (transcript reconstructed at {transcript_path}); blocked on the \ + parallel sidecar/client implementation of the spec §6 \ + AcpResumeSessionRequest contract" + )) +} 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..eac2849122 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/src/persistence.rs @@ -0,0 +1,1215 @@ +//! 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 +); +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<()> { + // Next seq for this session. `MAX(seq)` over an empty set is SQL NULL, which + // decodes to absent/`None`, so the first event gets seq 0. + let rows = query_rows( + ctx, + "SELECT MAX(seq) AS max_seq FROM agent_os_session_events WHERE session_id = ?", + &[json!(external_session_id)], + ) + .await?; + let next_seq = rows + .first() + .and_then(|row| row.get("max_seq")) + .and_then(JsonValue::as_i64) + .map(|max| max + 1) + .unwrap_or(0); + run_stmt( + ctx, + "INSERT INTO agent_os_session_events (session_id, seq, event, created_at) \ + VALUES (?, ?, ?, ?)", + &[ + json!(external_session_id), + json!(next_seq), + 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() + ))), + "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?, + ))), + "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..7f87f84bb2 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit-agent-os/tests/persistence.rs @@ -0,0 +1,126 @@ +//! 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\"}"); +} 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..626d604b07 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit/src/encoding.rs @@ -0,0 +1,426 @@ +//! 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..5d908a6fa3 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit/tests/encoding.rs @@ -0,0 +1,134 @@ +//! 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..8cedce86be --- /dev/null +++ b/rivetkit-rust/packages/rivetkit/tests/encoding_fixtures.rs @@ -0,0 +1,122 @@ +//! 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..7254785240 --- /dev/null +++ b/rivetkit-rust/packages/rivetkit/tests/encoding_roundtrip.rs @@ -0,0 +1,147 @@ +//! 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/agentos/package.json b/rivetkit-typescript/packages/agentos/package.json new file mode 100644 index 0000000000..3a65974982 --- /dev/null +++ b/rivetkit-typescript/packages/agentos/package.json @@ -0,0 +1,84 @@ +{ + "name": "@rivet-dev/agentos", + "version": "2.3.0-rc.12", + "description": "User-facing Agent OS integration for RivetKit actors", + "license": "Apache-2.0", + "keywords": [ + "rivetkit", + "agent-os", + "agentos", + "agents", + "actors", + "ai" + ], + "sideEffects": false, + "files": [ + "dist", + "package.json" + ], + "type": "module", + "exports": { + ".": { + "import": { + "types": "./dist/mod.d.ts", + "default": "./dist/mod.js" + }, + "require": { + "types": "./dist/mod.d.cts", + "default": "./dist/mod.cjs" + } + }, + "./client": { + "import": { + "browser": { + "types": "./dist/client.d.ts", + "default": "./dist/client.js" + }, + "types": "./dist/client.d.ts", + "default": "./dist/client.js" + }, + "require": { + "types": "./dist/client.d.cts", + "default": "./dist/client.cjs" + } + }, + "./react": { + "import": { + "types": "./dist/react.d.ts", + "default": "./dist/react.js" + }, + "require": { + "types": "./dist/react.d.cts", + "default": "./dist/react.cjs" + } + } + }, + "engines": { + "node": ">=22.0.0" + }, + "scripts": { + "build": "tsup src/mod.ts src/client.ts src/react.ts", + "check-types": "tsc --noEmit" + }, + "dependencies": { + "rivetkit": "workspace:^", + "@rivetkit/react": "workspace:*" + }, + "peerDependencies": { + "react": "^18 || ^19", + "react-dom": "^18 || ^19" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + }, + "devDependencies": { + "tsup": "^8.4.0", + "typescript": "^5.7.3" + }, + "stableVersion": "0.8.0" +} diff --git a/rivetkit-typescript/packages/agentos/src/client.ts b/rivetkit-typescript/packages/agentos/src/client.ts new file mode 100644 index 0000000000..4926dca1ca --- /dev/null +++ b/rivetkit-typescript/packages/agentos/src/client.ts @@ -0,0 +1,14 @@ +/** + * `@rivet-dev/agentos/client` — Agent OS client surface. + * + * Re-exports RivetKit's client entry (`createClient`, `ActorHandle`, + * `ActorConn`, etc.). This is deliberately kept OFF the root module so that + * server/Node actor code that imports `@rivet-dev/agentos` never transitively + * pulls in the browser/client bundle. + * + * The browser export condition is preserved through to `rivetkit/client` + * (which is external here), so consumers resolving this subpath in a browser + * environment still get RivetKit's browser client build. + */ + +export * from "rivetkit/client"; diff --git a/rivetkit-typescript/packages/agentos/src/mod.ts b/rivetkit-typescript/packages/agentos/src/mod.ts new file mode 100644 index 0000000000..f5dac0060b --- /dev/null +++ b/rivetkit-typescript/packages/agentos/src/mod.ts @@ -0,0 +1,38 @@ +/** + * `@rivet-dev/agentos` — the user-facing Agent OS integration for RivetKit. + * + * This package is the standalone home of the integration that historically + * shipped as the `rivetkit/agent-os` subpath export. It re-exports that + * surface verbatim so behavior is identical and the underlying Rust path is + * unchanged: the implementation still lives in `rivetkit` and continues to + * call into the published `@rivet-dev/agent-os-*` packages (sidecar, core, + * pi, etc.) exactly as before. + */ + +export { + agentOs, + type AgentOsActorDefinition, + nodeModulesMount, + type NodeModulesMountConfig, + type AgentOsActorConfig, + type AgentOsActorConfigInput, + agentOsActorConfigSchema, + type AgentOsActionContext, + type AgentOsActorState, + type AgentOsActorVars, + type AgentOsEvents, + type CronEventPayload, + type PermissionRequestPayload, + type PersistedSessionEvent, + type PersistedSessionRecord, + type ProcessExitPayload, + type ProcessOutputPayload, + type PromptResult, + type SerializableCronAction, + type SerializableCronJobOptions, + type SessionEventPayload, + type SessionRecord, + type ShellDataPayload, + type VmBootedPayload, + type VmShutdownPayload, +} from "rivetkit/agent-os"; diff --git a/rivetkit-typescript/packages/agentos/src/react.ts b/rivetkit-typescript/packages/agentos/src/react.ts new file mode 100644 index 0000000000..98f2e68f01 --- /dev/null +++ b/rivetkit-typescript/packages/agentos/src/react.ts @@ -0,0 +1,9 @@ +/** + * `@rivet-dev/agentos/react` — Agent OS React bindings. + * + * Re-exports `@rivetkit/react` (`createRivetKit`, `createRivetKitWithClient`, + * and the hooks). Kept on its own subpath so neither React nor the client + * bundle is pulled into server code that imports the root module. + */ + +export * from "@rivetkit/react"; diff --git a/rivetkit-typescript/packages/agentos/tsconfig.json b/rivetkit-typescript/packages/agentos/tsconfig.json new file mode 100644 index 0000000000..991a9b5f58 --- /dev/null +++ b/rivetkit-typescript/packages/agentos/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "types": [], + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/rivetkit-typescript/packages/agentos/tsup.config.ts b/rivetkit-typescript/packages/agentos/tsup.config.ts new file mode 100644 index 0000000000..f363b829fd --- /dev/null +++ b/rivetkit-typescript/packages/agentos/tsup.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from "tsup"; +import defaultConfig from "../../../tsup.base.ts"; + +export default defineConfig(defaultConfig); 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..2a9227c9b4 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 = "/home/nathan/agent-os-zid-patches/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/package.json b/rivetkit-typescript/packages/rivetkit/package.json index 4b864ab335..1e1a075003 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": "0.0.0-main.8794200", "@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": "0.0.0-main.8794200" }, "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": "0.0.0-main.8794200", + "@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..6f0a11a243 100644 --- a/rivetkit-typescript/packages/rivetkit/src/actor/definition.ts +++ b/rivetkit-typescript/packages/rivetkit/src/actor/definition.ts @@ -1,5 +1,9 @@ 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 +53,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 +99,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..3a473b3acc 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,270 @@ -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. + const definition = actor({ + actions: {}, + }) 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/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..1694359a4a --- /dev/null +++ b/rivetkit-typescript/packages/rivetkit/tests/agent-os-napi-factory.test.ts @@ -0,0 +1,66 @@ +/// 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..6868fcb228 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,57 @@ 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`); +} + describeDriverMatrix("Actor Agent Os", (driverTestConfig) => { describe.skipIf(driverTestConfig.skip?.agentOs || !hasAgentOsCore)( "Actor agentOS Tests", @@ -33,6 +85,41 @@ 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("mkdir and readdir", async (c) => { const { client } = await setupDriverTest(c, { ...driverTestConfig, @@ -139,6 +226,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/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