diff --git a/.awman/workflows/da698d0c-Dependency Upgrade.json b/.awman/workflows/da698d0c-Dependency Upgrade.json new file mode 100644 index 00000000..1e1c35a0 --- /dev/null +++ b/.awman/workflows/da698d0c-Dependency Upgrade.json @@ -0,0 +1,60 @@ +{ + "schema_version": 3, + "workflow_name": "Dependency Upgrade", + "workflow_hash": "e4ac7af6d39a410073bc00121782a779c07278fec41e5411bba7ccbb3548102d", + "work_item": null, + "step_states": { + "security-audit": "Succeeded", + "version-audit": "Skipped" + }, + "completed_steps": [ + "version-audit", + "security-audit" + ], + "current_step_index": null, + "started_at": "2026-05-28T14:42:22.766833Z", + "updated_at": "2026-05-28T14:54:21.503168Z", + "steps": [ + { + "name": "security-audit", + "depends_on": [] + }, + { + "name": "version-audit", + "depends_on": [ + "security-audit" + ] + } + ], + "current_phase": "Teardown", + "setup_completed": true, + "teardown_completed": false, + "setup_step_states": [ + { + "description": "run_shell: git status", + "status": "Succeeded" + } + ], + "teardown_step_states": [ + { + "description": "run_shell: make test", + "status": { + "Failed": { + "error": "warning: function `awman` is never used\n --> tests/binary_smoke/antigravity_0083.rs:14:4\n |\n14 | fn awman() -> Command {\n | ^^^^^\n |\n = note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default\n\nawman: immediate\nawman: immediate again\nawman: msg\nawman: queued message\nawman warning: another\nawman: immediate\nerror: test failed, to rerun pass `--lib`\nmake: *** [Makefile:18: test] Error 101\n" + } + } + }, + { + "description": "commit_changes: Update all available dependencies", + "status": "Running" + }, + { + "description": "push_branch", + "status": "Pending" + }, + { + "description": "create_pull_request: Security and Dependency Upgrades", + "status": "Pending" + } + ] +} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index f13f96c9..b2fd4a06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -129,6 +129,45 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -199,7 +238,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml_ng", - "sha2", + "sha2 0.11.0", "shell-words", "strip-ansi-escapes", "strsim", @@ -327,7 +366,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" dependencies = [ - "bit-vec", + "bit-vec 0.6.3", ] [[package]] @@ -336,6 +375,15 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -357,6 +405,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -543,9 +600,9 @@ dependencies = [ [[package]] name = "compact_str" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" dependencies = [ "castaway", "cfg-if", @@ -555,6 +612,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "convert_case" version = "0.10.0" @@ -589,6 +652,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -702,6 +774,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "csscolorparser" version = "0.6.2" @@ -746,6 +827,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "data-encoding" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" + [[package]] name = "deadpool" version = "0.12.3" @@ -770,6 +857,20 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.5.8" @@ -807,8 +908,19 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "const-oid", + "crypto-common 0.2.2", ] [[package]] @@ -844,9 +956,9 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", @@ -1279,9 +1391,9 @@ checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "http" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" dependencies = [ "bytes", "itoa", @@ -1322,11 +1434,20 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +checksum = "eb92f162bf56536459fc83c79b974bb12837acfed43d6bc370a7916d0ae15ecc" dependencies = [ "atomic-waker", "bytes", @@ -1724,9 +1845,9 @@ checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libredox" -version = "0.1.16" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" dependencies = [ "libc", ] @@ -1780,9 +1901,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.29" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" [[package]] name = "lru" @@ -1826,9 +1947,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" [[package]] name = "memmem" @@ -1945,6 +2066,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -1962,6 +2093,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2063,6 +2203,15 @@ dependencies = [ "objc2-core-foundation", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -2181,7 +2330,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -2599,14 +2748,15 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.13.2" +version = "0.14.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" dependencies = [ "pem", "ring", "rustls-pki-types", "time", + "x509-parser", "yasna", ] @@ -2661,9 +2811,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" dependencies = [ "base64", "bytes", @@ -2753,6 +2903,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "1.1.4" @@ -3012,8 +3171,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -3130,9 +3300,9 @@ dependencies = [ [[package]] name = "sqlite-wasm-rs" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdd578e94101503d97e2b286bbf8db2135035ca24b2ce4cbf3f9e2fb2bbf1eee" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" dependencies = [ "cc", "js-sys", @@ -3306,7 +3476,7 @@ dependencies = [ "pest", "pest_derive", "phf", - "sha2", + "sha2 0.10.9", "signal-hook", "siphasher", "terminfo", @@ -3393,12 +3563,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", + "itoa", "libc", "num-conv", "num_threads", "powerfmt", "serde_core", "time-core", + "time-macros", ] [[package]] @@ -3407,6 +3579,16 @@ version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.3" @@ -4034,7 +4216,7 @@ checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" dependencies = [ "getrandom 0.3.4", "mac_address", - "sha2", + "sha2 0.10.9", "thiserror 1.0.69", "uuid", ] @@ -4489,6 +4671,24 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + [[package]] name = "xattr" version = "1.6.1" @@ -4501,10 +4701,11 @@ dependencies = [ [[package]] name = "yasna" -version = "0.5.2" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" dependencies = [ + "bit-vec 0.9.1", "time", ] @@ -4533,18 +4734,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.48" +version = "0.8.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.48" +version = "0.8.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index bf2b7b37..9129e990 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ tokio-stream = "0.1" unicode-width = "0.2" flate2 = "1" tar = "0.4" -sha2 = "0.10" +sha2 = "0.11" arboard = "3" shell-words = "1" toml = "1" @@ -52,7 +52,7 @@ uuid = { version = "1", features = ["v4", "serde"] } tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] } tower-http = { version = "0.6", features = ["trace"] } chrono = { version = "0.4", features = ["serde"] } -rcgen = "0.13" +rcgen = "0.14" ring = "0.17" subtle = "2" thiserror = "2" diff --git a/aspec/work-items/0084-skipped-dependency-upgrades.md b/aspec/work-items/0084-skipped-dependency-upgrades.md new file mode 100644 index 00000000..c22a7e73 --- /dev/null +++ b/aspec/work-items/0084-skipped-dependency-upgrades.md @@ -0,0 +1,43 @@ +# Work Item: Task + +Title: Skipped dependency upgrades — tracking note +Issue: n/a + +## Summary: +Two direct dependencies were evaluated for upgrade during the 2026-05-28 dependency maintenance pass but could not be updated without introducing instability or requiring nightly Rust. This note records the reason for each skip so future maintainers can revisit when the blockers are resolved. + +## Skipped Packages + +### rusqlite 0.39 → 0.40 + +**Reason:** `rusqlite 0.40.0` (and its transitive dependency `libsqlite3-sys`) requires the `cfg_select` feature, which is an unstable Rust library feature not yet available on the stable toolchain pinned in `rust-toolchain.toml` (`1.94.0`). Attempting to build fails with: + +``` +error[E0658]: use of unstable library feature `cfg_select` +error: could not compile `libsqlite3-sys` (build script) +``` + +**Resolution path:** Once `cfg_select` is stabilised in a future Rust release, or `libsqlite3-sys` is updated to avoid the unstable feature, re-attempt `rusqlite = { version = "0.40", features = ["bundled"] }` and run the test suite. + +### libc 0.2 → 1.0.0-alpha.3 + +**Reason:** The `1.0.0-alpha.3` release is a pre-release (alpha). Adopting an alpha release in a production binary carries undefined stability risk — the API surface could change in subsequent alpha/beta/rc releases before the 1.0 stable lands. + +**Resolution path:** Once `libc 1.0.0` stable is released, evaluate the migration guide for breaking API changes against the code in `src/` (primarily unix-specific signal/process handling), make required changes, and run the full test suite. + +## Implementation Details: +- No code changes required — these are deferred, not rejected. +- All other available minor/breaking-version upgrades (sha2 0.10→0.11, rcgen 0.13→0.14) were applied successfully on the same date. + +## Edge Case Considerations: +- n/a (no code changes) + +## Test Considerations: +- n/a (no code changes) + +## Codebase Integration: +- n/a (no code changes) + +## Documentation + +No user-visible behaviour changed; no doc updates required. diff --git a/src/command/commands/exec_workflow.rs b/src/command/commands/exec_workflow.rs index de5666de..a125995e 100644 --- a/src/command/commands/exec_workflow.rs +++ b/src/command/commands/exec_workflow.rs @@ -619,6 +619,17 @@ impl Command for ExecWorkflowCommand { mount_path }; + // 4c. When running in a worktree, compute an extra overlay that mounts + // the main repo's `.git` directory into setup/teardown containers. + // Without this, the worktree's `.git` pointer file references a host + // path that doesn't exist inside the container, breaking all git ops. + let worktree_git_mount: Option = + if worktree_path.is_some() { + worktree_git_overlay(&mount_path)? + } else { + None + }; + // 5. Parse CLI overlay specs early so errors surface before PTY is activated. let cli_typed = { let mut all = Vec::new(); @@ -850,33 +861,23 @@ impl Command for ExecWorkflowCommand { let mut setup_failed = false; if !setup_steps.is_empty() && !engine.state().setup_completed { let base_image = resolve_base_image(&session, &git_root_for_scope); + let resolved = resolve_phase_overlays( + &self.engines, + &session, + &cli_typed, + &setup_entry_overlays, + worktree_git_mount.as_ref(), + ); - // Pre-validate each entry's overlays so that a parse error in - // ANY entry aborts the phase before any container starts. - // Without this, a bad entry midway through would only surface - // after earlier steps had already mutated the workspace. - let entry_overlays = setup_entry_overlays.clone(); - let mut validated: Vec<( - Vec, - std::collections::HashMap, - )> = Vec::with_capacity(setup_steps.len()); - for entry in &entry_overlays { - match collect_single_entry_overlays( - &self.engines, - &session, - &cli_typed, - entry.as_deref(), - ) { - Ok(pair) => validated.push(pair), - Err(e) => { - shared.lock().unwrap().write_message(UserMessage { - level: MessageLevel::Error, - text: format!("exec workflow: {e}"), - }); - setup_failed = true; - break; - } - } + // A bad overlay on ANY entry aborts the whole phase before + // any container starts — otherwise earlier steps would have + // already mutated the workspace. + if let Some(e) = resolved.iter().find_map(|r| r.as_ref().err()) { + shared.lock().unwrap().write_message(UserMessage { + level: MessageLevel::Error, + text: format!("exec workflow: {e}"), + }); + setup_failed = true; } if !setup_failed { @@ -884,26 +885,22 @@ impl Command for ExecWorkflowCommand { let mount = mount_path.clone(); let base = base_image.clone(); let shared_for_factory = Arc::clone(&shared); - let validated = std::sync::Arc::new(std::sync::Mutex::new(validated)); - // The setup container exec calls are blocking; keep the - // tokio worker free by routing them through block_in_place. let setup_result = tokio::task::block_in_place(|| { let factory = |idx: usize| -> Result< Box, EngineError, > { - let (overlays, env) = validated - .lock() - .unwrap() + let (overlays, env) = resolved .get(idx) - .cloned() .ok_or_else(|| { EngineError::Other(format!( - "internal: missing pre-validated overlays for setup step {idx}", + "internal: missing pre-resolved overlays for setup step {idx}", )) - })?; + })? + .as_ref() + .map_err(|e| EngineError::Other(e.to_string()))?; let container = - runtime.start_background(&base, &mount, &env, &overlays)?; + runtime.start_background(&base, &mount, env, overlays)?; Ok(Box::new(container)) }; let r = engine.run_setup(&setup_steps, &setup_abort_flags, factory); @@ -937,10 +934,11 @@ impl Command for ExecWorkflowCommand { // === TEARDOWN PHASE === // - // Same per-entry container pattern as setup. Teardown is - // best-effort: a per-step container failure (overlay error or - // backend failure) is reported and the next step still runs - // unless the step has `abort_on_failure = true`. + // Same per-entry container pattern as setup: overlays are + // pre-resolved via `resolve_phase_overlays` and the factory + // indexes into the results. Unlike setup, no upfront abort + // gate — per-entry overlay errors flow through the factory and + // `run_teardown` handles them as per-step failures (best-effort). // // If the setup or main phase triggered abort_on_failure, // teardown is skipped regardless of teardown_on_failure. @@ -950,27 +948,31 @@ impl Command for ExecWorkflowCommand { let should_run = teardown_on_failure || workflow_succeeded; if should_run { let base_image = resolve_base_image(&session, &git_root_for_scope); + let resolved = resolve_phase_overlays( + &self.engines, + &session, + &cli_typed, + &teardown_entry_overlays, + worktree_git_mount.as_ref(), + ); let runtime = Arc::clone(&self.engines.runtime); let mount = mount_path.clone(); - let entry_overlays = teardown_entry_overlays.clone(); - let engines_for_factory = self.engines.clone(); - let session_for_factory = session.clone(); - let cli_typed_for_factory = cli_typed.clone(); (teardown_aborted, any_teardown_failed) = tokio::task::block_in_place(|| { let factory = |idx: usize| -> Result< Box, EngineError, > { - let entry = entry_overlays.get(idx).and_then(|o| o.as_deref()); - let (overlays, env) = collect_single_entry_overlays( - &engines_for_factory, - &session_for_factory, - &cli_typed_for_factory, - entry, - ) - .map_err(|e| EngineError::Other(e.to_string()))?; + let (overlays, env) = resolved + .get(idx) + .ok_or_else(|| { + EngineError::Other(format!( + "internal: missing pre-resolved overlays for teardown step {idx}", + )) + })? + .as_ref() + .map_err(|e| EngineError::Other(e.to_string()))?; let container = runtime - .start_background(&base_image, &mount, &env, &overlays)?; + .start_background(&base_image, &mount, env, overlays)?; Ok(Box::new(container)) }; engine @@ -1158,13 +1160,16 @@ fn collect_single_entry_overlays( > { let collected = collect_all_overlay_specs(session, cli_typed.to_vec(), entry_overlays)?; + let container_home = crate::engine::overlay::detect_home_from_dockerfile( + &session.git_root().join("Dockerfile.dev"), + ); let request = crate::engine::overlay::OverlayRequest { directories: collected.directories, include_all_skills: false, named_skills: Vec::new(), agent: None, yolo: false, - container_home: None, + container_home, }; let overlay_specs = engines .overlay_engine @@ -1185,6 +1190,47 @@ fn collect_single_entry_overlays( Ok((overlay_specs, env)) } +/// Pre-resolve overlay specs and env vars for every entry in a setup or +/// teardown phase. +/// +/// Each entry is resolved independently via [`collect_single_entry_overlays`] +/// (per-step overlay isolation, WI-0082). When `worktree_git_mount` is +/// `Some`, the backing `.git` directory overlay is appended to every +/// successful entry so git operations work inside worktree-mounted +/// containers. +/// +/// Returns one `Result` per entry. The caller decides error policy: +/// - **Setup** aborts the entire phase on the first `Err`. +/// - **Teardown** passes errors through to the factory; `run_teardown` +/// handles per-step failures gracefully. +fn resolve_phase_overlays( + engines: &Engines, + session: &Session, + cli_typed: &[TypedOverlay], + entries: &[Option>], + worktree_git_mount: Option<&crate::engine::container::options::OverlaySpec>, +) -> Vec< + Result< + ( + Vec, + std::collections::HashMap, + ), + CommandError, + >, +> { + entries + .iter() + .map(|entry| { + let (mut overlays, env) = + collect_single_entry_overlays(engines, session, cli_typed, entry.as_deref())?; + if let Some(wt) = worktree_git_mount { + overlays.push(wt.clone()); + } + Ok((overlays, env)) + }) + .collect() +} + /// Extract a numeric work item number from strings like "0069", "69", "WI-69", /// etc. Returns the first run of decimal digits found in `s`, parsed as `u32`. fn parse_work_item_number(s: &str) -> Option { @@ -1218,6 +1264,30 @@ fn find_work_item_file(git_root: &std::path::Path, number: u32) -> Option/` directory. When only the worktree +/// is bind-mounted, that pointer dangles and every git command fails. This +/// overlay mounts the main `.git` directory at its host-absolute path so the +/// pointer resolves identically inside the container. +/// +/// Returns `Ok(None)` when `worktree_path` is a regular repo or has no `.git`. +fn worktree_git_overlay( + worktree_path: &std::path::Path, +) -> Result, EngineError> { + let main_git_dir = match crate::engine::git::resolve_worktree_git_dir(worktree_path)? { + Some(p) => p, + None => return Ok(None), + }; + Ok(Some(crate::engine::container::options::OverlaySpec { + host_path: main_git_dir.clone(), + container_path: main_git_dir, + permission: crate::engine::container::options::OverlayPermission::ReadWrite, + })) +} + #[cfg(test)] mod tests { use std::path::Path; diff --git a/src/engine/git/mod.rs b/src/engine/git/mod.rs index dcdc7cc6..5ec9d3fb 100644 --- a/src/engine/git/mod.rs +++ b/src/engine/git/mod.rs @@ -652,6 +652,47 @@ impl GitRootResolver for GitEngine { } } +/// Resolve the main `.git` directory backing a worktree checkout. +/// +/// A worktree's `.git` entry is a *file* containing `gitdir: ` where +/// `` points to `.git/worktrees//` in the main repository. +/// This function reads that pointer and returns the main `.git/` directory +/// (two levels up from the worktree entry). +/// +/// Returns `Ok(None)` when `worktree_path/.git` is a directory (regular +/// repo) or does not exist. +pub fn resolve_worktree_git_dir(worktree_path: &Path) -> Result, EngineError> { + let dot_git = worktree_path.join(".git"); + if !dot_git.exists() || dot_git.is_dir() { + return Ok(None); + } + let content = + std::fs::read_to_string(&dot_git).map_err(|e| EngineError::io(&dot_git, e))?; + let gitdir_line = content.trim().strip_prefix("gitdir: ").ok_or_else(|| { + EngineError::Git(format!( + "unexpected .git file format at {}: {}", + dot_git.display(), + content.trim(), + )) + })?; + let gitdir = if Path::new(gitdir_line).is_absolute() { + PathBuf::from(gitdir_line) + } else { + worktree_path.join(gitdir_line) + }; + // gitdir → .git/worktrees/ → parent .git/worktrees/ → parent .git/ + let main_git_dir = gitdir + .parent() + .and_then(|p| p.parent()) + .ok_or_else(|| { + EngineError::Git(format!( + "cannot derive main .git dir from worktree gitdir: {}", + gitdir.display(), + )) + })?; + Ok(Some(main_git_dir.to_path_buf())) +} + #[cfg(test)] mod tests { use super::*; @@ -792,4 +833,40 @@ mod tests { .expect("remove_worktree should succeed"); assert!(!wt_path.exists(), "worktree directory must be gone"); } + + #[test] + fn resolve_worktree_git_dir_returns_none_for_regular_repo() { + let tmp = tempfile::tempdir().unwrap(); + init_repo(tmp.path()); + let result = resolve_worktree_git_dir(tmp.path()).unwrap(); + assert!(result.is_none(), "regular repo should return None"); + } + + #[test] + fn resolve_worktree_git_dir_returns_none_for_non_git_dir() { + let tmp = tempfile::tempdir().unwrap(); + let result = resolve_worktree_git_dir(tmp.path()).unwrap(); + assert!(result.is_none(), "non-git dir should return None"); + } + + #[test] + fn resolve_worktree_git_dir_finds_main_git_dir() { + let repo_tmp = tempfile::tempdir().unwrap(); + let wt_tmp = tempfile::tempdir().unwrap(); + init_repo(repo_tmp.path()); + let g = GitEngine::new(); + let wt_path = wt_tmp.path().join("my-worktree"); + g.create_worktree(repo_tmp.path(), &wt_path, "awman/test-resolve") + .expect("create_worktree should succeed"); + + let result = resolve_worktree_git_dir(&wt_path) + .expect("should not error") + .expect("worktree should resolve to Some"); + + let expected = repo_tmp.path().join(".git").canonicalize().unwrap(); + let actual = result.canonicalize().unwrap(); + assert_eq!(actual, expected, "should resolve to main repo .git dir"); + + g.remove_worktree(repo_tmp.path(), &wt_path).unwrap(); + } } diff --git a/src/engine/overlay/mod.rs b/src/engine/overlay/mod.rs index a2a68bd2..c955435c 100644 --- a/src/engine/overlay/mod.rs +++ b/src/engine/overlay/mod.rs @@ -769,6 +769,30 @@ fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> { Ok(()) } +/// Parse a Dockerfile for the last non-root `USER` directive and return +/// `/home/`. Returns `None` when the file doesn't exist, can't be read, +/// or only uses root. +pub(crate) fn detect_home_from_dockerfile(path: &Path) -> Option { + let content = std::fs::read_to_string(path).ok()?; + let mut result: Option = None; + for line in content.lines() { + let trimmed = line.trim(); + let upper = trimmed.to_uppercase(); + if let Some(rest) = upper.strip_prefix("USER ") { + let name = rest.split_whitespace().next().unwrap_or("").trim(); + if !name.is_empty() && name != "ROOT" && name != "0" { + let orig_rest = &trimmed[5..]; // skip "USER " + let orig_name = orig_rest.split_whitespace().next().unwrap_or("root"); + result = Some(format!("/home/{orig_name}")); + } else { + // Switched back to root — reset. + result = None; + } + } + } + result +} + /// Detect the container home directory by inspecting `Dockerfile.`. /// /// Looks for a `USER ` directive (where `` is not "root" or "0") @@ -782,24 +806,8 @@ pub(crate) fn detect_container_home(home: &Path, agent: &str, git_root: &Path) - for dir in &search_dirs { let path = dir.join(&dockerfile_name); - if !path.exists() { - continue; - } - if let Ok(content) = std::fs::read_to_string(&path) { - for line in content.lines() { - let trimmed = line.trim(); - // Look for "USER " (case-insensitive directive). - let upper = trimmed.to_uppercase(); - if let Some(rest) = upper.strip_prefix("USER ") { - let name = rest.split_whitespace().next().unwrap_or("").trim(); - if !name.is_empty() && name != "ROOT" && name != "0" { - // Use original case from the line. - let orig_rest = &trimmed[5..]; // skip "USER " - let orig_name = orig_rest.split_whitespace().next().unwrap_or("root"); - return Some(format!("/home/{orig_name}")); - } - } - } + if let Some(home) = detect_home_from_dockerfile(&path) { + return Some(home); } } None @@ -1628,6 +1636,52 @@ mod tests { ); } + // ─── detect_home_from_dockerfile ────────────────────────────────────────── + + #[test] + fn detect_home_from_dockerfile_finds_non_root_user() { + let tmp = tempfile::tempdir().unwrap(); + let df = tmp.path().join("Dockerfile.dev"); + std::fs::write(&df, "FROM debian:bookworm\nUSER awman\nWORKDIR /workspace\n").unwrap(); + assert_eq!( + detect_home_from_dockerfile(&df), + Some("/home/awman".to_string()), + ); + } + + #[test] + fn detect_home_from_dockerfile_returns_none_for_root() { + let tmp = tempfile::tempdir().unwrap(); + let df = tmp.path().join("Dockerfile.dev"); + std::fs::write(&df, "FROM debian:bookworm\nUSER root\n").unwrap(); + assert!(detect_home_from_dockerfile(&df).is_none()); + } + + #[test] + fn detect_home_from_dockerfile_returns_none_when_missing() { + let tmp = tempfile::tempdir().unwrap(); + assert!(detect_home_from_dockerfile(&tmp.path().join("nonexistent")).is_none()); + } + + #[test] + fn detect_home_from_dockerfile_uses_last_non_root_user() { + let tmp = tempfile::tempdir().unwrap(); + let df = tmp.path().join("Dockerfile"); + std::fs::write(&df, "FROM debian\nUSER builder\nRUN make\nUSER runner\n").unwrap(); + assert_eq!( + detect_home_from_dockerfile(&df), + Some("/home/runner".to_string()), + ); + } + + #[test] + fn detect_home_from_dockerfile_resets_on_root_switch() { + let tmp = tempfile::tempdir().unwrap(); + let df = tmp.path().join("Dockerfile"); + std::fs::write(&df, "FROM debian\nUSER builder\nRUN make\nUSER root\n").unwrap(); + assert!(detect_home_from_dockerfile(&df).is_none()); + } + // ─── resolve_user_overlay missing-host fail-fast ───────────────────────── #[test]