Add thread/shellCommand to app server API surface (#14988)
This PR adds a new `thread/shellCommand` app server API so clients can implement `!` shell commands. These commands are executed within the sandbox, and the command text and output are visible to the model. The internal implementation mirrors the current TUI `!` behavior. - persist shell command execution as `CommandExecution` thread items, including source and formatted output metadata - bridge live and replayed app-server command execution events back into the existing `tui_app_server` exec rendering path This PR also wires `tui_app_server` to submit `!` commands through the new API.
This commit is contained in:
parent
10eb3ec7fc
commit
01df50cf42
43 changed files with 2580 additions and 86 deletions
|
|
@ -2881,6 +2881,22 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadShellCommandParams": {
|
||||
"properties": {
|
||||
"command": {
|
||||
"description": "Shell command string evaluated by the thread's configured shell. Unlike `command/exec`, this intentionally preserves shell syntax such as pipes, redirects, and quoting. This runs unsandboxed with full access rather than inheriting the thread sandbox policy.",
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSortKey": {
|
||||
"enum": [
|
||||
"created_at",
|
||||
|
|
@ -3586,6 +3602,30 @@
|
|||
"title": "Thread/compact/startRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/shellCommand"
|
||||
],
|
||||
"title": "Thread/shellCommandRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadShellCommandParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/shellCommandRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
|
|
|||
|
|
@ -745,6 +745,15 @@
|
|||
],
|
||||
"type": "object"
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
|
|
@ -2390,6 +2399,14 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -499,6 +499,30 @@
|
|||
"title": "Thread/compact/startRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/shellCommand"
|
||||
],
|
||||
"title": "Thread/shellCommandRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/ThreadShellCommandParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/shellCommandRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
|
@ -6121,6 +6145,15 @@
|
|||
"title": "CommandExecutionOutputDeltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
|
|
@ -12063,6 +12096,14 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/v2/CommandExecutionStatus"
|
||||
},
|
||||
|
|
@ -13102,6 +13143,29 @@
|
|||
"title": "ThreadSetNameResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadShellCommandParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"command": {
|
||||
"description": "Shell command string evaluated by the thread's configured shell. Unlike `command/exec`, this intentionally preserves shell syntax such as pipes, redirects, and quoting. This runs unsandboxed with full access rather than inheriting the thread sandbox policy.",
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadShellCommandParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadShellCommandResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ThreadShellCommandResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSortKey": {
|
||||
"enum": [
|
||||
"created_at",
|
||||
|
|
|
|||
|
|
@ -1026,6 +1026,30 @@
|
|||
"title": "Thread/compact/startRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"thread/shellCommand"
|
||||
],
|
||||
"title": "Thread/shellCommandRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/ThreadShellCommandParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Thread/shellCommandRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
|
@ -2754,6 +2778,15 @@
|
|||
"title": "CommandExecutionOutputDeltaNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
|
|
@ -9823,6 +9856,14 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
|
|
@ -10862,6 +10903,29 @@
|
|||
"title": "ThreadSetNameResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadShellCommandParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"command": {
|
||||
"description": "Shell command string evaluated by the thread's configured shell. Unlike `command/exec`, this intentionally preserves shell syntax such as pipes, redirects, and quoting. This runs unsandboxed with full access rather than inheriting the thread sandbox policy.",
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadShellCommandParams",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadShellCommandResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ThreadShellCommandResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ThreadSortKey": {
|
||||
"enum": [
|
||||
"created_at",
|
||||
|
|
|
|||
|
|
@ -177,6 +177,15 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
|
|
@ -642,6 +651,14 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -177,6 +177,15 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
|
|
@ -642,6 +651,14 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -291,6 +291,15 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
|
|
@ -756,6 +765,14 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -353,6 +353,15 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
|
|
@ -1236,6 +1245,14 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -291,6 +291,15 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
|
|
@ -994,6 +1003,14 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -291,6 +291,15 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
|
|
@ -994,6 +1003,14 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -291,6 +291,15 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
|
|
@ -994,6 +1003,14 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -353,6 +353,15 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
|
|
@ -1236,6 +1245,14 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -291,6 +291,15 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
|
|
@ -994,6 +1003,14 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"command": {
|
||||
"description": "Shell command string evaluated by the thread's configured shell. Unlike `command/exec`, this intentionally preserves shell syntax such as pipes, redirects, and quoting. This runs unsandboxed with full access rather than inheriting the thread sandbox policy.",
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"command",
|
||||
"threadId"
|
||||
],
|
||||
"title": "ThreadShellCommandParams",
|
||||
"type": "object"
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "ThreadShellCommandResponse",
|
||||
"type": "object"
|
||||
}
|
||||
|
|
@ -353,6 +353,15 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
|
|
@ -1236,6 +1245,14 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -291,6 +291,15 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
|
|
@ -994,6 +1003,14 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -291,6 +291,15 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
|
|
@ -994,6 +1003,14 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -291,6 +291,15 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
|
|
@ -756,6 +765,14 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -291,6 +291,15 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
|
|
@ -756,6 +765,14 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -291,6 +291,15 @@
|
|||
}
|
||||
]
|
||||
},
|
||||
"CommandExecutionSource": {
|
||||
"enum": [
|
||||
"agent",
|
||||
"userShell",
|
||||
"unifiedExecStartup",
|
||||
"unifiedExecInteraction"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"CommandExecutionStatus": {
|
||||
"enum": [
|
||||
"inProgress",
|
||||
|
|
@ -756,6 +765,14 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/CommandExecutionSource"
|
||||
}
|
||||
],
|
||||
"default": "agent"
|
||||
},
|
||||
"status": {
|
||||
"$ref": "#/definitions/CommandExecutionStatus"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import type { ThreadReadParams } from "./v2/ThreadReadParams";
|
|||
import type { ThreadResumeParams } from "./v2/ThreadResumeParams";
|
||||
import type { ThreadRollbackParams } from "./v2/ThreadRollbackParams";
|
||||
import type { ThreadSetNameParams } from "./v2/ThreadSetNameParams";
|
||||
import type { ThreadShellCommandParams } from "./v2/ThreadShellCommandParams";
|
||||
import type { ThreadStartParams } from "./v2/ThreadStartParams";
|
||||
import type { ThreadUnarchiveParams } from "./v2/ThreadUnarchiveParams";
|
||||
import type { ThreadUnsubscribeParams } from "./v2/ThreadUnsubscribeParams";
|
||||
|
|
@ -60,4 +61,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta
|
|||
/**
|
||||
* 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/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "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": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "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": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "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": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, };
|
||||
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/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "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": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "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": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "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": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, };
|
||||
|
|
|
|||
|
|
@ -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 CommandExecutionSource = "agent" | "userShell" | "unifiedExecStartup" | "unifiedExecInteraction";
|
||||
|
|
@ -8,6 +8,7 @@ import type { CollabAgentState } from "./CollabAgentState";
|
|||
import type { CollabAgentTool } from "./CollabAgentTool";
|
||||
import type { CollabAgentToolCallStatus } from "./CollabAgentToolCallStatus";
|
||||
import type { CommandAction } from "./CommandAction";
|
||||
import type { CommandExecutionSource } from "./CommandExecutionSource";
|
||||
import type { CommandExecutionStatus } from "./CommandExecutionStatus";
|
||||
import type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem";
|
||||
import type { DynamicToolCallStatus } from "./DynamicToolCallStatus";
|
||||
|
|
@ -32,7 +33,7 @@ cwd: string,
|
|||
/**
|
||||
* Identifier for the underlying PTY process (when available).
|
||||
*/
|
||||
processId: string | null, status: CommandExecutionStatus,
|
||||
processId: string | null, source: CommandExecutionSource, status: CommandExecutionStatus,
|
||||
/**
|
||||
* A best-effort parsing of the command to understand the action(s) it will perform.
|
||||
* This returns a list of CommandAction objects because a single shell command may
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type ThreadShellCommandParams = { threadId: string,
|
||||
/**
|
||||
* Shell command string evaluated by the thread's configured shell.
|
||||
* Unlike `command/exec`, this intentionally preserves shell syntax
|
||||
* such as pipes, redirects, and quoting. This runs unsandboxed with full
|
||||
* access rather than inheriting the thread sandbox policy.
|
||||
*/
|
||||
command: 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 ThreadShellCommandResponse = Record<string, never>;
|
||||
|
|
@ -55,6 +55,7 @@ export type { CommandExecutionOutputDeltaNotification } from "./CommandExecution
|
|||
export type { CommandExecutionRequestApprovalParams } from "./CommandExecutionRequestApprovalParams";
|
||||
export type { CommandExecutionRequestApprovalResponse } from "./CommandExecutionRequestApprovalResponse";
|
||||
export type { CommandExecutionRequestApprovalSkillMetadata } from "./CommandExecutionRequestApprovalSkillMetadata";
|
||||
export type { CommandExecutionSource } from "./CommandExecutionSource";
|
||||
export type { CommandExecutionStatus } from "./CommandExecutionStatus";
|
||||
export type { Config } from "./Config";
|
||||
export type { ConfigBatchWriteParams } from "./ConfigBatchWriteParams";
|
||||
|
|
@ -283,6 +284,8 @@ export type { ThreadRollbackParams } from "./ThreadRollbackParams";
|
|||
export type { ThreadRollbackResponse } from "./ThreadRollbackResponse";
|
||||
export type { ThreadSetNameParams } from "./ThreadSetNameParams";
|
||||
export type { ThreadSetNameResponse } from "./ThreadSetNameResponse";
|
||||
export type { ThreadShellCommandParams } from "./ThreadShellCommandParams";
|
||||
export type { ThreadShellCommandResponse } from "./ThreadShellCommandResponse";
|
||||
export type { ThreadSortKey } from "./ThreadSortKey";
|
||||
export type { ThreadSourceKind } from "./ThreadSourceKind";
|
||||
export type { ThreadStartParams } from "./ThreadStartParams";
|
||||
|
|
|
|||
|
|
@ -267,6 +267,10 @@ client_request_definitions! {
|
|||
params: v2::ThreadCompactStartParams,
|
||||
response: v2::ThreadCompactStartResponse,
|
||||
},
|
||||
ThreadShellCommand => "thread/shellCommand" {
|
||||
params: v2::ThreadShellCommandParams,
|
||||
response: v2::ThreadShellCommandResponse,
|
||||
},
|
||||
#[experimental("thread/backgroundTerminals/clean")]
|
||||
ThreadBackgroundTerminalsClean => "thread/backgroundTerminals/clean" {
|
||||
params: v2::ThreadBackgroundTerminalsCleanParams,
|
||||
|
|
|
|||
|
|
@ -341,6 +341,7 @@ impl ThreadHistoryBuilder {
|
|||
command,
|
||||
cwd: payload.cwd.clone(),
|
||||
process_id: payload.process_id.clone(),
|
||||
source: payload.source.into(),
|
||||
status: CommandExecutionStatus::InProgress,
|
||||
command_actions,
|
||||
aggregated_output: None,
|
||||
|
|
@ -371,6 +372,7 @@ impl ThreadHistoryBuilder {
|
|||
command,
|
||||
cwd: payload.cwd.clone(),
|
||||
process_id: payload.process_id.clone(),
|
||||
source: payload.source.into(),
|
||||
status,
|
||||
command_actions,
|
||||
aggregated_output,
|
||||
|
|
@ -1144,6 +1146,7 @@ impl From<&PendingTurn> for Turn {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::protocol::v2::CommandExecutionSource;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::dynamic_tools::DynamicToolCallOutputContentItem as CoreDynamicToolCallOutputContentItem;
|
||||
use codex_protocol::items::TurnItem as CoreTurnItem;
|
||||
|
|
@ -1745,6 +1748,7 @@ mod tests {
|
|||
command: "echo 'hello world'".into(),
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
process_id: Some("pid-1".into()),
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::Completed,
|
||||
command_actions: vec![CommandAction::Unknown {
|
||||
command: "echo hello world".into(),
|
||||
|
|
@ -1893,6 +1897,7 @@ mod tests {
|
|||
command: "ls".into(),
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
process_id: Some("pid-2".into()),
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::Declined,
|
||||
command_actions: vec![CommandAction::Unknown {
|
||||
command: "ls".into(),
|
||||
|
|
@ -1987,6 +1992,7 @@ mod tests {
|
|||
command: "echo done".into(),
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
process_id: Some("pid-42".into()),
|
||||
source: CommandExecutionSource::Agent,
|
||||
status: CommandExecutionStatus::Completed,
|
||||
command_actions: vec![CommandAction::Unknown {
|
||||
command: "echo done".into(),
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ use codex_protocol::protocol::AgentStatus as CoreAgentStatus;
|
|||
use codex_protocol::protocol::AskForApproval as CoreAskForApproval;
|
||||
use codex_protocol::protocol::CodexErrorInfo as CoreCodexErrorInfo;
|
||||
use codex_protocol::protocol::CreditsSnapshot as CoreCreditsSnapshot;
|
||||
use codex_protocol::protocol::ExecCommandSource as CoreExecCommandSource;
|
||||
use codex_protocol::protocol::ExecCommandStatus as CoreExecCommandStatus;
|
||||
use codex_protocol::protocol::GranularApprovalConfig as CoreGranularApprovalConfig;
|
||||
use codex_protocol::protocol::GuardianRiskLevel as CoreGuardianRiskLevel;
|
||||
|
|
@ -92,6 +93,7 @@ use schemars::JsonSchema;
|
|||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use serde_with::serde_as;
|
||||
use thiserror::Error;
|
||||
use ts_rs::TS;
|
||||
|
||||
|
|
@ -2871,6 +2873,23 @@ pub struct ThreadCompactStartParams {
|
|||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadCompactStartResponse {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadShellCommandParams {
|
||||
pub thread_id: String,
|
||||
/// Shell command string evaluated by the thread's configured shell.
|
||||
/// Unlike `command/exec`, this intentionally preserves shell syntax
|
||||
/// such as pipes, redirects, and quoting. This runs unsandboxed with full
|
||||
/// access rather than inheriting the thread sandbox policy.
|
||||
pub command: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct ThreadShellCommandResponse {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
|
@ -4137,6 +4156,8 @@ pub enum ThreadItem {
|
|||
cwd: PathBuf,
|
||||
/// Identifier for the underlying PTY process (when available).
|
||||
process_id: Option<String>,
|
||||
#[serde(default)]
|
||||
source: CommandExecutionSource,
|
||||
status: CommandExecutionStatus,
|
||||
/// A best-effort parsing of the command to understand the action(s) it will perform.
|
||||
/// This returns a list of CommandAction objects because a single shell command may
|
||||
|
|
@ -4417,6 +4438,17 @@ impl From<&CoreExecCommandStatus> for CommandExecutionStatus {
|
|||
}
|
||||
}
|
||||
|
||||
v2_enum_from_core! {
|
||||
#[derive(Default)]
|
||||
pub enum CommandExecutionSource from CoreExecCommandSource {
|
||||
#[default]
|
||||
Agent,
|
||||
UserShell,
|
||||
UnifiedExecStartup,
|
||||
UnifiedExecInteraction,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
|
@ -4863,6 +4895,7 @@ pub struct TerminalInteractionNotification {
|
|||
pub stdin: String,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
|
@ -6313,6 +6346,40 @@ mod tests {
|
|||
assert_eq!(decoded, params);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_shell_command_params_round_trip() {
|
||||
let params = ThreadShellCommandParams {
|
||||
thread_id: "thr_123".to_string(),
|
||||
command: "printf 'hello world\\n'".to_string(),
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(¶ms).expect("serialize thread/shellCommand params");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({
|
||||
"threadId": "thr_123",
|
||||
"command": "printf 'hello world\\n'",
|
||||
})
|
||||
);
|
||||
|
||||
let decoded = serde_json::from_value::<ThreadShellCommandParams>(value)
|
||||
.expect("deserialize thread/shellCommand params");
|
||||
assert_eq!(decoded, params);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_shell_command_response_round_trip() {
|
||||
let response = ThreadShellCommandResponse {};
|
||||
|
||||
let value =
|
||||
serde_json::to_value(&response).expect("serialize thread/shellCommand response");
|
||||
assert_eq!(value, json!({}));
|
||||
|
||||
let decoded = serde_json::from_value::<ThreadShellCommandResponse>(value)
|
||||
.expect("deserialize thread/shellCommand response");
|
||||
assert_eq!(decoded, response);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_exec_params_default_optional_streaming_flags() {
|
||||
let params = serde_json::from_value::<CommandExecParams>(json!({
|
||||
|
|
@ -6607,6 +6674,32 @@ mod tests {
|
|||
assert_eq!(decoded, notification);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_execution_output_delta_round_trips() {
|
||||
let notification = CommandExecutionOutputDeltaNotification {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item_id: "item-1".to_string(),
|
||||
delta: "\u{fffd}a\n".to_string(),
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(¬ification)
|
||||
.expect("serialize item/commandExecution/outputDelta notification");
|
||||
assert_eq!(
|
||||
value,
|
||||
json!({
|
||||
"threadId": "thread-1",
|
||||
"turnId": "turn-1",
|
||||
"itemId": "item-1",
|
||||
"delta": "\u{fffd}a\n",
|
||||
})
|
||||
);
|
||||
|
||||
let decoded = serde_json::from_value::<CommandExecutionOutputDeltaNotification>(value)
|
||||
.expect("deserialize round-trip");
|
||||
assert_eq!(decoded, notification);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandbox_policy_round_trips_external_sandbox_network_access() {
|
||||
let v2_policy = SandboxPolicy::ExternalSandbox {
|
||||
|
|
|
|||
|
|
@ -136,6 +136,7 @@ Example with notification opt-out:
|
|||
- `thread/name/set` — set or update a thread’s user-facing name for either a loaded thread or a persisted rollout; returns `{}` on success and emits `thread/name/updated` to initialized, opted-in clients. Thread names are not required to be unique; name lookups resolve to the most recently updated thread.
|
||||
- `thread/unarchive` — move an archived rollout file back into the sessions directory; returns the restored `thread` on success and emits `thread/unarchived`.
|
||||
- `thread/compact/start` — trigger conversation history compaction for a thread; returns `{}` immediately while progress streams through standard turn/item notifications.
|
||||
- `thread/shellCommand` — run a user-initiated `!` shell command against a thread; this runs unsandboxed with full access rather than inheriting the thread sandbox policy. Returns `{}` immediately while progress streams through standard turn/item notifications and any active turn receives the formatted output in its message stream.
|
||||
- `thread/backgroundTerminals/clean` — terminate all running background terminals for a thread (experimental; requires `capabilities.experimentalApi`); returns `{}` when the cleanup request is accepted.
|
||||
- `thread/rollback` — drop the last N turns from the agent’s in-memory context and persist a rollback marker in the rollout so future resumes see the pruned history; returns the updated `thread` (with `turns` populated) on success.
|
||||
- `turn/start` — add user input to a thread and begin Codex generation; responds with the initial `turn` object and streams `turn/started`, `item/*`, and `turn/completed` notifications. For `collaborationMode`, `settings.developer_instructions: null` means "use built-in instructions for the selected mode".
|
||||
|
|
@ -415,6 +416,31 @@ While compaction is running, the thread is effectively in a turn so clients shou
|
|||
{ "id": 25, "result": {} }
|
||||
```
|
||||
|
||||
### Example: Run a thread shell command
|
||||
|
||||
Use `thread/shellCommand` for the TUI `!` workflow. The request returns immediately with `{}`.
|
||||
This API runs unsandboxed with full access; it does not inherit the thread
|
||||
sandbox policy.
|
||||
|
||||
If the thread already has an active turn, the command runs as an auxiliary action on that turn. In that case, progress is emitted as standard `item/*` notifications on the existing turn and the formatted output is injected into the turn’s message stream:
|
||||
|
||||
- `item/started` with `item: { "type": "commandExecution", "source": "userShell", ... }`
|
||||
- zero or more `item/commandExecution/outputDelta`
|
||||
- `item/completed` with the same `commandExecution` item id
|
||||
|
||||
If the thread does not already have an active turn, the server starts a standalone turn for the shell command. In that case clients should expect:
|
||||
|
||||
- `turn/started`
|
||||
- `item/started` with `item: { "type": "commandExecution", "source": "userShell", ... }`
|
||||
- zero or more `item/commandExecution/outputDelta`
|
||||
- `item/completed` with the same `commandExecution` item id
|
||||
- `turn/completed`
|
||||
|
||||
```json
|
||||
{ "method": "thread/shellCommand", "id": 26, "params": { "threadId": "thr_b", "command": "git status --short" } }
|
||||
{ "id": 26, "result": {} }
|
||||
```
|
||||
|
||||
### Example: Start a turn (send user input)
|
||||
|
||||
Turns attach user input (text or images) to a thread and trigger Codex generation. The `input` field is a list of discriminated unions:
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ use codex_app_server_protocol::CommandExecutionOutputDeltaNotification;
|
|||
use codex_app_server_protocol::CommandExecutionRequestApprovalParams;
|
||||
use codex_app_server_protocol::CommandExecutionRequestApprovalResponse;
|
||||
use codex_app_server_protocol::CommandExecutionRequestApprovalSkillMetadata;
|
||||
use codex_app_server_protocol::CommandExecutionSource;
|
||||
use codex_app_server_protocol::CommandExecutionStatus;
|
||||
use codex_app_server_protocol::ContextCompactedNotification;
|
||||
use codex_app_server_protocol::DeprecationNoticeNotification;
|
||||
|
|
@ -1563,6 +1564,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
|||
command,
|
||||
cwd,
|
||||
process_id,
|
||||
source: exec_command_begin_event.source.into(),
|
||||
status: CommandExecutionStatus::InProgress,
|
||||
command_actions,
|
||||
aggregated_output: None,
|
||||
|
|
@ -1580,7 +1582,6 @@ pub(crate) async fn apply_bespoke_event_handling(
|
|||
}
|
||||
EventMsg::ExecCommandOutputDelta(exec_command_output_delta_event) => {
|
||||
let item_id = exec_command_output_delta_event.call_id.clone();
|
||||
let delta = String::from_utf8_lossy(&exec_command_output_delta_event.chunk).to_string();
|
||||
// The underlying EventMsg::ExecCommandOutputDelta is used for shell, unified_exec,
|
||||
// and apply_patch tool calls. We represent apply_patch with the FileChange item, and
|
||||
// everything else with the CommandExecution item.
|
||||
|
|
@ -1592,6 +1593,8 @@ pub(crate) async fn apply_bespoke_event_handling(
|
|||
state.turn_summary.file_change_started.contains(&item_id)
|
||||
};
|
||||
if is_file_change {
|
||||
let delta =
|
||||
String::from_utf8_lossy(&exec_command_output_delta_event.chunk).to_string();
|
||||
let notification = FileChangeOutputDeltaNotification {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.clone(),
|
||||
|
|
@ -1608,7 +1611,8 @@ pub(crate) async fn apply_bespoke_event_handling(
|
|||
thread_id: conversation_id.to_string(),
|
||||
turn_id: event_turn_id.clone(),
|
||||
item_id,
|
||||
delta,
|
||||
delta: String::from_utf8_lossy(&exec_command_output_delta_event.chunk)
|
||||
.to_string(),
|
||||
};
|
||||
outgoing
|
||||
.send_server_notification(ServerNotification::CommandExecutionOutputDelta(
|
||||
|
|
@ -1641,6 +1645,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
|||
aggregated_output,
|
||||
exit_code,
|
||||
duration,
|
||||
source,
|
||||
status,
|
||||
..
|
||||
} = exec_command_end_event;
|
||||
|
|
@ -1672,6 +1677,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
|||
command: shlex_join(&command),
|
||||
cwd,
|
||||
process_id,
|
||||
source: source.into(),
|
||||
status,
|
||||
command_actions,
|
||||
aggregated_output,
|
||||
|
|
@ -1935,6 +1941,7 @@ async fn complete_command_execution_item(
|
|||
command: String,
|
||||
cwd: PathBuf,
|
||||
process_id: Option<String>,
|
||||
source: CommandExecutionSource,
|
||||
command_actions: Vec<V2ParsedCommand>,
|
||||
status: CommandExecutionStatus,
|
||||
outgoing: &ThreadScopedOutgoingMessageSender,
|
||||
|
|
@ -1944,6 +1951,7 @@ async fn complete_command_execution_item(
|
|||
command,
|
||||
cwd,
|
||||
process_id,
|
||||
source,
|
||||
status,
|
||||
command_actions,
|
||||
aggregated_output: None,
|
||||
|
|
@ -2607,6 +2615,7 @@ async fn on_command_execution_request_approval_response(
|
|||
completion_item.command,
|
||||
completion_item.cwd,
|
||||
/*process_id*/ None,
|
||||
CommandExecutionSource::Agent,
|
||||
completion_item.command_actions,
|
||||
status,
|
||||
&outgoing,
|
||||
|
|
|
|||
|
|
@ -146,6 +146,8 @@ use codex_app_server_protocol::ThreadResumeResponse;
|
|||
use codex_app_server_protocol::ThreadRollbackParams;
|
||||
use codex_app_server_protocol::ThreadSetNameParams;
|
||||
use codex_app_server_protocol::ThreadSetNameResponse;
|
||||
use codex_app_server_protocol::ThreadShellCommandParams;
|
||||
use codex_app_server_protocol::ThreadShellCommandResponse;
|
||||
use codex_app_server_protocol::ThreadSortKey;
|
||||
use codex_app_server_protocol::ThreadSourceKind;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
|
|
@ -695,6 +697,10 @@ impl CodexMessageProcessor {
|
|||
self.thread_read(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::ThreadShellCommand { request_id, params } => {
|
||||
self.thread_shell_command(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::SkillsList { request_id, params } => {
|
||||
self.skills_list(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
|
|
@ -2974,6 +2980,58 @@ impl CodexMessageProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
async fn thread_shell_command(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: ThreadShellCommandParams,
|
||||
) {
|
||||
let ThreadShellCommandParams { thread_id, command } = params;
|
||||
let command = command.trim().to_string();
|
||||
if command.is_empty() {
|
||||
self.outgoing
|
||||
.send_error(
|
||||
request_id,
|
||||
JSONRPCErrorError {
|
||||
code: INVALID_REQUEST_ERROR_CODE,
|
||||
message: "command must not be empty".to_string(),
|
||||
data: None,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let (_, thread) = match self.load_thread(&thread_id).await {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match self
|
||||
.submit_core_op(
|
||||
&request_id,
|
||||
thread.as_ref(),
|
||||
Op::RunUserShellCommand { command },
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
self.outgoing
|
||||
.send_response(request_id, ThreadShellCommandResponse {})
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_internal_error(
|
||||
request_id,
|
||||
format!("failed to start shell command: {err}"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn thread_list(&self, request_id: ConnectionRequestId, params: ThreadListParams) {
|
||||
let ThreadListParams {
|
||||
cursor,
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ use codex_app_server_protocol::ThreadRealtimeStopParams;
|
|||
use codex_app_server_protocol::ThreadResumeParams;
|
||||
use codex_app_server_protocol::ThreadRollbackParams;
|
||||
use codex_app_server_protocol::ThreadSetNameParams;
|
||||
use codex_app_server_protocol::ThreadShellCommandParams;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadUnarchiveParams;
|
||||
use codex_app_server_protocol::ThreadUnsubscribeParams;
|
||||
|
|
@ -386,6 +387,15 @@ impl McpProcess {
|
|||
self.send_request("thread/compact/start", params).await
|
||||
}
|
||||
|
||||
/// Send a `thread/shellCommand` JSON-RPC request.
|
||||
pub async fn send_thread_shell_command_request(
|
||||
&mut self,
|
||||
params: ThreadShellCommandParams,
|
||||
) -> anyhow::Result<i64> {
|
||||
let params = Some(serde_json::to_value(params)?);
|
||||
self.send_request("thread/shellCommand", params).await
|
||||
}
|
||||
|
||||
/// Send a `thread/rollback` JSON-RPC request.
|
||||
pub async fn send_thread_rollback_request(
|
||||
&mut self,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ mod thread_name_websocket;
|
|||
mod thread_read;
|
||||
mod thread_resume;
|
||||
mod thread_rollback;
|
||||
mod thread_shell_command;
|
||||
mod thread_start;
|
||||
mod thread_status;
|
||||
mod thread_unarchive;
|
||||
|
|
|
|||
439
codex-rs/app-server/tests/suite/v2/thread_shell_command.rs
Normal file
439
codex-rs/app-server/tests/suite/v2/thread_shell_command.rs
Normal file
|
|
@ -0,0 +1,439 @@
|
|||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::create_final_assistant_message_sse_response;
|
||||
use app_test_support::create_mock_responses_server_sequence;
|
||||
use app_test_support::create_shell_command_sse_response;
|
||||
use app_test_support::format_with_current_shell_display;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::CommandExecutionApprovalDecision;
|
||||
use codex_app_server_protocol::CommandExecutionOutputDeltaNotification;
|
||||
use codex_app_server_protocol::CommandExecutionRequestApprovalResponse;
|
||||
use codex_app_server_protocol::CommandExecutionSource;
|
||||
use codex_app_server_protocol::CommandExecutionStatus;
|
||||
use codex_app_server_protocol::ItemCompletedNotification;
|
||||
use codex_app_server_protocol::ItemStartedNotification;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadReadParams;
|
||||
use codex_app_server_protocol::ThreadReadResponse;
|
||||
use codex_app_server_protocol::ThreadShellCommandParams;
|
||||
use codex_app_server_protocol::ThreadShellCommandResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_core::features::FEATURES;
|
||||
use codex_core::features::Feature;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_shell_command_runs_as_standalone_turn_and_persists_history() -> Result<()> {
|
||||
let tmp = TempDir::new()?;
|
||||
let codex_home = tmp.path().join("codex_home");
|
||||
std::fs::create_dir(&codex_home)?;
|
||||
let workspace = tmp.path().join("workspace");
|
||||
std::fs::create_dir(&workspace)?;
|
||||
|
||||
let server = create_mock_responses_server_sequence(vec![]).await;
|
||||
create_config_toml(
|
||||
codex_home.as_path(),
|
||||
&server.uri(),
|
||||
"never",
|
||||
&BTreeMap::default(),
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.as_path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
persist_extended_history: true,
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
|
||||
|
||||
let shell_id = mcp
|
||||
.send_thread_shell_command_request(ThreadShellCommandParams {
|
||||
thread_id: thread.id.clone(),
|
||||
command: "printf 'hello from bang\\n'".to_string(),
|
||||
})
|
||||
.await?;
|
||||
let shell_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(shell_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: ThreadShellCommandResponse = to_response::<ThreadShellCommandResponse>(shell_resp)?;
|
||||
|
||||
let started = wait_for_command_execution_started(&mut mcp, None).await?;
|
||||
let ThreadItem::CommandExecution {
|
||||
id, source, status, ..
|
||||
} = &started.item
|
||||
else {
|
||||
unreachable!("helper returns command execution item");
|
||||
};
|
||||
let command_id = id.clone();
|
||||
assert_eq!(source, &CommandExecutionSource::UserShell);
|
||||
assert_eq!(status, &CommandExecutionStatus::InProgress);
|
||||
|
||||
let delta = wait_for_command_execution_output_delta(&mut mcp, &command_id).await?;
|
||||
assert_eq!(delta.delta, "hello from bang\n");
|
||||
|
||||
let completed = wait_for_command_execution_completed(&mut mcp, Some(&command_id)).await?;
|
||||
let ThreadItem::CommandExecution {
|
||||
id,
|
||||
source,
|
||||
status,
|
||||
aggregated_output,
|
||||
exit_code,
|
||||
..
|
||||
} = &completed.item
|
||||
else {
|
||||
unreachable!("helper returns command execution item");
|
||||
};
|
||||
assert_eq!(id, &command_id);
|
||||
assert_eq!(source, &CommandExecutionSource::UserShell);
|
||||
assert_eq!(status, &CommandExecutionStatus::Completed);
|
||||
assert_eq!(aggregated_output.as_deref(), Some("hello from bang\n"));
|
||||
assert_eq!(*exit_code, Some(0));
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
let read_id = mcp
|
||||
.send_thread_read_request(ThreadReadParams {
|
||||
thread_id: thread.id,
|
||||
include_turns: true,
|
||||
})
|
||||
.await?;
|
||||
let read_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(read_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadReadResponse { thread } = to_response::<ThreadReadResponse>(read_resp)?;
|
||||
assert_eq!(thread.turns.len(), 1);
|
||||
let ThreadItem::CommandExecution {
|
||||
source,
|
||||
status,
|
||||
aggregated_output,
|
||||
..
|
||||
} = thread.turns[0]
|
||||
.items
|
||||
.iter()
|
||||
.find(|item| matches!(item, ThreadItem::CommandExecution { .. }))
|
||||
.expect("expected persisted command execution item")
|
||||
else {
|
||||
unreachable!("matched command execution item");
|
||||
};
|
||||
assert_eq!(source, &CommandExecutionSource::UserShell);
|
||||
assert_eq!(status, &CommandExecutionStatus::Completed);
|
||||
assert_eq!(aggregated_output.as_deref(), Some("hello from bang\n"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn thread_shell_command_uses_existing_active_turn() -> Result<()> {
|
||||
let tmp = TempDir::new()?;
|
||||
let codex_home = tmp.path().join("codex_home");
|
||||
std::fs::create_dir(&codex_home)?;
|
||||
let workspace = tmp.path().join("workspace");
|
||||
std::fs::create_dir(&workspace)?;
|
||||
|
||||
let responses = vec![
|
||||
create_shell_command_sse_response(
|
||||
vec![
|
||||
"python3".to_string(),
|
||||
"-c".to_string(),
|
||||
"print(42)".to_string(),
|
||||
],
|
||||
None,
|
||||
Some(5000),
|
||||
"call-approve",
|
||||
)?,
|
||||
create_final_assistant_message_sse_response("done")?,
|
||||
];
|
||||
let server = create_mock_responses_server_sequence(responses).await;
|
||||
create_config_toml(
|
||||
codex_home.as_path(),
|
||||
&server.uri(),
|
||||
"untrusted",
|
||||
&BTreeMap::default(),
|
||||
)?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.as_path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
persist_extended_history: true,
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(start_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response::<ThreadStartResponse>(start_resp)?;
|
||||
|
||||
let turn_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "run python".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
cwd: Some(workspace.clone()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let turn_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_id)),
|
||||
)
|
||||
.await??;
|
||||
let TurnStartResponse { turn } = to_response::<TurnStartResponse>(turn_resp)?;
|
||||
|
||||
let agent_started = wait_for_command_execution_started(&mut mcp, Some("call-approve")).await?;
|
||||
let ThreadItem::CommandExecution {
|
||||
command, source, ..
|
||||
} = &agent_started.item
|
||||
else {
|
||||
unreachable!("helper returns command execution item");
|
||||
};
|
||||
assert_eq!(source, &CommandExecutionSource::Agent);
|
||||
assert_eq!(
|
||||
command,
|
||||
&format_with_current_shell_display("python3 -c 'print(42)'")
|
||||
);
|
||||
|
||||
let server_req = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_request_message(),
|
||||
)
|
||||
.await??;
|
||||
let ServerRequest::CommandExecutionRequestApproval { request_id, .. } = server_req else {
|
||||
panic!("expected approval request");
|
||||
};
|
||||
|
||||
let shell_id = mcp
|
||||
.send_thread_shell_command_request(ThreadShellCommandParams {
|
||||
thread_id: thread.id.clone(),
|
||||
command: "printf 'active turn bang\\n'".to_string(),
|
||||
})
|
||||
.await?;
|
||||
let shell_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(shell_id)),
|
||||
)
|
||||
.await??;
|
||||
let _: ThreadShellCommandResponse = to_response::<ThreadShellCommandResponse>(shell_resp)?;
|
||||
|
||||
let started =
|
||||
wait_for_command_execution_started_by_source(&mut mcp, CommandExecutionSource::UserShell)
|
||||
.await?;
|
||||
assert_eq!(started.turn_id, turn.id);
|
||||
let command_id = match &started.item {
|
||||
ThreadItem::CommandExecution { id, .. } => id.clone(),
|
||||
_ => unreachable!("helper returns command execution item"),
|
||||
};
|
||||
let completed = wait_for_command_execution_completed(&mut mcp, Some(&command_id)).await?;
|
||||
assert_eq!(completed.turn_id, turn.id);
|
||||
let ThreadItem::CommandExecution {
|
||||
source,
|
||||
aggregated_output,
|
||||
..
|
||||
} = &completed.item
|
||||
else {
|
||||
unreachable!("helper returns command execution item");
|
||||
};
|
||||
assert_eq!(source, &CommandExecutionSource::UserShell);
|
||||
assert_eq!(aggregated_output.as_deref(), Some("active turn bang\n"));
|
||||
|
||||
mcp.send_response(
|
||||
request_id,
|
||||
serde_json::to_value(CommandExecutionRequestApprovalResponse {
|
||||
decision: CommandExecutionApprovalDecision::Decline,
|
||||
})?,
|
||||
)
|
||||
.await?;
|
||||
let _: TurnCompletedNotification = serde_json::from_value(
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??
|
||||
.params
|
||||
.expect("turn/completed params"),
|
||||
)?;
|
||||
|
||||
let read_id = mcp
|
||||
.send_thread_read_request(ThreadReadParams {
|
||||
thread_id: thread.id,
|
||||
include_turns: true,
|
||||
})
|
||||
.await?;
|
||||
let read_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(read_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadReadResponse { thread } = to_response::<ThreadReadResponse>(read_resp)?;
|
||||
assert_eq!(thread.turns.len(), 1);
|
||||
assert!(
|
||||
thread.turns[0].items.iter().any(|item| {
|
||||
matches!(
|
||||
item,
|
||||
ThreadItem::CommandExecution {
|
||||
source: CommandExecutionSource::UserShell,
|
||||
aggregated_output,
|
||||
..
|
||||
} if aggregated_output.as_deref() == Some("active turn bang\n")
|
||||
)
|
||||
}),
|
||||
"expected active-turn shell command to be persisted on the existing turn"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn wait_for_command_execution_started(
|
||||
mcp: &mut McpProcess,
|
||||
expected_id: Option<&str>,
|
||||
) -> Result<ItemStartedNotification> {
|
||||
loop {
|
||||
let notif = mcp
|
||||
.read_stream_until_notification_message("item/started")
|
||||
.await?;
|
||||
let started: ItemStartedNotification = serde_json::from_value(
|
||||
notif
|
||||
.params
|
||||
.ok_or_else(|| anyhow::anyhow!("missing item/started params"))?,
|
||||
)?;
|
||||
let ThreadItem::CommandExecution { id, .. } = &started.item else {
|
||||
continue;
|
||||
};
|
||||
if expected_id.is_none() || expected_id == Some(id.as_str()) {
|
||||
return Ok(started);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_command_execution_started_by_source(
|
||||
mcp: &mut McpProcess,
|
||||
expected_source: CommandExecutionSource,
|
||||
) -> Result<ItemStartedNotification> {
|
||||
loop {
|
||||
let started = wait_for_command_execution_started(mcp, None).await?;
|
||||
let ThreadItem::CommandExecution { source, .. } = &started.item else {
|
||||
continue;
|
||||
};
|
||||
if source == &expected_source {
|
||||
return Ok(started);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_command_execution_completed(
|
||||
mcp: &mut McpProcess,
|
||||
expected_id: Option<&str>,
|
||||
) -> Result<ItemCompletedNotification> {
|
||||
loop {
|
||||
let notif = mcp
|
||||
.read_stream_until_notification_message("item/completed")
|
||||
.await?;
|
||||
let completed: ItemCompletedNotification = serde_json::from_value(
|
||||
notif
|
||||
.params
|
||||
.ok_or_else(|| anyhow::anyhow!("missing item/completed params"))?,
|
||||
)?;
|
||||
let ThreadItem::CommandExecution { id, .. } = &completed.item else {
|
||||
continue;
|
||||
};
|
||||
if expected_id.is_none() || expected_id == Some(id.as_str()) {
|
||||
return Ok(completed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_command_execution_output_delta(
|
||||
mcp: &mut McpProcess,
|
||||
item_id: &str,
|
||||
) -> Result<CommandExecutionOutputDeltaNotification> {
|
||||
loop {
|
||||
let notif = mcp
|
||||
.read_stream_until_notification_message("item/commandExecution/outputDelta")
|
||||
.await?;
|
||||
let delta: CommandExecutionOutputDeltaNotification = serde_json::from_value(
|
||||
notif
|
||||
.params
|
||||
.ok_or_else(|| anyhow::anyhow!("missing output delta params"))?,
|
||||
)?;
|
||||
if delta.item_id == item_id {
|
||||
return Ok(delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn create_config_toml(
|
||||
codex_home: &Path,
|
||||
server_uri: &str,
|
||||
approval_policy: &str,
|
||||
feature_flags: &BTreeMap<Feature, bool>,
|
||||
) -> std::io::Result<()> {
|
||||
let feature_entries = feature_flags
|
||||
.iter()
|
||||
.map(|(feature, enabled)| {
|
||||
let key = FEATURES
|
||||
.iter()
|
||||
.find(|spec| spec.id == *feature)
|
||||
.map(|spec| spec.key)
|
||||
.unwrap_or_else(|| panic!("missing feature key for {feature:?}"));
|
||||
format!("{key} = {enabled}")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
std::fs::write(
|
||||
codex_home.join("config.toml"),
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "{approval_policy}"
|
||||
sandbox_mode = "read-only"
|
||||
|
||||
model_provider = "mock_provider"
|
||||
|
||||
[features]
|
||||
{feature_entries}
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "{server_uri}/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -331,6 +331,9 @@ async fn persist_user_shell_output(
|
|||
session
|
||||
.record_conversation_items(turn_context, std::slice::from_ref(&output_item))
|
||||
.await;
|
||||
// Standalone shell turns can run before any regular user turn, so
|
||||
// explicitly materialize rollout persistence after recording output.
|
||||
session.ensure_rollout_materialized().await;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2050,6 +2050,12 @@ impl App {
|
|||
app_server.thread_realtime_stop(thread_id).await?;
|
||||
Ok(true)
|
||||
}
|
||||
AppCommandView::RunUserShellCommand { command } => {
|
||||
app_server
|
||||
.thread_shell_command(thread_id, command.to_string())
|
||||
.await?;
|
||||
Ok(true)
|
||||
}
|
||||
AppCommandView::OverrideTurnContext { .. } => Ok(true),
|
||||
_ => Ok(false),
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -35,6 +35,9 @@ pub(crate) enum AppCommandView<'a> {
|
|||
RealtimeConversationAudio(&'a ConversationAudioParams),
|
||||
RealtimeConversationText(&'a ConversationTextParams),
|
||||
RealtimeConversationClose,
|
||||
RunUserShellCommand {
|
||||
command: &'a str,
|
||||
},
|
||||
UserTurn {
|
||||
items: &'a [UserInput],
|
||||
cwd: &'a PathBuf,
|
||||
|
|
@ -134,6 +137,10 @@ impl AppCommand {
|
|||
Self(Op::RealtimeConversationClose)
|
||||
}
|
||||
|
||||
pub(crate) fn run_user_shell_command(command: String) -> Self {
|
||||
Self(Op::RunUserShellCommand { command })
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn user_turn(
|
||||
items: Vec<UserInput>,
|
||||
|
|
@ -291,6 +298,7 @@ impl AppCommand {
|
|||
AppCommandView::RealtimeConversationText(params)
|
||||
}
|
||||
Op::RealtimeConversationClose => AppCommandView::RealtimeConversationClose,
|
||||
Op::RunUserShellCommand { command } => AppCommandView::RunUserShellCommand { command },
|
||||
Op::UserTurn {
|
||||
items,
|
||||
cwd,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ use codex_app_server_protocol::ThreadRollbackParams;
|
|||
use codex_app_server_protocol::ThreadRollbackResponse;
|
||||
use codex_app_server_protocol::ThreadSetNameParams;
|
||||
use codex_app_server_protocol::ThreadSetNameResponse;
|
||||
use codex_app_server_protocol::ThreadShellCommandParams;
|
||||
use codex_app_server_protocol::ThreadShellCommandResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadUnsubscribeParams;
|
||||
|
|
@ -492,6 +494,26 @@ impl AppServerSession {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_shell_command(
|
||||
&mut self,
|
||||
thread_id: ThreadId,
|
||||
command: String,
|
||||
) -> Result<()> {
|
||||
let request_id = self.next_request_id();
|
||||
let _: ThreadShellCommandResponse = self
|
||||
.client
|
||||
.request_typed(ClientRequest::ThreadShellCommand {
|
||||
request_id,
|
||||
params: ThreadShellCommandParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
command,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.wrap_err("thread/shellCommand failed in app-server TUI")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_background_terminals_clean(
|
||||
&mut self,
|
||||
thread_id: ThreadId,
|
||||
|
|
|
|||
|
|
@ -5134,13 +5134,7 @@ impl ChatWidget {
|
|||
)));
|
||||
return;
|
||||
}
|
||||
// TODO: Restore `!` support in app-server TUI once command execution can
|
||||
// persist transcript-visible output into thread history with parity to the
|
||||
// legacy TUI.
|
||||
self.add_to_history(history_cell::new_error_event(
|
||||
"`!` shell commands are unavailable in app-server TUI because command output is not yet persisted in thread history.".to_string(),
|
||||
));
|
||||
self.request_redraw();
|
||||
self.submit_op(AppCommand::run_user_shell_command(cmd.to_string()));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -5562,6 +5556,7 @@ impl ChatWidget {
|
|||
command,
|
||||
cwd,
|
||||
process_id,
|
||||
source,
|
||||
status,
|
||||
command_actions,
|
||||
aggregated_output,
|
||||
|
|
@ -5582,10 +5577,11 @@ impl ChatWidget {
|
|||
.into_iter()
|
||||
.map(codex_app_server_protocol::CommandAction::into_core)
|
||||
.collect(),
|
||||
source: ExecCommandSource::Agent,
|
||||
source: source.to_core(),
|
||||
interaction_input: None,
|
||||
});
|
||||
} else {
|
||||
let aggregated_output = aggregated_output.unwrap_or_default();
|
||||
self.on_exec_command_end(ExecCommandEndEvent {
|
||||
call_id: id,
|
||||
process_id,
|
||||
|
|
@ -5596,16 +5592,16 @@ impl ChatWidget {
|
|||
.into_iter()
|
||||
.map(codex_app_server_protocol::CommandAction::into_core)
|
||||
.collect(),
|
||||
source: ExecCommandSource::Agent,
|
||||
source: source.to_core(),
|
||||
interaction_input: None,
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
aggregated_output: aggregated_output.unwrap_or_default(),
|
||||
aggregated_output: aggregated_output.clone(),
|
||||
exit_code: exit_code.unwrap_or_default(),
|
||||
duration: Duration::from_millis(
|
||||
duration_ms.unwrap_or_default().max(0) as u64
|
||||
),
|
||||
formatted_output: String::new(),
|
||||
formatted_output: aggregated_output,
|
||||
status: match status {
|
||||
codex_app_server_protocol::CommandExecutionStatus::Completed => {
|
||||
codex_protocol::protocol::ExecCommandStatus::Completed
|
||||
|
|
@ -6144,6 +6140,7 @@ impl ChatWidget {
|
|||
command,
|
||||
cwd,
|
||||
process_id,
|
||||
source,
|
||||
command_actions,
|
||||
..
|
||||
} => {
|
||||
|
|
@ -6157,7 +6154,7 @@ impl ChatWidget {
|
|||
.into_iter()
|
||||
.map(codex_app_server_protocol::CommandAction::into_core)
|
||||
.collect(),
|
||||
source: ExecCommandSource::Agent,
|
||||
source: source.to_core(),
|
||||
interaction_input: None,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8840,7 +8840,7 @@ async fn user_shell_command_renders_output_not_exploring() {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bang_shell_command_is_disabled_in_app_server_tui() {
|
||||
async fn bang_shell_command_submits_run_user_shell_command_in_app_server_tui() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(None).await;
|
||||
let conversation_id = ThreadId::new();
|
||||
let rollout_file = NamedTempFile::new().unwrap();
|
||||
|
|
@ -8873,22 +8873,11 @@ async fn bang_shell_command_is_disabled_in_app_server_tui() {
|
|||
.set_composer_text("!echo hi".to_string(), Vec::new(), Vec::new());
|
||||
chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
|
||||
|
||||
assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty));
|
||||
|
||||
let mut rendered = None;
|
||||
while let Ok(event) = rx.try_recv() {
|
||||
if let AppEvent::InsertHistoryCell(cell) = event {
|
||||
rendered = Some(lines_to_single_string(&cell.display_lines(80)));
|
||||
break;
|
||||
}
|
||||
match op_rx.try_recv() {
|
||||
Ok(Op::RunUserShellCommand { command }) => assert_eq!(command, "echo hi"),
|
||||
other => panic!("expected RunUserShellCommand op, got {other:?}"),
|
||||
}
|
||||
let rendered = rendered.expect("expected disabled bang-shell error");
|
||||
assert!(
|
||||
rendered.contains(
|
||||
"`!` shell commands are unavailable in app-server TUI because command output is not yet persisted in thread history."
|
||||
),
|
||||
"expected bang-shell disabled message, got: {rendered}"
|
||||
);
|
||||
assert_matches!(rx.try_recv(), Err(TryRecvError::Empty));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue