use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use std::collections::{BTreeMap, HashMap};
use crate::knowledge::KnowledgeEntry;
use crate::store::WakeCascade;
type HmacSha256 = Hmac<Sha256>;
pub fn create_token(session_id: &str, step: u32) -> String {
let payload = format!("{}.{}", session_id, step);
let key = format!("wake-{}-ritual", session_id);
let mut mac =
HmacSha256::new_from_slice(key.as_bytes()).expect("HMAC can take key of any size");
mac.update(payload.as_bytes());
let signature = BASE64.encode(mac.finalize().into_bytes());
format!("{}.{}", payload, &signature[..16])
}
pub fn verify_token(token: &str) -> Result<(String, u32), String> {
let parts: Vec<&str> = token.split('.').collect();
if parts.len() != 3 {
return Err("Invalid token format".to_string());
}
let session_id = parts[0];
let step: u32 = parts[1]
.parse()
.map_err(|_| "Invalid step in token".to_string())?;
let provided_sig = parts[2];
let payload = format!("{}.{}", session_id, step);
let key = format!("wake-{}-ritual", session_id);
let mut mac =
HmacSha256::new_from_slice(key.as_bytes()).expect("HMAC can take key of any size");
mac.update(payload.as_bytes());
let expected_sig = BASE64.encode(mac.finalize().into_bytes());
if &expected_sig[..16] != provided_sig {
return Err("Invalid token signature".to_string());
}
Ok((session_id.to_string(), step))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PhraseSource {
Authored,
Derived,
Auto,
}
impl PhraseSource {
pub fn as_str(self) -> &'static str {
match self {
PhraseSource::Authored => "authored",
PhraseSource::Derived => "derived",
PhraseSource::Auto => "auto",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"authored" => Some(PhraseSource::Authored),
"derived" => Some(PhraseSource::Derived),
"auto" => Some(PhraseSource::Auto),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct SourceCounts {
#[serde(default)]
pub authored: u32,
#[serde(default)]
pub derived: u32,
#[serde(default)]
pub auto: u32,
}
impl SourceCounts {
fn bump(&mut self, source: PhraseSource) {
match source {
PhraseSource::Authored => self.authored += 1,
PhraseSource::Derived => self.derived += 1,
PhraseSource::Auto => self.auto += 1,
}
}
fn total(&self) -> u32 {
self.authored + self.derived + self.auto
}
fn add(&mut self, other: &SourceCounts) {
self.authored += other.authored;
self.derived += other.derived;
self.auto += other.auto;
}
}
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
pub struct BloomChunkMeta {
#[serde(default)]
pub unhinted: SourceCounts,
#[serde(default)]
pub revealed: SourceCounts,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WakeSession {
pub session_id: String,
pub agent: String,
pub wake: Option<i64>,
pub model_id: Option<String>,
pub bloom_ids: Vec<String>,
pub current_index: usize,
pub current_chunk_index: u16,
pub step: u32,
pub unhinted_count: u32,
pub revealed_count: u32,
pub unjudged_count: u32,
pub created_at: i64,
pub completed_at: Option<i64>,
pub prev_step: Option<u32>,
pub last_status: Option<String>,
pub bloom_chunk_meta: Vec<BloomChunkMeta>,
}
impl WakeSession {
pub fn new(
cascade: &WakeCascade,
agent: String,
wake: Option<i64>,
model_id: Option<String>,
) -> Self {
let bloom_ids: Vec<String> = cascade
.core
.iter()
.chain(cascade.recent.iter())
.chain(cascade.bridges.iter())
.map(|entry| entry.id.clone())
.collect();
let bloom_chunk_meta = vec![BloomChunkMeta::default(); bloom_ids.len()];
Self {
session_id: uuid::Uuid::new_v4().to_string(),
agent,
wake,
model_id,
bloom_ids,
current_index: 0,
current_chunk_index: 0,
step: 0,
unhinted_count: 0,
revealed_count: 0,
unjudged_count: 0,
created_at: chrono::Utc::now().timestamp(),
completed_at: None,
prev_step: None,
last_status: None,
bloom_chunk_meta,
}
}
pub fn current_bloom_id(&self) -> Option<&str> {
self.bloom_ids.get(self.current_index).map(|s| s.as_str())
}
pub fn total_blooms(&self) -> usize {
self.bloom_ids.len()
}
pub fn bucket_totals(&self) -> Buckets {
let mut buckets = Buckets::default();
for meta in &self.bloom_chunk_meta {
buckets.unhinted.add(&meta.unhinted);
buckets.revealed.add(&meta.revealed);
}
buckets
}
pub fn current_bloom_position(&self) -> usize {
self.current_index + 1
}
pub fn is_complete(&self) -> bool {
self.current_index >= self.bloom_ids.len()
}
pub fn advance(
&mut self,
bloom_total_chunks: u16,
bucket: crate::wake_guess::Bucket,
source: PhraseSource,
) {
debug_assert!(!self.is_complete(), "advance called on completed session");
debug_assert!(
(self.current_chunk_index as usize) < bloom_total_chunks.max(1) as usize,
"current_chunk_index {} >= bloom_total_chunks {}",
self.current_chunk_index,
bloom_total_chunks
);
match bucket {
crate::wake_guess::Bucket::Unhinted => self.unhinted_count += 1,
crate::wake_guess::Bucket::Revealed => self.revealed_count += 1,
}
if let Some(meta) = self.bloom_chunk_meta.get_mut(self.current_index) {
match bucket {
crate::wake_guess::Bucket::Unhinted => meta.unhinted.bump(source),
crate::wake_guess::Bucket::Revealed => meta.revealed.bump(source),
}
}
self.step = self.step.saturating_add(1);
self.advance_chunk_or_bloom(bloom_total_chunks);
}
fn advance_chunk_or_bloom(&mut self, bloom_total_chunks: u16) {
let next_chunk = self.current_chunk_index.saturating_add(1);
if (next_chunk as usize) < bloom_total_chunks.max(1) as usize {
self.current_chunk_index = next_chunk;
} else {
self.current_index += 1;
self.current_chunk_index = 0;
}
debug_assert!(
self.current_index <= self.bloom_ids.len(),
"current_index {} overshot bloom_ids.len() {}",
self.current_index,
self.bloom_ids.len()
);
}
pub fn clamp_if_chunks_shrank(&mut self, bloom_total_chunks: u16) -> bool {
let total = bloom_total_chunks.max(1) as usize;
if (self.current_chunk_index as usize) >= total {
self.advance_unjudged();
true
} else {
false
}
}
pub fn advance_unjudged(&mut self) {
self.unjudged_count = self.unjudged_count.saturating_add(1);
self.step = self.step.saturating_add(1);
self.current_index += 1;
self.current_chunk_index = 0;
}
pub fn current_bloom_is_missing(&self, present: &HashMap<String, KnowledgeEntry>) -> bool {
match self.current_bloom_id() {
Some(id) => !present.contains_key(id),
None => false,
}
}
}
pub fn authored_phrases(entry: &KnowledgeEntry) -> Vec<String> {
if !entry.wake_phrases.is_empty() {
entry.wake_phrases.clone()
} else if let Some(ref phrase) = entry.wake_phrase {
vec![phrase.clone()]
} else {
Vec::new()
}
}
#[derive(Debug, Serialize)]
pub struct WakeBeginResponse {
pub status: String,
pub session: String,
pub prompt: BloomPrompt,
pub progress: Progress,
pub excluded: BTreeMap<String, usize>,
pub wake: Option<i64>,
pub model: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub warnings: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct WakeRespondResponse {
pub status: String,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub replayed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub bucket: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub guess: Option<String>,
#[serde(rename = "match", skip_serializing_if = "Option::is_none")]
pub match_info: Option<MatchInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bloom: Option<BloomFull>,
pub session: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub next: Option<BloomPrompt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub progress: Option<Progress>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<Summary>,
pub wake: Option<i64>,
pub model: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct MatchInfo {
pub kind: String,
pub phrase_index: Option<usize>,
}
#[derive(Debug, Serialize)]
pub struct WakeErrorResponse {
pub status: String,
pub error: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub expected_id: Option<String>,
}
#[derive(Debug)]
pub struct WakeRejection(pub WakeErrorResponse);
impl std::fmt::Display for WakeRejection {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let json = serde_json::to_string(&self.0).map_err(|_| std::fmt::Error)?;
f.write_str(&json)
}
}
impl std::error::Error for WakeRejection {}
#[derive(Debug, Serialize)]
pub struct BloomPrompt {
pub id: String,
pub title: String,
pub phrase_source: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub chunk: Option<ChunkRef>,
}
#[derive(Debug, Serialize, Clone)]
pub struct ChunkRef {
pub index: u16,
pub total: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub oversized: Option<bool>,
}
#[derive(Debug, Serialize)]
pub struct BloomFull {
pub id: String,
pub title: String,
pub phrases: Vec<String>,
pub phrase_source: String,
pub content: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub chunk: Option<ChunkRef>,
}
#[derive(Debug, Serialize)]
pub struct Progress {
pub current: usize,
pub total: usize,
pub bloom_current: usize,
pub bloom_total: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub buckets: Option<BucketTotals>,
}
#[derive(Debug, Serialize)]
pub struct BucketTotals {
pub unhinted: u32,
pub revealed: u32,
}
#[derive(Debug, Serialize)]
pub struct Summary {
pub chunks: usize,
pub blooms: usize,
pub buckets: Buckets,
pub unjudged: u32,
}
#[derive(Debug, Clone, Copy, Default, Serialize, PartialEq, Eq)]
pub struct Buckets {
pub unhinted: SourceCounts,
pub revealed: SourceCounts,
}
impl Buckets {
pub fn totals(&self) -> BucketTotals {
BucketTotals {
unhinted: self.unhinted.total(),
revealed: self.revealed.total(),
}
}
}