RemoteRF 2.11.0__tar.gz → 2.12.3__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.
- {remoterf-2.11.0/src/remoterf.egg-info → remoterf-2.12.3}/PKG-INFO +4 -2
- {remoterf-2.11.0 → remoterf-2.12.3}/pyproject.toml +7 -3
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/admin.py +7 -5
- remoterf-2.12.3/src/remoterf/agent_cli.py +374 -0
- remoterf-2.12.3/src/remoterf/agent_docs.py +265 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/common/utils/banner.py +3 -6
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/config/config.py +4 -4
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/core/app.py +206 -420
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/core/availability.py +17 -68
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/core/grpc_acc.py +30 -19
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/core/grpc_client.py +41 -13
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/core/messages.py +4 -4
- remoterf-2.12.3/src/remoterf/data/agent_guide.md +135 -0
- remoterf-2.12.3/src/remoterf/data/mcp_tools.json +126 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/deployment/backend.py +28 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/dynamic_device.py +11 -0
- remoterf-2.12.3/src/remoterf/mcp_server.py +324 -0
- remoterf-2.12.3/src/remoterf/ops.py +811 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/remoterf_cli.py +114 -33
- {remoterf-2.11.0 → remoterf-2.12.3/src/remoterf.egg-info}/PKG-INFO +4 -2
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf.egg-info/SOURCES.txt +10 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf.egg-info/requires.txt +3 -0
- remoterf-2.12.3/tests/test_agent_cli.py +294 -0
- remoterf-2.12.3/tests/test_agent_docs.py +136 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_banner.py +4 -4
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_bundled_drivers.py +2 -2
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_cli_login_fresh.py +25 -2
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_device_routes.py +8 -8
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_devices_api.py +21 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_federated_devices.py +16 -0
- remoterf-2.12.3/tests/test_finish_prompt.py +36 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_homes.py +14 -8
- remoterf-2.12.3/tests/test_mcp_server.py +223 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_messages.py +11 -10
- remoterf-2.12.3/tests/test_ops.py +443 -0
- remoterf-2.11.0/tests/test_finish_prompt.py +0 -20
- {remoterf-2.11.0 → remoterf-2.12.3}/LICENSE +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/README.md +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/setup.cfg +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/setup.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/_remoterf_alias.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/__init__.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/common/__init__.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/common/grpc/__init__.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/common/grpc/deployment_capabilities.proto +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/common/grpc/deployment_capabilities_pb2.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/common/grpc/grpc.proto +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/common/grpc/grpc_pb2.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/common/grpc/grpc_pb2_grpc.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/common/grpc/v2_codec.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/common/tos_notice.txt +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/common/utils/__init__.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/common/utils/ansi_codes.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/common/utils/api_token.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/common/utils/list_string.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/common/utils/process_arg.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/config/__init__.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/config/cert_fetcher.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/core/__init__.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/core/acc_login.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/core/banner_note.txt +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/core/direct_path.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/core/dynamic_v2_transport.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/core/secure_channel.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/core/v2_errors.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/deployment/__init__.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/deployment/direct.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/deployment/homes.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/deployment/http.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/deployment/lanpass.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/deployment/state.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/__init__.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/_virtual.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/adalm_pluto/__init__.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/adalm_pluto/adalm_pluto_remote.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/dynamic_v2.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/hackrf/__init__.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/hackrf/hackrf_remote.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/rtl_sdr/__init__.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/rtl_sdr/rtl_sdr_remote.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/schema_review.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/sim_sdr/__init__.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/sim_sdr/sim_sdr_remote.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/support/__init__.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/support/ti_mmwave.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/support/uhd.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/support/uhd_dsp.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/support/uhd_v2.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/ti_mmwave/__init__.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/ti_mmwave/ti_mmwave_remote.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/usrp/__init__.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/usrp/usrp_remote.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/drivers/usrp/usrp_remote.pyi +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf/version.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf.egg-info/dependency_links.txt +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf.egg-info/entry_points.txt +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf.egg-info/top_level.txt +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/src/remoterf.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_batch.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_config_address.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_deployment_accounts.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_direct_path_fixed.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_direct_path_global.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_direct_path_partner.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_direct_path_race.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_direct_path_relay.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_dynamic_device_codegen.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_dynamic_v2.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_fetch_idl_fallback.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_gate_i_direct_path.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_grpc_client_target.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_lan_fallback.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_native_account_credentials.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_native_devices.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_old_import_name.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_partner_dynamic_v2.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_pluto_virtual.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_process_arg.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_relay_paging.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_remote_admin.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_remoterf_cli.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_rx_readahead.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_schema_review.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_ti_mmwave.py +0 -0
- {remoterf-2.11.0 → remoterf-2.12.3}/tests/test_upgrade_rescue.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remoterf
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.12.3
|
|
4
4
|
Summary: A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts.
|
|
5
|
-
Author-email: Ethan Ge <
|
|
5
|
+
Author-email: Ethan Ge <84103791+ethange1@users.noreply.github.com>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
7
7
|
Classifier: Programming Language :: Python :: 3
|
|
8
8
|
Classifier: Operating System :: OS Independent
|
|
@@ -21,6 +21,8 @@ Requires-Dist: pydantic<3.0,>=2.6
|
|
|
21
21
|
Requires-Dist: keyring<26,>=24
|
|
22
22
|
Requires-Dist: aioice<0.11,>=0.10
|
|
23
23
|
Requires-Dist: aioquic<2.0,>=1.2
|
|
24
|
+
Provides-Extra: mcp
|
|
25
|
+
Requires-Dist: mcp<3,>=2.3; extra == "mcp"
|
|
24
26
|
Dynamic: license-file
|
|
25
27
|
|
|
26
28
|
# Remote RF
|
|
@@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "remoterf"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.12.3"
|
|
8
8
|
description = "A python API to remotely access signal centric hardware. Client-side only! Courtesy of Wireless Lab @ UCLA & Prof. Ian Roberts."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "GPL-3.0-or-later"
|
|
11
11
|
license-files = ["LICENSE"]
|
|
12
|
-
authors = [{ name = "Ethan Ge", email = "
|
|
12
|
+
authors = [{ name = "Ethan Ge", email = "84103791+ethange1@users.noreply.github.com" }]
|
|
13
13
|
requires-python = ">=3.10"
|
|
14
14
|
dependencies = [
|
|
15
15
|
"remoterf-federation-core==0.16.0",
|
|
@@ -30,6 +30,10 @@ classifiers = [
|
|
|
30
30
|
"Operating System :: OS Independent",
|
|
31
31
|
]
|
|
32
32
|
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
# `remoterf mcp`: the local MCP server for AI assistants.
|
|
35
|
+
mcp = ["mcp>=2.3,<3"]
|
|
36
|
+
|
|
33
37
|
[project.scripts]
|
|
34
38
|
remoterf = "remoterf.remoterf_cli:main"
|
|
35
39
|
|
|
@@ -43,4 +47,4 @@ py-modules = ["remoterf", "_remoterf_alias"]
|
|
|
43
47
|
where = ["src"]
|
|
44
48
|
|
|
45
49
|
[tool.setuptools.package-data]
|
|
46
|
-
"*" = ["*.crt", "*.key", "*.pem", "*.txt", "*.pyi", "*.proto"]
|
|
50
|
+
"*" = ["*.crt", "*.key", "*.pem", "*.txt", "*.pyi", "*.proto", "*.md", "*.json"]
|
|
@@ -340,9 +340,11 @@ def cmd_login(target: str | None) -> int:
|
|
|
340
340
|
alias = entry.get("alias") or _resolve(lab)[3]
|
|
341
341
|
strict = trusted(alias)
|
|
342
342
|
if not strict:
|
|
343
|
-
print(f"This machine has not trusted {lab}'s admin host key. Safer
|
|
344
|
-
|
|
345
|
-
f"
|
|
343
|
+
print(f"This machine has not trusted {lab}'s admin host key. Safer options:\n"
|
|
344
|
+
"• Use a code from the lab console.\n"
|
|
345
|
+
f"• Or run `remoterf --remote-admin trust {lab} SHA256:…` with a verified fingerprint.\n"
|
|
346
|
+
f"• Copy {known_hosts_path()} from a paired machine.\n"
|
|
347
|
+
"Accepting it below on first sight trusts the "
|
|
346
348
|
"network you are on.", file=sys.stderr)
|
|
347
349
|
_save_lab(lab, {**entry, "user": user, "alias": alias})
|
|
348
350
|
return _exec(ssh_argv(lab, user, alias, strict=strict))
|
|
@@ -371,9 +373,9 @@ def cmd_hostkey(lab: str) -> int:
|
|
|
371
373
|
|
|
372
374
|
|
|
373
375
|
USAGE = """Usage:
|
|
374
|
-
remoterf --remote-admin [<user>@]<lab>
|
|
376
|
+
remoterf --remote-admin [<user>@]<lab> open a lab's admin console (prompts if <lab> is omitted)
|
|
375
377
|
remoterf --remote-admin pair <user>@<lab> <code> first time on this machine (code from: serverrf admins add/code)
|
|
376
|
-
remoterf --remote-admin trust <lab> SHA256:… trust a host key fingerprint
|
|
378
|
+
remoterf --remote-admin trust <lab> SHA256:… trust a verified host key fingerprint
|
|
377
379
|
remoterf --remote-admin hostkey <lab> show the lab's current host key"""
|
|
378
380
|
|
|
379
381
|
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
# Copyright (C) 2026 RemoteRF
|
|
2
|
+
#
|
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
6
|
+
# (at your option) any later version.
|
|
7
|
+
#
|
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
11
|
+
# GNU General Public License for more details.
|
|
12
|
+
#
|
|
13
|
+
# You should have received a copy of the GNU General Public License
|
|
14
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
15
|
+
|
|
16
|
+
"""The scriptable CLI over remoterf.ops: no prompts, no colour, no spinner.
|
|
17
|
+
With --json, stdout carries exactly one JSON object; messages go to stderr."""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
from contextlib import redirect_stdout
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
|
|
25
|
+
from . import ops
|
|
26
|
+
from .core.messages import explain
|
|
27
|
+
from .deployment.http import AccountBackendError
|
|
28
|
+
from .ops import RemoteRFOpsError
|
|
29
|
+
|
|
30
|
+
COMMANDS = ("auth", "devices", "slots", "reservations", "reserve", "cancel", "doctor")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class _Parser(argparse.ArgumentParser):
|
|
34
|
+
def error(self, message):
|
|
35
|
+
raise RemoteRFOpsError("usage", f"{self.prog}: {message}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _clean(value):
|
|
39
|
+
"""Server and deployment text, safe for a terminal."""
|
|
40
|
+
return "".join(c for c in str(value) if c.isprintable())
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ── auth ─────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
def _default_home():
|
|
46
|
+
"""The home `remoterf -l` would pick on its own."""
|
|
47
|
+
from .deployment import homes
|
|
48
|
+
|
|
49
|
+
last = homes.last_target()
|
|
50
|
+
if last:
|
|
51
|
+
return homes.GLOBAL_ACCOUNTS if last["kind"] == "global" else last["name"]
|
|
52
|
+
saved = sorted(homes.homes())
|
|
53
|
+
if len(saved) > 1 or saved and homes.has_global_account():
|
|
54
|
+
raise RemoteRFOpsError("usage", "Several logins are saved here; choose one with --home.")
|
|
55
|
+
return saved[0] if saved else homes.GLOBAL_ACCOUNTS
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _login_global(user, password):
|
|
59
|
+
"""`remoterf -l global` then login: Global selected, its session stored."""
|
|
60
|
+
from .deployment import homes
|
|
61
|
+
from .deployment.backend import HttpsJsonAccountBackend
|
|
62
|
+
from .deployment.state import origin, select_target
|
|
63
|
+
from .remoterf_cli import _global_origin
|
|
64
|
+
|
|
65
|
+
selected = origin(_global_origin())
|
|
66
|
+
backend = HttpsJsonAccountBackend(selected)
|
|
67
|
+
try:
|
|
68
|
+
deployment_id = backend.capabilities["deployment_id"]
|
|
69
|
+
select_target({"origin": selected, "transport": "https-json", "deployment_id": deployment_id})
|
|
70
|
+
homes.remember_global_account(selected, deployment_id)
|
|
71
|
+
homes.set_last_target("global")
|
|
72
|
+
backend.login(user, password)
|
|
73
|
+
try:
|
|
74
|
+
homes.sync_linked(backend.me()) # the labs `remoterf auth login --home <lab>` then needs
|
|
75
|
+
except Exception: # noqa: BLE001 - refreshed again at the next login
|
|
76
|
+
pass
|
|
77
|
+
finally:
|
|
78
|
+
backend.close()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _login_lab(home, user, password):
|
|
82
|
+
"""`remoterf -l NAME` then login: the deployment selected, its session token stored."""
|
|
83
|
+
from .core.grpc_acc import RemoteRFAccount
|
|
84
|
+
from .deployment import homes
|
|
85
|
+
from .deployment.backend import LegacyGrpcAccountBackend
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
if home in homes.homes():
|
|
89
|
+
homes.connect(home)
|
|
90
|
+
else:
|
|
91
|
+
home = homes.register_home(f"{home}.{homes.GLOBAL_DOMAIN}")[0]
|
|
92
|
+
backend = LegacyGrpcAccountBackend()
|
|
93
|
+
except (RuntimeError, ValueError) as exc:
|
|
94
|
+
raise RemoteRFOpsError("deployment_unreachable", f"Could not connect to {home}: {explain(exc, where=home)}") from exc
|
|
95
|
+
if "@" in user and homes.is_linked_home(home):
|
|
96
|
+
# Here the login is the username; an email would send the Global password as typed.
|
|
97
|
+
raise RemoteRFOpsError("usage", "Log in here with your username, not your email.")
|
|
98
|
+
# A lab this Global account has a login at gets a secret bound to it, not the password.
|
|
99
|
+
account = RemoteRFAccount(user, homes.lab_password(home, user, password), backend=backend, quiet=True)
|
|
100
|
+
if not account.login_user(remember=True):
|
|
101
|
+
raise RemoteRFOpsError("invalid_credentials")
|
|
102
|
+
if not account.session_token:
|
|
103
|
+
raise RemoteRFOpsError("deployment_error",
|
|
104
|
+
f"{home} issues no session token, so this login can't be saved; use `remoterf -l {home}`.")
|
|
105
|
+
homes.remember_login(home, account.username, account.session_token)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@ops._op
|
|
109
|
+
def _login(a):
|
|
110
|
+
from .config.config import _read_tos_notice, remember_tos_agreement, tos_agreed
|
|
111
|
+
from .deployment import homes
|
|
112
|
+
|
|
113
|
+
if a.accept_tos:
|
|
114
|
+
print(_read_tos_notice(), file=sys.stderr)
|
|
115
|
+
remember_tos_agreement()
|
|
116
|
+
elif not tos_agreed():
|
|
117
|
+
raise RemoteRFOpsError("usage", "Logging in needs your agreement to the RemoteRF Terms of Service: add --accept-tos.")
|
|
118
|
+
password = sys.stdin.readline().rstrip("\r\n")
|
|
119
|
+
if not password:
|
|
120
|
+
raise RemoteRFOpsError("usage", "No password on stdin.")
|
|
121
|
+
homes.migrate_legacy_target()
|
|
122
|
+
name, lab = homes.split_account(a.user)
|
|
123
|
+
home = (a.home or lab or _default_home()).lower()
|
|
124
|
+
user = name if lab == home else a.user # `jdoe@ucla` at ucla is `jdoe` there
|
|
125
|
+
if home == homes.GLOBAL_ACCOUNTS:
|
|
126
|
+
_login_global(user, password)
|
|
127
|
+
else:
|
|
128
|
+
_login_lab(home, user, password)
|
|
129
|
+
return ops.whoami(ops.saved_session())
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@ops._op
|
|
133
|
+
def _logout(a):
|
|
134
|
+
"""Revoke the saved session (best effort), then forget it locally."""
|
|
135
|
+
from .deployment.state import CredentialStore, load_target
|
|
136
|
+
|
|
137
|
+
target = load_target()
|
|
138
|
+
if target and target["transport"] == "https-json":
|
|
139
|
+
from .deployment.backend import selected_backend
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
backend = selected_backend()
|
|
143
|
+
try:
|
|
144
|
+
if backend.resume():
|
|
145
|
+
backend.logout()
|
|
146
|
+
finally:
|
|
147
|
+
backend.close()
|
|
148
|
+
except Exception: # noqa: BLE001 - revocation is best effort
|
|
149
|
+
pass
|
|
150
|
+
CredentialStore(target["origin"], target["deployment_id"]).clear()
|
|
151
|
+
return {"logged_out": True}
|
|
152
|
+
from .core.grpc_acc import RemoteRFAccount
|
|
153
|
+
from .core.grpc_client import _active_home_name
|
|
154
|
+
from .deployment import homes
|
|
155
|
+
|
|
156
|
+
home = _active_home_name()
|
|
157
|
+
saved = homes.recall_login(home) if home else None
|
|
158
|
+
if saved:
|
|
159
|
+
account = RemoteRFAccount(saved["username"], saved["secret"], quiet=True)
|
|
160
|
+
account.session_token = saved["secret"]
|
|
161
|
+
account.logout()
|
|
162
|
+
homes.forget_login(home)
|
|
163
|
+
return {"logged_out": True}
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@ops._op
|
|
167
|
+
def _apps(a):
|
|
168
|
+
s = ops.saved_session()
|
|
169
|
+
if s.home_kind != "global":
|
|
170
|
+
raise RemoteRFOpsError("invalid_argument", "Connected apps belong to a Global account (remoterf auth login --home global).")
|
|
171
|
+
backend = s.account.backend
|
|
172
|
+
try:
|
|
173
|
+
return backend.revoke_app(a.revoke) if a.revoke else backend.connected_apps()
|
|
174
|
+
except AccountBackendError as exc:
|
|
175
|
+
if exc.code != "not_found":
|
|
176
|
+
raise
|
|
177
|
+
raise RemoteRFOpsError("not_found", "Connected apps arrive with the RemoteRF Global connector, "
|
|
178
|
+
"which isn't available yet.") from None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# ── devices and reservations ─────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
def _reserve(a):
|
|
184
|
+
if not a.confirm:
|
|
185
|
+
raise RemoteRFOpsError("usage", "Dry run only. Re-run with --confirm to reserve this slot.")
|
|
186
|
+
return ops.reserve(ops.saved_session(), a.handle, a.start, a.end, reveal_token=a.reveal_token)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _cancel(a):
|
|
190
|
+
if not a.confirm:
|
|
191
|
+
raise RemoteRFOpsError("usage", "Dry run only. Re-run with --confirm to cancel this reservation.")
|
|
192
|
+
return ops.cancel(ops.saved_session(), a.reservation_id)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# ── doctor ───────────────────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
def _doctor(a):
|
|
198
|
+
"""What this install sees; never a secret, never a failure."""
|
|
199
|
+
import importlib.util
|
|
200
|
+
import platform
|
|
201
|
+
|
|
202
|
+
from .deployment import homes
|
|
203
|
+
from .deployment.state import CredentialStore, load_target, root
|
|
204
|
+
|
|
205
|
+
out = {"version": ops._version(), "python": platform.python_version(), "executable": sys.executable,
|
|
206
|
+
"config_dir": str(root().parent), "home": None, "home_kind": None, "endpoint": None,
|
|
207
|
+
"logged_in": False, "reachable": False, "mcp_installed": importlib.util.find_spec("mcp") is not None,
|
|
208
|
+
"problems": []}
|
|
209
|
+
try:
|
|
210
|
+
target = load_target()
|
|
211
|
+
if target and target["transport"] == "https-json":
|
|
212
|
+
store = CredentialStore(target["origin"], target["deployment_id"])
|
|
213
|
+
out.update(home=homes.GLOBAL_ACCOUNTS, home_kind="global", endpoint=target["origin"],
|
|
214
|
+
logged_in=(store.directory / "selected.json").exists())
|
|
215
|
+
else:
|
|
216
|
+
from .deployment.direct import resolve_active_profile
|
|
217
|
+
|
|
218
|
+
profile = resolve_active_profile()
|
|
219
|
+
if profile:
|
|
220
|
+
out.update(home=profile.home, home_kind="deployment", endpoint=profile.grpc_endpoint,
|
|
221
|
+
logged_in=bool(profile.home and homes.recall_login(profile.home)))
|
|
222
|
+
if out["endpoint"]:
|
|
223
|
+
from .remoterf_cli import _connected_server
|
|
224
|
+
|
|
225
|
+
out["reachable"] = _connected_server(3.0) is not None
|
|
226
|
+
except Exception as exc: # noqa: BLE001 - doctor reports, it never fails
|
|
227
|
+
out["problems"].append(explain(exc))
|
|
228
|
+
if not out["endpoint"]:
|
|
229
|
+
out["problems"].append("No home selected: run `remoterf auth login`.")
|
|
230
|
+
else:
|
|
231
|
+
if not out["logged_in"]:
|
|
232
|
+
out["problems"].append("No saved login: run `remoterf auth login`.")
|
|
233
|
+
if not out["reachable"]:
|
|
234
|
+
out["problems"].append(f"{out['endpoint']} did not answer.")
|
|
235
|
+
if not out["mcp_installed"]:
|
|
236
|
+
out["problems"].append('For `remoterf mcp`: pip install "remoterf[mcp]"')
|
|
237
|
+
return out
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# ── human output ─────────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
def _local(ts):
|
|
243
|
+
return datetime.fromtimestamp(ts).strftime("%a %Y-%m-%d %H:%M") if ts else "-"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _table(out, rows):
|
|
247
|
+
rows = [[_clean(c) for c in row] for row in rows]
|
|
248
|
+
widths = [max(len(r[i]) for r in rows) for i in range(len(rows[0]))] if rows else []
|
|
249
|
+
for row in rows:
|
|
250
|
+
print(" ".join(c.ljust(w) for c, w in zip(row, widths)).rstrip(), file=out)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _show_whoami(r, out):
|
|
254
|
+
print(_clean(f"{r['account']} ({r['home_kind']}); deployments: {', '.join(r['deployments']) or '-'}"), file=out)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _show_devices(r, out):
|
|
258
|
+
_table(out, [[d["handle"] or d["name"], d["state"] if d["online"] else "offline", d["type"] or "-", d["deployment"] or "-",
|
|
259
|
+
f"until {_local(d['until_ts'])}" if d["until_ts"] else ""] for d in r["devices"]])
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def _show_slots(r, out):
|
|
263
|
+
print(_clean(f"{r['handle']}: {r['slot_minutes']}-minute slots ({r['timezone']})"), file=sys.stderr)
|
|
264
|
+
_table(out, [[_local(s["start_ts"]), "-", _local(s["end_ts"])] for s in r["slots"]])
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _ref(x):
|
|
268
|
+
"""A Global reservation id '<deployment id>:10' as '10@ucla', which `remoterf cancel` takes."""
|
|
269
|
+
_, colon, num = str(x["reservation_id"]).rpartition(":")
|
|
270
|
+
return f"{num}@{x['handle'].rpartition('@')[2]}" if colon and x.get("handle") else x["reservation_id"]
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _show_reservations(r, out):
|
|
274
|
+
if not r["reservations"]:
|
|
275
|
+
print("No reservations.", file=out)
|
|
276
|
+
return
|
|
277
|
+
_table(out, [[_ref(x), x["handle"] or x["device"], _local(x["start_ts"]), "-", _local(x["end_ts"])]
|
|
278
|
+
for x in r["reservations"]])
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _show_reserved(r, out):
|
|
282
|
+
print(_clean(f"Reserved {r['handle']} {_local(r['start_ts'])} - {_local(r['end_ts'])} "
|
|
283
|
+
f"(reservation {_ref(r) or 'unknown'})."), file=out)
|
|
284
|
+
print(_clean(f'Use token="{r["use_as_token"]}"' + (f', or the raw token {r["token"]}' if r.get("token") else "")), file=out)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _show_apps(r, out):
|
|
288
|
+
if "apps" not in r:
|
|
289
|
+
print("Revoked.", file=out)
|
|
290
|
+
return
|
|
291
|
+
_table(out, [[x.get("client_id"), x.get("client_name"), " ".join(x.get("scopes") or ()), f"last used {x.get('last_used_at') or 'never'}"]
|
|
292
|
+
for x in r["apps"]] or [["No connected apps."]])
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _show_doctor(r, out):
|
|
296
|
+
for key, value in r.items():
|
|
297
|
+
if key != "problems":
|
|
298
|
+
print(_clean(f"{key}: {value}"), file=out)
|
|
299
|
+
for problem in r["problems"]:
|
|
300
|
+
print(_clean(f"! {problem}"), file=out)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# ── main ─────────────────────────────────────────────────────────────
|
|
304
|
+
|
|
305
|
+
def _parser():
|
|
306
|
+
common = argparse.ArgumentParser(add_help=False)
|
|
307
|
+
common.add_argument("--json", action="store_true", help="print one JSON object on stdout")
|
|
308
|
+
parser = _Parser(prog="remoterf", description="Scriptable RemoteRF commands (no prompts).")
|
|
309
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
310
|
+
|
|
311
|
+
def command(where, name, run, show, help):
|
|
312
|
+
p = where.add_parser(name, parents=[common], help=help, description=help)
|
|
313
|
+
p.set_defaults(run=run, show=show)
|
|
314
|
+
return p
|
|
315
|
+
|
|
316
|
+
auth = sub.add_parser("auth", help="the saved login").add_subparsers(dest="action", required=True)
|
|
317
|
+
p = command(auth, "login", _login, _show_whoami, "log in without prompts and keep the login saved")
|
|
318
|
+
p.add_argument("--home", help="global, or a deployment name (default: the account's deployment, else the last home used)")
|
|
319
|
+
p.add_argument("--user", required=True, help="NAME, or NAME@deployment")
|
|
320
|
+
p.add_argument("--password-stdin", action="store_true", required=True, help="read the password from stdin")
|
|
321
|
+
p.add_argument("--accept-tos", action="store_true", help="agree to the RemoteRF Terms of Service")
|
|
322
|
+
command(auth, "status", lambda a: ops.whoami(ops.saved_session()), _show_whoami, "show active login and deployments")
|
|
323
|
+
command(sub, "whoami", lambda a: ops.whoami(ops.saved_session()), _show_whoami, "same as `auth status`")
|
|
324
|
+
command(auth, "logout", _logout, lambda r, out: print("Logged out.", file=out), "end and forget the saved login")
|
|
325
|
+
p = command(auth, "apps", _apps, _show_apps, "apps connected through the RemoteRF Global connector (not available yet)")
|
|
326
|
+
p.add_argument("--revoke", metavar="CLIENT_ID", help="disconnect one app")
|
|
327
|
+
p = command(sub, "devices", lambda a: ops.list_devices(ops.saved_session(), deployment=a.deployment, online_only=not a.all),
|
|
328
|
+
_show_devices, "list devices and current availability")
|
|
329
|
+
p.add_argument("--deployment", help="only this deployment's devices")
|
|
330
|
+
p.add_argument("--all", action="store_true", help="include offline devices")
|
|
331
|
+
p = command(sub, "slots", lambda a: ops.find_slots(ops.saved_session(), a.handle, days=a.days),
|
|
332
|
+
_show_slots, "free slots on a device, aligned like resdev")
|
|
333
|
+
p.add_argument("handle", help="Name@deployment, for example Pluto_00@ucla")
|
|
334
|
+
p.add_argument("--days", type=int, default=1)
|
|
335
|
+
command(sub, "reservations", lambda a: ops.list_reservations(ops.saved_session()), _show_reservations, "your reservations")
|
|
336
|
+
p = command(sub, "reserve", _reserve, _show_reserved, "reserve a device (needs --confirm)")
|
|
337
|
+
p.add_argument("handle")
|
|
338
|
+
p.add_argument("--start", required=True, help="RFC 3339, for example 2026-10-15T14:00:00Z")
|
|
339
|
+
p.add_argument("--end", required=True)
|
|
340
|
+
p.add_argument("--confirm", action="store_true", help="confirm reservation")
|
|
341
|
+
p.add_argument("--reveal-token", action="store_true", help="include the raw reservation token")
|
|
342
|
+
p = command(sub, "cancel", _cancel, lambda r, out: print("Canceled.", file=out),
|
|
343
|
+
"cancel a reservation (needs --confirm)")
|
|
344
|
+
p.add_argument("reservation_id", help="as `remoterf reservations` shows it, or a JSON reservation_id")
|
|
345
|
+
p.add_argument("--confirm", action="store_true", help="confirm cancellation")
|
|
346
|
+
command(sub, "doctor", _doctor, _show_doctor, "check this install, its home and its login")
|
|
347
|
+
return parser
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def main(argv) -> int:
|
|
351
|
+
out = sys.stdout
|
|
352
|
+
as_json = "--json" in argv
|
|
353
|
+
try:
|
|
354
|
+
args = _parser().parse_args(argv)
|
|
355
|
+
as_json = args.json
|
|
356
|
+
with redirect_stdout(sys.stderr): # a stray library print must not reach stdout
|
|
357
|
+
result = args.run(args)
|
|
358
|
+
except RemoteRFOpsError as exc:
|
|
359
|
+
if as_json:
|
|
360
|
+
print(json.dumps(exc.to_dict()), file=out)
|
|
361
|
+
print(_clean(f"remoterf: {exc.message}"), file=sys.stderr)
|
|
362
|
+
return exc.exit_code
|
|
363
|
+
except KeyboardInterrupt:
|
|
364
|
+
print(file=sys.stderr)
|
|
365
|
+
return 130
|
|
366
|
+
except SystemExit as exc: # --help
|
|
367
|
+
return exc.code if isinstance(exc.code, int) else 0
|
|
368
|
+
if as_json:
|
|
369
|
+
print(json.dumps(result), file=out)
|
|
370
|
+
else:
|
|
371
|
+
for note in result.get("notes", ()):
|
|
372
|
+
print(_clean(note), file=sys.stderr)
|
|
373
|
+
args.show(result, out)
|
|
374
|
+
return 0
|