1use super::wire::{self, Message};
5use crate::llm::actions::{
6 protocol_trait::{ActionResult, Protocol, Server},
7 ActionDefinition, Parameter, ParameterDefinition, StartupExamples,
8};
9use crate::protocol::{log_template::LogTemplate, EventType, SpawnContext};
10use crate::state::app_state::AppState;
11use anyhow::{bail, ensure, Context, Result};
12use serde_json::{json, Value};
13use std::sync::atomic::{AtomicU32, Ordering};
14use std::sync::Arc;
15use std::sync::LazyLock;
16
17static INJECTED_SERIAL: AtomicU32 = AtomicU32::new(0x8000_0000);
20
21pub fn next_injected_serial() -> u32 {
22 INJECTED_SERIAL
23 .fetch_add(1, Ordering::Relaxed)
24 .max(0x8000_0000)
25}
26
27#[derive(Default, Clone)]
28pub struct DbusProtocol {
29 call: Option<Message>,
31 serials: Option<Arc<AtomicU32>>,
33}
34
35impl DbusProtocol {
36 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn for_call(call: Message, serials: Arc<AtomicU32>) -> Self {
42 Self {
43 call: Some(call),
44 serials: Some(serials),
45 }
46 }
47}
48
49pub fn p(name: &str, type_hint: &str, description: &str, required: bool) -> Parameter {
50 Parameter {
51 name: name.into(),
52 type_hint: type_hint.into(),
53 description: description.into(),
54 required,
55 }
56}
57
58pub fn action(
59 name: &str,
60 description: &str,
61 parameters: Vec<Parameter>,
62 example: Value,
63) -> ActionDefinition {
64 let info = match name {
65 "dbus_return" => "-> D-Bus return {signature}".to_string(),
66 "dbus_error" => "-> D-Bus error {name}: {preview(message,80)}".to_string(),
67 "dbus_emit_signal" => "-> D-Bus signal {interface}.{member} on {path}".to_string(),
68 "dbus_call" => "-> D-Bus call {destination} {interface}.{member}".to_string(),
69 _ => format!("-> D-Bus {}", name.trim_start_matches("dbus_")),
70 };
71 ActionDefinition {
72 name: name.into(),
73 description: description.into(),
74 parameters,
75 example,
76 log_template: Some(LogTemplate::new().with_info(info)),
77 }
78}
79
80pub const VALUES_HELP: &str = "Values in signature order as JSON: numbers, strings, booleans; arrays for a/(...); objects for a{...}; a variant as {\"signature\":\"s\",\"value\":\"x\"} or a plain value";
81
82pub fn return_action() -> ActionDefinition {
83 action(
84 "dbus_return",
85 "Answer the method call with a return value.",
86 vec![
87 p(
88 "signature",
89 "string",
90 "D-Bus signature of the values, e.g. \"s\", \"ai\", \"a{sv}\"; empty for none",
91 false,
92 ),
93 p("values", "array", VALUES_HELP, false),
94 ],
95 json!({"type":"dbus_return","signature":"s","values":["pong"]}),
96 )
97}
98
99pub fn error_action() -> ActionDefinition {
100 action(
101 "dbus_error",
102 "Refuse the method call with a D-Bus error.",
103 vec![
104 p(
105 "name",
106 "string",
107 "Error name, e.g. org.freedesktop.DBus.Error.InvalidArgs or one of your own",
108 true,
109 ),
110 p("message", "string", "Human-readable message", false),
111 ],
112 json!({"type":"dbus_error","name":"org.freedesktop.DBus.Error.AccessDenied","message":"not allowed"}),
113 )
114}
115
116pub fn signal_action() -> ActionDefinition {
117 action(
118 "dbus_emit_signal",
119 "Emit a signal to the peer.",
120 vec![
121 p("path", "string", "Object path the signal comes from", true),
122 p(
123 "interface",
124 "string",
125 "Interface name in dotted form, e.g. net.netget.Demo",
126 true,
127 ),
128 p(
129 "member",
130 "string",
131 "Signal name, e.g. Changed (letters, digits, underscore)",
132 true,
133 ),
134 p(
135 "signature",
136 "string",
137 "Signature of the values (empty for none)",
138 false,
139 ),
140 p("values", "array", VALUES_HELP, false),
141 ],
142 json!({"type":"dbus_emit_signal","path":"/net/netget/Demo","interface":"net.netget.Demo","member":"Changed","signature":"s","values":["ready"]}),
143 )
144}
145
146pub static METHOD_CALL_EVENT: LazyLock<EventType> = LazyLock::new(|| {
147 EventType::new(
148 "dbus_method_call",
149 "A peer called a method. Answer with dbus_return or dbus_error (unless no_reply_expected); dbus_emit_signal may accompany either.",
150 json!({"type":"dbus_return","signature":"s","values":["pong"]}),
151 )
152 .with_parameters(vec![
153 p("path", "string", "Object path the call was made on, e.g. /net/netget/Demo", true),
154 p("interface", "string", "Interface, when the caller named one", false),
155 p("member", "string", "Method name, e.g. ListNames (letters, digits, underscore)", true),
156 p("destination", "string", "Bus name the call was addressed to", false),
157 p("sender", "string", "The caller's unique name", false),
158 p("signature", "string", "D-Bus type signature of args, e.g. \"si\"", true),
159 p("args", "array", "The arguments as JSON, in signature order; variants as {signature, value}", true),
160 p("no_reply_expected", "boolean", "The caller wants no answer", true),
161 ])
162 .with_actions(vec![return_action(), error_action(), signal_action()])
163});
164
165pub static SIGNAL_EVENT: LazyLock<EventType> = LazyLock::new(|| {
166 EventType::new(
167 "dbus_signal_received",
168 "A peer emitted a signal. Nothing has to be answered.",
169 json!({"type":"dbus_emit_signal","path":"/net/netget/Demo","interface":"net.netget.Demo","member":"Seen","signature":"","values":[]}),
170 )
171 .with_parameters(vec![
172 p("path", "string", "Object path, e.g. /org/freedesktop/DBus", true),
173 p("interface", "string", "Interface name in dotted form, e.g. org.freedesktop.DBus", true),
174 p("member", "string", "Signal name, e.g. Changed (letters, digits, underscore)", true),
175 p("sender", "string", "The emitter's unique name", false),
176 p("signature", "string", "D-Bus type signature of args, e.g. \"si\"", true),
177 p("args", "array", "The arguments as JSON", true),
178 ])
179 .with_actions(vec![signal_action()])
180});
181
182pub fn answer_message(
184 call: Option<&Message>,
185 action: &Value,
186 sender: Option<&str>,
187) -> Result<Message> {
188 let kind = action["type"].as_str().unwrap_or_default();
189 let values = |key: &str| -> Result<Vec<Value>> {
190 match action.get(key) {
191 None | Some(Value::Null) => Ok(vec![]),
192 Some(Value::Array(a)) => Ok(a.clone()),
193 Some(other) => bail!("{key} must be an array, not {other}"),
194 }
195 };
196 let signature = action["signature"].as_str().unwrap_or_default();
197 let mut m = match kind {
198 "dbus_return" => {
199 let call =
200 call.context("dbus_return answers a method call; there is none to answer here")?;
201 Message::reply_to(call, signature, values("values")?)
202 }
203 "dbus_error" => {
204 let call =
205 call.context("dbus_error answers a method call; there is none to answer here")?;
206 let name = action["name"].as_str().context("dbus_error needs a name")?;
207 ensure!(
208 wire::valid_interface(name),
209 "{name:?} is not a D-Bus error name (two or more dotted elements)"
210 );
211 Message::error_to(call, name, action["message"].as_str().unwrap_or(name))
212 }
213 "dbus_emit_signal" => {
214 let text = |k: &str| {
215 action[k]
216 .as_str()
217 .with_context(|| format!("dbus_emit_signal needs {k}"))
218 };
219 let (path, interface, member) = (text("path")?, text("interface")?, text("member")?);
220 ensure!(
221 wire::valid_object_path(path),
222 "{path:?} is not an object path"
223 );
224 Message {
225 kind: wire::SIGNAL,
226 path: Some(path.into()),
227 interface: Some(interface.into()),
228 member: Some(member.into()),
229 destination: call.and_then(|c| c.sender.clone()),
230 sender: sender.map(Into::into),
231 signature: signature.into(),
232 body: values("values")?,
233 ..Default::default()
234 }
235 }
236 other => bail!("{other:?} is not a D-Bus answer action"),
237 };
238 if m.sender.is_none() {
239 m.sender = sender.map(Into::into);
240 }
241 m.serial = 1;
243 m.encode()?;
244 Ok(m)
245}
246
247impl Protocol for DbusProtocol {
248 fn protocol_name(&self) -> &'static str {
249 "D-Bus"
250 }
251 fn stack_name(&self) -> &'static str {
252 "ETH>IP>TCP>D-Bus"
253 }
254 fn keywords(&self) -> Vec<&'static str> {
255 vec!["dbus", "d-bus", "message bus", "freedesktop"]
256 }
257 fn description(&self) -> &'static str {
258 "D-Bus server: answers method calls from D-Bus clients (dbus-send, gdbus, any binding) over TCP or a Unix socket, acting as a small message bus"
259 }
260 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
261 vec![signal_action()]
262 }
263 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
264 vec![return_action(), error_action(), signal_action()]
265 }
266 fn get_event_types(&self) -> Vec<EventType> {
267 vec![METHOD_CALL_EVENT.clone(), SIGNAL_EVENT.clone()]
268 }
269 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
270 vec![
271 ParameterDefinition {
272 name: "socket_path".into(),
273 type_hint: "string".into(),
274 description: "Also listen on this Unix socket (unix:path=...), where EXTERNAL authentication checks the peer's uid".into(),
275 required: false,
276 example: json!("/tmp/netget-dbus.sock"),
277 default: None,
278 },
279 ParameterDefinition {
280 name: "allow_anonymous".into(),
281 type_hint: "boolean".into(),
282 description: "Accept SASL ANONYMOUS (the only mechanism a TCP peer can complete here)".into(),
283 required: false,
284 example: json!(false),
285 default: Some(json!(super::DEFAULT_ALLOW_ANONYMOUS)),
286 },
287 ParameterDefinition {
288 name: "idle_timeout_secs".into(),
289 type_hint: "number".into(),
290 description: "Seconds an authenticated connection may stay silent (1-86400)".into(),
291 required: false,
292 example: json!(600),
293 default: Some(json!(super::IDLE_TIMEOUT.as_secs())),
294 },
295 ]
296 }
297 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
298 use crate::protocol::metadata::*;
299 ProtocolMetadataV2::builder()
300 .state(DevelopmentState::Experimental)
301 .privilege_requirement(PrivilegeRequirement::None)
302 .implementation("Hand-rolled D-Bus 1 wire protocol (src/server/dbus/wire.rs): SASL EXTERNAL (checked against SO_PEERCRED on the Unix socket) and ANONYMOUS, both byte orders in, little-endian out, signature-driven JSON marshalling. Acts as a small bus: Hello, RequestName, ReleaseName, GetId, ListNames, NameHasOwner, GetNameOwner, AddMatch/RemoveMatch and org.freedesktop.DBus.Peer are answered in Rust; every other call goes to the model")
303 .llm_control("The answer to every method call (a return value with its signature, or a D-Bus error) and the signals sent back")
304 .e2e_testing("tests/server/dbus: dbus-send (libdbus, peer-to-peer over TCP), gdbus (GLib, as a bus client over the Unix socket) and python dbus-next (asyncio, receiving a signal); raw tests for the size and variant-depth bounds and the fail-closed error")
305 .notes("FAILS CLOSED: a call expecting a reply that the model does not answer gets org.freedesktop.DBus.Error.Failed (LimitsExceeded when the backend is overloaded), never silence. Messages over 1 MiB and values nested past 32 (variants count; the specification allows 64, but NetGet's event pipeline holds 64 JSON levels) close the connection. No Unix file descriptors, no DBUS_COOKIE_SHA1, no routing between peers: each connection talks to NetGet only.")
306 .answers_on_failure()
307 .max_inbound_bytes(wire::MAX_MESSAGE_BYTES)
308 .build()
309 }
310 fn example_prompt(&self) -> &'static str {
311 "Run a D-Bus server where net.netget.Demo.Ping answers pong and everything else is UnknownMethod"
312 }
313 fn get_startup_examples(&self) -> StartupExamples {
314 let base = json!({"type":"open_server","base_stack":"dbus","port":0,
315 "instruction":"Answer net.netget.Demo.Ping with the string pong; refuse anything else with org.freedesktop.DBus.Error.UnknownMethod"});
316 let mut scripted = base.clone();
317 scripted["event_handlers"] = json!([{"event_pattern":"dbus_method_call","handler":{"type":"script","language":"python",
318 "code":"import json,sys\ne=json.load(sys.stdin)['event']\nprint(json.dumps({'actions':[{'type':'dbus_return','signature':'s','values':['pong']}] if e['member']=='Ping' else [{'type':'dbus_error','name':'org.freedesktop.DBus.Error.UnknownMethod','message':e['member']}]}))"}}]);
319 let mut static_example = base.clone();
320 static_example["event_handlers"] = json!([{"event_pattern":"dbus_method_call","handler":{"type":"static","actions":[{"type":"dbus_return","signature":"s","values":["pong"]}]}}]);
321 StartupExamples::new(base, scripted, static_example)
322 }
323 fn group_name(&self) -> &'static str {
324 "Application"
325 }
326}
327
328impl Server for DbusProtocol {
329 fn spawn(
330 &self,
331 ctx: SpawnContext,
332 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
333 {
334 Box::pin(super::spawn(ctx))
335 }
336 fn execute_action(&self, action: Value) -> Result<ActionResult> {
337 if self.call.is_none()
338 && matches!(
339 action["type"].as_str(),
340 Some("dbus_return") | Some("dbus_error")
341 )
342 {
343 let mut placeholder = Message::call("/", None, "Check");
347 placeholder.serial = 1;
348 answer_message(Some(&placeholder), &action, None)?;
349 return Ok(ActionResult::NoAction);
350 }
351 let mut m = answer_message(
352 self.call.as_ref(),
353 &action,
354 self.call.as_ref().and_then(|c| c.destination.as_deref()),
355 )?;
356 m.serial = match &self.serials {
357 Some(counter) => counter.fetch_add(1, Ordering::Relaxed),
358 None => next_injected_serial(),
359 };
360 if m.kind != wire::SIGNAL {
361 if let Some(call) = &self.call {
362 if !call.expects_reply() {
363 return Ok(ActionResult::NoAction);
364 }
365 }
366 }
367 Ok(ActionResult::Output(m.encode()?))
368 }
369}