Compare commits

...

17 Commits

Author SHA1 Message Date
starr-openai
4ad6a1c7d7 codex: use tempdir-backed apply_patch test paths 2026-04-04 01:26:49 -07:00
starr-openai
0e3ac63173 codex: pin manual approval reviewer in approval tests 2026-04-04 00:23:22 -07:00
starr-openai
f8a9011be4 codex: emit apply_patch file change output deltas 2026-04-03 23:45:32 -07:00
starr-openai
48daea7e5a codex: normalize apply_patch fs cwd 2026-04-03 23:26:47 -07:00
starr-openai
4b05e85f0d codex: normalize apply_patch fs paths against symlinked ancestors 2026-04-03 23:13:55 -07:00
starr-openai
604b233fe5 codex: restore generated app-server schema fixtures 2026-04-03 22:57:31 -07:00
starr-openai
1b71b9c392 codex: pass cwd through sandboxed fs ops 2026-04-03 22:46:55 -07:00
starr-openai
f8f806b873 codex: keep apply_patch summaries relative 2026-04-03 22:15:21 -07:00
starr-openai
ae73368979 codex: fix apply_patch clippy borrow 2026-04-03 21:58:22 -07:00
starr-openai
e6cf2cccda codex: fix apply_patch test imports 2026-04-03 21:52:32 -07:00
starr-openai
f000b01947 codex: simplify apply_patch sandbox FS path 2026-04-03 21:45:59 -07:00
starr-openai
d1fc220934 codex: keep local apply_patch verification on host fs 2026-04-03 21:31:50 -07:00
starr-openai
26aa767e05 codex: move apply_patch onto environment filesystem 2026-04-03 21:31:50 -07:00
Mike Starr
f5337b95fc [codex] ez - rename env=>request in codex-rs/core/src/unified_exec/process_manager.rs 2026-04-03 21:31:50 -07:00
starr-openai
92080bfac1 Skip manual Bazel clippy test targets on Windows 2026-04-03 19:06:02 -07:00
starr-openai
12400ceaa9 Skip exec unit tests on Bazel windows targets 2026-04-03 18:59:18 -07:00
starr-openai
e8fbf47900 Add sandbox policy support to filesystem APIs 2026-04-03 18:57:43 -07:00
42 changed files with 3615 additions and 209 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -2048,6 +2048,7 @@ dependencies = [
"base64 0.22.1",
"clap",
"codex-app-server-protocol",
"codex-protocol",
"codex-utils-absolute-path",
"codex-utils-cargo-bin",
"codex-utils-pty",

View File

@@ -663,6 +663,17 @@
"FsCopyParams": {
"description": "Copy a file or directory tree on the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"destinationPath": {
"allOf": [
{
@@ -675,6 +686,17 @@
"description": "Required for directory copies; ignored for file copies.",
"type": "boolean"
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem copy."
},
"sourcePath": {
"allOf": [
{
@@ -693,6 +715,17 @@
"FsCreateDirectoryParams": {
"description": "Create a directory on the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"path": {
"allOf": [
{
@@ -707,6 +740,17 @@
"boolean",
"null"
]
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem mutation."
}
},
"required": [
@@ -717,6 +761,17 @@
"FsGetMetadataParams": {
"description": "Request metadata for an absolute path.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"path": {
"allOf": [
{
@@ -724,6 +779,17 @@
}
],
"description": "Absolute path to inspect."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem metadata lookup."
}
},
"required": [
@@ -734,6 +800,17 @@
"FsReadDirectoryParams": {
"description": "List direct child names for a directory.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"path": {
"allOf": [
{
@@ -741,6 +818,17 @@
}
],
"description": "Absolute directory path to read."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this directory read."
}
},
"required": [
@@ -751,6 +839,17 @@
"FsReadFileParams": {
"description": "Read a file from the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"path": {
"allOf": [
{
@@ -758,6 +857,17 @@
}
],
"description": "Absolute path to read."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem read."
}
},
"required": [
@@ -768,6 +878,17 @@
"FsRemoveParams": {
"description": "Remove a file or directory tree from the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"force": {
"description": "Whether missing paths should be ignored. Defaults to `true`.",
"type": [
@@ -789,6 +910,17 @@
"boolean",
"null"
]
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem mutation."
}
},
"required": [
@@ -829,6 +961,17 @@
"FsWriteFileParams": {
"description": "Write a file on the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"dataBase64": {
"description": "File contents encoded as base64.",
"type": "string"
@@ -840,6 +983,17 @@
}
],
"description": "Absolute path to write."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem write."
}
},
"required": [

View File

@@ -7502,6 +7502,17 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Copy a file or directory tree on the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"destinationPath": {
"allOf": [
{
@@ -7514,6 +7525,17 @@
"description": "Required for directory copies; ignored for file copies.",
"type": "boolean"
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/v2/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem copy."
},
"sourcePath": {
"allOf": [
{
@@ -7540,6 +7562,17 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Create a directory on the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"path": {
"allOf": [
{
@@ -7554,6 +7587,17 @@
"boolean",
"null"
]
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/v2/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem mutation."
}
},
"required": [
@@ -7572,6 +7616,17 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Request metadata for an absolute path.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"path": {
"allOf": [
{
@@ -7579,6 +7634,17 @@
}
],
"description": "Absolute path to inspect."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/v2/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem metadata lookup."
}
},
"required": [
@@ -7646,6 +7712,17 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "List direct child names for a directory.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"path": {
"allOf": [
{
@@ -7653,6 +7730,17 @@
}
],
"description": "Absolute directory path to read."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/v2/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this directory read."
}
},
"required": [
@@ -7683,6 +7771,17 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Read a file from the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"path": {
"allOf": [
{
@@ -7690,6 +7789,17 @@
}
],
"description": "Absolute path to read."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/v2/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem read."
}
},
"required": [
@@ -7717,6 +7827,17 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Remove a file or directory tree from the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"force": {
"description": "Whether missing paths should be ignored. Defaults to `true`.",
"type": [
@@ -7738,6 +7859,17 @@
"boolean",
"null"
]
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/v2/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem mutation."
}
},
"required": [
@@ -7820,6 +7952,17 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Write a file on the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/v2/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"dataBase64": {
"description": "File contents encoded as base64.",
"type": "string"
@@ -7831,6 +7974,17 @@
}
],
"description": "Absolute path to write."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/v2/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem write."
}
},
"required": [

View File

@@ -4170,6 +4170,17 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Copy a file or directory tree on the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"destinationPath": {
"allOf": [
{
@@ -4182,6 +4193,17 @@
"description": "Required for directory copies; ignored for file copies.",
"type": "boolean"
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem copy."
},
"sourcePath": {
"allOf": [
{
@@ -4208,6 +4230,17 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Create a directory on the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"path": {
"allOf": [
{
@@ -4222,6 +4255,17 @@
"boolean",
"null"
]
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem mutation."
}
},
"required": [
@@ -4240,6 +4284,17 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Request metadata for an absolute path.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"path": {
"allOf": [
{
@@ -4247,6 +4302,17 @@
}
],
"description": "Absolute path to inspect."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem metadata lookup."
}
},
"required": [
@@ -4314,6 +4380,17 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "List direct child names for a directory.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"path": {
"allOf": [
{
@@ -4321,6 +4398,17 @@
}
],
"description": "Absolute directory path to read."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this directory read."
}
},
"required": [
@@ -4351,6 +4439,17 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Read a file from the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"path": {
"allOf": [
{
@@ -4358,6 +4457,17 @@
}
],
"description": "Absolute path to read."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem read."
}
},
"required": [
@@ -4385,6 +4495,17 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Remove a file or directory tree from the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"force": {
"description": "Whether missing paths should be ignored. Defaults to `true`.",
"type": [
@@ -4406,6 +4527,17 @@
"boolean",
"null"
]
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem mutation."
}
},
"required": [
@@ -4488,6 +4620,17 @@
"$schema": "http://json-schema.org/draft-07/schema#",
"description": "Write a file on the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"dataBase64": {
"description": "File contents encoded as base64.",
"type": "string"
@@ -4499,6 +4642,17 @@
}
],
"description": "Absolute path to write."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem write."
}
},
"required": [

View File

@@ -4,10 +4,194 @@
"AbsolutePathBuf": {
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"NetworkAccess": {
"enum": [
"restricted",
"enabled"
],
"type": "string"
},
"ReadOnlyAccess": {
"oneOf": [
{
"properties": {
"includePlatformDefaults": {
"default": true,
"type": "boolean"
},
"readableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
},
"type": {
"enum": [
"restricted"
],
"title": "RestrictedReadOnlyAccessType",
"type": "string"
}
},
"required": [
"type"
],
"title": "RestrictedReadOnlyAccess",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"fullAccess"
],
"title": "FullAccessReadOnlyAccessType",
"type": "string"
}
},
"required": [
"type"
],
"title": "FullAccessReadOnlyAccess",
"type": "object"
}
]
},
"SandboxPolicy": {
"oneOf": [
{
"properties": {
"type": {
"enum": [
"dangerFullAccess"
],
"title": "DangerFullAccessSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DangerFullAccessSandboxPolicy",
"type": "object"
},
{
"properties": {
"access": {
"allOf": [
{
"$ref": "#/definitions/ReadOnlyAccess"
}
],
"default": {
"type": "fullAccess"
}
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"type": {
"enum": [
"readOnly"
],
"title": "ReadOnlySandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ReadOnlySandboxPolicy",
"type": "object"
},
{
"properties": {
"networkAccess": {
"allOf": [
{
"$ref": "#/definitions/NetworkAccess"
}
],
"default": "restricted"
},
"type": {
"enum": [
"externalSandbox"
],
"title": "ExternalSandboxSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ExternalSandboxSandboxPolicy",
"type": "object"
},
{
"properties": {
"excludeSlashTmp": {
"default": false,
"type": "boolean"
},
"excludeTmpdirEnvVar": {
"default": false,
"type": "boolean"
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"readOnlyAccess": {
"allOf": [
{
"$ref": "#/definitions/ReadOnlyAccess"
}
],
"default": {
"type": "fullAccess"
}
},
"type": {
"enum": [
"workspaceWrite"
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
"type"
],
"title": "WorkspaceWriteSandboxPolicy",
"type": "object"
}
]
}
},
"description": "Copy a file or directory tree on the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"destinationPath": {
"allOf": [
{
@@ -20,6 +204,17 @@
"description": "Required for directory copies; ignored for file copies.",
"type": "boolean"
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem copy."
},
"sourcePath": {
"allOf": [
{

View File

@@ -4,10 +4,194 @@
"AbsolutePathBuf": {
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"NetworkAccess": {
"enum": [
"restricted",
"enabled"
],
"type": "string"
},
"ReadOnlyAccess": {
"oneOf": [
{
"properties": {
"includePlatformDefaults": {
"default": true,
"type": "boolean"
},
"readableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
},
"type": {
"enum": [
"restricted"
],
"title": "RestrictedReadOnlyAccessType",
"type": "string"
}
},
"required": [
"type"
],
"title": "RestrictedReadOnlyAccess",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"fullAccess"
],
"title": "FullAccessReadOnlyAccessType",
"type": "string"
}
},
"required": [
"type"
],
"title": "FullAccessReadOnlyAccess",
"type": "object"
}
]
},
"SandboxPolicy": {
"oneOf": [
{
"properties": {
"type": {
"enum": [
"dangerFullAccess"
],
"title": "DangerFullAccessSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DangerFullAccessSandboxPolicy",
"type": "object"
},
{
"properties": {
"access": {
"allOf": [
{
"$ref": "#/definitions/ReadOnlyAccess"
}
],
"default": {
"type": "fullAccess"
}
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"type": {
"enum": [
"readOnly"
],
"title": "ReadOnlySandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ReadOnlySandboxPolicy",
"type": "object"
},
{
"properties": {
"networkAccess": {
"allOf": [
{
"$ref": "#/definitions/NetworkAccess"
}
],
"default": "restricted"
},
"type": {
"enum": [
"externalSandbox"
],
"title": "ExternalSandboxSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ExternalSandboxSandboxPolicy",
"type": "object"
},
{
"properties": {
"excludeSlashTmp": {
"default": false,
"type": "boolean"
},
"excludeTmpdirEnvVar": {
"default": false,
"type": "boolean"
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"readOnlyAccess": {
"allOf": [
{
"$ref": "#/definitions/ReadOnlyAccess"
}
],
"default": {
"type": "fullAccess"
}
},
"type": {
"enum": [
"workspaceWrite"
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
"type"
],
"title": "WorkspaceWriteSandboxPolicy",
"type": "object"
}
]
}
},
"description": "Create a directory on the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"path": {
"allOf": [
{
@@ -22,6 +206,17 @@
"boolean",
"null"
]
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem mutation."
}
},
"required": [

View File

@@ -4,10 +4,194 @@
"AbsolutePathBuf": {
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"NetworkAccess": {
"enum": [
"restricted",
"enabled"
],
"type": "string"
},
"ReadOnlyAccess": {
"oneOf": [
{
"properties": {
"includePlatformDefaults": {
"default": true,
"type": "boolean"
},
"readableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
},
"type": {
"enum": [
"restricted"
],
"title": "RestrictedReadOnlyAccessType",
"type": "string"
}
},
"required": [
"type"
],
"title": "RestrictedReadOnlyAccess",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"fullAccess"
],
"title": "FullAccessReadOnlyAccessType",
"type": "string"
}
},
"required": [
"type"
],
"title": "FullAccessReadOnlyAccess",
"type": "object"
}
]
},
"SandboxPolicy": {
"oneOf": [
{
"properties": {
"type": {
"enum": [
"dangerFullAccess"
],
"title": "DangerFullAccessSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DangerFullAccessSandboxPolicy",
"type": "object"
},
{
"properties": {
"access": {
"allOf": [
{
"$ref": "#/definitions/ReadOnlyAccess"
}
],
"default": {
"type": "fullAccess"
}
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"type": {
"enum": [
"readOnly"
],
"title": "ReadOnlySandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ReadOnlySandboxPolicy",
"type": "object"
},
{
"properties": {
"networkAccess": {
"allOf": [
{
"$ref": "#/definitions/NetworkAccess"
}
],
"default": "restricted"
},
"type": {
"enum": [
"externalSandbox"
],
"title": "ExternalSandboxSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ExternalSandboxSandboxPolicy",
"type": "object"
},
{
"properties": {
"excludeSlashTmp": {
"default": false,
"type": "boolean"
},
"excludeTmpdirEnvVar": {
"default": false,
"type": "boolean"
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"readOnlyAccess": {
"allOf": [
{
"$ref": "#/definitions/ReadOnlyAccess"
}
],
"default": {
"type": "fullAccess"
}
},
"type": {
"enum": [
"workspaceWrite"
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
"type"
],
"title": "WorkspaceWriteSandboxPolicy",
"type": "object"
}
]
}
},
"description": "Request metadata for an absolute path.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"path": {
"allOf": [
{
@@ -15,6 +199,17 @@
}
],
"description": "Absolute path to inspect."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem metadata lookup."
}
},
"required": [

View File

@@ -4,10 +4,194 @@
"AbsolutePathBuf": {
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"NetworkAccess": {
"enum": [
"restricted",
"enabled"
],
"type": "string"
},
"ReadOnlyAccess": {
"oneOf": [
{
"properties": {
"includePlatformDefaults": {
"default": true,
"type": "boolean"
},
"readableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
},
"type": {
"enum": [
"restricted"
],
"title": "RestrictedReadOnlyAccessType",
"type": "string"
}
},
"required": [
"type"
],
"title": "RestrictedReadOnlyAccess",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"fullAccess"
],
"title": "FullAccessReadOnlyAccessType",
"type": "string"
}
},
"required": [
"type"
],
"title": "FullAccessReadOnlyAccess",
"type": "object"
}
]
},
"SandboxPolicy": {
"oneOf": [
{
"properties": {
"type": {
"enum": [
"dangerFullAccess"
],
"title": "DangerFullAccessSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DangerFullAccessSandboxPolicy",
"type": "object"
},
{
"properties": {
"access": {
"allOf": [
{
"$ref": "#/definitions/ReadOnlyAccess"
}
],
"default": {
"type": "fullAccess"
}
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"type": {
"enum": [
"readOnly"
],
"title": "ReadOnlySandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ReadOnlySandboxPolicy",
"type": "object"
},
{
"properties": {
"networkAccess": {
"allOf": [
{
"$ref": "#/definitions/NetworkAccess"
}
],
"default": "restricted"
},
"type": {
"enum": [
"externalSandbox"
],
"title": "ExternalSandboxSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ExternalSandboxSandboxPolicy",
"type": "object"
},
{
"properties": {
"excludeSlashTmp": {
"default": false,
"type": "boolean"
},
"excludeTmpdirEnvVar": {
"default": false,
"type": "boolean"
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"readOnlyAccess": {
"allOf": [
{
"$ref": "#/definitions/ReadOnlyAccess"
}
],
"default": {
"type": "fullAccess"
}
},
"type": {
"enum": [
"workspaceWrite"
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
"type"
],
"title": "WorkspaceWriteSandboxPolicy",
"type": "object"
}
]
}
},
"description": "List direct child names for a directory.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"path": {
"allOf": [
{
@@ -15,6 +199,17 @@
}
],
"description": "Absolute directory path to read."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this directory read."
}
},
"required": [

View File

@@ -4,10 +4,194 @@
"AbsolutePathBuf": {
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"NetworkAccess": {
"enum": [
"restricted",
"enabled"
],
"type": "string"
},
"ReadOnlyAccess": {
"oneOf": [
{
"properties": {
"includePlatformDefaults": {
"default": true,
"type": "boolean"
},
"readableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
},
"type": {
"enum": [
"restricted"
],
"title": "RestrictedReadOnlyAccessType",
"type": "string"
}
},
"required": [
"type"
],
"title": "RestrictedReadOnlyAccess",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"fullAccess"
],
"title": "FullAccessReadOnlyAccessType",
"type": "string"
}
},
"required": [
"type"
],
"title": "FullAccessReadOnlyAccess",
"type": "object"
}
]
},
"SandboxPolicy": {
"oneOf": [
{
"properties": {
"type": {
"enum": [
"dangerFullAccess"
],
"title": "DangerFullAccessSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DangerFullAccessSandboxPolicy",
"type": "object"
},
{
"properties": {
"access": {
"allOf": [
{
"$ref": "#/definitions/ReadOnlyAccess"
}
],
"default": {
"type": "fullAccess"
}
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"type": {
"enum": [
"readOnly"
],
"title": "ReadOnlySandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ReadOnlySandboxPolicy",
"type": "object"
},
{
"properties": {
"networkAccess": {
"allOf": [
{
"$ref": "#/definitions/NetworkAccess"
}
],
"default": "restricted"
},
"type": {
"enum": [
"externalSandbox"
],
"title": "ExternalSandboxSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ExternalSandboxSandboxPolicy",
"type": "object"
},
{
"properties": {
"excludeSlashTmp": {
"default": false,
"type": "boolean"
},
"excludeTmpdirEnvVar": {
"default": false,
"type": "boolean"
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"readOnlyAccess": {
"allOf": [
{
"$ref": "#/definitions/ReadOnlyAccess"
}
],
"default": {
"type": "fullAccess"
}
},
"type": {
"enum": [
"workspaceWrite"
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
"type"
],
"title": "WorkspaceWriteSandboxPolicy",
"type": "object"
}
]
}
},
"description": "Read a file from the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"path": {
"allOf": [
{
@@ -15,6 +199,17 @@
}
],
"description": "Absolute path to read."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem read."
}
},
"required": [

View File

@@ -4,10 +4,194 @@
"AbsolutePathBuf": {
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"NetworkAccess": {
"enum": [
"restricted",
"enabled"
],
"type": "string"
},
"ReadOnlyAccess": {
"oneOf": [
{
"properties": {
"includePlatformDefaults": {
"default": true,
"type": "boolean"
},
"readableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
},
"type": {
"enum": [
"restricted"
],
"title": "RestrictedReadOnlyAccessType",
"type": "string"
}
},
"required": [
"type"
],
"title": "RestrictedReadOnlyAccess",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"fullAccess"
],
"title": "FullAccessReadOnlyAccessType",
"type": "string"
}
},
"required": [
"type"
],
"title": "FullAccessReadOnlyAccess",
"type": "object"
}
]
},
"SandboxPolicy": {
"oneOf": [
{
"properties": {
"type": {
"enum": [
"dangerFullAccess"
],
"title": "DangerFullAccessSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DangerFullAccessSandboxPolicy",
"type": "object"
},
{
"properties": {
"access": {
"allOf": [
{
"$ref": "#/definitions/ReadOnlyAccess"
}
],
"default": {
"type": "fullAccess"
}
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"type": {
"enum": [
"readOnly"
],
"title": "ReadOnlySandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ReadOnlySandboxPolicy",
"type": "object"
},
{
"properties": {
"networkAccess": {
"allOf": [
{
"$ref": "#/definitions/NetworkAccess"
}
],
"default": "restricted"
},
"type": {
"enum": [
"externalSandbox"
],
"title": "ExternalSandboxSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ExternalSandboxSandboxPolicy",
"type": "object"
},
{
"properties": {
"excludeSlashTmp": {
"default": false,
"type": "boolean"
},
"excludeTmpdirEnvVar": {
"default": false,
"type": "boolean"
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"readOnlyAccess": {
"allOf": [
{
"$ref": "#/definitions/ReadOnlyAccess"
}
],
"default": {
"type": "fullAccess"
}
},
"type": {
"enum": [
"workspaceWrite"
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
"type"
],
"title": "WorkspaceWriteSandboxPolicy",
"type": "object"
}
]
}
},
"description": "Remove a file or directory tree from the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"force": {
"description": "Whether missing paths should be ignored. Defaults to `true`.",
"type": [
@@ -29,6 +213,17 @@
"boolean",
"null"
]
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem mutation."
}
},
"required": [

View File

@@ -4,10 +4,194 @@
"AbsolutePathBuf": {
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"NetworkAccess": {
"enum": [
"restricted",
"enabled"
],
"type": "string"
},
"ReadOnlyAccess": {
"oneOf": [
{
"properties": {
"includePlatformDefaults": {
"default": true,
"type": "boolean"
},
"readableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
},
"type": {
"enum": [
"restricted"
],
"title": "RestrictedReadOnlyAccessType",
"type": "string"
}
},
"required": [
"type"
],
"title": "RestrictedReadOnlyAccess",
"type": "object"
},
{
"properties": {
"type": {
"enum": [
"fullAccess"
],
"title": "FullAccessReadOnlyAccessType",
"type": "string"
}
},
"required": [
"type"
],
"title": "FullAccessReadOnlyAccess",
"type": "object"
}
]
},
"SandboxPolicy": {
"oneOf": [
{
"properties": {
"type": {
"enum": [
"dangerFullAccess"
],
"title": "DangerFullAccessSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "DangerFullAccessSandboxPolicy",
"type": "object"
},
{
"properties": {
"access": {
"allOf": [
{
"$ref": "#/definitions/ReadOnlyAccess"
}
],
"default": {
"type": "fullAccess"
}
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"type": {
"enum": [
"readOnly"
],
"title": "ReadOnlySandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ReadOnlySandboxPolicy",
"type": "object"
},
{
"properties": {
"networkAccess": {
"allOf": [
{
"$ref": "#/definitions/NetworkAccess"
}
],
"default": "restricted"
},
"type": {
"enum": [
"externalSandbox"
],
"title": "ExternalSandboxSandboxPolicyType",
"type": "string"
}
},
"required": [
"type"
],
"title": "ExternalSandboxSandboxPolicy",
"type": "object"
},
{
"properties": {
"excludeSlashTmp": {
"default": false,
"type": "boolean"
},
"excludeTmpdirEnvVar": {
"default": false,
"type": "boolean"
},
"networkAccess": {
"default": false,
"type": "boolean"
},
"readOnlyAccess": {
"allOf": [
{
"$ref": "#/definitions/ReadOnlyAccess"
}
],
"default": {
"type": "fullAccess"
}
},
"type": {
"enum": [
"workspaceWrite"
],
"title": "WorkspaceWriteSandboxPolicyType",
"type": "string"
},
"writableRoots": {
"default": [],
"items": {
"$ref": "#/definitions/AbsolutePathBuf"
},
"type": "array"
}
},
"required": [
"type"
],
"title": "WorkspaceWriteSandboxPolicy",
"type": "object"
}
]
}
},
"description": "Write a file on the host filesystem.",
"properties": {
"cwd": {
"anyOf": [
{
"$ref": "#/definitions/AbsolutePathBuf"
},
{
"type": "null"
}
],
"description": "Optional cwd to resolve legacy sandbox workspace roots against."
},
"dataBase64": {
"description": "File contents encoded as base64.",
"type": "string"
@@ -19,6 +203,17 @@
}
],
"description": "Absolute path to write."
},
"sandboxPolicy": {
"anyOf": [
{
"$ref": "#/definitions/SandboxPolicy"
},
{
"type": "null"
}
],
"description": "Optional sandbox policy for this filesystem write."
}
},
"required": [

View File

@@ -2,6 +2,7 @@
// 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 { SandboxPolicy } from "./SandboxPolicy";
/**
* Copy a file or directory tree on the host filesystem.
@@ -18,4 +19,12 @@ destinationPath: AbsolutePathBuf,
/**
* Required for directory copies; ignored for file copies.
*/
recursive?: boolean, };
recursive?: boolean,
/**
* Optional sandbox policy for this filesystem copy.
*/
sandboxPolicy?: SandboxPolicy | null,
/**
* Optional cwd to resolve legacy sandbox workspace roots against.
*/
cwd?: AbsolutePathBuf | null, };

View File

@@ -2,6 +2,7 @@
// 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 { SandboxPolicy } from "./SandboxPolicy";
/**
* Create a directory on the host filesystem.
@@ -14,4 +15,12 @@ path: AbsolutePathBuf,
/**
* Whether parent directories should also be created. Defaults to `true`.
*/
recursive?: boolean | null, };
recursive?: boolean | null,
/**
* Optional sandbox policy for this filesystem mutation.
*/
sandboxPolicy?: SandboxPolicy | null,
/**
* Optional cwd to resolve legacy sandbox workspace roots against.
*/
cwd?: AbsolutePathBuf | null, };

View File

@@ -2,6 +2,7 @@
// 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 { SandboxPolicy } from "./SandboxPolicy";
/**
* Request metadata for an absolute path.
@@ -10,4 +11,12 @@ export type FsGetMetadataParams = {
/**
* Absolute path to inspect.
*/
path: AbsolutePathBuf, };
path: AbsolutePathBuf,
/**
* Optional sandbox policy for this filesystem metadata lookup.
*/
sandboxPolicy?: SandboxPolicy | null,
/**
* Optional cwd to resolve legacy sandbox workspace roots against.
*/
cwd?: AbsolutePathBuf | null, };

View File

@@ -2,6 +2,7 @@
// 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 { SandboxPolicy } from "./SandboxPolicy";
/**
* List direct child names for a directory.
@@ -10,4 +11,12 @@ export type FsReadDirectoryParams = {
/**
* Absolute directory path to read.
*/
path: AbsolutePathBuf, };
path: AbsolutePathBuf,
/**
* Optional sandbox policy for this directory read.
*/
sandboxPolicy?: SandboxPolicy | null,
/**
* Optional cwd to resolve legacy sandbox workspace roots against.
*/
cwd?: AbsolutePathBuf | null, };

View File

@@ -2,6 +2,7 @@
// 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 { SandboxPolicy } from "./SandboxPolicy";
/**
* Read a file from the host filesystem.
@@ -10,4 +11,12 @@ export type FsReadFileParams = {
/**
* Absolute path to read.
*/
path: AbsolutePathBuf, };
path: AbsolutePathBuf,
/**
* Optional sandbox policy for this filesystem read.
*/
sandboxPolicy?: SandboxPolicy | null,
/**
* Optional cwd to resolve legacy sandbox workspace roots against.
*/
cwd?: AbsolutePathBuf | null, };

View File

@@ -2,6 +2,7 @@
// 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 { SandboxPolicy } from "./SandboxPolicy";
/**
* Remove a file or directory tree from the host filesystem.
@@ -18,4 +19,12 @@ recursive?: boolean | null,
/**
* Whether missing paths should be ignored. Defaults to `true`.
*/
force?: boolean | null, };
force?: boolean | null,
/**
* Optional sandbox policy for this filesystem mutation.
*/
sandboxPolicy?: SandboxPolicy | null,
/**
* Optional cwd to resolve legacy sandbox workspace roots against.
*/
cwd?: AbsolutePathBuf | null, };

View File

@@ -2,6 +2,7 @@
// 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 { SandboxPolicy } from "./SandboxPolicy";
/**
* Write a file on the host filesystem.
@@ -14,4 +15,12 @@ path: AbsolutePathBuf,
/**
* File contents encoded as base64.
*/
dataBase64: string, };
dataBase64: string,
/**
* Optional sandbox policy for this filesystem write.
*/
sandboxPolicy?: SandboxPolicy | null,
/**
* Optional cwd to resolve legacy sandbox workspace roots against.
*/
cwd?: AbsolutePathBuf | null, };

View File

@@ -1610,14 +1610,18 @@ mod tests {
request_id: RequestId::Integer(9),
params: v2::FsGetMetadataParams {
path: absolute_path("tmp/example"),
sandbox_policy: None,
cwd: None,
},
};
assert_eq!(
json!({
"method": "fs/getMetadata",
"id": 9,
"params": {
"path": absolute_path_string("tmp/example")
"params": {
"path": absolute_path_string("tmp/example"),
"sandboxPolicy": null,
"cwd": null
}
}),
serde_json::to_value(&request)?,

View File

@@ -2168,6 +2168,12 @@ pub struct FeedbackUploadResponse {
pub struct FsReadFileParams {
/// Absolute path to read.
pub path: AbsolutePathBuf,
/// Optional sandbox policy for this filesystem read.
#[ts(optional = nullable)]
pub sandbox_policy: Option<SandboxPolicy>,
/// Optional cwd to resolve legacy sandbox workspace roots against.
#[ts(optional = nullable)]
pub cwd: Option<AbsolutePathBuf>,
}
/// Base64-encoded file contents returned by `fs/readFile`.
@@ -2188,6 +2194,12 @@ pub struct FsWriteFileParams {
pub path: AbsolutePathBuf,
/// File contents encoded as base64.
pub data_base64: String,
/// Optional sandbox policy for this filesystem write.
#[ts(optional = nullable)]
pub sandbox_policy: Option<SandboxPolicy>,
/// Optional cwd to resolve legacy sandbox workspace roots against.
#[ts(optional = nullable)]
pub cwd: Option<AbsolutePathBuf>,
}
/// Successful response for `fs/writeFile`.
@@ -2206,6 +2218,12 @@ pub struct FsCreateDirectoryParams {
/// Whether parent directories should also be created. Defaults to `true`.
#[ts(optional = nullable)]
pub recursive: Option<bool>,
/// Optional sandbox policy for this filesystem mutation.
#[ts(optional = nullable)]
pub sandbox_policy: Option<SandboxPolicy>,
/// Optional cwd to resolve legacy sandbox workspace roots against.
#[ts(optional = nullable)]
pub cwd: Option<AbsolutePathBuf>,
}
/// Successful response for `fs/createDirectory`.
@@ -2221,6 +2239,12 @@ pub struct FsCreateDirectoryResponse {}
pub struct FsGetMetadataParams {
/// Absolute path to inspect.
pub path: AbsolutePathBuf,
/// Optional sandbox policy for this filesystem metadata lookup.
#[ts(optional = nullable)]
pub sandbox_policy: Option<SandboxPolicy>,
/// Optional cwd to resolve legacy sandbox workspace roots against.
#[ts(optional = nullable)]
pub cwd: Option<AbsolutePathBuf>,
}
/// Metadata returned by `fs/getMetadata`.
@@ -2247,6 +2271,12 @@ pub struct FsGetMetadataResponse {
pub struct FsReadDirectoryParams {
/// Absolute directory path to read.
pub path: AbsolutePathBuf,
/// Optional sandbox policy for this directory read.
#[ts(optional = nullable)]
pub sandbox_policy: Option<SandboxPolicy>,
/// Optional cwd to resolve legacy sandbox workspace roots against.
#[ts(optional = nullable)]
pub cwd: Option<AbsolutePathBuf>,
}
/// A directory entry returned by `fs/readDirectory`.
@@ -2284,6 +2314,12 @@ pub struct FsRemoveParams {
/// Whether missing paths should be ignored. Defaults to `true`.
#[ts(optional = nullable)]
pub force: Option<bool>,
/// Optional sandbox policy for this filesystem mutation.
#[ts(optional = nullable)]
pub sandbox_policy: Option<SandboxPolicy>,
/// Optional cwd to resolve legacy sandbox workspace roots against.
#[ts(optional = nullable)]
pub cwd: Option<AbsolutePathBuf>,
}
/// Successful response for `fs/remove`.
@@ -2304,6 +2340,12 @@ pub struct FsCopyParams {
/// Required for directory copies; ignored for file copies.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub recursive: bool,
/// Optional sandbox policy for this filesystem copy.
#[ts(optional = nullable)]
pub sandbox_policy: Option<SandboxPolicy>,
/// Optional cwd to resolve legacy sandbox workspace roots against.
#[ts(optional = nullable)]
pub cwd: Option<AbsolutePathBuf>,
}
/// Successful response for `fs/copy`.
@@ -6526,6 +6568,8 @@ mod tests {
fn fs_read_file_params_round_trip() {
let params = FsReadFileParams {
path: absolute_path("tmp/example.txt"),
sandbox_policy: None,
cwd: None,
};
let value = serde_json::to_value(&params).expect("serialize fs/readFile params");
@@ -6533,6 +6577,8 @@ mod tests {
value,
json!({
"path": absolute_path_string("tmp/example.txt"),
"sandboxPolicy": null,
"cwd": null,
})
);
@@ -6546,6 +6592,8 @@ mod tests {
let params = FsCreateDirectoryParams {
path: absolute_path("tmp/example"),
recursive: None,
sandbox_policy: None,
cwd: None,
};
let value = serde_json::to_value(&params).expect("serialize fs/createDirectory params");
@@ -6554,6 +6602,8 @@ mod tests {
json!({
"path": absolute_path_string("tmp/example"),
"recursive": null,
"sandboxPolicy": null,
"cwd": null,
})
);
@@ -6567,6 +6617,8 @@ mod tests {
let params = FsWriteFileParams {
path: absolute_path("tmp/example.bin"),
data_base64: "AAE=".to_string(),
sandbox_policy: None,
cwd: None,
};
let value = serde_json::to_value(&params).expect("serialize fs/writeFile params");
@@ -6575,6 +6627,8 @@ mod tests {
json!({
"path": absolute_path_string("tmp/example.bin"),
"dataBase64": "AAE=",
"sandboxPolicy": null,
"cwd": null,
})
);
@@ -6589,6 +6643,8 @@ mod tests {
source_path: absolute_path("tmp/source"),
destination_path: absolute_path("tmp/destination"),
recursive: true,
sandbox_policy: None,
cwd: None,
};
let value = serde_json::to_value(&params).expect("serialize fs/copy params");
@@ -6598,6 +6654,8 @@ mod tests {
"sourcePath": absolute_path_string("tmp/source"),
"destinationPath": absolute_path_string("tmp/destination"),
"recursive": true,
"sandboxPolicy": null,
"cwd": null,
})
);

View File

@@ -161,13 +161,13 @@ Example with notification opt-out:
- `command/exec/resize` — resize a running PTY-backed `command/exec` session by `processId`; returns `{}`.
- `command/exec/terminate` — terminate a running `command/exec` session by `processId`; returns `{}`.
- `command/exec/outputDelta` — notification emitted for base64-encoded stdout/stderr chunks from a streaming `command/exec` session.
- `fs/readFile` — read an absolute file path and return `{ dataBase64 }`.
- `fs/writeFile` — write an absolute file path from base64-encoded `{ dataBase64 }`; returns `{}`.
- `fs/createDirectory` — create an absolute directory path; `recursive` defaults to `true`.
- `fs/getMetadata` — return metadata for an absolute path: `isDirectory`, `isFile`, `createdAtMs`, and `modifiedAtMs`.
- `fs/readDirectory` — list direct child entries for an absolute directory path; each entry contains `fileName`, `isDirectory`, and `isFile`, and `fileName` is just the child name, not a path.
- `fs/remove` — remove an absolute file or directory tree; `recursive` and `force` default to `true`.
- `fs/copy` — copy between absolute paths; directory copies require `recursive: true`.
- `fs/readFile` — read an absolute file path and return `{ dataBase64 }`; accepts optional `sandboxPolicy`.
- `fs/writeFile` — write an absolute file path from base64-encoded `{ dataBase64 }`; returns `{}` and accepts optional `sandboxPolicy`.
- `fs/createDirectory` — create an absolute directory path; `recursive` defaults to `true`, and requests may include optional `sandboxPolicy`.
- `fs/getMetadata` — return metadata for an absolute path: `isDirectory`, `isFile`, `createdAtMs`, and `modifiedAtMs`; accepts optional `sandboxPolicy`.
- `fs/readDirectory` — list direct child entries for an absolute directory path; each entry contains `fileName`, `isDirectory`, and `isFile`, and `fileName` is just the child name, not a path. Requests may include optional `sandboxPolicy`.
- `fs/remove` — remove an absolute file or directory tree; `recursive` and `force` default to `true`, and requests may include optional `sandboxPolicy`.
- `fs/copy` — copy between absolute paths; directory copies require `recursive: true`, and requests may include optional `sandboxPolicy`.
- `fs/watch` — subscribe this connection to filesystem change notifications for an absolute file or directory path; returns a `watchId` and canonicalized `path`.
- `fs/unwatch` — stop sending notifications for a prior `fs/watch`; returns `{}`.
- `fs/changed` — notification emitted when watched paths change, including the `watchId` and `changedPaths`.
@@ -772,7 +772,7 @@ Streaming stdin/stdout uses base64 so PTY sessions can carry arbitrary bytes:
These methods operate on absolute paths on the host filesystem and cover reading, writing, directory traversal, copying, removal, and change notifications.
All filesystem paths in this section must be absolute.
All filesystem paths in this section must be absolute. When provided, `sandboxPolicy` uses the same shape as `thread/start` and `command/exec`.
```json
{ "method": "fs/createDirectory", "id": 40, "params": {

View File

@@ -22,6 +22,7 @@ use codex_exec_server::CopyOptions;
use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::Environment;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::FileSystemOperationOptions;
use codex_exec_server::RemoveOptions;
use std::io;
use std::sync::Arc;
@@ -46,7 +47,10 @@ impl FsApi {
) -> Result<FsReadFileResponse, JSONRPCErrorError> {
let bytes = self
.file_system
.read_file(&params.path)
.read_file_with_options(
&params.path,
&fs_operation_options(params.sandbox_policy, params.cwd),
)
.await
.map_err(map_fs_error)?;
Ok(FsReadFileResponse {
@@ -64,7 +68,11 @@ impl FsApi {
))
})?;
self.file_system
.write_file(&params.path, bytes)
.write_file_with_options(
&params.path,
bytes,
&fs_operation_options(params.sandbox_policy, params.cwd),
)
.await
.map_err(map_fs_error)?;
Ok(FsWriteFileResponse {})
@@ -75,11 +83,12 @@ impl FsApi {
params: FsCreateDirectoryParams,
) -> Result<FsCreateDirectoryResponse, JSONRPCErrorError> {
self.file_system
.create_directory(
.create_directory_with_options(
&params.path,
CreateDirectoryOptions {
recursive: params.recursive.unwrap_or(true),
},
&fs_operation_options(params.sandbox_policy, params.cwd),
)
.await
.map_err(map_fs_error)?;
@@ -92,7 +101,10 @@ impl FsApi {
) -> Result<FsGetMetadataResponse, JSONRPCErrorError> {
let metadata = self
.file_system
.get_metadata(&params.path)
.get_metadata_with_options(
&params.path,
&fs_operation_options(params.sandbox_policy, params.cwd),
)
.await
.map_err(map_fs_error)?;
Ok(FsGetMetadataResponse {
@@ -109,7 +121,10 @@ impl FsApi {
) -> Result<FsReadDirectoryResponse, JSONRPCErrorError> {
let entries = self
.file_system
.read_directory(&params.path)
.read_directory_with_options(
&params.path,
&fs_operation_options(params.sandbox_policy, params.cwd),
)
.await
.map_err(map_fs_error)?;
Ok(FsReadDirectoryResponse {
@@ -129,12 +144,13 @@ impl FsApi {
params: FsRemoveParams,
) -> Result<FsRemoveResponse, JSONRPCErrorError> {
self.file_system
.remove(
.remove_with_options(
&params.path,
RemoveOptions {
recursive: params.recursive.unwrap_or(true),
force: params.force.unwrap_or(true),
},
&fs_operation_options(params.sandbox_policy, params.cwd),
)
.await
.map_err(map_fs_error)?;
@@ -146,12 +162,13 @@ impl FsApi {
params: FsCopyParams,
) -> Result<FsCopyResponse, JSONRPCErrorError> {
self.file_system
.copy(
.copy_with_options(
&params.source_path,
&params.destination_path,
CopyOptions {
recursive: params.recursive,
},
&fs_operation_options(params.sandbox_policy, params.cwd),
)
.await
.map_err(map_fs_error)?;
@@ -159,6 +176,16 @@ impl FsApi {
}
}
fn fs_operation_options(
sandbox_policy: Option<codex_app_server_protocol::SandboxPolicy>,
cwd: Option<codex_utils_absolute_path::AbsolutePathBuf>,
) -> FileSystemOperationOptions {
FileSystemOperationOptions {
sandbox_policy: sandbox_policy.map(|policy| policy.to_core()),
cwd,
}
}
pub(crate) fn invalid_request(message: impl Into<String>) -> JSONRPCErrorError {
JSONRPCErrorError {
code: INVALID_REQUEST_ERROR_CODE,

View File

@@ -13,7 +13,9 @@ use codex_app_server_protocol::FsUnwatchParams;
use codex_app_server_protocol::FsWatchResponse;
use codex_app_server_protocol::FsWriteFileParams;
use codex_app_server_protocol::JSONRPCNotification;
use codex_app_server_protocol::ReadOnlyAccess;
use codex_app_server_protocol::RequestId;
use codex_app_server_protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use serde_json::json;
@@ -61,6 +63,16 @@ fn absolute_path(path: PathBuf) -> AbsolutePathBuf {
AbsolutePathBuf::try_from(path).expect("path should be absolute")
}
fn read_only_sandbox_policy(readable_root: PathBuf) -> SandboxPolicy {
SandboxPolicy::ReadOnly {
access: ReadOnlyAccess::Restricted {
include_platform_defaults: false,
readable_roots: vec![absolute_path(readable_root)],
},
network_access: false,
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn fs_get_metadata_returns_only_used_fields() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -71,6 +83,8 @@ async fn fs_get_metadata_returns_only_used_fields() -> Result<()> {
let request_id = mcp
.send_fs_get_metadata_request(codex_app_server_protocol::FsGetMetadataParams {
path: absolute_path(file_path.clone()),
sandbox_policy: None,
cwd: None,
})
.await?;
let response = timeout(
@@ -129,6 +143,8 @@ async fn fs_methods_cover_current_fs_utils_surface() -> Result<()> {
.send_fs_create_directory_request(codex_app_server_protocol::FsCreateDirectoryParams {
path: absolute_path(nested_dir.clone()),
recursive: None,
sandbox_policy: None,
cwd: None,
})
.await?;
timeout(
@@ -141,6 +157,8 @@ async fn fs_methods_cover_current_fs_utils_surface() -> Result<()> {
.send_fs_write_file_request(FsWriteFileParams {
path: absolute_path(nested_file.clone()),
data_base64: STANDARD.encode("hello from app-server"),
sandbox_policy: None,
cwd: None,
})
.await?;
timeout(
@@ -153,6 +171,8 @@ async fn fs_methods_cover_current_fs_utils_surface() -> Result<()> {
.send_fs_write_file_request(FsWriteFileParams {
path: absolute_path(source_file.clone()),
data_base64: STANDARD.encode("hello from source root"),
sandbox_policy: None,
cwd: None,
})
.await?;
timeout(
@@ -164,6 +184,8 @@ async fn fs_methods_cover_current_fs_utils_surface() -> Result<()> {
let read_request_id = mcp
.send_fs_read_file_request(codex_app_server_protocol::FsReadFileParams {
path: absolute_path(nested_file.clone()),
sandbox_policy: None,
cwd: None,
})
.await?;
let read_response: FsReadFileResponse = to_response(
@@ -185,6 +207,8 @@ async fn fs_methods_cover_current_fs_utils_surface() -> Result<()> {
source_path: absolute_path(nested_file.clone()),
destination_path: absolute_path(copy_file_path.clone()),
recursive: false,
sandbox_policy: None,
cwd: None,
})
.await?;
timeout(
@@ -202,6 +226,8 @@ async fn fs_methods_cover_current_fs_utils_surface() -> Result<()> {
source_path: absolute_path(source_dir.clone()),
destination_path: absolute_path(copied_dir.clone()),
recursive: true,
sandbox_policy: None,
cwd: None,
})
.await?;
timeout(
@@ -217,6 +243,8 @@ async fn fs_methods_cover_current_fs_utils_surface() -> Result<()> {
let read_directory_request_id = mcp
.send_fs_read_directory_request(codex_app_server_protocol::FsReadDirectoryParams {
path: absolute_path(source_dir.clone()),
sandbox_policy: None,
cwd: None,
})
.await?;
let readdir_response = timeout(
@@ -249,6 +277,8 @@ async fn fs_methods_cover_current_fs_utils_surface() -> Result<()> {
path: absolute_path(copied_dir.clone()),
recursive: None,
force: None,
sandbox_policy: None,
cwd: None,
})
.await?;
timeout(
@@ -275,6 +305,8 @@ async fn fs_write_file_accepts_base64_bytes() -> Result<()> {
.send_fs_write_file_request(FsWriteFileParams {
path: absolute_path(file_path.clone()),
data_base64: STANDARD.encode(bytes),
sandbox_policy: None,
cwd: None,
})
.await?;
timeout(
@@ -287,6 +319,8 @@ async fn fs_write_file_accepts_base64_bytes() -> Result<()> {
let read_request_id = mcp
.send_fs_read_file_request(codex_app_server_protocol::FsReadFileParams {
path: absolute_path(file_path),
sandbox_policy: None,
cwd: None,
})
.await?;
let read_response: FsReadFileResponse = to_response(
@@ -316,6 +350,8 @@ async fn fs_write_file_rejects_invalid_base64() -> Result<()> {
.send_fs_write_file_request(FsWriteFileParams {
path: absolute_path(file_path),
data_base64: "%%%".to_string(),
sandbox_policy: None,
cwd: None,
})
.await?;
let error = timeout(
@@ -335,6 +371,70 @@ async fn fs_write_file_rejects_invalid_base64() -> Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn fs_read_file_respects_sandbox_policy() -> Result<()> {
let codex_home = TempDir::new()?;
let allowed_dir = codex_home.path().join("allowed");
let file_path = allowed_dir.join("note.txt");
std::fs::create_dir_all(&allowed_dir)?;
std::fs::write(&file_path, "sandboxed hello")?;
let mut mcp = initialized_mcp(&codex_home).await?;
let request_id = mcp
.send_fs_read_file_request(codex_app_server_protocol::FsReadFileParams {
path: absolute_path(file_path),
sandbox_policy: Some(read_only_sandbox_policy(allowed_dir)),
cwd: None,
})
.await?;
let response: FsReadFileResponse = to_response(
timeout(
DEFAULT_READ_TIMEOUT,
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
)
.await??,
)?;
assert_eq!(
response,
FsReadFileResponse {
data_base64: STANDARD.encode("sandboxed hello"),
}
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn fs_write_file_rejects_path_outside_sandbox_policy() -> Result<()> {
let codex_home = TempDir::new()?;
let allowed_dir = codex_home.path().join("allowed");
let blocked_path = codex_home.path().join("blocked.txt");
std::fs::create_dir_all(&allowed_dir)?;
let mut mcp = initialized_mcp(&codex_home).await?;
let request_id = mcp
.send_fs_write_file_request(FsWriteFileParams {
path: absolute_path(blocked_path.clone()),
data_base64: STANDARD.encode("nope"),
sandbox_policy: Some(read_only_sandbox_policy(allowed_dir)),
cwd: None,
})
.await?;
expect_error_message(
&mut mcp,
request_id,
format!(
"fs/write is not permitted by sandbox policy for path {}",
blocked_path.display()
)
.as_str(),
)
.await?;
assert!(!blocked_path.exists());
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn fs_methods_reject_relative_paths() -> Result<()> {
let codex_home = TempDir::new()?;
@@ -471,6 +571,8 @@ async fn fs_copy_rejects_directory_without_recursive() -> Result<()> {
source_path: absolute_path(source_dir),
destination_path: absolute_path(codex_home.path().join("dest")),
recursive: false,
sandbox_policy: None,
cwd: None,
})
.await?;
let error = timeout(
@@ -498,6 +600,8 @@ async fn fs_copy_rejects_copying_directory_into_descendant() -> Result<()> {
source_path: absolute_path(source_dir.clone()),
destination_path: absolute_path(source_dir.join("nested").join("copy")),
recursive: true,
sandbox_policy: None,
cwd: None,
})
.await?;
let error = timeout(
@@ -529,6 +633,8 @@ async fn fs_copy_preserves_symlinks_in_recursive_copy() -> Result<()> {
source_path: absolute_path(source_dir),
destination_path: absolute_path(copied_dir.clone()),
recursive: true,
sandbox_policy: None,
cwd: None,
})
.await?;
timeout(
@@ -569,6 +675,8 @@ async fn fs_copy_ignores_unknown_special_files_in_recursive_copy() -> Result<()>
source_path: absolute_path(source_dir),
destination_path: absolute_path(copied_dir.clone()),
recursive: true,
sandbox_policy: None,
cwd: None,
})
.await?;
timeout(
@@ -606,6 +714,8 @@ async fn fs_copy_rejects_standalone_fifo_source() -> Result<()> {
source_path: absolute_path(fifo_path),
destination_path: absolute_path(codex_home.path().join("copied")),
recursive: false,
sandbox_policy: None,
cwd: None,
})
.await?;
expect_error_message(

View File

@@ -11,6 +11,7 @@ use app_test_support::format_with_current_shell_display;
use app_test_support::to_response;
use codex_app_server::INPUT_TOO_LARGE_ERROR_CODE;
use codex_app_server::INVALID_PARAMS_ERROR_CODE;
use codex_app_server_protocol::ApprovalsReviewer;
use codex_app_server_protocol::ByteRange;
use codex_app_server_protocol::ClientInfo;
use codex_app_server_protocol::CollabAgentStatus;
@@ -1527,6 +1528,7 @@ async fn turn_start_file_change_approval_v2() -> Result<()> {
text_elements: Vec::new(),
}],
cwd: Some(workspace.clone()),
approvals_reviewer: Some(ApprovalsReviewer::User),
..Default::default()
})
.await?;

View File

@@ -1,7 +1,9 @@
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::sync::LazyLock;
use similar::TextDiff;
use tree_sitter::Parser;
use tree_sitter::Query;
use tree_sitter::QueryCursor;
@@ -12,9 +14,11 @@ use crate::ApplyPatchAction;
use crate::ApplyPatchArgs;
use crate::ApplyPatchError;
use crate::ApplyPatchFileChange;
use crate::ApplyPatchFileSystem;
use crate::ApplyPatchFileUpdate;
use crate::IoError;
use crate::MaybeApplyPatchVerified;
use crate::derive_new_contents_from_chunks_with_fs;
use crate::parser::Hunk;
use crate::parser::ParseError;
use crate::parser::parse_patch;
@@ -149,17 +153,7 @@ pub fn maybe_parse_apply_patch_verified(argv: &[String], cwd: &Path) -> MaybeApp
hunks,
workdir,
}) => {
let effective_cwd = workdir
.as_ref()
.map(|dir| {
let path = Path::new(dir);
if path.is_absolute() {
path.to_path_buf()
} else {
cwd.join(path)
}
})
.unwrap_or_else(|| cwd.to_path_buf());
let effective_cwd = resolve_effective_cwd(cwd, workdir.as_deref());
let mut changes = HashMap::new();
for hunk in hunks {
let path = hunk.resolve_path(&effective_cwd);
@@ -216,6 +210,101 @@ pub fn maybe_parse_apply_patch_verified(argv: &[String], cwd: &Path) -> MaybeApp
}
}
/// Async variant of [`maybe_parse_apply_patch_verified`] that reads file
/// contents through an abstract filesystem instead of directly from `std::fs`.
pub async fn maybe_parse_apply_patch_verified_with_fs(
argv: &[String],
cwd: &Path,
fs: &dyn ApplyPatchFileSystem,
) -> MaybeApplyPatchVerified {
if let [body] = argv
&& parse_patch(body).is_ok()
{
return MaybeApplyPatchVerified::CorrectnessError(ApplyPatchError::ImplicitInvocation);
}
if let Some((_, script)) = parse_shell_script(argv)
&& parse_patch(script).is_ok()
{
return MaybeApplyPatchVerified::CorrectnessError(ApplyPatchError::ImplicitInvocation);
}
match maybe_parse_apply_patch(argv) {
MaybeApplyPatch::Body(ApplyPatchArgs {
patch,
hunks,
workdir,
}) => {
let effective_cwd = resolve_effective_cwd(cwd, workdir.as_deref());
let mut changes = HashMap::new();
for hunk in hunks {
let path = hunk.resolve_path(&effective_cwd);
match hunk {
Hunk::AddFile { contents, .. } => {
changes.insert(path, ApplyPatchFileChange::Add { content: contents });
}
Hunk::DeleteFile { .. } => {
let content = match fs.read_text(&path).await {
Ok(content) => content,
Err(err) => {
return MaybeApplyPatchVerified::CorrectnessError(err);
}
};
changes.insert(path, ApplyPatchFileChange::Delete { content });
}
Hunk::UpdateFile {
move_path, chunks, ..
} => {
let applied =
match derive_new_contents_from_chunks_with_fs(&path, &chunks, fs).await
{
Ok(applied) => applied,
Err(err) => {
return MaybeApplyPatchVerified::CorrectnessError(err);
}
};
let unified_diff =
TextDiff::from_lines(&applied.original_contents, &applied.new_contents)
.unified_diff()
.context_radius(1)
.to_string();
changes.insert(
path,
ApplyPatchFileChange::Update {
unified_diff,
move_path: move_path.map(|p| effective_cwd.join(p)),
new_content: applied.new_contents,
},
);
}
}
}
MaybeApplyPatchVerified::Body(ApplyPatchAction {
changes,
patch,
cwd: effective_cwd,
})
}
MaybeApplyPatch::ShellParseError(err) => MaybeApplyPatchVerified::ShellParseError(err),
MaybeApplyPatch::PatchParseError(err) => {
MaybeApplyPatchVerified::CorrectnessError(err.into())
}
MaybeApplyPatch::NotApplyPatch => MaybeApplyPatchVerified::NotApplyPatch,
}
}
fn resolve_effective_cwd(cwd: &Path, workdir: Option<&str>) -> PathBuf {
workdir
.map(Path::new)
.map(|path| {
if path.is_absolute() {
path.to_path_buf()
} else {
cwd.join(path)
}
})
.unwrap_or_else(|| cwd.to_path_buf())
}
/// Extract the heredoc body (and optional `cd` workdir) from a `bash -lc` script
/// that invokes the apply_patch tool using a heredoc.
///

View File

@@ -4,8 +4,10 @@ mod seek_sequence;
mod standalone_executable;
use std::collections::HashMap;
use std::future::Future;
use std::path::Path;
use std::path::PathBuf;
use std::pin::Pin;
use anyhow::Context;
use anyhow::Result;
@@ -18,6 +20,7 @@ use similar::TextDiff;
use thiserror::Error;
pub use invocation::maybe_parse_apply_patch_verified;
pub use invocation::maybe_parse_apply_patch_verified_with_fs;
pub use standalone_executable::main;
use crate::invocation::ExtractHeredocError;
@@ -50,6 +53,15 @@ pub enum ApplyPatchError {
ImplicitInvocation,
}
impl ApplyPatchError {
pub fn io_error(context: impl Into<String>, source: std::io::Error) -> Self {
Self::IoError(IoError {
context: context.into(),
source,
})
}
}
impl From<std::io::Error> for ApplyPatchError {
fn from(err: std::io::Error) -> Self {
ApplyPatchError::IoError(IoError {
@@ -179,6 +191,33 @@ impl ApplyPatchAction {
}
}
/// Filesystem operations required to verify and apply a patch.
///
/// This keeps the patch parser/diff logic independent from the host filesystem
/// so callers can provide sandboxed or remote-backed implementations.
pub trait ApplyPatchFileSystem: Send + Sync {
fn read_text<'a>(
&'a self,
path: &'a Path,
) -> Pin<Box<dyn Future<Output = std::result::Result<String, ApplyPatchError>> + Send + 'a>>;
fn write_text<'a>(
&'a self,
path: &'a Path,
contents: String,
) -> Pin<Box<dyn Future<Output = std::result::Result<(), ApplyPatchError>> + Send + 'a>>;
fn create_dir_all<'a>(
&'a self,
path: &'a Path,
) -> Pin<Box<dyn Future<Output = std::result::Result<(), ApplyPatchError>> + Send + 'a>>;
fn remove_file<'a>(
&'a self,
path: &'a Path,
) -> Pin<Box<dyn Future<Output = std::result::Result<(), ApplyPatchError>> + Send + 'a>>;
}
/// Applies the patch and prints the result to stdout/stderr.
pub fn apply_patch(
patch: &str,
@@ -268,6 +307,7 @@ pub fn apply_hunks(
/// Applies each parsed patch hunk to the filesystem.
/// Returns an error if any of the changes could not be applied.
/// Tracks file paths affected by applying a patch.
#[derive(Debug, PartialEq, Eq)]
pub struct AffectedPaths {
pub added: Vec<PathBuf>,
pub modified: Vec<PathBuf>,
@@ -359,6 +399,14 @@ fn derive_new_contents_from_chunks(
}
};
derive_new_contents_from_text(&original_contents, path, chunks)
}
pub(crate) fn derive_new_contents_from_text(
original_contents: &str,
path: &Path,
chunks: &[UpdateFileChunk],
) -> std::result::Result<AppliedPatch, ApplyPatchError> {
let mut original_lines: Vec<String> = original_contents.split('\n').map(String::from).collect();
// Drop the trailing empty element that results from the final newline so
@@ -375,11 +423,88 @@ fn derive_new_contents_from_chunks(
}
let new_contents = new_lines.join("\n");
Ok(AppliedPatch {
original_contents,
original_contents: original_contents.to_string(),
new_contents,
})
}
pub(crate) async fn derive_new_contents_from_chunks_with_fs(
path: &Path,
chunks: &[UpdateFileChunk],
fs: &dyn ApplyPatchFileSystem,
) -> std::result::Result<AppliedPatch, ApplyPatchError> {
let original_contents = match fs.read_text(path).await {
Ok(contents) => contents,
Err(ApplyPatchError::IoError(IoError { source, .. })) => {
return Err(ApplyPatchError::IoError(IoError {
context: format!("Failed to read file to update {}", path.display()),
source,
}));
}
Err(err) => return Err(err),
};
derive_new_contents_from_text(&original_contents, path, chunks)
}
/// Apply a verified patch action through an abstract filesystem.
pub async fn apply_action_with_fs(
action: &ApplyPatchAction,
fs: &dyn ApplyPatchFileSystem,
) -> std::result::Result<AffectedPaths, ApplyPatchError> {
if action.is_empty() {
return Err(ApplyPatchError::IoError(IoError {
context: "No files were modified.".to_string(),
source: std::io::Error::other("empty patch"),
}));
}
let mut added = Vec::new();
let mut modified = Vec::new();
let mut deleted = Vec::new();
for (path, change) in action.changes() {
match change {
ApplyPatchFileChange::Add { content } => {
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
fs.create_dir_all(parent).await?;
}
fs.write_text(path, content.clone()).await?;
added.push(path.clone());
}
ApplyPatchFileChange::Delete { .. } => {
fs.remove_file(path).await?;
deleted.push(path.clone());
}
ApplyPatchFileChange::Update {
new_content,
move_path,
..
} => {
if let Some(dest) = move_path {
if let Some(parent) = dest.parent()
&& !parent.as_os_str().is_empty()
{
fs.create_dir_all(parent).await?;
}
fs.write_text(dest, new_content.clone()).await?;
fs.remove_file(path).await?;
modified.push(dest.clone());
} else {
fs.write_text(path, new_content.clone()).await?;
modified.push(path.clone());
}
}
}
}
Ok(AffectedPaths {
added,
modified,
deleted,
})
}
/// Compute a list of replacements needed to transform `original_lines` into the
/// new lines, given the patch `chunks`. Each replacement is returned as
/// `(start_index, old_len, new_lines)`.
@@ -556,9 +681,120 @@ mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::fs;
use std::future::Future;
use std::pin::Pin;
use std::string::ToString;
use std::sync::Arc;
use std::task::Context;
use std::task::Poll;
use std::task::Wake;
use std::task::Waker;
use tempfile::tempdir;
#[derive(Default)]
struct NoopWake;
impl Wake for NoopWake {
fn wake(self: Arc<Self>) {}
}
fn block_on<F: Future>(future: F) -> F::Output {
let waker = Waker::from(Arc::new(NoopWake));
let mut context = Context::from_waker(&waker);
let mut future = Pin::from(Box::new(future));
loop {
match future.as_mut().poll(&mut context) {
Poll::Ready(output) => return output,
Poll::Pending => std::thread::yield_now(),
}
}
}
#[derive(Clone, Default)]
struct TestApplyPatchFileSystem {
files: std::sync::Arc<std::sync::Mutex<HashMap<PathBuf, String>>>,
removed: std::sync::Arc<std::sync::Mutex<Vec<PathBuf>>>,
directories: std::sync::Arc<std::sync::Mutex<Vec<PathBuf>>>,
}
impl TestApplyPatchFileSystem {
fn with_file(path: &Path, content: &str) -> Self {
let file_system = Self::default();
file_system
.files
.lock()
.expect("lock files")
.insert(path.to_path_buf(), content.to_string());
file_system
}
}
impl ApplyPatchFileSystem for TestApplyPatchFileSystem {
fn read_text<'a>(
&'a self,
path: &'a Path,
) -> Pin<Box<dyn Future<Output = std::result::Result<String, ApplyPatchError>> + Send + 'a>>
{
Box::pin(async move {
self.files
.lock()
.expect("lock files")
.get(path)
.cloned()
.ok_or_else(|| {
ApplyPatchError::io_error(
format!("missing test file {}", path.display()),
std::io::Error::new(std::io::ErrorKind::NotFound, "missing"),
)
})
})
}
fn write_text<'a>(
&'a self,
path: &'a Path,
contents: String,
) -> Pin<Box<dyn Future<Output = std::result::Result<(), ApplyPatchError>> + Send + 'a>>
{
Box::pin(async move {
self.files
.lock()
.expect("lock files")
.insert(path.to_path_buf(), contents);
Ok(())
})
}
fn create_dir_all<'a>(
&'a self,
path: &'a Path,
) -> Pin<Box<dyn Future<Output = std::result::Result<(), ApplyPatchError>> + Send + 'a>>
{
Box::pin(async move {
self.directories
.lock()
.expect("lock dirs")
.push(path.to_path_buf());
Ok(())
})
}
fn remove_file<'a>(
&'a self,
path: &'a Path,
) -> Pin<Box<dyn Future<Output = std::result::Result<(), ApplyPatchError>> + Send + 'a>>
{
Box::pin(async move {
self.files.lock().expect("lock files").remove(path);
self.removed
.lock()
.expect("lock removed")
.push(path.to_path_buf());
Ok(())
})
}
}
/// Helper to construct a patch with the given body.
fn wrap_patch(body: &str) -> String {
format!("*** Begin Patch\n{body}\n*** End Patch")
@@ -1071,4 +1307,83 @@ g
let result = apply_patch(&patch, &mut stdout, &mut stderr);
assert!(result.is_err());
}
#[test]
fn apply_action_with_fs_updates_file_without_touching_host_fs() {
let path = PathBuf::from("/virtual/update.txt");
let patch = wrap_patch(&format!(
r#"*** Update File: {}
@@
-before
+after"#,
path.display()
));
let argv = vec!["apply_patch".to_string(), patch];
let fs = TestApplyPatchFileSystem::with_file(&path, "before\n");
let action = match block_on(maybe_parse_apply_patch_verified_with_fs(
&argv,
Path::new("/"),
&fs,
)) {
MaybeApplyPatchVerified::Body(action) => action,
other => panic!("expected patch body, got {other:?}"),
};
let affected = block_on(apply_action_with_fs(&action, &fs)).expect("apply action");
assert_eq!(affected.modified, vec![path.clone()]);
assert_eq!(
fs.files.lock().expect("lock files").get(&path).cloned(),
Some("after\n".to_string())
);
}
#[test]
fn maybe_parse_apply_patch_verified_with_fs_uses_abstract_fs_for_update_and_delete() {
let update_path = PathBuf::from("/virtual/update.txt");
let delete_path = PathBuf::from("/virtual/delete.txt");
let patch = wrap_patch(&format!(
r#"*** Update File: {}
@@
-before
+after
*** Delete File: {}"#,
update_path.display(),
delete_path.display()
));
let argv = vec!["apply_patch".to_string(), patch];
let fs = TestApplyPatchFileSystem::default();
fs.files
.lock()
.expect("lock files")
.insert(update_path.clone(), "before\n".to_string());
fs.files
.lock()
.expect("lock files")
.insert(delete_path.clone(), "gone\n".to_string());
let action = match block_on(maybe_parse_apply_patch_verified_with_fs(
&argv,
Path::new("/"),
&fs,
)) {
MaybeApplyPatchVerified::Body(action) => action,
other => panic!("expected patch body, got {other:?}"),
};
assert_eq!(
action.changes().get(&delete_path),
Some(&ApplyPatchFileChange::Delete {
content: "gone\n".to_string()
})
);
assert_eq!(
action.changes().get(&update_path),
Some(&ApplyPatchFileChange::Update {
unified_diff: "@@ -1 +1 @@\n-before\n+after\n".to_string(),
move_path: None,
new_content: "after\n".to_string(),
})
);
}
}

View File

@@ -4,11 +4,23 @@ use crate::safety::SafetyCheck;
use crate::safety::assess_patch_safety;
use crate::tools::sandboxing::ExecApprovalRequirement;
use codex_apply_patch::ApplyPatchAction;
use codex_apply_patch::ApplyPatchError;
use codex_apply_patch::ApplyPatchFileChange;
use codex_apply_patch::ApplyPatchFileSystem;
use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::FileSystemOperationOptions;
use codex_exec_server::RemoveOptions;
use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::FileSystemSandboxPolicy;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::future::Future;
use std::io;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
pub(crate) enum InternalApplyPatchInvocation {
/// The `apply_patch` call was handled programmatically, without any sort
@@ -18,21 +30,166 @@ pub(crate) enum InternalApplyPatchInvocation {
/// The `apply_patch` call was approved, either automatically because it
/// appears that it should be allowed based on the user's sandbox policy
/// *or* because the user explicitly approved it. In either case, we use
/// exec with [`codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1`] to realize
/// the `apply_patch` call,
/// but [`ApplyPatchExec::auto_approved`] is used to determine the sandbox
/// used with the `exec()`.
DelegateToExec(ApplyPatchExec),
/// or because the user explicitly approved it. The tool runtime realizes
/// the verified patch through the environment filesystem.
DelegateToRuntime(ApprovedApplyPatch),
}
#[derive(Debug)]
pub(crate) struct ApplyPatchExec {
pub(crate) struct ApprovedApplyPatch {
pub(crate) action: ApplyPatchAction,
pub(crate) auto_approved: bool,
pub(crate) exec_approval_requirement: ExecApprovalRequirement,
}
pub(crate) struct EnvironmentApplyPatchFileSystem {
file_system: Arc<dyn ExecutorFileSystem>,
operation_options: FileSystemOperationOptions,
}
impl EnvironmentApplyPatchFileSystem {
pub(crate) fn for_verification(file_system: Arc<dyn ExecutorFileSystem>, cwd: PathBuf) -> Self {
Self {
file_system,
operation_options: FileSystemOperationOptions {
cwd: absolute_path(cwd.as_path()).ok(),
..FileSystemOperationOptions::default()
},
}
}
pub(crate) fn for_apply(
file_system: Arc<dyn ExecutorFileSystem>,
cwd: PathBuf,
sandbox_policy: SandboxPolicy,
) -> Self {
Self {
file_system,
operation_options: FileSystemOperationOptions {
sandbox_policy: Some(sandbox_policy),
cwd: absolute_path(cwd.as_path()).ok(),
},
}
}
}
impl ApplyPatchFileSystem for EnvironmentApplyPatchFileSystem {
fn read_text<'a>(
&'a self,
path: &'a std::path::Path,
) -> Pin<Box<dyn Future<Output = std::result::Result<String, ApplyPatchError>> + Send + 'a>>
{
Box::pin(async move {
let path = absolute_path(path)?;
let bytes = self
.file_system
.read_file_with_options(&path, &self.operation_options)
.await
.map_err(|source| {
ApplyPatchError::io_error(format!("Failed to read {}", path.display()), source)
})?;
String::from_utf8(bytes).map_err(|source| {
ApplyPatchError::io_error(
format!("Failed to decode UTF-8 for {}", path.display()),
io::Error::new(io::ErrorKind::InvalidData, source.to_string()),
)
})
})
}
fn write_text<'a>(
&'a self,
path: &'a std::path::Path,
contents: String,
) -> Pin<Box<dyn Future<Output = std::result::Result<(), ApplyPatchError>> + Send + 'a>> {
Box::pin(async move {
let path = absolute_path(path)?;
let contents = contents.into_bytes();
self.file_system
.write_file_with_options(&path, contents, &self.operation_options)
.await
.map_err(|source| {
ApplyPatchError::io_error(
format!("Failed to write file {}", path.display()),
source,
)
})
})
}
fn create_dir_all<'a>(
&'a self,
path: &'a std::path::Path,
) -> Pin<Box<dyn Future<Output = std::result::Result<(), ApplyPatchError>> + Send + 'a>> {
Box::pin(async move {
let path = absolute_path(path)?;
self.file_system
.create_directory_with_options(
&path,
CreateDirectoryOptions { recursive: true },
&self.operation_options,
)
.await
.map_err(|source| {
ApplyPatchError::io_error(
format!("Failed to create parent directories for {}", path.display()),
source,
)
})
})
}
fn remove_file<'a>(
&'a self,
path: &'a std::path::Path,
) -> Pin<Box<dyn Future<Output = std::result::Result<(), ApplyPatchError>> + Send + 'a>> {
Box::pin(async move {
let path = absolute_path(path)?;
let remove_options = RemoveOptions {
recursive: false,
force: false,
};
self.file_system
.remove_with_options(&path, remove_options, &self.operation_options)
.await
.map_err(|source| {
ApplyPatchError::io_error(
format!("Failed to delete file {}", path.display()),
source,
)
})
})
}
}
fn absolute_path(path: &std::path::Path) -> std::result::Result<AbsolutePathBuf, ApplyPatchError> {
let path = AbsolutePathBuf::from_absolute_path(path).map_err(|error| {
ApplyPatchError::io_error(
format!("Expected absolute path for apply_patch: {}", path.display()),
io::Error::new(io::ErrorKind::InvalidInput, error.to_string()),
)
})?;
Ok(normalize_existing_ancestor_path(path))
}
fn normalize_existing_ancestor_path(path: AbsolutePathBuf) -> AbsolutePathBuf {
let raw_path = path.to_path_buf();
for ancestor in raw_path.ancestors() {
let Ok(canonical_ancestor) = ancestor.canonicalize() else {
continue;
};
let Ok(suffix) = raw_path.strip_prefix(ancestor) else {
continue;
};
if let Ok(normalized_path) =
AbsolutePathBuf::from_absolute_path(canonical_ancestor.join(suffix))
{
return normalized_path;
}
}
path
}
pub(crate) async fn apply_patch(
turn_context: &TurnContext,
file_system_sandbox_policy: &FileSystemSandboxPolicy,
@@ -49,7 +206,7 @@ pub(crate) async fn apply_patch(
SafetyCheck::AutoApprove {
user_explicitly_approved,
..
} => InternalApplyPatchInvocation::DelegateToExec(ApplyPatchExec {
} => InternalApplyPatchInvocation::DelegateToRuntime(ApprovedApplyPatch {
action,
auto_approved: !user_explicitly_approved,
exec_approval_requirement: ExecApprovalRequirement::Skip {
@@ -61,7 +218,7 @@ pub(crate) async fn apply_patch(
// Delegate the approval prompt (including cached approvals) to the
// tool runtime, consistent with how shell/unified_exec approvals
// are orchestrator-driven.
InternalApplyPatchInvocation::DelegateToExec(ApplyPatchExec {
InternalApplyPatchInvocation::DelegateToRuntime(ApprovedApplyPatch {
action,
auto_approved: false,
exec_approval_requirement: ExecApprovalRequirement::NeedsApproval {

View File

@@ -1,7 +1,17 @@
use super::*;
use async_trait::async_trait;
use codex_exec_server::CopyOptions;
use codex_exec_server::FileMetadata;
use codex_exec_server::FileSystemOperationOptions;
use codex_exec_server::ReadDirectoryEntry;
use codex_protocol::protocol::SandboxPolicy;
use pretty_assertions::assert_eq;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use tempfile::tempdir;
use tokio::io;
#[test]
fn convert_apply_patch_maps_add_variant() {
@@ -19,3 +29,226 @@ fn convert_apply_patch_maps_add_variant() {
})
);
}
#[cfg(unix)]
#[test]
fn absolute_path_normalizes_existing_symlink_ancestor() {
use std::os::unix::fs::symlink;
let tmp = tempdir().expect("tempdir");
let real_root = tmp.path().join("real");
let link_root = tmp.path().join("link");
std::fs::create_dir_all(&real_root).expect("create real root");
symlink(&real_root, &link_root).expect("create symlink");
let path = link_root.join("nested").join("file.txt");
let got = absolute_path(path.as_path()).expect("normalize absolute path");
let expected = AbsolutePathBuf::from_absolute_path(
real_root
.canonicalize()
.expect("canonicalize real root")
.join("nested/file.txt"),
)
.expect("expected normalized path");
assert_eq!(got, expected);
}
#[derive(Default)]
struct RecordingExecutorFileSystem {
raw_reads: Mutex<Vec<PathBuf>>,
option_reads: Mutex<Vec<FileSystemOperationOptions>>,
raw_writes: Mutex<Vec<PathBuf>>,
option_writes: Mutex<Vec<FileSystemOperationOptions>>,
raw_creates: Mutex<Vec<PathBuf>>,
option_creates: Mutex<Vec<FileSystemOperationOptions>>,
raw_removes: Mutex<Vec<PathBuf>>,
option_removes: Mutex<Vec<FileSystemOperationOptions>>,
}
#[async_trait]
impl ExecutorFileSystem for RecordingExecutorFileSystem {
async fn read_file(&self, path: &AbsolutePathBuf) -> io::Result<Vec<u8>> {
self.raw_reads
.lock()
.expect("raw_reads lock")
.push(path.as_path().to_path_buf());
Ok(b"before\n".to_vec())
}
async fn read_file_with_options(
&self,
path: &AbsolutePathBuf,
options: &FileSystemOperationOptions,
) -> io::Result<Vec<u8>> {
self.option_reads
.lock()
.expect("option_reads lock")
.push(options.clone());
self.read_file(path).await
}
async fn write_file(&self, path: &AbsolutePathBuf, _contents: Vec<u8>) -> io::Result<()> {
self.raw_writes
.lock()
.expect("raw_writes lock")
.push(path.as_path().to_path_buf());
Ok(())
}
async fn write_file_with_options(
&self,
path: &AbsolutePathBuf,
contents: Vec<u8>,
options: &FileSystemOperationOptions,
) -> io::Result<()> {
self.option_writes
.lock()
.expect("option_writes lock")
.push(options.clone());
self.write_file(path, contents).await
}
async fn create_directory(
&self,
path: &AbsolutePathBuf,
_options: CreateDirectoryOptions,
) -> io::Result<()> {
self.raw_creates
.lock()
.expect("raw_creates lock")
.push(path.as_path().to_path_buf());
Ok(())
}
async fn create_directory_with_options(
&self,
path: &AbsolutePathBuf,
options: CreateDirectoryOptions,
fs_options: &FileSystemOperationOptions,
) -> io::Result<()> {
self.option_creates
.lock()
.expect("option_creates lock")
.push(fs_options.clone());
self.create_directory(path, options).await
}
async fn get_metadata(&self, _path: &AbsolutePathBuf) -> io::Result<FileMetadata> {
Err(io::Error::other("unused"))
}
async fn read_directory(&self, _path: &AbsolutePathBuf) -> io::Result<Vec<ReadDirectoryEntry>> {
Err(io::Error::other("unused"))
}
async fn remove(&self, path: &AbsolutePathBuf, _options: RemoveOptions) -> io::Result<()> {
self.raw_removes
.lock()
.expect("raw_removes lock")
.push(path.as_path().to_path_buf());
Ok(())
}
async fn remove_with_options(
&self,
path: &AbsolutePathBuf,
options: RemoveOptions,
fs_options: &FileSystemOperationOptions,
) -> io::Result<()> {
self.option_removes
.lock()
.expect("option_removes lock")
.push(fs_options.clone());
self.remove(path, options).await
}
async fn copy(
&self,
_source_path: &AbsolutePathBuf,
_destination_path: &AbsolutePathBuf,
_options: CopyOptions,
) -> io::Result<()> {
Err(io::Error::other("unused"))
}
}
#[tokio::test]
async fn verification_filesystem_uses_default_operation_options() {
let file_system = Arc::new(RecordingExecutorFileSystem::default());
let tmp = tempdir().expect("tmp");
let cwd = tmp.path().join("apply-patch-verification");
let path = tmp.path().join("apply-patch-verification.txt");
let adapter =
EnvironmentApplyPatchFileSystem::for_verification(file_system.clone(), cwd.clone());
let content = adapter
.read_text(path.as_path())
.await
.expect("read through adapter");
assert_eq!(content, "before\n");
assert_eq!(
file_system
.option_reads
.lock()
.expect("option_reads lock")
.as_slice(),
[FileSystemOperationOptions {
sandbox_policy: None,
cwd: Some(absolute_path(cwd.as_path()).expect("normalized cwd")),
}]
);
assert_eq!(
file_system
.raw_reads
.lock()
.expect("raw_reads lock")
.as_slice(),
[absolute_path(path.as_path())
.expect("normalized path")
.into_path_buf()]
);
}
#[tokio::test]
async fn apply_filesystem_uses_sandbox_options() {
let file_system = Arc::new(RecordingExecutorFileSystem::default());
let sandbox_policy = SandboxPolicy::new_workspace_write_policy();
let tmp = tempdir().expect("tmp");
let cwd = tmp.path().join("apply-patch-sandboxed");
let path = cwd.join("new.txt");
let action = ApplyPatchAction::new_add_for_test(&path, "hello".to_string());
let adapter = EnvironmentApplyPatchFileSystem::for_apply(
file_system.clone(),
cwd.clone(),
sandbox_policy.clone(),
);
codex_apply_patch::apply_action_with_fs(&action, &adapter)
.await
.expect("apply patch through adapter");
assert_eq!(
file_system
.option_creates
.lock()
.expect("option_creates lock")
.as_slice(),
[FileSystemOperationOptions {
sandbox_policy: Some(sandbox_policy.clone()),
cwd: Some(absolute_path(cwd.as_path()).expect("normalized cwd")),
}]
);
assert_eq!(
file_system
.option_writes
.lock()
.expect("option_writes lock")
.as_slice(),
[FileSystemOperationOptions {
sandbox_policy: Some(sandbox_policy),
cwd: Some(absolute_path(cwd.as_path()).expect("normalized cwd")),
}]
);
}

View File

@@ -1,6 +1,7 @@
use std::path::Path;
use crate::apply_patch;
use crate::apply_patch::EnvironmentApplyPatchFileSystem;
use crate::apply_patch::InternalApplyPatchInvocation;
use crate::apply_patch::convert_apply_patch_to_protocol;
use crate::codex::Session;
@@ -25,6 +26,7 @@ use codex_apply_patch::ApplyPatchAction;
use codex_apply_patch::ApplyPatchFileChange;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_sandboxing::policy_transforms::EffectiveSandboxPermissions;
use codex_sandboxing::policy_transforms::effective_file_system_sandbox_policy;
use codex_sandboxing::policy_transforms::merge_permission_profiles;
use codex_sandboxing::policy_transforms::normalize_additional_permissions;
@@ -167,7 +169,7 @@ impl ToolHandler for ApplyPatchHandler {
// Avoid building temporary ExecParams/command vectors; derive directly from inputs.
let cwd = turn.cwd.clone();
let command = vec!["apply_patch".to_string(), patch_input.clone()];
match codex_apply_patch::maybe_parse_apply_patch_verified(&command, &cwd) {
match parse_apply_patch_verified(turn.as_ref(), &command, &cwd).await {
codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => {
let (file_paths, effective_additional_permissions, file_system_sandbox_policy) =
effective_patch_permissions(session.as_ref(), turn.as_ref(), &changes).await;
@@ -178,7 +180,7 @@ impl ToolHandler for ApplyPatchHandler {
let content = item?;
Ok(ApplyPatchToolOutput::from_text(content))
}
InternalApplyPatchInvocation::DelegateToExec(apply) => {
InternalApplyPatchInvocation::DelegateToRuntime(apply) => {
let changes = convert_apply_patch_to_protocol(&apply.action);
let emitter =
ToolEmitter::apply_patch(changes.clone(), apply.auto_approved);
@@ -194,12 +196,16 @@ impl ToolHandler for ApplyPatchHandler {
action: apply.action,
file_paths,
changes,
sandbox_policy: EffectiveSandboxPermissions::new(
turn.sandbox_policy.get(),
effective_additional_permissions
.additional_permissions
.as_ref(),
)
.sandbox_policy,
exec_approval_requirement: apply.exec_approval_requirement,
additional_permissions: effective_additional_permissions
.additional_permissions,
permissions_preapproved: effective_additional_permissions
.permissions_preapproved,
timeout_ms: None,
};
let mut orchestrator = ToolOrchestrator::new();
@@ -255,14 +261,14 @@ impl ToolHandler for ApplyPatchHandler {
pub(crate) async fn intercept_apply_patch(
command: &[String],
cwd: &Path,
timeout_ms: Option<u64>,
_timeout_ms: Option<u64>,
session: Arc<Session>,
turn: Arc<TurnContext>,
tracker: Option<&SharedTurnDiffTracker>,
call_id: &str,
tool_name: &str,
) -> Result<Option<FunctionToolOutput>, FunctionCallError> {
match codex_apply_patch::maybe_parse_apply_patch_verified(command, cwd) {
match parse_apply_patch_verified(turn.as_ref(), command, cwd).await {
codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => {
session
.record_model_warning(
@@ -281,7 +287,7 @@ pub(crate) async fn intercept_apply_patch(
let content = item?;
Ok(Some(FunctionToolOutput::from_text(content, Some(true))))
}
InternalApplyPatchInvocation::DelegateToExec(apply) => {
InternalApplyPatchInvocation::DelegateToRuntime(apply) => {
let changes = convert_apply_patch_to_protocol(&apply.action);
let emitter = ToolEmitter::apply_patch(changes.clone(), apply.auto_approved);
let event_ctx = ToolEventCtx::new(
@@ -296,12 +302,16 @@ pub(crate) async fn intercept_apply_patch(
action: apply.action,
file_paths: approval_keys,
changes,
sandbox_policy: EffectiveSandboxPermissions::new(
turn.sandbox_policy.get(),
effective_additional_permissions
.additional_permissions
.as_ref(),
)
.sandbox_policy,
exec_approval_requirement: apply.exec_approval_requirement,
additional_permissions: effective_additional_permissions
.additional_permissions,
permissions_preapproved: effective_additional_permissions
.permissions_preapproved,
timeout_ms,
};
let mut orchestrator = ToolOrchestrator::new();
@@ -346,6 +356,18 @@ pub(crate) async fn intercept_apply_patch(
}
}
async fn parse_apply_patch_verified(
turn: &TurnContext,
command: &[String],
cwd: &Path,
) -> codex_apply_patch::MaybeApplyPatchVerified {
let fs = EnvironmentApplyPatchFileSystem::for_verification(
turn.environment.get_filesystem(),
turn.cwd.to_path_buf(),
);
codex_apply_patch::maybe_parse_apply_patch_verified_with_fs(command, cwd, &fs).await
}
#[cfg(test)]
#[path = "apply_patch_tests.rs"]
mod tests;

View File

@@ -1,15 +1,12 @@
//! Apply Patch runtime: executes verified patches under the orchestrator.
//!
//! Assumes `apply_patch` verification/approval happened upstream. Reuses that
//! decision to avoid re-prompting, builds the self-invocation command for
//! `codex --codex-run-as-apply-patch`, and runs under the current
//! `SandboxAttempt` with a minimal environment.
use crate::exec::ExecCapturePolicy;
//! decision to avoid re-prompting, then applies the verified action directly
//! through the turn environment's filesystem with the effective sandbox policy.
use crate::apply_patch::EnvironmentApplyPatchFileSystem;
use crate::guardian::GuardianApprovalRequest;
use crate::guardian::review_approval_request;
use crate::guardian::routes_approval_to_guardian;
use crate::sandboxing::ExecOptions;
use crate::sandboxing::execute_env;
use crate::tools::sandboxing::Approvable;
use crate::tools::sandboxing::ApprovalCtx;
use crate::tools::sandboxing::ExecApprovalRequirement;
@@ -20,28 +17,30 @@ use crate::tools::sandboxing::ToolError;
use crate::tools::sandboxing::ToolRuntime;
use crate::tools::sandboxing::with_cached_approval;
use codex_apply_patch::ApplyPatchAction;
use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1;
use codex_protocol::exec_output::ExecToolCallOutput;
use codex_protocol::models::PermissionProfile;
use codex_protocol::exec_output::StreamOutput;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
use codex_protocol::protocol::ExecCommandOutputDeltaEvent;
use codex_protocol::protocol::ExecOutputStream;
use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::ReviewDecision;
use codex_sandboxing::SandboxCommand;
use codex_protocol::protocol::SandboxPolicy;
use codex_sandboxing::SandboxablePreference;
use codex_utils_absolute_path::AbsolutePathBuf;
use futures::future::BoxFuture;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;
#[derive(Debug)]
pub struct ApplyPatchRequest {
pub action: ApplyPatchAction,
pub file_paths: Vec<AbsolutePathBuf>,
pub changes: std::collections::HashMap<PathBuf, FileChange>,
pub sandbox_policy: SandboxPolicy,
pub exec_approval_requirement: ExecApprovalRequirement,
pub additional_permissions: Option<PermissionProfile>,
pub permissions_preapproved: bool,
pub timeout_ms: Option<u64>,
}
#[derive(Default)]
@@ -64,59 +63,75 @@ impl ApplyPatchRuntime {
}
}
#[cfg(target_os = "windows")]
fn build_sandbox_command(
async fn run_with_environment_fs(
req: &ApplyPatchRequest,
codex_home: &std::path::Path,
) -> Result<SandboxCommand, ToolError> {
Ok(Self::build_sandbox_command_with_program(
req,
codex_windows_sandbox::resolve_current_exe_for_launch(codex_home, "codex.exe"),
))
}
#[cfg(not(target_os = "windows"))]
fn build_sandbox_command(
req: &ApplyPatchRequest,
codex_self_exe: Option<&PathBuf>,
) -> Result<SandboxCommand, ToolError> {
let exe = Self::resolve_apply_patch_program(codex_self_exe)?;
Ok(Self::build_sandbox_command_with_program(req, exe))
}
#[cfg(not(target_os = "windows"))]
fn resolve_apply_patch_program(codex_self_exe: Option<&PathBuf>) -> Result<PathBuf, ToolError> {
if let Some(path) = codex_self_exe {
return Ok(path.clone());
fs: EnvironmentApplyPatchFileSystem,
ctx: &ToolCtx,
) -> Result<ExecToolCallOutput, ToolError> {
let affected: codex_apply_patch::AffectedPaths =
codex_apply_patch::apply_action_with_fs(&req.action, &fs)
.await
.map_err(|err| ToolError::Rejected(err.to_string()))?;
let affected = relativize_affected_paths(&affected, &req.action.cwd);
let mut stdout = Vec::new();
codex_apply_patch::print_summary(&affected, &mut stdout)
.map_err(|err| ToolError::Rejected(err.to_string()))?;
let stdout = String::from_utf8(stdout).map_err(|err| {
ToolError::Rejected(format!("apply_patch wrote non-UTF-8 output: {err}"))
})?;
if !stdout.is_empty() {
ctx.session
.send_event(
ctx.turn.as_ref(),
EventMsg::ExecCommandOutputDelta(ExecCommandOutputDeltaEvent {
call_id: ctx.call_id.clone(),
stream: ExecOutputStream::Stdout,
chunk: stdout.clone().into_bytes(),
}),
)
.await;
}
std::env::current_exe()
.map_err(|e| ToolError::Rejected(format!("failed to determine codex exe: {e}")))
}
fn build_sandbox_command_with_program(req: &ApplyPatchRequest, exe: PathBuf) -> SandboxCommand {
SandboxCommand {
program: exe.into_os_string(),
args: vec![
CODEX_CORE_APPLY_PATCH_ARG1.to_string(),
req.action.patch.clone(),
],
cwd: req.action.cwd.clone(),
// Run apply_patch with a minimal environment for determinism and to avoid leaks.
env: HashMap::new(),
additional_permissions: req.additional_permissions.clone(),
}
}
fn stdout_stream(ctx: &ToolCtx) -> Option<crate::exec::StdoutStream> {
Some(crate::exec::StdoutStream {
sub_id: ctx.turn.sub_id.clone(),
call_id: ctx.call_id.clone(),
tx_event: ctx.session.get_tx_event(),
Ok(ExecToolCallOutput {
exit_code: 0,
stdout: StreamOutput::new(stdout.clone()),
stderr: StreamOutput::new(String::new()),
aggregated_output: StreamOutput::new(stdout),
duration: Duration::ZERO,
timed_out: false,
})
}
}
fn relativize_affected_paths(
affected: &codex_apply_patch::AffectedPaths,
cwd: &Path,
) -> codex_apply_patch::AffectedPaths {
codex_apply_patch::AffectedPaths {
added: affected
.added
.iter()
.map(|path| summary_path(path, cwd))
.collect(),
modified: affected
.modified
.iter()
.map(|path| summary_path(path, cwd))
.collect(),
deleted: affected
.deleted
.iter()
.map(|path| summary_path(path, cwd))
.collect(),
}
}
fn summary_path(path: &Path, cwd: &Path) -> PathBuf {
match path.strip_prefix(cwd) {
Ok(relative) if !relative.as_os_str().is_empty() => relative.to_path_buf(),
_ => path.to_path_buf(),
}
}
impl Sandboxable for ApplyPatchRuntime {
fn sandbox_preference(&self) -> SandboxablePreference {
SandboxablePreference::Auto
@@ -208,24 +223,15 @@ impl ToolRuntime<ApplyPatchRequest, ExecToolCallOutput> for ApplyPatchRuntime {
async fn run(
&mut self,
req: &ApplyPatchRequest,
attempt: &SandboxAttempt<'_>,
_attempt: &SandboxAttempt<'_>,
ctx: &ToolCtx,
) -> Result<ExecToolCallOutput, ToolError> {
#[cfg(target_os = "windows")]
let command = Self::build_sandbox_command(req, &ctx.turn.config.codex_home)?;
#[cfg(not(target_os = "windows"))]
let command = Self::build_sandbox_command(req, ctx.turn.codex_self_exe.as_ref())?;
let options = ExecOptions {
expiration: req.timeout_ms.into(),
capture_policy: ExecCapturePolicy::ShellTool,
};
let env = attempt
.env_for(command, options, /*network*/ None)
.map_err(|err| ToolError::Codex(err.into()))?;
let out = execute_env(env, Self::stdout_stream(ctx))
.await
.map_err(ToolError::Codex)?;
Ok(out)
let fs = EnvironmentApplyPatchFileSystem::for_apply(
ctx.turn.environment.get_filesystem(),
req.action.cwd.clone(),
req.sandbox_policy.clone(),
);
Self::run_with_environment_fs(req, fs, ctx).await
}
}

View File

@@ -1,8 +1,9 @@
use super::*;
use codex_protocol::protocol::GranularApprovalConfig;
use codex_protocol::protocol::SandboxPolicy;
use pretty_assertions::assert_eq;
use std::collections::HashMap;
#[cfg(not(target_os = "windows"))]
use std::path::Path;
use std::path::PathBuf;
#[test]
@@ -46,13 +47,12 @@ fn guardian_review_request_includes_patch_context() {
content: "hello".to_string(),
},
)]),
sandbox_policy: SandboxPolicy::DangerFullAccess,
exec_approval_requirement: ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: None,
},
additional_permissions: None,
permissions_preapproved: false,
timeout_ms: None,
};
let guardian_request = ApplyPatchRuntime::build_guardian_review_request(&request, "call-1");
@@ -68,70 +68,23 @@ fn guardian_review_request_includes_patch_context() {
);
}
#[cfg(not(target_os = "windows"))]
#[test]
fn build_sandbox_command_prefers_configured_codex_self_exe_for_apply_patch() {
let path = std::env::temp_dir().join("apply-patch-current-exe-test.txt");
let action = ApplyPatchAction::new_add_for_test(&path, "hello".to_string());
let request = ApplyPatchRequest {
action,
file_paths: vec![
AbsolutePathBuf::from_absolute_path(&path).expect("temp path should be absolute"),
],
changes: HashMap::from([(
path,
FileChange::Add {
content: "hello".to_string(),
},
)]),
exec_approval_requirement: ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: None,
},
additional_permissions: None,
permissions_preapproved: false,
timeout_ms: None,
};
let codex_self_exe = PathBuf::from("/tmp/codex");
let command = ApplyPatchRuntime::build_sandbox_command(&request, Some(&codex_self_exe))
.expect("build sandbox command");
assert_eq!(command.program, codex_self_exe.into_os_string());
}
#[cfg(not(target_os = "windows"))]
#[test]
fn build_sandbox_command_falls_back_to_current_exe_for_apply_patch() {
let path = std::env::temp_dir().join("apply-patch-current-exe-test.txt");
let action = ApplyPatchAction::new_add_for_test(&path, "hello".to_string());
let request = ApplyPatchRequest {
action,
file_paths: vec![
AbsolutePathBuf::from_absolute_path(&path).expect("temp path should be absolute"),
],
changes: HashMap::from([(
path,
FileChange::Add {
content: "hello".to_string(),
},
)]),
exec_approval_requirement: ExecApprovalRequirement::NeedsApproval {
reason: None,
proposed_execpolicy_amendment: None,
},
additional_permissions: None,
permissions_preapproved: false,
timeout_ms: None,
fn summary_paths_are_relative_to_cwd_when_possible() {
let cwd = Path::new("/workspace");
let affected = codex_apply_patch::AffectedPaths {
added: vec![PathBuf::from("/workspace/nested/new.txt")],
modified: vec![PathBuf::from("/workspace/existing.txt")],
deleted: vec![PathBuf::from("/outside/delete.txt")],
};
let command = ApplyPatchRuntime::build_sandbox_command(&request, /*codex_self_exe*/ None)
.expect("build sandbox command");
let got = relativize_affected_paths(&affected, cwd);
assert_eq!(
command.program,
std::env::current_exe()
.expect("current exe")
.into_os_string()
got,
codex_apply_patch::AffectedPaths {
added: vec![PathBuf::from("nested/new.txt")],
modified: vec![PathBuf::from("existing.txt")],
deleted: vec![PathBuf::from("/outside/delete.txt")],
}
);
}

View File

@@ -14,6 +14,7 @@ use codex_features::Feature;
use codex_protocol::approvals::NetworkApprovalProtocol;
use codex_protocol::approvals::NetworkPolicyAmendment;
use codex_protocol::approvals::NetworkPolicyRuleAction;
use codex_protocol::config_types::ApprovalsReviewer;
use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::EventMsg;
@@ -589,7 +590,7 @@ async fn submit_turn(
final_output_json_schema: None,
cwd: test.cwd.path().to_path_buf(),
approval_policy,
approvals_reviewer: None,
approvals_reviewer: Some(ApprovalsReviewer::User),
sandbox_policy,
model: session_model,
effort: None,

View File

@@ -20,6 +20,7 @@ async-trait = { workspace = true }
base64 = { workspace = true }
clap = { workspace = true, features = ["derive"] }
codex-app-server-protocol = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-absolute-path = { workspace = true }
codex-utils-pty = { workspace = true }
futures = { workspace = true }
@@ -42,6 +43,7 @@ tracing = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }
codex-protocol = { workspace = true }
codex-utils-cargo-bin = { workspace = true }
pretty_assertions = { workspace = true }
tempfile = { workspace = true }

View File

@@ -1,4 +1,5 @@
use async_trait::async_trait;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use tokio::io;
@@ -18,6 +19,12 @@ pub struct CopyOptions {
pub recursive: bool,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct FileSystemOperationOptions {
pub sandbox_policy: Option<SandboxPolicy>,
pub cwd: Option<AbsolutePathBuf>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FileMetadata {
pub is_directory: bool,
@@ -39,27 +46,88 @@ pub type FileSystemResult<T> = io::Result<T>;
pub trait ExecutorFileSystem: Send + Sync {
async fn read_file(&self, path: &AbsolutePathBuf) -> FileSystemResult<Vec<u8>>;
async fn read_file_with_options(
&self,
path: &AbsolutePathBuf,
_options: &FileSystemOperationOptions,
) -> FileSystemResult<Vec<u8>> {
self.read_file(path).await
}
async fn write_file(&self, path: &AbsolutePathBuf, contents: Vec<u8>) -> FileSystemResult<()>;
async fn write_file_with_options(
&self,
path: &AbsolutePathBuf,
contents: Vec<u8>,
_options: &FileSystemOperationOptions,
) -> FileSystemResult<()> {
self.write_file(path, contents).await
}
async fn create_directory(
&self,
path: &AbsolutePathBuf,
options: CreateDirectoryOptions,
) -> FileSystemResult<()>;
async fn create_directory_with_options(
&self,
path: &AbsolutePathBuf,
create_directory_options: CreateDirectoryOptions,
_options: &FileSystemOperationOptions,
) -> FileSystemResult<()> {
self.create_directory(path, create_directory_options).await
}
async fn get_metadata(&self, path: &AbsolutePathBuf) -> FileSystemResult<FileMetadata>;
async fn get_metadata_with_options(
&self,
path: &AbsolutePathBuf,
_options: &FileSystemOperationOptions,
) -> FileSystemResult<FileMetadata> {
self.get_metadata(path).await
}
async fn read_directory(
&self,
path: &AbsolutePathBuf,
) -> FileSystemResult<Vec<ReadDirectoryEntry>>;
async fn read_directory_with_options(
&self,
path: &AbsolutePathBuf,
_options: &FileSystemOperationOptions,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
self.read_directory(path).await
}
async fn remove(&self, path: &AbsolutePathBuf, options: RemoveOptions) -> FileSystemResult<()>;
async fn remove_with_options(
&self,
path: &AbsolutePathBuf,
remove_options: RemoveOptions,
_options: &FileSystemOperationOptions,
) -> FileSystemResult<()> {
self.remove(path, remove_options).await
}
async fn copy(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
options: CopyOptions,
) -> FileSystemResult<()>;
async fn copy_with_options(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
copy_options: CopyOptions,
_options: &FileSystemOperationOptions,
) -> FileSystemResult<()> {
self.copy(source_path, destination_path, copy_options).await
}
}

View File

@@ -39,6 +39,7 @@ pub use file_system::CopyOptions;
pub use file_system::CreateDirectoryOptions;
pub use file_system::ExecutorFileSystem;
pub use file_system::FileMetadata;
pub use file_system::FileSystemOperationOptions;
pub use file_system::FileSystemResult;
pub use file_system::ReadDirectoryEntry;
pub use file_system::RemoveOptions;

View File

@@ -1,4 +1,5 @@
use async_trait::async_trait;
use codex_protocol::permissions::FileSystemSandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::path::Component;
use std::path::Path;
@@ -11,6 +12,7 @@ use crate::CopyOptions;
use crate::CreateDirectoryOptions;
use crate::ExecutorFileSystem;
use crate::FileMetadata;
use crate::FileSystemOperationOptions;
use crate::FileSystemResult;
use crate::ReadDirectoryEntry;
use crate::RemoveOptions;
@@ -33,10 +35,29 @@ impl ExecutorFileSystem for LocalFileSystem {
tokio::fs::read(path.as_path()).await
}
async fn read_file_with_options(
&self,
path: &AbsolutePathBuf,
options: &FileSystemOperationOptions,
) -> FileSystemResult<Vec<u8>> {
enforce_read_access(path, options)?;
self.read_file(path).await
}
async fn write_file(&self, path: &AbsolutePathBuf, contents: Vec<u8>) -> FileSystemResult<()> {
tokio::fs::write(path.as_path(), contents).await
}
async fn write_file_with_options(
&self,
path: &AbsolutePathBuf,
contents: Vec<u8>,
options: &FileSystemOperationOptions,
) -> FileSystemResult<()> {
enforce_write_access(path, options)?;
self.write_file(path, contents).await
}
async fn create_directory(
&self,
path: &AbsolutePathBuf,
@@ -50,6 +71,16 @@ impl ExecutorFileSystem for LocalFileSystem {
Ok(())
}
async fn create_directory_with_options(
&self,
path: &AbsolutePathBuf,
create_directory_options: CreateDirectoryOptions,
options: &FileSystemOperationOptions,
) -> FileSystemResult<()> {
enforce_write_access(path, options)?;
self.create_directory(path, create_directory_options).await
}
async fn get_metadata(&self, path: &AbsolutePathBuf) -> FileSystemResult<FileMetadata> {
let metadata = tokio::fs::metadata(path.as_path()).await?;
Ok(FileMetadata {
@@ -60,6 +91,15 @@ impl ExecutorFileSystem for LocalFileSystem {
})
}
async fn get_metadata_with_options(
&self,
path: &AbsolutePathBuf,
options: &FileSystemOperationOptions,
) -> FileSystemResult<FileMetadata> {
enforce_read_access(path, options)?;
self.get_metadata(path).await
}
async fn read_directory(
&self,
path: &AbsolutePathBuf,
@@ -77,6 +117,15 @@ impl ExecutorFileSystem for LocalFileSystem {
Ok(entries)
}
async fn read_directory_with_options(
&self,
path: &AbsolutePathBuf,
options: &FileSystemOperationOptions,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
enforce_read_access(path, options)?;
self.read_directory(path).await
}
async fn remove(&self, path: &AbsolutePathBuf, options: RemoveOptions) -> FileSystemResult<()> {
match tokio::fs::symlink_metadata(path.as_path()).await {
Ok(metadata) => {
@@ -97,6 +146,16 @@ impl ExecutorFileSystem for LocalFileSystem {
}
}
async fn remove_with_options(
&self,
path: &AbsolutePathBuf,
remove_options: RemoveOptions,
options: &FileSystemOperationOptions,
) -> FileSystemResult<()> {
enforce_write_access(path, options)?;
self.remove(path, remove_options).await
}
async fn copy(
&self,
source_path: &AbsolutePathBuf,
@@ -147,6 +206,70 @@ impl ExecutorFileSystem for LocalFileSystem {
.await
.map_err(|err| io::Error::other(format!("filesystem task failed: {err}")))?
}
async fn copy_with_options(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
copy_options: CopyOptions,
options: &FileSystemOperationOptions,
) -> FileSystemResult<()> {
enforce_read_access(source_path, options)?;
enforce_write_access(destination_path, options)?;
self.copy(source_path, destination_path, copy_options).await
}
}
fn enforce_read_access(
path: &AbsolutePathBuf,
options: &FileSystemOperationOptions,
) -> FileSystemResult<()> {
enforce_access(
path,
options,
FileSystemSandboxPolicy::can_read_path_with_cwd,
"read",
)
}
fn enforce_write_access(
path: &AbsolutePathBuf,
options: &FileSystemOperationOptions,
) -> FileSystemResult<()> {
enforce_access(
path,
options,
FileSystemSandboxPolicy::can_write_path_with_cwd,
"write",
)
}
fn enforce_access(
path: &AbsolutePathBuf,
options: &FileSystemOperationOptions,
is_allowed: fn(&FileSystemSandboxPolicy, &Path, &Path) -> bool,
access_kind: &str,
) -> FileSystemResult<()> {
let Some(sandbox_policy) = &options.sandbox_policy else {
return Ok(());
};
let cwd = match &options.cwd {
Some(cwd) => cwd.clone().into_path_buf(),
None => std::env::current_dir()
.map_err(|err| io::Error::other(format!("failed to read current dir: {err}")))?,
};
let file_system_policy = FileSystemSandboxPolicy::from(sandbox_policy);
if is_allowed(&file_system_policy, path.as_path(), cwd.as_path()) {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!(
"fs/{access_kind} is not permitted by sandbox policy for path {}",
path.as_path().display()
),
))
}
}
fn copy_dir_recursive(source: &Path, target: &Path) -> io::Result<()> {

View File

@@ -18,6 +18,7 @@ use crate::ExecServerClient;
use crate::ExecServerError;
use crate::ExecutorFileSystem;
use crate::FileMetadata;
use crate::FileSystemOperationOptions;
use crate::FileSystemResult;
use crate::ReadDirectoryEntry;
use crate::RemoveOptions;
@@ -42,7 +43,37 @@ impl ExecutorFileSystem for RemoteFileSystem {
trace!("remote fs read_file");
let response = self
.client
.fs_read_file(FsReadFileParams { path: path.clone() })
.fs_read_file(FsReadFileParams {
path: path.clone(),
sandbox_policy: None,
cwd: None,
})
.await
.map_err(map_remote_error)?;
STANDARD.decode(response.data_base64).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("remote fs/readFile returned invalid base64 dataBase64: {err}"),
)
})
}
async fn read_file_with_options(
&self,
path: &AbsolutePathBuf,
options: &FileSystemOperationOptions,
) -> FileSystemResult<Vec<u8>> {
trace!("remote fs read_file_with_options");
let response = self
.client
.fs_read_file(FsReadFileParams {
path: path.clone(),
sandbox_policy: options
.sandbox_policy
.clone()
.map(codex_app_server_protocol::SandboxPolicy::from),
cwd: options.cwd.clone(),
})
.await
.map_err(map_remote_error)?;
STANDARD.decode(response.data_base64).map_err(|err| {
@@ -59,6 +90,30 @@ impl ExecutorFileSystem for RemoteFileSystem {
.fs_write_file(FsWriteFileParams {
path: path.clone(),
data_base64: STANDARD.encode(contents),
sandbox_policy: None,
cwd: None,
})
.await
.map_err(map_remote_error)?;
Ok(())
}
async fn write_file_with_options(
&self,
path: &AbsolutePathBuf,
contents: Vec<u8>,
options: &FileSystemOperationOptions,
) -> FileSystemResult<()> {
trace!("remote fs write_file_with_options");
self.client
.fs_write_file(FsWriteFileParams {
path: path.clone(),
data_base64: STANDARD.encode(contents),
sandbox_policy: options
.sandbox_policy
.clone()
.map(codex_app_server_protocol::SandboxPolicy::from),
cwd: options.cwd.clone(),
})
.await
.map_err(map_remote_error)?;
@@ -75,6 +130,30 @@ impl ExecutorFileSystem for RemoteFileSystem {
.fs_create_directory(FsCreateDirectoryParams {
path: path.clone(),
recursive: Some(options.recursive),
sandbox_policy: None,
cwd: None,
})
.await
.map_err(map_remote_error)?;
Ok(())
}
async fn create_directory_with_options(
&self,
path: &AbsolutePathBuf,
create_directory_options: CreateDirectoryOptions,
options: &FileSystemOperationOptions,
) -> FileSystemResult<()> {
trace!("remote fs create_directory_with_options");
self.client
.fs_create_directory(FsCreateDirectoryParams {
path: path.clone(),
recursive: Some(create_directory_options.recursive),
sandbox_policy: options
.sandbox_policy
.clone()
.map(codex_app_server_protocol::SandboxPolicy::from),
cwd: options.cwd.clone(),
})
.await
.map_err(map_remote_error)?;
@@ -85,7 +164,37 @@ impl ExecutorFileSystem for RemoteFileSystem {
trace!("remote fs get_metadata");
let response = self
.client
.fs_get_metadata(FsGetMetadataParams { path: path.clone() })
.fs_get_metadata(FsGetMetadataParams {
path: path.clone(),
sandbox_policy: None,
cwd: None,
})
.await
.map_err(map_remote_error)?;
Ok(FileMetadata {
is_directory: response.is_directory,
is_file: response.is_file,
created_at_ms: response.created_at_ms,
modified_at_ms: response.modified_at_ms,
})
}
async fn get_metadata_with_options(
&self,
path: &AbsolutePathBuf,
options: &FileSystemOperationOptions,
) -> FileSystemResult<FileMetadata> {
trace!("remote fs get_metadata_with_options");
let response = self
.client
.fs_get_metadata(FsGetMetadataParams {
path: path.clone(),
sandbox_policy: options
.sandbox_policy
.clone()
.map(codex_app_server_protocol::SandboxPolicy::from),
cwd: options.cwd.clone(),
})
.await
.map_err(map_remote_error)?;
Ok(FileMetadata {
@@ -103,7 +212,40 @@ impl ExecutorFileSystem for RemoteFileSystem {
trace!("remote fs read_directory");
let response = self
.client
.fs_read_directory(FsReadDirectoryParams { path: path.clone() })
.fs_read_directory(FsReadDirectoryParams {
path: path.clone(),
sandbox_policy: None,
cwd: None,
})
.await
.map_err(map_remote_error)?;
Ok(response
.entries
.into_iter()
.map(|entry| ReadDirectoryEntry {
file_name: entry.file_name,
is_directory: entry.is_directory,
is_file: entry.is_file,
})
.collect())
}
async fn read_directory_with_options(
&self,
path: &AbsolutePathBuf,
options: &FileSystemOperationOptions,
) -> FileSystemResult<Vec<ReadDirectoryEntry>> {
trace!("remote fs read_directory_with_options");
let response = self
.client
.fs_read_directory(FsReadDirectoryParams {
path: path.clone(),
sandbox_policy: options
.sandbox_policy
.clone()
.map(codex_app_server_protocol::SandboxPolicy::from),
cwd: options.cwd.clone(),
})
.await
.map_err(map_remote_error)?;
Ok(response
@@ -124,6 +266,31 @@ impl ExecutorFileSystem for RemoteFileSystem {
path: path.clone(),
recursive: Some(options.recursive),
force: Some(options.force),
sandbox_policy: None,
cwd: None,
})
.await
.map_err(map_remote_error)?;
Ok(())
}
async fn remove_with_options(
&self,
path: &AbsolutePathBuf,
remove_options: RemoveOptions,
options: &FileSystemOperationOptions,
) -> FileSystemResult<()> {
trace!("remote fs remove_with_options");
self.client
.fs_remove(FsRemoveParams {
path: path.clone(),
recursive: Some(remove_options.recursive),
force: Some(remove_options.force),
sandbox_policy: options
.sandbox_policy
.clone()
.map(codex_app_server_protocol::SandboxPolicy::from),
cwd: options.cwd.clone(),
})
.await
.map_err(map_remote_error)?;
@@ -142,6 +309,32 @@ impl ExecutorFileSystem for RemoteFileSystem {
source_path: source_path.clone(),
destination_path: destination_path.clone(),
recursive: options.recursive,
sandbox_policy: None,
cwd: None,
})
.await
.map_err(map_remote_error)?;
Ok(())
}
async fn copy_with_options(
&self,
source_path: &AbsolutePathBuf,
destination_path: &AbsolutePathBuf,
copy_options: CopyOptions,
options: &FileSystemOperationOptions,
) -> FileSystemResult<()> {
trace!("remote fs copy_with_options");
self.client
.fs_copy(FsCopyParams {
source_path: source_path.clone(),
destination_path: destination_path.clone(),
recursive: copy_options.recursive,
sandbox_policy: options
.sandbox_policy
.clone()
.map(codex_app_server_protocol::SandboxPolicy::from),
cwd: options.cwd.clone(),
})
.await
.map_err(map_remote_error)?;

View File

@@ -22,6 +22,7 @@ use codex_app_server_protocol::JSONRPCErrorError;
use crate::CopyOptions;
use crate::CreateDirectoryOptions;
use crate::ExecutorFileSystem;
use crate::FileSystemOperationOptions;
use crate::RemoveOptions;
use crate::local_file_system::LocalFileSystem;
use crate::rpc::internal_error;
@@ -39,7 +40,10 @@ impl FileSystemHandler {
) -> Result<FsReadFileResponse, JSONRPCErrorError> {
let bytes = self
.file_system
.read_file(&params.path)
.read_file_with_options(
&params.path,
&fs_operation_options(params.sandbox_policy, params.cwd),
)
.await
.map_err(map_fs_error)?;
Ok(FsReadFileResponse {
@@ -57,7 +61,11 @@ impl FileSystemHandler {
))
})?;
self.file_system
.write_file(&params.path, bytes)
.write_file_with_options(
&params.path,
bytes,
&fs_operation_options(params.sandbox_policy, params.cwd),
)
.await
.map_err(map_fs_error)?;
Ok(FsWriteFileResponse {})
@@ -68,11 +76,12 @@ impl FileSystemHandler {
params: FsCreateDirectoryParams,
) -> Result<FsCreateDirectoryResponse, JSONRPCErrorError> {
self.file_system
.create_directory(
.create_directory_with_options(
&params.path,
CreateDirectoryOptions {
recursive: params.recursive.unwrap_or(true),
},
&fs_operation_options(params.sandbox_policy, params.cwd),
)
.await
.map_err(map_fs_error)?;
@@ -85,7 +94,10 @@ impl FileSystemHandler {
) -> Result<FsGetMetadataResponse, JSONRPCErrorError> {
let metadata = self
.file_system
.get_metadata(&params.path)
.get_metadata_with_options(
&params.path,
&fs_operation_options(params.sandbox_policy, params.cwd),
)
.await
.map_err(map_fs_error)?;
Ok(FsGetMetadataResponse {
@@ -102,7 +114,10 @@ impl FileSystemHandler {
) -> Result<FsReadDirectoryResponse, JSONRPCErrorError> {
let entries = self
.file_system
.read_directory(&params.path)
.read_directory_with_options(
&params.path,
&fs_operation_options(params.sandbox_policy, params.cwd),
)
.await
.map_err(map_fs_error)?;
Ok(FsReadDirectoryResponse {
@@ -122,12 +137,13 @@ impl FileSystemHandler {
params: FsRemoveParams,
) -> Result<FsRemoveResponse, JSONRPCErrorError> {
self.file_system
.remove(
.remove_with_options(
&params.path,
RemoveOptions {
recursive: params.recursive.unwrap_or(true),
force: params.force.unwrap_or(true),
},
&fs_operation_options(params.sandbox_policy, params.cwd),
)
.await
.map_err(map_fs_error)?;
@@ -139,12 +155,13 @@ impl FileSystemHandler {
params: FsCopyParams,
) -> Result<FsCopyResponse, JSONRPCErrorError> {
self.file_system
.copy(
.copy_with_options(
&params.source_path,
&params.destination_path,
CopyOptions {
recursive: params.recursive,
},
&fs_operation_options(params.sandbox_policy, params.cwd),
)
.await
.map_err(map_fs_error)?;
@@ -152,6 +169,16 @@ impl FileSystemHandler {
}
}
fn fs_operation_options(
sandbox_policy: Option<codex_app_server_protocol::SandboxPolicy>,
cwd: Option<codex_utils_absolute_path::AbsolutePathBuf>,
) -> FileSystemOperationOptions {
FileSystemOperationOptions {
sandbox_policy: sandbox_policy.map(|policy| policy.to_core()),
cwd,
}
}
fn map_fs_error(err: io::Error) -> JSONRPCErrorError {
if err.kind() == io::ErrorKind::InvalidInput {
invalid_request(err.to_string())

View File

@@ -12,8 +12,11 @@ use codex_exec_server::CopyOptions;
use codex_exec_server::CreateDirectoryOptions;
use codex_exec_server::Environment;
use codex_exec_server::ExecutorFileSystem;
use codex_exec_server::FileSystemOperationOptions;
use codex_exec_server::ReadDirectoryEntry;
use codex_exec_server::RemoveOptions;
use codex_protocol::protocol::ReadOnlyAccess;
use codex_protocol::protocol::SandboxPolicy;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
@@ -56,6 +59,48 @@ fn absolute_path(path: std::path::PathBuf) -> AbsolutePathBuf {
}
}
fn read_only_options(readable_root: std::path::PathBuf) -> FileSystemOperationOptions {
FileSystemOperationOptions {
sandbox_policy: Some(SandboxPolicy::ReadOnly {
access: ReadOnlyAccess::Restricted {
include_platform_defaults: false,
readable_roots: vec![absolute_path(readable_root)],
},
network_access: false,
}),
cwd: None,
}
}
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_write_with_sandbox_policy_uses_supplied_cwd(use_remote: bool) -> Result<()> {
let context = create_file_system_context(use_remote).await?;
let file_system = context.file_system;
let tmp = TempDir::new()?;
let workspace_root = absolute_path(tmp.path().to_path_buf());
let file_path = tmp.path().join("workspace-write.txt");
let options = FileSystemOperationOptions {
sandbox_policy: Some(SandboxPolicy::new_workspace_write_policy()),
cwd: Some(workspace_root),
};
file_system
.write_file_with_options(
&absolute_path(file_path.clone()),
b"allowed".to_vec(),
&options,
)
.await
.with_context(|| format!("mode={use_remote}"))?;
assert_eq!(std::fs::read_to_string(file_path)?, "allowed");
Ok(())
}
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -212,6 +257,66 @@ async fn file_system_copy_rejects_directory_without_recursive(use_remote: bool)
Ok(())
}
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_read_with_sandbox_policy_allows_readable_root(use_remote: bool) -> Result<()> {
let context = create_file_system_context(use_remote).await?;
let file_system = context.file_system;
let tmp = TempDir::new()?;
let allowed_dir = tmp.path().join("allowed");
let file_path = allowed_dir.join("note.txt");
std::fs::create_dir_all(&allowed_dir)?;
std::fs::write(&file_path, "sandboxed hello")?;
let contents = file_system
.read_file_with_options(&absolute_path(file_path), &read_only_options(allowed_dir))
.await
.with_context(|| format!("mode={use_remote}"))?;
assert_eq!(contents, b"sandboxed hello");
Ok(())
}
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn file_system_write_with_sandbox_policy_rejects_unwritable_path(
use_remote: bool,
) -> Result<()> {
let context = create_file_system_context(use_remote).await?;
let file_system = context.file_system;
let tmp = TempDir::new()?;
let allowed_dir = tmp.path().join("allowed");
let blocked_path = tmp.path().join("blocked.txt");
std::fs::create_dir_all(&allowed_dir)?;
let error = match file_system
.write_file_with_options(
&absolute_path(blocked_path.clone()),
b"nope".to_vec(),
&read_only_options(allowed_dir),
)
.await
{
Ok(()) => anyhow::bail!("write should be blocked"),
Err(error) => error,
};
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(
error.to_string(),
format!(
"fs/write is not permitted by sandbox policy for path {}",
blocked_path.display()
)
);
assert!(!blocked_path.exists());
Ok(())
}
#[test_case(false ; "local")]
#[test_case(true ; "remote")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]

View File

@@ -4,4 +4,8 @@ codex_rust_crate(
name = "exec",
crate_name = "codex_exec",
test_tags = ["no-sandbox"],
unit_test_target_compatible_with = select({
"@platforms//os:windows": ["@platforms//:incompatible"],
"//conditions:default": [],
}),
)

View File

@@ -129,6 +129,7 @@ def codex_rust_crate(
integration_test_timeout = None,
test_data_extra = [],
test_tags = [],
unit_test_target_compatible_with = None,
unit_test_timeout = None,
extra_binaries = []):
"""Defines a Rust crate with library, binaries, and tests wired for Bazel + Cargo parity.
@@ -164,6 +165,8 @@ def codex_rust_crate(
test_data_extra: Extra runtime data for tests.
test_tags: Tags applied to unit + integration test targets.
Typically used to disable the sandbox, but see https://bazel.build/reference/be/common-definitions#common.tags
unit_test_target_compatible_with: Optional target compatibility
constraints for the generated unit-test binary and wrapper.
unit_test_timeout: Optional Bazel timeout for the unit-test target
generated from `src/**/*.rs`.
extra_binaries: Additional binary labels to surface as test data and
@@ -235,6 +238,7 @@ def codex_rust_crate(
)
unit_test_binary = name + "-unit-tests-bin"
unit_test_target_compatible_with = unit_test_target_compatible_with or []
rust_test(
name = unit_test_binary,
crate = name,
@@ -253,6 +257,7 @@ def codex_rust_crate(
rustc_env = rustc_env,
data = test_data_extra,
tags = test_tags + ["manual"],
target_compatible_with = unit_test_target_compatible_with,
)
unit_test_kwargs = {}
@@ -265,6 +270,7 @@ def codex_rust_crate(
test_bin = ":" + unit_test_binary,
workspace_root_marker = "//codex-rs/utils/cargo-bin:repo_root.marker",
tags = test_tags,
target_compatible_with = unit_test_target_compatible_with,
**unit_test_kwargs
)

View File

@@ -5,9 +5,16 @@ set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "${repo_root}"
# Resolve the dynamic targets before printing anything so callers do not
# continue with a partial list if `bazel query` fails.
manual_rust_test_targets="$(bazel query 'kind("rust_test rule", attr(tags, "manual", //codex-rs/... except //codex-rs/v8-poc/...))')"
manual_rust_test_targets=""
if [[ "${RUNNER_OS:-}" != "Windows" ]]; then
# Resolve the dynamic targets before printing anything so callers do not
# continue with a partial list if `bazel query` fails.
#
# The generated manual `*-unit-tests-bin` targets pull in Windows-incompatible
# V8/Python dependencies under gnullvm, so only include them on platforms
# where they currently analyze successfully.
manual_rust_test_targets="$(bazel query 'kind("rust_test rule", attr(tags, "manual", //codex-rs/... except //codex-rs/v8-poc/...))')"
fi
printf '%s\n' \
"//codex-rs/..." \
@@ -17,4 +24,6 @@ printf '%s\n' \
# underlying `rust_test` binaries. Add the internal manual `*-unit-tests-bin`
# targets explicitly so inline `#[cfg(test)]` code is linted like
# `cargo clippy --tests`.
printf '%s\n' "${manual_rust_test_targets}"
if [[ -n "${manual_rust_test_targets}" ]]; then
printf '%s\n' "${manual_rust_test_targets}"
fi