Skip to content

Commit b272c65

Browse files
0xrinegadeclaude
andcommitted
feat(collab): Add real-time collaborative investigation system with federation
Implements a comprehensive collaborative blockchain forensics system: ## Core Collaborative Features - Shared tmux sessions for synchronized multi-terminal investigation - User presence tracking with cursor positions and activity status - Shared annotations with severity levels (Info/Important/Warning/Critical) - Invite codes for easy session sharing (6-char alphanumeric) - WebSocket server for browser-based participants ## Federation Network - Peer-to-peer session discovery across OSVM nodes - Persistent federation state (~/.osvm/collab/federation.json) - Auto-save on peer add/remove operations - Node ID generation via CRC32 hash - Session announcements with metadata (participants, status, tags) ## TUI Dashboard (Tab 6: Federation) - ASCII network topology graph visualization - Peer list with health status (Online/Offline/Connecting) - Session discovery display with participant counts - Live annotation stream with toggle (A key) - Interactive peer management (a=add, d=delete, j/k/n/p=navigate) - Background health polling (30s interval) - Refresh on demand (r key) - Session join from TUI (J key after s/S selection) ## CLI Commands - osvm collab start/join/list - Session management - osvm collab annotate - Add investigation notes - osvm collab server - WebSocket bridge - osvm collab peers add/remove/list - Peer management - osvm collab discover - Find federated sessions - osvm collab publish - Announce local session - osvm collab status - Show federation network state ## Integration Tests - Session announcement serialization - Federated annotation serialization - WebSocket URL generation - Node ID consistency verification - Full federation flow test (ignored, requires tmux) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent d80612c commit b272c65

File tree

16 files changed

+7833
-17
lines changed

16 files changed

+7833
-17
lines changed

src/clparse.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod audit;
99
mod balance;
1010
mod bbs;
1111
mod chat;
12+
mod collab;
1213
mod database;
1314
mod deploy;
1415
mod doctor;
@@ -265,6 +266,7 @@ Issues & feedback: https://github.com/opensvm/osvm-cli/issues",
265266
// Core commands - using modular builders
266267
.subcommand(balance::build_balance_command())
267268
.subcommand(bbs::build_bbs_command())
269+
.subcommand(collab::build_collab_command())
268270
.subcommand(rpc::build_rpc_command())
269271
.subcommand(examples::build_examples_command())
270272
.subcommand(chat::build_chat_command())

src/clparse/collab.rs

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
//! Collaborative Investigation command definitions
2+
//!
3+
//! Real-time collaborative blockchain investigation with shared TUI sessions,
4+
//! user presence, and annotations.
5+
6+
use clap::{Arg, ArgAction, Command};
7+
8+
pub fn build_collab_command() -> Command {
9+
Command::new("collab")
10+
.about("Real-time collaborative blockchain investigation")
11+
.long_about(
12+
"Collaborative Investigation System for team-based blockchain forensics.\n\
13+
\n\
14+
Enables multiple investigators to share a TUI session in real-time,\n\
15+
see each other's cursors, and add annotations to wallets and transactions.\n\
16+
\n\
17+
Features:\n\
18+
• Shared tmux sessions - multiple terminals, same view\n\
19+
• WebSocket bridge - browser-based participants\n\
20+
• User presence - see who's connected and their cursors\n\
21+
• Shared annotations - notes visible to all participants\n\
22+
• Invite codes - easy session sharing\n\
23+
\n\
24+
Quick Start:\n\
25+
• osvm collab start --wallet 5Q544f... # Start investigation\n\
26+
• osvm collab join ABC123 # Join with invite code\n\
27+
• osvm collab annotate <WALLET> \"Note\" # Add annotation\n\
28+
• osvm collab server # Start WebSocket server",
29+
)
30+
.arg_required_else_help(true)
31+
// Start a new session
32+
.subcommand(
33+
Command::new("start")
34+
.about("Start a new collaborative investigation session")
35+
.arg(
36+
Arg::new("name")
37+
.long("name")
38+
.short('n')
39+
.value_name("NAME")
40+
.help("Session name (e.g., \"Hack Investigation\")"),
41+
)
42+
.arg(
43+
Arg::new("wallet")
44+
.long("wallet")
45+
.short('w')
46+
.value_name("ADDRESS")
47+
.help("Target wallet address to investigate"),
48+
)
49+
.arg(
50+
Arg::new("max")
51+
.long("max-participants")
52+
.short('m')
53+
.value_name("NUM")
54+
.help("Maximum number of participants (default: 10)"),
55+
)
56+
.arg(
57+
Arg::new("password")
58+
.long("password")
59+
.short('P')
60+
.value_name("PASS")
61+
.help("Password to protect the session"),
62+
),
63+
)
64+
// Join an existing session
65+
.subcommand(
66+
Command::new("join")
67+
.about("Join an existing collaborative session")
68+
.arg(
69+
Arg::new("code")
70+
.value_name("INVITE_CODE")
71+
.help("6-character invite code (e.g., ABC123)")
72+
.required(true)
73+
.index(1),
74+
)
75+
.arg(
76+
Arg::new("name")
77+
.long("name")
78+
.short('n')
79+
.value_name("NAME")
80+
.help("Your display name"),
81+
),
82+
)
83+
// List active sessions
84+
.subcommand(
85+
Command::new("list")
86+
.about("List active collaborative sessions")
87+
.visible_alias("ls"),
88+
)
89+
// Add annotation
90+
.subcommand(
91+
Command::new("annotate")
92+
.about("Add an annotation to a wallet or transaction")
93+
.visible_alias("note")
94+
.arg(
95+
Arg::new("target")
96+
.value_name("TARGET")
97+
.help("Wallet address or transaction signature")
98+
.required(true)
99+
.index(1),
100+
)
101+
.arg(
102+
Arg::new("text")
103+
.value_name("TEXT")
104+
.help("Annotation text")
105+
.required(true)
106+
.index(2),
107+
)
108+
.arg(
109+
Arg::new("severity")
110+
.long("severity")
111+
.short('s')
112+
.value_name("LEVEL")
113+
.value_parser(["info", "important", "warning", "critical", "question"])
114+
.help("Severity level (info, important, warning, critical, question)"),
115+
),
116+
)
117+
// List annotations
118+
.subcommand(
119+
Command::new("annotations")
120+
.about("List all annotations in the session"),
121+
)
122+
// Start WebSocket server
123+
.subcommand(
124+
Command::new("server")
125+
.about("Start WebSocket server for browser-based participants")
126+
.arg(
127+
Arg::new("port")
128+
.long("port")
129+
.short('p')
130+
.value_name("PORT")
131+
.default_value("8080")
132+
.help("Port to listen on"),
133+
)
134+
.arg(
135+
Arg::new("host")
136+
.long("host")
137+
.value_name("HOST")
138+
.default_value("0.0.0.0")
139+
.help("Host to bind to"),
140+
),
141+
)
142+
// Federation: Peer management
143+
.subcommand(
144+
Command::new("peers")
145+
.about("Manage federation peers")
146+
.arg_required_else_help(true)
147+
.subcommand(
148+
Command::new("add")
149+
.about("Add a federation peer")
150+
.arg(
151+
Arg::new("address")
152+
.value_name("ADDRESS")
153+
.help("Peer address (e.g., http://192.168.1.100:8080)")
154+
.required(true)
155+
.index(1),
156+
),
157+
)
158+
.subcommand(
159+
Command::new("remove")
160+
.about("Remove a federation peer")
161+
.arg(
162+
Arg::new("address")
163+
.value_name("ADDRESS")
164+
.help("Peer address to remove")
165+
.required(true)
166+
.index(1),
167+
),
168+
)
169+
.subcommand(
170+
Command::new("list")
171+
.about("List configured federation peers"),
172+
),
173+
)
174+
// Federation: Discover sessions
175+
.subcommand(
176+
Command::new("discover")
177+
.about("Discover collaborative sessions from federated peers"),
178+
)
179+
// Federation: Publish session
180+
.subcommand(
181+
Command::new("publish")
182+
.about("Publish local session to the federation network")
183+
.arg(
184+
Arg::new("session_id")
185+
.long("session")
186+
.value_name("ID")
187+
.help("Session ID to publish (default: current session)"),
188+
),
189+
)
190+
// Federation: Status
191+
.subcommand(
192+
Command::new("status")
193+
.about("Show federation network status"),
194+
)
195+
// Help
196+
.subcommand(
197+
Command::new("help")
198+
.about("Show help for collab commands"),
199+
)
200+
}

src/main.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ fn is_known_command(sub_command: &str) -> bool {
5353
| "ovsm"
5454
| "mcp"
5555
| "bbs"
56+
| "collab"
5657
| "mount"
5758
| "snapshot"
5859
| "stream"
@@ -342,6 +343,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
342343
.map_err(|e| e.into());
343344
}
344345

346+
// Handle collab command early - real-time collaborative investigation
347+
if sub_command == "collab" {
348+
return handle_collab_command(sub_matches).await;
349+
}
350+
345351
// Handle snapshot command early - it doesn't need keypair or Solana config
346352
if sub_command == "snapshot" {
347353
return commands::snapshot::execute_snapshot_command(sub_matches)
@@ -809,6 +815,92 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
809815
Ok(())
810816
}
811817

818+
/// Handle collaborative investigation commands
819+
async fn handle_collab_command(
820+
sub_matches: &clap::ArgMatches,
821+
) -> Result<(), Box<dyn std::error::Error>> {
822+
use crate::services::collab_service;
823+
824+
// Get the subcommand
825+
let subcommand = sub_matches.subcommand();
826+
827+
match subcommand {
828+
Some(("start", args)) => {
829+
let name = args.get_one::<String>("name").cloned();
830+
let wallet = args.get_one::<String>("wallet").cloned();
831+
let max = args.get_one::<String>("max").and_then(|s| s.parse().ok());
832+
let password = args.get_one::<String>("password").cloned();
833+
collab_service::start_session(name, wallet, max, password).await?;
834+
}
835+
Some(("join", args)) => {
836+
let code = args.get_one::<String>("code")
837+
.ok_or("Invite code required")?;
838+
let name = args.get_one::<String>("name").cloned();
839+
collab_service::join_session(code, name).await?;
840+
}
841+
Some(("list", _)) | Some(("ls", _)) => {
842+
collab_service::list_sessions().await?;
843+
}
844+
Some(("annotate", args)) | Some(("note", args)) => {
845+
let target = args.get_one::<String>("target")
846+
.ok_or("Target required")?;
847+
let text = args.get_one::<String>("text")
848+
.ok_or("Annotation text required")?;
849+
let severity = args.get_one::<String>("severity").cloned();
850+
collab_service::add_annotation(None, target, text, severity).await?;
851+
}
852+
Some(("annotations", _)) => {
853+
collab_service::list_annotations(None).await?;
854+
}
855+
Some(("server", args)) => {
856+
let port = args.get_one::<String>("port")
857+
.and_then(|s| s.parse().ok())
858+
.unwrap_or(8080);
859+
let host = args.get_one::<String>("host").cloned();
860+
collab_service::start_server(port, host).await?;
861+
}
862+
// Federation commands
863+
Some(("peers", peer_args)) => {
864+
match peer_args.subcommand() {
865+
Some(("add", add_args)) => {
866+
let address = add_args.get_one::<String>("address")
867+
.ok_or("Peer address required")?;
868+
collab_service::add_federation_peer(address).await?;
869+
}
870+
Some(("remove", rm_args)) => {
871+
let address = rm_args.get_one::<String>("address")
872+
.ok_or("Peer address required")?;
873+
collab_service::remove_federation_peer(address).await?;
874+
}
875+
Some(("list", _)) | None => {
876+
collab_service::list_federation_peers().await?;
877+
}
878+
_ => {
879+
collab_service::list_federation_peers().await?;
880+
}
881+
}
882+
}
883+
Some(("discover", _)) => {
884+
collab_service::discover_sessions().await?;
885+
}
886+
Some(("publish", _)) => {
887+
collab_service::publish_session_to_federation(None).await?;
888+
}
889+
Some(("status", _)) => {
890+
collab_service::show_federation_status().await?;
891+
}
892+
Some(("help", _)) | None => {
893+
collab_service::print_help();
894+
}
895+
Some((cmd, _)) => {
896+
eprintln!("Unknown collab subcommand: {}", cmd);
897+
collab_service::print_help();
898+
}
899+
}
900+
901+
Ok(())
902+
}
903+
812904
/// Log command execution to ClickHouse if available
813905
async fn log_command_execution(
814906
command_name: &str,

0 commit comments

Comments
 (0)