Compare commits

...

4 Commits

Author SHA1 Message Date
viyatb-oai
c39f880b15 refactor(secrets): add backend trait 2026-01-29 00:17:01 -08:00
viyatb-oai
a2dfb93d85 docs(secrets): clarify local backend intent 2026-01-29 00:17:01 -08:00
viyatb-oai
fb45925072 feat(core): wire secrets backend config 2026-01-28 23:35:47 -08:00
viyatb-oai
73811db351 feat(secrets): add codex-secrets crate 2026-01-28 23:15:33 -08:00
11 changed files with 1086 additions and 28 deletions

542
codex-rs/Cargo.lock generated
View File

@@ -175,6 +175,16 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aes"
version = "0.8.4"
@@ -186,6 +196,49 @@ dependencies = [
"cpufeatures",
]
[[package]]
name = "age"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf640be7658959746f1f0f2faab798f6098a9436a8e18e148d18bc9875e13c4b"
dependencies = [
"age-core",
"base64 0.21.7",
"bech32",
"chacha20poly1305",
"cookie-factory",
"hmac",
"i18n-embed",
"i18n-embed-fl",
"lazy_static",
"nom 7.1.3",
"pin-project",
"rand 0.8.5",
"rust-embed",
"scrypt",
"sha2",
"subtle",
"x25519-dalek",
"zeroize",
]
[[package]]
name = "age-core"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2bf6a89c984ca9d850913ece2da39e1d200563b0a94b002b253beee4c5acf99"
dependencies = [
"base64 0.21.7",
"chacha20poly1305",
"cookie-factory",
"hkdf",
"io_tee",
"nom 7.1.3",
"rand 0.8.5",
"secrecy",
"sha2",
]
[[package]]
name = "ahash"
version = "0.8.12"
@@ -330,7 +383,7 @@ name = "app_test_support"
version = "0.0.0"
dependencies = [
"anyhow",
"base64",
"base64 0.22.1",
"chrono",
"codex-app-server-protocol",
"codex-core",
@@ -686,6 +739,12 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "base64"
version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
@@ -698,6 +757,21 @@ version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e050f626429857a27ddccb31e0aca21356bfa709c04041aefddac081a8f068a"
[[package]]
name = "basic-toml"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
dependencies = [
"serde",
]
[[package]]
name = "bech32"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445"
[[package]]
name = "beef"
version = "0.5.2"
@@ -717,7 +791,7 @@ dependencies = [
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"rustc-hash 2.1.1",
"shlex",
"syn 2.0.104",
]
@@ -911,6 +985,30 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
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",
]
[[package]]
name = "chacha20poly1305"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
dependencies = [
"aead",
"chacha20",
"cipher",
"poly1305",
"zeroize",
]
[[package]]
name = "chardetng"
version = "0.1.17"
@@ -950,6 +1048,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
"zeroize",
]
[[package]]
@@ -1081,7 +1180,7 @@ dependencies = [
"anyhow",
"app_test_support",
"axum",
"base64",
"base64 0.22.1",
"chrono",
"codex-app-server-protocol",
"codex-arg0",
@@ -1295,7 +1394,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"async-trait",
"base64",
"base64 0.22.1",
"chrono",
"clap",
"codex-cloud-tasks-client",
@@ -1358,7 +1457,7 @@ dependencies = [
"assert_matches",
"async-channel",
"async-trait",
"base64",
"base64 0.22.1",
"chardetng",
"chrono",
"clap",
@@ -1376,6 +1475,7 @@ dependencies = [
"codex-otel",
"codex-protocol",
"codex-rmcp-client",
"codex-secrets",
"codex-state",
"codex-utils-absolute-path",
"codex-utils-cargo-bin",
@@ -1639,7 +1739,7 @@ name = "codex-login"
version = "0.0.0"
dependencies = [
"anyhow",
"base64",
"base64 0.22.1",
"chrono",
"codex-app-server-protocol",
"codex-core",
@@ -1843,6 +1943,24 @@ dependencies = [
"which",
]
[[package]]
name = "codex-secrets"
version = "0.0.0"
dependencies = [
"age",
"anyhow",
"base64 0.22.1",
"codex-keyring-store",
"pretty_assertions",
"rand 0.9.2",
"schemars 0.8.22",
"serde",
"serde_json",
"sha2",
"tempfile",
"tracing",
]
[[package]]
name = "codex-state"
version = "0.0.0"
@@ -1879,7 +1997,7 @@ dependencies = [
"anyhow",
"arboard",
"assert_matches",
"base64",
"base64 0.22.1",
"chrono",
"clap",
"codex-ansi-escape",
@@ -1983,7 +2101,7 @@ dependencies = [
name = "codex-utils-image"
version = "0.0.0"
dependencies = [
"base64",
"base64 0.22.1",
"codex-utils-cache",
"image",
"tempfile",
@@ -2036,7 +2154,7 @@ name = "codex-windows-sandbox"
version = "0.0.0"
dependencies = [
"anyhow",
"base64",
"base64 0.22.1",
"chrono",
"codex-protocol",
"codex-utils-absolute-path",
@@ -2187,6 +2305,15 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "cookie-factory"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9885fa71e26b8ab7855e2ec7cae6e9b380edff76cd052e07c683a0319d51b3a2"
dependencies = [
"futures",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -2219,7 +2346,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"assert_cmd",
"base64",
"base64 0.22.1",
"codex-core",
"codex-protocol",
"codex-utils-absolute-path",
@@ -2409,6 +2536,32 @@ version = "0.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"fiat-crypto",
"rustc_version",
"subtle",
"zeroize",
]
[[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.104",
]
[[package]]
name = "darling"
version = "0.20.11"
@@ -3099,6 +3252,12 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "fiat-crypto"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "filedescriptor"
version = "0.8.3"
@@ -3110,6 +3269,15 @@ dependencies = [
"winapi",
]
[[package]]
name = "find-crate"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a98bbaacea1c0eb6a0876280051b892eb73594fd90cf3b20e9c817029c57d2"
dependencies = [
"toml 0.5.11",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.7"
@@ -3164,6 +3332,50 @@ dependencies = [
"num-traits",
]
[[package]]
name = "fluent"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb74634707bebd0ce645a981148e8fb8c7bccd4c33c652aeffd28bf2f96d555a"
dependencies = [
"fluent-bundle",
"unic-langid",
]
[[package]]
name = "fluent-bundle"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fe0a21ee80050c678013f82edf4b705fe2f26f1f9877593d13198612503f493"
dependencies = [
"fluent-langneg",
"fluent-syntax",
"intl-memoizer",
"intl_pluralrules",
"rustc-hash 1.1.0",
"self_cell 0.10.3",
"smallvec",
"unic-langid",
]
[[package]]
name = "fluent-langneg"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eebbe59450baee8282d71676f3bfed5689aeab00b27545e83e5f14b1195e8b0"
dependencies = [
"unic-langid",
]
[[package]]
name = "fluent-syntax"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a530c4694a6a8d528794ee9bbd8ba0122e779629ac908d15ad5a7ae7763a33d"
dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "flume"
version = "0.11.1"
@@ -3816,7 +4028,7 @@ version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
dependencies = [
"base64",
"base64 0.22.1",
"bytes",
"futures-channel",
"futures-core",
@@ -3836,6 +4048,72 @@ dependencies = [
"windows-registry",
]
[[package]]
name = "i18n-config"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef"
dependencies = [
"basic-toml",
"log",
"serde",
"serde_derive",
"thiserror 1.0.69",
"unic-langid",
]
[[package]]
name = "i18n-embed"
version = "0.15.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "669ffc2c93f97e6ddf06ddbe999fcd6782e3342978bb85f7d3c087c7978404c4"
dependencies = [
"arc-swap",
"fluent",
"fluent-langneg",
"fluent-syntax",
"i18n-embed-impl",
"intl-memoizer",
"log",
"parking_lot",
"rust-embed",
"thiserror 1.0.69",
"unic-langid",
"walkdir",
]
[[package]]
name = "i18n-embed-fl"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04b2969d0b3fc6143776c535184c19722032b43e6a642d710fa3f88faec53c2d"
dependencies = [
"find-crate",
"fluent",
"fluent-syntax",
"i18n-config",
"i18n-embed",
"proc-macro-error2",
"proc-macro2",
"quote",
"strsim 0.11.1",
"syn 2.0.104",
"unic-langid",
]
[[package]]
name = "i18n-embed-impl"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f2cc0e0523d1fe6fc2c6f66e5038624ea8091b3e7748b5e8e0c84b1698db6c2"
dependencies = [
"find-crate",
"i18n-config",
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "iana-time-zone"
version = "0.1.63"
@@ -4160,6 +4438,25 @@ dependencies = [
"syn 2.0.104",
]
[[package]]
name = "intl-memoizer"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "310da2e345f5eb861e7a07ee182262e94975051db9e4223e909ba90f392f163f"
dependencies = [
"type-map",
"unic-langid",
]
[[package]]
name = "intl_pluralrules"
version = "7.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "078ea7b7c29a2b4df841a7f6ac8775ff6074020c6776d48491ce2268e068f972"
dependencies = [
"unic-langid",
]
[[package]]
name = "inventory"
version = "0.3.20"
@@ -4169,6 +4466,12 @@ dependencies = [
"rustversion",
]
[[package]]
name = "io_tee"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b3f7cef34251886990511df1c61443aa928499d598a9473929ab5a90a527304"
[[package]]
name = "ipconfig"
version = "0.3.2"
@@ -5042,7 +5345,7 @@ version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51e219e79014df21a225b1860a479e2dcd7cbd9130f4defd4bd0e191ea31d67d"
dependencies = [
"base64",
"base64 0.22.1",
"chrono",
"getrandom 0.2.16",
"http 1.3.1",
@@ -5154,6 +5457,12 @@ version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openssl"
version = "0.10.73"
@@ -5273,7 +5582,7 @@ version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7175df06de5eaee9909d4805a3d07e28bb752c34cab57fa9cff549da596b30f"
dependencies = [
"base64",
"base64 0.22.1",
"const-hex",
"opentelemetry",
"opentelemetry_sdk",
@@ -5420,6 +5729,16 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
]
[[package]]
name = "pem-rfc7468"
version = "0.7.0"
@@ -5530,7 +5849,7 @@ version = "1.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1"
dependencies = [
"base64",
"base64 0.22.1",
"indexmap 2.12.0",
"quick-xml 0.38.0",
"serde",
@@ -5564,6 +5883,17 @@ dependencies = [
"windows-sys 0.61.1",
]
[[package]]
name = "poly1305"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]]
name = "portable-atomic"
version = "1.11.1"
@@ -5681,6 +6011,28 @@ dependencies = [
"toml_edit 0.23.10+spec-1.0.0",
]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn 2.0.104",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
@@ -5820,7 +6172,7 @@ dependencies = [
"pin-project-lite",
"quinn-proto",
"quinn-udp",
"rustc-hash",
"rustc-hash 2.1.1",
"rustls",
"socket2 0.6.1",
"thiserror 2.0.17",
@@ -5840,7 +6192,7 @@ dependencies = [
"lru-slab",
"rand 0.9.2",
"ring",
"rustc-hash",
"rustc-hash 2.1.1",
"rustls",
"rustls-pki-types",
"slab",
@@ -5986,7 +6338,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "453d60af031e23af2d48995e41b17023f6150044738680508b63671f8d7417dd"
dependencies = [
"ahash",
"base64",
"base64 0.22.1",
"bitflags 2.10.0",
"chrono",
"const_format",
@@ -6066,7 +6418,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d74fe0cd9bd4440827dc6dc0f504cf66065396532e798891dee2c1b740b2285"
dependencies = [
"ahash",
"base64",
"base64 0.22.1",
"chrono",
"const_format",
"httpdate",
@@ -6461,7 +6813,7 @@ version = "0.12.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
dependencies = [
"base64",
"base64 0.22.1",
"bytes",
"encoding_rs",
"futures-channel",
@@ -6531,7 +6883,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528d42f8176e6e5e71ea69182b17d1d0a19a6b3b894b564678b74cd7cab13cfa"
dependencies = [
"async-trait",
"base64",
"base64 0.22.1",
"bytes",
"chrono",
"futures",
@@ -6592,12 +6944,52 @@ dependencies = [
"zeroize",
]
[[package]]
name = "rust-embed"
version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04113cb9355a377d83f06ef1f0a45b8ab8cd7d8b1288160717d66df5c7988d27"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0902e4c7c8e997159ab384e6d0fc91c221375f6894346ae107f47dd0f3ccaa"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn 2.0.104",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "8.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1"
dependencies = [
"sha2",
"walkdir",
]
[[package]]
name = "rustc-demangle"
version = "0.1.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.1"
@@ -6721,6 +7113,15 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa20"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
dependencies = [
"cipher",
]
[[package]]
name = "same-file"
version = "1.0.6"
@@ -6864,6 +7265,17 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "scrypt"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
dependencies = [
"pbkdf2",
"salsa20",
"sha2",
]
[[package]]
name = "sdd"
version = "3.0.10"
@@ -6879,6 +7291,15 @@ dependencies = [
"libc",
]
[[package]]
name = "secrecy"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a"
dependencies = [
"zeroize",
]
[[package]]
name = "secret-service"
version = "4.0.0"
@@ -6934,6 +7355,21 @@ dependencies = [
"libc",
]
[[package]]
name = "self_cell"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14e4d63b804dc0c7ec4a1e52bcb63f02c7ac94476755aa579edac21e01f915d"
dependencies = [
"self_cell 1.2.2",
]
[[package]]
name = "self_cell"
version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89"
[[package]]
name = "semver"
version = "1.0.27"
@@ -7178,7 +7614,7 @@ version = "3.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7"
dependencies = [
"base64",
"base64 0.22.1",
"chrono",
"hex",
"indexmap 1.9.3",
@@ -7464,7 +7900,7 @@ version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
dependencies = [
"base64",
"base64 0.22.1",
"bytes",
"chrono",
"crc",
@@ -7542,7 +7978,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
dependencies = [
"atoi",
"base64",
"base64 0.22.1",
"bitflags 2.10.0",
"byteorder",
"bytes",
@@ -7587,7 +8023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
dependencies = [
"atoi",
"base64",
"base64 0.22.1",
"bitflags 2.10.0",
"byteorder",
"chrono",
@@ -8406,7 +8842,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb7613188ce9f7df5bfe185db26c5814347d110db17920415cf2fbcad85e7203"
dependencies = [
"async-trait",
"base64",
"base64 0.22.1",
"bytes",
"http 1.3.1",
"http-body",
@@ -8713,6 +9149,15 @@ dependencies = [
"utf-8",
]
[[package]]
name = "type-map"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90"
dependencies = [
"rustc-hash 2.1.1",
]
[[package]]
name = "typenum"
version = "1.18.0"
@@ -8745,6 +9190,25 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
[[package]]
name = "unic-langid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05"
dependencies = [
"unic-langid-impl",
]
[[package]]
name = "unic-langid-impl"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658"
dependencies = [
"serde",
"tinystr",
]
[[package]]
name = "unicase"
version = "2.8.1"
@@ -8819,6 +9283,16 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
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",
"subtle",
]
[[package]]
name = "unsafe-libyaml"
version = "0.2.11"
@@ -8837,7 +9311,7 @@ version = "3.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d39cb1dbab692d82a977c0392ffac19e188bd9186a9f32806f0aaa859d75585a"
dependencies = [
"base64",
"base64 0.22.1",
"der",
"log",
"native-tls",
@@ -8854,7 +9328,7 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d81f9efa9df032be5934a46a068815a10a042b494b6a58cb0a1a97bb5467ed6f"
dependencies = [
"base64",
"base64 0.22.1",
"http 1.3.1",
"httparse",
"log",
@@ -9837,7 +10311,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08db1edfb05d9b3c1542e521aea074442088292f00b5f28e435c714a98f85031"
dependencies = [
"assert-json-diff",
"base64",
"base64 0.22.1",
"deadpool",
"futures",
"http 1.3.1",
@@ -9904,6 +10378,18 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d"
[[package]]
name = "x25519-dalek"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
dependencies = [
"curve25519-dalek",
"rand_core 0.6.4",
"serde",
"zeroize",
]
[[package]]
name = "xdg-home"
version = "1.3.0"

View File

@@ -16,6 +16,7 @@ members = [
"cli",
"common",
"core",
"secrets",
"exec",
"exec-server",
"execpolicy",
@@ -76,6 +77,7 @@ codex-cli = { path = "cli"}
codex-client = { path = "codex-client" }
codex-common = { path = "common" }
codex-core = { path = "core" }
codex-secrets = { path = "secrets" }
codex-exec = { path = "exec" }
codex-execpolicy = { path = "execpolicy" }
codex-feedback = { path = "feedback" }
@@ -110,6 +112,7 @@ mcp-types = { path = "mcp-types" }
mcp_test_support = { path = "mcp-server/tests/common" }
# External
age = "0.11.1"
allocative = "0.3.3"
ansi-to-tui = "7.0.0"
anyhow = "1"

View File

@@ -37,6 +37,7 @@ codex-keyring-store = { workspace = true }
codex-otel = { workspace = true }
codex-protocol = { workspace = true }
codex-rmcp-client = { workspace = true }
codex-secrets = { workspace = true }
codex-state = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-pty = { workspace = true }

View File

@@ -860,6 +860,26 @@
},
"type": "object"
},
"SecretsBackendKind": {
"enum": [
"local"
],
"type": "string"
},
"SecretsConfigToml": {
"additionalProperties": false,
"properties": {
"backend": {
"allOf": [
{
"$ref": "#/definitions/SecretsBackendKind"
}
],
"default": null
}
},
"type": "object"
},
"ShellEnvironmentPolicyInherit": {
"oneOf": [
{
@@ -1472,6 +1492,15 @@
],
"description": "Sandbox configuration to apply if `sandbox` is `WorkspaceWrite`."
},
"secrets": {
"allOf": [
{
"$ref": "#/definitions/SecretsConfigToml"
}
],
"default": null,
"description": "Secrets configuration. Defaults to a local encrypted file backend."
},
"shell_environment_policy": {
"allOf": [
{

View File

@@ -13,6 +13,7 @@ use crate::config::types::OtelConfig;
use crate::config::types::OtelConfigToml;
use crate::config::types::OtelExporterKind;
use crate::config::types::SandboxWorkspaceWrite;
use crate::config::types::SecretsConfigToml;
use crate::config::types::ShellEnvironmentPolicy;
use crate::config::types::ShellEnvironmentPolicyToml;
use crate::config::types::SkillsConfig;
@@ -39,6 +40,7 @@ use crate::project_doc::DEFAULT_PROJECT_DOC_FILENAME;
use crate::project_doc::LOCAL_PROJECT_DOC_FILENAME;
use crate::protocol::AskForApproval;
use crate::protocol::SandboxPolicy;
use crate::secrets::SecretsBackendKind;
use crate::windows_sandbox::WindowsSandboxLevelExt;
use codex_app_server_protocol::Tools;
use codex_app_server_protocol::UserSavedConfig;
@@ -228,6 +230,9 @@ pub struct Config {
/// auto: Use the OS-specific keyring service if available, otherwise use a file.
pub cli_auth_credentials_store_mode: AuthCredentialsStoreMode,
/// Active secrets backend. Defaults to the local encrypted file backend.
pub secrets_backend: SecretsBackendKind,
/// Definition for MCP servers that Codex can reach out to for tool calls.
pub mcp_servers: Constrained<HashMap<String, McpServerConfig>>,
@@ -823,6 +828,10 @@ pub struct ConfigToml {
#[serde(default)]
pub cli_auth_credentials_store: Option<AuthCredentialsStoreMode>,
/// Secrets configuration. Defaults to a local encrypted file backend.
#[serde(default)]
pub secrets: Option<SecretsConfigToml>,
/// Definition for MCP servers that Codex can reach out to for tool calls.
#[serde(default)]
// Uses the raw MCP input shape (custom deserialization) rather than `McpServerConfig`.
@@ -1452,6 +1461,11 @@ impl Config {
});
let forced_login_method = cfg.forced_login_method;
let secrets_backend = cfg
.secrets
.as_ref()
.and_then(|secrets| secrets.backend)
.unwrap_or_default();
let model = model.or(config_profile.model).or(cfg.model);
@@ -1533,6 +1547,7 @@ impl Config {
// The config.toml omits "_mode" because it's a config file. However, "_mode"
// is important in code to differentiate the mode from the store implementation.
cli_auth_credentials_store_mode: cfg.cli_auth_credentials_store.unwrap_or_default(),
secrets_backend,
mcp_servers,
// The config.toml omits "_mode" because it's a config file. However, "_mode"
// is important in code to differentiate the mode from the store implementation.
@@ -3760,6 +3775,7 @@ model_verbosity = "high"
notify: None,
cwd: fixture.cwd(),
cli_auth_credentials_store_mode: Default::default(),
secrets_backend: SecretsBackendKind::Local,
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: Default::default(),
mcp_oauth_callback_port: None,
@@ -3844,6 +3860,7 @@ model_verbosity = "high"
notify: None,
cwd: fixture.cwd(),
cli_auth_credentials_store_mode: Default::default(),
secrets_backend: SecretsBackendKind::Local,
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: Default::default(),
mcp_oauth_callback_port: None,
@@ -3943,6 +3960,7 @@ model_verbosity = "high"
notify: None,
cwd: fixture.cwd(),
cli_auth_credentials_store_mode: Default::default(),
secrets_backend: SecretsBackendKind::Local,
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: Default::default(),
mcp_oauth_callback_port: None,
@@ -4028,6 +4046,7 @@ model_verbosity = "high"
notify: None,
cwd: fixture.cwd(),
cli_auth_credentials_store_mode: Default::default(),
secrets_backend: SecretsBackendKind::Local,
mcp_servers: Constrained::allow_any(HashMap::new()),
mcp_oauth_credentials_store_mode: Default::default(),
mcp_oauth_callback_port: None,

View File

@@ -4,6 +4,7 @@
// definitions that do not contain business logic.
use crate::config_loader::RequirementSource;
use crate::secrets::SecretsBackendKind;
pub use codex_protocol::config_types::AltScreenMode;
pub use codex_protocol::config_types::ModeKind;
pub use codex_protocol::config_types::Personality;
@@ -24,6 +25,13 @@ use serde::de::Error as SerdeError;
pub const DEFAULT_OTEL_ENVIRONMENT: &str = "dev";
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq, JsonSchema)]
#[schemars(deny_unknown_fields)]
pub struct SecretsConfigToml {
#[serde(default)]
pub backend: Option<SecretsBackendKind>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum McpServerDisabledReason {
Unknown,

View File

@@ -87,6 +87,7 @@ pub mod project_doc;
mod rollout;
pub(crate) mod safety;
pub mod seatbelt;
pub mod secrets;
pub mod shell;
pub mod shell_snapshot;
pub mod skills;

View File

@@ -0,0 +1,7 @@
pub use codex_secrets::LocalSecretsBackend;
pub use codex_secrets::SecretListEntry;
pub use codex_secrets::SecretName;
pub use codex_secrets::SecretScope;
pub use codex_secrets::SecretsBackendKind;
pub use codex_secrets::SecretsManager;
pub use codex_secrets::environment_id_from_cwd;

View File

@@ -0,0 +1,24 @@
[package]
name = "codex-secrets"
version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
age = { workspace = true }
anyhow = { workspace = true }
base64 = { workspace = true }
codex-keyring-store = { workspace = true }
rand = { workspace = true }
schemars = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }
tempfile = { workspace = true }

232
codex-rs/secrets/src/lib.rs Normal file
View File

@@ -0,0 +1,232 @@
use std::fmt;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::Result;
use codex_keyring_store::DefaultKeyringStore;
use codex_keyring_store::KeyringStore;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use sha2::Digest;
use sha2::Sha256;
mod local;
pub use local::LocalSecretsBackend;
const KEYRING_SERVICE: &str = "codex";
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SecretName(String);
impl SecretName {
pub fn new(raw: &str) -> Result<Self> {
let trimmed = raw.trim();
anyhow::ensure!(!trimmed.is_empty(), "secret name must not be empty");
anyhow::ensure!(
trimmed
.chars()
.all(|ch| ch.is_ascii_uppercase() || ch.is_ascii_digit() || ch == '_'),
"secret name must contain only A-Z, 0-9, or _"
);
Ok(Self(trimmed.to_string()))
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl fmt::Display for SecretName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SecretScope {
Global,
Environment(String),
}
impl SecretScope {
pub fn environment(environment_id: impl Into<String>) -> Result<Self> {
let env_id = environment_id.into();
let trimmed = env_id.trim();
anyhow::ensure!(!trimmed.is_empty(), "environment id must not be empty");
Ok(Self::Environment(trimmed.to_string()))
}
pub fn canonical_key(&self, name: &SecretName) -> String {
// Stable, env-safe identifier used as the on-disk map key.
match self {
Self::Global => format!("global/{}", name.as_str()),
Self::Environment(environment_id) => {
format!("env/{environment_id}/{}", name.as_str())
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SecretListEntry {
pub scope: SecretScope,
pub name: SecretName,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum SecretsBackendKind {
Local,
}
impl Default for SecretsBackendKind {
fn default() -> Self {
Self::Local
}
}
pub trait SecretsBackend: Send + Sync {
fn set(&self, scope: &SecretScope, name: &SecretName, value: &str) -> Result<()>;
fn get(&self, scope: &SecretScope, name: &SecretName) -> Result<Option<String>>;
fn delete(&self, scope: &SecretScope, name: &SecretName) -> Result<bool>;
fn list(&self, scope_filter: Option<&SecretScope>) -> Result<Vec<SecretListEntry>>;
}
#[derive(Clone)]
pub struct SecretsManager {
backend: Arc<dyn SecretsBackend>,
}
impl SecretsManager {
pub fn new(codex_home: PathBuf, backend_kind: SecretsBackendKind) -> Self {
let keyring_store: Arc<dyn KeyringStore> = Arc::new(DefaultKeyringStore);
Self::new_with_keyring_store(codex_home, backend_kind, keyring_store)
}
pub fn new_with_keyring_store(
codex_home: PathBuf,
backend_kind: SecretsBackendKind,
keyring_store: Arc<dyn KeyringStore>,
) -> Self {
let backend: Arc<dyn SecretsBackend> = match backend_kind {
SecretsBackendKind::Local => {
Arc::new(LocalSecretsBackend::new(codex_home, keyring_store))
}
};
Self { backend }
}
pub fn set(&self, scope: &SecretScope, name: &SecretName, value: &str) -> Result<()> {
self.backend.set(scope, name, value)
}
pub fn get(&self, scope: &SecretScope, name: &SecretName) -> Result<Option<String>> {
self.backend.get(scope, name)
}
pub fn delete(&self, scope: &SecretScope, name: &SecretName) -> Result<bool> {
self.backend.delete(scope, name)
}
pub fn list(&self, scope_filter: Option<&SecretScope>) -> Result<Vec<SecretListEntry>> {
self.backend.list(scope_filter)
}
}
pub fn environment_id_from_cwd(cwd: &Path) -> String {
if let Some(repo_root) = get_git_repo_root(cwd)
&& let Some(name) = repo_root.file_name()
{
let name = name.to_string_lossy().trim().to_string();
if !name.is_empty() {
return name;
}
}
let canonical = cwd
.canonicalize()
.unwrap_or_else(|_| cwd.to_path_buf())
.to_string_lossy()
.into_owned();
let mut hasher = Sha256::new();
hasher.update(canonical.as_bytes());
let digest = hasher.finalize();
let hex = format!("{digest:x}");
let short = hex.get(..12).unwrap_or(hex.as_str());
format!("cwd-{short}")
}
fn get_git_repo_root(base_dir: &Path) -> Option<PathBuf> {
let mut dir = base_dir.to_path_buf();
loop {
if dir.join(".git").exists() {
return Some(dir);
}
if !dir.pop() {
break;
}
}
None
}
pub(crate) fn compute_keyring_account(codex_home: &Path) -> String {
let canonical = codex_home
.canonicalize()
.unwrap_or_else(|_| codex_home.to_path_buf())
.to_string_lossy()
.into_owned();
let mut hasher = Sha256::new();
hasher.update(canonical.as_bytes());
let digest = hasher.finalize();
let hex = format!("{digest:x}");
let short = hex.get(..16).unwrap_or(hex.as_str());
format!("secrets|{short}")
}
pub(crate) fn keyring_service() -> &'static str {
KEYRING_SERVICE
}
#[cfg(test)]
mod tests {
use super::*;
use codex_keyring_store::tests::MockKeyringStore;
use pretty_assertions::assert_eq;
#[test]
fn environment_id_fallback_has_cwd_prefix() {
let dir = tempfile::tempdir().expect("tempdir");
let env_id = environment_id_from_cwd(dir.path());
assert!(env_id.starts_with("cwd-"));
}
#[test]
fn manager_round_trips_local_backend() -> Result<()> {
let codex_home = tempfile::tempdir().expect("tempdir");
let keyring = Arc::new(MockKeyringStore::default());
let manager = SecretsManager::new_with_keyring_store(
codex_home.path().to_path_buf(),
SecretsBackendKind::Local,
keyring,
);
let scope = SecretScope::Global;
let name = SecretName::new("GITHUB_TOKEN")?;
manager.set(&scope, &name, "token-1")?;
assert_eq!(manager.get(&scope, &name)?, Some("token-1".to_string()));
let listed = manager.list(None)?;
assert_eq!(listed.len(), 1);
assert_eq!(listed[0].name, name);
assert!(manager.delete(&scope, &name)?);
assert_eq!(manager.get(&scope, &name)?, None);
Ok(())
}
}

View File

@@ -0,0 +1,248 @@
use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::sync::atomic::compiler_fence;
use age::decrypt;
use age::encrypt;
use age::scrypt::Identity as ScryptIdentity;
use age::scrypt::Recipient as ScryptRecipient;
use age::secrecy::ExposeSecret;
use age::secrecy::SecretString;
use anyhow::Context;
use anyhow::Result;
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
use codex_keyring_store::KeyringStore;
use rand::TryRngCore;
use rand::rngs::OsRng;
use serde::Deserialize;
use serde::Serialize;
use tracing::warn;
use super::SecretListEntry;
use super::SecretName;
use super::SecretScope;
use super::SecretsBackend;
use super::compute_keyring_account;
use super::keyring_service;
const SECRETS_VERSION: u8 = 1;
const LOCAL_SECRETS_FILENAME: &str = "local.age";
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
struct SecretsFile {
version: u8,
secrets: BTreeMap<String, String>,
}
impl SecretsFile {
fn new_empty() -> Self {
Self {
version: SECRETS_VERSION,
secrets: BTreeMap::new(),
}
}
}
#[derive(Debug, Clone)]
pub struct LocalSecretsBackend {
codex_home: PathBuf,
keyring_store: Arc<dyn KeyringStore>,
}
impl LocalSecretsBackend {
pub fn new(codex_home: PathBuf, keyring_store: Arc<dyn KeyringStore>) -> Self {
Self {
codex_home,
keyring_store,
}
}
pub fn set(&self, scope: &SecretScope, name: &SecretName, value: &str) -> Result<()> {
anyhow::ensure!(!value.is_empty(), "secret value must not be empty");
let canonical_key = scope.canonical_key(name);
let mut file = self.load_file()?;
file.secrets.insert(canonical_key, value.to_string());
self.save_file(&file)
}
pub fn get(&self, scope: &SecretScope, name: &SecretName) -> Result<Option<String>> {
let canonical_key = scope.canonical_key(name);
let file = self.load_file()?;
Ok(file.secrets.get(&canonical_key).cloned())
}
pub fn delete(&self, scope: &SecretScope, name: &SecretName) -> Result<bool> {
let canonical_key = scope.canonical_key(name);
let mut file = self.load_file()?;
let removed = file.secrets.remove(&canonical_key).is_some();
if removed {
self.save_file(&file)?;
}
Ok(removed)
}
pub fn list(&self, scope_filter: Option<&SecretScope>) -> Result<Vec<SecretListEntry>> {
let file = self.load_file()?;
let mut entries = Vec::new();
for canonical_key in file.secrets.keys() {
let Some(entry) = parse_canonical_key(canonical_key) else {
warn!("skipping invalid canonical secret key: {canonical_key}");
continue;
};
if let Some(scope) = scope_filter
&& entry.scope != *scope
{
continue;
}
entries.push(entry);
}
Ok(entries)
}
fn secrets_dir(&self) -> PathBuf {
self.codex_home.join("secrets")
}
fn secrets_path(&self) -> PathBuf {
self.secrets_dir().join(LOCAL_SECRETS_FILENAME)
}
fn load_file(&self) -> Result<SecretsFile> {
let path = self.secrets_path();
if !path.exists() {
return Ok(SecretsFile::new_empty());
}
let ciphertext = fs::read(&path)
.with_context(|| format!("failed to read secrets file at {}", path.display()))?;
let passphrase = self.load_or_create_passphrase()?;
let plaintext = decrypt_with_passphrase(&ciphertext, &passphrase)?;
let mut parsed: SecretsFile = serde_json::from_slice(&plaintext).with_context(|| {
format!(
"failed to deserialize decrypted secrets file at {}",
path.display()
)
})?;
if parsed.version == 0 {
parsed.version = SECRETS_VERSION;
}
Ok(parsed)
}
fn save_file(&self, file: &SecretsFile) -> Result<()> {
let dir = self.secrets_dir();
fs::create_dir_all(&dir)
.with_context(|| format!("failed to create secrets dir {}", dir.display()))?;
let passphrase = self.load_or_create_passphrase()?;
let plaintext = serde_json::to_vec(file).context("failed to serialize secrets file")?;
let ciphertext = encrypt_with_passphrase(&plaintext, &passphrase)?;
let path = self.secrets_path();
fs::write(&path, ciphertext)
.with_context(|| format!("failed to write secrets file at {}", path.display()))?;
Ok(())
}
fn load_or_create_passphrase(&self) -> Result<SecretString> {
let account = compute_keyring_account(&self.codex_home);
match self
.keyring_store
.load(keyring_service(), &account)
.map_err(|err| anyhow::anyhow!(err.message()))?
{
Some(existing) => Ok(SecretString::from(existing)),
None => {
// Generate a high-entropy key and persist it in the OS keyring.
// This keeps secrets out of plaintext config while remaining
// fully local/offline for the MVP.
let generated = generate_passphrase()?;
self.keyring_store
.save(keyring_service(), &account, generated.expose_secret())
.map_err(|err| anyhow::anyhow!(err.message()))
.context("failed to persist secrets key in keyring")?;
Ok(generated)
}
}
}
}
impl SecretsBackend for LocalSecretsBackend {
fn set(&self, scope: &SecretScope, name: &SecretName, value: &str) -> Result<()> {
LocalSecretsBackend::set(self, scope, name, value)
}
fn get(&self, scope: &SecretScope, name: &SecretName) -> Result<Option<String>> {
LocalSecretsBackend::get(self, scope, name)
}
fn delete(&self, scope: &SecretScope, name: &SecretName) -> Result<bool> {
LocalSecretsBackend::delete(self, scope, name)
}
fn list(&self, scope_filter: Option<&SecretScope>) -> Result<Vec<SecretListEntry>> {
LocalSecretsBackend::list(self, scope_filter)
}
}
fn generate_passphrase() -> Result<SecretString> {
let mut bytes = [0_u8; 32];
let mut rng = OsRng;
rng.try_fill_bytes(&mut bytes)
.context("failed to generate random secrets key")?;
// Base64 keeps the keyring payload ASCII-safe without reducing entropy.
let encoded = BASE64_STANDARD.encode(bytes);
wipe_bytes(&mut bytes);
Ok(SecretString::from(encoded))
}
fn wipe_bytes(bytes: &mut [u8]) {
for byte in bytes {
// Volatile writes make it much harder for the compiler to elide the wipe.
// SAFETY: `byte` is a valid mutable reference into `bytes`.
unsafe { std::ptr::write_volatile(byte, 0) };
}
compiler_fence(Ordering::SeqCst);
}
fn encrypt_with_passphrase(plaintext: &[u8], passphrase: &SecretString) -> Result<Vec<u8>> {
let recipient = ScryptRecipient::new(passphrase.clone());
encrypt(&recipient, plaintext).context("failed to encrypt secrets file")
}
fn decrypt_with_passphrase(ciphertext: &[u8], passphrase: &SecretString) -> Result<Vec<u8>> {
let identity = ScryptIdentity::new(passphrase.clone());
decrypt(&identity, ciphertext).context("failed to decrypt secrets file")
}
fn parse_canonical_key(canonical_key: &str) -> Option<SecretListEntry> {
let mut parts = canonical_key.split('/');
let scope_kind = parts.next()?;
match scope_kind {
"global" => {
let name = parts.next()?;
if parts.next().is_some() {
return None;
}
let name = SecretName::new(name).ok()?;
Some(SecretListEntry {
scope: SecretScope::Global,
name,
})
}
"env" => {
let environment_id = parts.next()?;
let name = parts.next()?;
if parts.next().is_some() {
return None;
}
let name = SecretName::new(name).ok()?;
let scope = SecretScope::environment(environment_id.to_string()).ok()?;
Some(SecretListEntry { scope, name })
}
_ => None,
}
}