Skip to main content

ados_protocol/ipc/
injector.rs

1//! The autonomous-injector declaration a writer sends on a command socket.
2
3/// Control-frame prefix a writer uses to declare that it is an AUTONOMOUS
4/// INJECTOR: a producer of commands generated on the node rather than by a
5/// human operator (the plugin host driving guided setpoints, the MSP relay).
6/// The rest of the payload is the writer's `client_id`, optionally followed by
7/// `;ticket=<ticket>` — the attestation the FC-write path verifies against the
8/// pairing key so a spoofed `client_id` cannot claim the injector lane.
9///
10/// Like [`super::IPC_DECLARE_OFF_BOX_SOURCE`] this is one-way and downward: declaring
11/// injector can only ever SUBJECT the writer to the PIC arbiter (an operator
12/// holding the manual-control claim refuses it), never grant it authority. A
13/// writer that does not declare is treated as the operator/human path and is
14/// NEVER gated — the load-bearing invariant. Sticky for the connection, never
15/// forwarded to the flight controller.
16pub const IPC_DECLARE_INJECTOR_PREFIX: &[u8] = b"\x00ados-ipc/injector=";
17
18/// A writer's declared autonomous-injector identity (see
19/// [`IPC_DECLARE_INJECTOR_PREFIX`]). Carried raw to the FC-write path, which
20/// verifies `ticket` against the pairing key before treating `client_id` as the
21/// attested injector; a `None` ticket is an unverified claim that can never win
22/// the lane away from a human holder.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct InjectorClaim {
25    pub client_id: String,
26    pub ticket: Option<String>,
27}
28
29/// Build the [`IPC_DECLARE_INJECTOR_PREFIX`] control frame a writer sends to
30/// declare itself an autonomous injector. The inverse of
31/// [`parse_injector_declaration`], kept beside it so a producer's declaration
32/// and the router's parse cannot drift on the wire format. `ticket` is the
33/// pairing-key attestation for `client_id`; omit it on an unpaired node.
34pub fn encode_injector_declaration(client_id: &str, ticket: Option<&str>) -> Vec<u8> {
35    let mut out = IPC_DECLARE_INJECTOR_PREFIX.to_vec();
36    out.extend_from_slice(client_id.as_bytes());
37    if let Some(t) = ticket {
38        out.extend_from_slice(b";ticket=");
39        out.extend_from_slice(t.as_bytes());
40    }
41    out
42}
43
44/// Parse an [`IPC_DECLARE_INJECTOR_PREFIX`] control frame into an
45/// [`InjectorClaim`]. `None` when the payload is not an injector declaration or
46/// carries an empty client id (a declaration with no identity is not a claim).
47pub fn parse_injector_declaration(payload: &[u8]) -> Option<InjectorClaim> {
48    let body = payload.strip_prefix(IPC_DECLARE_INJECTOR_PREFIX)?;
49    let text = std::str::from_utf8(body).ok()?;
50    let (client_id, ticket) = match text.split_once(";ticket=") {
51        Some((id, tkt)) => (id, (!tkt.is_empty()).then(|| tkt.to_string())),
52        None => (text, None),
53    };
54    if client_id.is_empty() {
55        return None;
56    }
57    Some(InjectorClaim {
58        client_id: client_id.to_string(),
59        ticket,
60    })
61}