mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
Merge branch 'main' into codex/viyatb/secrets-core-prb
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
name: 🖥️ Codex App Bug
|
||||
description: Report an issue with the Codex App
|
||||
labels:
|
||||
- app
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Before submitting a new issue, please search for existing issues to see if your issue has already been reported.
|
||||
If it has, please add a 👍 reaction (no need to leave a comment) to the existing issue instead of creating a new one.
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: What version of the Codex App are you using (From “About Codex” dialog)?
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: plan
|
||||
attributes:
|
||||
label: What subscription do you have?
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: What issue are you seeing?
|
||||
description: Please include the full error messages and prompts with PII redacted. If possible, please provide text instead of a screenshot.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: What steps can reproduce the bug?
|
||||
description: Explain the bug and provide a code snippet that can reproduce it. Please include session id, token limit usage, context window usage if applicable.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: What is the expected behavior?
|
||||
description: If possible, please provide text instead of a screenshot.
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Is there anything else you think we should know?
|
||||
+6
-7
@@ -1,8 +1,7 @@
|
||||
name: 🧑💻 VS Code Extension
|
||||
description: Report an issue with the VS Code extension
|
||||
name: 🧑💻 IDE Extension Bug
|
||||
description: Report an issue with the IDE extension
|
||||
labels:
|
||||
- extension
|
||||
- needs triage
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
@@ -13,7 +12,7 @@ body:
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: What version of the VS Code extension are you using?
|
||||
label: What version of the IDE extension are you using?
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -34,20 +33,20 @@ body:
|
||||
attributes:
|
||||
label: What platform is your computer?
|
||||
description: |
|
||||
For MacOS and Linux: copy the output of `uname -mprs`
|
||||
For macOS and Linux: copy the output of `uname -mprs`
|
||||
For Windows: copy the output of `"$([Environment]::OSVersion | ForEach-Object VersionString) $(if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" })"` in the PowerShell console
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: What issue are you seeing?
|
||||
description: Please include the full error messages and prompts with PII redacted. If possible, please provide text instead of a screenshot.
|
||||
description: Please include the full error messages and prompts with PII redacted. If possible, please provide text instead of a screenshot.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: What steps can reproduce the bug?
|
||||
description: Explain the bug and provide a code snippet that can reproduce it. Please include session id, token limit usage, context window usage if applicable.
|
||||
description: Explain the bug and provide a code snippet that can reproduce it.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -1,5 +1,5 @@
|
||||
name: 🪲 Bug Report
|
||||
description: Report an issue that should be fixed
|
||||
name: 💻 CLI Bug
|
||||
description: Report an issue in the Codex CLI
|
||||
labels:
|
||||
- bug
|
||||
- needs triage
|
||||
@@ -7,19 +7,16 @@ body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thank you for submitting a bug report! It helps make Codex better for everyone.
|
||||
|
||||
If you need help or support using Codex, and are not reporting a bug, please post on [codex/discussions](https://github.com/openai/codex/discussions), where you can ask questions or engage with others on ideas for how to improve codex.
|
||||
Before submitting a new issue, please search for existing issues to see if your issue has already been reported.
|
||||
If it has, please add a 👍 reaction (no need to leave a comment) to the existing issue instead of creating a new one.
|
||||
|
||||
Make sure you are running the [latest](https://npmjs.com/package/@openai/codex) version of Codex CLI. The bug you are experiencing may already have been fixed.
|
||||
|
||||
Please try to include as much information as possible.
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
label: What version of Codex is running?
|
||||
description: (App) look in "About Codex" dialog; (CLI) use `codex --version`; (IDE Extension) look in extensions panel.
|
||||
label: What version of Codex CLI is running?
|
||||
description: use `codex --version`
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
@@ -32,13 +29,13 @@ body:
|
||||
id: model
|
||||
attributes:
|
||||
label: Which model were you using?
|
||||
description: Like `gpt-4.1`, `o4-mini`, `o3`, etc.
|
||||
description: Like `gpt-5.2`, `gpt-5.2-codex`, etc.
|
||||
- type: input
|
||||
id: platform
|
||||
attributes:
|
||||
label: What platform is your computer?
|
||||
description: |
|
||||
For MacOS and Linux: copy the output of `uname -mprs`
|
||||
For macOS and Linux: copy the output of `uname -mprs`
|
||||
For Windows: copy the output of `"$([Environment]::OSVersion | ForEach-Object VersionString) $(if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" })"` in the PowerShell console
|
||||
- type: input
|
||||
id: terminal
|
||||
@@ -58,7 +55,7 @@ body:
|
||||
id: steps
|
||||
attributes:
|
||||
label: What steps can reproduce the bug?
|
||||
description: Explain the bug and provide a code snippet that can reproduce it. Please include session id, token limit usage, context window usage if applicable.
|
||||
description: Explain the bug and provide a code snippet that can reproduce it. Please include thread id if applicable.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
@@ -0,0 +1,37 @@
|
||||
name: 🪲 Other Bug
|
||||
description: Report an issue in Codex Web, integrations, or other Codex components
|
||||
labels:
|
||||
- bug
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Before submitting a new issue, please search for existing issues to see if your issue has already been reported.
|
||||
If it has, please add a 👍 reaction (no need to leave a comment) to the existing issue instead of creating a new one.
|
||||
|
||||
If you need help or support using Codex and are not reporting a bug, please post on [codex/discussions](https://github.com/openai/codex/discussions), where you can ask questions or engage with others on ideas for how to improve codex.
|
||||
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: What issue are you seeing?
|
||||
description: Please include the full error messages and prompts with PII redacted. If possible, please provide text instead of a screenshot.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: What steps can reproduce the bug?
|
||||
description: Explain the bug and provide a code snippet that can reproduce it.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: What is the expected behavior?
|
||||
description: If possible, please provide text instead of a screenshot.
|
||||
- type: textarea
|
||||
id: notes
|
||||
attributes:
|
||||
label: Additional information
|
||||
description: Is there anything else you think we should know?
|
||||
+1
-1
@@ -16,7 +16,7 @@ body:
|
||||
id: variant
|
||||
attributes:
|
||||
label: What variant of Codex are you using?
|
||||
description: (e.g., CLI, App, IDE Extension, Web)
|
||||
description: (e.g., App, IDE Extension, CLI, Web)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
Generated
+124
@@ -398,6 +398,15 @@ dependencies = [
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arboard"
|
||||
version = "3.6.1"
|
||||
@@ -924,6 +933,25 @@ dependencies = [
|
||||
"bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
|
||||
dependencies = [
|
||||
"bzip2-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bzip2-sys"
|
||||
version = "0.1.13+1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
@@ -1579,6 +1607,7 @@ dependencies = [
|
||||
"which",
|
||||
"wildmatch",
|
||||
"wiremock",
|
||||
"zip",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
@@ -2354,6 +2383,12 @@ dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.6.0"
|
||||
@@ -2772,6 +2807,12 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deflate64"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204"
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.7.10"
|
||||
@@ -2804,6 +2845,17 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "1.0.0"
|
||||
@@ -4930,6 +4982,27 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lzma-rs"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"crc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lzma-sys"
|
||||
version = "0.1.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "maplit"
|
||||
version = "1.0.2"
|
||||
@@ -10516,6 +10589,15 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xz2"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
|
||||
dependencies = [
|
||||
"lzma-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
@@ -10702,12 +10784,54 @@ dependencies = [
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "2.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arbitrary",
|
||||
"bzip2",
|
||||
"constant_time_eq",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"deflate64",
|
||||
"displaydoc",
|
||||
"flate2",
|
||||
"getrandom 0.3.4",
|
||||
"hmac",
|
||||
"indexmap 2.13.0",
|
||||
"lzma-rs",
|
||||
"memchr",
|
||||
"pbkdf2",
|
||||
"sha1",
|
||||
"thiserror 2.0.18",
|
||||
"time",
|
||||
"xz2",
|
||||
"zeroize",
|
||||
"zopfli",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"crc32fast",
|
||||
"log",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
|
||||
@@ -249,6 +249,7 @@ walkdir = "2.5.0"
|
||||
webbrowser = "1.0"
|
||||
which = "8"
|
||||
wildmatch = "2.6.1"
|
||||
zip = "2.4.2"
|
||||
|
||||
wiremock = "0.6"
|
||||
zeroize = "1.8.2"
|
||||
|
||||
@@ -2183,6 +2183,24 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"SkillsRemoteReadParams": {
|
||||
"type": "object"
|
||||
},
|
||||
"SkillsRemoteWriteParams": {
|
||||
"properties": {
|
||||
"hazelnutId": {
|
||||
"type": "string"
|
||||
},
|
||||
"isPreload": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hazelnutId",
|
||||
"isPreload"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"TextElement": {
|
||||
"properties": {
|
||||
"byteRange": {
|
||||
@@ -3312,6 +3330,54 @@
|
||||
"title": "Skills/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"skills/remote/read"
|
||||
],
|
||||
"title": "Skills/remote/readRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/SkillsRemoteReadParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Skills/remote/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"skills/remote/write"
|
||||
],
|
||||
"title": "Skills/remote/writeRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/SkillsRemoteWriteParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Skills/remote/writeRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -2012,6 +2012,59 @@
|
||||
"title": "ListSkillsResponseEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "List of remote skills available to the agent.",
|
||||
"properties": {
|
||||
"skills": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteSkillSummary"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"list_remote_skills_response"
|
||||
],
|
||||
"title": "ListRemoteSkillsResponseEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"skills",
|
||||
"type"
|
||||
],
|
||||
"title": "ListRemoteSkillsResponseEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Remote skill downloaded to local cache.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"remote_skill_downloaded"
|
||||
],
|
||||
"title": "RemoteSkillDownloadedEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "RemoteSkillDownloadedEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Notification that skill data may have been updated and clients may want to reload.",
|
||||
"properties": {
|
||||
@@ -3429,6 +3482,25 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"RemoteSkillSummary": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RequestId": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -6536,6 +6608,59 @@
|
||||
"title": "ListSkillsResponseEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "List of remote skills available to the agent.",
|
||||
"properties": {
|
||||
"skills": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteSkillSummary"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"list_remote_skills_response"
|
||||
],
|
||||
"title": "ListRemoteSkillsResponseEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"skills",
|
||||
"type"
|
||||
],
|
||||
"title": "ListRemoteSkillsResponseEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Remote skill downloaded to local cache.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"remote_skill_downloaded"
|
||||
],
|
||||
"title": "RemoteSkillDownloadedEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "RemoteSkillDownloadedEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Notification that skill data may have been updated and clients may want to reload.",
|
||||
"properties": {
|
||||
|
||||
@@ -2590,6 +2590,59 @@
|
||||
"title": "ListSkillsResponseEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "List of remote skills available to the agent.",
|
||||
"properties": {
|
||||
"skills": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteSkillSummary"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"list_remote_skills_response"
|
||||
],
|
||||
"title": "ListRemoteSkillsResponseEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"skills",
|
||||
"type"
|
||||
],
|
||||
"title": "ListRemoteSkillsResponseEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Remote skill downloaded to local cache.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"remote_skill_downloaded"
|
||||
],
|
||||
"title": "RemoteSkillDownloadedEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "RemoteSkillDownloadedEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Notification that skill data may have been updated and clients may want to reload.",
|
||||
"properties": {
|
||||
@@ -4469,6 +4522,25 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RemoteSkillSummary": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RequestId": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -718,6 +718,54 @@
|
||||
"title": "Skills/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"skills/remote/read"
|
||||
],
|
||||
"title": "Skills/remote/readRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/SkillsRemoteReadParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Skills/remote/readRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"skills/remote/write"
|
||||
],
|
||||
"title": "Skills/remote/writeRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/SkillsRemoteWriteParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Skills/remote/writeRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -3852,6 +3900,59 @@
|
||||
"title": "ListSkillsResponseEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "List of remote skills available to the agent.",
|
||||
"properties": {
|
||||
"skills": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/RemoteSkillSummary"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"list_remote_skills_response"
|
||||
],
|
||||
"title": "ListRemoteSkillsResponseEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"skills",
|
||||
"type"
|
||||
],
|
||||
"title": "ListRemoteSkillsResponseEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Remote skill downloaded to local cache.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"remote_skill_downloaded"
|
||||
],
|
||||
"title": "RemoteSkillDownloadedEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "RemoteSkillDownloadedEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Notification that skill data may have been updated and clients may want to reload.",
|
||||
"properties": {
|
||||
@@ -6351,6 +6452,25 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"RemoteSkillSummary": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RemoveConversationListenerParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
@@ -12382,6 +12502,25 @@
|
||||
"title": "ReasoningTextDeltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"RemoteSkillSummary": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ResidencyRequirement": {
|
||||
"enum": [
|
||||
"us"
|
||||
@@ -13417,6 +13556,65 @@
|
||||
"title": "SkillsListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"SkillsRemoteReadParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "SkillsRemoteReadParams",
|
||||
"type": "object"
|
||||
},
|
||||
"SkillsRemoteReadResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/RemoteSkillSummary"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "SkillsRemoteReadResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"SkillsRemoteWriteParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"hazelnutId": {
|
||||
"type": "string"
|
||||
},
|
||||
"isPreload": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hazelnutId",
|
||||
"isPreload"
|
||||
],
|
||||
"title": "SkillsRemoteWriteParams",
|
||||
"type": "object"
|
||||
},
|
||||
"SkillsRemoteWriteResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"path"
|
||||
],
|
||||
"title": "SkillsRemoteWriteResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"SubAgentSource": {
|
||||
"oneOf": [
|
||||
{
|
||||
|
||||
@@ -2012,6 +2012,59 @@
|
||||
"title": "ListSkillsResponseEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "List of remote skills available to the agent.",
|
||||
"properties": {
|
||||
"skills": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteSkillSummary"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"list_remote_skills_response"
|
||||
],
|
||||
"title": "ListRemoteSkillsResponseEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"skills",
|
||||
"type"
|
||||
],
|
||||
"title": "ListRemoteSkillsResponseEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Remote skill downloaded to local cache.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"remote_skill_downloaded"
|
||||
],
|
||||
"title": "RemoteSkillDownloadedEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "RemoteSkillDownloadedEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Notification that skill data may have been updated and clients may want to reload.",
|
||||
"properties": {
|
||||
@@ -3429,6 +3482,25 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"RemoteSkillSummary": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RequestId": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -2012,6 +2012,59 @@
|
||||
"title": "ListSkillsResponseEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "List of remote skills available to the agent.",
|
||||
"properties": {
|
||||
"skills": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteSkillSummary"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"list_remote_skills_response"
|
||||
],
|
||||
"title": "ListRemoteSkillsResponseEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"skills",
|
||||
"type"
|
||||
],
|
||||
"title": "ListRemoteSkillsResponseEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Remote skill downloaded to local cache.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"remote_skill_downloaded"
|
||||
],
|
||||
"title": "RemoteSkillDownloadedEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "RemoteSkillDownloadedEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Notification that skill data may have been updated and clients may want to reload.",
|
||||
"properties": {
|
||||
@@ -3429,6 +3482,25 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"RemoteSkillSummary": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RequestId": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -2012,6 +2012,59 @@
|
||||
"title": "ListSkillsResponseEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "List of remote skills available to the agent.",
|
||||
"properties": {
|
||||
"skills": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteSkillSummary"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"list_remote_skills_response"
|
||||
],
|
||||
"title": "ListRemoteSkillsResponseEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"skills",
|
||||
"type"
|
||||
],
|
||||
"title": "ListRemoteSkillsResponseEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Remote skill downloaded to local cache.",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"remote_skill_downloaded"
|
||||
],
|
||||
"title": "RemoteSkillDownloadedEventMsgType",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"path",
|
||||
"type"
|
||||
],
|
||||
"title": "RemoteSkillDownloadedEventMsg",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "Notification that skill data may have been updated and clients may want to reload.",
|
||||
"properties": {
|
||||
@@ -3429,6 +3482,25 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"RemoteSkillSummary": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"RequestId": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "SkillsRemoteReadParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"RemoteSkillSummary": {
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"description",
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/RemoteSkillSummary"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "SkillsRemoteReadResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"hazelnutId": {
|
||||
"type": "string"
|
||||
},
|
||||
"isPreload": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"hazelnutId",
|
||||
"isPreload"
|
||||
],
|
||||
"title": "SkillsRemoteWriteParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"path"
|
||||
],
|
||||
"title": "SkillsRemoteWriteResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -36,6 +36,8 @@ import type { ModelListParams } from "./v2/ModelListParams";
|
||||
import type { ReviewStartParams } from "./v2/ReviewStartParams";
|
||||
import type { SkillsConfigWriteParams } from "./v2/SkillsConfigWriteParams";
|
||||
import type { SkillsListParams } from "./v2/SkillsListParams";
|
||||
import type { SkillsRemoteReadParams } from "./v2/SkillsRemoteReadParams";
|
||||
import type { SkillsRemoteWriteParams } from "./v2/SkillsRemoteWriteParams";
|
||||
import type { ThreadArchiveParams } from "./v2/ThreadArchiveParams";
|
||||
import type { ThreadForkParams } from "./v2/ThreadForkParams";
|
||||
import type { ThreadListParams } from "./v2/ThreadListParams";
|
||||
@@ -52,4 +54,4 @@ import type { TurnStartParams } from "./v2/TurnStartParams";
|
||||
/**
|
||||
* Request from the client to the server.
|
||||
*/
|
||||
export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "newConversation", id: RequestId, params: NewConversationParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "listConversations", id: RequestId, params: ListConversationsParams, } | { "method": "resumeConversation", id: RequestId, params: ResumeConversationParams, } | { "method": "forkConversation", id: RequestId, params: ForkConversationParams, } | { "method": "archiveConversation", id: RequestId, params: ArchiveConversationParams, } | { "method": "sendUserMessage", id: RequestId, params: SendUserMessageParams, } | { "method": "sendUserTurn", id: RequestId, params: SendUserTurnParams, } | { "method": "interruptConversation", id: RequestId, params: InterruptConversationParams, } | { "method": "addConversationListener", id: RequestId, params: AddConversationListenerParams, } | { "method": "removeConversationListener", id: RequestId, params: RemoveConversationListenerParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "loginApiKey", id: RequestId, params: LoginApiKeyParams, } | { "method": "loginChatGpt", id: RequestId, params: undefined, } | { "method": "cancelLoginChatGpt", id: RequestId, params: CancelLoginChatGptParams, } | { "method": "logoutChatGpt", id: RequestId, params: undefined, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "getUserSavedConfig", id: RequestId, params: undefined, } | { "method": "setDefaultModel", id: RequestId, params: SetDefaultModelParams, } | { "method": "getUserAgent", id: RequestId, params: undefined, } | { "method": "userInfo", id: RequestId, params: undefined, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, } | { "method": "execOneOffCommand", id: RequestId, params: ExecOneOffCommandParams, };
|
||||
export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/remote/read", id: RequestId, params: SkillsRemoteReadParams, } | { "method": "skills/remote/write", id: RequestId, params: SkillsRemoteWriteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "newConversation", id: RequestId, params: NewConversationParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "listConversations", id: RequestId, params: ListConversationsParams, } | { "method": "resumeConversation", id: RequestId, params: ResumeConversationParams, } | { "method": "forkConversation", id: RequestId, params: ForkConversationParams, } | { "method": "archiveConversation", id: RequestId, params: ArchiveConversationParams, } | { "method": "sendUserMessage", id: RequestId, params: SendUserMessageParams, } | { "method": "sendUserTurn", id: RequestId, params: SendUserTurnParams, } | { "method": "interruptConversation", id: RequestId, params: InterruptConversationParams, } | { "method": "addConversationListener", id: RequestId, params: AddConversationListenerParams, } | { "method": "removeConversationListener", id: RequestId, params: RemoveConversationListenerParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "loginApiKey", id: RequestId, params: LoginApiKeyParams, } | { "method": "loginChatGpt", id: RequestId, params: undefined, } | { "method": "cancelLoginChatGpt", id: RequestId, params: CancelLoginChatGptParams, } | { "method": "logoutChatGpt", id: RequestId, params: undefined, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "getUserSavedConfig", id: RequestId, params: undefined, } | { "method": "setDefaultModel", id: RequestId, params: SetDefaultModelParams, } | { "method": "getUserAgent", id: RequestId, params: undefined, } | { "method": "userInfo", id: RequestId, params: undefined, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, } | { "method": "execOneOffCommand", id: RequestId, params: ExecOneOffCommandParams, };
|
||||
|
||||
@@ -33,6 +33,7 @@ import type { GetHistoryEntryResponseEvent } from "./GetHistoryEntryResponseEven
|
||||
import type { ItemCompletedEvent } from "./ItemCompletedEvent";
|
||||
import type { ItemStartedEvent } from "./ItemStartedEvent";
|
||||
import type { ListCustomPromptsResponseEvent } from "./ListCustomPromptsResponseEvent";
|
||||
import type { ListRemoteSkillsResponseEvent } from "./ListRemoteSkillsResponseEvent";
|
||||
import type { ListSkillsResponseEvent } from "./ListSkillsResponseEvent";
|
||||
import type { McpListToolsResponseEvent } from "./McpListToolsResponseEvent";
|
||||
import type { McpStartupCompleteEvent } from "./McpStartupCompleteEvent";
|
||||
@@ -45,6 +46,7 @@ import type { PlanDeltaEvent } from "./PlanDeltaEvent";
|
||||
import type { RawResponseItemEvent } from "./RawResponseItemEvent";
|
||||
import type { ReasoningContentDeltaEvent } from "./ReasoningContentDeltaEvent";
|
||||
import type { ReasoningRawContentDeltaEvent } from "./ReasoningRawContentDeltaEvent";
|
||||
import type { RemoteSkillDownloadedEvent } from "./RemoteSkillDownloadedEvent";
|
||||
import type { RequestUserInputEvent } from "./RequestUserInputEvent";
|
||||
import type { ReviewRequest } from "./ReviewRequest";
|
||||
import type { SessionConfiguredEvent } from "./SessionConfiguredEvent";
|
||||
@@ -70,4 +72,4 @@ import type { WebSearchEndEvent } from "./WebSearchEndEvent";
|
||||
* Response event from the agent
|
||||
* NOTE: Make sure none of these values have optional types, as it will mess up the extension code-gen.
|
||||
*/
|
||||
export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent;
|
||||
export type EventMsg = { "type": "error" } & ErrorEvent | { "type": "warning" } & WarningEvent | { "type": "context_compacted" } & ContextCompactedEvent | { "type": "thread_rolled_back" } & ThreadRolledBackEvent | { "type": "task_started" } & TurnStartedEvent | { "type": "task_complete" } & TurnCompleteEvent | { "type": "token_count" } & TokenCountEvent | { "type": "agent_message" } & AgentMessageEvent | { "type": "user_message" } & UserMessageEvent | { "type": "agent_message_delta" } & AgentMessageDeltaEvent | { "type": "agent_reasoning" } & AgentReasoningEvent | { "type": "agent_reasoning_delta" } & AgentReasoningDeltaEvent | { "type": "agent_reasoning_raw_content" } & AgentReasoningRawContentEvent | { "type": "agent_reasoning_raw_content_delta" } & AgentReasoningRawContentDeltaEvent | { "type": "agent_reasoning_section_break" } & AgentReasoningSectionBreakEvent | { "type": "session_configured" } & SessionConfiguredEvent | { "type": "thread_name_updated" } & ThreadNameUpdatedEvent | { "type": "mcp_startup_update" } & McpStartupUpdateEvent | { "type": "mcp_startup_complete" } & McpStartupCompleteEvent | { "type": "mcp_tool_call_begin" } & McpToolCallBeginEvent | { "type": "mcp_tool_call_end" } & McpToolCallEndEvent | { "type": "web_search_begin" } & WebSearchBeginEvent | { "type": "web_search_end" } & WebSearchEndEvent | { "type": "exec_command_begin" } & ExecCommandBeginEvent | { "type": "exec_command_output_delta" } & ExecCommandOutputDeltaEvent | { "type": "terminal_interaction" } & TerminalInteractionEvent | { "type": "exec_command_end" } & ExecCommandEndEvent | { "type": "view_image_tool_call" } & ViewImageToolCallEvent | { "type": "exec_approval_request" } & ExecApprovalRequestEvent | { "type": "request_user_input" } & RequestUserInputEvent | { "type": "dynamic_tool_call_request" } & DynamicToolCallRequest | { "type": "elicitation_request" } & ElicitationRequestEvent | { "type": "apply_patch_approval_request" } & ApplyPatchApprovalRequestEvent | { "type": "deprecation_notice" } & DeprecationNoticeEvent | { "type": "background_event" } & BackgroundEventEvent | { "type": "undo_started" } & UndoStartedEvent | { "type": "undo_completed" } & UndoCompletedEvent | { "type": "stream_error" } & StreamErrorEvent | { "type": "patch_apply_begin" } & PatchApplyBeginEvent | { "type": "patch_apply_end" } & PatchApplyEndEvent | { "type": "turn_diff" } & TurnDiffEvent | { "type": "get_history_entry_response" } & GetHistoryEntryResponseEvent | { "type": "mcp_list_tools_response" } & McpListToolsResponseEvent | { "type": "list_custom_prompts_response" } & ListCustomPromptsResponseEvent | { "type": "list_skills_response" } & ListSkillsResponseEvent | { "type": "list_remote_skills_response" } & ListRemoteSkillsResponseEvent | { "type": "remote_skill_downloaded" } & RemoteSkillDownloadedEvent | { "type": "skills_update_available" } | { "type": "plan_update" } & UpdatePlanArgs | { "type": "turn_aborted" } & TurnAbortedEvent | { "type": "shutdown_complete" } | { "type": "entered_review_mode" } & ReviewRequest | { "type": "exited_review_mode" } & ExitedReviewModeEvent | { "type": "raw_response_item" } & RawResponseItemEvent | { "type": "item_started" } & ItemStartedEvent | { "type": "item_completed" } & ItemCompletedEvent | { "type": "agent_message_content_delta" } & AgentMessageContentDeltaEvent | { "type": "plan_delta" } & PlanDeltaEvent | { "type": "reasoning_content_delta" } & ReasoningContentDeltaEvent | { "type": "reasoning_raw_content_delta" } & ReasoningRawContentDeltaEvent | { "type": "collab_agent_spawn_begin" } & CollabAgentSpawnBeginEvent | { "type": "collab_agent_spawn_end" } & CollabAgentSpawnEndEvent | { "type": "collab_agent_interaction_begin" } & CollabAgentInteractionBeginEvent | { "type": "collab_agent_interaction_end" } & CollabAgentInteractionEndEvent | { "type": "collab_waiting_begin" } & CollabWaitingBeginEvent | { "type": "collab_waiting_end" } & CollabWaitingEndEvent | { "type": "collab_close_begin" } & CollabCloseBeginEvent | { "type": "collab_close_end" } & CollabCloseEndEvent;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { RemoteSkillSummary } from "./RemoteSkillSummary";
|
||||
|
||||
/**
|
||||
* Response payload for `Op::ListRemoteSkills`.
|
||||
*/
|
||||
export type ListRemoteSkillsResponseEvent = { skills: Array<RemoteSkillSummary>, };
|
||||
@@ -0,0 +1,8 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* Response payload for `Op::DownloadRemoteSkill`.
|
||||
*/
|
||||
export type RemoteSkillDownloadedEvent = { id: string, name: string, path: string, };
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type RemoteSkillSummary = { id: string, name: string, description: string, };
|
||||
@@ -98,6 +98,7 @@ export type { ItemStartedEvent } from "./ItemStartedEvent";
|
||||
export type { ListConversationsParams } from "./ListConversationsParams";
|
||||
export type { ListConversationsResponse } from "./ListConversationsResponse";
|
||||
export type { ListCustomPromptsResponseEvent } from "./ListCustomPromptsResponseEvent";
|
||||
export type { ListRemoteSkillsResponseEvent } from "./ListRemoteSkillsResponseEvent";
|
||||
export type { ListSkillsResponseEvent } from "./ListSkillsResponseEvent";
|
||||
export type { LocalShellAction } from "./LocalShellAction";
|
||||
export type { LocalShellExecAction } from "./LocalShellExecAction";
|
||||
@@ -140,6 +141,8 @@ export type { ReasoningItemContent } from "./ReasoningItemContent";
|
||||
export type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary";
|
||||
export type { ReasoningRawContentDeltaEvent } from "./ReasoningRawContentDeltaEvent";
|
||||
export type { ReasoningSummary } from "./ReasoningSummary";
|
||||
export type { RemoteSkillDownloadedEvent } from "./RemoteSkillDownloadedEvent";
|
||||
export type { RemoteSkillSummary } from "./RemoteSkillSummary";
|
||||
export type { RemoveConversationListenerParams } from "./RemoveConversationListenerParams";
|
||||
export type { RemoveConversationSubscriptionResponse } from "./RemoveConversationSubscriptionResponse";
|
||||
export type { RequestId } from "./RequestId";
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type RemoteSkillSummary = { id: string, name: string, description: string, };
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type SkillsRemoteReadParams = Record<string, never>;
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { RemoteSkillSummary } from "./RemoteSkillSummary";
|
||||
|
||||
export type SkillsRemoteReadResponse = { data: Array<RemoteSkillSummary>, };
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type SkillsRemoteWriteParams = { hazelnutId: string, isPreload: boolean, };
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type SkillsRemoteWriteResponse = { id: string, name: string, path: string, };
|
||||
@@ -96,6 +96,7 @@ export type { ReasoningEffortOption } from "./ReasoningEffortOption";
|
||||
export type { ReasoningSummaryPartAddedNotification } from "./ReasoningSummaryPartAddedNotification";
|
||||
export type { ReasoningSummaryTextDeltaNotification } from "./ReasoningSummaryTextDeltaNotification";
|
||||
export type { ReasoningTextDeltaNotification } from "./ReasoningTextDeltaNotification";
|
||||
export type { RemoteSkillSummary } from "./RemoteSkillSummary";
|
||||
export type { ResidencyRequirement } from "./ResidencyRequirement";
|
||||
export type { ReviewDelivery } from "./ReviewDelivery";
|
||||
export type { ReviewStartParams } from "./ReviewStartParams";
|
||||
@@ -116,6 +117,10 @@ export type { SkillsConfigWriteResponse } from "./SkillsConfigWriteResponse";
|
||||
export type { SkillsListEntry } from "./SkillsListEntry";
|
||||
export type { SkillsListParams } from "./SkillsListParams";
|
||||
export type { SkillsListResponse } from "./SkillsListResponse";
|
||||
export type { SkillsRemoteReadParams } from "./SkillsRemoteReadParams";
|
||||
export type { SkillsRemoteReadResponse } from "./SkillsRemoteReadResponse";
|
||||
export type { SkillsRemoteWriteParams } from "./SkillsRemoteWriteParams";
|
||||
export type { SkillsRemoteWriteResponse } from "./SkillsRemoteWriteResponse";
|
||||
export type { TerminalInteractionNotification } from "./TerminalInteractionNotification";
|
||||
export type { TextElement } from "./TextElement";
|
||||
export type { TextPosition } from "./TextPosition";
|
||||
|
||||
@@ -228,6 +228,14 @@ client_request_definitions! {
|
||||
params: v2::SkillsListParams,
|
||||
response: v2::SkillsListResponse,
|
||||
},
|
||||
SkillsRemoteRead => "skills/remote/read" {
|
||||
params: v2::SkillsRemoteReadParams,
|
||||
response: v2::SkillsRemoteReadResponse,
|
||||
},
|
||||
SkillsRemoteWrite => "skills/remote/write" {
|
||||
params: v2::SkillsRemoteWriteParams,
|
||||
response: v2::SkillsRemoteWriteResponse,
|
||||
},
|
||||
AppsList => "app/list" {
|
||||
params: v2::AppsListParams,
|
||||
response: v2::AppsListResponse,
|
||||
|
||||
@@ -1554,6 +1554,44 @@ pub struct SkillsListResponse {
|
||||
pub data: Vec<SkillsListEntry>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct SkillsRemoteReadParams {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct RemoteSkillSummary {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct SkillsRemoteReadResponse {
|
||||
pub data: Vec<RemoteSkillSummary>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct SkillsRemoteWriteParams {
|
||||
pub hazelnut_id: String,
|
||||
pub is_preload: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct SkillsRemoteWriteResponse {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
|
||||
@@ -93,6 +93,8 @@ Example (from OpenAI's official VSCode extension):
|
||||
- `model/list` — list available models (with reasoning effort options).
|
||||
- `collaborationMode/list` — list available collaboration mode presets (experimental, no pagination).
|
||||
- `skills/list` — list skills for one or more `cwd` values (optional `forceReload`).
|
||||
- `skills/remote/read` — list public remote skills (**under development; do not call from production clients yet**).
|
||||
- `skills/remote/write` — download a public remote skill by `hazelnutId`; `isPreload=true` writes to `.codex/vendor_imports/skills` under `codex_home` (**under development; do not call from production clients yet**).
|
||||
- `app/list` — list available apps.
|
||||
- `skills/config/write` — write user-level skill config by path.
|
||||
- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes.
|
||||
|
||||
@@ -97,6 +97,10 @@ use codex_app_server_protocol::SkillsConfigWriteParams;
|
||||
use codex_app_server_protocol::SkillsConfigWriteResponse;
|
||||
use codex_app_server_protocol::SkillsListParams;
|
||||
use codex_app_server_protocol::SkillsListResponse;
|
||||
use codex_app_server_protocol::SkillsRemoteReadParams;
|
||||
use codex_app_server_protocol::SkillsRemoteReadResponse;
|
||||
use codex_app_server_protocol::SkillsRemoteWriteParams;
|
||||
use codex_app_server_protocol::SkillsRemoteWriteResponse;
|
||||
use codex_app_server_protocol::Thread;
|
||||
use codex_app_server_protocol::ThreadArchiveParams;
|
||||
use codex_app_server_protocol::ThreadArchiveResponse;
|
||||
@@ -176,6 +180,8 @@ use codex_core::read_head_for_summary;
|
||||
use codex_core::read_session_meta_line;
|
||||
use codex_core::rollout_date_parts;
|
||||
use codex_core::sandboxing::SandboxPermissions;
|
||||
use codex_core::skills::remote::download_remote_skill;
|
||||
use codex_core::skills::remote::list_remote_skills;
|
||||
use codex_core::state_db::get_state_db;
|
||||
use codex_core::token_data::parse_id_token;
|
||||
use codex_core::windows_sandbox::WindowsSandboxLevelExt;
|
||||
@@ -464,6 +470,12 @@ impl CodexMessageProcessor {
|
||||
ClientRequest::SkillsList { request_id, params } => {
|
||||
self.skills_list(request_id, params).await;
|
||||
}
|
||||
ClientRequest::SkillsRemoteRead { request_id, params } => {
|
||||
self.skills_remote_read(request_id, params).await;
|
||||
}
|
||||
ClientRequest::SkillsRemoteWrite { request_id, params } => {
|
||||
self.skills_remote_write(request_id, params).await;
|
||||
}
|
||||
ClientRequest::AppsList { request_id, params } => {
|
||||
self.apps_list(request_id, params).await;
|
||||
}
|
||||
@@ -4053,6 +4065,61 @@ impl CodexMessageProcessor {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn skills_remote_read(&self, request_id: RequestId, _params: SkillsRemoteReadParams) {
|
||||
match list_remote_skills(&self.config).await {
|
||||
Ok(skills) => {
|
||||
let data = skills
|
||||
.into_iter()
|
||||
.map(|skill| codex_app_server_protocol::RemoteSkillSummary {
|
||||
id: skill.id,
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
})
|
||||
.collect();
|
||||
self.outgoing
|
||||
.send_response(request_id, SkillsRemoteReadResponse { data })
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("failed to read remote skills: {err}"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn skills_remote_write(&self, request_id: RequestId, params: SkillsRemoteWriteParams) {
|
||||
let SkillsRemoteWriteParams {
|
||||
hazelnut_id,
|
||||
is_preload,
|
||||
} = params;
|
||||
let response = download_remote_skill(&self.config, hazelnut_id.as_str(), is_preload).await;
|
||||
|
||||
match response {
|
||||
Ok(downloaded) => {
|
||||
self.outgoing
|
||||
.send_response(
|
||||
request_id,
|
||||
SkillsRemoteWriteResponse {
|
||||
id: downloaded.id,
|
||||
name: downloaded.name,
|
||||
path: downloaded.path,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("failed to download remote skill: {err}"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn skills_config_write(&self, request_id: RequestId, params: SkillsConfigWriteParams) {
|
||||
let SkillsConfigWriteParams { path, enabled } = params;
|
||||
let edits = vec![ConfigEdit::SetSkillConfig { path, enabled }];
|
||||
|
||||
+137
-4
@@ -1,3 +1,4 @@
|
||||
use std::fs::File;
|
||||
use std::future::Future;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
@@ -10,8 +11,24 @@ use tempfile::TempDir;
|
||||
const LINUX_SANDBOX_ARG0: &str = "codex-linux-sandbox";
|
||||
const APPLY_PATCH_ARG0: &str = "apply_patch";
|
||||
const MISSPELLED_APPLY_PATCH_ARG0: &str = "applypatch";
|
||||
const LOCK_FILENAME: &str = ".lock";
|
||||
|
||||
pub fn arg0_dispatch() -> Option<TempDir> {
|
||||
/// Keeps the per-session PATH entry alive and locked for the process lifetime.
|
||||
pub struct Arg0PathEntryGuard {
|
||||
_temp_dir: TempDir,
|
||||
_lock_file: File,
|
||||
}
|
||||
|
||||
impl Arg0PathEntryGuard {
|
||||
fn new(temp_dir: TempDir, lock_file: File) -> Self {
|
||||
Self {
|
||||
_temp_dir: temp_dir,
|
||||
_lock_file: lock_file,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn arg0_dispatch() -> Option<Arg0PathEntryGuard> {
|
||||
// Determine if we were invoked via the special alias.
|
||||
let mut args = std::env::args_os();
|
||||
let argv0 = args.next().unwrap_or_default();
|
||||
@@ -149,7 +166,7 @@ where
|
||||
///
|
||||
/// IMPORTANT: This function modifies the PATH environment variable, so it MUST
|
||||
/// be called before multiple threads are spawned.
|
||||
pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<TempDir> {
|
||||
pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<Arg0PathEntryGuard> {
|
||||
let codex_home = codex_core::config::find_codex_home()?;
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
@@ -167,7 +184,7 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<TempDir> {
|
||||
|
||||
std::fs::create_dir_all(&codex_home)?;
|
||||
// Use a CODEX_HOME-scoped temp root to avoid cluttering the top-level directory.
|
||||
let temp_root = codex_home.join("tmp").join("path");
|
||||
let temp_root = codex_home.join("tmp").join("arg0");
|
||||
std::fs::create_dir_all(&temp_root)?;
|
||||
#[cfg(unix)]
|
||||
{
|
||||
@@ -177,11 +194,25 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<TempDir> {
|
||||
std::fs::set_permissions(&temp_root, std::fs::Permissions::from_mode(0o700))?;
|
||||
}
|
||||
|
||||
// Best-effort cleanup of stale per-session dirs. Ignore failures so startup proceeds.
|
||||
if let Err(err) = janitor_cleanup(&temp_root) {
|
||||
eprintln!("WARNING: failed to clean up stale arg0 temp dirs: {err}");
|
||||
}
|
||||
|
||||
let temp_dir = tempfile::Builder::new()
|
||||
.prefix("codex-arg0")
|
||||
.tempdir_in(&temp_root)?;
|
||||
let path = temp_dir.path();
|
||||
|
||||
let lock_path = path.join(LOCK_FILENAME);
|
||||
let lock_file = File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(&lock_path)?;
|
||||
lock_file.try_lock()?;
|
||||
|
||||
for filename in &[
|
||||
APPLY_PATCH_ARG0,
|
||||
MISSPELLED_APPLY_PATCH_ARG0,
|
||||
@@ -231,5 +262,107 @@ pub fn prepend_path_entry_for_codex_aliases() -> std::io::Result<TempDir> {
|
||||
std::env::set_var("PATH", updated_path_env_var);
|
||||
}
|
||||
|
||||
Ok(temp_dir)
|
||||
Ok(Arg0PathEntryGuard::new(temp_dir, lock_file))
|
||||
}
|
||||
|
||||
fn janitor_cleanup(temp_root: &Path) -> std::io::Result<()> {
|
||||
let entries = match std::fs::read_dir(temp_root) {
|
||||
Ok(entries) => entries,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(()),
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if !path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip the directory if locking fails or the lock is currently held.
|
||||
let Some(_lock_file) = try_lock_dir(&path)? else {
|
||||
continue;
|
||||
};
|
||||
|
||||
match std::fs::remove_dir_all(&path) {
|
||||
Ok(()) => {}
|
||||
// Expected TOCTOU race: directory can disappear after read_dir/lock checks.
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => continue,
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_lock_dir(dir: &Path) -> std::io::Result<Option<File>> {
|
||||
let lock_path = dir.join(LOCK_FILENAME);
|
||||
let lock_file = match File::options().read(true).write(true).open(&lock_path) {
|
||||
Ok(file) => file,
|
||||
Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None),
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
|
||||
match lock_file.try_lock() {
|
||||
Ok(()) => Ok(Some(lock_file)),
|
||||
Err(std::fs::TryLockError::WouldBlock) => Ok(None),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::LOCK_FILENAME;
|
||||
use super::janitor_cleanup;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::path::Path;
|
||||
|
||||
fn create_lock(dir: &Path) -> std::io::Result<File> {
|
||||
let lock_path = dir.join(LOCK_FILENAME);
|
||||
File::options()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(false)
|
||||
.open(lock_path)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn janitor_skips_dirs_without_lock_file() -> std::io::Result<()> {
|
||||
let root = tempfile::tempdir()?;
|
||||
let dir = root.path().join("no-lock");
|
||||
fs::create_dir(&dir)?;
|
||||
|
||||
janitor_cleanup(root.path())?;
|
||||
|
||||
assert!(dir.exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn janitor_skips_dirs_with_held_lock() -> std::io::Result<()> {
|
||||
let root = tempfile::tempdir()?;
|
||||
let dir = root.path().join("locked");
|
||||
fs::create_dir(&dir)?;
|
||||
let lock_file = create_lock(&dir)?;
|
||||
lock_file.try_lock()?;
|
||||
|
||||
janitor_cleanup(root.path())?;
|
||||
|
||||
assert!(dir.exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn janitor_removes_dirs_with_unlocked_lock() -> std::io::Result<()> {
|
||||
let root = tempfile::tempdir()?;
|
||||
let dir = root.path().join("stale");
|
||||
fs::create_dir(&dir)?;
|
||||
create_lock(&dir)?;
|
||||
|
||||
janitor_cleanup(root.path())?;
|
||||
|
||||
assert!(!dir.exists());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,6 +108,7 @@ url = { workspace = true }
|
||||
uuid = { workspace = true, features = ["serde", "v4", "v5"] }
|
||||
which = { workspace = true }
|
||||
wildmatch = { workspace = true }
|
||||
zip = { workspace = true }
|
||||
|
||||
[features]
|
||||
deterministic_process_ids = []
|
||||
|
||||
@@ -2469,6 +2469,22 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
|
||||
Op::ListSkills { cwds, force_reload } => {
|
||||
handlers::list_skills(&sess, sub.id.clone(), cwds, force_reload).await;
|
||||
}
|
||||
Op::ListRemoteSkills => {
|
||||
handlers::list_remote_skills(&sess, &config, sub.id.clone()).await;
|
||||
}
|
||||
Op::DownloadRemoteSkill {
|
||||
hazelnut_id,
|
||||
is_preload,
|
||||
} => {
|
||||
handlers::download_remote_skill(
|
||||
&sess,
|
||||
&config,
|
||||
sub.id.clone(),
|
||||
hazelnut_id,
|
||||
is_preload,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Op::Undo => {
|
||||
handlers::undo(&sess, sub.id.clone()).await;
|
||||
}
|
||||
@@ -2535,9 +2551,12 @@ mod handlers {
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ListCustomPromptsResponseEvent;
|
||||
use codex_protocol::protocol::ListRemoteSkillsResponseEvent;
|
||||
use codex_protocol::protocol::ListSkillsResponseEvent;
|
||||
use codex_protocol::protocol::McpServerRefreshConfig;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::RemoteSkillDownloadedEvent;
|
||||
use codex_protocol::protocol::RemoteSkillSummary;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::ReviewRequest;
|
||||
use codex_protocol::protocol::SkillsListEntry;
|
||||
@@ -2895,6 +2914,77 @@ mod handlers {
|
||||
sess.send_event_raw(event).await;
|
||||
}
|
||||
|
||||
pub async fn list_remote_skills(sess: &Session, config: &Arc<Config>, sub_id: String) {
|
||||
let response = crate::skills::remote::list_remote_skills(config)
|
||||
.await
|
||||
.map(|skills| {
|
||||
skills
|
||||
.into_iter()
|
||||
.map(|skill| RemoteSkillSummary {
|
||||
id: skill.id,
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
match response {
|
||||
Ok(skills) => {
|
||||
let event = Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::ListRemoteSkillsResponse(ListRemoteSkillsResponseEvent {
|
||||
skills,
|
||||
}),
|
||||
};
|
||||
sess.send_event_raw(event).await;
|
||||
}
|
||||
Err(err) => {
|
||||
let event = Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: format!("failed to list remote skills: {err}"),
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
}),
|
||||
};
|
||||
sess.send_event_raw(event).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn download_remote_skill(
|
||||
sess: &Session,
|
||||
config: &Arc<Config>,
|
||||
sub_id: String,
|
||||
hazelnut_id: String,
|
||||
is_preload: bool,
|
||||
) {
|
||||
match crate::skills::remote::download_remote_skill(config, hazelnut_id.as_str(), is_preload)
|
||||
.await
|
||||
{
|
||||
Ok(result) => {
|
||||
let event = Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::RemoteSkillDownloaded(RemoteSkillDownloadedEvent {
|
||||
id: result.id,
|
||||
name: result.name,
|
||||
path: result.path,
|
||||
}),
|
||||
};
|
||||
sess.send_event_raw(event).await;
|
||||
}
|
||||
Err(err) => {
|
||||
let event = Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: format!("failed to download remote skill {hazelnut_id}: {err}"),
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
}),
|
||||
};
|
||||
sess.send_event_raw(event).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn undo(sess: &Arc<Session>, sub_id: String) {
|
||||
let turn_context = sess.new_default_turn_with_sub_id(sub_id).await;
|
||||
sess.spawn_task(turn_context, Vec::new(), UndoTask::new())
|
||||
|
||||
+267
-14
@@ -1103,7 +1103,11 @@ impl ConfigToml {
|
||||
profile_sandbox_mode: Option<SandboxMode>,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
resolved_cwd: &Path,
|
||||
sandbox_policy_constraint: Option<&Constrained<SandboxPolicy>>,
|
||||
) -> SandboxPolicyResolution {
|
||||
let sandbox_mode_was_explicit = sandbox_mode_override.is_some()
|
||||
|| profile_sandbox_mode.is_some()
|
||||
|| self.sandbox_mode.is_some();
|
||||
let resolved_sandbox_mode = sandbox_mode_override
|
||||
.or(profile_sandbox_mode)
|
||||
.or(self.sandbox_mode)
|
||||
@@ -1137,13 +1141,30 @@ impl ConfigToml {
|
||||
SandboxMode::DangerFullAccess => SandboxPolicy::DangerFullAccess,
|
||||
};
|
||||
let mut forced_auto_mode_downgraded_on_windows = false;
|
||||
if cfg!(target_os = "windows")
|
||||
&& matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite)
|
||||
// If the experimental Windows sandbox is enabled, do not force a downgrade.
|
||||
&& windows_sandbox_level == codex_protocol::config_types::WindowsSandboxLevel::Disabled
|
||||
let mut downgrade_workspace_write_if_unsupported = |policy: &mut SandboxPolicy| {
|
||||
if cfg!(target_os = "windows")
|
||||
// If the experimental Windows sandbox is enabled, do not force a downgrade.
|
||||
&& windows_sandbox_level
|
||||
== codex_protocol::config_types::WindowsSandboxLevel::Disabled
|
||||
&& matches!(&*policy, SandboxPolicy::WorkspaceWrite { .. })
|
||||
{
|
||||
*policy = SandboxPolicy::new_read_only_policy();
|
||||
forced_auto_mode_downgraded_on_windows = true;
|
||||
}
|
||||
};
|
||||
if matches!(resolved_sandbox_mode, SandboxMode::WorkspaceWrite) {
|
||||
downgrade_workspace_write_if_unsupported(&mut sandbox_policy);
|
||||
}
|
||||
if !sandbox_mode_was_explicit
|
||||
&& let Some(constraint) = sandbox_policy_constraint
|
||||
&& let Err(err) = constraint.can_set(&sandbox_policy)
|
||||
{
|
||||
sandbox_policy = SandboxPolicy::new_read_only_policy();
|
||||
forced_auto_mode_downgraded_on_windows = true;
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"default sandbox policy is disallowed by requirements; falling back to required default"
|
||||
);
|
||||
sandbox_policy = constraint.get().clone();
|
||||
downgrade_workspace_write_if_unsupported(&mut sandbox_policy);
|
||||
}
|
||||
SandboxPolicyResolution {
|
||||
policy: sandbox_policy,
|
||||
@@ -1373,6 +1394,9 @@ impl Config {
|
||||
let active_project = cfg
|
||||
.get_active_project(&resolved_cwd)
|
||||
.unwrap_or(ProjectConfig { trust_level: None });
|
||||
let sandbox_mode_was_explicit = sandbox_mode.is_some()
|
||||
|| config_profile.sandbox_mode.is_some()
|
||||
|| cfg.sandbox_mode.is_some();
|
||||
|
||||
let windows_sandbox_level = WindowsSandboxLevel::from_features(&features);
|
||||
let SandboxPolicyResolution {
|
||||
@@ -1383,6 +1407,7 @@ impl Config {
|
||||
config_profile.sandbox_mode,
|
||||
windows_sandbox_level,
|
||||
&resolved_cwd,
|
||||
Some(&requirements.sandbox_policy),
|
||||
);
|
||||
if let SandboxPolicy::WorkspaceWrite { writable_roots, .. } = &mut sandbox_policy {
|
||||
for path in additional_writable_roots {
|
||||
@@ -1391,7 +1416,10 @@ impl Config {
|
||||
}
|
||||
}
|
||||
}
|
||||
let approval_policy = approval_policy_override
|
||||
let approval_policy_was_explicit = approval_policy_override.is_some()
|
||||
|| config_profile.approval_policy.is_some()
|
||||
|| cfg.approval_policy.is_some();
|
||||
let mut approval_policy = approval_policy_override
|
||||
.or(config_profile.approval_policy)
|
||||
.or(cfg.approval_policy)
|
||||
.unwrap_or_else(|| {
|
||||
@@ -1403,16 +1431,20 @@ impl Config {
|
||||
AskForApproval::default()
|
||||
}
|
||||
});
|
||||
if !approval_policy_was_explicit
|
||||
&& let Err(err) = requirements.approval_policy.can_set(&approval_policy)
|
||||
{
|
||||
tracing::warn!(
|
||||
error = %err,
|
||||
"default approval policy is disallowed by requirements; falling back to required default"
|
||||
);
|
||||
approval_policy = requirements.approval_policy.value();
|
||||
}
|
||||
let web_search_mode = resolve_web_search_mode(&cfg, &config_profile, &features);
|
||||
// TODO(dylan): We should be able to leverage ConfigLayerStack so that
|
||||
// we can reliably check this at every config level.
|
||||
let did_user_set_custom_approval_policy_or_sandbox_mode = approval_policy_override
|
||||
.is_some()
|
||||
|| config_profile.approval_policy.is_some()
|
||||
|| cfg.approval_policy.is_some()
|
||||
|| sandbox_mode.is_some()
|
||||
|| config_profile.sandbox_mode.is_some()
|
||||
|| cfg.sandbox_mode.is_some();
|
||||
let did_user_set_custom_approval_policy_or_sandbox_mode =
|
||||
approval_policy_was_explicit || sandbox_mode_was_explicit;
|
||||
|
||||
let mut model_providers = built_in_model_providers();
|
||||
// Merge user-defined providers into the built-in list.
|
||||
@@ -1934,6 +1966,7 @@ network_access = false # This should be ignored.
|
||||
None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
&PathBuf::from("/tmp/test"),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
resolution,
|
||||
@@ -1958,6 +1991,7 @@ network_access = true # This should be ignored.
|
||||
None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
&PathBuf::from("/tmp/test"),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
resolution,
|
||||
@@ -1990,6 +2024,7 @@ exclude_slash_tmp = true
|
||||
None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
&PathBuf::from("/tmp/test"),
|
||||
None,
|
||||
);
|
||||
if cfg!(target_os = "windows") {
|
||||
assert_eq!(
|
||||
@@ -2039,6 +2074,7 @@ trust_level = "trusted"
|
||||
None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
&PathBuf::from("/tmp/test"),
|
||||
None,
|
||||
);
|
||||
if cfg!(target_os = "windows") {
|
||||
assert_eq!(
|
||||
@@ -4350,6 +4386,7 @@ trust_level = "untrusted"
|
||||
None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
&PathBuf::from("/tmp/test"),
|
||||
None,
|
||||
);
|
||||
|
||||
// Verify that untrusted projects get WorkspaceWrite (or ReadOnly on Windows due to downgrade)
|
||||
@@ -4370,6 +4407,103 @@ trust_level = "untrusted"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_sandbox_policy_falls_back_to_constraint_value_for_implicit_defaults()
|
||||
-> anyhow::Result<()> {
|
||||
let project_dir = TempDir::new()?;
|
||||
let project_path = project_dir.path().to_path_buf();
|
||||
let project_key = project_path.to_string_lossy().to_string();
|
||||
let cfg = ConfigToml {
|
||||
projects: Some(HashMap::from([(
|
||||
project_key,
|
||||
ProjectConfig {
|
||||
trust_level: Some(TrustLevel::Trusted),
|
||||
},
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
let constrained = Constrained::new(SandboxPolicy::DangerFullAccess, |candidate| {
|
||||
if matches!(candidate, SandboxPolicy::DangerFullAccess) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: format!("{candidate:?}"),
|
||||
allowed: "[DangerFullAccess]".to_string(),
|
||||
requirement_source: RequirementSource::Unknown,
|
||||
})
|
||||
}
|
||||
})?;
|
||||
|
||||
let resolution = cfg.derive_sandbox_policy(
|
||||
None,
|
||||
None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
&project_path,
|
||||
Some(&constrained),
|
||||
);
|
||||
|
||||
assert_eq!(resolution.policy, SandboxPolicy::DangerFullAccess);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_sandbox_policy_preserves_windows_downgrade_for_unsupported_fallback()
|
||||
-> anyhow::Result<()> {
|
||||
let project_dir = TempDir::new()?;
|
||||
let project_path = project_dir.path().to_path_buf();
|
||||
let project_key = project_path.to_string_lossy().to_string();
|
||||
let cfg = ConfigToml {
|
||||
projects: Some(HashMap::from([(
|
||||
project_key,
|
||||
ProjectConfig {
|
||||
trust_level: Some(TrustLevel::Trusted),
|
||||
},
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
let constrained =
|
||||
Constrained::new(SandboxPolicy::new_workspace_write_policy(), |candidate| {
|
||||
if matches!(candidate, SandboxPolicy::WorkspaceWrite { .. }) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "sandbox_mode",
|
||||
candidate: format!("{candidate:?}"),
|
||||
allowed: "[WorkspaceWrite]".to_string(),
|
||||
requirement_source: RequirementSource::Unknown,
|
||||
})
|
||||
}
|
||||
})?;
|
||||
|
||||
let resolution = cfg.derive_sandbox_policy(
|
||||
None,
|
||||
None,
|
||||
WindowsSandboxLevel::Disabled,
|
||||
&project_path,
|
||||
Some(&constrained),
|
||||
);
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
assert_eq!(
|
||||
resolution,
|
||||
SandboxPolicyResolution {
|
||||
policy: SandboxPolicy::ReadOnly,
|
||||
forced_auto_mode_downgraded_on_windows: true,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
assert_eq!(
|
||||
resolution,
|
||||
SandboxPolicyResolution {
|
||||
policy: SandboxPolicy::new_workspace_write_policy(),
|
||||
forced_auto_mode_downgraded_on_windows: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_oss_provider_explicit_override() {
|
||||
let config_toml = ConfigToml::default();
|
||||
@@ -4525,6 +4659,125 @@ mcp_oauth_callback_port = 5678
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn requirements_disallowing_default_sandbox_falls_back_to_required_default()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Some(crate::config_loader::ConfigRequirementsToml {
|
||||
allowed_sandbox_modes: Some(vec![
|
||||
crate::config_loader::SandboxModeRequirement::ReadOnly,
|
||||
]),
|
||||
..Default::default()
|
||||
})
|
||||
}))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(*config.sandbox_policy.get(), SandboxPolicy::ReadOnly);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn explicit_sandbox_mode_still_errors_when_disallowed_by_requirements()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"sandbox_mode = "danger-full-access"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let requirements = crate::config_loader::ConfigRequirementsToml {
|
||||
allowed_approval_policies: None,
|
||||
allowed_sandbox_modes: Some(vec![
|
||||
crate::config_loader::SandboxModeRequirement::ReadOnly,
|
||||
]),
|
||||
mcp_servers: None,
|
||||
rules: None,
|
||||
enforce_residency: None,
|
||||
};
|
||||
|
||||
let err = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cloud_requirements(CloudRequirementsLoader::new(
|
||||
async move { Some(requirements) },
|
||||
))
|
||||
.build()
|
||||
.await
|
||||
.expect_err("explicit disallowed mode should still fail");
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
let message = err.to_string();
|
||||
assert!(message.contains("invalid value for `sandbox_mode`"));
|
||||
assert!(message.contains("set by cloud requirements"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn requirements_disallowing_default_approval_falls_back_to_required_default()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let workspace = TempDir::new()?;
|
||||
let workspace_key = workspace.path().to_string_lossy().replace('\\', "\\\\");
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
format!(
|
||||
r#"
|
||||
[projects."{workspace_key}"]
|
||||
trust_level = "untrusted"
|
||||
"#
|
||||
),
|
||||
)?;
|
||||
|
||||
let config = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(workspace.path().to_path_buf()))
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Some(crate::config_loader::ConfigRequirementsToml {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
|
||||
..Default::default()
|
||||
})
|
||||
}))
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
assert_eq!(config.approval_policy.value(), AskForApproval::OnRequest);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn explicit_approval_policy_still_errors_when_disallowed_by_requirements()
|
||||
-> std::io::Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
std::fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"approval_policy = "untrusted"
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let err = ConfigBuilder::default()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.fallback_cwd(Some(codex_home.path().to_path_buf()))
|
||||
.cloud_requirements(CloudRequirementsLoader::new(async {
|
||||
Some(crate::config_loader::ConfigRequirementsToml {
|
||||
allowed_approval_policies: Some(vec![AskForApproval::OnRequest]),
|
||||
..Default::default()
|
||||
})
|
||||
}))
|
||||
.build()
|
||||
.await
|
||||
.expect_err("explicit disallowed approval policy should fail");
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
|
||||
let message = err.to_string();
|
||||
assert!(message.contains("invalid value for `approval_policy`"));
|
||||
assert!(message.contains("set by cloud requirements"));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -463,16 +463,8 @@ impl McpConnectionManager {
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
pub async fn list_all_tools(&self) -> HashMap<String, ToolInfo> {
|
||||
let mut tools = HashMap::new();
|
||||
for (server_name, managed_client) in &self.clients {
|
||||
let client = if server_name == CODEX_APPS_MCP_SERVER_NAME {
|
||||
// Avoid blocking on codex_apps_mcp startup; use tools only when ready.
|
||||
match managed_client.client.clone().now_or_never() {
|
||||
Some(Ok(client)) => Some(client),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
managed_client.client().await.ok()
|
||||
};
|
||||
for managed_client in self.clients.values() {
|
||||
let client = managed_client.client().await.ok();
|
||||
if let Some(client) = client {
|
||||
tools.extend(qualify_tools(filter_tools(
|
||||
client.tools,
|
||||
|
||||
@@ -90,6 +90,8 @@ pub(crate) fn should_persist_event_msg(ev: &EventMsg) -> bool {
|
||||
| EventMsg::McpStartupComplete(_)
|
||||
| EventMsg::ListCustomPromptsResponse(_)
|
||||
| EventMsg::ListSkillsResponse(_)
|
||||
| EventMsg::ListRemoteSkillsResponse(_)
|
||||
| EventMsg::RemoteSkillDownloaded(_)
|
||||
| EventMsg::PlanUpdate(_)
|
||||
| EventMsg::ShutdownComplete
|
||||
| EventMsg::ViewImageToolCall(_)
|
||||
|
||||
@@ -3,6 +3,7 @@ pub mod injection;
|
||||
pub mod loader;
|
||||
pub mod manager;
|
||||
pub mod model;
|
||||
pub mod remote;
|
||||
pub mod render;
|
||||
pub mod system;
|
||||
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::default_client::build_reqwest_client;
|
||||
|
||||
const REMOTE_SKILLS_API_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RemoteSkillSummary {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RemoteSkillDownload {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub base_sediment_id: String,
|
||||
pub files: HashMap<String, RemoteSkillFileRange>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RemoteSkillDownloadResult {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct RemoteSkillFileRange {
|
||||
pub start: u64,
|
||||
pub length: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RemoteSkillsResponse {
|
||||
hazelnuts: Vec<RemoteSkill>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RemoteSkill {
|
||||
id: String,
|
||||
name: String,
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RemoteSkillsDownloadResponse {
|
||||
hazelnuts: Vec<RemoteSkillDownloadPayload>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RemoteSkillDownloadPayload {
|
||||
id: String,
|
||||
name: String,
|
||||
#[serde(rename = "base_sediment_id")]
|
||||
base_sediment_id: String,
|
||||
files: HashMap<String, RemoteSkillFileRangePayload>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct RemoteSkillFileRangePayload {
|
||||
start: u64,
|
||||
length: u64,
|
||||
}
|
||||
|
||||
pub async fn list_remote_skills(config: &Config) -> Result<Vec<RemoteSkillSummary>> {
|
||||
let base_url = config.chatgpt_base_url.trim_end_matches('/');
|
||||
let base_url = base_url.strip_suffix("/backend-api").unwrap_or(base_url);
|
||||
let url = format!("{base_url}/public-api/hazelnuts/");
|
||||
|
||||
let client = build_reqwest_client();
|
||||
let response = client
|
||||
.get(&url)
|
||||
.timeout(REMOTE_SKILLS_API_TIMEOUT)
|
||||
.query(&[("product_surface", "codex")])
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("Failed to send request to {url}"))?;
|
||||
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
if !status.is_success() {
|
||||
anyhow::bail!("Request failed with status {status} from {url}: {body}");
|
||||
}
|
||||
|
||||
let parsed: RemoteSkillsResponse =
|
||||
serde_json::from_str(&body).context("Failed to parse skills response")?;
|
||||
|
||||
Ok(parsed
|
||||
.hazelnuts
|
||||
.into_iter()
|
||||
.map(|skill| RemoteSkillSummary {
|
||||
id: skill.id,
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub async fn download_remote_skill(
|
||||
config: &Config,
|
||||
hazelnut_id: &str,
|
||||
is_preload: bool,
|
||||
) -> Result<RemoteSkillDownloadResult> {
|
||||
let hazelnut = fetch_remote_skill(config, hazelnut_id).await?;
|
||||
|
||||
let client = build_reqwest_client();
|
||||
let base_url = config.chatgpt_base_url.trim_end_matches('/');
|
||||
let base_url = base_url.strip_suffix("/backend-api").unwrap_or(base_url);
|
||||
let url = format!("{base_url}/public-api/hazelnuts/{hazelnut_id}/export");
|
||||
let response = client
|
||||
.get(&url)
|
||||
.timeout(REMOTE_SKILLS_API_TIMEOUT)
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("Failed to send download request to {url}"))?;
|
||||
|
||||
let status = response.status();
|
||||
let body = response.bytes().await.context("Failed to read download")?;
|
||||
if !status.is_success() {
|
||||
let body_text = String::from_utf8_lossy(&body);
|
||||
anyhow::bail!("Download failed with status {status} from {url}: {body_text}");
|
||||
}
|
||||
|
||||
if !is_zip_payload(&body) {
|
||||
anyhow::bail!("Downloaded remote skill payload is not a zip archive");
|
||||
}
|
||||
|
||||
let preferred_dir_name = if hazelnut.name.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(hazelnut.name.as_str())
|
||||
};
|
||||
let dir_name = preferred_dir_name
|
||||
.and_then(validate_dir_name_format)
|
||||
.or_else(|| validate_dir_name_format(&hazelnut.id))
|
||||
.ok_or_else(|| anyhow::anyhow!("Remote skill has no valid directory name"))?;
|
||||
let output_root = if is_preload {
|
||||
config
|
||||
.codex_home
|
||||
.join("vendor_imports")
|
||||
.join("skills")
|
||||
.join("skills")
|
||||
.join(".curated")
|
||||
} else {
|
||||
config.codex_home.join("skills").join("downloaded")
|
||||
};
|
||||
let output_dir = output_root.join(dir_name);
|
||||
tokio::fs::create_dir_all(&output_dir)
|
||||
.await
|
||||
.context("Failed to create downloaded skills directory")?;
|
||||
|
||||
let allowed_files = hazelnut.files.keys().cloned().collect::<HashSet<String>>();
|
||||
let zip_bytes = body.to_vec();
|
||||
let output_dir_clone = output_dir.clone();
|
||||
let prefix_candidates = vec![hazelnut.name.clone(), hazelnut.id.clone()];
|
||||
tokio::task::spawn_blocking(move || {
|
||||
extract_zip_to_dir(
|
||||
zip_bytes,
|
||||
&output_dir_clone,
|
||||
&allowed_files,
|
||||
&prefix_candidates,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.context("Zip extraction task failed")??;
|
||||
|
||||
Ok(RemoteSkillDownloadResult {
|
||||
id: hazelnut.id,
|
||||
name: hazelnut.name,
|
||||
path: output_dir,
|
||||
})
|
||||
}
|
||||
|
||||
fn safe_join(base: &Path, name: &str) -> Result<PathBuf> {
|
||||
let path = Path::new(name);
|
||||
for component in path.components() {
|
||||
match component {
|
||||
Component::Normal(_) => {}
|
||||
_ => {
|
||||
anyhow::bail!("Invalid file path in remote skill payload: {name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(base.join(path))
|
||||
}
|
||||
|
||||
fn validate_dir_name_format(name: &str) -> Option<String> {
|
||||
let mut components = Path::new(name).components();
|
||||
match (components.next(), components.next()) {
|
||||
(Some(Component::Normal(component)), None) => {
|
||||
let value = component.to_string_lossy().to_string();
|
||||
if value.is_empty() { None } else { Some(value) }
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_zip_payload(bytes: &[u8]) -> bool {
|
||||
bytes.starts_with(b"PK\x03\x04")
|
||||
|| bytes.starts_with(b"PK\x05\x06")
|
||||
|| bytes.starts_with(b"PK\x07\x08")
|
||||
}
|
||||
|
||||
fn extract_zip_to_dir(
|
||||
bytes: Vec<u8>,
|
||||
output_dir: &Path,
|
||||
allowed_files: &HashSet<String>,
|
||||
prefix_candidates: &[String],
|
||||
) -> Result<()> {
|
||||
let cursor = std::io::Cursor::new(bytes);
|
||||
let mut archive = zip::ZipArchive::new(cursor).context("Failed to open zip archive")?;
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i).context("Failed to read zip entry")?;
|
||||
if file.is_dir() {
|
||||
continue;
|
||||
}
|
||||
let raw_name = file.name().to_string();
|
||||
let normalized = normalize_zip_name(&raw_name, prefix_candidates);
|
||||
let Some(normalized) = normalized else {
|
||||
continue;
|
||||
};
|
||||
if !allowed_files.contains(&normalized) {
|
||||
continue;
|
||||
}
|
||||
let file_path = safe_join(output_dir, &normalized)?;
|
||||
if let Some(parent) = file_path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("Failed to create parent dir for {normalized}"))?;
|
||||
}
|
||||
let mut out = std::fs::File::create(&file_path)
|
||||
.with_context(|| format!("Failed to create file {normalized}"))?;
|
||||
std::io::copy(&mut file, &mut out)
|
||||
.with_context(|| format!("Failed to write skill file {normalized}"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_zip_name(name: &str, prefix_candidates: &[String]) -> Option<String> {
|
||||
let mut trimmed = name.trim_start_matches("./");
|
||||
for prefix in prefix_candidates {
|
||||
if prefix.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let prefix = format!("{prefix}/");
|
||||
if let Some(rest) = trimmed.strip_prefix(&prefix) {
|
||||
trimmed = rest;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if trimmed.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
async fn fetch_remote_skill(config: &Config, hazelnut_id: &str) -> Result<RemoteSkillDownload> {
|
||||
let base_url = config.chatgpt_base_url.trim_end_matches('/');
|
||||
let base_url = base_url.strip_suffix("/backend-api").unwrap_or(base_url);
|
||||
let url = format!("{base_url}/public-api/hazelnuts/");
|
||||
|
||||
let client = build_reqwest_client();
|
||||
let response = client
|
||||
.get(&url)
|
||||
.timeout(REMOTE_SKILLS_API_TIMEOUT)
|
||||
.query(&[("product_surface", "codex")])
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("Failed to send request to {url}"))?;
|
||||
|
||||
let status = response.status();
|
||||
let body = response.text().await.unwrap_or_default();
|
||||
if !status.is_success() {
|
||||
anyhow::bail!("Request failed with status {status} from {url}: {body}");
|
||||
}
|
||||
|
||||
let parsed: RemoteSkillsDownloadResponse =
|
||||
serde_json::from_str(&body).context("Failed to parse skills response")?;
|
||||
let hazelnut = parsed
|
||||
.hazelnuts
|
||||
.into_iter()
|
||||
.find(|hazelnut| hazelnut.id == hazelnut_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("Remote skill {hazelnut_id} not found"))?;
|
||||
|
||||
Ok(RemoteSkillDownload {
|
||||
id: hazelnut.id,
|
||||
name: hazelnut.name,
|
||||
base_sediment_id: hazelnut.base_sediment_id,
|
||||
files: hazelnut
|
||||
.files
|
||||
.into_iter()
|
||||
.map(|(name, range)| {
|
||||
(
|
||||
name,
|
||||
RemoteSkillFileRange {
|
||||
start: range.start,
|
||||
length: range.length,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
}
|
||||
@@ -1,16 +1,57 @@
|
||||
// Aggregates all former standalone integration tests as modules.
|
||||
use std::ffi::OsString;
|
||||
|
||||
use codex_arg0::Arg0PathEntryGuard;
|
||||
use codex_arg0::arg0_dispatch;
|
||||
use ctor::ctor;
|
||||
use tempfile::TempDir;
|
||||
|
||||
struct TestCodexAliasesGuard {
|
||||
_codex_home: TempDir,
|
||||
_arg0: Arg0PathEntryGuard,
|
||||
_previous_codex_home: Option<OsString>,
|
||||
}
|
||||
|
||||
const CODEX_HOME_ENV_VAR: &str = "CODEX_HOME";
|
||||
|
||||
// This code runs before any other tests are run.
|
||||
// It allows the test binary to behave like codex and dispatch to apply_patch and codex-linux-sandbox
|
||||
// based on the arg0.
|
||||
// NOTE: this doesn't work on ARM
|
||||
#[ctor]
|
||||
pub static CODEX_ALIASES_TEMP_DIR: TempDir = unsafe {
|
||||
pub static CODEX_ALIASES_TEMP_DIR: TestCodexAliasesGuard = unsafe {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
arg0_dispatch().unwrap()
|
||||
let codex_home = tempfile::Builder::new()
|
||||
.prefix("codex-core-tests")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let previous_codex_home = std::env::var_os(CODEX_HOME_ENV_VAR);
|
||||
// arg0_dispatch() creates helper links under CODEX_HOME/tmp. Point it at a
|
||||
// test-owned temp dir so startup never mutates the developer's real ~/.codex.
|
||||
//
|
||||
// Safety: #[ctor] runs before tests start, so no test threads exist yet.
|
||||
unsafe {
|
||||
std::env::set_var(CODEX_HOME_ENV_VAR, codex_home.path());
|
||||
}
|
||||
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let arg0 = arg0_dispatch().unwrap();
|
||||
// Restore the process environment immediately so later tests observe the
|
||||
// same CODEX_HOME state they started with.
|
||||
match previous_codex_home.as_ref() {
|
||||
Some(value) => unsafe {
|
||||
std::env::set_var(CODEX_HOME_ENV_VAR, value);
|
||||
},
|
||||
None => unsafe {
|
||||
std::env::remove_var(CODEX_HOME_ENV_VAR);
|
||||
},
|
||||
}
|
||||
|
||||
TestCodexAliasesGuard {
|
||||
_codex_home: codex_home,
|
||||
_arg0: arg0,
|
||||
_previous_codex_home: previous_codex_home,
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
|
||||
@@ -773,6 +773,8 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
| EventMsg::McpListToolsResponse(_)
|
||||
| EventMsg::ListCustomPromptsResponse(_)
|
||||
| EventMsg::ListSkillsResponse(_)
|
||||
| EventMsg::ListRemoteSkillsResponse(_)
|
||||
| EventMsg::RemoteSkillDownloaded(_)
|
||||
| EventMsg::RawResponseItem(_)
|
||||
| EventMsg::UserMessage(_)
|
||||
| EventMsg::EnteredReviewMode(_)
|
||||
|
||||
@@ -322,6 +322,8 @@ async fn run_codex_tool_session_inner(
|
||||
| EventMsg::McpListToolsResponse(_)
|
||||
| EventMsg::ListCustomPromptsResponse(_)
|
||||
| EventMsg::ListSkillsResponse(_)
|
||||
| EventMsg::ListRemoteSkillsResponse(_)
|
||||
| EventMsg::RemoteSkillDownloaded(_)
|
||||
| EventMsg::ExecCommandBegin(_)
|
||||
| EventMsg::TerminalInteraction(_)
|
||||
| EventMsg::ExecCommandOutputDelta(_)
|
||||
|
||||
@@ -269,6 +269,15 @@ pub enum Op {
|
||||
force_reload: bool,
|
||||
},
|
||||
|
||||
/// Request the list of remote skills available via ChatGPT sharing.
|
||||
ListRemoteSkills,
|
||||
|
||||
/// Download a remote skill by id into the local skills cache.
|
||||
DownloadRemoteSkill {
|
||||
hazelnut_id: String,
|
||||
is_preload: bool,
|
||||
},
|
||||
|
||||
/// Request the agent to summarize the current conversation context.
|
||||
/// The agent will use its existing context (either conversation history or previous response id)
|
||||
/// to generate a summary which will be returned as an AgentMessage event.
|
||||
@@ -822,6 +831,12 @@ pub enum EventMsg {
|
||||
/// List of skills available to the agent.
|
||||
ListSkillsResponse(ListSkillsResponseEvent),
|
||||
|
||||
/// List of remote skills available to the agent.
|
||||
ListRemoteSkillsResponse(ListRemoteSkillsResponseEvent),
|
||||
|
||||
/// Remote skill downloaded to local cache.
|
||||
RemoteSkillDownloaded(RemoteSkillDownloadedEvent),
|
||||
|
||||
/// Notification that skill data may have been updated and clients may want to reload.
|
||||
SkillsUpdateAvailable,
|
||||
|
||||
@@ -2127,6 +2142,27 @@ pub struct ListSkillsResponseEvent {
|
||||
pub skills: Vec<SkillsListEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct RemoteSkillSummary {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
/// Response payload for `Op::ListRemoteSkills`.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct ListRemoteSkillsResponseEvent {
|
||||
pub skills: Vec<RemoteSkillSummary>,
|
||||
}
|
||||
|
||||
/// Response payload for `Op::DownloadRemoteSkill`.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct RemoteSkillDownloadedEvent {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
|
||||
@@ -3526,6 +3526,7 @@ impl ChatWidget {
|
||||
EventMsg::McpListToolsResponse(ev) => self.on_list_mcp_tools(ev),
|
||||
EventMsg::ListCustomPromptsResponse(ev) => self.on_list_custom_prompts(ev),
|
||||
EventMsg::ListSkillsResponse(ev) => self.on_list_skills(ev),
|
||||
EventMsg::ListRemoteSkillsResponse(_) | EventMsg::RemoteSkillDownloaded(_) => {}
|
||||
EventMsg::SkillsUpdateAvailable => {
|
||||
self.submit_op(Op::ListSkills {
|
||||
cwds: Vec::new(),
|
||||
|
||||
@@ -233,7 +233,9 @@ pub unsafe fn dacl_has_write_deny_for_sid(p_dacl: *mut ACL, psid: *mut c_void) -
|
||||
| FILE_APPEND_DATA
|
||||
| FILE_WRITE_EA
|
||||
| FILE_WRITE_ATTRIBUTES
|
||||
| GENERIC_WRITE_MASK;
|
||||
| GENERIC_WRITE_MASK
|
||||
| DELETE
|
||||
| FILE_DELETE_CHILD;
|
||||
for i in 0..info.AceCount {
|
||||
let mut p_ace: *mut c_void = std::ptr::null_mut();
|
||||
if GetAce(p_dacl as *const ACL, i, &mut p_ace) == 0 {
|
||||
@@ -477,7 +479,9 @@ pub unsafe fn add_deny_write_ace(path: &Path, psid: *mut c_void) -> Result<bool>
|
||||
| FILE_APPEND_DATA
|
||||
| FILE_WRITE_EA
|
||||
| FILE_WRITE_ATTRIBUTES
|
||||
| GENERIC_WRITE_MASK;
|
||||
| GENERIC_WRITE_MASK
|
||||
| DELETE
|
||||
| FILE_DELETE_CHILD;
|
||||
explicit.grfAccessMode = DENY_ACCESS;
|
||||
explicit.grfInheritance = CONTAINER_INHERIT_ACE | OBJECT_INHERIT_ACE;
|
||||
explicit.Trustee = trustee;
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::acl::path_mask_allows;
|
||||
use crate::cap::cap_sid_file;
|
||||
use crate::cap::load_or_create_cap_sids;
|
||||
use crate::logging::{debug_log, log_note};
|
||||
use crate::path_normalization::canonical_path_key;
|
||||
use crate::policy::SandboxPolicy;
|
||||
use crate::token::convert_string_sid_to_sid;
|
||||
use crate::token::world_sid;
|
||||
@@ -31,11 +32,6 @@ const SKIP_DIR_SUFFIXES: &[&str] = &[
|
||||
"/programdata",
|
||||
];
|
||||
|
||||
pub(crate) fn normalize_path_key(p: &Path) -> String {
|
||||
let n = dunce::canonicalize(p).unwrap_or_else(|_| p.to_path_buf());
|
||||
n.to_string_lossy().replace('\\', "/").to_ascii_lowercase()
|
||||
}
|
||||
|
||||
fn unique_push(set: &mut HashSet<PathBuf>, out: &mut Vec<PathBuf>, p: PathBuf) {
|
||||
if let Ok(abs) = p.canonicalize() {
|
||||
if set.insert(abs.clone()) {
|
||||
@@ -131,7 +127,7 @@ pub fn audit_everyone_writable(
|
||||
checked += 1;
|
||||
let has = check_world_writable(&p);
|
||||
if has {
|
||||
let key = normalize_path_key(&p);
|
||||
let key = canonical_path_key(&p);
|
||||
if seen.insert(key) {
|
||||
flagged.push(p);
|
||||
}
|
||||
@@ -149,7 +145,7 @@ pub fn audit_everyone_writable(
|
||||
checked += 1;
|
||||
let has_root = check_world_writable(&root);
|
||||
if has_root {
|
||||
let key = normalize_path_key(&root);
|
||||
let key = canonical_path_key(&root);
|
||||
if seen.insert(key) {
|
||||
flagged.push(root.clone());
|
||||
}
|
||||
@@ -181,7 +177,7 @@ pub fn audit_everyone_writable(
|
||||
checked += 1;
|
||||
let has_child = check_world_writable(&p);
|
||||
if has_child {
|
||||
let key = normalize_path_key(&p);
|
||||
let key = canonical_path_key(&p);
|
||||
if seen.insert(key) {
|
||||
flagged.push(p);
|
||||
}
|
||||
|
||||
@@ -5,14 +5,23 @@ use rand::RngCore;
|
||||
use rand::SeedableRng;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use crate::path_normalization::canonical_path_key;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct CapSids {
|
||||
pub workspace: String,
|
||||
pub readonly: String,
|
||||
/// Per-workspace capability SIDs keyed by canonicalized CWD string.
|
||||
///
|
||||
/// This is used to isolate workspaces from other workspace sandbox writes and to
|
||||
/// apply per-workspace denies (e.g. protect `CWD/.codex`)
|
||||
/// without permanently affecting other workspaces.
|
||||
#[serde(default)]
|
||||
pub workspace_by_cwd: HashMap<String, String>,
|
||||
}
|
||||
|
||||
pub fn cap_sid_file(codex_home: &Path) -> PathBuf {
|
||||
@@ -30,8 +39,7 @@ fn make_random_cap_sid_string() -> String {
|
||||
|
||||
fn persist_caps(path: &Path, caps: &CapSids) -> Result<()> {
|
||||
if let Some(dir) = path.parent() {
|
||||
fs::create_dir_all(dir)
|
||||
.with_context(|| format!("create cap sid dir {}", dir.display()))?;
|
||||
fs::create_dir_all(dir).with_context(|| format!("create cap sid dir {}", dir.display()))?;
|
||||
}
|
||||
let json = serde_json::to_string(caps)?;
|
||||
fs::write(path, json).with_context(|| format!("write cap sid file {}", path.display()))?;
|
||||
@@ -52,6 +60,7 @@ pub fn load_or_create_cap_sids(codex_home: &Path) -> Result<CapSids> {
|
||||
let caps = CapSids {
|
||||
workspace: t.to_string(),
|
||||
readonly: make_random_cap_sid_string(),
|
||||
workspace_by_cwd: HashMap::new(),
|
||||
};
|
||||
persist_caps(&path, &caps)?;
|
||||
return Ok(caps);
|
||||
@@ -60,7 +69,53 @@ pub fn load_or_create_cap_sids(codex_home: &Path) -> Result<CapSids> {
|
||||
let caps = CapSids {
|
||||
workspace: make_random_cap_sid_string(),
|
||||
readonly: make_random_cap_sid_string(),
|
||||
workspace_by_cwd: HashMap::new(),
|
||||
};
|
||||
persist_caps(&path, &caps)?;
|
||||
Ok(caps)
|
||||
}
|
||||
|
||||
/// Returns the workspace-specific capability SID for `cwd`, creating and persisting it if missing.
|
||||
pub fn workspace_cap_sid_for_cwd(codex_home: &Path, cwd: &Path) -> Result<String> {
|
||||
let path = cap_sid_file(codex_home);
|
||||
let mut caps = load_or_create_cap_sids(codex_home)?;
|
||||
let key = canonical_path_key(cwd);
|
||||
if let Some(sid) = caps.workspace_by_cwd.get(&key) {
|
||||
return Ok(sid.clone());
|
||||
}
|
||||
let sid = make_random_cap_sid_string();
|
||||
caps.workspace_by_cwd.insert(key, sid.clone());
|
||||
persist_caps(&path, &caps)?;
|
||||
Ok(sid)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::load_or_create_cap_sids;
|
||||
use super::workspace_cap_sid_for_cwd;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn equivalent_cwd_spellings_share_workspace_sid_key() {
|
||||
let temp = tempfile::tempdir().expect("tempdir");
|
||||
let codex_home = temp.path().join("codex-home");
|
||||
std::fs::create_dir_all(&codex_home).expect("create codex home");
|
||||
|
||||
let workspace = temp.path().join("WorkspaceRoot");
|
||||
std::fs::create_dir_all(&workspace).expect("create workspace root");
|
||||
|
||||
let canonical = dunce::canonicalize(&workspace).expect("canonical workspace root");
|
||||
let alt_spelling = PathBuf::from(canonical.to_string_lossy().replace('\\', "/").to_ascii_uppercase());
|
||||
|
||||
let first_sid =
|
||||
workspace_cap_sid_for_cwd(&codex_home, canonical.as_path()).expect("first sid");
|
||||
let second_sid =
|
||||
workspace_cap_sid_for_cwd(&codex_home, alt_spelling.as_path()).expect("second sid");
|
||||
|
||||
assert_eq!(first_sid, second_sid);
|
||||
|
||||
let caps = load_or_create_cap_sids(&codex_home).expect("load caps");
|
||||
assert_eq!(caps.workspace_by_cwd.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ use anyhow::Result;
|
||||
use codex_windows_sandbox::allow_null_device;
|
||||
use codex_windows_sandbox::convert_string_sid_to_sid;
|
||||
use codex_windows_sandbox::create_process_as_user;
|
||||
use codex_windows_sandbox::create_readonly_token_with_cap_from;
|
||||
use codex_windows_sandbox::create_workspace_write_token_with_cap_from;
|
||||
use codex_windows_sandbox::create_readonly_token_with_caps_from;
|
||||
use codex_windows_sandbox::create_workspace_write_token_with_caps_from;
|
||||
use codex_windows_sandbox::get_current_token_for_restriction;
|
||||
use codex_windows_sandbox::hide_current_user_profile_dir;
|
||||
use codex_windows_sandbox::log_note;
|
||||
@@ -20,7 +20,9 @@ use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use windows_sys::Win32::Foundation::CloseHandle;
|
||||
use windows_sys::Win32::Foundation::GetLastError;
|
||||
use windows_sys::Win32::Foundation::LocalFree;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Foundation::HLOCAL;
|
||||
use windows_sys::Win32::Storage::FileSystem::CreateFileW;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_READ;
|
||||
use windows_sys::Win32::Storage::FileSystem::FILE_GENERIC_WRITE;
|
||||
@@ -48,7 +50,7 @@ struct RunnerRequest {
|
||||
codex_home: PathBuf,
|
||||
// Real user's CODEX_HOME for shared data (caps, config).
|
||||
real_codex_home: PathBuf,
|
||||
cap_sid: String,
|
||||
cap_sids: Vec<String>,
|
||||
command: Vec<String>,
|
||||
cwd: PathBuf,
|
||||
env_map: HashMap<String, String>,
|
||||
@@ -112,27 +114,43 @@ pub fn main() -> Result<()> {
|
||||
);
|
||||
|
||||
let policy = parse_policy(&req.policy_json_or_preset).context("parse policy_json_or_preset")?;
|
||||
let psid_cap: *mut c_void = unsafe { convert_string_sid_to_sid(&req.cap_sid).unwrap() };
|
||||
let mut cap_psids: Vec<*mut c_void> = Vec::new();
|
||||
for sid in &req.cap_sids {
|
||||
let Some(psid) = (unsafe { convert_string_sid_to_sid(sid) }) else {
|
||||
anyhow::bail!("ConvertStringSidToSidW failed for capability SID");
|
||||
};
|
||||
cap_psids.push(psid);
|
||||
}
|
||||
if cap_psids.is_empty() {
|
||||
anyhow::bail!("runner: empty capability SID list");
|
||||
}
|
||||
|
||||
// Create restricted token from current process token.
|
||||
let base = unsafe { get_current_token_for_restriction()? };
|
||||
let token_res: Result<(HANDLE, *mut c_void)> = unsafe {
|
||||
let token_res: Result<HANDLE> = unsafe {
|
||||
match &policy {
|
||||
SandboxPolicy::ReadOnly => create_readonly_token_with_cap_from(base, psid_cap),
|
||||
SandboxPolicy::ReadOnly => create_readonly_token_with_caps_from(base, &cap_psids),
|
||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
create_workspace_write_token_with_cap_from(base, psid_cap)
|
||||
create_workspace_write_token_with_caps_from(base, &cap_psids)
|
||||
}
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
};
|
||||
let (h_token, psid_to_use) = token_res?;
|
||||
let h_token = token_res?;
|
||||
unsafe {
|
||||
CloseHandle(base);
|
||||
}
|
||||
unsafe {
|
||||
allow_null_device(psid_to_use);
|
||||
for psid in &cap_psids {
|
||||
allow_null_device(*psid);
|
||||
}
|
||||
for psid in cap_psids {
|
||||
if !psid.is_null() {
|
||||
LocalFree(psid as HLOCAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Open named pipes for stdio.
|
||||
|
||||
@@ -196,7 +196,7 @@ mod windows_impl {
|
||||
codex_home: PathBuf,
|
||||
// Real user's CODEX_HOME for shared data (caps, config).
|
||||
real_codex_home: PathBuf,
|
||||
cap_sid: String,
|
||||
cap_sids: Vec<String>,
|
||||
request_file: Option<PathBuf>,
|
||||
command: Vec<String>,
|
||||
cwd: PathBuf,
|
||||
@@ -239,14 +239,17 @@ mod windows_impl {
|
||||
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
|
||||
}
|
||||
let caps = load_or_create_cap_sids(codex_home)?;
|
||||
let (psid_to_use, cap_sid_str) = match &policy {
|
||||
let (psid_to_use, cap_sids) = match &policy {
|
||||
SandboxPolicy::ReadOnly => (
|
||||
unsafe { convert_string_sid_to_sid(&caps.readonly).unwrap() },
|
||||
caps.readonly.clone(),
|
||||
vec![caps.readonly.clone()],
|
||||
),
|
||||
SandboxPolicy::WorkspaceWrite { .. } => (
|
||||
unsafe { convert_string_sid_to_sid(&caps.workspace).unwrap() },
|
||||
caps.workspace.clone(),
|
||||
vec![
|
||||
caps.workspace.clone(),
|
||||
crate::cap::workspace_cap_sid_for_cwd(codex_home, cwd)?,
|
||||
],
|
||||
),
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
unreachable!("DangerFullAccess handled above")
|
||||
@@ -294,7 +297,7 @@ mod windows_impl {
|
||||
sandbox_policy_cwd: sandbox_policy_cwd.to_path_buf(),
|
||||
codex_home: sandbox_base.clone(),
|
||||
real_codex_home: codex_home.to_path_buf(),
|
||||
cap_sid: cap_sid_str.clone(),
|
||||
cap_sids: cap_sids.clone(),
|
||||
request_file: Some(req_file.clone()),
|
||||
command: command.clone(),
|
||||
cwd: cwd.to_path_buf(),
|
||||
|
||||
@@ -5,8 +5,21 @@ macro_rules! windows_modules {
|
||||
}
|
||||
|
||||
windows_modules!(
|
||||
acl, allow, audit, cap, dpapi, env, hide_users, identity, logging, policy, process, token,
|
||||
winutil
|
||||
acl,
|
||||
allow,
|
||||
audit,
|
||||
cap,
|
||||
dpapi,
|
||||
env,
|
||||
hide_users,
|
||||
identity,
|
||||
logging,
|
||||
path_normalization,
|
||||
policy,
|
||||
process,
|
||||
token,
|
||||
winutil,
|
||||
workspace_acl
|
||||
);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -19,6 +32,8 @@ mod elevated_impl;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod setup_error;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use acl::add_deny_write_ace;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use acl::allow_null_device;
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -36,6 +51,8 @@ pub use audit::apply_world_writable_scan_and_denies;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use cap::load_or_create_cap_sids;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use cap::workspace_cap_sid_for_cwd;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use dpapi::protect as dpapi_protect;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use dpapi::unprotect as dpapi_unprotect;
|
||||
@@ -54,6 +71,8 @@ pub use logging::log_note;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use logging::LOG_FILE_NAME;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use path_normalization::canonicalize_path;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use policy::parse_policy;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use policy::SandboxPolicy;
|
||||
@@ -88,7 +107,9 @@ pub use token::convert_string_sid_to_sid;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use token::create_readonly_token_with_cap_from;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use token::create_workspace_write_token_with_cap_from;
|
||||
pub use token::create_readonly_token_with_caps_from;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use token::create_workspace_write_token_with_caps_from;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use token::get_current_token_for_restriction;
|
||||
#[cfg(target_os = "windows")]
|
||||
@@ -99,6 +120,10 @@ pub use windows_impl::CaptureResult;
|
||||
pub use winutil::string_from_sid_bytes;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use winutil::to_wide;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use workspace_acl::is_command_cwd_root;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use workspace_acl::protect_workspace_codex_dir;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub use stub::apply_world_writable_scan_and_denies;
|
||||
@@ -116,6 +141,7 @@ mod windows_impl {
|
||||
use super::allow::compute_allow_paths;
|
||||
use super::allow::AllowDenyPaths;
|
||||
use super::cap::load_or_create_cap_sids;
|
||||
use super::cap::workspace_cap_sid_for_cwd;
|
||||
use super::env::apply_no_network_to_env;
|
||||
use super::env::ensure_non_interactive_pager;
|
||||
use super::env::normalize_null_device_env;
|
||||
@@ -123,13 +149,17 @@ mod windows_impl {
|
||||
use super::logging::log_failure;
|
||||
use super::logging::log_start;
|
||||
use super::logging::log_success;
|
||||
use super::path_normalization::canonicalize_path;
|
||||
use super::policy::parse_policy;
|
||||
use super::policy::SandboxPolicy;
|
||||
use super::process::make_env_block;
|
||||
use super::token::convert_string_sid_to_sid;
|
||||
use super::token::create_workspace_write_token_with_caps_from;
|
||||
use super::winutil::format_last_error;
|
||||
use super::winutil::quote_windows_arg;
|
||||
use super::winutil::to_wide;
|
||||
use super::workspace_acl::is_command_cwd_root;
|
||||
use super::workspace_acl::protect_workspace_codex_dir;
|
||||
use anyhow::Result;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
@@ -229,15 +259,25 @@ mod windows_impl {
|
||||
anyhow::bail!("DangerFullAccess and ExternalSandbox are not supported for sandboxing")
|
||||
}
|
||||
let caps = load_or_create_cap_sids(codex_home)?;
|
||||
let (h_token, psid_to_use): (HANDLE, *mut c_void) = unsafe {
|
||||
let (h_token, psid_generic, psid_workspace): (HANDLE, *mut c_void, Option<*mut c_void>) = unsafe {
|
||||
match &policy {
|
||||
SandboxPolicy::ReadOnly => {
|
||||
let psid = convert_string_sid_to_sid(&caps.readonly).unwrap();
|
||||
super::token::create_readonly_token_with_cap(psid)?
|
||||
let (h, _) = super::token::create_readonly_token_with_cap(psid)?;
|
||||
(h, psid, None)
|
||||
}
|
||||
SandboxPolicy::WorkspaceWrite { .. } => {
|
||||
let psid = convert_string_sid_to_sid(&caps.workspace).unwrap();
|
||||
super::token::create_workspace_write_token_with_cap(psid)?
|
||||
let psid_generic = convert_string_sid_to_sid(&caps.workspace).unwrap();
|
||||
let ws_sid = workspace_cap_sid_for_cwd(codex_home, cwd)?;
|
||||
let psid_workspace = convert_string_sid_to_sid(&ws_sid).unwrap();
|
||||
let base = super::token::get_current_token_for_restriction()?;
|
||||
let h_res = create_workspace_write_token_with_caps_from(
|
||||
base,
|
||||
&[psid_generic, psid_workspace],
|
||||
);
|
||||
windows_sys::Win32::Foundation::CloseHandle(base);
|
||||
let h = h_res?;
|
||||
(h, psid_generic, Some(psid_workspace))
|
||||
}
|
||||
SandboxPolicy::DangerFullAccess | SandboxPolicy::ExternalSandbox { .. } => {
|
||||
unreachable!("DangerFullAccess handled above")
|
||||
@@ -261,29 +301,39 @@ mod windows_impl {
|
||||
let persist_aces = is_workspace_write;
|
||||
let AllowDenyPaths { allow, deny } =
|
||||
compute_allow_paths(&policy, sandbox_policy_cwd, ¤t_dir, &env_map);
|
||||
let canonical_cwd = canonicalize_path(¤t_dir);
|
||||
let mut guards: Vec<(PathBuf, *mut c_void)> = Vec::new();
|
||||
unsafe {
|
||||
for p in &allow {
|
||||
if let Ok(added) = add_allow_ace(p, psid_to_use) {
|
||||
let psid = if is_workspace_write && is_command_cwd_root(p, &canonical_cwd) {
|
||||
psid_workspace.unwrap_or(psid_generic)
|
||||
} else {
|
||||
psid_generic
|
||||
};
|
||||
if let Ok(added) = add_allow_ace(p, psid) {
|
||||
if added {
|
||||
if persist_aces {
|
||||
if p.is_dir() {
|
||||
// best-effort seeding omitted intentionally
|
||||
}
|
||||
} else {
|
||||
guards.push((p.clone(), psid_to_use));
|
||||
guards.push((p.clone(), psid));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for p in &deny {
|
||||
if let Ok(added) = add_deny_write_ace(p, psid_to_use) {
|
||||
if let Ok(added) = add_deny_write_ace(p, psid_generic) {
|
||||
if added && !persist_aces {
|
||||
guards.push((p.clone(), psid_to_use));
|
||||
guards.push((p.clone(), psid_generic));
|
||||
}
|
||||
}
|
||||
}
|
||||
allow_null_device(psid_to_use);
|
||||
allow_null_device(psid_generic);
|
||||
if let Some(psid) = psid_workspace {
|
||||
allow_null_device(psid);
|
||||
let _ = protect_workspace_codex_dir(¤t_dir, psid);
|
||||
}
|
||||
}
|
||||
|
||||
let (stdin_pair, stdout_pair, stderr_pair) = unsafe { setup_stdio_pipes()? };
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn canonicalize_path(path: &Path) -> PathBuf {
|
||||
dunce::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
|
||||
}
|
||||
|
||||
pub fn canonical_path_key(path: &Path) -> String {
|
||||
canonicalize_path(path)
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/")
|
||||
.to_ascii_lowercase()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::canonical_path_key;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn canonical_path_key_normalizes_case_and_separators() {
|
||||
let windows_style = Path::new(r"C:\Users\Dev\Repo");
|
||||
let slash_style = Path::new("c:/users/dev/repo");
|
||||
|
||||
assert_eq!(canonical_path_key(windows_style), canonical_path_key(slash_style));
|
||||
}
|
||||
}
|
||||
@@ -6,18 +6,22 @@ use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64;
|
||||
use base64::Engine;
|
||||
use codex_windows_sandbox::canonicalize_path;
|
||||
use codex_windows_sandbox::convert_string_sid_to_sid;
|
||||
use codex_windows_sandbox::ensure_allow_mask_aces_with_inheritance;
|
||||
use codex_windows_sandbox::ensure_allow_write_aces;
|
||||
use codex_windows_sandbox::extract_setup_failure;
|
||||
use codex_windows_sandbox::hide_newly_created_users;
|
||||
use codex_windows_sandbox::is_command_cwd_root;
|
||||
use codex_windows_sandbox::load_or_create_cap_sids;
|
||||
use codex_windows_sandbox::log_note;
|
||||
use codex_windows_sandbox::path_mask_allows;
|
||||
use codex_windows_sandbox::protect_workspace_codex_dir;
|
||||
use codex_windows_sandbox::sandbox_dir;
|
||||
use codex_windows_sandbox::sandbox_secrets_dir;
|
||||
use codex_windows_sandbox::string_from_sid_bytes;
|
||||
use codex_windows_sandbox::to_wide;
|
||||
use codex_windows_sandbox::workspace_cap_sid_for_cwd;
|
||||
use codex_windows_sandbox::write_setup_error_report;
|
||||
use codex_windows_sandbox::SetupErrorCode;
|
||||
use codex_windows_sandbox::SetupErrorReport;
|
||||
@@ -75,6 +79,7 @@ struct Payload {
|
||||
offline_username: String,
|
||||
online_username: String,
|
||||
codex_home: PathBuf,
|
||||
command_cwd: PathBuf,
|
||||
read_roots: Vec<PathBuf>,
|
||||
write_roots: Vec<PathBuf>,
|
||||
real_user: String,
|
||||
@@ -560,6 +565,11 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
|
||||
))
|
||||
})?
|
||||
};
|
||||
let workspace_sid_str = workspace_cap_sid_for_cwd(&payload.codex_home, &payload.command_cwd)?;
|
||||
let workspace_psid = unsafe {
|
||||
convert_string_sid_to_sid(&workspace_sid_str)
|
||||
.ok_or_else(|| anyhow::anyhow!("convert workspace capability SID failed"))?
|
||||
};
|
||||
let mut refresh_errors: Vec<String> = Vec::new();
|
||||
if !refresh_only {
|
||||
let firewall_result = firewall::ensure_offline_outbound_block(&offline_sid_str, log);
|
||||
@@ -609,12 +619,12 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
|
||||
let cap_sid_str = caps.workspace.clone();
|
||||
let sandbox_group_sid_str =
|
||||
string_from_sid_bytes(&sandbox_group_sid).map_err(anyhow::Error::msg)?;
|
||||
let sid_strings = vec![sandbox_group_sid_str, cap_sid_str];
|
||||
let write_mask =
|
||||
FILE_GENERIC_READ | FILE_GENERIC_WRITE | FILE_GENERIC_EXECUTE | DELETE | FILE_DELETE_CHILD;
|
||||
let mut grant_tasks: Vec<PathBuf> = Vec::new();
|
||||
|
||||
let mut seen_write_roots: HashSet<PathBuf> = HashSet::new();
|
||||
let canonical_command_cwd = canonicalize_path(&payload.command_cwd);
|
||||
|
||||
for root in &payload.write_roots {
|
||||
if !seen_write_roots.insert(root.clone()) {
|
||||
@@ -628,7 +638,21 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
|
||||
continue;
|
||||
}
|
||||
let mut need_grant = false;
|
||||
for (label, psid) in [("sandbox_group", sandbox_group_psid), ("cap", cap_psid)] {
|
||||
let is_command_cwd = is_command_cwd_root(root, &canonical_command_cwd);
|
||||
let cap_label = if is_command_cwd {
|
||||
"workspace_cap"
|
||||
} else {
|
||||
"cap"
|
||||
};
|
||||
let cap_psid_for_root = if is_command_cwd {
|
||||
workspace_psid
|
||||
} else {
|
||||
cap_psid
|
||||
};
|
||||
for (label, psid) in [
|
||||
("sandbox_group", sandbox_group_psid),
|
||||
(cap_label, cap_psid_for_root),
|
||||
] {
|
||||
let has = match path_mask_allows(root, &[psid], write_mask, true) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
@@ -667,7 +691,12 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
|
||||
let (tx, rx) = mpsc::channel::<(PathBuf, Result<bool>)>();
|
||||
std::thread::scope(|scope| {
|
||||
for root in grant_tasks {
|
||||
let sid_strings = sid_strings.clone();
|
||||
let is_command_cwd = is_command_cwd_root(&root, &canonical_command_cwd);
|
||||
let sid_strings = if is_command_cwd {
|
||||
vec![sandbox_group_sid_str.clone(), workspace_sid_str.clone()]
|
||||
} else {
|
||||
vec![sandbox_group_sid_str.clone(), cap_sid_str.clone()]
|
||||
};
|
||||
let tx = tx.clone();
|
||||
scope.spawn(move || {
|
||||
// Convert SID strings to psids locally in this thread.
|
||||
@@ -758,6 +787,31 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
|
||||
let _ = std::fs::remove_file(&legacy_users);
|
||||
}
|
||||
}
|
||||
|
||||
// Protect the current workspace's `.codex` directory from tampering (write/delete) by using a
|
||||
// workspace-specific capability SID. If `.codex` doesn't exist yet, skip it (it will be picked
|
||||
// up on the next refresh).
|
||||
match unsafe { protect_workspace_codex_dir(&payload.command_cwd, workspace_psid) } {
|
||||
Ok(true) => {
|
||||
let cwd_codex = payload.command_cwd.join(".codex");
|
||||
log_line(
|
||||
log,
|
||||
&format!(
|
||||
"applied deny ACE to protect workspace .codex {}",
|
||||
cwd_codex.display()
|
||||
),
|
||||
)?;
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(err) => {
|
||||
let cwd_codex = payload.command_cwd.join(".codex");
|
||||
refresh_errors.push(format!("deny ACE failed on {}: {err}", cwd_codex.display()));
|
||||
log_line(
|
||||
log,
|
||||
&format!("deny ACE failed on {}: {err}", cwd_codex.display()),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
unsafe {
|
||||
if !sandbox_group_psid.is_null() {
|
||||
LocalFree(sandbox_group_psid as HLOCAL);
|
||||
@@ -765,6 +819,9 @@ fn run_setup_full(payload: &Payload, log: &mut File, sbx_dir: &Path) -> Result<(
|
||||
if !cap_psid.is_null() {
|
||||
LocalFree(cap_psid as HLOCAL);
|
||||
}
|
||||
if !workspace_psid.is_null() {
|
||||
LocalFree(workspace_psid as HLOCAL);
|
||||
}
|
||||
}
|
||||
if refresh_only && !refresh_errors.is_empty() {
|
||||
log_line(
|
||||
|
||||
@@ -12,6 +12,7 @@ use std::process::Stdio;
|
||||
use crate::allow::compute_allow_paths;
|
||||
use crate::allow::AllowDenyPaths;
|
||||
use crate::logging::log_note;
|
||||
use crate::path_normalization::canonical_path_key;
|
||||
use crate::policy::SandboxPolicy;
|
||||
use crate::setup_error::clear_setup_error_report;
|
||||
use crate::setup_error::failure;
|
||||
@@ -82,6 +83,7 @@ pub fn run_setup_refresh(
|
||||
offline_username: OFFLINE_USERNAME.to_string(),
|
||||
online_username: ONLINE_USERNAME.to_string(),
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
command_cwd: command_cwd.to_path_buf(),
|
||||
read_roots,
|
||||
write_roots,
|
||||
real_user: std::env::var("USERNAME").unwrap_or_else(|_| "Administrators".to_string()),
|
||||
@@ -259,6 +261,7 @@ struct ElevationPayload {
|
||||
offline_username: String,
|
||||
online_username: String,
|
||||
codex_home: PathBuf,
|
||||
command_cwd: PathBuf,
|
||||
read_roots: Vec<PathBuf>,
|
||||
write_roots: Vec<PathBuf>,
|
||||
real_user: String,
|
||||
@@ -475,6 +478,7 @@ pub fn run_elevated_setup(
|
||||
offline_username: OFFLINE_USERNAME.to_string(),
|
||||
online_username: ONLINE_USERNAME.to_string(),
|
||||
codex_home: codex_home.to_path_buf(),
|
||||
command_cwd: command_cwd.to_path_buf(),
|
||||
read_roots,
|
||||
write_roots,
|
||||
real_user: std::env::var("USERNAME").unwrap_or_else(|_| "Administrators".to_string()),
|
||||
@@ -517,14 +521,14 @@ fn build_payload_roots(
|
||||
fn filter_sensitive_write_roots(mut roots: Vec<PathBuf>, codex_home: &Path) -> Vec<PathBuf> {
|
||||
// Never grant capability write access to CODEX_HOME or anything under CODEX_HOME/.sandbox.
|
||||
// These locations contain sandbox control/state and must remain tamper-resistant.
|
||||
let codex_home_key = crate::audit::normalize_path_key(codex_home);
|
||||
let sbx_dir_key = crate::audit::normalize_path_key(&sandbox_dir(codex_home));
|
||||
let codex_home_key = canonical_path_key(codex_home);
|
||||
let sbx_dir_key = canonical_path_key(&sandbox_dir(codex_home));
|
||||
let sbx_dir_prefix = format!("{}/", sbx_dir_key.trim_end_matches('/'));
|
||||
let secrets_dir_key = crate::audit::normalize_path_key(&sandbox_secrets_dir(codex_home));
|
||||
let secrets_dir_key = canonical_path_key(&sandbox_secrets_dir(codex_home));
|
||||
let secrets_dir_prefix = format!("{}/", secrets_dir_key.trim_end_matches('/'));
|
||||
|
||||
roots.retain(|root| {
|
||||
let key = crate::audit::normalize_path_key(root);
|
||||
let key = canonical_path_key(root);
|
||||
key != codex_home_key
|
||||
&& key != sbx_dir_key
|
||||
&& !key.starts_with(&sbx_dir_prefix)
|
||||
|
||||
@@ -282,17 +282,6 @@ unsafe fn enable_single_privilege(h_token: HANDLE, name: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Caller must close the returned token handle.
|
||||
pub unsafe fn create_workspace_write_token_with_cap(
|
||||
psid_capability: *mut c_void,
|
||||
) -> Result<(HANDLE, *mut c_void)> {
|
||||
let base = get_current_token_for_restriction()?;
|
||||
let res = create_workspace_write_token_with_cap_from(base, psid_capability);
|
||||
CloseHandle(base);
|
||||
res
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Caller must close the returned token handle.
|
||||
pub unsafe fn create_readonly_token_with_cap(
|
||||
@@ -306,61 +295,63 @@ pub unsafe fn create_readonly_token_with_cap(
|
||||
|
||||
/// # Safety
|
||||
/// Caller must close the returned token handle; base_token must be a valid primary token.
|
||||
pub unsafe fn create_workspace_write_token_with_cap_from(
|
||||
base_token: HANDLE,
|
||||
psid_capability: *mut c_void,
|
||||
) -> Result<(HANDLE, *mut c_void)> {
|
||||
let mut logon_sid_bytes = get_logon_sid_bytes(base_token)?;
|
||||
let psid_logon = logon_sid_bytes.as_mut_ptr() as *mut c_void;
|
||||
let mut everyone = world_sid()?;
|
||||
let psid_everyone = everyone.as_mut_ptr() as *mut c_void;
|
||||
let mut entries: [SID_AND_ATTRIBUTES; 3] = std::mem::zeroed();
|
||||
// Exact set and order: Capability, Logon, Everyone
|
||||
entries[0].Sid = psid_capability;
|
||||
entries[0].Attributes = 0;
|
||||
entries[1].Sid = psid_logon;
|
||||
entries[1].Attributes = 0;
|
||||
entries[2].Sid = psid_everyone;
|
||||
entries[2].Attributes = 0;
|
||||
let mut new_token: HANDLE = 0;
|
||||
let flags = DISABLE_MAX_PRIVILEGE | LUA_TOKEN | WRITE_RESTRICTED;
|
||||
let ok = CreateRestrictedToken(
|
||||
base_token,
|
||||
flags,
|
||||
0,
|
||||
std::ptr::null(),
|
||||
0,
|
||||
std::ptr::null(),
|
||||
3,
|
||||
entries.as_mut_ptr(),
|
||||
&mut new_token,
|
||||
);
|
||||
if ok == 0 {
|
||||
return Err(anyhow!("CreateRestrictedToken failed: {}", GetLastError()));
|
||||
}
|
||||
set_default_dacl(new_token, &[psid_logon, psid_everyone, psid_capability])?;
|
||||
enable_single_privilege(new_token, "SeChangeNotifyPrivilege")?;
|
||||
Ok((new_token, psid_capability))
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Caller must close the returned token handle; base_token must be a valid primary token.
|
||||
pub unsafe fn create_readonly_token_with_cap_from(
|
||||
base_token: HANDLE,
|
||||
psid_capability: *mut c_void,
|
||||
) -> Result<(HANDLE, *mut c_void)> {
|
||||
let new_token = create_token_with_caps_from(base_token, &[psid_capability])?;
|
||||
Ok((new_token, psid_capability))
|
||||
}
|
||||
|
||||
/// Create a restricted token that includes all provided capability SIDs.
|
||||
///
|
||||
/// # Safety
|
||||
/// Caller must close the returned token handle; base_token must be a valid primary token.
|
||||
pub unsafe fn create_workspace_write_token_with_caps_from(
|
||||
base_token: HANDLE,
|
||||
psid_capabilities: &[*mut c_void],
|
||||
) -> Result<HANDLE> {
|
||||
create_token_with_caps_from(base_token, psid_capabilities)
|
||||
}
|
||||
|
||||
/// Create a restricted token that includes all provided capability SIDs.
|
||||
///
|
||||
/// # Safety
|
||||
/// Caller must close the returned token handle; base_token must be a valid primary token.
|
||||
pub unsafe fn create_readonly_token_with_caps_from(
|
||||
base_token: HANDLE,
|
||||
psid_capabilities: &[*mut c_void],
|
||||
) -> Result<HANDLE> {
|
||||
create_token_with_caps_from(base_token, psid_capabilities)
|
||||
}
|
||||
|
||||
unsafe fn create_token_with_caps_from(
|
||||
base_token: HANDLE,
|
||||
psid_capabilities: &[*mut c_void],
|
||||
) -> Result<HANDLE> {
|
||||
if psid_capabilities.is_empty() {
|
||||
return Err(anyhow!("no capability SIDs provided"));
|
||||
}
|
||||
let mut logon_sid_bytes = get_logon_sid_bytes(base_token)?;
|
||||
let psid_logon = logon_sid_bytes.as_mut_ptr() as *mut c_void;
|
||||
let mut everyone = world_sid()?;
|
||||
let psid_everyone = everyone.as_mut_ptr() as *mut c_void;
|
||||
let mut entries: [SID_AND_ATTRIBUTES; 3] = std::mem::zeroed();
|
||||
// Exact set and order: Capability, Logon, Everyone
|
||||
entries[0].Sid = psid_capability;
|
||||
entries[0].Attributes = 0;
|
||||
entries[1].Sid = psid_logon;
|
||||
entries[1].Attributes = 0;
|
||||
entries[2].Sid = psid_everyone;
|
||||
entries[2].Attributes = 0;
|
||||
|
||||
// Exact order: Capabilities..., Logon, Everyone
|
||||
let mut entries: Vec<SID_AND_ATTRIBUTES> =
|
||||
vec![std::mem::zeroed(); psid_capabilities.len() + 2];
|
||||
for (i, psid) in psid_capabilities.iter().enumerate() {
|
||||
entries[i].Sid = *psid;
|
||||
entries[i].Attributes = 0;
|
||||
}
|
||||
let logon_idx = psid_capabilities.len();
|
||||
entries[logon_idx].Sid = psid_logon;
|
||||
entries[logon_idx].Attributes = 0;
|
||||
entries[logon_idx + 1].Sid = psid_everyone;
|
||||
entries[logon_idx + 1].Attributes = 0;
|
||||
|
||||
let mut new_token: HANDLE = 0;
|
||||
let flags = DISABLE_MAX_PRIVILEGE | LUA_TOKEN | WRITE_RESTRICTED;
|
||||
let ok = CreateRestrictedToken(
|
||||
@@ -370,14 +361,20 @@ pub unsafe fn create_readonly_token_with_cap_from(
|
||||
std::ptr::null(),
|
||||
0,
|
||||
std::ptr::null(),
|
||||
3,
|
||||
entries.len() as u32,
|
||||
entries.as_mut_ptr(),
|
||||
&mut new_token,
|
||||
);
|
||||
if ok == 0 {
|
||||
return Err(anyhow!("CreateRestrictedToken failed: {}", GetLastError()));
|
||||
}
|
||||
set_default_dacl(new_token, &[psid_logon, psid_everyone, psid_capability])?;
|
||||
|
||||
let mut dacl_sids: Vec<*mut c_void> = Vec::with_capacity(psid_capabilities.len() + 2);
|
||||
dacl_sids.push(psid_logon);
|
||||
dacl_sids.push(psid_everyone);
|
||||
dacl_sids.extend_from_slice(psid_capabilities);
|
||||
set_default_dacl(new_token, &dacl_sids)?;
|
||||
|
||||
enable_single_privilege(new_token, "SeChangeNotifyPrivilege")?;
|
||||
Ok((new_token, psid_capability))
|
||||
Ok(new_token)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
use crate::acl::add_deny_write_ace;
|
||||
use crate::path_normalization::canonicalize_path;
|
||||
use anyhow::Result;
|
||||
use std::ffi::c_void;
|
||||
use std::path::Path;
|
||||
|
||||
pub fn is_command_cwd_root(root: &Path, canonical_command_cwd: &Path) -> bool {
|
||||
canonicalize_path(root) == canonical_command_cwd
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Caller must ensure `psid` is a valid SID pointer.
|
||||
pub unsafe fn protect_workspace_codex_dir(cwd: &Path, psid: *mut c_void) -> Result<bool> {
|
||||
let cwd_codex = cwd.join(".codex");
|
||||
if cwd_codex.is_dir() {
|
||||
add_deny_write_ace(&cwd_codex, psid)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user