diff --git a/resources/rules.json b/resources/rules.json index daa9e82..c2e9905 100644 --- a/resources/rules.json +++ b/resources/rules.json @@ -1,7 +1,14 @@ { "version": "1.0", + "demo_only_domains": ["baidu.com", "www.baidu.com"], "domains": { - "allowed": ["oa.example.com", "erp.example.com", "hr.example.com"] + "allowed": [ + "oa.example.com", + "erp.example.com", + "hr.example.com", + "baidu.com", + "www.baidu.com" + ] }, "pipe_actions": { "allowed": ["click", "type", "navigate", "getText"], diff --git a/src/pipe/browser_tool.rs b/src/pipe/browser_tool.rs index ffaaa05..b2464be 100644 --- a/src/pipe/browser_tool.rs +++ b/src/pipe/browser_tool.rs @@ -97,6 +97,11 @@ impl BrowserPipeTool { "received duplicate init after handshake".to_string(), )); } + BrowserMessage::SubmitTask { .. } => { + return Err(PipeError::UnexpectedMessage( + "received submit_task while waiting for response".to_string(), + )); + } } } } diff --git a/src/pipe/protocol.rs b/src/pipe/protocol.rs index 8493ba5..f78423e 100644 --- a/src/pipe/protocol.rs +++ b/src/pipe/protocol.rs @@ -12,6 +12,9 @@ pub enum BrowserMessage { #[serde(default)] capabilities: Vec, }, + SubmitTask { + instruction: String, + }, Response { seq: u64, success: bool, @@ -31,6 +34,14 @@ pub enum AgentMessage { agent_id: String, supported_actions: Vec, }, + LogEntry { + level: String, + message: String, + }, + TaskComplete { + success: bool, + summary: String, + }, Command { seq: u64, action: Action, diff --git a/src/security/hmac.rs b/src/security/hmac.rs index 26b1542..e11e353 100644 --- a/src/security/hmac.rs +++ b/src/security/hmac.rs @@ -1,6 +1,7 @@ use hmac::{Hmac, Mac}; use serde_json::Value; use sha2::{Digest, Sha256}; +use std::fmt::Write as _; use crate::pipe::Action; use crate::security::SecurityError; @@ -36,13 +37,53 @@ pub fn sign_command( let mut mac = HmacSha256::new_from_slice(session_key) .map_err(|err| SecurityError::Hmac(err.to_string()))?; - mac.update(seq.to_string().as_bytes()); - mac.update(b"|"); - mac.update(action.as_str().as_bytes()); - mac.update(b"|"); - mac.update(expected_domain.as_bytes()); - mac.update(b"|"); - mac.update(serde_json::to_string(params)?.as_bytes()); + let canonical = format!( + "{}\n{}\n{}\n{}", + seq, + action.as_str(), + stable_json_string(params)?, + expected_domain + ); + mac.update(canonical.as_bytes()); Ok(hex::encode(mac.finalize().into_bytes())) } + +fn stable_json_string(value: &Value) -> Result { + match value { + Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => { + serde_json::to_string(value).map_err(SecurityError::from) + } + Value::Array(items) => { + let mut out = String::from("["); + for (index, item) in items.iter().enumerate() { + if index > 0 { + out.push(','); + } + out.push_str(&stable_json_string(item)?); + } + out.push(']'); + Ok(out) + } + Value::Object(map) => { + let mut keys: Vec<&str> = map.keys().map(String::as_str).collect(); + keys.sort_unstable(); + + let mut out = String::from("{"); + for (index, key) in keys.iter().enumerate() { + if index > 0 { + out.push(','); + } + write!( + out, + "{}:{}", + serde_json::to_string(key)?, + stable_json_string(&map[*key])? + ) + .map_err(|err| SecurityError::Hmac(err.to_string()))?; + } + out.push('}'); + Ok(out) + } + } +} diff --git a/tests/task_protocol_test.rs b/tests/task_protocol_test.rs new file mode 100644 index 0000000..14ffba5 --- /dev/null +++ b/tests/task_protocol_test.rs @@ -0,0 +1,96 @@ +use hmac::{Hmac, Mac}; +use serde_json::json; +use sgclaw::pipe::{Action, AgentMessage, BrowserMessage}; +use sgclaw::security::sign_command; +use sha2::Sha256; + +type HmacSha256 = Hmac; + +#[test] +fn browser_submit_task_round_trip_uses_task_wire_format() { + let raw = r#"{"type":"submit_task","instruction":"打开百度并搜索今日汇率"}"#; + let message: BrowserMessage = serde_json::from_str(raw).unwrap(); + + assert_eq!( + message, + BrowserMessage::SubmitTask { + instruction: "打开百度并搜索今日汇率".to_string(), + } + ); + assert_eq!(serde_json::to_string(&message).unwrap(), raw); +} + +#[test] +fn agent_task_complete_and_log_entry_serialize_with_expected_tags() { + let complete_raw = serde_json::to_string(&AgentMessage::TaskComplete { + success: true, + summary: "任务执行完成".to_string(), + }) + .unwrap(); + let log_raw = serde_json::to_string(&AgentMessage::LogEntry { + level: "info".to_string(), + message: "click #submit".to_string(), + }) + .unwrap(); + + assert_eq!( + complete_raw, + r#"{"type":"task_complete","success":true,"summary":"任务执行完成"}"# + ); + assert_eq!( + log_raw, + r#"{"type":"log_entry","level":"info","message":"click #submit"}"# + ); +} + +#[test] +fn sign_command_uses_newline_canonical_string_with_stable_json() { + let session_key = vec![1, 2, 3, 4, 5, 6, 7, 8]; + let params = json!({ + "z": 9, + "a": { "b": 2, "a": 1 } + }); + let canonical = r#"42 +click +{"a":{"a":1,"b":2},"z":9} +oa.example.com"#; + + let mut mac = HmacSha256::new_from_slice(&session_key).unwrap(); + mac.update(canonical.as_bytes()); + let expected = hex::encode(mac.finalize().into_bytes()); + + let actual = sign_command(&session_key, 42, &Action::Click, ¶ms, "oa.example.com").unwrap(); + assert_eq!(actual, expected); +} + +#[test] +fn sign_command_is_order_independent_for_object_params() { + let session_key = vec![8, 7, 6, 5, 4, 3, 2, 1]; + let params_a = json!({ + "z": 9, + "a": { "b": 2, "a": 1 } + }); + let params_b = json!({ + "a": { "a": 1, "b": 2 }, + "z": 9 + }); + + let sig_a = sign_command( + &session_key, + 7, + &Action::Navigate, + ¶ms_a, + "oa.example.com", + ) + .unwrap(); + let sig_b = sign_command( + &session_key, + 7, + &Action::Navigate, + ¶ms_b, + "oa.example.com", + ) + .unwrap(); + + assert_eq!(sig_a, sig_b); +}