Compare commits
4 Commits
6aa1aec4e0
...
aa902fdd90
| Author | SHA1 | Date | |
|---|---|---|---|
| aa902fdd90 | |||
| d721b3fb94 | |||
| 4bf19f11a9 | |||
| 946fbecf4a |
@ -1 +1 @@
|
||||
0.2.2
|
||||
0.3.0
|
||||
|
||||
@ -1 +1 @@
|
||||
0.2.2
|
||||
0.3.0
|
||||
|
||||
430
envd-rs/Cargo.lock
generated
430
envd-rs/Cargo.lock
generated
@ -124,9 +124,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
@ -195,9 +195,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.1"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
@ -258,24 +258,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.20.2"
|
||||
version = "3.20.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.11.1"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.61"
|
||||
version = "1.2.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
|
||||
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@ -297,9 +297,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.44"
|
||||
version = "0.4.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
|
||||
checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"num-traits",
|
||||
@ -489,9 +489,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "6.1.0"
|
||||
version = "6.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
|
||||
checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossbeam-utils",
|
||||
@ -514,9 +514,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
@ -529,7 +529,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "envd"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"axum",
|
||||
@ -592,13 +592,12 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.27"
|
||||
version = "0.2.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||
checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -759,22 +758,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi 6.0.0",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.13"
|
||||
version = "0.4.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
|
||||
checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@ -807,9 +804,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.0"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@ -834,9 +831,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "1.4.0"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
||||
checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"itoa",
|
||||
@ -885,9 +882,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.9.0"
|
||||
version = "1.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
|
||||
checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
|
||||
dependencies = [
|
||||
"atomic-waker",
|
||||
"bytes",
|
||||
@ -931,7 +928,7 @@ dependencies = [
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
"windows-core 0.62.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -943,12 +940,6 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
@ -956,9 +947,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.0",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"hashbrown 0.17.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1014,21 +1003,20 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.98"
|
||||
version = "0.3.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08"
|
||||
checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.1.1"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||
checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
@ -1036,11 +1024,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.0.4"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
|
||||
checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bitflags 2.13.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@ -1050,30 +1038,12 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"libc",
|
||||
"plain",
|
||||
"redox_syscall 0.7.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
@ -1091,9 +1061,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
@ -1112,9 +1082,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.0"
|
||||
version = "2.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
@ -1144,9 +1114,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.2.0"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
|
||||
checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
@ -1177,7 +1147,7 @@ version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"bitflags 2.13.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@ -1189,7 +1159,7 @@ version = "7.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c533b4c39709f9ba5005d8002048266593c1cfaf3c5f0739d5b8ab0c6c504009"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"bitflags 2.13.0",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
@ -1268,7 +1238,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall 0.5.18",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
@ -1281,18 +1251,18 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.11"
|
||||
version = "1.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517"
|
||||
checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "1.1.11"
|
||||
version = "1.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6"
|
||||
checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1311,12 +1281,6 @@ version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "plain"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
@ -1338,9 +1302,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@ -1383,16 +1347,7 @@ version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"bitflags 2.13.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1408,9 +1363,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.10"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
@ -1418,7 +1373,7 @@ version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"bitflags 2.13.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
@ -1452,12 +1407,6 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
@ -1490,9 +1439,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.149"
|
||||
version = "1.0.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
@ -1546,9 +1495,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
@ -1574,15 +1523,15 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
version = "1.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
|
||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.3"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
@ -1608,9 +1557,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.117"
|
||||
version = "2.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1644,7 +1593,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.2",
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
@ -1681,9 +1630,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.1"
|
||||
version = "1.52.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
|
||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
@ -1739,11 +1688,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tower-http"
|
||||
version = "0.6.8"
|
||||
version = "0.6.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
|
||||
checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"bitflags 2.13.0",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
@ -1760,7 +1709,6 @@ dependencies = [
|
||||
"tokio-util",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1852,9 +1800,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.0"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
@ -1868,12 +1816,6 @@ version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
@ -1910,27 +1852,18 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.3+wasi-0.2.9"
|
||||
version = "1.0.4+wasi-0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||
checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487"
|
||||
dependencies = [
|
||||
"wit-bindgen 0.57.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen 0.51.0",
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.121"
|
||||
version = "0.2.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790"
|
||||
checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@ -1941,9 +1874,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.121"
|
||||
version = "0.2.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578"
|
||||
checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@ -1951,9 +1884,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.121"
|
||||
version = "0.2.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2"
|
||||
checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
@ -1964,47 +1897,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.121"
|
||||
version = "0.2.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441"
|
||||
checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags 2.11.1",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
@ -2042,7 +1941,7 @@ version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-core 0.57.0",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
@ -2052,12 +1951,25 @@ version = "0.57.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-result",
|
||||
"windows-implement 0.57.0",
|
||||
"windows-interface 0.57.0",
|
||||
"windows-result 0.1.2",
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement 0.60.2",
|
||||
"windows-interface 0.59.3",
|
||||
"windows-link",
|
||||
"windows-result 0.4.1",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.57.0"
|
||||
@ -2069,6 +1981,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.57.0"
|
||||
@ -2080,6 +2003,17 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
@ -2095,6 +2029,24 @@ dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
@ -2177,114 +2129,26 @@ version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.11.1",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.2"
|
||||
version = "1.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||
dependencies = [
|
||||
"zeroize_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize_derive"
|
||||
version = "1.4.3"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e"
|
||||
checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "envd"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.95"
|
||||
|
||||
|
||||
@ -30,8 +30,5 @@ pub async fn get_activity(State(state): State<Arc<AppState>>) -> impl IntoRespon
|
||||
disk_bps: state.disk_bps(),
|
||||
};
|
||||
|
||||
(
|
||||
[(header::CACHE_CONTROL, "no-store")],
|
||||
Json(body),
|
||||
)
|
||||
([(header::CACHE_CONTROL, "no-store")], Json(body))
|
||||
}
|
||||
|
||||
@ -346,7 +346,10 @@ fn step_realtime_clock(nanos: i64) {
|
||||
tracing::warn!(error = %std::io::Error::last_os_error(),
|
||||
"clock_settime(CLOCK_REALTIME) failed");
|
||||
} else {
|
||||
tracing::info!(nanos, "stepped CLOCK_REALTIME from host timestamp on resume");
|
||||
tracing::info!(
|
||||
nanos,
|
||||
"stepped CLOCK_REALTIME from host timestamp on resume"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -336,7 +336,12 @@ test -f "${HOME}/.bashrc" && . "${HOME}/.bashrc""#;
|
||||
// ($1 is cmd_str; $0 of the inner shell is the shell path).
|
||||
// - with args: exec the program + args directly, no shell interpretation
|
||||
// (backward-compatible program/argv form).
|
||||
let target = if args.is_empty() {
|
||||
let target = if cmd_str.is_empty() && args.is_empty() {
|
||||
// No command at all (e.g. an interactive PTY session with no explicit
|
||||
// command): launch the user's login shell directly. Under a pty its
|
||||
// stdin is a tty, so it starts interactively.
|
||||
format!(r#""{shell}""#)
|
||||
} else if args.is_empty() {
|
||||
format!(r#""{shell}" -c "$1" "{shell}""#)
|
||||
} else {
|
||||
r#""$@""#.to_string()
|
||||
@ -388,7 +393,7 @@ exec {nice_prefix}{target}"#
|
||||
let master_fd = pty_result.master;
|
||||
let slave_fd = pty_result.slave;
|
||||
|
||||
let mut command = std::process::Command::new("/bin/bash");
|
||||
let mut command = std::process::Command::new(&shell);
|
||||
command
|
||||
.args(&wrapper_args)
|
||||
.env_clear()
|
||||
@ -488,7 +493,7 @@ exec {nice_prefix}{target}"#
|
||||
end_rx,
|
||||
})
|
||||
} else {
|
||||
let mut command = std::process::Command::new("/bin/bash");
|
||||
let mut command = std::process::Command::new(&shell);
|
||||
command
|
||||
.args(&wrapper_args)
|
||||
.env_clear()
|
||||
|
||||
@ -72,7 +72,12 @@ impl ProcessServiceImpl {
|
||||
ConnectError::new(ErrorCode::InvalidArgument, "process config required")
|
||||
})?;
|
||||
|
||||
let username = self.state.defaults.user();
|
||||
// Per-request user overrides the sandbox default when provided.
|
||||
let username = if proc_config.user.is_empty() {
|
||||
self.state.defaults.user()
|
||||
} else {
|
||||
proc_config.user.to_string()
|
||||
};
|
||||
let user = lookup_user(&username).map_err(|e| ConnectError::new(ErrorCode::Internal, e))?;
|
||||
|
||||
let cmd_raw: &str = proc_config.cmd;
|
||||
|
||||
@ -2,7 +2,7 @@ import { apiFetch, apiFetchMultipart, type ApiResult } from '$lib/api/client';
|
||||
|
||||
export type BuildLogEntry = {
|
||||
step: number;
|
||||
phase: string; // "pre-build", "recipe", or "post-build"
|
||||
phase: string; // "pre-build", "recipe", "post-build", or "healthcheck"
|
||||
cmd: string;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
|
||||
@ -103,7 +103,11 @@ export function createBuildConsole(buildId: string) {
|
||||
exit: ev.exit ?? 0,
|
||||
elapsedMs: ev.elapsed_ms ?? 0
|
||||
});
|
||||
if (typeof ev.step === 'number' && ev.step > currentStep) currentStep = ev.step;
|
||||
// The healthcheck is shown as a trailing pseudo-step but is not
|
||||
// counted in total_steps, so it must not advance the counter.
|
||||
if (ev.phase !== 'healthcheck' && typeof ev.step === 'number' && ev.step > currentStep) {
|
||||
currentStep = ev.step;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'build-status':
|
||||
|
||||
@ -27,6 +27,8 @@
|
||||
return 'var(--color-text-bright)';
|
||||
case 'WORKDIR':
|
||||
return 'var(--color-text-tertiary)';
|
||||
case 'HEALTHCHECK':
|
||||
return 'var(--color-accent-bright)';
|
||||
default:
|
||||
return 'var(--color-text-muted)';
|
||||
}
|
||||
@ -111,9 +113,8 @@
|
||||
{/if}
|
||||
</div>
|
||||
<code class="mt-1.5 block truncate font-mono text-meta">
|
||||
<span style="color: {keywordColor(kw)}">{kw}</span>{#if rest}
|
||||
<span class="text-[var(--color-text-secondary)]">{rest}</span>
|
||||
{/if}
|
||||
<span style="color: {keywordColor(kw)}">{kw}</span>{#if rest}{' '}<span
|
||||
class="text-[var(--color-text-secondary)]">{rest}</span>{/if}
|
||||
</code>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@ -278,7 +278,7 @@
|
||||
if (tag) {
|
||||
msg.tag = tag;
|
||||
} else {
|
||||
msg.cmd = '/bin/bash';
|
||||
// No cmd: the server launches the user's default login shell.
|
||||
msg.envs = { TERM: 'xterm-256color' };
|
||||
}
|
||||
wsSend(ws, JSON.stringify(msg));
|
||||
|
||||
@ -23,7 +23,6 @@ import (
|
||||
|
||||
const (
|
||||
ptyKeepaliveInterval = 30 * time.Second
|
||||
ptyDefaultCmd = "/bin/bash"
|
||||
ptyDefaultCols = 80
|
||||
ptyDefaultRows = 24
|
||||
)
|
||||
@ -171,10 +170,9 @@ func (h *ptyHandler) handleStart(
|
||||
sandboxIDStr string,
|
||||
msg wsPtyIn,
|
||||
) {
|
||||
// An empty cmd is intentional: envd launches the user's default login shell
|
||||
// (resolved from /etc/passwd) when no command is given.
|
||||
cmd := msg.Cmd
|
||||
if cmd == "" {
|
||||
cmd = ptyDefaultCmd
|
||||
}
|
||||
cols := msg.Cols
|
||||
if cols == 0 {
|
||||
cols = ptyDefaultCols
|
||||
@ -237,6 +235,7 @@ func (h *ptyHandler) handleConnect(
|
||||
stream, err := agent.PtyAttach(ctx, connect.NewRequest(&pb.PtyAttachRequest{
|
||||
SandboxId: sandboxIDStr,
|
||||
Tag: msg.Tag,
|
||||
Reconnect: true,
|
||||
}))
|
||||
if err != nil {
|
||||
ws.writeJSON(wsPtyOut{Type: "error", Data: "failed to connect to pty: " + err.Error(), Fatal: true})
|
||||
|
||||
@ -2,7 +2,7 @@ openapi: "3.1.0"
|
||||
info:
|
||||
title: Wrenn API
|
||||
description: API for Wrenn — the runtime where AI coding agents live, work, and ship.
|
||||
version: "0.2.2"
|
||||
version: "0.3.0"
|
||||
|
||||
servers:
|
||||
- url: http://localhost:8080
|
||||
@ -2868,13 +2868,13 @@ paths:
|
||||
parameters:
|
||||
- name: before
|
||||
in: query
|
||||
schema: {type: string, format: date-time}
|
||||
schema: { type: string, format: date-time }
|
||||
- name: before_id
|
||||
in: query
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
- name: limit
|
||||
in: query
|
||||
schema: {type: integer, minimum: 1, maximum: 200, default: 50}
|
||||
schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
|
||||
responses:
|
||||
"200":
|
||||
description: Audit log page (all teams)
|
||||
@ -2947,7 +2947,7 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@ -2980,7 +2980,7 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"204":
|
||||
description: Deleted
|
||||
@ -3031,7 +3031,7 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: {type: object}
|
||||
items: { type: object }
|
||||
|
||||
/v1/admin/users/{id}/active:
|
||||
put:
|
||||
@ -3044,7 +3044,7 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@ -3053,7 +3053,7 @@ paths:
|
||||
type: object
|
||||
required: [active]
|
||||
properties:
|
||||
active: {type: boolean}
|
||||
active: { type: boolean }
|
||||
responses:
|
||||
"204":
|
||||
description: Updated
|
||||
@ -3086,7 +3086,7 @@ paths:
|
||||
- name: name
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"204":
|
||||
description: Deleted
|
||||
@ -3102,13 +3102,13 @@ paths:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {type: object}
|
||||
schema: { type: object }
|
||||
responses:
|
||||
"201":
|
||||
description: Build created
|
||||
content:
|
||||
application/json:
|
||||
schema: {type: object}
|
||||
schema: { type: object }
|
||||
get:
|
||||
summary: List builds (admin)
|
||||
operationId: adminListBuilds
|
||||
@ -3122,7 +3122,7 @@ paths:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items: {type: object}
|
||||
items: { type: object }
|
||||
|
||||
/v1/admin/builds/{id}:
|
||||
get:
|
||||
@ -3135,13 +3135,13 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"200":
|
||||
description: Build detail
|
||||
content:
|
||||
application/json:
|
||||
schema: {type: object}
|
||||
schema: { type: object }
|
||||
|
||||
/v1/admin/builds/{id}/cancel:
|
||||
post:
|
||||
@ -3154,7 +3154,7 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"204":
|
||||
description: Cancelled
|
||||
@ -3174,7 +3174,7 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"101":
|
||||
description: WebSocket upgrade — streams build console events
|
||||
@ -3220,7 +3220,7 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
get:
|
||||
summary: Get capsule detail (admin)
|
||||
operationId: adminGetCapsule
|
||||
@ -3271,7 +3271,7 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
@ -3301,7 +3301,7 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
post:
|
||||
summary: Execute a command on any capsule (admin)
|
||||
operationId: adminExecCommand
|
||||
@ -3341,7 +3341,7 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
get:
|
||||
summary: Get per-capsule resource metrics (admin)
|
||||
operationId: adminGetCapsuleMetrics
|
||||
@ -3375,7 +3375,7 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
get:
|
||||
summary: List running processes on any capsule (admin)
|
||||
operationId: adminListProcesses
|
||||
@ -3403,11 +3403,11 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
- name: selector
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
description: Process PID (numeric) or tag (string)
|
||||
delete:
|
||||
summary: Kill a process on any capsule (admin)
|
||||
@ -3440,7 +3440,7 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
post:
|
||||
summary: Upload a file to any capsule (admin)
|
||||
operationId: adminUploadFile
|
||||
@ -3455,8 +3455,8 @@ paths:
|
||||
type: object
|
||||
required: [path, file]
|
||||
properties:
|
||||
path: {type: string}
|
||||
file: {type: string, format: binary}
|
||||
path: { type: string }
|
||||
file: { type: string, format: binary }
|
||||
responses:
|
||||
"204":
|
||||
description: File uploaded
|
||||
@ -3480,7 +3480,7 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
post:
|
||||
summary: Download a file from any capsule (admin)
|
||||
operationId: adminDownloadFile
|
||||
@ -3515,7 +3515,7 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
post:
|
||||
summary: List directory contents on any capsule (admin)
|
||||
operationId: adminListDir
|
||||
@ -3549,7 +3549,7 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
post:
|
||||
summary: Create a directory on any capsule (admin)
|
||||
operationId: adminMakeDir
|
||||
@ -3583,7 +3583,7 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
post:
|
||||
summary: Remove a file or directory on any capsule (admin)
|
||||
operationId: adminRemovePath
|
||||
@ -3613,7 +3613,7 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
get:
|
||||
summary: Stream command execution on any capsule via WebSocket (admin)
|
||||
operationId: adminExecStream
|
||||
@ -3637,7 +3637,7 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
get:
|
||||
summary: Interactive PTY session on any capsule via WebSocket (admin)
|
||||
operationId: adminPtySession
|
||||
@ -3662,11 +3662,11 @@ paths:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
- name: selector
|
||||
in: path
|
||||
required: true
|
||||
schema: {type: string}
|
||||
schema: { type: string }
|
||||
description: Process PID (numeric) or tag (string)
|
||||
get:
|
||||
summary: Stream process output on any capsule via WebSocket (admin)
|
||||
@ -3867,7 +3867,7 @@ components:
|
||||
to 60 (the agent's startup envelope).
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: {type: string}
|
||||
additionalProperties: { type: string }
|
||||
nullable: true
|
||||
description: >
|
||||
Optional user-supplied key/value labels attached to the capsule.
|
||||
@ -3952,7 +3952,21 @@ components:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
enum: [pending, starting, running, pausing, paused, snapshotting, resuming, stopping, hibernated, stopped, missing, error]
|
||||
enum:
|
||||
[
|
||||
pending,
|
||||
starting,
|
||||
running,
|
||||
pausing,
|
||||
paused,
|
||||
snapshotting,
|
||||
resuming,
|
||||
stopping,
|
||||
hibernated,
|
||||
stopped,
|
||||
missing,
|
||||
error,
|
||||
]
|
||||
template:
|
||||
type: string
|
||||
vcpus:
|
||||
@ -3977,7 +3991,7 @@ components:
|
||||
format: date-time
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: {type: string}
|
||||
additionalProperties: { type: string }
|
||||
nullable: true
|
||||
description: |
|
||||
User-supplied key/value labels attached at create-time. Once the
|
||||
@ -4037,7 +4051,7 @@ components:
|
||||
cannot be deleted.
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: {type: string}
|
||||
additionalProperties: { type: string }
|
||||
nullable: true
|
||||
|
||||
AdminTemplate:
|
||||
@ -4722,15 +4736,15 @@ components:
|
||||
AuditLogEntry:
|
||||
type: object
|
||||
properties:
|
||||
id: {type: string}
|
||||
actor_type: {type: string, enum: [user, api_key, host, system]}
|
||||
actor_id: {type: string}
|
||||
actor_name: {type: string}
|
||||
resource_type: {type: string}
|
||||
resource_id: {type: string}
|
||||
action: {type: string}
|
||||
scope: {type: string}
|
||||
status: {type: string, enum: [success, failure]}
|
||||
id: { type: string }
|
||||
actor_type: { type: string, enum: [user, api_key, host, system] }
|
||||
actor_id: { type: string }
|
||||
actor_name: { type: string }
|
||||
resource_type: { type: string }
|
||||
resource_id: { type: string }
|
||||
action: { type: string }
|
||||
scope: { type: string }
|
||||
status: { type: string, enum: [success, failure] }
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
@ -4767,17 +4781,17 @@ components:
|
||||
resource:
|
||||
type: object
|
||||
properties:
|
||||
id: {type: string}
|
||||
type: {type: string}
|
||||
id: { type: string }
|
||||
type: { type: string }
|
||||
actor:
|
||||
type: object
|
||||
properties:
|
||||
type: {type: string, enum: [user, api_key, system]}
|
||||
id: {type: string}
|
||||
name: {type: string}
|
||||
type: { type: string, enum: [user, api_key, system] }
|
||||
id: { type: string }
|
||||
name: { type: string }
|
||||
metadata:
|
||||
type: object
|
||||
additionalProperties: {type: string}
|
||||
additionalProperties: { type: string }
|
||||
description: |
|
||||
Event-specific context. Examples: `reason` (ttl_expired,
|
||||
host_failure, cleanup_after_create_error, orphaned),
|
||||
|
||||
@ -23,14 +23,14 @@ type PtyEvent struct {
|
||||
// PtyStart starts a new PTY process in the guest and returns a channel of events.
|
||||
// The tag is the stable identifier used to reconnect via PtyConnect.
|
||||
// The channel is closed when the process ends or ctx is cancelled.
|
||||
// NOTE: The user parameter from PtyAttachRequest is not yet supported by envd's
|
||||
// ProcessConfig proto. When envd adds user support, thread it through here.
|
||||
func (c *Client) PtyStart(ctx context.Context, tag, cmd string, args []string, cols, rows uint32, envs map[string]string, cwd string) (<-chan PtyEvent, error) {
|
||||
// An empty user runs as the sandbox default user.
|
||||
func (c *Client) PtyStart(ctx context.Context, tag, cmd string, args []string, cols, rows uint32, envs map[string]string, cwd, user string) (<-chan PtyEvent, error) {
|
||||
stdin := true
|
||||
cfg := &envdpb.ProcessConfig{
|
||||
Cmd: cmd,
|
||||
Args: args,
|
||||
Envs: envs,
|
||||
User: user,
|
||||
}
|
||||
if cwd != "" {
|
||||
cfg.Cwd = &cwd
|
||||
|
||||
@ -682,7 +682,7 @@ func (s *Server) PtyAttach(
|
||||
) error {
|
||||
msg := req.Msg
|
||||
|
||||
events, err := s.mgr.PtyAttach(ctx, msg.SandboxId, msg.Tag, msg.Cmd, msg.Args, msg.Cols, msg.Rows, msg.Envs, msg.Cwd)
|
||||
events, err := s.mgr.PtyAttach(ctx, msg.SandboxId, msg.Tag, msg.Cmd, msg.Args, msg.Cols, msg.Rows, msg.Envs, msg.Cwd, msg.User, msg.Reconnect)
|
||||
if err != nil {
|
||||
return connect.NewError(connect.CodeInternal, fmt.Errorf("pty attach: %w", err))
|
||||
}
|
||||
|
||||
@ -21,30 +21,33 @@ type ExecContext struct {
|
||||
var envRegex = regexp.MustCompile(`\$\$|\$\{([a-zA-Z0-9_]*)\}|\$([a-zA-Z0-9_]+)`)
|
||||
|
||||
// WrappedCommand returns the full shell command for a RUN step with context
|
||||
// applied. The result is passed as the argument to /bin/sh -c.
|
||||
// applied. The result is handed to the exec layer as a bare command (no
|
||||
// "-c" wrapper), so the user's default login shell — resolved by envd from
|
||||
// /etc/passwd inside the VM — interprets it.
|
||||
//
|
||||
// If WORKDIR and/or ENV are set, they are prepended as a shell preamble:
|
||||
//
|
||||
// cd '/the/dir' && KEY='val' /bin/sh -c 'original command'
|
||||
// cd '/the/dir' && export KEY='val' && original command
|
||||
//
|
||||
// If USER is set to a non-root user, the entire command is wrapped with su:
|
||||
// If USER is set to a non-root user, the entire command is wrapped with su.
|
||||
// Dropping `-s` lets su run it under that user's login shell rather than
|
||||
// forcing /bin/sh:
|
||||
//
|
||||
// su <user> -s /bin/sh -c '<preamble + command>'
|
||||
// su <user> -c '<preamble + command>'
|
||||
func (c *ExecContext) WrappedCommand(cmd string) string {
|
||||
inner := c.innerCommand(cmd)
|
||||
if c.User != "" && c.User != "root" {
|
||||
return "su " + shellescape(c.User) + " -s /bin/sh -c " + shellescape(inner)
|
||||
return "su " + shellescape(c.User) + " -c " + shellescape(inner)
|
||||
}
|
||||
return inner
|
||||
}
|
||||
|
||||
// innerCommand builds the command with workdir/env preamble but without user wrapping.
|
||||
// innerCommand applies the workdir/env preamble to cmd without user wrapping.
|
||||
// The preamble cds into WORKDIR and exports ENV vars, then the command runs in
|
||||
// the same (login) shell — no nested shell is named, so the user's default
|
||||
// shell interprets the command and any pipes/operators within it.
|
||||
func (c *ExecContext) innerCommand(cmd string) string {
|
||||
prefix := c.shellPrefix()
|
||||
if prefix == "" {
|
||||
return cmd
|
||||
}
|
||||
return prefix + "/bin/sh -c " + shellescape(cmd)
|
||||
return c.shellPrefix() + cmd
|
||||
}
|
||||
|
||||
// StartCommand returns the shell command for a START step. The process is
|
||||
@ -55,16 +58,22 @@ func (c *ExecContext) innerCommand(cmd string) string {
|
||||
// Multiple START steps can be issued to run several background processes
|
||||
// simultaneously before a healthcheck is evaluated.
|
||||
func (c *ExecContext) StartCommand(cmd string) string {
|
||||
// Launch the background process under the user's login shell. $SHELL is set
|
||||
// to that shell by su (non-root) or by envd (root), with /bin/sh as a safe
|
||||
// fallback if it is somehow unset.
|
||||
prefix := c.shellPrefix()
|
||||
inner := prefix + "nohup /bin/sh -c " + shellescape(cmd) + " >/dev/null 2>&1 &"
|
||||
inner := prefix + `nohup "${SHELL:-/bin/sh}" -c ` + shellescape(cmd) + " >/dev/null 2>&1 &"
|
||||
if c.User != "" && c.User != "root" {
|
||||
return "su " + shellescape(c.User) + " -s /bin/sh -c " + shellescape(inner)
|
||||
return "su " + shellescape(c.User) + " -c " + shellescape(inner)
|
||||
}
|
||||
return inner
|
||||
}
|
||||
|
||||
// shellPrefix builds the "cd ... && KEY=val " preamble for a shell command.
|
||||
// Returns an empty string when no context is set.
|
||||
// shellPrefix builds the "cd '/dir' && export KEY='val' && " preamble for a
|
||||
// shell command. ENV vars are exported (not just assignment-prefixed) so they
|
||||
// apply to the whole command — including any pipes or && chains — and to child
|
||||
// processes, matching Dockerfile ENV semantics. Returns an empty string when no
|
||||
// context is set.
|
||||
func (c *ExecContext) shellPrefix() string {
|
||||
if c.WorkDir == "" && len(c.EnvVars) == 0 {
|
||||
return ""
|
||||
@ -75,16 +84,20 @@ func (c *ExecContext) shellPrefix() string {
|
||||
sb.WriteString(shellescape(c.WorkDir))
|
||||
sb.WriteString(" && ")
|
||||
}
|
||||
keys := make([]string, 0, len(c.EnvVars))
|
||||
for k := range c.EnvVars {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
for _, k := range keys {
|
||||
sb.WriteString(k)
|
||||
sb.WriteByte('=')
|
||||
sb.WriteString(shellescape(c.EnvVars[k]))
|
||||
sb.WriteByte(' ')
|
||||
if len(c.EnvVars) > 0 {
|
||||
keys := make([]string, 0, len(c.EnvVars))
|
||||
for k := range c.EnvVars {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
slices.Sort(keys)
|
||||
sb.WriteString("export")
|
||||
for _, k := range keys {
|
||||
sb.WriteByte(' ')
|
||||
sb.WriteString(k)
|
||||
sb.WriteByte('=')
|
||||
sb.WriteString(shellescape(c.EnvVars[k]))
|
||||
}
|
||||
sb.WriteString(" && ")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
@ -20,31 +20,31 @@ func TestExecContext_WrappedCommand(t *testing.T) {
|
||||
name: "workdir only",
|
||||
ctx: ExecContext{WorkDir: "/app"},
|
||||
cmd: "npm install",
|
||||
want: "cd '/app' && /bin/sh -c 'npm install'",
|
||||
want: "cd '/app' && npm install",
|
||||
},
|
||||
{
|
||||
name: "env only",
|
||||
ctx: ExecContext{EnvVars: map[string]string{"PORT": "8080"}},
|
||||
cmd: "node server.js",
|
||||
want: "PORT='8080' /bin/sh -c 'node server.js'",
|
||||
want: "export PORT='8080' && node server.js",
|
||||
},
|
||||
{
|
||||
name: "workdir with space",
|
||||
ctx: ExecContext{WorkDir: "/my project"},
|
||||
cmd: "make build",
|
||||
want: "cd '/my project' && /bin/sh -c 'make build'",
|
||||
want: "cd '/my project' && make build",
|
||||
},
|
||||
{
|
||||
name: "command with single quotes",
|
||||
ctx: ExecContext{WorkDir: "/app"},
|
||||
cmd: "echo 'hello'",
|
||||
want: "cd '/app' && /bin/sh -c 'echo '\\''hello'\\'''",
|
||||
want: "cd '/app' && echo 'hello'",
|
||||
},
|
||||
{
|
||||
name: "env value with single quotes",
|
||||
ctx: ExecContext{EnvVars: map[string]string{"MSG": "it's fine"}},
|
||||
cmd: "echo $MSG",
|
||||
want: "MSG='it'\\''s fine' /bin/sh -c 'echo $MSG'",
|
||||
want: "export MSG='it'\\''s fine' && echo $MSG",
|
||||
},
|
||||
{
|
||||
name: "env expansion with pre-expanded PATH",
|
||||
@ -52,7 +52,25 @@ func TestExecContext_WrappedCommand(t *testing.T) {
|
||||
EnvVars: map[string]string{"PATH": "/usr/bin", "FOO": "/opt/venv/bin:/usr/bin"},
|
||||
},
|
||||
cmd: "make build",
|
||||
want: "FOO='/opt/venv/bin:/usr/bin' PATH='/usr/bin' /bin/sh -c 'make build'",
|
||||
want: "export FOO='/opt/venv/bin:/usr/bin' PATH='/usr/bin' && make build",
|
||||
},
|
||||
{
|
||||
name: "non-root user wraps with su login shell",
|
||||
ctx: ExecContext{User: "wrenn-user"},
|
||||
cmd: "whoami",
|
||||
want: "su 'wrenn-user' -c 'whoami'",
|
||||
},
|
||||
{
|
||||
name: "non-root user with workdir and env",
|
||||
ctx: ExecContext{User: "wrenn-user", WorkDir: "/app", EnvVars: map[string]string{"PORT": "8080"}},
|
||||
cmd: "node server.js",
|
||||
want: "su 'wrenn-user' -c 'cd '\\''/app'\\'' && export PORT='\\''8080'\\'' && node server.js'",
|
||||
},
|
||||
{
|
||||
name: "root user is not su-wrapped",
|
||||
ctx: ExecContext{User: "root", WorkDir: "/app"},
|
||||
cmd: "ls",
|
||||
want: "cd '/app' && ls",
|
||||
},
|
||||
}
|
||||
|
||||
@ -88,19 +106,25 @@ func TestExecContext_StartCommand(t *testing.T) {
|
||||
name: "no context",
|
||||
ctx: ExecContext{},
|
||||
cmd: "python3 app.py",
|
||||
want: "nohup /bin/sh -c 'python3 app.py' >/dev/null 2>&1 &",
|
||||
want: `nohup "${SHELL:-/bin/sh}" -c 'python3 app.py' >/dev/null 2>&1 &`,
|
||||
},
|
||||
{
|
||||
name: "with workdir",
|
||||
ctx: ExecContext{WorkDir: "/app"},
|
||||
cmd: "python3 server.py",
|
||||
want: "cd '/app' && nohup /bin/sh -c 'python3 server.py' >/dev/null 2>&1 &",
|
||||
want: `cd '/app' && nohup "${SHELL:-/bin/sh}" -c 'python3 server.py' >/dev/null 2>&1 &`,
|
||||
},
|
||||
{
|
||||
name: "with env",
|
||||
ctx: ExecContext{EnvVars: map[string]string{"PORT": "9000"}},
|
||||
cmd: "node index.js",
|
||||
want: "PORT='9000' nohup /bin/sh -c 'node index.js' >/dev/null 2>&1 &",
|
||||
want: `export PORT='9000' && nohup "${SHELL:-/bin/sh}" -c 'node index.js' >/dev/null 2>&1 &`,
|
||||
},
|
||||
{
|
||||
name: "non-root user wraps start with su",
|
||||
ctx: ExecContext{User: "wrenn-user"},
|
||||
cmd: "python3 app.py",
|
||||
want: `su 'wrenn-user' -c 'nohup "${SHELL:-/bin/sh}" -c '\''python3 app.py'\'' >/dev/null 2>&1 &'`,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -183,8 +183,7 @@ func execRun(
|
||||
start := time.Now()
|
||||
resp, err := execFn(execCtx, connect.NewRequest(&pb.ExecRequest{
|
||||
SandboxId: sandboxID,
|
||||
Cmd: "/bin/sh",
|
||||
Args: []string{"-c", bctx.WrappedCommand(st.Shell)},
|
||||
Cmd: bctx.WrappedCommand(st.Shell),
|
||||
TimeoutSec: int32(timeout.Seconds()),
|
||||
}))
|
||||
|
||||
@ -360,8 +359,7 @@ func execRawShell(
|
||||
start := time.Now()
|
||||
resp, err := execFn(execCtx, connect.NewRequest(&pb.ExecRequest{
|
||||
SandboxId: sandboxID,
|
||||
Cmd: "/bin/sh",
|
||||
Args: []string{"-c", shellCmd},
|
||||
Cmd: shellCmd,
|
||||
TimeoutSec: int32(timeout.Seconds()),
|
||||
}))
|
||||
|
||||
@ -398,8 +396,7 @@ func execStart(
|
||||
start := time.Now()
|
||||
resp, err := execFn(execCtx, connect.NewRequest(&pb.ExecRequest{
|
||||
SandboxId: sandboxID,
|
||||
Cmd: "/bin/sh",
|
||||
Args: []string{"-c", bctx.StartCommand(st.Shell)},
|
||||
Cmd: bctx.StartCommand(st.Shell),
|
||||
TimeoutSec: 10,
|
||||
}))
|
||||
|
||||
|
||||
@ -721,16 +721,18 @@ func (m *Manager) SetDefaults(ctx context.Context, sandboxID, defaultUser string
|
||||
}
|
||||
|
||||
// PtyAttach starts a new PTY process or reconnects to an existing one.
|
||||
// If cmd is non-empty, starts a new process. If empty, reconnects using tag.
|
||||
func (m *Manager) PtyAttach(ctx context.Context, sandboxID, tag, cmd string, args []string, cols, rows uint32, envs map[string]string, cwd string) (<-chan envdclient.PtyEvent, error) {
|
||||
// When reconnect is true it reattaches to the process identified by tag;
|
||||
// otherwise it starts a new process (cmd may be empty to launch the user's
|
||||
// default login shell). user empty means the sandbox default user.
|
||||
func (m *Manager) PtyAttach(ctx context.Context, sandboxID, tag, cmd string, args []string, cols, rows uint32, envs map[string]string, cwd, user string, reconnect bool) (<-chan envdclient.PtyEvent, error) {
|
||||
c, err := m.activeClient(sandboxID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cmd != "" {
|
||||
return c.PtyStart(ctx, tag, cmd, args, cols, rows, envs, cwd)
|
||||
if reconnect {
|
||||
return c.PtyConnect(ctx, tag)
|
||||
}
|
||||
return c.PtyConnect(ctx, tag)
|
||||
return c.PtyStart(ctx, tag, cmd, args, cols, rows, envs, cwd, user)
|
||||
}
|
||||
|
||||
// PtySendInput sends raw bytes to a PTY process in a sandbox.
|
||||
|
||||
@ -410,7 +410,15 @@ func (s *BuildService) executeBuild(ctx context.Context, buildIDStr string) {
|
||||
templateDefaultUser := bctx.User
|
||||
templateDefaultEnv := filterBuildEnv(bctx.EnvVars)
|
||||
|
||||
// Phase 3: Post-build (as root) — cleanup.
|
||||
// Phase 3: Healthcheck — runs before cleanup so a failing healthcheck aborts
|
||||
// immediately and skips post-build (the sandbox is destroyed regardless). It
|
||||
// runs as the template's default user, against the app the recipe started.
|
||||
if !s.runHealthcheck(buildCtx, buildID, build, agent, sandboxIDStr, &logs, streamFn, templateDefaultUser, log) {
|
||||
return
|
||||
}
|
||||
|
||||
// Phase 4: Post-build (as root) — cleanup. Only reached once the healthcheck
|
||||
// has passed, so we never clean up an image that fails verification.
|
||||
bctx.User = "root"
|
||||
if !build.SkipPrePost {
|
||||
if !runPhase("post-build", postBuildSteps, 0) {
|
||||
@ -418,7 +426,7 @@ func (s *BuildService) executeBuild(ctx context.Context, buildIDStr string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Finalize: healthcheck/snapshot/flatten → persist template → mark success.
|
||||
// Finalize: snapshot/flatten → persist template → mark success.
|
||||
s.finalizeBuild(buildCtx, buildID, build, agent, sandboxIDStr, templateDefaultUser, templateDefaultEnv, sandboxMetadata, log)
|
||||
}
|
||||
|
||||
@ -517,8 +525,9 @@ func (s *BuildService) provisionBuildSandbox(
|
||||
return agent, sandboxIDStr, sandboxMetadata, nil
|
||||
}
|
||||
|
||||
// finalizeBuild handles the healthcheck/snapshot/flatten step and persists the
|
||||
// template record. Called after all recipe phases complete successfully.
|
||||
// finalizeBuild snapshots (or flattens) the verified rootfs and persists the
|
||||
// template record. Called after the recipe, healthcheck, and post-build phases
|
||||
// all complete successfully.
|
||||
func (s *BuildService) finalizeBuild(
|
||||
ctx context.Context,
|
||||
buildID pgtype.UUID,
|
||||
@ -532,23 +541,7 @@ func (s *BuildService) finalizeBuild(
|
||||
) {
|
||||
var sizeBytes int64
|
||||
if build.Healthcheck != "" {
|
||||
hc, err := recipe.ParseHealthcheck(build.Healthcheck)
|
||||
if err != nil {
|
||||
s.destroySandbox(ctx, agent, sandboxIDStr)
|
||||
s.failBuild(ctx, buildID, fmt.Sprintf("invalid healthcheck: %v", err))
|
||||
return
|
||||
}
|
||||
log.Info("running healthcheck", "cmd", hc.Cmd, "interval", hc.Interval, "timeout", hc.Timeout, "start_period", hc.StartPeriod, "retries", hc.Retries)
|
||||
if err := s.waitForHealthcheck(ctx, agent, sandboxIDStr, hc, defaultUser); err != nil {
|
||||
s.destroySandbox(ctx, agent, sandboxIDStr)
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
s.failBuild(ctx, buildID, fmt.Sprintf("healthcheck failed: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("healthcheck passed, creating snapshot")
|
||||
log.Info("creating snapshot")
|
||||
snapResp, err := agent.CreateSnapshot(ctx, connect.NewRequest(&pb.CreateSnapshotRequest{
|
||||
SandboxId: sandboxIDStr,
|
||||
Name: build.Name,
|
||||
@ -630,19 +623,115 @@ func (s *BuildService) finalizeBuild(
|
||||
log.Info("template build completed successfully", "name", build.Name)
|
||||
}
|
||||
|
||||
// waitForHealthcheck repeatedly executes the healthcheck command inside the
|
||||
// healthcheckResult captures what a healthcheck run produced, for recording as
|
||||
// a build pseudo-step. Output is the merged per-attempt console log (capped to
|
||||
// hcMaxOutputBytes); Exit is the final attempt's exit code.
|
||||
type healthcheckResult struct {
|
||||
Output string
|
||||
Exit int32
|
||||
Attempts int
|
||||
Elapsed int64 // total wall time in milliseconds
|
||||
}
|
||||
|
||||
// hcMaxOutputBytes bounds how much healthcheck output is retained for the cold
|
||||
// log/replay, so a long-polling check can't grow the persisted log unbounded.
|
||||
// The live stream is never truncated — this only caps what we store.
|
||||
const hcMaxOutputBytes = 32 * 1024
|
||||
|
||||
// runHealthcheck executes the configured healthcheck (if any) as a streamed
|
||||
// pseudo-step, before the post-build cleanup phase. It emits step-start /
|
||||
// output / step-end events live, appends the resulting log entry to *logs, and
|
||||
// persists it. The healthcheck is numbered after every recipe/post-build step
|
||||
// (total_steps+1) and deliberately does not advance current_step.
|
||||
//
|
||||
// Returns true when the build should continue (healthcheck passed or none was
|
||||
// configured), false when it must stop (invalid/failed healthcheck, or the
|
||||
// build was cancelled) — in which case the sandbox has been destroyed and the
|
||||
// build marked failed.
|
||||
func (s *BuildService) runHealthcheck(
|
||||
ctx context.Context,
|
||||
buildID pgtype.UUID,
|
||||
build db.TemplateBuild,
|
||||
agent buildAgentClient,
|
||||
sandboxIDStr string,
|
||||
logs *[]recipe.BuildLogEntry,
|
||||
streamFn recipe.StreamExecFunc,
|
||||
user string,
|
||||
log *slog.Logger,
|
||||
) bool {
|
||||
if build.Healthcheck == "" {
|
||||
return true
|
||||
}
|
||||
hc, err := recipe.ParseHealthcheck(build.Healthcheck)
|
||||
if err != nil {
|
||||
s.destroySandbox(ctx, agent, sandboxIDStr)
|
||||
s.failBuild(ctx, buildID, fmt.Sprintf("invalid healthcheck: %v", err))
|
||||
return false
|
||||
}
|
||||
log.Info("running healthcheck", "cmd", hc.Cmd, "interval", hc.Interval, "timeout", hc.Timeout, "start_period", hc.StartPeriod, "retries", hc.Retries)
|
||||
|
||||
hcStep := int(build.TotalSteps) + 1
|
||||
buildIDStr := id.FormatBuildID(buildID)
|
||||
hcCmd := "HEALTHCHECK " + hc.Cmd
|
||||
publishBuildEvent(ctx, s.Redis, buildIDStr, BuildStreamEvent{
|
||||
Type: "step-start", Step: hcStep, Phase: "healthcheck", Cmd: hcCmd,
|
||||
})
|
||||
|
||||
// Forward each output chunk to the live console as it arrives.
|
||||
onChunk := func(data []byte) {
|
||||
publishBuildEvent(ctx, s.Redis, buildIDStr, BuildStreamEvent{
|
||||
Type: "output", Step: hcStep, Data: base64.StdEncoding.EncodeToString(data),
|
||||
})
|
||||
}
|
||||
|
||||
res, hcErr := s.streamHealthcheck(ctx, sandboxIDStr, hc, user, streamFn, onChunk)
|
||||
entry := recipe.BuildLogEntry{
|
||||
Step: hcStep, Phase: "healthcheck", Cmd: hcCmd,
|
||||
Stdout: res.Output, Exit: res.Exit, Elapsed: res.Elapsed, Ok: hcErr == nil,
|
||||
}
|
||||
*logs = append(*logs, entry)
|
||||
// Persist while pinning current_step to total_steps so the progress counter
|
||||
// doesn't overshoot — the healthcheck is shown but not counted.
|
||||
s.updateLogs(ctx, buildID, int(build.TotalSteps), *logs)
|
||||
publishBuildEvent(ctx, s.Redis, buildIDStr, BuildStreamEvent{
|
||||
Type: "step-end", Step: hcStep, Phase: "healthcheck", Cmd: hcCmd,
|
||||
Exit: entry.Exit, Ok: entry.Ok, ElapsedMs: entry.Elapsed,
|
||||
})
|
||||
|
||||
if hcErr != nil {
|
||||
s.destroySandbox(ctx, agent, sandboxIDStr)
|
||||
if ctx.Err() != nil {
|
||||
return false
|
||||
}
|
||||
s.failBuild(ctx, buildID, fmt.Sprintf("healthcheck failed: %v", hcErr))
|
||||
return false
|
||||
}
|
||||
log.Info("healthcheck passed")
|
||||
return true
|
||||
}
|
||||
|
||||
// streamHealthcheck repeatedly runs the healthcheck command in a PTY inside the
|
||||
// sandbox according to the config's interval, timeout, start-period, and
|
||||
// retries.
|
||||
// During the start period, failures are not counted toward the retry budget.
|
||||
// Returns nil on the first successful check, or an error if retries are
|
||||
// exhausted, the deadline passes, or the context is cancelled.
|
||||
func (s *BuildService) waitForHealthcheck(ctx context.Context, agent buildAgentClient, sandboxIDStr string, hc recipe.HealthcheckConfig, user string) error {
|
||||
// retries, forwarding output live via onChunk. During the start period,
|
||||
// failures are not counted toward the retry budget. Returns a nil error on the
|
||||
// first successful check, or an error if retries are exhausted, the deadline
|
||||
// passes, or the context is cancelled. The returned healthcheckResult always
|
||||
// carries the captured per-attempt output regardless of outcome.
|
||||
func (s *BuildService) streamHealthcheck(
|
||||
ctx context.Context,
|
||||
sandboxIDStr string,
|
||||
hc recipe.HealthcheckConfig,
|
||||
user string,
|
||||
streamFn recipe.StreamExecFunc,
|
||||
onChunk func(data []byte),
|
||||
) (result healthcheckResult, err error) {
|
||||
// Wrap the healthcheck command with su when a non-root user is set, so that
|
||||
// ~ expands to the correct home directory and the process runs with the
|
||||
// right UID (matching the template's default user).
|
||||
cmd := hc.Cmd
|
||||
if user != "" && user != "root" {
|
||||
cmd = "su " + recipe.Shellescape(user) + " -s /bin/sh -c " + recipe.Shellescape(hc.Cmd)
|
||||
// Drop `-s` so su runs the check under the user's login shell.
|
||||
cmd = "su " + recipe.Shellescape(user) + " -c " + recipe.Shellescape(hc.Cmd)
|
||||
}
|
||||
ticker := time.NewTicker(hc.Interval)
|
||||
defer ticker.Stop()
|
||||
@ -658,41 +747,82 @@ func (s *BuildService) waitForHealthcheck(ctx context.Context, agent buildAgentC
|
||||
|
||||
startedAt := time.Now()
|
||||
failCount := 0
|
||||
attempt := 0
|
||||
var output strings.Builder
|
||||
// emit sends a line to the live console and accumulates it (capped) for the
|
||||
// cold log used on reconnect/replay.
|
||||
emit := func(line string) {
|
||||
onChunk([]byte(line))
|
||||
if output.Len() < hcMaxOutputBytes {
|
||||
output.WriteString(line)
|
||||
}
|
||||
}
|
||||
// Populate the result on every return path.
|
||||
defer func() {
|
||||
result.Attempts = attempt
|
||||
result.Elapsed = time.Since(startedAt).Milliseconds()
|
||||
result.Output = strings.TrimRight(output.String(), "\r\n")
|
||||
}()
|
||||
|
||||
// runAttempt streams one healthcheck invocation, forwarding output, and
|
||||
// returns its exit code (or an error if the exec never completed).
|
||||
runAttempt := func() (int32, error) {
|
||||
attemptCtx, cancel := context.WithTimeout(ctx, hc.Timeout)
|
||||
defer cancel()
|
||||
ch, err := streamFn(attemptCtx, sandboxIDStr, cmd)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
var exit int32
|
||||
gotDone := false
|
||||
for chunk := range ch {
|
||||
if chunk.Err != nil {
|
||||
return -1, chunk.Err
|
||||
}
|
||||
if chunk.Done {
|
||||
exit, gotDone = chunk.Exit, true
|
||||
continue
|
||||
}
|
||||
emit(string(chunk.Data))
|
||||
}
|
||||
if !gotDone {
|
||||
return -1, fmt.Errorf("healthcheck stream ended without completion")
|
||||
}
|
||||
return exit, nil
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
return result, ctx.Err()
|
||||
case <-deadlineCh:
|
||||
return fmt.Errorf("healthcheck timed out: exceeded %d attempts over %s", failCount, time.Since(startedAt))
|
||||
return result, fmt.Errorf("healthcheck timed out: exceeded %d attempts over %s", failCount, time.Since(startedAt))
|
||||
case <-ticker.C:
|
||||
execCtx, cancel := context.WithTimeout(ctx, hc.Timeout)
|
||||
resp, err := agent.Exec(execCtx, connect.NewRequest(&pb.ExecRequest{
|
||||
SandboxId: sandboxIDStr,
|
||||
Cmd: "/bin/sh",
|
||||
Args: []string{"-c", cmd},
|
||||
TimeoutSec: int32(hc.Timeout.Seconds()),
|
||||
}))
|
||||
cancel()
|
||||
attempt++
|
||||
emit(fmt.Sprintf("\x1b[2m── attempt %d ──\x1b[0m\r\n", attempt))
|
||||
exit, attemptErr := runAttempt()
|
||||
result.Exit = exit
|
||||
|
||||
if err != nil {
|
||||
slog.Debug("healthcheck exec error (retrying)", "error", err)
|
||||
if attemptErr != nil {
|
||||
emit(fmt.Sprintf("exec error: %v\r\n", attemptErr))
|
||||
slog.Debug("healthcheck exec error (retrying)", "error", attemptErr)
|
||||
if time.Since(startedAt) >= hc.StartPeriod {
|
||||
failCount++
|
||||
if hc.Retries > 0 && failCount >= hc.Retries {
|
||||
return fmt.Errorf("healthcheck failed after %d retries: exec error: %w", failCount, err)
|
||||
return result, fmt.Errorf("healthcheck failed after %d retries: exec error: %w", failCount, attemptErr)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if resp.Msg.ExitCode == 0 {
|
||||
return nil
|
||||
emit(fmt.Sprintf("\x1b[2m→ exit %d\x1b[0m\r\n", exit))
|
||||
if exit == 0 {
|
||||
return result, nil
|
||||
}
|
||||
slog.Debug("healthcheck failed (retrying)", "exit_code", resp.Msg.ExitCode)
|
||||
slog.Debug("healthcheck failed (retrying)", "exit_code", exit)
|
||||
if time.Since(startedAt) >= hc.StartPeriod {
|
||||
failCount++
|
||||
if hc.Retries > 0 && failCount >= hc.Retries {
|
||||
return fmt.Errorf("healthcheck failed after %d retries: exit code %d", failCount, resp.Msg.ExitCode)
|
||||
return result, fmt.Errorf("healthcheck failed after %d retries: exit code %d", failCount, exit)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -755,10 +885,12 @@ func (s *BuildService) ptyStreamExec(agent buildAgentClient) recipe.StreamExecFu
|
||||
stream, err := agent.PtyAttach(ctx, connect.NewRequest(&pb.PtyAttachRequest{
|
||||
SandboxId: sandboxID,
|
||||
Tag: tag,
|
||||
Cmd: "/bin/sh",
|
||||
Args: []string{"-c", shellCmd},
|
||||
Cols: buildPtyCols,
|
||||
Rows: buildPtyRows,
|
||||
// Bare command: envd wraps it in the user's login shell (resolved
|
||||
// from /etc/passwd), so build steps run under the image's default
|
||||
// shell instead of a forced /bin/sh.
|
||||
Cmd: shellCmd,
|
||||
Cols: buildPtyCols,
|
||||
Rows: buildPtyRows,
|
||||
}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -836,8 +968,7 @@ func (s *BuildService) fetchSandboxEnv(ctx context.Context,
|
||||
agent buildAgentClient, sandboxIDStr string) (map[string]string, error) {
|
||||
resp, err := agent.Exec(ctx, connect.NewRequest(&pb.ExecRequest{
|
||||
SandboxId: sandboxIDStr,
|
||||
Cmd: "/bin/sh",
|
||||
Args: []string{"-c", "env"},
|
||||
Cmd: "env",
|
||||
TimeoutSec: 10,
|
||||
}))
|
||||
if err != nil {
|
||||
@ -922,8 +1053,7 @@ func (s *BuildService) uploadAndExtractArchive(
|
||||
|
||||
resp, err := agent.Exec(ctx, connect.NewRequest(&pb.ExecRequest{
|
||||
SandboxId: sandboxID,
|
||||
Cmd: "/bin/sh",
|
||||
Args: []string{"-c", fullCmd},
|
||||
Cmd: fullCmd,
|
||||
TimeoutSec: 120,
|
||||
}))
|
||||
if err != nil {
|
||||
|
||||
@ -117,11 +117,13 @@ func (x *PTY) GetSize() *PTY_Size {
|
||||
}
|
||||
|
||||
type ProcessConfig struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Cmd string `protobuf:"bytes,1,opt,name=cmd,proto3" json:"cmd,omitempty"`
|
||||
Args []string `protobuf:"bytes,2,rep,name=args,proto3" json:"args,omitempty"`
|
||||
Envs map[string]string `protobuf:"bytes,3,rep,name=envs,proto3" json:"envs,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
Cwd *string `protobuf:"bytes,4,opt,name=cwd,proto3,oneof" json:"cwd,omitempty"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Cmd string `protobuf:"bytes,1,opt,name=cmd,proto3" json:"cmd,omitempty"`
|
||||
Args []string `protobuf:"bytes,2,rep,name=args,proto3" json:"args,omitempty"`
|
||||
Envs map[string]string `protobuf:"bytes,3,rep,name=envs,proto3" json:"envs,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
|
||||
Cwd *string `protobuf:"bytes,4,opt,name=cwd,proto3,oneof" json:"cwd,omitempty"`
|
||||
// User to run the process as. Empty means the sandbox default user.
|
||||
User string `protobuf:"bytes,5,opt,name=user,proto3" json:"user,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@ -184,6 +186,13 @@ func (x *ProcessConfig) GetCwd() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ProcessConfig) GetUser() string {
|
||||
if x != nil {
|
||||
return x.User
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ListRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
@ -1713,12 +1722,13 @@ const file_process_proto_rawDesc = "" +
|
||||
"\x04size\x18\x01 \x01(\v2\x11.process.PTY.SizeR\x04size\x1a.\n" +
|
||||
"\x04Size\x12\x12\n" +
|
||||
"\x04cols\x18\x01 \x01(\rR\x04cols\x12\x12\n" +
|
||||
"\x04rows\x18\x02 \x01(\rR\x04rows\"\xc3\x01\n" +
|
||||
"\x04rows\x18\x02 \x01(\rR\x04rows\"\xd7\x01\n" +
|
||||
"\rProcessConfig\x12\x10\n" +
|
||||
"\x03cmd\x18\x01 \x01(\tR\x03cmd\x12\x12\n" +
|
||||
"\x04args\x18\x02 \x03(\tR\x04args\x124\n" +
|
||||
"\x04envs\x18\x03 \x03(\v2 .process.ProcessConfig.EnvsEntryR\x04envs\x12\x15\n" +
|
||||
"\x03cwd\x18\x04 \x01(\tH\x00R\x03cwd\x88\x01\x01\x1a7\n" +
|
||||
"\x03cwd\x18\x04 \x01(\tH\x00R\x03cwd\x88\x01\x01\x12\x12\n" +
|
||||
"\x04user\x18\x05 \x01(\tR\x04user\x1a7\n" +
|
||||
"\tEnvsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\x06\n" +
|
||||
|
||||
@ -37,6 +37,9 @@ message ProcessConfig {
|
||||
|
||||
map<string, string> envs = 3;
|
||||
optional string cwd = 4;
|
||||
|
||||
// User to run the process as. Empty means the sandbox default user.
|
||||
string user = 5;
|
||||
}
|
||||
|
||||
message ListRequest {}
|
||||
|
||||
@ -2985,8 +2985,8 @@ type PtyAttachRequest struct {
|
||||
// Tag is the stable identifier for this PTY session (e.g. "pty-abc123de").
|
||||
// Chosen by the caller and used to reconnect later.
|
||||
Tag string `protobuf:"bytes,2,opt,name=tag,proto3" json:"tag,omitempty"`
|
||||
// If cmd is non-empty, a new process is started. If empty, reconnects to
|
||||
// the existing process identified by tag.
|
||||
// Command to run for a new session. May be empty to launch the user's
|
||||
// default login shell. Ignored when reconnect is true.
|
||||
Cmd string `protobuf:"bytes,3,opt,name=cmd,proto3" json:"cmd,omitempty"`
|
||||
Args []string `protobuf:"bytes,4,rep,name=args,proto3" json:"args,omitempty"`
|
||||
Cols uint32 `protobuf:"varint,5,opt,name=cols,proto3" json:"cols,omitempty"`
|
||||
@ -2996,7 +2996,11 @@ type PtyAttachRequest struct {
|
||||
// Working directory. Empty means default.
|
||||
Cwd string `protobuf:"bytes,8,opt,name=cwd,proto3" json:"cwd,omitempty"`
|
||||
// User to run as. Empty means default (root).
|
||||
User string `protobuf:"bytes,9,opt,name=user,proto3" json:"user,omitempty"`
|
||||
User string `protobuf:"bytes,9,opt,name=user,proto3" json:"user,omitempty"`
|
||||
// If true, reconnect to the existing process identified by tag instead of
|
||||
// starting a new one. Distinguishes reconnect from a start whose cmd is
|
||||
// empty (login-shell default).
|
||||
Reconnect bool `protobuf:"varint,10,opt,name=reconnect,proto3" json:"reconnect,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@ -3094,6 +3098,13 @@ func (x *PtyAttachRequest) GetUser() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PtyAttachRequest) GetReconnect() bool {
|
||||
if x != nil {
|
||||
return x.Reconnect
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type PtyAttachResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Types that are valid to be assigned to Event:
|
||||
@ -4490,7 +4501,7 @@ const file_hostagent_proto_rawDesc = "" +
|
||||
"templateId\"8\n" +
|
||||
"\x17GetTemplateSizeResponse\x12\x1d\n" +
|
||||
"\n" +
|
||||
"size_bytes\x18\x01 \x01(\x03R\tsizeBytes\"\xae\x02\n" +
|
||||
"size_bytes\x18\x01 \x01(\x03R\tsizeBytes\"\xcc\x02\n" +
|
||||
"\x10PtyAttachRequest\x12\x1d\n" +
|
||||
"\n" +
|
||||
"sandbox_id\x18\x01 \x01(\tR\tsandboxId\x12\x10\n" +
|
||||
@ -4501,7 +4512,9 @@ const file_hostagent_proto_rawDesc = "" +
|
||||
"\x04rows\x18\x06 \x01(\rR\x04rows\x12<\n" +
|
||||
"\x04envs\x18\a \x03(\v2(.hostagent.v1.PtyAttachRequest.EnvsEntryR\x04envs\x12\x10\n" +
|
||||
"\x03cwd\x18\b \x01(\tR\x03cwd\x12\x12\n" +
|
||||
"\x04user\x18\t \x01(\tR\x04user\x1a7\n" +
|
||||
"\x04user\x18\t \x01(\tR\x04user\x12\x1c\n" +
|
||||
"\treconnect\x18\n" +
|
||||
" \x01(\bR\treconnect\x1a7\n" +
|
||||
"\tEnvsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xb8\x01\n" +
|
||||
|
||||
@ -471,8 +471,8 @@ message PtyAttachRequest {
|
||||
// Tag is the stable identifier for this PTY session (e.g. "pty-abc123de").
|
||||
// Chosen by the caller and used to reconnect later.
|
||||
string tag = 2;
|
||||
// If cmd is non-empty, a new process is started. If empty, reconnects to
|
||||
// the existing process identified by tag.
|
||||
// Command to run for a new session. May be empty to launch the user's
|
||||
// default login shell. Ignored when reconnect is true.
|
||||
string cmd = 3;
|
||||
repeated string args = 4;
|
||||
uint32 cols = 5;
|
||||
@ -483,6 +483,10 @@ message PtyAttachRequest {
|
||||
string cwd = 8;
|
||||
// User to run as. Empty means default (root).
|
||||
string user = 9;
|
||||
// If true, reconnect to the existing process identified by tag instead of
|
||||
// starting a new one. Distinguishes reconnect from a start whose cmd is
|
||||
// empty (login-shell default).
|
||||
bool reconnect = 10;
|
||||
}
|
||||
|
||||
message PtyAttachResponse {
|
||||
|
||||
@ -1,169 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
try:
|
||||
import websocket
|
||||
except ImportError:
|
||||
print("websocket-client is required: pip install websocket-client")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def create_kernel(base_url: str, token: str) -> str:
|
||||
url = f"{base_url}/api/kernels"
|
||||
headers = {}
|
||||
if token:
|
||||
headers["X-API-Key"] = token
|
||||
|
||||
req = urllib.request.Request(url, method="POST", data=b"", headers=headers)
|
||||
resp = urllib.request.urlopen(req)
|
||||
data = json.loads(resp.read())
|
||||
kernel_id = data["id"]
|
||||
print(f"Created kernel: {kernel_id}")
|
||||
return kernel_id
|
||||
|
||||
|
||||
def execute_code(ws: websocket.WebSocket, code: str) -> dict:
|
||||
msg_id = str(uuid.uuid4())
|
||||
session_id = str(uuid.uuid4())
|
||||
msg = {
|
||||
"header": {
|
||||
"msg_type": "execute_request",
|
||||
"msg_id": msg_id,
|
||||
"username": "",
|
||||
"session": session_id,
|
||||
"version": "5.3",
|
||||
"date": "",
|
||||
},
|
||||
"parent_header": {},
|
||||
"metadata": {},
|
||||
"content": {
|
||||
"code": code,
|
||||
"silent": False,
|
||||
"store_history": True,
|
||||
"user_expressions": {},
|
||||
},
|
||||
"buffers": [],
|
||||
"channel": "shell",
|
||||
}
|
||||
ws.send(json.dumps(msg))
|
||||
|
||||
result = {"stdout": "", "stderr": "", "output": None, "error": None}
|
||||
|
||||
while True:
|
||||
resp = json.loads(ws.recv())
|
||||
|
||||
# Filter out messages from other executions by matching msg_id
|
||||
parent_id = resp.get("parent_header", {}).get("msg_id")
|
||||
if parent_id != msg_id:
|
||||
continue
|
||||
|
||||
msg_type = resp.get("msg_type", "")
|
||||
|
||||
if msg_type == "stream":
|
||||
result["stdout"] += resp["content"]["text"]
|
||||
elif msg_type == "error":
|
||||
result["error"] = "\n".join(resp["content"].get("traceback", []))
|
||||
elif msg_type == "execute_result":
|
||||
result["output"] = resp["content"]["data"]
|
||||
elif msg_type == "status":
|
||||
if resp["content"]["execution_state"] == "idle":
|
||||
break
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Test Jupyter kernel state management in a sandbox"
|
||||
)
|
||||
parser.add_argument(
|
||||
"sandbox_id",
|
||||
help="Sandbox ID (e.g. cl-8nxizn9ygtczplsnn9jve38be)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--domain",
|
||||
default="localhost:8080",
|
||||
help="Proxy domain (default: localhost:8080)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--port",
|
||||
default="8888",
|
||||
help="Jupyter port inside the sandbox (default: 8888)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--key",
|
||||
default="",
|
||||
help="Wrenn API Token",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
base_url = f"http://{args.port}-{args.sandbox_id}.{args.domain}"
|
||||
ws_base = base_url.replace("http", "ws", 1)
|
||||
|
||||
print(f"Testing Jupyter kernel at {base_url}")
|
||||
print()
|
||||
|
||||
kernel_id = create_kernel(base_url, args.key)
|
||||
|
||||
ws_url = f"{ws_base}/api/kernels/{kernel_id}/channels"
|
||||
|
||||
# Pass auth headers to the WebSocket if a token was provided
|
||||
ws_headers = {}
|
||||
if args.key:
|
||||
ws_headers["X-API-Key"] = args.key
|
||||
|
||||
ws = websocket.create_connection(ws_url, header=ws_headers)
|
||||
print("Connected to kernel WebSocket")
|
||||
print()
|
||||
|
||||
tests = [
|
||||
("variable assignment", "x = 42", None),
|
||||
("read variable", "x * 2", "84"),
|
||||
("import", "import math", None),
|
||||
("use import", "math.sqrt(144)", "12.0"),
|
||||
("function definition", "def greet(name): return f'hello {name}'", None),
|
||||
# Fixed: Jupyter 'execute_result' strings include the literal single quotes
|
||||
("call function", "greet('sandbox')", "'hello sandbox'"),
|
||||
("list mutation", "items = [1, 2, 3]; items.append(4); items", "[1, 2, 3, 4]"),
|
||||
]
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
for name, code, expected in tests:
|
||||
print(f" {name}: {code}")
|
||||
result = execute_code(ws, code)
|
||||
|
||||
if result["error"]:
|
||||
print(f" ERROR: {result['error']}")
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
output = result["stdout"].strip()
|
||||
if not output and result["output"]:
|
||||
if "text/plain" in result["output"]:
|
||||
output = result["output"]["text/plain"].strip()
|
||||
|
||||
if expected is not None:
|
||||
if output == expected:
|
||||
print(f" PASS (got: {output})")
|
||||
passed += 1
|
||||
else:
|
||||
print(f" FAIL (expected: {expected}, got: {output})")
|
||||
failed += 1
|
||||
else:
|
||||
print(" OK")
|
||||
passed += 1
|
||||
|
||||
ws.close()
|
||||
print()
|
||||
print(f"Results: {passed} passed, {failed} failed")
|
||||
sys.exit(1 if failed else 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user