blob: e0df254567cd59e220eae4384046dacd897d0852 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
use cua_driver_sdk::{
ConfiguredDriverOptions, CuaDriver, CuaDriverSession, DriverError, RuntimeAuthorizationOptions,
SessionPermissionMode, TrustedSessionOptions, worker::ActionCompletion,
};
use maka_agent_facts::capability::CallResult;
use maka_agent_facts::{language::Language, tools::ToolError};
use serde_json::Value;
use std::sync::Arc;
use tokio_util::sync::CancellationToken;
#[derive(Default)]
enum State {
#[default]
Idle,
Active(Arc<CuaDriver>),
Closed,
}
/// One native runtime per Host process, created only after authorization/T1.
/// Serializing complete calls also keeps observation/action ordering explicit.
#[derive(Default)]
pub struct Driver {
state: State,
sessions: std::collections::HashMap<String, Arc<CuaDriverSession>>,
pub(crate) cursors: crate::cursor::Host,
pub(crate) windows: crate::desktop::worker::Worker,
}
impl Driver {
/// Embedding hosts may provide their trusted cursor-capable executable.
pub fn with_cursor_executable(path: std::path::PathBuf) -> Self {
Self {
cursors: crate::cursor::Host::with_executable(path),
..Default::default()
}
}
pub async fn release_session(&mut self, session: &str) -> Result<(), ToolError> {
if let Some(bound) = self.sessions.remove(session) {
let ended = bound
.end_session(cua_driver_contract::EndSessionInput { session: None })
.await;
// Always revoke the owned lease, including after expiry. A refused
// end cannot dispatch native work; its old private episode is left
// to the SDK's bounded idle cleanup. Transport/cleanup uncertainty
// still stops the Host rather than fabricating settlement.
bound.close();
match ended {
Ok(_) | Err(DriverError::Shutdown) => {}
Err(DriverError::Tool { error_code, .. })
if matches!(
error_code.as_str(),
"permission_denied" | "session_ended" | "authorization_revoked"
) => {}
Err(error) => return Err(ToolError::CleanupUnconfirmed(error.to_string())),
}
}
Ok(())
}
pub async fn invoke(
&mut self,
name: &str,
input: Value,
session_id: &str,
cancellation: &CancellationToken,
language: Language,
) -> Result<CallResult, ToolError> {
if ![
"list_apps",
"list_windows",
"get_window_state",
"click",
"type_text",
"press_key",
"scroll",
"drag",
"set_value",
"launch_app",
]
.contains(&name)
{
return Err(ToolError::Failed("unsupported native operation".into()));
}
if cancellation.is_cancelled() {
return Err(ToolError::Failed(
"Computer Use cancelled before dispatch".into(),
));
}
let session = self.session(session_id, language).await?;
// Every operation, including inventory whose schema has no `session`
// field, uses the same bound SDK surface. Once dispatched, await its
// result: dropping an FFI future is not evidence of settlement.
let result = session
.call_tool(name.into(), input.to_string())
.await
.map_err(|error| native_error(error, language))?;
if result.is_error {
let code = result
.error_code
.as_deref()
.unwrap_or("computer_use_refused");
let message = refusal(code, &result.text, language);
return Err(ToolError::Failed(format!("{code}: {message}")));
}
serde_json::from_str(&result.raw_json)
.map_err(|error| ToolError::OutcomeUnknown(format!("invalid native result: {error}")))
}
async fn session(
&mut self,
id: &str,
language: Language,
) -> Result<&CuaDriverSession, ToolError> {
if matches!(self.state, State::Idle) {
self.state = State::Active(
CuaDriver::create_configured(ConfiguredDriverOptions {
claude_code_compatibility: false,
authorization: RuntimeAuthorizationOptions {
allowed_modes: vec![SessionPermissionMode::Standard],
compatibility_mode: SessionPermissionMode::Standard,
compatibility_capability_manifest_path: None,
compatibility_bounded_manifest_path: None,
unrestricted_acknowledged: false,
max_session_ttl_seconds: 86400,
max_idle_ttl_seconds: 86400,
},
})
// No operation has been dispatched during construction. In
// particular, a non-interactive desktop is a known refusal.
.map_err(|error| ToolError::Failed(error.to_string()))?,
);
}
let State::Active(driver) = &self.state else {
return Err(ToolError::Failed("Computer Use Session is closed".into()));
};
if !self.sessions.contains_key(id) {
let session = driver
.create_trusted_session(TrustedSessionOptions {
// SDK lifecycle tombstones belong to their original transport.
// A reset owns a fresh episode, never revives an old lease's ID.
public_session: format!("maka-{}", uuid::Uuid::new_v4()),
mode: SessionPermissionMode::Standard,
ttl_seconds: 86400,
idle_ttl_seconds: 86400,
capability_manifest_path: None,
bounded_manifest_path: None,
})
.map_err(|error| native_error(error, language))?;
session
.start_session(cua_driver_contract::StartSessionInput {
session: None,
capture_scope: None,
cursor_theme: None,
})
.await
.map_err(|error| native_error(error, language))?;
self.sessions.insert(id.into(), session);
}
Ok(self.sessions.get(id).expect("bound session created"))
}
/// Host calls this after all Session resources retire.
pub async fn close(&mut self) -> Result<(), ToolError> {
let failure = self.windows.close().await.err();
self.cursors.stop().await;
let previous = std::mem::replace(&mut self.state, State::Closed);
self.sessions.clear();
if let State::Active(driver) = previous {
driver
.shutdown()
.await
.map_err(|error| ToolError::CleanupUnconfirmed(error.to_string()))?;
}
failure.map_or(Ok(()), Err)
}
}
fn refusal(code: &str, reason: &str, language: Language) -> String {
if code.starts_with("background_unavailable") {
return format!(
"{}\n{}",
reason.chars().take(8192).collect::<String>(),
crate::language::background_unavailable(language)
);
}
let terminal = matches!(code, "session_ended" | "authorization_revoked")
// The pinned SDK reports expiry under permission_denied, without a
// structured subcode. Keep this adaptation at its native boundary;
// ordinary permission refusals must not imply that reset grants access.
|| (code == "permission_denied" && reason.contains("authorization context expired"));
let mut message: String = reason.chars().take(8192).collect();
if terminal {
message.push_str(crate::language::terminal(language));
}
message
}
fn native_error(error: DriverError, language: Language) -> ToolError {
match error {
DriverError::Shutdown => ToolError::Failed(crate::language::inactive(language).into()),
DriverError::ActionInterrupted {
completion: ActionCompletion::NotStarted,
reason,
} => ToolError::Failed(reason),
DriverError::ActionInterrupted { reason, .. } => ToolError::OutcomeUnknown(reason),
DriverError::Transport { .. }
| DriverError::Protocol { .. }
| DriverError::Worker { .. }
| DriverError::Remote { .. } => ToolError::OutcomeUnknown(error.to_string()),
error => ToolError::Failed(error.to_string()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use cua_driver_contract::{EndSessionInput, GetSessionInput, StartSessionInput};
// This uses only SDK session administration, never desktop observation or
// input. It exercises the real binding/expiry/reset ownership contract.
#[tokio::test]
#[cfg_attr(
windows,
ignore = "the SDK requires an interactive Windows desktop even for lifecycle administration"
)]
async fn bound_sessions_ignore_ended_implicit_state_and_reset_independently() {
let mut driver = Driver::default();
driver.session("a", Language::En).await.unwrap();
driver.session("b", Language::En).await.unwrap();
let a = driver.sessions["a"].clone();
let b = driver.sessions["b"].clone();
let State::Active(native) = &driver.state else {
unreachable!()
};
native
.start_session(StartSessionInput {
session: None,
capture_scope: None,
cursor_theme: None,
})
.await
.unwrap();
native
.end_session(EndSessionInput { session: None })
.await
.unwrap();
let before = a
.get_session(GetSessionInput { session: None })
.await
.unwrap();
let other = b
.get_session(GetSessionInput { session: None })
.await
.unwrap();
assert_ne!(before.session, other.session);
assert!(
before.expires_in_seconds > 300,
"bound lifecycle must use its explicit idle lease"
);
driver.release_session("a").await.unwrap();
assert!(
a.get_session(GetSessionInput { session: None })
.await
.is_err()
);
let after = driver
.session("a", Language::En)
.await
.unwrap()
.get_session(GetSessionInput { session: None })
.await
.unwrap();
assert_ne!(
before.session, after.session,
"reset owns a fresh lifecycle episode"
);
assert_eq!(
other.session,
b.get_session(GetSessionInput { session: None })
.await
.unwrap()
.session
);
// Terminal authority must not make reset itself impossible.
b.end_session(EndSessionInput { session: None })
.await
.unwrap();
driver.release_session("b").await.unwrap();
driver
.session("b", Language::En)
.await
.unwrap()
.get_session(GetSessionInput { session: None })
.await
.unwrap();
driver.release_session("a").await.unwrap();
driver.release_session("b").await.unwrap();
// Exercise the SDK's real expiry diagnostic, not a fabricated error.
let State::Active(native) = &driver.state else {
unreachable!()
};
let expired = native
.create_trusted_session(TrustedSessionOptions {
public_session: format!("expiry-{}", uuid::Uuid::new_v4()),
mode: SessionPermissionMode::Standard,
ttl_seconds: 1,
idle_ttl_seconds: 1,
capability_manifest_path: None,
bounded_manifest_path: None,
})
.unwrap();
expired
.start_session(StartSessionInput {
session: None,
capture_scope: None,
cursor_theme: None,
})
.await
.unwrap();
driver.sessions.insert("expired".into(), expired);
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
// SDK authorization rejects this before any desktop observation.
let failure = driver
.invoke(
"list_apps",
serde_json::json!({}),
"expired",
&CancellationToken::new(),
Language::En,
)
.await
.unwrap_err()
.to_string();
assert!(
failure.contains("authorization context expired"),
"{failure}"
);
assert!(failure.contains("cua_reset"), "{failure}");
assert!(
!refusal(
"permission_denied",
"desktop observation is forbidden by policy",
Language::En,
)
.contains("cua_reset")
);
driver.release_session("expired").await.unwrap();
driver
.session("expired", Language::En)
.await
.unwrap()
.get_session(GetSessionInput { session: None })
.await
.unwrap();
driver.release_session("expired").await.unwrap();
driver.close().await.unwrap();
}
}