feat(auto-review) Handle request_permissions calls (#18393)

## Summary
When auto-review is enabled, it should handle request_permissions tool.
We'll need to clean up the UX but I'm planning to do that in a separate
pass

## Testing
- [x] Ran locally
<img width="893" height="396" alt="Screenshot 2026-04-17 at 1 16 13 PM"
src="https://github.com/user-attachments/assets/4c045c5f-1138-4c6c-ac6e-2cb6be4514d8"
/>

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Dylan Hurd
2026-04-20 21:48:57 -07:00
committed by GitHub
Unverified
parent 543a08dac9
commit 86535c9901
31 changed files with 2232 additions and 370 deletions
@@ -64,6 +64,57 @@
},
"type": "object"
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": [
"array",
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"write": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
},
"AdditionalNetworkPermissions": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"AgentMessageDeltaNotification": {
"properties": {
"delta": {
@@ -1019,6 +1070,217 @@
],
"type": "object"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"current_working_directory"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "CurrentWorkingDirectoryFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -1368,6 +1630,32 @@
],
"title": "McpToolCallGuardianApprovalReviewAction",
"type": "object"
},
{
"properties": {
"permissions": {
"$ref": "#/definitions/RequestPermissionProfile"
},
"reason": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"requestPermissions"
],
"title": "RequestPermissionsGuardianApprovalReviewActionType",
"type": "string"
}
},
"required": [
"permissions",
"type"
],
"title": "RequestPermissionsGuardianApprovalReviewAction",
"type": "object"
}
]
},
@@ -2285,6 +2573,32 @@
}
]
},
"RequestPermissionProfile": {
"additionalProperties": false,
"properties": {
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalFileSystemPermissions"
},
{
"type": "null"
}
]
},
"network": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalNetworkPermissions"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"ServerRequestResolvedNotification": {
"properties": {
"requestId": {
@@ -5,63 +5,12 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": [
"array",
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"write": {
"items": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
},
"AdditionalNetworkPermissions": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"AdditionalPermissionProfile": {
"properties": {
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalFileSystemPermissions"
"$ref": "#/definitions/v2/AdditionalFileSystemPermissions"
},
{
"type": "null"
@@ -71,7 +20,7 @@
"network": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalNetworkPermissions"
"$ref": "#/definitions/v2/AdditionalNetworkPermissions"
},
{
"type": "null"
@@ -2268,217 +2217,6 @@
"title": "FileChangeRequestApprovalResponse",
"type": "object"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"current_working_directory"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "CurrentWorkingDirectoryFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"FuzzyFileSearchMatchType": {
"enum": [
"file",
@@ -2611,7 +2349,7 @@
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalFileSystemPermissions"
"$ref": "#/definitions/v2/AdditionalFileSystemPermissions"
},
{
"type": "null"
@@ -2621,7 +2359,7 @@
"network": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalNetworkPermissions"
"$ref": "#/definitions/v2/AdditionalNetworkPermissions"
},
{
"type": "null"
@@ -3620,7 +3358,7 @@
"type": "string"
},
"permissions": {
"$ref": "#/definitions/RequestPermissionProfile"
"$ref": "#/definitions/v2/RequestPermissionProfile"
},
"reason": {
"type": [
@@ -3678,32 +3416,6 @@
],
"title": "RequestId"
},
"RequestPermissionProfile": {
"additionalProperties": false,
"properties": {
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalFileSystemPermissions"
},
{
"type": "null"
}
]
},
"network": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalNetworkPermissions"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"ReviewDecision": {
"description": "User's decision in response to an ExecApprovalRequest.",
"oneOf": [
@@ -5413,6 +5125,57 @@
],
"type": "string"
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/v2/FileSystemSandboxEntry"
},
"type": [
"array",
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"write": {
"items": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
},
"AdditionalNetworkPermissions": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"AgentMessageDeltaNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -8041,6 +7804,217 @@
"title": "FileChangePatchUpdatedNotification",
"type": "object"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/v2/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/v2/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/v2/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"current_working_directory"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "CurrentWorkingDirectoryFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -8834,6 +8808,32 @@
],
"title": "McpToolCallGuardianApprovalReviewAction",
"type": "object"
},
{
"properties": {
"permissions": {
"$ref": "#/definitions/v2/RequestPermissionProfile"
},
"reason": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"requestPermissions"
],
"title": "RequestPermissionsGuardianApprovalReviewActionType",
"type": "string"
}
},
"required": [
"permissions",
"type"
],
"title": "RequestPermissionsGuardianApprovalReviewAction",
"type": "object"
}
]
},
@@ -11641,6 +11641,32 @@
}
]
},
"RequestPermissionProfile": {
"additionalProperties": false,
"properties": {
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/v2/AdditionalFileSystemPermissions"
},
{
"type": "null"
}
]
},
"network": {
"anyOf": [
{
"$ref": "#/definitions/v2/AdditionalNetworkPermissions"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"ResidencyRequirement": {
"enum": [
"us"
@@ -128,6 +128,57 @@
],
"type": "string"
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": [
"array",
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"write": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
},
"AdditionalNetworkPermissions": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"AgentMessageDeltaNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -4370,6 +4421,217 @@
"title": "FileChangePatchUpdatedNotification",
"type": "object"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"current_working_directory"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "CurrentWorkingDirectoryFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"FileUpdateChange": {
"properties": {
"diff": {
@@ -5274,6 +5536,32 @@
],
"title": "McpToolCallGuardianApprovalReviewAction",
"type": "object"
},
{
"properties": {
"permissions": {
"$ref": "#/definitions/RequestPermissionProfile"
},
"reason": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"requestPermissions"
],
"title": "RequestPermissionsGuardianApprovalReviewActionType",
"type": "string"
}
},
"required": [
"permissions",
"type"
],
"title": "RequestPermissionsGuardianApprovalReviewAction",
"type": "object"
}
]
},
@@ -8125,6 +8413,32 @@
}
]
},
"RequestPermissionProfile": {
"additionalProperties": false,
"properties": {
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalFileSystemPermissions"
},
{
"type": "null"
}
]
},
"network": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalNetworkPermissions"
},
{
"type": "null"
}
]
}
},
"type": "object"
},
"ResidencyRequirement": {
"enum": [
"us"
@@ -5,6 +5,57 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": [
"array",
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"write": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
},
"AdditionalNetworkPermissions": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"AutoReviewDecisionSource": {
"description": "[UNSTABLE] Source that produced a terminal approval auto-review decision.",
"enum": [
@@ -12,6 +63,217 @@
],
"type": "string"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"current_working_directory"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "CurrentWorkingDirectoryFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"GuardianApprovalReview": {
"description": "[UNSTABLE] Temporary approval auto-review payload used by `item/autoApprovalReview/*` notifications. This shape is expected to change soon.",
"properties": {
@@ -217,6 +479,32 @@
],
"title": "McpToolCallGuardianApprovalReviewAction",
"type": "object"
},
{
"properties": {
"permissions": {
"$ref": "#/definitions/RequestPermissionProfile"
},
"reason": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"requestPermissions"
],
"title": "RequestPermissionsGuardianApprovalReviewActionType",
"type": "string"
}
},
"required": [
"permissions",
"type"
],
"title": "RequestPermissionsGuardianApprovalReviewAction",
"type": "object"
}
]
},
@@ -266,6 +554,32 @@
"socks5Udp"
],
"type": "string"
},
"RequestPermissionProfile": {
"additionalProperties": false,
"properties": {
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalFileSystemPermissions"
},
{
"type": "null"
}
]
},
"network": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalNetworkPermissions"
},
{
"type": "null"
}
]
}
},
"type": "object"
}
},
"description": "[UNSTABLE] Temporary notification payload for approval auto-review. This shape is expected to change soon.",
@@ -5,6 +5,268 @@
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"AdditionalFileSystemPermissions": {
"properties": {
"entries": {
"items": {
"$ref": "#/definitions/FileSystemSandboxEntry"
},
"type": [
"array",
"null"
]
},
"globScanMaxDepth": {
"format": "uint",
"minimum": 1.0,
"type": [
"integer",
"null"
]
},
"read": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
},
"write": {
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": [
"array",
"null"
]
}
},
"type": "object"
},
"AdditionalNetworkPermissions": {
"properties": {
"enabled": {
"type": [
"boolean",
"null"
]
}
},
"type": "object"
},
"FileSystemAccessMode": {
"enum": [
"read",
"write",
"none"
],
"type": "string"
},
"FileSystemPath": {
"oneOf": [
{
"properties": {
"path": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": {
"enum": [
"path"
],
"title": "PathFileSystemPathType",
"type": "string"
}
},
"required": [
"path",
"type"
],
"title": "PathFileSystemPath",
"type": "object"
},
{
"properties": {
"pattern": {
"type": "string"
},
"type": {
"enum": [
"glob_pattern"
],
"title": "GlobPatternFileSystemPathType",
"type": "string"
}
},
"required": [
"pattern",
"type"
],
"title": "GlobPatternFileSystemPath",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"special"
],
"title": "SpecialFileSystemPathType",
"type": "string"
},
"value": {
"$ref": "#/definitions/FileSystemSpecialPath"
}
},
"required": [
"type",
"value"
],
"title": "SpecialFileSystemPath",
"type": "object"
}
]
},
"FileSystemSandboxEntry": {
"properties": {
"access": {
"$ref": "#/definitions/FileSystemAccessMode"
},
"path": {
"$ref": "#/definitions/FileSystemPath"
}
},
"required": [
"access",
"path"
],
"type": "object"
},
"FileSystemSpecialPath": {
"oneOf": [
{
"properties": {
"kind": {
"enum": [
"root"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "RootFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"minimal"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "MinimalFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"current_working_directory"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "CurrentWorkingDirectoryFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"project_roots"
],
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind"
],
"title": "KindFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"tmpdir"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "TmpdirFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"slash_tmp"
],
"type": "string"
}
},
"required": [
"kind"
],
"title": "SlashTmpFileSystemSpecialPath",
"type": "object"
},
{
"properties": {
"kind": {
"enum": [
"unknown"
],
"type": "string"
},
"path": {
"type": "string"
},
"subpath": {
"type": [
"string",
"null"
]
}
},
"required": [
"kind",
"path"
],
"type": "object"
}
]
},
"GuardianApprovalReview": {
"description": "[UNSTABLE] Temporary approval auto-review payload used by `item/autoApprovalReview/*` notifications. This shape is expected to change soon.",
"properties": {
@@ -210,6 +472,32 @@
],
"title": "McpToolCallGuardianApprovalReviewAction",
"type": "object"
},
{
"properties": {
"permissions": {
"$ref": "#/definitions/RequestPermissionProfile"
},
"reason": {
"type": [
"string",
"null"
]
},
"type": {
"enum": [
"requestPermissions"
],
"title": "RequestPermissionsGuardianApprovalReviewActionType",
"type": "string"
}
},
"required": [
"permissions",
"type"
],
"title": "RequestPermissionsGuardianApprovalReviewAction",
"type": "object"
}
]
},
@@ -259,6 +547,32 @@
"socks5Udp"
],
"type": "string"
},
"RequestPermissionProfile": {
"additionalProperties": false,
"properties": {
"fileSystem": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalFileSystemPermissions"
},
{
"type": "null"
}
]
},
"network": {
"anyOf": [
{
"$ref": "#/definitions/AdditionalNetworkPermissions"
},
{
"type": "null"
}
]
}
},
"type": "object"
}
},
"description": "[UNSTABLE] Temporary notification payload for approval auto-review. This shape is expected to change soon.",
@@ -4,5 +4,6 @@
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { GuardianCommandSource } from "./GuardianCommandSource";
import type { NetworkApprovalProtocol } from "./NetworkApprovalProtocol";
import type { RequestPermissionProfile } from "./RequestPermissionProfile";
export type GuardianApprovalReviewAction = { "type": "command", source: GuardianCommandSource, command: string, cwd: AbsolutePathBuf, } | { "type": "execve", source: GuardianCommandSource, program: string, argv: Array<string>, cwd: AbsolutePathBuf, } | { "type": "applyPatch", cwd: AbsolutePathBuf, files: Array<AbsolutePathBuf>, } | { "type": "networkAccess", target: string, host: string, protocol: NetworkApprovalProtocol, port: number, } | { "type": "mcpToolCall", server: string, toolName: string, connectorId: string | null, connectorName: string | null, toolTitle: string | null, };
export type GuardianApprovalReviewAction = { "type": "command", source: GuardianCommandSource, command: string, cwd: AbsolutePathBuf, } | { "type": "execve", source: GuardianCommandSource, program: string, argv: Array<string>, cwd: AbsolutePathBuf, } | { "type": "applyPatch", cwd: AbsolutePathBuf, files: Array<AbsolutePathBuf>, } | { "type": "networkAccess", target: string, host: string, protocol: NetworkApprovalProtocol, port: number, } | { "type": "mcpToolCall", server: string, toolName: string, connectorId: string | null, connectorName: string | null, toolTitle: string | null, } | { "type": "requestPermissions", reason: string | null, permissions: RequestPermissionProfile, };
@@ -199,7 +199,8 @@ pub fn build_item_from_guardian_event(
}
GuardianAssessmentAction::ApplyPatch { .. }
| GuardianAssessmentAction::NetworkAccess { .. }
| GuardianAssessmentAction::McpToolCall { .. } => None,
| GuardianAssessmentAction::McpToolCall { .. }
| GuardianAssessmentAction::RequestPermissions { .. } => None,
}
}
@@ -5142,6 +5142,14 @@ pub struct GuardianMcpToolCallReviewAction {
pub tool_title: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct GuardianRequestPermissionsReviewAction {
pub reason: Option<String>,
pub permissions: RequestPermissionProfile,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(tag = "type", rename_all = "camelCase")]
#[ts(tag = "type", rename_all = "camelCase")]
@@ -5185,6 +5193,12 @@ pub enum GuardianApprovalReviewAction {
connector_name: Option<String>,
tool_title: Option<String>,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
RequestPermissions {
reason: Option<String>,
permissions: RequestPermissionProfile,
},
}
impl From<CoreGuardianAssessmentAction> for GuardianApprovalReviewAction {
@@ -5237,6 +5251,13 @@ impl From<CoreGuardianAssessmentAction> for GuardianApprovalReviewAction {
connector_name,
tool_title,
},
CoreGuardianAssessmentAction::RequestPermissions {
reason,
permissions,
} => Self::RequestPermissions {
reason,
permissions: permissions.into(),
},
}
}
}
@@ -5291,6 +5312,13 @@ impl From<GuardianApprovalReviewAction> for CoreGuardianAssessmentAction {
connector_name,
tool_title,
},
GuardianApprovalReviewAction::RequestPermissions {
reason,
permissions,
} => Self::RequestPermissions {
reason,
permissions: permissions.into(),
},
}
}
}
+6 -36
View File
@@ -26,15 +26,14 @@ use codex_protocol::user_input::UserInput;
use serde_json::Value;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio::sync::oneshot;
use tokio::time::timeout;
use tokio_util::sync::CancellationToken;
use crate::config::Config;
use crate::guardian::GuardianApprovalRequest;
use crate::guardian::new_guardian_review_id;
use crate::guardian::review_approval_request_with_cancel;
use crate::guardian::routes_approval_to_guardian;
use crate::guardian::spawn_approval_request_review;
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_ACCEPT;
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION;
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC;
@@ -457,7 +456,7 @@ async fn handle_exec_approval(
} = event;
let decision = if routes_approval_to_guardian(parent_ctx) {
let review_cancel = cancel_token.child_token();
let review_rx = spawn_guardian_review(
let review_rx = spawn_approval_request_review(
Arc::clone(parent_session),
Arc::clone(parent_ctx),
new_guardian_review_id(),
@@ -565,7 +564,7 @@ async fn handle_patch_approval(
})
.collect::<Vec<_>>()
.join("\n");
let review_rx = spawn_guardian_review(
let review_rx = spawn_approval_request_review(
Arc::clone(parent_session),
Arc::clone(parent_ctx),
new_guardian_review_id(),
@@ -686,7 +685,7 @@ async fn maybe_auto_review_mcp_request_user_input(
)
.await;
let review_cancel = cancel_token.child_token();
let review_rx = spawn_guardian_review(
let review_rx = spawn_approval_request_review(
Arc::clone(parent_session),
Arc::clone(parent_ctx),
new_guardian_review_id(),
@@ -730,36 +729,6 @@ async fn maybe_auto_review_mcp_request_user_input(
})
}
fn spawn_guardian_review(
session: Arc<Session>,
turn: Arc<TurnContext>,
review_id: String,
request: GuardianApprovalRequest,
retry_reason: Option<String>,
cancel_token: CancellationToken,
) -> oneshot::Receiver<ReviewDecision> {
let (tx, rx) = oneshot::channel();
std::thread::spawn(move || {
let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
else {
let _ = tx.send(ReviewDecision::Denied);
return;
};
let decision = runtime.block_on(review_approval_request_with_cancel(
&session,
&turn,
review_id,
request,
retry_reason,
cancel_token,
));
let _ = tx.send(decision);
});
rx
}
async fn handle_request_permissions(
codex: &Codex,
parent_session: &Arc<Session>,
@@ -772,7 +741,8 @@ async fn handle_request_permissions(
reason: event.reason,
permissions: event.permissions,
};
let response_fut = parent_session.request_permissions(parent_ctx, call_id.clone(), args);
let response_fut =
parent_session.request_permissions(parent_ctx, call_id.clone(), args, cancel_token.clone());
let response =
await_request_permissions_with_cancel(response_fut, parent_session, &call_id, cancel_token)
.await;
+39 -2
View File
@@ -4,6 +4,7 @@ use codex_protocol::approvals::GuardianAssessmentAction;
use codex_protocol::approvals::GuardianCommandSource;
use codex_protocol::approvals::NetworkApprovalProtocol;
use codex_protocol::models::PermissionProfile;
use codex_protocol::request_permissions::RequestPermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use serde::Serialize;
use serde_json::Value;
@@ -65,6 +66,12 @@ pub(crate) enum GuardianApprovalRequest {
tool_description: Option<String>,
annotations: Option<GuardianMcpAnnotations>,
},
RequestPermissions {
id: String,
turn_id: String,
reason: Option<String>,
permissions: RequestPermissionProfile,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
@@ -123,6 +130,15 @@ struct McpToolCallApprovalAction<'a> {
annotations: Option<&'a GuardianMcpAnnotations>,
}
#[derive(Serialize)]
struct RequestPermissionsApprovalAction<'a> {
tool: &'static str,
turn_id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
reason: Option<&'a String>,
permissions: &'a RequestPermissionProfile,
}
fn serialize_guardian_action(value: impl Serialize) -> serde_json::Result<Value> {
serde_json::to_value(value)
}
@@ -293,6 +309,17 @@ pub(crate) fn guardian_approval_request_to_json(
tool_description: tool_description.as_ref(),
annotations: annotations.as_ref(),
}),
GuardianApprovalRequest::RequestPermissions {
id: _,
turn_id,
reason,
permissions,
} => serialize_guardian_action(RequestPermissionsApprovalAction {
tool: "request_permissions",
turn_id,
reason: reason.as_ref(),
permissions,
}),
}
}
@@ -352,6 +379,14 @@ pub(crate) fn guardian_assessment_action(
connector_name: connector_name.clone(),
tool_title: tool_title.clone(),
},
GuardianApprovalRequest::RequestPermissions {
reason,
permissions,
..
} => GuardianAssessmentAction::RequestPermissions {
reason: reason.clone(),
permissions: permissions.clone(),
},
}
}
@@ -360,7 +395,8 @@ pub(crate) fn guardian_request_target_item_id(request: &GuardianApprovalRequest)
GuardianApprovalRequest::Shell { id, .. }
| GuardianApprovalRequest::ExecCommand { id, .. }
| GuardianApprovalRequest::ApplyPatch { id, .. }
| GuardianApprovalRequest::McpToolCall { id, .. } => Some(id),
| GuardianApprovalRequest::McpToolCall { id, .. }
| GuardianApprovalRequest::RequestPermissions { id, .. } => Some(id),
GuardianApprovalRequest::NetworkAccess { .. } => None,
#[cfg(unix)]
GuardianApprovalRequest::Execve { id, .. } => Some(id),
@@ -372,7 +408,8 @@ pub(crate) fn guardian_request_turn_id<'a>(
default_turn_id: &'a str,
) -> &'a str {
match request {
GuardianApprovalRequest::NetworkAccess { turn_id, .. } => turn_id,
GuardianApprovalRequest::NetworkAccess { turn_id, .. }
| GuardianApprovalRequest::RequestPermissions { turn_id, .. } => turn_id,
GuardianApprovalRequest::Shell { .. }
| GuardianApprovalRequest::ExecCommand { .. }
| GuardianApprovalRequest::ApplyPatch { .. }
+2
View File
@@ -31,8 +31,10 @@ pub(crate) use review::guardian_timeout_message;
pub(crate) use review::is_guardian_reviewer_source;
pub(crate) use review::new_guardian_review_id;
pub(crate) use review::review_approval_request;
#[cfg(test)]
pub(crate) use review::review_approval_request_with_cancel;
pub(crate) use review::routes_approval_to_guardian;
pub(crate) use review::spawn_approval_request_review;
pub(crate) use review_session::GuardianReviewSessionManager;
const GUARDIAN_PREFERRED_MODEL: &str = "codex-auto-review";
+38 -5
View File
@@ -11,6 +11,7 @@ use codex_protocol::protocol::GuardianUserAuthorization;
use codex_protocol::protocol::ReviewDecision;
use codex_protocol::protocol::SubAgentSource;
use codex_protocol::protocol::WarningEvent;
use tokio::sync::oneshot;
use tokio_util::sync::CancellationToken;
use crate::session::session::Session;
@@ -89,12 +90,14 @@ fn guardian_risk_level_str(level: GuardianRiskLevel) -> &'static str {
}
}
/// Whether this turn should route `on-request` approval prompts through the
/// guardian reviewer instead of surfacing them to the user. ARC may still
/// block actions earlier in the flow.
/// Whether this turn should route allowed approval prompts through the guardian
/// reviewer instead of surfacing them to the user. ARC may still block actions
/// earlier in the flow.
pub(crate) fn routes_approval_to_guardian(turn: &TurnContext) -> bool {
turn.approval_policy.value() == AskForApproval::OnRequest
&& turn.config.approvals_reviewer == ApprovalsReviewer::GuardianSubagent
matches!(
turn.approval_policy.value(),
AskForApproval::OnRequest | AskForApproval::Granular(_)
) && turn.config.approvals_reviewer == ApprovalsReviewer::GuardianSubagent
}
pub(crate) fn is_guardian_reviewer_source(
@@ -335,6 +338,36 @@ pub(crate) async fn review_approval_request_with_cancel(
.await
}
pub(crate) fn spawn_approval_request_review(
session: Arc<Session>,
turn: Arc<TurnContext>,
review_id: String,
request: GuardianApprovalRequest,
retry_reason: Option<String>,
cancel_token: CancellationToken,
) -> oneshot::Receiver<ReviewDecision> {
let (tx, rx) = oneshot::channel();
std::thread::spawn(move || {
let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
else {
let _ = tx.send(ReviewDecision::Denied);
return;
};
let decision = runtime.block_on(review_approval_request_with_cancel(
&session,
&turn,
review_id,
request,
retry_reason,
cancel_token,
));
let _ = tx.send(decision);
});
rx
}
/// Runs the guardian in a locked-down reusable review session.
///
/// The guardian itself should not mutate state or trigger further approvals, so
+21 -1
View File
@@ -28,6 +28,7 @@ use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::GuardianAssessmentStatus;
use codex_protocol::protocol::GuardianRiskLevel;
use codex_protocol::protocol::GuardianUserAuthorization;
@@ -741,7 +742,7 @@ fn guardian_timeout_message_distinguishes_timeout_from_policy_denial() {
}
#[tokio::test]
async fn routes_approval_to_guardian_requires_auto_only_review_policy() {
async fn routes_approval_to_guardian_requires_guardian_reviewer() {
let (_session, mut turn) = crate::session::tests::make_session_and_context().await;
let mut config = (*turn.config).clone();
config.approvals_reviewer = ApprovalsReviewer::User;
@@ -755,6 +756,25 @@ async fn routes_approval_to_guardian_requires_auto_only_review_policy() {
assert!(routes_approval_to_guardian(&turn));
}
#[tokio::test]
async fn routes_approval_to_guardian_allows_granular_review_policy() {
let (_session, mut turn) = crate::session::tests::make_session_and_context().await;
let mut config = (*turn.config).clone();
config.approvals_reviewer = ApprovalsReviewer::GuardianSubagent;
turn.config = Arc::new(config);
turn.approval_policy
.set(AskForApproval::Granular(GranularApprovalConfig {
sandbox_approval: true,
rules: true,
skill_approval: true,
request_permissions: true,
mcp_elicitations: true,
}))
.expect("test setup should allow updating approval policy");
assert!(routes_approval_to_guardian(&turn));
}
#[test]
fn build_guardian_transcript_reserves_separate_budget_for_tool_evidence() {
let repeated = "signal ".repeat(8_000);
+116 -26
View File
@@ -1894,12 +1894,13 @@ impl Session {
}
pub async fn request_permissions(
&self,
turn_context: &TurnContext,
self: &Arc<Self>,
turn_context: &Arc<TurnContext>,
call_id: String,
args: RequestPermissionsArgs,
cancellation_token: CancellationToken,
) -> Option<RequestPermissionsResponse> {
match turn_context.approval_policy.value() {
match turn_context.as_ref().approval_policy.value() {
AskForApproval::Never => {
return Some(RequestPermissionsResponse {
permissions: RequestPermissionProfile::default(),
@@ -1920,6 +1921,72 @@ impl Session {
| AskForApproval::Granular(_) => {}
}
if crate::guardian::routes_approval_to_guardian(turn_context.as_ref()) {
let requested_permissions = args.permissions;
let originating_turn_state = {
let active = self.active_turn.lock().await;
active.as_ref().map(|active| Arc::clone(&active.turn_state))
};
let review_id = crate::guardian::new_guardian_review_id();
let session = Arc::clone(self);
let turn = Arc::clone(turn_context);
let request = crate::guardian::GuardianApprovalRequest::RequestPermissions {
id: call_id,
turn_id: turn_context.sub_id.clone(),
reason: args.reason,
permissions: requested_permissions.clone(),
};
let review_rx = crate::guardian::spawn_approval_request_review(
session,
turn,
review_id,
request,
/*retry_reason*/ None,
cancellation_token.clone(),
);
let decision = tokio::select! {
biased;
_ = cancellation_token.cancelled() => return None,
decision = review_rx => decision.unwrap_or(ReviewDecision::Denied),
};
let response = match decision {
ReviewDecision::Approved | ReviewDecision::ApprovedExecpolicyAmendment { .. } => {
RequestPermissionsResponse {
permissions: requested_permissions,
scope: PermissionGrantScope::Turn,
}
}
ReviewDecision::ApprovedForSession => RequestPermissionsResponse {
permissions: requested_permissions,
scope: PermissionGrantScope::Session,
},
ReviewDecision::NetworkPolicyAmendment {
network_policy_amendment,
} => match network_policy_amendment.action {
NetworkPolicyRuleAction::Allow => RequestPermissionsResponse {
permissions: requested_permissions,
scope: PermissionGrantScope::Turn,
},
NetworkPolicyRuleAction::Deny => RequestPermissionsResponse {
permissions: RequestPermissionProfile::default(),
scope: PermissionGrantScope::Turn,
},
},
ReviewDecision::Abort | ReviewDecision::Denied | ReviewDecision::TimedOut => {
RequestPermissionsResponse {
permissions: RequestPermissionProfile::default(),
scope: PermissionGrantScope::Turn,
}
}
};
self.record_granted_request_permissions_for_turn(
&response,
originating_turn_state.as_ref(),
)
.await;
return Some(response);
}
let (tx_response, rx_response) = oneshot::channel();
let prev_entry = {
let mut active = self.active_turn.lock().await;
@@ -1935,17 +2002,25 @@ impl Session {
warn!("Overwriting existing pending request_permissions for call_id: {call_id}");
}
// TODO(ccunningham): Support auto-review for request_permissions /
// with_additional_permissions. V0 still routes this surface through
// the existing manual RequestPermissions event flow.
let event = EventMsg::RequestPermissions(RequestPermissionsEvent {
call_id,
call_id: call_id.clone(),
turn_id: turn_context.sub_id.clone(),
reason: args.reason,
permissions: args.permissions,
});
self.send_event(turn_context, event).await;
rx_response.await.ok()
self.send_event(turn_context.as_ref(), event).await;
tokio::select! {
biased;
_ = cancellation_token.cancelled() => {
let mut active = self.active_turn.lock().await;
if let Some(at) = active.as_mut() {
let mut ts = at.turn_state.lock().await;
let _ = ts.remove_pending_request_permissions(&call_id);
}
None
}
response = rx_response => response.ok(),
}
}
pub async fn request_user_input(
@@ -2010,31 +2085,24 @@ impl Session {
call_id: &str,
response: RequestPermissionsResponse,
) {
let mut granted_for_session = None;
let entry = {
let (entry, originating_turn_state) = {
let mut active = self.active_turn.lock().await;
match active.as_mut() {
Some(at) => {
let mut ts = at.turn_state.lock().await;
let entry = ts.remove_pending_request_permissions(call_id);
if entry.is_some() && !response.permissions.is_empty() {
match response.scope {
PermissionGrantScope::Turn => {
ts.record_granted_permissions(response.permissions.clone().into());
}
PermissionGrantScope::Session => {
granted_for_session = Some(response.permissions.clone());
}
}
}
entry
let originating_turn_state = entry.as_ref().map(|_| Arc::clone(&at.turn_state));
(entry, originating_turn_state)
}
None => None,
None => (None, None),
}
};
if let Some(permissions) = granted_for_session {
let mut state = self.state.lock().await;
state.record_granted_permissions(permissions.into());
if entry.is_some() {
self.record_granted_request_permissions_for_turn(
&response,
originating_turn_state.as_ref(),
)
.await;
}
match entry {
Some(tx_response) => {
@@ -2046,6 +2114,28 @@ impl Session {
}
}
async fn record_granted_request_permissions_for_turn(
&self,
response: &RequestPermissionsResponse,
originating_turn_state: Option<&Arc<Mutex<crate::state::TurnState>>>,
) {
if response.permissions.is_empty() {
return;
}
match response.scope {
PermissionGrantScope::Turn => {
if let Some(turn_state) = originating_turn_state {
let mut ts = turn_state.lock().await;
ts.record_granted_permissions(response.permissions.clone().into());
}
}
PermissionGrantScope::Session => {
let mut state = self.state.lock().await;
state.record_granted_permissions(response.permissions.clone().into());
}
}
}
pub(crate) async fn granted_turn_permissions(&self) -> Option<PermissionProfile> {
let active = self.active_turn.lock().await;
let active = active.as_ref()?;
+42 -2
View File
@@ -3438,6 +3438,41 @@ async fn notify_request_permissions_response_ignores_unmatched_call_id() {
assert_eq!(session.granted_turn_permissions().await, None);
}
#[tokio::test]
async fn record_granted_request_permissions_for_turn_uses_originating_turn() {
let (session, _turn_context) = make_session_and_context().await;
let originating_active_turn = ActiveTurn::default();
let originating_turn_state = Arc::clone(&originating_active_turn.turn_state);
*session.active_turn.lock().await = Some(originating_active_turn);
let current_active_turn = ActiveTurn::default();
let current_turn_state = Arc::clone(&current_active_turn.turn_state);
*session.active_turn.lock().await = Some(current_active_turn);
let requested_permissions = RequestPermissionProfile {
network: Some(codex_protocol::models::NetworkPermissions {
enabled: Some(true),
}),
..RequestPermissionProfile::default()
};
session
.record_granted_request_permissions_for_turn(
&codex_protocol::request_permissions::RequestPermissionsResponse {
permissions: requested_permissions.clone(),
scope: PermissionGrantScope::Turn,
},
Some(&originating_turn_state),
)
.await;
assert_eq!(
originating_turn_state.lock().await.granted_permissions(),
Some(requested_permissions.into())
);
assert_eq!(current_turn_state.lock().await.granted_permissions(), None);
assert_eq!(session.granted_turn_permissions().await, None);
}
#[tokio::test]
async fn request_permissions_emits_event_when_granular_policy_allows_requests() {
let (session, mut turn_context, rx) = make_session_and_context_with_rx().await;
@@ -3474,7 +3509,7 @@ async fn request_permissions_emits_event_when_granular_policy_allows_requests()
async move {
session
.request_permissions(
turn_context.as_ref(),
&turn_context,
call_id,
codex_protocol::request_permissions::RequestPermissionsArgs {
reason: Some("need network".to_string()),
@@ -3485,6 +3520,7 @@ async fn request_permissions_emits_event_when_granular_policy_allows_requests()
..RequestPermissionProfile::default()
},
},
CancellationToken::new(),
)
.await
}
@@ -3532,7 +3568,7 @@ async fn request_permissions_is_auto_denied_when_granular_policy_blocks_tool_req
let call_id = "call-1".to_string();
let response = session
.request_permissions(
turn_context.as_ref(),
&turn_context,
call_id,
codex_protocol::request_permissions::RequestPermissionsArgs {
reason: Some("need network".to_string()),
@@ -3543,6 +3579,7 @@ async fn request_permissions_is_auto_denied_when_granular_policy_blocks_tool_req
..RequestPermissionProfile::default()
},
},
CancellationToken::new(),
)
.await;
@@ -6360,6 +6397,7 @@ async fn fatal_tool_error_stops_turn_and_reports_error() {
.dispatch_tool_call_with_code_mode_result(
Arc::clone(&session),
Arc::clone(&turn_context),
CancellationToken::new(),
tracker,
call,
ToolCallSource::Direct,
@@ -6602,6 +6640,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
.handle(ToolInvocation {
session: Arc::clone(&session),
turn: Arc::clone(&turn_context),
cancellation_token: CancellationToken::new(),
tracker: Arc::clone(&turn_diff_tracker),
call_id,
tool_name: codex_tools::ToolName::plain(tool_name),
@@ -6680,6 +6719,7 @@ async fn unified_exec_rejects_escalated_permissions_when_policy_not_on_request()
.handle(ToolInvocation {
session: Arc::clone(&session),
turn: Arc::clone(&turn_context),
cancellation_token: CancellationToken::new(),
tracker: Arc::clone(&tracker),
call_id: "exec-call".to_string(),
tool_name: codex_tools::ToolName::plain("exec_command"),
@@ -17,6 +17,7 @@ use codex_execpolicy::Evaluation;
use codex_execpolicy::RuleMatch;
use codex_features::Feature;
use codex_model_provider::create_model_provider;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::models::ContentItem;
use codex_protocol::models::NetworkPermissions;
use codex_protocol::models::PermissionProfile;
@@ -25,26 +26,214 @@ use codex_protocol::models::function_call_output_content_items_to_text;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_protocol::permissions::NetworkSandboxPolicy;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::request_permissions::PermissionGrantScope;
use codex_protocol::request_permissions::RequestPermissionProfile;
use codex_protocol::request_permissions::RequestPermissionsArgs;
use codex_protocol::request_permissions::RequestPermissionsResponse;
use core_test_support::PathExt;
use core_test_support::TempDirExt;
use core_test_support::codex_linux_sandbox_exe_or_skip;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed;
use core_test_support::responses::ev_response_created;
use core_test_support::responses::mount_response_once;
use core_test_support::responses::mount_sse_once;
use core_test_support::responses::sse;
use core_test_support::responses::sse_response;
use core_test_support::responses::start_mock_server;
use pretty_assertions::assert_eq;
use serde::Deserialize;
use std::collections::HashMap;
use std::fs;
use std::sync::Arc;
use std::time::Duration;
use tempfile::tempdir;
use tokio_util::sync::CancellationToken;
fn expect_text_output(output: &FunctionToolOutput) -> String {
function_call_output_content_items_to_text(&output.body).unwrap_or_default()
}
#[tokio::test]
async fn request_permissions_routes_to_guardian_when_reviewer_is_enabled() {
let server = start_mock_server().await;
let guardian_request_log = mount_sse_once(
&server,
sse(vec![
ev_response_created("resp-guardian"),
ev_assistant_message(
"msg-guardian",
&serde_json::json!({
"risk_level": "low",
"user_authorization": "high",
"outcome": "allow",
"rationale": "The request grants narrowly scoped network access for this turn.",
})
.to_string(),
),
ev_completed("resp-guardian"),
]),
)
.await;
let (mut session, mut turn_context_raw) = make_session_and_context().await;
*session.active_turn.lock().await = Some(ActiveTurn::default());
turn_context_raw
.approval_policy
.set(AskForApproval::OnRequest)
.expect("test setup should allow updating approval policy");
turn_context_raw
.features
.enable(Feature::GuardianApproval)
.expect("test setup should allow enabling guardian approvals");
let mut config = (*turn_context_raw.config).clone();
config.approvals_reviewer = ApprovalsReviewer::GuardianSubagent;
config.model_provider.base_url = Some(format!("{}/v1", server.uri()));
let config = Arc::new(config);
let models_manager = Arc::new(crate::test_support::models_manager_with_provider(
config.codex_home.to_path_buf(),
Arc::clone(&session.services.auth_manager),
config.model_provider.clone(),
));
session.services.models_manager = models_manager;
turn_context_raw.config = Arc::clone(&config);
turn_context_raw.provider = create_model_provider(
config.model_provider.clone(),
turn_context_raw.auth_manager.clone(),
);
let session = Arc::new(session);
let turn_context = Arc::new(turn_context_raw);
let requested_permissions = RequestPermissionProfile {
network: Some(NetworkPermissions {
enabled: Some(true),
}),
..RequestPermissionProfile::default()
};
let response = tokio::time::timeout(
Duration::from_secs(45),
session.request_permissions(
&turn_context,
"perm-call-1".to_string(),
RequestPermissionsArgs {
reason: Some("need network".to_string()),
permissions: requested_permissions.clone(),
},
CancellationToken::new(),
),
)
.await
.expect("request_permissions should not wait for a client approval");
assert_eq!(
response,
Some(RequestPermissionsResponse {
permissions: requested_permissions.clone(),
scope: PermissionGrantScope::Turn,
})
);
assert_eq!(
session.granted_turn_permissions().await,
Some(requested_permissions.into())
);
let guardian_request = guardian_request_log.single_request();
assert_eq!(guardian_request.path(), "/v1/responses");
assert!(guardian_request.body_contains_text("request_permissions"));
assert!(guardian_request.body_contains_text("need network"));
}
#[tokio::test]
async fn request_permissions_guardian_review_stops_when_cancelled() {
let server = start_mock_server().await;
let _guardian_request_log = mount_response_once(
&server,
sse_response(sse(vec![ev_response_created("resp-guardian-delayed")]))
.set_delay(Duration::from_secs(60)),
)
.await;
let (mut session, mut turn_context, rx_event) = make_session_and_context_with_rx().await;
*session.active_turn.lock().await = Some(ActiveTurn::default());
let turn_context_raw = Arc::get_mut(&mut turn_context).expect("single turn context ref");
turn_context_raw
.approval_policy
.set(AskForApproval::OnRequest)
.expect("test setup should allow updating approval policy");
turn_context_raw
.features
.enable(Feature::GuardianApproval)
.expect("test setup should allow enabling guardian approvals");
let mut config = (*turn_context_raw.config).clone();
config.approvals_reviewer = ApprovalsReviewer::GuardianSubagent;
config.model_provider.base_url = Some(format!("{}/v1", server.uri()));
let config = Arc::new(config);
let models_manager = Arc::new(crate::test_support::models_manager_with_provider(
config.codex_home.to_path_buf(),
Arc::clone(&session.services.auth_manager),
config.model_provider.clone(),
));
Arc::get_mut(&mut session)
.expect("single session ref")
.services
.models_manager = models_manager;
turn_context_raw.config = Arc::clone(&config);
turn_context_raw.provider = create_model_provider(
config.model_provider.clone(),
turn_context_raw.auth_manager.clone(),
);
let requested_permissions = RequestPermissionProfile {
network: Some(NetworkPermissions {
enabled: Some(true),
}),
..RequestPermissionProfile::default()
};
let cancellation_token = CancellationToken::new();
let request_handle = tokio::spawn({
let session = Arc::clone(&session);
let turn_context = Arc::clone(&turn_context);
let requested_permissions = requested_permissions.clone();
let cancellation_token = cancellation_token.clone();
async move {
session
.request_permissions(
&turn_context,
"perm-call-cancelled".to_string(),
RequestPermissionsArgs {
reason: Some("need network".to_string()),
permissions: requested_permissions,
},
cancellation_token,
)
.await
}
});
timeout(Duration::from_secs(5), async {
loop {
let event = rx_event.recv().await.expect("event channel should be open");
if matches!(
event.msg,
codex_protocol::protocol::EventMsg::GuardianAssessment(_)
) {
break;
}
}
})
.await
.expect("guardian review should start before cancellation");
cancellation_token.cancel();
let response = timeout(Duration::from_secs(5), request_handle)
.await
.expect("request_permissions should stop when cancelled")
.expect("request_permissions task should not panic");
assert_eq!(response, None);
assert_eq!(session.granted_turn_permissions().await, None);
}
#[tokio::test]
async fn guardian_allows_shell_additional_permissions_requests_past_policy_validation() {
let server = start_mock_server().await;
@@ -146,6 +335,7 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid
.handle(ToolInvocation {
session: Arc::clone(&session),
turn: Arc::clone(&turn_context),
cancellation_token: CancellationToken::new(),
tracker: Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
call_id: "test-call".to_string(),
tool_name: codex_tools::ToolName::plain("shell"),
@@ -212,6 +402,7 @@ async fn guardian_allows_unified_exec_additional_permissions_requests_past_polic
.handle(ToolInvocation {
session: Arc::clone(&session),
turn: Arc::clone(&turn_context),
cancellation_token: CancellationToken::new(),
tracker: Arc::clone(&tracker),
call_id: "exec-call".to_string(),
tool_name: codex_tools::ToolName::plain("exec_command"),
@@ -325,6 +516,7 @@ async fn shell_handler_allows_sticky_turn_permissions_without_inline_request_per
.handle(ToolInvocation {
session: Arc::clone(&session),
turn: Arc::clone(&turn_context),
cancellation_token: CancellationToken::new(),
tracker: Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new())),
call_id: "sticky-turn-grant".to_string(),
tool_name: codex_tools::ToolName::plain("shell"),
+2
View File
@@ -26,6 +26,7 @@ use std::borrow::Cow;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
pub type SharedTurnDiffTracker = Arc<Mutex<TurnDiffTracker>>;
@@ -40,6 +41,7 @@ pub enum ToolCallSource {
pub struct ToolInvocation {
pub session: Arc<Session>,
pub turn: Arc<TurnContext>,
pub cancellation_token: CancellationToken,
pub tracker: SharedTurnDiffTracker,
pub call_id: String,
pub tool_name: ToolName,
+8 -1
View File
@@ -109,6 +109,7 @@ impl ToolHandler for JsReplHandler {
let ToolInvocation {
session,
turn,
cancellation_token,
tracker,
payload,
call_id,
@@ -134,7 +135,13 @@ impl ToolHandler for JsReplHandler {
let started_at = Instant::now();
emit_js_repl_exec_begin(session.as_ref(), turn.as_ref(), &call_id).await;
let result = manager
.execute(Arc::clone(&session), Arc::clone(&turn), tracker, args)
.execute_with_cancellation(
Arc::clone(&session),
Arc::clone(&turn),
cancellation_token,
tracker,
args,
)
.await;
let result = match result {
Ok(result) => result,
@@ -68,6 +68,7 @@ fn invocation(
ToolInvocation {
session,
turn,
cancellation_token: CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::default())),
call_id: "call-1".to_string(),
tool_name: codex_tools::ToolName::plain(tool_name),
@@ -22,6 +22,7 @@ impl ToolHandler for RequestPermissionsHandler {
let ToolInvocation {
session,
turn,
cancellation_token,
call_id,
payload,
..
@@ -48,7 +49,7 @@ impl ToolHandler for RequestPermissionsHandler {
}
let response = session
.request_permissions(turn.as_ref(), call_id, args)
.request_permissions(&turn, call_id, args, cancellation_token)
.await
.ok_or_else(|| {
FunctionCallError::RespondToModel(
@@ -27,6 +27,7 @@ async fn multi_agent_v2_request_user_input_rejects_subagent_threads() {
.handle(ToolInvocation {
session: Arc::new(session),
turn: Arc::new(turn),
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::default())),
call_id: "call-1".to_string(),
tool_name: codex_tools::ToolName::plain(REQUEST_USER_INPUT_TOOL_NAME),
@@ -225,6 +225,7 @@ async fn shell_pre_tool_use_payload_uses_joined_command() {
handler.pre_tool_use_payload(&ToolInvocation {
session: session.into(),
turn: turn.into(),
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
call_id: "call-41".to_string(),
tool_name: codex_tools::ToolName::plain("shell"),
@@ -250,6 +251,7 @@ async fn shell_command_pre_tool_use_payload_uses_raw_command() {
handler.pre_tool_use_payload(&ToolInvocation {
session: session.into(),
turn: turn.into(),
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
call_id: "call-42".to_string(),
tool_name: codex_tools::ToolName::plain("shell_command"),
@@ -210,6 +210,7 @@ async fn exec_command_pre_tool_use_payload_uses_raw_command() {
handler.pre_tool_use_payload(&ToolInvocation {
session: session.into(),
turn: turn.into(),
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
call_id: "call-43".to_string(),
tool_name: codex_tools::ToolName::plain("exec_command"),
@@ -233,6 +234,7 @@ async fn exec_command_pre_tool_use_payload_skips_write_stdin() {
handler.pre_tool_use_payload(&ToolInvocation {
session: session.into(),
turn: turn.into(),
cancellation_token: tokio_util::sync::CancellationToken::new(),
tracker: Arc::new(Mutex::new(TurnDiffTracker::new())),
call_id: "call-44".to_string(),
tool_name: codex_tools::ToolName::plain("write_stdin"),
+17
View File
@@ -132,6 +132,7 @@ struct KernelState {
struct ExecContext {
session: Arc<Session>,
turn: Arc<TurnContext>,
cancellation_token: CancellationToken,
tracker: SharedTurnDiffTracker,
}
@@ -836,12 +837,25 @@ impl JsReplManager {
}
}
#[cfg(test)]
pub async fn execute(
&self,
session: Arc<Session>,
turn: Arc<TurnContext>,
tracker: SharedTurnDiffTracker,
args: JsReplArgs,
) -> Result<JsExecResult, FunctionCallError> {
self.execute_with_cancellation(session, turn, CancellationToken::new(), tracker, args)
.await
}
pub async fn execute_with_cancellation(
&self,
session: Arc<Session>,
turn: Arc<TurnContext>,
cancellation_token: CancellationToken,
tracker: SharedTurnDiffTracker,
args: JsReplArgs,
) -> Result<JsExecResult, FunctionCallError> {
let _permit = self.exec_lock.clone().acquire_owned().await.map_err(|_| {
FunctionCallError::RespondToModel("js_repl execution unavailable".to_string())
@@ -892,6 +906,7 @@ impl JsReplManager {
ExecContext {
session: Arc::clone(&session),
turn: Arc::clone(&turn),
cancellation_token,
tracker,
},
);
@@ -1640,12 +1655,14 @@ impl JsReplManager {
let session = Arc::clone(&exec.session);
let turn = Arc::clone(&exec.turn);
let cancellation_token = exec.cancellation_token.clone();
let tracker = Arc::clone(&exec.tracker);
match router
.dispatch_tool_call_with_code_mode_result(
session,
turn,
cancellation_token,
tracker,
call,
crate::tools::router::ToolCallSource::JsRepl,
+2
View File
@@ -92,6 +92,7 @@ impl ToolCallRuntime {
let turn = Arc::clone(&self.turn_context);
let tracker = Arc::clone(&self.tracker);
let lock = Arc::clone(&self.parallel_execution);
let invocation_cancellation_token = cancellation_token.clone();
let started = Instant::now();
let display_name = call.tool_name.display();
@@ -122,6 +123,7 @@ impl ToolCallRuntime {
.dispatch_tool_call_with_code_mode_result(
session,
turn,
invocation_cancellation_token,
tracker,
call.clone(),
source,
+3
View File
@@ -24,6 +24,7 @@ use codex_tools::ToolsConfig;
use std::collections::HashMap;
use std::collections::HashSet;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
use tracing::instrument;
pub use crate::tools::context::ToolCallSource;
@@ -267,6 +268,7 @@ impl ToolRouter {
&self,
session: Arc<Session>,
turn: Arc<TurnContext>,
cancellation_token: CancellationToken,
tracker: SharedTurnDiffTracker,
call: ToolCall,
source: ToolCallSource,
@@ -292,6 +294,7 @@ impl ToolRouter {
let invocation = ToolInvocation {
session,
turn,
cancellation_token,
tracker,
call_id,
tool_name,
+4
View File
@@ -7,6 +7,7 @@ use crate::tools::context::ToolPayload;
use crate::turn_diff_tracker::TurnDiffTracker;
use codex_protocol::models::ResponseItem;
use codex_tools::ToolName;
use tokio_util::sync::CancellationToken;
use super::ToolCall;
use super::ToolCallSource;
@@ -52,6 +53,7 @@ async fn js_repl_tools_only_blocks_direct_tool_calls() -> anyhow::Result<()> {
.dispatch_tool_call_with_code_mode_result(
session,
turn,
CancellationToken::new(),
tracker,
call,
ToolCallSource::Direct,
@@ -106,6 +108,7 @@ async fn js_repl_tools_only_allows_js_repl_source_calls() -> anyhow::Result<()>
.dispatch_tool_call_with_code_mode_result(
session,
turn,
CancellationToken::new(),
tracker,
call,
ToolCallSource::JsRepl,
@@ -155,6 +158,7 @@ async fn js_repl_tools_only_blocks_namespaced_js_repl_tool() -> anyhow::Result<(
.dispatch_tool_call_with_code_mode_result(
session,
turn,
CancellationToken::new(),
tracker,
call,
ToolCallSource::Direct,
+5
View File
@@ -6,6 +6,7 @@ use crate::permissions::NetworkSandboxPolicy;
use crate::protocol::FileChange;
use crate::protocol::ReviewDecision;
use crate::protocol::SandboxPolicy;
use crate::request_permissions::RequestPermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -163,6 +164,10 @@ pub enum GuardianAssessmentAction {
connector_name: Option<String>,
tool_title: Option<String>,
},
RequestPermissions {
reason: Option<String>,
permissions: RequestPermissionProfile,
},
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
+24 -1
View File
@@ -3607,6 +3607,14 @@ impl ChatWidget {
/// render the final approved/denied history cell when guardian returns a
/// decision.
fn on_guardian_assessment(&mut self, ev: GuardianAssessmentEvent) {
let permission_request_summary = |subject: &str, reason: &Option<String>| {
reason
.as_deref()
.map(str::trim)
.filter(|reason| !reason.is_empty())
.map(|reason| format!("{subject}: {reason}"))
.unwrap_or_else(|| subject.to_string())
};
let guardian_action_summary = |action: &GuardianAssessmentAction| match action {
GuardianAssessmentAction::Command { command, .. } => Some(command.clone()),
GuardianAssessmentAction::Execve { program, argv, .. } => {
@@ -3636,6 +3644,9 @@ impl ChatWidget {
let label = connector_name.as_deref().unwrap_or(server.as_str());
Some(format!("MCP {tool_name} on {label}"))
}
GuardianAssessmentAction::RequestPermissions { reason, .. } => {
Some(permission_request_summary("permission request", reason))
}
};
let guardian_command = |action: &GuardianAssessmentAction| match action {
GuardianAssessmentAction::Command { command, .. } => shlex::split(command)
@@ -3649,7 +3660,8 @@ impl ChatWidget {
.filter(|command| !command.is_empty()),
GuardianAssessmentAction::ApplyPatch { .. }
| GuardianAssessmentAction::NetworkAccess { .. }
| GuardianAssessmentAction::McpToolCall { .. } => None,
| GuardianAssessmentAction::McpToolCall { .. }
| GuardianAssessmentAction::RequestPermissions { .. } => None,
};
if ev.status == GuardianAssessmentStatus::InProgress
@@ -3740,6 +3752,11 @@ impl ChatWidget {
"codex could access {target}"
))
}
GuardianAssessmentAction::RequestPermissions { reason, .. } => {
history_cell::new_guardian_timed_out_action_request(
permission_request_summary("codex could request permissions", reason),
)
}
GuardianAssessmentAction::Command { .. } => unreachable!(),
GuardianAssessmentAction::Execve { .. } => unreachable!(),
}
@@ -3778,6 +3795,12 @@ impl ChatWidget {
"codex to access {target}"
))
}
GuardianAssessmentAction::RequestPermissions { reason, .. } => {
history_cell::new_guardian_denied_action_request(permission_request_summary(
"codex to request permissions",
reason,
))
}
GuardianAssessmentAction::Command { .. } => unreachable!(),
GuardianAssessmentAction::Execve { .. } => unreachable!(),
}
@@ -0,0 +1,16 @@
---
source: tui/src/chatwidget/tests/guardian.rs
expression: normalize_snapshot_paths(term.backend().vt100().screen().contents())
---
✔ Request approved for permission request: Need write access for generated
report assets.
• Working (0s • esc to interrupt)
Ask Codex to do anything
gpt-5.4 default · /tmp/project
@@ -121,6 +121,86 @@ async fn guardian_approved_exec_renders_approved_request() {
);
}
#[tokio::test]
async fn guardian_approved_request_permissions_renders_request_summary() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
chat.show_welcome_banner = false;
let action = GuardianAssessmentAction::RequestPermissions {
reason: Some("Need write access for generated report assets.".to_string()),
permissions: RequestPermissionProfile {
file_system: Some(FileSystemPermissions::from_read_write_roots(
/*read*/ None,
Some(vec![test_path_buf("/tmp/reports").abs()]),
)),
..RequestPermissionProfile::default()
},
};
chat.handle_codex_event(Event {
id: "guardian-in-progress".into(),
msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent {
id: "guardian-request-permissions".into(),
target_item_id: None,
turn_id: "turn-1".into(),
status: GuardianAssessmentStatus::InProgress,
risk_level: None,
user_authorization: None,
rationale: None,
decision_source: None,
action: action.clone(),
}),
});
let status = chat
.bottom_pane
.status_widget()
.expect("status indicator should be visible");
assert_eq!(status.header(), "Reviewing approval request");
assert_eq!(
status.details(),
Some("permission request: Need write access for generated report assets.")
);
chat.handle_codex_event(Event {
id: "guardian-assessment".into(),
msg: EventMsg::GuardianAssessment(GuardianAssessmentEvent {
id: "guardian-request-permissions".into(),
target_item_id: None,
turn_id: "turn-1".into(),
status: GuardianAssessmentStatus::Approved,
risk_level: Some(GuardianRiskLevel::Low),
user_authorization: Some(GuardianUserAuthorization::High),
rationale: Some("Request is scoped to report output.".into()),
decision_source: Some(GuardianAssessmentDecisionSource::Agent),
action,
}),
});
let width: u16 = 110;
let ui_height: u16 = chat.desired_height(width);
let vt_height: u16 = 12;
let viewport = Rect::new(0, vt_height - ui_height - 1, width, ui_height);
let backend = VT100Backend::new(width, vt_height);
let mut term = crate::custom_terminal::Terminal::with_options(backend).expect("terminal");
term.set_viewport_area(viewport);
for lines in drain_insert_history(&mut rx) {
crate::insert_history::insert_history_lines(&mut term, lines)
.expect("Failed to insert history lines in test");
}
term.draw(|f| {
chat.render(f.area(), f.buffer_mut());
})
.expect("draw guardian request permissions approval history");
assert_chatwidget_snapshot!(
"guardian_approved_request_permissions_renders_request_summary",
normalize_snapshot_paths(term.backend().vt100().screen().contents())
);
}
#[tokio::test]
async fn guardian_timed_out_exec_renders_warning_and_timed_out_request() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;