Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- codex-rs/app-server-client/src/lib.rs +2136 -0
- codex-rs/app-server-client/src/path.rs +58 -0
- codex-rs/app-server-client/src/remote.rs +1088 -0
- codex-rs/app-server-protocol/schema/json/AttestationGenerateResponse.json +14 -0
- codex-rs/app-server-protocol/schema/json/ClientNotification.json +22 -0
- codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalResponse.json +116 -0
- codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json +146 -0
- codex-rs/app-server-protocol/schema/json/FileChangeRequestApprovalResponse.json +47 -0
- codex-rs/app-server-protocol/schema/json/FuzzyFileSearchParams.json +26 -0
- codex-rs/app-server-protocol/schema/json/FuzzyFileSearchSessionUpdatedNotification.json +74 -0
- codex-rs/app-server-protocol/schema/json/JSONRPCError.json +48 -0
- codex-rs/app-server-protocol/schema/json/JSONRPCNotification.json +15 -0
- codex-rs/app-server-protocol/schema/json/JSONRPCRequest.json +60 -0
- codex-rs/app-server-protocol/schema/json/McpServerElicitationRequestResponse.json +29 -0
- codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json +322 -0
- codex-rs/app-server-protocol/schema/json/ToolRequestUserInputResponse.json +34 -0
- codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +0 -0
- codex-rs/app-server-protocol/schema/typescript/AbsolutePathBuf.ts +14 -0
- codex-rs/app-server-protocol/schema/typescript/AgentMessageInputContent.ts +5 -0
- codex-rs/app-server-protocol/schema/typescript/AgentPath.ts +5 -0
- codex-rs/app-server-protocol/schema/typescript/ApplyPatchApprovalParams.ts +21 -0
- codex-rs/app-server-protocol/schema/typescript/ApplyPatchApprovalResponse.ts +6 -0
- codex-rs/app-server-protocol/schema/typescript/AuthMode.ts +8 -0
- codex-rs/app-server-protocol/schema/typescript/AutoCompactTokenLimitScope.ts +9 -0
- codex-rs/app-server-protocol/schema/typescript/ClientInfo.ts +5 -0
- codex-rs/app-server-protocol/schema/typescript/ClientNotification.ts +5 -0
- codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +107 -0
- codex-rs/app-server-protocol/schema/typescript/CodexResponseHandoffMode.ts +5 -0
- codex-rs/app-server-protocol/schema/typescript/CollaborationMode.ts +10 -0
- codex-rs/app-server-protocol/schema/typescript/ConfigurationReasoning.ts +9 -0
- codex-rs/app-server-protocol/schema/typescript/ContentItem.ts +6 -0
- codex-rs/app-server-protocol/schema/typescript/ConversationGitInfo.ts +5 -0
- codex-rs/app-server-protocol/schema/typescript/ConversationSummary.ts +8 -0
- codex-rs/app-server-protocol/schema/typescript/ConversationTextRole.ts +5 -0
- codex-rs/app-server-protocol/schema/typescript/ExecCommandApprovalParams.ts +16 -0
- codex-rs/app-server-protocol/schema/typescript/ExecCommandApprovalResponse.ts +6 -0
- codex-rs/app-server-protocol/schema/typescript/ExecPolicyAmendment.ts +12 -0
- codex-rs/app-server-protocol/schema/typescript/FileChange.ts +5 -0
- codex-rs/app-server-protocol/schema/typescript/ForcedLoginMethod.ts +5 -0
- codex-rs/app-server-protocol/schema/typescript/FunctionCallOutputBody.ts +6 -0
- codex-rs/app-server-protocol/schema/typescript/FunctionCallOutputContentItem.ts +10 -0
- codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchMatchType.ts +5 -0
- codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchParams.ts +5 -0
- codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchResponse.ts +6 -0
- codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchResult.ts +9 -0
- codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchSessionCompletedNotification.ts +5 -0
- codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchSessionUpdatedNotification.ts +6 -0
- codex-rs/app-server-protocol/schema/typescript/GetAuthStatusParams.ts +5 -0
- codex-rs/app-server-protocol/schema/typescript/GetAuthStatusResponse.ts +6 -0
- codex-rs/app-server-protocol/schema/typescript/GetConversationSummaryParams.ts +6 -0
codex-rs/app-server-client/src/lib.rs
ADDED
|
@@ -0,0 +1,2136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Shared in-process app-server client facade for CLI surfaces.
|
| 2 |
+
//!
|
| 3 |
+
//! This crate wraps [`codex_app_server::in_process`] behind a single async API
|
| 4 |
+
//! used by surfaces like TUI and exec. It centralizes:
|
| 5 |
+
//!
|
| 6 |
+
//! - Runtime startup and initialize-capabilities handshake.
|
| 7 |
+
//! - Typed caller-provided startup identity (`SessionSource` + client name).
|
| 8 |
+
//! - Typed and raw request/notification dispatch.
|
| 9 |
+
//! - Server request resolution and rejection.
|
| 10 |
+
//! - Ordered, lossless event consumption that cannot block request processing.
|
| 11 |
+
//! - Bounded graceful shutdown with abort fallback.
|
| 12 |
+
//!
|
| 13 |
+
//! The facade interposes a worker task between the caller and the underlying
|
| 14 |
+
//! [`InProcessClientHandle`](codex_app_server::in_process::InProcessClientHandle),
|
| 15 |
+
//! bridging async `mpsc` channels on both sides. Commands and the underlying
|
| 16 |
+
//! runtime remain bounded; the local consumer event queue is unbounded so
|
| 17 |
+
//! unread notifications cannot prevent request responses from being delivered.
|
| 18 |
+
|
| 19 |
+
mod path;
|
| 20 |
+
mod remote;
|
| 21 |
+
|
| 22 |
+
use std::error::Error;
|
| 23 |
+
use std::fmt;
|
| 24 |
+
use std::io::Error as IoError;
|
| 25 |
+
use std::io::ErrorKind;
|
| 26 |
+
use std::io::Result as IoResult;
|
| 27 |
+
use std::sync::Arc;
|
| 28 |
+
use std::time::Duration;
|
| 29 |
+
|
| 30 |
+
pub use codex_app_server::app_server_control_socket_path;
|
| 31 |
+
pub use codex_app_server::in_process::DEFAULT_IN_PROCESS_CHANNEL_CAPACITY;
|
| 32 |
+
pub use codex_app_server::in_process::InProcessServerEvent;
|
| 33 |
+
use codex_app_server::in_process::InProcessStartArgs;
|
| 34 |
+
use codex_app_server::in_process::LogDbLayer;
|
| 35 |
+
pub use codex_app_server::in_process::StateDbHandle;
|
| 36 |
+
use codex_app_server_protocol::ClientInfo;
|
| 37 |
+
use codex_app_server_protocol::ClientNotification;
|
| 38 |
+
use codex_app_server_protocol::ClientRequest;
|
| 39 |
+
use codex_app_server_protocol::ConfigWarningNotification;
|
| 40 |
+
use codex_app_server_protocol::InitializeCapabilities;
|
| 41 |
+
use codex_app_server_protocol::InitializeParams;
|
| 42 |
+
use codex_app_server_protocol::JSONRPCErrorError;
|
| 43 |
+
use codex_app_server_protocol::RequestId;
|
| 44 |
+
use codex_app_server_protocol::Result as JsonRpcResult;
|
| 45 |
+
use codex_app_server_protocol::ServerNotification;
|
| 46 |
+
use codex_app_server_protocol::ServerRequest;
|
| 47 |
+
use codex_arg0::Arg0DispatchPaths;
|
| 48 |
+
use codex_config::CloudConfigBundleLoader;
|
| 49 |
+
use codex_config::LoaderOverrides;
|
| 50 |
+
use codex_config::NoopThreadConfigLoader;
|
| 51 |
+
use codex_core::config::Config;
|
| 52 |
+
pub use codex_core::otel_init::build_provider as build_otel_provider;
|
| 53 |
+
pub use codex_exec_server::EnvironmentManager;
|
| 54 |
+
pub use codex_exec_server::ExecServerRuntimePaths;
|
| 55 |
+
use codex_feedback::CodexFeedback;
|
| 56 |
+
use codex_protocol::protocol::SessionSource;
|
| 57 |
+
use codex_utils_absolute_path::AbsolutePathBuf;
|
| 58 |
+
use serde::de::DeserializeOwned;
|
| 59 |
+
use tokio::sync::mpsc;
|
| 60 |
+
use tokio::sync::oneshot;
|
| 61 |
+
use tokio::time::timeout;
|
| 62 |
+
use toml::Value as TomlValue;
|
| 63 |
+
use tracing::warn;
|
| 64 |
+
|
| 65 |
+
pub use crate::path::AppServerPath;
|
| 66 |
+
pub use crate::remote::RemoteAppServerClient;
|
| 67 |
+
pub use crate::remote::RemoteAppServerConnectArgs;
|
| 68 |
+
pub use crate::remote::RemoteAppServerEndpoint;
|
| 69 |
+
|
| 70 |
+
/// Transitional access to core-only embedded app-server types.
|
| 71 |
+
///
|
| 72 |
+
/// New TUI behavior should prefer the app-server protocol methods. This
|
| 73 |
+
/// module exists so clients can remove a direct `codex-core` dependency
|
| 74 |
+
/// while legacy startup/config paths are migrated to RPCs.
|
| 75 |
+
pub mod legacy_core {
|
| 76 |
+
pub mod config {
|
| 77 |
+
pub use codex_core::config::*;
|
| 78 |
+
|
| 79 |
+
pub mod edit {
|
| 80 |
+
pub use codex_core::config::edit::*;
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
|
| 86 |
+
// Covers the embedded drain, its analytics flush, and final task join.
|
| 87 |
+
const IN_PROCESS_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(45);
|
| 88 |
+
|
| 89 |
+
/// Raw app-server request result for typed in-process requests.
|
| 90 |
+
///
|
| 91 |
+
/// Even on the in-process path, successful responses still travel back through
|
| 92 |
+
/// the same JSON-RPC result envelope used by socket/stdio transports because
|
| 93 |
+
/// `MessageProcessor` continues to produce that shape internally.
|
| 94 |
+
pub type RequestResult = std::result::Result<JsonRpcResult, JSONRPCErrorError>;
|
| 95 |
+
|
| 96 |
+
#[derive(Debug, Clone)]
|
| 97 |
+
pub enum AppServerEvent {
|
| 98 |
+
Lagged { skipped: usize },
|
| 99 |
+
ServerNotification(Box<ServerNotification>),
|
| 100 |
+
ServerRequest(Box<ServerRequest>),
|
| 101 |
+
Disconnected { message: String },
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
impl From<InProcessServerEvent> for AppServerEvent {
|
| 105 |
+
fn from(value: InProcessServerEvent) -> Self {
|
| 106 |
+
match value {
|
| 107 |
+
InProcessServerEvent::Lagged { skipped } => Self::Lagged { skipped },
|
| 108 |
+
InProcessServerEvent::ServerNotification(notification) => {
|
| 109 |
+
Self::ServerNotification(notification)
|
| 110 |
+
}
|
| 111 |
+
InProcessServerEvent::ServerRequest(request) => Self::ServerRequest(request),
|
| 112 |
+
}
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
/// Layered error for [`InProcessAppServerClient::request_typed`].
|
| 117 |
+
///
|
| 118 |
+
/// This keeps transport failures, server-side JSON-RPC failures, and response
|
| 119 |
+
/// decode failures distinct so callers can decide whether to retry, surface a
|
| 120 |
+
/// server error, or treat the response as an internal request/response mismatch.
|
| 121 |
+
#[derive(Debug)]
|
| 122 |
+
pub enum TypedRequestError {
|
| 123 |
+
Transport {
|
| 124 |
+
method: String,
|
| 125 |
+
source: IoError,
|
| 126 |
+
},
|
| 127 |
+
Server {
|
| 128 |
+
method: String,
|
| 129 |
+
source: JSONRPCErrorError,
|
| 130 |
+
},
|
| 131 |
+
Deserialize {
|
| 132 |
+
method: String,
|
| 133 |
+
source: serde_json::Error,
|
| 134 |
+
},
|
| 135 |
+
}
|
| 136 |
+
|
| 137 |
+
impl fmt::Display for TypedRequestError {
|
| 138 |
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
| 139 |
+
match self {
|
| 140 |
+
Self::Transport { method, source } => {
|
| 141 |
+
write!(f, "{method} transport error: {source}")
|
| 142 |
+
}
|
| 143 |
+
Self::Server { method, source } => {
|
| 144 |
+
write!(
|
| 145 |
+
f,
|
| 146 |
+
"{method} failed: {} (code {})",
|
| 147 |
+
source.message, source.code
|
| 148 |
+
)?;
|
| 149 |
+
if let Some(data) = source.data.as_ref() {
|
| 150 |
+
write!(f, ", data: {data}")?;
|
| 151 |
+
}
|
| 152 |
+
Ok(())
|
| 153 |
+
}
|
| 154 |
+
Self::Deserialize { method, source } => {
|
| 155 |
+
write!(f, "{method} response decode error: {source}")
|
| 156 |
+
}
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
impl Error for TypedRequestError {
|
| 162 |
+
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
| 163 |
+
match self {
|
| 164 |
+
Self::Transport { source, .. } => Some(source),
|
| 165 |
+
Self::Server { .. } => None,
|
| 166 |
+
Self::Deserialize { source, .. } => Some(source),
|
| 167 |
+
}
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
#[derive(Clone)]
|
| 172 |
+
pub struct InProcessClientStartArgs {
|
| 173 |
+
/// Resolved argv0 dispatch paths used by command execution internals.
|
| 174 |
+
pub arg0_paths: Arg0DispatchPaths,
|
| 175 |
+
/// Shared config used to initialize app-server runtime.
|
| 176 |
+
pub config: Arc<Config>,
|
| 177 |
+
/// CLI config overrides that are already parsed into TOML values.
|
| 178 |
+
pub cli_overrides: Vec<(String, TomlValue)>,
|
| 179 |
+
/// Loader override knobs used by config API paths.
|
| 180 |
+
pub loader_overrides: LoaderOverrides,
|
| 181 |
+
/// Whether config API paths should reject unknown config fields.
|
| 182 |
+
pub strict_config: bool,
|
| 183 |
+
/// Preloaded cloud config bundle provider.
|
| 184 |
+
pub cloud_config_bundle: CloudConfigBundleLoader,
|
| 185 |
+
/// Feedback sink used by app-server/core telemetry and logs.
|
| 186 |
+
pub feedback: CodexFeedback,
|
| 187 |
+
/// SQLite tracing layer used to flush recently emitted logs before feedback upload.
|
| 188 |
+
pub log_db: Option<LogDbLayer>,
|
| 189 |
+
/// Process-wide SQLite state handle shared with the embedded app-server.
|
| 190 |
+
pub state_db: Option<StateDbHandle>,
|
| 191 |
+
/// Environment manager used by core execution and filesystem operations.
|
| 192 |
+
pub environment_manager: Arc<EnvironmentManager>,
|
| 193 |
+
/// Startup warnings emitted after initialize succeeds.
|
| 194 |
+
pub config_warnings: Vec<ConfigWarningNotification>,
|
| 195 |
+
/// Session source recorded in app-server thread metadata.
|
| 196 |
+
pub session_source: SessionSource,
|
| 197 |
+
/// Whether auth loading should honor the `CODEX_API_KEY` environment variable.
|
| 198 |
+
pub enable_codex_api_key_env: bool,
|
| 199 |
+
/// Client name reported during initialize.
|
| 200 |
+
pub client_name: String,
|
| 201 |
+
/// Client version reported during initialize.
|
| 202 |
+
pub client_version: String,
|
| 203 |
+
/// Whether experimental APIs are requested at initialize time.
|
| 204 |
+
pub experimental_api: bool,
|
| 205 |
+
/// Whether MCP servers may send `openai/form` elicitation requests.
|
| 206 |
+
pub mcp_server_openai_form_elicitation: bool,
|
| 207 |
+
/// Notification methods this client opts out of receiving.
|
| 208 |
+
pub opt_out_notification_methods: Vec<String>,
|
| 209 |
+
/// Queue capacity for command and embedded-runtime channels (clamped to at least 1).
|
| 210 |
+
pub channel_capacity: usize,
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
impl InProcessClientStartArgs {
|
| 214 |
+
/// Builds initialize params from caller-provided metadata.
|
| 215 |
+
pub fn initialize_params(&self) -> InitializeParams {
|
| 216 |
+
let capabilities = InitializeCapabilities {
|
| 217 |
+
experimental_api: self.experimental_api,
|
| 218 |
+
request_attestation: false,
|
| 219 |
+
extensions: None,
|
| 220 |
+
opt_out_notification_methods: if self.opt_out_notification_methods.is_empty() {
|
| 221 |
+
None
|
| 222 |
+
} else {
|
| 223 |
+
Some(self.opt_out_notification_methods.clone())
|
| 224 |
+
},
|
| 225 |
+
mcp_server_openai_form_elicitation: self.mcp_server_openai_form_elicitation,
|
| 226 |
+
};
|
| 227 |
+
|
| 228 |
+
InitializeParams {
|
| 229 |
+
client_info: ClientInfo {
|
| 230 |
+
name: self.client_name.clone(),
|
| 231 |
+
title: None,
|
| 232 |
+
version: self.client_version.clone(),
|
| 233 |
+
},
|
| 234 |
+
capabilities: Some(capabilities),
|
| 235 |
+
}
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
fn into_runtime_start_args(self) -> InProcessStartArgs {
|
| 239 |
+
let initialize = self.initialize_params();
|
| 240 |
+
InProcessStartArgs {
|
| 241 |
+
arg0_paths: self.arg0_paths,
|
| 242 |
+
config: self.config,
|
| 243 |
+
cli_overrides: self.cli_overrides,
|
| 244 |
+
loader_overrides: self.loader_overrides,
|
| 245 |
+
strict_config: self.strict_config,
|
| 246 |
+
cloud_config_bundle: self.cloud_config_bundle,
|
| 247 |
+
thread_config_loader: Arc::new(NoopThreadConfigLoader),
|
| 248 |
+
feedback: self.feedback,
|
| 249 |
+
log_db: self.log_db,
|
| 250 |
+
state_db: self.state_db,
|
| 251 |
+
environment_manager: self.environment_manager,
|
| 252 |
+
config_warnings: self.config_warnings,
|
| 253 |
+
session_source: self.session_source,
|
| 254 |
+
enable_codex_api_key_env: self.enable_codex_api_key_env,
|
| 255 |
+
initialize,
|
| 256 |
+
channel_capacity: self.channel_capacity,
|
| 257 |
+
}
|
| 258 |
+
}
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
/// Internal command sent from public facade methods to the worker task.
|
| 262 |
+
///
|
| 263 |
+
/// Each variant carries a oneshot sender so the caller can `await` the
|
| 264 |
+
/// result without holding a mutable reference to the client.
|
| 265 |
+
enum ClientCommand {
|
| 266 |
+
Request {
|
| 267 |
+
request: Box<ClientRequest>,
|
| 268 |
+
response_tx: oneshot::Sender<IoResult<RequestResult>>,
|
| 269 |
+
},
|
| 270 |
+
Notify {
|
| 271 |
+
notification: ClientNotification,
|
| 272 |
+
response_tx: oneshot::Sender<IoResult<()>>,
|
| 273 |
+
},
|
| 274 |
+
ResolveServerRequest {
|
| 275 |
+
request_id: RequestId,
|
| 276 |
+
result: JsonRpcResult,
|
| 277 |
+
response_tx: oneshot::Sender<IoResult<()>>,
|
| 278 |
+
},
|
| 279 |
+
RejectServerRequest {
|
| 280 |
+
request_id: RequestId,
|
| 281 |
+
error: JSONRPCErrorError,
|
| 282 |
+
response_tx: oneshot::Sender<IoResult<()>>,
|
| 283 |
+
},
|
| 284 |
+
Shutdown {
|
| 285 |
+
response_tx: oneshot::Sender<IoResult<()>>,
|
| 286 |
+
},
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
/// Async facade over the in-process app-server runtime.
|
| 290 |
+
///
|
| 291 |
+
/// This type owns a worker task that bridges between:
|
| 292 |
+
/// - caller-facing async `mpsc` channels used by TUI/exec
|
| 293 |
+
/// - [`codex_app_server::in_process::InProcessClientHandle`], which speaks to
|
| 294 |
+
/// the embedded `MessageProcessor`
|
| 295 |
+
///
|
| 296 |
+
/// The facade intentionally preserves the server's request/notification/event
|
| 297 |
+
/// model instead of exposing direct core runtime handles. That keeps in-process
|
| 298 |
+
/// callers aligned with app-server behavior while still avoiding a process
|
| 299 |
+
/// boundary.
|
| 300 |
+
pub struct InProcessAppServerClient {
|
| 301 |
+
command_tx: mpsc::Sender<ClientCommand>,
|
| 302 |
+
event_rx: mpsc::UnboundedReceiver<InProcessServerEvent>,
|
| 303 |
+
worker_handle: tokio::task::JoinHandle<()>,
|
| 304 |
+
}
|
| 305 |
+
|
| 306 |
+
#[derive(Clone)]
|
| 307 |
+
pub struct InProcessAppServerRequestHandle {
|
| 308 |
+
command_tx: mpsc::Sender<ClientCommand>,
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
#[derive(Clone)]
|
| 312 |
+
pub enum AppServerRequestHandle {
|
| 313 |
+
InProcess(InProcessAppServerRequestHandle),
|
| 314 |
+
Remote(crate::remote::RemoteAppServerRequestHandle),
|
| 315 |
+
}
|
| 316 |
+
|
| 317 |
+
pub enum AppServerClient {
|
| 318 |
+
InProcess(InProcessAppServerClient),
|
| 319 |
+
Remote(RemoteAppServerClient),
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
impl InProcessAppServerClient {
|
| 323 |
+
/// Starts the in-process runtime and facade worker task.
|
| 324 |
+
///
|
| 325 |
+
/// The returned client is ready for requests and ordered event consumption.
|
| 326 |
+
/// Request queues remain bounded without blocking on unread notifications.
|
| 327 |
+
pub async fn start(args: InProcessClientStartArgs) -> IoResult<Self> {
|
| 328 |
+
let channel_capacity = args.channel_capacity.max(1);
|
| 329 |
+
let mut handle =
|
| 330 |
+
codex_app_server::in_process::start(args.into_runtime_start_args()).await?;
|
| 331 |
+
let request_sender = handle.sender();
|
| 332 |
+
let (command_tx, mut command_rx) = mpsc::channel::<ClientCommand>(channel_capacity);
|
| 333 |
+
// e9996ec62a preserved transcript events by awaiting a bounded queue, but that can
|
| 334 |
+
// deadlock a foreground request whose response is behind unread notifications.
|
| 335 |
+
// Match the remote-client fix in 79ea57715636: only this local consumer queue is
|
| 336 |
+
// unbounded; commands and the embedded runtime stay bounded and events remain ordered.
|
| 337 |
+
let (event_tx, event_rx) = mpsc::unbounded_channel::<InProcessServerEvent>();
|
| 338 |
+
|
| 339 |
+
let worker_handle = tokio::spawn(async move {
|
| 340 |
+
let mut event_stream_enabled = true;
|
| 341 |
+
loop {
|
| 342 |
+
tokio::select! {
|
| 343 |
+
command = command_rx.recv() => {
|
| 344 |
+
match command {
|
| 345 |
+
Some(ClientCommand::Request { request, response_tx }) => {
|
| 346 |
+
let request_sender = request_sender.clone();
|
| 347 |
+
// Request waits happen on a detached task so
|
| 348 |
+
// this loop can keep draining runtime events
|
| 349 |
+
// while the request is blocked on client input.
|
| 350 |
+
tokio::spawn(async move {
|
| 351 |
+
// Device ceremonies belong to the waiting UI. Preserve
|
| 352 |
+
// its cancellation through this buffering task.
|
| 353 |
+
let cancellable = matches!(*request,
|
| 354 |
+
ClientRequest::UserVerificationStatus { .. }
|
| 355 |
+
| ClientRequest::UserVerificationEnroll { .. }
|
| 356 |
+
| ClientRequest::UserVerificationDelete { .. }
|
| 357 |
+
| ClientRequest::UserVerificationVerify { .. });
|
| 358 |
+
let mut response_tx = response_tx;
|
| 359 |
+
tokio::select! {
|
| 360 |
+
_ = response_tx.closed(), if cancellable => {}
|
| 361 |
+
result = request_sender.request(*request) => {
|
| 362 |
+
let _ = response_tx.send(result);
|
| 363 |
+
}
|
| 364 |
+
}
|
| 365 |
+
});
|
| 366 |
+
}
|
| 367 |
+
Some(ClientCommand::Notify {
|
| 368 |
+
notification,
|
| 369 |
+
response_tx,
|
| 370 |
+
}) => {
|
| 371 |
+
let result = request_sender.notify(notification);
|
| 372 |
+
let _ = response_tx.send(result);
|
| 373 |
+
}
|
| 374 |
+
Some(ClientCommand::ResolveServerRequest {
|
| 375 |
+
request_id,
|
| 376 |
+
result,
|
| 377 |
+
response_tx,
|
| 378 |
+
}) => {
|
| 379 |
+
let send_result =
|
| 380 |
+
request_sender.respond_to_server_request(request_id, result);
|
| 381 |
+
let _ = response_tx.send(send_result);
|
| 382 |
+
}
|
| 383 |
+
Some(ClientCommand::RejectServerRequest {
|
| 384 |
+
request_id,
|
| 385 |
+
error,
|
| 386 |
+
response_tx,
|
| 387 |
+
}) => {
|
| 388 |
+
let send_result = request_sender.fail_server_request(request_id, error);
|
| 389 |
+
let _ = response_tx.send(send_result);
|
| 390 |
+
}
|
| 391 |
+
Some(ClientCommand::Shutdown { response_tx }) => {
|
| 392 |
+
let shutdown_result = handle.shutdown().await;
|
| 393 |
+
let _ = response_tx.send(shutdown_result);
|
| 394 |
+
break;
|
| 395 |
+
}
|
| 396 |
+
None => {
|
| 397 |
+
let _ = handle.shutdown().await;
|
| 398 |
+
break;
|
| 399 |
+
}
|
| 400 |
+
}
|
| 401 |
+
}
|
| 402 |
+
event = handle.next_event(), if event_stream_enabled => {
|
| 403 |
+
let Some(event) = event else {
|
| 404 |
+
break;
|
| 405 |
+
};
|
| 406 |
+
if let InProcessServerEvent::ServerRequest(request) = &event
|
| 407 |
+
&& let ServerRequest::ChatgptAuthTokensRefresh { request_id, .. } =
|
| 408 |
+
request.as_ref()
|
| 409 |
+
{
|
| 410 |
+
let send_result = request_sender.fail_server_request(
|
| 411 |
+
request_id.clone(),
|
| 412 |
+
JSONRPCErrorError {
|
| 413 |
+
code: -32000,
|
| 414 |
+
message: "chatgpt auth token refresh is not supported for in-process app-server clients".to_string(),
|
| 415 |
+
data: None,
|
| 416 |
+
},
|
| 417 |
+
);
|
| 418 |
+
if let Err(err) = send_result {
|
| 419 |
+
warn!(
|
| 420 |
+
"failed to reject unsupported chatgpt auth token refresh request: {err}"
|
| 421 |
+
);
|
| 422 |
+
}
|
| 423 |
+
continue;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
if event_tx.send(event).is_err() {
|
| 427 |
+
event_stream_enabled = false;
|
| 428 |
+
}
|
| 429 |
+
}
|
| 430 |
+
}
|
| 431 |
+
}
|
| 432 |
+
});
|
| 433 |
+
|
| 434 |
+
Ok(Self {
|
| 435 |
+
command_tx,
|
| 436 |
+
event_rx,
|
| 437 |
+
worker_handle,
|
| 438 |
+
})
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
pub fn request_handle(&self) -> InProcessAppServerRequestHandle {
|
| 442 |
+
InProcessAppServerRequestHandle {
|
| 443 |
+
command_tx: self.command_tx.clone(),
|
| 444 |
+
}
|
| 445 |
+
}
|
| 446 |
+
|
| 447 |
+
/// Sends a typed client request and returns raw JSON-RPC result.
|
| 448 |
+
///
|
| 449 |
+
/// Callers that expect a concrete response type should usually prefer
|
| 450 |
+
/// [`request_typed`](Self::request_typed).
|
| 451 |
+
pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
|
| 452 |
+
let (response_tx, response_rx) = oneshot::channel();
|
| 453 |
+
self.command_tx
|
| 454 |
+
.send(ClientCommand::Request {
|
| 455 |
+
request: Box::new(request),
|
| 456 |
+
response_tx,
|
| 457 |
+
})
|
| 458 |
+
.await
|
| 459 |
+
.map_err(|_| {
|
| 460 |
+
IoError::new(
|
| 461 |
+
ErrorKind::BrokenPipe,
|
| 462 |
+
"in-process app-server worker channel is closed",
|
| 463 |
+
)
|
| 464 |
+
})?;
|
| 465 |
+
response_rx.await.map_err(|_| {
|
| 466 |
+
IoError::new(
|
| 467 |
+
ErrorKind::BrokenPipe,
|
| 468 |
+
"in-process app-server request channel is closed",
|
| 469 |
+
)
|
| 470 |
+
})?
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
/// Sends a typed client request and decodes the successful response body.
|
| 474 |
+
///
|
| 475 |
+
/// This still deserializes from a JSON value produced by app-server's
|
| 476 |
+
/// JSON-RPC result envelope. Because the caller chooses `T`, `Deserialize`
|
| 477 |
+
/// failures indicate an internal request/response mismatch at the call site
|
| 478 |
+
/// (or an in-process bug), not transport skew from an external client.
|
| 479 |
+
pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
|
| 480 |
+
where
|
| 481 |
+
T: DeserializeOwned,
|
| 482 |
+
{
|
| 483 |
+
let method = request.method_name();
|
| 484 |
+
let response =
|
| 485 |
+
self.request(request)
|
| 486 |
+
.await
|
| 487 |
+
.map_err(|source| TypedRequestError::Transport {
|
| 488 |
+
method: method.to_string(),
|
| 489 |
+
source,
|
| 490 |
+
})?;
|
| 491 |
+
let result = response.map_err(|source| TypedRequestError::Server {
|
| 492 |
+
method: method.to_string(),
|
| 493 |
+
source,
|
| 494 |
+
})?;
|
| 495 |
+
serde_json::from_value(result).map_err(|source| TypedRequestError::Deserialize {
|
| 496 |
+
method: method.to_string(),
|
| 497 |
+
source,
|
| 498 |
+
})
|
| 499 |
+
}
|
| 500 |
+
|
| 501 |
+
/// Sends a typed client notification.
|
| 502 |
+
pub async fn notify(&self, notification: ClientNotification) -> IoResult<()> {
|
| 503 |
+
let (response_tx, response_rx) = oneshot::channel();
|
| 504 |
+
self.command_tx
|
| 505 |
+
.send(ClientCommand::Notify {
|
| 506 |
+
notification,
|
| 507 |
+
response_tx,
|
| 508 |
+
})
|
| 509 |
+
.await
|
| 510 |
+
.map_err(|_| {
|
| 511 |
+
IoError::new(
|
| 512 |
+
ErrorKind::BrokenPipe,
|
| 513 |
+
"in-process app-server worker channel is closed",
|
| 514 |
+
)
|
| 515 |
+
})?;
|
| 516 |
+
response_rx.await.map_err(|_| {
|
| 517 |
+
IoError::new(
|
| 518 |
+
ErrorKind::BrokenPipe,
|
| 519 |
+
"in-process app-server notify channel is closed",
|
| 520 |
+
)
|
| 521 |
+
})?
|
| 522 |
+
}
|
| 523 |
+
|
| 524 |
+
/// Resolves a pending server request.
|
| 525 |
+
///
|
| 526 |
+
/// This should only be called with request IDs obtained from the current
|
| 527 |
+
/// client's event stream.
|
| 528 |
+
pub async fn resolve_server_request(
|
| 529 |
+
&self,
|
| 530 |
+
request_id: RequestId,
|
| 531 |
+
result: JsonRpcResult,
|
| 532 |
+
) -> IoResult<()> {
|
| 533 |
+
let (response_tx, response_rx) = oneshot::channel();
|
| 534 |
+
self.command_tx
|
| 535 |
+
.send(ClientCommand::ResolveServerRequest {
|
| 536 |
+
request_id,
|
| 537 |
+
result,
|
| 538 |
+
response_tx,
|
| 539 |
+
})
|
| 540 |
+
.await
|
| 541 |
+
.map_err(|_| {
|
| 542 |
+
IoError::new(
|
| 543 |
+
ErrorKind::BrokenPipe,
|
| 544 |
+
"in-process app-server worker channel is closed",
|
| 545 |
+
)
|
| 546 |
+
})?;
|
| 547 |
+
response_rx.await.map_err(|_| {
|
| 548 |
+
IoError::new(
|
| 549 |
+
ErrorKind::BrokenPipe,
|
| 550 |
+
"in-process app-server resolve channel is closed",
|
| 551 |
+
)
|
| 552 |
+
})?
|
| 553 |
+
}
|
| 554 |
+
|
| 555 |
+
/// Rejects a pending server request with JSON-RPC error payload.
|
| 556 |
+
pub async fn reject_server_request(
|
| 557 |
+
&self,
|
| 558 |
+
request_id: RequestId,
|
| 559 |
+
error: JSONRPCErrorError,
|
| 560 |
+
) -> IoResult<()> {
|
| 561 |
+
let (response_tx, response_rx) = oneshot::channel();
|
| 562 |
+
self.command_tx
|
| 563 |
+
.send(ClientCommand::RejectServerRequest {
|
| 564 |
+
request_id,
|
| 565 |
+
error,
|
| 566 |
+
response_tx,
|
| 567 |
+
})
|
| 568 |
+
.await
|
| 569 |
+
.map_err(|_| {
|
| 570 |
+
IoError::new(
|
| 571 |
+
ErrorKind::BrokenPipe,
|
| 572 |
+
"in-process app-server worker channel is closed",
|
| 573 |
+
)
|
| 574 |
+
})?;
|
| 575 |
+
response_rx.await.map_err(|_| {
|
| 576 |
+
IoError::new(
|
| 577 |
+
ErrorKind::BrokenPipe,
|
| 578 |
+
"in-process app-server reject channel is closed",
|
| 579 |
+
)
|
| 580 |
+
})?
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
/// Returns the next in-process event, or `None` when worker exits.
|
| 584 |
+
///
|
| 585 |
+
/// Events remain ordered and are retained while callers await requests.
|
| 586 |
+
pub async fn next_event(&mut self) -> Option<InProcessServerEvent> {
|
| 587 |
+
self.event_rx.recv().await
|
| 588 |
+
}
|
| 589 |
+
|
| 590 |
+
/// Shuts down worker and in-process runtime with bounded wait.
|
| 591 |
+
///
|
| 592 |
+
/// If graceful shutdown exceeds timeout, the worker task is aborted to
|
| 593 |
+
/// avoid leaking background tasks in embedding callers.
|
| 594 |
+
pub async fn shutdown(self) -> IoResult<()> {
|
| 595 |
+
let Self {
|
| 596 |
+
command_tx,
|
| 597 |
+
event_rx,
|
| 598 |
+
worker_handle,
|
| 599 |
+
} = self;
|
| 600 |
+
let mut worker_handle = worker_handle;
|
| 601 |
+
// Stop forwarding caller-facing events before asking the worker to shut down.
|
| 602 |
+
drop(event_rx);
|
| 603 |
+
let (response_tx, response_rx) = oneshot::channel();
|
| 604 |
+
if command_tx
|
| 605 |
+
.send(ClientCommand::Shutdown { response_tx })
|
| 606 |
+
.await
|
| 607 |
+
.is_ok()
|
| 608 |
+
&& let Ok(command_result) = timeout(IN_PROCESS_SHUTDOWN_TIMEOUT, response_rx).await
|
| 609 |
+
{
|
| 610 |
+
command_result.map_err(|_| {
|
| 611 |
+
IoError::new(
|
| 612 |
+
ErrorKind::BrokenPipe,
|
| 613 |
+
"in-process app-server shutdown channel is closed",
|
| 614 |
+
)
|
| 615 |
+
})??;
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
if let Err(_elapsed) = timeout(IN_PROCESS_SHUTDOWN_TIMEOUT, &mut worker_handle).await {
|
| 619 |
+
worker_handle.abort();
|
| 620 |
+
let _ = worker_handle.await;
|
| 621 |
+
}
|
| 622 |
+
Ok(())
|
| 623 |
+
}
|
| 624 |
+
}
|
| 625 |
+
|
| 626 |
+
impl InProcessAppServerRequestHandle {
|
| 627 |
+
pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
|
| 628 |
+
let (response_tx, response_rx) = oneshot::channel();
|
| 629 |
+
self.command_tx
|
| 630 |
+
.send(ClientCommand::Request {
|
| 631 |
+
request: Box::new(request),
|
| 632 |
+
response_tx,
|
| 633 |
+
})
|
| 634 |
+
.await
|
| 635 |
+
.map_err(|_| {
|
| 636 |
+
IoError::new(
|
| 637 |
+
ErrorKind::BrokenPipe,
|
| 638 |
+
"in-process app-server worker channel is closed",
|
| 639 |
+
)
|
| 640 |
+
})?;
|
| 641 |
+
response_rx.await.map_err(|_| {
|
| 642 |
+
IoError::new(
|
| 643 |
+
ErrorKind::BrokenPipe,
|
| 644 |
+
"in-process app-server request channel is closed",
|
| 645 |
+
)
|
| 646 |
+
})?
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
|
| 650 |
+
where
|
| 651 |
+
T: DeserializeOwned,
|
| 652 |
+
{
|
| 653 |
+
let method = request.method_name();
|
| 654 |
+
let response =
|
| 655 |
+
self.request(request)
|
| 656 |
+
.await
|
| 657 |
+
.map_err(|source| TypedRequestError::Transport {
|
| 658 |
+
method: method.to_string(),
|
| 659 |
+
source,
|
| 660 |
+
})?;
|
| 661 |
+
let result = response.map_err(|source| TypedRequestError::Server {
|
| 662 |
+
method: method.to_string(),
|
| 663 |
+
source,
|
| 664 |
+
})?;
|
| 665 |
+
serde_json::from_value(result).map_err(|source| TypedRequestError::Deserialize {
|
| 666 |
+
method: method.to_string(),
|
| 667 |
+
source,
|
| 668 |
+
})
|
| 669 |
+
}
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
impl AppServerRequestHandle {
|
| 673 |
+
pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
|
| 674 |
+
match self {
|
| 675 |
+
Self::InProcess(handle) => handle.request(request).await,
|
| 676 |
+
Self::Remote(handle) => handle.request(request).await,
|
| 677 |
+
}
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
|
| 681 |
+
where
|
| 682 |
+
T: DeserializeOwned,
|
| 683 |
+
{
|
| 684 |
+
match self {
|
| 685 |
+
Self::InProcess(handle) => handle.request_typed(request).await,
|
| 686 |
+
Self::Remote(handle) => handle.request_typed(request).await,
|
| 687 |
+
}
|
| 688 |
+
}
|
| 689 |
+
}
|
| 690 |
+
|
| 691 |
+
impl AppServerClient {
|
| 692 |
+
/// App-server platform family, which can differ from the executor's platform.
|
| 693 |
+
/// Older remote servers may omit this metadata.
|
| 694 |
+
pub fn platform_family(&self) -> Option<&str> {
|
| 695 |
+
match self {
|
| 696 |
+
Self::InProcess(_) => Some(std::env::consts::FAMILY),
|
| 697 |
+
Self::Remote(client) => client.platform_family(),
|
| 698 |
+
}
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
/// App-server operating system as reported at initialization, including unknown values.
|
| 702 |
+
pub fn platform_os(&self) -> Option<&str> {
|
| 703 |
+
match self {
|
| 704 |
+
Self::InProcess(_) => Some(std::env::consts::OS),
|
| 705 |
+
Self::Remote(client) => client.platform_os(),
|
| 706 |
+
}
|
| 707 |
+
}
|
| 708 |
+
|
| 709 |
+
pub fn codex_home(&self, local_codex_home: &AbsolutePathBuf) -> Option<AppServerPath> {
|
| 710 |
+
match self {
|
| 711 |
+
Self::InProcess(_) => Some(AppServerPath::from_app_server(
|
| 712 |
+
local_codex_home.display().to_string(),
|
| 713 |
+
)),
|
| 714 |
+
Self::Remote(client) => client.codex_home().map(AppServerPath::from_app_server),
|
| 715 |
+
}
|
| 716 |
+
}
|
| 717 |
+
|
| 718 |
+
pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
|
| 719 |
+
match self {
|
| 720 |
+
Self::InProcess(client) => client.request(request).await,
|
| 721 |
+
Self::Remote(client) => client.request(request).await,
|
| 722 |
+
}
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
|
| 726 |
+
where
|
| 727 |
+
T: DeserializeOwned,
|
| 728 |
+
{
|
| 729 |
+
match self {
|
| 730 |
+
Self::InProcess(client) => client.request_typed(request).await,
|
| 731 |
+
Self::Remote(client) => client.request_typed(request).await,
|
| 732 |
+
}
|
| 733 |
+
}
|
| 734 |
+
|
| 735 |
+
pub async fn notify(&self, notification: ClientNotification) -> IoResult<()> {
|
| 736 |
+
match self {
|
| 737 |
+
Self::InProcess(client) => client.notify(notification).await,
|
| 738 |
+
Self::Remote(client) => client.notify(notification).await,
|
| 739 |
+
}
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
pub async fn resolve_server_request(
|
| 743 |
+
&self,
|
| 744 |
+
request_id: RequestId,
|
| 745 |
+
result: JsonRpcResult,
|
| 746 |
+
) -> IoResult<()> {
|
| 747 |
+
match self {
|
| 748 |
+
Self::InProcess(client) => client.resolve_server_request(request_id, result).await,
|
| 749 |
+
Self::Remote(client) => client.resolve_server_request(request_id, result).await,
|
| 750 |
+
}
|
| 751 |
+
}
|
| 752 |
+
|
| 753 |
+
pub async fn reject_server_request(
|
| 754 |
+
&self,
|
| 755 |
+
request_id: RequestId,
|
| 756 |
+
error: JSONRPCErrorError,
|
| 757 |
+
) -> IoResult<()> {
|
| 758 |
+
match self {
|
| 759 |
+
Self::InProcess(client) => client.reject_server_request(request_id, error).await,
|
| 760 |
+
Self::Remote(client) => client.reject_server_request(request_id, error).await,
|
| 761 |
+
}
|
| 762 |
+
}
|
| 763 |
+
|
| 764 |
+
pub async fn next_event(&mut self) -> Option<AppServerEvent> {
|
| 765 |
+
match self {
|
| 766 |
+
Self::InProcess(client) => client.next_event().await.map(Into::into),
|
| 767 |
+
Self::Remote(client) => client.next_event().await,
|
| 768 |
+
}
|
| 769 |
+
}
|
| 770 |
+
|
| 771 |
+
pub async fn shutdown(self) -> IoResult<()> {
|
| 772 |
+
match self {
|
| 773 |
+
Self::InProcess(client) => client.shutdown().await,
|
| 774 |
+
Self::Remote(client) => client.shutdown().await,
|
| 775 |
+
}
|
| 776 |
+
}
|
| 777 |
+
|
| 778 |
+
pub fn request_handle(&self) -> AppServerRequestHandle {
|
| 779 |
+
match self {
|
| 780 |
+
Self::InProcess(client) => AppServerRequestHandle::InProcess(client.request_handle()),
|
| 781 |
+
Self::Remote(client) => AppServerRequestHandle::Remote(client.request_handle()),
|
| 782 |
+
}
|
| 783 |
+
}
|
| 784 |
+
}
|
| 785 |
+
|
| 786 |
+
#[cfg(test)]
|
| 787 |
+
mod tests {
|
| 788 |
+
use super::*;
|
| 789 |
+
use codex_app_server_protocol::AccountUpdatedNotification;
|
| 790 |
+
use codex_app_server_protocol::ConfigRequirementsReadResponse;
|
| 791 |
+
use codex_app_server_protocol::GetAccountResponse;
|
| 792 |
+
use codex_app_server_protocol::JSONRPCMessage;
|
| 793 |
+
use codex_app_server_protocol::JSONRPCRequest;
|
| 794 |
+
use codex_app_server_protocol::JSONRPCResponse;
|
| 795 |
+
use codex_app_server_protocol::ServerNotification;
|
| 796 |
+
use codex_app_server_protocol::SessionSource as ApiSessionSource;
|
| 797 |
+
use codex_app_server_protocol::ThreadSettingsUpdateParams;
|
| 798 |
+
use codex_app_server_protocol::ThreadSettingsUpdateResponse;
|
| 799 |
+
use codex_app_server_protocol::ThreadStartParams;
|
| 800 |
+
use codex_app_server_protocol::ThreadStartResponse;
|
| 801 |
+
use codex_app_server_protocol::ToolRequestUserInputParams;
|
| 802 |
+
use codex_app_server_protocol::ToolRequestUserInputQuestion;
|
| 803 |
+
use codex_core::config::ConfigBuilder;
|
| 804 |
+
use codex_core::init_state_db;
|
| 805 |
+
use codex_protocol::config_types::Personality;
|
| 806 |
+
use codex_uds::UnixListener;
|
| 807 |
+
use codex_utils_absolute_path::AbsolutePathBuf;
|
| 808 |
+
use futures::SinkExt;
|
| 809 |
+
use futures::StreamExt;
|
| 810 |
+
use pretty_assertions::assert_eq;
|
| 811 |
+
use std::ops::Deref;
|
| 812 |
+
use std::path::Path;
|
| 813 |
+
use tempfile::TempDir;
|
| 814 |
+
use tokio::net::TcpListener;
|
| 815 |
+
use tokio::time::Duration;
|
| 816 |
+
use tokio::time::timeout;
|
| 817 |
+
use tokio_tungstenite::accept_async;
|
| 818 |
+
use tokio_tungstenite::accept_hdr_async;
|
| 819 |
+
use tokio_tungstenite::tungstenite::Message;
|
| 820 |
+
use tokio_tungstenite::tungstenite::handshake::server::Request as WebSocketRequest;
|
| 821 |
+
use tokio_tungstenite::tungstenite::handshake::server::Response as WebSocketResponse;
|
| 822 |
+
use tokio_tungstenite::tungstenite::http::header::AUTHORIZATION;
|
| 823 |
+
|
| 824 |
+
async fn build_test_config() -> Config {
|
| 825 |
+
match ConfigBuilder::default().build().await {
|
| 826 |
+
Ok(config) => config,
|
| 827 |
+
Err(_) => Config::load_default_with_cli_overrides(Vec::new())
|
| 828 |
+
.await
|
| 829 |
+
.expect("default config should load"),
|
| 830 |
+
}
|
| 831 |
+
}
|
| 832 |
+
|
| 833 |
+
async fn build_test_config_for_codex_home(codex_home: &Path) -> Config {
|
| 834 |
+
match ConfigBuilder::default()
|
| 835 |
+
.codex_home(codex_home.to_path_buf())
|
| 836 |
+
.build()
|
| 837 |
+
.await
|
| 838 |
+
{
|
| 839 |
+
Ok(config) => config,
|
| 840 |
+
Err(_) => Config::load_default_with_cli_overrides_for_codex_home(
|
| 841 |
+
codex_home.to_path_buf(),
|
| 842 |
+
Vec::new(),
|
| 843 |
+
)
|
| 844 |
+
.await
|
| 845 |
+
.expect("default config should load"),
|
| 846 |
+
}
|
| 847 |
+
}
|
| 848 |
+
|
| 849 |
+
struct TestClient {
|
| 850 |
+
_codex_home: TempDir,
|
| 851 |
+
client: InProcessAppServerClient,
|
| 852 |
+
}
|
| 853 |
+
|
| 854 |
+
impl Deref for TestClient {
|
| 855 |
+
type Target = InProcessAppServerClient;
|
| 856 |
+
|
| 857 |
+
fn deref(&self) -> &Self::Target {
|
| 858 |
+
&self.client
|
| 859 |
+
}
|
| 860 |
+
}
|
| 861 |
+
|
| 862 |
+
impl TestClient {
|
| 863 |
+
async fn shutdown(self) -> IoResult<()> {
|
| 864 |
+
self.client.shutdown().await
|
| 865 |
+
}
|
| 866 |
+
}
|
| 867 |
+
|
| 868 |
+
async fn start_test_client_with_capacity(
|
| 869 |
+
session_source: SessionSource,
|
| 870 |
+
channel_capacity: usize,
|
| 871 |
+
) -> TestClient {
|
| 872 |
+
let codex_home = TempDir::new().expect("temp dir");
|
| 873 |
+
let config = Arc::new(build_test_config_for_codex_home(codex_home.path()).await);
|
| 874 |
+
let state_db = init_state_db(config.as_ref())
|
| 875 |
+
.await
|
| 876 |
+
.expect("state db should initialize for in-process test");
|
| 877 |
+
let client = InProcessAppServerClient::start(InProcessClientStartArgs {
|
| 878 |
+
arg0_paths: Arg0DispatchPaths::default(),
|
| 879 |
+
config,
|
| 880 |
+
cli_overrides: Vec::new(),
|
| 881 |
+
loader_overrides: LoaderOverrides::default(),
|
| 882 |
+
strict_config: false,
|
| 883 |
+
cloud_config_bundle: CloudConfigBundleLoader::default(),
|
| 884 |
+
feedback: CodexFeedback::new(),
|
| 885 |
+
log_db: None,
|
| 886 |
+
state_db: Some(state_db),
|
| 887 |
+
environment_manager: Arc::new(EnvironmentManager::default_for_tests()),
|
| 888 |
+
config_warnings: Vec::new(),
|
| 889 |
+
session_source,
|
| 890 |
+
enable_codex_api_key_env: false,
|
| 891 |
+
client_name: "codex-app-server-client-test".to_string(),
|
| 892 |
+
client_version: "0.0.0-test".to_string(),
|
| 893 |
+
experimental_api: true,
|
| 894 |
+
mcp_server_openai_form_elicitation: false,
|
| 895 |
+
opt_out_notification_methods: Vec::new(),
|
| 896 |
+
channel_capacity,
|
| 897 |
+
})
|
| 898 |
+
.await
|
| 899 |
+
.expect("in-process app-server client should start");
|
| 900 |
+
|
| 901 |
+
TestClient {
|
| 902 |
+
_codex_home: codex_home,
|
| 903 |
+
client,
|
| 904 |
+
}
|
| 905 |
+
}
|
| 906 |
+
|
| 907 |
+
async fn start_test_client(session_source: SessionSource) -> TestClient {
|
| 908 |
+
start_test_client_with_capacity(session_source, DEFAULT_IN_PROCESS_CHANNEL_CAPACITY).await
|
| 909 |
+
}
|
| 910 |
+
|
| 911 |
+
async fn start_test_remote_server<F, Fut>(handler: F) -> String
|
| 912 |
+
where
|
| 913 |
+
F: FnOnce(tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>) -> Fut
|
| 914 |
+
+ Send
|
| 915 |
+
+ 'static,
|
| 916 |
+
Fut: std::future::Future<Output = ()> + Send + 'static,
|
| 917 |
+
{
|
| 918 |
+
start_test_remote_server_with_auth(/*expected_auth_token*/ None, handler).await
|
| 919 |
+
}
|
| 920 |
+
|
| 921 |
+
async fn start_test_remote_server_with_auth<F, Fut>(
|
| 922 |
+
expected_auth_token: Option<String>,
|
| 923 |
+
handler: F,
|
| 924 |
+
) -> String
|
| 925 |
+
where
|
| 926 |
+
F: FnOnce(tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>) -> Fut
|
| 927 |
+
+ Send
|
| 928 |
+
+ 'static,
|
| 929 |
+
Fut: std::future::Future<Output = ()> + Send + 'static,
|
| 930 |
+
{
|
| 931 |
+
let listener = TcpListener::bind("127.0.0.1:0")
|
| 932 |
+
.await
|
| 933 |
+
.expect("listener should bind");
|
| 934 |
+
let addr = listener.local_addr().expect("listener address");
|
| 935 |
+
tokio::spawn(async move {
|
| 936 |
+
let (stream, _) = listener.accept().await.expect("accept should succeed");
|
| 937 |
+
let websocket = accept_hdr_async(
|
| 938 |
+
stream,
|
| 939 |
+
move |request: &WebSocketRequest, response: WebSocketResponse| {
|
| 940 |
+
let provided_auth_token = request
|
| 941 |
+
.headers()
|
| 942 |
+
.get(AUTHORIZATION)
|
| 943 |
+
.and_then(|value| value.to_str().ok())
|
| 944 |
+
.map(str::to_owned);
|
| 945 |
+
let expected_auth_token = expected_auth_token
|
| 946 |
+
.as_ref()
|
| 947 |
+
.map(|token| format!("Bearer {token}"));
|
| 948 |
+
assert_eq!(provided_auth_token, expected_auth_token);
|
| 949 |
+
Ok(response)
|
| 950 |
+
},
|
| 951 |
+
)
|
| 952 |
+
.await
|
| 953 |
+
.expect("websocket upgrade should succeed");
|
| 954 |
+
handler(websocket).await;
|
| 955 |
+
});
|
| 956 |
+
format!("ws://{addr}")
|
| 957 |
+
}
|
| 958 |
+
|
| 959 |
+
async fn expect_remote_initialize<S>(websocket: &mut tokio_tungstenite::WebSocketStream<S>)
|
| 960 |
+
where
|
| 961 |
+
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
|
| 962 |
+
{
|
| 963 |
+
expect_remote_initialize_with_metadata(
|
| 964 |
+
websocket,
|
| 965 |
+
serde_json::json!({
|
| 966 |
+
"userAgent": "codex_cli_rs/9.8.7-test (Test OS; x86_64) rust",
|
| 967 |
+
"codexHome": "/server/.codex",
|
| 968 |
+
}),
|
| 969 |
+
)
|
| 970 |
+
.await;
|
| 971 |
+
}
|
| 972 |
+
|
| 973 |
+
async fn expect_remote_initialize_with_metadata<S>(
|
| 974 |
+
websocket: &mut tokio_tungstenite::WebSocketStream<S>,
|
| 975 |
+
metadata: serde_json::Value,
|
| 976 |
+
) where
|
| 977 |
+
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
|
| 978 |
+
{
|
| 979 |
+
let JSONRPCMessage::Request(request) = read_websocket_message(websocket).await else {
|
| 980 |
+
panic!("expected initialize request");
|
| 981 |
+
};
|
| 982 |
+
assert_eq!(request.method, "initialize");
|
| 983 |
+
write_websocket_message(
|
| 984 |
+
websocket,
|
| 985 |
+
JSONRPCMessage::Response(JSONRPCResponse {
|
| 986 |
+
id: request.id,
|
| 987 |
+
result: metadata,
|
| 988 |
+
}),
|
| 989 |
+
)
|
| 990 |
+
.await;
|
| 991 |
+
|
| 992 |
+
let JSONRPCMessage::Notification(notification) = read_websocket_message(websocket).await
|
| 993 |
+
else {
|
| 994 |
+
panic!("expected initialized notification");
|
| 995 |
+
};
|
| 996 |
+
assert_eq!(notification.method, "initialized");
|
| 997 |
+
}
|
| 998 |
+
|
| 999 |
+
async fn read_websocket_message<S>(
|
| 1000 |
+
websocket: &mut tokio_tungstenite::WebSocketStream<S>,
|
| 1001 |
+
) -> JSONRPCMessage
|
| 1002 |
+
where
|
| 1003 |
+
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
|
| 1004 |
+
{
|
| 1005 |
+
loop {
|
| 1006 |
+
let frame = websocket
|
| 1007 |
+
.next()
|
| 1008 |
+
.await
|
| 1009 |
+
.expect("frame should be available")
|
| 1010 |
+
.expect("frame should decode");
|
| 1011 |
+
match frame {
|
| 1012 |
+
Message::Text(text) => {
|
| 1013 |
+
return serde_json::from_str::<JSONRPCMessage>(&text)
|
| 1014 |
+
.expect("text frame should be valid JSON-RPC");
|
| 1015 |
+
}
|
| 1016 |
+
Message::Binary(_) | Message::Ping(_) | Message::Pong(_) | Message::Frame(_) => {
|
| 1017 |
+
continue;
|
| 1018 |
+
}
|
| 1019 |
+
Message::Close(_) => panic!("unexpected close frame"),
|
| 1020 |
+
}
|
| 1021 |
+
}
|
| 1022 |
+
}
|
| 1023 |
+
|
| 1024 |
+
async fn write_websocket_message<S>(
|
| 1025 |
+
websocket: &mut tokio_tungstenite::WebSocketStream<S>,
|
| 1026 |
+
message: JSONRPCMessage,
|
| 1027 |
+
) where
|
| 1028 |
+
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin,
|
| 1029 |
+
{
|
| 1030 |
+
websocket
|
| 1031 |
+
.send(Message::Text(
|
| 1032 |
+
serde_json::to_string(&message)
|
| 1033 |
+
.expect("message should serialize")
|
| 1034 |
+
.into(),
|
| 1035 |
+
))
|
| 1036 |
+
.await
|
| 1037 |
+
.expect("message should send");
|
| 1038 |
+
}
|
| 1039 |
+
|
| 1040 |
+
fn command_execution_output_delta_notification(delta: &str) -> ServerNotification {
|
| 1041 |
+
ServerNotification::CommandExecutionOutputDelta(
|
| 1042 |
+
codex_app_server_protocol::CommandExecutionOutputDeltaNotification {
|
| 1043 |
+
thread_id: "thread".to_string(),
|
| 1044 |
+
turn_id: "turn".to_string(),
|
| 1045 |
+
item_id: "item".to_string(),
|
| 1046 |
+
delta: delta.to_string(),
|
| 1047 |
+
},
|
| 1048 |
+
)
|
| 1049 |
+
}
|
| 1050 |
+
|
| 1051 |
+
fn agent_message_delta_notification(delta: &str) -> ServerNotification {
|
| 1052 |
+
ServerNotification::AgentMessageDelta(
|
| 1053 |
+
codex_app_server_protocol::AgentMessageDeltaNotification {
|
| 1054 |
+
thread_id: "thread".to_string(),
|
| 1055 |
+
turn_id: "turn".to_string(),
|
| 1056 |
+
item_id: "item".to_string(),
|
| 1057 |
+
delta: delta.to_string(),
|
| 1058 |
+
},
|
| 1059 |
+
)
|
| 1060 |
+
}
|
| 1061 |
+
|
| 1062 |
+
fn item_completed_notification(text: &str) -> ServerNotification {
|
| 1063 |
+
ServerNotification::ItemCompleted(codex_app_server_protocol::ItemCompletedNotification {
|
| 1064 |
+
thread_id: "thread".to_string(),
|
| 1065 |
+
turn_id: "turn".to_string(),
|
| 1066 |
+
completed_at_ms: 0,
|
| 1067 |
+
item: codex_app_server_protocol::ThreadItem::AgentMessage {
|
| 1068 |
+
id: "item".to_string(),
|
| 1069 |
+
text: text.to_string(),
|
| 1070 |
+
phase: None,
|
| 1071 |
+
memory_citation: None,
|
| 1072 |
+
delivery: None,
|
| 1073 |
+
questions: None,
|
| 1074 |
+
},
|
| 1075 |
+
})
|
| 1076 |
+
}
|
| 1077 |
+
|
| 1078 |
+
fn turn_completed_notification() -> ServerNotification {
|
| 1079 |
+
ServerNotification::TurnCompleted(codex_app_server_protocol::TurnCompletedNotification {
|
| 1080 |
+
thread_id: "thread".to_string(),
|
| 1081 |
+
turn: codex_app_server_protocol::Turn {
|
| 1082 |
+
id: "turn".to_string(),
|
| 1083 |
+
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
| 1084 |
+
items: Vec::new(),
|
| 1085 |
+
status: codex_app_server_protocol::TurnStatus::Completed,
|
| 1086 |
+
error: None,
|
| 1087 |
+
started_at: None,
|
| 1088 |
+
completed_at: Some(0),
|
| 1089 |
+
duration_ms: Some(1),
|
| 1090 |
+
},
|
| 1091 |
+
})
|
| 1092 |
+
}
|
| 1093 |
+
|
| 1094 |
+
fn test_remote_connect_args(websocket_url: String) -> RemoteAppServerConnectArgs {
|
| 1095 |
+
RemoteAppServerConnectArgs {
|
| 1096 |
+
endpoint: RemoteAppServerEndpoint::WebSocket {
|
| 1097 |
+
websocket_url,
|
| 1098 |
+
auth_token: None,
|
| 1099 |
+
},
|
| 1100 |
+
client_name: "codex-app-server-client-test".to_string(),
|
| 1101 |
+
client_version: "0.0.0-test".to_string(),
|
| 1102 |
+
experimental_api: true,
|
| 1103 |
+
mcp_server_openai_form_elicitation: false,
|
| 1104 |
+
opt_out_notification_methods: Vec::new(),
|
| 1105 |
+
channel_capacity: 8,
|
| 1106 |
+
}
|
| 1107 |
+
}
|
| 1108 |
+
|
| 1109 |
+
#[test]
|
| 1110 |
+
fn remote_initialize_params_forward_openai_form_capability() {
|
| 1111 |
+
let mut args = test_remote_connect_args("ws://localhost/rpc".to_string());
|
| 1112 |
+
args.mcp_server_openai_form_elicitation = true;
|
| 1113 |
+
|
| 1114 |
+
assert!(
|
| 1115 |
+
args.initialize_params()
|
| 1116 |
+
.capabilities
|
| 1117 |
+
.expect("initialize capabilities")
|
| 1118 |
+
.mcp_server_openai_form_elicitation
|
| 1119 |
+
);
|
| 1120 |
+
}
|
| 1121 |
+
|
| 1122 |
+
#[tokio::test]
|
| 1123 |
+
async fn typed_request_roundtrip_works() {
|
| 1124 |
+
let TestClient {
|
| 1125 |
+
_codex_home,
|
| 1126 |
+
client,
|
| 1127 |
+
} = start_test_client(SessionSource::Exec).await;
|
| 1128 |
+
let client = AppServerClient::InProcess(client);
|
| 1129 |
+
assert_eq!(
|
| 1130 |
+
(client.platform_family(), client.platform_os()),
|
| 1131 |
+
(Some(std::env::consts::FAMILY), Some(std::env::consts::OS))
|
| 1132 |
+
);
|
| 1133 |
+
let _response: ConfigRequirementsReadResponse = client
|
| 1134 |
+
.request_typed(ClientRequest::ConfigRequirementsRead {
|
| 1135 |
+
request_id: RequestId::Integer(1),
|
| 1136 |
+
params: None,
|
| 1137 |
+
})
|
| 1138 |
+
.await
|
| 1139 |
+
.expect("typed request should succeed");
|
| 1140 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 1141 |
+
}
|
| 1142 |
+
|
| 1143 |
+
#[tokio::test]
|
| 1144 |
+
async fn typed_request_reports_json_rpc_errors() {
|
| 1145 |
+
let client = start_test_client(SessionSource::Exec).await;
|
| 1146 |
+
let err = client
|
| 1147 |
+
.request_typed::<ConfigRequirementsReadResponse>(ClientRequest::ThreadRead {
|
| 1148 |
+
request_id: RequestId::Integer(99),
|
| 1149 |
+
params: codex_app_server_protocol::ThreadReadParams {
|
| 1150 |
+
thread_id: "missing-thread".to_string(),
|
| 1151 |
+
include_turns: false,
|
| 1152 |
+
},
|
| 1153 |
+
})
|
| 1154 |
+
.await
|
| 1155 |
+
.expect_err("missing thread should return a JSON-RPC error");
|
| 1156 |
+
assert!(
|
| 1157 |
+
err.to_string().starts_with("thread/read failed:"),
|
| 1158 |
+
"expected method-qualified JSON-RPC failure message"
|
| 1159 |
+
);
|
| 1160 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 1161 |
+
}
|
| 1162 |
+
|
| 1163 |
+
#[tokio::test]
|
| 1164 |
+
async fn caller_provided_session_source_is_applied() {
|
| 1165 |
+
for (session_source, expected_source) in [
|
| 1166 |
+
(SessionSource::Exec, ApiSessionSource::Exec),
|
| 1167 |
+
(SessionSource::Cli, ApiSessionSource::Cli),
|
| 1168 |
+
] {
|
| 1169 |
+
let client = start_test_client(session_source).await;
|
| 1170 |
+
let parsed: ThreadStartResponse = client
|
| 1171 |
+
.request_typed(ClientRequest::ThreadStart {
|
| 1172 |
+
request_id: RequestId::Integer(2),
|
| 1173 |
+
params: ThreadStartParams {
|
| 1174 |
+
ephemeral: Some(true),
|
| 1175 |
+
..ThreadStartParams::default()
|
| 1176 |
+
},
|
| 1177 |
+
})
|
| 1178 |
+
.await
|
| 1179 |
+
.expect("thread/start should succeed");
|
| 1180 |
+
assert_eq!(parsed.thread.source, expected_source);
|
| 1181 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 1182 |
+
}
|
| 1183 |
+
}
|
| 1184 |
+
|
| 1185 |
+
#[tokio::test]
|
| 1186 |
+
async fn threads_started_via_app_server_are_visible_through_typed_requests() {
|
| 1187 |
+
let client = start_test_client(SessionSource::Cli).await;
|
| 1188 |
+
|
| 1189 |
+
let response: ThreadStartResponse = client
|
| 1190 |
+
.request_typed(ClientRequest::ThreadStart {
|
| 1191 |
+
request_id: RequestId::Integer(3),
|
| 1192 |
+
params: ThreadStartParams {
|
| 1193 |
+
ephemeral: Some(true),
|
| 1194 |
+
..ThreadStartParams::default()
|
| 1195 |
+
},
|
| 1196 |
+
})
|
| 1197 |
+
.await
|
| 1198 |
+
.expect("thread/start should succeed");
|
| 1199 |
+
let read = client
|
| 1200 |
+
.request_typed::<codex_app_server_protocol::ThreadReadResponse>(
|
| 1201 |
+
ClientRequest::ThreadRead {
|
| 1202 |
+
request_id: RequestId::Integer(4),
|
| 1203 |
+
params: codex_app_server_protocol::ThreadReadParams {
|
| 1204 |
+
thread_id: response.thread.id.clone(),
|
| 1205 |
+
include_turns: false,
|
| 1206 |
+
},
|
| 1207 |
+
},
|
| 1208 |
+
)
|
| 1209 |
+
.await
|
| 1210 |
+
.expect("thread/read should return the newly started thread");
|
| 1211 |
+
assert_eq!(read.thread.id, response.thread.id);
|
| 1212 |
+
|
| 1213 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 1214 |
+
}
|
| 1215 |
+
|
| 1216 |
+
#[tokio::test]
|
| 1217 |
+
async fn tiny_channel_capacity_still_supports_request_roundtrip() {
|
| 1218 |
+
let client =
|
| 1219 |
+
start_test_client_with_capacity(SessionSource::Exec, /*channel_capacity*/ 1).await;
|
| 1220 |
+
let _response: ConfigRequirementsReadResponse = client
|
| 1221 |
+
.request_typed(ClientRequest::ConfigRequirementsRead {
|
| 1222 |
+
request_id: RequestId::Integer(1),
|
| 1223 |
+
params: None,
|
| 1224 |
+
})
|
| 1225 |
+
.await
|
| 1226 |
+
.expect("typed request should succeed");
|
| 1227 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 1228 |
+
}
|
| 1229 |
+
|
| 1230 |
+
#[tokio::test]
|
| 1231 |
+
async fn unread_lossless_notifications_do_not_block_in_process_requests() {
|
| 1232 |
+
let mut client =
|
| 1233 |
+
start_test_client_with_capacity(SessionSource::Cli, /*channel_capacity*/ 1).await;
|
| 1234 |
+
let thread: ThreadStartResponse = client
|
| 1235 |
+
.request_typed(ClientRequest::ThreadStart {
|
| 1236 |
+
request_id: RequestId::Integer(1),
|
| 1237 |
+
params: ThreadStartParams {
|
| 1238 |
+
ephemeral: Some(true),
|
| 1239 |
+
personality: Some(Personality::None),
|
| 1240 |
+
..ThreadStartParams::default()
|
| 1241 |
+
},
|
| 1242 |
+
})
|
| 1243 |
+
.await
|
| 1244 |
+
.expect("thread/start should succeed");
|
| 1245 |
+
let request_handle = client.request_handle();
|
| 1246 |
+
|
| 1247 |
+
timeout(Duration::from_secs(2), async {
|
| 1248 |
+
for (index, personality) in [
|
| 1249 |
+
Personality::Friendly,
|
| 1250 |
+
Personality::Pragmatic,
|
| 1251 |
+
Personality::Friendly,
|
| 1252 |
+
Personality::Pragmatic,
|
| 1253 |
+
]
|
| 1254 |
+
.into_iter()
|
| 1255 |
+
.enumerate()
|
| 1256 |
+
{
|
| 1257 |
+
let _: ThreadSettingsUpdateResponse = request_handle
|
| 1258 |
+
.request_typed(ClientRequest::ThreadSettingsUpdate {
|
| 1259 |
+
request_id: RequestId::Integer((index + 2) as i64),
|
| 1260 |
+
params: ThreadSettingsUpdateParams {
|
| 1261 |
+
thread_id: thread.thread.id.clone(),
|
| 1262 |
+
personality: Some(personality),
|
| 1263 |
+
..ThreadSettingsUpdateParams::default()
|
| 1264 |
+
},
|
| 1265 |
+
})
|
| 1266 |
+
.await
|
| 1267 |
+
.expect("thread/settings/update should succeed");
|
| 1268 |
+
}
|
| 1269 |
+
|
| 1270 |
+
let _: ConfigRequirementsReadResponse = request_handle
|
| 1271 |
+
.request_typed(ClientRequest::ConfigRequirementsRead {
|
| 1272 |
+
request_id: RequestId::Integer(10),
|
| 1273 |
+
params: None,
|
| 1274 |
+
})
|
| 1275 |
+
.await
|
| 1276 |
+
.expect("configuration request should succeed");
|
| 1277 |
+
})
|
| 1278 |
+
.await
|
| 1279 |
+
.expect("unread lossless notifications must not block app-server requests");
|
| 1280 |
+
|
| 1281 |
+
let mut personalities = Vec::new();
|
| 1282 |
+
timeout(Duration::from_secs(2), async {
|
| 1283 |
+
while personalities.len() < 4 {
|
| 1284 |
+
if let Some(InProcessServerEvent::ServerNotification(notification)) =
|
| 1285 |
+
client.client.next_event().await
|
| 1286 |
+
&& let ServerNotification::ThreadSettingsUpdated(notification) =
|
| 1287 |
+
notification.as_ref()
|
| 1288 |
+
{
|
| 1289 |
+
personalities.push(notification.thread_settings.personality);
|
| 1290 |
+
}
|
| 1291 |
+
}
|
| 1292 |
+
})
|
| 1293 |
+
.await
|
| 1294 |
+
.expect("queued settings notifications should remain readable");
|
| 1295 |
+
assert_eq!(
|
| 1296 |
+
personalities,
|
| 1297 |
+
vec![
|
| 1298 |
+
Some(Personality::Friendly),
|
| 1299 |
+
Some(Personality::Pragmatic),
|
| 1300 |
+
Some(Personality::Friendly),
|
| 1301 |
+
Some(Personality::Pragmatic),
|
| 1302 |
+
]
|
| 1303 |
+
);
|
| 1304 |
+
|
| 1305 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 1306 |
+
}
|
| 1307 |
+
|
| 1308 |
+
#[tokio::test]
|
| 1309 |
+
async fn remote_platform_metadata_preserves_reported_and_missing_values() {
|
| 1310 |
+
for (family, os) in [
|
| 1311 |
+
(Some("windows"), Some("windows")),
|
| 1312 |
+
(Some("unix"), Some("linux")),
|
| 1313 |
+
(Some("future-family"), Some("future-os")),
|
| 1314 |
+
(Some("unix"), None),
|
| 1315 |
+
(None, Some("linux")),
|
| 1316 |
+
(None, None),
|
| 1317 |
+
] {
|
| 1318 |
+
let websocket_url = start_test_remote_server(move |mut websocket| async move {
|
| 1319 |
+
let mut metadata = serde_json::json!({});
|
| 1320 |
+
if let Some(family) = family {
|
| 1321 |
+
metadata["platformFamily"] = family.into();
|
| 1322 |
+
}
|
| 1323 |
+
if let Some(os) = os {
|
| 1324 |
+
metadata["platformOs"] = os.into();
|
| 1325 |
+
}
|
| 1326 |
+
expect_remote_initialize_with_metadata(&mut websocket, metadata).await;
|
| 1327 |
+
websocket.close(None).await.expect("close should succeed");
|
| 1328 |
+
})
|
| 1329 |
+
.await;
|
| 1330 |
+
let client = AppServerClient::Remote(
|
| 1331 |
+
RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
|
| 1332 |
+
.await
|
| 1333 |
+
.expect("remote client should connect"),
|
| 1334 |
+
);
|
| 1335 |
+
assert_eq!(
|
| 1336 |
+
(client.platform_family(), client.platform_os()),
|
| 1337 |
+
(family, os)
|
| 1338 |
+
);
|
| 1339 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 1340 |
+
}
|
| 1341 |
+
}
|
| 1342 |
+
|
| 1343 |
+
#[tokio::test]
|
| 1344 |
+
async fn remote_typed_request_roundtrip_works() {
|
| 1345 |
+
let websocket_url = start_test_remote_server(|mut websocket| async move {
|
| 1346 |
+
expect_remote_initialize(&mut websocket).await;
|
| 1347 |
+
let JSONRPCMessage::Request(request) = read_websocket_message(&mut websocket).await
|
| 1348 |
+
else {
|
| 1349 |
+
panic!("expected account/read request");
|
| 1350 |
+
};
|
| 1351 |
+
assert_eq!(request.method, "account/read");
|
| 1352 |
+
write_websocket_message(
|
| 1353 |
+
&mut websocket,
|
| 1354 |
+
JSONRPCMessage::Response(JSONRPCResponse {
|
| 1355 |
+
id: request.id,
|
| 1356 |
+
result: serde_json::to_value(GetAccountResponse {
|
| 1357 |
+
workspace_routing: None,
|
| 1358 |
+
account: None,
|
| 1359 |
+
requires_openai_auth: false,
|
| 1360 |
+
})
|
| 1361 |
+
.expect("response should serialize"),
|
| 1362 |
+
}),
|
| 1363 |
+
)
|
| 1364 |
+
.await;
|
| 1365 |
+
websocket.close(None).await.expect("close should succeed");
|
| 1366 |
+
})
|
| 1367 |
+
.await;
|
| 1368 |
+
let client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
|
| 1369 |
+
.await
|
| 1370 |
+
.expect("remote client should connect");
|
| 1371 |
+
|
| 1372 |
+
assert_eq!(client.server_version(), Some("9.8.7-test"));
|
| 1373 |
+
assert_eq!(client.codex_home(), Some("/server/.codex"));
|
| 1374 |
+
let response: GetAccountResponse = client
|
| 1375 |
+
.request_typed(ClientRequest::GetAccount {
|
| 1376 |
+
request_id: RequestId::Integer(1),
|
| 1377 |
+
params: codex_app_server_protocol::GetAccountParams {
|
| 1378 |
+
refresh_token: false,
|
| 1379 |
+
},
|
| 1380 |
+
})
|
| 1381 |
+
.await
|
| 1382 |
+
.expect("typed request should succeed");
|
| 1383 |
+
assert_eq!(response.account, None);
|
| 1384 |
+
|
| 1385 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 1386 |
+
}
|
| 1387 |
+
|
| 1388 |
+
#[tokio::test]
|
| 1389 |
+
async fn remote_unix_socket_typed_request_roundtrip_works() {
|
| 1390 |
+
let socket_dir = TempDir::new().expect("socket dir");
|
| 1391 |
+
let socket_path = AbsolutePathBuf::from_absolute_path(socket_dir.path().join("codex.sock"))
|
| 1392 |
+
.expect("socket path should resolve");
|
| 1393 |
+
let mut listener = UnixListener::bind(socket_path.as_path())
|
| 1394 |
+
.await
|
| 1395 |
+
.expect("listener should bind");
|
| 1396 |
+
tokio::spawn(async move {
|
| 1397 |
+
let stream = listener.accept().await.expect("accept should succeed");
|
| 1398 |
+
let mut websocket = accept_async(stream)
|
| 1399 |
+
.await
|
| 1400 |
+
.expect("websocket upgrade should succeed");
|
| 1401 |
+
expect_remote_initialize(&mut websocket).await;
|
| 1402 |
+
let JSONRPCMessage::Request(request) = read_websocket_message(&mut websocket).await
|
| 1403 |
+
else {
|
| 1404 |
+
panic!("expected account/read request");
|
| 1405 |
+
};
|
| 1406 |
+
assert_eq!(request.method, "account/read");
|
| 1407 |
+
write_websocket_message(
|
| 1408 |
+
&mut websocket,
|
| 1409 |
+
JSONRPCMessage::Response(JSONRPCResponse {
|
| 1410 |
+
id: request.id,
|
| 1411 |
+
result: serde_json::to_value(GetAccountResponse {
|
| 1412 |
+
workspace_routing: None,
|
| 1413 |
+
account: None,
|
| 1414 |
+
requires_openai_auth: false,
|
| 1415 |
+
})
|
| 1416 |
+
.expect("response should serialize"),
|
| 1417 |
+
}),
|
| 1418 |
+
)
|
| 1419 |
+
.await;
|
| 1420 |
+
websocket.close(None).await.expect("close should succeed");
|
| 1421 |
+
});
|
| 1422 |
+
let client = RemoteAppServerClient::connect(RemoteAppServerConnectArgs {
|
| 1423 |
+
endpoint: RemoteAppServerEndpoint::UnixSocket { socket_path },
|
| 1424 |
+
client_name: "codex-app-server-client-test".to_string(),
|
| 1425 |
+
client_version: "0.0.0-test".to_string(),
|
| 1426 |
+
experimental_api: true,
|
| 1427 |
+
mcp_server_openai_form_elicitation: false,
|
| 1428 |
+
opt_out_notification_methods: Vec::new(),
|
| 1429 |
+
channel_capacity: 8,
|
| 1430 |
+
})
|
| 1431 |
+
.await
|
| 1432 |
+
.expect("remote client should connect");
|
| 1433 |
+
|
| 1434 |
+
let response: GetAccountResponse = client
|
| 1435 |
+
.request_typed(ClientRequest::GetAccount {
|
| 1436 |
+
request_id: RequestId::Integer(1),
|
| 1437 |
+
params: codex_app_server_protocol::GetAccountParams {
|
| 1438 |
+
refresh_token: false,
|
| 1439 |
+
},
|
| 1440 |
+
})
|
| 1441 |
+
.await
|
| 1442 |
+
.expect("typed request should succeed");
|
| 1443 |
+
assert_eq!(response.account, None);
|
| 1444 |
+
|
| 1445 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 1446 |
+
}
|
| 1447 |
+
|
| 1448 |
+
#[tokio::test]
|
| 1449 |
+
async fn remote_typed_request_accepts_large_single_frame_response() {
|
| 1450 |
+
let padding = "x".repeat((17 << 20) + 1024);
|
| 1451 |
+
let websocket_url = start_test_remote_server(move |mut websocket| async move {
|
| 1452 |
+
expect_remote_initialize(&mut websocket).await;
|
| 1453 |
+
let JSONRPCMessage::Request(request) = read_websocket_message(&mut websocket).await
|
| 1454 |
+
else {
|
| 1455 |
+
panic!("expected account/read request");
|
| 1456 |
+
};
|
| 1457 |
+
assert_eq!(request.method, "account/read");
|
| 1458 |
+
write_websocket_message(
|
| 1459 |
+
&mut websocket,
|
| 1460 |
+
JSONRPCMessage::Response(JSONRPCResponse {
|
| 1461 |
+
id: request.id,
|
| 1462 |
+
result: serde_json::json!({
|
| 1463 |
+
"account": null,
|
| 1464 |
+
"requiresOpenaiAuth": false,
|
| 1465 |
+
"padding": padding,
|
| 1466 |
+
}),
|
| 1467 |
+
}),
|
| 1468 |
+
)
|
| 1469 |
+
.await;
|
| 1470 |
+
websocket.close(None).await.expect("close should succeed");
|
| 1471 |
+
})
|
| 1472 |
+
.await;
|
| 1473 |
+
let client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
|
| 1474 |
+
.await
|
| 1475 |
+
.expect("remote client should connect");
|
| 1476 |
+
|
| 1477 |
+
let response: GetAccountResponse = client
|
| 1478 |
+
.request_typed(ClientRequest::GetAccount {
|
| 1479 |
+
request_id: RequestId::Integer(1),
|
| 1480 |
+
params: codex_app_server_protocol::GetAccountParams {
|
| 1481 |
+
refresh_token: false,
|
| 1482 |
+
},
|
| 1483 |
+
})
|
| 1484 |
+
.await
|
| 1485 |
+
.expect("large typed request should succeed");
|
| 1486 |
+
assert_eq!(
|
| 1487 |
+
response,
|
| 1488 |
+
GetAccountResponse {
|
| 1489 |
+
workspace_routing: None,
|
| 1490 |
+
account: None,
|
| 1491 |
+
requires_openai_auth: false,
|
| 1492 |
+
}
|
| 1493 |
+
);
|
| 1494 |
+
|
| 1495 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 1496 |
+
}
|
| 1497 |
+
|
| 1498 |
+
#[tokio::test]
|
| 1499 |
+
async fn remote_connect_includes_auth_header_when_configured() {
|
| 1500 |
+
let auth_token = "remote-bearer-token".to_string();
|
| 1501 |
+
let websocket_url = start_test_remote_server_with_auth(
|
| 1502 |
+
Some(auth_token.clone()),
|
| 1503 |
+
|mut websocket| async move {
|
| 1504 |
+
expect_remote_initialize(&mut websocket).await;
|
| 1505 |
+
websocket.close(None).await.expect("close should succeed");
|
| 1506 |
+
},
|
| 1507 |
+
)
|
| 1508 |
+
.await;
|
| 1509 |
+
let client = RemoteAppServerClient::connect(RemoteAppServerConnectArgs {
|
| 1510 |
+
endpoint: RemoteAppServerEndpoint::WebSocket {
|
| 1511 |
+
websocket_url,
|
| 1512 |
+
auth_token: Some(auth_token),
|
| 1513 |
+
},
|
| 1514 |
+
client_name: "codex-app-server-client-test".to_string(),
|
| 1515 |
+
client_version: "0.0.0-test".to_string(),
|
| 1516 |
+
experimental_api: true,
|
| 1517 |
+
mcp_server_openai_form_elicitation: false,
|
| 1518 |
+
opt_out_notification_methods: Vec::new(),
|
| 1519 |
+
channel_capacity: 8,
|
| 1520 |
+
})
|
| 1521 |
+
.await
|
| 1522 |
+
.expect("remote client should connect");
|
| 1523 |
+
|
| 1524 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 1525 |
+
}
|
| 1526 |
+
|
| 1527 |
+
#[tokio::test]
|
| 1528 |
+
async fn remote_connect_rejects_non_loopback_ws_when_auth_configured() {
|
| 1529 |
+
let result = RemoteAppServerClient::connect(RemoteAppServerConnectArgs {
|
| 1530 |
+
endpoint: RemoteAppServerEndpoint::WebSocket {
|
| 1531 |
+
websocket_url: "ws://example.com:4500".to_string(),
|
| 1532 |
+
auth_token: Some("remote-bearer-token".to_string()),
|
| 1533 |
+
},
|
| 1534 |
+
client_name: "codex-app-server-client-test".to_string(),
|
| 1535 |
+
client_version: "0.0.0-test".to_string(),
|
| 1536 |
+
experimental_api: true,
|
| 1537 |
+
mcp_server_openai_form_elicitation: false,
|
| 1538 |
+
opt_out_notification_methods: Vec::new(),
|
| 1539 |
+
channel_capacity: 8,
|
| 1540 |
+
})
|
| 1541 |
+
.await;
|
| 1542 |
+
let err = match result {
|
| 1543 |
+
Ok(_) => panic!("non-loopback ws should be rejected before connect"),
|
| 1544 |
+
Err(err) => err,
|
| 1545 |
+
};
|
| 1546 |
+
assert_eq!(err.kind(), ErrorKind::InvalidInput);
|
| 1547 |
+
assert!(
|
| 1548 |
+
err.to_string()
|
| 1549 |
+
.contains("remote auth tokens require `wss://` or loopback `ws://` URLs")
|
| 1550 |
+
);
|
| 1551 |
+
}
|
| 1552 |
+
|
| 1553 |
+
#[test]
|
| 1554 |
+
fn remote_auth_token_transport_policy_allows_wss_and_loopback_ws() {
|
| 1555 |
+
assert!(crate::remote::websocket_url_supports_auth_token(
|
| 1556 |
+
&url::Url::parse("wss://example.com:443").expect("wss URL should parse")
|
| 1557 |
+
));
|
| 1558 |
+
assert!(crate::remote::websocket_url_supports_auth_token(
|
| 1559 |
+
&url::Url::parse("ws://127.0.0.1:4500").expect("loopback ws URL should parse")
|
| 1560 |
+
));
|
| 1561 |
+
assert!(!crate::remote::websocket_url_supports_auth_token(
|
| 1562 |
+
&url::Url::parse("ws://example.com:4500").expect("non-loopback ws URL should parse")
|
| 1563 |
+
));
|
| 1564 |
+
}
|
| 1565 |
+
|
| 1566 |
+
#[tokio::test]
|
| 1567 |
+
async fn remote_duplicate_request_id_keeps_original_waiter() {
|
| 1568 |
+
let (first_request_seen_tx, first_request_seen_rx) = tokio::sync::oneshot::channel();
|
| 1569 |
+
let websocket_url = start_test_remote_server(|mut websocket| async move {
|
| 1570 |
+
expect_remote_initialize(&mut websocket).await;
|
| 1571 |
+
let JSONRPCMessage::Request(request) = read_websocket_message(&mut websocket).await
|
| 1572 |
+
else {
|
| 1573 |
+
panic!("expected account/read request");
|
| 1574 |
+
};
|
| 1575 |
+
assert_eq!(request.method, "account/read");
|
| 1576 |
+
first_request_seen_tx
|
| 1577 |
+
.send(request.id.clone())
|
| 1578 |
+
.expect("request id should send");
|
| 1579 |
+
assert!(
|
| 1580 |
+
timeout(
|
| 1581 |
+
Duration::from_millis(100),
|
| 1582 |
+
read_websocket_message(&mut websocket)
|
| 1583 |
+
)
|
| 1584 |
+
.await
|
| 1585 |
+
.is_err(),
|
| 1586 |
+
"duplicate request should not be forwarded to the server"
|
| 1587 |
+
);
|
| 1588 |
+
write_websocket_message(
|
| 1589 |
+
&mut websocket,
|
| 1590 |
+
JSONRPCMessage::Response(JSONRPCResponse {
|
| 1591 |
+
id: request.id,
|
| 1592 |
+
result: serde_json::to_value(GetAccountResponse {
|
| 1593 |
+
workspace_routing: None,
|
| 1594 |
+
account: None,
|
| 1595 |
+
requires_openai_auth: false,
|
| 1596 |
+
})
|
| 1597 |
+
.expect("response should serialize"),
|
| 1598 |
+
}),
|
| 1599 |
+
)
|
| 1600 |
+
.await;
|
| 1601 |
+
let _ = websocket.next().await;
|
| 1602 |
+
})
|
| 1603 |
+
.await;
|
| 1604 |
+
let client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
|
| 1605 |
+
.await
|
| 1606 |
+
.expect("remote client should connect");
|
| 1607 |
+
let first_request_handle = client.request_handle();
|
| 1608 |
+
let second_request_handle = first_request_handle.clone();
|
| 1609 |
+
|
| 1610 |
+
let first_request = tokio::spawn(async move {
|
| 1611 |
+
first_request_handle
|
| 1612 |
+
.request_typed::<GetAccountResponse>(ClientRequest::GetAccount {
|
| 1613 |
+
request_id: RequestId::Integer(1),
|
| 1614 |
+
params: codex_app_server_protocol::GetAccountParams {
|
| 1615 |
+
refresh_token: false,
|
| 1616 |
+
},
|
| 1617 |
+
})
|
| 1618 |
+
.await
|
| 1619 |
+
});
|
| 1620 |
+
|
| 1621 |
+
let first_request_id = first_request_seen_rx
|
| 1622 |
+
.await
|
| 1623 |
+
.expect("server should observe the first request");
|
| 1624 |
+
assert_eq!(first_request_id, RequestId::Integer(1));
|
| 1625 |
+
|
| 1626 |
+
let second_err = second_request_handle
|
| 1627 |
+
.request_typed::<GetAccountResponse>(ClientRequest::GetAccount {
|
| 1628 |
+
request_id: RequestId::Integer(1),
|
| 1629 |
+
params: codex_app_server_protocol::GetAccountParams {
|
| 1630 |
+
refresh_token: false,
|
| 1631 |
+
},
|
| 1632 |
+
})
|
| 1633 |
+
.await
|
| 1634 |
+
.expect_err("duplicate request id should be rejected");
|
| 1635 |
+
assert_eq!(
|
| 1636 |
+
second_err.to_string(),
|
| 1637 |
+
"account/read transport error: duplicate remote app-server request id `1`"
|
| 1638 |
+
);
|
| 1639 |
+
|
| 1640 |
+
let first_response = first_request
|
| 1641 |
+
.await
|
| 1642 |
+
.expect("first request task should join")
|
| 1643 |
+
.expect("first request should succeed");
|
| 1644 |
+
assert_eq!(
|
| 1645 |
+
first_response,
|
| 1646 |
+
GetAccountResponse {
|
| 1647 |
+
workspace_routing: None,
|
| 1648 |
+
account: None,
|
| 1649 |
+
requires_openai_auth: false,
|
| 1650 |
+
}
|
| 1651 |
+
);
|
| 1652 |
+
|
| 1653 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 1654 |
+
}
|
| 1655 |
+
|
| 1656 |
+
#[tokio::test]
|
| 1657 |
+
async fn remote_notifications_arrive_over_websocket() {
|
| 1658 |
+
let websocket_url = start_test_remote_server(|mut websocket| async move {
|
| 1659 |
+
expect_remote_initialize(&mut websocket).await;
|
| 1660 |
+
write_websocket_message(
|
| 1661 |
+
&mut websocket,
|
| 1662 |
+
JSONRPCMessage::Notification(
|
| 1663 |
+
serde_json::from_value(
|
| 1664 |
+
serde_json::to_value(ServerNotification::AccountUpdated(
|
| 1665 |
+
AccountUpdatedNotification {
|
| 1666 |
+
auth_mode: None,
|
| 1667 |
+
plan_type: None,
|
| 1668 |
+
},
|
| 1669 |
+
))
|
| 1670 |
+
.expect("notification should serialize"),
|
| 1671 |
+
)
|
| 1672 |
+
.expect("notification should convert to JSON-RPC"),
|
| 1673 |
+
),
|
| 1674 |
+
)
|
| 1675 |
+
.await;
|
| 1676 |
+
})
|
| 1677 |
+
.await;
|
| 1678 |
+
let mut client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
|
| 1679 |
+
.await
|
| 1680 |
+
.expect("remote client should connect");
|
| 1681 |
+
|
| 1682 |
+
let event = client.next_event().await.expect("event should arrive");
|
| 1683 |
+
assert!(matches!(
|
| 1684 |
+
event,
|
| 1685 |
+
AppServerEvent::ServerNotification(notification)
|
| 1686 |
+
if matches!(notification.as_ref(), ServerNotification::AccountUpdated(_))
|
| 1687 |
+
));
|
| 1688 |
+
|
| 1689 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 1690 |
+
}
|
| 1691 |
+
|
| 1692 |
+
#[tokio::test]
|
| 1693 |
+
async fn remote_backpressure_preserves_transcript_notifications() {
|
| 1694 |
+
let (done_tx, done_rx) = tokio::sync::oneshot::channel();
|
| 1695 |
+
let websocket_url = start_test_remote_server(|mut websocket| async move {
|
| 1696 |
+
expect_remote_initialize(&mut websocket).await;
|
| 1697 |
+
for notification in [
|
| 1698 |
+
command_execution_output_delta_notification("stdout-1"),
|
| 1699 |
+
command_execution_output_delta_notification("stdout-2"),
|
| 1700 |
+
agent_message_delta_notification("hello"),
|
| 1701 |
+
item_completed_notification("hello"),
|
| 1702 |
+
turn_completed_notification(),
|
| 1703 |
+
] {
|
| 1704 |
+
write_websocket_message(
|
| 1705 |
+
&mut websocket,
|
| 1706 |
+
JSONRPCMessage::Notification(
|
| 1707 |
+
serde_json::from_value(
|
| 1708 |
+
serde_json::to_value(notification)
|
| 1709 |
+
.expect("notification should serialize"),
|
| 1710 |
+
)
|
| 1711 |
+
.expect("notification should convert to JSON-RPC"),
|
| 1712 |
+
),
|
| 1713 |
+
)
|
| 1714 |
+
.await;
|
| 1715 |
+
}
|
| 1716 |
+
let _ = done_rx.await;
|
| 1717 |
+
})
|
| 1718 |
+
.await;
|
| 1719 |
+
let mut client = RemoteAppServerClient::connect(RemoteAppServerConnectArgs {
|
| 1720 |
+
channel_capacity: 1,
|
| 1721 |
+
..test_remote_connect_args(websocket_url)
|
| 1722 |
+
})
|
| 1723 |
+
.await
|
| 1724 |
+
.expect("remote client should connect");
|
| 1725 |
+
|
| 1726 |
+
let first_event = timeout(Duration::from_secs(2), client.next_event())
|
| 1727 |
+
.await
|
| 1728 |
+
.expect("first event should arrive before timeout")
|
| 1729 |
+
.expect("event stream should stay open");
|
| 1730 |
+
assert!(matches!(
|
| 1731 |
+
first_event,
|
| 1732 |
+
AppServerEvent::ServerNotification(notification)
|
| 1733 |
+
if matches!(
|
| 1734 |
+
notification.as_ref(),
|
| 1735 |
+
ServerNotification::CommandExecutionOutputDelta(notification)
|
| 1736 |
+
if notification.delta == "stdout-1"
|
| 1737 |
+
)
|
| 1738 |
+
));
|
| 1739 |
+
|
| 1740 |
+
let mut remaining_events = Vec::new();
|
| 1741 |
+
for _ in 0..4 {
|
| 1742 |
+
remaining_events.push(
|
| 1743 |
+
timeout(Duration::from_secs(2), client.next_event())
|
| 1744 |
+
.await
|
| 1745 |
+
.expect("event should arrive before timeout")
|
| 1746 |
+
.expect("event stream should stay open"),
|
| 1747 |
+
);
|
| 1748 |
+
}
|
| 1749 |
+
|
| 1750 |
+
let mut transcript_event_names = Vec::new();
|
| 1751 |
+
for event in &remaining_events {
|
| 1752 |
+
match event {
|
| 1753 |
+
AppServerEvent::Lagged { skipped: 1 } => {}
|
| 1754 |
+
AppServerEvent::ServerNotification(notification) => match notification.as_ref() {
|
| 1755 |
+
ServerNotification::CommandExecutionOutputDelta(notification)
|
| 1756 |
+
if notification.delta == "stdout-2" => {}
|
| 1757 |
+
ServerNotification::AgentMessageDelta(notification)
|
| 1758 |
+
if notification.delta == "hello" =>
|
| 1759 |
+
{
|
| 1760 |
+
transcript_event_names.push("agent_message_delta");
|
| 1761 |
+
}
|
| 1762 |
+
ServerNotification::ItemCompleted(notification)
|
| 1763 |
+
if matches!(
|
| 1764 |
+
¬ification.item,
|
| 1765 |
+
codex_app_server_protocol::ThreadItem::AgentMessage { text, .. }
|
| 1766 |
+
if text == "hello"
|
| 1767 |
+
) =>
|
| 1768 |
+
{
|
| 1769 |
+
transcript_event_names.push("item_completed");
|
| 1770 |
+
}
|
| 1771 |
+
ServerNotification::TurnCompleted(notification)
|
| 1772 |
+
if notification.turn.status
|
| 1773 |
+
== codex_app_server_protocol::TurnStatus::Completed =>
|
| 1774 |
+
{
|
| 1775 |
+
transcript_event_names.push("turn_completed");
|
| 1776 |
+
}
|
| 1777 |
+
_ => panic!("unexpected remaining event: {event:?}"),
|
| 1778 |
+
},
|
| 1779 |
+
_ => panic!("unexpected remaining event: {event:?}"),
|
| 1780 |
+
}
|
| 1781 |
+
}
|
| 1782 |
+
assert_eq!(
|
| 1783 |
+
transcript_event_names,
|
| 1784 |
+
vec!["agent_message_delta", "item_completed", "turn_completed"]
|
| 1785 |
+
);
|
| 1786 |
+
|
| 1787 |
+
done_tx
|
| 1788 |
+
.send(())
|
| 1789 |
+
.expect("server completion signal should send");
|
| 1790 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 1791 |
+
}
|
| 1792 |
+
|
| 1793 |
+
#[tokio::test]
|
| 1794 |
+
async fn remote_server_request_resolution_roundtrip_works() {
|
| 1795 |
+
let websocket_url = start_test_remote_server(|mut websocket| async move {
|
| 1796 |
+
expect_remote_initialize(&mut websocket).await;
|
| 1797 |
+
let request_id = RequestId::String("srv-1".to_string());
|
| 1798 |
+
let server_request = JSONRPCRequest {
|
| 1799 |
+
id: request_id.clone(),
|
| 1800 |
+
method: "item/tool/requestUserInput".to_string(),
|
| 1801 |
+
params: Some(
|
| 1802 |
+
serde_json::to_value(ToolRequestUserInputParams {
|
| 1803 |
+
thread_id: "thread-1".to_string(),
|
| 1804 |
+
turn_id: "turn-1".to_string(),
|
| 1805 |
+
item_id: "call-1".to_string(),
|
| 1806 |
+
questions: vec![ToolRequestUserInputQuestion {
|
| 1807 |
+
id: "question-1".to_string(),
|
| 1808 |
+
header: "Mode".to_string(),
|
| 1809 |
+
question: "Pick one".to_string(),
|
| 1810 |
+
is_other: false,
|
| 1811 |
+
is_secret: false,
|
| 1812 |
+
options: Some(vec![]),
|
| 1813 |
+
}],
|
| 1814 |
+
is_blocking: true,
|
| 1815 |
+
auto_resolution_ms: None,
|
| 1816 |
+
})
|
| 1817 |
+
.expect("params should serialize"),
|
| 1818 |
+
),
|
| 1819 |
+
trace: None,
|
| 1820 |
+
};
|
| 1821 |
+
write_websocket_message(&mut websocket, JSONRPCMessage::Request(server_request)).await;
|
| 1822 |
+
|
| 1823 |
+
let JSONRPCMessage::Response(response) = read_websocket_message(&mut websocket).await
|
| 1824 |
+
else {
|
| 1825 |
+
panic!("expected server request response");
|
| 1826 |
+
};
|
| 1827 |
+
assert_eq!(response.id, request_id);
|
| 1828 |
+
})
|
| 1829 |
+
.await;
|
| 1830 |
+
let mut client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
|
| 1831 |
+
.await
|
| 1832 |
+
.expect("remote client should connect");
|
| 1833 |
+
|
| 1834 |
+
let AppServerEvent::ServerRequest(request) = client
|
| 1835 |
+
.next_event()
|
| 1836 |
+
.await
|
| 1837 |
+
.expect("request event should arrive")
|
| 1838 |
+
else {
|
| 1839 |
+
panic!("expected server request event");
|
| 1840 |
+
};
|
| 1841 |
+
client
|
| 1842 |
+
.resolve_server_request(request.id().clone(), serde_json::json!({}))
|
| 1843 |
+
.await
|
| 1844 |
+
.expect("server request should resolve");
|
| 1845 |
+
|
| 1846 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 1847 |
+
}
|
| 1848 |
+
|
| 1849 |
+
#[tokio::test]
|
| 1850 |
+
async fn remote_server_request_received_during_initialize_is_delivered() {
|
| 1851 |
+
let websocket_url = start_test_remote_server(|mut websocket| async move {
|
| 1852 |
+
let JSONRPCMessage::Request(request) = read_websocket_message(&mut websocket).await
|
| 1853 |
+
else {
|
| 1854 |
+
panic!("expected initialize request");
|
| 1855 |
+
};
|
| 1856 |
+
assert_eq!(request.method, "initialize");
|
| 1857 |
+
|
| 1858 |
+
let request_id = RequestId::String("srv-init".to_string());
|
| 1859 |
+
write_websocket_message(
|
| 1860 |
+
&mut websocket,
|
| 1861 |
+
JSONRPCMessage::Request(JSONRPCRequest {
|
| 1862 |
+
id: request_id.clone(),
|
| 1863 |
+
method: "item/tool/requestUserInput".to_string(),
|
| 1864 |
+
params: Some(
|
| 1865 |
+
serde_json::to_value(ToolRequestUserInputParams {
|
| 1866 |
+
thread_id: "thread-1".to_string(),
|
| 1867 |
+
turn_id: "turn-1".to_string(),
|
| 1868 |
+
item_id: "call-1".to_string(),
|
| 1869 |
+
questions: vec![ToolRequestUserInputQuestion {
|
| 1870 |
+
id: "question-1".to_string(),
|
| 1871 |
+
header: "Mode".to_string(),
|
| 1872 |
+
question: "Pick one".to_string(),
|
| 1873 |
+
is_other: false,
|
| 1874 |
+
is_secret: false,
|
| 1875 |
+
options: Some(vec![]),
|
| 1876 |
+
}],
|
| 1877 |
+
is_blocking: true,
|
| 1878 |
+
auto_resolution_ms: None,
|
| 1879 |
+
})
|
| 1880 |
+
.expect("params should serialize"),
|
| 1881 |
+
),
|
| 1882 |
+
trace: None,
|
| 1883 |
+
}),
|
| 1884 |
+
)
|
| 1885 |
+
.await;
|
| 1886 |
+
write_websocket_message(
|
| 1887 |
+
&mut websocket,
|
| 1888 |
+
JSONRPCMessage::Response(JSONRPCResponse {
|
| 1889 |
+
id: request.id,
|
| 1890 |
+
result: serde_json::json!({}),
|
| 1891 |
+
}),
|
| 1892 |
+
)
|
| 1893 |
+
.await;
|
| 1894 |
+
|
| 1895 |
+
let JSONRPCMessage::Notification(notification) =
|
| 1896 |
+
read_websocket_message(&mut websocket).await
|
| 1897 |
+
else {
|
| 1898 |
+
panic!("expected initialized notification");
|
| 1899 |
+
};
|
| 1900 |
+
assert_eq!(notification.method, "initialized");
|
| 1901 |
+
|
| 1902 |
+
let JSONRPCMessage::Response(response) = read_websocket_message(&mut websocket).await
|
| 1903 |
+
else {
|
| 1904 |
+
panic!("expected server request response");
|
| 1905 |
+
};
|
| 1906 |
+
assert_eq!(response.id, request_id);
|
| 1907 |
+
})
|
| 1908 |
+
.await;
|
| 1909 |
+
let mut client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
|
| 1910 |
+
.await
|
| 1911 |
+
.expect("remote client should connect");
|
| 1912 |
+
|
| 1913 |
+
let AppServerEvent::ServerRequest(request) = client
|
| 1914 |
+
.next_event()
|
| 1915 |
+
.await
|
| 1916 |
+
.expect("request event should arrive")
|
| 1917 |
+
else {
|
| 1918 |
+
panic!("expected server request event");
|
| 1919 |
+
};
|
| 1920 |
+
client
|
| 1921 |
+
.resolve_server_request(request.id().clone(), serde_json::json!({}))
|
| 1922 |
+
.await
|
| 1923 |
+
.expect("server request should resolve");
|
| 1924 |
+
|
| 1925 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 1926 |
+
}
|
| 1927 |
+
|
| 1928 |
+
#[tokio::test]
|
| 1929 |
+
async fn remote_unknown_server_request_is_rejected() {
|
| 1930 |
+
let websocket_url = start_test_remote_server(|mut websocket| async move {
|
| 1931 |
+
expect_remote_initialize(&mut websocket).await;
|
| 1932 |
+
let request_id = RequestId::String("srv-unknown".to_string());
|
| 1933 |
+
write_websocket_message(
|
| 1934 |
+
&mut websocket,
|
| 1935 |
+
JSONRPCMessage::Request(JSONRPCRequest {
|
| 1936 |
+
id: request_id.clone(),
|
| 1937 |
+
method: "thread/unknown".to_string(),
|
| 1938 |
+
params: None,
|
| 1939 |
+
trace: None,
|
| 1940 |
+
}),
|
| 1941 |
+
)
|
| 1942 |
+
.await;
|
| 1943 |
+
|
| 1944 |
+
let JSONRPCMessage::Error(response) = read_websocket_message(&mut websocket).await
|
| 1945 |
+
else {
|
| 1946 |
+
panic!("expected JSON-RPC error response");
|
| 1947 |
+
};
|
| 1948 |
+
assert_eq!(response.id, request_id);
|
| 1949 |
+
assert_eq!(response.error.code, -32601);
|
| 1950 |
+
assert_eq!(
|
| 1951 |
+
response.error.message,
|
| 1952 |
+
"unsupported remote app-server request `thread/unknown`"
|
| 1953 |
+
);
|
| 1954 |
+
})
|
| 1955 |
+
.await;
|
| 1956 |
+
let client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
|
| 1957 |
+
.await
|
| 1958 |
+
.expect("remote client should connect");
|
| 1959 |
+
|
| 1960 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 1961 |
+
}
|
| 1962 |
+
|
| 1963 |
+
#[tokio::test]
|
| 1964 |
+
async fn remote_disconnect_surfaces_as_event() {
|
| 1965 |
+
let websocket_url = start_test_remote_server(|mut websocket| async move {
|
| 1966 |
+
expect_remote_initialize(&mut websocket).await;
|
| 1967 |
+
websocket.close(None).await.expect("close should succeed");
|
| 1968 |
+
})
|
| 1969 |
+
.await;
|
| 1970 |
+
let mut client = RemoteAppServerClient::connect(test_remote_connect_args(websocket_url))
|
| 1971 |
+
.await
|
| 1972 |
+
.expect("remote client should connect");
|
| 1973 |
+
|
| 1974 |
+
let event = client
|
| 1975 |
+
.next_event()
|
| 1976 |
+
.await
|
| 1977 |
+
.expect("disconnect event should arrive");
|
| 1978 |
+
assert!(matches!(event, AppServerEvent::Disconnected { .. }));
|
| 1979 |
+
}
|
| 1980 |
+
|
| 1981 |
+
#[test]
|
| 1982 |
+
fn typed_request_error_exposes_sources() {
|
| 1983 |
+
let transport = TypedRequestError::Transport {
|
| 1984 |
+
method: "config/read".to_string(),
|
| 1985 |
+
source: IoError::new(ErrorKind::BrokenPipe, "closed"),
|
| 1986 |
+
};
|
| 1987 |
+
assert_eq!(std::error::Error::source(&transport).is_some(), true);
|
| 1988 |
+
|
| 1989 |
+
let server = TypedRequestError::Server {
|
| 1990 |
+
method: "thread/read".to_string(),
|
| 1991 |
+
source: JSONRPCErrorError {
|
| 1992 |
+
code: -32603,
|
| 1993 |
+
data: Some(serde_json::json!({"detail": "config lock mismatch"})),
|
| 1994 |
+
message: "internal".to_string(),
|
| 1995 |
+
},
|
| 1996 |
+
};
|
| 1997 |
+
assert_eq!(std::error::Error::source(&server).is_some(), false);
|
| 1998 |
+
assert_eq!(
|
| 1999 |
+
server.to_string(),
|
| 2000 |
+
"thread/read failed: internal (code -32603), data: {\"detail\":\"config lock mismatch\"}"
|
| 2001 |
+
);
|
| 2002 |
+
|
| 2003 |
+
let deserialize = TypedRequestError::Deserialize {
|
| 2004 |
+
method: "thread/start".to_string(),
|
| 2005 |
+
source: serde_json::from_str::<u32>("\"nope\"")
|
| 2006 |
+
.expect_err("invalid integer should return deserialize error"),
|
| 2007 |
+
};
|
| 2008 |
+
assert_eq!(std::error::Error::source(&deserialize).is_some(), true);
|
| 2009 |
+
}
|
| 2010 |
+
|
| 2011 |
+
#[tokio::test]
|
| 2012 |
+
async fn next_event_surfaces_lagged_markers() {
|
| 2013 |
+
let (command_tx, _command_rx) = mpsc::channel(1);
|
| 2014 |
+
let (event_tx, event_rx) = mpsc::unbounded_channel();
|
| 2015 |
+
let worker_handle = tokio::spawn(async {});
|
| 2016 |
+
event_tx
|
| 2017 |
+
.send(InProcessServerEvent::Lagged { skipped: 3 })
|
| 2018 |
+
.expect("lagged marker should enqueue");
|
| 2019 |
+
drop(event_tx);
|
| 2020 |
+
|
| 2021 |
+
let mut client = InProcessAppServerClient {
|
| 2022 |
+
command_tx,
|
| 2023 |
+
event_rx,
|
| 2024 |
+
worker_handle,
|
| 2025 |
+
};
|
| 2026 |
+
|
| 2027 |
+
let event = timeout(Duration::from_secs(2), client.next_event())
|
| 2028 |
+
.await
|
| 2029 |
+
.expect("lagged marker should arrive before timeout");
|
| 2030 |
+
assert!(matches!(
|
| 2031 |
+
event,
|
| 2032 |
+
Some(InProcessServerEvent::Lagged { skipped: 3 })
|
| 2033 |
+
));
|
| 2034 |
+
|
| 2035 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 2036 |
+
}
|
| 2037 |
+
|
| 2038 |
+
#[tokio::test]
|
| 2039 |
+
async fn runtime_start_args_forward_environment_manager_and_openai_form_capability() {
|
| 2040 |
+
let config = Arc::new(build_test_config().await);
|
| 2041 |
+
let environment_manager = Arc::new(
|
| 2042 |
+
EnvironmentManager::create_for_tests(
|
| 2043 |
+
Some("ws://127.0.0.1:8765".to_string()),
|
| 2044 |
+
Some(
|
| 2045 |
+
ExecServerRuntimePaths::new(
|
| 2046 |
+
std::env::current_exe().expect("current exe"),
|
| 2047 |
+
/*codex_linux_sandbox_exe*/ None,
|
| 2048 |
+
)
|
| 2049 |
+
.expect("runtime paths"),
|
| 2050 |
+
),
|
| 2051 |
+
)
|
| 2052 |
+
.await,
|
| 2053 |
+
);
|
| 2054 |
+
|
| 2055 |
+
let runtime_args = InProcessClientStartArgs {
|
| 2056 |
+
arg0_paths: Arg0DispatchPaths::default(),
|
| 2057 |
+
config: config.clone(),
|
| 2058 |
+
cli_overrides: Vec::new(),
|
| 2059 |
+
loader_overrides: LoaderOverrides::default(),
|
| 2060 |
+
strict_config: false,
|
| 2061 |
+
cloud_config_bundle: CloudConfigBundleLoader::default(),
|
| 2062 |
+
feedback: CodexFeedback::new(),
|
| 2063 |
+
log_db: None,
|
| 2064 |
+
state_db: None,
|
| 2065 |
+
environment_manager: environment_manager.clone(),
|
| 2066 |
+
config_warnings: Vec::new(),
|
| 2067 |
+
session_source: SessionSource::Exec,
|
| 2068 |
+
enable_codex_api_key_env: false,
|
| 2069 |
+
client_name: "codex-app-server-client-test".to_string(),
|
| 2070 |
+
client_version: "0.0.0-test".to_string(),
|
| 2071 |
+
experimental_api: true,
|
| 2072 |
+
mcp_server_openai_form_elicitation: true,
|
| 2073 |
+
opt_out_notification_methods: Vec::new(),
|
| 2074 |
+
channel_capacity: DEFAULT_IN_PROCESS_CHANNEL_CAPACITY,
|
| 2075 |
+
}
|
| 2076 |
+
.into_runtime_start_args();
|
| 2077 |
+
|
| 2078 |
+
assert_eq!(runtime_args.config, config);
|
| 2079 |
+
assert!(
|
| 2080 |
+
runtime_args
|
| 2081 |
+
.initialize
|
| 2082 |
+
.capabilities
|
| 2083 |
+
.expect("initialize capabilities")
|
| 2084 |
+
.mcp_server_openai_form_elicitation
|
| 2085 |
+
);
|
| 2086 |
+
assert!(Arc::ptr_eq(
|
| 2087 |
+
&runtime_args.environment_manager,
|
| 2088 |
+
&environment_manager
|
| 2089 |
+
));
|
| 2090 |
+
assert!(
|
| 2091 |
+
runtime_args
|
| 2092 |
+
.environment_manager
|
| 2093 |
+
.default_environment()
|
| 2094 |
+
.expect("default environment")
|
| 2095 |
+
.is_remote()
|
| 2096 |
+
);
|
| 2097 |
+
}
|
| 2098 |
+
|
| 2099 |
+
#[tokio::test]
|
| 2100 |
+
async fn shutdown_completes_promptly_without_retained_managers() {
|
| 2101 |
+
let client = start_test_client(SessionSource::Cli).await;
|
| 2102 |
+
|
| 2103 |
+
timeout(Duration::from_secs(1), client.shutdown())
|
| 2104 |
+
.await
|
| 2105 |
+
.expect("shutdown should not wait for the 5s fallback timeout")
|
| 2106 |
+
.expect("shutdown should complete");
|
| 2107 |
+
}
|
| 2108 |
+
|
| 2109 |
+
#[tokio::test(start_paused = true)]
|
| 2110 |
+
async fn shutdown_waits_for_in_process_drain() {
|
| 2111 |
+
use std::sync::atomic::AtomicBool;
|
| 2112 |
+
use std::sync::atomic::Ordering;
|
| 2113 |
+
|
| 2114 |
+
let (command_tx, mut command_rx) = mpsc::channel(1);
|
| 2115 |
+
let (_event_tx, event_rx) = mpsc::unbounded_channel();
|
| 2116 |
+
let completed = Arc::new(AtomicBool::new(false));
|
| 2117 |
+
let worker_completed = Arc::clone(&completed);
|
| 2118 |
+
let worker_handle = tokio::spawn(async move {
|
| 2119 |
+
let response_tx = match command_rx.recv().await {
|
| 2120 |
+
Some(ClientCommand::Shutdown { response_tx }) => response_tx,
|
| 2121 |
+
_ => panic!("expected shutdown command"),
|
| 2122 |
+
};
|
| 2123 |
+
tokio::time::sleep(Duration::from_secs(30)).await;
|
| 2124 |
+
worker_completed.store(true, Ordering::Release);
|
| 2125 |
+
let _ = response_tx.send(Ok(()));
|
| 2126 |
+
});
|
| 2127 |
+
let client = InProcessAppServerClient {
|
| 2128 |
+
command_tx,
|
| 2129 |
+
event_rx,
|
| 2130 |
+
worker_handle,
|
| 2131 |
+
};
|
| 2132 |
+
|
| 2133 |
+
client.shutdown().await.expect("shutdown should complete");
|
| 2134 |
+
assert!(completed.load(Ordering::Acquire));
|
| 2135 |
+
}
|
| 2136 |
+
}
|
codex-rs/app-server-client/src/path.rs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
//! Paths resolved using the app-server host's platform rules.
|
| 2 |
+
|
| 3 |
+
use std::fmt;
|
| 4 |
+
|
| 5 |
+
#[derive(Clone, Debug, PartialEq, Eq)]
|
| 6 |
+
pub struct AppServerPath(String);
|
| 7 |
+
|
| 8 |
+
impl AppServerPath {
|
| 9 |
+
pub fn from_app_server(path: impl Into<String>) -> Self {
|
| 10 |
+
Self(path.into())
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
pub fn from_absolute_str(raw: &str) -> Option<Self> {
|
| 14 |
+
(raw.starts_with('/') || is_windows_absolute_path(raw)).then(|| Self(raw.to_string()))
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
pub fn as_str(&self) -> &str {
|
| 18 |
+
&self.0
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
pub fn components(&self) -> Vec<&str> {
|
| 22 |
+
let separators = if is_windows_absolute_path(&self.0) {
|
| 23 |
+
&['/', '\\'][..]
|
| 24 |
+
} else {
|
| 25 |
+
&['/'][..]
|
| 26 |
+
};
|
| 27 |
+
self.0
|
| 28 |
+
.split(separators)
|
| 29 |
+
.filter(|part| !part.is_empty())
|
| 30 |
+
.collect()
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
pub fn join(&self, segment: impl AsRef<str>) -> Self {
|
| 34 |
+
let is_windows = is_windows_absolute_path(&self.0);
|
| 35 |
+
let (path, separator) = if is_windows {
|
| 36 |
+
(self.0.trim_end_matches(['/', '\\']), '\\')
|
| 37 |
+
} else {
|
| 38 |
+
(self.0.trim_end_matches('/'), '/')
|
| 39 |
+
};
|
| 40 |
+
Self(format!("{path}{separator}{}", segment.as_ref()))
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
impl fmt::Display for AppServerPath {
|
| 45 |
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
| 46 |
+
self.0.fmt(f)
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
fn is_windows_absolute_path(path: &str) -> bool {
|
| 51 |
+
let bytes = path.as_bytes();
|
| 52 |
+
(bytes.len() >= 3
|
| 53 |
+
&& bytes[0].is_ascii_alphabetic()
|
| 54 |
+
&& bytes[1] == b':'
|
| 55 |
+
&& matches!(bytes[2], b'\\' | b'/'))
|
| 56 |
+
|| path.starts_with("\\\\")
|
| 57 |
+
|| path.starts_with("//")
|
| 58 |
+
}
|
codex-rs/app-server-client/src/remote.rs
ADDED
|
@@ -0,0 +1,1088 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/*
|
| 2 |
+
This module implements the remote app-server client transport.
|
| 3 |
+
|
| 4 |
+
It owns the remote connection lifecycle, including the initialize/initialized
|
| 5 |
+
handshake, JSON-RPC request/response routing, server-request resolution, and
|
| 6 |
+
notification streaming. Remote connections always carry WebSocket frames, over
|
| 7 |
+
either TCP WebSocket URLs or local Unix sockets. The rest of the crate uses the
|
| 8 |
+
same `AppServerEvent` surface for both in-process and remote transports, so
|
| 9 |
+
callers such as the TUI can switch between them without changing their
|
| 10 |
+
higher-level session logic.
|
| 11 |
+
*/
|
| 12 |
+
|
| 13 |
+
use std::collections::HashMap;
|
| 14 |
+
use std::collections::VecDeque;
|
| 15 |
+
use std::io::Error as IoError;
|
| 16 |
+
use std::io::ErrorKind;
|
| 17 |
+
use std::io::Result as IoResult;
|
| 18 |
+
use std::time::Duration;
|
| 19 |
+
|
| 20 |
+
use crate::AppServerEvent;
|
| 21 |
+
use crate::RequestResult;
|
| 22 |
+
use crate::SHUTDOWN_TIMEOUT;
|
| 23 |
+
use crate::TypedRequestError;
|
| 24 |
+
use codex_app_server_protocol::ClientInfo;
|
| 25 |
+
use codex_app_server_protocol::ClientNotification;
|
| 26 |
+
use codex_app_server_protocol::ClientRequest;
|
| 27 |
+
use codex_app_server_protocol::InitializeCapabilities;
|
| 28 |
+
use codex_app_server_protocol::InitializeParams;
|
| 29 |
+
use codex_app_server_protocol::JSONRPCError;
|
| 30 |
+
use codex_app_server_protocol::JSONRPCErrorError;
|
| 31 |
+
use codex_app_server_protocol::JSONRPCMessage;
|
| 32 |
+
use codex_app_server_protocol::JSONRPCNotification;
|
| 33 |
+
use codex_app_server_protocol::JSONRPCRequest;
|
| 34 |
+
use codex_app_server_protocol::JSONRPCResponse;
|
| 35 |
+
use codex_app_server_protocol::RequestId;
|
| 36 |
+
use codex_app_server_protocol::Result as JsonRpcResult;
|
| 37 |
+
use codex_app_server_protocol::ServerNotification;
|
| 38 |
+
use codex_app_server_protocol::ServerRequest;
|
| 39 |
+
use codex_uds::UnixStream;
|
| 40 |
+
use codex_utils_absolute_path::AbsolutePathBuf;
|
| 41 |
+
use codex_utils_rustls_provider::ensure_rustls_crypto_provider;
|
| 42 |
+
use futures::SinkExt;
|
| 43 |
+
use futures::StreamExt;
|
| 44 |
+
use serde::de::DeserializeOwned;
|
| 45 |
+
use tokio::io::AsyncRead;
|
| 46 |
+
use tokio::io::AsyncWrite;
|
| 47 |
+
use tokio::net::TcpStream;
|
| 48 |
+
use tokio::sync::mpsc;
|
| 49 |
+
use tokio::sync::oneshot;
|
| 50 |
+
use tokio::time::timeout;
|
| 51 |
+
use tokio_tungstenite::MaybeTlsStream;
|
| 52 |
+
use tokio_tungstenite::WebSocketStream;
|
| 53 |
+
use tokio_tungstenite::client_async_with_config;
|
| 54 |
+
use tokio_tungstenite::connect_async_with_config;
|
| 55 |
+
use tokio_tungstenite::tungstenite::Error as TungsteniteError;
|
| 56 |
+
use tokio_tungstenite::tungstenite::Message;
|
| 57 |
+
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
| 58 |
+
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
| 59 |
+
use tokio_tungstenite::tungstenite::http::header::AUTHORIZATION;
|
| 60 |
+
use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
|
| 61 |
+
use tracing::warn;
|
| 62 |
+
use url::Url;
|
| 63 |
+
|
| 64 |
+
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
| 65 |
+
const INITIALIZE_TIMEOUT: Duration = Duration::from_secs(10);
|
| 66 |
+
const REMOTE_APP_SERVER_MAX_WEBSOCKET_MESSAGE_SIZE: usize = 128 << 20;
|
| 67 |
+
// Tungstenite still needs an HTTP request URI for the WebSocket handshake;
|
| 68 |
+
// the bytes travel over the Unix socket, not TCP.
|
| 69 |
+
const UDS_WEBSOCKET_HANDSHAKE_URL: &str = "ws://localhost/rpc";
|
| 70 |
+
|
| 71 |
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
| 72 |
+
pub enum RemoteAppServerEndpoint {
|
| 73 |
+
WebSocket {
|
| 74 |
+
websocket_url: String,
|
| 75 |
+
auth_token: Option<String>,
|
| 76 |
+
},
|
| 77 |
+
UnixSocket {
|
| 78 |
+
socket_path: AbsolutePathBuf,
|
| 79 |
+
},
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
#[derive(Debug, Clone)]
|
| 83 |
+
pub struct RemoteAppServerConnectArgs {
|
| 84 |
+
pub endpoint: RemoteAppServerEndpoint,
|
| 85 |
+
pub client_name: String,
|
| 86 |
+
pub client_version: String,
|
| 87 |
+
pub experimental_api: bool,
|
| 88 |
+
pub mcp_server_openai_form_elicitation: bool,
|
| 89 |
+
pub opt_out_notification_methods: Vec<String>,
|
| 90 |
+
pub channel_capacity: usize,
|
| 91 |
+
}
|
| 92 |
+
impl RemoteAppServerConnectArgs {
|
| 93 |
+
pub(crate) fn initialize_params(&self) -> InitializeParams {
|
| 94 |
+
let capabilities = InitializeCapabilities {
|
| 95 |
+
experimental_api: self.experimental_api,
|
| 96 |
+
request_attestation: false,
|
| 97 |
+
extensions: None,
|
| 98 |
+
opt_out_notification_methods: if self.opt_out_notification_methods.is_empty() {
|
| 99 |
+
None
|
| 100 |
+
} else {
|
| 101 |
+
Some(self.opt_out_notification_methods.clone())
|
| 102 |
+
},
|
| 103 |
+
mcp_server_openai_form_elicitation: self.mcp_server_openai_form_elicitation,
|
| 104 |
+
};
|
| 105 |
+
|
| 106 |
+
InitializeParams {
|
| 107 |
+
client_info: ClientInfo {
|
| 108 |
+
name: self.client_name.clone(),
|
| 109 |
+
title: None,
|
| 110 |
+
version: self.client_version.clone(),
|
| 111 |
+
},
|
| 112 |
+
capabilities: Some(capabilities),
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
pub(crate) fn websocket_url_supports_auth_token(url: &Url) -> bool {
|
| 118 |
+
match (url.scheme(), url.host()) {
|
| 119 |
+
("wss", Some(_)) => true,
|
| 120 |
+
("ws", Some(url::Host::Domain(domain))) => domain.eq_ignore_ascii_case("localhost"),
|
| 121 |
+
("ws", Some(url::Host::Ipv4(addr))) => addr.is_loopback(),
|
| 122 |
+
("ws", Some(url::Host::Ipv6(addr))) => addr.is_loopback(),
|
| 123 |
+
_ => false,
|
| 124 |
+
}
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
enum RemoteClientCommand {
|
| 128 |
+
Request {
|
| 129 |
+
request: Box<JSONRPCRequest>,
|
| 130 |
+
response_tx: oneshot::Sender<IoResult<RequestResult>>,
|
| 131 |
+
},
|
| 132 |
+
Notify {
|
| 133 |
+
notification: ClientNotification,
|
| 134 |
+
response_tx: oneshot::Sender<IoResult<()>>,
|
| 135 |
+
},
|
| 136 |
+
ResolveServerRequest {
|
| 137 |
+
request_id: RequestId,
|
| 138 |
+
result: JsonRpcResult,
|
| 139 |
+
response_tx: oneshot::Sender<IoResult<()>>,
|
| 140 |
+
},
|
| 141 |
+
RejectServerRequest {
|
| 142 |
+
request_id: RequestId,
|
| 143 |
+
error: JSONRPCErrorError,
|
| 144 |
+
response_tx: oneshot::Sender<IoResult<()>>,
|
| 145 |
+
},
|
| 146 |
+
Shutdown {
|
| 147 |
+
response_tx: oneshot::Sender<IoResult<()>>,
|
| 148 |
+
},
|
| 149 |
+
}
|
| 150 |
+
|
| 151 |
+
#[derive(Default)]
|
| 152 |
+
struct RemoteServerMetadata {
|
| 153 |
+
server_version: Option<String>,
|
| 154 |
+
codex_home: Option<String>,
|
| 155 |
+
platform_family: Option<String>,
|
| 156 |
+
platform_os: Option<String>,
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
pub struct RemoteAppServerClient {
|
| 160 |
+
command_tx: mpsc::Sender<RemoteClientCommand>,
|
| 161 |
+
event_rx: mpsc::UnboundedReceiver<AppServerEvent>,
|
| 162 |
+
pending_events: VecDeque<AppServerEvent>,
|
| 163 |
+
metadata: RemoteServerMetadata,
|
| 164 |
+
worker_handle: tokio::task::JoinHandle<()>,
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
#[derive(Clone)]
|
| 168 |
+
pub struct RemoteAppServerRequestHandle {
|
| 169 |
+
command_tx: mpsc::Sender<RemoteClientCommand>,
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
enum SocketPeerPolicy {
|
| 173 |
+
ExplicitEndpoint,
|
| 174 |
+
#[cfg(windows)]
|
| 175 |
+
NonElevatedCurrentUser,
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
impl RemoteAppServerClient {
|
| 179 |
+
pub async fn connect(args: RemoteAppServerConnectArgs) -> IoResult<Self> {
|
| 180 |
+
Self::connect_with_policy(args, SocketPeerPolicy::ExplicitEndpoint).await
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
/// Connects to an implicitly discovered Windows daemon, verifying its peer
|
| 184 |
+
/// token before the WebSocket handshake or any session requests.
|
| 185 |
+
#[cfg(windows)]
|
| 186 |
+
pub async fn connect_local_daemon(args: RemoteAppServerConnectArgs) -> IoResult<Self> {
|
| 187 |
+
if !matches!(args.endpoint, RemoteAppServerEndpoint::UnixSocket { .. }) {
|
| 188 |
+
return Err(IoError::new(
|
| 189 |
+
ErrorKind::InvalidInput,
|
| 190 |
+
"local daemon requires a Unix socket",
|
| 191 |
+
));
|
| 192 |
+
}
|
| 193 |
+
Self::connect_with_policy(args, SocketPeerPolicy::NonElevatedCurrentUser).await
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
async fn connect_with_policy(
|
| 197 |
+
args: RemoteAppServerConnectArgs,
|
| 198 |
+
peer_policy: SocketPeerPolicy,
|
| 199 |
+
) -> IoResult<Self> {
|
| 200 |
+
let channel_capacity = args.channel_capacity.max(1);
|
| 201 |
+
let initialize_params = args.initialize_params();
|
| 202 |
+
match args.endpoint {
|
| 203 |
+
RemoteAppServerEndpoint::WebSocket {
|
| 204 |
+
websocket_url,
|
| 205 |
+
auth_token,
|
| 206 |
+
} => {
|
| 207 |
+
let (endpoint, stream) =
|
| 208 |
+
connect_websocket_endpoint(websocket_url, auth_token).await?;
|
| 209 |
+
Self::connect_with_stream(channel_capacity, endpoint, stream, initialize_params)
|
| 210 |
+
.await
|
| 211 |
+
}
|
| 212 |
+
RemoteAppServerEndpoint::UnixSocket { socket_path } => {
|
| 213 |
+
let (endpoint, stream) =
|
| 214 |
+
connect_unix_socket_endpoint(socket_path, peer_policy).await?;
|
| 215 |
+
Self::connect_with_stream(channel_capacity, endpoint, stream, initialize_params)
|
| 216 |
+
.await
|
| 217 |
+
}
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
pub fn server_version(&self) -> Option<&str> {
|
| 222 |
+
self.metadata.server_version.as_deref()
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
pub fn codex_home(&self) -> Option<&str> {
|
| 226 |
+
self.metadata.codex_home.as_deref()
|
| 227 |
+
}
|
| 228 |
+
|
| 229 |
+
pub fn platform_family(&self) -> Option<&str> {
|
| 230 |
+
self.metadata.platform_family.as_deref()
|
| 231 |
+
}
|
| 232 |
+
|
| 233 |
+
pub fn platform_os(&self) -> Option<&str> {
|
| 234 |
+
self.metadata.platform_os.as_deref()
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
async fn connect_with_stream<S>(
|
| 238 |
+
channel_capacity: usize,
|
| 239 |
+
endpoint: String,
|
| 240 |
+
stream: WebSocketStream<S>,
|
| 241 |
+
initialize_params: InitializeParams,
|
| 242 |
+
) -> IoResult<Self>
|
| 243 |
+
where
|
| 244 |
+
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
| 245 |
+
{
|
| 246 |
+
let mut stream = stream;
|
| 247 |
+
let (pending_events, metadata) = initialize_remote_connection(
|
| 248 |
+
&mut stream,
|
| 249 |
+
&endpoint,
|
| 250 |
+
initialize_params,
|
| 251 |
+
INITIALIZE_TIMEOUT,
|
| 252 |
+
)
|
| 253 |
+
.await?;
|
| 254 |
+
|
| 255 |
+
let (command_tx, mut command_rx) = mpsc::channel::<RemoteClientCommand>(channel_capacity);
|
| 256 |
+
let (event_tx, event_rx) = mpsc::unbounded_channel::<AppServerEvent>();
|
| 257 |
+
let worker_handle = tokio::spawn(async move {
|
| 258 |
+
let mut pending_requests =
|
| 259 |
+
HashMap::<RequestId, oneshot::Sender<IoResult<RequestResult>>>::new();
|
| 260 |
+
let mut worker_exit_error: Option<(ErrorKind, String)> = None;
|
| 261 |
+
loop {
|
| 262 |
+
tokio::select! {
|
| 263 |
+
command = command_rx.recv() => {
|
| 264 |
+
let Some(command) = command else {
|
| 265 |
+
let _ = stream.close(None).await;
|
| 266 |
+
break;
|
| 267 |
+
};
|
| 268 |
+
match command {
|
| 269 |
+
RemoteClientCommand::Request { request, response_tx } => {
|
| 270 |
+
let request_id = request.id.clone();
|
| 271 |
+
if pending_requests.contains_key(&request_id) {
|
| 272 |
+
let _ = response_tx.send(Err(IoError::new(
|
| 273 |
+
ErrorKind::InvalidInput,
|
| 274 |
+
format!("duplicate remote app-server request id `{request_id}`"),
|
| 275 |
+
)));
|
| 276 |
+
continue;
|
| 277 |
+
}
|
| 278 |
+
pending_requests.insert(request_id.clone(), response_tx);
|
| 279 |
+
if let Err(err) = write_jsonrpc_message(
|
| 280 |
+
&mut stream,
|
| 281 |
+
JSONRPCMessage::Request(*request),
|
| 282 |
+
&endpoint,
|
| 283 |
+
)
|
| 284 |
+
.await
|
| 285 |
+
{
|
| 286 |
+
let err_message = err.to_string();
|
| 287 |
+
let message = format!(
|
| 288 |
+
"remote app server at `{endpoint}` write failed: {err_message}"
|
| 289 |
+
);
|
| 290 |
+
if let Some(response_tx) = pending_requests.remove(&request_id) {
|
| 291 |
+
let _ = response_tx.send(Err(err));
|
| 292 |
+
}
|
| 293 |
+
let _ = deliver_event(
|
| 294 |
+
&event_tx,
|
| 295 |
+
AppServerEvent::Disconnected {
|
| 296 |
+
message: message.clone(),
|
| 297 |
+
},
|
| 298 |
+
);
|
| 299 |
+
worker_exit_error = Some((ErrorKind::BrokenPipe, message));
|
| 300 |
+
break;
|
| 301 |
+
}
|
| 302 |
+
}
|
| 303 |
+
RemoteClientCommand::Notify { notification, response_tx } => {
|
| 304 |
+
let result = write_jsonrpc_message(
|
| 305 |
+
&mut stream,
|
| 306 |
+
JSONRPCMessage::Notification(
|
| 307 |
+
jsonrpc_notification_from_client_notification(notification),
|
| 308 |
+
),
|
| 309 |
+
&endpoint,
|
| 310 |
+
)
|
| 311 |
+
.await;
|
| 312 |
+
let _ = response_tx.send(result);
|
| 313 |
+
}
|
| 314 |
+
RemoteClientCommand::ResolveServerRequest {
|
| 315 |
+
request_id,
|
| 316 |
+
result,
|
| 317 |
+
response_tx,
|
| 318 |
+
} => {
|
| 319 |
+
let result = write_jsonrpc_message(
|
| 320 |
+
&mut stream,
|
| 321 |
+
JSONRPCMessage::Response(JSONRPCResponse {
|
| 322 |
+
id: request_id,
|
| 323 |
+
result,
|
| 324 |
+
}),
|
| 325 |
+
&endpoint,
|
| 326 |
+
)
|
| 327 |
+
.await;
|
| 328 |
+
let _ = response_tx.send(result);
|
| 329 |
+
}
|
| 330 |
+
RemoteClientCommand::RejectServerRequest {
|
| 331 |
+
request_id,
|
| 332 |
+
error,
|
| 333 |
+
response_tx,
|
| 334 |
+
} => {
|
| 335 |
+
let result = write_jsonrpc_message(
|
| 336 |
+
&mut stream,
|
| 337 |
+
JSONRPCMessage::Error(JSONRPCError {
|
| 338 |
+
error,
|
| 339 |
+
id: request_id,
|
| 340 |
+
}),
|
| 341 |
+
&endpoint,
|
| 342 |
+
)
|
| 343 |
+
.await;
|
| 344 |
+
let _ = response_tx.send(result);
|
| 345 |
+
}
|
| 346 |
+
RemoteClientCommand::Shutdown { response_tx } => {
|
| 347 |
+
let close_result = stream.close(None).await.or_else(|err| {
|
| 348 |
+
if websocket_close_error_is_already_closed(&err) {
|
| 349 |
+
Ok(())
|
| 350 |
+
} else {
|
| 351 |
+
Err(IoError::other(format!(
|
| 352 |
+
"failed to close websocket app server `{endpoint}`: {err}"
|
| 353 |
+
)))
|
| 354 |
+
}
|
| 355 |
+
});
|
| 356 |
+
let _ = response_tx.send(close_result);
|
| 357 |
+
break;
|
| 358 |
+
}
|
| 359 |
+
}
|
| 360 |
+
}
|
| 361 |
+
message = stream.next() => {
|
| 362 |
+
match message {
|
| 363 |
+
Some(Ok(Message::Text(text))) => {
|
| 364 |
+
match serde_json::from_str::<JSONRPCMessage>(&text) {
|
| 365 |
+
Ok(JSONRPCMessage::Response(response)) => {
|
| 366 |
+
if let Some(response_tx) = pending_requests.remove(&response.id) {
|
| 367 |
+
let _ = response_tx.send(Ok(Ok(response.result)));
|
| 368 |
+
}
|
| 369 |
+
}
|
| 370 |
+
Ok(JSONRPCMessage::Error(error)) => {
|
| 371 |
+
if let Some(response_tx) = pending_requests.remove(&error.id) {
|
| 372 |
+
let _ = response_tx.send(Ok(Err(error.error)));
|
| 373 |
+
}
|
| 374 |
+
}
|
| 375 |
+
Ok(JSONRPCMessage::Notification(notification)) => {
|
| 376 |
+
if let Some(event) =
|
| 377 |
+
app_server_event_from_notification(notification)
|
| 378 |
+
&& let Err(err) = deliver_event(
|
| 379 |
+
&event_tx,
|
| 380 |
+
event,
|
| 381 |
+
)
|
| 382 |
+
{
|
| 383 |
+
warn!(%err, "failed to deliver remote app-server event");
|
| 384 |
+
break;
|
| 385 |
+
}
|
| 386 |
+
}
|
| 387 |
+
Ok(JSONRPCMessage::Request(request)) => {
|
| 388 |
+
let request_id = request.id.clone();
|
| 389 |
+
let method = request.method.clone();
|
| 390 |
+
match ServerRequest::try_from(request) {
|
| 391 |
+
Ok(request) => {
|
| 392 |
+
if let Err(err) = deliver_event(
|
| 393 |
+
&event_tx,
|
| 394 |
+
AppServerEvent::ServerRequest(Box::new(request)),
|
| 395 |
+
)
|
| 396 |
+
{
|
| 397 |
+
warn!(%err, "failed to deliver remote app-server server request");
|
| 398 |
+
break;
|
| 399 |
+
}
|
| 400 |
+
}
|
| 401 |
+
Err(err) => {
|
| 402 |
+
warn!(%err, method, "rejecting unknown remote app-server request");
|
| 403 |
+
if let Err(reject_err) = write_jsonrpc_message(
|
| 404 |
+
&mut stream,
|
| 405 |
+
JSONRPCMessage::Error(JSONRPCError {
|
| 406 |
+
error: JSONRPCErrorError {
|
| 407 |
+
code: -32601,
|
| 408 |
+
message: format!(
|
| 409 |
+
"unsupported remote app-server request `{method}`"
|
| 410 |
+
),
|
| 411 |
+
data: None,
|
| 412 |
+
},
|
| 413 |
+
id: request_id,
|
| 414 |
+
}),
|
| 415 |
+
&endpoint,
|
| 416 |
+
)
|
| 417 |
+
.await
|
| 418 |
+
{
|
| 419 |
+
let err_message = reject_err.to_string();
|
| 420 |
+
let message = format!(
|
| 421 |
+
"remote app server at `{endpoint}` write failed: {err_message}"
|
| 422 |
+
);
|
| 423 |
+
let _ = deliver_event(
|
| 424 |
+
&event_tx,
|
| 425 |
+
AppServerEvent::Disconnected {
|
| 426 |
+
message: message.clone(),
|
| 427 |
+
},
|
| 428 |
+
);
|
| 429 |
+
worker_exit_error =
|
| 430 |
+
Some((ErrorKind::BrokenPipe, message));
|
| 431 |
+
break;
|
| 432 |
+
}
|
| 433 |
+
}
|
| 434 |
+
}
|
| 435 |
+
}
|
| 436 |
+
Err(err) => {
|
| 437 |
+
let message = format!(
|
| 438 |
+
"remote app server at `{endpoint}` sent invalid JSON-RPC: {err}"
|
| 439 |
+
);
|
| 440 |
+
let _ = deliver_event(
|
| 441 |
+
&event_tx,
|
| 442 |
+
AppServerEvent::Disconnected {
|
| 443 |
+
message: message.clone(),
|
| 444 |
+
},
|
| 445 |
+
);
|
| 446 |
+
worker_exit_error =
|
| 447 |
+
Some((ErrorKind::InvalidData, message));
|
| 448 |
+
break;
|
| 449 |
+
}
|
| 450 |
+
}
|
| 451 |
+
}
|
| 452 |
+
Some(Ok(Message::Close(frame))) => {
|
| 453 |
+
let reason = frame
|
| 454 |
+
.as_ref()
|
| 455 |
+
.map(|frame| frame.reason.to_string())
|
| 456 |
+
.filter(|reason| !reason.is_empty())
|
| 457 |
+
.unwrap_or_else(|| "connection closed".to_string());
|
| 458 |
+
let message = format!(
|
| 459 |
+
"remote app server at `{endpoint}` disconnected: {reason}"
|
| 460 |
+
);
|
| 461 |
+
let _ = deliver_event(
|
| 462 |
+
&event_tx,
|
| 463 |
+
AppServerEvent::Disconnected {
|
| 464 |
+
message: message.clone(),
|
| 465 |
+
},
|
| 466 |
+
);
|
| 467 |
+
worker_exit_error = Some((
|
| 468 |
+
ErrorKind::ConnectionAborted,
|
| 469 |
+
message,
|
| 470 |
+
));
|
| 471 |
+
break;
|
| 472 |
+
}
|
| 473 |
+
Some(Ok(Message::Binary(_)))
|
| 474 |
+
| Some(Ok(Message::Ping(_)))
|
| 475 |
+
| Some(Ok(Message::Pong(_)))
|
| 476 |
+
| Some(Ok(Message::Frame(_))) => {}
|
| 477 |
+
Some(Err(err)) => {
|
| 478 |
+
let message = format!(
|
| 479 |
+
"remote app server at `{endpoint}` transport failed: {err}"
|
| 480 |
+
);
|
| 481 |
+
let _ = deliver_event(
|
| 482 |
+
&event_tx,
|
| 483 |
+
AppServerEvent::Disconnected {
|
| 484 |
+
message: message.clone(),
|
| 485 |
+
},
|
| 486 |
+
);
|
| 487 |
+
worker_exit_error = Some((ErrorKind::InvalidData, message));
|
| 488 |
+
break;
|
| 489 |
+
}
|
| 490 |
+
None => {
|
| 491 |
+
let message = format!(
|
| 492 |
+
"remote app server at `{endpoint}` closed the connection"
|
| 493 |
+
);
|
| 494 |
+
let _ = deliver_event(
|
| 495 |
+
&event_tx,
|
| 496 |
+
AppServerEvent::Disconnected {
|
| 497 |
+
message: message.clone(),
|
| 498 |
+
},
|
| 499 |
+
);
|
| 500 |
+
worker_exit_error = Some((ErrorKind::UnexpectedEof, message));
|
| 501 |
+
break;
|
| 502 |
+
}
|
| 503 |
+
}
|
| 504 |
+
}
|
| 505 |
+
}
|
| 506 |
+
}
|
| 507 |
+
|
| 508 |
+
let (err_kind, err_message) = worker_exit_error.unwrap_or_else(|| {
|
| 509 |
+
(
|
| 510 |
+
ErrorKind::BrokenPipe,
|
| 511 |
+
"remote app-server worker channel is closed".to_string(),
|
| 512 |
+
)
|
| 513 |
+
});
|
| 514 |
+
for (_, response_tx) in pending_requests {
|
| 515 |
+
let _ = response_tx.send(Err(IoError::new(err_kind, err_message.clone())));
|
| 516 |
+
}
|
| 517 |
+
});
|
| 518 |
+
|
| 519 |
+
Ok(Self {
|
| 520 |
+
command_tx,
|
| 521 |
+
event_rx,
|
| 522 |
+
pending_events: pending_events.into(),
|
| 523 |
+
metadata,
|
| 524 |
+
worker_handle,
|
| 525 |
+
})
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
pub fn request_handle(&self) -> RemoteAppServerRequestHandle {
|
| 529 |
+
RemoteAppServerRequestHandle {
|
| 530 |
+
command_tx: self.command_tx.clone(),
|
| 531 |
+
}
|
| 532 |
+
}
|
| 533 |
+
|
| 534 |
+
pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
|
| 535 |
+
self.request_handle().request(request).await
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
|
| 539 |
+
where
|
| 540 |
+
T: DeserializeOwned,
|
| 541 |
+
{
|
| 542 |
+
let method = request.method_name();
|
| 543 |
+
let response =
|
| 544 |
+
self.request(request)
|
| 545 |
+
.await
|
| 546 |
+
.map_err(|source| TypedRequestError::Transport {
|
| 547 |
+
method: method.to_string(),
|
| 548 |
+
source,
|
| 549 |
+
})?;
|
| 550 |
+
let result = response.map_err(|source| TypedRequestError::Server {
|
| 551 |
+
method: method.to_string(),
|
| 552 |
+
source,
|
| 553 |
+
})?;
|
| 554 |
+
serde_json::from_value(result).map_err(|source| TypedRequestError::Deserialize {
|
| 555 |
+
method: method.to_string(),
|
| 556 |
+
source,
|
| 557 |
+
})
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
pub async fn notify(&self, notification: ClientNotification) -> IoResult<()> {
|
| 561 |
+
let (response_tx, response_rx) = oneshot::channel();
|
| 562 |
+
self.command_tx
|
| 563 |
+
.send(RemoteClientCommand::Notify {
|
| 564 |
+
notification,
|
| 565 |
+
response_tx,
|
| 566 |
+
})
|
| 567 |
+
.await
|
| 568 |
+
.map_err(|_| {
|
| 569 |
+
IoError::new(
|
| 570 |
+
ErrorKind::BrokenPipe,
|
| 571 |
+
"remote app-server worker channel is closed",
|
| 572 |
+
)
|
| 573 |
+
})?;
|
| 574 |
+
response_rx.await.map_err(|_| {
|
| 575 |
+
IoError::new(
|
| 576 |
+
ErrorKind::BrokenPipe,
|
| 577 |
+
"remote app-server notify channel is closed",
|
| 578 |
+
)
|
| 579 |
+
})?
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
pub async fn resolve_server_request(
|
| 583 |
+
&self,
|
| 584 |
+
request_id: RequestId,
|
| 585 |
+
result: JsonRpcResult,
|
| 586 |
+
) -> IoResult<()> {
|
| 587 |
+
let (response_tx, response_rx) = oneshot::channel();
|
| 588 |
+
self.command_tx
|
| 589 |
+
.send(RemoteClientCommand::ResolveServerRequest {
|
| 590 |
+
request_id,
|
| 591 |
+
result,
|
| 592 |
+
response_tx,
|
| 593 |
+
})
|
| 594 |
+
.await
|
| 595 |
+
.map_err(|_| {
|
| 596 |
+
IoError::new(
|
| 597 |
+
ErrorKind::BrokenPipe,
|
| 598 |
+
"remote app-server worker channel is closed",
|
| 599 |
+
)
|
| 600 |
+
})?;
|
| 601 |
+
response_rx.await.map_err(|_| {
|
| 602 |
+
IoError::new(
|
| 603 |
+
ErrorKind::BrokenPipe,
|
| 604 |
+
"remote app-server resolve channel is closed",
|
| 605 |
+
)
|
| 606 |
+
})?
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
pub async fn reject_server_request(
|
| 610 |
+
&self,
|
| 611 |
+
request_id: RequestId,
|
| 612 |
+
error: JSONRPCErrorError,
|
| 613 |
+
) -> IoResult<()> {
|
| 614 |
+
let (response_tx, response_rx) = oneshot::channel();
|
| 615 |
+
self.command_tx
|
| 616 |
+
.send(RemoteClientCommand::RejectServerRequest {
|
| 617 |
+
request_id,
|
| 618 |
+
error,
|
| 619 |
+
response_tx,
|
| 620 |
+
})
|
| 621 |
+
.await
|
| 622 |
+
.map_err(|_| {
|
| 623 |
+
IoError::new(
|
| 624 |
+
ErrorKind::BrokenPipe,
|
| 625 |
+
"remote app-server worker channel is closed",
|
| 626 |
+
)
|
| 627 |
+
})?;
|
| 628 |
+
response_rx.await.map_err(|_| {
|
| 629 |
+
IoError::new(
|
| 630 |
+
ErrorKind::BrokenPipe,
|
| 631 |
+
"remote app-server reject channel is closed",
|
| 632 |
+
)
|
| 633 |
+
})?
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
pub async fn next_event(&mut self) -> Option<AppServerEvent> {
|
| 637 |
+
if let Some(event) = self.pending_events.pop_front() {
|
| 638 |
+
return Some(event);
|
| 639 |
+
}
|
| 640 |
+
self.event_rx.recv().await
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
pub async fn shutdown(self) -> IoResult<()> {
|
| 644 |
+
let Self {
|
| 645 |
+
command_tx,
|
| 646 |
+
event_rx,
|
| 647 |
+
pending_events: _pending_events,
|
| 648 |
+
metadata: _,
|
| 649 |
+
worker_handle,
|
| 650 |
+
} = self;
|
| 651 |
+
let mut worker_handle = worker_handle;
|
| 652 |
+
drop(event_rx);
|
| 653 |
+
let (response_tx, response_rx) = oneshot::channel();
|
| 654 |
+
if command_tx
|
| 655 |
+
.send(RemoteClientCommand::Shutdown { response_tx })
|
| 656 |
+
.await
|
| 657 |
+
.is_ok()
|
| 658 |
+
&& let Ok(Ok(close_result)) = timeout(SHUTDOWN_TIMEOUT, response_rx).await
|
| 659 |
+
{
|
| 660 |
+
close_result?;
|
| 661 |
+
}
|
| 662 |
+
|
| 663 |
+
if let Err(_elapsed) = timeout(SHUTDOWN_TIMEOUT, &mut worker_handle).await {
|
| 664 |
+
worker_handle.abort();
|
| 665 |
+
let _ = worker_handle.await;
|
| 666 |
+
}
|
| 667 |
+
Ok(())
|
| 668 |
+
}
|
| 669 |
+
}
|
| 670 |
+
|
| 671 |
+
impl RemoteAppServerRequestHandle {
|
| 672 |
+
pub async fn request(&self, request: ClientRequest) -> IoResult<RequestResult> {
|
| 673 |
+
self.request_json_rpc(jsonrpc_request_from_client_request(request))
|
| 674 |
+
.await
|
| 675 |
+
}
|
| 676 |
+
|
| 677 |
+
pub async fn request_json_rpc(&self, request: JSONRPCRequest) -> IoResult<RequestResult> {
|
| 678 |
+
let (response_tx, response_rx) = oneshot::channel();
|
| 679 |
+
self.command_tx
|
| 680 |
+
.send(RemoteClientCommand::Request {
|
| 681 |
+
request: Box::new(request),
|
| 682 |
+
response_tx,
|
| 683 |
+
})
|
| 684 |
+
.await
|
| 685 |
+
.map_err(|_| {
|
| 686 |
+
IoError::new(
|
| 687 |
+
ErrorKind::BrokenPipe,
|
| 688 |
+
"remote app-server worker channel is closed",
|
| 689 |
+
)
|
| 690 |
+
})?;
|
| 691 |
+
response_rx.await.map_err(|_| {
|
| 692 |
+
IoError::new(
|
| 693 |
+
ErrorKind::BrokenPipe,
|
| 694 |
+
"remote app-server request channel is closed",
|
| 695 |
+
)
|
| 696 |
+
})?
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
pub async fn request_typed<T>(&self, request: ClientRequest) -> Result<T, TypedRequestError>
|
| 700 |
+
where
|
| 701 |
+
T: DeserializeOwned,
|
| 702 |
+
{
|
| 703 |
+
let method = request.method_name();
|
| 704 |
+
let response =
|
| 705 |
+
self.request(request)
|
| 706 |
+
.await
|
| 707 |
+
.map_err(|source| TypedRequestError::Transport {
|
| 708 |
+
method: method.to_string(),
|
| 709 |
+
source,
|
| 710 |
+
})?;
|
| 711 |
+
let result = response.map_err(|source| TypedRequestError::Server {
|
| 712 |
+
method: method.to_string(),
|
| 713 |
+
source,
|
| 714 |
+
})?;
|
| 715 |
+
serde_json::from_value(result).map_err(|source| TypedRequestError::Deserialize {
|
| 716 |
+
method: method.to_string(),
|
| 717 |
+
source,
|
| 718 |
+
})
|
| 719 |
+
}
|
| 720 |
+
}
|
| 721 |
+
|
| 722 |
+
async fn connect_websocket_endpoint(
|
| 723 |
+
websocket_url: String,
|
| 724 |
+
auth_token: Option<String>,
|
| 725 |
+
) -> IoResult<(String, WebSocketStream<MaybeTlsStream<TcpStream>>)> {
|
| 726 |
+
let url = Url::parse(&websocket_url).map_err(|err| {
|
| 727 |
+
IoError::new(
|
| 728 |
+
ErrorKind::InvalidInput,
|
| 729 |
+
format!("invalid websocket URL `{websocket_url}`: {err}"),
|
| 730 |
+
)
|
| 731 |
+
})?;
|
| 732 |
+
if auth_token.is_some() && !websocket_url_supports_auth_token(&url) {
|
| 733 |
+
return Err(IoError::new(
|
| 734 |
+
ErrorKind::InvalidInput,
|
| 735 |
+
format!(
|
| 736 |
+
"remote auth tokens require `wss://` or loopback `ws://` URLs; got `{websocket_url}`"
|
| 737 |
+
),
|
| 738 |
+
));
|
| 739 |
+
}
|
| 740 |
+
|
| 741 |
+
let mut request = url.as_str().into_client_request().map_err(|err| {
|
| 742 |
+
IoError::new(
|
| 743 |
+
ErrorKind::InvalidInput,
|
| 744 |
+
format!("invalid websocket URL `{websocket_url}`: {err}"),
|
| 745 |
+
)
|
| 746 |
+
})?;
|
| 747 |
+
if let Some(auth_token) = auth_token.as_deref() {
|
| 748 |
+
let header_value =
|
| 749 |
+
HeaderValue::from_str(&format!("Bearer {auth_token}")).map_err(|err| {
|
| 750 |
+
IoError::new(
|
| 751 |
+
ErrorKind::InvalidInput,
|
| 752 |
+
format!("invalid remote authorization header value: {err}"),
|
| 753 |
+
)
|
| 754 |
+
})?;
|
| 755 |
+
request.headers_mut().insert(AUTHORIZATION, header_value);
|
| 756 |
+
}
|
| 757 |
+
|
| 758 |
+
ensure_rustls_crypto_provider();
|
| 759 |
+
let websocket_config = remote_websocket_config();
|
| 760 |
+
let stream = timeout(
|
| 761 |
+
CONNECT_TIMEOUT,
|
| 762 |
+
connect_async_with_config(
|
| 763 |
+
request,
|
| 764 |
+
Some(websocket_config),
|
| 765 |
+
/*disable_nagle*/ false,
|
| 766 |
+
),
|
| 767 |
+
)
|
| 768 |
+
.await
|
| 769 |
+
.map_err(|_| {
|
| 770 |
+
IoError::new(
|
| 771 |
+
ErrorKind::TimedOut,
|
| 772 |
+
format!("timed out connecting to remote app server at `{websocket_url}`"),
|
| 773 |
+
)
|
| 774 |
+
})?
|
| 775 |
+
.map(|(stream, _response)| stream)
|
| 776 |
+
.map_err(|err| {
|
| 777 |
+
IoError::other(format!(
|
| 778 |
+
"failed to connect to remote app server at `{websocket_url}`: {err}"
|
| 779 |
+
))
|
| 780 |
+
})?;
|
| 781 |
+
|
| 782 |
+
Ok((websocket_url, stream))
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
async fn connect_unix_socket_endpoint(
|
| 786 |
+
socket_path: AbsolutePathBuf,
|
| 787 |
+
peer_policy: SocketPeerPolicy,
|
| 788 |
+
) -> IoResult<(String, WebSocketStream<UnixStream>)> {
|
| 789 |
+
let endpoint = format!("unix://{}", socket_path.display());
|
| 790 |
+
let request = UDS_WEBSOCKET_HANDSHAKE_URL
|
| 791 |
+
.into_client_request()
|
| 792 |
+
.map_err(|err| {
|
| 793 |
+
IoError::new(
|
| 794 |
+
ErrorKind::InvalidInput,
|
| 795 |
+
format!("invalid UDS websocket handshake URL: {err}"),
|
| 796 |
+
)
|
| 797 |
+
})?;
|
| 798 |
+
let stream = timeout(CONNECT_TIMEOUT, UnixStream::connect(socket_path.as_path()))
|
| 799 |
+
.await
|
| 800 |
+
.map_err(|_| {
|
| 801 |
+
IoError::new(
|
| 802 |
+
ErrorKind::TimedOut,
|
| 803 |
+
format!("timed out connecting to remote app server at `{endpoint}`"),
|
| 804 |
+
)
|
| 805 |
+
})?
|
| 806 |
+
.map_err(|err| {
|
| 807 |
+
IoError::other(format!(
|
| 808 |
+
"failed to connect to remote app server at `{endpoint}`: {err}"
|
| 809 |
+
))
|
| 810 |
+
})?;
|
| 811 |
+
match peer_policy {
|
| 812 |
+
SocketPeerPolicy::ExplicitEndpoint => {}
|
| 813 |
+
#[cfg(windows)]
|
| 814 |
+
SocketPeerPolicy::NonElevatedCurrentUser => stream.ensure_non_elevated_peer()?,
|
| 815 |
+
}
|
| 816 |
+
let websocket_config = remote_websocket_config();
|
| 817 |
+
let stream = timeout(
|
| 818 |
+
CONNECT_TIMEOUT,
|
| 819 |
+
client_async_with_config(request, stream, Some(websocket_config)),
|
| 820 |
+
)
|
| 821 |
+
.await
|
| 822 |
+
.map_err(|_| {
|
| 823 |
+
IoError::new(
|
| 824 |
+
ErrorKind::TimedOut,
|
| 825 |
+
format!("timed out upgrading remote app server at `{endpoint}`"),
|
| 826 |
+
)
|
| 827 |
+
})?
|
| 828 |
+
.map(|(stream, _response)| stream)
|
| 829 |
+
.map_err(|err| {
|
| 830 |
+
IoError::other(format!(
|
| 831 |
+
"failed to upgrade remote app server at `{endpoint}`: {err}"
|
| 832 |
+
))
|
| 833 |
+
})?;
|
| 834 |
+
|
| 835 |
+
Ok((endpoint, stream))
|
| 836 |
+
}
|
| 837 |
+
|
| 838 |
+
fn remote_websocket_config() -> WebSocketConfig {
|
| 839 |
+
WebSocketConfig::default()
|
| 840 |
+
.max_frame_size(Some(REMOTE_APP_SERVER_MAX_WEBSOCKET_MESSAGE_SIZE))
|
| 841 |
+
.max_message_size(Some(REMOTE_APP_SERVER_MAX_WEBSOCKET_MESSAGE_SIZE))
|
| 842 |
+
}
|
| 843 |
+
|
| 844 |
+
async fn initialize_remote_connection<S>(
|
| 845 |
+
stream: &mut WebSocketStream<S>,
|
| 846 |
+
endpoint: &str,
|
| 847 |
+
params: InitializeParams,
|
| 848 |
+
initialize_timeout: Duration,
|
| 849 |
+
) -> IoResult<(Vec<AppServerEvent>, RemoteServerMetadata)>
|
| 850 |
+
where
|
| 851 |
+
S: AsyncRead + AsyncWrite + Unpin,
|
| 852 |
+
{
|
| 853 |
+
let initialize_request_id = RequestId::String("initialize".to_string());
|
| 854 |
+
let mut pending_events = Vec::new();
|
| 855 |
+
let mut metadata = RemoteServerMetadata::default();
|
| 856 |
+
write_jsonrpc_message(
|
| 857 |
+
stream,
|
| 858 |
+
JSONRPCMessage::Request(jsonrpc_request_from_client_request(
|
| 859 |
+
ClientRequest::Initialize {
|
| 860 |
+
request_id: initialize_request_id.clone(),
|
| 861 |
+
params,
|
| 862 |
+
},
|
| 863 |
+
)),
|
| 864 |
+
endpoint,
|
| 865 |
+
)
|
| 866 |
+
.await?;
|
| 867 |
+
|
| 868 |
+
timeout(initialize_timeout, async {
|
| 869 |
+
loop {
|
| 870 |
+
match stream.next().await {
|
| 871 |
+
Some(Ok(Message::Text(text))) => {
|
| 872 |
+
let message = serde_json::from_str::<JSONRPCMessage>(&text).map_err(|err| {
|
| 873 |
+
IoError::other(format!(
|
| 874 |
+
"remote app server at `{endpoint}` sent invalid initialize response: {err}"
|
| 875 |
+
))
|
| 876 |
+
})?;
|
| 877 |
+
match message {
|
| 878 |
+
JSONRPCMessage::Response(response) if response.id == initialize_request_id => {
|
| 879 |
+
metadata.server_version = response
|
| 880 |
+
.result
|
| 881 |
+
.get("userAgent")
|
| 882 |
+
.and_then(serde_json::Value::as_str)
|
| 883 |
+
.and_then(|user_agent| {
|
| 884 |
+
let (_, rest) = user_agent.split_once('/')?;
|
| 885 |
+
rest.split_whitespace().next().map(str::to_string)
|
| 886 |
+
});
|
| 887 |
+
metadata.codex_home = response
|
| 888 |
+
.result
|
| 889 |
+
.get("codexHome")
|
| 890 |
+
.and_then(serde_json::Value::as_str)
|
| 891 |
+
.filter(|codex_home| !codex_home.is_empty())
|
| 892 |
+
.map(str::to_string);
|
| 893 |
+
metadata.platform_family = response.result.get("platformFamily")
|
| 894 |
+
.and_then(serde_json::Value::as_str).map(str::to_string);
|
| 895 |
+
metadata.platform_os = response.result.get("platformOs")
|
| 896 |
+
.and_then(serde_json::Value::as_str).map(str::to_string);
|
| 897 |
+
break Ok(());
|
| 898 |
+
}
|
| 899 |
+
JSONRPCMessage::Error(error) if error.id == initialize_request_id => {
|
| 900 |
+
break Err(IoError::other(format!(
|
| 901 |
+
"remote app server at `{endpoint}` rejected initialize: {}",
|
| 902 |
+
error.error.message
|
| 903 |
+
)));
|
| 904 |
+
}
|
| 905 |
+
JSONRPCMessage::Notification(notification) => {
|
| 906 |
+
if let Some(event) = app_server_event_from_notification(notification) {
|
| 907 |
+
pending_events.push(event);
|
| 908 |
+
}
|
| 909 |
+
}
|
| 910 |
+
JSONRPCMessage::Request(request) => {
|
| 911 |
+
let request_id = request.id.clone();
|
| 912 |
+
let method = request.method.clone();
|
| 913 |
+
match ServerRequest::try_from(request) {
|
| 914 |
+
Ok(request) => {
|
| 915 |
+
pending_events
|
| 916 |
+
.push(AppServerEvent::ServerRequest(Box::new(request)));
|
| 917 |
+
}
|
| 918 |
+
Err(err) => {
|
| 919 |
+
warn!(%err, method, "rejecting unknown remote app-server request during initialize");
|
| 920 |
+
write_jsonrpc_message(
|
| 921 |
+
stream,
|
| 922 |
+
JSONRPCMessage::Error(JSONRPCError {
|
| 923 |
+
error: JSONRPCErrorError {
|
| 924 |
+
code: -32601,
|
| 925 |
+
message: format!(
|
| 926 |
+
"unsupported remote app-server request `{method}`"
|
| 927 |
+
),
|
| 928 |
+
data: None,
|
| 929 |
+
},
|
| 930 |
+
id: request_id,
|
| 931 |
+
}),
|
| 932 |
+
endpoint,
|
| 933 |
+
)
|
| 934 |
+
.await?;
|
| 935 |
+
}
|
| 936 |
+
}
|
| 937 |
+
}
|
| 938 |
+
JSONRPCMessage::Response(_) | JSONRPCMessage::Error(_) => {}
|
| 939 |
+
}
|
| 940 |
+
}
|
| 941 |
+
Some(Ok(Message::Binary(_)))
|
| 942 |
+
| Some(Ok(Message::Ping(_)))
|
| 943 |
+
| Some(Ok(Message::Pong(_)))
|
| 944 |
+
| Some(Ok(Message::Frame(_))) => {}
|
| 945 |
+
Some(Ok(Message::Close(frame))) => {
|
| 946 |
+
let reason = frame
|
| 947 |
+
.as_ref()
|
| 948 |
+
.map(|frame| frame.reason.to_string())
|
| 949 |
+
.filter(|reason| !reason.is_empty())
|
| 950 |
+
.unwrap_or_else(|| "connection closed during initialize".to_string());
|
| 951 |
+
break Err(IoError::new(
|
| 952 |
+
ErrorKind::ConnectionAborted,
|
| 953 |
+
format!(
|
| 954 |
+
"remote app server at `{endpoint}` closed during initialize: {reason}"
|
| 955 |
+
),
|
| 956 |
+
));
|
| 957 |
+
}
|
| 958 |
+
Some(Err(err)) => {
|
| 959 |
+
break Err(IoError::other(format!(
|
| 960 |
+
"remote app server at `{endpoint}` transport failed during initialize: {err}"
|
| 961 |
+
)));
|
| 962 |
+
}
|
| 963 |
+
None => {
|
| 964 |
+
break Err(IoError::new(
|
| 965 |
+
ErrorKind::UnexpectedEof,
|
| 966 |
+
format!("remote app server at `{endpoint}` closed during initialize"),
|
| 967 |
+
));
|
| 968 |
+
}
|
| 969 |
+
}
|
| 970 |
+
}
|
| 971 |
+
})
|
| 972 |
+
.await
|
| 973 |
+
.map_err(|_| {
|
| 974 |
+
IoError::new(
|
| 975 |
+
ErrorKind::TimedOut,
|
| 976 |
+
format!("timed out waiting for initialize response from `{endpoint}`"),
|
| 977 |
+
)
|
| 978 |
+
})??;
|
| 979 |
+
|
| 980 |
+
write_jsonrpc_message(
|
| 981 |
+
stream,
|
| 982 |
+
JSONRPCMessage::Notification(jsonrpc_notification_from_client_notification(
|
| 983 |
+
ClientNotification::Initialized,
|
| 984 |
+
)),
|
| 985 |
+
endpoint,
|
| 986 |
+
)
|
| 987 |
+
.await?;
|
| 988 |
+
|
| 989 |
+
Ok((pending_events, metadata))
|
| 990 |
+
}
|
| 991 |
+
|
| 992 |
+
fn app_server_event_from_notification(notification: JSONRPCNotification) -> Option<AppServerEvent> {
|
| 993 |
+
match ServerNotification::try_from(notification) {
|
| 994 |
+
Ok(notification) => Some(AppServerEvent::ServerNotification(Box::new(notification))),
|
| 995 |
+
Err(_) => None,
|
| 996 |
+
}
|
| 997 |
+
}
|
| 998 |
+
|
| 999 |
+
fn deliver_event(
|
| 1000 |
+
event_tx: &mpsc::UnboundedSender<AppServerEvent>,
|
| 1001 |
+
event: AppServerEvent,
|
| 1002 |
+
) -> IoResult<()> {
|
| 1003 |
+
event_tx.send(event).map_err(|_| {
|
| 1004 |
+
IoError::new(
|
| 1005 |
+
ErrorKind::BrokenPipe,
|
| 1006 |
+
"remote app-server event consumer channel is closed",
|
| 1007 |
+
)
|
| 1008 |
+
})
|
| 1009 |
+
}
|
| 1010 |
+
|
| 1011 |
+
fn jsonrpc_request_from_client_request(request: ClientRequest) -> JSONRPCRequest {
|
| 1012 |
+
let value = match serde_json::to_value(request) {
|
| 1013 |
+
Ok(value) => value,
|
| 1014 |
+
Err(err) => panic!("client request should serialize: {err}"),
|
| 1015 |
+
};
|
| 1016 |
+
match serde_json::from_value(value) {
|
| 1017 |
+
Ok(request) => request,
|
| 1018 |
+
Err(err) => panic!("client request should encode as JSON-RPC request: {err}"),
|
| 1019 |
+
}
|
| 1020 |
+
}
|
| 1021 |
+
|
| 1022 |
+
fn jsonrpc_notification_from_client_notification(
|
| 1023 |
+
notification: ClientNotification,
|
| 1024 |
+
) -> JSONRPCNotification {
|
| 1025 |
+
let value = match serde_json::to_value(notification) {
|
| 1026 |
+
Ok(value) => value,
|
| 1027 |
+
Err(err) => panic!("client notification should serialize: {err}"),
|
| 1028 |
+
};
|
| 1029 |
+
match serde_json::from_value(value) {
|
| 1030 |
+
Ok(notification) => notification,
|
| 1031 |
+
Err(err) => panic!("client notification should encode as JSON-RPC notification: {err}"),
|
| 1032 |
+
}
|
| 1033 |
+
}
|
| 1034 |
+
|
| 1035 |
+
async fn write_jsonrpc_message<S>(
|
| 1036 |
+
stream: &mut WebSocketStream<S>,
|
| 1037 |
+
message: JSONRPCMessage,
|
| 1038 |
+
endpoint: &str,
|
| 1039 |
+
) -> IoResult<()>
|
| 1040 |
+
where
|
| 1041 |
+
S: AsyncRead + AsyncWrite + Unpin,
|
| 1042 |
+
{
|
| 1043 |
+
let payload = serde_json::to_string(&message).map_err(IoError::other)?;
|
| 1044 |
+
stream
|
| 1045 |
+
.send(Message::Text(payload.into()))
|
| 1046 |
+
.await
|
| 1047 |
+
.map_err(|err| {
|
| 1048 |
+
IoError::other(format!(
|
| 1049 |
+
"failed to write websocket message to `{endpoint}`: {err}"
|
| 1050 |
+
))
|
| 1051 |
+
})
|
| 1052 |
+
}
|
| 1053 |
+
|
| 1054 |
+
fn websocket_close_error_is_already_closed(err: &TungsteniteError) -> bool {
|
| 1055 |
+
match err {
|
| 1056 |
+
TungsteniteError::ConnectionClosed | TungsteniteError::AlreadyClosed => true,
|
| 1057 |
+
TungsteniteError::Io(err) => matches!(
|
| 1058 |
+
err.kind(),
|
| 1059 |
+
ErrorKind::BrokenPipe | ErrorKind::ConnectionReset | ErrorKind::NotConnected
|
| 1060 |
+
),
|
| 1061 |
+
_ => false,
|
| 1062 |
+
}
|
| 1063 |
+
}
|
| 1064 |
+
#[cfg(test)]
|
| 1065 |
+
mod tests {
|
| 1066 |
+
use super::*;
|
| 1067 |
+
|
| 1068 |
+
#[tokio::test]
|
| 1069 |
+
async fn shutdown_tolerates_worker_exit_after_command_is_queued() {
|
| 1070 |
+
let (command_tx, mut command_rx) = mpsc::channel(1);
|
| 1071 |
+
let (_event_tx, event_rx) = mpsc::unbounded_channel::<AppServerEvent>();
|
| 1072 |
+
let worker_handle = tokio::spawn(async move {
|
| 1073 |
+
let _ = command_rx.recv().await;
|
| 1074 |
+
});
|
| 1075 |
+
let client = RemoteAppServerClient {
|
| 1076 |
+
command_tx,
|
| 1077 |
+
event_rx,
|
| 1078 |
+
pending_events: VecDeque::new(),
|
| 1079 |
+
metadata: RemoteServerMetadata::default(),
|
| 1080 |
+
worker_handle,
|
| 1081 |
+
};
|
| 1082 |
+
|
| 1083 |
+
client
|
| 1084 |
+
.shutdown()
|
| 1085 |
+
.await
|
| 1086 |
+
.expect("shutdown should complete when worker exits first");
|
| 1087 |
+
}
|
| 1088 |
+
}
|
codex-rs/app-server-protocol/schema/json/AttestationGenerateResponse.json
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"properties": {
|
| 4 |
+
"token": {
|
| 5 |
+
"description": "Opaque client attestation token.",
|
| 6 |
+
"type": "string"
|
| 7 |
+
}
|
| 8 |
+
},
|
| 9 |
+
"required": [
|
| 10 |
+
"token"
|
| 11 |
+
],
|
| 12 |
+
"title": "AttestationGenerateResponse",
|
| 13 |
+
"type": "object"
|
| 14 |
+
}
|
codex-rs/app-server-protocol/schema/json/ClientNotification.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"oneOf": [
|
| 4 |
+
{
|
| 5 |
+
"properties": {
|
| 6 |
+
"method": {
|
| 7 |
+
"enum": [
|
| 8 |
+
"initialized"
|
| 9 |
+
],
|
| 10 |
+
"title": "InitializedNotificationMethod",
|
| 11 |
+
"type": "string"
|
| 12 |
+
}
|
| 13 |
+
},
|
| 14 |
+
"required": [
|
| 15 |
+
"method"
|
| 16 |
+
],
|
| 17 |
+
"title": "InitializedNotification",
|
| 18 |
+
"type": "object"
|
| 19 |
+
}
|
| 20 |
+
],
|
| 21 |
+
"title": "ClientNotification"
|
| 22 |
+
}
|
codex-rs/app-server-protocol/schema/json/CommandExecutionRequestApprovalResponse.json
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"definitions": {
|
| 4 |
+
"CommandExecutionApprovalDecision": {
|
| 5 |
+
"oneOf": [
|
| 6 |
+
{
|
| 7 |
+
"description": "User approved the command.",
|
| 8 |
+
"enum": [
|
| 9 |
+
"accept"
|
| 10 |
+
],
|
| 11 |
+
"type": "string"
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"description": "User approved the command and future prompts in the same session-scoped approval cache should run without prompting.",
|
| 15 |
+
"enum": [
|
| 16 |
+
"acceptForSession"
|
| 17 |
+
],
|
| 18 |
+
"type": "string"
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"additionalProperties": false,
|
| 22 |
+
"description": "User approved the command, and wants to apply the proposed execpolicy amendment so future matching commands can run without prompting.",
|
| 23 |
+
"properties": {
|
| 24 |
+
"acceptWithExecpolicyAmendment": {
|
| 25 |
+
"properties": {
|
| 26 |
+
"execpolicy_amendment": {
|
| 27 |
+
"items": {
|
| 28 |
+
"type": "string"
|
| 29 |
+
},
|
| 30 |
+
"type": "array"
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
"required": [
|
| 34 |
+
"execpolicy_amendment"
|
| 35 |
+
],
|
| 36 |
+
"type": "object"
|
| 37 |
+
}
|
| 38 |
+
},
|
| 39 |
+
"required": [
|
| 40 |
+
"acceptWithExecpolicyAmendment"
|
| 41 |
+
],
|
| 42 |
+
"title": "AcceptWithExecpolicyAmendmentCommandExecutionApprovalDecision",
|
| 43 |
+
"type": "object"
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
"additionalProperties": false,
|
| 47 |
+
"description": "User chose a persistent network policy rule (allow/deny) for this host.",
|
| 48 |
+
"properties": {
|
| 49 |
+
"applyNetworkPolicyAmendment": {
|
| 50 |
+
"properties": {
|
| 51 |
+
"network_policy_amendment": {
|
| 52 |
+
"$ref": "#/definitions/NetworkPolicyAmendment"
|
| 53 |
+
}
|
| 54 |
+
},
|
| 55 |
+
"required": [
|
| 56 |
+
"network_policy_amendment"
|
| 57 |
+
],
|
| 58 |
+
"type": "object"
|
| 59 |
+
}
|
| 60 |
+
},
|
| 61 |
+
"required": [
|
| 62 |
+
"applyNetworkPolicyAmendment"
|
| 63 |
+
],
|
| 64 |
+
"title": "ApplyNetworkPolicyAmendmentCommandExecutionApprovalDecision",
|
| 65 |
+
"type": "object"
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"description": "User denied the command. The agent will continue the turn.",
|
| 69 |
+
"enum": [
|
| 70 |
+
"decline"
|
| 71 |
+
],
|
| 72 |
+
"type": "string"
|
| 73 |
+
},
|
| 74 |
+
{
|
| 75 |
+
"description": "User denied the command. The turn will also be immediately interrupted.",
|
| 76 |
+
"enum": [
|
| 77 |
+
"cancel"
|
| 78 |
+
],
|
| 79 |
+
"type": "string"
|
| 80 |
+
}
|
| 81 |
+
]
|
| 82 |
+
},
|
| 83 |
+
"NetworkPolicyAmendment": {
|
| 84 |
+
"properties": {
|
| 85 |
+
"action": {
|
| 86 |
+
"$ref": "#/definitions/NetworkPolicyRuleAction"
|
| 87 |
+
},
|
| 88 |
+
"host": {
|
| 89 |
+
"type": "string"
|
| 90 |
+
}
|
| 91 |
+
},
|
| 92 |
+
"required": [
|
| 93 |
+
"action",
|
| 94 |
+
"host"
|
| 95 |
+
],
|
| 96 |
+
"type": "object"
|
| 97 |
+
},
|
| 98 |
+
"NetworkPolicyRuleAction": {
|
| 99 |
+
"enum": [
|
| 100 |
+
"allow",
|
| 101 |
+
"deny"
|
| 102 |
+
],
|
| 103 |
+
"type": "string"
|
| 104 |
+
}
|
| 105 |
+
},
|
| 106 |
+
"properties": {
|
| 107 |
+
"decision": {
|
| 108 |
+
"$ref": "#/definitions/CommandExecutionApprovalDecision"
|
| 109 |
+
}
|
| 110 |
+
},
|
| 111 |
+
"required": [
|
| 112 |
+
"decision"
|
| 113 |
+
],
|
| 114 |
+
"title": "CommandExecutionRequestApprovalResponse",
|
| 115 |
+
"type": "object"
|
| 116 |
+
}
|
codex-rs/app-server-protocol/schema/json/ExecCommandApprovalResponse.json
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"definitions": {
|
| 4 |
+
"NetworkPolicyAmendment": {
|
| 5 |
+
"properties": {
|
| 6 |
+
"action": {
|
| 7 |
+
"$ref": "#/definitions/NetworkPolicyRuleAction"
|
| 8 |
+
},
|
| 9 |
+
"host": {
|
| 10 |
+
"type": "string"
|
| 11 |
+
}
|
| 12 |
+
},
|
| 13 |
+
"required": [
|
| 14 |
+
"action",
|
| 15 |
+
"host"
|
| 16 |
+
],
|
| 17 |
+
"type": "object"
|
| 18 |
+
},
|
| 19 |
+
"NetworkPolicyRuleAction": {
|
| 20 |
+
"enum": [
|
| 21 |
+
"allow",
|
| 22 |
+
"deny"
|
| 23 |
+
],
|
| 24 |
+
"type": "string"
|
| 25 |
+
},
|
| 26 |
+
"ReviewDecision": {
|
| 27 |
+
"description": "User's decision in response to an ExecApprovalRequest.",
|
| 28 |
+
"oneOf": [
|
| 29 |
+
{
|
| 30 |
+
"description": "User has approved this command and the agent should execute it.",
|
| 31 |
+
"enum": [
|
| 32 |
+
"approved"
|
| 33 |
+
],
|
| 34 |
+
"type": "string"
|
| 35 |
+
},
|
| 36 |
+
{
|
| 37 |
+
"additionalProperties": false,
|
| 38 |
+
"description": "User has approved this command and wants to apply the proposed execpolicy amendment so future matching commands are permitted.",
|
| 39 |
+
"properties": {
|
| 40 |
+
"approved_execpolicy_amendment": {
|
| 41 |
+
"properties": {
|
| 42 |
+
"proposed_execpolicy_amendment": {
|
| 43 |
+
"items": {
|
| 44 |
+
"type": "string"
|
| 45 |
+
},
|
| 46 |
+
"type": "array"
|
| 47 |
+
}
|
| 48 |
+
},
|
| 49 |
+
"required": [
|
| 50 |
+
"proposed_execpolicy_amendment"
|
| 51 |
+
],
|
| 52 |
+
"type": "object"
|
| 53 |
+
}
|
| 54 |
+
},
|
| 55 |
+
"required": [
|
| 56 |
+
"approved_execpolicy_amendment"
|
| 57 |
+
],
|
| 58 |
+
"title": "ApprovedExecpolicyAmendmentReviewDecision",
|
| 59 |
+
"type": "object"
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"description": "User has approved this request and wants future prompts in the same session-scoped approval cache to be automatically approved for the remainder of the session.",
|
| 63 |
+
"enum": [
|
| 64 |
+
"approved_for_session"
|
| 65 |
+
],
|
| 66 |
+
"type": "string"
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"description": "User has approved this MCP tool call and wants to amend its policy so matching future calls are automatically approved across sessions.",
|
| 70 |
+
"enum": [
|
| 71 |
+
"approved_mcp_policy_amendment"
|
| 72 |
+
],
|
| 73 |
+
"type": "string"
|
| 74 |
+
},
|
| 75 |
+
{
|
| 76 |
+
"additionalProperties": false,
|
| 77 |
+
"description": "User chose to persist a network policy rule (allow/deny) for future requests to the same host.",
|
| 78 |
+
"properties": {
|
| 79 |
+
"network_policy_amendment": {
|
| 80 |
+
"properties": {
|
| 81 |
+
"network_policy_amendment": {
|
| 82 |
+
"$ref": "#/definitions/NetworkPolicyAmendment"
|
| 83 |
+
}
|
| 84 |
+
},
|
| 85 |
+
"required": [
|
| 86 |
+
"network_policy_amendment"
|
| 87 |
+
],
|
| 88 |
+
"type": "object"
|
| 89 |
+
}
|
| 90 |
+
},
|
| 91 |
+
"required": [
|
| 92 |
+
"network_policy_amendment"
|
| 93 |
+
],
|
| 94 |
+
"title": "NetworkPolicyAmendmentReviewDecision",
|
| 95 |
+
"type": "object"
|
| 96 |
+
},
|
| 97 |
+
{
|
| 98 |
+
"additionalProperties": false,
|
| 99 |
+
"description": "User has denied this command and the agent should not execute it, but it should continue the session and try something else.",
|
| 100 |
+
"properties": {
|
| 101 |
+
"denied": {
|
| 102 |
+
"properties": {
|
| 103 |
+
"rejection": {
|
| 104 |
+
"type": "string"
|
| 105 |
+
}
|
| 106 |
+
},
|
| 107 |
+
"required": [
|
| 108 |
+
"rejection"
|
| 109 |
+
],
|
| 110 |
+
"type": "object"
|
| 111 |
+
}
|
| 112 |
+
},
|
| 113 |
+
"required": [
|
| 114 |
+
"denied"
|
| 115 |
+
],
|
| 116 |
+
"title": "DeniedReviewDecision",
|
| 117 |
+
"type": "object"
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"description": "Automatic approval review timed out before reaching a decision.",
|
| 121 |
+
"enum": [
|
| 122 |
+
"timed_out"
|
| 123 |
+
],
|
| 124 |
+
"type": "string"
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"description": "User has denied this command and the agent should not do anything until the user's next command.",
|
| 128 |
+
"enum": [
|
| 129 |
+
"abort"
|
| 130 |
+
],
|
| 131 |
+
"type": "string"
|
| 132 |
+
}
|
| 133 |
+
]
|
| 134 |
+
}
|
| 135 |
+
},
|
| 136 |
+
"properties": {
|
| 137 |
+
"decision": {
|
| 138 |
+
"$ref": "#/definitions/ReviewDecision"
|
| 139 |
+
}
|
| 140 |
+
},
|
| 141 |
+
"required": [
|
| 142 |
+
"decision"
|
| 143 |
+
],
|
| 144 |
+
"title": "ExecCommandApprovalResponse",
|
| 145 |
+
"type": "object"
|
| 146 |
+
}
|
codex-rs/app-server-protocol/schema/json/FileChangeRequestApprovalResponse.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"definitions": {
|
| 4 |
+
"FileChangeApprovalDecision": {
|
| 5 |
+
"oneOf": [
|
| 6 |
+
{
|
| 7 |
+
"description": "User approved the file changes.",
|
| 8 |
+
"enum": [
|
| 9 |
+
"accept"
|
| 10 |
+
],
|
| 11 |
+
"type": "string"
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"description": "User approved the file changes and future changes to the same files should run without prompting.",
|
| 15 |
+
"enum": [
|
| 16 |
+
"acceptForSession"
|
| 17 |
+
],
|
| 18 |
+
"type": "string"
|
| 19 |
+
},
|
| 20 |
+
{
|
| 21 |
+
"description": "User denied the file changes. The agent will continue the turn.",
|
| 22 |
+
"enum": [
|
| 23 |
+
"decline"
|
| 24 |
+
],
|
| 25 |
+
"type": "string"
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"description": "User denied the file changes. The turn will also be immediately interrupted.",
|
| 29 |
+
"enum": [
|
| 30 |
+
"cancel"
|
| 31 |
+
],
|
| 32 |
+
"type": "string"
|
| 33 |
+
}
|
| 34 |
+
]
|
| 35 |
+
}
|
| 36 |
+
},
|
| 37 |
+
"properties": {
|
| 38 |
+
"decision": {
|
| 39 |
+
"$ref": "#/definitions/FileChangeApprovalDecision"
|
| 40 |
+
}
|
| 41 |
+
},
|
| 42 |
+
"required": [
|
| 43 |
+
"decision"
|
| 44 |
+
],
|
| 45 |
+
"title": "FileChangeRequestApprovalResponse",
|
| 46 |
+
"type": "object"
|
| 47 |
+
}
|
codex-rs/app-server-protocol/schema/json/FuzzyFileSearchParams.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"properties": {
|
| 4 |
+
"cancellationToken": {
|
| 5 |
+
"type": [
|
| 6 |
+
"string",
|
| 7 |
+
"null"
|
| 8 |
+
]
|
| 9 |
+
},
|
| 10 |
+
"query": {
|
| 11 |
+
"type": "string"
|
| 12 |
+
},
|
| 13 |
+
"roots": {
|
| 14 |
+
"items": {
|
| 15 |
+
"type": "string"
|
| 16 |
+
},
|
| 17 |
+
"type": "array"
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"required": [
|
| 21 |
+
"query",
|
| 22 |
+
"roots"
|
| 23 |
+
],
|
| 24 |
+
"title": "FuzzyFileSearchParams",
|
| 25 |
+
"type": "object"
|
| 26 |
+
}
|
codex-rs/app-server-protocol/schema/json/FuzzyFileSearchSessionUpdatedNotification.json
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"definitions": {
|
| 4 |
+
"FuzzyFileSearchMatchType": {
|
| 5 |
+
"enum": [
|
| 6 |
+
"file",
|
| 7 |
+
"directory"
|
| 8 |
+
],
|
| 9 |
+
"type": "string"
|
| 10 |
+
},
|
| 11 |
+
"FuzzyFileSearchResult": {
|
| 12 |
+
"description": "Superset of [`codex_file_search::FileMatch`]",
|
| 13 |
+
"properties": {
|
| 14 |
+
"file_name": {
|
| 15 |
+
"type": "string"
|
| 16 |
+
},
|
| 17 |
+
"indices": {
|
| 18 |
+
"items": {
|
| 19 |
+
"format": "uint32",
|
| 20 |
+
"minimum": 0.0,
|
| 21 |
+
"type": "integer"
|
| 22 |
+
},
|
| 23 |
+
"type": [
|
| 24 |
+
"array",
|
| 25 |
+
"null"
|
| 26 |
+
]
|
| 27 |
+
},
|
| 28 |
+
"match_type": {
|
| 29 |
+
"$ref": "#/definitions/FuzzyFileSearchMatchType"
|
| 30 |
+
},
|
| 31 |
+
"path": {
|
| 32 |
+
"type": "string"
|
| 33 |
+
},
|
| 34 |
+
"root": {
|
| 35 |
+
"type": "string"
|
| 36 |
+
},
|
| 37 |
+
"score": {
|
| 38 |
+
"format": "uint32",
|
| 39 |
+
"minimum": 0.0,
|
| 40 |
+
"type": "integer"
|
| 41 |
+
}
|
| 42 |
+
},
|
| 43 |
+
"required": [
|
| 44 |
+
"file_name",
|
| 45 |
+
"match_type",
|
| 46 |
+
"path",
|
| 47 |
+
"root",
|
| 48 |
+
"score"
|
| 49 |
+
],
|
| 50 |
+
"type": "object"
|
| 51 |
+
}
|
| 52 |
+
},
|
| 53 |
+
"properties": {
|
| 54 |
+
"files": {
|
| 55 |
+
"items": {
|
| 56 |
+
"$ref": "#/definitions/FuzzyFileSearchResult"
|
| 57 |
+
},
|
| 58 |
+
"type": "array"
|
| 59 |
+
},
|
| 60 |
+
"query": {
|
| 61 |
+
"type": "string"
|
| 62 |
+
},
|
| 63 |
+
"sessionId": {
|
| 64 |
+
"type": "string"
|
| 65 |
+
}
|
| 66 |
+
},
|
| 67 |
+
"required": [
|
| 68 |
+
"files",
|
| 69 |
+
"query",
|
| 70 |
+
"sessionId"
|
| 71 |
+
],
|
| 72 |
+
"title": "FuzzyFileSearchSessionUpdatedNotification",
|
| 73 |
+
"type": "object"
|
| 74 |
+
}
|
codex-rs/app-server-protocol/schema/json/JSONRPCError.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"definitions": {
|
| 4 |
+
"JSONRPCErrorError": {
|
| 5 |
+
"properties": {
|
| 6 |
+
"code": {
|
| 7 |
+
"format": "int64",
|
| 8 |
+
"type": "integer"
|
| 9 |
+
},
|
| 10 |
+
"data": true,
|
| 11 |
+
"message": {
|
| 12 |
+
"type": "string"
|
| 13 |
+
}
|
| 14 |
+
},
|
| 15 |
+
"required": [
|
| 16 |
+
"code",
|
| 17 |
+
"message"
|
| 18 |
+
],
|
| 19 |
+
"type": "object"
|
| 20 |
+
},
|
| 21 |
+
"RequestId": {
|
| 22 |
+
"anyOf": [
|
| 23 |
+
{
|
| 24 |
+
"type": "string"
|
| 25 |
+
},
|
| 26 |
+
{
|
| 27 |
+
"format": "int64",
|
| 28 |
+
"type": "integer"
|
| 29 |
+
}
|
| 30 |
+
]
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
"description": "A response to a request that indicates an error occurred.",
|
| 34 |
+
"properties": {
|
| 35 |
+
"error": {
|
| 36 |
+
"$ref": "#/definitions/JSONRPCErrorError"
|
| 37 |
+
},
|
| 38 |
+
"id": {
|
| 39 |
+
"$ref": "#/definitions/RequestId"
|
| 40 |
+
}
|
| 41 |
+
},
|
| 42 |
+
"required": [
|
| 43 |
+
"error",
|
| 44 |
+
"id"
|
| 45 |
+
],
|
| 46 |
+
"title": "JSONRPCError",
|
| 47 |
+
"type": "object"
|
| 48 |
+
}
|
codex-rs/app-server-protocol/schema/json/JSONRPCNotification.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"description": "A notification which does not expect a response.",
|
| 4 |
+
"properties": {
|
| 5 |
+
"method": {
|
| 6 |
+
"type": "string"
|
| 7 |
+
},
|
| 8 |
+
"params": true
|
| 9 |
+
},
|
| 10 |
+
"required": [
|
| 11 |
+
"method"
|
| 12 |
+
],
|
| 13 |
+
"title": "JSONRPCNotification",
|
| 14 |
+
"type": "object"
|
| 15 |
+
}
|
codex-rs/app-server-protocol/schema/json/JSONRPCRequest.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"definitions": {
|
| 4 |
+
"RequestId": {
|
| 5 |
+
"anyOf": [
|
| 6 |
+
{
|
| 7 |
+
"type": "string"
|
| 8 |
+
},
|
| 9 |
+
{
|
| 10 |
+
"format": "int64",
|
| 11 |
+
"type": "integer"
|
| 12 |
+
}
|
| 13 |
+
]
|
| 14 |
+
},
|
| 15 |
+
"W3cTraceContext": {
|
| 16 |
+
"properties": {
|
| 17 |
+
"traceparent": {
|
| 18 |
+
"type": [
|
| 19 |
+
"string",
|
| 20 |
+
"null"
|
| 21 |
+
]
|
| 22 |
+
},
|
| 23 |
+
"tracestate": {
|
| 24 |
+
"type": [
|
| 25 |
+
"string",
|
| 26 |
+
"null"
|
| 27 |
+
]
|
| 28 |
+
}
|
| 29 |
+
},
|
| 30 |
+
"type": "object"
|
| 31 |
+
}
|
| 32 |
+
},
|
| 33 |
+
"description": "A request that expects a response.",
|
| 34 |
+
"properties": {
|
| 35 |
+
"id": {
|
| 36 |
+
"$ref": "#/definitions/RequestId"
|
| 37 |
+
},
|
| 38 |
+
"method": {
|
| 39 |
+
"type": "string"
|
| 40 |
+
},
|
| 41 |
+
"params": true,
|
| 42 |
+
"trace": {
|
| 43 |
+
"anyOf": [
|
| 44 |
+
{
|
| 45 |
+
"$ref": "#/definitions/W3cTraceContext"
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"type": "null"
|
| 49 |
+
}
|
| 50 |
+
],
|
| 51 |
+
"description": "Optional W3C Trace Context for distributed tracing."
|
| 52 |
+
}
|
| 53 |
+
},
|
| 54 |
+
"required": [
|
| 55 |
+
"id",
|
| 56 |
+
"method"
|
| 57 |
+
],
|
| 58 |
+
"title": "JSONRPCRequest",
|
| 59 |
+
"type": "object"
|
| 60 |
+
}
|
codex-rs/app-server-protocol/schema/json/McpServerElicitationRequestResponse.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"definitions": {
|
| 4 |
+
"McpServerElicitationAction": {
|
| 5 |
+
"enum": [
|
| 6 |
+
"accept",
|
| 7 |
+
"decline",
|
| 8 |
+
"cancel"
|
| 9 |
+
],
|
| 10 |
+
"type": "string"
|
| 11 |
+
}
|
| 12 |
+
},
|
| 13 |
+
"properties": {
|
| 14 |
+
"_meta": {
|
| 15 |
+
"description": "Optional client metadata for form-mode action handling."
|
| 16 |
+
},
|
| 17 |
+
"action": {
|
| 18 |
+
"$ref": "#/definitions/McpServerElicitationAction"
|
| 19 |
+
},
|
| 20 |
+
"content": {
|
| 21 |
+
"description": "Structured user input for accepted elicitations, mirroring RMCP `CreateElicitationResult`.\n\nThis is nullable because decline/cancel responses have no content."
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
"required": [
|
| 25 |
+
"action"
|
| 26 |
+
],
|
| 27 |
+
"title": "McpServerElicitationRequestResponse",
|
| 28 |
+
"type": "object"
|
| 29 |
+
}
|
codex-rs/app-server-protocol/schema/json/PermissionsRequestApprovalResponse.json
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"definitions": {
|
| 4 |
+
"AdditionalFileSystemPermissions": {
|
| 5 |
+
"properties": {
|
| 6 |
+
"entries": {
|
| 7 |
+
"items": {
|
| 8 |
+
"$ref": "#/definitions/FileSystemSandboxEntry"
|
| 9 |
+
},
|
| 10 |
+
"type": [
|
| 11 |
+
"array",
|
| 12 |
+
"null"
|
| 13 |
+
]
|
| 14 |
+
},
|
| 15 |
+
"globScanMaxDepth": {
|
| 16 |
+
"format": "uint",
|
| 17 |
+
"minimum": 1.0,
|
| 18 |
+
"type": [
|
| 19 |
+
"integer",
|
| 20 |
+
"null"
|
| 21 |
+
]
|
| 22 |
+
},
|
| 23 |
+
"read": {
|
| 24 |
+
"description": "This will be removed in favor of `entries`.",
|
| 25 |
+
"items": {
|
| 26 |
+
"$ref": "#/definitions/LegacyAppPathString"
|
| 27 |
+
},
|
| 28 |
+
"type": [
|
| 29 |
+
"array",
|
| 30 |
+
"null"
|
| 31 |
+
]
|
| 32 |
+
},
|
| 33 |
+
"write": {
|
| 34 |
+
"description": "This will be removed in favor of `entries`.",
|
| 35 |
+
"items": {
|
| 36 |
+
"$ref": "#/definitions/LegacyAppPathString"
|
| 37 |
+
},
|
| 38 |
+
"type": [
|
| 39 |
+
"array",
|
| 40 |
+
"null"
|
| 41 |
+
]
|
| 42 |
+
}
|
| 43 |
+
},
|
| 44 |
+
"type": "object"
|
| 45 |
+
},
|
| 46 |
+
"AdditionalNetworkPermissions": {
|
| 47 |
+
"properties": {
|
| 48 |
+
"enabled": {
|
| 49 |
+
"type": [
|
| 50 |
+
"boolean",
|
| 51 |
+
"null"
|
| 52 |
+
]
|
| 53 |
+
}
|
| 54 |
+
},
|
| 55 |
+
"type": "object"
|
| 56 |
+
},
|
| 57 |
+
"FileSystemAccessMode": {
|
| 58 |
+
"enum": [
|
| 59 |
+
"read",
|
| 60 |
+
"write",
|
| 61 |
+
"deny"
|
| 62 |
+
],
|
| 63 |
+
"type": "string"
|
| 64 |
+
},
|
| 65 |
+
"FileSystemPath": {
|
| 66 |
+
"oneOf": [
|
| 67 |
+
{
|
| 68 |
+
"properties": {
|
| 69 |
+
"path": {
|
| 70 |
+
"$ref": "#/definitions/LegacyAppPathString"
|
| 71 |
+
},
|
| 72 |
+
"type": {
|
| 73 |
+
"enum": [
|
| 74 |
+
"path"
|
| 75 |
+
],
|
| 76 |
+
"title": "PathFileSystemPathType",
|
| 77 |
+
"type": "string"
|
| 78 |
+
}
|
| 79 |
+
},
|
| 80 |
+
"required": [
|
| 81 |
+
"path",
|
| 82 |
+
"type"
|
| 83 |
+
],
|
| 84 |
+
"title": "PathFileSystemPath",
|
| 85 |
+
"type": "object"
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"properties": {
|
| 89 |
+
"pattern": {
|
| 90 |
+
"type": "string"
|
| 91 |
+
},
|
| 92 |
+
"type": {
|
| 93 |
+
"enum": [
|
| 94 |
+
"glob_pattern"
|
| 95 |
+
],
|
| 96 |
+
"title": "GlobPatternFileSystemPathType",
|
| 97 |
+
"type": "string"
|
| 98 |
+
}
|
| 99 |
+
},
|
| 100 |
+
"required": [
|
| 101 |
+
"pattern",
|
| 102 |
+
"type"
|
| 103 |
+
],
|
| 104 |
+
"title": "GlobPatternFileSystemPath",
|
| 105 |
+
"type": "object"
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"properties": {
|
| 109 |
+
"type": {
|
| 110 |
+
"enum": [
|
| 111 |
+
"special"
|
| 112 |
+
],
|
| 113 |
+
"title": "SpecialFileSystemPathType",
|
| 114 |
+
"type": "string"
|
| 115 |
+
},
|
| 116 |
+
"value": {
|
| 117 |
+
"$ref": "#/definitions/FileSystemSpecialPath"
|
| 118 |
+
}
|
| 119 |
+
},
|
| 120 |
+
"required": [
|
| 121 |
+
"type",
|
| 122 |
+
"value"
|
| 123 |
+
],
|
| 124 |
+
"title": "SpecialFileSystemPath",
|
| 125 |
+
"type": "object"
|
| 126 |
+
}
|
| 127 |
+
]
|
| 128 |
+
},
|
| 129 |
+
"FileSystemSandboxEntry": {
|
| 130 |
+
"properties": {
|
| 131 |
+
"access": {
|
| 132 |
+
"$ref": "#/definitions/FileSystemAccessMode"
|
| 133 |
+
},
|
| 134 |
+
"path": {
|
| 135 |
+
"$ref": "#/definitions/FileSystemPath"
|
| 136 |
+
}
|
| 137 |
+
},
|
| 138 |
+
"required": [
|
| 139 |
+
"access",
|
| 140 |
+
"path"
|
| 141 |
+
],
|
| 142 |
+
"type": "object"
|
| 143 |
+
},
|
| 144 |
+
"FileSystemSpecialPath": {
|
| 145 |
+
"oneOf": [
|
| 146 |
+
{
|
| 147 |
+
"properties": {
|
| 148 |
+
"kind": {
|
| 149 |
+
"enum": [
|
| 150 |
+
"root"
|
| 151 |
+
],
|
| 152 |
+
"type": "string"
|
| 153 |
+
}
|
| 154 |
+
},
|
| 155 |
+
"required": [
|
| 156 |
+
"kind"
|
| 157 |
+
],
|
| 158 |
+
"title": "RootFileSystemSpecialPath",
|
| 159 |
+
"type": "object"
|
| 160 |
+
},
|
| 161 |
+
{
|
| 162 |
+
"properties": {
|
| 163 |
+
"kind": {
|
| 164 |
+
"enum": [
|
| 165 |
+
"minimal"
|
| 166 |
+
],
|
| 167 |
+
"type": "string"
|
| 168 |
+
}
|
| 169 |
+
},
|
| 170 |
+
"required": [
|
| 171 |
+
"kind"
|
| 172 |
+
],
|
| 173 |
+
"title": "MinimalFileSystemSpecialPath",
|
| 174 |
+
"type": "object"
|
| 175 |
+
},
|
| 176 |
+
{
|
| 177 |
+
"properties": {
|
| 178 |
+
"kind": {
|
| 179 |
+
"enum": [
|
| 180 |
+
"project_roots"
|
| 181 |
+
],
|
| 182 |
+
"type": "string"
|
| 183 |
+
},
|
| 184 |
+
"subpath": {
|
| 185 |
+
"anyOf": [
|
| 186 |
+
{
|
| 187 |
+
"$ref": "#/definitions/LegacyAppPathString"
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"type": "null"
|
| 191 |
+
}
|
| 192 |
+
]
|
| 193 |
+
}
|
| 194 |
+
},
|
| 195 |
+
"required": [
|
| 196 |
+
"kind"
|
| 197 |
+
],
|
| 198 |
+
"title": "KindFileSystemSpecialPath",
|
| 199 |
+
"type": "object"
|
| 200 |
+
},
|
| 201 |
+
{
|
| 202 |
+
"properties": {
|
| 203 |
+
"kind": {
|
| 204 |
+
"enum": [
|
| 205 |
+
"tmpdir"
|
| 206 |
+
],
|
| 207 |
+
"type": "string"
|
| 208 |
+
}
|
| 209 |
+
},
|
| 210 |
+
"required": [
|
| 211 |
+
"kind"
|
| 212 |
+
],
|
| 213 |
+
"title": "TmpdirFileSystemSpecialPath",
|
| 214 |
+
"type": "object"
|
| 215 |
+
},
|
| 216 |
+
{
|
| 217 |
+
"properties": {
|
| 218 |
+
"kind": {
|
| 219 |
+
"enum": [
|
| 220 |
+
"slash_tmp"
|
| 221 |
+
],
|
| 222 |
+
"type": "string"
|
| 223 |
+
}
|
| 224 |
+
},
|
| 225 |
+
"required": [
|
| 226 |
+
"kind"
|
| 227 |
+
],
|
| 228 |
+
"title": "SlashTmpFileSystemSpecialPath",
|
| 229 |
+
"type": "object"
|
| 230 |
+
},
|
| 231 |
+
{
|
| 232 |
+
"properties": {
|
| 233 |
+
"kind": {
|
| 234 |
+
"enum": [
|
| 235 |
+
"unknown"
|
| 236 |
+
],
|
| 237 |
+
"type": "string"
|
| 238 |
+
},
|
| 239 |
+
"path": {
|
| 240 |
+
"type": "string"
|
| 241 |
+
},
|
| 242 |
+
"subpath": {
|
| 243 |
+
"anyOf": [
|
| 244 |
+
{
|
| 245 |
+
"$ref": "#/definitions/LegacyAppPathString"
|
| 246 |
+
},
|
| 247 |
+
{
|
| 248 |
+
"type": "null"
|
| 249 |
+
}
|
| 250 |
+
]
|
| 251 |
+
}
|
| 252 |
+
},
|
| 253 |
+
"required": [
|
| 254 |
+
"kind",
|
| 255 |
+
"path"
|
| 256 |
+
],
|
| 257 |
+
"type": "object"
|
| 258 |
+
}
|
| 259 |
+
]
|
| 260 |
+
},
|
| 261 |
+
"GrantedPermissionProfile": {
|
| 262 |
+
"properties": {
|
| 263 |
+
"fileSystem": {
|
| 264 |
+
"anyOf": [
|
| 265 |
+
{
|
| 266 |
+
"$ref": "#/definitions/AdditionalFileSystemPermissions"
|
| 267 |
+
},
|
| 268 |
+
{
|
| 269 |
+
"type": "null"
|
| 270 |
+
}
|
| 271 |
+
]
|
| 272 |
+
},
|
| 273 |
+
"network": {
|
| 274 |
+
"anyOf": [
|
| 275 |
+
{
|
| 276 |
+
"$ref": "#/definitions/AdditionalNetworkPermissions"
|
| 277 |
+
},
|
| 278 |
+
{
|
| 279 |
+
"type": "null"
|
| 280 |
+
}
|
| 281 |
+
]
|
| 282 |
+
}
|
| 283 |
+
},
|
| 284 |
+
"type": "object"
|
| 285 |
+
},
|
| 286 |
+
"LegacyAppPathString": {
|
| 287 |
+
"type": "string"
|
| 288 |
+
},
|
| 289 |
+
"PermissionGrantScope": {
|
| 290 |
+
"enum": [
|
| 291 |
+
"turn",
|
| 292 |
+
"session"
|
| 293 |
+
],
|
| 294 |
+
"type": "string"
|
| 295 |
+
}
|
| 296 |
+
},
|
| 297 |
+
"properties": {
|
| 298 |
+
"permissions": {
|
| 299 |
+
"$ref": "#/definitions/GrantedPermissionProfile"
|
| 300 |
+
},
|
| 301 |
+
"scope": {
|
| 302 |
+
"allOf": [
|
| 303 |
+
{
|
| 304 |
+
"$ref": "#/definitions/PermissionGrantScope"
|
| 305 |
+
}
|
| 306 |
+
],
|
| 307 |
+
"default": "turn"
|
| 308 |
+
},
|
| 309 |
+
"strictAutoReview": {
|
| 310 |
+
"description": "Review every subsequent command in this turn before normal sandboxed execution.",
|
| 311 |
+
"type": [
|
| 312 |
+
"boolean",
|
| 313 |
+
"null"
|
| 314 |
+
]
|
| 315 |
+
}
|
| 316 |
+
},
|
| 317 |
+
"required": [
|
| 318 |
+
"permissions"
|
| 319 |
+
],
|
| 320 |
+
"title": "PermissionsRequestApprovalResponse",
|
| 321 |
+
"type": "object"
|
| 322 |
+
}
|
codex-rs/app-server-protocol/schema/json/ToolRequestUserInputResponse.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
| 3 |
+
"definitions": {
|
| 4 |
+
"ToolRequestUserInputAnswer": {
|
| 5 |
+
"description": "EXPERIMENTAL. Captures a user's answer to a request_user_input question.",
|
| 6 |
+
"properties": {
|
| 7 |
+
"answers": {
|
| 8 |
+
"items": {
|
| 9 |
+
"type": "string"
|
| 10 |
+
},
|
| 11 |
+
"type": "array"
|
| 12 |
+
}
|
| 13 |
+
},
|
| 14 |
+
"required": [
|
| 15 |
+
"answers"
|
| 16 |
+
],
|
| 17 |
+
"type": "object"
|
| 18 |
+
}
|
| 19 |
+
},
|
| 20 |
+
"description": "EXPERIMENTAL. Response payload mapping question ids to answers.",
|
| 21 |
+
"properties": {
|
| 22 |
+
"answers": {
|
| 23 |
+
"additionalProperties": {
|
| 24 |
+
"$ref": "#/definitions/ToolRequestUserInputAnswer"
|
| 25 |
+
},
|
| 26 |
+
"type": "object"
|
| 27 |
+
}
|
| 28 |
+
},
|
| 29 |
+
"required": [
|
| 30 |
+
"answers"
|
| 31 |
+
],
|
| 32 |
+
"title": "ToolRequestUserInputResponse",
|
| 33 |
+
"type": "object"
|
| 34 |
+
}
|
codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
codex-rs/app-server-protocol/schema/typescript/AbsolutePathBuf.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* A path that is guaranteed to be absolute and normalized (though it is not
|
| 7 |
+
* guaranteed to be canonicalized or exist on the filesystem).
|
| 8 |
+
*
|
| 9 |
+
* IMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set
|
| 10 |
+
* using [AbsolutePathBufGuard::new]. If no base path is set, the
|
| 11 |
+
* deserialization will fail unless the path being deserialized is already
|
| 12 |
+
* absolute.
|
| 13 |
+
*/
|
| 14 |
+
export type AbsolutePathBuf = string;
|
codex-rs/app-server-protocol/schema/typescript/AgentMessageInputContent.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
|
| 5 |
+
export type AgentMessageInputContent = { "type": "input_text", text: string, } | { "type": "encrypted_content", encrypted_content: string, };
|
codex-rs/app-server-protocol/schema/typescript/AgentPath.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
|
| 5 |
+
export type AgentPath = string;
|
codex-rs/app-server-protocol/schema/typescript/ApplyPatchApprovalParams.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
import type { FileChange } from "./FileChange";
|
| 5 |
+
import type { ThreadId } from "./ThreadId";
|
| 6 |
+
|
| 7 |
+
export type ApplyPatchApprovalParams = { conversationId: ThreadId,
|
| 8 |
+
/**
|
| 9 |
+
* Use to correlate this with [codex_protocol::protocol::PatchApplyBeginEvent]
|
| 10 |
+
* and [codex_protocol::protocol::PatchApplyEndEvent].
|
| 11 |
+
*/
|
| 12 |
+
callId: string, fileChanges: { [key in string]?: FileChange },
|
| 13 |
+
/**
|
| 14 |
+
* Optional explanatory reason (e.g. request for extra write access).
|
| 15 |
+
*/
|
| 16 |
+
reason: string | null,
|
| 17 |
+
/**
|
| 18 |
+
* When set, the agent is asking the user to allow writes under this root
|
| 19 |
+
* for the remainder of the session (unclear if this is honored today).
|
| 20 |
+
*/
|
| 21 |
+
grantRoot: string | null, };
|
codex-rs/app-server-protocol/schema/typescript/ApplyPatchApprovalResponse.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
import type { ReviewDecision } from "./ReviewDecision";
|
| 5 |
+
|
| 6 |
+
export type ApplyPatchApprovalResponse = { decision: ReviewDecision, };
|
codex-rs/app-server-protocol/schema/typescript/AuthMode.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* Authentication mode for OpenAI-backed providers.
|
| 7 |
+
*/
|
| 8 |
+
export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "headers" | "agentIdentity" | "personalAccessToken" | "bedrockApiKey" | "bedrockAccessKeys";
|
codex-rs/app-server-protocol/schema/typescript/AutoCompactTokenLimitScope.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* Selects which part of the active context is charged against
|
| 7 |
+
* `model_auto_compact_token_limit`.
|
| 8 |
+
*/
|
| 9 |
+
export type AutoCompactTokenLimitScope = "total" | "body_after_prefix";
|
codex-rs/app-server-protocol/schema/typescript/ClientInfo.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
|
| 5 |
+
export type ClientInfo = { name: string, title: string | null, version: string, };
|
codex-rs/app-server-protocol/schema/typescript/ClientNotification.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
|
| 5 |
+
export type ClientNotification = { "method": "initialized" };
|
codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
import type { FuzzyFileSearchParams } from "./FuzzyFileSearchParams";
|
| 5 |
+
import type { GetAuthStatusParams } from "./GetAuthStatusParams";
|
| 6 |
+
import type { GetConversationSummaryParams } from "./GetConversationSummaryParams";
|
| 7 |
+
import type { GitDiffToRemoteParams } from "./GitDiffToRemoteParams";
|
| 8 |
+
import type { InitializeParams } from "./InitializeParams";
|
| 9 |
+
import type { RequestId } from "./RequestId";
|
| 10 |
+
import type { AppsInstalledParams } from "./v2/AppsInstalledParams";
|
| 11 |
+
import type { AppsListParams } from "./v2/AppsListParams";
|
| 12 |
+
import type { AppsReadParams } from "./v2/AppsReadParams";
|
| 13 |
+
import type { CancelLoginAccountParams } from "./v2/CancelLoginAccountParams";
|
| 14 |
+
import type { CommandExecParams } from "./v2/CommandExecParams";
|
| 15 |
+
import type { CommandExecResizeParams } from "./v2/CommandExecResizeParams";
|
| 16 |
+
import type { CommandExecTerminateParams } from "./v2/CommandExecTerminateParams";
|
| 17 |
+
import type { CommandExecWriteParams } from "./v2/CommandExecWriteParams";
|
| 18 |
+
import type { ConfigBatchWriteParams } from "./v2/ConfigBatchWriteParams";
|
| 19 |
+
import type { ConfigReadParams } from "./v2/ConfigReadParams";
|
| 20 |
+
import type { ConfigValueWriteParams } from "./v2/ConfigValueWriteParams";
|
| 21 |
+
import type { ConsumeAccountRateLimitResetCreditParams } from "./v2/ConsumeAccountRateLimitResetCreditParams";
|
| 22 |
+
import type { ExperimentalFeatureEnablementSetParams } from "./v2/ExperimentalFeatureEnablementSetParams";
|
| 23 |
+
import type { ExperimentalFeatureListParams } from "./v2/ExperimentalFeatureListParams";
|
| 24 |
+
import type { ExternalAgentConfigDetectParams } from "./v2/ExternalAgentConfigDetectParams";
|
| 25 |
+
import type { ExternalAgentConfigImportHistoryRecordParams } from "./v2/ExternalAgentConfigImportHistoryRecordParams";
|
| 26 |
+
import type { ExternalAgentConfigImportParams } from "./v2/ExternalAgentConfigImportParams";
|
| 27 |
+
import type { FeedbackUploadParams } from "./v2/FeedbackUploadParams";
|
| 28 |
+
import type { FsCopyParams } from "./v2/FsCopyParams";
|
| 29 |
+
import type { FsCreateDirectoryParams } from "./v2/FsCreateDirectoryParams";
|
| 30 |
+
import type { FsGetMetadataParams } from "./v2/FsGetMetadataParams";
|
| 31 |
+
import type { FsReadDirectoryParams } from "./v2/FsReadDirectoryParams";
|
| 32 |
+
import type { FsReadFileParams } from "./v2/FsReadFileParams";
|
| 33 |
+
import type { FsRemoveParams } from "./v2/FsRemoveParams";
|
| 34 |
+
import type { FsUnwatchParams } from "./v2/FsUnwatchParams";
|
| 35 |
+
import type { FsWatchParams } from "./v2/FsWatchParams";
|
| 36 |
+
import type { FsWriteFileParams } from "./v2/FsWriteFileParams";
|
| 37 |
+
import type { GetAccountParams } from "./v2/GetAccountParams";
|
| 38 |
+
import type { GetAccountRateLimitsParams } from "./v2/GetAccountRateLimitsParams";
|
| 39 |
+
import type { GetAccountTokenUsageParams } from "./v2/GetAccountTokenUsageParams";
|
| 40 |
+
import type { HooksListParams } from "./v2/HooksListParams";
|
| 41 |
+
import type { ListMcpServerStatusParams } from "./v2/ListMcpServerStatusParams";
|
| 42 |
+
import type { LoginAccountParams } from "./v2/LoginAccountParams";
|
| 43 |
+
import type { MarketplaceAddParams } from "./v2/MarketplaceAddParams";
|
| 44 |
+
import type { MarketplaceRemoveParams } from "./v2/MarketplaceRemoveParams";
|
| 45 |
+
import type { MarketplaceUpgradeParams } from "./v2/MarketplaceUpgradeParams";
|
| 46 |
+
import type { McpResourceReadParams } from "./v2/McpResourceReadParams";
|
| 47 |
+
import type { McpServerOauthLoginParams } from "./v2/McpServerOauthLoginParams";
|
| 48 |
+
import type { McpServerToolCallParams } from "./v2/McpServerToolCallParams";
|
| 49 |
+
import type { ModelListParams } from "./v2/ModelListParams";
|
| 50 |
+
import type { ModelProviderCapabilitiesReadParams } from "./v2/ModelProviderCapabilitiesReadParams";
|
| 51 |
+
import type { PermissionProfileListParams } from "./v2/PermissionProfileListParams";
|
| 52 |
+
import type { PluginInstallParams } from "./v2/PluginInstallParams";
|
| 53 |
+
import type { PluginInstalledParams } from "./v2/PluginInstalledParams";
|
| 54 |
+
import type { PluginListParams } from "./v2/PluginListParams";
|
| 55 |
+
import type { PluginReadParams } from "./v2/PluginReadParams";
|
| 56 |
+
import type { PluginReconcileParams } from "./v2/PluginReconcileParams";
|
| 57 |
+
import type { PluginShareCheckoutParams } from "./v2/PluginShareCheckoutParams";
|
| 58 |
+
import type { PluginShareDeleteParams } from "./v2/PluginShareDeleteParams";
|
| 59 |
+
import type { PluginShareListParams } from "./v2/PluginShareListParams";
|
| 60 |
+
import type { PluginShareSaveParams } from "./v2/PluginShareSaveParams";
|
| 61 |
+
import type { PluginShareUpdateTargetsParams } from "./v2/PluginShareUpdateTargetsParams";
|
| 62 |
+
import type { PluginSkillReadParams } from "./v2/PluginSkillReadParams";
|
| 63 |
+
import type { PluginUninstallParams } from "./v2/PluginUninstallParams";
|
| 64 |
+
import type { ReviewStartParams } from "./v2/ReviewStartParams";
|
| 65 |
+
import type { SendAddCreditsNudgeEmailParams } from "./v2/SendAddCreditsNudgeEmailParams";
|
| 66 |
+
import type { SkillsConfigWriteParams } from "./v2/SkillsConfigWriteParams";
|
| 67 |
+
import type { SkillsExtraRootsSetParams } from "./v2/SkillsExtraRootsSetParams";
|
| 68 |
+
import type { SkillsListParams } from "./v2/SkillsListParams";
|
| 69 |
+
import type { ThreadApproveGuardianDeniedActionParams } from "./v2/ThreadApproveGuardianDeniedActionParams";
|
| 70 |
+
import type { ThreadArchiveParams } from "./v2/ThreadArchiveParams";
|
| 71 |
+
import type { ThreadAttachmentAddParams } from "./v2/ThreadAttachmentAddParams";
|
| 72 |
+
import type { ThreadAttachmentListParams } from "./v2/ThreadAttachmentListParams";
|
| 73 |
+
import type { ThreadAttachmentRemoveParams } from "./v2/ThreadAttachmentRemoveParams";
|
| 74 |
+
import type { ThreadCompactStartParams } from "./v2/ThreadCompactStartParams";
|
| 75 |
+
import type { ThreadDeleteParams } from "./v2/ThreadDeleteParams";
|
| 76 |
+
import type { ThreadForkParams } from "./v2/ThreadForkParams";
|
| 77 |
+
import type { ThreadGoalClearParams } from "./v2/ThreadGoalClearParams";
|
| 78 |
+
import type { ThreadGoalGetParams } from "./v2/ThreadGoalGetParams";
|
| 79 |
+
import type { ThreadGoalSetParams } from "./v2/ThreadGoalSetParams";
|
| 80 |
+
import type { ThreadInjectItemsParams } from "./v2/ThreadInjectItemsParams";
|
| 81 |
+
import type { ThreadItemsListParams } from "./v2/ThreadItemsListParams";
|
| 82 |
+
import type { ThreadListParams } from "./v2/ThreadListParams";
|
| 83 |
+
import type { ThreadLoadedListParams } from "./v2/ThreadLoadedListParams";
|
| 84 |
+
import type { ThreadMetadataUpdateParams } from "./v2/ThreadMetadataUpdateParams";
|
| 85 |
+
import type { ThreadReadParams } from "./v2/ThreadReadParams";
|
| 86 |
+
import type { ThreadResumeParams } from "./v2/ThreadResumeParams";
|
| 87 |
+
import type { ThreadRevertParams } from "./v2/ThreadRevertParams";
|
| 88 |
+
import type { ThreadSectionCreateParams } from "./v2/ThreadSectionCreateParams";
|
| 89 |
+
import type { ThreadSectionDeleteParams } from "./v2/ThreadSectionDeleteParams";
|
| 90 |
+
import type { ThreadSectionListParams } from "./v2/ThreadSectionListParams";
|
| 91 |
+
import type { ThreadSectionMoveParams } from "./v2/ThreadSectionMoveParams";
|
| 92 |
+
import type { ThreadSectionUpdateParams } from "./v2/ThreadSectionUpdateParams";
|
| 93 |
+
import type { ThreadSetNameParams } from "./v2/ThreadSetNameParams";
|
| 94 |
+
import type { ThreadShellCommandParams } from "./v2/ThreadShellCommandParams";
|
| 95 |
+
import type { ThreadStartParams } from "./v2/ThreadStartParams";
|
| 96 |
+
import type { ThreadTurnsListParams } from "./v2/ThreadTurnsListParams";
|
| 97 |
+
import type { ThreadUnarchiveParams } from "./v2/ThreadUnarchiveParams";
|
| 98 |
+
import type { ThreadUnsubscribeParams } from "./v2/ThreadUnsubscribeParams";
|
| 99 |
+
import type { TurnInterruptParams } from "./v2/TurnInterruptParams";
|
| 100 |
+
import type { TurnStartParams } from "./v2/TurnStartParams";
|
| 101 |
+
import type { TurnSteerParams } from "./v2/TurnSteerParams";
|
| 102 |
+
import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupStartParams";
|
| 103 |
+
|
| 104 |
+
/**
|
| 105 |
+
* Request from the client to the server.
|
| 106 |
+
*/
|
| 107 |
+
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/delete", id: RequestId, params: ThreadDeleteParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/goal/set", id: RequestId, params: ThreadGoalSetParams, } | { "method": "thread/goal/get", id: RequestId, params: ThreadGoalGetParams, } | { "method": "thread/goal/clear", id: RequestId, params: ThreadGoalClearParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/attachment/add", id: RequestId, params: ThreadAttachmentAddParams, } | { "method": "thread/attachment/list", id: RequestId, params: ThreadAttachmentListParams, } | { "method": "thread/attachment/remove", id: RequestId, params: ThreadAttachmentRemoveParams, } | { "method": "thread/section/move", id: RequestId, params: ThreadSectionMoveParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "method": "thread/revert", id: RequestId, params: ThreadRevertParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "threadSection/list", id: RequestId, params: ThreadSectionListParams, } | { "method": "threadSection/create", id: RequestId, params: ThreadSectionCreateParams, } | { "method": "threadSection/update", id: RequestId, params: ThreadSectionUpdateParams, } | { "method": "threadSection/delete", id: RequestId, params: ThreadSectionDeleteParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "thread/turns/list", id: RequestId, params: ThreadTurnsListParams, } | { "method": "thread/items/list", id: RequestId, params: ThreadItemsListParams, } | { "method": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "skills/extraRoots/set", id: RequestId, params: SkillsExtraRootsSetParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/installed", id: RequestId, params: PluginInstalledParams, } | { "method": "plugin/reconcile", id: RequestId, params: PluginReconcileParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/checkout", id: RequestId, params: PluginShareCheckoutParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/read", id: RequestId, params: AppsReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "app/installed", id: RequestId, params: AppsInstalledParams, } | { "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": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "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": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "permissionProfile/list", id: RequestId, params: PermissionProfileListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "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?: GetAccountRateLimitsParams | undefined, } | { "method": "account/rateLimitResetCredit/consume", id: RequestId, params: ConsumeAccountRateLimitResetCreditParams, } | { "method": "account/usage/read", id: RequestId, params?: GetAccountTokenUsageParams | undefined, } | { "method": "account/workspaceMessages/read", id: RequestId, params: undefined, } | { "method": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "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": "externalAgentConfig/import/recordHistory", id: RequestId, params: ExternalAgentConfigImportHistoryRecordParams, } | { "method": "externalAgentConfig/import/readHistories", id: RequestId, params: undefined, } | { "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, };
|
codex-rs/app-server-protocol/schema/typescript/CodexResponseHandoffMode.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
|
| 5 |
+
export type CodexResponseHandoffMode = "thinking" | "commentary" | "bemTags";
|
codex-rs/app-server-protocol/schema/typescript/CollaborationMode.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
import type { ModeKind } from "./ModeKind";
|
| 5 |
+
import type { Settings } from "./Settings";
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* Collaboration mode for a Codex session.
|
| 9 |
+
*/
|
| 10 |
+
export type CollaborationMode = { mode: ModeKind, settings: Settings, };
|
codex-rs/app-server-protocol/schema/typescript/ConfigurationReasoning.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
import type { ReasoningEffort } from "./ReasoningEffort";
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Reasoning settings interpreted by the backend for the routed model.
|
| 8 |
+
*/
|
| 9 |
+
export type ConfigurationReasoning = { effort: ReasoningEffort, };
|
codex-rs/app-server-protocol/schema/typescript/ContentItem.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
import type { ImageDetail } from "./ImageDetail";
|
| 5 |
+
|
| 6 |
+
export type ContentItem = { "type": "input_text", text: string, } | { "type": "input_image", detail?: ImageDetail, } & ({ image_url: string, } | { file_id: string, }) | { "type": "input_audio", audio_url: string, } | { "type": "output_text", text: string, };
|
codex-rs/app-server-protocol/schema/typescript/ConversationGitInfo.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
|
| 5 |
+
export type ConversationGitInfo = { sha: string | null, branch: string | null, origin_url: string | null, };
|
codex-rs/app-server-protocol/schema/typescript/ConversationSummary.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
import type { ConversationGitInfo } from "./ConversationGitInfo";
|
| 5 |
+
import type { SessionSource } from "./SessionSource";
|
| 6 |
+
import type { ThreadId } from "./ThreadId";
|
| 7 |
+
|
| 8 |
+
export type ConversationSummary = { conversationId: ThreadId, path: string, preview: string, timestamp: string | null, updatedAt: string | null, modelProvider: string, cwd: string, cliVersion: string, source: SessionSource, gitInfo: ConversationGitInfo | null, };
|
codex-rs/app-server-protocol/schema/typescript/ConversationTextRole.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
|
| 5 |
+
export type ConversationTextRole = "user" | "developer" | "assistant";
|
codex-rs/app-server-protocol/schema/typescript/ExecCommandApprovalParams.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
import type { ParsedCommand } from "./ParsedCommand";
|
| 5 |
+
import type { ThreadId } from "./ThreadId";
|
| 6 |
+
|
| 7 |
+
export type ExecCommandApprovalParams = { conversationId: ThreadId,
|
| 8 |
+
/**
|
| 9 |
+
* Use to correlate this with [codex_protocol::protocol::ExecCommandBeginEvent]
|
| 10 |
+
* and [codex_protocol::protocol::ExecCommandEndEvent].
|
| 11 |
+
*/
|
| 12 |
+
callId: string,
|
| 13 |
+
/**
|
| 14 |
+
* Identifier for this specific approval callback.
|
| 15 |
+
*/
|
| 16 |
+
approvalId: string | null, command: Array<string>, cwd: string, reason: string | null, parsedCmd: Array<ParsedCommand>, };
|
codex-rs/app-server-protocol/schema/typescript/ExecCommandApprovalResponse.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
import type { ReviewDecision } from "./ReviewDecision";
|
| 5 |
+
|
| 6 |
+
export type ExecCommandApprovalResponse = { decision: ReviewDecision, };
|
codex-rs/app-server-protocol/schema/typescript/ExecPolicyAmendment.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
|
| 5 |
+
/**
|
| 6 |
+
* Proposed execpolicy change to allow commands starting with this prefix.
|
| 7 |
+
*
|
| 8 |
+
* The `command` tokens form the prefix that would be added as an execpolicy
|
| 9 |
+
* `prefix_rule(..., decision="allow")`, letting the agent bypass approval for
|
| 10 |
+
* commands that start with this token sequence.
|
| 11 |
+
*/
|
| 12 |
+
export type ExecPolicyAmendment = Array<string>;
|
codex-rs/app-server-protocol/schema/typescript/FileChange.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
|
| 5 |
+
export type FileChange = { "type": "add", content: string, } | { "type": "delete", content: string, } | { "type": "update", unified_diff: string, move_path: string | null, };
|
codex-rs/app-server-protocol/schema/typescript/ForcedLoginMethod.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
|
| 5 |
+
export type ForcedLoginMethod = "chatgpt" | "api";
|
codex-rs/app-server-protocol/schema/typescript/FunctionCallOutputBody.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
import type { FunctionCallOutputContentItem } from "./FunctionCallOutputContentItem";
|
| 5 |
+
|
| 6 |
+
export type FunctionCallOutputBody = string | Array<FunctionCallOutputContentItem>;
|
codex-rs/app-server-protocol/schema/typescript/FunctionCallOutputContentItem.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
import type { ImageDetail } from "./ImageDetail";
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Responses API compatible content items that can be returned by a tool call.
|
| 8 |
+
* This is a subset of ContentItem with the types we support as function call outputs.
|
| 9 |
+
*/
|
| 10 |
+
export type FunctionCallOutputContentItem = { "type": "input_text", text: string, } | { "type": "input_image", detail?: ImageDetail, } & ({ image_url: string, } | { file_id: string, }) | { "type": "input_audio", audio_url: string, } | { "type": "encrypted_content", encrypted_content: string, };
|
codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchMatchType.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
|
| 5 |
+
export type FuzzyFileSearchMatchType = "file" | "directory";
|
codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchParams.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
|
| 5 |
+
export type FuzzyFileSearchParams = { query: string, roots: Array<string>, cancellationToken: string | null, };
|
codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchResponse.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
import type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult";
|
| 5 |
+
|
| 6 |
+
export type FuzzyFileSearchResponse = { files: Array<FuzzyFileSearchResult>, };
|
codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchResult.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
import type { FuzzyFileSearchMatchType } from "./FuzzyFileSearchMatchType";
|
| 5 |
+
|
| 6 |
+
/**
|
| 7 |
+
* Superset of [`codex_file_search::FileMatch`]
|
| 8 |
+
*/
|
| 9 |
+
export type FuzzyFileSearchResult = { root: string, path: string, match_type: FuzzyFileSearchMatchType, file_name: string, score: number, indices: Array<number> | null, };
|
codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchSessionCompletedNotification.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
|
| 5 |
+
export type FuzzyFileSearchSessionCompletedNotification = { sessionId: string, };
|
codex-rs/app-server-protocol/schema/typescript/FuzzyFileSearchSessionUpdatedNotification.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
import type { FuzzyFileSearchResult } from "./FuzzyFileSearchResult";
|
| 5 |
+
|
| 6 |
+
export type FuzzyFileSearchSessionUpdatedNotification = { sessionId: string, query: string, files: Array<FuzzyFileSearchResult>, };
|
codex-rs/app-server-protocol/schema/typescript/GetAuthStatusParams.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
|
| 5 |
+
export type GetAuthStatusParams = { includeToken: boolean | null, refreshToken: boolean | null, };
|
codex-rs/app-server-protocol/schema/typescript/GetAuthStatusResponse.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
import type { AuthMode } from "./AuthMode";
|
| 5 |
+
|
| 6 |
+
export type GetAuthStatusResponse = { authMethod: AuthMode | null, authToken: string | null, requiresOpenaiAuth: boolean | null, };
|
codex-rs/app-server-protocol/schema/typescript/GetConversationSummaryParams.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
| 2 |
+
|
| 3 |
+
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
| 4 |
+
import type { ThreadId } from "./ThreadId";
|
| 5 |
+
|
| 6 |
+
export type GetConversationSummaryParams = { rolloutPath: string, } | { conversationId: ThreadId, };
|