Add managed command checkpoints requiring fresh human approval (#58045)

## Why

Managed command policies need to require a human decision independently of automatic review, hook allowances, and saved approvals.

## What changed

- Support `decision = "prompt-user"` in managed `requirements.toml` prefix rules, requiring a fresh, one-time human decision for matching commands.
- Add experimental `humanCommand` approval requests for clients advertising both `experimentalApi` and `requiredHumanApproval`. Accept only `accept`, `decline`, or `cancel`; reject commands when no capable client is available.
- Restrict responses to capable clients that received the request, and replay pending prompts to compatible clients on reconnect.
- Enable the capability in the TUI and update protocol schemas and client bindings.

## Testing

Add coverage for fresh decisions overriding saved allowances, hook allowances, and automatic review, plus child-thread approval routing and rejection of responses from incompatible or unrelated clients.

GitOrigin-RevId: 0b95ff6aab7f8a04b7298c8a4329d1fddf6a603e
This commit is contained in:
jif
2026-10-29 11:23:51 +00:00
committed by copyberry
parent 058e99a3aa
commit 9cc14e7988
70 changed files with 976 additions and 83 deletions
+9
View File
@@ -207,6 +207,8 @@ where
#[derive(Clone)]
pub struct InProcessClientStartArgs {
/// This client collects fresh human decisions for managed command checkpoints.
pub required_human_approval: bool,
/// Resolved argv0 dispatch paths used by command execution internals.
pub arg0_paths: Arg0DispatchPaths,
/// Shared config used to initialize app-server runtime.
@@ -253,6 +255,7 @@ impl InProcessClientStartArgs {
/// Builds initialize params from caller-provided metadata.
pub fn initialize_params(&self) -> InitializeParams {
let capabilities = InitializeCapabilities {
required_human_approval: self.required_human_approval,
explicit_gateway_oauth: false,
experimental_api: self.experimental_api,
request_attestation: false,
@@ -949,6 +952,7 @@ mod tests {
.await
.expect("state db should initialize for in-process test");
let client = InProcessAppServerClient::start(InProcessClientStartArgs {
required_human_approval: false,
arg0_paths: Arg0DispatchPaths::default(),
config,
cli_overrides: Vec::new(),
@@ -1172,6 +1176,7 @@ mod tests {
fn test_remote_connect_args(websocket_url: String) -> RemoteAppServerConnectArgs {
RemoteAppServerConnectArgs {
required_human_approval: false,
endpoint: RemoteAppServerEndpoint::WebSocket {
websocket_url,
auth_token: None,
@@ -1536,6 +1541,7 @@ mod tests {
websocket.close(None).await.expect("close should succeed");
});
let client = RemoteAppServerClient::connect(RemoteAppServerConnectArgs {
required_human_approval: false,
endpoint: RemoteAppServerEndpoint::UnixSocket { socket_path },
client_name: "codex-app-server-client-test".to_string(),
client_version: "0.0.0-test".to_string(),
@@ -1623,6 +1629,7 @@ mod tests {
)
.await;
let client = RemoteAppServerClient::connect(RemoteAppServerConnectArgs {
required_human_approval: false,
endpoint: RemoteAppServerEndpoint::WebSocket {
websocket_url,
auth_token: Some(auth_token),
@@ -1643,6 +1650,7 @@ mod tests {
#[tokio::test]
async fn remote_connect_rejects_non_loopback_ws_when_auth_configured() {
let result = RemoteAppServerClient::connect(RemoteAppServerConnectArgs {
required_human_approval: false,
endpoint: RemoteAppServerEndpoint::WebSocket {
websocket_url: "ws://example.com:4500".to_string(),
auth_token: Some("remote-bearer-token".to_string()),
@@ -2178,6 +2186,7 @@ mod tests {
);
let runtime_args = InProcessClientStartArgs {
required_human_approval: false,
arg0_paths: Arg0DispatchPaths::default(),
config: config.clone(),
cli_overrides: Vec::new(),
+3
View File
@@ -84,6 +84,8 @@ pub enum RemoteAppServerEndpoint {
#[derive(Debug, Clone)]
pub struct RemoteAppServerConnectArgs {
/// This client collects fresh human decisions for managed command checkpoints.
pub required_human_approval: bool,
pub endpoint: RemoteAppServerEndpoint,
pub client_name: String,
pub client_version: String,
@@ -95,6 +97,7 @@ pub struct RemoteAppServerConnectArgs {
impl RemoteAppServerConnectArgs {
pub(crate) fn initialize_params(&self) -> InitializeParams {
let capabilities = InitializeCapabilities {
required_human_approval: self.required_human_approval,
explicit_gateway_oauth: false,
experimental_api: self.experimental_api,
request_attestation: false,