From 270365ea4c457e353b1f02594983eb6d1bd8aa00 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Wed, 28 Oct 2026 16:12:03 +0000 Subject: [PATCH] feat(config): land the wave-3 per-feature family-seam handles (w3-seam-config) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ServeConfig gains cfg-gated SessionSeam / GitSeam Option fields (whole-Option struct per seam — the shape decision recorded in the task notes): unset → the owned rows decline even though the feature is on, no construction-time validation - SessionSeam carries the alktty backends map + backend selector + ownership provider; GitSeam carries the alkgit backend quartet + caller-owned pipeline semaphore + push_options_negotiated - optional deps alktty 0.8.1 / alkgit 0.1.0 wired feature-to-dep (pins verified against crates.io: licenses, MSRV, shared alkcall resolution); gix-hash 0.28 as dev-dep only (alkgit does not re-export ObjectId for the trait doubles) - per-feature construction/unset-declines/Debug rows in config.rs; accessors session_seam_configured / git_seam_configured; docs state what each seam handle carries Verified: cargo test (per-feature + --all-features), clippy ×2 (-D warnings), fmt, cargo doc --no-deps, cargo tree feature isolation (git-build: no alktty; session-build: no alkgit), taskgraph validate --- Cargo.lock | 792 ++++++++++++++++++++++++- Cargo.toml | 26 +- src/config.rs | 434 +++++++++++++- src/lib.rs | 4 + tasks/implementation/w3-seam-config.md | 116 +++- 5 files changed, 1351 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ec55adc..9d49d79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,13 +83,40 @@ dependencies = [ "zeroize", ] +[[package]] +name = "alkgit" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81d274e66dc4977171951a3cdf63f7a1b87179387a486e58130662d4b456ab6" +dependencies = [ + "alkcall", + "async-trait", + "bstr", + "bytes", + "futures", + "gix-hash", + "gix-packetline", + "gix-protocol", + "gix-transport", + "gix-validate", + "parking_lot", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-util", +] + [[package]] name = "alkssh" version = "0.1.0" dependencies = [ "alkcall", + "alkgit", + "alktty", "alkvault", "async-trait", + "gix-hash", "parking_lot", "russh", "thiserror", @@ -98,6 +125,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "alktty" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2265167842b7d3ea09bc235f48450d3dcf22049a2ca71f8155de00acbf97c226" +dependencies = [ + "alkcall", + "async-trait", + "bytes", + "futures", + "futures-core", + "libc", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "tracing", +] + [[package]] name = "alkvault" version = "0.1.0" @@ -258,6 +305,12 @@ dependencies = [ "hex-conservative", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.13.2" @@ -317,6 +370,17 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -560,6 +624,46 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" +[[package]] +name = "defmt" +version = "0.3.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4deef977660e373c458df605a913d4ab121db0ccd18d1ad6976dff7d37cddb53" +dependencies = [ + "defmt 1.2.0", +] + +[[package]] +name = "defmt" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4a0795ac740cb54d7b3775c9c17345f6aaecf884c01733b39d6017738e0e3e1" +dependencies = [ + "bitflags 1.3.2", + "defmt-macros", +] + +[[package]] +name = "defmt-macros" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "840bb056bd99c495c9188198a258ab787c79f306d864db3e6e8451442d6d8366" +dependencies = [ + "defmt-parser", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "defmt-parser" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71958f69fb2de856c5dd51ac9377e986ca1aa5919f931bf5395a807277f889a0" +dependencies = [ + "thiserror", +] + [[package]] name = "delegate" version = "0.13.5" @@ -751,6 +855,24 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "faster-hex" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04839bdf9d8c10f66806fad16b852fc72aab80873aebc3cb69d85b4fa41543ed" +dependencies = [ + "autocfg", + "defmt 0.3.101", + "heapless", + "serde", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + [[package]] name = "ff" version = "0.14.0" @@ -854,6 +976,19 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.34" @@ -964,6 +1099,440 @@ dependencies = [ "zeroize", ] +[[package]] +name = "gix-actor" +version = "0.44.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78243d3371019288a40a75ba1a58de2815c32591fc87b9f7cb01918bce364ca8" +dependencies = [ + "bstr", + "gix-date", + "gix-error", +] + +[[package]] +name = "gix-chunk" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6a775aba54f0d65698126cc4214b6bf8ddb208312e4ef4240dfe34b0697b87" +dependencies = [ + "gix-error", +] + +[[package]] +name = "gix-command" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df63215f103299169e239dfe76f5cfa4a3d6d4adf69a01774085e3e0423cba34" +dependencies = [ + "bstr", + "gix-error", + "gix-path", + "gix-quote", + "gix-trace", +] + +[[package]] +name = "gix-commitgraph" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfede9936d4b789867c09722576aea4ceb0e3aacf962eef035ebab0686b41a80" +dependencies = [ + "bstr", + "gix-chunk", + "gix-error", + "gix-hash", + "memmap2", + "nonempty", +] + +[[package]] +name = "gix-config-value" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cdada8bd9c97dc6522505c08226d9c88f0a1286f2c06b9e1bf51c3bf7ad7b2b" +dependencies = [ + "bitflags 2.13.2", + "bstr", + "gix-error", + "gix-path", + "gix-utils", + "libc", +] + +[[package]] +name = "gix-credentials" +version = "0.42.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8487f709886267337ee39231a5e68f3c6a4e28fd59e056fc9e66e9638941c633" +dependencies = [ + "bstr", + "gix-command", + "gix-config-value", + "gix-date", + "gix-error", + "gix-path", + "gix-prompt", + "gix-quote", + "gix-sec", + "gix-trace", + "gix-url", + "percent-encoding", +] + +[[package]] +name = "gix-date" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c538190012dbb536df54734ec38cc1109270c73f2f76585c011dd5b0c9f087c" +dependencies = [ + "bstr", + "gix-error", + "itoa", + "jiff", +] + +[[package]] +name = "gix-error" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87ff4b3f3daf01234d38d176b0ec00e92de514cc0e3fb8a8898c5cb7ccd22bb" +dependencies = [ + "bstr", +] + +[[package]] +name = "gix-fs" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808f23d897ddfe847d9b7206bda947901885be623bdf96b226a2dccf87f018c7" +dependencies = [ + "bstr", + "gix-error", + "gix-parallel", + "gix-path", + "gix-utils", + "libc", + "walkdir", +] + +[[package]] +name = "gix-hash" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4802ffb87e051795822bfdaa8ab9075b23e316684f7054e6ce4a246b2b76f4e" +dependencies = [ + "faster-hex", + "gix-error", + "gix-utils", + "sha1dc", +] + +[[package]] +name = "gix-hashtable" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f66822aee11ccb28d6a2e63b587254e578fb52a138bd5806b58d8560125592b" +dependencies = [ + "gix-hash", + "hashbrown 0.17.1", + "parking_lot", +] + +[[package]] +name = "gix-lock" +version = "26.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64783de116c477c5ea4a7f3b56e18ab363dabfdae2724291ea0587b642cadb3d" +dependencies = [ + "gix-error", + "gix-fs", + "gix-tempfile", + "gix-utils", +] + +[[package]] +name = "gix-macros" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f11f5429ca73cc3758c9f53af56a9fba100a8701c5b4bc30e29b8b0b15288991" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gix-negotiate" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ddfb0d6da849bd938d929336e07c1f26970d9f6ec59621df74ec3f79c128ea" +dependencies = [ + "bitflags 2.13.2", + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-object", + "gix-revwalk", +] + +[[package]] +name = "gix-object" +version = "0.66.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beaf9086dd00b9d2a177ca3ae83210ce926cee7c0faa112e053851221cd8f94d" +dependencies = [ + "bstr", + "gix-actor", + "gix-date", + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-utils", + "gix-validate", + "itoa", + "smallvec", +] + +[[package]] +name = "gix-packetline" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ebb59ef7aff9d435946a5e5c3d9cfb673be7665839fd72961c08c733a934fb7" +dependencies = [ + "bstr", + "faster-hex", + "futures-io", + "futures-lite", + "gix-error", + "gix-trace", + "pin-project-lite", +] + +[[package]] +name = "gix-parallel" +version = "0.50.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58e7e10f8343e05f6ad8651f97dffa1caca1531bb274bfc8e3050e80e3ed5946" +dependencies = [ + "gix-trace", +] + +[[package]] +name = "gix-path" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83976e926a485e6fb4b8d9a0d7a30c53c1f607b81b5d310f06f92fd8385bf16a" +dependencies = [ + "bstr", + "gix-error", + "gix-trace", + "gix-validate", +] + +[[package]] +name = "gix-prompt" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d9650eedebb85e55577831db217403333416573356de722a040415c416f1cec" +dependencies = [ + "gix-command", + "gix-config-value", + "gix-error", + "parking_lot", + "rustix", +] + +[[package]] +name = "gix-protocol" +version = "0.67.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "832b5dc46a3c0a6f6fa8f8e571c11074add4b545c6789799ae6d6c36ae1a294e" +dependencies = [ + "async-trait", + "bstr", + "futures-io", + "futures-lite", + "gix-credentials", + "gix-date", + "gix-error", + "gix-hash", + "gix-lock", + "gix-macros", + "gix-negotiate", + "gix-object", + "gix-ref", + "gix-refspec", + "gix-revwalk", + "gix-shallow", + "gix-trace", + "gix-transport", + "gix-utils", + "nonempty", +] + +[[package]] +name = "gix-quote" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2413e65271303e9b674ec4785d469df4c3c36e888dbd165e3035de0405c6d9d1" +dependencies = [ + "bstr", + "gix-error", + "gix-utils", +] + +[[package]] +name = "gix-ref" +version = "0.69.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68686e4697b9cd15d1b152ef51df870513210ed22938eacc13b3672b76035f13" +dependencies = [ + "gix-actor", + "gix-error", + "gix-fs", + "gix-hash", + "gix-lock", + "gix-object", + "gix-parallel", + "gix-path", + "gix-tempfile", + "gix-utils", + "gix-validate", + "memmap2", +] + +[[package]] +name = "gix-refspec" +version = "0.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2569309da6fdc230973cf5b846508154c0e813fda60f2b2da768cc5655ed312c" +dependencies = [ + "bstr", + "gix-error", + "gix-hash", + "gix-validate", + "smallvec", +] + +[[package]] +name = "gix-revwalk" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "800479fbbf1e7f94f663e07943a91add5499343e7aac611a81e6a0460fd44af6" +dependencies = [ + "gix-commitgraph", + "gix-date", + "gix-error", + "gix-hash", + "gix-hashtable", + "gix-object", + "smallvec", +] + +[[package]] +name = "gix-sec" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9598d1f35baec7038c2d278bfcc18f1d8951f41dcc3f51ab6175661dfe5a6137" +dependencies = [ + "bitflags 2.13.2", + "gix-path", + "libc", + "windows-sys", +] + +[[package]] +name = "gix-shallow" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d805949578724b5164c90ec6d19567832ccaa5ba8a3fe26f7befc52007e2407" +dependencies = [ + "bstr", + "gix-error", + "gix-hash", + "gix-lock", + "nonempty", +] + +[[package]] +name = "gix-tempfile" +version = "26.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fdb135a043d6106b59bc58d59a326b277e495ec26fb9e02a1e2b6354bf4b5a" +dependencies = [ + "gix-fs", + "libc", + "parking_lot", + "tempfile", + "windows-sys", +] + +[[package]] +name = "gix-trace" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c477c029518d3f235c26db21ff7cccc1fa13c0c31b05c005a55b7affd4ffbd81" + +[[package]] +name = "gix-transport" +version = "0.61.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e43d6b0097ea9f2f266d5d6c43b8f615ed3da7c686ec8771cd17c1cb33cb0de" +dependencies = [ + "async-trait", + "bstr", + "futures-io", + "futures-lite", + "gix-command", + "gix-error", + "gix-packetline", + "gix-path", + "gix-quote", + "gix-sec", + "gix-trace", + "gix-url", + "gix-utils", + "pin-project-lite", +] + +[[package]] +name = "gix-url" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def12e1016b872f1094442882f627905c59bd73480435291a4958cf373a8f28d" +dependencies = [ + "bstr", + "gix-error", + "gix-path", + "gix-utils", + "percent-encoding", +] + +[[package]] +name = "gix-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11a9446060447e9ec58f5d333b98bed53389dfa10de5d8dd46fc76d01dfc2954" +dependencies = [ + "bstr", + "fastrand", + "getrandom 0.4.3", + "prodash", + "unicode-normalization", +] + +[[package]] +name = "gix-validate" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b617c17754bf37ee7a9b48e33ffb806abecbbed779ba415199ac1480acf699" +dependencies = [ + "bstr", + "gix-error", +] + [[package]] name = "group" version = "0.14.0" @@ -975,6 +1544,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.16.1" @@ -986,6 +1564,22 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "hex" version = "0.4.3" @@ -1190,6 +1784,60 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jiff" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b005715dcbeb0089a3c0dab99f2ff1cc3b2525323552703d648585d342a383" +dependencies = [ + "defmt 1.2.0", + "jiff-core", + "jiff-static", + "jiff-tzdb-platform", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", + "windows-link", +] + +[[package]] +name = "jiff-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e52fe76043ccecc9005d2305ebaadf7d7fc0cc89ca6baa10a94d6bc68c7128c" +dependencies = [ + "defmt 1.2.0", + "log", +] + +[[package]] +name = "jiff-static" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cc9817253cf7c7ee4684451bd327e88d6f3658014e54a29198625590650695c" +dependencies = [ + "jiff-core", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jiff-tzdb" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa8377070c6bae868759445e5a77f66d84f0b72f3a054bfb00e6d038b8282da7" + +[[package]] +name = "jiff-tzdb-platform" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8" +dependencies = [ + "jiff-tzdb", +] + [[package]] name = "jobserver" version = "0.1.35" @@ -1279,6 +1927,12 @@ version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.3" @@ -1312,6 +1966,15 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" +[[package]] +name = "memmap2" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b0ca1155a9ecc25aa12e8313a119f32edf0255fea2f44839c4ac0a12b2ac157" +dependencies = [ + "libc", +] + [[package]] name = "micromap" version = "0.3.0" @@ -1361,12 +2024,18 @@ version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ - "bitflags", + "bitflags 2.13.2", "cfg-if", "cfg_aliases", "libc", ] +[[package]] +name = "nonempty" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9737e026353e5cd0736f98eddae28665118eb6f6600902a7f50db585621fecb6" + [[package]] name = "num" version = "0.4.3" @@ -1530,6 +2199,12 @@ dependencies = [ "windows-strings", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1661,6 +2336,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.6" @@ -1715,6 +2405,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prodash" +version = "31.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf52d87428247c5aa3760ac8e7d8b13ec7b3579b955a8c3f311668b6860acba5" +dependencies = [ + "parking_lot", +] + [[package]] name = "quote" version = "1.0.47" @@ -1789,7 +2488,7 @@ version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ - "bitflags", + "bitflags 2.13.2", ] [[package]] @@ -1821,7 +2520,7 @@ dependencies = [ "ahash", "fluent-uri", "getrandom 0.3.4", - "hashbrown", + "hashbrown 0.16.1", "itoa", "micromap", "parking_lot", @@ -1876,7 +2575,7 @@ checksum = "3bb77a4b259312b2abbc541bca1c13cb4e9b61c560a7fd894068c3b92e0c9dc3" dependencies = [ "aes", "aws-lc-rs", - "bitflags", + "bitflags 2.13.2", "block-padding", "byteorder", "bytes", @@ -1969,6 +2668,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e76742e5294153d4dde1207dbec5bdd81eef6fabdc0d2118babadabfd858aa" +dependencies = [ + "bitflags 2.13.2", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + [[package]] name = "rustversion" version = "1.0.23" @@ -1985,6 +2697,15 @@ dependencies = [ "cipher", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -2087,6 +2808,12 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "sha1dc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b0ebf6537e3e8db8bb7d21ecb6d7399738c953c757945c6b95e46c4e95df7b" + [[package]] name = "sha2" version = "0.10.9" @@ -2297,6 +3024,19 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys", +] + [[package]] name = "thiserror" version = "2.0.21" @@ -2361,6 +3101,31 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "tokio-stream" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e464cf451ba96ebfc6f9b6542f17ee8b8956e33f1e40d9690624e59d7a7f8a4b" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "tracing" version = "0.1.44" @@ -2484,6 +3249,16 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -2555,6 +3330,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + [[package]] name = "windows" version = "0.62.2" diff --git a/Cargo.toml b/Cargo.toml index 971d9bd..bc5f6da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,12 +26,16 @@ name = "alkssh" [features] # ADR-003: flat and independent — no feature implies another; `default = []` # by design (a default build has no capabilities; every unfeatured open -# declines fail-closed). Features gate nothing yet — the wave-2 decline core -# is their first consumer; the per-feature sibling deps land with their -# first consuming wave. +# declines fail-closed). The wave-2 decline core is the first `cfg` +# consumer; the wave-3 seam-config handles are the second (each optional +# sibling dep is gated feature-to-dep so a git-build never compiles +# alktty and vice versa — the wave-1 manifest rule: per-feature sibling +# deps land with their first consuming wave, which for alktty/alkgit is +# the wave-3 config fields; alktunnels still lands with its first +# consuming code, w3-tunnel-direct). default = [] -session = [] -git = [] +session = ["dep:alktty"] +git = ["dep:alkgit"] tunnel = [] sftp = [] @@ -46,6 +50,14 @@ async-trait = "0.1" russh = { version = "0.65", default-features = false, features = ["aws-lc-rs"] } alkcall = "0.10" alkvault = "0.1" +# Wave-3 seam-handle types (`config.rs`'s `SessionSeam`/`GitSeam`). +# Pins verified against crates.io (2026-10-28): alktty 0.8.1, alkgit +# 0.1.0 — both MIT OR Apache-2.0, both rust_version 1.88 ≤ our 1.89, +# both over alkcall ^0.10 (shared with our `alkcall = "0.10"` → 0.10.1 +# resolution). alktty's `local` feature stays off — alkssh never +# touches a real backend; the `local` shapes stay alktty-side. +alktty = { version = "0.8.1", default-features = false, optional = true } +alkgit = { version = "0.1.0", default-features = false, optional = true } tokio = { version = "1.54", features = ["full"] } thiserror = "2.0" tracing = "0.1" @@ -53,6 +65,10 @@ parking_lot = "0.12" zeroize = "1.9" [dev-dependencies] +# The git-seam test doubles name `GitPackGen`'s `gix_hash::ObjectId` +# parameters and alkgit does not re-export them — test-graph only, +# version matched to alkgit's `^0.28` req (same crate in the lockfile). +gix-hash = { version = "0.28", features = ["sha1"] } [profile.release] lto = "thin" \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 8e2adc6..e6407d2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,8 +8,12 @@ //! - [`ServeConfig`] — the server direction's assembly-supplied serving //! configuration: the OQ-08 fields ([`ServeConfig::shell_argv`] / //! [`ServeConfig::exec_wrapper_argv`]), the injected identity, -//! host-key and tunnel handles, the OQ-09 `cwd`/`env` values, and the -//! [`Limits`]. +//! host-key and tunnel handles, the per-feature family-seam handles +//! (the wave-3 extension — [`SessionSeam`] carries the alktty +//! backends map + backend selector + ownership provider; +//! [`GitSeam`] carries the alkgit backend quartet + the caller-owned +//! pipeline + the push-options flag), the OQ-09 `cwd`/`env` values, +//! and the [`Limits`]. //! - [`DialConfig`] — the client direction's dial configuration //! (`client.md` §The dial surface): address, key source, host-key //! policy, and the bounded-resource knobs (carried by [`Limits`]). @@ -22,7 +26,14 @@ //! request time (wave-2/3 consumption), never a fallback, never //! construction-time validation (`error.rs`'s no-config-error //! contract). No construction-time validation exists in this -//! module. +//! module. The wave-3 seam handles extend the same rule per +//! feature: [`ServeConfig::session_seam`] unset → the session rows +//! decline even though the `session` feature is on; +//! [`ServeConfig::git_seam`] unset → the git row declines even +//! though `git` is on (the decline matrix's config-driven notes; +//! the feature alone configures nothing). The tunnel handle stays +//! the flat `Option>` as landed — a single-field +//! seam needs no struct. //! - **Injected handles, never crate-resolved identity or trust**: //! the [`IdentityProvider`], [`HostKeySource`] and //! [`HostKeyPolicy`] handles are `Arc` the assembly @@ -44,12 +55,20 @@ //! budget model. use std::collections::BTreeMap; +#[cfg(feature = "session")] +use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; use std::time::Duration; use alkcall::channels::client::ChannelClient; use alkcall::core::auth::IdentityProvider; +#[cfg(feature = "session")] +use alkcall::core::ownership::OwnershipProvider; +#[cfg(feature = "git")] +use alkgit::{GitPackGen, GitPackIngest, GitRefs, GitRegistry}; +#[cfg(feature = "session")] +use alktty::TtyBackend; use crate::host_key::HostKeyPolicy; use crate::key_source::{HostKeySource, KeySource}; @@ -172,6 +191,101 @@ impl Default for Limits { } } +/// The `session` feature's alktty seam handle (`mapping.md` §The +/// seams): what `drive_session[_pre_negotiated]` takes that the +/// assembly composes (alktty 0.8.1's `adapter.rs:205-241` signature). +/// **Shape decision** (recorded at `w3-seam-config`): a whole-`Option` +/// struct per seam, not flat `Option` fields — each seam is consumed +/// atomically by exactly one seam call, so a struct admits no +/// half-supplied seam states and the unset-declines contract reads as +/// one `Option`: **`ServeConfig::session_seam` unset → the session +/// rows decline even though the `session` feature is on** (the +/// decline matrix's config-driven note), checked at request time by +/// the mappings — no construction-time validation. Cloned into each +/// spawned serving task (all payload types `Arc` or plain data). +#[cfg(feature = "session")] +#[derive(Clone)] +pub struct SessionSeam { + /// The alktty backends map — the `TtyAdapter`-style registry keyed + /// by the `NegotiateRequest.backend` selector string. **Assembled + /// by the assembly**: alkssh never touches a real backend, and the + /// `local` backend shapes stay alktty-side (this dep's `local` + /// feature is not enabled — the map holds assembly-supplied + /// implementations of alktty's [`TtyBackend`] only). + pub backends: Arc>>, + /// The backend **selector** — the `NegotiateRequest.backend` + /// string alkssh writes into the locally-constructed negotiation + /// (`w3-tty-session`'s negotiation rows; the wire command/args + /// never reach it). Assembly-supplied, never wire-derived. + pub backend: String, + /// The ADR-050 ownership provider — passed through + /// `drive_session[_pre_negotiated]` verbatim (the family ACL + /// layer riding alongside; an alkcall type, no new dep). + pub ownership: Option>, +} + +#[cfg(feature = "session")] +impl std::fmt::Debug for SessionSeam { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut keys: Vec<&String> = self.backends.keys().collect(); + keys.sort(); + f.debug_struct("SessionSeam") + .field("backend_keys", &keys) + .field("backend", &self.backend) + .field("ownership_set", &self.ownership.is_some()) + .finish() + } +} + +/// The `git` feature's alkgit seam handle (`mapping.md` §The seams): +/// what `serve_duplex_resolved` takes that the assembly composes +/// (alkgit 0.1.0's `duplex.rs:1412-1426` signature — the four backend +/// traits, the caller-owned pipeline, the push-options gate). Same +/// whole-`Option`-struct decision as [`SessionSeam`]: +/// **`ServeConfig::git_seam` unset → the git row declines even though +/// the `git` feature is on**, checked at request time by the +/// mappings — no construction-time validation. +#[cfg(feature = "git")] +#[derive(Clone)] +pub struct GitSeam { + /// The repo-id resolution trait (`GitRegistry::resolve`) — the + /// ADR-007 pre-protocol gate's other half; alkssh never resolves + /// (the collapsed-generic-denial posture lives in alkgit). + pub registry: Arc, + /// The ref listing + CAS transaction trait + /// (`list_refs`/`apply_updates`). + pub refs: Arc, + /// The pack generation + negotiation-ack trait + /// (`generate`/`common_haves`). + pub packgen: Arc, + /// The pack-preparation trait (`prepare` — never applies; the + /// `GitRefs::apply_updates` CAS stays the single ingestion home). + pub ingest: Arc, + /// The assembler's push-options flag (alkgit's ADR-013 §11 gate, + /// passed through verbatim) — `false` in the v1 default. + pub push_options_negotiated: bool, + /// The caller-owned, server-wide pipeline permit — alkgit's + /// pack-pipeline concurrency bound, *shared across every session + /// the serving direction takes* (the caller keeps its own handle; + /// `Arc` for the share). One per `DialConfig`-independent serve + /// direction, never per channel. + pub pipeline: Arc, +} + +#[cfg(feature = "git")] +impl std::fmt::Debug for GitSeam { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GitSeam") + .field("registry", &"") + .field("refs", &"") + .field("packgen", &"") + .field("ingest", &"") + .field("push_options_negotiated", &self.push_options_negotiated) + .field("pipeline_permits", &self.pipeline.available_permits()) + .finish() + } +} + /// The server direction's assembly-supplied serving configuration /// (`overview.md` §Server stack): everything wave-2's serving loop /// takes at construction. Pure data + injected handles — no russh @@ -215,6 +329,22 @@ pub struct ServeConfig { /// `NegotiateRequest.env` — **never wire-derived**. Empty by /// default. pub session_env: BTreeMap, + /// The `session` feature's alktty seam handle ([`SessionSeam`] — + /// the backends map + backend selector + ownership provider the + /// `drive_session[_pre_negotiated]` rows take). **Unset → the + /// session rows decline even though the `session` feature is on** + /// (the decline matrix's config-driven note; the feature alone + /// configures nothing). Wave-3 extension of the wave-1 config + /// surface — shape decision recorded on [`SessionSeam`]. + #[cfg(feature = "session")] + pub session_seam: Option, + /// The `git` feature's alkgit seam handle ([`GitSeam`] — the four + /// backend traits + the caller-owned pipeline + the push-options + /// flag the `serve_duplex_resolved` row takes). **Unset → the git + /// row declines even though the `git` feature is on** (same + /// postures as [`ServeConfig::session_seam`]). + #[cfg(feature = "git")] + pub git_seam: Option, /// The injected identity provider (alkcall's /// `IdentityProvider::resolve_from_fingerprint`) — the assembly /// supplies it; the crate never resolves identities itself and @@ -255,6 +385,10 @@ impl ServeConfig { exec_wrapper_argv: None, session_cwd: None, session_env: BTreeMap::new(), + #[cfg(feature = "session")] + session_seam: None, + #[cfg(feature = "git")] + git_seam: None, identity_provider, host_key_source, channel_client: None, @@ -284,17 +418,47 @@ impl ServeConfig { pub fn tunnel_configured(&self) -> bool { self.channel_client.is_some() } + + /// Whether the session rows' family seam is configured in this + /// configuration ([`SessionSeam`] present; the `session` feature + /// gate applies at the dispatch on top): `false` means the rows + /// decline — the unset-declines contract, checked at request time + /// by the wave-3 mappings, never here. + #[cfg(feature = "session")] + pub fn session_seam_configured(&self) -> bool { + self.session_seam.is_some() + } + + /// Whether the git row's family seam is configured in this + /// configuration ([`GitSeam`] present; the `git` feature gate + /// applies at the dispatch on top): `false` means the row declines + /// — the unset-declines contract, checked at request time by the + /// wave-3 mappings, never here. + #[cfg(feature = "git")] + pub fn git_seam_configured(&self) -> bool { + self.git_seam.is_some() + } } impl std::fmt::Debug for ServeConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ServeConfig") + let mut debug = f.debug_struct("ServeConfig"); + debug .field("shell_argv", &self.shell_argv) .field("exec_wrapper_argv", &self.exec_wrapper_argv) .field("session_cwd", &self.session_cwd) .field("session_env", &self.session_env) .field("identity_provider", &"") - .field("host_key_source", &"") + .field("host_key_source", &""); + #[cfg(feature = "session")] + { + debug.field("session_seam", &self.session_seam.is_some()); + } + #[cfg(feature = "git")] + { + debug.field("git_seam", &self.git_seam.is_some()); + } + debug .field("channel_client", &self.channel_client.as_ref().is_some()) .field("limits", &self.limits) .finish() @@ -503,6 +667,169 @@ mod tests { assert_eq!(serve.session_env.len(), 1); } + #[cfg(feature = "git")] + mod git_seam { + use alkgit::{ + GitPackGen, GitPackIngest, GitRefs, GitRegistry, PreparedPush, PushOptions, RefLine, + RefOutcome, RefUpdate, RegistryError, RepoRecord, StorageError, + }; + use async_trait::async_trait; + use gix_hash::ObjectId; + + use super::super::{GitSeam, ServeConfig}; + use super::*; + + struct NoopRegistry; + + #[async_trait] + impl GitRegistry for NoopRegistry { + async fn resolve(&self, _repo_id: &str) -> Result { + Err(RegistryError::NotFound) + } + } + + struct NoopRefs; + + #[async_trait] + impl GitRefs for NoopRefs { + async fn list_refs(&self, _repo: &RepoRecord) -> Result, StorageError> { + Ok(Vec::new()) + } + + async fn apply_updates( + &self, + _repo: &RepoRecord, + _updates: Vec, + _atomic: bool, + ) -> Result, StorageError> { + Ok(Vec::new()) + } + } + + struct NoopPackGen; + + #[async_trait] + impl GitPackGen for NoopPackGen { + async fn generate( + &self, + _repo: &RepoRecord, + _wants: Vec, + _boundary_haves: Vec, + _limits: alkgit::Limits, + _sink: Box, + ) -> Result<(), StorageError> { + Ok(()) + } + + async fn common_haves( + &self, + _repo: &RepoRecord, + _haves: Vec, + ) -> Result, StorageError> { + Ok(Vec::new()) + } + } + + struct NoopPackIngest; + + #[async_trait] + impl GitPackIngest for NoopPackIngest { + async fn prepare( + &self, + _repo: &RepoRecord, + _updates: Vec, + _pack: Box, + _push_options: Option, + _limits: alkgit::Limits, + ) -> Result { + Ok(PreparedPush { + unpack: Ok(()), + updates: Vec::new(), + }) + } + } + + fn assembled_seam() -> GitSeam { + GitSeam { + registry: Arc::new(NoopRegistry), + refs: Arc::new(NoopRefs), + packgen: Arc::new(NoopPackGen), + ingest: Arc::new(NoopPackIngest), + push_options_negotiated: false, + pipeline: Arc::new(tokio::sync::Semaphore::new(2)), + } + } + + #[test] + fn serve_config_git_seam_unset_declines_though_the_feature_is_on() { + let serve = ServeConfig::new(Arc::new(NoopIdentityProvider), host_key_source()); + + assert!(serve.git_seam.is_none()); + assert!(!serve.git_seam_configured()); + + let debug = format!("{serve:?}"); + assert!(debug.contains("git_seam: false")); + } + + #[test] + fn serve_config_holds_the_git_seam_whole_and_cloneable() { + let mut serve = ServeConfig::new(Arc::new(NoopIdentityProvider), host_key_source()); + let seam = assembled_seam(); + let registry = Arc::clone(&seam.registry); + + serve.git_seam = Some(seam); + + assert!(serve.git_seam_configured()); + let held = serve.git_seam.as_ref().expect("the seam holds"); + assert!(Arc::ptr_eq(®istry, &held.registry)); + assert!(!held.push_options_negotiated); + assert_eq!(held.pipeline.available_permits(), 2); + + let cloned = held.clone(); + assert!(Arc::ptr_eq(&cloned.registry, ®istry)); + assert!(!cloned.push_options_negotiated); + assert_eq!(cloned.pipeline.available_permits(), 2); + + let debug = format!("{serve:?}"); + assert!(debug.contains("git_seam: true")); + } + + #[test] + fn git_seam_pipeline_is_the_single_server_wide_share() { + let mut serve = ServeConfig::new(Arc::new(NoopIdentityProvider), host_key_source()); + let pipeline = Arc::new(tokio::sync::Semaphore::new(2)); + + let mut seam = assembled_seam(); + seam.pipeline = Arc::clone(&pipeline); + serve.git_seam = Some(seam); + let held = serve.git_seam.as_ref().expect("the seam holds"); + let cloned = held.clone(); + + assert!(Arc::ptr_eq(&pipeline, &held.pipeline)); + assert!(Arc::ptr_eq(&pipeline, &cloned.pipeline)); + + let _permit = cloned + .pipeline + .try_acquire() + .expect("a permit is available"); + assert_eq!(held.pipeline.available_permits(), 1); + } + + #[test] + fn git_seam_debug_shapes_itself_not_the_backends() { + let seam = assembled_seam(); + let debug = format!("{seam:?}"); + + assert!(debug.contains("GitSeam")); + assert!(debug.contains("")); + assert!(debug.contains("")); + assert!(debug.contains("")); + assert!(debug.contains("")); + assert!(debug.contains("push_options_negotiated: false")); + assert!(debug.contains("pipeline_permits: 2")); + } + } + #[test] fn dial_config_defaults_carry_the_keepalive_posture() { let dial = DialConfig::new("runner.example:22", key_source(), Arc::new(RejectAll)); @@ -528,6 +855,103 @@ mod tests { assert!(Arc::ptr_eq(&policy, &dial.host_key_policy)); } + // The wave-3 per-feature seam handles: the construction shapes, the + // unset-declines-preserved rows and the Debug posture — each block + // only runs in the build that carries its feature (compile-time + // feature isolation: no git build ever touches alktty types, + // no session build ever touches alkgit types). + #[cfg(feature = "session")] + mod session_seam { + use alktty::{TtyBackend, TtyError, TtyHandle, TtyParams}; + use async_trait::async_trait; + + use super::super::{ServeConfig, SessionSeam}; + use super::*; + + struct StubBackend; + + #[async_trait] + impl TtyBackend for StubBackend { + async fn allocate(&self, _params: &TtyParams) -> Result { + Err(TtyError::AllocFailed { + message: "stub".into(), + }) + } + } + + fn assembled_seam() -> SessionSeam { + let mut backends = std::collections::HashMap::new(); + backends.insert("stub".to_string(), Arc::new(StubBackend) as _); + SessionSeam { + backends: Arc::new(backends), + backend: "stub".to_string(), + ownership: None, + } + } + + #[test] + fn serve_config_session_seam_unset_declines_though_the_feature_is_on() { + let serve = ServeConfig::new(Arc::new(NoopIdentityProvider), host_key_source()); + + assert!(serve.session_seam.is_none()); + assert!(!serve.session_seam_configured()); + + let debug = format!("{serve:?}"); + assert!(debug.contains("session_seam: false")); + } + + #[test] + fn serve_config_holds_the_session_seam_whole_and_cloneable() { + let mut serve = ServeConfig::new(Arc::new(NoopIdentityProvider), host_key_source()); + let seam = assembled_seam(); + let backends = Arc::clone(&seam.backends); + + serve.session_seam = Some(seam); + + assert!(serve.session_seam_configured()); + let held = serve.session_seam.as_ref().expect("the seam holds"); + assert!(Arc::ptr_eq(&backends, &held.backends)); + assert_eq!(held.backend, "stub"); + assert!(held.ownership.is_none()); + + let cloned = held.clone(); + assert!(Arc::ptr_eq(&cloned.backends, &backends)); + assert_eq!(cloned.backend, "stub"); + + let debug = format!("{serve:?}"); + assert!(debug.contains("session_seam: true")); + } + + #[test] + fn session_seam_configure_is_one_option_independent_of_the_oq08_fields() { + let mut serve = ServeConfig::new(Arc::new(NoopIdentityProvider), host_key_source()); + serve.session_seam = Some(assembled_seam()); + + assert!(serve.session_seam_configured()); + assert!(!serve.shell_configured()); + assert!(!serve.exec_wrapper_configured()); + + serve.shell_argv = Some(vec!["/bin/sh".to_string(), "-l".to_string()]); + assert!(serve.shell_configured()); + assert!(serve.session_seam_configured()); + + serve.session_seam = None; + assert!(serve.shell_configured()); + assert!(!serve.session_seam_configured()); + } + + #[test] + fn session_seam_debug_shapes_itself_not_the_backends() { + let seam = assembled_seam(); + let debug = format!("{seam:?}"); + + assert!(debug.contains("SessionSeam")); + assert!(debug.contains(r#"backend_keys: ["stub"]"#)); + assert!(debug.contains(r#"backend: "stub""#)); + assert!(debug.contains("ownership_set: false")); + } + } + #[tokio::test] async fn dial_config_key_source_stays_fail_closed_through_the_config() { let dial = DialConfig::new("fleet.example:22", key_source(), Arc::new(RejectAll)); diff --git a/src/lib.rs b/src/lib.rs index 4f25598..1771108 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,6 +36,10 @@ mod key_source; mod serve; pub use adapter::SshAdapter; +#[cfg(feature = "git")] +pub use config::GitSeam; +#[cfg(feature = "session")] +pub use config::SessionSeam; pub use config::{DialConfig, Limits, ServeConfig}; pub use error::{AdapterError, KeySourceError}; pub use fingerprint::fingerprint; diff --git a/tasks/implementation/w3-seam-config.md b/tasks/implementation/w3-seam-config.md index e1e929a..151d968 100644 --- a/tasks/implementation/w3-seam-config.md +++ b/tasks/implementation/w3-seam-config.md @@ -1,7 +1,7 @@ --- id: w3-seam-config name: Wave 3 — serve-config seam handles (alktty/alkgit per-feature) -status: pending +status: completed depends_on: [review-wave-2, w1-config] scope: narrow risk: low @@ -65,17 +65,17 @@ each seam handle carries). ## Acceptance Criteria -- [ ] `ServeConfig` gains the feature-gated seam handles (the final +- [x] `ServeConfig` gains the feature-gated seam handles (the final shape decided and recorded); unset → the owned rows decline, structurally carried, no construction-time validation -- [ ] The optional deps + `dep:` feature wiring land with crates.io +- [x] The optional deps + `dep:` feature wiring land with crates.io pins verified; a git-build never compiles alktty and vice versa (no code path outside the cfg gate touches the types) -- [ ] The none-build's `ServeConfig::new` shape is unchanged; +- [x] The none-build's `ServeConfig::new` shape is unchanged; feature-gated fields absent from featureless builds -- [ ] Tests: the per-feature construction shapes, the +- [x] Tests: the per-feature construction shapes, the unset-declines-preserved rows, the `Debug` posture -- [ ] `cargo test`, `cargo clippy --all-targets -- -D warnings`, +- [x] `cargo test`, `cargo clippy --all-targets -- -D warnings`, `cargo clippy --all-features --all-targets -- -D warnings`, `cargo fmt --check`, `cargo doc --no-deps` pass @@ -93,4 +93,106 @@ each seam handle carries). ## Notes -> To be filled at implementation time. \ No newline at end of file +### What landed (2026-10-28) + +- `Cargo.toml`: optional `alktty = "0.8.1"` / + `alkgit = "0.1.0"` (both `default-features = false`), wired + `session = ["dep:alktty"]`, `git = ["dep:alkgit"]`. alktty's `local` + feature stays off — alkssh never touches a real backend; the `local` + shapes stay alktty-side. Feature-graph comment updated (wave-3 + seam-config is the deps' first consumer; alktunnels still lands with + `w3-tunnel-direct`). +- `src/config.rs` (re-exported through `lib.rs`): the two seam-handle + types + the two cfg-gated `ServeConfig` fields + the cfg-gated + `session_seam_configured()` / `git_seam_configured()` accessors + + config-module docs updated (the direction list and the + unset-declines rule both state the seam handles now). + +### Shape decision (the task's decide-and-record item): whole-`Option` struct per seam + +`SessionSeam{backends, backend, ownership}` and +`GitSeam{registry, refs, packgen, ingest, push_options_negotiated, +pipeline}` held as `cfg`-gated `Option` fields +(`session_seam` / `git_seam`), each `Clone` (all payload types `Arc` +or plain data — the spawned serving tasks clone the seam), each with a +hand-rolled `Debug`. **Flat `Option` fields were rejected**: each seam +is consumed atomically by exactly one seam call +(`drive_session[_pre_negotiated]` / `serve_duplex_resolved`), so a +struct admits no half-supplied seam states (backends without a +selector; three traits without the fourth) that would all map to the +same all-or-nothing decline — the unset-declines contract reads as one +`Option` per seam, "unset → the owned rows decline even though the +feature is on" (the decline matrix's config-driven note), checked at +request time by the mappings, never at construction. The already-landed +tunnel handle stays the flat +`Option>` — a single-field seam needs no struct. +Naming: struct names carry the crate (the consuming mappings will see +it in the field docs); the fields carry the row vocabulary +(`session_seam` / `git_seam`, matching the decline matrix's row names). + +### Pins verified against crates.io (2026-10-28, this task re-verified) + +- `alktty 0.8.1`: published 2026-10-22, not yanked, + `MIT OR Apache-2.0`, `rust_version = "1.88"` ≤ our 1.89; + `alkcall ^0.10.1` — shares our `alkcall = "0.10"` resolution + (0.10.1), so no alkcall fork in the lockfile. +- `alkgit 0.1.0`: `MIT OR Apache-2.0`, `rust_version = "1.88"`; + `alkcall ^0.10` — same shared resolution. +- Signatures confirmed against the published crates (the registry + sources under `~/.cargo/registry/src/`): alktty's + `drive_session[_pre_negotiated]` (adapter.rs:205-241) takes + `(writer, reader, [req,] backends: Arc>>, ownership: Option>, + identity: Option)` — the identity is the per-connection + resolved one, not config; alkgit's `serve_duplex_resolved` + (duplex.rs:1412) takes the four traits + + `push_options_negotiated: bool` + `pipeline: Arc`. + `TtyBackend` / `NegotiateRequest` / `TerminalParamsWire` and + `GitRegistry/GitRefs/GitPackGen/GitPackIngest` are all re-exported + from the two lib roots. + +### One course correction absorbed: `ObjectId` is not re-exported by alkgit + +`GitPackGen::generate`/`common_haves` name `Vec` +and alkgit's lib root does not re-export `ObjectId` — a backend-trait +doubled in tests has to name the type, so `gix-hash = "0.28"` (matching +alkgit's `^0.28` req; the same crate already in the lockfile) lands as +a **`[dev-dependencies]` entry only**, exercised exclusively by the +cfg-gated git test module. The lib dependency graph keeps the +feature-to-dep isolation below; the dev graph is universal by design +(tokio and friends are too). Flagged for `w3-git-mapping`: the +hand-rolled in-mem quartet will lean on this dev-dep for `ObjectId` +(same as this task's noop doubles, modeled on alkgit's own +`backend.rs:164-254` noop shapes — own code, not copied). + +### Dependency isolation — verified structurally, not by assertion + +`cargo tree -e features --features git`: zero `alktty` occurrences; +`cargo tree -e features --features session`: zero `alkgit` +occurrences; `--all-features`: both present. Combined with the cfg +gates (all `alktty`/`alkgit` type references live inside +`#[cfg(feature = "session")]` / `#[cfg(feature = "git")]` blocks — +imports, fields, accessors, Debug, constructor, tests), the "a +git-build never compiles alktty and vice versa" invariant is +compile-proven for both consumption sides. + +### The unset-declines wire-level rows stay the mappings' consumption + +The wave-3 mappings own the request-time decline checks; this task +lands the config-level contract rows (constructor posture, `Option` +configured() semantics, seam-vs-OQ-08-field independence, Debug +posture — 8 unit rows across the two feature modules) and leaves the +wave-2 harness rows untouched (featured-but-unset already declines at +the row the stubs own; the real serving paths make those rows +meaningful in `w3-git-mapping` / `w3-tty-session`, whose seam +consumption reads exactly these fields). + +### Verification (all green) + +`cargo test` (none build, 9 binaries + doctests) · +`cargo test --features session` / `--features git` / +`--all-features` (67 unit rows under all-features) · +`cargo clippy --all-targets -- -D warnings` ✓ · +`cargo clippy --all-features --all-targets -- -D warnings` ✓ · +`cargo fmt --check` ✓ · `cargo doc --no-deps` 0 warnings · +`taskgraph validate` ✓. \ No newline at end of file