mirror of
https://github.com/openai/codex.git
synced 2026-04-08 14:54:47 +00:00
Compare commits
17 Commits
dev/window
...
starr/appl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ad6a1c7d7 | ||
|
|
0e3ac63173 | ||
|
|
f8a9011be4 | ||
|
|
48daea7e5a | ||
|
|
4b05e85f0d | ||
|
|
604b233fe5 | ||
|
|
1b71b9c392 | ||
|
|
f8f806b873 | ||
|
|
ae73368979 | ||
|
|
e6cf2cccda | ||
|
|
f000b01947 | ||
|
|
d1fc220934 | ||
|
|
26aa767e05 | ||
|
|
f5337b95fc | ||
|
|
92080bfac1 | ||
|
|
12400ceaa9 | ||
|
|
e8fbf47900 |
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
{
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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, };
|
||||
|
||||
@@ -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, };
|
||||
|
||||
@@ -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, };
|
||||
|
||||
@@ -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, };
|
||||
|
||||
@@ -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, };
|
||||
|
||||
@@ -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, };
|
||||
|
||||
@@ -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, };
|
||||
|
||||
@@ -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)?,
|
||||
|
||||
@@ -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(¶ms).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(¶ms).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(¶ms).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(¶ms).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,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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(¶ms.path)
|
||||
.read_file_with_options(
|
||||
¶ms.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(¶ms.path, bytes)
|
||||
.write_file_with_options(
|
||||
¶ms.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(
|
||||
¶ms.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(¶ms.path)
|
||||
.get_metadata_with_options(
|
||||
¶ms.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(¶ms.path)
|
||||
.read_directory_with_options(
|
||||
¶ms.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(
|
||||
¶ms.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(
|
||||
¶ms.source_path,
|
||||
¶ms.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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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(),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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")],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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(¶ms.path)
|
||||
.read_file_with_options(
|
||||
¶ms.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(¶ms.path, bytes)
|
||||
.write_file_with_options(
|
||||
¶ms.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(
|
||||
¶ms.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(¶ms.path)
|
||||
.get_metadata_with_options(
|
||||
¶ms.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(¶ms.path)
|
||||
.read_directory_with_options(
|
||||
¶ms.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(
|
||||
¶ms.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(
|
||||
¶ms.source_path,
|
||||
¶ms.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())
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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": [],
|
||||
}),
|
||||
)
|
||||
|
||||
6
defs.bzl
6
defs.bzl
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user