mirror of
https://github.com/openai/codex.git
synced 2026-04-04 04:44:47 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cece4f1e49 |
@@ -7,6 +7,15 @@
|
||||
},
|
||||
"AdditionalFileSystemPermissions": {
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"read": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
@@ -253,6 +262,199 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"FileSystemAccessMode": {
|
||||
"description": "Access mode for a filesystem entry.\n\nWhen two equally specific entries target the same path, we compare these by conflict precedence rather than by capability breadth: `none` beats `write`, and `write` beats `read`.",
|
||||
"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": {
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"description": "WARNING: `:special_path` tokens are part of config compatibility. Do not make older runtimes reject newly introduced tokens. New parser support should be additive, while unknown values must stay representable so config from a newer Codex degrades to warn-and-ignore instead of failing to load. Codex 0.112.0 rejected unknown values here, which broke forward compatibility for newer config. Preserves future special-path tokens so older runtimes can ignore them without rejecting config authored by a newer release.",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"subpath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"NetworkApprovalContext": {
|
||||
"properties": {
|
||||
"host": {
|
||||
|
||||
@@ -7,6 +7,15 @@
|
||||
},
|
||||
"AdditionalFileSystemPermissions": {
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"read": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
@@ -39,6 +48,199 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"FileSystemAccessMode": {
|
||||
"description": "Access mode for a filesystem entry.\n\nWhen two equally specific entries target the same path, we compare these by conflict precedence rather than by capability breadth: `none` beats `write`, and `write` beats `read`.",
|
||||
"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": {
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"description": "WARNING: `:special_path` tokens are part of config compatibility. Do not make older runtimes reject newly introduced tokens. New parser support should be additive, while unknown values must stay representable so config from a newer Codex degrades to warn-and-ignore instead of failing to load. Codex 0.112.0 rejected unknown values here, which broke forward compatibility for newer config. Preserves future special-path tokens so older runtimes can ignore them without rejecting config authored by a newer release.",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"subpath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"RequestPermissionProfile": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
|
||||
@@ -7,6 +7,15 @@
|
||||
},
|
||||
"AdditionalFileSystemPermissions": {
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"read": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
@@ -39,6 +48,199 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"FileSystemAccessMode": {
|
||||
"description": "Access mode for a filesystem entry.\n\nWhen two equally specific entries target the same path, we compare these by conflict precedence rather than by capability breadth: `none` beats `write`, and `write` beats `read`.",
|
||||
"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": {
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"description": "WARNING: `:special_path` tokens are part of config compatibility. Do not make older runtimes reject newly introduced tokens. New parser support should be additive, while unknown values must stay representable so config from a newer Codex degrades to warn-and-ignore instead of failing to load. Codex 0.112.0 rejected unknown values here, which broke forward compatibility for newer config. Preserves future special-path tokens so older runtimes can ignore them without rejecting config authored by a newer release.",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"subpath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"GrantedPermissionProfile": {
|
||||
"properties": {
|
||||
"fileSystem": {
|
||||
|
||||
@@ -7,6 +7,15 @@
|
||||
},
|
||||
"AdditionalFileSystemPermissions": {
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"read": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
@@ -582,6 +591,199 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"FileSystemAccessMode": {
|
||||
"description": "Access mode for a filesystem entry.\n\nWhen two equally specific entries target the same path, we compare these by conflict precedence rather than by capability breadth: `none` beats `write`, and `write` beats `read`.",
|
||||
"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": {
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"description": "WARNING: `:special_path` tokens are part of config compatibility. Do not make older runtimes reject newly introduced tokens. New parser support should be additive, while unknown values must stay representable so config from a newer Codex degrades to warn-and-ignore instead of failing to load. Codex 0.112.0 rejected unknown values here, which broke forward compatibility for newer config. Preserves future special-path tokens so older runtimes can ignore them without rejecting config authored by a newer release.",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"subpath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"McpElicitationArrayType": {
|
||||
"enum": [
|
||||
"array"
|
||||
|
||||
@@ -7,6 +7,15 @@
|
||||
},
|
||||
"AdditionalFileSystemPermissions": {
|
||||
"properties": {
|
||||
"entries": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/FileSystemSandboxEntry"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"read": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
@@ -2078,6 +2087,199 @@
|
||||
"title": "FileChangeRequestApprovalResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"FileSystemAccessMode": {
|
||||
"description": "Access mode for a filesystem entry.\n\nWhen two equally specific entries target the same path, we compare these by conflict precedence rather than by capability breadth: `none` beats `write`, and `write` beats `read`.",
|
||||
"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": {
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"description": "WARNING: `:special_path` tokens are part of config compatibility. Do not make older runtimes reject newly introduced tokens. New parser support should be additive, while unknown values must stay representable so config from a newer Codex degrades to warn-and-ignore instead of failing to load. Codex 0.112.0 rejected unknown values here, which broke forward compatibility for newer config. Preserves future special-path tokens so older runtimes can ignore them without rejecting config authored by a newer release.",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"subpath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"kind",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"FuzzyFileSearchMatchType": {
|
||||
"enum": [
|
||||
"file",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* Access mode for a filesystem entry.
|
||||
*
|
||||
* When two equally specific entries target the same path, we compare these by
|
||||
* conflict precedence rather than by capability breadth: `none` beats
|
||||
* `write`, and `write` beats `read`.
|
||||
*/
|
||||
export type FileSystemAccessMode = "read" | "write" | "none";
|
||||
@@ -0,0 +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 { AbsolutePathBuf } from "./AbsolutePathBuf";
|
||||
import type { FileSystemSpecialPath } from "./FileSystemSpecialPath";
|
||||
|
||||
export type FileSystemPath = { "type": "path", path: AbsolutePathBuf, } | { "type": "special", value: FileSystemSpecialPath, };
|
||||
@@ -0,0 +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 { FileSystemAccessMode } from "./FileSystemAccessMode";
|
||||
import type { FileSystemPath } from "./FileSystemPath";
|
||||
|
||||
export type FileSystemSandboxEntry = { path: FileSystemPath, access: FileSystemAccessMode, };
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type FileSystemSpecialPath = { "kind": "root" } | { "kind": "minimal" } | { "kind": "current_working_directory" } | { "kind": "project_roots", subpath?: string, } | { "kind": "tmpdir" } | { "kind": "slash_tmp" } | { "kind": "unknown", path: string, subpath?: string, };
|
||||
@@ -16,6 +16,10 @@ export type { ExecCommandApprovalParams } from "./ExecCommandApprovalParams";
|
||||
export type { ExecCommandApprovalResponse } from "./ExecCommandApprovalResponse";
|
||||
export type { ExecPolicyAmendment } from "./ExecPolicyAmendment";
|
||||
export type { FileChange } from "./FileChange";
|
||||
export type { FileSystemAccessMode } from "./FileSystemAccessMode";
|
||||
export type { FileSystemPath } from "./FileSystemPath";
|
||||
export type { FileSystemSandboxEntry } from "./FileSystemSandboxEntry";
|
||||
export type { FileSystemSpecialPath } from "./FileSystemSpecialPath";
|
||||
export type { ForcedLoginMethod } from "./ForcedLoginMethod";
|
||||
export type { FunctionCallOutputBody } from "./FunctionCallOutputBody";
|
||||
export type { FunctionCallOutputContentItem } from "./FunctionCallOutputContentItem";
|
||||
|
||||
@@ -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 { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
import type { FileSystemSandboxEntry } from "../FileSystemSandboxEntry";
|
||||
|
||||
export type AdditionalFileSystemPermissions = { read: Array<AbsolutePathBuf> | null, write: Array<AbsolutePathBuf> | null, };
|
||||
export type AdditionalFileSystemPermissions = { read: Array<AbsolutePathBuf> | null, write: Array<AbsolutePathBuf> | null, entries: Array<FileSystemSandboxEntry> | null, };
|
||||
|
||||
@@ -1834,6 +1834,7 @@ mod tests {
|
||||
file_system: Some(v2::AdditionalFileSystemPermissions {
|
||||
read: Some(vec![absolute_path("/tmp/allowed")]),
|
||||
write: None,
|
||||
entries: None,
|
||||
}),
|
||||
}),
|
||||
proposed_execpolicy_amendment: None,
|
||||
|
||||
@@ -41,6 +41,9 @@ use codex_protocol::openai_models::ModelAvailabilityNux as CoreModelAvailability
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::openai_models::default_input_modalities;
|
||||
use codex_protocol::parse_command::ParsedCommand as CoreParsedCommand;
|
||||
use codex_protocol::permissions::FileSystemAccessMode as CoreFileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath as CoreFileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry as CoreFileSystemSandboxEntry;
|
||||
use codex_protocol::plan_tool::PlanItemArg as CorePlanItemArg;
|
||||
use codex_protocol::plan_tool::StepStatus as CorePlanStepStatus;
|
||||
use codex_protocol::protocol::AgentStatus as CoreAgentStatus;
|
||||
@@ -1087,22 +1090,46 @@ impl From<CoreNetworkApprovalContext> for NetworkApprovalContext {
|
||||
pub struct AdditionalFileSystemPermissions {
|
||||
pub read: Option<Vec<AbsolutePathBuf>>,
|
||||
pub write: Option<Vec<AbsolutePathBuf>>,
|
||||
pub entries: Option<Vec<CoreFileSystemSandboxEntry>>,
|
||||
}
|
||||
|
||||
impl From<CoreFileSystemPermissions> for AdditionalFileSystemPermissions {
|
||||
fn from(value: CoreFileSystemPermissions) -> Self {
|
||||
let entries = value
|
||||
.entries
|
||||
.iter()
|
||||
.any(|entry| {
|
||||
entry.access == CoreFileSystemAccessMode::None
|
||||
|| !matches!(entry.path, CoreFileSystemPath::Path { .. })
|
||||
})
|
||||
.then_some(value.entries.clone());
|
||||
let read = value
|
||||
.explicit_path_entries()
|
||||
.filter_map(|(path, access)| {
|
||||
(access == CoreFileSystemAccessMode::Read).then_some(path.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let write = value
|
||||
.explicit_path_entries()
|
||||
.filter_map(|(path, access)| {
|
||||
(access == CoreFileSystemAccessMode::Write).then_some(path.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
Self {
|
||||
read: value.read,
|
||||
write: value.write,
|
||||
read: (!read.is_empty()).then_some(read),
|
||||
write: (!write.is_empty()).then_some(write),
|
||||
entries,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AdditionalFileSystemPermissions> for CoreFileSystemPermissions {
|
||||
fn from(value: AdditionalFileSystemPermissions) -> Self {
|
||||
Self {
|
||||
read: value.read,
|
||||
write: value.write,
|
||||
match value.entries {
|
||||
Some(entries) if !entries.is_empty() => CoreFileSystemPermissions { entries },
|
||||
Some(_) | None => {
|
||||
CoreFileSystemPermissions::from_read_write_roots(value.read, value.write)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6124,6 +6151,7 @@ mod tests {
|
||||
AbsolutePathBuf::try_from(PathBuf::from(read_write_path))
|
||||
.expect("path must be absolute"),
|
||||
]),
|
||||
entries: None,
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -6134,16 +6162,16 @@ mod tests {
|
||||
network: Some(CoreNetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: Some(CoreFileSystemPermissions {
|
||||
read: Some(vec![
|
||||
file_system: Some(CoreFileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![
|
||||
AbsolutePathBuf::try_from(PathBuf::from(read_only_path))
|
||||
.expect("path must be absolute"),
|
||||
]),
|
||||
write: Some(vec![
|
||||
Some(vec![
|
||||
AbsolutePathBuf::try_from(PathBuf::from(read_write_path))
|
||||
.expect("path must be absolute"),
|
||||
]),
|
||||
}),
|
||||
)),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -6217,6 +6245,7 @@ mod tests {
|
||||
AbsolutePathBuf::try_from(PathBuf::from(read_write_path))
|
||||
.expect("path must be absolute"),
|
||||
]),
|
||||
entries: None,
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -6227,20 +6256,102 @@ mod tests {
|
||||
network: Some(CoreNetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: Some(CoreFileSystemPermissions {
|
||||
read: Some(vec![
|
||||
file_system: Some(CoreFileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![
|
||||
AbsolutePathBuf::try_from(PathBuf::from(read_only_path))
|
||||
.expect("path must be absolute"),
|
||||
]),
|
||||
write: Some(vec![
|
||||
Some(vec![
|
||||
AbsolutePathBuf::try_from(PathBuf::from(read_write_path))
|
||||
.expect("path must be absolute"),
|
||||
]),
|
||||
}),
|
||||
)),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn additional_file_system_permissions_empty_entries_fall_back_to_read_write_roots() {
|
||||
let read_only_path = if cfg!(windows) {
|
||||
r"C:\tmp\read-only"
|
||||
} else {
|
||||
"/tmp/read-only"
|
||||
};
|
||||
let read_write_path = if cfg!(windows) {
|
||||
r"C:\tmp\read-write"
|
||||
} else {
|
||||
"/tmp/read-write"
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
CoreFileSystemPermissions::from(AdditionalFileSystemPermissions {
|
||||
read: Some(vec![
|
||||
AbsolutePathBuf::try_from(PathBuf::from(read_only_path))
|
||||
.expect("path must be absolute"),
|
||||
]),
|
||||
write: Some(vec![
|
||||
AbsolutePathBuf::try_from(PathBuf::from(read_write_path))
|
||||
.expect("path must be absolute"),
|
||||
]),
|
||||
entries: Some(Vec::new()),
|
||||
}),
|
||||
CoreFileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![
|
||||
AbsolutePathBuf::try_from(PathBuf::from(read_only_path))
|
||||
.expect("path must be absolute"),
|
||||
]),
|
||||
Some(vec![
|
||||
AbsolutePathBuf::try_from(PathBuf::from(read_write_path))
|
||||
.expect("path must be absolute"),
|
||||
]),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn additional_permission_profile_preserves_canonical_file_system_entries() {
|
||||
let deny_path = if cfg!(windows) {
|
||||
r"C:\tmp\secret.txt"
|
||||
} else {
|
||||
"/tmp/secret.txt"
|
||||
};
|
||||
let file_system = CoreFileSystemPermissions {
|
||||
entries: vec![
|
||||
CoreFileSystemSandboxEntry {
|
||||
path: CoreFileSystemPath::Special {
|
||||
value: codex_protocol::permissions::FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: CoreFileSystemAccessMode::Write,
|
||||
},
|
||||
CoreFileSystemSandboxEntry {
|
||||
path: CoreFileSystemPath::Path {
|
||||
path: AbsolutePathBuf::try_from(PathBuf::from(deny_path))
|
||||
.expect("path must be absolute"),
|
||||
},
|
||||
access: CoreFileSystemAccessMode::None,
|
||||
},
|
||||
],
|
||||
};
|
||||
let permissions = CorePermissionProfile {
|
||||
file_system: Some(file_system.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let additional_permissions = AdditionalPermissionProfile::from(permissions.clone());
|
||||
assert_eq!(
|
||||
additional_permissions.file_system,
|
||||
Some(AdditionalFileSystemPermissions {
|
||||
read: None,
|
||||
write: None,
|
||||
entries: Some(file_system.entries),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
CorePermissionProfile::from(additional_permissions),
|
||||
permissions
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_request_approval_response_defaults_scope_to_turn() {
|
||||
let response = serde_json::from_value::<PermissionsRequestApprovalResponse>(json!({
|
||||
|
||||
@@ -3083,10 +3083,10 @@ mod tests {
|
||||
network: Some(CoreNetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: Some(CoreFileSystemPermissions {
|
||||
read: Some(vec![absolute_path(input_path)]),
|
||||
write: Some(vec![absolute_path(output_path)]),
|
||||
}),
|
||||
file_system: Some(CoreFileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![absolute_path(input_path)]),
|
||||
Some(vec![absolute_path(output_path)]),
|
||||
)),
|
||||
};
|
||||
let cases = vec![
|
||||
(
|
||||
@@ -3113,10 +3113,10 @@ mod tests {
|
||||
},
|
||||
}),
|
||||
CoreRequestPermissionProfile {
|
||||
file_system: Some(CoreFileSystemPermissions {
|
||||
read: None,
|
||||
write: Some(vec![absolute_path(output_path)]),
|
||||
}),
|
||||
file_system: Some(CoreFileSystemPermissions::from_read_write_roots(
|
||||
/*read*/ None,
|
||||
Some(vec![absolute_path(output_path)]),
|
||||
)),
|
||||
..CoreRequestPermissionProfile::default()
|
||||
},
|
||||
),
|
||||
@@ -3131,10 +3131,10 @@ mod tests {
|
||||
},
|
||||
}),
|
||||
CoreRequestPermissionProfile {
|
||||
file_system: Some(CoreFileSystemPermissions {
|
||||
read: Some(vec![absolute_path(input_path)]),
|
||||
write: Some(vec![absolute_path(output_path)]),
|
||||
}),
|
||||
file_system: Some(CoreFileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![absolute_path(input_path)]),
|
||||
Some(vec![absolute_path(output_path)]),
|
||||
)),
|
||||
..CoreRequestPermissionProfile::default()
|
||||
},
|
||||
),
|
||||
|
||||
@@ -799,6 +799,7 @@ mod tests {
|
||||
codex_app_server_protocol::AdditionalFileSystemPermissions {
|
||||
read: Some(vec![absolute_path("/tmp/allowed")]),
|
||||
write: None,
|
||||
entries: None,
|
||||
},
|
||||
),
|
||||
},
|
||||
@@ -861,6 +862,7 @@ mod tests {
|
||||
codex_app_server_protocol::AdditionalFileSystemPermissions {
|
||||
read: Some(vec![absolute_path("/tmp/allowed")]),
|
||||
write: None,
|
||||
entries: None,
|
||||
},
|
||||
),
|
||||
},
|
||||
@@ -887,7 +889,8 @@ mod tests {
|
||||
"network": null,
|
||||
"fileSystem": {
|
||||
"read": [allowed_path],
|
||||
"write": null,
|
||||
"write": null,
|
||||
"entries": null,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
@@ -93,6 +93,7 @@ async fn request_permissions_round_trip() -> Result<()> {
|
||||
file_system: Some(codex_app_server_protocol::AdditionalFileSystemPermissions {
|
||||
read: None,
|
||||
write: Some(vec![requested_writes[0].clone()]),
|
||||
entries: None,
|
||||
}),
|
||||
},
|
||||
scope: PermissionGrantScope::Turn,
|
||||
|
||||
@@ -1301,6 +1301,12 @@ impl Session {
|
||||
per_turn_config.service_tier = session_configuration.service_tier;
|
||||
per_turn_config.personality = session_configuration.personality;
|
||||
per_turn_config.approvals_reviewer = session_configuration.approvals_reviewer;
|
||||
per_turn_config.permissions.approval_policy = session_configuration.approval_policy.clone();
|
||||
per_turn_config.permissions.sandbox_policy = session_configuration.sandbox_policy.clone();
|
||||
per_turn_config.permissions.file_system_sandbox_policy =
|
||||
session_configuration.file_system_sandbox_policy.clone();
|
||||
per_turn_config.permissions.network_sandbox_policy =
|
||||
session_configuration.network_sandbox_policy;
|
||||
let resolved_web_search_mode = resolve_web_search_mode_for_turn(
|
||||
&per_turn_config.web_search_mode,
|
||||
session_configuration.sandbox_policy.get(),
|
||||
@@ -3323,6 +3329,11 @@ impl Session {
|
||||
ts.granted_permissions()
|
||||
}
|
||||
|
||||
pub(crate) async fn cwd(&self) -> AbsolutePathBuf {
|
||||
let state = self.state.lock().await;
|
||||
state.session_configuration.cwd.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn granted_session_permissions(&self) -> Option<PermissionProfile> {
|
||||
let state = self.state.lock().await;
|
||||
state.granted_permissions()
|
||||
|
||||
@@ -19,6 +19,7 @@ use assert_matches::assert_matches;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_features::Feature;
|
||||
use codex_features::FeaturesToml;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
@@ -545,6 +546,13 @@ fn default_permissions_profile_populates_runtime_sandbox_policy() -> std::io::Re
|
||||
},
|
||||
]),
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.runtime_permission_profile(),
|
||||
PermissionProfile::from_runtime_permissions(
|
||||
&config.permissions.file_system_sandbox_policy,
|
||||
config.permissions.network_sandbox_policy,
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.sandbox_policy.get(),
|
||||
&SandboxPolicy::WorkspaceWrite {
|
||||
@@ -1164,6 +1172,14 @@ exclude_slash_tmp = true
|
||||
NetworkSandboxPolicy::from(sandbox_policy),
|
||||
"case `{name}` should preserve network semantics from legacy config"
|
||||
);
|
||||
assert_eq!(
|
||||
config.permissions.runtime_permission_profile(),
|
||||
PermissionProfile::from_runtime_permissions(
|
||||
&config.permissions.file_system_sandbox_policy,
|
||||
config.permissions.network_sandbox_policy,
|
||||
),
|
||||
"case `{name}` should populate canonical permission profile from runtime policies"
|
||||
);
|
||||
assert_eq!(
|
||||
config
|
||||
.permissions
|
||||
|
||||
@@ -78,6 +78,8 @@ use codex_protocol::config_types::WebSearchConfig;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use codex_protocol::config_types::WebSearchToolConfig;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
#[cfg(test)]
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::permissions::FileSystemSandboxPolicy;
|
||||
@@ -212,6 +214,16 @@ pub struct Permissions {
|
||||
pub windows_sandbox_private_desktop: bool,
|
||||
}
|
||||
|
||||
impl Permissions {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn runtime_permission_profile(&self) -> PermissionProfile {
|
||||
PermissionProfile::from_runtime_permissions(
|
||||
&self.file_system_sandbox_policy,
|
||||
self.network_sandbox_policy,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Application configuration loaded from disk and merged with overrides.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Config {
|
||||
|
||||
@@ -80,10 +80,10 @@ fn write_permissions_for_paths(
|
||||
.ok()?;
|
||||
|
||||
let permissions = (!write_paths.is_empty()).then_some(PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(write_paths),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(write_paths),
|
||||
)),
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
|
||||
@@ -74,7 +74,13 @@ fn write_permissions_for_paths_keep_dirs_outside_workspace_root() {
|
||||
.expect("outside dir should be absolute");
|
||||
|
||||
assert_eq!(
|
||||
permissions.and_then(|profile| profile.file_system.and_then(|fs| fs.write)),
|
||||
Some(vec![expected_outside])
|
||||
permissions,
|
||||
Some(PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![expected_outside]),
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ mod tool_suggest;
|
||||
pub(crate) mod unified_exec;
|
||||
mod view_image;
|
||||
|
||||
use codex_sandboxing::policy_transforms::intersect_permission_profiles;
|
||||
use codex_sandboxing::policy_transforms::merge_permission_profiles;
|
||||
use codex_sandboxing::policy_transforms::normalize_additional_permissions;
|
||||
use codex_sandboxing::policy_transforms::permission_profile_is_preapproved;
|
||||
use codex_utils_absolute_path::AbsolutePathBufGuard;
|
||||
pub use plan::PLAN_TOOL;
|
||||
use serde::Deserialize;
|
||||
@@ -195,13 +195,18 @@ pub(super) async fn apply_granted_turn_permissions(
|
||||
additional_permissions.as_ref(),
|
||||
granted_permissions.as_ref(),
|
||||
);
|
||||
let permissions_preapproved = match (effective_permissions.as_ref(), granted_permissions) {
|
||||
(Some(effective_permissions), Some(granted_permissions)) => {
|
||||
intersect_permission_profiles(effective_permissions.clone(), granted_permissions)
|
||||
== *effective_permissions
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
let session_cwd = session.cwd().await;
|
||||
let permissions_preapproved =
|
||||
match (effective_permissions.as_ref(), granted_permissions.as_ref()) {
|
||||
(Some(effective_permissions), Some(granted_permissions)) => {
|
||||
permission_profile_is_preapproved(
|
||||
effective_permissions,
|
||||
granted_permissions,
|
||||
session_cwd.as_path(),
|
||||
)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let sandbox_permissions =
|
||||
if effective_permissions.is_some() && !sandbox_permissions.uses_additional_permissions() {
|
||||
@@ -243,12 +248,12 @@ mod tests {
|
||||
|
||||
fn file_system_permissions(path: &std::path::Path) -> PermissionProfile {
|
||||
PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: None,
|
||||
write: Some(vec![
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
/*read*/ None,
|
||||
Some(vec![
|
||||
AbsolutePathBuf::from_absolute_path(path).expect("absolute path"),
|
||||
]),
|
||||
}),
|
||||
)),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,10 +186,10 @@ fn exec_command_args_resolve_relative_additional_permissions_against_workdir() -
|
||||
assert_eq!(
|
||||
args.additional_permissions,
|
||||
Some(PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: None,
|
||||
write: Some(vec![AbsolutePathBuf::try_from(expected_write)?]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
/*read*/ None,
|
||||
Some(vec![AbsolutePathBuf::try_from(expected_write)?]),
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
);
|
||||
|
||||
@@ -250,12 +250,12 @@ fn map_exec_result_preserves_stdout_and_stderr() {
|
||||
#[test]
|
||||
fn shell_request_escalation_execution_is_explicit() {
|
||||
let requested_permissions = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: None,
|
||||
write: Some(vec![
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
/*read*/ None,
|
||||
Some(vec![
|
||||
AbsolutePathBuf::from_absolute_path("/tmp/output").unwrap(),
|
||||
]),
|
||||
}),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
let sandbox_policy = SandboxPolicy::WorkspaceWrite {
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_features::Feature;
|
||||
use codex_protocol::config_types::ApprovalsReviewer;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExecApprovalRequestEvent;
|
||||
@@ -293,20 +294,20 @@ fn workspace_write_excluding_tmp() -> SandboxPolicy {
|
||||
|
||||
fn requested_directory_write_permissions(path: &Path) -> RequestPermissionProfile {
|
||||
RequestPermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![absolute_path(path)]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![absolute_path(path)]),
|
||||
)),
|
||||
..RequestPermissionProfile::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn normalized_directory_write_permissions(path: &Path) -> Result<RequestPermissionProfile> {
|
||||
Ok(RequestPermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![AbsolutePathBuf::try_from(path.canonicalize()?)?]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![AbsolutePathBuf::try_from(path.canonicalize()?)?]),
|
||||
)),
|
||||
..RequestPermissionProfile::default()
|
||||
})
|
||||
}
|
||||
@@ -343,10 +344,10 @@ async fn with_additional_permissions_requires_approval_under_on_request() -> Res
|
||||
let call_id = "request_permissions_skip_approval";
|
||||
let command = "touch requested-dir/requested-but-unused.txt";
|
||||
let requested_permissions = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![absolute_path(&requested_dir_canonical)]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![absolute_path(&requested_dir_canonical)]),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
let event = shell_event_with_request_permissions(call_id, command, &requested_permissions)?;
|
||||
@@ -521,10 +522,10 @@ async fn relative_additional_permissions_resolve_against_tool_workdir() -> Resul
|
||||
let call_id = "request_permissions_relative_workdir";
|
||||
let command = "touch relative-write.txt";
|
||||
let expected_permissions = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: None,
|
||||
write: Some(vec![absolute_path(&nested_dir_canonical)]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
/*read*/ None,
|
||||
Some(vec![absolute_path(&nested_dir_canonical)]),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
let event = shell_event_with_raw_request_permissions(
|
||||
@@ -624,10 +625,10 @@ async fn read_only_with_additional_permissions_does_not_widen_to_unrequested_cwd
|
||||
"cwd-widened", unrequested_write, unrequested_write
|
||||
);
|
||||
let requested_permissions = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![absolute_path(&requested_write)]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![absolute_path(&requested_write)]),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
let event = shell_event_with_request_permissions(call_id, &command, &requested_permissions)?;
|
||||
@@ -725,10 +726,10 @@ async fn read_only_with_additional_permissions_does_not_widen_to_unrequested_tmp
|
||||
"tmp-widened", tmp_write, tmp_write
|
||||
);
|
||||
let requested_permissions = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![absolute_path(&requested_write)]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![absolute_path(&requested_write)]),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
let event = shell_event_with_request_permissions(call_id, &command, &requested_permissions)?;
|
||||
@@ -824,19 +825,19 @@ async fn workspace_write_with_additional_permissions_can_write_outside_cwd() ->
|
||||
"outside-cwd-ok", outside_write, outside_write
|
||||
);
|
||||
let requested_permissions = RequestPermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![absolute_path(outside_dir.path())]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![absolute_path(outside_dir.path())]),
|
||||
)),
|
||||
..RequestPermissionProfile::default()
|
||||
};
|
||||
let normalized_requested_permissions = RequestPermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![AbsolutePathBuf::try_from(
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![AbsolutePathBuf::try_from(
|
||||
outside_dir.path().canonicalize()?,
|
||||
)?]),
|
||||
}),
|
||||
)),
|
||||
..RequestPermissionProfile::default()
|
||||
};
|
||||
let event = shell_event_with_request_permissions(call_id, &command, &requested_permissions)?;
|
||||
@@ -926,19 +927,19 @@ async fn with_additional_permissions_denied_approval_blocks_execution() -> Resul
|
||||
"should-not-write", outside_write, outside_write
|
||||
);
|
||||
let requested_permissions = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![absolute_path(outside_dir.path())]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![absolute_path(outside_dir.path())]),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
let normalized_requested_permissions = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![AbsolutePathBuf::try_from(
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![AbsolutePathBuf::try_from(
|
||||
outside_dir.path().canonicalize()?,
|
||||
)?]),
|
||||
}),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
let event = shell_event_with_request_permissions(call_id, &command, &requested_permissions)?;
|
||||
@@ -1028,19 +1029,19 @@ async fn request_permissions_grants_apply_to_later_exec_command_calls() -> Resul
|
||||
"sticky-grant-ok", outside_write, outside_write
|
||||
);
|
||||
let requested_permissions = RequestPermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![absolute_path(outside_dir.path())]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![absolute_path(outside_dir.path())]),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
let normalized_requested_permissions = RequestPermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![AbsolutePathBuf::try_from(
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![AbsolutePathBuf::try_from(
|
||||
outside_dir.path().canonicalize()?,
|
||||
)?]),
|
||||
}),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
let responses = mount_sse_sequence(
|
||||
@@ -1492,35 +1493,35 @@ async fn partial_request_permissions_grants_do_not_preapprove_new_permissions()
|
||||
);
|
||||
|
||||
let requested_permissions = RequestPermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![
|
||||
absolute_path(first_dir.path()),
|
||||
absolute_path(second_dir.path()),
|
||||
]),
|
||||
}),
|
||||
)),
|
||||
..RequestPermissionProfile::default()
|
||||
};
|
||||
let normalized_requested_permissions = RequestPermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![
|
||||
AbsolutePathBuf::try_from(first_dir.path().canonicalize()?)?,
|
||||
AbsolutePathBuf::try_from(second_dir.path().canonicalize()?)?,
|
||||
]),
|
||||
}),
|
||||
)),
|
||||
..RequestPermissionProfile::default()
|
||||
};
|
||||
let granted_permissions = normalized_directory_write_permissions(first_dir.path())?;
|
||||
let second_dir_permissions = requested_directory_write_permissions(second_dir.path());
|
||||
let merged_permissions = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![
|
||||
AbsolutePathBuf::try_from(first_dir.path().canonicalize()?)?,
|
||||
AbsolutePathBuf::try_from(second_dir.path().canonicalize()?)?,
|
||||
]),
|
||||
}),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -1584,16 +1585,31 @@ async fn partial_request_permissions_grants_do_not_preapprove_new_permissions()
|
||||
let approval_file_system = approval_permissions
|
||||
.file_system
|
||||
.unwrap_or_else(|| panic!("expected filesystem permissions"));
|
||||
assert!(approval_file_system.read.as_ref().is_none_or(Vec::is_empty));
|
||||
|
||||
let mut approval_writes = approval_file_system.write.unwrap_or_default();
|
||||
assert_eq!(
|
||||
approval_file_system
|
||||
.explicit_path_entries()
|
||||
.filter(|(_, access)| *access == FileSystemAccessMode::Read)
|
||||
.count(),
|
||||
0
|
||||
);
|
||||
|
||||
let mut approval_writes = approval_file_system
|
||||
.explicit_path_entries()
|
||||
.filter_map(|(path, access)| {
|
||||
(access == FileSystemAccessMode::Write).then_some(path.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
approval_writes.sort_by_key(|path| path.display().to_string());
|
||||
|
||||
let mut expected_writes = merged_permissions
|
||||
.file_system
|
||||
.unwrap_or_else(|| panic!("expected merged filesystem permissions"))
|
||||
.write
|
||||
.unwrap_or_default();
|
||||
.explicit_path_entries()
|
||||
.filter_map(|(path, access)| {
|
||||
(access == FileSystemAccessMode::Write).then_some(path.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
expected_writes.sort_by_key(|path| path.display().to_string());
|
||||
|
||||
assert_eq!(approval_writes, expected_writes);
|
||||
|
||||
@@ -81,20 +81,20 @@ fn workspace_write_excluding_tmp() -> SandboxPolicy {
|
||||
|
||||
fn requested_directory_write_permissions(path: &Path) -> RequestPermissionProfile {
|
||||
RequestPermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![absolute_path(path)]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![absolute_path(path)]),
|
||||
)),
|
||||
..RequestPermissionProfile::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn normalized_directory_write_permissions(path: &Path) -> Result<RequestPermissionProfile> {
|
||||
Ok(RequestPermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![AbsolutePathBuf::try_from(path.canonicalize()?)?]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![AbsolutePathBuf::try_from(path.canonicalize()?)?]),
|
||||
)),
|
||||
..RequestPermissionProfile::default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
@@ -14,6 +15,12 @@ use ts_rs::TS;
|
||||
use crate::config_types::ApprovalsReviewer;
|
||||
use crate::config_types::CollaborationMode;
|
||||
use crate::config_types::SandboxMode;
|
||||
use crate::permissions::FileSystemAccessMode;
|
||||
use crate::permissions::FileSystemPath;
|
||||
use crate::permissions::FileSystemSandboxEntry;
|
||||
use crate::permissions::FileSystemSandboxPolicy;
|
||||
use crate::permissions::FileSystemSpecialPath;
|
||||
use crate::permissions::NetworkSandboxPolicy;
|
||||
use crate::protocol::AskForApproval;
|
||||
use crate::protocol::COLLABORATION_MODE_CLOSE_TAG;
|
||||
use crate::protocol::COLLABORATION_MODE_OPEN_TAG;
|
||||
@@ -80,15 +87,119 @@ impl SandboxPermissions {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, JsonSchema, TS)]
|
||||
pub struct FileSystemPermissions {
|
||||
pub read: Option<Vec<AbsolutePathBuf>>,
|
||||
pub write: Option<Vec<AbsolutePathBuf>>,
|
||||
pub entries: Vec<FileSystemSandboxEntry>,
|
||||
}
|
||||
|
||||
impl FileSystemPermissions {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.read.is_none() && self.write.is_none()
|
||||
self.entries.is_empty()
|
||||
}
|
||||
|
||||
pub fn from_read_write_roots(
|
||||
read: Option<Vec<AbsolutePathBuf>>,
|
||||
write: Option<Vec<AbsolutePathBuf>>,
|
||||
) -> Self {
|
||||
let mut entries = Vec::new();
|
||||
if let Some(read) = read {
|
||||
entries.extend(read.into_iter().map(|path| FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path },
|
||||
access: FileSystemAccessMode::Read,
|
||||
}));
|
||||
}
|
||||
if let Some(write) = write {
|
||||
entries.extend(write.into_iter().map(|path| FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path },
|
||||
access: FileSystemAccessMode::Write,
|
||||
}));
|
||||
}
|
||||
Self { entries }
|
||||
}
|
||||
|
||||
pub fn explicit_path_entries(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (&AbsolutePathBuf, FileSystemAccessMode)> {
|
||||
self.entries.iter().filter_map(|entry| match &entry.path {
|
||||
FileSystemPath::Path { path } => Some((path, entry.access)),
|
||||
FileSystemPath::Special { .. } => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn as_legacy_permissions(&self) -> Option<LegacyFileSystemPermissions> {
|
||||
let mut read = Vec::new();
|
||||
let mut write = Vec::new();
|
||||
|
||||
for entry in &self.entries {
|
||||
let FileSystemPath::Path { path } = &entry.path else {
|
||||
return None;
|
||||
};
|
||||
match entry.access {
|
||||
FileSystemAccessMode::Read => read.push(path.clone()),
|
||||
FileSystemAccessMode::Write => write.push(path.clone()),
|
||||
FileSystemAccessMode::None => return None,
|
||||
}
|
||||
}
|
||||
|
||||
Some(LegacyFileSystemPermissions {
|
||||
read: (!read.is_empty()).then_some(read),
|
||||
write: (!write.is_empty()).then_some(write),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct LegacyFileSystemPermissions {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
read: Option<Vec<AbsolutePathBuf>>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
write: Option<Vec<AbsolutePathBuf>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct CanonicalFileSystemPermissions {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
entries: Vec<FileSystemSandboxEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum FileSystemPermissionsDe {
|
||||
Canonical(CanonicalFileSystemPermissions),
|
||||
Legacy(LegacyFileSystemPermissions),
|
||||
}
|
||||
|
||||
impl Serialize for FileSystemPermissions {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
if let Some(legacy) = self.as_legacy_permissions() {
|
||||
legacy.serialize(serializer)
|
||||
} else {
|
||||
CanonicalFileSystemPermissions {
|
||||
entries: self.entries.clone(),
|
||||
}
|
||||
.serialize(serializer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for FileSystemPermissions {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
match FileSystemPermissionsDe::deserialize(deserializer)? {
|
||||
FileSystemPermissionsDe::Canonical(CanonicalFileSystemPermissions { entries }) => {
|
||||
Ok(Self { entries })
|
||||
}
|
||||
FileSystemPermissionsDe::Legacy(LegacyFileSystemPermissions { read, write }) => {
|
||||
Ok(Self::from_read_write_roots(read, write))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +224,80 @@ impl PermissionProfile {
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.network.is_none() && self.file_system.is_none()
|
||||
}
|
||||
|
||||
pub fn from_runtime_permissions(
|
||||
file_system_sandbox_policy: &FileSystemSandboxPolicy,
|
||||
network_sandbox_policy: NetworkSandboxPolicy,
|
||||
) -> Self {
|
||||
Self {
|
||||
network: Some(network_sandbox_policy.into()),
|
||||
file_system: Some(file_system_sandbox_policy.into()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_legacy_sandbox_policy(sandbox_policy: &SandboxPolicy, cwd: &Path) -> Self {
|
||||
Self::from_runtime_permissions(
|
||||
&FileSystemSandboxPolicy::from_legacy_sandbox_policy(sandbox_policy, cwd),
|
||||
NetworkSandboxPolicy::from(sandbox_policy),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn file_system_sandbox_policy(&self) -> FileSystemSandboxPolicy {
|
||||
self.file_system.as_ref().map_or_else(
|
||||
|| FileSystemSandboxPolicy::restricted(Vec::new()),
|
||||
FileSystemSandboxPolicy::from,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn network_sandbox_policy(&self) -> NetworkSandboxPolicy {
|
||||
if self
|
||||
.network
|
||||
.as_ref()
|
||||
.and_then(|network| network.enabled)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
NetworkSandboxPolicy::Enabled
|
||||
} else {
|
||||
NetworkSandboxPolicy::Restricted
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_legacy_sandbox_policy(&self, cwd: &Path) -> io::Result<SandboxPolicy> {
|
||||
self.file_system_sandbox_policy()
|
||||
.to_legacy_sandbox_policy(self.network_sandbox_policy(), cwd)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NetworkSandboxPolicy> for NetworkPermissions {
|
||||
fn from(value: NetworkSandboxPolicy) -> Self {
|
||||
Self {
|
||||
enabled: Some(value.is_enabled()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&FileSystemSandboxPolicy> for FileSystemPermissions {
|
||||
fn from(value: &FileSystemSandboxPolicy) -> Self {
|
||||
let entries = match value.kind {
|
||||
crate::permissions::FileSystemSandboxKind::Restricted => value.entries.clone(),
|
||||
crate::permissions::FileSystemSandboxKind::Unrestricted
|
||||
| crate::permissions::FileSystemSandboxKind::ExternalSandbox => {
|
||||
vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
}]
|
||||
}
|
||||
};
|
||||
Self { entries }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&FileSystemPermissions> for FileSystemSandboxPolicy {
|
||||
fn from(value: &FileSystemPermissions) -> Self {
|
||||
FileSystemSandboxPolicy::restricted(value.entries.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, JsonSchema, TS)]
|
||||
@@ -1436,6 +1621,10 @@ mod tests {
|
||||
use std::path::PathBuf;
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn absolute_path(path: &str) -> AbsolutePathBuf {
|
||||
AbsolutePathBuf::from_absolute_path(path).expect("path must be absolute")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_permissions_helpers_match_documented_semantics() {
|
||||
let cases = [
|
||||
@@ -1546,6 +1735,58 @@ mod tests {
|
||||
assert_eq!(permission_profile.is_empty(), false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_system_permissions_deserialize_legacy_read_write_shape() {
|
||||
let file_system = serde_json::from_value::<FileSystemPermissions>(serde_json::json!({
|
||||
"read": [absolute_path("/tmp/read-only")],
|
||||
"write": [absolute_path("/tmp/read-write")],
|
||||
}))
|
||||
.expect("deserialize legacy filesystem permissions");
|
||||
|
||||
assert_eq!(
|
||||
file_system,
|
||||
FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![absolute_path("/tmp/read-only")]),
|
||||
Some(vec![absolute_path("/tmp/read-write")]),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_system_permissions_serialize_explicit_paths_as_legacy_read_write_shape() {
|
||||
let file_system = FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![absolute_path("/tmp/read-only")]),
|
||||
Some(vec![absolute_path("/tmp/read-write")]),
|
||||
);
|
||||
|
||||
let value = serde_json::to_value(file_system).expect("serialize filesystem permissions");
|
||||
|
||||
assert_eq!(
|
||||
value,
|
||||
serde_json::json!({
|
||||
"read": [absolute_path("/tmp/read-only")],
|
||||
"write": [absolute_path("/tmp/read-write")],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_system_permissions_round_trip_special_entries_through_canonical_shape() {
|
||||
let file_system = FileSystemPermissions {
|
||||
entries: vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::CurrentWorkingDirectory,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
}],
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(&file_system).expect("serialize filesystem permissions");
|
||||
let reparsed = serde_json::from_value::<FileSystemPermissions>(value)
|
||||
.expect("deserialize filesystem permissions");
|
||||
|
||||
assert_eq!(reparsed, file_system);
|
||||
}
|
||||
#[test]
|
||||
fn convert_mcp_content_to_items_builds_data_urls_when_missing_prefix() {
|
||||
let contents = vec![serde_json::json!({
|
||||
|
||||
@@ -45,6 +45,7 @@ impl NetworkSandboxPolicy {
|
||||
Copy,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Hash,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
Serialize,
|
||||
@@ -71,7 +72,7 @@ impl FileSystemAccessMode {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
#[ts(tag = "kind")]
|
||||
pub enum FileSystemSpecialPath {
|
||||
@@ -114,7 +115,7 @@ impl FileSystemSpecialPath {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)]
|
||||
pub struct FileSystemSandboxEntry {
|
||||
pub path: FileSystemPath,
|
||||
pub access: FileSystemAccessMode,
|
||||
@@ -155,7 +156,7 @@ struct FileSystemSemanticSignature {
|
||||
unreadable_roots: Vec<AbsolutePathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
#[ts(tag = "type")]
|
||||
pub enum FileSystemPath {
|
||||
@@ -369,6 +370,50 @@ impl FileSystemSandboxPolicy {
|
||||
self.resolve_access_with_cwd(path, cwd).can_write()
|
||||
}
|
||||
|
||||
pub fn covers_entry_with_cwd(&self, entry: &FileSystemSandboxEntry, cwd: &Path) -> bool {
|
||||
match &entry.path {
|
||||
FileSystemPath::Path { path } => match entry.access {
|
||||
FileSystemAccessMode::Read => self.can_read_path_with_cwd(path.as_path(), cwd),
|
||||
FileSystemAccessMode::Write => self.can_write_path_with_cwd(path.as_path(), cwd),
|
||||
FileSystemAccessMode::None => {
|
||||
self.resolve_access_with_cwd(path.as_path(), cwd) == FileSystemAccessMode::None
|
||||
}
|
||||
},
|
||||
FileSystemPath::Special { value } => match value {
|
||||
FileSystemSpecialPath::Root => match entry.access {
|
||||
FileSystemAccessMode::Read => self.has_full_disk_read_access(),
|
||||
FileSystemAccessMode::Write => self.has_full_disk_write_access(),
|
||||
FileSystemAccessMode::None => {
|
||||
!self.has_full_disk_read_access() && !self.has_full_disk_write_access()
|
||||
}
|
||||
},
|
||||
FileSystemSpecialPath::Minimal => match entry.access {
|
||||
FileSystemAccessMode::Read => {
|
||||
self.has_full_disk_read_access() || self.include_platform_defaults()
|
||||
}
|
||||
FileSystemAccessMode::Write => self.has_full_disk_write_access(),
|
||||
FileSystemAccessMode::None => {
|
||||
!self.has_full_disk_read_access() && !self.include_platform_defaults()
|
||||
}
|
||||
},
|
||||
_ => resolve_file_system_special_path(
|
||||
value,
|
||||
AbsolutePathBuf::from_absolute_path(cwd).ok().as_ref(),
|
||||
)
|
||||
.is_some_and(|path| match entry.access {
|
||||
FileSystemAccessMode::Read => self.can_read_path_with_cwd(path.as_path(), cwd),
|
||||
FileSystemAccessMode::Write => {
|
||||
self.can_write_path_with_cwd(path.as_path(), cwd)
|
||||
}
|
||||
FileSystemAccessMode::None => {
|
||||
self.resolve_access_with_cwd(path.as_path(), cwd)
|
||||
== FileSystemAccessMode::None
|
||||
}
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_additional_readable_roots(
|
||||
mut self,
|
||||
cwd: &Path,
|
||||
|
||||
@@ -130,10 +130,10 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() {
|
||||
network: Some(NetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![path]),
|
||||
write: Some(Vec::new()),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![path]),
|
||||
Some(Vec::new()),
|
||||
)),
|
||||
}),
|
||||
},
|
||||
policy: &SandboxPolicy::ExternalSandbox {
|
||||
@@ -183,10 +183,10 @@ fn transform_additional_permissions_preserves_denied_entries() {
|
||||
cwd: cwd.clone(),
|
||||
env: HashMap::new(),
|
||||
additional_permissions: Some(PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: None,
|
||||
write: Some(vec![allowed_path.clone()]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
/*read*/ None,
|
||||
Some(vec![allowed_path.clone()]),
|
||||
)),
|
||||
..Default::default()
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::NetworkPermissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSandboxKind;
|
||||
@@ -13,6 +12,7 @@ use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use dunce::canonicalize;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EffectiveSandboxPermissions {
|
||||
@@ -45,13 +45,26 @@ pub fn normalize_additional_permissions(
|
||||
let file_system = additional_permissions
|
||||
.file_system
|
||||
.map(|file_system| {
|
||||
let read = file_system
|
||||
.read
|
||||
.map(|paths| normalize_permission_paths(paths, "file_system.read"));
|
||||
let write = file_system
|
||||
.write
|
||||
.map(|paths| normalize_permission_paths(paths, "file_system.write"));
|
||||
FileSystemPermissions { read, write }
|
||||
let mut entries = Vec::with_capacity(file_system.entries.len());
|
||||
for entry in file_system.entries {
|
||||
let path = match entry.path {
|
||||
FileSystemPath::Path { path } => FileSystemPath::Path {
|
||||
path: canonicalize(path.as_path())
|
||||
.ok()
|
||||
.and_then(|path| AbsolutePathBuf::from_absolute_path(path).ok())
|
||||
.unwrap_or(path),
|
||||
},
|
||||
FileSystemPath::Special { value } => FileSystemPath::Special { value },
|
||||
};
|
||||
let normalized_entry = FileSystemSandboxEntry {
|
||||
path,
|
||||
access: entry.access,
|
||||
};
|
||||
if !entries.contains(&normalized_entry) {
|
||||
entries.push(normalized_entry);
|
||||
}
|
||||
}
|
||||
FileSystemPermissions { entries }
|
||||
})
|
||||
.filter(|file_system| !file_system.is_empty());
|
||||
Ok(PermissionProfile {
|
||||
@@ -89,8 +102,7 @@ pub fn merge_permission_profiles(
|
||||
};
|
||||
let file_system = match (base.file_system.as_ref(), permissions.file_system.as_ref()) {
|
||||
(Some(base), Some(permissions)) => Some(FileSystemPermissions {
|
||||
read: merge_permission_paths(base.read.as_ref(), permissions.read.as_ref()),
|
||||
write: merge_permission_paths(base.write.as_ref(), permissions.write.as_ref()),
|
||||
entries: merge_permission_entries(&base.entries, &permissions.entries),
|
||||
})
|
||||
.filter(|file_system| !file_system.is_empty()),
|
||||
(Some(base), None) => Some(base.clone()),
|
||||
@@ -115,12 +127,13 @@ pub fn intersect_permission_profiles(
|
||||
let file_system = requested
|
||||
.file_system
|
||||
.map(|requested_file_system| {
|
||||
let granted_file_system = granted.file_system.unwrap_or_default();
|
||||
let read =
|
||||
intersect_permission_paths(requested_file_system.read, granted_file_system.read);
|
||||
let write =
|
||||
intersect_permission_paths(requested_file_system.write, granted_file_system.write);
|
||||
FileSystemPermissions { read, write }
|
||||
let granted_entries = granted.file_system.unwrap_or_default().entries;
|
||||
let entries = requested_file_system
|
||||
.entries
|
||||
.into_iter()
|
||||
.filter(|entry| granted_entries.contains(entry))
|
||||
.collect();
|
||||
FileSystemPermissions { entries }
|
||||
})
|
||||
.filter(|file_system| !file_system.is_empty());
|
||||
let network = match (requested.network, granted.network) {
|
||||
@@ -143,67 +156,46 @@ pub fn intersect_permission_profiles(
|
||||
}
|
||||
}
|
||||
|
||||
fn intersect_permission_paths(
|
||||
requested: Option<Vec<AbsolutePathBuf>>,
|
||||
granted: Option<Vec<AbsolutePathBuf>>,
|
||||
) -> Option<Vec<AbsolutePathBuf>> {
|
||||
requested.and_then(|requested_paths| {
|
||||
if requested_paths.is_empty() {
|
||||
return granted.map(|_| Vec::new());
|
||||
pub fn permission_profile_is_preapproved(
|
||||
requested: &PermissionProfile,
|
||||
granted: &PermissionProfile,
|
||||
cwd: &Path,
|
||||
) -> bool {
|
||||
let network_preapproved = !requested
|
||||
.network
|
||||
.as_ref()
|
||||
.and_then(|network| network.enabled)
|
||||
.unwrap_or(false)
|
||||
|| granted
|
||||
.network
|
||||
.as_ref()
|
||||
.and_then(|network| network.enabled)
|
||||
.unwrap_or(false);
|
||||
let file_system_preapproved = match requested.file_system.as_ref() {
|
||||
Some(requested_file_system) => {
|
||||
let granted_policy = granted.file_system_sandbox_policy();
|
||||
requested_file_system
|
||||
.entries
|
||||
.iter()
|
||||
.all(|entry| granted_policy.covers_entry_with_cwd(entry, cwd))
|
||||
}
|
||||
None => true,
|
||||
};
|
||||
|
||||
let granted_paths = granted.unwrap_or_default();
|
||||
Some(
|
||||
requested_paths
|
||||
.into_iter()
|
||||
.filter(|path| granted_paths.contains(path))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
.filter(|paths| !paths.is_empty())
|
||||
})
|
||||
network_preapproved && file_system_preapproved
|
||||
}
|
||||
|
||||
fn normalize_permission_paths(
|
||||
paths: Vec<AbsolutePathBuf>,
|
||||
_permission_kind: &str,
|
||||
) -> Vec<AbsolutePathBuf> {
|
||||
let mut out = Vec::with_capacity(paths.len());
|
||||
let mut seen = HashSet::new();
|
||||
|
||||
for path in paths {
|
||||
let canonicalized = canonicalize(path.as_path())
|
||||
.ok()
|
||||
.and_then(|path| AbsolutePathBuf::from_absolute_path(path).ok())
|
||||
.unwrap_or(path);
|
||||
if seen.insert(canonicalized.clone()) {
|
||||
out.push(canonicalized);
|
||||
fn merge_permission_entries(
|
||||
base: &[FileSystemSandboxEntry],
|
||||
permissions: &[FileSystemSandboxEntry],
|
||||
) -> Vec<FileSystemSandboxEntry> {
|
||||
let mut merged = Vec::with_capacity(base.len() + permissions.len());
|
||||
for entry in base.iter().chain(permissions.iter()) {
|
||||
if !merged.contains(entry) {
|
||||
merged.push(entry.clone());
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn merge_permission_paths(
|
||||
base: Option<&Vec<AbsolutePathBuf>>,
|
||||
permissions: Option<&Vec<AbsolutePathBuf>>,
|
||||
) -> Option<Vec<AbsolutePathBuf>> {
|
||||
match (base, permissions) {
|
||||
(Some(base), Some(permissions)) => {
|
||||
let mut merged = Vec::with_capacity(base.len() + permissions.len());
|
||||
let mut seen = HashSet::with_capacity(base.len() + permissions.len());
|
||||
|
||||
for path in base.iter().chain(permissions.iter()) {
|
||||
if seen.insert(path.clone()) {
|
||||
merged.push(path.clone());
|
||||
}
|
||||
}
|
||||
|
||||
Some(merged).filter(|paths| !paths.is_empty())
|
||||
}
|
||||
(Some(base), None) => Some(base.clone()),
|
||||
(None, Some(permissions)) => Some(permissions.clone()),
|
||||
(None, None) => None,
|
||||
}
|
||||
merged
|
||||
}
|
||||
|
||||
fn dedup_absolute_paths(paths: Vec<AbsolutePathBuf>) -> Vec<AbsolutePathBuf> {
|
||||
@@ -217,7 +209,7 @@ fn dedup_absolute_paths(paths: Vec<AbsolutePathBuf>) -> Vec<AbsolutePathBuf> {
|
||||
out
|
||||
}
|
||||
|
||||
fn additional_permission_roots(
|
||||
fn additional_permission_explicit_path_roots(
|
||||
additional_permissions: &PermissionProfile,
|
||||
) -> (Vec<AbsolutePathBuf>, Vec<AbsolutePathBuf>) {
|
||||
(
|
||||
@@ -225,14 +217,24 @@ fn additional_permission_roots(
|
||||
additional_permissions
|
||||
.file_system
|
||||
.as_ref()
|
||||
.and_then(|file_system| file_system.read.clone())
|
||||
.map(|file_system| {
|
||||
file_system
|
||||
.explicit_path_entries()
|
||||
.filter_map(|(path, access)| access.can_read().then_some(path.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
dedup_absolute_paths(
|
||||
additional_permissions
|
||||
.file_system
|
||||
.as_ref()
|
||||
.and_then(|file_system| file_system.write.clone())
|
||||
.map(|file_system| {
|
||||
file_system
|
||||
.explicit_path_entries()
|
||||
.filter_map(|(path, access)| access.can_write().then_some(path.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
),
|
||||
)
|
||||
@@ -240,28 +242,14 @@ fn additional_permission_roots(
|
||||
|
||||
fn merge_file_system_policy_with_additional_permissions(
|
||||
file_system_policy: &FileSystemSandboxPolicy,
|
||||
extra_reads: Vec<AbsolutePathBuf>,
|
||||
extra_writes: Vec<AbsolutePathBuf>,
|
||||
additional_permissions: &FileSystemPermissions,
|
||||
) -> FileSystemSandboxPolicy {
|
||||
match file_system_policy.kind {
|
||||
FileSystemSandboxKind::Restricted => {
|
||||
let mut merged_policy = file_system_policy.clone();
|
||||
for path in extra_reads {
|
||||
let entry = FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path },
|
||||
access: FileSystemAccessMode::Read,
|
||||
};
|
||||
if !merged_policy.entries.contains(&entry) {
|
||||
merged_policy.entries.push(entry);
|
||||
}
|
||||
}
|
||||
for path in extra_writes {
|
||||
let entry = FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path { path },
|
||||
access: FileSystemAccessMode::Write,
|
||||
};
|
||||
if !merged_policy.entries.contains(&entry) {
|
||||
merged_policy.entries.push(entry);
|
||||
for entry in &additional_permissions.entries {
|
||||
if !merged_policy.entries.contains(entry) {
|
||||
merged_policy.entries.push(entry.clone());
|
||||
}
|
||||
}
|
||||
merged_policy
|
||||
@@ -280,14 +268,15 @@ pub fn effective_file_system_sandbox_policy(
|
||||
return file_system_policy.clone();
|
||||
};
|
||||
|
||||
let (extra_reads, extra_writes) = additional_permission_roots(additional_permissions);
|
||||
if extra_reads.is_empty() && extra_writes.is_empty() {
|
||||
let Some(file_system_permissions) = additional_permissions.file_system.as_ref() else {
|
||||
return file_system_policy.clone();
|
||||
};
|
||||
if file_system_permissions.is_empty() {
|
||||
file_system_policy.clone()
|
||||
} else {
|
||||
merge_file_system_policy_with_additional_permissions(
|
||||
file_system_policy,
|
||||
extra_reads,
|
||||
extra_writes,
|
||||
file_system_permissions,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -347,7 +336,11 @@ fn sandbox_policy_with_additional_permissions(
|
||||
return sandbox_policy.clone();
|
||||
}
|
||||
|
||||
let (extra_reads, extra_writes) = additional_permission_roots(additional_permissions);
|
||||
// Legacy SandboxPolicy remains a best-effort projection during the
|
||||
// migration to PermissionProfile-backed thread permissions. The direct
|
||||
// filesystem sandbox policy carries the full permission shape.
|
||||
let (extra_reads, extra_writes) =
|
||||
additional_permission_explicit_path_roots(additional_permissions);
|
||||
|
||||
match sandbox_policy {
|
||||
SandboxPolicy::DangerFullAccess => SandboxPolicy::DangerFullAccess,
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::effective_file_system_sandbox_policy;
|
||||
use super::intersect_permission_profiles;
|
||||
use super::merge_file_system_policy_with_additional_permissions;
|
||||
use super::normalize_additional_permissions;
|
||||
use super::permission_profile_is_preapproved;
|
||||
use super::sandbox_policy_with_additional_permissions;
|
||||
use super::should_require_platform_sandbox;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
@@ -107,10 +108,10 @@ fn normalize_additional_permissions_preserves_network() {
|
||||
network: Some(NetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![path.clone()]),
|
||||
write: Some(vec![path.clone()]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![path.clone()]),
|
||||
Some(vec![path.clone()]),
|
||||
)),
|
||||
})
|
||||
.expect("permissions");
|
||||
|
||||
@@ -122,10 +123,10 @@ fn normalize_additional_permissions_preserves_network() {
|
||||
);
|
||||
assert_eq!(
|
||||
permissions.file_system,
|
||||
Some(FileSystemPermissions {
|
||||
read: Some(vec![path.clone()]),
|
||||
write: Some(vec![path]),
|
||||
})
|
||||
Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![path.clone()]),
|
||||
Some(vec![path]),
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -147,20 +148,20 @@ fn normalize_additional_permissions_canonicalizes_symlinked_write_paths() {
|
||||
.expect("absolute canonical write dir");
|
||||
|
||||
let permissions = normalize_additional_permissions(PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![link_write_dir]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![link_write_dir]),
|
||||
)),
|
||||
..Default::default()
|
||||
})
|
||||
.expect("permissions");
|
||||
|
||||
assert_eq!(
|
||||
permissions.file_system,
|
||||
Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![expected_write_dir]),
|
||||
})
|
||||
Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![expected_write_dir]),
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -168,50 +169,22 @@ fn normalize_additional_permissions_canonicalizes_symlinked_write_paths() {
|
||||
fn normalize_additional_permissions_drops_empty_nested_profiles() {
|
||||
let permissions = normalize_additional_permissions(PermissionProfile {
|
||||
network: Some(NetworkPermissions { enabled: None }),
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: None,
|
||||
write: None,
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::default()),
|
||||
})
|
||||
.expect("permissions");
|
||||
|
||||
assert_eq!(permissions, PermissionProfile::default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_permission_profiles_preserves_explicit_empty_requested_reads() {
|
||||
let temp_dir = TempDir::new().expect("create temp dir");
|
||||
let path = AbsolutePathBuf::from_absolute_path(
|
||||
canonicalize(temp_dir.path()).expect("canonicalize temp dir"),
|
||||
)
|
||||
.expect("absolute temp dir");
|
||||
let requested = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![path]),
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let granted = requested.clone();
|
||||
|
||||
assert_eq!(
|
||||
intersect_permission_profiles(requested.clone(), granted),
|
||||
requested
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_permission_profiles_drops_ungranted_nonempty_path_requests() {
|
||||
let temp_dir = TempDir::new().expect("create temp dir");
|
||||
let path = AbsolutePathBuf::from_absolute_path(
|
||||
canonicalize(temp_dir.path()).expect("canonicalize temp dir"),
|
||||
)
|
||||
.expect("absolute temp dir");
|
||||
let requested = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![path]),
|
||||
write: None,
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(Vec::from(["/tmp/requested"
|
||||
.try_into()
|
||||
.expect("absolute path")])),
|
||||
/*write*/ None,
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
@@ -222,24 +195,65 @@ fn intersect_permission_profiles_drops_ungranted_nonempty_path_requests() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intersect_permission_profiles_drops_explicit_empty_reads_without_grant() {
|
||||
fn permission_profile_is_preapproved_when_granted_parent_directory_covers_requested_child() {
|
||||
let temp_dir = TempDir::new().expect("create temp dir");
|
||||
let path = AbsolutePathBuf::from_absolute_path(
|
||||
let parent = AbsolutePathBuf::from_absolute_path(
|
||||
canonicalize(temp_dir.path()).expect("canonicalize temp dir"),
|
||||
)
|
||||
.expect("absolute temp dir");
|
||||
let child = AbsolutePathBuf::from_absolute_path(parent.as_path().join("child"))
|
||||
.expect("absolute child path");
|
||||
let granted = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
/*read*/ None,
|
||||
Some(vec![parent]),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
let requested = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![path]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
/*read*/ None,
|
||||
Some(vec![child]),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
intersect_permission_profiles(requested, PermissionProfile::default()),
|
||||
PermissionProfile::default()
|
||||
);
|
||||
assert!(permission_profile_is_preapproved(
|
||||
&requested,
|
||||
&granted,
|
||||
temp_dir.path(),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permission_profile_is_preapproved_when_granted_cwd_covers_requested_child() {
|
||||
let temp_dir = TempDir::new().expect("create temp dir");
|
||||
let child = AbsolutePathBuf::from_absolute_path(temp_dir.path().join("child"))
|
||||
.expect("absolute child path");
|
||||
let granted = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
entries: vec![FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::CurrentWorkingDirectory,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
}],
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let requested = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
/*read*/ None,
|
||||
Some(vec![child]),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
assert!(permission_profile_is_preapproved(
|
||||
&requested,
|
||||
&granted,
|
||||
temp_dir.path(),
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -261,10 +275,10 @@ fn read_only_additional_permissions_can_enable_network_without_writes() {
|
||||
network: Some(NetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![path.clone()]),
|
||||
write: Some(Vec::new()),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![path.clone()]),
|
||||
Some(Vec::new()),
|
||||
)),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -295,10 +309,10 @@ fn external_sandbox_additional_permissions_can_enable_network() {
|
||||
network: Some(NetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![path]),
|
||||
write: Some(Vec::new()),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![path]),
|
||||
Some(Vec::new()),
|
||||
)),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -334,8 +348,10 @@ fn merge_file_system_policy_with_additional_permissions_preserves_unreadable_roo
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
]),
|
||||
vec![allowed_path.clone()],
|
||||
Vec::new(),
|
||||
&FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![allowed_path.clone()]),
|
||||
Some(Vec::new()),
|
||||
),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -405,10 +421,10 @@ fn effective_file_system_sandbox_policy_merges_additional_write_roots() {
|
||||
},
|
||||
]);
|
||||
let additional_permissions = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![]),
|
||||
write: Some(vec![allowed_path.clone()]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![]),
|
||||
Some(vec![allowed_path.clone()]),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,9 @@ use codex_features::Features;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::mcp::RequestId;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::ElicitationAction;
|
||||
use codex_protocol::protocol::FileChange;
|
||||
use codex_protocol::protocol::NetworkApprovalContext;
|
||||
@@ -743,22 +746,20 @@ pub(crate) fn format_additional_permissions_rule(
|
||||
parts.push("network".to_string());
|
||||
}
|
||||
if let Some(file_system) = additional_permissions.file_system.as_ref() {
|
||||
if let Some(read) = file_system.read.as_ref() {
|
||||
let reads = read
|
||||
.iter()
|
||||
.map(|path| format!("`{}`", path.display()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
if let Some(reads) = format_file_system_permissions(file_system, FileSystemAccessMode::Read)
|
||||
{
|
||||
parts.push(format!("read {reads}"));
|
||||
}
|
||||
if let Some(write) = file_system.write.as_ref() {
|
||||
let writes = write
|
||||
.iter()
|
||||
.map(|path| format!("`{}`", path.display()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
if let Some(writes) =
|
||||
format_file_system_permissions(file_system, FileSystemAccessMode::Write)
|
||||
{
|
||||
parts.push(format!("write {writes}"));
|
||||
}
|
||||
if let Some(denies) =
|
||||
format_file_system_permissions(file_system, FileSystemAccessMode::None)
|
||||
{
|
||||
parts.push(format!("deny {denies}"));
|
||||
}
|
||||
}
|
||||
if parts.is_empty() {
|
||||
None
|
||||
@@ -767,6 +768,40 @@ pub(crate) fn format_additional_permissions_rule(
|
||||
}
|
||||
}
|
||||
|
||||
fn format_file_system_permissions(
|
||||
file_system: &codex_protocol::models::FileSystemPermissions,
|
||||
access: FileSystemAccessMode,
|
||||
) -> Option<String> {
|
||||
let values = file_system
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|entry| entry.access == access)
|
||||
.map(|entry| format!("`{}`", format_file_system_path(&entry.path)))
|
||||
.collect::<Vec<_>>();
|
||||
(!values.is_empty()).then(|| values.join(", "))
|
||||
}
|
||||
|
||||
fn format_file_system_path(path: &FileSystemPath) -> String {
|
||||
match path {
|
||||
FileSystemPath::Path { path } => path.display().to_string(),
|
||||
FileSystemPath::Special { value } => match value {
|
||||
FileSystemSpecialPath::Root => ":root".to_string(),
|
||||
FileSystemSpecialPath::Minimal => ":minimal".to_string(),
|
||||
FileSystemSpecialPath::CurrentWorkingDirectory => ":cwd".to_string(),
|
||||
FileSystemSpecialPath::ProjectRoots { subpath } => subpath.as_ref().map_or_else(
|
||||
|| ":project_roots".to_string(),
|
||||
|subpath| format!(":project_roots/{}", subpath.display()),
|
||||
),
|
||||
FileSystemSpecialPath::Tmpdir => ":tmpdir".to_string(),
|
||||
FileSystemSpecialPath::SlashTmp => "/tmp".to_string(),
|
||||
FileSystemSpecialPath::Unknown { path, subpath } => subpath.as_ref().map_or_else(
|
||||
|| path.clone(),
|
||||
|subpath| format!("{path}/{}", subpath.display()),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_requested_permissions_rule(
|
||||
permissions: &RequestPermissionProfile,
|
||||
) -> Option<String> {
|
||||
@@ -848,6 +883,10 @@ mod tests {
|
||||
use crate::app_event::AppEvent;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::NetworkPermissions;
|
||||
use codex_protocol::permissions::FileSystemAccessMode;
|
||||
use codex_protocol::permissions::FileSystemPath;
|
||||
use codex_protocol::permissions::FileSystemSandboxEntry;
|
||||
use codex_protocol::permissions::FileSystemSpecialPath;
|
||||
use codex_protocol::protocol::ExecPolicyAmendment;
|
||||
use codex_protocol::protocol::NetworkApprovalProtocol;
|
||||
use codex_protocol::protocol::NetworkPolicyAmendment;
|
||||
@@ -880,6 +919,7 @@ mod tests {
|
||||
[
|
||||
(absolute_path("/tmp/readme.txt"), "/tmp/readme.txt"),
|
||||
(absolute_path("/tmp/out.txt"), "/tmp/out.txt"),
|
||||
(absolute_path("/tmp/secret.txt"), "/tmp/secret.txt"),
|
||||
]
|
||||
.into_iter()
|
||||
.fold(rendered, |rendered, (path, normalized)| {
|
||||
@@ -910,10 +950,10 @@ mod tests {
|
||||
network: Some(NetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![absolute_path("/tmp/readme.txt")]),
|
||||
write: Some(vec![absolute_path("/tmp/out.txt")]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![absolute_path("/tmp/readme.txt")]),
|
||||
Some(vec![absolute_path("/tmp/out.txt")]),
|
||||
)),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1190,10 +1230,10 @@ mod tests {
|
||||
#[test]
|
||||
fn additional_permissions_exec_options_hide_execpolicy_amendment() {
|
||||
let additional_permissions = PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![absolute_path("/tmp/readme.txt")]),
|
||||
write: Some(vec![absolute_path("/tmp/out.txt")]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![absolute_path("/tmp/readme.txt")]),
|
||||
Some(vec![absolute_path("/tmp/out.txt")]),
|
||||
)),
|
||||
..Default::default()
|
||||
};
|
||||
let options = exec_options(
|
||||
@@ -1271,10 +1311,10 @@ mod tests {
|
||||
network: Some(NetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![absolute_path("/tmp/readme.txt")]),
|
||||
write: Some(vec![absolute_path("/tmp/out.txt")]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![absolute_path("/tmp/readme.txt")]),
|
||||
Some(vec![absolute_path("/tmp/out.txt")]),
|
||||
)),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1321,10 +1361,10 @@ mod tests {
|
||||
network: Some(NetworkPermissions {
|
||||
enabled: Some(true),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions {
|
||||
read: Some(vec![absolute_path("/tmp/readme.txt")]),
|
||||
write: Some(vec![absolute_path("/tmp/out.txt")]),
|
||||
}),
|
||||
file_system: Some(FileSystemPermissions::from_read_write_roots(
|
||||
Some(vec![absolute_path("/tmp/readme.txt")]),
|
||||
Some(vec![absolute_path("/tmp/out.txt")]),
|
||||
)),
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1335,6 +1375,46 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn additional_permissions_special_entries_prompt_snapshot() {
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx);
|
||||
let exec_request = ApprovalRequest::Exec {
|
||||
thread_id: ThreadId::new(),
|
||||
thread_label: None,
|
||||
id: "test".into(),
|
||||
command: vec!["cat".into(), "/tmp/readme.txt".into()],
|
||||
reason: Some("need broader filesystem access".into()),
|
||||
available_decisions: vec![ReviewDecision::Approved, ReviewDecision::Abort],
|
||||
network_approval_context: None,
|
||||
additional_permissions: Some(PermissionProfile {
|
||||
file_system: Some(FileSystemPermissions {
|
||||
entries: vec![
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Special {
|
||||
value: FileSystemSpecialPath::Root,
|
||||
},
|
||||
access: FileSystemAccessMode::Write,
|
||||
},
|
||||
FileSystemSandboxEntry {
|
||||
path: FileSystemPath::Path {
|
||||
path: absolute_path("/tmp/secret.txt"),
|
||||
},
|
||||
access: FileSystemAccessMode::None,
|
||||
},
|
||||
],
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
|
||||
let view = ApprovalOverlay::new(exec_request, tx, Features::with_defaults());
|
||||
assert_snapshot!(
|
||||
"approval_overlay_additional_permissions_special_entries_prompt",
|
||||
normalize_snapshot_paths(render_overlay_lines(&view, /*width*/ 120))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn permissions_prompt_snapshot() {
|
||||
let (tx, _rx) = unbounded_channel::<AppEvent>();
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
source: tui/src/bottom_pane/approval_overlay.rs
|
||||
expression: "normalize_snapshot_paths(render_overlay_lines(&view, 120))"
|
||||
---
|
||||
Would you like to run the following command?
|
||||
|
||||
Reason: need broader filesystem access
|
||||
|
||||
Permission rule: write `:root`; deny `/tmp/secret.txt`
|
||||
|
||||
$ cat /tmp/readme.txt
|
||||
|
||||
› 1. Yes, proceed (y)
|
||||
2. No, and tell Codex what to do differently (esc)
|
||||
|
||||
Press enter to confirm or esc to cancel
|
||||
Reference in New Issue
Block a user