Merge branch 'main' into codex/viyatb/secrets-core-prb

This commit is contained in:
viyatb-oai
2026-02-03 14:23:23 -08:00
committed by GitHub
59 changed files with 2393 additions and 156 deletions
+47
View File
@@ -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?
@@ -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
+37
View File
@@ -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?
@@ -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
+124
View File
@@ -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"
+1
View File
@@ -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")]
+2
View File
@@ -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
View File
@@ -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(())
}
}
+1
View File
@@ -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 = []
+90
View File
@@ -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
View File
@@ -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)]
+2 -10
View File
@@ -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,
+2
View File
@@ -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(_)
+1
View File
@@ -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;
+314
View File
@@ -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(),
})
}
+43 -2
View File
@@ -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(_)
+36
View File
@@ -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")]
+1
View File
@@ -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(),
+6 -2
View File
@@ -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;
+4 -8
View File
@@ -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);
}
+57 -2
View File
@@ -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(),
+62 -12
View File
@@ -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, &current_dir, &env_map);
let canonical_cwd = canonicalize_path(&current_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(&current_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)
+56 -59
View File
@@ -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)
}
}