mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
Add Daytona execution environment and replace --docker with --execution-env
Replace the `--docker` boolean flag with `--execution-env <local|docker|daytona>` to support three execution environments. The new `DaytonaExecutionEnvironment` uses the Daytona cloud sandbox SDK to run agent tools remotely, auto-cloning the current git repo into the sandbox via `gh auth token` credentials. Setup commands from TOML task configs now run inside the execution environment (via `exec_command` after `initialize()`) rather than locally, so they work correctly for both Docker and Daytona sandboxes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Entire-Checkpoint: 306da8ff6de1
This commit is contained in:
parent
93ab7b8dbd
commit
8d593ff68e
12 changed files with 1041 additions and 41 deletions
|
|
@ -1,3 +1,4 @@
|
|||
ANTHROPIC_API_KEY=
|
||||
OPENAI_API_KEY=
|
||||
GEMINI_API_KEY=
|
||||
export ANTHROPIC_API_KEY=
|
||||
export OPENAI_API_KEY=
|
||||
export GEMINI_API_KEY=
|
||||
export DAYTONA_API_KEY=
|
||||
336
Cargo.lock
generated
336
Cargo.lock
generated
|
|
@ -198,10 +198,12 @@ dependencies = [
|
|||
"axum",
|
||||
"chrono",
|
||||
"clap",
|
||||
"daytona-sdk",
|
||||
"dialoguer",
|
||||
"dirs",
|
||||
"dotenvy",
|
||||
"futures",
|
||||
"git2",
|
||||
"http-body-util",
|
||||
"llm",
|
||||
"nom",
|
||||
|
|
@ -215,6 +217,7 @@ dependencies = [
|
|||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"toml",
|
||||
"tower",
|
||||
"uuid",
|
||||
|
|
@ -549,6 +552,16 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.10.1"
|
||||
|
|
@ -590,12 +603,93 @@ dependencies = [
|
|||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
||||
|
||||
[[package]]
|
||||
name = "daytona-api-client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/brynary/daytona-sdk-rust#572e584d06ccc0c6f110679eacc2d3f8d45c03c6"
|
||||
dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"reqwest-middleware",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"serde_with",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "daytona-sdk"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/brynary/daytona-sdk-rust#572e584d06ccc0c6f110679eacc2d3f8d45c03c6"
|
||||
dependencies = [
|
||||
"daytona-api-client",
|
||||
"daytona-toolbox-client",
|
||||
"futures-util",
|
||||
"reqwest 0.12.28",
|
||||
"reqwest-middleware",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "daytona-toolbox-client"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/brynary/daytona-sdk-rust#572e584d06ccc0c6f110679eacc2d3f8d45c03c6"
|
||||
dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"reqwest-middleware",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_repr",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
|
|
@ -699,6 +793,15 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
|
|
@ -808,6 +911,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.2"
|
||||
|
|
@ -1220,7 +1338,23 @@ dependencies = [
|
|||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tower-service",
|
||||
"webpki-roots",
|
||||
"webpki-roots 1.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hyper-tls"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-util",
|
||||
"native-tls",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1241,9 +1375,11 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"system-configuration",
|
||||
"tokio",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"windows-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1372,6 +1508,12 @@ version = "2.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
|
|
@ -1662,6 +1804,16 @@ version = "0.3.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
|
|
@ -1679,6 +1831,23 @@ dependencies = [
|
|||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "native-tls"
|
||||
version = "0.2.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"openssl",
|
||||
"openssl-probe 0.2.1",
|
||||
"openssl-sys",
|
||||
"schannel",
|
||||
"security-framework",
|
||||
"security-framework-sys",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
|
|
@ -1792,6 +1961,32 @@ version = "1.70.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.75"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"foreign-types",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"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",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
version = "0.1.6"
|
||||
|
|
@ -2210,16 +2405,22 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
|||
dependencies = [
|
||||
"base64",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"http-body-util",
|
||||
"hyper",
|
||||
"hyper-rustls",
|
||||
"hyper-tls",
|
||||
"hyper-util",
|
||||
"js-sys",
|
||||
"log",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"native-tls",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"quinn",
|
||||
|
|
@ -2230,6 +2431,7 @@ dependencies = [
|
|||
"serde_urlencoded",
|
||||
"sync_wrapper",
|
||||
"tokio",
|
||||
"tokio-native-tls",
|
||||
"tokio-rustls",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
|
|
@ -2240,7 +2442,7 @@ dependencies = [
|
|||
"wasm-bindgen-futures",
|
||||
"wasm-streams",
|
||||
"web-sys",
|
||||
"webpki-roots",
|
||||
"webpki-roots 1.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -2282,6 +2484,21 @@ dependencies = [
|
|||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "reqwest-middleware"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57f17d28a6e6acfe1733fe24bcd30774d13bffa4b8a22535b4c8c98423088d4e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"http",
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"thiserror 1.0.69",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
version = "0.17.14"
|
||||
|
|
@ -2358,7 +2575,7 @@ version = "0.6.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
|
||||
dependencies = [
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"jni",
|
||||
"log",
|
||||
|
|
@ -2458,7 +2675,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation",
|
||||
"core-foundation 0.10.1",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"security-framework-sys",
|
||||
|
|
@ -2591,9 +2808,22 @@ dependencies = [
|
|||
"schemars 1.2.1",
|
||||
"serde_core",
|
||||
"serde_json",
|
||||
"serde_with_macros",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_with_macros"
|
||||
version = "3.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
|
|
@ -2710,6 +2940,27 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"core-foundation 0.9.4",
|
||||
"system-configuration-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration-sys"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tabwriter"
|
||||
version = "1.4.1"
|
||||
|
|
@ -2877,6 +3128,16 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-native-tls"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-rustls"
|
||||
version = "0.26.4"
|
||||
|
|
@ -2899,6 +3160,22 @@ dependencies = [
|
|||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.26.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"tokio",
|
||||
"tokio-rustls",
|
||||
"tungstenite",
|
||||
"webpki-roots 0.26.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
|
|
@ -3037,12 +3314,37 @@ version = "0.2.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.26.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
"rand 0.9.2",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"sha1",
|
||||
"thiserror 2.0.18",
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-general-category"
|
||||
version = "1.1.0"
|
||||
|
|
@ -3085,6 +3387,12 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utf-8"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
|
|
@ -3323,6 +3631,15 @@ dependencies = [
|
|||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
|
||||
dependencies = [
|
||||
"webpki-roots 1.0.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.6"
|
||||
|
|
@ -3404,6 +3721,17 @@ version = "0.2.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
|
|
|
|||
|
|
@ -34,3 +34,4 @@ walkdir = "2"
|
|||
regex = "1"
|
||||
aho-corasick = "1"
|
||||
toml = "0.8"
|
||||
daytona-sdk = { git = "https://github.com/brynary/daytona-sdk-rust", package = "daytona-sdk" }
|
||||
|
|
|
|||
|
|
@ -41,6 +41,9 @@ nom = "7"
|
|||
toml.workspace = true
|
||||
dirs = "6"
|
||||
dialoguer.workspace = true
|
||||
daytona-sdk.workspace = true
|
||||
git2.workspace = true
|
||||
tokio-util.workspace = true
|
||||
axum = { version = "0.8", optional = true }
|
||||
tower = { version = "0.5", optional = true }
|
||||
tokio-stream = { workspace = true, optional = true, features = ["sync"] }
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use agent::{
|
|||
use llm::client::Client;
|
||||
use terminal::Styles;
|
||||
|
||||
use crate::cli::ExecutionEnvKind;
|
||||
use crate::context::Context;
|
||||
use crate::error::AttractorError;
|
||||
use crate::graph::Node;
|
||||
|
|
@ -24,24 +25,27 @@ pub struct AgentBackend {
|
|||
provider: Option<String>,
|
||||
verbose: u8,
|
||||
styles: &'static Styles,
|
||||
docker: bool,
|
||||
execution_env: ExecutionEnvKind,
|
||||
setup_commands: Vec<String>,
|
||||
}
|
||||
|
||||
impl AgentBackend {
|
||||
#[must_use]
|
||||
pub const fn new(
|
||||
pub fn new(
|
||||
model: String,
|
||||
provider: Option<String>,
|
||||
verbose: u8,
|
||||
styles: &'static Styles,
|
||||
docker: bool,
|
||||
execution_env: ExecutionEnvKind,
|
||||
setup_commands: Vec<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
model,
|
||||
provider,
|
||||
verbose,
|
||||
styles,
|
||||
docker,
|
||||
execution_env,
|
||||
setup_commands,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -146,17 +150,27 @@ impl CodergenBackend for AgentBackend {
|
|||
let profile = self.build_profile();
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||
|
||||
let exec_env: Arc<dyn ExecutionEnvironment> = if self.docker {
|
||||
let config = DockerConfig {
|
||||
host_working_directory: cwd.to_string_lossy().to_string(),
|
||||
..DockerConfig::default()
|
||||
};
|
||||
Arc::new(
|
||||
DockerExecutionEnvironment::new(config)
|
||||
.map_err(|e| AttractorError::Handler(format!("Failed to create Docker environment: {e}")))?,
|
||||
)
|
||||
} else {
|
||||
Arc::new(LocalExecutionEnvironment::new(cwd))
|
||||
let exec_env: Arc<dyn ExecutionEnvironment> = match self.execution_env {
|
||||
ExecutionEnvKind::Docker => {
|
||||
let config = DockerConfig {
|
||||
host_working_directory: cwd.to_string_lossy().to_string(),
|
||||
..DockerConfig::default()
|
||||
};
|
||||
Arc::new(
|
||||
DockerExecutionEnvironment::new(config)
|
||||
.map_err(|e| AttractorError::Handler(format!("Failed to create Docker environment: {e}")))?,
|
||||
)
|
||||
}
|
||||
ExecutionEnvKind::Daytona => {
|
||||
let daytona_client = daytona_sdk::Client::new()
|
||||
.await
|
||||
.map_err(|e| AttractorError::Handler(format!("Failed to create Daytona client: {e}")))?;
|
||||
Arc::new(crate::daytona_env::DaytonaExecutionEnvironment::new(
|
||||
daytona_client,
|
||||
crate::daytona_env::DaytonaConfig::default(),
|
||||
))
|
||||
}
|
||||
ExecutionEnvKind::Local => Arc::new(LocalExecutionEnvironment::new(cwd)),
|
||||
};
|
||||
|
||||
let config = SessionConfig {
|
||||
|
|
@ -164,6 +178,7 @@ impl CodergenBackend for AgentBackend {
|
|||
..SessionConfig::default()
|
||||
};
|
||||
|
||||
let exec_env_for_setup = Arc::clone(&exec_env);
|
||||
let mut session = Session::new(client, profile, exec_env, config);
|
||||
|
||||
// File change tracking: shared between spawned task and main fn.
|
||||
|
|
@ -284,6 +299,22 @@ impl CodergenBackend for AgentBackend {
|
|||
});
|
||||
|
||||
session.initialize().await;
|
||||
|
||||
// Run setup commands inside the execution environment
|
||||
for cmd in &self.setup_commands {
|
||||
let result = exec_env_for_setup
|
||||
.exec_command(cmd, 300_000, None, None, None)
|
||||
.await
|
||||
.map_err(|e| AttractorError::Handler(format!("Setup command failed: {e}")))?;
|
||||
if result.exit_code != 0 {
|
||||
return Err(AttractorError::Handler(format!(
|
||||
"Setup command failed (exit code {}): {cmd}\n{}",
|
||||
result.exit_code,
|
||||
result.stderr,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
session.process_input(prompt).await.map_err(|e| {
|
||||
AttractorError::Handler(format!("Agent session failed: {e}"))
|
||||
})?;
|
||||
|
|
@ -385,3 +416,23 @@ fn format_tool_args(args: &serde_json::Value) -> String {
|
|||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn agent_backend_stores_execution_env_kind() {
|
||||
let styles = Box::leak(Box::new(Styles::new(false)));
|
||||
let backend = AgentBackend::new(
|
||||
"claude-opus-4-6".to_string(),
|
||||
None,
|
||||
0,
|
||||
styles,
|
||||
ExecutionEnvKind::Daytona,
|
||||
vec!["npm install".to_string()],
|
||||
);
|
||||
assert_eq!(backend.execution_env, ExecutionEnvKind::Daytona);
|
||||
assert_eq!(backend.setup_commands, vec!["npm install".to_string()]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,10 @@ pub mod validate;
|
|||
|
||||
use std::path::Path;
|
||||
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use terminal::Styles;
|
||||
|
||||
use agent::AgentEvent;
|
||||
|
|
@ -16,6 +18,41 @@ use crate::event::PipelineEvent;
|
|||
use crate::outcome::StageUsage;
|
||||
use crate::validation::{Diagnostic, Severity};
|
||||
|
||||
/// Execution environment for agent tool operations.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, ValueEnum)]
|
||||
pub enum ExecutionEnvKind {
|
||||
/// Run tools on the local host (default)
|
||||
#[default]
|
||||
Local,
|
||||
/// Run tools inside a Docker container
|
||||
Docker,
|
||||
/// Run tools inside a Daytona cloud sandbox
|
||||
Daytona,
|
||||
}
|
||||
|
||||
impl fmt::Display for ExecutionEnvKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Local => write!(f, "local"),
|
||||
Self::Docker => write!(f, "docker"),
|
||||
Self::Daytona => write!(f, "daytona"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ExecutionEnvKind {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
"local" => Ok(Self::Local),
|
||||
"docker" => Ok(Self::Docker),
|
||||
"daytona" => Ok(Self::Daytona),
|
||||
other => Err(format!("unknown execution environment: {other}")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "attractor", version, about = "DOT-based pipeline runner for AI workflows")]
|
||||
pub struct Cli {
|
||||
|
|
@ -67,9 +104,9 @@ pub struct RunArgs {
|
|||
#[arg(short, long, action = clap::ArgAction::Count)]
|
||||
pub verbose: u8,
|
||||
|
||||
/// Run agent tools inside a Docker container
|
||||
#[arg(long)]
|
||||
pub docker: bool,
|
||||
/// Execution environment for agent tools
|
||||
#[arg(long, value_enum, default_value_t)]
|
||||
pub execution_env: ExecutionEnvKind,
|
||||
}
|
||||
|
||||
#[derive(Args)]
|
||||
|
|
@ -101,9 +138,9 @@ pub struct ServeArgs {
|
|||
#[arg(long)]
|
||||
pub dry_run: bool,
|
||||
|
||||
/// Run agent tools inside a Docker container
|
||||
#[arg(long)]
|
||||
pub docker: bool,
|
||||
/// Execution environment for agent tools
|
||||
#[arg(long, value_enum, default_value_t)]
|
||||
pub execution_env: ExecutionEnvKind,
|
||||
}
|
||||
|
||||
/// Read a .dot file from disk.
|
||||
|
|
@ -669,3 +706,29 @@ pub fn format_tokens_human(tokens: i64) -> String {
|
|||
tokens.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn execution_env_kind_default_is_local() {
|
||||
assert_eq!(ExecutionEnvKind::default(), ExecutionEnvKind::Local);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execution_env_kind_from_str() {
|
||||
assert_eq!("local".parse::<ExecutionEnvKind>().unwrap(), ExecutionEnvKind::Local);
|
||||
assert_eq!("docker".parse::<ExecutionEnvKind>().unwrap(), ExecutionEnvKind::Docker);
|
||||
assert_eq!("daytona".parse::<ExecutionEnvKind>().unwrap(), ExecutionEnvKind::Daytona);
|
||||
assert_eq!("LOCAL".parse::<ExecutionEnvKind>().unwrap(), ExecutionEnvKind::Local);
|
||||
assert!("invalid".parse::<ExecutionEnvKind>().is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn execution_env_kind_display() {
|
||||
assert_eq!(ExecutionEnvKind::Local.to_string(), "local");
|
||||
assert_eq!(ExecutionEnvKind::Docker.to_string(), "docker");
|
||||
assert_eq!(ExecutionEnvKind::Daytona.to_string(), "daytona");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,17 +52,15 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu
|
|||
std::env::set_current_dir(dir)
|
||||
.map_err(|e| anyhow::anyhow!("Failed to set working directory to {dir}: {e}"))?;
|
||||
}
|
||||
if let Some(ref setup) = cfg.setup {
|
||||
let cwd = std::env::current_dir()?;
|
||||
eprintln!(
|
||||
"{dim}Running {} setup command(s)…{reset}",
|
||||
setup.commands.len(),
|
||||
dim = styles.dim, reset = styles.reset,
|
||||
);
|
||||
task_config::run_setup(setup, &cwd).await?;
|
||||
}
|
||||
}
|
||||
|
||||
// Collect setup commands — they'll be run inside the execution environment
|
||||
let setup_commands: Vec<String> = task_cfg
|
||||
.as_ref()
|
||||
.and_then(|c| c.setup.as_ref())
|
||||
.map(|s| s.commands.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
// 1. Parse and validate pipeline
|
||||
let source = read_dot_file(&dot_path)?;
|
||||
let source = match task_cfg.as_ref().and_then(|c| c.vars.as_ref()) {
|
||||
|
|
@ -296,7 +294,8 @@ pub async fn run_command(args: RunArgs, styles: &'static Styles) -> anyhow::Resu
|
|||
provider.clone(),
|
||||
args.verbose,
|
||||
styles,
|
||||
args.docker,
|
||||
args.execution_env,
|
||||
setup_commands.clone(),
|
||||
)))
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -66,7 +66,8 @@ pub async fn serve_command(args: ServeArgs, styles: &'static Styles) -> anyhow::
|
|||
provider.clone(),
|
||||
0,
|
||||
styles,
|
||||
args.docker,
|
||||
args.execution_env,
|
||||
Vec::new(),
|
||||
)))
|
||||
}
|
||||
})
|
||||
|
|
|
|||
468
crates/attractor/src/daytona_env.rs
Normal file
468
crates/attractor/src/daytona_env.rs
Normal file
|
|
@ -0,0 +1,468 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::time::Instant;
|
||||
|
||||
use agent::execution_env::{format_lines_numbered, DirEntry, ExecResult, ExecutionEnvironment, GrepOptions};
|
||||
use async_trait::async_trait;
|
||||
|
||||
/// Configuration for a Daytona cloud sandbox execution environment.
|
||||
pub struct DaytonaConfig {
|
||||
/// Docker image to use for the sandbox.
|
||||
pub image: String,
|
||||
/// Working directory inside the sandbox.
|
||||
pub working_directory: String,
|
||||
}
|
||||
|
||||
impl Default for DaytonaConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
image: "ubuntu:22.04".to_string(),
|
||||
working_directory: "/home/daytona/workspace".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Execution environment that runs all operations inside a Daytona cloud sandbox.
|
||||
pub struct DaytonaExecutionEnvironment {
|
||||
config: DaytonaConfig,
|
||||
client: daytona_sdk::Client,
|
||||
sandbox: tokio::sync::OnceCell<daytona_sdk::Sandbox>,
|
||||
}
|
||||
|
||||
impl DaytonaExecutionEnvironment {
|
||||
#[must_use]
|
||||
pub fn new(client: daytona_sdk::Client, config: DaytonaConfig) -> Self {
|
||||
Self {
|
||||
config,
|
||||
client,
|
||||
sandbox: tokio::sync::OnceCell::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a path: relative paths are prepended with the working directory.
|
||||
fn resolve_path(&self, path: &str) -> String {
|
||||
if Path::new(path).is_absolute() {
|
||||
path.to_string()
|
||||
} else {
|
||||
format!("{}/{}", self.config.working_directory, path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the sandbox, returning an error if not yet initialized.
|
||||
fn sandbox(&self) -> Result<&daytona_sdk::Sandbox, String> {
|
||||
self.sandbox
|
||||
.get()
|
||||
.ok_or_else(|| "Daytona sandbox not initialized — call initialize() first".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect the git remote URL and current branch from a local repository.
|
||||
///
|
||||
/// Uses `git2` to discover the repo at `path`, reads the `origin` remote URL
|
||||
/// and the HEAD branch name.
|
||||
pub fn detect_repo_info(path: &Path) -> Result<(String, Option<String>), String> {
|
||||
let repo = git2::Repository::discover(path)
|
||||
.map_err(|e| format!("Failed to discover git repo at {}: {e}", path.display()))?;
|
||||
|
||||
let url = repo
|
||||
.find_remote("origin")
|
||||
.map_err(|e| format!("Failed to find 'origin' remote: {e}"))?
|
||||
.url()
|
||||
.ok_or_else(|| "origin remote URL is not valid UTF-8".to_string())?
|
||||
.to_string();
|
||||
|
||||
let branch = repo
|
||||
.head()
|
||||
.ok()
|
||||
.and_then(|head| head.shorthand().map(String::from));
|
||||
|
||||
Ok((url, branch))
|
||||
}
|
||||
|
||||
/// Get a GitHub authentication token via `gh auth token`.
|
||||
pub fn get_gh_token() -> Result<String, String> {
|
||||
let output = std::process::Command::new("gh")
|
||||
.args(["auth", "token"])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run 'gh auth token': {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!(
|
||||
"gh auth token failed (exit code {}): {stderr}",
|
||||
output.status.code().unwrap_or(-1)
|
||||
));
|
||||
}
|
||||
|
||||
let token = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
if token.is_empty() {
|
||||
return Err("gh auth token returned empty string".to_string());
|
||||
}
|
||||
Ok(token)
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ExecutionEnvironment for DaytonaExecutionEnvironment {
|
||||
async fn initialize(&self) -> Result<(), String> {
|
||||
let cwd = std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."));
|
||||
|
||||
let params = daytona_sdk::CreateParams::Image(daytona_sdk::ImageParams {
|
||||
base: daytona_sdk::SandboxBaseParams {
|
||||
ephemeral: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
image: daytona_sdk::ImageSource::Name(self.config.image.clone()),
|
||||
resources: None,
|
||||
});
|
||||
|
||||
let sandbox = self
|
||||
.client
|
||||
.create(params, daytona_sdk::CreateSandboxOptions::default())
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create Daytona sandbox: {e}"))?;
|
||||
|
||||
// Clone the repo into the sandbox
|
||||
match detect_repo_info(&cwd) {
|
||||
Ok((url, branch)) => {
|
||||
let token = get_gh_token()
|
||||
.map_err(|e| format!("Failed to get GitHub token for Daytona clone: {e}"))?;
|
||||
|
||||
let git_svc = sandbox
|
||||
.git()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get Daytona git service: {e}"))?;
|
||||
|
||||
git_svc
|
||||
.clone(
|
||||
&url,
|
||||
&self.config.working_directory,
|
||||
daytona_sdk::GitCloneOptions {
|
||||
branch,
|
||||
username: Some("x-access-token".to_string()),
|
||||
password: Some(token),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to clone repo into Daytona sandbox: {e}"))?;
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Warning: could not detect git repo for Daytona clone: {e}");
|
||||
// Create working directory even without a repo
|
||||
let fs_svc = sandbox
|
||||
.fs()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get Daytona fs service: {e}"))?;
|
||||
fs_svc
|
||||
.create_folder(&self.config.working_directory, None)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to create working directory: {e}"))?;
|
||||
}
|
||||
}
|
||||
|
||||
self.sandbox
|
||||
.set(sandbox)
|
||||
.map_err(|_| "Daytona sandbox already initialized".to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cleanup(&self) -> Result<(), String> {
|
||||
if let Some(sandbox) = self.sandbox.get() {
|
||||
sandbox
|
||||
.delete()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to delete Daytona sandbox: {e}"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn working_directory(&self) -> &str {
|
||||
&self.config.working_directory
|
||||
}
|
||||
|
||||
fn platform(&self) -> &str {
|
||||
"linux"
|
||||
}
|
||||
|
||||
fn os_version(&self) -> String {
|
||||
"Linux (Daytona)".to_string()
|
||||
}
|
||||
|
||||
async fn read_file(
|
||||
&self,
|
||||
path: &str,
|
||||
offset: Option<usize>,
|
||||
limit: Option<usize>,
|
||||
) -> Result<String, String> {
|
||||
let sandbox = self.sandbox()?;
|
||||
let resolved = self.resolve_path(path);
|
||||
|
||||
let fs_svc = sandbox
|
||||
.fs()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get fs service: {e}"))?;
|
||||
|
||||
let bytes = fs_svc
|
||||
.download_file(&resolved)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to read file {resolved}: {e}"))?;
|
||||
|
||||
let content =
|
||||
String::from_utf8(bytes).map_err(|e| format!("File is not valid UTF-8: {e}"))?;
|
||||
|
||||
Ok(format_lines_numbered(&content, offset, limit))
|
||||
}
|
||||
|
||||
async fn write_file(&self, path: &str, content: &str) -> Result<(), String> {
|
||||
let sandbox = self.sandbox()?;
|
||||
let resolved = self.resolve_path(path);
|
||||
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = Path::new(&resolved).parent() {
|
||||
let parent_str = parent.to_string_lossy();
|
||||
if parent_str != "/" {
|
||||
let fs_svc = sandbox
|
||||
.fs()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get fs service: {e}"))?;
|
||||
let _ = fs_svc.create_folder(&parent_str, None).await;
|
||||
}
|
||||
}
|
||||
|
||||
let fs_svc = sandbox
|
||||
.fs()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get fs service: {e}"))?;
|
||||
|
||||
fs_svc
|
||||
.upload_file_bytes(&resolved, content.as_bytes())
|
||||
.await
|
||||
.map_err(|e| format!("Failed to write file {resolved}: {e}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_file(&self, path: &str) -> Result<(), String> {
|
||||
let sandbox = self.sandbox()?;
|
||||
let resolved = self.resolve_path(path);
|
||||
|
||||
let fs_svc = sandbox
|
||||
.fs()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get fs service: {e}"))?;
|
||||
|
||||
fs_svc
|
||||
.delete_file(&resolved, false)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to delete file {resolved}: {e}"))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn file_exists(&self, path: &str) -> Result<bool, String> {
|
||||
let sandbox = self.sandbox()?;
|
||||
let resolved = self.resolve_path(path);
|
||||
|
||||
let fs_svc = sandbox
|
||||
.fs()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get fs service: {e}"))?;
|
||||
|
||||
match fs_svc.get_file_info(&resolved).await {
|
||||
Ok(_) => Ok(true),
|
||||
Err(daytona_sdk::DaytonaError::NotFound { .. }) => Ok(false),
|
||||
Err(e) => Err(format!("Failed to check file existence {resolved}: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_directory(
|
||||
&self,
|
||||
path: &str,
|
||||
_depth: Option<usize>,
|
||||
) -> Result<Vec<DirEntry>, String> {
|
||||
let sandbox = self.sandbox()?;
|
||||
let resolved = self.resolve_path(path);
|
||||
|
||||
let fs_svc = sandbox
|
||||
.fs()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get fs service: {e}"))?;
|
||||
|
||||
let files = fs_svc
|
||||
.list_files(&resolved)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to list directory {resolved}: {e}"))?;
|
||||
|
||||
Ok(files
|
||||
.into_iter()
|
||||
.map(|f| DirEntry {
|
||||
name: f.name,
|
||||
is_dir: f.is_dir,
|
||||
size: if f.size > 0 {
|
||||
Some(f.size as u64)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn exec_command(
|
||||
&self,
|
||||
command: &str,
|
||||
timeout_ms: u64,
|
||||
working_dir: Option<&str>,
|
||||
_env_vars: Option<&HashMap<String, String>>,
|
||||
_cancel_token: Option<tokio_util::sync::CancellationToken>,
|
||||
) -> Result<ExecResult, String> {
|
||||
let sandbox = self.sandbox()?;
|
||||
let start = Instant::now();
|
||||
|
||||
let cwd = working_dir
|
||||
.map(|d| self.resolve_path(d))
|
||||
.unwrap_or_else(|| self.config.working_directory.clone());
|
||||
|
||||
let process_svc = sandbox
|
||||
.process()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get process service: {e}"))?;
|
||||
|
||||
let options = daytona_sdk::ExecuteCommandOptions {
|
||||
cwd: Some(cwd),
|
||||
timeout: Some(std::time::Duration::from_millis(timeout_ms)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let result = process_svc
|
||||
.execute_command(command, options)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to execute command: {e}"))?;
|
||||
|
||||
let duration_ms = start.elapsed().as_millis() as u64;
|
||||
|
||||
// The Daytona SDK returns combined output in `result` field.
|
||||
// Separate stderr isn't available in the simple execute_command API.
|
||||
Ok(ExecResult {
|
||||
stdout: result.result.clone(),
|
||||
stderr: String::new(),
|
||||
exit_code: result.exit_code,
|
||||
timed_out: false,
|
||||
duration_ms,
|
||||
})
|
||||
}
|
||||
|
||||
async fn grep(
|
||||
&self,
|
||||
pattern: &str,
|
||||
path: &str,
|
||||
options: &GrepOptions,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let resolved = self.resolve_path(path);
|
||||
|
||||
// Build rg command (same approach as Docker env)
|
||||
let mut cmd = "rg --line-number --no-heading".to_string();
|
||||
if options.case_insensitive {
|
||||
cmd.push_str(" -i");
|
||||
}
|
||||
if let Some(ref glob_filter) = options.glob_filter {
|
||||
cmd.push_str(&format!(" --glob '{glob_filter}'"));
|
||||
}
|
||||
if let Some(max) = options.max_results {
|
||||
cmd.push_str(&format!(" --max-count {max}"));
|
||||
}
|
||||
cmd.push_str(&format!(" -- '{}' '{}'", pattern.replace('\'', "'\\''"), resolved));
|
||||
|
||||
let result = self.exec_command(&cmd, 30_000, None, None, None).await?;
|
||||
|
||||
if result.exit_code == 1 {
|
||||
// rg exits 1 for no matches
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if result.exit_code != 0 {
|
||||
return Err(format!("grep failed (exit {}): {}", result.exit_code, result.stderr));
|
||||
}
|
||||
|
||||
Ok(result
|
||||
.stdout
|
||||
.lines()
|
||||
.map(String::from)
|
||||
.collect())
|
||||
}
|
||||
|
||||
async fn glob(&self, pattern: &str, path: Option<&str>) -> Result<Vec<String>, String> {
|
||||
let base = path
|
||||
.map(|p| self.resolve_path(p))
|
||||
.unwrap_or_else(|| self.config.working_directory.clone());
|
||||
|
||||
let cmd = format!(
|
||||
"find '{}' -name '{}' -type f | sort",
|
||||
base.replace('\'', "'\\''"),
|
||||
pattern.replace('\'', "'\\''"),
|
||||
);
|
||||
|
||||
let result = self.exec_command(&cmd, 30_000, None, None, None).await?;
|
||||
|
||||
if result.exit_code != 0 {
|
||||
return Err(format!("glob failed (exit {}): {}", result.exit_code, result.stderr));
|
||||
}
|
||||
|
||||
Ok(result
|
||||
.stdout
|
||||
.lines()
|
||||
.filter(|l| !l.is_empty())
|
||||
.map(String::from)
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn daytona_config_defaults() {
|
||||
let config = DaytonaConfig::default();
|
||||
assert_eq!(config.image, "ubuntu:22.04");
|
||||
assert_eq!(config.working_directory, "/home/daytona/workspace");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_git_remote_from_repo() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let repo = git2::Repository::init(dir.path()).unwrap();
|
||||
|
||||
repo.remote("origin", "https://github.com/org/repo.git")
|
||||
.unwrap();
|
||||
|
||||
let (url, _branch) = detect_repo_info(dir.path()).unwrap();
|
||||
assert_eq!(url, "https://github.com/org/repo.git");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_git_branch_from_repo() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let repo = git2::Repository::init(dir.path()).unwrap();
|
||||
|
||||
// Create an initial commit so HEAD points to a branch
|
||||
let sig = git2::Signature::now("Test", "test@test.com").unwrap();
|
||||
let tree_id = repo.index().unwrap().write_tree().unwrap();
|
||||
let tree = repo.find_tree(tree_id).unwrap();
|
||||
repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[])
|
||||
.unwrap();
|
||||
|
||||
repo.remote("origin", "https://github.com/org/repo.git")
|
||||
.unwrap();
|
||||
|
||||
let (_, branch) = detect_repo_info(dir.path()).unwrap();
|
||||
// git init creates "master" or "main" depending on git config
|
||||
assert!(branch.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // requires `gh` CLI installed and authenticated
|
||||
fn gh_auth_token_returns_nonempty_string() {
|
||||
let token = get_gh_token().unwrap();
|
||||
assert!(!token.is_empty());
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ pub mod artifact;
|
|||
pub mod checkpoint;
|
||||
pub mod cli;
|
||||
pub mod condition;
|
||||
pub mod daytona_env;
|
||||
pub mod context;
|
||||
pub mod engine;
|
||||
pub mod error;
|
||||
|
|
|
|||
83
crates/attractor/tests/daytona_integration.rs
Normal file
83
crates/attractor/tests/daytona_integration.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
//! Integration tests for `DaytonaExecutionEnvironment`.
|
||||
//!
|
||||
//! These tests require a `DAYTONA_API_KEY` environment variable and network access.
|
||||
//! Run with: `cargo test --package attractor -- --ignored daytona`
|
||||
|
||||
use agent::ExecutionEnvironment;
|
||||
use attractor::daytona_env::{DaytonaConfig, DaytonaExecutionEnvironment};
|
||||
|
||||
async fn create_env() -> DaytonaExecutionEnvironment {
|
||||
dotenvy::dotenv().ok();
|
||||
let client = daytona_sdk::Client::new()
|
||||
.await
|
||||
.expect("Failed to create Daytona client — is DAYTONA_API_KEY set?");
|
||||
DaytonaExecutionEnvironment::new(client, DaytonaConfig::default())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn daytona_exec_command() {
|
||||
let env = create_env().await;
|
||||
env.initialize().await.unwrap();
|
||||
|
||||
let result = env
|
||||
.exec_command("echo hello", 30_000, None, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(result.exit_code, 0);
|
||||
assert!(result.stdout.contains("hello"));
|
||||
|
||||
env.cleanup().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn daytona_file_round_trip() {
|
||||
let env = create_env().await;
|
||||
env.initialize().await.unwrap();
|
||||
|
||||
let test_path = "test_round_trip.txt";
|
||||
let content = "Hello from Daytona integration test!";
|
||||
|
||||
// Write
|
||||
env.write_file(test_path, content).await.unwrap();
|
||||
|
||||
// Exists
|
||||
assert!(env.file_exists(test_path).await.unwrap());
|
||||
|
||||
// Read
|
||||
let read_back = env.read_file(test_path, None, None).await.unwrap();
|
||||
assert!(read_back.contains(content));
|
||||
|
||||
// Delete
|
||||
env.delete_file(test_path).await.unwrap();
|
||||
assert!(!env.file_exists(test_path).await.unwrap());
|
||||
|
||||
env.cleanup().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn daytona_full_lifecycle() {
|
||||
let env = create_env().await;
|
||||
|
||||
// Initialize (creates sandbox + clones repo)
|
||||
env.initialize().await.unwrap();
|
||||
|
||||
// Verify platform
|
||||
assert_eq!(env.platform(), "linux");
|
||||
|
||||
// Verify working directory is accessible
|
||||
let result = env
|
||||
.exec_command("pwd", 10_000, None, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(result.exit_code, 0);
|
||||
|
||||
// List directory
|
||||
let entries = env.list_directory(".", None).await.unwrap();
|
||||
assert!(!entries.is_empty());
|
||||
|
||||
// Cleanup (deletes sandbox)
|
||||
env.cleanup().await.unwrap();
|
||||
}
|
||||
|
|
@ -6586,7 +6586,8 @@ async fn attractor_e2e_with_real_llm() {
|
|||
None,
|
||||
0,
|
||||
&TEST_STYLES,
|
||||
false,
|
||||
attractor::cli::ExecutionEnvKind::Local,
|
||||
Vec::new(),
|
||||
)) as Box<dyn attractor::handler::codergen::CodergenBackend>)
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue