mirror of
https://github.com/openai/codex.git
synced 2026-09-16 12:13:30 +00:00
Add local secrets backend and skills auth POC
This commit is contained in:
11
.codex/skills/secrets-poc/SKILL.json
Normal file
11
.codex/skills/secrets-poc/SKILL.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"tools": [
|
||||
{
|
||||
"type": "env_var",
|
||||
"value": "TEST_SECRET",
|
||||
"description": "POC secret"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
6
.codex/skills/secrets-poc/SKILL.md
Normal file
6
.codex/skills/secrets-poc/SKILL.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
name: secrets-poc
|
||||
description: Secrets auth POC (repo-local)
|
||||
---
|
||||
|
||||
When invoked, run `printenv TEST_SECRET` with the shell tool and report it.
|
||||
524
codex-rs/Cargo.lock
generated
524
codex-rs/Cargo.lock
generated
@@ -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",
|
||||
@@ -1352,13 +1451,14 @@ dependencies = [
|
||||
name = "codex-core"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"age",
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
"assert_cmd",
|
||||
"assert_matches",
|
||||
"async-channel",
|
||||
"async-trait",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"chardetng",
|
||||
"chrono",
|
||||
"clap",
|
||||
@@ -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",
|
||||
@@ -1879,7 +1979,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
"assert_matches",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"clap",
|
||||
"codex-ansi-escape",
|
||||
@@ -1983,7 +2083,7 @@ dependencies = [
|
||||
name = "codex-utils-image"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"codex-utils-cache",
|
||||
"image",
|
||||
"tempfile",
|
||||
@@ -2036,7 +2136,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 +2287,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 +2328,7 @@ version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"assert_cmd",
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"codex-core",
|
||||
"codex-protocol",
|
||||
"codex-utils-absolute-path",
|
||||
@@ -2409,6 +2518,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 +3234,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 +3251,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 +3314,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 +4010,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 +4030,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 +4420,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 +4448,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 +5327,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 +5439,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 +5564,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 +5711,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 +5831,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 +5865,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 +5993,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 +6154,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 +6174,7 @@ dependencies = [
|
||||
"lru-slab",
|
||||
"rand 0.9.2",
|
||||
"ring",
|
||||
"rustc-hash",
|
||||
"rustc-hash 2.1.1",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"slab",
|
||||
@@ -5986,7 +6320,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 +6400,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 +6795,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 +6865,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 +6926,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 +7095,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 +7247,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 +7273,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 +7337,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 +7596,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 +7882,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 +7960,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 +8005,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 +8824,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 +9131,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 +9172,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 +9265,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 +9293,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 +9310,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 +10293,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 +10360,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"
|
||||
|
||||
@@ -110,6 +110,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"
|
||||
|
||||
@@ -32,10 +32,12 @@ use std::path::PathBuf;
|
||||
use supports_color::Stream;
|
||||
|
||||
mod mcp_cmd;
|
||||
mod secrets_cmd;
|
||||
#[cfg(not(windows))]
|
||||
mod wsl_paths;
|
||||
|
||||
use crate::mcp_cmd::McpCli;
|
||||
use crate::secrets_cmd::SecretsCli;
|
||||
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
@@ -86,6 +88,9 @@ enum Subcommand {
|
||||
/// Remove stored authentication credentials.
|
||||
Logout(LogoutCommand),
|
||||
|
||||
/// Manage secrets.
|
||||
Secrets(SecretsCli),
|
||||
|
||||
/// [experimental] Run Codex as an MCP server and manage MCP servers.
|
||||
Mcp(McpCli),
|
||||
|
||||
@@ -513,6 +518,13 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
|
||||
prepend_config_flags(&mut mcp_cli.config_overrides, root_config_overrides.clone());
|
||||
mcp_cli.run().await?;
|
||||
}
|
||||
Some(Subcommand::Secrets(mut secrets_cli)) => {
|
||||
prepend_config_flags(
|
||||
&mut secrets_cli.config_overrides,
|
||||
root_config_overrides.clone(),
|
||||
);
|
||||
secrets_cli.run().await?;
|
||||
}
|
||||
Some(Subcommand::AppServer(app_server_cli)) => match app_server_cli.subcommand {
|
||||
None => {
|
||||
codex_app_server::run_main(
|
||||
|
||||
185
codex-rs/cli/src/secrets_cmd.rs
Normal file
185
codex-rs/cli/src/secrets_cmd.rs
Normal file
@@ -0,0 +1,185 @@
|
||||
use std::io::IsTerminal;
|
||||
use std::io::Read;
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use clap::Parser;
|
||||
use codex_common::CliConfigOverrides;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::secrets::SecretName;
|
||||
use codex_core::secrets::SecretScope;
|
||||
use codex_core::secrets::SecretsManager;
|
||||
use codex_core::secrets::environment_id_from_cwd;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SecretsCli {
|
||||
#[clap(flatten)]
|
||||
pub config_overrides: CliConfigOverrides,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub subcommand: SecretsSubcommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, clap::Subcommand)]
|
||||
pub enum SecretsSubcommand {
|
||||
/// Store or update a secret value.
|
||||
Set(SecretsSetArgs),
|
||||
/// List secret names (values are never displayed).
|
||||
List(SecretsListArgs),
|
||||
/// Delete a secret value.
|
||||
Delete(SecretsDeleteArgs),
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SecretsScopeArgs {
|
||||
/// Use the global scope instead of the current environment scope.
|
||||
#[arg(long, default_value_t = false, conflicts_with = "environment_id")]
|
||||
pub global: bool,
|
||||
|
||||
/// Explicit environment identifier for scoping the secret.
|
||||
#[arg(long = "env")]
|
||||
pub environment_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SecretsSetArgs {
|
||||
/// Secret name (A-Z, 0-9, underscore only).
|
||||
pub name: String,
|
||||
|
||||
/// Secret value. Prefer piping via stdin to avoid shell history.
|
||||
#[arg(long)]
|
||||
pub value: Option<String>,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub scope: SecretsScopeArgs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SecretsListArgs {
|
||||
#[clap(flatten)]
|
||||
pub scope: SecretsScopeArgs,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SecretsDeleteArgs {
|
||||
/// Secret name (A-Z, 0-9, underscore only).
|
||||
pub name: String,
|
||||
|
||||
#[clap(flatten)]
|
||||
pub scope: SecretsScopeArgs,
|
||||
}
|
||||
|
||||
impl SecretsCli {
|
||||
pub async fn run(self) -> Result<()> {
|
||||
let config = load_config(self.config_overrides).await?;
|
||||
let manager = SecretsManager::new(config.codex_home.clone(), config.secrets_backend);
|
||||
match self.subcommand {
|
||||
SecretsSubcommand::Set(args) => run_set(&config, &manager, args),
|
||||
SecretsSubcommand::List(args) => run_list(&config, &manager, args),
|
||||
SecretsSubcommand::Delete(args) => run_delete(&config, &manager, args),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_config(cli_config_overrides: CliConfigOverrides) -> Result<Config> {
|
||||
let cli_overrides = cli_config_overrides
|
||||
.parse_overrides()
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
Config::load_with_cli_overrides(cli_overrides)
|
||||
.await
|
||||
.context("failed to load configuration")
|
||||
}
|
||||
|
||||
fn run_set(config: &Config, manager: &SecretsManager, args: SecretsSetArgs) -> Result<()> {
|
||||
let name = SecretName::new(&args.name)?;
|
||||
let scope = resolve_scope(config, &args.scope)?;
|
||||
let value = resolve_value(args.value)?;
|
||||
manager.set(&scope, &name, &value)?;
|
||||
println!("Stored {name} in {}.", scope_label(&scope));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_list(config: &Config, manager: &SecretsManager, args: SecretsListArgs) -> Result<()> {
|
||||
let default_scope = default_environment_scope(config)?;
|
||||
let scope_filter = match (args.scope.global, args.scope.environment_id.as_deref()) {
|
||||
(true, _) => Some(SecretScope::Global),
|
||||
(false, Some(env_id)) => Some(SecretScope::environment(env_id.to_string())?),
|
||||
(false, None) => None,
|
||||
};
|
||||
|
||||
let mut entries = manager.list(None)?;
|
||||
entries.retain(|entry| match scope_filter.as_ref() {
|
||||
Some(scope) => &entry.scope == scope,
|
||||
None => entry.scope == SecretScope::Global || entry.scope == default_scope,
|
||||
});
|
||||
|
||||
entries.sort_by(|a, b| {
|
||||
scope_label(&a.scope)
|
||||
.cmp(&scope_label(&b.scope))
|
||||
.then(a.name.cmp(&b.name))
|
||||
});
|
||||
|
||||
if entries.is_empty() {
|
||||
println!("No secrets found.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for entry in entries {
|
||||
println!("{} {}", scope_label(&entry.scope), entry.name);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn run_delete(config: &Config, manager: &SecretsManager, args: SecretsDeleteArgs) -> Result<()> {
|
||||
let name = SecretName::new(&args.name)?;
|
||||
let scope = resolve_scope(config, &args.scope)?;
|
||||
let removed = manager.delete(&scope, &name)?;
|
||||
if removed {
|
||||
println!("Deleted {name} from {}.", scope_label(&scope));
|
||||
} else {
|
||||
println!("No secret named {name} found in {}.", scope_label(&scope));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_scope(config: &Config, scope_args: &SecretsScopeArgs) -> Result<SecretScope> {
|
||||
if scope_args.global {
|
||||
return Ok(SecretScope::Global);
|
||||
}
|
||||
if let Some(env_id) = scope_args.environment_id.as_deref() {
|
||||
return SecretScope::environment(env_id.to_string());
|
||||
}
|
||||
default_environment_scope(config)
|
||||
}
|
||||
|
||||
fn default_environment_scope(config: &Config) -> Result<SecretScope> {
|
||||
let environment_id = environment_id_from_cwd(&config.cwd);
|
||||
SecretScope::environment(environment_id)
|
||||
}
|
||||
|
||||
fn resolve_value(explicit: Option<String>) -> Result<String> {
|
||||
if let Some(value) = explicit {
|
||||
return Ok(value);
|
||||
}
|
||||
|
||||
if std::io::stdin().is_terminal() {
|
||||
bail!("secret value must be provided via --value or piped stdin");
|
||||
}
|
||||
|
||||
let mut buf = String::new();
|
||||
std::io::stdin()
|
||||
.read_to_string(&mut buf)
|
||||
.context("failed to read secret value from stdin")?;
|
||||
let trimmed = buf.trim_end_matches(['\n', '\r']).to_string();
|
||||
anyhow::ensure!(!trimmed.is_empty(), "secret value must not be empty");
|
||||
Ok(trimmed)
|
||||
}
|
||||
|
||||
fn scope_label(scope: &SecretScope) -> String {
|
||||
match scope {
|
||||
SecretScope::Global => "global".to_string(),
|
||||
SecretScope::Environment(env_id) => format!("env/{env_id}"),
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ path = "src/bin/config_schema.rs"
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
age = { workspace = true }
|
||||
anyhow = { workspace = true }
|
||||
arc-swap = "1.8.0"
|
||||
async-channel = { workspace = true }
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -155,6 +155,7 @@ use crate::rollout::RolloutRecorder;
|
||||
use crate::rollout::RolloutRecorderParams;
|
||||
use crate::rollout::map_session_init_error;
|
||||
use crate::rollout::metadata;
|
||||
use crate::secrets::resolve_skill_env_dependencies;
|
||||
use crate::shell;
|
||||
use crate::shell_snapshot::ShellSnapshot;
|
||||
use crate::skills::SkillError;
|
||||
@@ -1935,6 +1936,41 @@ impl Session {
|
||||
state.record_mcp_dependency_prompted(names);
|
||||
}
|
||||
|
||||
pub(crate) async fn skill_secret_prompted(&self) -> HashSet<String> {
|
||||
let state = self.state.lock().await;
|
||||
state.skill_secret_prompted()
|
||||
}
|
||||
|
||||
pub(crate) async fn record_skill_secret_prompted<I>(&self, keys: I)
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
let mut state = self.state.lock().await;
|
||||
state.record_skill_secret_prompted(keys);
|
||||
}
|
||||
|
||||
pub(crate) async fn set_skill_env_overrides(
|
||||
&self,
|
||||
sub_id: String,
|
||||
overrides: HashMap<String, String>,
|
||||
) {
|
||||
let mut state = self.state.lock().await;
|
||||
state.set_skill_env_overrides(sub_id, overrides);
|
||||
}
|
||||
|
||||
pub(crate) async fn skill_env_overrides(
|
||||
&self,
|
||||
sub_id: &str,
|
||||
) -> Option<HashMap<String, String>> {
|
||||
let state = self.state.lock().await;
|
||||
state.skill_env_overrides(sub_id)
|
||||
}
|
||||
|
||||
pub(crate) async fn clear_skill_env_overrides(&self, sub_id: &str) {
|
||||
let mut state = self.state.lock().await;
|
||||
state.clear_skill_env_overrides(sub_id);
|
||||
}
|
||||
|
||||
pub(crate) async fn set_server_reasoning_included(&self, included: bool) {
|
||||
let mut state = self.state.lock().await;
|
||||
state.set_server_reasoning_included(included);
|
||||
@@ -3178,6 +3214,20 @@ pub(crate) async fn run_turn(
|
||||
)
|
||||
.await;
|
||||
|
||||
let skill_secrets = resolve_skill_env_dependencies(
|
||||
sess.as_ref(),
|
||||
turn_context.as_ref(),
|
||||
&cancellation_token,
|
||||
&mentioned_skills,
|
||||
)
|
||||
.await;
|
||||
sess.set_skill_env_overrides(turn_context.sub_id.clone(), skill_secrets.overrides)
|
||||
.await;
|
||||
for message in skill_secrets.warnings {
|
||||
sess.send_event(&turn_context, EventMsg::Warning(WarningEvent { message }))
|
||||
.await;
|
||||
}
|
||||
|
||||
let otel_manager = turn_context.client.get_otel_manager();
|
||||
let SkillInjections {
|
||||
items: skill_items,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
215
codex-rs/core/src/secrets/local.rs
Normal file
215
codex-rs/core/src/secrets/local.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
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::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 => {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")?;
|
||||
let encoded = BASE64_STANDARD.encode(bytes);
|
||||
Ok(SecretString::from(encoded))
|
||||
}
|
||||
|
||||
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()?;
|
||||
Some(SecretListEntry {
|
||||
scope: SecretScope::Environment(environment_id.to_string()),
|
||||
name,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
211
codex-rs/core/src/secrets/mod.rs
Normal file
211
codex-rs/core/src/secrets/mod.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
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;
|
||||
|
||||
use crate::git_info::get_git_repo_root;
|
||||
|
||||
mod local;
|
||||
mod skill_dependencies;
|
||||
|
||||
pub use local::LocalSecretsBackend;
|
||||
pub(crate) use skill_dependencies::resolve_skill_env_dependencies;
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SecretsManager {
|
||||
backend: Arc<LocalSecretsBackend>,
|
||||
}
|
||||
|
||||
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 {
|
||||
match backend_kind {
|
||||
SecretsBackendKind::Local => Self {
|
||||
backend: Arc::new(LocalSecretsBackend::new(codex_home, keyring_store)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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}")
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
382
codex-rs/core/src/secrets/skill_dependencies.rs
Normal file
382
codex-rs/core/src/secrets/skill_dependencies.rs
Normal file
@@ -0,0 +1,382 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codex_protocol::request_user_input::RequestUserInputAnswer;
|
||||
use codex_protocol::request_user_input::RequestUserInputArgs;
|
||||
use codex_protocol::request_user_input::RequestUserInputQuestion;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::secrets::SecretName;
|
||||
use crate::secrets::SecretScope;
|
||||
use crate::secrets::SecretsManager;
|
||||
use crate::secrets::environment_id_from_cwd;
|
||||
use crate::skills::SkillMetadata;
|
||||
use crate::skills::model::SkillToolDependency;
|
||||
|
||||
const SKILL_SECRET_PROMPT_PREFIX: &str = "skill-secret";
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct SkillSecretsOutcome {
|
||||
pub overrides: HashMap<String, String>,
|
||||
pub warnings: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct SecretDependencyContext {
|
||||
name: SecretName,
|
||||
skills: Vec<String>,
|
||||
description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct MissingSecret {
|
||||
context: SecretDependencyContext,
|
||||
canonical_key: String,
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_skill_env_dependencies(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
cancellation_token: &CancellationToken,
|
||||
mentioned_skills: &[SkillMetadata],
|
||||
) -> SkillSecretsOutcome {
|
||||
if mentioned_skills.is_empty() {
|
||||
return SkillSecretsOutcome::default();
|
||||
}
|
||||
|
||||
let config = turn_context.client.config();
|
||||
let manager = SecretsManager::new(config.codex_home.clone(), config.secrets_backend);
|
||||
|
||||
let env_scope = match SecretScope::environment(environment_id_from_cwd(&turn_context.cwd)) {
|
||||
Ok(scope) => scope,
|
||||
Err(err) => {
|
||||
return SkillSecretsOutcome {
|
||||
overrides: HashMap::new(),
|
||||
warnings: vec![format!("failed to resolve environment scope: {err}")],
|
||||
};
|
||||
}
|
||||
};
|
||||
let global_scope = SecretScope::Global;
|
||||
|
||||
let mut outcome = SkillSecretsOutcome::default();
|
||||
let dependencies = collect_env_var_dependencies(mentioned_skills, &mut outcome.warnings);
|
||||
if dependencies.is_empty() {
|
||||
return outcome;
|
||||
}
|
||||
|
||||
let mut missing = Vec::new();
|
||||
|
||||
for context in dependencies {
|
||||
match resolve_secret_value(
|
||||
&manager,
|
||||
&env_scope,
|
||||
&global_scope,
|
||||
&context,
|
||||
&mut outcome.warnings,
|
||||
) {
|
||||
Some(value) => {
|
||||
outcome
|
||||
.overrides
|
||||
.insert(context.name.as_str().to_string(), value);
|
||||
}
|
||||
None => {
|
||||
missing.push(MissingSecret {
|
||||
canonical_key: env_scope.canonical_key(&context.name),
|
||||
context,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if missing.is_empty() {
|
||||
return outcome;
|
||||
}
|
||||
|
||||
let prompted = sess.skill_secret_prompted().await;
|
||||
let mut already_prompted = Vec::new();
|
||||
let mut unprompted_missing = Vec::new();
|
||||
for entry in missing {
|
||||
if prompted.contains(&entry.canonical_key) {
|
||||
already_prompted.push(entry);
|
||||
} else {
|
||||
unprompted_missing.push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
for entry in already_prompted {
|
||||
outcome
|
||||
.warnings
|
||||
.push(missing_secret_warning(&entry.context));
|
||||
}
|
||||
|
||||
if unprompted_missing.is_empty() {
|
||||
return outcome;
|
||||
}
|
||||
|
||||
let responses = prompt_for_missing_secrets(
|
||||
sess,
|
||||
turn_context,
|
||||
cancellation_token,
|
||||
&env_scope,
|
||||
&unprompted_missing,
|
||||
)
|
||||
.await;
|
||||
|
||||
let prompted_keys = unprompted_missing
|
||||
.iter()
|
||||
.map(|entry| entry.canonical_key.clone());
|
||||
sess.record_skill_secret_prompted(prompted_keys).await;
|
||||
|
||||
let mut resolved_from_prompt = HashMap::new();
|
||||
for entry in &unprompted_missing {
|
||||
let Some(answer) = responses.answers.get(&entry.canonical_key) else {
|
||||
outcome
|
||||
.warnings
|
||||
.push(missing_secret_warning(&entry.context));
|
||||
continue;
|
||||
};
|
||||
let Some(value) = first_non_empty_answer(answer) else {
|
||||
outcome
|
||||
.warnings
|
||||
.push(missing_secret_warning(&entry.context));
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Err(err) = manager.set(&env_scope, &entry.context.name, &value) {
|
||||
outcome.warnings.push(format!(
|
||||
"failed to persist secret {}: {err}",
|
||||
entry.context.name
|
||||
));
|
||||
}
|
||||
|
||||
resolved_from_prompt.insert(entry.context.name.as_str().to_string(), value);
|
||||
}
|
||||
|
||||
outcome.overrides.extend(resolved_from_prompt);
|
||||
outcome
|
||||
}
|
||||
|
||||
fn collect_env_var_dependencies(
|
||||
mentioned_skills: &[SkillMetadata],
|
||||
warnings: &mut Vec<String>,
|
||||
) -> Vec<SecretDependencyContext> {
|
||||
let mut contexts: BTreeMap<String, SecretDependencyContext> = BTreeMap::new();
|
||||
|
||||
for skill in mentioned_skills {
|
||||
let Some(dependencies) = skill.dependencies.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for tool in &dependencies.tools {
|
||||
if !tool.r#type.eq_ignore_ascii_case("env_var") {
|
||||
continue;
|
||||
}
|
||||
add_dependency_context(&mut contexts, skill, tool, warnings);
|
||||
}
|
||||
}
|
||||
|
||||
contexts
|
||||
.into_values()
|
||||
.map(|mut context| {
|
||||
context.skills.sort();
|
||||
context.skills.dedup();
|
||||
context
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn add_dependency_context(
|
||||
contexts: &mut BTreeMap<String, SecretDependencyContext>,
|
||||
skill: &SkillMetadata,
|
||||
tool: &SkillToolDependency,
|
||||
warnings: &mut Vec<String>,
|
||||
) {
|
||||
let name = match SecretName::new(&tool.value) {
|
||||
Ok(name) => name,
|
||||
Err(err) => {
|
||||
warnings.push(format!(
|
||||
"skill {} declares invalid env_var dependency {}: {err}",
|
||||
skill.name, tool.value
|
||||
));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let key = name.as_str().to_string();
|
||||
let description = tool.description.clone();
|
||||
contexts
|
||||
.entry(key)
|
||||
.and_modify(|context| {
|
||||
context.skills.push(skill.name.clone());
|
||||
if context.description.is_none() {
|
||||
context.description = description.clone();
|
||||
}
|
||||
})
|
||||
.or_insert_with(|| SecretDependencyContext {
|
||||
name,
|
||||
skills: vec![skill.name.clone()],
|
||||
description,
|
||||
});
|
||||
}
|
||||
|
||||
fn resolve_secret_value(
|
||||
manager: &SecretsManager,
|
||||
env_scope: &SecretScope,
|
||||
global_scope: &SecretScope,
|
||||
context: &SecretDependencyContext,
|
||||
warnings: &mut Vec<String>,
|
||||
) -> Option<String> {
|
||||
if let Some(env_value) = read_non_empty_env(context.name.as_str()) {
|
||||
return Some(env_value);
|
||||
}
|
||||
|
||||
match manager.get(env_scope, &context.name) {
|
||||
Ok(Some(value)) if !value.trim().is_empty() => return Some(value),
|
||||
Ok(Some(_)) | Ok(None) => {}
|
||||
Err(err) => warnings.push(format!(
|
||||
"failed to read secret {} from env scope: {err}",
|
||||
context.name
|
||||
)),
|
||||
}
|
||||
|
||||
match manager.get(global_scope, &context.name) {
|
||||
Ok(Some(value)) if !value.trim().is_empty() => Some(value),
|
||||
Ok(Some(_)) | Ok(None) => None,
|
||||
Err(err) => {
|
||||
warnings.push(format!(
|
||||
"failed to read secret {} from global scope: {err}",
|
||||
context.name
|
||||
));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_non_empty_env(name: &str) -> Option<String> {
|
||||
match std::env::var(name) {
|
||||
Ok(value) if !value.trim().is_empty() => Some(value),
|
||||
Ok(_) => None,
|
||||
Err(std::env::VarError::NotPresent) => None,
|
||||
Err(std::env::VarError::NotUnicode(_)) => {
|
||||
warn!("environment variable {name} contains invalid unicode; treating as missing");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn prompt_for_missing_secrets(
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
cancellation_token: &CancellationToken,
|
||||
env_scope: &SecretScope,
|
||||
missing: &[MissingSecret],
|
||||
) -> RequestUserInputResponse {
|
||||
let questions = missing
|
||||
.iter()
|
||||
.map(|entry| build_secret_question(env_scope, entry))
|
||||
.collect();
|
||||
let args = RequestUserInputArgs { questions };
|
||||
let call_id = format!("{SKILL_SECRET_PROMPT_PREFIX}-{}", turn_context.sub_id);
|
||||
let response_fut = sess.request_user_input(turn_context, call_id, args);
|
||||
let sub_id = turn_context.sub_id.as_str();
|
||||
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = cancellation_token.cancelled() => {
|
||||
let empty = RequestUserInputResponse { answers: HashMap::new() };
|
||||
sess.notify_user_input_response(sub_id, empty.clone()).await;
|
||||
empty
|
||||
}
|
||||
response = response_fut => response.unwrap_or_else(|| RequestUserInputResponse { answers: HashMap::new() }),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_secret_question(
|
||||
env_scope: &SecretScope,
|
||||
missing: &MissingSecret,
|
||||
) -> RequestUserInputQuestion {
|
||||
let scope_label = match env_scope {
|
||||
SecretScope::Global => "global".to_string(),
|
||||
SecretScope::Environment(env_id) => format!("env/{env_id}"),
|
||||
};
|
||||
let skills = missing.context.skills.join(", ");
|
||||
let description = missing
|
||||
.context
|
||||
.description
|
||||
.as_deref()
|
||||
.map(|text| format!(" {text}"))
|
||||
.unwrap_or_default();
|
||||
RequestUserInputQuestion {
|
||||
id: missing.canonical_key.clone(),
|
||||
header: format!("Missing secret: {}", missing.context.name),
|
||||
question: format!(
|
||||
"Skill(s) {skills} require {}.{description} Enter a value to store for {scope_label}.",
|
||||
missing.context.name,
|
||||
),
|
||||
is_other: false,
|
||||
options: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn first_non_empty_answer(answer: &RequestUserInputAnswer) -> Option<String> {
|
||||
answer
|
||||
.answers
|
||||
.iter()
|
||||
.find(|value| !value.trim().is_empty())
|
||||
.map(|value| value.to_string())
|
||||
}
|
||||
|
||||
fn missing_secret_warning(context: &SecretDependencyContext) -> String {
|
||||
let skills = context.skills.join(", ");
|
||||
format!(
|
||||
"Required secret {} is missing for skill(s) {skills}. Use `codex secrets set {}` to configure it.",
|
||||
context.name, context.name
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anyhow::Context;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::PathBuf;
|
||||
|
||||
fn skill_with_env(name: &str, env_var: &str) -> SkillMetadata {
|
||||
SkillMetadata {
|
||||
name: name.to_string(),
|
||||
description: name.to_string(),
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: Some(crate::skills::model::SkillDependencies {
|
||||
tools: vec![crate::skills::model::SkillToolDependency {
|
||||
r#type: "env_var".to_string(),
|
||||
value: env_var.to_string(),
|
||||
description: Some("token".to_string()),
|
||||
transport: None,
|
||||
command: None,
|
||||
url: None,
|
||||
}],
|
||||
}),
|
||||
path: PathBuf::from(name),
|
||||
scope: codex_protocol::protocol::SkillScope::User,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolves_from_environment_without_prompting() -> anyhow::Result<()> {
|
||||
let (session, turn) = crate::codex::make_session_and_context().await;
|
||||
let home = std::env::var("HOME").context("HOME must be set for tests")?;
|
||||
let skills = vec![skill_with_env("skill", "HOME")];
|
||||
let outcome =
|
||||
resolve_skill_env_dependencies(&session, &turn, &CancellationToken::new(), &skills)
|
||||
.await;
|
||||
|
||||
assert_eq!(outcome.warnings, Vec::<String>::new());
|
||||
assert_eq!(outcome.overrides.get("HOME"), Some(&home));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
//! Session-wide mutable state.
|
||||
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::codex::SessionConfiguration;
|
||||
@@ -17,6 +18,8 @@ pub(crate) struct SessionState {
|
||||
pub(crate) latest_rate_limits: Option<RateLimitSnapshot>,
|
||||
pub(crate) server_reasoning_included: bool,
|
||||
pub(crate) mcp_dependency_prompted: HashSet<String>,
|
||||
pub(crate) skill_secret_prompted: HashSet<String>,
|
||||
pub(crate) skill_env_overrides: HashMap<String, HashMap<String, String>>,
|
||||
/// Whether the session's initial context has been seeded into history.
|
||||
///
|
||||
/// TODO(owen): This is a temporary solution to avoid updating a thread's updated_at
|
||||
@@ -34,6 +37,8 @@ impl SessionState {
|
||||
latest_rate_limits: None,
|
||||
server_reasoning_included: false,
|
||||
mcp_dependency_prompted: HashSet::new(),
|
||||
skill_secret_prompted: HashSet::new(),
|
||||
skill_env_overrides: HashMap::new(),
|
||||
initial_context_seeded: false,
|
||||
}
|
||||
}
|
||||
@@ -112,6 +117,37 @@ impl SessionState {
|
||||
pub(crate) fn mcp_dependency_prompted(&self) -> HashSet<String> {
|
||||
self.mcp_dependency_prompted.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn record_skill_secret_prompted<I>(&mut self, keys: I)
|
||||
where
|
||||
I: IntoIterator<Item = String>,
|
||||
{
|
||||
self.skill_secret_prompted.extend(keys);
|
||||
}
|
||||
|
||||
pub(crate) fn skill_secret_prompted(&self) -> HashSet<String> {
|
||||
self.skill_secret_prompted.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn set_skill_env_overrides(
|
||||
&mut self,
|
||||
sub_id: String,
|
||||
overrides: HashMap<String, String>,
|
||||
) {
|
||||
if overrides.is_empty() {
|
||||
self.skill_env_overrides.remove(&sub_id);
|
||||
return;
|
||||
}
|
||||
self.skill_env_overrides.insert(sub_id, overrides);
|
||||
}
|
||||
|
||||
pub(crate) fn skill_env_overrides(&self, sub_id: &str) -> Option<HashMap<String, String>> {
|
||||
self.skill_env_overrides.get(sub_id).cloned()
|
||||
}
|
||||
|
||||
pub(crate) fn clear_skill_env_overrides(&mut self, sub_id: &str) {
|
||||
self.skill_env_overrides.remove(sub_id);
|
||||
}
|
||||
}
|
||||
|
||||
// Sometimes new snapshots don't include credits or plan information.
|
||||
|
||||
@@ -191,6 +191,7 @@ impl Session {
|
||||
false
|
||||
};
|
||||
drop(active);
|
||||
self.clear_skill_env_overrides(&turn_context.sub_id).await;
|
||||
if should_close_processes {
|
||||
self.close_unified_exec_processes().await;
|
||||
}
|
||||
|
||||
@@ -101,10 +101,14 @@ impl SessionTask for UserShellCommandTask {
|
||||
)
|
||||
.await;
|
||||
|
||||
let mut env = create_env(&turn_context.shell_environment_policy);
|
||||
if let Some(overrides) = session.skill_env_overrides(&turn_context.sub_id).await {
|
||||
env.extend(overrides);
|
||||
}
|
||||
let exec_env = ExecEnv {
|
||||
command: exec_command.clone(),
|
||||
cwd: cwd.clone(),
|
||||
env: create_env(&turn_context.shell_environment_policy),
|
||||
env,
|
||||
// TODO(zhao-oai): Now that we have ExecExpiration::Cancellation, we
|
||||
// should use that instead of an "arbitrarily large" timeout here.
|
||||
expiration: USER_SHELL_TIMEOUT_MS.into(),
|
||||
|
||||
@@ -216,7 +216,7 @@ impl ShellHandler {
|
||||
async fn run_exec_like(args: RunExecLikeArgs) -> Result<ToolOutput, FunctionCallError> {
|
||||
let RunExecLikeArgs {
|
||||
tool_name,
|
||||
exec_params,
|
||||
mut exec_params,
|
||||
prefix_rule,
|
||||
session,
|
||||
turn,
|
||||
@@ -233,6 +233,10 @@ impl ShellHandler {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(overrides) = session.skill_env_overrides(&turn.sub_id).await {
|
||||
exec_params.env.extend(overrides);
|
||||
}
|
||||
|
||||
// Approval policy guard for explicit escalation in non-OnRequest modes.
|
||||
if exec_params
|
||||
.sandbox_permissions
|
||||
|
||||
@@ -483,7 +483,14 @@ impl UnifiedExecProcessManager {
|
||||
cwd: PathBuf,
|
||||
context: &UnifiedExecContext,
|
||||
) -> Result<UnifiedExecProcess, UnifiedExecError> {
|
||||
let env = apply_unified_exec_env(create_env(&context.turn.shell_environment_policy));
|
||||
let mut env = apply_unified_exec_env(create_env(&context.turn.shell_environment_policy));
|
||||
if let Some(overrides) = context
|
||||
.session
|
||||
.skill_env_overrides(&context.turn.sub_id)
|
||||
.await
|
||||
{
|
||||
env.extend(overrides);
|
||||
}
|
||||
let features = context.session.features();
|
||||
let mut orchestrator = ToolOrchestrator::new();
|
||||
let mut runtime = UnifiedExecRuntime::new(self);
|
||||
|
||||
Reference in New Issue
Block a user