7 Commits

Author SHA1 Message Date
eric 2a7b71fc78 Merge pull request 'fix: accept PKI direct messages without channel' (#4) from codex/accept-pki-direct-messages into main
release / contract-test (push) Successful in 14s
release / test-and-release (push) Successful in 13s
2026-09-08 05:00:03 +00:00
eric facf1742ef fix: accept PKI direct messages without channel 2026-09-07 21:59:18 -07:00
eric d7aa18682b Merge pull request 'release v0.1.3: bump version, fix publish auth across http->https redirect' (#3) from v0.1.3-final into main
release / test-and-release (push) Successful in 12s
release / contract-test (push) Successful in 17s
2026-09-08 04:01:44 +00:00
eric b8e9b65995 fix(ci): forward auth token across http->https redirect on release publish
curl drops the Authorization header when -L follows Gitea's 308 redirect from
http server_url to https, so the API replied 'token is required' and publish
failed with no release id. Add --location-trusted to every publish curl.
2026-09-07 21:01:34 -07:00
eric 29b048e066 chore(plugin): release v0.1.3 2026-09-07 21:01:21 -07:00
eric 932171c34d Merge pull request 'fix(ci): follow Gitea http->https redirect when publishing releases' (#1) from ci-publish-fix into main
release / contract-test (push) Successful in 45s
release / test-and-release (push) Failing after 12s
2026-09-08 03:56:33 +00:00
eric cdc67bc263 fix(ci): follow Gitea http->https redirect when publishing releases
server_url resolves to http and Gitea answers the create-release POST with a
308 redirect; curl without -L returned an empty body and the grep-based id
extraction failed under set -e/pipefail, failing v0.1.3 publish. Add -L,
reuse an existing release on retry, and parse with python3.
2026-09-07 20:56:20 -07:00
4 changed files with 75 additions and 13 deletions
+21 -5
View File
@@ -81,21 +81,37 @@ jobs:
echo "Publishing release for $tag_name" echo "Publishing release for $tag_name"
wheel_file=$(ls plugin/dist/*.whl | head -n 1) wheel_file=$(ls plugin/dist/*.whl | head -n 1)
tar_file=$(ls plugin/dist/*.tar.gz | head -n 1) tar_file=$(ls plugin/dist/*.tar.gz | head -n 1)
api_url="${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
# Create release # server_url is http and Gitea answers with a 308 redirect to https.
response=$(curl -s -k -X POST "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases" \ # curl needs -L to follow, AND --location-trusted: without it curl
# drops the Authorization header on the scheme-changing redirect and
# Gitea replies "token is required". Release may already exist
# (retried run) — reuse it instead of failing.
fetch_id() { python3 -c "import json,sys;print(json.load(sys.stdin).get('id') or '')" 2>/dev/null; }
response=$(curl -s -k -L --location-trusted -H "Authorization: token $GITEA_TOKEN" \
"$api_url/releases/tags/$tag_name")
release_id=$(printf '%s' "$response" | fetch_id)
if [ -z "$release_id" ]; then
response=$(curl -s -k -L --location-trusted -X POST "$api_url/releases" \
-H "Authorization: token $GITEA_TOKEN" \ -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{\"tag_name\": \"$tag_name\", \"name\": \"$tag_name\", \"body\": \"Release $tag_name for hermes-meshtastic plugin\"}") -d "{\"tag_name\": \"$tag_name\", \"name\": \"$tag_name\", \"body\": \"Release $tag_name for hermes-meshtastic plugin\"}")
release_id=$(printf '%s' "$response" | fetch_id)
fi
release_id=$(echo "$response" | grep -o '"id":[0-9]*' | head -n 1 | cut -d: -f2)
if [ -n "$release_id" ]; then if [ -n "$release_id" ]; then
curl -s -k -X POST "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/$release_id/assets?name=$(basename $wheel_file)" \ curl -s -k -L --location-trusted -X POST "$api_url/releases/$release_id/assets?name=$(basename "$wheel_file")" \
-H "Authorization: token $GITEA_TOKEN" \ -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \ -H "Content-Type: application/octet-stream" \
--data-binary "@$wheel_file" --data-binary "@$wheel_file"
curl -s -k -X POST "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/$release_id/assets?name=$(basename $tar_file)" \ curl -s -k -L --location-trusted -X POST "$api_url/releases/$release_id/assets?name=$(basename "$tar_file")" \
-H "Authorization: token $GITEA_TOKEN" \ -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/octet-stream" \ -H "Content-Type: application/octet-stream" \
--data-binary "@$tar_file" --data-binary "@$tar_file"
else
echo "ERROR: could not create or fetch release $tag_name" >&2
exit 1
fi fi
+12 -4
View File
@@ -214,13 +214,21 @@ def get_adapter_class():
"""Handle incoming text packet from Meshtastic pubsub (worker thread).""" """Handle incoming text packet from Meshtastic pubsub (worker thread)."""
try: try:
ch = packet.get("channel", 0) ch = packet.get("channel", 0)
if self.channel_index is not None and ch != self.channel_index: my_info = getattr(interface, "myInfo", None)
my_node_num = getattr(my_info, "my_node_num", 0)
is_direct_to_me = packet.get("to") == my_node_num
is_pki_dm = is_direct_to_me and packet.get("pkiEncrypted", False)
if (
self.channel_index is not None
and ch != self.channel_index
and not is_pki_dm
):
# Ignore packets from different channels (e.g. public channel 0). # Ignore packets from different channels (e.g. public channel 0).
# PKI direct messages omit the channel field and decode as
# channel 0 even when sent from a secondary channel.
return return
from_id = packet.get("fromId") or f"!{packet.get('from', 0):08x}" from_id = packet.get("fromId") or f"!{packet.get('from', 0):08x}"
my_info = getattr(interface, "myInfo", None)
my_node_num = getattr(my_info, "my_node_num", 0)
if packet.get("from") == my_node_num: if packet.get("from") == my_node_num:
# Prevent self-message loops. # Prevent self-message loops.
return return
@@ -237,7 +245,7 @@ def get_adapter_class():
return return
to_id = packet.get("toId") to_id = packet.get("toId")
is_dm = to_id != "^all" and packet.get("to") == my_node_num is_dm = to_id != "^all" and is_direct_to_me
chat_id = from_id if is_dm else f"channel_{ch}" chat_id = from_id if is_dm else f"channel_{ch}"
chat_type = "dm" if is_dm else "channel" chat_type = "dm" if is_dm else "channel"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project] [project]
name = "hermes-meshtastic" name = "hermes-meshtastic"
version = "0.1.2" version = "0.1.4"
description = "Meshtastic LoRa mesh gateway adapter plugin for Hermes Agent" description = "Meshtastic LoRa mesh gateway adapter plugin for Hermes Agent"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"
@@ -36,6 +36,7 @@ so the gate can never silently skip.
import inspect import inspect
import os import os
from types import SimpleNamespace
import pytest import pytest
@@ -228,3 +229,40 @@ def test_inbound_event_construction_is_valid(registered_entry):
) )
assert event.source.platform.value == "meshtastic" assert event.source.platform.value == "meshtastic"
assert event.text == "hello over LoRa" assert event.text == "hello over LoRa"
def test_pki_direct_message_without_channel_reaches_gateway(
registered_entry, monkeypatch
):
"""PKI DMs omit channel and must bypass the secondary-channel filter."""
adapter = _make_adapter(registered_entry)
adapter._loop = SimpleNamespace(is_running=lambda: True)
captured = {}
async def handle_message(event):
captured["event"] = event
def submit(coro, loop):
captured["loop"] = loop
with pytest.raises(StopIteration):
coro.send(None)
adapter.handle_message = handle_message
monkeypatch.setattr(plugin_adapter.asyncio, "run_coroutine_threadsafe", submit)
interface = SimpleNamespace(myInfo=SimpleNamespace(my_node_num=0x1BA1BC60))
adapter._on_meshtastic_receive(
{
"from": 0x1BA1496C,
"to": 0x1BA1BC60,
"toId": "!1ba1bc60",
"pkiEncrypted": True,
"decoded": {"text": "hello over PKI"},
"id": 42,
},
interface,
)
assert captured["loop"] is adapter._loop
assert captured["event"].text == "hello over PKI"
assert captured["event"].source.chat_id == "!1ba1496c"