4 Commits
15 changed files with 3202 additions and 34 deletions
Generated
+519 -1
View File
@@ -26,6 +26,16 @@ dependencies = [
"pom", "pom",
] ]
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common 0.1.7",
"generic-array",
]
[[package]] [[package]]
name = "aes" name = "aes"
version = "0.8.4" version = "0.8.4"
@@ -37,6 +47,20 @@ dependencies = [
"cpufeatures 0.2.17", "cpufeatures 0.2.17",
] ]
[[package]]
name = "aes-gcm"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
dependencies = [
"aead",
"aes",
"cipher",
"ctr",
"ghash",
"subtle",
]
[[package]] [[package]]
name = "agen" name = "agen"
version = "0.2.1" version = "0.2.1"
@@ -326,6 +350,12 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "base16ct"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.7" version = "0.21.7"
@@ -338,6 +368,12 @@ version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]] [[package]]
name = "base64urlsafedata" name = "base64urlsafedata"
version = "0.5.5" version = "0.5.5"
@@ -349,6 +385,17 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "bcrypt-pbkdf"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2"
dependencies = [
"blowfish",
"pbkdf2",
"sha2 0.10.9",
]
[[package]] [[package]]
name = "bit-set" name = "bit-set"
version = "0.5.3" version = "0.5.3"
@@ -403,6 +450,16 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "blowfish"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
dependencies = [
"byteorder",
"cipher",
]
[[package]] [[package]]
name = "bstr" name = "bstr"
version = "1.12.1" version = "1.12.1"
@@ -435,6 +492,12 @@ version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.11.1" version = "1.11.1"
@@ -495,6 +558,17 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]]
name = "chacha20"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures 0.2.17",
]
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.44" version = "0.4.44"
@@ -654,6 +728,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]] [[package]]
name = "const-oid" name = "const-oid"
version = "0.10.2" version = "0.10.2"
@@ -937,6 +1017,18 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-bigint"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"generic-array",
"rand_core 0.6.4",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.7" version = "0.1.7"
@@ -966,6 +1058,41 @@ dependencies = [
"phf 0.11.3", "phf 0.11.3",
] ]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"curve25519-dalek-derive",
"digest 0.10.7",
"fiat-crypto",
"rustc_version",
"subtle",
]
[[package]]
name = "curve25519-dalek-derive"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.23.0" version = "0.23.0"
@@ -1056,6 +1183,16 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4"
[[package]]
name = "der"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
dependencies = [
"const-oid 0.9.6",
"zeroize",
]
[[package]] [[package]]
name = "der-parser" name = "der-parser"
version = "9.0.0" version = "9.0.0"
@@ -1114,7 +1251,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [ dependencies = [
"block-buffer 0.10.4", "block-buffer 0.10.4",
"const-oid 0.9.6",
"crypto-common 0.1.7", "crypto-common 0.1.7",
"subtle",
] ]
[[package]] [[package]]
@@ -1124,7 +1263,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c"
dependencies = [ dependencies = [
"block-buffer 0.12.0", "block-buffer 0.12.0",
"const-oid", "const-oid 0.10.2",
"crypto-common 0.2.1", "crypto-common 0.2.1",
] ]
@@ -1175,12 +1314,66 @@ dependencies = [
"cipher", "cipher",
] ]
[[package]]
name = "ecdsa"
version = "0.16.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
dependencies = [
"der",
"digest 0.10.7",
"elliptic-curve",
"rfc6979",
"signature",
"spki",
]
[[package]]
name = "ed25519"
version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
dependencies = [
"signature",
]
[[package]]
name = "ed25519-dalek"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
dependencies = [
"curve25519-dalek",
"ed25519",
"sha2 0.10.9",
"subtle",
]
[[package]] [[package]]
name = "either" name = "either"
version = "1.15.0" version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "elliptic-curve"
version = "0.13.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [
"base16ct",
"crypto-bigint",
"digest 0.10.7",
"ff",
"generic-array",
"group",
"pkcs8",
"rand_core 0.6.4",
"sec1",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "embedded-io" name = "embedded-io"
version = "0.4.0" version = "0.4.0"
@@ -1284,6 +1477,22 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "ff"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
dependencies = [
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]] [[package]]
name = "filedescriptor" name = "filedescriptor"
version = "0.8.3" version = "0.8.3"
@@ -1526,6 +1735,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [ dependencies = [
"typenum", "typenum",
"version_check", "version_check",
"zeroize",
] ]
[[package]] [[package]]
@@ -1568,6 +1778,16 @@ dependencies = [
"wasip3", "wasip3",
] ]
[[package]]
name = "ghash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
dependencies = [
"opaque-debug",
"polyval",
]
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.33.0" version = "0.33.0"
@@ -1636,6 +1856,17 @@ dependencies = [
"memmap2", "memmap2",
] ]
[[package]]
name = "group"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
"ff",
"rand_core 0.6.4",
"subtle",
]
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.4.13" version = "0.4.13"
@@ -1724,6 +1955,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hmac"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest 0.10.7",
]
[[package]] [[package]]
name = "html5ever" name = "html5ever"
version = "0.26.0" version = "0.26.0"
@@ -2212,6 +2452,9 @@ name = "lazy_static"
version = "1.5.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
dependencies = [
"spin",
]
[[package]] [[package]]
name = "leb128fmt" name = "leb128fmt"
@@ -2672,6 +2915,22 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-bigint-dig"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
dependencies = [
"lazy_static",
"libm",
"num-integer",
"num-iter",
"num-traits",
"rand 0.8.5",
"smallvec",
"zeroize",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.2.1" version = "0.2.1"
@@ -2698,6 +2957,16 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "num-iter"
version = "0.1.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
dependencies = [
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-traits" name = "num-traits"
version = "0.2.19" version = "0.2.19"
@@ -2705,6 +2974,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"libm",
] ]
[[package]] [[package]]
@@ -2765,6 +3035,12 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]] [[package]]
name = "openssl" name = "openssl"
version = "0.10.76" version = "0.10.76"
@@ -2818,6 +3094,44 @@ dependencies = [
"num-traits", "num-traits",
] ]
[[package]]
name = "p256"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
dependencies = [
"ecdsa",
"elliptic-curve",
"primeorder",
"sha2 0.10.9",
]
[[package]]
name = "p384"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
dependencies = [
"ecdsa",
"elliptic-curve",
"primeorder",
"sha2 0.10.9",
]
[[package]]
name = "p521"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fc9e2161f1f215afdfce23677034ae137bbd45016a880c2eb3ba8eb95f085b2"
dependencies = [
"base16ct",
"ecdsa",
"elliptic-curve",
"primeorder",
"rand_core 0.6.4",
"sha2 0.10.9",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.5" version = "0.12.5"
@@ -2847,6 +3161,15 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest 0.10.7",
]
[[package]] [[package]]
name = "pdf-extract" name = "pdf-extract"
version = "0.10.0" version = "0.10.0"
@@ -2864,6 +3187,15 @@ dependencies = [
"unicode-normalization", "unicode-normalization",
] ]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
dependencies = [
"base64ct",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.3.2" version = "2.3.2"
@@ -3009,6 +3341,27 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkcs1"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
dependencies = [
"der",
"pkcs8",
"spki",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"spki",
]
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.32" version = "0.3.32"
@@ -3021,6 +3374,29 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
[[package]]
name = "poly1305"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures 0.2.17",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "polyval"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"opaque-debug",
"universal-hash",
]
[[package]] [[package]]
name = "pom" name = "pom"
version = "1.1.0" version = "1.1.0"
@@ -3101,6 +3477,15 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "primeorder"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
dependencies = [
"elliptic-curve",
]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.106" version = "1.0.106"
@@ -3525,6 +3910,16 @@ dependencies = [
"web-sys", "web-sys",
] ]
[[package]]
name = "rfc6979"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
dependencies = [
"hmac",
"subtle",
]
[[package]] [[package]]
name = "ring" name = "ring"
version = "0.17.14" version = "0.17.14"
@@ -3539,6 +3934,27 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "rsa"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
dependencies = [
"const-oid 0.9.6",
"digest 0.10.7",
"num-bigint-dig",
"num-integer",
"num-traits",
"pkcs1",
"pkcs8",
"rand_core 0.6.4",
"sha2 0.10.9",
"signature",
"spki",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "rusqlite" name = "rusqlite"
version = "0.37.0" version = "0.37.0"
@@ -3745,6 +4161,20 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sec1"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
"base16ct",
"der",
"generic-array",
"pkcs8",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "secrets" name = "secrets"
version = "0.1.0" version = "0.1.0"
@@ -4060,6 +4490,16 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "signature"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
dependencies = [
"digest 0.10.7",
"rand_core 0.6.4",
]
[[package]] [[package]]
name = "simd-adler32" name = "simd-adler32"
version = "0.3.9" version = "0.3.9"
@@ -4103,6 +4543,72 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "spin"
version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
[[package]]
name = "spki"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
dependencies = [
"base64ct",
"der",
]
[[package]]
name = "ssh-cipher"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f"
dependencies = [
"aes",
"aes-gcm",
"cbc",
"chacha20",
"cipher",
"ctr",
"poly1305",
"ssh-encoding",
"subtle",
]
[[package]]
name = "ssh-encoding"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15"
dependencies = [
"base64ct",
"pem-rfc7468",
"sha2 0.10.9",
]
[[package]]
name = "ssh-key"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b86f5297f0f04d08cabaa0f6bff7cb6aec4d9c3b49d87990d63da9d9156a8c3"
dependencies = [
"bcrypt-pbkdf",
"ed25519-dalek",
"p256",
"p384",
"p521",
"rand_core 0.6.4",
"rsa",
"sec1",
"sha2 0.10.9",
"signature",
"ssh-cipher",
"ssh-encoding",
"subtle",
"zeroize",
]
[[package]] [[package]]
name = "stable_deref_trait" name = "stable_deref_trait"
version = "1.2.1" version = "1.2.1"
@@ -4934,6 +5440,16 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common 0.1.7",
"subtle",
]
[[package]] [[package]]
name = "unsafe-libyaml" name = "unsafe-libyaml"
version = "0.2.11" version = "0.2.11"
@@ -6259,11 +6775,13 @@ dependencies = [
"project-record", "project-record",
"protocol", "protocol",
"reqwest", "reqwest",
"ring",
"rusqlite", "rusqlite",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"sha2 0.11.0", "sha2 0.11.0",
"ssh-key",
"tempfile", "tempfile",
"thiserror 2.0.18", "thiserror 2.0.18",
"ticket", "ticket",
+1
View File
@@ -115,6 +115,7 @@ tar = "0.4"
rusqlite = { version = "0.37", features = ["backup", "bundled"] } rusqlite = { version = "0.37", features = ["backup", "bundled"] }
ring = "0.17.14" ring = "0.17.14"
sha2 = "0.11" sha2 = "0.11"
ssh-key = { version = "0.6.7", features = ["ed25519", "encryption"] }
tempfile = "3.27" tempfile = "3.27"
thiserror = "2.0" thiserror = "2.0"
tokio = "1.52" tokio = "1.52"
+110
View File
@@ -373,6 +373,116 @@ pub struct UpdateWorkspaceMemorySettingsRequest {
pub language: String, pub language: String,
} }
/// Public metadata for one Workspace-scoped Repository SSH credential.
///
/// Secret references and secret material are deliberately not part of this DTO.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct RepositorySshCredential {
pub credential_id: String,
pub workspace_id: String,
pub name: String,
pub public_key_algorithm: String,
pub public_key_fingerprint: String,
pub current_revision: u64,
pub status: String,
pub created_at: String,
pub rotated_at: Option<String>,
#[serde(default)]
pub referenced_repositories: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct CreateRepositorySshCredentialRequest {
pub operation_id: String,
pub credential_id: String,
pub name: String,
pub private_key: String,
#[serde(default)]
pub passphrase: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct RotateRepositorySshCredentialRequest {
pub operation_id: String,
pub expected_revision: u64,
pub private_key: String,
#[serde(default)]
pub passphrase: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct DeleteRepositorySshCredentialRequest {
pub operation_id: String,
pub expected_revision: u64,
}
/// Public metadata for an explicitly pinned SSH host key.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct RepositorySshHostTrust {
pub host_trust_id: String,
pub workspace_id: String,
pub hostname: String,
pub port: u16,
pub key_algorithm: String,
pub host_key: String,
pub fingerprint: String,
pub current_revision: u64,
pub created_at: String,
pub updated_at: String,
#[serde(default)]
pub referenced_repositories: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct PutRepositorySshHostTrustRequest {
pub operation_id: String,
pub host_trust_id: String,
pub hostname: String,
pub port: u16,
pub host_key: String,
#[serde(default)]
pub expected_revision: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct DeleteRepositorySshHostTrustRequest {
pub operation_id: String,
pub expected_revision: u64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RepositoryAccessMode {
ReadOnly,
ReadWrite,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct RepositorySshAccessBinding {
pub repository_id: String,
pub credential_id: String,
pub host_trust_id: String,
pub access: RepositoryAccessMode,
}
/// Secret-free active Repository access projection consumed by later Runtime work.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct RepositoryAccessProjection {
pub workspace_id: String,
pub config_revision: u64,
pub projection_digest: String,
pub bindings: Vec<RepositorySshAccessBinding>,
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
+2
View File
@@ -25,11 +25,13 @@ manifest.workspace = true
protocol = { workspace = true } protocol = { workspace = true }
project-record.workspace = true project-record.workspace = true
reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "native-tls"] } reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "native-tls"] }
ring.workspace = true
rusqlite.workspace = true rusqlite.workspace = true
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true serde_json.workspace = true
serde_yaml.workspace = true serde_yaml.workspace = true
sha2.workspace = true sha2.workspace = true
ssh-key.workspace = true
thiserror.workspace = true thiserror.workspace = true
ticket.workspace = true ticket.workspace = true
memory.workspace = true memory.workspace = true
+3
View File
@@ -20,6 +20,7 @@ pub mod records;
#[cfg(feature = "typescript")] #[cfg(feature = "typescript")]
pub use records::ticket_api_typescript; pub use records::ticket_api_typescript;
pub mod repositories; pub mod repositories;
pub mod repository_access;
pub mod repository_source; pub mod repository_source;
pub mod resource_broker; pub mod resource_broker;
pub mod retention; pub mod retention;
@@ -116,6 +117,8 @@ pub enum Error {
TicketAssignmentConflict(String), TicketAssignmentConflict(String),
#[error("Workdir attachment conflict: {0}")] #[error("Workdir attachment conflict: {0}")]
WorkdirAttachmentConflict(String), WorkdirAttachmentConflict(String),
#[error("Workspace permission denied: {0}")]
WorkspacePermissionDenied(String),
#[error("Workspace config update conflict: {0}")] #[error("Workspace config update conflict: {0}")]
WorkspaceConfigConflict(String), WorkspaceConfigConflict(String),
#[error("Registry inconsistency: {0}")] #[error("Registry inconsistency: {0}")]
File diff suppressed because it is too large Load Diff
+270 -4
View File
@@ -4,7 +4,7 @@ use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, Weak}; use std::sync::{Arc, Mutex, Weak};
use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade}; use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade};
use axum::extract::{Path as AxumPath, Query, Request, State}; use axum::extract::{Extension, Path as AxumPath, Query, Request, State};
use axum::http::header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH, LOCATION, ORIGIN, SET_COOKIE}; use axum::http::header::{CONTENT_TYPE, ETAG, IF_NONE_MATCH, LOCATION, ORIGIN, SET_COOKIE};
use axum::http::{HeaderMap, Method, StatusCode, Uri}; use axum::http::{HeaderMap, Method, StatusCode, Uri};
use axum::middleware::{self, Next}; use axum::middleware::{self, Next};
@@ -58,8 +58,12 @@ use worker::feature::builtin::{WorkerObservationSubject, WorkerObservationSubjec
use worker_runtime::resource::{BackendResourceError, BackendResourceFetchRequest}; use worker_runtime::resource::{BackendResourceError, BackendResourceFetchRequest};
use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend}; use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend};
use workspace_api::{ use workspace_api::{
ObjectiveCreateRequest, ObjectiveEditRequest, ObjectiveLinkTicketRequest, CreateRepositorySshCredentialRequest, DeleteRepositorySshCredentialRequest,
ObjectiveStateRequest, TICKET_ORCHESTRATION_PLANS_QUERY_PATH, TICKET_RELATIONS_QUERY_PATH, DeleteRepositorySshHostTrustRequest, ObjectiveCreateRequest, ObjectiveEditRequest,
ObjectiveLinkTicketRequest, ObjectiveStateRequest, PutRepositorySshHostTrustRequest,
RepositoryAccessProjection, RepositorySshCredential, RepositorySshHostTrust,
RotateRepositorySshCredentialRequest, TICKET_ORCHESTRATION_PLANS_QUERY_PATH,
TICKET_RELATIONS_QUERY_PATH,
}; };
use crate::auth::{ use crate::auth::{
@@ -109,6 +113,10 @@ use crate::repositories::{
ConfiguredRepository, RepositoryListProjection, RepositoryLogRead, RepositoryLookupError, ConfiguredRepository, RepositoryListProjection, RepositoryLogRead, RepositoryLookupError,
RepositoryRegistryReader, RepositorySummary, RepositoryRegistryReader, RepositorySummary,
}; };
use crate::repository_access::{
RepositoryAccessConfigSchemaProvider, RepositorySecretService,
project_repository_access_candidate, project_repository_access_state,
};
use crate::resource_broker::BackendResourceBroker; use crate::resource_broker::BackendResourceBroker;
use crate::runtime_settings::RuntimeConfigSchemaProvider; use crate::runtime_settings::RuntimeConfigSchemaProvider;
use crate::runtime_subscription::RuntimeSubscriptionBroker; use crate::runtime_subscription::RuntimeSubscriptionBroker;
@@ -342,6 +350,7 @@ pub struct WorkspaceApi {
pub(crate) config: ServerConfig, pub(crate) config: ServerConfig,
pub(crate) store: Arc<dyn ControlPlaneStore>, pub(crate) store: Arc<dyn ControlPlaneStore>,
config_store: Arc<crate::SqliteWorkspaceStore>, config_store: Arc<crate::SqliteWorkspaceStore>,
repository_secrets: Arc<RepositorySecretService>,
config_schema_registry: crate::config_source::WorkspaceConfigSchemaRegistry, config_schema_registry: crate::config_source::WorkspaceConfigSchemaRegistry,
prompt_projection_cache: crate::prompt_settings::WorkspacePromptProjectionCache, prompt_projection_cache: crate::prompt_settings::WorkspacePromptProjectionCache,
authority: SqliteWorkspaceAuthority, authority: SqliteWorkspaceAuthority,
@@ -1053,6 +1062,7 @@ async fn authorize_workspace_api_request(
return StatusCode::FORBIDDEN.into_response(); return StatusCode::FORBIDDEN.into_response();
} }
} }
request.extensions_mut().insert(actor);
next.run(request).await next.run(request).await
} }
@@ -1343,12 +1353,17 @@ impl WorkspaceApi {
let config_store = Arc::new(crate::SqliteWorkspaceStore::open( let config_store = Arc::new(crate::SqliteWorkspaceStore::open(
config.database_path.clone(), config.database_path.clone(),
)?); )?);
let repository_secrets = Arc::new(RepositorySecretService::open(
config_store.clone(),
&config.database_path,
)?);
let config_schema_registry = crate::config_source::WorkspaceConfigSchemaRegistry::default() let config_schema_registry = crate::config_source::WorkspaceConfigSchemaRegistry::default()
.with_provider(Arc::new( .with_provider(Arc::new(
crate::profile_settings::ProfileConfigSchemaProvider, crate::profile_settings::ProfileConfigSchemaProvider,
)) ))
.with_provider(Arc::new(crate::prompt_settings::PromptConfigSchemaProvider)) .with_provider(Arc::new(crate::prompt_settings::PromptConfigSchemaProvider))
.with_provider(Arc::new(RuntimeConfigSchemaProvider)) .with_provider(Arc::new(RuntimeConfigSchemaProvider))
.with_provider(Arc::new(RepositoryAccessConfigSchemaProvider))
.with_provider(Arc::new(skills::SkillConfigSchemaProvider)); .with_provider(Arc::new(skills::SkillConfigSchemaProvider));
config_store.ensure_workspace_config_materialized_with_schema( config_store.ensure_workspace_config_materialized_with_schema(
&config.workspace_id, &config.workspace_id,
@@ -1357,6 +1372,7 @@ impl WorkspaceApi {
)?; )?;
let api = Self { let api = Self {
config_store, config_store,
repository_secrets,
config_schema_registry, config_schema_registry,
prompt_projection_cache: prompt_projection_cache:
crate::prompt_settings::WorkspacePromptProjectionCache::default(), crate::prompt_settings::WorkspacePromptProjectionCache::default(),
@@ -1863,6 +1879,32 @@ fn build_inner_router(api: WorkspaceApi) -> Router {
get(scoped_get_workspace_memory_settings) get(scoped_get_workspace_memory_settings)
.put(scoped_update_workspace_memory_settings), .put(scoped_update_workspace_memory_settings),
) )
.route(
"/api/w/{workspace_id}/settings/repository-access",
get(scoped_get_repository_access_projection),
)
.route(
"/api/w/{workspace_id}/settings/repository-access/credentials",
get(scoped_list_repository_ssh_credentials)
.post(scoped_create_repository_ssh_credential),
)
.route(
"/api/w/{workspace_id}/settings/repository-access/credentials/{credential_id}",
delete(scoped_delete_repository_ssh_credential),
)
.route(
"/api/w/{workspace_id}/settings/repository-access/credentials/{credential_id}/rotate",
post(scoped_rotate_repository_ssh_credential),
)
.route(
"/api/w/{workspace_id}/settings/repository-access/host-trusts",
get(scoped_list_repository_ssh_host_trusts)
.post(scoped_put_repository_ssh_host_trust),
)
.route(
"/api/w/{workspace_id}/settings/repository-access/host-trusts/{host_trust_id}",
delete(scoped_delete_repository_ssh_host_trust),
)
.route( .route(
"/api/w/{workspace_id}/config/source-tree", "/api/w/{workspace_id}/config/source-tree",
get(scoped_get_workspace_config_tree), get(scoped_get_workspace_config_tree),
@@ -2986,6 +3028,18 @@ struct ScopedRepositoryPath {
repository_id: String, repository_id: String,
} }
#[derive(Debug, Deserialize)]
struct ScopedRepositoryCredentialPath {
workspace_id: String,
credential_id: String,
}
#[derive(Debug, Deserialize)]
struct ScopedRepositoryHostTrustPath {
workspace_id: String,
host_trust_id: String,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct ScopedProfileArchivePath { struct ScopedProfileArchivePath {
workspace_id: String, workspace_id: String,
@@ -3274,6 +3328,162 @@ async fn scoped_update_workspace_memory_settings(
})) }))
} }
async fn require_manage_repository_secrets(
api: &WorkspaceApi,
workspace_id: &str,
actor: &RequestActor,
) -> ApiResult<()> {
validate_workspace_scope(api, workspace_id)?;
let workspace = api
.store
.get_workspace(workspace_id)
.await?
.ok_or(Error::WorkspaceIdMismatch)?;
if workspace.owner_account_id.as_deref() != Some(actor.account_id.as_str()) {
return Err(Error::WorkspacePermissionDenied(
"ManageSecrets requires the Workspace owner account".to_string(),
)
.into());
}
Ok(())
}
fn active_repository_access_projection(
api: &WorkspaceApi,
workspace_id: &str,
) -> ApiResult<RepositoryAccessProjection> {
let state = api
.config_store
.load_workspace_config(workspace_id)?
.ok_or_else(|| Error::InvalidRecordId("virtual config source tree".into()))?;
Ok(project_repository_access_state(
&*api.store,
&api.repository_secrets,
workspace_id,
&state,
)?)
}
async fn scoped_get_repository_access_projection(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Extension(actor): Extension<RequestActor>,
) -> ApiResult<Json<RepositoryAccessProjection>> {
require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?;
Ok(Json(active_repository_access_projection(
&api,
&path.workspace_id,
)?))
}
async fn scoped_list_repository_ssh_credentials(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Extension(actor): Extension<RequestActor>,
) -> ApiResult<Json<Vec<RepositorySshCredential>>> {
require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?;
let projection = active_repository_access_projection(&api, &path.workspace_id)?;
Ok(Json(
api.repository_secrets
.list_credentials(&path.workspace_id, &projection)?,
))
}
async fn scoped_create_repository_ssh_credential(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Extension(actor): Extension<RequestActor>,
Json(request): Json<CreateRepositorySshCredentialRequest>,
) -> ApiResult<(StatusCode, Json<RepositorySshCredential>)> {
require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?;
let credential =
api.repository_secrets
.create_credential(&path.workspace_id, request, &actor.account_id)?;
Ok((StatusCode::CREATED, Json(credential)))
}
async fn scoped_rotate_repository_ssh_credential(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRepositoryCredentialPath>,
Extension(actor): Extension<RequestActor>,
Json(request): Json<RotateRepositorySshCredentialRequest>,
) -> ApiResult<Json<RepositorySshCredential>> {
require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?;
Ok(Json(api.repository_secrets.rotate_credential(
&path.workspace_id,
&path.credential_id,
request,
&actor.account_id,
)?))
}
async fn scoped_delete_repository_ssh_credential(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRepositoryCredentialPath>,
Extension(actor): Extension<RequestActor>,
Json(request): Json<DeleteRepositorySshCredentialRequest>,
) -> ApiResult<StatusCode> {
require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?;
let projection = active_repository_access_projection(&api, &path.workspace_id)?;
api.repository_secrets.delete_credential(
&path.workspace_id,
&path.credential_id,
request,
&actor.account_id,
&projection,
)?;
Ok(StatusCode::NO_CONTENT)
}
async fn scoped_list_repository_ssh_host_trusts(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Extension(actor): Extension<RequestActor>,
) -> ApiResult<Json<Vec<RepositorySshHostTrust>>> {
require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?;
let projection = active_repository_access_projection(&api, &path.workspace_id)?;
Ok(Json(
api.repository_secrets
.list_host_trusts(&path.workspace_id, &projection)?,
))
}
async fn scoped_put_repository_ssh_host_trust(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>,
Extension(actor): Extension<RequestActor>,
Json(request): Json<PutRepositorySshHostTrustRequest>,
) -> ApiResult<(StatusCode, Json<RepositorySshHostTrust>)> {
require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?;
let status = if request.expected_revision.is_some() {
StatusCode::OK
} else {
StatusCode::CREATED
};
let host_trust =
api.repository_secrets
.put_host_trust(&path.workspace_id, request, &actor.account_id)?;
Ok((status, Json(host_trust)))
}
async fn scoped_delete_repository_ssh_host_trust(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRepositoryHostTrustPath>,
Extension(actor): Extension<RequestActor>,
Json(request): Json<DeleteRepositorySshHostTrustRequest>,
) -> ApiResult<StatusCode> {
require_manage_repository_secrets(&api, &path.workspace_id, &actor).await?;
let projection = active_repository_access_projection(&api, &path.workspace_id)?;
api.repository_secrets.delete_host_trust(
&path.workspace_id,
&path.host_trust_id,
request,
&actor.account_id,
&projection,
)?;
Ok(StatusCode::NO_CONTENT)
}
async fn scoped_get_workspace_config_tree( async fn scoped_get_workspace_config_tree(
State(api): State<WorkspaceApi>, State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedWorkspacePath>, AxumPath(path): AxumPath<ScopedWorkspacePath>,
@@ -3333,6 +3543,12 @@ async fn scoped_commit_workspace_config_tree(
api.config_schema_registry.compose()?, api.config_schema_registry.compose()?,
)?; )?;
crate::prompt_settings::validate_evaluated_prompt_catalog(&candidate.evaluation)?; crate::prompt_settings::validate_evaluated_prompt_catalog(&candidate.evaluation)?;
project_repository_access_candidate(
&*api.store,
&api.repository_secrets,
&path.workspace_id,
&candidate,
)?;
let state = api let state = api
.config_store .config_store
.commit_evaluated_workspace_config(&path.workspace_id, &candidate)?; .commit_evaluated_workspace_config(&path.workspace_id, &candidate)?;
@@ -14070,7 +14286,9 @@ impl ApiError {
impl IntoResponse for ApiError { impl IntoResponse for ApiError {
fn into_response(self) -> Response { fn into_response(self) -> Response {
let status = match &self.error { let status = match &self.error {
Error::BrowserReopenConfirmationRequired => StatusCode::FORBIDDEN, Error::BrowserReopenConfirmationRequired | Error::WorkspacePermissionDenied(_) => {
StatusCode::FORBIDDEN
}
Error::TicketAssignmentConflict(_) Error::TicketAssignmentConflict(_)
| Error::WorkdirAttachmentConflict(_) | Error::WorkdirAttachmentConflict(_)
| Error::WorkspaceConfigConflict(_) => StatusCode::CONFLICT, | Error::WorkspaceConfigConflict(_) => StatusCode::CONFLICT,
@@ -19157,6 +19375,54 @@ mod tests {
assert_eq!(detail.provenance.id, "workspace:triage-errors"); assert_eq!(detail.provenance.id, "workspace:triage-errors");
} }
#[tokio::test]
async fn repository_secret_management_is_owner_only() {
let temp = tempfile::tempdir().unwrap();
let api = test_api(temp.path()).await;
let timestamp = Utc::now().to_rfc3339();
api.store
.upsert_account(&crate::store::AccountRecord {
account_id: "owner-account".to_string(),
kind: "user".to_string(),
handle: "owner".to_string(),
display_name: "Owner".to_string(),
created_at: timestamp.clone(),
updated_at: timestamp,
})
.unwrap();
let mut workspace = api
.store
.get_workspace(TEST_WORKSPACE_ID)
.await
.unwrap()
.unwrap();
workspace.owner_account_id = Some("owner-account".to_string());
api.store.upsert_workspace(&workspace).await.unwrap();
let owner = RequestActor {
user_id: "owner-user".to_string(),
account_id: "owner-account".to_string(),
handle: "owner".to_string(),
display_name: "Owner".to_string(),
auth_method: ActorAuthMethod::BrowserSession,
};
require_manage_repository_secrets(&api, TEST_WORKSPACE_ID, &owner)
.await
.unwrap();
let non_owner = RequestActor {
user_id: "other-user".to_string(),
account_id: "other-account".to_string(),
handle: "other".to_string(),
display_name: "Other".to_string(),
auth_method: ActorAuthMethod::ApiToken,
};
let error = require_manage_repository_secrets(&api, TEST_WORKSPACE_ID, &non_owner)
.await
.unwrap_err();
assert_eq!(error.into_response().status(), StatusCode::FORBIDDEN);
}
async fn test_api_with_recording_backend( async fn test_api_with_recording_backend(
workspace_root: impl Into<PathBuf>, workspace_root: impl Into<PathBuf>,
) -> (WorkspaceApi, Arc<DeterministicExecutionBackend>) { ) -> (WorkspaceApi, Arc<DeterministicExecutionBackend>) {
+176 -22
View File
@@ -252,6 +252,11 @@ const MIGRATIONS: &[Migration] = &[
name: "create Workdir create operations", name: "create Workdir create operations",
apply: create_workdir_create_operations, apply: create_workdir_create_operations,
}, },
Migration {
version: 46,
name: "create Workspace Repository SSH secret authority",
apply: create_repository_ssh_secret_authority,
},
]; ];
struct Migration { struct Migration {
@@ -6703,6 +6708,110 @@ fn create_workdir_create_operations(conn: &Connection) -> Result<()> {
Ok(()) Ok(())
} }
fn create_repository_ssh_secret_authority(conn: &Connection) -> Result<()> {
conn.execute_batch(
r#"
CREATE TABLE repository_ssh_credentials (
workspace_id TEXT NOT NULL,
credential_id TEXT NOT NULL,
name TEXT NOT NULL,
public_key_algorithm TEXT NOT NULL,
public_key_fingerprint TEXT NOT NULL,
current_revision INTEGER NOT NULL CHECK (current_revision >= 1),
status TEXT NOT NULL CHECK (status IN ('active', 'revoked')),
created_at TEXT NOT NULL,
rotated_at TEXT,
PRIMARY KEY (workspace_id, credential_id),
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
);
CREATE TABLE repository_ssh_credential_revisions (
workspace_id TEXT NOT NULL,
credential_id TEXT NOT NULL,
revision INTEGER NOT NULL CHECK (revision >= 1),
public_key_algorithm TEXT NOT NULL,
public_key_fingerprint TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (workspace_id, credential_id, revision),
FOREIGN KEY (workspace_id, credential_id)
REFERENCES repository_ssh_credentials(workspace_id, credential_id)
ON DELETE CASCADE
);
CREATE TABLE server_secret_versions (
workspace_id TEXT NOT NULL,
secret_id TEXT NOT NULL,
revision INTEGER NOT NULL CHECK (revision >= 1),
purpose TEXT NOT NULL CHECK (purpose IN ('private_key', 'passphrase')),
encryption_algorithm TEXT NOT NULL CHECK (encryption_algorithm = 'aes-256-gcm-v1'),
nonce BLOB NOT NULL CHECK (length(nonce) = 12),
ciphertext BLOB NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (workspace_id, secret_id, revision, purpose),
FOREIGN KEY (workspace_id, secret_id, revision)
REFERENCES repository_ssh_credential_revisions(workspace_id, credential_id, revision)
ON DELETE CASCADE
);
CREATE TABLE repository_ssh_host_trusts (
workspace_id TEXT NOT NULL,
host_trust_id TEXT NOT NULL,
hostname TEXT NOT NULL,
port INTEGER NOT NULL CHECK (port >= 1 AND port <= 65535),
key_algorithm TEXT NOT NULL,
host_key TEXT NOT NULL,
fingerprint TEXT NOT NULL,
current_revision INTEGER NOT NULL CHECK (current_revision >= 1),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (workspace_id, host_trust_id),
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
);
CREATE TABLE repository_ssh_host_trust_revisions (
workspace_id TEXT NOT NULL,
host_trust_id TEXT NOT NULL,
revision INTEGER NOT NULL CHECK (revision >= 1),
hostname TEXT NOT NULL,
port INTEGER NOT NULL CHECK (port >= 1 AND port <= 65535),
key_algorithm TEXT NOT NULL,
host_key TEXT NOT NULL,
fingerprint TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (workspace_id, host_trust_id, revision),
FOREIGN KEY (workspace_id, host_trust_id)
REFERENCES repository_ssh_host_trusts(workspace_id, host_trust_id)
ON DELETE CASCADE
);
CREATE TABLE repository_secret_operations (
workspace_id TEXT NOT NULL,
operation_id TEXT NOT NULL,
request_fingerprint TEXT NOT NULL,
resource_kind TEXT NOT NULL CHECK (resource_kind IN ('credential', 'host_trust')),
resource_id TEXT NOT NULL,
result_revision INTEGER NOT NULL CHECK (result_revision >= 1),
created_at TEXT NOT NULL,
PRIMARY KEY (workspace_id, operation_id),
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
);
CREATE TABLE repository_secret_audit_events (
workspace_id TEXT NOT NULL,
event_id TEXT NOT NULL,
kind TEXT NOT NULL,
resource_id TEXT NOT NULL,
revision INTEGER NOT NULL CHECK (revision >= 1),
actor_account_id TEXT NOT NULL,
created_at TEXT NOT NULL,
PRIMARY KEY (workspace_id, event_id),
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
);
CREATE INDEX idx_repository_ssh_credentials_workspace_status
ON repository_ssh_credentials(workspace_id, status, credential_id);
CREATE INDEX idx_repository_ssh_host_trusts_workspace_host
ON repository_ssh_host_trusts(workspace_id, hostname, port);
CREATE INDEX idx_repository_secret_audit_workspace_created
ON repository_secret_audit_events(workspace_id, created_at, event_id);
"#,
)?;
Ok(())
}
fn create_workspace_catalog_operations(conn: &Connection) -> Result<()> { fn create_workspace_catalog_operations(conn: &Connection) -> Result<()> {
conn.execute_batch( conn.execute_batch(
r#" r#"
@@ -9588,7 +9697,7 @@ mod tests {
apply_migrations(&conn).unwrap(); apply_migrations(&conn).unwrap();
assert_eq!(current_schema_version(&conn).unwrap(), 45); assert_eq!(current_schema_version(&conn).unwrap(), 46);
let remote = conn let remote = conn
.query_row( .query_row(
"SELECT source_kind, source_uri, source_revision, source_fingerprint, observed_status \ "SELECT source_kind, source_uri, source_revision, source_fingerprint, observed_status \
@@ -9666,7 +9775,7 @@ mod tests {
let before = std::fs::read(&path).unwrap(); let before = std::fs::read(&path).unwrap();
let plan = SqliteWorkspaceStore::migration_plan(&path).unwrap(); let plan = SqliteWorkspaceStore::migration_plan(&path).unwrap();
assert_eq!(plan.current_schema_version, 36); assert_eq!(plan.current_schema_version, 36);
assert_eq!(plan.target_schema_version, 45); assert_eq!(plan.target_schema_version, 46);
assert!(plan.migration_required); assert!(plan.migration_required);
assert_eq!(plan.worker_count, 1); assert_eq!(plan.worker_count, 1);
assert_eq!(plan.mappings[0].legacy_worker_id, 7); assert_eq!(plan.mappings[0].legacy_worker_id, 7);
@@ -9680,7 +9789,7 @@ mod tests {
store store
.with_conn(|conn| { .with_conn(|conn| {
assert!(table_exists(conn, "worker_diagnostics_archives")?); assert!(table_exists(conn, "worker_diagnostics_archives")?);
assert_eq!(current_schema_version(conn)?, 45); assert_eq!(current_schema_version(conn)?, 46);
Ok(()) Ok(())
}) })
.unwrap(); .unwrap();
@@ -9816,7 +9925,7 @@ mod tests {
), ),
] ]
); );
assert_eq!(current_schema_version(&conn).unwrap(), 45); assert_eq!(current_schema_version(&conn).unwrap(), 46);
let foreign_key_error: Option<String> = conn let foreign_key_error: Option<String> = conn
.query_row("PRAGMA foreign_key_check", [], |row| row.get(0)) .query_row("PRAGMA foreign_key_check", [], |row| row.get(0))
.optional() .optional()
@@ -9945,7 +10054,7 @@ INSERT INTO worker_orphan_diagnostics (
apply_migrations(&conn).unwrap(); apply_migrations(&conn).unwrap();
assert_eq!(current_schema_version(&conn).unwrap(), 45); assert_eq!(current_schema_version(&conn).unwrap(), 46);
assert!(!table_exists(&conn, "worker_control_delegation_operations").unwrap()); assert!(!table_exists(&conn, "worker_control_delegation_operations").unwrap());
let controller_worker_id: String = conn let controller_worker_id: String = conn
.query_row( .query_row(
@@ -10063,7 +10172,7 @@ INSERT INTO worker_orphan_diagnostics (
apply_migrations(&conn).unwrap(); apply_migrations(&conn).unwrap();
assert_eq!(current_schema_version(&conn).unwrap(), 45); assert_eq!(current_schema_version(&conn).unwrap(), 46);
assert!(table_exists(&conn, "worker_workdir_attachment_reservations").unwrap()); assert!(table_exists(&conn, "worker_workdir_attachment_reservations").unwrap());
} }
@@ -10081,7 +10190,7 @@ INSERT INTO worker_orphan_diagnostics (
apply_migrations(&conn).unwrap(); apply_migrations(&conn).unwrap();
assert_eq!(current_schema_version(&conn).unwrap(), 45); assert_eq!(current_schema_version(&conn).unwrap(), 46);
let settings = conn let settings = conn
.query_row( .query_row(
"SELECT settings_revision, language FROM workspace_memory_settings \ "SELECT settings_revision, language FROM workspace_memory_settings \
@@ -10122,7 +10231,7 @@ CREATE TABLE flow_events (event_id TEXT PRIMARY KEY);
apply_migrations(&conn).unwrap(); apply_migrations(&conn).unwrap();
assert_eq!(current_schema_version(&conn).unwrap(), 45); assert_eq!(current_schema_version(&conn).unwrap(), 46);
assert!(table_exists(&conn, "flow_sources").unwrap()); assert!(table_exists(&conn, "flow_sources").unwrap());
assert!(table_exists(&conn, "flow_source_revisions").unwrap()); assert!(table_exists(&conn, "flow_source_revisions").unwrap());
assert!(!table_exists(&conn, "flow_instances").unwrap()); assert!(!table_exists(&conn, "flow_instances").unwrap());
@@ -10189,7 +10298,7 @@ INSERT INTO worker_workdir_attachment_reservations (
apply_migrations(&conn).unwrap(); apply_migrations(&conn).unwrap();
assert_eq!(current_schema_version(&conn).unwrap(), 45); assert_eq!(current_schema_version(&conn).unwrap(), 46);
let repositories_sql: String = conn let repositories_sql: String = conn
.query_row( .query_row(
"SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'repositories'", "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'repositories'",
@@ -10372,7 +10481,7 @@ INSERT INTO workdir_registry (
let db = dir.path().join("control-plane.sqlite"); let db = dir.path().join("control-plane.sqlite");
let store = SqliteWorkspaceStore::open(&db).unwrap(); let store = SqliteWorkspaceStore::open(&db).unwrap();
assert_eq!(store.schema_version().await.unwrap(), 45); assert_eq!(store.schema_version().await.unwrap(), 46);
assert!( assert!(
!store !store
.with_conn(|conn| table_exists(conn, "worker_workspace_credentials")) .with_conn(|conn| table_exists(conn, "worker_workspace_credentials"))
@@ -10389,7 +10498,7 @@ INSERT INTO workdir_registry (
store.upsert_workspace(&record).await.unwrap(); store.upsert_workspace(&record).await.unwrap();
let reopened = SqliteWorkspaceStore::open(&db).unwrap(); let reopened = SqliteWorkspaceStore::open(&db).unwrap();
assert_eq!(reopened.schema_version().await.unwrap(), 45); assert_eq!(reopened.schema_version().await.unwrap(), 46);
assert_eq!( assert_eq!(
reopened.get_workspace("local-dev").await.unwrap(), reopened.get_workspace("local-dev").await.unwrap(),
Some(record) Some(record)
@@ -11143,7 +11252,7 @@ INSERT INTO worker_registry (
let migrated = SqliteWorkspaceStore::open(&db_path).unwrap(); let migrated = SqliteWorkspaceStore::open(&db_path).unwrap();
migrated migrated
.with_conn(|conn| { .with_conn(|conn| {
assert_eq!(current_schema_version(conn)?, 45); assert_eq!(current_schema_version(conn)?, 46);
assert_eq!( assert_eq!(
conn.query_row("PRAGMA foreign_keys", [], |row| row.get::<_, i64>(0))?, conn.query_row("PRAGMA foreign_keys", [], |row| row.get::<_, i64>(0))?,
1, 1,
@@ -11492,14 +11601,21 @@ INSERT INTO worker_registry (
configure_sqlite(&conn).unwrap(); configure_sqlite(&conn).unwrap();
apply_migrations(&conn).unwrap(); apply_migrations(&conn).unwrap();
conn.execute_batch( conn.execute_batch(
"DROP TABLE workdir_create_operations; "DROP TABLE repository_secret_audit_events;
DELETE FROM __yoi_schema_migrations WHERE version = 45;", DROP TABLE repository_secret_operations;
DROP TABLE server_secret_versions;
DROP TABLE repository_ssh_credential_revisions;
DROP TABLE repository_ssh_credentials;
DROP TABLE repository_ssh_host_trust_revisions;
DROP TABLE repository_ssh_host_trusts;
DROP TABLE workdir_create_operations;
DELETE FROM __yoi_schema_migrations WHERE version IN (45, 46);",
) )
.unwrap(); .unwrap();
assert_eq!(current_schema_version(&conn).unwrap(), 44); assert_eq!(current_schema_version(&conn).unwrap(), 44);
apply_migrations(&conn).unwrap(); apply_migrations(&conn).unwrap();
assert_eq!(current_schema_version(&conn).unwrap(), 45); assert_eq!(current_schema_version(&conn).unwrap(), 46);
assert!(table_exists(&conn, "workdir_create_operations").unwrap()); assert!(table_exists(&conn, "workdir_create_operations").unwrap());
let columns = table_columns(&conn, "workdir_create_operations").unwrap(); let columns = table_columns(&conn, "workdir_create_operations").unwrap();
for required in [ for required in [
@@ -11518,19 +11634,57 @@ INSERT INTO worker_registry (
} }
} }
#[test]
fn schema_v46_adds_repository_ssh_secret_authority_to_v45_database() {
let conn = Connection::open_in_memory().unwrap();
configure_sqlite(&conn).unwrap();
apply_migrations(&conn).unwrap();
conn.execute_batch(
"DROP TABLE repository_secret_audit_events;
DROP TABLE repository_secret_operations;
DROP TABLE server_secret_versions;
DROP TABLE repository_ssh_credential_revisions;
DROP TABLE repository_ssh_credentials;
DROP TABLE repository_ssh_host_trust_revisions;
DROP TABLE repository_ssh_host_trusts;
DELETE FROM __yoi_schema_migrations WHERE version = 46;",
)
.unwrap();
assert_eq!(current_schema_version(&conn).unwrap(), 45);
apply_migrations(&conn).unwrap();
assert_eq!(current_schema_version(&conn).unwrap(), 46);
for table in [
"repository_ssh_credentials",
"repository_ssh_credential_revisions",
"server_secret_versions",
"repository_ssh_host_trusts",
"repository_ssh_host_trust_revisions",
"repository_secret_operations",
"repository_secret_audit_events",
] {
assert!(table_exists(&conn, table).unwrap(), "missing table {table}");
}
let foreign_key_error: Option<String> = conn
.query_row("PRAGMA foreign_key_check", [], |row| row.get(0))
.optional()
.unwrap();
assert!(foreign_key_error.is_none());
}
#[test] #[test]
fn server_refuses_a_database_from_a_newer_schema_generation() { fn server_refuses_a_database_from_a_newer_schema_generation() {
let conn = Connection::open_in_memory().unwrap(); let conn = Connection::open_in_memory().unwrap();
configure_sqlite(&conn).unwrap(); configure_sqlite(&conn).unwrap();
apply_migrations(&conn).unwrap(); apply_migrations(&conn).unwrap();
conn.execute( conn.execute(
"INSERT INTO __yoi_schema_migrations (version, name) VALUES (46, 'future')", "INSERT INTO __yoi_schema_migrations (version, name) VALUES (47, 'future')",
[], [],
) )
.unwrap(); .unwrap();
let error = apply_migrations(&conn).unwrap_err().to_string(); let error = apply_migrations(&conn).unwrap_err().to_string();
assert!(error.contains("schema version 46 is newer"), "{error}"); assert!(error.contains("schema version 47 is newer"), "{error}");
assert!(error.contains("refusing to serve"), "{error}"); assert!(error.contains("refusing to serve"), "{error}");
} }
@@ -11751,7 +11905,7 @@ VALUES ('workspace-b', 'ticket-b', 'related', 'ticket-a', NULL, 'tester', '2026-
apply_migrations(&mut conn).unwrap(); apply_migrations(&mut conn).unwrap();
assert_eq!(current_schema_version(&conn).unwrap(), 45); assert_eq!(current_schema_version(&conn).unwrap(), 46);
let workspace_id: Option<String> = conn let workspace_id: Option<String> = conn
.query_row( .query_row(
"SELECT workspace_id FROM trusted_runtime_records WHERE runtime_id = 'runtime-a'", "SELECT workspace_id FROM trusted_runtime_records WHERE runtime_id = 'runtime-a'",
@@ -12374,7 +12528,7 @@ WHERE workspace_id = 'workspace-a'
.unwrap(); .unwrap();
let store = SqliteWorkspaceStore::from_connection(conn).unwrap(); let store = SqliteWorkspaceStore::from_connection(conn).unwrap();
assert_eq!(store.schema_version().await.unwrap(), 45); assert_eq!(store.schema_version().await.unwrap(), 46);
store store
.with_conn(|conn| { .with_conn(|conn| {
@@ -12563,7 +12717,7 @@ CREATE TABLE ticket_assignment_operations (
#[tokio::test] #[tokio::test]
async fn repository_records_round_trip() { async fn repository_records_round_trip() {
let store = SqliteWorkspaceStore::in_memory().unwrap(); let store = SqliteWorkspaceStore::in_memory().unwrap();
assert_eq!(store.schema_version().await.unwrap(), 45); assert_eq!(store.schema_version().await.unwrap(), 46);
let workspace = WorkspaceRecord { let workspace = WorkspaceRecord {
workspace_id: "local-dev".to_string(), workspace_id: "local-dev".to_string(),
owner_account_id: None, owner_account_id: None,
@@ -12641,7 +12795,7 @@ CREATE TABLE ticket_assignment_operations (
#[tokio::test] #[tokio::test]
async fn memory_authority_records_round_trip_and_close_staging() { async fn memory_authority_records_round_trip_and_close_staging() {
let store = SqliteWorkspaceStore::in_memory().unwrap(); let store = SqliteWorkspaceStore::in_memory().unwrap();
assert_eq!(store.schema_version().await.unwrap(), 45); assert_eq!(store.schema_version().await.unwrap(), 46);
let workspace = WorkspaceRecord { let workspace = WorkspaceRecord {
workspace_id: "local-dev".to_string(), workspace_id: "local-dev".to_string(),
owner_account_id: None, owner_account_id: None,
@@ -13048,7 +13202,7 @@ CREATE TABLE ticket_assignment_operations (
#[tokio::test] #[tokio::test]
async fn account_and_login_records_round_trip() { async fn account_and_login_records_round_trip() {
let store = SqliteWorkspaceStore::in_memory().unwrap(); let store = SqliteWorkspaceStore::in_memory().unwrap();
assert_eq!(store.schema_version().await.unwrap(), 45); assert_eq!(store.schema_version().await.unwrap(), 46);
let now = "2026-07-22T00:00:00Z".to_string(); let now = "2026-07-22T00:00:00Z".to_string();
let account = AccountRecord { let account = AccountRecord {
account_id: "acct-user-alice".to_string(), account_id: "acct-user-alice".to_string(),
+1 -1
View File
@@ -4,6 +4,6 @@ Use the available typed Ticket tools as the authority for Ticket reads and mutat
Read the relevant Ticket before making implementation, routing, review, state, or closure decisions. Do not infer the current contract from an id, title, notification, or remembered summary alone. Check related or potentially duplicate Tickets when creating or materially rescoping work. Read the relevant Ticket before making implementation, routing, review, state, or closure decisions. Do not infer the current contract from an id, title, notification, or remembered summary alone. Check related or potentially duplicate Tickets when creating or materially rescoping work.
Keep durable Ticket records centered on user intent, confirmed background, requirements, acceptance criteria, binding decisions, and implementation/review evidence. Use `QueryObjective` for bounded Objective discovery and `ShowObjective` for authoritative revision and linked-Ticket context when coordinating broader work. Separate confirmed facts from user claims, hypotheses, and open questions. Avoid prematurely turning implementation tactics into requirements. Keep durable Ticket records centered on user intent, confirmed background, requirements, acceptance criteria, binding decisions, and implementation/review evidence. For implementation and review workflows, keep routine revision, verdict, fix, and rereview evidence on the Merge Request; use Ticket comments only for blockers or decisions requiring orchestration attention and the final approved handoff. Use `QueryObjective` for bounded Objective discovery and `ShowObjective` for authoritative revision and linked-Ticket context when coordinating broader work. Separate confirmed facts from user claims, hypotheses, and open questions. Avoid prematurely turning implementation tactics into requirements.
Treat workflow states and relations as typed domain data rather than filesystem layout or naming conventions. Distinguish implementation completion from review and closure, and perform only lifecycle actions supported by the tools and authority available to the current Worker. Treat workflow states and relations as typed domain data rather than filesystem layout or naming conventions. Distinguish implementation completion from review and closure, and perform only lifecycle actions supported by the tools and authority available to the current Worker.
+2
View File
@@ -1,5 +1,7 @@
You are the assigned Coder. Implement the requested scope in the provided Workdir and keep durable evidence on the Ticket and its Merge Request. You are the assigned Coder. Implement the requested scope in the provided Workdir and keep durable evidence on the Ticket and its Merge Request.
Use the Merge Request as the routine authority for review requests, verdicts, fixes, and rereview cycles. Do not add a Ticket comment for each review or fix iteration. Add a Ticket comment only when a blocker or decision requires Orchestrator attention, or once after approval to hand off the final implementation and validation evidence.
Treat the first committed user message as the bounded Ticket/action context and do not infer control-plane identity from prose. Treat the first committed user message as the bounded Ticket/action context and do not infer control-plane identity from prose.
Before opening a Merge Request, publish only the committed Ticket work branch with a normal non-force push and verify that the Ticket repository remote resolves it to the exact local `HEAD`; a local branch name or dirty Workdir is not immutable review evidence. Do not push the target branch, tags, or unrelated refs, and never force-push. Before opening a Merge Request, publish only the committed Ticket work branch with a normal non-force push and verify that the Ticket repository remote resolves it to the exact local `HEAD`; a local branch name or dirty Workdir is not immutable review evidence. Do not push the target branch, tags, or unrelated refs, and never force-push.
@@ -38,14 +38,36 @@ Deno.test("settings section navigation stays under the settings route", () => {
} }
}); });
Deno.test("settings shell advertises no fake browser admin model", () => { Deno.test("settings shell advertises scoped account authority", () => {
assert( assert(
SETTINGS_PERMISSION_NOTICE.includes("no browser user, role, permission"), SETTINGS_PERMISSION_NOTICE.includes("authenticated account authority"),
"notice should explicitly deny a browser permission model", "notice should identify authenticated account authority",
); );
assert( assert(
SETTINGS_PERMISSION_NOTICE.includes("does not create an admin role"), SETTINGS_PERMISSION_NOTICE.includes("current Workspace owner"),
"notice should not imply an admin role exists", "notice should state the Repository secret permission boundary",
);
assert(
SETTINGS_PERMISSION_NOTICE.includes("does not expose secret material"),
"notice should not imply that stored secret material is readable",
);
});
Deno.test("Repository access settings are editable and canonically routed", () => {
const section = SETTINGS_SECTIONS.find((entry) =>
entry.id === "repository-access"
);
assert(
section?.status === "editable",
"Repository Access should be editable",
);
assert(
settingsSectionHref("repository-access") === "/settings/repository-access",
"Repository Access should have a dedicated settings route",
);
assert(
section?.bullets.join("\n").includes("write-only"),
"Repository Access copy should preserve write-only secret semantics",
); );
}); });
@@ -8,6 +8,7 @@ export type SettingsSectionId =
| "runtime-connections" | "runtime-connections"
| "runtime-inventory" | "runtime-inventory"
| "configuration-sources" | "configuration-sources"
| "repository-access"
| "profile-sources" | "profile-sources"
| "backend-config" | "backend-config"
| "workspace-identity"; | "workspace-identity";
@@ -72,7 +73,7 @@ export type RemoteRuntimeTestResponse = {
export const SETTINGS_ROUTE = "/settings"; export const SETTINGS_ROUTE = "/settings";
export const SETTINGS_PERMISSION_NOTICE = export const SETTINGS_PERMISSION_NOTICE =
"Yoi currently has no browser user, role, permission, or multi-user authorization model. This local settings surface uses typed Backend APIs only; it does not create an admin role or grant broad mutation authority."; "Workspace settings use authenticated account authority and Workspace-scoped typed Backend APIs. Repository secret management requires the current Workspace owner; this surface does not expose secret material or grant Runtime execution authority.";
export const SETTINGS_SECTIONS: readonly SettingsSection[] = [ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
{ {
@@ -111,6 +112,18 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
"Profile launch data is projected from this active revision; remaining Skill, Prompt, and Plugin consumers migrate in their follow-up cutovers.", "Profile launch data is projected from this active revision; remaining Skill, Prompt, and Plugin consumers migrate in their follow-up cutovers.",
], ],
}, },
{
id: "repository-access",
label: "Repository Access",
status: "editable",
summary:
"Manage Workspace-scoped SSH credentials and pinned host keys without exposing stored secret material.",
bullets: [
"Private keys and passphrases are write-only; list and detail responses contain public metadata only.",
"Host trust requires an explicitly pinned key and never uses accept-new or TOFU.",
"Repository bindings are committed through the shared Workspace configuration editor and validated against these records.",
],
},
{ {
id: "profile-sources", id: "profile-sources",
label: "Profile Sources", label: "Profile Sources",
@@ -175,6 +188,8 @@ export function settingsSectionHref(id: SettingsSectionId): string {
return `${SETTINGS_ROUTE}/runtimes`; return `${SETTINGS_ROUTE}/runtimes`;
case "configuration-sources": case "configuration-sources":
return `${SETTINGS_ROUTE}/configuration`; return `${SETTINGS_ROUTE}/configuration`;
case "repository-access":
return `${SETTINGS_ROUTE}/repository-access`;
case "profile-sources": case "profile-sources":
return `${SETTINGS_ROUTE}/profiles`; return `${SETTINGS_ROUTE}/profiles`;
case "workspace-identity": case "workspace-identity":
@@ -31,6 +31,10 @@
<dt>Source</dt> <dt>Source</dt>
<dd>{data.repository.item.source.kind} · {data.repository.item.source.uri}</dd> <dd>{data.repository.item.source.kind} · {data.repository.item.source.uri}</dd>
</div> </div>
<div>
<dt>Repository access</dt>
<dd><a href={`/w/${encodeURIComponent(data.workspace.workspace_id)}/settings/repository-access`}>Manage SSH credentials and pinned host keys</a></dd>
</div>
<div> <div>
<dt>Source revision</dt> <dt>Source revision</dt>
<dd>{data.repository.item.source_revision} · {data.repository.item.source_fingerprint}</dd> <dd>{data.repository.item.source_revision} · {data.repository.item.source_fingerprint}</dd>
@@ -0,0 +1,242 @@
<script lang="ts">
import { untrack } from 'svelte';
import type { PageProps } from './$types';
import type { RepositorySshCredential, RepositorySshHostTrust } from './+page';
let { data }: PageProps = $props();
let credentials = $state<RepositorySshCredential[]>(untrack(() => data.credentials));
let hostTrusts = $state<RepositorySshHostTrust[]>(untrack(() => data.hostTrusts));
let message = $state<string | null>(null);
let pending = $state(false);
let credentialId = $state('');
let credentialName = $state('');
let privateKey = $state('');
let passphrase = $state('');
let rotateCredentialId = $state<string | null>(null);
let rotatePrivateKey = $state('');
let rotatePassphrase = $state('');
let hostTrustId = $state('');
let hostname = $state('');
let port = $state(22);
let hostKey = $state('');
let hostExpectedRevision = $state<number | null>(null);
const base = $derived(`/api/w/${encodeURIComponent(data.workspaceId)}/settings/repository-access`);
function operationId(prefix: string): string {
return `${prefix}-${crypto.randomUUID()}`;
}
async function request<T>(path: string, method: string, body: unknown): Promise<T> {
const response = await fetch(`${base}${path}`, {
method,
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body)
});
if (!response.ok) {
let detail = `request failed (${response.status})`;
try {
const payload = (await response.json()) as { error?: string; message?: string };
detail = payload.message ?? payload.error ?? detail;
} catch {
// Do not surface submitted secret values from response bodies.
}
throw new Error(detail);
}
if (response.status === 204) return undefined as T;
return (await response.json()) as T;
}
async function createCredential() {
pending = true;
message = null;
try {
const created = await request<RepositorySshCredential>('/credentials', 'POST', {
operation_id: operationId('credential-create'),
credential_id: credentialId,
name: credentialName,
private_key: privateKey,
passphrase: passphrase || null
});
credentials = [...credentials, created].sort((a, b) => a.credential_id.localeCompare(b.credential_id));
credentialId = '';
credentialName = '';
privateKey = '';
passphrase = '';
message = `Credential ${created.credential_id} created. Pasted secret fields were cleared.`;
} catch (error) {
message = error instanceof Error ? error.message : 'Credential creation failed';
} finally {
pending = false;
}
}
async function rotateCredential(credential: RepositorySshCredential) {
pending = true;
message = null;
try {
const rotated = await request<RepositorySshCredential>(
`/credentials/${encodeURIComponent(credential.credential_id)}/rotate`,
'POST',
{
operation_id: operationId('credential-rotate'),
expected_revision: credential.current_revision,
private_key: rotatePrivateKey,
passphrase: rotatePassphrase || null
}
);
credentials = credentials.map((entry) => entry.credential_id === rotated.credential_id ? rotated : entry);
rotatePrivateKey = '';
rotatePassphrase = '';
rotateCredentialId = null;
message = `Credential ${rotated.credential_id} rotated to revision ${rotated.current_revision}. Pasted secret fields were cleared.`;
} catch (error) {
message = error instanceof Error ? error.message : 'Credential rotation failed';
} finally {
pending = false;
}
}
async function deleteCredential(credential: RepositorySshCredential) {
if (!confirm(`Delete credential ${credential.credential_id}?`)) return;
pending = true;
message = null;
try {
await request(`/credentials/${encodeURIComponent(credential.credential_id)}`, 'DELETE', {
operation_id: operationId('credential-delete'),
expected_revision: credential.current_revision
});
credentials = credentials.filter((entry) => entry.credential_id !== credential.credential_id);
message = `Credential ${credential.credential_id} deleted.`;
} catch (error) {
message = error instanceof Error ? error.message : 'Credential deletion failed';
} finally {
pending = false;
}
}
async function createHostTrust() {
pending = true;
message = null;
try {
const created = await request<RepositorySshHostTrust>('/host-trusts', 'POST', {
operation_id: operationId('host-trust-create'),
host_trust_id: hostTrustId,
hostname,
port,
host_key: hostKey,
expected_revision: hostExpectedRevision
});
hostTrusts = hostExpectedRevision === null
? [...hostTrusts, created].sort((a, b) => a.host_trust_id.localeCompare(b.host_trust_id))
: hostTrusts.map((entry) => entry.host_trust_id === created.host_trust_id ? created : entry);
hostTrustId = '';
hostname = '';
port = 22;
hostKey = '';
hostExpectedRevision = null;
message = `Host trust ${created.host_trust_id} saved at revision ${created.current_revision}.`;
} catch (error) {
message = error instanceof Error ? error.message : 'Host trust creation failed';
} finally {
pending = false;
}
}
function editHostTrust(hostTrust: RepositorySshHostTrust) {
hostTrustId = hostTrust.host_trust_id;
hostname = hostTrust.hostname;
port = hostTrust.port;
hostKey = hostTrust.host_key;
hostExpectedRevision = hostTrust.current_revision;
}
async function deleteHostTrust(hostTrust: RepositorySshHostTrust) {
if (!confirm(`Delete host trust ${hostTrust.host_trust_id}?`)) return;
pending = true;
message = null;
try {
await request(`/host-trusts/${encodeURIComponent(hostTrust.host_trust_id)}`, 'DELETE', {
operation_id: operationId('host-trust-delete'),
expected_revision: hostTrust.current_revision
});
hostTrusts = hostTrusts.filter((entry) => entry.host_trust_id !== hostTrust.host_trust_id);
message = `Host trust ${hostTrust.host_trust_id} deleted.`;
} catch (error) {
message = error instanceof Error ? error.message : 'Host trust deletion failed';
} finally {
pending = false;
}
}
</script>
<svelte:head><title>Repository Access · Yoi Workspace</title></svelte:head>
<section class="card settings-section">
<header class="settings-section-header">
<div><p class="eyebrow">owner only</p><h2>Repository Access</h2></div>
<span class="badge success">encrypted</span>
</header>
<p>Manage Workspace-scoped SSH credentials and pinned host keys. Private keys and passphrases are write-only and never returned by this page.</p>
{#if message}<p class="status-message">{message}</p>{/if}
<div class="settings-runtime-list">
<h3>SSH credentials</h3>
{#if credentials.length === 0}<p>No credentials configured.</p>{/if}
{#each credentials as credential (credential.credential_id)}
<div class="card">
<strong>{credential.name}</strong> <code>{credential.credential_id}</code>
<p>{credential.public_key_algorithm} · {credential.public_key_fingerprint} · revision {credential.current_revision}</p>
<p>References: {credential.referenced_repositories.join(', ') || 'none'}</p>
<div class="settings-action-row">
<button type="button" onclick={() => (rotateCredentialId = rotateCredentialId === credential.credential_id ? null : credential.credential_id)}>Rotate</button>
<button type="button" class="danger" disabled={pending || credential.referenced_repositories.length > 0} onclick={() => void deleteCredential(credential)}>Delete</button>
</div>
{#if rotateCredentialId === credential.credential_id}
<form class="settings-runtime-form" onsubmit={(event) => { event.preventDefault(); void rotateCredential(credential); }}>
<label><span>New private key</span><textarea bind:value={rotatePrivateKey} required rows="8" autocomplete="off"></textarea></label>
<label><span>Passphrase (only for an encrypted key)</span><input type="password" bind:value={rotatePassphrase} autocomplete="new-password" /></label>
<button type="submit" disabled={pending}>Rotate credential</button>
</form>
{/if}
</div>
{/each}
<form class="settings-runtime-form" onsubmit={(event) => { event.preventDefault(); void createCredential(); }}>
<h3>Add SSH credential</h3>
<label><span>Credential id</span><input bind:value={credentialId} required pattern="[A-Za-z0-9_.-]+" maxlength="128" /></label>
<label><span>Name</span><input bind:value={credentialName} required maxlength="200" /></label>
<label><span>OpenSSH private key (ssh-ed25519)</span><textarea bind:value={privateKey} required rows="10" autocomplete="off"></textarea></label>
<label><span>Passphrase (only for an encrypted key)</span><input type="password" bind:value={passphrase} autocomplete="new-password" /></label>
<button type="submit" disabled={pending}>Add credential</button>
</form>
</div>
<div class="settings-runtime-list">
<h3>Pinned SSH host keys</h3>
{#if hostTrusts.length === 0}<p>No host trust records configured.</p>{/if}
{#each hostTrusts as hostTrust (hostTrust.host_trust_id)}
<div class="card">
<strong>{hostTrust.hostname}:{hostTrust.port}</strong> <code>{hostTrust.host_trust_id}</code>
<p>{hostTrust.key_algorithm} · {hostTrust.fingerprint} · revision {hostTrust.current_revision}</p>
<p>References: {hostTrust.referenced_repositories.join(', ') || 'none'}</p>
<div class="settings-action-row">
<button type="button" onclick={() => editHostTrust(hostTrust)}>Rotate key</button>
<button type="button" class="danger" disabled={pending || hostTrust.referenced_repositories.length > 0} onclick={() => void deleteHostTrust(hostTrust)}>Delete</button>
</div>
</div>
{/each}
<form class="settings-runtime-form" onsubmit={(event) => { event.preventDefault(); void createHostTrust(); }}>
<h3>{hostExpectedRevision === null ? 'Add pinned host key' : 'Rotate pinned host key'}</h3>
<label><span>Host trust id</span><input bind:value={hostTrustId} disabled={hostExpectedRevision !== null} required pattern="[A-Za-z0-9_.-]+" maxlength="128" /></label>
<label><span>Hostname</span><input bind:value={hostname} required /></label>
<label><span>Port</span><input type="number" bind:value={port} min="1" max="65535" required /></label>
<label><span>OpenSSH public host key (ssh-ed25519)</span><textarea bind:value={hostKey} required rows="4"></textarea></label>
<button type="submit" disabled={pending}>{hostExpectedRevision === null ? 'Add host key' : 'Save new revision'}</button>
{#if hostExpectedRevision !== null}<button type="button" onclick={() => { hostTrustId = ''; hostname = ''; port = 22; hostKey = ''; hostExpectedRevision = null; }}>Cancel</button>{/if}
</form>
</div>
</section>
@@ -0,0 +1,50 @@
import type { PageLoad } from "./$types";
import { loadJson } from "$lib/workspace/api/http";
export interface RepositorySshCredential {
credential_id: string;
workspace_id: string;
name: string;
public_key_algorithm: string;
public_key_fingerprint: string;
current_revision: number;
status: string;
created_at: string;
rotated_at: string | null;
referenced_repositories: string[];
}
export interface RepositorySshHostTrust {
host_trust_id: string;
workspace_id: string;
hostname: string;
port: number;
key_algorithm: string;
host_key: string;
fingerprint: string;
current_revision: number;
created_at: string;
updated_at: string;
referenced_repositories: string[];
}
export const load: PageLoad = async ({ fetch, params }) => {
const base = `/api/w/${
encodeURIComponent(params.workspaceId)
}/settings/repository-access`;
const [credentialResult, hostTrustResult] = await Promise.all([
loadJson<RepositorySshCredential[]>(fetch, `${base}/credentials`),
loadJson<RepositorySshHostTrust[]>(fetch, `${base}/host-trusts`),
]);
if (!credentialResult.data || !hostTrustResult.data) {
throw new Error(
credentialResult.error ?? hostTrustResult.error ??
"Repository access settings unavailable",
);
}
return {
workspaceId: params.workspaceId,
credentials: credentialResult.data,
hostTrusts: hostTrustResult.data,
};
};