Files
HaloKeymind/host_cli_service/index.html
T
2026-09-06 06:02:19 +00:00

2033 lines
46 KiB
HTML

<!doctype html>
<html lang="en" class="no-js">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="description" content="Documentation for the open source MeshCore firmware">
<link rel="canonical" href="https://mikecarper.github.io/MeshCore/host_cli_service/">
<link rel="prev" href="../hardware_validation_checklist/">
<link rel="next" href="../kiss_modem_protocol/">
<link rel="icon" href="../assets/images/favicon.png">
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.7">
<title>LoRa CLI Host Service - MeshCore Docs</title>
<link rel="stylesheet" href="../assets/stylesheets/main.ec1eaa64.min.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback">
<style>:root{--md-text-font:"Roboto";--md-code-font:"Roboto Mono"}</style>
<link rel="stylesheet" href="../_stylesheets/extra.css">
<link rel="stylesheet" href="../_stylesheets/firmware_picker.css">
<link rel="stylesheet" href="../_stylesheets/telemetry_decoder.css">
<link rel="stylesheet" href="../_stylesheets/filter_tool.css">
<script>__md_scope=new URL("..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
</head>
<body dir="ltr">
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
<label class="md-overlay" for="__drawer"></label>
<div data-md-component="skip">
<a href="#lora-cli-host-service" class="md-skip">
Skip to content
</a>
</div>
<div data-md-component="announce">
</div>
<header class="md-header md-header--shadow" data-md-component="header">
<nav class="md-header__inner md-grid" aria-label="Header">
<a href=".." title="MeshCore Docs" class="md-header__button md-logo" aria-label="MeshCore Docs" data-md-component="logo">
<img src="../_assets/meshcore.svg" alt="logo">
</a>
<label class="md-header__button md-icon" for="__drawer">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
</label>
<div class="md-header__title" data-md-component="header-title">
<div class="md-header__ellipsis">
<div class="md-header__topic">
<span class="md-ellipsis">
MeshCore Docs
</span>
</div>
<div class="md-header__topic" data-md-component="header-topic">
<span class="md-ellipsis">
LoRa CLI Host Service
</span>
</div>
</div>
</div>
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
<label class="md-header__button md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
</label>
<div class="md-search" data-md-component="search" role="dialog">
<label class="md-search__overlay" for="__search"></label>
<div class="md-search__inner" role="search">
<form class="md-search__form" name="search">
<input type="text" class="md-search__input" name="query" aria-label="Search" placeholder="Search" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
<label class="md-search__icon md-icon" for="__search">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
</label>
<nav class="md-search__options" aria-label="Search">
<button type="reset" class="md-search__icon md-icon" title="Clear" aria-label="Clear" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</button>
</nav>
<div class="md-search__suggest" data-md-component="search-suggest"></div>
</form>
<div class="md-search__output">
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
<div class="md-search-result" data-md-component="search-result">
<div class="md-search-result__meta">
Initializing search
</div>
<ol class="md-search-result__list" role="presentation"></ol>
</div>
</div>
</div>
</div>
</div>
<div class="md-header__source">
<a href="https://github.com/mikecarper/MeshCore/" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
mikecarper/MeshCore
</div>
</a>
</div>
</nav>
</header>
<div class="md-container" data-md-component="container">
<main class="md-main" data-md-component="main">
<div class="md-main__inner md-grid">
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--primary" aria-label="Navigation" data-md-level="0">
<label class="md-nav__title" for="__drawer">
<a href=".." title="MeshCore Docs" class="md-nav__button md-logo" aria-label="MeshCore Docs" data-md-component="logo">
<img src="../_assets/meshcore.svg" alt="logo">
</a>
MeshCore Docs
</label>
<div class="md-nav__source">
<a href="https://github.com/mikecarper/MeshCore/" title="Go to repository" class="md-source" data-md-component="source">
<div class="md-source__icon md-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><!--! Font Awesome Free 7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2025 Fonticons, Inc.--><path d="M439.6 236.1 244 40.5c-5.4-5.5-12.8-8.5-20.4-8.5s-15 3-20.4 8.4L162.5 81l51.5 51.5c27.1-9.1 52.7 16.8 43.4 43.7l49.7 49.7c34.2-11.8 61.2 31 35.5 56.7-26.5 26.5-70.2-2.9-56-37.3L240.3 199v121.9c25.3 12.5 22.3 41.8 9.1 55-6.4 6.4-15.2 10.1-24.3 10.1s-17.8-3.6-24.3-10.1c-17.6-17.6-11.1-46.9 11.2-56v-123c-20.8-8.5-24.6-30.7-18.6-45L142.6 101 8.5 235.1C3 240.6 0 247.9 0 255.5s3 15 8.5 20.4l195.6 195.7c5.4 5.4 12.7 8.4 20.4 8.4s15-3 20.4-8.4l194.7-194.7c5.4-5.4 8.4-12.8 8.4-20.4s-3-15-8.4-20.4"/></svg>
</div>
<div class="md-source__repository">
mikecarper/MeshCore
</div>
</a>
</div>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href=".." class="md-nav__link">
<span class="md-ellipsis">
Introduction
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../WiFi/" class="md-nav__link">
<span class="md-ellipsis">
WiFi and MQTT by Firmware Type
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../cli_build_matrix/" class="md-nav__link">
<span class="md-ellipsis">
CLI Availability by Firmware Build
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../cli_command_availability/" class="md-nav__link">
<span class="md-ellipsis">
CLI Command Availability Matrix
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../cli_commands/" class="md-nav__link">
<span class="md-ellipsis">
CLI Commands
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../companion_offline_queue/" class="md-nav__link">
<span class="md-ellipsis">
Companion Offline Message Queue
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../companion_protocol/" class="md-nav__link">
<span class="md-ellipsis">
Companion Protocol
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../companion_radio_full/" class="md-nav__link">
<span class="md-ellipsis">
Full Companion
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../docs/" class="md-nav__link">
<span class="md-ellipsis">
Local Documentation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../esp32_memory_budget/" class="md-nav__link">
<span class="md-ellipsis">
Classic ESP32 image memory budget
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../faq/" class="md-nav__link">
<span class="md-ellipsis">
Frequently Asked Questions
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../filter_tool/" class="md-nav__link">
<span class="md-ellipsis">
Filter policy playground
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../firmware_picker/" class="md-nav__link">
<span class="md-ellipsis">
Firmware picker
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../flood_filtering/" class="md-nav__link">
<span class="md-ellipsis">
Flood Filtering and Moderation
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../full_companion_features/" class="md-nav__link">
<span class="md-ellipsis">
Full Companion: turn features on and off
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../full_companion_usb_switcher/" class="md-nav__link">
<span class="md-ellipsis">
Full Companion USB CLI and binary switcher
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../gps_tracking/" class="md-nav__link">
<span class="md-ellipsis">
GPS Tracking
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../halo_keymind_settings/" class="md-nav__link">
<span class="md-ellipsis">
Halo and Keymind Branch Settings
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../hardware_validation_checklist/" class="md-nav__link">
<span class="md-ellipsis">
Hardware validation checklist
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--active">
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
<label class="md-nav__link md-nav__link--active" for="__toc">
<span class="md-ellipsis">
LoRa CLI Host Service
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<a href="./" class="md-nav__link md-nav__link--active">
<span class="md-ellipsis">
LoRa CLI Host Service
</span>
</a>
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#run-alongside-meshcoretomqtt" class="md-nav__link">
<span class="md-ellipsis">
Run alongside meshcoretomqtt
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#allowlisted-programs-and-arguments" class="md-nav__link">
<span class="md-ellipsis">
Allowlisted programs and arguments
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#opt-in-network-and-reboot-recovery" class="md-nav__link">
<span class="md-ellipsis">
Opt-in network and reboot recovery
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#opt-in-clock-recovery" class="md-nav__link">
<span class="md-ellipsis">
Opt-in clock recovery
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#trust-and-injection-controls" class="md-nav__link">
<span class="md-ellipsis">
Trust and injection controls
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#limits" class="md-nav__link">
<span class="md-ellipsis">
Limits
</span>
</a>
</li>
</ul>
</nav>
</li>
<li class="md-nav__item">
<a href="../kiss_modem_protocol/" class="md-nav__link">
<span class="md-ellipsis">
MeshCore KISS Modem Protocol
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../lora_ota_automation/" class="md-nav__link">
<span class="md-ellipsis">
Scripted LoRa OTA from start to finish
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../mbedtls-tls-footprint/" class="md-nav__link">
<span class="md-ellipsis">
Shrinking the per-connection TLS footprint on non-PSRAM observers
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../nrf52_companion_storage_recovery/" class="md-nav__link">
<span class="md-ellipsis">
nRF52 Companion automatic ExtraFS recovery
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../nrf52_power_management/" class="md-nav__link">
<span class="md-ellipsis">
nRF52 Power Management
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../number_allocations/" class="md-nav__link">
<span class="md-ellipsis">
Number Allocations
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../ota_easy/" class="md-nav__link">
<span class="md-ellipsis">
Easy firmware updates over LoRa
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../ota_meshtower_v2_sdcard/" class="md-nav__link">
<span class="md-ellipsis">
MeshTower V2 microSD LoRa OTA
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../ota_nrf52_bootloader_update/" class="md-nav__link">
<span class="md-ellipsis">
nRF52 bootloader updates over LoRa
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../ota_nrf52_qspi/" class="md-nav__link">
<span class="md-ellipsis">
nRF52 repeater LoRa OTA with external QSPI
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../ota_protocol/" class="md-nav__link">
<span class="md-ellipsis">
MeshCore OTA - .mota container &amp; LoRa protocol
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../ota_user_guide/" class="md-nav__link">
<span class="md-ellipsis">
Updating your node over the air (OTA) - user guide
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../packet_format/" class="md-nav__link">
<span class="md-ellipsis">
Packet Format
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../payloads/" class="md-nav__link">
<span class="md-ellipsis">
Payload Format
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../qr_codes/" class="md-nav__link">
<span class="md-ellipsis">
QR Codes
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../rak3401_mota_chain/" class="md-nav__link">
<span class="md-ellipsis">
RAK3401 1W repeater compact LoRa update chain
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../role_feature_switches/" class="md-nav__link">
<span class="md-ellipsis">
Feature switches by role — 1.17.1.5 USA Cascade
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../spiffs_regular_file_reads/" class="md-nav__link">
<span class="md-ellipsis">
SPIFFS regular-file reads and login replay state
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../stats_binary_frames/" class="md-nav__link">
<span class="md-ellipsis">
Stats Binary Frame Structures
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../telemetry_decoder/" class="md-nav__link">
<span class="md-ellipsis">
Telemetry decoder
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../terminal_chat_cli/" class="md-nav__link">
<span class="md-ellipsis">
Terminal Chat CLI
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../usb_serial_backpressure/" class="md-nav__link">
<span class="md-ellipsis">
Native USB backpressure and radio liveness
</span>
</a>
</li>
<li class="md-nav__item md-nav__item--nested">
<input class="md-nav__toggle md-toggle " type="checkbox" id="__nav_43" >
<label class="md-nav__link" for="__nav_43" id="__nav_43_label" tabindex="0">
<span class="md-ellipsis">
Releases
</span>
<span class="md-nav__icon md-icon"></span>
</label>
<nav class="md-nav" data-md-level="1" aria-labelledby="__nav_43_label" aria-expanded="false">
<label class="md-nav__title" for="__nav_43">
<span class="md-nav__icon md-icon"></span>
Releases
</label>
<ul class="md-nav__list" data-md-scrollfix>
<li class="md-nav__item">
<a href="../releases/1.17.1.5-doc-audit/" class="md-nav__link">
<span class="md-ellipsis">
Markdown audit — 1.17.1.5 USA Cascade
</span>
</a>
</li>
<li class="md-nav__item">
<a href="../releases/1.17.1.5/" class="md-nav__link">
<span class="md-ellipsis">
MeshCore 1.17.1.5 Dev — USA Cascade
</span>
</a>
</li>
</ul>
</nav>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
<div class="md-sidebar__scrollwrap">
<div class="md-sidebar__inner">
<nav class="md-nav md-nav--secondary" aria-label="Table of contents">
<label class="md-nav__title" for="__toc">
<span class="md-nav__icon md-icon"></span>
Table of contents
</label>
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
<li class="md-nav__item">
<a href="#run-alongside-meshcoretomqtt" class="md-nav__link">
<span class="md-ellipsis">
Run alongside meshcoretomqtt
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#allowlisted-programs-and-arguments" class="md-nav__link">
<span class="md-ellipsis">
Allowlisted programs and arguments
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#opt-in-network-and-reboot-recovery" class="md-nav__link">
<span class="md-ellipsis">
Opt-in network and reboot recovery
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#opt-in-clock-recovery" class="md-nav__link">
<span class="md-ellipsis">
Opt-in clock recovery
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#trust-and-injection-controls" class="md-nav__link">
<span class="md-ellipsis">
Trust and injection controls
</span>
</a>
</li>
<li class="md-nav__item">
<a href="#limits" class="md-nav__link">
<span class="md-ellipsis">
Limits
</span>
</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="md-content" data-md-component="content">
<article class="md-content__inner md-typeset">
<a href="https://github.com/mikecarper/MeshCore/edit/keymindCascade/docs/host_cli_service.md" title="Edit this page" class="md-content__button md-icon" rel="edit">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 20H6V4h7v5h5v3.1l2-2V8l-6-6H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h4zm10.2-7c.1 0 .3.1.4.2l1.3 1.3c.2.2.2.6 0 .8l-1 1-2.1-2.1 1-1c.1-.1.2-.2.4-.2m0 3.9L14.1 23H12v-2.1l6.1-6.1z"/></svg>
</a>
<h1 id="lora-cli-host-service">LoRa CLI Host Service</h1>
<p>The repeater <code>host &lt;text&gt;</code> command lets an authenticated LoRa administrator
request a small, explicitly allowed operation from a USB-connected computer.
The included Raspberry Pi endpoint supports these exact requests:</p>
<pre><code class="language-text">cmd host help
cmd host cpu-temp
cmd host hostname
cmd host uptime
cmd host load
cmd host memory
cmd host disk-free
cmd host clock status
cmd host clock sync
cmd host clock set &lt;unix_epoch&gt;
cmd host network restart
cmd host reboot
cmd host action status &lt;operation_id&gt;
cmd host run &lt;alias&gt; [arguments]
</code></pre>
<p>The first seven actions after <code>help</code> are read-only. Clock changes, network
restart, and reboot are opt-in actions and are disabled unless explicitly
enabled.
Text such as <code>reboot now</code>, <code>cpu-temp; reboot</code>, and embedded newlines is not a
command: the endpoint accepts only an exact allowlist match.</p>
<p>The bridge is included in normal repeater firmware except on Wio-E5. Its
specialized RS232 bridge image omits the host service because the combined
application exceeds the fixed 240 KiB partition; use the normal Wio-E5
repeater image when the USB/MQTT host service is needed.</p>
<h2 id="run-alongside-meshcoretomqtt">Run alongside meshcoretomqtt</h2>
<p><code>meshcoretomqtt</code> must remain the only process that opens the repeater USB serial
port. The endpoint communicates with it through the same MQTT broker, which can
be local to the Raspberry Pi:</p>
<pre><code class="language-text">LoRa -&gt; repeater -&gt; USB -&gt; meshcoretomqtt -&gt; MQTT -&gt; host endpoint
LoRa &lt;- repeater &lt;- USB &lt;- meshcoretomqtt &lt;- MQTT &lt;- host endpoint
</code></pre>
<p>Clock-recovery deployments must run the loopback broker, <code>meshcoretomqtt</code>, and
endpoint on the same Pi. A wrong Pi clock can prevent TLS validation against a
remote broker, while split host clocks can reject the signed live claim before
the correction arrives. Remote brokers remain suitable for ordinary commands
when the connection and both clocks are already healthy.</p>
<p>Launch <code>meshcoretomqtt</code> with its existing arguments plus <code>--debug</code>; its current
debug-topic parser needs that flag to publish the repeater request record. Do
not open the serial TTY from the host endpoint too.</p>
<p>This feature does not compare the repeater wall clock with the Pi clock.
Minutes of drift, an unset repeater clock, and later clock corrections are safe:
request freshness comes from a live one-time challenge. The normal
<code>meshcoretomqtt</code> <code>sync_time</code> setting may remain enabled for its other uses, but
host-command authorization does not depend on it.</p>
<p>Generate a dedicated service key using the Python environment installed by
<code>meshcoretomqtt</code>:</p>
<pre><code class="language-sh">sudo /opt/mctomqtt/venv/bin/python3 host_cli_service.py \
--generate-key /etc/mctomqtt/host-cli-key.json
sudo chown mctomqtt:mctomqtt /etc/mctomqtt/host-cli-key.json
sudo chmod 600 /etc/mctomqtt/host-cli-key.json
</code></pre>
<p>Add the printed public key to the existing <code>meshcoretomqtt</code> configuration:</p>
<pre><code class="language-toml">[remote_serial]
enabled = true
allowed_companions = [
&quot;SERVICE_PUBLIC_KEY_PRINTED_ABOVE&quot;
]
nonce_ttl = 120
command_timeout = 10
</code></pre>
<p>Restart <code>meshcoretomqtt</code>, then run the endpoint. Replace <code>USA</code> with the exact
three-character IATA namespace configured in the broker and supply the
repeater's complete 64-character public key:</p>
<pre><code class="language-sh">sudo -u mctomqtt /opt/mctomqtt/venv/bin/python3 host_cli_service.py \
--broker 127.0.0.1 \
--iata USA \
--repeater-key REPEATER_PUBLIC_KEY \
--service-key /etc/mctomqtt/host-cli-key.json
</code></pre>
<p>Add <code>--username NAME --password-file FILE</code> for broker authentication. Add
<code>--tls</code>, and optionally <code>--ca-cert FILE</code>, for TLS. Custom installations can use
<code>--request-topic</code> and <code>--command-topic</code> to match their broker namespace. The
endpoint source and its systemd/configuration details are in
<a href="https://github.com/mikecarper/MeshCore/tree/keymindCascade/examples/host_cli_service"><code>examples/host_cli_service</code></a>.</p>
<h2 id="allowlisted-programs-and-arguments">Allowlisted programs and arguments</h2>
<p>Add <code>--programs-file FILE</code> to expose locally selected programs as
<code>host run &lt;alias&gt; [arguments]</code>. The example JSON file fixes each executable,
fixed leading arguments, maximum 1-5 second runtime, and the exact remote
argument schema. For example:</p>
<pre><code class="language-text">cmd host run fan on 15
</code></pre>
<p>The example maps that request to the fixed process argument vector:</p>
<pre><code class="language-text">/usr/local/bin/mesh-fan-control --source lora on 15
</code></pre>
<p>The supported argument rules are an explicit non-option <code>choice</code>, a bounded
nonnegative <code>integer</code>, or a short <code>token</code> using a restricted ASCII character
set. Alias lookup and every argument validation happen before process creation.
Execution uses an absolute configured path, no stdin, <code>shell=False</code>, a minimal
environment, and <code>/</code> as its working directory. Extra arguments, leading-option
injection, shell syntax, control characters, and invalid quoting are rejected.
The allowlist file and executable must not be group- or world-writable.</p>
<p>Copy and edit
<a href="https://github.com/mikecarper/MeshCore/blob/keymindCascade/examples/host_cli_service/programs.example.json"><code>programs.example.json</code></a>,
then give the service account only the operating-system permissions needed by
those trusted programs.</p>
<h2 id="opt-in-network-and-reboot-recovery">Opt-in network and reboot recovery</h2>
<p>These actions use their own socket-activated root broker, independently of the
clock-control broker. Installing or enabling clock control does not grant host
recovery actions. Install the following as root-owned files:</p>
<pre><code class="language-sh">sudo install -o root -g root -m 0755 meshcore_host_actions.py \
/usr/local/sbin/meshcore-host-actions
sudo install -o root -g root -m 0644 meshcore-host-actions.socket \
/etc/systemd/system/meshcore-host-actions.socket
sudo install -o root -g root -m 0644 meshcore-host-actions.service \
/etc/systemd/system/meshcore-host-actions.service
sudo install -o root -g root -m 0644 meshcore-networkmanager-restart.service \
/etc/systemd/system/meshcore-networkmanager-restart.service
sudo install -o root -g root -m 0644 meshcore-host-reboot.timer \
/etc/systemd/system/meshcore-host-reboot.timer
sudo install -o root -g root -m 0644 meshcore-host-reboot.service \
/etc/systemd/system/meshcore-host-reboot.service
</code></pre>
<p>The root-owned broker policy enables nothing by default. Create a systemd
drop-in and select <code>network-restart</code>, <code>reboot</code>, or both as an exact
comma-separated list:</p>
<pre><code class="language-sh">sudo systemctl edit meshcore-host-actions.service
</code></pre>
<pre><code class="language-ini">[Service]
Environment=MESHCORE_HOST_ACTIONS=network-restart,reboot
</code></pre>
<p>Reload systemd and enable only the broker socket:</p>
<pre><code class="language-sh">sudo systemctl daemon-reload
sudo systemctl enable --now meshcore-host-actions.socket
</code></pre>
<p>After changing the policy on an already active installation, restart
<code>meshcore-host-actions.service</code> so the broker reads the new value.</p>
<p>Finally add <code>--allow-network-restart</code>, <code>--allow-reboot</code>, or both to the
unprivileged endpoint. An action must pass both gates: the endpoint flag and
the root-owned broker policy. The endpoint never invokes <code>sudo</code>; remove legacy
<code>wifi-restart</code> program aliases and host-action sudoers entries because sudo
aliases are not a fallback for this hardened service. Use the exact
<code>host network restart</code> action instead.</p>
<p>Systemd creates <code>/run/meshcore-host-actions.sock</code> as <code>root:mctomqtt</code> mode
<code>0660</code>. The root broker authenticates both the endpoint UID and primary GID
with <code>SO_PEERCRED</code>, while the endpoint authenticates the root-created listener.
The protocol is one bounded ASCII line. Neither the caller nor request text can
choose an executable, unit, path, or argument. The broker can start only the
fixed static NetworkManager restart service or the fixed reboot timer; the
broker and action units have empty capability sets and systemd sandboxing.
The configured <code>mctomqtt</code> UID and its effective primary GID are the delegated
local trust boundary; supplementary group membership alone is rejected.</p>
<p>Side effects follow a fail-closed two-phase sequence. The endpoint reserves a
canonical 128-bit operation ID with <code>PREPARE</code>, publishes its reply with MQTT
QoS 1, and requires <code>wait_for_publish</code> plus <code>is_published</code> confirmation before
sending one <code>COMMIT</code>. It does not block the Paho network callback while waiting.
No confirmation means no commit. Reboot uses a fixed approximately 10-second
timer that starts only after commit. The MQTT confirmation proves acceptance
by the local broker, not delivery over USB and LoRa; there is no correlated
serial delivery acknowledgement, so the physical reply is best effort.</p>
<p><code>network restart</code> deliberately drops Wi-Fi and Tailscale management while
NetworkManager restarts. Use a loopback MQTT broker and keep the broker,
<code>meshcoretomqtt</code>, host endpoint, and USB device services independent of
NetworkManager: they must not have <code>Requires=</code>, <code>BindsTo=</code>, or ordering
dependencies on it. This preserves the local reply/commit path while remote
management temporarily disappears.</p>
<p>The operation ID is derived from the authenticated repeater key, request ID,
and nonce. The reply displays it, and its state can be queried later:</p>
<pre><code class="language-text">cmd host action status 0123456789ABCDEF0123456789ABCDEF
</code></pre>
<p>The root broker retains at most 64 records for the current boot. It persists
<code>prepared</code> before replying and <code>in-progress</code> before scheduling, schedules a
committed operation at most once, and never automatically retries an ambiguous
outcome. A broker restart converts recovered <code>in-progress</code> to <code>ambiguous</code>.
Only an old uncommitted <code>prepared</code> record may be evicted; otherwise a full
store rejects new work. Reusing an operation ID for a different action is
rejected. <code>scheduled</code> means PID 1 accepted the fixed unit job; it does not claim
that the subsequent network restart or reboot completed successfully.</p>
<p>A distinct reboot is rejected while another reboot is committed or ambiguous,
so a second request cannot falsely promise a fresh 10-second timer while the
first timer is already running. A new reboot reservation may supersede only an
older, uncommitted reboot reservation; the superseded operation can no longer
be committed.</p>
<h2 id="opt-in-clock-recovery">Opt-in clock recovery</h2>
<p><code>clock status</code> reports the Pi epoch and NTP synchronization state without root
access. To enable the exact <code>clock sync</code> and <code>clock set &lt;unix_epoch&gt;</code> actions,
install
<a href="https://github.com/mikecarper/MeshCore/blob/keymindCascade/examples/host_cli_service/meshcore_clock_control.py"><code>meshcore_clock_control.py</code></a>
as <code>/usr/local/sbin/meshcore-clock-control</code>, owned by root and mode <code>0755</code>,
then install the accompanying
<a href="https://github.com/mikecarper/MeshCore/blob/keymindCascade/examples/host_cli_service/meshcore-clock-control.socket"><code>meshcore-clock-control.socket</code></a>
and
<a href="https://github.com/mikecarper/MeshCore/blob/keymindCascade/examples/host_cli_service/meshcore-clock-control.service"><code>meshcore-clock-control.service</code></a>
and static
<a href="https://github.com/mikecarper/MeshCore/blob/keymindCascade/examples/host_cli_service/meshcore-chrony-step.service"><code>meshcore-chrony-step.service</code></a>
files in <code>/etc/systemd/system</code> as root-owned mode-<code>0644</code> files:</p>
<pre><code class="language-sh">sudo install -o root -g root -m 0755 meshcore_clock_control.py \
/usr/local/sbin/meshcore-clock-control
sudo install -o root -g root -m 0644 meshcore-clock-control.socket \
/etc/systemd/system/meshcore-clock-control.socket
sudo install -o root -g root -m 0644 meshcore-clock-control.service \
/etc/systemd/system/meshcore-clock-control.service
sudo install -o root -g root -m 0644 meshcore-chrony-step.service \
/etc/systemd/system/meshcore-chrony-step.service
sudo systemctl daemon-reload
sudo systemctl enable --now meshcore-clock-control.socket
</code></pre>
<p>The socket is fixed at <code>/run/meshcore-clock-control.sock</code>; systemd creates it
as <code>root:mctomqtt</code> with mode <code>0660</code>. Run the endpoint as user and primary group
<code>mctomqtt</code>, then add <code>--allow-clock-control</code>. The endpoint validates the socket
metadata and authenticates the connected server as root with Linux
<code>SO_PEERCRED</code>. The root service independently requires the peer's UID and
primary GID to match <code>mctomqtt:mctomqtt</code> before it reads or executes a request.
No clock-control sudo rule is used; remove old clock-helper sudoers lines when
upgrading.</p>
<p>Both layers require one canonical unsigned decimal argument with no sign,
leading zero, whitespace, or trailing text. The accepted epoch range is 2020
through 2099. The private protocol accepts only the complete bounded ASCII
lines <code>sync</code> and <code>set &lt;epoch&gt;</code>. Every child process uses a fixed absolute argv,
<code>shell=False</code>, and a timeout no longer than 1.5 seconds.</p>
<p><code>clock set</code> changes <code>CLOCK_REALTIME</code> without disabling NTP, then requests an NTP
step. If the NTP request fails, the response honestly reports that the clock
changed and which synchronization step remains incomplete. If <code>clock sync</code>
enables NTP but the explicit step/restart fails, that partial outcome is also
reported. When chrony is available, the root broker starts only the fixed
static <code>meshcore-chrony-step.service</code>. That hardened one-shot runs
<code>/usr/bin/chronyc -a makestep</code> as <code>_chrony:_chrony</code>, with no capabilities,
<code>AF_UNIX</code> only, and a one-second start timeout; it can therefore reach chrony's
private runtime socket without broadening the root broker. The <code>_chrony</code>
account comes from the Debian/Raspberry Pi chrony package. Without chrony, the
broker restarts systemd-timesyncd. Clock drift cannot authorize a captured command:
the signed request still needs the repeater's live one-time claim, whose
deadline is monotonic rather than wall-clock based.</p>
<h2 id="trust-and-injection-controls">Trust and injection controls</h2>
<p>The MQTT broker transports records but does not establish their authenticity.
The endpoint checks the configured repeater identity and verifies its Ed25519
signature over the complete request ID, random nonce, and Base64URL request. It
also validates framing, UTF-8, and byte limits. It does not execute that first
record.</p>
<p>Instead, the endpoint stores the request in memory, creates a random 64-bit
challenge, and sends <code>@claim=&lt;random&gt;</code> through <code>meshcoretomqtt</code>'s signed serial
channel. The repeater accepts it only from physical USB while the exact ID and
nonce are pending, then signs a <code>CLAIMED</code> proof containing the challenge. Only
a matching live proof lets the endpoint perform allowlist matching and execute
the action. It consumes the proof before execution, so MQTT redelivery cannot
execute an action twice.</p>
<p>A captured request only causes a new challenge that an idle repeater refuses.
A captured proof does not match a new challenge, and restarting the endpoint
forgets pending challenges. Repeater/Pi clock drift therefore cannot turn an
old <code>reboot</code> or <code>run</code> record into a valid action. The reply and claim commands
still use short-lived JWTs; <code>meshcoretomqtt</code> checks their signer allowlist,
target, expiration, signature, and separate replay nonce. Wall-clock expiry and
its in-memory nonce cache are not sufficient across a backward clock jump plus
restart; the firmware's live one-time claim is what protects these host
actions. Keep recovery-mode <code>allowed_companions</code> limited to the dedicated
service key, with the endpoint and <code>meshcoretomqtt</code> on the same Pi.</p>
<p>Newlines and other reply control characters are converted to spaces and are
also independently rejected by the firmware parser.</p>
<p>Protect the service private key and use broker ACLs that allow it to subscribe
only to the selected repeater debug topic and publish only to that repeater
serial-command topic. A private key listed in <code>allowed_companions</code> is trusted to
sign serial requests.</p>
<h2 id="limits">Limits</h2>
<ul>
<li>The request is at most 155 UTF-8 bytes after <code>host</code>, or 152 bytes with the
legacy three-byte companion correlation prefix.</li>
<li>The complete LoRa reply is at most 162 UTF-8 bytes.</li>
<li>Only one host request can be pending per repeater.</li>
<li>Only one privileged action or action-status query can be active; overlapping
work is rejected before reservation.</li>
<li>The live claim must complete within 4 seconds. Allowlisted programs have a
configured 1-5 second child-process timeout. Privileged actions and status
queries share one 5-second broker/reply-confirmation deadline.</li>
<li><code>host.reply</code> is accepted only through physical USB, not LoRa or Ethernet.</li>
</ul>
<p>Use <code>cmd get host</code> to report the bridge state and limits.</p>
</article>
</div>
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
</div>
</main>
<footer class="md-footer">
<div class="md-footer-meta md-typeset">
<div class="md-footer-meta__inner md-grid">
<div class="md-copyright">
Made with
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
Material for MkDocs
</a>
</div>
</div>
</div>
</footer>
</div>
<div class="md-dialog" data-md-component="dialog">
<div class="md-dialog__inner md-typeset"></div>
</div>
<script id="__config" type="application/json">{"annotate": null, "base": "..", "features": ["content.action.edit", "content.code.copy", "search.highlight", "search.suggest"], "search": "../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "Copied to clipboard", "clipboard.copy": "Copy to clipboard", "search.result.more.one": "1 more on this page", "search.result.more.other": "# more on this page", "search.result.none": "No matching documents", "search.result.one": "1 matching document", "search.result.other": "# matching documents", "search.result.placeholder": "Type to start searching", "search.result.term.missing": "Missing", "select.version": "Select version"}, "version": null}</script>
<script src="../assets/javascripts/bundle.d7400e89.min.js"></script>
<script src="../_javascript/firmware_picker.js"></script>
<script src="../_javascript/telemetry_decoder.js"></script>
<script src="../_javascript/filter_tool.js"></script>
</body>
</html>