use ed25519_dalek::{Signer, SigningKey};
use std::env;
use std::path::Path;
use tokio::fs;
#[derive(Debug)]
pub struct AgentIdentity {
pub name: String,
pub sekuire_id: String,
private_key: Option<SigningKey>,
}
pub fn calculate_sekuire_id(model: &str, system_prompt: &str, tools: &str) -> String {
let prompt_hash = blake3::hash(system_prompt.trim().as_bytes());
let canonical_tools = match serde_json::from_str::<serde_json::Value>(tools) {
Ok(parsed) => serde_json::to_string(&parsed).unwrap_or_else(|_| tools.to_string()),
Err(_) => tools.to_string(),
};
let tools_hash = blake3::hash(canonical_tools.as_bytes());
let fingerprint = format!(
"model:{}|prompt:{}|tools:{}",
model, prompt_hash, tools_hash
);
blake3::hash(fingerprint.as_bytes()).to_string()
}
fn parse_signing_key(hex_str: &str) -> Option<SigningKey> {
let bytes = hex::decode(hex_str.trim()).ok()?;
match bytes.len() {
64 => {
let seed: [u8; 32] = bytes[..32].try_into().ok()?;
Some(SigningKey::from_bytes(&seed))
}
32 => {
let seed: [u8; 32] = bytes.try_into().ok()?;
Some(SigningKey::from_bytes(&seed))
}
_ => None,
}
}
impl AgentIdentity {
pub async fn load() -> Result<Self, Box<dyn std::error::Error>> {
let manifest_path = Path::new("sekuire.json");
let mut name = "Unknown".to_string();
let mut sekuire_id = "8a6ecffc...".to_string();
if manifest_path.exists() {
let content = fs::read_to_string(manifest_path).await?;
let v: serde_json::Value = serde_json::from_str(&content)?;
if let Some(n) = v.get("name").and_then(|v| v.as_str()) {
name = n.to_string();
}
} else if let Ok(n) = env::var("SEKUIRE_AGENT_NAME") {
name = n;
if let Ok(id) = env::var("SEKUIRE_AGENT_ID") {
sekuire_id = id;
}
} else {
return Err("sekuire.json not found and env vars missing".into());
}
let mut private_key_hex = env::var("SEKUIRE_PRIVATE_KEY").ok();
if private_key_hex.is_none() {
let key_path = Path::new(".sekuire/secret.key");
if key_path.exists() {
private_key_hex = Some(fs::read_to_string(key_path).await?.trim().to_string());
}
}
let private_key = private_key_hex.and_then(|h| parse_signing_key(&h));
Ok(Self {
name,
sekuire_id,
private_key,
})
}
pub fn name(&self) -> &str {
&self.name
}
pub fn sekuire_id(&self) -> &str {
&self.sekuire_id
}
pub fn sign(&self, payload: &[u8]) -> String {
match &self.private_key {
Some(key) => {
let signature = key.sign(payload);
hex::encode(signature.to_bytes())
}
None => String::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
use std::path::PathBuf;
#[derive(Deserialize)]
struct IdentityTestFile {
vectors: Vec<IdentityVector>,
}
#[derive(Deserialize)]
struct IdentityVector {
name: String,
input: IdentityInput,
expected: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct IdentityInput {
model: String,
system_prompt: String,
tools: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SigningTestFile {
key_pair: SigningKeyPair,
vectors: Vec<SigningVector>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SigningKeyPair {
public_key: String,
secret_key: String,
}
#[derive(Deserialize)]
struct SigningVector {
name: String,
message: String,
signature: String,
}
fn read_test_vector_file(file_name: &str) -> String {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let mut candidates = Vec::new();
if let Ok(override_dir) = std::env::var("SEKUIRE_TEST_VECTORS_DIR") {
candidates.push(PathBuf::from(override_dir).join(file_name));
}
candidates.push(
manifest_dir
.join("../platform/packages/protocol/test-vectors")
.join(file_name),
);
candidates.push(manifest_dir.join("../oags/specs/test-vectors").join(file_name));
candidates.push(
manifest_dir
.join("../vision/oss/specs/test-vectors")
.join(file_name),
);
let mut attempted_paths = Vec::new();
for path in candidates {
let display_path = path.display().to_string();
attempted_paths.push(display_path.clone());
if path.exists() {
return std::fs::read_to_string(&path).unwrap_or_else(|err| {
panic!(
"failed to read {} at {}: {}",
file_name,
display_path,
err
)
});
}
}
panic!(
"failed to locate {}. attempted paths:\n{}",
file_name,
attempted_paths.join("\n")
);
}
#[test]
fn test_identity_vectors() {
let content = read_test_vector_file("identity.json");
let test_file: IdentityTestFile =
serde_json::from_str(&content).expect("failed to parse identity test vectors");
for vector in &test_file.vectors {
let result = calculate_sekuire_id(
&vector.input.model,
&vector.input.system_prompt,
&vector.input.tools,
);
assert_eq!(
result, vector.expected,
"identity vector '{}' failed: got {}, expected {}",
vector.name, result, vector.expected
);
}
}
#[test]
fn test_signing_vectors() {
let content = read_test_vector_file("signing.json");
let test_file: SigningTestFile =
serde_json::from_str(&content).expect("failed to parse signing test vectors");
let signing_key =
parse_signing_key(&test_file.key_pair.secret_key).expect("failed to parse secret key");
let public_key_hex = hex::encode(signing_key.verifying_key().as_bytes());
assert_eq!(
public_key_hex, test_file.key_pair.public_key,
"derived public key does not match expected"
);
let identity = AgentIdentity {
name: "test".to_string(),
sekuire_id: "test".to_string(),
private_key: Some(signing_key),
};
for vector in &test_file.vectors {
let sig = identity.sign(vector.message.as_bytes());
assert_eq!(
sig, vector.signature,
"signing vector '{}' failed: got {}, expected {}",
vector.name, sig, vector.signature
);
}
}
}