mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
[codex-app-server] add native integrity state bridge [ci changed_files]
This commit is contained in:
Generated
+1
@@ -1913,6 +1913,7 @@ dependencies = [
|
||||
"codex-arg0",
|
||||
"codex-backend-client",
|
||||
"codex-chatgpt",
|
||||
"codex-client",
|
||||
"codex-cloud-config",
|
||||
"codex-config",
|
||||
"codex-core",
|
||||
|
||||
@@ -861,6 +861,24 @@ client_request_definitions! {
|
||||
serialization: global("remote-control-clients"),
|
||||
response: v2::RemoteControlClientsRevokeResponse,
|
||||
},
|
||||
#[experimental("nativeIntegrityState/read")]
|
||||
NativeIntegrityStateRead => "nativeIntegrityState/read" {
|
||||
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
|
||||
serialization: global_shared_read("native-integrity-state"),
|
||||
response: v2::NativeIntegrityStateReadResponse,
|
||||
},
|
||||
#[experimental("nativeIntegrityState/write")]
|
||||
NativeIntegrityStateWrite => "nativeIntegrityState/write" {
|
||||
params: v2::NativeIntegrityStateWriteParams,
|
||||
serialization: global("native-integrity-state"),
|
||||
response: v2::NativeIntegrityStateWriteResponse,
|
||||
},
|
||||
#[experimental("nativeIntegrityState/clear")]
|
||||
NativeIntegrityStateClear => "nativeIntegrityState/clear" {
|
||||
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
|
||||
serialization: global("native-integrity-state"),
|
||||
response: v2::NativeIntegrityStateClearResponse,
|
||||
},
|
||||
#[experimental("collaborationMode/list")]
|
||||
/// Lists collaboration mode presets.
|
||||
CollaborationModeList => "collaborationMode/list" {
|
||||
|
||||
@@ -14,6 +14,7 @@ mod hook;
|
||||
mod item;
|
||||
mod mcp;
|
||||
mod model;
|
||||
mod native_integrity_state;
|
||||
mod notification;
|
||||
mod permissions;
|
||||
mod plugin;
|
||||
@@ -40,6 +41,7 @@ pub use hook::*;
|
||||
pub use item::*;
|
||||
pub use mcp::*;
|
||||
pub use model::*;
|
||||
pub use native_integrity_state::*;
|
||||
pub use notification::*;
|
||||
pub use permissions::*;
|
||||
pub use plugin::*;
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct NativeIntegrityStateReadResponse {
|
||||
pub state: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct NativeIntegrityStateWriteParams {
|
||||
pub state: String,
|
||||
/// When present, write only if the calling surface still stores this state.
|
||||
#[ts(optional = nullable)]
|
||||
pub expected_state: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct NativeIntegrityStateWriteResponse {
|
||||
pub written: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct NativeIntegrityStateClearResponse {}
|
||||
@@ -53,6 +53,7 @@ codex-utils-pty = { workspace = true }
|
||||
codex-backend-client = { workspace = true }
|
||||
codex-file-search = { workspace = true }
|
||||
codex-chatgpt = { workspace = true }
|
||||
codex-client = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-image-generation-extension = { workspace = true }
|
||||
codex-memories-extension = { workspace = true }
|
||||
|
||||
@@ -214,6 +214,9 @@ Example with notification opt-out:
|
||||
- `remoteControl/client/list` — experimental; list controller devices granted access to an environment. Pass `environmentId` and optional `cursor`, `limit`, and `order`; returns picker-oriented client metadata plus `nextCursor`. This signed-in account-management operation works while the local relay is disabled or unenrolled.
|
||||
- `remoteControl/client/revoke` — experimental; revoke one controller device's grant for an environment. Pass `environmentId` and `clientId`; returns an empty object. This signed-in account-management operation works while the local relay is disabled or unenrolled.
|
||||
- `remoteControl/status/changed` — notification emitted when the remote-control status or client-visible environment id changes. `status` is one of `disabled`, `connecting`, `connected`, or `errored`; `serverName` is the local machine name used by this app-server process; `environmentId` is a string when the app-server has a current enrollment and `null` when that enrollment is cleared, invalidated, or remote control is disabled. Newly initialized app-server clients always receive the current status snapshot.
|
||||
- `nativeIntegrityState/read` — experimental; read the integrity state file for the initialized client's recognized native surface. Returns `{ state }`, where `state` is `null` when no file exists.
|
||||
- `nativeIntegrityState/write` — experimental; write the initialized client's recognized native-surface state file. Pass `expectedState` to write only when the file still contains that value; omit it to replace the file unconditionally. Returns `{ written }`.
|
||||
- `nativeIntegrityState/clear` — experimental; delete the integrity state file for the initialized client's recognized native surface. Returns `{}`.
|
||||
- `skills/config/write` — write user-level skill config by name or absolute path.
|
||||
- `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, install MCPs if any, and return the effective plugin auth policy plus any apps that still need auth (**under development; do not call from production clients yet**).
|
||||
- `plugin/uninstall` — uninstall a local plugin by `pluginId` in `<plugin>@<marketplace>` form by removing its cached files and clearing its user-level config entry, or uninstall a remote ChatGPT plugin by backend `pluginId` by forwarding the uninstall to the ChatGPT plugin backend and removing any downloaded remote-plugin cache (**under development; do not call from production clients yet**).
|
||||
|
||||
@@ -29,6 +29,7 @@ use crate::request_processors::GitRequestProcessor;
|
||||
use crate::request_processors::InitializeRequestProcessor;
|
||||
use crate::request_processors::MarketplaceRequestProcessor;
|
||||
use crate::request_processors::McpRequestProcessor;
|
||||
use crate::request_processors::NativeIntegrityStateRequestProcessor;
|
||||
use crate::request_processors::PluginRequestProcessor;
|
||||
use crate::request_processors::ProcessExecRequestProcessor;
|
||||
use crate::request_processors::RemoteControlRequestProcessor;
|
||||
@@ -176,6 +177,7 @@ pub(crate) struct MessageProcessor {
|
||||
initialize_processor: InitializeRequestProcessor,
|
||||
marketplace_processor: MarketplaceRequestProcessor,
|
||||
mcp_processor: McpRequestProcessor,
|
||||
native_integrity_state_processor: NativeIntegrityStateRequestProcessor,
|
||||
plugin_processor: PluginRequestProcessor,
|
||||
remote_control_processor: RemoteControlRequestProcessor,
|
||||
search_processor: SearchRequestProcessor,
|
||||
@@ -400,6 +402,8 @@ impl MessageProcessor {
|
||||
outgoing.clone(),
|
||||
config_manager.clone(),
|
||||
);
|
||||
let native_integrity_state_processor =
|
||||
NativeIntegrityStateRequestProcessor::new(config.codex_home.to_path_buf());
|
||||
let plugin_processor = PluginRequestProcessor::new(
|
||||
auth_manager.clone(),
|
||||
Arc::clone(&thread_manager),
|
||||
@@ -503,6 +507,7 @@ impl MessageProcessor {
|
||||
initialize_processor,
|
||||
marketplace_processor,
|
||||
mcp_processor,
|
||||
native_integrity_state_processor,
|
||||
plugin_processor,
|
||||
remote_control_processor,
|
||||
search_processor,
|
||||
@@ -932,6 +937,18 @@ impl MessageProcessor {
|
||||
.clients_revoke(params)
|
||||
.await
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::NativeIntegrityStateRead { .. } => self
|
||||
.native_integrity_state_processor
|
||||
.read(app_server_client_name.as_deref())
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::NativeIntegrityStateWrite { params, .. } => self
|
||||
.native_integrity_state_processor
|
||||
.write(app_server_client_name.as_deref(), params)
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::NativeIntegrityStateClear { .. } => self
|
||||
.native_integrity_state_processor
|
||||
.clear(app_server_client_name.as_deref())
|
||||
.map(|response| Some(response.into())),
|
||||
ClientRequest::ConfigRequirementsRead { params: _, .. } => self
|
||||
.config_processor
|
||||
.config_requirements_read()
|
||||
|
||||
@@ -466,6 +466,7 @@ mod git_processor;
|
||||
mod initialize_processor;
|
||||
mod marketplace_processor;
|
||||
mod mcp_processor;
|
||||
mod native_integrity_state_processor;
|
||||
mod plugins;
|
||||
mod process_exec_processor;
|
||||
mod remote_control_processor;
|
||||
@@ -488,6 +489,7 @@ pub(crate) use git_processor::GitRequestProcessor;
|
||||
pub(crate) use initialize_processor::InitializeRequestProcessor;
|
||||
pub(crate) use marketplace_processor::MarketplaceRequestProcessor;
|
||||
pub(crate) use mcp_processor::McpRequestProcessor;
|
||||
pub(crate) use native_integrity_state_processor::NativeIntegrityStateRequestProcessor;
|
||||
pub(crate) use plugins::PluginRequestProcessor;
|
||||
pub(crate) use process_exec_processor::ProcessExecRequestProcessor;
|
||||
pub(crate) use remote_control_processor::RemoteControlRequestProcessor;
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::NativeIntegrityStateClearResponse;
|
||||
use codex_app_server_protocol::NativeIntegrityStateReadResponse;
|
||||
use codex_app_server_protocol::NativeIntegrityStateWriteParams;
|
||||
use codex_app_server_protocol::NativeIntegrityStateWriteResponse;
|
||||
use codex_client::NativeIntegrityStateStore;
|
||||
use codex_client::NativeIntegritySurface;
|
||||
|
||||
use crate::error_code::internal_error;
|
||||
use crate::error_code::invalid_request;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct NativeIntegrityStateRequestProcessor {
|
||||
store: NativeIntegrityStateStore,
|
||||
}
|
||||
|
||||
impl NativeIntegrityStateRequestProcessor {
|
||||
pub(crate) fn new(codex_home: PathBuf) -> Self {
|
||||
Self {
|
||||
store: NativeIntegrityStateStore::new(codex_home),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read(
|
||||
&self,
|
||||
app_server_client_name: Option<&str>,
|
||||
) -> Result<NativeIntegrityStateReadResponse, JSONRPCErrorError> {
|
||||
let surface = surface_for_client_name(app_server_client_name)?;
|
||||
let state = self
|
||||
.store
|
||||
.load(surface)
|
||||
.map_err(map_read_error)?
|
||||
.map(|state_file| state_file.state);
|
||||
Ok(NativeIntegrityStateReadResponse { state })
|
||||
}
|
||||
|
||||
pub(crate) fn write(
|
||||
&self,
|
||||
app_server_client_name: Option<&str>,
|
||||
params: NativeIntegrityStateWriteParams,
|
||||
) -> Result<NativeIntegrityStateWriteResponse, JSONRPCErrorError> {
|
||||
let surface = surface_for_client_name(app_server_client_name)?;
|
||||
let written = match params.expected_state {
|
||||
Some(expected_state) => {
|
||||
self.store
|
||||
.compare_and_store(surface, &expected_state, params.state)
|
||||
}
|
||||
None => self.store.replace(surface, params.state).map(|()| true),
|
||||
}
|
||||
.map_err(map_write_error)?;
|
||||
Ok(NativeIntegrityStateWriteResponse { written })
|
||||
}
|
||||
|
||||
pub(crate) fn clear(
|
||||
&self,
|
||||
app_server_client_name: Option<&str>,
|
||||
) -> Result<NativeIntegrityStateClearResponse, JSONRPCErrorError> {
|
||||
let surface = surface_for_client_name(app_server_client_name)?;
|
||||
self.store.clear(surface).map_err(map_clear_error)?;
|
||||
Ok(NativeIntegrityStateClearResponse {})
|
||||
}
|
||||
}
|
||||
|
||||
fn surface_for_client_name(
|
||||
app_server_client_name: Option<&str>,
|
||||
) -> Result<NativeIntegritySurface, JSONRPCErrorError> {
|
||||
let client_name = app_server_client_name
|
||||
.ok_or_else(|| invalid_request("native integrity state requires an initialized client"))?;
|
||||
NativeIntegritySurface::try_from_app_server_client_name(client_name).ok_or_else(|| {
|
||||
invalid_request(format!(
|
||||
"native integrity state is unavailable for app-server client {client_name:?}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn map_read_error(error: io::Error) -> JSONRPCErrorError {
|
||||
internal_error(format!("failed to read native integrity state: {error}"))
|
||||
}
|
||||
|
||||
fn map_write_error(error: io::Error) -> JSONRPCErrorError {
|
||||
if error.kind() == io::ErrorKind::InvalidData {
|
||||
invalid_request(error.to_string())
|
||||
} else {
|
||||
internal_error(format!("failed to write native integrity state: {error}"))
|
||||
}
|
||||
}
|
||||
|
||||
fn map_clear_error(error: io::Error) -> JSONRPCErrorError {
|
||||
internal_error(format!("failed to clear native integrity state: {error}"))
|
||||
}
|
||||
@@ -29,6 +29,7 @@ mod mcp_tool;
|
||||
mod memory_reset;
|
||||
mod model_list;
|
||||
mod model_provider_capabilities_read;
|
||||
mod native_integrity_state;
|
||||
mod output_schema;
|
||||
mod permission_profile_list;
|
||||
mod plan_item;
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use app_test_support::TestAppServer;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::JSONRPCMessage;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::NativeIntegrityStateClearResponse;
|
||||
use codex_app_server_protocol::NativeIntegrityStateReadResponse;
|
||||
use codex_app_server_protocol::NativeIntegrityStateWriteParams;
|
||||
use codex_app_server_protocol::NativeIntegrityStateWriteResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const STATE_N: &str = "ois1.a.b.c";
|
||||
const STATE_N_PLUS_ONE: &str = "ois1.d.e.f";
|
||||
|
||||
#[tokio::test]
|
||||
async fn native_integrity_state_bridge_reads_writes_rotates_and_clears_surface_state() -> Result<()>
|
||||
{
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let initialized = mcp
|
||||
.initialize_with_client_info(client_info("codex_desktop_ssh"))
|
||||
.await?;
|
||||
let JSONRPCMessage::Response(_) = initialized else {
|
||||
anyhow::bail!("expected initialize response, got {initialized:?}");
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
read_state(&mut mcp).await?,
|
||||
NativeIntegrityStateReadResponse { state: None }
|
||||
);
|
||||
assert_eq!(
|
||||
write_state(
|
||||
&mut mcp,
|
||||
NativeIntegrityStateWriteParams {
|
||||
state: STATE_N.to_string(),
|
||||
expected_state: None,
|
||||
},
|
||||
)
|
||||
.await?,
|
||||
NativeIntegrityStateWriteResponse { written: true }
|
||||
);
|
||||
assert_eq!(
|
||||
read_state(&mut mcp).await?,
|
||||
NativeIntegrityStateReadResponse {
|
||||
state: Some(STATE_N.to_string()),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
write_state(
|
||||
&mut mcp,
|
||||
NativeIntegrityStateWriteParams {
|
||||
state: STATE_N_PLUS_ONE.to_string(),
|
||||
expected_state: Some("stale-state".to_string()),
|
||||
},
|
||||
)
|
||||
.await?,
|
||||
NativeIntegrityStateWriteResponse { written: false }
|
||||
);
|
||||
assert_eq!(
|
||||
read_state(&mut mcp).await?,
|
||||
NativeIntegrityStateReadResponse {
|
||||
state: Some(STATE_N.to_string()),
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
write_state(
|
||||
&mut mcp,
|
||||
NativeIntegrityStateWriteParams {
|
||||
state: STATE_N_PLUS_ONE.to_string(),
|
||||
expected_state: Some(STATE_N.to_string()),
|
||||
},
|
||||
)
|
||||
.await?,
|
||||
NativeIntegrityStateWriteResponse { written: true }
|
||||
);
|
||||
assert_eq!(
|
||||
read_state(&mut mcp).await?,
|
||||
NativeIntegrityStateReadResponse {
|
||||
state: Some(STATE_N_PLUS_ONE.to_string()),
|
||||
}
|
||||
);
|
||||
|
||||
let request_id = mcp
|
||||
.send_raw_request("nativeIntegrityState/clear", /*params*/ None)
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(
|
||||
to_response::<NativeIntegrityStateClearResponse>(response)?,
|
||||
NativeIntegrityStateClearResponse {}
|
||||
);
|
||||
assert_eq!(
|
||||
read_state(&mut mcp).await?,
|
||||
NativeIntegrityStateReadResponse { state: None }
|
||||
);
|
||||
assert!(
|
||||
!codex_home
|
||||
.path()
|
||||
.join("state/codex_desktop_ssh.json")
|
||||
.exists()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn native_integrity_state_bridge_rejects_unknown_client_surface() -> Result<()> {
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut mcp = TestAppServer::new(codex_home.path()).await?;
|
||||
let initialized = mcp
|
||||
.initialize_with_client_info(client_info("third_party_client"))
|
||||
.await?;
|
||||
let JSONRPCMessage::Response(_) = initialized else {
|
||||
anyhow::bail!("expected initialize response, got {initialized:?}");
|
||||
};
|
||||
|
||||
let request_id = mcp
|
||||
.send_raw_request("nativeIntegrityState/read", /*params*/ None)
|
||||
.await?;
|
||||
let error = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_error_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(error.error.code, -32600);
|
||||
assert_eq!(
|
||||
error.error.message,
|
||||
"native integrity state is unavailable for app-server client \"third_party_client\""
|
||||
);
|
||||
assert_eq!(error.error.data, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn read_state(mcp: &mut TestAppServer) -> Result<NativeIntegrityStateReadResponse> {
|
||||
let request_id = mcp
|
||||
.send_raw_request("nativeIntegrityState/read", /*params*/ None)
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
to_response(response)
|
||||
}
|
||||
|
||||
async fn write_state(
|
||||
mcp: &mut TestAppServer,
|
||||
params: NativeIntegrityStateWriteParams,
|
||||
) -> Result<NativeIntegrityStateWriteResponse> {
|
||||
let request_id = mcp
|
||||
.send_raw_request(
|
||||
"nativeIntegrityState/write",
|
||||
Some(serde_json::to_value(params)?),
|
||||
)
|
||||
.await?;
|
||||
let response: JSONRPCResponse = timeout(
|
||||
DEFAULT_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(request_id)),
|
||||
)
|
||||
.await??;
|
||||
to_response(response)
|
||||
}
|
||||
|
||||
fn client_info(name: &str) -> ClientInfo {
|
||||
ClientInfo {
|
||||
name: name.to_string(),
|
||||
title: None,
|
||||
version: "1.0.0".to_string(),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user