feat: align task pipe protocol and hmac
This commit is contained in:
@@ -1,7 +1,14 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0",
|
"version": "1.0",
|
||||||
|
"demo_only_domains": ["baidu.com", "www.baidu.com"],
|
||||||
"domains": {
|
"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": {
|
"pipe_actions": {
|
||||||
"allowed": ["click", "type", "navigate", "getText"],
|
"allowed": ["click", "type", "navigate", "getText"],
|
||||||
|
|||||||
@@ -97,6 +97,11 @@ impl<T: Transport> BrowserPipeTool<T> {
|
|||||||
"received duplicate init after handshake".to_string(),
|
"received duplicate init after handshake".to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
BrowserMessage::SubmitTask { .. } => {
|
||||||
|
return Err(PipeError::UnexpectedMessage(
|
||||||
|
"received submit_task while waiting for response".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ pub enum BrowserMessage {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
capabilities: Vec<String>,
|
capabilities: Vec<String>,
|
||||||
},
|
},
|
||||||
|
SubmitTask {
|
||||||
|
instruction: String,
|
||||||
|
},
|
||||||
Response {
|
Response {
|
||||||
seq: u64,
|
seq: u64,
|
||||||
success: bool,
|
success: bool,
|
||||||
@@ -31,6 +34,14 @@ pub enum AgentMessage {
|
|||||||
agent_id: String,
|
agent_id: String,
|
||||||
supported_actions: Vec<Action>,
|
supported_actions: Vec<Action>,
|
||||||
},
|
},
|
||||||
|
LogEntry {
|
||||||
|
level: String,
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
TaskComplete {
|
||||||
|
success: bool,
|
||||||
|
summary: String,
|
||||||
|
},
|
||||||
Command {
|
Command {
|
||||||
seq: u64,
|
seq: u64,
|
||||||
action: Action,
|
action: Action,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
use hmac::{Hmac, Mac};
|
use hmac::{Hmac, Mac};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
use crate::pipe::Action;
|
use crate::pipe::Action;
|
||||||
use crate::security::SecurityError;
|
use crate::security::SecurityError;
|
||||||
@@ -36,13 +37,53 @@ pub fn sign_command(
|
|||||||
|
|
||||||
let mut mac = HmacSha256::new_from_slice(session_key)
|
let mut mac = HmacSha256::new_from_slice(session_key)
|
||||||
.map_err(|err| SecurityError::Hmac(err.to_string()))?;
|
.map_err(|err| SecurityError::Hmac(err.to_string()))?;
|
||||||
mac.update(seq.to_string().as_bytes());
|
let canonical = format!(
|
||||||
mac.update(b"|");
|
"{}\n{}\n{}\n{}",
|
||||||
mac.update(action.as_str().as_bytes());
|
seq,
|
||||||
mac.update(b"|");
|
action.as_str(),
|
||||||
mac.update(expected_domain.as_bytes());
|
stable_json_string(params)?,
|
||||||
mac.update(b"|");
|
expected_domain
|
||||||
mac.update(serde_json::to_string(params)?.as_bytes());
|
);
|
||||||
|
mac.update(canonical.as_bytes());
|
||||||
|
|
||||||
Ok(hex::encode(mac.finalize().into_bytes()))
|
Ok(hex::encode(mac.finalize().into_bytes()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn stable_json_string(value: &Value) -> Result<String, SecurityError> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
96
tests/task_protocol_test.rs
Normal file
96
tests/task_protocol_test.rs
Normal file
@@ -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<Sha256>;
|
||||||
|
|
||||||
|
#[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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user