remoteRF-server 1.0.4__tar.gz → 1.0.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/PKG-INFO +5 -31
  2. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/README.md +4 -30
  3. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/pyproject.toml +1 -1
  4. remoterf_server-1.0.5/src/remoteRF_server/drivers/usrp/policy.py +41 -0
  5. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/drivers/usrp/uhd_manifest.py +8 -25
  6. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/drivers/usrp/usrp_schema.py +1 -2
  7. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/server/dynamic_v2.py +4 -34
  8. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server.egg-info/PKG-INFO +5 -31
  9. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/tests/test_dynamic_v2.py +40 -77
  10. remoterf_server-1.0.4/src/remoteRF_server/drivers/usrp/policy.py +0 -363
  11. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/setup.cfg +0 -0
  12. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/__init__.py +0 -0
  13. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/__init__.py +0 -0
  14. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  15. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  16. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  17. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  18. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  19. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/grpc/v2_codec.py +0 -0
  20. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/idl/__init__.py +0 -0
  21. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/idl/device_schema.py +0 -0
  22. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/idl/pluto_mimo_schema.py +0 -0
  23. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/idl/pluto_schema.py +0 -0
  24. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/idl/schema.py +0 -0
  25. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/idl/v2.py +0 -0
  26. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/utils/__init__.py +0 -0
  27. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  28. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/utils/api_token.py +0 -0
  29. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  30. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/utils/db_location.py +0 -0
  31. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/utils/list_string.py +0 -0
  32. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/common/utils/process_arg.py +0 -0
  33. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/drivers/__init__.py +0 -0
  34. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  35. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/drivers/adalm_pluto/pluto_mimo_schema.py +0 -0
  36. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
  37. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/drivers/adalm_pluto/pluto_schema.py +0 -0
  38. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/drivers/usrp/__init__.py +0 -0
  39. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/host/__init__.py +0 -0
  40. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/host/host_auth_token.py +0 -0
  41. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/host/host_directory_store.py +0 -0
  42. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
  43. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/server/__init__.py +0 -0
  44. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/server/acc_perms.py +0 -0
  45. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/server/cert_provider.py +0 -0
  46. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/server/device_manager.py +0 -0
  47. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/server/grpc_server.py +0 -0
  48. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/server/reservation.py +0 -0
  49. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/server/rpc_manager.py +0 -0
  50. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/server/user_group_cli.py +0 -0
  51. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/server/user_group_handler.py +0 -0
  52. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/server/v2_errors.py +0 -0
  53. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/serverrf_cli.py +0 -0
  54. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/tools/__init__.py +0 -0
  55. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/tools/gen_certs.py +0 -0
  56. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/tools/gist_status.py +0 -0
  57. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/tools/gist_status_testing.py +0 -0
  58. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server/tools/introspect_uhd.py +0 -0
  59. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server.egg-info/SOURCES.txt +0 -0
  60. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  61. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  62. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server.egg-info/requires.txt +0 -0
  63. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/src/remoteRF_server.egg-info/top_level.txt +0 -0
  64. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/tests/test_cert_provider.py +0 -0
  65. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/tests/test_default_usrp_schema.py +0 -0
  66. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/tests/test_idl_schema.py +0 -0
  67. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/tests/test_pluto_compat.py +0 -0
  68. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/tests/test_process_arg.py +0 -0
  69. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/tests/test_reservation_timestamps.py +0 -0
  70. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/tests/test_rpc_manager_calln.py +0 -0
  71. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/tests/test_usrp_client_server_hardware.py +0 -0
  72. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/tests/test_usrp_client_server_localhost.py +0 -0
  73. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/tests/test_usrp_remoterf_rx_hardware.py +0 -0
  74. {remoterf_server-1.0.4 → remoterf_server-1.0.5}/tests/test_usrp_uhd410_hardware.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.0.4
3
+ Version: 1.0.5
4
4
  Summary: RemoteRF server-side control package
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -329,36 +329,10 @@ devices:
329
329
  serial: 31A2B3C
330
330
  ```
331
331
 
332
- Transmit is denied if `~/.config/remoterf/usrp_policy.yml` is missing,
333
- unreadable, invalid, or does not explicitly enable and bound the requested
334
- operation. Low-level writes are also denied unless both globally enabled and
335
- method/path/address allowlisted. A starting policy looks like:
336
-
337
- ```yaml
338
- defaults:
339
- tx_enabled: false
340
- low_level_write_enabled: false
341
-
342
- devices:
343
- "31A2B3C":
344
- tx_enabled: true
345
- tx_frequency_ranges_hz:
346
- - [902000000, 928000000]
347
- max_tx_gain_db: 20
348
- max_tx_normalized_gain: 0.5
349
- max_tx_power_dbm: -10
350
- max_tx_bandwidth_hz: 2000000
351
- max_tx_sample_rate_sps: 2000000
352
- allowed_tx_antennas: [TX/RX]
353
- max_burst_seconds: 1.0
354
- max_duty_cycle: 0.1
355
- duty_cycle_window_seconds: 60
356
- low_level_write_enabled: false
357
- low_level_write_methods: []
358
- gpio_write_masks: {}
359
- property_write_paths: []
360
- register_write_addresses: []
361
- ```
332
+ All USRP operations exposed by the Dynamic v2 schema are available without a
333
+ separate policy file. This includes transmit controls, transmit streaming,
334
+ GPIO/register methods, and property-tree writes. `usrp_policy.yml` is not read
335
+ or required by the current server.
362
336
 
363
337
  On every qualified target, generate the introspection record and enforce the
364
338
  pinned API gate:
@@ -314,36 +314,10 @@ devices:
314
314
  serial: 31A2B3C
315
315
  ```
316
316
 
317
- Transmit is denied if `~/.config/remoterf/usrp_policy.yml` is missing,
318
- unreadable, invalid, or does not explicitly enable and bound the requested
319
- operation. Low-level writes are also denied unless both globally enabled and
320
- method/path/address allowlisted. A starting policy looks like:
321
-
322
- ```yaml
323
- defaults:
324
- tx_enabled: false
325
- low_level_write_enabled: false
326
-
327
- devices:
328
- "31A2B3C":
329
- tx_enabled: true
330
- tx_frequency_ranges_hz:
331
- - [902000000, 928000000]
332
- max_tx_gain_db: 20
333
- max_tx_normalized_gain: 0.5
334
- max_tx_power_dbm: -10
335
- max_tx_bandwidth_hz: 2000000
336
- max_tx_sample_rate_sps: 2000000
337
- allowed_tx_antennas: [TX/RX]
338
- max_burst_seconds: 1.0
339
- max_duty_cycle: 0.1
340
- duty_cycle_window_seconds: 60
341
- low_level_write_enabled: false
342
- low_level_write_methods: []
343
- gpio_write_masks: {}
344
- property_write_paths: []
345
- register_write_addresses: []
346
- ```
317
+ All USRP operations exposed by the Dynamic v2 schema are available without a
318
+ separate policy file. This includes transmit controls, transmit streaming,
319
+ GPIO/register methods, and property-tree writes. `usrp_policy.yml` is not read
320
+ or required by the current server.
347
321
 
348
322
  On every qualified target, generate the introspection record and enforce the
349
323
  pinned API gate:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoteRF-server"
7
- version = "1.0.4"
7
+ version = "1.0.5"
8
8
  description = "RemoteRF server-side control package"
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -0,0 +1,41 @@
1
+ """Compatibility shim for unrestricted USRP access.
2
+
3
+ USRP policy enforcement is currently disabled. ``USRPPolicy`` remains importable
4
+ so older integrations do not fail at import time, but every supported control,
5
+ streaming, property-tree, GPIO, and register operation passes through without
6
+ policy checks.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from pathlib import Path
11
+ from typing import Any, Mapping
12
+
13
+
14
+ class USRPPolicy:
15
+ """No-op policy retained for protocol and integration compatibility."""
16
+
17
+ def __init__(self, config: Mapping[str, Any] | None = None, *, source: str = ""):
18
+ self.config = dict(config or {})
19
+ self.source = source
20
+
21
+ @classmethod
22
+ def load(cls, *, gid: int, serial: str = "", path: Path | None = None) -> "USRPPolicy":
23
+ del gid, serial, path
24
+ return cls()
25
+
26
+ @property
27
+ def tx_enabled(self) -> bool:
28
+ return True
29
+
30
+ @property
31
+ def restrictions(self) -> dict[str, Any]:
32
+ return {}
33
+
34
+ def check_control(self, method: str, args: Mapping[str, Any]) -> None:
35
+ del method, args
36
+
37
+ def check_tx_duration(self, duration: float) -> None:
38
+ del duration
39
+
40
+ def check_handle_call(self, type_id: str, method: str, args: Mapping[str, Any]) -> None:
41
+ del type_id, method, args
@@ -135,11 +135,6 @@ HANDLE_METHOD_RETURN_TYPES = {
135
135
  ("uhd.property.dboard_iface", "get_desired"): "uhd.usrp.dboard_iface",
136
136
  }
137
137
 
138
- LOW_LEVEL_WRITES = {
139
- "set_gpio_attr", "set_gpio_src", "set_user_register",
140
- "set_rx_filter", "set_tx_filter",
141
- }
142
-
143
138
  USRP2901_UNSUPPORTED_HANDLE_TYPES = {
144
139
  "uhd.usrp.radio_control",
145
140
  "uhd.usrp.mboard_controller",
@@ -1305,7 +1300,7 @@ def _objects() -> list[dict]:
1305
1300
  "uhd.property_tree",
1306
1301
  kind="remote_handle",
1307
1302
  mutability="remote_handle",
1308
- ownership={"scope": "session", "default_access": "read_only"},
1303
+ ownership={"scope": "session", "default_access": "read_write"},
1309
1304
  ),
1310
1305
  *[
1311
1306
  object_type(
@@ -1315,7 +1310,7 @@ def _objects() -> list[dict]:
1315
1310
  ownership={
1316
1311
  "scope": "session",
1317
1312
  "parent": "uhd.property_tree",
1318
- "default_access": "read_only",
1313
+ "default_access": "read_write",
1319
1314
  },
1320
1315
  )
1321
1316
  for suffix in (
@@ -1331,25 +1326,25 @@ def _objects() -> list[dict]:
1331
1326
  "uhd.usrp.radio_control",
1332
1327
  kind="remote_handle",
1333
1328
  mutability="remote_handle",
1334
- ownership={"scope": "session", "default_access": "read_only"},
1329
+ ownership={"scope": "session", "default_access": "read_write"},
1335
1330
  ),
1336
1331
  object_type(
1337
1332
  "uhd.usrp.mboard_controller",
1338
1333
  kind="remote_handle",
1339
1334
  mutability="remote_handle",
1340
- ownership={"scope": "session", "default_access": "read_only"},
1335
+ ownership={"scope": "session", "default_access": "read_write"},
1341
1336
  ),
1342
1337
  object_type(
1343
1338
  "uhd.usrp.dboard_iface",
1344
1339
  kind="remote_handle",
1345
1340
  mutability="remote_handle",
1346
- ownership={"scope": "session", "default_access": "read_only"},
1341
+ ownership={"scope": "session", "default_access": "read_write"},
1347
1342
  ),
1348
1343
  object_type(
1349
1344
  "uhd.usrp.user_settings_iface",
1350
1345
  kind="remote_handle",
1351
1346
  mutability="remote_handle",
1352
- ownership={"scope": "session", "default_access": "read_only"},
1347
+ ownership={"scope": "session", "default_access": "read_write"},
1353
1348
  ),
1354
1349
  ]
1355
1350
 
@@ -1415,7 +1410,6 @@ def _streamer_methods() -> list[dict]:
1415
1410
  )
1416
1411
  ],
1417
1412
  execution="sample_stream",
1418
- policy="tx_samples",
1419
1413
  ),
1420
1414
  method(
1421
1415
  "recv_async_msg", "uhd.usrp.TXStreamer",
@@ -1514,7 +1508,6 @@ def _property_tree_methods() -> list[dict]:
1514
1508
  )
1515
1509
  ],
1516
1510
  execution="server_handle",
1517
- policy="low_level_write",
1518
1511
  )
1519
1512
  )
1520
1513
  return methods
@@ -1531,13 +1524,13 @@ CAPABILITY_FIELDS = (
1531
1524
  "tx_bandwidth_ranges_per_channel", "master_clock_rate_range", "time_sources",
1532
1525
  "clock_sources", "gpio_banks", "sensors", "supported_cpu_formats",
1533
1526
  "supported_otw_formats", "maximum_remote_payload_bytes",
1534
- "supported_streaming_protocol_versions", "policy_restrictions",
1527
+ "supported_streaming_protocol_versions",
1535
1528
  "remote_resource_limits", "advanced_interfaces",
1536
1529
  )
1537
1530
 
1538
1531
  ERRORS = (
1539
1532
  "RemoteRFTransportError", "RemoteRFSessionExpiredError",
1540
- "RemoteRFReservationError", "RemoteRFPolicyError",
1533
+ "RemoteRFReservationError",
1541
1534
  "RemoteRFStaleHandleError", "RemoteRFProtocolError",
1542
1535
  "RemoteRFOverloadError",
1543
1536
  "RemoteRFServerOverloadedError", "RemoteRFNativeUHDError",
@@ -1558,15 +1551,6 @@ def build_usrp_schema_v2(driver_version: str = "0.0.1") -> dict:
1558
1551
  coverage: dict[str, str] = {}
1559
1552
  for name in MULTIUSRP_METHODS:
1560
1553
  execution = "server_optimized" if name in {"recv_num_samps", "send_waveform"} else "server_control"
1561
- policy = None
1562
- if (
1563
- name.startswith("set_tx_")
1564
- or name == "set_normalized_tx_gain"
1565
- or name == "send_waveform"
1566
- ):
1567
- policy = "tx_control"
1568
- elif name in LOW_LEVEL_WRITES:
1569
- policy = "low_level_write"
1570
1554
  availability = {"runtime_member": name}
1571
1555
  if name in OPTIONAL_METHODS:
1572
1556
  availability["optional"] = True
@@ -1577,7 +1561,6 @@ def build_usrp_schema_v2(driver_version: str = "0.0.1") -> dict:
1577
1561
  signatures.get(name, _fallback_overload()),
1578
1562
  execution=execution,
1579
1563
  availability=availability,
1580
- policy=policy,
1581
1564
  )
1582
1565
  )
1583
1566
  coverage[f"uhd.usrp.MultiUSRP.{name}"] = (
@@ -518,7 +518,7 @@ class UsrpSchema(DeviceSchema):
518
518
  return str(value)
519
519
  return ""
520
520
 
521
- def get_capabilities_v2(self, *, policy_restrictions=None):
521
+ def get_capabilities_v2(self):
522
522
  device = self.device
523
523
  if device is None:
524
524
  raise RuntimeError("USRP session is not open")
@@ -615,7 +615,6 @@ class UsrpSchema(DeviceSchema):
615
615
  },
616
616
  "maximum_remote_payload_bytes": 4 * 1024 * 1024,
617
617
  "supported_streaming_protocol_versions": ["1.0"],
618
- "policy_restrictions": dict(policy_restrictions or {}),
619
618
  }
620
619
 
621
620
  def _streamers(self):
@@ -23,6 +23,8 @@ from ..common.idl.v2 import (
23
23
  SCHEMA_VERSION,
24
24
  STREAMING_PROTOCOL_VERSION,
25
25
  )
26
+ # Retained as a module attribute for integrations that patched the former
27
+ # policy loader. Runtime access is no longer gated by it.
26
28
  from ..drivers.usrp.policy import USRPPolicy
27
29
  from ..drivers.usrp.uhd_manifest import (
28
30
  HANDLE_METHOD_RETURN_TYPES,
@@ -39,7 +41,6 @@ from .device_manager import (
39
41
  from .v2_errors import (
40
42
  RemoteRFNativeUHDError,
41
43
  RemoteRFOverloadError,
42
- RemoteRFPolicyError,
43
44
  RemoteRFProtocolError,
44
45
  RemoteRFReservationError,
45
46
  RemoteRFServerOverloadedError,
@@ -466,7 +467,6 @@ class DeviceSession:
466
467
  gid: int
467
468
  schema: Any
468
469
  schema_v2: dict
469
- policy: USRPPolicy
470
470
  root_handle: str
471
471
  capabilities: dict
472
472
  handles: dict[str, HandleEntry] = field(default_factory=dict)
@@ -569,10 +569,7 @@ class SessionManager:
569
569
  "the USRP is already open through a legacy v1 session"
570
570
  )
571
571
  live_schema = create()
572
- policy = USRPPolicy.load(gid=gid, serial=context.get("ident", ""))
573
- capabilities = live_schema.get_capabilities_v2(
574
- policy_restrictions=policy.restrictions
575
- )
572
+ capabilities = live_schema.get_capabilities_v2()
576
573
  capabilities["remote_resource_limits"] = {
577
574
  "maximum_sessions": _MAX_SESSIONS,
578
575
  "maximum_handles_per_session": _MAX_HANDLES_PER_SESSION,
@@ -601,7 +598,6 @@ class SessionManager:
601
598
  gid=gid,
602
599
  schema=live_schema,
603
600
  schema_v2=schema_v2,
604
- policy=policy,
605
601
  root_handle=root_handle,
606
602
  capabilities=capabilities,
607
603
  )
@@ -865,10 +861,6 @@ class SessionManager:
865
861
  args,
866
862
  positional_args,
867
863
  )
868
- policy_args = dict(args)
869
- if positional_args:
870
- policy_args["__args__"] = positional_args
871
- session.policy.check_control(method_name, policy_args)
872
864
  native_args = {
873
865
  key: self._resolve_handle_argument(session, value)
874
866
  for key, value in args.items()
@@ -884,7 +876,7 @@ class SessionManager:
884
876
  native_args,
885
877
  positional_args=native_positional,
886
878
  )
887
- except (RemoteRFPolicyError, RemoteRFProtocolError):
879
+ except RemoteRFProtocolError:
888
880
  raise
889
881
  except Exception as exc:
890
882
  raise RemoteRFNativeUHDError(
@@ -953,15 +945,6 @@ class SessionManager:
953
945
  except queue.Empty:
954
946
  return (False if mutate_metadata else None), mutations
955
947
 
956
- if not entry.type_id.endswith(("RXStreamer", "TXStreamer")):
957
- policy_args = dict(args)
958
- if positional_args:
959
- policy_args["__args__"] = positional_args
960
- session.policy.check_handle_call(
961
- entry.type_id,
962
- method_name,
963
- policy_args,
964
- )
965
948
  target = getattr(entry.value, method_name, None)
966
949
  if not callable(target):
967
950
  raise RemoteRFProtocolError(
@@ -1413,19 +1396,6 @@ class SampleDataV1Servicer(grpc_pb2_grpc.SampleDataV1Servicer):
1413
1396
  ):
1414
1397
  if not entry.type_id.endswith("TXStreamer"):
1415
1398
  raise RemoteRFStaleHandleError("TX data used a non-TX handle")
1416
- channels = entry.stream_channels or [0]
1417
- rates = []
1418
- get_rate = getattr(session.schema.device, "get_tx_rate", None)
1419
- if callable(get_rate):
1420
- for channel in channels:
1421
- rates.append(float(get_rate(channel)))
1422
- if not rates or min(rates) <= 0:
1423
- raise RemoteRFPolicyError(
1424
- "TX sample rate is unavailable for duty-cycle enforcement"
1425
- )
1426
- session.policy.check_tx_duration(
1427
- float(request.sample_count) / min(rates)
1428
- )
1429
1399
  if len(request.payload) != byte_count:
1430
1400
  raise RemoteRFProtocolError(
1431
1401
  "TX payload byte length does not match dtype/shape"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.0.4
3
+ Version: 1.0.5
4
4
  Summary: RemoteRF server-side control package
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -329,36 +329,10 @@ devices:
329
329
  serial: 31A2B3C
330
330
  ```
331
331
 
332
- Transmit is denied if `~/.config/remoterf/usrp_policy.yml` is missing,
333
- unreadable, invalid, or does not explicitly enable and bound the requested
334
- operation. Low-level writes are also denied unless both globally enabled and
335
- method/path/address allowlisted. A starting policy looks like:
336
-
337
- ```yaml
338
- defaults:
339
- tx_enabled: false
340
- low_level_write_enabled: false
341
-
342
- devices:
343
- "31A2B3C":
344
- tx_enabled: true
345
- tx_frequency_ranges_hz:
346
- - [902000000, 928000000]
347
- max_tx_gain_db: 20
348
- max_tx_normalized_gain: 0.5
349
- max_tx_power_dbm: -10
350
- max_tx_bandwidth_hz: 2000000
351
- max_tx_sample_rate_sps: 2000000
352
- allowed_tx_antennas: [TX/RX]
353
- max_burst_seconds: 1.0
354
- max_duty_cycle: 0.1
355
- duty_cycle_window_seconds: 60
356
- low_level_write_enabled: false
357
- low_level_write_methods: []
358
- gpio_write_masks: {}
359
- property_write_paths: []
360
- register_write_addresses: []
361
- ```
332
+ All USRP operations exposed by the Dynamic v2 schema are available without a
333
+ separate policy file. This includes transmit controls, transmit streaming,
334
+ GPIO/register methods, and property-tree writes. `usrp_policy.yml` is not read
335
+ or required by the current server.
362
336
 
363
337
  On every qualified target, generate the introspection record and enforce the
364
338
  pinned API gate:
@@ -29,7 +29,6 @@ from remoteRF_server.server.dynamic_v2 import (
29
29
  )
30
30
  from remoteRF_server.server.v2_errors import (
31
31
  RemoteRFOverloadError,
32
- RemoteRFPolicyError,
33
32
  RemoteRFProtocolError,
34
33
  RemoteRFServerOverloadedError,
35
34
  RemoteRFSessionExpiredError,
@@ -241,76 +240,19 @@ class DynamicV2ServerTests(unittest.TestCase):
241
240
  )
242
241
  self.assertEqual(rejected.error.code, "RemoteRFProtocolError")
243
242
 
244
- def test_policy_denies_tx_by_default_and_enforces_configured_limits(self):
245
- denied = USRPPolicy({})
246
- with self.assertRaises(RemoteRFPolicyError):
247
- denied.check_control("set_tx_freq", {"tune_request": 915e6})
248
- with self.assertRaises(RemoteRFPolicyError):
249
- denied.check_control("set_normalized_tx_gain", {"gain": 0.1})
250
- with self.assertRaises(RemoteRFPolicyError):
251
- denied.check_control("set_gpio_attr", {})
252
-
253
- policy = USRPPolicy(
254
- {
255
- "tx_enabled": True,
256
- "tx_frequency_ranges_hz": [[900e6, 930e6]],
257
- "max_tx_gain_db": 20,
258
- "max_tx_sample_rate_sps": 2e6,
259
- "max_burst_seconds": 1.0,
260
- "max_duty_cycle": 0.1,
261
- "duty_cycle_window_seconds": 60,
262
- }
263
- )
264
- policy.check_control("set_tx_freq", {"tune_request": 915e6})
265
- policy.check_control("set_tx_gain", {"gain": 19})
266
- with self.assertRaises(RemoteRFPolicyError):
267
- policy.check_control("set_tx_freq", {"tune_request": 2.4e9})
268
- with self.assertRaises(RemoteRFPolicyError):
269
- policy.check_control("set_tx_gain", {"gain": 21})
270
-
271
- normalized = USRPPolicy(
272
- {
273
- "tx_enabled": True,
274
- "max_tx_normalized_gain": 0.5,
275
- }
276
- )
277
- normalized.check_control("set_normalized_tx_gain", {"gain": 0.5})
278
- with self.assertRaises(RemoteRFPolicyError):
279
- normalized.check_control("set_normalized_tx_gain", {"gain": 0.6})
280
-
281
- low_level = USRPPolicy(
282
- {
283
- "low_level_write_enabled": True,
284
- "low_level_write_methods": ["set_gpio_attr", "set"],
285
- "gpio_write_masks": {"FP0": 0x03},
286
- "property_write_paths": ["/mboards/0/user"],
287
- }
288
- )
289
- low_level.check_control(
290
- "set_gpio_attr",
291
- {"bank": "FP0", "mask": 0x03},
292
- )
293
- with self.assertRaises(RemoteRFPolicyError):
294
- low_level.check_control(
295
- "set_gpio_attr",
296
- {"bank": "FP0", "mask": 0x04},
297
- )
298
- with self.assertRaises(RemoteRFPolicyError):
299
- low_level.check_control(
300
- "set_gpio_attr",
301
- {"bank": "FP0"},
302
- )
303
- low_level.check_handle_call(
243
+ def test_policy_compatibility_shim_allows_all_operations(self):
244
+ policy = USRPPolicy({"tx_enabled": False, "max_tx_gain_db": 0})
245
+ self.assertTrue(policy.tx_enabled)
246
+ self.assertEqual(policy.restrictions, {})
247
+ policy.check_control("set_tx_freq", {"tune_request": 2.4e9})
248
+ policy.check_control("set_tx_gain", {"gain": 100})
249
+ policy.check_control("set_gpio_attr", {"bank": "FP0", "mask": 0xFFFFFFFF})
250
+ policy.check_tx_duration(3600)
251
+ policy.check_handle_call(
304
252
  "uhd.property_tree",
305
253
  "set",
306
- {"__args__": ["/mboards/0/user/value", 1]},
254
+ {"__args__": ["/mboards/0/unsafe", 1]},
307
255
  )
308
- with self.assertRaises(RemoteRFPolicyError):
309
- low_level.check_handle_call(
310
- "uhd.property_tree",
311
- "set",
312
- {"__args__": ["/mboards/0/unsafe", 1]},
313
- )
314
256
 
315
257
  def test_sessions_use_opaque_typed_generation_checked_handles(self):
316
258
  inventory = FakeInventorySchema()
@@ -344,6 +286,20 @@ class DynamicV2ServerTests(unittest.TestCase):
344
286
  self.assertTrue(session.session_id.startswith("sess_"))
345
287
  self.assertTrue(session.root_handle.startswith("dev_"))
346
288
  self.assertNotIn("rx:1", session.root_handle)
289
+ manager.invoke(
290
+ session.session_id,
291
+ session.root_handle,
292
+ "set_tx_freq",
293
+ {"tune_request": 2.4e9},
294
+ overload_id="default",
295
+ )
296
+ manager.invoke(
297
+ session.session_id,
298
+ session.root_handle,
299
+ "set_gpio_attr",
300
+ {"bank": "FP0", "attr": "OUT", "value": 1},
301
+ overload_id="default",
302
+ )
347
303
  result, _ = manager.invoke(
348
304
  session.session_id,
349
305
  session.root_handle,
@@ -413,15 +369,22 @@ class DynamicV2ServerTests(unittest.TestCase):
413
369
  overload_id="default",
414
370
  )
415
371
  self.assertEqual(value, 3)
416
- with self.assertRaises(RemoteRFPolicyError):
417
- manager.invoke(
418
- session.session_id,
419
- prop["__remoterf_handle__"],
420
- "set",
421
- {},
422
- positional_args=[4],
423
- overload_id="default",
424
- )
372
+ manager.invoke(
373
+ session.session_id,
374
+ prop["__remoterf_handle__"],
375
+ "set",
376
+ {},
377
+ positional_args=[4],
378
+ overload_id="default",
379
+ )
380
+ updated, _ = manager.invoke(
381
+ session.session_id,
382
+ prop["__remoterf_handle__"],
383
+ "get",
384
+ {},
385
+ overload_id="default",
386
+ )
387
+ self.assertEqual(updated, 4)
425
388
  with self.assertRaises(RemoteRFProtocolError):
426
389
  manager.invoke(
427
390
  session.session_id,
@@ -1,363 +0,0 @@
1
- """Server-owned USRP policy enforcement.
2
-
3
- Missing, unreadable, or incomplete policy files intentionally deny TX and
4
- low-level writes. Read-only control and RX remain available.
5
- """
6
- from __future__ import annotations
7
-
8
- import os
9
- import time
10
- from collections import deque
11
- from pathlib import Path
12
- from typing import Any, Mapping
13
-
14
- from ...server.v2_errors import RemoteRFPolicyError
15
-
16
- try:
17
- import yaml
18
- except ImportError: # pragma: no cover - deployment dependency, deny-safe fallback
19
- yaml = None
20
-
21
-
22
- def default_policy_path() -> Path:
23
- root = Path(
24
- os.getenv(
25
- "REMOTERF_CONFIG_DIR",
26
- Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config")) / "remoterf",
27
- )
28
- )
29
- return root / "usrp_policy.yml"
30
-
31
-
32
- def _inside_ranges(value: float, ranges) -> bool:
33
- for entry in ranges or ():
34
- try:
35
- low, high = float(entry[0]), float(entry[1])
36
- except (TypeError, ValueError, IndexError):
37
- continue
38
- if low <= float(value) <= high:
39
- return True
40
- return False
41
-
42
-
43
- def _argument(args: Mapping[str, Any], *names: str, position: int | None = None):
44
- for name in names:
45
- if name in args:
46
- return args[name]
47
- positional = args.get("__args__", ())
48
- if position is not None and isinstance(positional, (list, tuple)):
49
- if 0 <= position < len(positional):
50
- return positional[position]
51
- return None
52
-
53
-
54
- class USRPPolicy:
55
- def __init__(self, config: Mapping[str, Any] | None = None, *, source: str = ""):
56
- self.config = dict(config or {})
57
- self.source = source
58
- self._tx_history: deque[tuple[float, float]] = deque()
59
-
60
- @classmethod
61
- def load(cls, *, gid: int, serial: str = "", path: Path | None = None) -> "USRPPolicy":
62
- path = path or default_policy_path()
63
- if yaml is None or not path.exists():
64
- return cls({}, source=str(path))
65
- try:
66
- raw = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
67
- if not isinstance(raw, Mapping):
68
- raise TypeError("policy root must be a mapping")
69
- defaults = dict(raw.get("defaults") or {})
70
- devices = dict(raw.get("devices") or {})
71
- selected = devices.get(str(gid)) or devices.get(serial) or {}
72
- merged = dict(defaults)
73
- merged.update(dict(selected or {}))
74
- except Exception:
75
- return cls({}, source=str(path))
76
- return cls(merged, source=str(path))
77
-
78
- @property
79
- def tx_enabled(self) -> bool:
80
- return self.config.get("tx_enabled") is True
81
-
82
- @property
83
- def restrictions(self) -> dict[str, Any]:
84
- return {
85
- "tx_enabled": self.tx_enabled,
86
- "tx_frequency_ranges_hz": self.config.get("tx_frequency_ranges_hz", []),
87
- "max_tx_gain_db": self.config.get("max_tx_gain_db"),
88
- "max_tx_power_dbm": self.config.get("max_tx_power_dbm"),
89
- "max_tx_bandwidth_hz": self.config.get("max_tx_bandwidth_hz"),
90
- "max_tx_sample_rate_sps": self.config.get("max_tx_sample_rate_sps"),
91
- "allowed_tx_antennas": self.config.get("allowed_tx_antennas", []),
92
- "max_burst_seconds": self.config.get("max_burst_seconds"),
93
- "max_duty_cycle": self.config.get("max_duty_cycle"),
94
- "low_level_write_enabled": self.config.get("low_level_write_enabled") is True,
95
- "low_level_write_methods": self.config.get("low_level_write_methods", []),
96
- }
97
-
98
- def _require_tx(self) -> None:
99
- if not self.tx_enabled:
100
- raise RemoteRFPolicyError(
101
- "USRP transmit is disabled until an administrator configures usrp_policy.yml",
102
- details={"policy_source": self.source},
103
- )
104
-
105
- def check_control(self, method: str, args: Mapping[str, Any]) -> None:
106
- if (
107
- method.startswith("set_tx_")
108
- or method == "set_normalized_tx_gain"
109
- or method == "send_waveform"
110
- ):
111
- self._require_tx()
112
-
113
- if method in {"set_tx_freq", "set_tx_lo_freq", "send_waveform"}:
114
- value = _argument(
115
- args,
116
- "tune_request",
117
- "freq",
118
- "frequency",
119
- position=0,
120
- )
121
- if isinstance(value, Mapping):
122
- value = value.get("target_freq", value.get("freq"))
123
- ranges = self.config.get("tx_frequency_ranges_hz") or []
124
- if value is None or not ranges or not _inside_ranges(float(value), ranges):
125
- raise RemoteRFPolicyError(
126
- f"TX frequency is outside the configured policy ranges",
127
- details={"method": method, "requested_hz": value, "allowed_ranges_hz": ranges},
128
- method=method,
129
- )
130
-
131
- if method == "set_tx_gain":
132
- limit = self.config.get("max_tx_gain_db")
133
- value = _argument(args, "gain", position=0)
134
- if limit is None or value is None or float(value) > float(limit):
135
- raise RemoteRFPolicyError(
136
- "TX gain exceeds or lacks an administrator limit",
137
- details={"requested_db": value, "maximum_db": limit},
138
- method=method,
139
- )
140
-
141
- if method == "set_normalized_tx_gain":
142
- limit = self.config.get("max_tx_normalized_gain")
143
- value = _argument(args, "gain", position=0)
144
- if limit is None or value is None or float(value) > float(limit):
145
- raise RemoteRFPolicyError(
146
- "normalized TX gain exceeds or lacks an administrator limit",
147
- details={"requested": value, "maximum": limit},
148
- method=method,
149
- )
150
-
151
- if method == "set_tx_power_reference":
152
- limit = self.config.get("max_tx_power_dbm")
153
- value = _argument(args, "power_dbm", "power", position=0)
154
- if limit is None or value is None or float(value) > float(limit):
155
- raise RemoteRFPolicyError(
156
- "TX power reference exceeds or lacks an administrator limit",
157
- details={"requested_dbm": value, "maximum_dbm": limit},
158
- method=method,
159
- )
160
-
161
- if method == "set_tx_bandwidth":
162
- limit = self.config.get("max_tx_bandwidth_hz")
163
- value = _argument(args, "bandwidth", position=0)
164
- if limit is None or value is None or float(value) > float(limit):
165
- raise RemoteRFPolicyError(
166
- "TX bandwidth exceeds or lacks an administrator limit",
167
- details={"requested_hz": value, "maximum_hz": limit},
168
- method=method,
169
- )
170
-
171
- if method == "set_tx_rate":
172
- limit = self.config.get("max_tx_sample_rate_sps")
173
- value = _argument(args, "rate", position=0)
174
- if limit is None or value is None or float(value) > float(limit):
175
- raise RemoteRFPolicyError(
176
- "TX sample rate exceeds or lacks an administrator limit",
177
- details={"requested_sps": value, "maximum_sps": limit},
178
- method=method,
179
- )
180
-
181
- if method == "set_tx_antenna":
182
- allowed = {
183
- str(value)
184
- for value in (self.config.get("allowed_tx_antennas") or ())
185
- }
186
- antenna = str(_argument(args, "antenna", position=0) or "")
187
- if not allowed or antenna not in allowed:
188
- raise RemoteRFPolicyError(
189
- "TX antenna is not in the administrator allowlist",
190
- details={"requested": antenna, "allowed": sorted(allowed)},
191
- method=method,
192
- )
193
-
194
- if method == "set_tx_lo_source":
195
- allowed = {
196
- str(value)
197
- for value in (self.config.get("allowed_tx_lo_sources") or ())
198
- }
199
- source = str(_argument(args, "source", position=0) or "")
200
- if not allowed or source not in allowed:
201
- raise RemoteRFPolicyError(
202
- "TX LO source is not in the administrator allowlist",
203
- details={"requested": source, "allowed": sorted(allowed)},
204
- method=method,
205
- )
206
-
207
- if (
208
- method == "set_tx_lo_export_enabled"
209
- and bool(_argument(args, "enabled", "enable", position=0))
210
- and self.config.get("allow_tx_lo_export") is not True
211
- ):
212
- raise RemoteRFPolicyError(
213
- "TX LO export is disabled by administrator policy",
214
- method=method,
215
- )
216
-
217
- if (
218
- method == "set_rx_lo_export_enabled"
219
- and bool(_argument(args, "enabled", "enable", position=0))
220
- and self.config.get("allow_rx_lo_export") is not True
221
- ):
222
- raise RemoteRFPolicyError(
223
- "RX LO export is disabled by administrator policy",
224
- method=method,
225
- )
226
-
227
- if method == "send_waveform":
228
- self.check_control(
229
- "set_tx_rate",
230
- {"rate": _argument(args, "rate", position=3)},
231
- )
232
- self.check_control(
233
- "set_tx_gain",
234
- {"gain": _argument(args, "gain", position=5)},
235
- )
236
- self.check_tx_duration(
237
- float(_argument(args, "duration", position=1) or 0.0)
238
- )
239
-
240
- if method in {
241
- "set_gpio_attr", "set_gpio_src", "set_user_register",
242
- "set_rx_filter", "set_tx_filter",
243
- }:
244
- self._check_low_level_write("", method, args)
245
-
246
- def check_tx_duration(self, duration: float) -> None:
247
- self._require_tx()
248
- if duration < 0:
249
- raise RemoteRFPolicyError("TX duration cannot be negative")
250
- maximum = self.config.get("max_burst_seconds")
251
- try:
252
- maximum_value = float(maximum)
253
- except (TypeError, ValueError):
254
- maximum_value = -1.0
255
- if maximum_value < 0 or duration > maximum_value:
256
- raise RemoteRFPolicyError(
257
- "TX duration exceeds or lacks an administrator limit",
258
- details={"requested_seconds": duration, "maximum_seconds": maximum},
259
- )
260
-
261
- try:
262
- window = float(self.config.get("duty_cycle_window_seconds", 60.0))
263
- except (TypeError, ValueError):
264
- window = 0.0
265
- fraction = self.config.get("max_duty_cycle")
266
- try:
267
- fraction_value = float(fraction)
268
- except (TypeError, ValueError):
269
- fraction_value = -1.0
270
- if window <= 0 or not 0 <= fraction_value <= 1:
271
- raise RemoteRFPolicyError("TX duty-cycle policy is not configured")
272
- now = time.monotonic()
273
- while self._tx_history and self._tx_history[0][0] < now - window:
274
- self._tx_history.popleft()
275
- used = sum(item[1] for item in self._tx_history)
276
- if used + duration > window * fraction_value:
277
- raise RemoteRFPolicyError(
278
- "TX duty-cycle limit would be exceeded",
279
- details={"window_seconds": window, "maximum_fraction": fraction},
280
- )
281
- self._tx_history.append((now, duration))
282
-
283
- def check_handle_call(self, type_id: str, method: str, args: Mapping[str, Any]) -> None:
284
- """Keep returned low-level interfaces read-only unless allowlisted."""
285
- read_prefixes = ("get", "read", "list", "exists", "to_", "peek")
286
- if (
287
- method.startswith(read_prefixes)
288
- or method.startswith("access_")
289
- or method in {"access", "subtree"}
290
- ):
291
- return
292
- self._check_low_level_write(type_id, method, args)
293
-
294
- def _check_low_level_write(
295
- self,
296
- type_id: str,
297
- method: str,
298
- args: Mapping[str, Any],
299
- ) -> None:
300
- allowed = set(self.config.get("low_level_write_methods") or ())
301
- if self.config.get("low_level_write_enabled") is not True or method not in allowed:
302
- raise RemoteRFPolicyError(
303
- f"{type_id}.{method} is not allowlisted for low-level writes",
304
- details={"type_id": type_id, "method": method},
305
- method=method,
306
- )
307
-
308
- if method == "set_gpio_attr":
309
- bank = str(args.get("bank", ""))
310
- mask = _argument(args, "mask", position=3)
311
- requested_mask = int((1 << 32) - 1 if mask is None else mask)
312
- allowed_mask = int(
313
- dict(self.config.get("gpio_write_masks") or {}).get(bank, 0)
314
- )
315
- if requested_mask & ~allowed_mask:
316
- raise RemoteRFPolicyError(
317
- "GPIO write mask exceeds the administrator allowlist",
318
- details={
319
- "bank": bank,
320
- "requested_mask": requested_mask,
321
- "allowed_mask": allowed_mask,
322
- },
323
- method=method,
324
- )
325
-
326
- path_value = _argument(
327
- args,
328
- "path",
329
- "property_path",
330
- "key",
331
- position=0,
332
- )
333
- path = str(path_value) if path_value is not None else ""
334
- if path:
335
- allowed_paths = tuple(
336
- str(value)
337
- for value in (self.config.get("property_write_paths") or ())
338
- )
339
- if not allowed_paths or not any(
340
- path == prefix or path.startswith(prefix.rstrip("/") + "/")
341
- for prefix in allowed_paths
342
- ):
343
- raise RemoteRFPolicyError(
344
- "property-tree write path is not allowlisted",
345
- details={"path": path, "allowed_paths": list(allowed_paths)},
346
- method=method,
347
- )
348
-
349
- if method in {"poke32", "poke64", "write_register", "set_user_register"}:
350
- address = _argument(args, "addr", "address", position=0)
351
- allowed_addresses = {
352
- int(value, 0) if isinstance(value, str) else int(value)
353
- for value in (self.config.get("register_write_addresses") or ())
354
- }
355
- if address is None or int(address) not in allowed_addresses:
356
- raise RemoteRFPolicyError(
357
- "register write address is not allowlisted",
358
- details={
359
- "address": address,
360
- "allowed_addresses": sorted(allowed_addresses),
361
- },
362
- method=method,
363
- )