ados-protocol 0.106.2

Wire contracts for the ADOS Drone Agent: length-prefixed IPC framing, plugin RPC envelope and capability tokens, the vision frame bus, MSP and MAVLink helpers.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
# ADOS capability catalog — single source of truth.
#
# Codegen (`cargo run -p ados-capabilities-codegen`) emits, from this file:
#   - Python: src/ados/plugins/_capabilities_generated.py  (agent catalog)
#   - Rust:   crates/ados-protocol/src/capabilities.rs       (agent catalog)
#   - TS:     ADOSMissionControl/src/lib/plugins/agent-capabilities.generated.ts (agent mirror)
#   - TS:     ADOSMissionControl/src/lib/plugins/gcs-capabilities.generated.ts   (GCS catalog)
#
# Two sections:
#   [[agent]] — capabilities a plugin manifest may declare for the agent half.
#   [[gcs]]   — capabilities for the GCS half (UI slots + GCS data access).
#
# `enforced = true` means a grant of this capability changes what the plugin can
# actually do, through one of exactly three mechanisms:
#   1. the generated wire-dispatch gate (`[[method]]` below names the cap),
#   2. a handler-side check finer than a method (topic / message-id classified;
#      listed in `ados_plugin_host::HANDLER_GATED_CAPS`),
#   3. the generated systemd unit's sandbox — cgroup device policy, socket
#      address families, or the mount namespace (listed in
#      `ados_plugin_host::SANDBOX_ENFORCED_CAPS`).
# `enforced = false` means the grant is advisory metadata only. A capability
# that nothing backs does not belong in this file at all: the install dialog
# renders every declared permission to the operator, so a decorative row teaches
# them the risk labels are noise. Two guard tests in `ados-plugin-host/src/lib.rs`
# fail the build in both directions (a claimed gate that does not exist, and a
# real gate the catalog understates).
#
# category is one of: hardware | flight_control | data_network | compute_process | ui_slot
# risk is one of:     low | medium | high | critical
#
# Edit here, then run codegen. Do not edit the generated files by hand.

# ===========================================================================
# AGENT capabilities (58)
# ===========================================================================

[[agent]]
id = "event.publish"
label = "Publish events on the plugin event bus"
description = "Lets the plugin emit events on topics it has been granted access to. Other plugins and host services that subscribe to the same topic will receive the payload."
category = "data_network"
risk = "low"
risk_reason = "Bus messages are sandboxed and do not affect flight."
enforced = true

[[agent]]
id = "event.subscribe"
label = "Receive events from the plugin event bus"
description = "Lets the plugin subscribe to topics it has been granted access to. The plugin sees payloads from other plugins and from host services that publish on those topics."
category = "data_network"
risk = "low"
risk_reason = "Read-only on a sandboxed in-process bus."
enforced = true

[[agent]]
id = "mavlink.read"
label = "Read MAVLink messages from the flight controller"
description = "Lets the plugin observe the live MAVLink message stream, including telemetry, mode changes, and status text. Useful for analytics, logging, and dashboards."
category = "flight_control"
risk = "low"
risk_reason = "Read-only on the MAVLink stream."
enforced = true

[[agent]]
id = "mavlink.write"
label = "Send MAVLink commands to the flight controller"
description = "Lets the plugin inject MAVLink commands to the autopilot, including mode changes, arming, and parameter writes. An untrusted plugin with this capability can take control of the aircraft."
category = "flight_control"
risk = "high"
risk_reason = "Can change flight mode, arm motors, and alter parameters in flight."
enforced = true

[[agent]]
id = "msp.read"
label = "Read MSP messages from the flight controller"
description = "Lets the plugin observe the raw MSP byte stream from a Betaflight / iNav / KISS flight controller, so it can decode telemetry, sensor, and status replies. Read-only on the MSP stream; the sibling of mavlink.read for an FC that speaks MSP instead of MAVLink."
category = "flight_control"
risk = "low"
risk_reason = "Read-only on the MSP stream."
enforced = true

[[agent]]
id = "msp.write"
label = "Send MSP commands to the flight controller"
description = "Lets the plugin write raw MSP commands to a Betaflight / iNav / KISS flight controller, including MSP_SET_RAW_RC (the RC channel values that fly the aircraft in a rate mode). An untrusted plugin with this capability can command the aircraft, exactly like mavlink.write on a MAVLink FC."
category = "flight_control"
risk = "high"
risk_reason = "Can drive the RC channels and arm the aircraft on an MSP flight controller."
enforced = true

[[agent]]
id = "mavlink.component.camera"
label = "Act as a MAVLink camera component"
description = "Lets the plugin register itself as a MAVLink camera component and respond to camera-related MAVLink messages from the GCS and the autopilot."
category = "flight_control"
risk = "medium"
risk_reason = "Owns a MAVLink component id; can interfere with the real camera if mis-configured."
enforced = false

[[agent]]
id = "mavlink.component.gimbal"
label = "Act as a MAVLink gimbal component"
description = "Lets the plugin register itself as a MAVLink gimbal manager or device and handle gimbal control messages. Required by gimbal driver plugins."
category = "flight_control"
risk = "medium"
risk_reason = "Owns a MAVLink component id and can move the gimbal."
enforced = false

[[agent]]
id = "mavlink.component.payload"
label = "Act as a MAVLink payload component"
description = "Lets the plugin register itself as a MAVLink payload component for actuators, droppers, and other mission-specific hardware."
category = "flight_control"
risk = "medium"
risk_reason = "Owns a MAVLink component id and can trigger payload actuators."
enforced = false

[[agent]]
id = "mavlink.component.peripheral"
label = "Act as a generic MAVLink peripheral component"
description = "Lets the plugin claim a MAVLink component id for a peripheral that does not fit the camera, gimbal, or payload categories. Used by sensor bridges and experimental devices."
category = "flight_control"
risk = "medium"
risk_reason = "Owns a MAVLink component id on the same bus as the autopilot."
enforced = false

[[agent]]
id = "mavlink.component.vio"
label = "Act as a MAVLink visual-inertial-odometry source"
description = "Lets the plugin register as a VIO source that publishes pose estimates over MAVLink. The autopilot can fuse these directly into the EKF, so a faulty source can cause unsafe flight behavior."
category = "flight_control"
risk = "high"
risk_reason = "Pose estimates feed the EKF and influence position control."
enforced = true

[[agent]]
id = "estimator.pose.inject"
label = "Inject pose estimates into the EKF"
description = "Lets the plugin push pose samples (position, attitude, or both) directly into the autopilot state estimator. A bad pose stream produces unsafe position commands and fly-aways."
category = "flight_control"
risk = "high"
risk_reason = "Directly biases the EKF; a malicious or buggy plugin can crash the aircraft."
enforced = true

[[agent]]
id = "telemetry.read"
label = "Read telemetry streams from the agent"
description = "Lets the plugin read the same telemetry feed the GCS consumes (battery, GPS, attitude, mode). Useful for logging and analytics."
category = "data_network"
risk = "low"
risk_reason = "Read-only on existing telemetry topics."
enforced = true

[[agent]]
id = "sensor.stream.read"
label = "Read the sensor bus (IMU, GNSS, triggers, clock)"
description = "Lets the plugin subscribe to the agent's sensor bus: timestamped IMU batches, raw GNSS receiver bytes, camera trigger records, barometer, magnetometer and clock samples, plus the clock state file. Used by capture and odometry plugins that need every sample on the host timebase."
category = "data_network"
risk = "medium"
risk_reason = "High-rate raw sensor data, including the receiver's position stream. Enforced by the generated unit's mount namespace: without this grant the sensor bus socket and the clock file are not bound into the plugin's hidden run directory, so a connect fails with ENOENT."
enforced = true

[[agent]]
id = "capture.storage.write"
label = "Write capture sessions to the capture storage volume"
description = "Lets the plugin read the capture-storage state and write capture session directories under the agent's capture root, and raise the capture-active marker that switches the flight controller to its capture sensor rates. The agent's storage janitor still owns the budget, admission and eviction."
category = "data_network"
risk = "medium"
risk_reason = "Writes large files to the capture volume and changes the flight controller's stream rates while a session records. Enforced by the generated unit's mount namespace: without this grant the capture root, the capture-active directory and the storage state are not bound into the plugin's sandbox."
enforced = true

[[agent]]
id = "telemetry.extend"
label = "Publish new telemetry fields to the GCS"
description = "Lets the plugin add new fields to the telemetry stream that ships to the GCS. Used by sensor plugins that surface custom readings on the dashboard."
category = "data_network"
risk = "low"
risk_reason = "Adds fields; does not alter existing ones."
enforced = true

[[agent]]
id = "sensor.camera.register"
label = "Register a camera as a system sensor"
description = "Lets the plugin declare a camera device the rest of the agent and the GCS can address (live preview, recording, mission usage)."
category = "hardware"
risk = "medium"
risk_reason = "Camera streams may be recorded and uploaded; sensor identity matters for safety."
enforced = false

[[agent]]
id = "sensor.depth.register"
label = "Register a depth sensor as a system sensor"
description = "Lets the plugin declare a depth-camera or stereo sensor. Downstream services can consume depth maps for obstacle avoidance and SLAM."
category = "hardware"
risk = "medium"
risk_reason = "Depth data may feed obstacle avoidance; faulty data can influence avoidance decisions."
enforced = false

[[agent]]
id = "sensor.lidar.register"
label = "Register a LiDAR as a system sensor"
description = "Lets the plugin declare a LiDAR device. Downstream services can consume point clouds for mapping and obstacle avoidance."
category = "hardware"
risk = "medium"
risk_reason = "Point clouds may feed obstacle avoidance; faulty data can influence avoidance decisions."
enforced = false

[[agent]]
id = "sensor.imu.register"
label = "Register an auxiliary IMU as a system sensor"
description = "Lets the plugin declare a secondary IMU device the rest of the agent can address. Often paired with VIO and odometry workflows."
category = "hardware"
risk = "medium"
risk_reason = "IMU output may feed state estimation downstream."
enforced = false

[[agent]]
id = "sensor.payload.register"
label = "Register a payload device as a system sensor"
description = "Lets the plugin declare a payload device (sprayer, dropper, sampler) so missions and the GCS can address it."
category = "hardware"
risk = "medium"
risk_reason = "Payload identity drives mission actions; mis-registration can trigger the wrong actuator."
enforced = false

[[agent]]
id = "hardware.uart"
label = "Open UART serial ports on the host"
description = "Lets the plugin open and exchange data over UART serial ports. Required by GPS, range-finder, and modem drivers."
category = "hardware"
risk = "medium"
risk_reason = "Serial ports may be shared with the autopilot or critical peripherals. Enforced by the generated unit's cgroup device policy: without this grant the plugin's unit carries PrivateDevices=yes and no DeviceAllow for char-ttyUSB / char-ttyACM / char-tty, so an open() of a serial node fails with EPERM."
enforced = true

[[agent]]
id = "hardware.i2c"
label = "Read and write I2C devices on the host"
description = "Lets the plugin talk to I2C devices on the SBC's I2C buses. Used by sensor drivers and small peripherals."
category = "hardware"
risk = "medium"
risk_reason = "I2C buses are shared; a misbehaving plugin can stall the bus for other devices. Enforced by the generated unit's cgroup device policy: without this grant no DeviceAllow for char-i2c is emitted, so /dev/i2c-* is unreachable."
enforced = true

[[agent]]
id = "hardware.spi"
label = "Read and write SPI devices on the host"
description = "Lets the plugin talk to SPI devices on the SBC's SPI buses. Used by displays and high-rate sensor drivers."
category = "hardware"
risk = "medium"
risk_reason = "SPI buses are shared with on-board peripherals like the front-panel display. Enforced by the generated unit's cgroup device policy: without this grant no DeviceAllow for char-spidev is emitted, so /dev/spidev* is unreachable."
enforced = true

[[agent]]
id = "hardware.gpio"
label = "Read and toggle GPIO pins on the host"
description = "Lets the plugin read input pins and drive output pins on the SBC. Used by switches, LEDs, and discrete peripheral control."
category = "hardware"
risk = "medium"
risk_reason = "GPIO outputs can drive external hardware; mis-driven pins can damage attached devices. Enforced by the generated unit's cgroup device policy: without this grant no DeviceAllow for char-gpiochip is emitted, so /dev/gpiochip* is unreachable."
enforced = true

[[agent]]
id = "hardware.gpio_out"
label = "Drive GPIO output lines (status buzzer or LED)"
description = "Lets the plugin drive a host GPIO output line high or low and play a bounded buzzer or LED beep pattern through the agent's GPIO-output service. Output-only; it does not read input pins."
category = "hardware"
risk = "high"
risk_reason = "Drives physical output hardware; a mis-driven pin can energize or damage an attached device."
enforced = true

[[agent]]
id = "hardware.usb"
label = "Claim raw USB devices on the host"
description = "Lets the plugin attach to and exchange bulk transfers with USB devices. Required by vendor SDK drivers that talk to specific USB peripherals."
category = "hardware"
risk = "medium"
risk_reason = "Claiming a USB device prevents other software from using it. Enforced by the generated unit's cgroup device policy: without this grant no DeviceAllow for char-usb_device is emitted, so a raw /dev/bus/usb transfer is unreachable."
enforced = true

[[agent]]
id = "hardware.usb.uvc"
label = "Read frames from USB UVC cameras"
description = "Lets the plugin capture video frames from USB Video Class cameras. Required by webcam and machine-vision drivers."
category = "hardware"
risk = "medium"
risk_reason = "Video frames may be recorded, transmitted, or processed off-vehicle. Enforced by the generated unit's cgroup device policy: without this grant no DeviceAllow for char-video4linux is emitted, so /dev/video* is unreachable."
enforced = true

[[agent]]
id = "hardware.camera.csi"
label = "Read frames from MIPI CSI cameras"
description = "Lets the plugin capture video frames from CSI-attached cameras on the SBC's camera connector. Required by native camera drivers."
category = "hardware"
risk = "medium"
risk_reason = "Video frames may be recorded, transmitted, or processed off-vehicle. Enforced by the generated unit's cgroup device policy: without this grant no DeviceAllow for char-video4linux or char-dri is emitted, so the CSI capture nodes are unreachable."
enforced = true

[[agent]]
id = "video.source.set"
label = "Configure the agent's video stream sources"
description = "Lets the plugin declare the camera and stream sources the agent's video pipeline should serve (one primary plus optional secondary legs), so a smart-camera or pod driver can auto-configure its own feeds without an operator hand-typing them. Applying a new source list restarts the video pipeline."
category = "data_network"
risk = "medium"
risk_reason = "Reconfigures which video sources the vehicle serves and restarts the video pipeline; a wrong source list can black out the operator's video."
enforced = true

[[agent]]
id = "video.whep.relay"
label = "Relay live video playback sessions"
description = "Lets the plugin open and close WebRTC playback (WHEP) sessions on the agent's live camera streams on behalf of a viewer it serves: it hands the agent a viewer's SDP offer for a camera and relays the answer back. The agent only plays out streams it already publishes; the plugin cannot change them or reach the media server directly."
category = "data_network"
risk = "medium"
risk_reason = "Gives the plugin the node's live camera video to hand to its own viewers, wherever it can reach them."
enforced = true

[[agent]]
id = "sensors.rig.write"
label = "Store the sensor rig calibration"
description = "Lets the plugin store the rig document (sensor roles, intrinsics and extrinsics) the agent validates and the sensor hub fuses with, through the same validation the operator's rig write runs. An invalid document is refused and the stored one kept."
category = "hardware"
risk = "medium"
risk_reason = "A wrong but valid calibration skews every fused sensor sample and capture until it is corrected."
enforced = true

[[agent]]
id = "camera.control.write"
label = "Set camera exposure and gain"
description = "Lets the plugin set the exposure time, gain and auto-exposure mode of a capture camera (one configured for capture, which the vision engine streams from), and read back the values the camera actually took. Other cameras, including the video pipeline's own, are refused; simulated and network cameras answer that nothing was applied."
category = "hardware"
risk = "medium"
risk_reason = "Changes the image every consumer of that camera sees (the operator's video, detection, tracking and capture); a wrong exposure can wash out or black out the picture until it is set back."
enforced = true

[[agent]]
id = "mission.read"
label = "Read mission plans loaded on the agent"
description = "Lets the plugin read the active mission, waypoints, fences, and rally points stored on the agent."
category = "flight_control"
risk = "low"
risk_reason = "Read-only on mission data."
enforced = true

[[agent]]
id = "mission.write"
label = "Upload or modify mission plans on the agent"
description = "Lets the plugin upload new missions, edit waypoints, or change fence and rally definitions. Used by mission-planner and pattern-generator plugins."
category = "flight_control"
risk = "high"
risk_reason = "Mission changes drive autonomous flight paths."
enforced = true

[[agent]]
id = "network.outbound"
label = "Open outbound network connections"
description = "Lets the plugin make outbound TCP, UDP, and HTTP connections from the agent host, and find services on the local network over mDNS through the agent. Required by plugins that talk to external services."
category = "data_network"
risk = "medium"
risk_reason = "Exfiltration risk; outbound traffic can carry telemetry off the aircraft. Enforced by the generated unit's socket policy: without this grant the unit carries RestrictAddressFamilies=AF_UNIX and IPAddressDeny=any, so socket(AF_INET) fails with EAFNOSUPPORT. The mdns.browse host method is gated on it at dispatch."
enforced = true

[[agent]]
id = "filesystem.host"
label = "Read and write files on the host filesystem"
description = "Lets the plugin read and write files outside its own data directory. Required by plugins that ingest logs, maps, or other host-provided files."
category = "compute_process"
risk = "medium"
risk_reason = "Broad host filesystem access; mis-use can corrupt agent state. Enforced by the generated unit's mount namespace: without this grant the unit carries InaccessiblePaths for the operator data roots and ProtectHome=yes, so a read outside the plugin's own tree fails with EACCES."
enforced = true

[[agent]]
id = "recording.write"
label = "Write recordings into the agent recording store"
description = "Lets the plugin save video, telemetry, or analysis outputs into the agent's recording store so the GCS and post-flight tools can find them."
category = "data_network"
risk = "low"
risk_reason = "Writes are sandboxed to the recording directory."
enforced = true

[[agent]]
id = "process.spawn"
label = "Spawn subprocesses on the agent host"
description = "Lets the plugin launch helper subprocesses listed in its manifest's spawn allowlist. Required to ship vendor-binary drivers and language runtimes outside the Python plugin host."
category = "compute_process"
risk = "high"
risk_reason = "Subprocesses run outside the plugin host sandbox and with the plugin's privileges."
enforced = true

[[agent]]
id = "vision.frame.read"
label = "Read camera frames from the vision engine"
description = "Lets the plugin subscribe to normalized camera frames the vision engine publishes (resolution and pixel format set by the engine). Frames are shared read-only; the plugin does not open the camera itself."
category = "compute_process"
risk = "medium"
risk_reason = "Frames may contain sensitive imagery; a frame consumer can process it off the normal recording path."
enforced = true

[[agent]]
id = "vision.model.register"
label = "Register an inference model with the vision engine"
description = "Lets the plugin register an inference model (and its input format and output classes) so the vision engine can load and run it, or so the plugin can run it against shared frames. Registered models appear to detection consumers."
category = "compute_process"
risk = "medium"
risk_reason = "A model can consume the shared accelerator and CPU; a heavy or faulty model can starve other vision consumers."
enforced = true

[[agent]]
id = "vision.model.read"
label = "Read the plugin's resolved model delivery status"
description = "Lets the plugin read the resolution of its own declared models: for each model the state (resolved, needs-model, verify-failed), the resolved on-disk path and runtime when resolved, and the reason otherwise. This is how a plugin finds where its delivered model was cached so it can load it. Reads only this plugin's own resolution; it does not read frames, other plugins' models, or command the vehicle."
category = "compute_process"
risk = "low"
risk_reason = "Read-only access to the plugin's own model-delivery metadata; no frames, no other plugin's data, no vehicle command."
enforced = true

[[agent]]
id = "vision.detection.publish"
label = "Publish detections to the vision engine"
description = "Lets the plugin publish detection results (bounding boxes, class labels, confidence, track ids) that other plugins, overlays, and mission logic can consume. Detections are derived data; they do not command the vehicle."
category = "data_network"
risk = "low"
risk_reason = "Detections are derived data on a sandboxed topic and do not affect flight."
enforced = true

[[agent]]
id = "vision.offload.advertise"
label = "Advertise a perception offload link"
description = "Lets the plugin report the perception offload link it holds (whether a compute node is paired, whether the bearer to it is good enough, its address and the model it runs). The node's perception tier and status read it. The agent counts the node as paired only when the advertised address answers, and only one plugin owns the link at a time."
category = "compute_process"
risk = "medium"
risk_reason = "The advertised link changes the perception tier the node reports and where its detections come from."
enforced = true

[[agent]]
id = "vision.detection.subscribe"
label = "Subscribe to detections from the vision engine"
description = "Lets the plugin receive the detection batches the vision engine publishes (bounding boxes, class labels, confidence, and the tracker's stable track id and lock state on the locked object). Detections are derived data; subscribing does not command the vehicle or open the camera."
category = "data_network"
risk = "low"
risk_reason = "Detections are derived data on a sandboxed topic and do not affect flight."
enforced = true

[[agent]]
id = "vision.track.designate"
label = "Set the vision engine's follow target"
description = "Lets the plugin choose which object the vision engine's single-object tracker locks onto (the follow target), by naming the box to track. This selects a target for downstream follow / framing logic; designation alone does not command the vehicle."
category = "flight_control"
risk = "medium"
risk_reason = "Choosing the follow target steers downstream follow/framing behaviour, though designation alone does not command flight."
enforced = true

[[agent]]
id = "button.subscribe"
label = "Subscribe to onboard button press events"
description = "Lets the plugin receive press events (short, long, chord) from the host's front-panel buttons. Used by node plugins that drive an on-device workflow from physical buttons. Read-only; it does not remap or consume the buttons from other consumers."
category = "hardware"
risk = "medium"
risk_reason = "Button events drive plugin actions; an input source should be reviewed like any other."
enforced = true

[[agent]]
id = "flight.guided_setpoint"
label = "Emit guided-mode setpoints"
description = "Lets the plugin command the flight controller with guided-mode position or velocity setpoints (and the guided-mode entry and exit handshake) through a scoped sender, instead of raw MAVLink writes. The setpoint stream is the only flight-command surface this capability grants."
category = "flight_control"
risk = "high"
risk_reason = "Commands vehicle motion in guided mode; a faulty stream can fly the aircraft."
enforced = true

[[agent]]
id = "flight.rate_setpoint"
label = "Emit attitude/rate setpoints"
description = "Lets a plugin command the flight controller with SET_ATTITUDE_TARGET setpoints (an attitude quaternion and/or body rates, plus collective thrust) through a scoped sender, for GUIDED-mode vehicles that accept attitude/rate offboard control. This is a second flight-command surface alongside guided-mode position/velocity setpoints; the setpoint stream is the only flight-command surface this capability grants."
category = "flight_control"
risk = "critical"
risk_reason = "Directly commands aircraft attitude/body rates and thrust at tick rate; a faulty stream can fly the aircraft outside guided-mode limits."
enforced = true

[[agent]]
id = "mavlink.tunnel"
label = "Send and receive MAVLink TUNNEL payloads"
description = "Lets the plugin exchange application payloads over MAVLink TUNNEL frames (a private payload_type) on the existing link, without the full MAVLink write surface. Used for plugin-to-plugin or node-to-node application data carried inside the MAVLink stream."
category = "flight_control"
risk = "high"
risk_reason = "Shares the flight-control link; a misbehaving tunnel can congest MAVLink traffic."
enforced = true

[[agent]]
id = "radio.aux_stream"
label = "Open and use an auxiliary radio stream"
description = "Lets the plugin open a dedicated auxiliary stream on the radio link (a separate radio-port from the data and control planes) managed by the radio service, and read or write application frames on it. Used by plugins that need an isolated low-rate channel between nodes."
category = "data_network"
risk = "high"
risk_reason = "Consumes radio airtime on the shared adapter and owns an application channel between nodes."
enforced = true

[[agent]]
id = "display.oled.page"
label = "Contribute a page to the onboard display"
description = "Lets the plugin register a page (its rendered content and touch or hit zones) on the host's onboard display, so a plugin can present status without recompiling the display service. The host owns page switching and arbitration between contributed pages."
category = "hardware"
risk = "low"
risk_reason = "Draws to the local display only; no effect on flight or the network."
enforced = true

[[agent]]
id = "mcp.expose"
label = "Expose the plugin's tools to the AI-control (MCP) surface"
description = "Lets the plugin publish its declared tools, resources, and prompts to an MCP client, and lets the host route a tool invocation into the plugin. Exposure alone grants no host authority: each tool acts only through the plugin's OTHER granted capabilities, so a tool can never do anything the plugin could not already do."
category = "data_network"
risk = "medium"
risk_reason = "Publishes a callable interface to an AI client; the effect of any call is still bounded by the plugin's other capabilities and the MCP token's scope."
enforced = true

[[agent]]
id = "hardware.gpu"
label = "Use the GPU or neural accelerator"
description = "Lets the plugin open the host's GPU and neural-accelerator device nodes (DRM render nodes, Mali, RKNPU, NVIDIA) for inference or rendering. Without it the plugin runs in a private /dev with no accelerator."
category = "hardware"
risk = "medium"
risk_reason = "Direct device access to a shared accelerator; a runaway workload can starve the vision engine. Enforced by the generated unit's cgroup device policy (DeviceAllow for the accelerator nodes plus the video and render groups)."
enforced = true

[[agent]]
id = "network.listen"
label = "Accept inbound network connections"
description = "Lets a declared plugin service bind the TCP ports it lists in its manifest (listen_ports) so other nodes on the network can reach it, and advertise those ports on the local network over mDNS through the agent. Every other port stays closed to the plugin, and the plugin's main process never listens."
category = "data_network"
risk = "high"
risk_reason = "Opens a network entry point into a plugin process on the agent host. Enforced by the generated unit: every plugin unit carries SocketBindDeny=any, and only a granted service unit gets SocketBindAllow for its declared ports. The mdns.advertise host method is gated on it at dispatch and publishes only a declared port."
enforced = true

[[agent]]
id = "cloud.publish"
label = "Publish data to the cloud relay"
description = "Lets the plugin publish small messages (at most 64 KiB each) on its own cloud stream through the agent's cloud relay, so a signed-in operator can receive them off the local network. Publishing detections onto the shared detection topic additionally needs vision.detection.publish."
category = "data_network"
risk = "medium"
risk_reason = "Sends plugin data off the aircraft to the cloud under the device's identity."
enforced = true

[[agent]]
id = "cloud.records"
label = "Store records in the operator's cloud account"
description = "Lets the plugin write small JSON records (at most 64 KiB each) into its own collection in the operator's cloud account, where the plugin's GCS half can read them back."
category = "data_network"
risk = "medium"
risk_reason = "Persists plugin data off the aircraft under the device's identity; confined to the plugin's own namespace."
enforced = true

[[agent]]
id = "node.info.read"
label = "Read this node's hardware and role facts"
description = "Lets the plugin read what kind of node it runs on: the node profile, the board and whether it has a neural accelerator, the ground-station role, whether the camera pipeline is up, and the main video stream's size and frame rate. Read-only facts; no telemetry, imagery, credentials or configuration values."
category = "hardware"
risk = "low"
risk_reason = "Read-only node identity facts; no effect on flight, no imagery and no secrets."
enforced = true

# ===========================================================================
# GCS capabilities (26): 13 UI slots + 13 data/command/mission/cloud/bus
# ===========================================================================

[[gcs]]
id = "ui.slot.fc-tab"
label = "Add a tab to the flight controller panel"
description = "Lets the plugin add a tab to the per-drone flight controller area. The tab renders inside a sandboxed iframe with no host DOM access."
category = "ui_slot"
risk = "low"
risk_reason = "Sandboxed iframe; no host DOM access."

[[gcs]]
id = "ui.slot.hardware-tab"
label = "Add a tab to the hardware page"
description = "Lets the plugin add a tab on the Hardware page so the operator can inspect or configure devices the plugin manages."
category = "ui_slot"
risk = "low"
risk_reason = "Sandboxed iframe inside the Hardware page."

[[gcs]]
id = "ui.slot.mission-template"
label = "Register a mission template"
description = "Lets the plugin add a new entry to the mission template picker so the operator can start missions with the plugin's parameters."
category = "ui_slot"
risk = "low"
risk_reason = "Template metadata only; missions still need approval."

[[gcs]]
id = "ui.slot.map-overlay"
label = "Draw an overlay on the map"
description = "Lets the plugin draw geometry on the map view (polygons, markers, heatmaps). The overlay is rendered in a sandboxed canvas."
category = "ui_slot"
risk = "low"
risk_reason = "Visual layer only; no flight-control effect."

[[gcs]]
id = "ui.slot.video-overlay"
label = "Draw an overlay on the live video"
description = "Lets the plugin draw overlays on top of the live video feed (detection boxes, telemetry HUDs). The overlay is rendered above the player."
category = "ui_slot"
risk = "low"
risk_reason = "Visual layer above the video element."

[[gcs]]
id = "ui.slot.notification-channel"
label = "Show notifications in the GCS"
description = "Lets the plugin push notifications into the GCS notification center so it can surface alerts and status changes to the operator."
category = "ui_slot"
risk = "low"
risk_reason = "Toasts only; each is labelled with the plugin id and rate-limited per plugin."

[[gcs]]
id = "ui.slot.settings-section"
label = "Add a section to the Settings page"
description = "Lets the plugin add its own section to the GCS Settings page so the operator can configure the plugin from a single place."
category = "ui_slot"
risk = "low"
risk_reason = "Sandboxed iframe inside the Settings page."

[[gcs]]
id = "ui.slot.flight-skill"
label = "Contribute a flight skill to the cockpit"
description = "Lets the plugin register a flight skill that appears in the cockpit Skill Bar, can be bound to a hotkey or gamepad button, and activated by the operator. Activation flips the plugin's per-drone config; the cockpit's arm and confirmation gates still apply."
category = "ui_slot"
risk = "medium"
risk_reason = "A bindable skill triggers plugin behavior; the plugin still needs its own flight-control capabilities to act, and the cockpit's arm/confirm gates still apply."

[[gcs]]
id = "ui.slot.node-detail-tab"
label = "Add a tab to the node detail panel"
description = "Lets the plugin add a per-node tab inside the node detail panel on any node profile (drone, ground station, compute). The tab is scoped to the currently selected node."
category = "ui_slot"
risk = "low"
risk_reason = "Sandboxed iframe scoped to one node."

[[gcs]]
id = "ui.slot.cockpit-panel"
label = "Add a panel to the cockpit quick settings"
description = "Lets the plugin add a panel to the in-flight cockpit quick-settings surface, scoped to the currently selected drone."
category = "ui_slot"
risk = "low"
risk_reason = "Sandboxed iframe scoped to the cockpit of one drone."

[[gcs]]
id = "ui.slot.cockpit-widget"
label = "Add a widget to the cockpit HUD"
description = "Lets the plugin place a small widget in one of the cockpit's corner zones over the live video, scoped to the drone the cockpit is flying. The widget renders inside a sandboxed iframe with no host DOM access."
category = "ui_slot"
risk = "low"
risk_reason = "Sandboxed iframe in a cockpit corner zone; display only."

[[gcs]]
id = "vision.track.designate"
label = "Choose the follow target from the GCS"
description = "Lets the plugin ask the GCS to set the vision engine's follow target on the drone it is bound to, by naming the detection box to track. Designation selects a target for downstream follow and framing logic; it does not command the vehicle."
category = "flight_control"
risk = "medium"
risk_reason = "Choosing the follow target steers downstream follow/framing behaviour, though designation alone does not command flight."

[[gcs]]
id = "telemetry.subscribe"
label = "Read live telemetry from drones"
description = "Lets the plugin subscribe to live telemetry streams (attitude, GPS, battery, mode) for the drones the operator has paired with the GCS."
category = "data_network"
risk = "low"
risk_reason = "Read-only on the telemetry stream."

[[gcs]]
id = "command.send"
label = "Send commands to drones from the GCS"
description = "Lets the plugin ask the GCS to send takeoff, land or return-to-launch to the drone it is bound to, or retarget that drone's follow tracker. The operator confirms every one of these in a prompt that names the drone. Arming, mode changes and every other command are refused. The same grant lets the plugin write its own per-drone configuration without a prompt."
category = "flight_control"
risk = "medium"
risk_reason = "Each confirmed command takes effect immediately during a flight."

[[gcs]]
id = "recording.write"
label = "Save recordings to the GCS library"
description = "Lets the plugin write recordings (video, telemetry CSV, analysis output) into the GCS recording library so the operator can replay them later."
category = "data_network"
risk = "low"
risk_reason = "Writes are scoped to the recording library."

[[gcs]]
id = "mission.read"
label = "Read mission plans loaded in the GCS"
description = "Lets the plugin read the active mission, waypoints, fences, and rally points that the operator has loaded into the GCS planner."
category = "flight_control"
risk = "low"
risk_reason = "Read-only on mission data."

[[gcs]]
id = "mission.write"
label = "Edit mission plans in the GCS"
description = "Lets the plugin upload, edit, or replace missions in the GCS planner. Used by pattern generators and mission-template plugins."
category = "flight_control"
risk = "high"
risk_reason = "Mission changes drive autonomous flight paths."

[[gcs]]
id = "cloud.read"
label = "Read fleet data from the cloud"
description = "Lets the plugin read shared fleet data from the cloud backend (drone status rows, paired devices, fleet roster)."
category = "data_network"
risk = "low"
risk_reason = "Read-only on the cloud rows the user is authorised for."

[[gcs]]
id = "perception.read"
label = "Read perception state in the GCS"
description = "Lets the plugin read the drone's perception state: where detection runs (on the drone or offloaded to a compute node), the bound compute node, and the live session health (feed freshness, throughput). Read-only on derived data; it does not open or read the camera."
category = "data_network"
risk = "low"
risk_reason = "Read-only on derived detection metadata; no camera access."

[[gcs]]
id = "perception.subscribe"
label = "Subscribe to live detections in the GCS"
description = "Lets the plugin subscribe to the live detection stream (bounding boxes, classes, track ids) for the drones the operator has paired, the same detections the cockpit already renders. Read-only; the detections are derived data, not the raw camera feed."
category = "data_network"
risk = "low"
risk_reason = "Read-only on the derived detection stream; no camera access."

[[gcs]]
id = "mcp.expose"
label = "Expose the plugin's GCS tools to the AI-control (MCP) surface"
description = "Lets the plugin publish its declared GCS-half tools, resources, and prompts to an MCP client. A GCS-only tool routes through the GCS bridge (command / mission), so its effect is still bounded by the plugin's other GCS capabilities and the MCP token's scope."
category = "data_network"
risk = "medium"
risk_reason = "Publishes a callable interface to an AI client; effect is bounded by the plugin's other capabilities and the token scope."

[[gcs]]
id = "event.publish"
label = "Publish events on the GCS plugin event bus"
description = "Lets the plugin emit events on the in-memory GCS event bus other installed plugin panels subscribe to. The bus never leaves the browser tab; it carries no host authority of its own."
category = "data_network"
risk = "low"
risk_reason = "In-tab message bus between plugin panels; no host or vehicle effect."

[[gcs]]
id = "event.subscribe"
label = "Receive events from the GCS plugin event bus"
description = "Lets the plugin subscribe to topics on the in-memory GCS event bus and see payloads other installed plugin panels publish."
category = "data_network"
risk = "low"
risk_reason = "Read-only on an in-tab message bus."

[[gcs]]
id = "ui.slot.node-agent-page"
label = "Add a page to a node's Agent sidebar"
description = "Lets the plugin add a page to the sectioned Agent sidebar of a node's detail panel, next to the built-in companion and configuration pages. The page is scoped to the selected node."
category = "ui_slot"
risk = "low"
risk_reason = "Scoped to one node's detail panel; the page acts only through the plugin's other capabilities."

[[gcs]]
id = "ui.slot.node-surface"
label = "Add a top-level tab to a node's detail panel"
description = "Lets the plugin add a top-level surface to the node detail tab strip for the node profiles it declares (for example a workstation overview). The surface is scoped to the selected node."
category = "ui_slot"
risk = "low"
risk_reason = "Scoped to one node's detail panel; the surface acts only through the plugin's other capabilities."

[[gcs]]
id = "cloud.records"
label = "Store the plugin's own records in the cloud"
description = "Lets the plugin list, read, write and delete records in its own cloud collection for the signed-in operator (for example job history). Records are keyed by plugin, so a plugin can never read another plugin's records. Each record is capped at 64 KiB and each plugin at 5000 records."
category = "data_network"
risk = "low"
risk_reason = "Confined to the plugin's own namespace for the signed-in operator."

# ===========================================================================
# Plugin RPC dispatch table (53): wire method -> dispatch-level required cap.
#
# The single source of truth for the gate the native host applies BEFORE routing
# a request. Codegen emits this into both the Rust host
# (crates/ados-protocol/src/dispatch.rs, consumed by ados-plugin-host) and the
# plugin runtime (src/ados/plugins/_dispatch_generated.py, read by
# plugins/ipc_client.py) so the two halves of the wire contract cannot drift.
#
# open = true          -> the method is ungated at the dispatch level on purpose
#                         (the event surface and ping; per-topic checks run
#                         inline in the handler; config is scoped to the calling
#                         plugin). Every row names exactly one of required_cap,
#                         inline or open, so a new method can never ship ungated
#                         by omission.
# inline = true        -> the method's cap is decided inline by the handler from
#                         the request payload (the component-kind / driver-kind
#                         classification), so the dispatch gate is None but the
#                         method is NOT open — codegen emits None and the marker
#                         documents that an inline gate exists.
#
# Every method id here is the exact wire string. Edit here, then run codegen.
# ===========================================================================

# ---- ungated event surface (per-topic check inline) ----
[[method]]
id = "event.publish"
open = true

[[method]]
id = "event.subscribe"
open = true

[[method]]
id = "ping"
open = true

# ---- MCP tool invocation (host -> plugin request/response) ----
# The one host->plugin request method: the host asks the plugin to run one of
# its declared tools and awaits a correlated response. Gated on mcp.expose so a
# plugin without it never has a tool invoked, checked by the runner against the
# same generated table the host uses.
[[method]]
id = "tool.invoke"
required_cap = "mcp.expose"

# ---- telemetry ----
[[method]]
id = "telemetry.subscribe"
required_cap = "telemetry.read"

[[method]]
id = "telemetry.extend"
required_cap = "telemetry.extend"

# ---- mission / recording ----
[[method]]
id = "mission.read"
required_cap = "mission.read"

[[method]]
id = "mission.write"
required_cap = "mission.write"

[[method]]
id = "recording.start"
required_cap = "recording.write"

[[method]]
id = "recording.stop"
required_cap = "recording.write"

# ---- mavlink ----
[[method]]
id = "mavlink.subscribe"
required_cap = "mavlink.read"

[[method]]
id = "mavlink.send"
required_cap = "mavlink.write"

# ---- msp (the byte plane for a Betaflight / iNav / KISS FC) ----
# msp.subscribe arms a per-connection push of raw FC->host MSP bytes (the server
# short-circuits it, like mavlink.subscribe); msp.send writes raw MSP bytes to
# the FC. The codec that builds/parses those bytes lives at the plugin edge.
[[method]]
id = "msp.subscribe"
required_cap = "msp.read"

[[method]]
id = "msp.send"
required_cap = "msp.write"

# Send one application payload over a MAVLink TUNNEL frame tagged with a private
# payload_type, a transparent opaque pipe separate from the full MAVLink write
# surface. Fully gated at the dispatch level on the tunnel capability.
[[method]]
id = "mavlink.tunnel.send"
required_cap = "mavlink.tunnel"

[[method]]
id = "mavlink.register_component"
# The matching component-kind cap is decided inline from the requested kind.
inline = true

# ---- peripheral / driver / camera ----
[[method]]
id = "peripheral.register_driver"
# The exact sensor.*.register cap depends on the driver kind; handler enforces.
inline = true

[[method]]
id = "peripheral.unregister_driver"
inline = true

[[method]]
id = "camera.claim"
required_cap = "sensor.camera.register"

[[method]]
id = "camera.release"
required_cap = "sensor.camera.register"

[[method]]
id = "camera.get_frame"
required_cap = "sensor.camera.register"

# ---- video source configuration ----
# Declare the camera/stream source list the agent's video pipeline serves (one
# primary leg plus optional secondary legs). The host forwards it to the
# supervisor's video command socket, which persists the top-level cameras list and restarts
# the video service. Fully gated at the dispatch level on the video-source cap.
[[method]]
id = "video.source.set"
required_cap = "video.source.set"

# ---- live video relay ----
# Open a WebRTC playback (WHEP) session on one of the agent's published camera
# legs for a viewer the plugin serves, and close one the same plugin opened.
# The host relays to the loopback media server the sandbox cannot reach; the
# plugin never sees a host address. Fully gated at the dispatch level.
[[method]]
id = "video.whep.offer"
required_cap = "video.whep.relay"

[[method]]
id = "video.whep.close"
required_cap = "video.whep.relay"

# ---- sensor rig ----
# Store a rig document through the control plane's own rig route (the same
# validation and hub reload the operator's write gets). Fully gated at the
# dispatch level.
[[method]]
id = "sensors.rig.write"
required_cap = "sensors.rig.write"

# ---- camera controls ----
# Set exposure, gain or auto-exposure on a camera the vision engine owns. The
# host relays to the engine, which applies it on the device it already streams
# from and reads the values back. Fully gated at the dispatch level.
[[method]]
id = "camera.control.set"
required_cap = "camera.control.write"

# ---- config kv (per-drone / global) ----
[[method]]
id = "config.get"
open = true

[[method]]
id = "config.set"
open = true

# ---- vendor binary spawn ----
[[method]]
id = "process.spawn"
required_cap = "process.spawn"

# ---- vision: frame subscribe, model register, inference, detection publish ----
# The host proxies these to the vision engine, which owns the cameras and the
# inference backend. Every row carries a dispatch-level cap so a vision method
# can never reach a host (Rust or Python) ungated.
[[method]]
id = "vision.subscribe_frames"
required_cap = "vision.frame.read"

[[method]]
id = "vision.register_model"
required_cap = "vision.model.register"

[[method]]
id = "vision.read_model"
required_cap = "vision.model.read"

[[method]]
id = "vision.infer"
required_cap = "vision.model.register"

[[method]]
id = "vision.publish_detection"
required_cap = "vision.detection.publish"

[[method]]
id = "vision.subscribe_detections"
required_cap = "vision.detection.subscribe"

[[method]]
id = "vision.designate_track"
required_cap = "vision.track.designate"

# ---- display ----
# Set the content (title, rows, touch zones) of the host's reserved data-driven
# display page. The host writes the page sidecar the display reads, so the
# method is fully gated at the dispatch level on the display capability.
[[method]]
id = "display.page.set"
required_cap = "display.oled.page"

# Subscribe to touch-zone taps on the plugin's reserved display page. Arms a
# per-connection push stream; the host watches the display sidecar's tap record
# and pushes each tap to the owning plugin as a `display.zone.tapped` event.
# Read-only and scoped to the plugin's own page.
[[method]]
id = "display.zone.subscribe"
required_cap = "display.oled.page"

# ---- front-panel buttons ----
# Subscribe to the host's front-panel button presses. Arms a per-connection push
# stream (like the vision subscriptions): the host reads the already-decoded
# events off the button fanout socket and pushes each one to the plugin as a
# `button.deliver` event. Read-only — a subscriber observes presses and neither
# remaps them nor consumes them from the other consumers on that bus.
[[method]]
id = "button.subscribe"
required_cap = "button.subscribe"

# ---- gpio output (status buzzer / LED) ----
# Drive a host GPIO output line, and play a bounded buzzer/LED beep pattern. The
# host forwards each to the gpio-output service's command socket. Fully gated at
# the dispatch level on the GPIO-output capability.
[[method]]
id = "gpio.output.set"
required_cap = "hardware.gpio_out"

[[method]]
id = "gpio.buzzer.beep"
required_cap = "hardware.gpio_out"

# ---- guided setpoint sender ----
# Send one position/velocity setpoint to the flight controller via the scoped
# guided-setpoint sender (a single MAVLink SET_POSITION_TARGET frame), instead of
# the raw MAVLink write surface. Fully gated at the dispatch level on the
# guided-setpoint capability. The method is a single-shot send: it owns no flight
# state or mode, so a caller that wants to hold a velocity must re-send (the
# autopilot brakes a few seconds after the last setpoint) and must itself confirm
# the vehicle is in its guided mode.
[[method]]
id = "flight.guided_setpoint.send"
required_cap = "flight.guided_setpoint"


# ---- attitude/rate setpoint sender ----
# Send one SET_ATTITUDE_TARGET frame (an attitude quaternion and/or body rates
# plus collective thrust) to the flight controller via the scoped attitude
# sender, instead of the raw MAVLink write surface. Fully gated at the dispatch
# level on the attitude/rate-setpoint capability. Single-shot like the guided
# sender: the host owns no flight state or mode, so a caller holding an attitude
# must re-send above the autopilot's setpoint timeout and must itself have the
# vehicle in a mode that accepts offboard attitude (e.g. GUIDED).
[[method]]
id = "flight.rate_setpoint.send"
required_cap = "flight.rate_setpoint"

# ---- auxiliary radio stream ----
# Open or close a dedicated auxiliary stream on the radio link (a separate
# radio-port from the data and control planes) managed by the radio service. The
# host forwards each to the radio service's command socket, which starts (open) or
# stops (close) the additional transmit/receive pair on the shared adapter. The
# stream is SAFE-BY-DEFAULT: nothing starts at boot; it exists only between an
# explicit open and the matching close (or plugin disconnect). Fully gated at the
# dispatch level on the auxiliary-stream capability.
[[method]]
id = "radio.aux_stream.open"
required_cap = "radio.aux_stream"

[[method]]
id = "radio.aux_stream.close"
required_cap = "radio.aux_stream"

# Send one application datagram on an open auxiliary stream — the payload is
# framed on the neutral application channel and written to the radio service's
# command socket. No per-send state: the stream is between the matching open and
# close.
[[method]]
id = "radio.aux_stream.send"
required_cap = "radio.aux_stream"

# Subscribe to application datagrams received on an open auxiliary stream. The
# host forwards each received frame to the subscribing plugin as a push (the
# same delivery shape as button presses).
[[method]]
id = "radio.aux_stream.subscribe"
required_cap = "radio.aux_stream"

# ---- cloud relay ----
# Publish one message on the plugin's own cloud stream, or write one record into
# the plugin's own cloud collection. The host forwards each to the cloud relay's
# local publish socket, which stamps the device identity. Publishing onto the
# shared detection stream is additionally checked inline against
# vision.detection.publish.
[[method]]
id = "cloud.publish"
required_cap = "cloud.publish"

[[method]]
id = "cloud.records.put"
required_cap = "cloud.records"

# ---- perception offload advertisement ----
# A plugin that runs perception offload reports the link it holds: whether a
# compute node is paired, whether the bearer to it is good enough, its address
# and the model it runs. The host checks the address answers, records the
# plugin as the link's owner, stamps the facts and writes the offload-link
# sidecar the perception-tier decision reads; the plugin re-advertises within
# the sidecar's staleness window or the link reads as gone. Gated on its own
# capability, since an advertised link steers where the node's detections
# come from.
[[method]]
id = "offload.advertise"
required_cap = "vision.offload.advertise"

# ---- node facts ----
# Read the node's profile, board, ground-station role, camera readiness and main
# stream geometry: the facts the agent's own services decide on, served from
# their sources so a plugin never reads the sidecars its sandbox hides. Gated at
# the dispatch level like every other read of host state (telemetry.read,
# mission.read); only the plugin-scoped surfaces (its own config, the event bus,
# ping) are ungated.
[[method]]
id = "node.info"
required_cap = "node.info.read"

# ---- local-network service discovery ----
# DNS-SD on the LAN, done by the host: a sandboxed plugin cannot bind the
# multicast port (every plugin unit carries SocketBindDeny=any). Advertise
# publishes one of the plugin's declared listen_ports under the system hostname
# and withdraws it when the connection that made it ends; the agent's own
# service types are refused. Browse collects the instances of one service type
# for a bounded window. Advertising offers a port to the network, so it is
# gated like binding one; browsing finds peers to connect out to.
[[method]]
id = "mdns.advertise"
required_cap = "network.listen"

[[method]]
id = "mdns.browse"
required_cap = "network.outbound"