qcode/bridge/mod.rs
1//! The bridge between tabs: the agent of one tab lists the other agent tabs of its workspace
2//! and sends one of them a message.
3//!
4//! Three parts, each in its own place:
5//!
6//! - a small MCP server ([`SCRIPT`]) a harness starts inside its container, with two tools,
7//! `list_tabs` and `send_message`. It decides nothing; it asks QCode through a socket;
8//! - the socket, one per open workspace in the workspace's `Containers/MCP/` folder, which profile
9//! containers see read-only at [`MCP_DIR`] ([`socket`]), and the
10//! line of JSON each question and answer is ([`protocol`]);
11//! - the rules QCode answers by ([`rules`]): the person approves the first message between two
12//! tabs, a tab without the network never sends to one with it, and a chain of messages and a
13//! busy sender are both cut off.
14//!
15//! The server is registered in each harness's own user settings in the workspace's home volume
16//! ([`config`]), so the harness starts it like any server the person added.
17//!
18//! Which tab asks is known from a token: every harness tab is started with one in
19//! [`TOKEN_VARIABLE`], the server hands it back with every question, and QCode looks the tab up
20//! by it. A token is random rather than the tab's number, so an agent cannot speak for a tab it
21//! is not in by guessing.
22
23pub mod config;
24pub mod protocol;
25pub mod rules;
26pub mod socket;
27
28#[cfg(test)]
29mod live;
30
31use std::collections::hash_map::RandomState;
32use std::hash::{BuildHasher, Hasher};
33
34use crate::base::paths::MCP_DIR;
35
36/// The MCP server a harness starts, carried inside the binary and written into each open
37/// workspace's `Containers/MCP/` folder, so the server a container starts is always the one this
38/// QCode speaks with.
39pub const SCRIPT: &str = include_str!("../../assets/bridge/qcode-bridge.mjs");
40
41/// The name of the server's file in `Containers/MCP/`.
42pub const SCRIPT_NAME: &str = "qcode-bridge.mjs";
43
44/// The name of the socket in `Containers/MCP/`. The server finds it beside itself.
45pub const SOCKET_NAME: &str = "bridge.sock";
46
47/// The environment variable a harness tab carries its token in.
48pub const TOKEN_VARIABLE: &str = "QCODE_BRIDGE";
49
50/// The name the server is registered under in every harness's settings.
51pub const SERVER_NAME: &str = "qcode";
52
53/// Where the server is inside a profile container.
54#[must_use]
55pub fn script_in_container() -> String {
56 format!("{MCP_DIR}/{SCRIPT_NAME}")
57}
58
59/// A new token for a tab: 128 bits nobody outside this process can predict.
60///
61/// The standard library's hasher is keyed from the operating system's randomness once per
62/// process and never shows its keys, so what it makes of a counter cannot be told in advance by
63/// anything in a container. That is all a token has to be, and it needs no dependency.
64#[must_use]
65pub fn token() -> String {
66 let state = RandomState::new();
67 let mut words = [0u64; 2];
68 for (index, word) in words.iter_mut().enumerate() {
69 let mut hasher = state.build_hasher();
70 hasher.write_usize(index);
71 hasher.write_u128(
72 std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_nanos(),
73 );
74 *word = hasher.finish();
75 }
76 format!("{:016x}{:016x}", words[0], words[1])
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn tokens_are_long_and_never_the_same_twice() {
85 let many: std::collections::HashSet<String> = (0..1000).map(|_| token()).collect();
86 assert_eq!(many.len(), 1000);
87 assert!(many.iter().all(|token| token.len() == 32 && token.chars().all(|c| c.is_ascii_hexdigit())));
88 }
89
90 #[test]
91 fn the_server_sits_in_the_mounted_folder_beside_the_socket_it_looks_for() {
92 assert_eq!(script_in_container(), "/run/qcode-mcp/qcode-bridge.mjs");
93 assert!(SCRIPT.contains(&format!("\"{SOCKET_NAME}\"")), "the server looks for another socket");
94 assert!(SCRIPT.contains(TOKEN_VARIABLE), "the server reads another variable");
95 }
96
97 #[test]
98 fn the_server_names_both_tools_and_speaks_both_eras_of_the_protocol() {
99 for word in ["\"list_tabs\"", "\"send_message\"", "\"initialize\"", "\"server/discover\"", "\"tools/call\""] {
100 assert!(SCRIPT.contains(word), "{word}");
101 }
102 assert!(SCRIPT.contains("2026-07-28") && SCRIPT.contains("2025-11-25"));
103 // The kinds QCode reads, offered to the agent as the only ones there are.
104 assert!(SCRIPT.contains(r#"enum: ["info", "question", "report"]"#));
105 }
106}