Merge remote-tracking branch 'origin/main' into pakrym/unrevert-skills-watcher-app-server-full-ci

This commit is contained in:
pakrym-oai
2026-05-07 23:40:17 -07:00
71 changed files with 1045 additions and 566 deletions
+11
View File
File diff suppressed because one or more lines are too long
+146
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"
+2 -1
View File
@@ -340,8 +340,9 @@ impl AnalyticsEventsClient {
});
}
pub fn track_server_response(&self, response: ServerResponse) {
pub fn track_server_response(&self, completed_at_ms: u64, response: ServerResponse) {
self.record_fact(AnalyticsFact::ServerResponse {
completed_at_ms,
response: Box::new(response),
});
}
+4 -3
View File
@@ -18,6 +18,7 @@ use crate::facts::TurnStatus;
use crate::facts::TurnSteerRejectionReason;
use crate::facts::TurnSteerResult;
use crate::facts::TurnSubmissionType;
use crate::now_unix_millis;
use crate::now_unix_seconds;
use codex_app_server_protocol::CodexErrorInfo;
use codex_app_server_protocol::CommandExecutionSource;
@@ -261,7 +262,7 @@ pub struct GuardianReviewTrackContext {
approval_request_source: GuardianApprovalRequestSource,
reviewed_action: GuardianReviewedAction,
review_timeout_ms: u64,
started_at: u64,
pub started_at_ms: u64,
started_instant: Instant,
}
@@ -283,7 +284,7 @@ impl GuardianReviewTrackContext {
approval_request_source,
reviewed_action,
review_timeout_ms,
started_at: now_unix_seconds(),
started_at_ms: now_unix_millis(),
started_instant: Instant::now(),
}
}
@@ -316,7 +317,7 @@ impl GuardianReviewTrackContext {
tool_call_count: None,
time_to_first_token_ms: result.time_to_first_token_ms,
completion_latency_ms: Some(self.started_instant.elapsed().as_millis() as u64),
started_at: self.started_at,
started_at: self.started_at_ms / 1_000,
completed_at: Some(now_unix_seconds()),
input_tokens: result.token_usage.as_ref().map(|usage| usage.input_tokens),
cached_input_tokens: result
+1
View File
@@ -296,6 +296,7 @@ pub(crate) enum AnalyticsFact {
request: Box<ServerRequest>,
},
ServerResponse {
completed_at_ms: u64,
response: Box<ServerResponse>,
},
Notification(Box<ServerNotification>),
+1
View File
@@ -325,6 +325,7 @@ impl AnalyticsReducer {
} => {}
AnalyticsFact::ServerResponse {
response: _response,
..
} => {}
AnalyticsFact::Custom(input) => match input {
CustomAnalyticsFact::SubAgentThreadStarted(input) => {
+12 -30
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"
],
@@ -3212,24 +3223,6 @@
],
"type": "object"
},
"SkillsListExtraRootsForCwd": {
"properties": {
"cwd": {
"type": "string"
},
"extraUserRoots": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"cwd",
"extraUserRoots"
],
"type": "object"
},
"SkillsListParams": {
"properties": {
"cwds": {
@@ -3242,17 +3235,6 @@
"forceReload": {
"description": "When true, bypass the skills cache and re-scan skills from disk.",
"type": "boolean"
},
"perCwdExtraUserRoots": {
"default": null,
"description": "Optional per-cwd extra roots to scan as user-scoped skills.",
"items": {
"$ref": "#/definitions/SkillsListExtraRootsForCwd"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
@@ -6206,4 +6188,4 @@
}
],
"title": "ClientRequest"
}
}
@@ -593,6 +593,11 @@
"null"
]
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this approval request started.",
"format": "int64",
"type": "integer"
},
"threadId": {
"type": "string"
},
@@ -602,6 +607,7 @@
},
"required": [
"itemId",
"startedAtMs",
"threadId",
"turnId"
],
@@ -18,6 +18,11 @@
"null"
]
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this approval request started.",
"format": "int64",
"type": "integer"
},
"threadId": {
"type": "string"
},
@@ -27,6 +32,7 @@
},
"required": [
"itemId",
"startedAtMs",
"threadId",
"turnId"
],
@@ -297,6 +297,11 @@
"null"
]
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this approval request started.",
"format": "int64",
"type": "integer"
},
"threadId": {
"type": "string"
},
@@ -308,6 +313,7 @@
"cwd",
"itemId",
"permissions",
"startedAtMs",
"threadId",
"turnId"
],
@@ -1963,6 +1963,11 @@
"action": {
"$ref": "#/definitions/GuardianApprovalReviewAction"
},
"completedAtMs": {
"description": "Unix timestamp (in milliseconds) when this review completed.",
"format": "int64",
"type": "integer"
},
"decisionSource": {
"$ref": "#/definitions/AutoReviewDecisionSource"
},
@@ -1973,6 +1978,11 @@
"description": "Stable identifier for this review.",
"type": "string"
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this review started.",
"format": "int64",
"type": "integer"
},
"targetItemId": {
"description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.",
"type": [
@@ -1989,9 +1999,11 @@
},
"required": [
"action",
"completedAtMs",
"decisionSource",
"review",
"reviewId",
"startedAtMs",
"threadId",
"turnId"
],
@@ -2010,6 +2022,11 @@
"description": "Stable identifier for this review.",
"type": "string"
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this review started.",
"format": "int64",
"type": "integer"
},
"targetItemId": {
"description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.",
"type": [
@@ -2028,6 +2045,7 @@
"action",
"review",
"reviewId",
"startedAtMs",
"threadId",
"turnId"
],
@@ -417,6 +417,11 @@
"null"
]
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this approval request started.",
"format": "int64",
"type": "integer"
},
"threadId": {
"type": "string"
},
@@ -426,6 +431,7 @@
},
"required": [
"itemId",
"startedAtMs",
"threadId",
"turnId"
],
@@ -598,6 +604,11 @@
"null"
]
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this approval request started.",
"format": "int64",
"type": "integer"
},
"threadId": {
"type": "string"
},
@@ -607,6 +618,7 @@
},
"required": [
"itemId",
"startedAtMs",
"threadId",
"turnId"
],
@@ -1587,6 +1599,11 @@
"null"
]
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this approval request started.",
"format": "int64",
"type": "integer"
},
"threadId": {
"type": "string"
},
@@ -1598,6 +1615,7 @@
"cwd",
"itemId",
"permissions",
"startedAtMs",
"threadId",
"turnId"
],
@@ -2146,6 +2146,11 @@
"null"
]
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this approval request started.",
"format": "int64",
"type": "integer"
},
"threadId": {
"type": "string"
},
@@ -2155,6 +2160,7 @@
},
"required": [
"itemId",
"startedAtMs",
"threadId",
"turnId"
],
@@ -2411,6 +2417,11 @@
"null"
]
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this approval request started.",
"format": "int64",
"type": "integer"
},
"threadId": {
"type": "string"
},
@@ -2420,6 +2431,7 @@
},
"required": [
"itemId",
"startedAtMs",
"threadId",
"turnId"
],
@@ -3591,6 +3603,11 @@
"null"
]
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this approval request started.",
"format": "int64",
"type": "integer"
},
"threadId": {
"type": "string"
},
@@ -3602,6 +3619,7 @@
"cwd",
"itemId",
"permissions",
"startedAtMs",
"threadId",
"turnId"
],
@@ -9878,6 +9896,11 @@
"action": {
"$ref": "#/definitions/v2/GuardianApprovalReviewAction"
},
"completedAtMs": {
"description": "Unix timestamp (in milliseconds) when this review completed.",
"format": "int64",
"type": "integer"
},
"decisionSource": {
"$ref": "#/definitions/v2/AutoReviewDecisionSource"
},
@@ -9888,6 +9911,11 @@
"description": "Stable identifier for this review.",
"type": "string"
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this review started.",
"format": "int64",
"type": "integer"
},
"targetItemId": {
"description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.",
"type": [
@@ -9904,9 +9932,11 @@
},
"required": [
"action",
"completedAtMs",
"decisionSource",
"review",
"reviewId",
"startedAtMs",
"threadId",
"turnId"
],
@@ -9927,6 +9957,11 @@
"description": "Stable identifier for this review.",
"type": "string"
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this review started.",
"format": "int64",
"type": "integer"
},
"targetItemId": {
"description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.",
"type": [
@@ -9945,6 +9980,7 @@
"action",
"review",
"reviewId",
"startedAtMs",
"threadId",
"turnId"
],
@@ -12378,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"
},
@@ -12392,6 +12438,7 @@
}
},
"required": [
"discoverability",
"remotePluginId",
"shareTargets"
],
@@ -12401,6 +12448,9 @@
"PluginShareUpdateTargetsResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"discoverability": {
"$ref": "#/definitions/v2/PluginShareDiscoverability"
},
"principals": {
"items": {
"$ref": "#/definitions/v2/PluginSharePrincipal"
@@ -12409,6 +12459,7 @@
}
},
"required": [
"discoverability",
"principals"
],
"title": "PluginShareUpdateTargetsResponse",
@@ -14730,24 +14781,6 @@
],
"type": "object"
},
"SkillsListExtraRootsForCwd": {
"properties": {
"cwd": {
"type": "string"
},
"extraUserRoots": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"cwd",
"extraUserRoots"
],
"type": "object"
},
"SkillsListParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -14761,17 +14794,6 @@
"forceReload": {
"description": "When true, bypass the skills cache and re-scan skills from disk.",
"type": "boolean"
},
"perCwdExtraUserRoots": {
"default": null,
"description": "Optional per-cwd extra roots to scan as user-scoped skills.",
"items": {
"$ref": "#/definitions/v2/SkillsListExtraRootsForCwd"
},
"type": [
"array",
"null"
]
}
},
"title": "SkillsListParams",
@@ -18389,4 +18411,4 @@
},
"title": "CodexAppServerProtocol",
"type": "object"
}
}
@@ -6489,6 +6489,11 @@
"action": {
"$ref": "#/definitions/GuardianApprovalReviewAction"
},
"completedAtMs": {
"description": "Unix timestamp (in milliseconds) when this review completed.",
"format": "int64",
"type": "integer"
},
"decisionSource": {
"$ref": "#/definitions/AutoReviewDecisionSource"
},
@@ -6499,6 +6504,11 @@
"description": "Stable identifier for this review.",
"type": "string"
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this review started.",
"format": "int64",
"type": "integer"
},
"targetItemId": {
"description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.",
"type": [
@@ -6515,9 +6525,11 @@
},
"required": [
"action",
"completedAtMs",
"decisionSource",
"review",
"reviewId",
"startedAtMs",
"threadId",
"turnId"
],
@@ -6538,6 +6550,11 @@
"description": "Stable identifier for this review.",
"type": "string"
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this review started.",
"format": "int64",
"type": "integer"
},
"targetItemId": {
"description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.",
"type": [
@@ -6556,6 +6573,7 @@
"action",
"review",
"reviewId",
"startedAtMs",
"threadId",
"turnId"
],
@@ -8989,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"
},
@@ -9003,6 +9031,7 @@
}
},
"required": [
"discoverability",
"remotePluginId",
"shareTargets"
],
@@ -9012,6 +9041,9 @@
"PluginShareUpdateTargetsResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"discoverability": {
"$ref": "#/definitions/PluginShareDiscoverability"
},
"principals": {
"items": {
"$ref": "#/definitions/PluginSharePrincipal"
@@ -9020,6 +9052,7 @@
}
},
"required": [
"discoverability",
"principals"
],
"title": "PluginShareUpdateTargetsResponse",
@@ -12616,24 +12649,6 @@
],
"type": "object"
},
"SkillsListExtraRootsForCwd": {
"properties": {
"cwd": {
"type": "string"
},
"extraUserRoots": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"cwd",
"extraUserRoots"
],
"type": "object"
},
"SkillsListParams": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -12647,17 +12662,6 @@
"forceReload": {
"description": "When true, bypass the skills cache and re-scan skills from disk.",
"type": "boolean"
},
"perCwdExtraUserRoots": {
"default": null,
"description": "Optional per-cwd extra roots to scan as user-scoped skills.",
"items": {
"$ref": "#/definitions/SkillsListExtraRootsForCwd"
},
"type": [
"array",
"null"
]
}
},
"title": "SkillsListParams",
@@ -16274,4 +16278,4 @@
},
"title": "CodexAppServerProtocolV2",
"type": "object"
}
}
@@ -574,6 +574,11 @@
"action": {
"$ref": "#/definitions/GuardianApprovalReviewAction"
},
"completedAtMs": {
"description": "Unix timestamp (in milliseconds) when this review completed.",
"format": "int64",
"type": "integer"
},
"decisionSource": {
"$ref": "#/definitions/AutoReviewDecisionSource"
},
@@ -584,6 +589,11 @@
"description": "Stable identifier for this review.",
"type": "string"
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this review started.",
"format": "int64",
"type": "integer"
},
"targetItemId": {
"description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.",
"type": [
@@ -600,9 +610,11 @@
},
"required": [
"action",
"completedAtMs",
"decisionSource",
"review",
"reviewId",
"startedAtMs",
"threadId",
"turnId"
],
@@ -574,6 +574,11 @@
"description": "Stable identifier for this review.",
"type": "string"
},
"startedAtMs": {
"description": "Unix timestamp (in milliseconds) when this review started.",
"format": "int64",
"type": "integer"
},
"targetItemId": {
"description": "Identifier for the reviewed item or tool call when one exists.\n\nIn most cases, one review maps to one target item. The exceptions are - execve reviews, where a single command may contain multiple execve calls to review (only possible when using the shell_zsh_fork feature) - network policy reviews, where there is no target item\n\nA network call is triggered by a CommandExecution item, so having a target_item_id set to the CommandExecution item would be misleading because the review is about the network call, not the command execution. Therefore, target_item_id is set to None for network policy reviews.",
"type": [
@@ -592,6 +597,7 @@
"action",
"review",
"reviewId",
"startedAtMs",
"threadId",
"turnId"
],
@@ -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"
],
@@ -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",
@@ -1,25 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"SkillsListExtraRootsForCwd": {
"properties": {
"cwd": {
"type": "string"
},
"extraUserRoots": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"cwd",
"extraUserRoots"
],
"type": "object"
}
},
"properties": {
"cwds": {
"description": "When empty, defaults to the current session working directory.",
@@ -31,17 +11,6 @@
"forceReload": {
"description": "When true, bypass the skills cache and re-scan skills from disk.",
"type": "boolean"
},
"perCwdExtraUserRoots": {
"default": null,
"description": "Optional per-cwd extra roots to scan as user-scoped skills.",
"items": {
"$ref": "#/definitions/SkillsListExtraRootsForCwd"
},
"type": [
"array",
"null"
]
}
},
"title": "SkillsListParams",
@@ -8,6 +8,9 @@ import type { NetworkApprovalContext } from "./NetworkApprovalContext";
import type { NetworkPolicyAmendment } from "./NetworkPolicyAmendment";
export type CommandExecutionRequestApprovalParams = {threadId: string, turnId: string, itemId: string, /**
* Unix timestamp (in milliseconds) when this approval request started.
*/
startedAtMs: number, /**
* Unique identifier for this specific approval callback.
*
* For regular shell/unified_exec approvals, this is null.
@@ -3,6 +3,10 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type FileChangeRequestApprovalParams = { threadId: string, turnId: string, itemId: string,
/**
* Unix timestamp (in milliseconds) when this approval request started.
*/
startedAtMs: number,
/**
* Optional explanatory reason (e.g. request for extra write access).
*/
@@ -10,6 +10,14 @@ import type { GuardianApprovalReviewAction } from "./GuardianApprovalReviewActio
* shape is expected to change soon.
*/
export type ItemGuardianApprovalReviewCompletedNotification = { threadId: string, turnId: string,
/**
* Unix timestamp (in milliseconds) when this review started.
*/
startedAtMs: number,
/**
* Unix timestamp (in milliseconds) when this review completed.
*/
completedAtMs: number,
/**
* Stable identifier for this review.
*/
@@ -9,6 +9,10 @@ import type { GuardianApprovalReviewAction } from "./GuardianApprovalReviewActio
* shape is expected to change soon.
*/
export type ItemGuardianApprovalReviewStartedNotification = { threadId: string, turnId: string,
/**
* Unix timestamp (in milliseconds) when this review started.
*/
startedAtMs: number,
/**
* Stable identifier for this review.
*/
@@ -4,4 +4,8 @@
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { RequestPermissionProfile } from "./RequestPermissionProfile";
export type PermissionsRequestApprovalParams = { threadId: string, turnId: string, itemId: string, cwd: AbsolutePathBuf, reason: string | null, permissions: RequestPermissionProfile, };
export type PermissionsRequestApprovalParams = { threadId: string, turnId: string, itemId: string,
/**
* Unix timestamp (in milliseconds) when this approval request started.
*/
startedAtMs: number, cwd: AbsolutePathBuf, reason: string | null, permissions: RequestPermissionProfile, };
@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SkillsListExtraRootsForCwd = { cwd: string, extraUserRoots: Array<string>, };
export type PluginShareUpdateDiscoverability = "UNLISTED" | "PRIVATE";
@@ -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>, };
@@ -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, };
@@ -1,7 +1,6 @@
// 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 { SkillsListExtraRootsForCwd } from "./SkillsListExtraRootsForCwd";
export type SkillsListParams = {
/**
@@ -11,8 +10,4 @@ cwds?: Array<string>,
/**
* When true, bypass the skills cache and re-scan skills from disk.
*/
forceReload?: boolean,
/**
* Optional per-cwd extra roots to scan as user-scoped skills.
*/
perCwdExtraUserRoots?: Array<SkillsListExtraRootsForCwd> | null, };
forceReload?: boolean, };
+1 -1
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";
@@ -336,7 +337,6 @@ export type { SkillsChangedNotification } from "./SkillsChangedNotification";
export type { SkillsConfigWriteParams } from "./SkillsConfigWriteParams";
export type { SkillsConfigWriteResponse } from "./SkillsConfigWriteResponse";
export type { SkillsListEntry } from "./SkillsListEntry";
export type { SkillsListExtraRootsForCwd } from "./SkillsListExtraRootsForCwd";
export type { SkillsListParams } from "./SkillsListParams";
export type { SkillsListResponse } from "./SkillsListResponse";
export type { SortDirection } from "./SortDirection";
@@ -1656,7 +1656,6 @@ mod tests {
params: v2::SkillsListParams {
cwds: Vec::new(),
force_reload: false,
per_cwd_extra_user_roots: None,
},
};
assert_eq!(
@@ -2967,6 +2966,7 @@ mod tests {
thread_id: "thr_123".to_string(),
turn_id: "turn_123".to_string(),
item_id: "call_123".to_string(),
started_at_ms: 0,
approval_id: None,
reason: None,
network_approval_context: None,
@@ -243,6 +243,7 @@ pub fn guardian_auto_approval_review_notification(
thread_id: conversation_id.to_string(),
turn_id,
review_id: assessment.id.clone(),
started_at_ms: assessment.started_at_ms,
target_item_id: assessment.target_item_id.clone(),
review,
action,
@@ -258,6 +259,10 @@ pub fn guardian_auto_approval_review_notification(
thread_id: conversation_id.to_string(),
turn_id,
review_id: assessment.id.clone(),
started_at_ms: assessment.started_at_ms,
completed_at_ms: assessment
.completed_at_ms
.unwrap_or(assessment.started_at_ms),
target_item_id: assessment.target_item_id.clone(),
decision_source: assessment
.decision_source
@@ -2143,6 +2143,8 @@ mod tests {
id: "review-guardian-exec".into(),
target_item_id: Some("guardian-exec".into()),
turn_id: "turn-1".into(),
started_at_ms: 1_000,
completed_at_ms: None,
status: GuardianAssessmentStatus::InProgress,
risk_level: None,
user_authorization: None,
@@ -2160,6 +2162,8 @@ mod tests {
id: "review-guardian-exec".into(),
target_item_id: Some("guardian-exec".into()),
turn_id: "turn-1".into(),
started_at_ms: 1_000,
completed_at_ms: Some(1_042),
status: GuardianAssessmentStatus::Denied,
risk_level: Some(codex_protocol::protocol::GuardianRiskLevel::High),
user_authorization: Some(codex_protocol::protocol::GuardianUserAuthorization::Low),
@@ -2222,6 +2226,8 @@ mod tests {
id: "review-guardian-execve".into(),
target_item_id: Some("guardian-execve".into()),
turn_id: "turn-1".into(),
started_at_ms: 2_000,
completed_at_ms: None,
status: GuardianAssessmentStatus::InProgress,
risk_level: None,
user_authorization: None,
@@ -2525,6 +2531,7 @@ mod tests {
EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
call_id: "patch-call".into(),
turn_id: turn_id.to_string(),
started_at_ms: 0,
changes: [(
PathBuf::from("README.md"),
codex_protocol::protocol::FileChange::Add {
@@ -1073,6 +1073,9 @@ pub struct ItemStartedNotification {
pub struct ItemGuardianApprovalReviewStartedNotification {
pub thread_id: String,
pub turn_id: String,
/// Unix timestamp (in milliseconds) when this review started.
#[ts(type = "number")]
pub started_at_ms: i64,
/// Stable identifier for this review.
pub review_id: String,
/// Identifier for the reviewed item or tool call when one exists.
@@ -1099,6 +1102,12 @@ pub struct ItemGuardianApprovalReviewStartedNotification {
pub struct ItemGuardianApprovalReviewCompletedNotification {
pub thread_id: String,
pub turn_id: String,
/// Unix timestamp (in milliseconds) when this review started.
#[ts(type = "number")]
pub started_at_ms: i64,
/// Unix timestamp (in milliseconds) when this review completed.
#[ts(type = "number")]
pub completed_at_ms: i64,
/// Stable identifier for this review.
pub review_id: String,
/// Identifier for the reviewed item or tool call when one exists.
@@ -1248,6 +1257,9 @@ pub struct CommandExecutionRequestApprovalParams {
pub thread_id: String,
pub turn_id: String,
pub item_id: String,
/// Unix timestamp (in milliseconds) when this approval request started.
#[ts(type = "number")]
pub started_at_ms: i64,
/// Unique identifier for this specific approval callback.
///
/// For regular shell/unified_exec approvals, this is null.
@@ -1321,6 +1333,9 @@ pub struct FileChangeRequestApprovalParams {
pub thread_id: String,
pub turn_id: String,
pub item_id: String,
/// Unix timestamp (in milliseconds) when this approval request started.
#[ts(type = "number")]
pub started_at_ms: i64,
/// Optional explanatory reason (e.g. request for extra write access).
#[ts(optional = nullable)]
pub reason: Option<String>,
@@ -826,6 +826,9 @@ pub struct PermissionsRequestApprovalParams {
pub thread_id: String,
pub turn_id: String,
pub item_id: String,
/// Unix timestamp (in milliseconds) when this approval request started.
#[ts(type = "number")]
pub started_at_ms: i64,
pub cwd: AbsolutePathBuf,
pub reason: Option<String>,
pub permissions: RequestPermissionProfile,
@@ -15,7 +15,7 @@ use serde::Serialize;
use std::path::PathBuf;
use ts_rs::TS;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct SkillsListParams {
@@ -26,19 +26,6 @@ pub struct SkillsListParams {
/// When true, bypass the skills cache and re-scan skills from disk.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub force_reload: bool,
/// Optional per-cwd extra roots to scan as user-scoped skills.
#[serde(default)]
#[ts(optional = nullable)]
pub per_cwd_extra_user_roots: Option<Vec<SkillsListExtraRootsForCwd>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct SkillsListExtraRootsForCwd {
pub cwd: PathBuf,
pub extra_user_roots: Vec<PathBuf>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -231,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>,
}
@@ -239,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)]
@@ -288,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 {
@@ -277,6 +277,7 @@ fn command_execution_request_approval_rejects_relative_additional_permission_pat
"threadId": "thr_123",
"turnId": "turn_123",
"itemId": "call_123",
"startedAtMs": 1,
"command": "cat file",
"cwd": absolute_path_string("tmp"),
"commandActions": null,
@@ -317,6 +318,7 @@ fn permissions_request_approval_uses_request_permission_profile() {
"threadId": "thr_123",
"turnId": "turn_123",
"itemId": "call_123",
"startedAtMs": 1,
"cwd": absolute_path_string("repo"),
"reason": "Select a workspace root",
"permissions": {
@@ -379,6 +381,7 @@ fn permissions_request_approval_rejects_macos_permissions() {
"threadId": "thr_123",
"turnId": "turn_123",
"itemId": "call_123",
"startedAtMs": 1,
"cwd": absolute_path_string("repo"),
"reason": "Select a workspace root",
"permissions": {
@@ -2505,33 +2508,20 @@ fn skills_list_params_serialization_uses_force_reload() {
serde_json::to_value(SkillsListParams {
cwds: Vec::new(),
force_reload: false,
per_cwd_extra_user_roots: None,
})
.unwrap(),
json!({
"perCwdExtraUserRoots": null,
}),
json!({}),
);
assert_eq!(
serde_json::to_value(SkillsListParams {
cwds: vec![PathBuf::from("/repo")],
force_reload: true,
per_cwd_extra_user_roots: Some(vec![SkillsListExtraRootsForCwd {
cwd: PathBuf::from("/repo"),
extra_user_roots: vec![PathBuf::from("/shared/skills"), PathBuf::from("/tmp/x")],
}]),
})
.unwrap(),
json!({
"cwds": ["/repo"],
"forceReload": true,
"perCwdExtraUserRoots": [
{
"cwd": "/repo",
"extraUserRoots": ["/shared/skills", "/tmp/x"],
}
],
}),
);
}
@@ -2946,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(),
@@ -2954,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",
@@ -2968,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!({
@@ -2976,6 +2969,7 @@ fn plugin_share_params_and_response_serialization_use_camel_case_fields() {
"principalId": "user-1",
"name": "Gavin",
}],
"discoverability": "UNLISTED",
}),
);
@@ -1945,6 +1945,7 @@ impl CodexClient {
thread_id,
turn_id,
item_id,
started_at_ms: _,
approval_id,
reason,
network_approval_context,
@@ -2020,6 +2021,7 @@ impl CodexClient {
thread_id,
turn_id,
item_id,
started_at_ms: _,
reason,
grant_root,
} = params;
+1 -9
View File
@@ -1485,21 +1485,13 @@ $skill-creator Add a new skill for triaging flaky CI and include step-by-step us
```
Use `skills/list` to fetch the available skills (optionally scoped by `cwds`, with `forceReload`).
You can also add `perCwdExtraUserRoots` to scan additional absolute paths as `user` scope for specific `cwd` entries.
Entries whose `cwd` is not present in `cwds` are ignored.
`skills/list` might reuse a cached skills result per `cwd`; setting `forceReload` to `true` refreshes the result from disk.
The server also emits `skills/changed` notifications when watched local skill files change. Treat this as an invalidation signal and re-run `skills/list` with your current params when needed.
```json
{ "method": "skills/list", "id": 25, "params": {
"cwds": ["/Users/me/project", "/Users/me/other-project"],
"forceReload": true,
"perCwdExtraUserRoots": [
{
"cwd": "/Users/me/project",
"extraUserRoots": ["/Users/me/shared-skills"]
}
]
"forceReload": true
} }
{ "id": 25, "result": {
"data": [{
@@ -503,6 +503,7 @@ pub(crate) async fn apply_bespoke_event_handling(
thread_id: conversation_id.to_string(),
turn_id: event.turn_id.clone(),
item_id: item_id.clone(),
started_at_ms: event.started_at_ms,
reason: event.reason.clone(),
grant_root: event.grant_root.clone(),
};
@@ -534,6 +535,7 @@ pub(crate) async fn apply_bespoke_event_handling(
call_id,
approval_id,
turn_id,
started_at_ms,
command,
cwd,
reason,
@@ -607,6 +609,7 @@ pub(crate) async fn apply_bespoke_event_handling(
thread_id: conversation_id.to_string(),
turn_id: turn_id.clone(),
item_id: call_id.clone(),
started_at_ms,
approval_id: approval_id.clone(),
reason,
network_approval_context,
@@ -756,6 +759,7 @@ pub(crate) async fn apply_bespoke_event_handling(
thread_id: conversation_id.to_string(),
turn_id: request.turn_id.clone(),
item_id: request.call_id.clone(),
started_at_ms: request.started_at_ms,
cwd: request_cwd.clone(),
reason: request.reason,
permissions: request.permissions.into(),
@@ -2241,6 +2245,9 @@ mod tests {
id: format!("review-{id}"),
target_item_id: Some(id.to_string()),
turn_id: turn_id.to_string(),
started_at_ms: 1_000,
completed_at_ms: (!matches!(status, GuardianAssessmentStatus::InProgress))
.then_some(1_042),
status,
risk_level,
user_authorization,
@@ -2305,6 +2312,8 @@ mod tests {
id: "review-1".to_string(),
target_item_id: Some("item-1".to_string()),
turn_id: String::new(),
started_at_ms: 1_000,
completed_at_ms: None,
status: codex_protocol::protocol::GuardianAssessmentStatus::InProgress,
risk_level: None,
user_authorization: None,
@@ -2318,6 +2327,7 @@ mod tests {
ServerNotification::ItemGuardianApprovalReviewStarted(payload) => {
assert_eq!(payload.thread_id, conversation_id.to_string());
assert_eq!(payload.turn_id, "turn-from-event");
assert_eq!(payload.started_at_ms, 1_000);
assert_eq!(payload.review_id, "review-1");
assert_eq!(payload.target_item_id.as_deref(), Some("item-1"));
assert_eq!(
@@ -2348,6 +2358,8 @@ mod tests {
id: "review-2".to_string(),
target_item_id: Some("item-2".to_string()),
turn_id: "turn-from-assessment".to_string(),
started_at_ms: 1_000,
completed_at_ms: Some(1_042),
status: codex_protocol::protocol::GuardianAssessmentStatus::Denied,
risk_level: Some(codex_protocol::protocol::GuardianRiskLevel::High),
user_authorization: Some(codex_protocol::protocol::GuardianUserAuthorization::Low),
@@ -2363,6 +2375,8 @@ mod tests {
ServerNotification::ItemGuardianApprovalReviewCompleted(payload) => {
assert_eq!(payload.thread_id, conversation_id.to_string());
assert_eq!(payload.turn_id, "turn-from-assessment");
assert_eq!(payload.started_at_ms, 1_000);
assert_eq!(payload.completed_at_ms, 1_042);
assert_eq!(payload.review_id, "review-2");
assert_eq!(payload.target_item_id.as_deref(), Some("item-2"));
assert_eq!(payload.decision_source, AutoReviewDecisionSource::Agent);
@@ -2398,6 +2412,8 @@ mod tests {
id: "review-3".to_string(),
target_item_id: None,
turn_id: "turn-from-assessment".to_string(),
started_at_ms: 1_000,
completed_at_ms: Some(1_042),
status: codex_protocol::protocol::GuardianAssessmentStatus::Aborted,
risk_level: None,
user_authorization: None,
+16 -1
View File
@@ -2,6 +2,8 @@ use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::AtomicI64;
use std::sync::atomic::Ordering;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use codex_analytics::AnalyticsEventsClient;
use codex_app_server_protocol::ClientResponsePayload;
@@ -357,8 +359,10 @@ impl OutgoingMessageSender {
match entry {
Some((id, entry)) => {
let completed_at_ms = now_unix_timestamp_ms();
if let Ok(response) = entry.request.response_from_result(result.clone()) {
self.analytics_events_client.track_server_response(response);
self.analytics_events_client
.track_server_response(completed_at_ms, response);
}
if let Err(err) = entry.callback.send(Ok(result)) {
warn!("could not notify callback for {id:?} due to: {err:?}");
@@ -648,6 +652,15 @@ impl OutgoingMessageSender {
}
}
fn now_unix_timestamp_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis()
.try_into()
.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use std::time::Duration;
@@ -903,6 +916,7 @@ mod tests {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "item-1".to_string(),
started_at_ms: 0,
approval_id: None,
reason: None,
network_approval_context: None,
@@ -1195,6 +1209,7 @@ mod tests {
thread_id: thread_id.to_string(),
turn_id: "turn-1".to_string(),
item_id: "call-2".to_string(),
started_at_ms: 0,
reason: None,
grant_root: None,
},
@@ -125,6 +125,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;
@@ -382,44 +382,12 @@ impl CatalogRequestProcessor {
&self,
params: SkillsListParams,
) -> Result<SkillsListResponse, JSONRPCErrorError> {
let SkillsListParams {
cwds,
force_reload,
per_cwd_extra_user_roots,
} = params;
let SkillsListParams { cwds, force_reload } = params;
let cwds = if cwds.is_empty() {
vec![self.config.cwd.to_path_buf()]
} else {
cwds
};
let cwd_set: HashSet<PathBuf> = cwds.iter().cloned().collect();
let mut extra_roots_by_cwd: HashMap<PathBuf, Vec<AbsolutePathBuf>> = HashMap::new();
for entry in per_cwd_extra_user_roots.unwrap_or_default() {
if !cwd_set.contains(&entry.cwd) {
warn!(
cwd = %entry.cwd.display(),
"ignoring per-cwd extra roots for cwd not present in skills/list cwds"
);
continue;
}
let mut valid_extra_roots = Vec::new();
for root in entry.extra_user_roots {
let root =
AbsolutePathBuf::from_absolute_path_checked(root.as_path()).map_err(|_| {
invalid_request(format!(
"skills/list perCwdExtraUserRoots extraUserRoots paths must be absolute: {}",
root.display()
))
})?;
valid_extra_roots.push(root);
}
extra_roots_by_cwd
.entry(entry.cwd)
.or_default()
.extend(valid_extra_roots);
}
let config = self.load_latest_config(/*fallback_cwd*/ None).await?;
let auth = self.auth_manager.auth().await;
@@ -436,7 +404,6 @@ impl CatalogRequestProcessor {
let mut data = futures::stream::iter(cwds.into_iter().enumerate())
.map(|(index, cwd)| {
let config = &config;
let extra_roots_by_cwd = &extra_roots_by_cwd;
let fs = fs.clone();
let plugins_manager = &plugins_manager;
let skills_manager = &skills_manager;
@@ -458,9 +425,6 @@ impl CatalogRequestProcessor {
);
}
};
let extra_roots = extra_roots_by_cwd
.get(&cwd)
.map_or(&[][..], std::vec::Vec::as_slice);
let effective_skill_roots = if workspace_codex_plugins_enabled {
let plugins_input = config.plugins_config_input();
plugins_manager
@@ -479,12 +443,7 @@ impl CatalogRequestProcessor {
config.bundled_skills_enabled(),
);
let outcome = skills_manager
.skills_for_cwd_with_extra_user_roots(
&skills_input,
force_reload,
extra_roots,
fs,
)
.skills_for_cwd(&skills_input, force_reload, fs)
.await;
let errors = errors_to_info(&outcome.errors);
let skills = skills_to_info(&outcome.skills, &outcome.disabled_paths);
@@ -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>,
@@ -258,6 +258,7 @@ async fn command_execution_request_approval_strips_additional_permissions_withou
thread_id: "thr_123".to_string(),
turn_id: "turn_123".to_string(),
item_id: "call_123".to_string(),
started_at_ms: 0,
approval_id: None,
reason: Some("Need extra read access".to_string()),
network_approval_context: None,
@@ -322,6 +323,7 @@ async fn command_execution_request_approval_keeps_additional_permissions_with_ca
thread_id: "thr_123".to_string(),
turn_id: "turn_123".to_string(),
item_id: "call_123".to_string(),
started_at_ms: 0,
approval_id: None,
reason: Some("Need extra read access".to_string()),
network_approval_context: None,
@@ -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(())
+15 -136
View File
@@ -11,7 +11,6 @@ use codex_app_server_protocol::PluginListParams;
use codex_app_server_protocol::PluginListResponse;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SkillsChangedNotification;
use codex_app_server_protocol::SkillsListExtraRootsForCwd;
use codex_app_server_protocol::SkillsListParams;
use codex_app_server_protocol::SkillsListResponse;
use codex_app_server_protocol::ThreadStartParams;
@@ -133,44 +132,6 @@ fn write_cached_remote_plugin_with_skill(
Ok(skill_path)
}
#[tokio::test]
async fn skills_list_includes_skills_from_per_cwd_extra_user_roots() -> Result<()> {
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
let extra_root = TempDir::new()?;
write_skill(&extra_root, "extra-skill")?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_skills_list_request(SkillsListParams {
cwds: vec![cwd.path().to_path_buf()],
force_reload: true,
per_cwd_extra_user_roots: Some(vec![SkillsListExtraRootsForCwd {
cwd: cwd.path().to_path_buf(),
extra_user_roots: vec![extra_root.path().to_path_buf()],
}]),
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let SkillsListResponse { data } = to_response(response)?;
assert_eq!(data.len(), 1);
assert_eq!(data[0].cwd.as_path(), cwd.path());
assert!(
data[0]
.skills
.iter()
.any(|skill| skill.name == "extra-skill")
);
Ok(())
}
#[tokio::test]
async fn skills_list_loads_remote_installed_plugin_skills_from_cache() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -266,7 +227,6 @@ async fn skills_list_loads_remote_installed_plugin_skills_from_cache() -> Result
.send_skills_list_request(SkillsListParams {
cwds: vec![cwd.path().to_path_buf()],
force_reload: true,
per_cwd_extra_user_roots: None,
})
.await?;
let stale_skills_list_response: JSONRPCResponse = timeout(
@@ -317,7 +277,6 @@ async fn skills_list_loads_remote_installed_plugin_skills_from_cache() -> Result
.send_skills_list_request(SkillsListParams {
cwds: vec![cwd.path().to_path_buf()],
force_reload: false,
per_cwd_extra_user_roots: None,
})
.await?;
let skills_list_response: JSONRPCResponse = timeout(
@@ -392,7 +351,6 @@ async fn skills_list_excludes_plugin_skills_when_workspace_codex_plugins_disable
.send_skills_list_request(SkillsListParams {
cwds: vec![repo_root.path().to_path_buf()],
force_reload: true,
per_cwd_extra_user_roots: None,
})
.await?;
@@ -424,9 +382,13 @@ async fn skills_list_excludes_plugin_skills_when_workspace_codex_plugins_disable
async fn skills_list_skips_cwd_roots_when_environment_disabled() -> Result<()> {
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
let extra_root = TempDir::new()?;
write_skill(&codex_home, "home-skill")?;
write_skill(&extra_root, "extra-skill")?;
let repo_skill_dir = cwd.path().join(".codex/skills/repo-skill");
std::fs::create_dir_all(&repo_skill_dir)?;
std::fs::write(
repo_skill_dir.join("SKILL.md"),
"---\nname: repo-skill\ndescription: from repo root\n---\n\n# Body\n",
)?;
let mut mcp = McpProcess::new_with_env(
codex_home.path(),
@@ -439,10 +401,6 @@ async fn skills_list_skips_cwd_roots_when_environment_disabled() -> Result<()> {
.send_skills_list_request(SkillsListParams {
cwds: vec![cwd.path().to_path_buf()],
force_reload: true,
per_cwd_extra_user_roots: Some(vec![SkillsListExtraRootsForCwd {
cwd: cwd.path().to_path_buf(),
extra_user_roots: vec![extra_root.path().to_path_buf()],
}]),
})
.await?;
@@ -465,41 +423,7 @@ async fn skills_list_skips_cwd_roots_when_environment_disabled() -> Result<()> {
data[0]
.skills
.iter()
.all(|skill| skill.name != "extra-skill")
);
Ok(())
}
#[tokio::test]
async fn skills_list_rejects_relative_extra_user_roots() -> Result<()> {
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_skills_list_request(SkillsListParams {
cwds: vec![cwd.path().to_path_buf()],
force_reload: true,
per_cwd_extra_user_roots: Some(vec![SkillsListExtraRootsForCwd {
cwd: cwd.path().to_path_buf(),
extra_user_roots: vec![std::path::PathBuf::from("relative/skills")],
}]),
})
.await?;
let err = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
)
.await??;
assert!(
err.error
.message
.contains("perCwdExtraUserRoots extraUserRoots paths must be absolute"),
"unexpected error: {}",
err.error.message
.all(|skill| skill.name != "repo-skill")
);
Ok(())
}
@@ -517,7 +441,6 @@ async fn skills_list_accepts_relative_cwds() -> Result<()> {
.send_skills_list_request(SkillsListParams {
cwds: vec![relative_cwd.clone()],
force_reload: true,
per_cwd_extra_user_roots: None,
})
.await?;
@@ -549,7 +472,6 @@ async fn skills_list_preserves_requested_cwd_order() -> Result<()> {
second_cwd.path().to_path_buf(),
],
force_reload: true,
per_cwd_extra_user_roots: None,
})
.await?;
@@ -571,61 +493,19 @@ async fn skills_list_preserves_requested_cwd_order() -> Result<()> {
Ok(())
}
#[tokio::test]
async fn skills_list_ignores_per_cwd_extra_roots_for_unknown_cwd() -> Result<()> {
let codex_home = TempDir::new()?;
let requested_cwd = TempDir::new()?;
let unknown_cwd = TempDir::new()?;
let extra_root = TempDir::new()?;
write_skill(&extra_root, "ignored-extra-skill")?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
let request_id = mcp
.send_skills_list_request(SkillsListParams {
cwds: vec![requested_cwd.path().to_path_buf()],
force_reload: true,
per_cwd_extra_user_roots: Some(vec![SkillsListExtraRootsForCwd {
cwd: unknown_cwd.path().to_path_buf(),
extra_user_roots: vec![extra_root.path().to_path_buf()],
}]),
})
.await?;
let response: JSONRPCResponse = timeout(
DEFAULT_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??;
let SkillsListResponse { data } = to_response(response)?;
assert_eq!(data.len(), 1);
assert_eq!(data[0].cwd.as_path(), requested_cwd.path());
assert!(
data[0]
.skills
.iter()
.all(|skill| skill.name != "ignored-extra-skill")
);
Ok(())
}
#[tokio::test]
async fn skills_list_uses_cached_result_until_force_reload() -> Result<()> {
let codex_home = TempDir::new()?;
let cwd = TempDir::new()?;
let extra_root = TempDir::new()?;
write_skill(&extra_root, "late-extra-skill")?;
let mut mcp = McpProcess::new(codex_home.path()).await?;
timeout(DEFAULT_TIMEOUT, mcp.initialize()).await??;
// Seed the cwd cache first without extra roots.
// Seed the cwd cache before the cwd-local skill exists.
let first_request_id = mcp
.send_skills_list_request(SkillsListParams {
cwds: vec![cwd.path().to_path_buf()],
force_reload: false,
per_cwd_extra_user_roots: None,
})
.await?;
let first_response: JSONRPCResponse = timeout(
@@ -642,14 +522,17 @@ async fn skills_list_uses_cached_result_until_force_reload() -> Result<()> {
.all(|skill| skill.name != "late-extra-skill")
);
let skill_dir = cwd.path().join(".codex/skills/late-extra-skill");
std::fs::create_dir_all(&skill_dir)?;
std::fs::write(
skill_dir.join("SKILL.md"),
"---\nname: late-extra-skill\ndescription: late skill\n---\n\n# Body\n",
)?;
let second_request_id = mcp
.send_skills_list_request(SkillsListParams {
cwds: vec![cwd.path().to_path_buf()],
force_reload: false,
per_cwd_extra_user_roots: Some(vec![SkillsListExtraRootsForCwd {
cwd: cwd.path().to_path_buf(),
extra_user_roots: vec![extra_root.path().to_path_buf()],
}]),
})
.await?;
let second_response: JSONRPCResponse = timeout(
@@ -670,10 +553,6 @@ async fn skills_list_uses_cached_result_until_force_reload() -> Result<()> {
.send_skills_list_request(SkillsListParams {
cwds: vec![cwd.path().to_path_buf()],
force_reload: true,
per_cwd_extra_user_roots: Some(vec![SkillsListExtraRootsForCwd {
cwd: cwd.path().to_path_buf(),
extra_user_roots: vec![extra_root.path().to_path_buf()],
}]),
})
.await?;
let third_response: JSONRPCResponse = timeout(
+1 -1
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 }
+1
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;
+63 -5
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,
+74 -14
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,
}
);
}
-33
View File
@@ -131,17 +131,6 @@ impl SkillsManager {
input: &SkillsLoadInput,
force_reload: bool,
fs: Option<Arc<dyn ExecutorFileSystem>>,
) -> SkillLoadOutcome {
self.skills_for_cwd_with_extra_user_roots(input, force_reload, &[], fs)
.await
}
pub async fn skills_for_cwd_with_extra_user_roots(
&self,
input: &SkillsLoadInput,
force_reload: bool,
extra_user_roots: &[AbsolutePathBuf],
fs: Option<Arc<dyn ExecutorFileSystem>>,
) -> SkillLoadOutcome {
let use_cwd_cache = fs.is_some();
if use_cwd_cache
@@ -161,18 +150,6 @@ impl SkillsManager {
if !bundled_skills_enabled_from_stack(&input.config_layer_stack) {
roots.retain(|root| root.scope != SkillScope::System);
}
if let Some(fs) = fs {
roots.extend(
normalize_extra_user_roots(extra_user_roots)
.into_iter()
.map(|path| SkillRoot {
path,
scope: SkillScope::User,
file_system: Arc::clone(&fs),
plugin_id: None,
}),
);
}
let skill_config_rules = skill_config_rules_from_stack(&input.config_layer_stack);
let outcome = self.build_skill_outcome(roots, &skill_config_rules).await;
if use_cwd_cache {
@@ -300,16 +277,6 @@ fn finalize_skill_outcome(
outcome
}
fn normalize_extra_user_roots(extra_user_roots: &[AbsolutePathBuf]) -> Vec<AbsolutePathBuf> {
let mut normalized: Vec<AbsolutePathBuf> = extra_user_roots
.iter()
.map(|root| root.canonicalize().unwrap_or_else(|_| root.clone()))
.collect();
normalized.sort_unstable();
normalized.dedup();
normalized
}
#[cfg(test)]
#[path = "manager_tests.rs"]
mod tests;
+25 -136
View File
@@ -11,7 +11,6 @@ use codex_exec_server::LOCAL_FS;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_absolute_path::test_support::PathBufExt;
use codex_utils_absolute_path::test_support::PathExt;
use codex_utils_absolute_path::test_support::test_path_buf;
use codex_utils_plugins::PluginSkillRoot;
use pretty_assertions::assert_eq;
use std::collections::HashSet;
@@ -263,75 +262,13 @@ async fn skills_for_config_disables_plugin_skills_by_name() {
}
#[tokio::test]
async fn skills_for_cwd_reuses_cached_entry_even_when_entry_has_extra_roots() {
async fn skills_for_cwd_loads_repo_and_user_roots_with_local_fs() {
let codex_home = tempfile::tempdir().expect("tempdir");
let cwd = tempfile::tempdir().expect("tempdir");
let extra_root = tempfile::tempdir().expect("tempdir");
let config_layer_stack = config_stack(&codex_home, "");
let skills_manager = SkillsManager::new(
codex_home.path().abs(),
/*bundled_skills_enabled*/ true,
);
let _ = skills_for_config_with_stack(&skills_manager, &cwd, &config_layer_stack, &[]).await;
write_user_skill(&extra_root, "x", "extra-skill", "from extra root");
let extra_root_path = extra_root.path().abs();
let base_input = SkillsLoadInput::new(
cwd.path().abs(),
Vec::new(),
config_layer_stack.clone(),
bundled_skills_enabled_from_stack(&config_layer_stack),
);
let outcome_with_extra = skills_manager
.skills_for_cwd_with_extra_user_roots(
&base_input,
/*force_reload*/ true,
std::slice::from_ref(&extra_root_path),
Some(Arc::clone(&LOCAL_FS)),
)
.await;
assert!(
outcome_with_extra
.skills
.iter()
.any(|skill| skill.name == "extra-skill")
);
assert!(
outcome_with_extra
.skills
.iter()
.any(|skill| skill.scope == SkillScope::System)
);
// The cwd-only API returns the current cached entry for this cwd, even when that entry
// was produced with extra roots.
let base_input = SkillsLoadInput::new(
cwd.path().abs(),
Vec::new(),
config_layer_stack.clone(),
bundled_skills_enabled_from_stack(&config_layer_stack),
);
let outcome_without_extra = skills_manager
.skills_for_cwd(
&base_input,
/*force_reload*/ false,
Some(Arc::clone(&LOCAL_FS)),
)
.await;
assert_eq!(outcome_without_extra.skills, outcome_with_extra.skills);
assert_eq!(outcome_without_extra.errors, outcome_with_extra.errors);
}
#[tokio::test]
async fn skills_for_cwd_loads_repo_user_and_extra_roots_with_local_fs() {
let codex_home = tempfile::tempdir().expect("tempdir");
let cwd = tempfile::tempdir().expect("tempdir");
let extra_root = tempfile::tempdir().expect("tempdir");
let repo_dot_codex = cwd.path().join(".codex");
fs::create_dir_all(&repo_dot_codex).expect("create repo config dir");
write_user_skill(&codex_home, "user", "user-skill", "from local user root");
write_user_skill(&extra_root, "extra", "extra-skill", "from extra root");
let repo_skill_dir = repo_dot_codex.join("skills/repo");
fs::create_dir_all(&repo_skill_dir).expect("create repo skill dir");
fs::write(
@@ -366,10 +303,9 @@ async fn skills_for_cwd_loads_repo_user_and_extra_roots_with_local_fs() {
);
let outcome = skills_manager
.skills_for_cwd_with_extra_user_roots(
.skills_for_cwd(
&skills_input,
/*force_reload*/ true,
&[extra_root.path().abs()],
Some(Arc::clone(&LOCAL_FS)),
)
.await;
@@ -386,19 +322,16 @@ async fn skills_for_cwd_loads_repo_user_and_extra_roots_with_local_fs() {
.collect::<HashSet<_>>();
assert!(loaded_names.contains("user-skill"));
assert!(loaded_names.contains("repo-skill"));
assert!(loaded_names.contains("extra-skill"));
}
#[tokio::test]
async fn skills_for_cwd_without_fs_skips_repo_and_extra_roots() {
async fn skills_for_cwd_without_fs_skips_repo_roots() {
let codex_home = tempfile::tempdir().expect("tempdir");
let cwd = tempfile::tempdir().expect("tempdir");
let extra_root = tempfile::tempdir().expect("tempdir");
let repo_dot_codex = cwd.path().join(".codex");
fs::create_dir_all(&repo_dot_codex).expect("create repo config dir");
write_user_skill(&codex_home, "user", "user-skill", "from local user root");
write_user_skill(&extra_root, "extra", "extra-skill", "from extra root");
let repo_skill_dir = repo_dot_codex.join("skills/repo");
fs::create_dir_all(&repo_skill_dir).expect("create repo skill dir");
fs::write(
@@ -433,12 +366,7 @@ async fn skills_for_cwd_without_fs_skips_repo_and_extra_roots() {
);
let outcome = skills_manager
.skills_for_cwd_with_extra_user_roots(
&skills_input,
/*force_reload*/ true,
&[extra_root.path().abs()],
/*fs*/ None,
)
.skills_for_cwd(&skills_input, /*force_reload*/ true, /*fs*/ None)
.await;
assert!(
@@ -453,7 +381,6 @@ async fn skills_for_cwd_without_fs_skips_repo_and_extra_roots() {
.collect::<HashSet<_>>();
assert!(loaded_names.contains("user-skill"));
assert!(!loaded_names.contains("repo-skill"));
assert!(!loaded_names.contains("extra-skill"));
}
#[tokio::test]
@@ -499,22 +426,15 @@ async fn skills_for_config_excludes_bundled_skills_when_disabled_in_config() {
}
#[tokio::test]
async fn skills_for_cwd_with_extra_roots_only_refreshes_on_force_reload() {
async fn skills_for_cwd_uses_cached_result_until_force_reload() {
let codex_home = tempfile::tempdir().expect("tempdir");
let cwd = tempfile::tempdir().expect("tempdir");
let extra_root_a = tempfile::tempdir().expect("tempdir");
let extra_root_b = tempfile::tempdir().expect("tempdir");
let config_layer_stack = config_stack(&codex_home, "");
let skills_manager = SkillsManager::new(
codex_home.path().abs(),
/*bundled_skills_enabled*/ true,
);
let _ = skills_for_config_with_stack(&skills_manager, &cwd, &config_layer_stack, &[]).await;
write_user_skill(&extra_root_a, "x", "extra-skill-a", "from extra root a");
write_user_skill(&extra_root_b, "x", "extra-skill-b", "from extra root b");
let extra_root_a_path = extra_root_a.path().abs();
let base_input = SkillsLoadInput::new(
cwd.path().abs(),
Vec::new(),
@@ -522,32 +442,25 @@ async fn skills_for_cwd_with_extra_roots_only_refreshes_on_force_reload() {
bundled_skills_enabled_from_stack(&config_layer_stack),
);
let outcome_a = skills_manager
.skills_for_cwd_with_extra_user_roots(
&base_input,
/*force_reload*/ true,
std::slice::from_ref(&extra_root_a_path),
Some(Arc::clone(&LOCAL_FS)),
)
.await;
assert!(
outcome_a
.skills
.iter()
.any(|skill| skill.name == "extra-skill-a")
);
assert!(
outcome_a
.skills
.iter()
.all(|skill| skill.name != "extra-skill-b")
);
let extra_root_b_path = extra_root_b.path().abs();
let outcome_b = skills_manager
.skills_for_cwd_with_extra_user_roots(
.skills_for_cwd(
&base_input,
/*force_reload*/ false,
Some(Arc::clone(&LOCAL_FS)),
)
.await;
assert!(
outcome_a
.skills
.iter()
.all(|skill| skill.name != "late-skill")
);
write_user_skill(&codex_home, "late", "late-skill", "added after cache");
let outcome_b = skills_manager
.skills_for_cwd(
&base_input,
/*force_reload*/ false,
std::slice::from_ref(&extra_root_b_path),
Some(Arc::clone(&LOCAL_FS)),
)
.await;
@@ -555,20 +468,13 @@ async fn skills_for_cwd_with_extra_roots_only_refreshes_on_force_reload() {
outcome_b
.skills
.iter()
.any(|skill| skill.name == "extra-skill-a")
);
assert!(
outcome_b
.skills
.iter()
.all(|skill| skill.name != "extra-skill-b")
.all(|skill| skill.name != "late-skill")
);
let outcome_reloaded = skills_manager
.skills_for_cwd_with_extra_user_roots(
.skills_for_cwd(
&base_input,
/*force_reload*/ true,
std::slice::from_ref(&extra_root_b_path),
Some(Arc::clone(&LOCAL_FS)),
)
.await;
@@ -576,25 +482,8 @@ async fn skills_for_cwd_with_extra_roots_only_refreshes_on_force_reload() {
outcome_reloaded
.skills
.iter()
.any(|skill| skill.name == "extra-skill-b")
.any(|skill| skill.name == "late-skill")
);
assert!(
outcome_reloaded
.skills
.iter()
.all(|skill| skill.name != "extra-skill-a")
);
}
#[test]
fn normalize_extra_user_roots_is_stable_for_equivalent_inputs() {
let a = test_path_buf("/tmp/a").abs();
let b = test_path_buf("/tmp/b").abs();
let first = normalize_extra_user_roots(&[a.clone(), b.clone(), a.clone()]);
let second = normalize_extra_user_roots(&[b, a]);
assert_eq!(first, second);
}
#[cfg_attr(windows, ignore)]
@@ -225,6 +225,7 @@ async fn handle_request_permissions_uses_tool_call_id_for_round_trip() {
RequestPermissionsEvent {
call_id: request_call_id,
turn_id: "child-turn-1".to_string(),
started_at_ms: 0,
reason: Some("need access".to_string()),
permissions: RequestPermissionProfile {
network: Some(NetworkPermissions {
@@ -313,6 +314,7 @@ async fn handle_exec_approval_uses_call_id_for_guardian_review_and_approval_id_f
call_id: "command-item-1".to_string(),
approval_id: Some("callback-approval-1".to_string()),
turn_id: "child-turn-1".to_string(),
started_at_ms: 0,
command: vec!["rm".to_string(), "-rf".to_string(), "tmp".to_string()],
cwd: test_path_buf("/tmp").abs(),
reason: Some("unsafe subcommand".to_string()),
+12
View File
@@ -23,6 +23,7 @@ use tokio_util::sync::CancellationToken;
use crate::session::session::Session;
use crate::session::turn_context::TurnContext;
use crate::turn_timing::now_unix_timestamp_ms;
use super::GUARDIAN_REVIEW_TIMEOUT;
use super::GUARDIAN_REVIEWER_NAME;
@@ -251,6 +252,7 @@ async fn run_guardian_review(
guardian_reviewed_action(&request),
GUARDIAN_REVIEW_TIMEOUT.as_millis() as u64,
);
let started_at_ms = review_tracking.started_at_ms.try_into().unwrap_or_default();
session
.send_event(
turn.as_ref(),
@@ -258,6 +260,8 @@ async fn run_guardian_review(
id: review_id.clone(),
target_item_id: target_item_id.clone(),
turn_id: assessment_turn_id.clone(),
started_at_ms,
completed_at_ms: None,
status: GuardianAssessmentStatus::InProgress,
risk_level: None,
user_authorization: None,
@@ -289,6 +293,8 @@ async fn run_guardian_review(
id: review_id,
target_item_id,
turn_id: assessment_turn_id.clone(),
started_at_ms,
completed_at_ms: Some(now_unix_timestamp_ms()),
status: GuardianAssessmentStatus::Aborted,
risk_level: None,
user_authorization: None,
@@ -372,6 +378,8 @@ async fn run_guardian_review(
id: review_id,
target_item_id,
turn_id: assessment_turn_id.clone(),
started_at_ms,
completed_at_ms: Some(now_unix_timestamp_ms()),
status: GuardianAssessmentStatus::TimedOut,
risk_level: None,
user_authorization: None,
@@ -402,6 +410,8 @@ async fn run_guardian_review(
id: review_id,
target_item_id,
turn_id: assessment_turn_id.clone(),
started_at_ms,
completed_at_ms: Some(now_unix_timestamp_ms()),
status: GuardianAssessmentStatus::Aborted,
risk_level: None,
user_authorization: None,
@@ -495,6 +505,8 @@ async fn run_guardian_review(
id: review_id,
target_item_id,
turn_id: assessment_turn_id.clone(),
started_at_ms,
completed_at_ms: Some(now_unix_timestamp_ms()),
status,
risk_level: Some(assessment.risk_level),
user_authorization: Some(assessment.user_authorization),
+3
View File
@@ -1934,6 +1934,7 @@ impl Session {
call_id,
approval_id,
turn_id: turn_context.sub_id.clone(),
started_at_ms: now_unix_timestamp_ms(),
command,
cwd,
reason,
@@ -1980,6 +1981,7 @@ impl Session {
let event = EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
call_id,
turn_id: turn_context.sub_id.clone(),
started_at_ms: now_unix_timestamp_ms(),
changes,
reason,
grant_root,
@@ -2143,6 +2145,7 @@ impl Session {
let event = EventMsg::RequestPermissions(RequestPermissionsEvent {
call_id: call_id.clone(),
turn_id: turn_context.sub_id.clone(),
started_at_ms: now_unix_timestamp_ms(),
reason: args.reason,
permissions: requested_permissions,
cwd: Some(cwd),
@@ -222,6 +222,7 @@ async fn run_codex_tool_session_inner(
let approval_id = ev.effective_approval_id();
let ExecApprovalRequestEvent {
turn_id: _,
started_at_ms: _,
command,
cwd,
call_id,
@@ -278,6 +279,7 @@ async fn run_codex_tool_session_inner(
EventMsg::ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent {
call_id,
turn_id: _,
started_at_ms: _,
reason,
grant_root,
changes,
@@ -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")
+10
View File
@@ -187,6 +187,12 @@ pub struct GuardianAssessmentEvent {
/// Uses `#[serde(default)]` for backwards compatibility.
#[serde(default)]
pub turn_id: String,
#[serde(default)]
#[ts(type = "number")]
pub started_at_ms: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(optional, type = "number")]
pub completed_at_ms: Option<i64>,
pub status: GuardianAssessmentStatus,
/// Coarse risk label. Omitted while the assessment is in progress.
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -223,6 +229,8 @@ pub struct ExecApprovalRequestEvent {
/// Uses `#[serde(default)]` for backwards compatibility.
#[serde(default)]
pub turn_id: String,
#[ts(type = "number")]
pub started_at_ms: i64,
/// The command to be executed.
pub command: Vec<String>,
/// The command's working directory.
@@ -370,6 +378,8 @@ pub struct ApplyPatchApprovalRequestEvent {
/// Uses `#[serde(default)]` for backwards compatibility with older senders.
#[serde(default)]
pub turn_id: String,
#[ts(type = "number")]
pub started_at_ms: i64,
pub changes: HashMap<PathBuf, FileChange>,
/// Optional explanatory reason (e.g. request for extra write access).
#[serde(skip_serializing_if = "Option::is_none")]
@@ -71,6 +71,8 @@ pub struct RequestPermissionsEvent {
/// Uses `#[serde(default)]` for backwards compatibility.
#[serde(default)]
pub turn_id: String,
#[ts(type = "number")]
pub started_at_ms: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
pub permissions: RequestPermissionProfile,
+62
View File
@@ -216,6 +216,68 @@ async fn load_rollout_items_skips_legacy_ghost_snapshot_lines() -> std::io::Resu
Ok(())
}
#[tokio::test]
async fn load_rollout_items_preserves_legacy_guardian_assessment_lines() -> std::io::Result<()> {
let home = TempDir::new().expect("temp dir");
let rollout_path = home.path().join("rollout.jsonl");
let mut file = File::create(&rollout_path)?;
let thread_id = ThreadId::new();
let ts = "2025-01-03T12:00:00Z";
writeln!(
file,
"{}",
serde_json::json!({
"timestamp": ts,
"type": "session_meta",
"payload": {
"id": thread_id,
"timestamp": ts,
"cwd": ".",
"originator": "test_originator",
"cli_version": "test_version",
"source": "cli",
"model_provider": "test-provider",
},
})
)?;
writeln!(
file,
"{}",
serde_json::json!({
"timestamp": ts,
"type": "event_msg",
"payload": {
"type": "guardian_assessment",
"id": "guardian-1",
"turn_id": "turn-1",
"status": "in_progress",
"action": {
"type": "command",
"source": "shell",
"command": "rm -rf /tmp/guardian",
"cwd": if cfg!(windows) { r"C:\tmp" } else { "/tmp" },
},
},
})
)?;
let (items, loaded_thread_id, parse_errors) =
RolloutRecorder::load_rollout_items(&rollout_path).await?;
assert_eq!(loaded_thread_id, Some(thread_id));
assert_eq!(parse_errors, 0);
assert_eq!(items.len(), 2);
let RolloutItem::EventMsg(EventMsg::GuardianAssessment(assessment)) = &items[1] else {
panic!("expected guardian assessment rollout item");
};
assert_eq!(assessment.id, "guardian-1");
assert_eq!(assessment.turn_id, "turn-1");
assert_eq!(assessment.started_at_ms, 0);
Ok(())
}
#[tokio::test]
async fn load_rollout_items_filters_legacy_ghost_snapshots_from_compaction_history()
-> std::io::Result<()> {
@@ -429,6 +429,7 @@ mod tests {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "call-1".to_string(),
started_at_ms: 0,
approval_id: Some("approval-1".to_string()),
reason: None,
network_approval_context: None,
@@ -481,6 +482,7 @@ mod tests {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "perm-1".to_string(),
started_at_ms: 0,
cwd: absolute_path(if cfg!(windows) { r"C:\tmp" } else { "/tmp" }),
reason: None,
permissions: serde_json::from_value(json!({
@@ -686,6 +688,7 @@ mod tests {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "patch-1".to_string(),
started_at_ms: 0,
reason: None,
grant_root: None,
},
@@ -715,6 +718,7 @@ mod tests {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "call-1".to_string(),
started_at_ms: 0,
approval_id: Some("approval-1".to_string()),
reason: None,
network_approval_context: None,
@@ -648,7 +648,6 @@ pub(super) async fn fetch_skills_list(
params: SkillsListParams {
cwds: vec![cwd],
force_reload: true,
per_cwd_extra_user_roots: None,
},
})
.await
@@ -612,6 +612,7 @@ mod tests {
thread_id: "thread-1".to_string(),
turn_id: turn_id.to_string(),
item_id: call_id.to_string(),
started_at_ms: 0,
approval_id: approval_id.map(str::to_string),
reason: None,
network_approval_context: None,
@@ -633,6 +634,7 @@ mod tests {
thread_id: "thread-1".to_string(),
turn_id: turn_id.to_string(),
item_id: call_id.to_string(),
started_at_ms: 0,
reason: None,
grant_root: None,
},
+3
View File
@@ -2659,6 +2659,7 @@ async fn inactive_thread_file_change_approval_recovers_buffered_changes() {
thread_id: thread_id.to_string(),
turn_id: "turn-approval".to_string(),
item_id: "patch-approval".to_string(),
started_at_ms: 0,
reason: Some("command failed; retry without sandbox?".to_string()),
grant_root: None,
},
@@ -2709,6 +2710,7 @@ async fn inactive_thread_permissions_approval_preserves_file_system_permissions(
thread_id: thread_id.to_string(),
turn_id: "turn-approval".to_string(),
item_id: "call-approval".to_string(),
started_at_ms: 0,
cwd: test_absolute_path("/tmp"),
reason: Some("Need access to .git".to_string()),
permissions: codex_app_server_protocol::RequestPermissionProfile {
@@ -4259,6 +4261,7 @@ fn exec_approval_request(
thread_id: thread_id.to_string(),
turn_id: turn_id.to_string(),
item_id: item_id.to_string(),
started_at_ms: 0,
approval_id: approval_id.map(str::to_string),
reason: Some("needs approval".to_string()),
network_approval_context: None,
+1
View File
@@ -465,6 +465,7 @@ mod tests {
thread_id: thread_id.to_string(),
turn_id: turn_id.to_string(),
item_id: item_id.to_string(),
started_at_ms: 0,
approval_id: approval_id.map(str::to_string),
reason: Some("needs approval".to_string()),
network_approval_context: None,
-1
View File
@@ -621,7 +621,6 @@ impl App {
.skills_list(codex_app_server_protocol::SkillsListParams {
cwds: cwds.clone(),
force_reload: *force_reload,
per_cwd_extra_user_roots: None,
})
.await,
"failed to refresh skills",
+2
View File
@@ -88,6 +88,8 @@ mod tests {
id: format!("review-{id}"),
target_item_id: None,
turn_id: "turn-1".to_string(),
started_at_ms: 0,
completed_at_ms: Some(1),
status: GuardianAssessmentStatus::Denied,
risk_level: None,
user_authorization: None,
+16 -3
View File
@@ -1637,6 +1637,7 @@ fn request_permissions_from_params(
RequestPermissionsEvent {
turn_id: params.turn_id,
call_id: params.item_id,
started_at_ms: params.started_at_ms,
reason: params.reason,
permissions: params.permissions.into(),
cwd: Some(params.cwd),
@@ -6374,8 +6375,9 @@ impl ChatWidget {
self.on_guardian_review_notification(
notification.review_id,
notification.turn_id,
notification.started_at_ms,
notification.review,
/*decision_source*/ None,
/*completion*/ None,
notification.action,
);
}
@@ -6383,8 +6385,9 @@ impl ChatWidget {
self.on_guardian_review_notification(
notification.review_id,
notification.turn_id,
notification.started_at_ms,
notification.review,
Some(notification.decision_source),
Some((notification.completed_at_ms, notification.decision_source)),
notification.action,
);
}
@@ -6567,14 +6570,24 @@ impl ChatWidget {
&mut self,
id: String,
turn_id: String,
started_at_ms: i64,
review: codex_app_server_protocol::GuardianApprovalReview,
decision_source: Option<codex_app_server_protocol::AutoReviewDecisionSource>,
completion: Option<(i64, codex_app_server_protocol::AutoReviewDecisionSource)>,
action: GuardianApprovalReviewAction,
) {
let (completed_at_ms, decision_source) = match completion {
Some((completed_at_ms, decision_source)) => {
(Some(completed_at_ms), Some(decision_source))
}
None => (None, None),
};
self.on_guardian_assessment(GuardianAssessmentEvent {
id,
target_item_id: None,
turn_id,
started_at_ms,
completed_at_ms,
status: match review.status {
codex_app_server_protocol::GuardianApprovalReviewStatus::InProgress => {
GuardianAssessmentStatus::InProgress
@@ -55,6 +55,7 @@ fn app_server_exec_approval_request_splits_shell_wrapped_command() {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "item-1".to_string(),
started_at_ms: 0,
approval_id: Some("approval-1".to_string()),
reason: None,
network_approval_context: None,
@@ -93,6 +94,7 @@ fn app_server_exec_approval_request_preserves_permissions_context() {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "item-1".to_string(),
started_at_ms: 0,
approval_id: Some("approval-1".to_string()),
reason: None,
network_approval_context: Some(codex_app_server_protocol::NetworkApprovalContext {
@@ -156,6 +158,7 @@ fn app_server_request_permissions_preserves_file_system_permissions() {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "item-1".to_string(),
started_at_ms: 0,
cwd: cwd.clone(),
reason: Some("Select a workspace root".to_string()),
permissions: codex_app_server_protocol::RequestPermissionProfile {
@@ -54,6 +54,7 @@ fn app_server_exec_approval_request_splits_shell_wrapped_command() {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
item_id: "item-1".to_string(),
started_at_ms: 0,
approval_id: Some("approval-1".to_string()),
reason: None,
network_approval_context: None,
@@ -6,6 +6,8 @@ fn auto_review_denial_event() -> GuardianAssessmentEvent {
id: "auto-review-recent-1".into(),
target_item_id: Some("target-auto-review-recent-1".into()),
turn_id: "turn-recent-1".into(),
started_at_ms: 0,
completed_at_ms: Some(1),
status: GuardianAssessmentStatus::Denied,
risk_level: Some(GuardianRiskLevel::High),
user_authorization: Some(GuardianUserAuthorization::Low),
@@ -73,6 +75,8 @@ async fn guardian_denied_exec_renders_warning_and_denied_request() {
id: "guardian-1".into(),
target_item_id: Some("guardian-target-1".into()),
turn_id: "turn-1".into(),
started_at_ms: 0,
completed_at_ms: None,
status: GuardianAssessmentStatus::InProgress,
risk_level: None,
user_authorization: None,
@@ -85,6 +89,8 @@ async fn guardian_denied_exec_renders_warning_and_denied_request() {
id: "guardian-1".into(),
target_item_id: Some("guardian-target-1".into()),
turn_id: "turn-1".into(),
started_at_ms: 0,
completed_at_ms: Some(1),
status: GuardianAssessmentStatus::Denied,
risk_level: Some(GuardianRiskLevel::High),
user_authorization: Some(GuardianUserAuthorization::Low),
@@ -127,6 +133,8 @@ async fn guardian_approved_exec_renders_approved_request() {
id: "thread:child-thread:guardian-1".into(),
target_item_id: Some("guardian-approved-target".into()),
turn_id: "turn-1".into(),
started_at_ms: 0,
completed_at_ms: Some(1),
status: GuardianAssessmentStatus::Approved,
risk_level: Some(GuardianRiskLevel::Low),
user_authorization: Some(GuardianUserAuthorization::High),
@@ -183,6 +191,8 @@ async fn guardian_approved_request_permissions_renders_request_summary() {
id: "guardian-request-permissions".into(),
target_item_id: None,
turn_id: "turn-1".into(),
started_at_ms: 0,
completed_at_ms: None,
status: GuardianAssessmentStatus::InProgress,
risk_level: None,
user_authorization: None,
@@ -205,6 +215,8 @@ async fn guardian_approved_request_permissions_renders_request_summary() {
id: "guardian-request-permissions".into(),
target_item_id: None,
turn_id: "turn-1".into(),
started_at_ms: 0,
completed_at_ms: Some(1),
status: GuardianAssessmentStatus::Approved,
risk_level: Some(GuardianRiskLevel::Low),
user_authorization: Some(GuardianUserAuthorization::High),
@@ -253,6 +265,8 @@ async fn guardian_timed_out_exec_renders_warning_and_timed_out_request() {
id: "guardian-1".into(),
target_item_id: Some("guardian-target-1".into()),
turn_id: "turn-1".into(),
started_at_ms: 0,
completed_at_ms: None,
status: GuardianAssessmentStatus::InProgress,
risk_level: None,
user_authorization: None,
@@ -265,6 +279,8 @@ async fn guardian_timed_out_exec_renders_warning_and_timed_out_request() {
id: "guardian-1".into(),
target_item_id: Some("guardian-target-1".into()),
turn_id: "turn-1".into(),
started_at_ms: 0,
completed_at_ms: Some(1),
status: GuardianAssessmentStatus::TimedOut,
risk_level: None,
user_authorization: None,
@@ -315,6 +331,7 @@ async fn app_server_guardian_review_started_sets_review_status() {
ItemGuardianApprovalReviewStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
review_id: "guardian-1".to_string(),
target_item_id: Some("guardian-target-1".to_string()),
review: GuardianApprovalReview {
@@ -356,6 +373,7 @@ async fn app_server_guardian_review_denied_renders_denied_request_snapshot() {
ItemGuardianApprovalReviewStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
review_id: "guardian-1".to_string(),
target_item_id: Some("guardian-target-1".to_string()),
review: GuardianApprovalReview {
@@ -375,6 +393,8 @@ async fn app_server_guardian_review_denied_renders_denied_request_snapshot() {
ItemGuardianApprovalReviewCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
completed_at_ms: 1,
review_id: "guardian-1".to_string(),
target_item_id: Some("guardian-target-1".to_string()),
decision_source: AppServerGuardianApprovalReviewDecisionSource::Agent,
@@ -431,6 +451,7 @@ async fn app_server_guardian_review_timed_out_renders_timed_out_request_snapshot
ItemGuardianApprovalReviewStartedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
review_id: "guardian-1".to_string(),
target_item_id: Some("guardian-target-1".to_string()),
review: GuardianApprovalReview {
@@ -450,6 +471,8 @@ async fn app_server_guardian_review_timed_out_renders_timed_out_request_snapshot
ItemGuardianApprovalReviewCompletedNotification {
thread_id: "thread-1".to_string(),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
completed_at_ms: 1,
review_id: "guardian-1".to_string(),
target_item_id: Some("guardian-target-1".to_string()),
decision_source: AppServerGuardianApprovalReviewDecisionSource::Agent,
@@ -506,6 +529,8 @@ async fn guardian_parallel_reviews_render_aggregate_status_snapshot() {
id: id.to_string(),
target_item_id: Some(format!("{id}-target")),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
completed_at_ms: None,
status: GuardianAssessmentStatus::InProgress,
risk_level: None,
user_authorization: None,
@@ -535,6 +560,8 @@ async fn guardian_parallel_reviews_keep_remaining_review_visible_after_denial()
id: "guardian-1".to_string(),
target_item_id: Some("guardian-1-target".to_string()),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
completed_at_ms: None,
status: GuardianAssessmentStatus::InProgress,
risk_level: None,
user_authorization: None,
@@ -550,6 +577,8 @@ async fn guardian_parallel_reviews_keep_remaining_review_visible_after_denial()
id: "guardian-2".to_string(),
target_item_id: Some("guardian-2-target".to_string()),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
completed_at_ms: None,
status: GuardianAssessmentStatus::InProgress,
risk_level: None,
user_authorization: None,
@@ -565,6 +594,8 @@ async fn guardian_parallel_reviews_keep_remaining_review_visible_after_denial()
id: "guardian-1".to_string(),
target_item_id: Some("guardian-1-target".to_string()),
turn_id: "turn-1".to_string(),
started_at_ms: 0,
completed_at_ms: Some(1),
status: GuardianAssessmentStatus::Denied,
risk_level: Some(GuardianRiskLevel::High),
user_authorization: Some(GuardianUserAuthorization::Low),