Compare commits

...

9 Commits

Author SHA1 Message Date
viyatb-oai
935cfa3949 Merge branch 'main' into codex/viyatb/deny-read-enforcement 2026-05-07 23:05:17 -07:00
xl-openai
ae15343243 feat: Update plugin share settings with discoverability (#21637)
Requires discoverability on plugin/share/updateTargets so the server can
manage workspace link access consistently, including auto-adding the
workspace principal for UNLISTED.

Also rejects LISTED on share creation and blocks client-supplied
workspace principals while preserving response parsing for LISTED.
2026-05-07 21:28:18 -07:00
Celia Chen
9cbd4c0371 feat: enable AWS login credentials for Bedrock auth (#21623)
## Summary

Codex's Amazon Bedrock provider signs Mantle requests with SigV4 using
credentials resolved by the AWS SDK. That worked for standard AWS
profiles and environment credentials, but AWS CLI console-login profiles
created by `aws login` require the SDK's `credentials-login` feature to
resolve `login_session` credentials.

This change enables that credential provider so Bedrock can use AWS
console-login credentials through the existing provider-owned AWS auth
path.

While testing the console-login path, we also hit a Mantle-specific
SigV4 regression from the new split between `session_id` and
`thread_id`. Mantle does not preserve legacy OpenAI compatibility
headers that use `snake_case` before SigV4 verification, so signing
those headers can make the server reconstruct a different canonical
request. The Bedrock auth path now removes that header class before
signing, keeping preserved hyphenated Codex/AWS headers such as
`x-codex-turn-metadata` signed normally.

## Changes

- Enable `aws-config`'s `credentials-login` feature in
`codex-rs/aws-auth`.
- Add a compile-time regression test for
`aws_config::login::LoginCredentialsProvider`.
- Strip `snake_case` compatibility headers from Bedrock Mantle SigV4
requests before signing.
- Expand the Bedrock auth regression test to cover `session_id`,
`thread_id`, and future headers of the same shape.
- Refresh Cargo and Bazel lockfiles for the added `aws-sdk-signin`
dependency.

## Tests
- tested with `aws login` locally and verified that it works as
intended.
2026-05-08 04:07:59 +00:00
viyatb-oai
f48d18d229 refactor(core): centralize first-attempt sandbox override
Co-authored-by: Codex noreply@openai.com
2026-05-05 17:03:51 -07:00
viyatb-oai
1efc5b0681 test(core): avoid known-safe command in exec-policy test
Co-authored-by: Codex noreply@openai.com
2026-05-05 13:16:24 -07:00
viyatb-oai
212e89c0f5 fix(permissions): preserve exec-policy bypass semantics
Co-authored-by: Codex noreply@openai.com
2026-05-05 13:16:24 -07:00
viyatb-oai
8357e97075 fix(core): tighten deny-read rebase cleanup
Co-authored-by: Codex noreply@openai.com
2026-05-05 13:16:23 -07:00
viyatb-oai
0026ec9e54 fix(core): preserve deny-read during escalation
Keep approval/escalation flow intact while ensuring deny-read policies do not allow first-attempt sandbox bypass. Centralize the clamp in sandbox override selection and remove the special rejection helper.

Co-authored-by: Codex <noreply@openai.com>
2026-05-05 13:16:05 -07:00
viyatb-oai
86f31a2ef9 fix(core): enforce explicit deny-read paths 2026-05-05 13:15:30 -07:00
27 changed files with 699 additions and 68 deletions

11
MODULE.bazel.lock generated

File diff suppressed because one or more lines are too long

146
codex-rs/Cargo.lock generated
View File

@@ -757,6 +757,7 @@ checksum = "96571e6996817bf3d58f6b569e4b9fd2e9d2fcf9f7424eed07b2ce9bb87535e5"
dependencies = [
"aws-credential-types",
"aws-runtime",
"aws-sdk-signin",
"aws-sdk-sso",
"aws-sdk-ssooidc",
"aws-sdk-sts",
@@ -767,15 +768,20 @@ dependencies = [
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-types",
"base64-simd",
"bytes",
"fastrand",
"hex",
"http 1.4.0",
"p256",
"rand 0.8.5",
"ring",
"sha2",
"time",
"tokio",
"tracing",
"url",
"uuid",
"zeroize",
]
@@ -838,6 +844,28 @@ dependencies = [
"uuid",
]
[[package]]
name = "aws-sdk-signin"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c084bd63941916e1348cb8d9e05ac2e49bdd40a380e9167702683184c6c6be53"
dependencies = [
"aws-credential-types",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-types",
"bytes",
"fastrand",
"http 0.2.12",
"regex-lite",
"tracing",
]
[[package]]
name = "aws-sdk-sso"
version = "1.91.0"
@@ -1180,6 +1208,12 @@ dependencies = [
"windows-link",
]
[[package]]
name = "base16ct"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
[[package]]
name = "base64"
version = "0.21.7"
@@ -4415,6 +4449,18 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-bigint"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
dependencies = [
"generic-array",
"rand_core 0.6.4",
"subtle",
"zeroize",
]
[[package]]
name = "crypto-common"
version = "0.1.7"
@@ -5128,6 +5174,20 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555"
[[package]]
name = "ecdsa"
version = "0.16.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
dependencies = [
"der",
"digest",
"elliptic-curve",
"rfc6979",
"signature",
"spki",
]
[[package]]
name = "ed25519"
version = "2.2.3"
@@ -5161,6 +5221,26 @@ dependencies = [
"serde",
]
[[package]]
name = "elliptic-curve"
version = "0.13.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
dependencies = [
"base16ct",
"crypto-bigint",
"digest",
"ff",
"generic-array",
"group",
"pem-rfc7468",
"pkcs8",
"rand_core 0.6.4",
"sec1",
"subtle",
"zeroize",
]
[[package]]
name = "ena"
version = "0.14.3"
@@ -5414,6 +5494,16 @@ dependencies = [
"simd-adler32",
]
[[package]]
name = "ff"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
dependencies = [
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "fiat-crypto"
version = "0.2.9"
@@ -6808,6 +6898,17 @@ dependencies = [
"system-deps",
]
[[package]]
name = "group"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
dependencies = [
"ff",
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "gzip-header"
version = "1.0.0"
@@ -9307,6 +9408,18 @@ dependencies = [
"supports-color 3.0.2",
]
[[package]]
name = "p256"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
dependencies = [
"ecdsa",
"elliptic-curve",
"primeorder",
"sha2",
]
[[package]]
name = "parking"
version = "2.2.1"
@@ -9736,6 +9849,15 @@ dependencies = [
"syn 2.0.114",
]
[[package]]
name = "primeorder"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
dependencies = [
"elliptic-curve",
]
[[package]]
name = "proc-macro-crate"
version = "3.4.0"
@@ -10686,6 +10808,16 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7"
[[package]]
name = "rfc6979"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
dependencies = [
"hmac",
"subtle",
]
[[package]]
name = "ring"
version = "0.17.14"
@@ -11145,6 +11277,20 @@ version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca"
[[package]]
name = "sec1"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
dependencies = [
"base16ct",
"der",
"generic-array",
"pkcs8",
"subtle",
"zeroize",
]
[[package]]
name = "seccompiler"
version = "0.5.0"

View File

@@ -2091,8 +2091,18 @@
],
"type": "object"
},
"PluginShareUpdateDiscoverability": {
"enum": [
"UNLISTED",
"PRIVATE"
],
"type": "string"
},
"PluginShareUpdateTargetsParams": {
"properties": {
"discoverability": {
"$ref": "#/definitions/PluginShareUpdateDiscoverability"
},
"remotePluginId": {
"type": "string"
},
@@ -2104,6 +2114,7 @@
}
},
"required": [
"discoverability",
"remotePluginId",
"shareTargets"
],
@@ -6177,4 +6188,4 @@
}
],
"title": "ClientRequest"
}
}

View File

@@ -12414,9 +12414,19 @@
],
"type": "object"
},
"PluginShareUpdateDiscoverability": {
"enum": [
"UNLISTED",
"PRIVATE"
],
"type": "string"
},
"PluginShareUpdateTargetsParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"discoverability": {
"$ref": "#/definitions/v2/PluginShareUpdateDiscoverability"
},
"remotePluginId": {
"type": "string"
},
@@ -12428,6 +12438,7 @@
}
},
"required": [
"discoverability",
"remotePluginId",
"shareTargets"
],
@@ -12437,6 +12448,9 @@
"PluginShareUpdateTargetsResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"discoverability": {
"$ref": "#/definitions/v2/PluginShareDiscoverability"
},
"principals": {
"items": {
"$ref": "#/definitions/v2/PluginSharePrincipal"
@@ -12445,6 +12459,7 @@
}
},
"required": [
"discoverability",
"principals"
],
"title": "PluginShareUpdateTargetsResponse",
@@ -18396,4 +18411,4 @@
},
"title": "CodexAppServerProtocol",
"type": "object"
}
}

View File

@@ -9007,9 +9007,19 @@
],
"type": "object"
},
"PluginShareUpdateDiscoverability": {
"enum": [
"UNLISTED",
"PRIVATE"
],
"type": "string"
},
"PluginShareUpdateTargetsParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"discoverability": {
"$ref": "#/definitions/PluginShareUpdateDiscoverability"
},
"remotePluginId": {
"type": "string"
},
@@ -9021,6 +9031,7 @@
}
},
"required": [
"discoverability",
"remotePluginId",
"shareTargets"
],
@@ -9030,6 +9041,9 @@
"PluginShareUpdateTargetsResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"discoverability": {
"$ref": "#/definitions/PluginShareDiscoverability"
},
"principals": {
"items": {
"$ref": "#/definitions/PluginSharePrincipal"
@@ -9038,6 +9052,7 @@
}
},
"required": [
"discoverability",
"principals"
],
"title": "PluginShareUpdateTargetsResponse",
@@ -16263,4 +16278,4 @@
},
"title": "CodexAppServerProtocolV2",
"type": "object"
}
}

View File

@@ -23,9 +23,19 @@
"principalType"
],
"type": "object"
},
"PluginShareUpdateDiscoverability": {
"enum": [
"UNLISTED",
"PRIVATE"
],
"type": "string"
}
},
"properties": {
"discoverability": {
"$ref": "#/definitions/PluginShareUpdateDiscoverability"
},
"remotePluginId": {
"type": "string"
},
@@ -37,6 +47,7 @@
}
},
"required": [
"discoverability",
"remotePluginId",
"shareTargets"
],

View File

@@ -1,6 +1,14 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"PluginShareDiscoverability": {
"enum": [
"LISTED",
"UNLISTED",
"PRIVATE"
],
"type": "string"
},
"PluginSharePrincipal": {
"properties": {
"name": {
@@ -30,6 +38,9 @@
}
},
"properties": {
"discoverability": {
"$ref": "#/definitions/PluginShareDiscoverability"
},
"principals": {
"items": {
"$ref": "#/definitions/PluginSharePrincipal"
@@ -38,6 +49,7 @@
}
},
"required": [
"discoverability",
"principals"
],
"title": "PluginShareUpdateTargetsResponse",

View File

@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type PluginShareUpdateDiscoverability = "UNLISTED" | "PRIVATE";

View File

@@ -2,5 +2,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PluginShareTarget } from "./PluginShareTarget";
import type { PluginShareUpdateDiscoverability } from "./PluginShareUpdateDiscoverability";
export type PluginShareUpdateTargetsParams = { remotePluginId: string, shareTargets: Array<PluginShareTarget>, };
export type PluginShareUpdateTargetsParams = { remotePluginId: string, discoverability: PluginShareUpdateDiscoverability, shareTargets: Array<PluginShareTarget>, };

View File

@@ -1,6 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PluginShareDiscoverability } from "./PluginShareDiscoverability";
import type { PluginSharePrincipal } from "./PluginSharePrincipal";
export type PluginShareUpdateTargetsResponse = { principals: Array<PluginSharePrincipal>, };
export type PluginShareUpdateTargetsResponse = { principals: Array<PluginSharePrincipal>, discoverability: PluginShareDiscoverability, };

View File

@@ -287,6 +287,7 @@ export type { PluginSharePrincipalType } from "./PluginSharePrincipalType";
export type { PluginShareSaveParams } from "./PluginShareSaveParams";
export type { PluginShareSaveResponse } from "./PluginShareSaveResponse";
export type { PluginShareTarget } from "./PluginShareTarget";
export type { PluginShareUpdateDiscoverability } from "./PluginShareUpdateDiscoverability";
export type { PluginShareUpdateTargetsParams } from "./PluginShareUpdateTargetsParams";
export type { PluginShareUpdateTargetsResponse } from "./PluginShareUpdateTargetsResponse";
export type { PluginSkillReadParams } from "./PluginSkillReadParams";

View File

@@ -218,6 +218,7 @@ pub struct PluginShareSaveResponse {
#[ts(export_to = "v2/")]
pub struct PluginShareUpdateTargetsParams {
pub remote_plugin_id: String,
pub discoverability: PluginShareUpdateDiscoverability,
pub share_targets: Vec<PluginShareTarget>,
}
@@ -226,6 +227,7 @@ pub struct PluginShareUpdateTargetsParams {
#[ts(export_to = "v2/")]
pub struct PluginShareUpdateTargetsResponse {
pub principals: Vec<PluginSharePrincipal>,
pub discoverability: PluginShareDiscoverability,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -275,6 +277,17 @@ pub enum PluginShareDiscoverability {
Private,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[ts(export_to = "v2/")]
pub enum PluginShareUpdateDiscoverability {
#[serde(rename = "UNLISTED")]
#[ts(rename = "UNLISTED")]
Unlisted,
#[serde(rename = "PRIVATE")]
#[ts(rename = "PRIVATE")]
Private,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[ts(export_to = "v2/")]
pub enum PluginSharePrincipalType {

View File

@@ -2936,6 +2936,7 @@ fn plugin_share_params_and_response_serialization_use_camel_case_fields() {
assert_eq!(
serde_json::to_value(PluginShareUpdateTargetsParams {
remote_plugin_id: "plugins~Plugin_00000000000000000000000000000000".to_string(),
discoverability: PluginShareUpdateDiscoverability::Unlisted,
share_targets: vec![PluginShareTarget {
principal_type: PluginSharePrincipalType::Group,
principal_id: "group-1".to_string(),
@@ -2944,6 +2945,7 @@ fn plugin_share_params_and_response_serialization_use_camel_case_fields() {
.unwrap(),
json!({
"remotePluginId": "plugins~Plugin_00000000000000000000000000000000",
"discoverability": "UNLISTED",
"shareTargets": [{
"principalType": "group",
"principalId": "group-1",
@@ -2958,6 +2960,7 @@ fn plugin_share_params_and_response_serialization_use_camel_case_fields() {
principal_id: "user-1".to_string(),
name: "Gavin".to_string(),
}],
discoverability: PluginShareDiscoverability::Unlisted,
})
.unwrap(),
json!({
@@ -2966,6 +2969,7 @@ fn plugin_share_params_and_response_serialization_use_camel_case_fields() {
"principalId": "user-1",
"name": "Gavin",
}],
"discoverability": "UNLISTED",
}),
);

View File

@@ -124,6 +124,7 @@ use codex_app_server_protocol::PluginSharePrincipalType;
use codex_app_server_protocol::PluginShareSaveParams;
use codex_app_server_protocol::PluginShareSaveResponse;
use codex_app_server_protocol::PluginShareTarget;
use codex_app_server_protocol::PluginShareUpdateDiscoverability;
use codex_app_server_protocol::PluginShareUpdateTargetsParams;
use codex_app_server_protocol::PluginShareUpdateTargetsResponse;
use codex_app_server_protocol::PluginSkillReadParams;

View File

@@ -133,6 +133,33 @@ fn remote_plugin_share_discoverability(
}
}
fn remote_plugin_share_update_discoverability(
discoverability: PluginShareUpdateDiscoverability,
) -> codex_core_plugins::remote::RemotePluginShareUpdateDiscoverability {
match discoverability {
PluginShareUpdateDiscoverability::Unlisted => {
codex_core_plugins::remote::RemotePluginShareUpdateDiscoverability::Unlisted
}
PluginShareUpdateDiscoverability::Private => {
codex_core_plugins::remote::RemotePluginShareUpdateDiscoverability::Private
}
}
}
fn validate_client_plugin_share_targets(
targets: &[PluginShareTarget],
) -> Result<(), JSONRPCErrorError> {
if targets
.iter()
.any(|target| target.principal_type == PluginSharePrincipalType::Workspace)
{
return Err(invalid_request(
"shareTargets cannot include workspace principals; use discoverability UNLISTED for workspace link access",
));
}
Ok(())
}
fn remote_plugin_share_targets(
targets: Vec<PluginShareTarget>,
) -> Vec<codex_core_plugins::remote::RemotePluginShareTarget> {
@@ -729,9 +756,17 @@ impl PluginRequestProcessor {
}
if remote_plugin_id.is_some() && (discoverability.is_some() || share_targets.is_some()) {
return Err(invalid_request(
"discoverability and shareTargets are only supported when creating a plugin share; use plugin/share/updateTargets to update share targets",
"discoverability and shareTargets are only supported when creating a plugin share; use plugin/share/updateTargets to update share settings",
));
}
if discoverability == Some(PluginShareDiscoverability::Listed) {
return Err(invalid_request(
"discoverability LISTED is not supported for plugin/share/save; use UNLISTED or PRIVATE",
));
}
if let Some(share_targets) = share_targets.as_ref() {
validate_client_plugin_share_targets(share_targets)?;
}
let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
@@ -765,11 +800,14 @@ impl PluginRequestProcessor {
let (config, auth) = self.load_plugin_share_config_and_auth().await?;
let PluginShareUpdateTargetsParams {
remote_plugin_id,
discoverability,
share_targets,
} = params;
if remote_plugin_id.is_empty() || !is_valid_remote_plugin_id(&remote_plugin_id) {
return Err(invalid_request("invalid remote plugin id"));
}
validate_client_plugin_share_targets(&share_targets)?;
let requested_share_targets = share_targets.clone();
let remote_plugin_service_config = RemotePluginServiceConfig {
chatgpt_base_url: config.chatgpt_base_url.clone(),
@@ -779,6 +817,7 @@ impl PluginRequestProcessor {
auth.as_ref(),
&remote_plugin_id,
remote_plugin_share_targets(share_targets),
remote_plugin_share_update_discoverability(discoverability),
)
.await
.map_err(|err| {
@@ -790,7 +829,14 @@ impl PluginRequestProcessor {
.principals
.into_iter()
.map(plugin_share_principal_from_remote)
.filter(|principal| {
requested_share_targets.iter().any(|target| {
target.principal_type == principal.principal_type
&& target.principal_id == principal.principal_id
})
})
.collect(),
discoverability: remote_plugin_share_discoverability_to_info(result.discoverability),
})
}
@@ -1487,6 +1533,22 @@ fn remote_plugin_share_context_to_info(
}
}
fn remote_plugin_share_discoverability_to_info(
discoverability: codex_core_plugins::remote::RemotePluginShareDiscoverability,
) -> PluginShareDiscoverability {
match discoverability {
codex_core_plugins::remote::RemotePluginShareDiscoverability::Listed => {
PluginShareDiscoverability::Listed
}
codex_core_plugins::remote::RemotePluginShareDiscoverability::Unlisted => {
PluginShareDiscoverability::Unlisted
}
codex_core_plugins::remote::RemotePluginShareDiscoverability::Private => {
PluginShareDiscoverability::Private
}
}
}
fn remote_plugin_detail_to_info(
detail: RemoteCatalogPluginDetail,
apps: Vec<AppSummary>,

View File

@@ -219,7 +219,7 @@ async fn plugin_share_save_forwards_access_policy() -> Result<()> {
.and(body_json(json!({
"file_id": "file_123",
"etag": "\"upload_etag_123\"",
"discoverability": "PRIVATE",
"discoverability": "UNLISTED",
"share_targets": [
{
"principal_type": "user",
@@ -227,7 +227,7 @@ async fn plugin_share_save_forwards_access_policy() -> Result<()> {
},
{
"principal_type": "workspace",
"principal_id": "workspace-1",
"principal_id": "account-123",
},
],
})))
@@ -247,16 +247,12 @@ async fn plugin_share_save_forwards_access_policy() -> Result<()> {
"plugin/share/save",
Some(json!({
"pluginPath": expected_plugin_path,
"discoverability": "PRIVATE",
"discoverability": "UNLISTED",
"shareTargets": [
{
"principalType": "user",
"principalId": "user-1",
},
{
"principalType": "workspace",
"principalId": "workspace-1",
},
],
})),
)
@@ -279,6 +275,124 @@ async fn plugin_share_save_forwards_access_policy() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn plugin_share_save_rejects_listed_discoverability() -> Result<()> {
let codex_home = TempDir::new()?;
let plugin_root = TempDir::new()?;
let plugin_path = write_test_plugin(plugin_root.path(), "demo-plugin")?;
let server = MockServer::start().await;
write_remote_plugin_config(codex_home.path(), &format!("{}/backend-api", server.uri()))?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("chatgpt-token")
.account_id("account-123")
.chatgpt_user_id("user-123")
.chatgpt_account_id("account-123"),
AuthCredentialsStoreMode::File,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_raw_request(
"plugin/share/save",
Some(json!({
"pluginPath": AbsolutePathBuf::try_from(plugin_path)?,
"discoverability": "LISTED",
})),
)
.await?;
let error: JSONRPCError = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(error.error.code, -32600);
assert_eq!(
error.error.message,
"discoverability LISTED is not supported for plugin/share/save; use UNLISTED or PRIVATE"
);
Ok(())
}
#[tokio::test]
async fn plugin_share_rejects_workspace_targets_from_client() -> Result<()> {
let codex_home = TempDir::new()?;
let plugin_root = TempDir::new()?;
let plugin_path = write_test_plugin(plugin_root.path(), "demo-plugin")?;
let server = MockServer::start().await;
write_remote_plugin_config(codex_home.path(), &format!("{}/backend-api", server.uri()))?;
write_chatgpt_auth(
codex_home.path(),
ChatGptAuthFixture::new("chatgpt-token")
.account_id("account-123")
.chatgpt_user_id("user-123")
.chatgpt_account_id("account-123"),
AuthCredentialsStoreMode::File,
)?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_raw_request(
"plugin/share/save",
Some(json!({
"pluginPath": AbsolutePathBuf::try_from(plugin_path)?,
"discoverability": "UNLISTED",
"shareTargets": [
{
"principalType": "workspace",
"principalId": "account-123",
},
],
})),
)
.await?;
let error: JSONRPCError = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(error.error.code, -32600);
assert_eq!(
error.error.message,
"shareTargets cannot include workspace principals; use discoverability UNLISTED for workspace link access"
);
let request_id = mcp
.send_raw_request(
"plugin/share/updateTargets",
Some(json!({
"remotePluginId": "plugins_123",
"discoverability": "UNLISTED",
"shareTargets": [
{
"principalType": "workspace",
"principalId": "account-123",
},
],
})),
)
.await?;
let error: JSONRPCError = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert_eq!(error.error.code, -32600);
assert_eq!(
error.error.message,
"shareTargets cannot include workspace principals; use discoverability UNLISTED for workspace link access"
);
Ok(())
}
#[tokio::test]
async fn plugin_share_save_rejects_access_policy_for_existing_plugin() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -323,7 +437,7 @@ async fn plugin_share_save_rejects_access_policy_for_existing_plugin() -> Result
assert_eq!(error.error.code, -32600);
assert_eq!(
error.error.message,
"discoverability and shareTargets are only supported when creating a plugin share; use plugin/share/updateTargets to update share targets"
"discoverability and shareTargets are only supported when creating a plugin share; use plugin/share/updateTargets to update share settings"
);
Ok(())
}
@@ -420,24 +534,39 @@ async fn plugin_share_update_targets_updates_share_targets() -> Result<()> {
)?;
Mock::given(method("PUT"))
.and(path("/backend-api/public/plugins/plugins_123/shares"))
.and(path("/backend-api/ps/plugins/plugins_123/shares"))
.and(header("authorization", "Bearer chatgpt-token"))
.and(header("chatgpt-account-id", "account-123"))
.and(body_json(json!({
"discoverability": "UNLISTED",
"targets": [
{
"principal_type": "user",
"principal_id": "user-1",
},
{
"principal_type": "workspace",
"principal_id": "account-123",
},
],
})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"principals": [
{
"principal_type": "user",
"principal_id": "owner-1",
"name": "Owner",
},
{
"principal_type": "user",
"principal_id": "user-1",
"name": "Gavin",
},
{
"principal_type": "workspace",
"principal_id": "account-123",
"name": "Workspace",
},
],
})))
.expect(1)
@@ -451,6 +580,7 @@ async fn plugin_share_update_targets_updates_share_targets() -> Result<()> {
"plugin/share/updateTargets",
Some(json!({
"remotePluginId": "plugins_123",
"discoverability": "UNLISTED",
"shareTargets": [
{
"principalType": "user",
@@ -476,6 +606,7 @@ async fn plugin_share_update_targets_updates_share_targets() -> Result<()> {
principal_id: "user-1".to_string(),
name: "Gavin".to_string(),
}],
discoverability: codex_app_server_protocol::PluginShareDiscoverability::Unlisted,
}
);
Ok(())

View File

@@ -13,7 +13,7 @@ path = "src/lib.rs"
workspace = true
[dependencies]
aws-config = { workspace = true }
aws-config = { workspace = true, features = ["credentials-login"] }
aws-credential-types = { workspace = true }
aws-sigv4 = { workspace = true }
aws-types = { workspace = true }

View File

@@ -35,6 +35,7 @@ pub use share::RemotePluginSharePrincipal;
pub use share::RemotePluginSharePrincipalType;
pub use share::RemotePluginShareSaveResult;
pub use share::RemotePluginShareTarget;
pub use share::RemotePluginShareUpdateDiscoverability;
pub use share::RemotePluginShareUpdateTargetsResult;
pub use share::delete_remote_plugin_share;
pub use share::list_remote_plugin_shares;

View File

@@ -32,7 +32,7 @@ pub struct RemotePluginShareAccessPolicy {
pub share_targets: Option<Vec<RemotePluginShareTarget>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RemotePluginShareDiscoverability {
Listed,
@@ -40,6 +40,13 @@ pub enum RemotePluginShareDiscoverability {
Private,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum RemotePluginShareUpdateDiscoverability {
Unlisted,
Private,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RemotePluginSharePrincipalType {
@@ -64,6 +71,7 @@ pub struct RemotePluginSharePrincipal {
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RemotePluginShareUpdateTargetsResult {
pub principals: Vec<RemotePluginSharePrincipal>,
pub discoverability: RemotePluginShareDiscoverability,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
@@ -100,12 +108,14 @@ struct RemoteWorkspacePluginCreateResponse {
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct RemotePluginShareUpdateTargetsRequest {
discoverability: RemotePluginShareUpdateDiscoverability,
targets: Vec<RemotePluginShareTarget>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
struct RemotePluginShareUpdateTargetsResponse {
principals: Vec<RemotePluginSharePrincipal>,
discoverability: Option<RemotePluginShareDiscoverability>,
}
pub async fn save_remote_plugin_share(
@@ -137,6 +147,9 @@ pub async fn save_remote_plugin_share(
.etag
.ok_or(RemotePluginCatalogError::MissingUploadEtag)?;
put_workspace_plugin_upload(&upload.upload_url, archive_bytes).await?;
let share_targets = access_policy.share_targets;
let share_targets =
ensure_unlisted_workspace_target(auth, access_policy.discoverability, share_targets)?;
let response = finalize_workspace_plugin_upload(
config,
auth,
@@ -145,7 +158,7 @@ pub async fn save_remote_plugin_share(
file_id: upload.file_id,
etag,
discoverability: access_policy.discoverability,
share_targets: access_policy.share_targets,
share_targets,
},
)
.await?;
@@ -245,19 +258,64 @@ pub async fn update_remote_plugin_share_targets(
auth: Option<&CodexAuth>,
remote_plugin_id: &str,
targets: Vec<RemotePluginShareTarget>,
discoverability: RemotePluginShareUpdateDiscoverability,
) -> Result<RemotePluginShareUpdateTargetsResult, RemotePluginCatalogError> {
let auth = ensure_chatgpt_auth(auth)?;
let target_discoverability = match discoverability {
RemotePluginShareUpdateDiscoverability::Unlisted => {
RemotePluginShareDiscoverability::Unlisted
}
RemotePluginShareUpdateDiscoverability::Private => {
RemotePluginShareDiscoverability::Private
}
};
let targets =
ensure_unlisted_workspace_target(auth, Some(target_discoverability), Some(targets))?
.unwrap_or_default();
let base_url = config.chatgpt_base_url.trim_end_matches('/');
let url = format!("{base_url}/public/plugins/{remote_plugin_id}/shares");
let url = format!("{base_url}/ps/plugins/{remote_plugin_id}/shares");
let client = build_reqwest_client();
let request = authenticated_request(client.put(&url), auth)?
.json(&RemotePluginShareUpdateTargetsRequest { targets });
let request = authenticated_request(client.put(&url), auth)?.json(
&RemotePluginShareUpdateTargetsRequest {
discoverability,
targets,
},
);
let response: RemotePluginShareUpdateTargetsResponse = send_and_decode(request, &url).await?;
Ok(RemotePluginShareUpdateTargetsResult {
principals: response.principals,
// TODO: Remove this fallback once deployed plugin-service responses always include
// discoverability per the API schema.
discoverability: response.discoverability.unwrap_or(target_discoverability),
})
}
fn ensure_unlisted_workspace_target(
auth: &CodexAuth,
discoverability: Option<RemotePluginShareDiscoverability>,
targets: Option<Vec<RemotePluginShareTarget>>,
) -> Result<Option<Vec<RemotePluginShareTarget>>, RemotePluginCatalogError> {
if discoverability != Some(RemotePluginShareDiscoverability::Unlisted) {
return Ok(targets);
}
let account_id = auth.get_account_id().ok_or_else(|| {
RemotePluginCatalogError::UnexpectedResponse(
"workspace plugin share requires an account id".to_string(),
)
})?;
let mut targets = targets.unwrap_or_default();
if !targets.iter().any(|target| {
target.principal_type == RemotePluginSharePrincipalType::Workspace
&& target.principal_id == account_id
}) {
targets.push(RemotePluginShareTarget {
principal_type: RemotePluginSharePrincipalType::Workspace,
principal_id: account_id,
});
}
Ok(Some(targets))
}
async fn fetch_created_workspace_plugins(
config: &RemotePluginServiceConfig,
auth: &CodexAuth,

View File

@@ -204,7 +204,7 @@ async fn save_remote_plugin_share_creates_workspace_plugin() {
.and(body_json(json!({
"file_id": "file_123",
"etag": "\"upload_etag_123\"",
"discoverability": "PRIVATE",
"discoverability": "UNLISTED",
"share_targets": [
{
"principal_type": "user",
@@ -212,7 +212,7 @@ async fn save_remote_plugin_share_creates_workspace_plugin() {
},
{
"principal_type": "workspace",
"principal_id": "workspace-1",
"principal_id": "account_id",
},
],
})))
@@ -231,17 +231,11 @@ async fn save_remote_plugin_share_creates_workspace_plugin() {
&plugin_path,
/*remote_plugin_id*/ None,
RemotePluginShareAccessPolicy {
discoverability: Some(RemotePluginShareDiscoverability::Private),
share_targets: Some(vec![
RemotePluginShareTarget {
principal_type: RemotePluginSharePrincipalType::User,
principal_id: "user-1".to_string(),
},
RemotePluginShareTarget {
principal_type: RemotePluginSharePrincipalType::Workspace,
principal_id: "workspace-1".to_string(),
},
]),
discoverability: Some(RemotePluginShareDiscoverability::Unlisted),
share_targets: Some(vec![RemotePluginShareTarget {
principal_type: RemotePluginSharePrincipalType::User,
principal_id: "user-1".to_string(),
}]),
},
)
.await
@@ -401,10 +395,11 @@ async fn update_remote_plugin_share_targets_updates_targets() {
let auth = test_auth();
Mock::given(method("PUT"))
.and(path("/backend-api/public/plugins/plugins_123/shares"))
.and(path("/backend-api/ps/plugins/plugins_123/shares"))
.and(header("authorization", "Bearer Access Token"))
.and(header("chatgpt-account-id", "account_id"))
.and(body_json(json!({
"discoverability": "UNLISTED",
"targets": [
{
"principal_type": "user",
@@ -414,6 +409,10 @@ async fn update_remote_plugin_share_targets_updates_targets() {
"principal_type": "group",
"principal_id": "group-1",
},
{
"principal_type": "workspace",
"principal_id": "account_id",
},
],
})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
@@ -429,6 +428,7 @@ async fn update_remote_plugin_share_targets_updates_targets() {
"name": "Engineering",
},
],
"discoverability": "UNLISTED",
})))
.expect(1)
.mount(&server)
@@ -448,6 +448,7 @@ async fn update_remote_plugin_share_targets_updates_targets() {
principal_id: "group-1".to_string(),
},
],
RemotePluginShareUpdateDiscoverability::Unlisted,
)
.await
.unwrap();
@@ -467,6 +468,65 @@ async fn update_remote_plugin_share_targets_updates_targets() {
name: "Engineering".to_string(),
},
],
discoverability: RemotePluginShareDiscoverability::Unlisted,
}
);
}
#[tokio::test]
async fn update_remote_plugin_share_targets_falls_back_to_requested_discoverability() {
let server = MockServer::start().await;
let config = test_config(&server);
let auth = test_auth();
Mock::given(method("PUT"))
.and(path("/backend-api/ps/plugins/plugins_123/shares"))
.and(header("authorization", "Bearer Access Token"))
.and(header("chatgpt-account-id", "account_id"))
.and(body_json(json!({
"discoverability": "PRIVATE",
"targets": [
{
"principal_type": "user",
"principal_id": "user-1",
},
],
})))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"principals": [
{
"principal_type": "user",
"principal_id": "user-1",
"name": "Gavin",
},
],
})))
.expect(1)
.mount(&server)
.await;
let result = update_remote_plugin_share_targets(
&config,
Some(&auth),
"plugins_123",
vec![RemotePluginShareTarget {
principal_type: RemotePluginSharePrincipalType::User,
principal_id: "user-1".to_string(),
}],
RemotePluginShareUpdateDiscoverability::Private,
)
.await
.unwrap();
assert_eq!(
result,
RemotePluginShareUpdateTargetsResult {
principals: vec![RemotePluginSharePrincipal {
principal_type: RemotePluginSharePrincipalType::User,
principal_id: "user-1".to_string(),
name: "Gavin".to_string(),
}],
discoverability: RemotePluginShareDiscoverability::Private,
}
);
}

View File

@@ -1705,8 +1705,12 @@ async fn proposed_execpolicy_amendment_is_present_when_heuristics_allow() {
async fn proposed_execpolicy_amendment_is_suppressed_when_policy_matches_allow() {
assert_exec_approval_requirement_for_command(
ExecApprovalRequirementScenario {
policy_src: Some(r#"prefix_rule(pattern=["echo"], decision="allow")"#.to_string()),
command: vec!["echo".to_string(), "safe".to_string()],
policy_src: Some(r#"prefix_rule(pattern=["python3"], decision="allow")"#.to_string()),
command: vec![
"python3".to_string(),
"-c".to_string(),
"print(1)".to_string(),
],
approval_policy: AskForApproval::OnRequest,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),

View File

@@ -26,6 +26,7 @@ use crate::tools::sandboxing::ToolCtx;
use crate::tools::sandboxing::ToolError;
use crate::tools::sandboxing::ToolRuntime;
use crate::tools::sandboxing::default_exec_approval_requirement;
use crate::tools::sandboxing::sandbox_override_for_first_attempt;
use codex_hooks::PermissionRequestDecision;
use codex_otel::ToolDecisionSource;
use codex_protocol::error::CodexErr;
@@ -148,6 +149,11 @@ impl ToolOrchestrator {
let requirement = tool.exec_approval_requirement(req).unwrap_or_else(|| {
default_exec_approval_requirement(approval_policy, &file_system_sandbox_policy)
});
let sandbox_override = sandbox_override_for_first_attempt(
tool.sandbox_permissions(req),
&requirement,
&file_system_sandbox_policy,
);
match requirement {
ExecApprovalRequirement::Skip { .. } => {
if strict_auto_review {
@@ -214,7 +220,7 @@ impl ToolOrchestrator {
// 2) First attempt under the selected sandbox.
let managed_network_active = turn_ctx.network.is_some();
let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) {
let initial_sandbox = match sandbox_override {
SandboxOverride::BypassSandboxFirstAttempt => SandboxType::None,
SandboxOverride::NoOverride => self.sandbox.select_initial(
&file_system_sandbox_policy,

View File

@@ -27,13 +27,11 @@ use crate::tools::sandboxing::ApprovalCtx;
use crate::tools::sandboxing::ExecApprovalRequirement;
use crate::tools::sandboxing::PermissionRequestPayload;
use crate::tools::sandboxing::SandboxAttempt;
use crate::tools::sandboxing::SandboxOverride;
use crate::tools::sandboxing::Sandboxable;
use crate::tools::sandboxing::ToolCtx;
use crate::tools::sandboxing::ToolError;
use crate::tools::sandboxing::ToolRuntime;
use crate::tools::sandboxing::managed_network_for_sandbox_permissions;
use crate::tools::sandboxing::sandbox_override_for_first_attempt;
use crate::tools::sandboxing::with_cached_approval;
use codex_network_proxy::NetworkProxy;
use codex_protocol::exec_output::ExecToolCallOutput;
@@ -209,8 +207,8 @@ impl Approvable<ShellRequest> for ShellRuntime {
))
}
fn sandbox_mode_for_first_attempt(&self, req: &ShellRequest) -> SandboxOverride {
sandbox_override_for_first_attempt(req.sandbox_permissions, &req.exec_approval_requirement)
fn sandbox_permissions(&self, req: &ShellRequest) -> SandboxPermissions {
req.sandbox_permissions
}
}

View File

@@ -25,13 +25,11 @@ use crate::tools::sandboxing::ApprovalCtx;
use crate::tools::sandboxing::ExecApprovalRequirement;
use crate::tools::sandboxing::PermissionRequestPayload;
use crate::tools::sandboxing::SandboxAttempt;
use crate::tools::sandboxing::SandboxOverride;
use crate::tools::sandboxing::Sandboxable;
use crate::tools::sandboxing::ToolCtx;
use crate::tools::sandboxing::ToolError;
use crate::tools::sandboxing::ToolRuntime;
use crate::tools::sandboxing::managed_network_for_sandbox_permissions;
use crate::tools::sandboxing::sandbox_override_for_first_attempt;
use crate::tools::sandboxing::with_cached_approval;
use crate::unified_exec::NoopSpawnLifecycle;
use crate::unified_exec::UnifiedExecError;
@@ -211,8 +209,8 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
))
}
fn sandbox_mode_for_first_attempt(&self, req: &UnifiedExecRequest) -> SandboxOverride {
sandbox_override_for_first_attempt(req.sandbox_permissions, &req.exec_approval_requirement)
fn sandbox_permissions(&self, req: &UnifiedExecRequest) -> SandboxPermissions {
req.sandbox_permissions
}
}

View File

@@ -247,18 +247,27 @@ pub(crate) enum SandboxOverride {
pub(crate) fn sandbox_override_for_first_attempt(
sandbox_permissions: SandboxPermissions,
exec_approval_requirement: &ExecApprovalRequirement,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
) -> SandboxOverride {
// ExecPolicy `Allow` can intentionally imply full trust (Skip + bypass_sandbox=true),
// which supersedes `with_additional_permissions` sandboxed execution hints.
if sandbox_permissions.requires_escalated_permissions()
|| matches!(
exec_approval_requirement,
ExecApprovalRequirement::Skip {
bypass_sandbox: true,
..
}
)
{
if matches!(
exec_approval_requirement,
ExecApprovalRequirement::Skip {
bypass_sandbox: true,
..
}
) {
return SandboxOverride::BypassSandboxFirstAttempt;
}
// Deny-read restrictions suppress explicit escalation because that path
// would otherwise discard the filesystem policy entirely.
if file_system_sandbox_policy.has_denied_read_restrictions() {
return SandboxOverride::NoOverride;
}
if sandbox_permissions.requires_escalated_permissions() {
SandboxOverride::BypassSandboxFirstAttempt
} else {
SandboxOverride::NoOverride
@@ -288,11 +297,10 @@ pub(crate) trait Approvable<Req> {
// requests touching a subset can be auto-approved.
fn approval_keys(&self, req: &Req) -> Vec<Self::ApprovalKey>;
/// Some tools may request to skip the sandbox on the first attempt
/// (e.g., when the request explicitly asks for escalated permissions).
/// Defaults to `NoOverride`.
fn sandbox_mode_for_first_attempt(&self, _req: &Req) -> SandboxOverride {
SandboxOverride::NoOverride
/// Return per-request sandbox permissions for first-attempt sandbox
/// selection. Most tools use the ambient sandbox policy unchanged.
fn sandbox_permissions(&self, _req: &Req) -> SandboxPermissions {
SandboxPermissions::UseDefault
}
fn should_bypass_approval(&self, policy: AskForApproval, already_approved: bool) -> bool {

View File

@@ -1,6 +1,10 @@
use super::*;
use crate::sandboxing::SandboxPermissions;
use crate::tools::hook_names::HookToolName;
use codex_protocol::permissions::FileSystemAccessMode;
use codex_protocol::permissions::FileSystemPath;
use codex_protocol::permissions::FileSystemSandboxEntry;
use codex_protocol::permissions::FileSystemSpecialPath;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::NetworkAccess;
use pretty_assertions::assert_eq;
@@ -120,6 +124,7 @@ fn additional_permissions_allow_bypass_sandbox_first_attempt_when_execpolicy_ski
bypass_sandbox: true,
proposed_execpolicy_amendment: None,
},
&FileSystemSandboxPolicy::default(),
),
SandboxOverride::BypassSandboxFirstAttempt
);
@@ -134,7 +139,43 @@ fn guardian_bypasses_sandbox_for_explicit_escalation_on_first_attempt() {
bypass_sandbox: false,
proposed_execpolicy_amendment: None,
},
&FileSystemSandboxPolicy::default(),
),
SandboxOverride::BypassSandboxFirstAttempt
);
}
#[test]
fn deny_read_blocks_explicit_escalation_but_preserves_policy_bypass() {
let file_system_policy = FileSystemSandboxPolicy::restricted(vec![FileSystemSandboxEntry {
path: FileSystemPath::Special {
value: FileSystemSpecialPath::Root,
},
access: FileSystemAccessMode::None,
}]);
assert_eq!(
sandbox_override_for_first_attempt(
SandboxPermissions::RequireEscalated,
&ExecApprovalRequirement::Skip {
bypass_sandbox: false,
proposed_execpolicy_amendment: None,
},
&file_system_policy,
),
SandboxOverride::NoOverride,
"explicit escalation would drop deny-read filesystem policy, so keep the first attempt sandboxed",
);
assert_eq!(
sandbox_override_for_first_attempt(
SandboxPermissions::WithAdditionalPermissions,
&ExecApprovalRequirement::Skip {
bypass_sandbox: true,
proposed_execpolicy_amendment: None,
},
&file_system_policy,
),
SandboxOverride::BypassSandboxFirstAttempt,
"exec-policy allow rules intentionally bypass sandbox even when deny-read entries exist",
);
}

View File

@@ -20,7 +20,6 @@ use super::mantle::aws_auth_config;
use super::mantle::region_from_config;
const AWS_BEARER_TOKEN_BEDROCK_ENV_VAR: &str = "AWS_BEARER_TOKEN_BEDROCK";
const LEGACY_SESSION_ID_HEADER: &str = "session_id";
pub(super) enum BedrockAuthMethod {
EnvBearerToken { token: String, region: String },
@@ -87,10 +86,18 @@ fn aws_auth_error_to_auth_error(error: AwsAuthError) -> AuthError {
}
fn remove_headers_not_preserved_by_bedrock_mantle(headers: &mut HeaderMap) {
// The Bedrock Mantle front door does not preserve this legacy OpenAI header
// for SigV4 verification. Signing it makes the richer Codex agent request
// fail even though raw Responses requests work.
headers.remove(LEGACY_SESSION_ID_HEADER);
// The Bedrock Mantle front door does not preserve legacy OpenAI
// compatibility headers that use snake_case, such as `session_id` and
// `thread_id`, before SigV4 verification. Signing that header class makes
// richer Codex agent requests fail even though raw Responses requests work.
let headers_to_remove = headers
.keys()
.filter(|name| name.as_str().contains('_'))
.cloned()
.collect::<Vec<_>>();
for name in headers_to_remove {
headers.remove(name);
}
}
/// AWS SigV4 auth provider for Bedrock Mantle OpenAI-compatible requests.
@@ -182,10 +189,18 @@ mod tests {
}
#[test]
fn bedrock_mantle_sigv4_strips_legacy_session_id_header() {
fn bedrock_mantle_sigv4_strips_headers_not_preserved_by_mantle() {
let mut headers = HeaderMap::new();
headers.insert(
LEGACY_SESSION_ID_HEADER,
"session_id",
HeaderValue::from_static("019dae79-15c3-70c3-8736-3219b8602b37"),
);
headers.insert(
"thread_id",
HeaderValue::from_static("019dae79-15c3-70c3-8736-3219b8602b37"),
);
headers.insert(
"future_identity_header",
HeaderValue::from_static("019dae79-15c3-70c3-8736-3219b8602b37"),
);
headers.insert(
@@ -195,7 +210,9 @@ mod tests {
remove_headers_not_preserved_by_bedrock_mantle(&mut headers);
assert!(!headers.contains_key(LEGACY_SESSION_ID_HEADER));
assert!(!headers.contains_key("session_id"));
assert!(!headers.contains_key("thread_id"));
assert!(!headers.contains_key("future_identity_header"));
assert_eq!(
headers
.get("x-client-request-id")