M0: scaffold Cargo workspace, core types, event bus, permission engine, storage actor
Sets up the Cargo workspace (harness-core, harness-tools, harness-providers, harness-mcp, harness-lsp, harness-app, harness-tui) and the ten architecture docs under docs/. harness-core gets its foundational types (session/message/part/model ids), an in-process event bus, a table-driven permission evaluate function, and a SQLite-backed storage actor with schema and roundtrip coverage. Every crate compiles empty and a bin stub prints its version.
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
use serde::Serialize;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::types::{Message, Part, PartId, Session, SessionId};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RunOutcome {
|
||||
Stopped,
|
||||
Aborted,
|
||||
Errored { message: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Level {
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PermissionRequest {
|
||||
pub id: String,
|
||||
pub session_id: SessionId,
|
||||
pub permission: String,
|
||||
pub patterns: Vec<String>,
|
||||
pub metadata: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct JobRecordEvent(pub serde_json::Value);
|
||||
|
||||
/// Serializable so a future HTTP server can pipe this straight to SSE.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum AppEvent {
|
||||
SessionCreated {
|
||||
session: Session,
|
||||
},
|
||||
SessionUpdated {
|
||||
session: Session,
|
||||
},
|
||||
MessageCreated {
|
||||
message: Message,
|
||||
},
|
||||
MessageUpdated {
|
||||
message: Message,
|
||||
},
|
||||
PartUpdated {
|
||||
part: Part,
|
||||
},
|
||||
PartDelta {
|
||||
part_id: PartId,
|
||||
message_id: crate::types::MessageId,
|
||||
delta: String,
|
||||
},
|
||||
RunStarted {
|
||||
session_id: SessionId,
|
||||
},
|
||||
RunFinished {
|
||||
session_id: SessionId,
|
||||
outcome: RunOutcome,
|
||||
},
|
||||
PermissionAsked {
|
||||
request: PermissionRequest,
|
||||
},
|
||||
PermissionResolved {
|
||||
id: String,
|
||||
},
|
||||
JobUpdated {
|
||||
job: JobRecordEvent,
|
||||
},
|
||||
AuthPrompt {
|
||||
provider: String,
|
||||
user_code: String,
|
||||
verification_uri: String,
|
||||
},
|
||||
ServerNotice {
|
||||
level: Level,
|
||||
text: String,
|
||||
},
|
||||
}
|
||||
|
||||
const EVENT_BUS_CAPACITY: usize = 1024;
|
||||
|
||||
/// Thin wrapper over `broadcast`; subscribers that lag just miss old events (`RecvError::Lagged`).
|
||||
#[derive(Clone)]
|
||||
pub struct EventBus(broadcast::Sender<AppEvent>);
|
||||
|
||||
impl EventBus {
|
||||
pub fn new() -> Self {
|
||||
let (tx, _rx) = broadcast::channel(EVENT_BUS_CAPACITY);
|
||||
Self(tx)
|
||||
}
|
||||
|
||||
pub fn publish(&self, event: AppEvent) {
|
||||
// No subscribers is a normal state (e.g. headless `harness run`); ignore the error.
|
||||
let _ = self.0.send(event);
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<AppEvent> {
|
||||
self.0.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EventBus {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn publish_reaches_subscriber() {
|
||||
let bus = EventBus::new();
|
||||
let mut rx = bus.subscribe();
|
||||
bus.publish(AppEvent::RunStarted {
|
||||
session_id: SessionId::new(),
|
||||
});
|
||||
let event = rx.recv().await.unwrap();
|
||||
assert!(matches!(event, AppEvent::RunStarted { .. }));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn publish_with_no_subscribers_does_not_panic() {
|
||||
let bus = EventBus::new();
|
||||
bus.publish(AppEvent::RunStarted {
|
||||
session_id: SessionId::new(),
|
||||
});
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multiple_subscribers_each_get_the_event() {
|
||||
let bus = EventBus::new();
|
||||
let mut rx1 = bus.subscribe();
|
||||
let mut rx2 = bus.subscribe();
|
||||
bus.publish(AppEvent::PermissionResolved { id: "abc".into() });
|
||||
assert!(matches!(
|
||||
rx1.recv().await.unwrap(),
|
||||
AppEvent::PermissionResolved { .. }
|
||||
));
|
||||
assert!(matches!(
|
||||
rx2.recv().await.unwrap(),
|
||||
AppEvent::PermissionResolved { .. }
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user