feat(automation): MeshCore scope condition + self-origin guard (#3914)#3920
feat(automation): MeshCore scope condition + self-origin guard (#3914)#3920Yeraze wants to merge 1 commit into
Conversation
Adds a first-class `condition.meshcoreScope` block so an automation can answer a MeshCore message based on its region scope — the #3914 use case of nudging newbies who post unscoped or on a very broad region. mode ∈ {named, unscoped, scoped}; `named` takes a comma-separated `regions` list plus an optional `includeUnscoped` toggle, so "region de OR unscoped" is a single block. It reads the inbound scopeCode/scopeName already exposed by buildMeshCoreMessageContext; Meshtastic messages carry no scope and never match. The reply half already works via action.sendMessage scopeMode:'trigger'. Also adds a self-origin guard to the engine: events originating from our OWN node (message fromNodeNum / MeshCore fromPublicKey == local node, and our own telemetry / node updates) are dropped before any rule runs. This stops an action.sendMessage reply — which re-enters the bus via emitNewMessage/emitMeshCoreMessage — from re-triggering its own rule in an infinite mesh loop, and stops our own periodic telemetry/node-info from spuriously firing rules. Identity is resolved per-source/protocol via optional NodeDataProvider.getLocalNodeNum/getSelfPublicKey accessors (real impls read the manager registries; absent in unit tests → no drop). Geofence checks are intentionally exempt. Mirrors the legacy MeshCore auto-responder self-reply guard. Tests: condition unscoped/scoped/named(+includeUnscoped) incl. the "de OR unscoped" case and Meshtastic non-match; graph validation of the new block; engine self-exclusion for Meshtastic messages, MeshCore (case-insensitive key) and telemetry. Full suite green (7933/0). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015AFBA76hsjqhsXe1BdnYub
|
Claude finished @Yeraze's task —— View job PR Review: feat(automation): MeshCore scope condition + self-origin guard (#3914)
Overall this is a well-structured, focused PR. The implementation is clean, follows the project's established patterns, and comes with solid test coverage. A few observations below. Code Quality & Best Practices
The implementation is clear and correct. The
private async isSelfMeshtastic(sourceId: string | null, fromNodeNum: number | null | undefined): Promise<boolean> {
if (!this.data.getLocalNodeNum || fromNodeNum == null) return false;
const local = await this.data.getLocalNodeNum(sourceId);
return local != null && Number(local) === Number(fromNodeNum);
}The
async getLocalNodeNum(sourceId) {
try {
if (!sourceId) return null;
const nodeNum = sourceManagerRegistry.getManager(sourceId)?.getLocalNodeInfo()?.nodeNum;
return nodeNum != null ? Number(nodeNum) : null;
} catch {
return null;
}
},Good: the Potential Bugs / Edge Cases1. In const mode = String(p.mode ?? 'named');And in const mode = p.mode == null ? 'named' : p.mode;The validator defaults to 2. async onNode(kind, nodeNum, changedKeys, sourceId): Promise<number> {
if (await this.isSelfMeshtastic(sourceId, nodeNum)) return 0;
...
}There is no corresponding MeshCore self-guard for 3.
Performance ConsiderationsThe self-origin guard adds two SecurityNo concerns. The Test CoverageCoverage is good for a feature of this scope:
Missing test:
Minor Nits
SummaryThis is a solid implementation. The logic is correct, the tests cover the main cases well, and the self-origin guard is a meaningful safety rail for the entire engine (not just this feature). The suggestions above are minor — the main actionable items are:
|
Summary
Implements the #3914 request: answer a MeshCore message based on its channel scope — e.g. nudge newbies who post unscoped or on a very broad region (like
de) toward better node config. Adds a first-classcondition.meshcoreScopeblock so this is a single, friendly condition rather than a hand-wired chain. Also adds a general self-origin guard to the Automation Engine so our own traffic never triggers rules (most importantly, so an auto-reply can't re-trigger itself in an infinite mesh loop).Changes
condition.meshcoreScopeblock (src/types/automation.ts,conditionEvaluator.ts, buildercatalog.ts):mode ∈ {named, unscoped, scoped}.namedtakes a comma-separatedregionslist plus an optional "also match unscoped" toggle → expresses "region de OR unscoped" in one block.scopeCode/scopeNamealready exposed bybuildMeshCoreMessageContext([FEAT] Configurable flood hop limit for automated Unscoped messages #3833). Meshtastic messages carry no scope and therefore never match (degrades gracefully). The reply half already works viaaction.sendMessagescopeMode: 'trigger'.namedrequiresregionsorincludeUnscoped; unknown modes rejected.scopeName/scopeCode/scoped/fromName) documented in the Substitutions drawer + typo-checker.automationEngineService.ts,engineContext.ts,meshNodeData.ts):fromNodeNum(Meshtastic) /fromPublicKey(MeshCore, case-insensitive) == the source's local node, plus our own telemetry and node-updates.action.sendMessagereplies (which re-enter the bus viaemitNewMessage/emitMeshCoreMessage) from re-triggering their own rule, and stops our own periodic telemetry/node-info from spuriously firing rules.NodeDataProvider.getLocalNodeNum/getSelfPublicKey(real impls read the Meshtastic + MeshCore manager registries; absent in unit tests → no drop). Geofence checks intentionally exempt. Mirrors the legacy MeshCore auto-responder self-reply guard.Issues Resolved
Fixes #3914
Documentation Updates
docs/internal/dev-notes/AUTOMATION_ENGINE_PLAN.md— addedcondition.meshcoreScopeto the block catalog and the self-origin guard to the engine safety rails.Testing
success: true)tsc --noEmit)namedrejected)🤖 Generated with Claude Code