nrprotocol 0.1.0__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.
@@ -0,0 +1,15 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Elmadani SALKA
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files, to deal in the Software
7
+ without restriction, including without limitation the rights to use, copy,
8
+ modify, merge, publish, distribute, sublicense, and/or sell copies of the
9
+ Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
@@ -0,0 +1,223 @@
1
+ Metadata-Version: 2.4
2
+ Name: nrprotocol
3
+ Version: 0.1.0
4
+ Summary: Node Reach Protocol — The universal standard for AI-to-world control.
5
+ Author-email: Elmadani SALKA <Elmadani.SALKA@proton.me>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ElmadaniS/nrp
8
+ Project-URL: Repository, https://github.com/ElmadaniS/nrp
9
+ Project-URL: Issues, https://github.com/ElmadaniS/nrp/issues
10
+ Keywords: ai,robotics,iot,protocol,mcp,nrp,llm,edge
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
+ Classifier: Topic :: System :: Networking
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ <div align="center">
22
+
23
+ <img src="logo.svg" width="80" alt="Halyn">
24
+
25
+ # NRP
26
+
27
+ ### Node Reach Protocol
28
+
29
+ **The universal standard for AI-to-world control.**
30
+
31
+ MCP connects LLMs to software.<br>
32
+ **NRP connects LLMs to everything else.**
33
+
34
+ Servers · Robots · Drones · Sensors · Vehicles · APIs · Factories · Smart Homes
35
+
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
37
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10+-green.svg)](https://python.org)
38
+ [![Tests](https://img.shields.io/badge/Tests-48%20passed-brightgreen.svg)]()
39
+
40
+ </div>
41
+
42
+ ---
43
+
44
+ ## The Problem
45
+
46
+ Every device speaks a different language. SSH for servers. ROS2 for robots. MQTT for sensors. OPC-UA for factories. REST for APIs. Hundreds of protocols. Thousands of SDKs. No standard.
47
+
48
+ MCP standardized software integration. NRP does the same for hardware and physical systems.
49
+
50
+ ## The Protocol
51
+
52
+ 3 methods. That is the interface.
53
+
54
+ ```
55
+ OBSERVE → read state (sensors, metrics, cameras, APIs)
56
+ ACT → change state (commands, movements, writes, calls)
57
+ SHIELD → safety limits (boundaries the AI cannot cross)
58
+ ```
59
+
60
+ Every device becomes a **node**. Every node has an address:
61
+
62
+ ```
63
+ nrp://factory/robot/arm-7
64
+ nrp://farm/sensor/soil-north
65
+ nrp://cloud/api/stripe
66
+ nrp://home/light/kitchen
67
+ nrp://fleet/vehicle/truck-42
68
+ ```
69
+
70
+ Every node **describes itself**. The AI reads the manifest and knows what to do. Zero configuration. Zero documentation.
71
+
72
+ ## Write a Driver in 100 Lines
73
+
74
+ ```python
75
+ from nrp import NRPDriver, NRPManifest, ChannelSpec, ActionSpec, ShieldSpec
76
+
77
+ class MyRobot(NRPDriver):
78
+
79
+ def manifest(self) -> NRPManifest:
80
+ return NRPManifest(
81
+ nrp_id=self._nrp_id,
82
+ manufacturer="Unitree", model="G1",
83
+ observe=[
84
+ ChannelSpec("joints", "float[]", unit="rad", rate="100Hz"),
85
+ ChannelSpec("battery", "int", unit="percent"),
86
+ ChannelSpec("camera", "image", rate="30Hz"),
87
+ ],
88
+ act=[
89
+ ActionSpec("walk", {"speed": "float m/s"}, "Walk forward", dangerous=True),
90
+ ActionSpec("pick", {"target": "string"}, "Pick an object", dangerous=True),
91
+ ActionSpec("stand", {}, "Stand still"),
92
+ ],
93
+ shield=[
94
+ ShieldSpec("max_speed", "limit", 1.5, "m/s"),
95
+ ShieldSpec("workspace", "zone", [0, 0, 10, 10], "meters"),
96
+ ],
97
+ )
98
+
99
+ async def observe(self, channels=None):
100
+ return {"joints": self.read_joints(), "battery": self.read_battery()}
101
+
102
+ async def act(self, command, args):
103
+ if command == "walk":
104
+ return self.walk(args["speed"])
105
+ if command == "pick":
106
+ return self.pick(args["target"])
107
+
108
+ def shield_rules(self):
109
+ return [ShieldRule("max_speed", ShieldType.LIMIT, 1.5)]
110
+ ```
111
+
112
+
113
+
114
+ ## What the AI Sees
115
+
116
+ When a node connects, the AI receives a human-readable description:
117
+
118
+ ```
119
+ Node: nrp://factory/robot/g1-01
120
+ Device: Unitree G1
121
+ Observe (read state):
122
+ joints: float[] (rad) — Joint angles at 100Hz
123
+ battery: int (percent) — Battery level
124
+ camera: image — Camera feed at 30Hz
125
+ Act (commands):
126
+ walk(speed: float m/s) [DANGEROUS] — Walk forward
127
+ pick(target: string) [DANGEROUS] — Pick an object
128
+ stand() — Stand still
129
+ Shield (safety limits):
130
+ max_speed: limit = 1.5 m/s
131
+ workspace: zone = [0, 0, 10, 10] meters
132
+ ```
133
+
134
+
135
+
136
+ ## Real-Time Events
137
+
138
+ Nodes push events. The AI reacts without polling.
139
+
140
+ ```python
141
+ # The node pushes
142
+ await driver.emit("battery_low", Severity.WARNING, percent=8)
143
+ await driver.emit("collision", Severity.EMERGENCY, force=45.2)
144
+
145
+ # The control plane routes
146
+ bus.subscribe("battery_*", alert_handler)
147
+ bus.subscribe("nrp://factory/*", factory_monitor)
148
+ ```
149
+
150
+ **Emergency events bypass all queues** and are processed synchronously.
151
+
152
+ ## Specification
153
+
154
+ | Document | Description |
155
+ |----------|-------------|
156
+ | [IDENTITY.md](spec/IDENTITY.md) | Universal addressing: `nrp://scope/kind/name` |
157
+ | [MANIFEST.md](spec/MANIFEST.md) | Self-describing nodes: channels, actions, shields |
158
+ | [EVENTS.md](spec/EVENTS.md) | Real-time push: severity levels, emergency bypass |
159
+ | [NRP_SPEC.md](spec/NRP_SPEC.md) | Protocol overview |
160
+
161
+ ## Install
162
+
163
+ ```bash
164
+ pip install nrp
165
+ ```
166
+
167
+ ## Examples
168
+
169
+ - [`hello_ssh.py`](examples/hello_ssh.py) — Connect to a server in 60 lines
170
+ - [`multi_node.py`](examples/multi_node.py) — 3 sensors, 1 conversation
171
+
172
+ ## Built With NRP
173
+
174
+ | Project | Description |
175
+ |---------|-------------|
176
+ | [Halyn](https://github.com/ElmadaniS/halyn) | NRP control plane with domain-scoped authorization |
177
+
178
+ ## Why Not Just Use MCP?
179
+
180
+ MCP is brilliant for software. But:
181
+
182
+ - MCP has no concept of **physical safety** (shield rules)
183
+ - MCP has no concept of **real-time events** (push, not pull)
184
+ - MCP has no concept of **self-describing hardware** (manifests)
185
+ - MCP has no concept of **universal device identity** (`nrp://`)
186
+ - MCP tools are defined by the server. NRP tools are declared by the device.
187
+
188
+ NRP complements MCP. A control plane exposes NRP nodes as MCP tools — transparent to the LLM.
189
+
190
+ ## Architecture
191
+
192
+ ```
193
+ Any LLM (Claude, GPT, Ollama, local)
194
+
195
+ │ MCP (software)
196
+
197
+
198
+ Control Plane (e.g. Halyn)
199
+
200
+ │ NRP (physical + digital world)
201
+
202
+ ├──→ Servers (SSH)
203
+ ├──→ Robots (ROS2, Unitree, DJI)
204
+ ├──→ Sensors (MQTT)
205
+ ├──→ APIs (REST, GraphQL — auto-introspected)
206
+ ├──→ Containers (Docker)
207
+ ├──→ Browsers (Chrome CDP)
208
+ ├──→ Factories (OPC-UA, Modbus)
209
+ ├──→ Vehicles (CAN, DDS)
210
+ └──→ Anything with an interface
211
+ ```
212
+
213
+ ## Contributing
214
+
215
+ See [CONTRIBUTING.md](CONTRIBUTING.md). A driver is 4 methods, ~100 lines. If it has an interface, it can be an NRP node.
216
+
217
+ ## License
218
+
219
+ MIT — Free forever. Use it. Build on it.
220
+
221
+ ## Author
222
+
223
+ **Elmadani SALKA**
@@ -0,0 +1,203 @@
1
+ <div align="center">
2
+
3
+ <img src="logo.svg" width="80" alt="Halyn">
4
+
5
+ # NRP
6
+
7
+ ### Node Reach Protocol
8
+
9
+ **The universal standard for AI-to-world control.**
10
+
11
+ MCP connects LLMs to software.<br>
12
+ **NRP connects LLMs to everything else.**
13
+
14
+ Servers · Robots · Drones · Sensors · Vehicles · APIs · Factories · Smart Homes
15
+
16
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
17
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10+-green.svg)](https://python.org)
18
+ [![Tests](https://img.shields.io/badge/Tests-48%20passed-brightgreen.svg)]()
19
+
20
+ </div>
21
+
22
+ ---
23
+
24
+ ## The Problem
25
+
26
+ Every device speaks a different language. SSH for servers. ROS2 for robots. MQTT for sensors. OPC-UA for factories. REST for APIs. Hundreds of protocols. Thousands of SDKs. No standard.
27
+
28
+ MCP standardized software integration. NRP does the same for hardware and physical systems.
29
+
30
+ ## The Protocol
31
+
32
+ 3 methods. That is the interface.
33
+
34
+ ```
35
+ OBSERVE → read state (sensors, metrics, cameras, APIs)
36
+ ACT → change state (commands, movements, writes, calls)
37
+ SHIELD → safety limits (boundaries the AI cannot cross)
38
+ ```
39
+
40
+ Every device becomes a **node**. Every node has an address:
41
+
42
+ ```
43
+ nrp://factory/robot/arm-7
44
+ nrp://farm/sensor/soil-north
45
+ nrp://cloud/api/stripe
46
+ nrp://home/light/kitchen
47
+ nrp://fleet/vehicle/truck-42
48
+ ```
49
+
50
+ Every node **describes itself**. The AI reads the manifest and knows what to do. Zero configuration. Zero documentation.
51
+
52
+ ## Write a Driver in 100 Lines
53
+
54
+ ```python
55
+ from nrp import NRPDriver, NRPManifest, ChannelSpec, ActionSpec, ShieldSpec
56
+
57
+ class MyRobot(NRPDriver):
58
+
59
+ def manifest(self) -> NRPManifest:
60
+ return NRPManifest(
61
+ nrp_id=self._nrp_id,
62
+ manufacturer="Unitree", model="G1",
63
+ observe=[
64
+ ChannelSpec("joints", "float[]", unit="rad", rate="100Hz"),
65
+ ChannelSpec("battery", "int", unit="percent"),
66
+ ChannelSpec("camera", "image", rate="30Hz"),
67
+ ],
68
+ act=[
69
+ ActionSpec("walk", {"speed": "float m/s"}, "Walk forward", dangerous=True),
70
+ ActionSpec("pick", {"target": "string"}, "Pick an object", dangerous=True),
71
+ ActionSpec("stand", {}, "Stand still"),
72
+ ],
73
+ shield=[
74
+ ShieldSpec("max_speed", "limit", 1.5, "m/s"),
75
+ ShieldSpec("workspace", "zone", [0, 0, 10, 10], "meters"),
76
+ ],
77
+ )
78
+
79
+ async def observe(self, channels=None):
80
+ return {"joints": self.read_joints(), "battery": self.read_battery()}
81
+
82
+ async def act(self, command, args):
83
+ if command == "walk":
84
+ return self.walk(args["speed"])
85
+ if command == "pick":
86
+ return self.pick(args["target"])
87
+
88
+ def shield_rules(self):
89
+ return [ShieldRule("max_speed", ShieldType.LIMIT, 1.5)]
90
+ ```
91
+
92
+
93
+
94
+ ## What the AI Sees
95
+
96
+ When a node connects, the AI receives a human-readable description:
97
+
98
+ ```
99
+ Node: nrp://factory/robot/g1-01
100
+ Device: Unitree G1
101
+ Observe (read state):
102
+ joints: float[] (rad) — Joint angles at 100Hz
103
+ battery: int (percent) — Battery level
104
+ camera: image — Camera feed at 30Hz
105
+ Act (commands):
106
+ walk(speed: float m/s) [DANGEROUS] — Walk forward
107
+ pick(target: string) [DANGEROUS] — Pick an object
108
+ stand() — Stand still
109
+ Shield (safety limits):
110
+ max_speed: limit = 1.5 m/s
111
+ workspace: zone = [0, 0, 10, 10] meters
112
+ ```
113
+
114
+
115
+
116
+ ## Real-Time Events
117
+
118
+ Nodes push events. The AI reacts without polling.
119
+
120
+ ```python
121
+ # The node pushes
122
+ await driver.emit("battery_low", Severity.WARNING, percent=8)
123
+ await driver.emit("collision", Severity.EMERGENCY, force=45.2)
124
+
125
+ # The control plane routes
126
+ bus.subscribe("battery_*", alert_handler)
127
+ bus.subscribe("nrp://factory/*", factory_monitor)
128
+ ```
129
+
130
+ **Emergency events bypass all queues** and are processed synchronously.
131
+
132
+ ## Specification
133
+
134
+ | Document | Description |
135
+ |----------|-------------|
136
+ | [IDENTITY.md](spec/IDENTITY.md) | Universal addressing: `nrp://scope/kind/name` |
137
+ | [MANIFEST.md](spec/MANIFEST.md) | Self-describing nodes: channels, actions, shields |
138
+ | [EVENTS.md](spec/EVENTS.md) | Real-time push: severity levels, emergency bypass |
139
+ | [NRP_SPEC.md](spec/NRP_SPEC.md) | Protocol overview |
140
+
141
+ ## Install
142
+
143
+ ```bash
144
+ pip install nrp
145
+ ```
146
+
147
+ ## Examples
148
+
149
+ - [`hello_ssh.py`](examples/hello_ssh.py) — Connect to a server in 60 lines
150
+ - [`multi_node.py`](examples/multi_node.py) — 3 sensors, 1 conversation
151
+
152
+ ## Built With NRP
153
+
154
+ | Project | Description |
155
+ |---------|-------------|
156
+ | [Halyn](https://github.com/ElmadaniS/halyn) | NRP control plane with domain-scoped authorization |
157
+
158
+ ## Why Not Just Use MCP?
159
+
160
+ MCP is brilliant for software. But:
161
+
162
+ - MCP has no concept of **physical safety** (shield rules)
163
+ - MCP has no concept of **real-time events** (push, not pull)
164
+ - MCP has no concept of **self-describing hardware** (manifests)
165
+ - MCP has no concept of **universal device identity** (`nrp://`)
166
+ - MCP tools are defined by the server. NRP tools are declared by the device.
167
+
168
+ NRP complements MCP. A control plane exposes NRP nodes as MCP tools — transparent to the LLM.
169
+
170
+ ## Architecture
171
+
172
+ ```
173
+ Any LLM (Claude, GPT, Ollama, local)
174
+
175
+ │ MCP (software)
176
+
177
+
178
+ Control Plane (e.g. Halyn)
179
+
180
+ │ NRP (physical + digital world)
181
+
182
+ ├──→ Servers (SSH)
183
+ ├──→ Robots (ROS2, Unitree, DJI)
184
+ ├──→ Sensors (MQTT)
185
+ ├──→ APIs (REST, GraphQL — auto-introspected)
186
+ ├──→ Containers (Docker)
187
+ ├──→ Browsers (Chrome CDP)
188
+ ├──→ Factories (OPC-UA, Modbus)
189
+ ├──→ Vehicles (CAN, DDS)
190
+ └──→ Anything with an interface
191
+ ```
192
+
193
+ ## Contributing
194
+
195
+ See [CONTRIBUTING.md](CONTRIBUTING.md). A driver is 4 methods, ~100 lines. If it has an interface, it can be an NRP node.
196
+
197
+ ## License
198
+
199
+ MIT — Free forever. Use it. Build on it.
200
+
201
+ ## Author
202
+
203
+ **Elmadani SALKA**
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "nrprotocol"
3
+ version = "0.1.0"
4
+ description = "Node Reach Protocol — The universal standard for AI-to-world control."
5
+ requires-python = ">=3.10"
6
+ license = {text = "MIT"}
7
+ authors = [{name = "Elmadani SALKA", email = "Elmadani.SALKA@proton.me"}]
8
+ readme = "README.md"
9
+ keywords = ["ai", "robotics", "iot", "protocol", "mcp", "nrp", "llm", "edge"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Programming Language :: Python :: 3",
14
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
15
+ "Topic :: System :: Networking",
16
+ ]
17
+ dependencies = []
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/ElmadaniS/nrp"
21
+ Repository = "https://github.com/ElmadaniS/nrp"
22
+ Issues = "https://github.com/ElmadaniS/nrp/issues"
23
+
24
+ [tool.setuptools.packages.find]
25
+ where = ["sdk/python"]
26
+
27
+ [build-system]
28
+ requires = ["setuptools>=68"]
29
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,27 @@
1
+ """
2
+ NRP — Node Reach Protocol
3
+
4
+ The universal protocol for AI-to-world control.
5
+ MCP connects LLMs to software. NRP connects LLMs to everything else.
6
+
7
+ from nrp import NRPDriver, NRPId, NRPManifest, EventBus
8
+
9
+ https://github.com/navigia/nrp
10
+ """
11
+
12
+ from .identity import NRPId
13
+ from .manifest import NRPManifest, ChannelSpec, ActionSpec, ShieldSpec
14
+ from .events import NRPEvent, EventBus, EventSSE, Severity
15
+ from .driver import NRPDriver, ShieldRule, ShieldType
16
+
17
+ __version__ = "0.1.0"
18
+ __author__ = "Elmadani SALKA"
19
+ __license__ = "MIT"
20
+
21
+ __all__ = [
22
+ "NRPId",
23
+ "NRPManifest", "ChannelSpec", "ActionSpec", "ShieldSpec",
24
+ "NRPEvent", "EventBus", "EventSSE", "Severity",
25
+ "NRPDriver", "ShieldRule", "ShieldType",
26
+ ]
27
+
@@ -0,0 +1,112 @@
1
+ # Copyright (c) 2026 Elmadani SALKA. All rights reserved.
2
+ # Licensed under the MIT License. See LICENSE file.
3
+ """
4
+ NRP Driver v2 — Universal interface with Manifest + Events.
5
+
6
+ Every device implements:
7
+ manifest() — declare what you are and what you can do
8
+ observe() — read state
9
+ act() — change state
10
+ on_event() — push events to the control plane
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from abc import ABC, abstractmethod
16
+ from dataclasses import dataclass, field
17
+ from enum import Enum
18
+ from typing import Any, Callable, Awaitable
19
+
20
+ from .identity import NRPId
21
+ from .manifest import NRPManifest, ChannelSpec, ActionSpec, ShieldSpec
22
+ from .events import NRPEvent, EventBus, Severity
23
+
24
+
25
+ class ShieldType(str, Enum):
26
+ LIMIT = "limit"
27
+ ZONE = "zone"
28
+ THRESHOLD = "threshold"
29
+ PATTERN = "pattern"
30
+ COMMAND = "command"
31
+ CONFIRM = "confirm"
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class ShieldRule:
36
+ name: str
37
+ type: ShieldType
38
+ value: Any = None
39
+ unit: str = ""
40
+ description: str = ""
41
+
42
+
43
+ class NRPDriver(ABC):
44
+ """
45
+ Base class for all NRP drivers.
46
+
47
+ Implement this to connect ANY device to AI.
48
+ Subclass and implement manifest(), observe(), act(), shield_rules().
49
+ """
50
+
51
+ def __init__(self) -> None:
52
+ self._event_bus: EventBus | None = None
53
+ self._nrp_id: NRPId | None = None
54
+
55
+ def bind(self, nrp_id: NRPId, event_bus: EventBus) -> None:
56
+ """Called by NRP Bridge at registration. Gives driver access to events."""
57
+ self._nrp_id = nrp_id
58
+ self._event_bus = event_bus
59
+
60
+ # ─── The 4 core methods ─────────────────────────
61
+
62
+ @abstractmethod
63
+ def manifest(self) -> NRPManifest:
64
+ """Declare everything about this node. Called once at registration."""
65
+
66
+ @abstractmethod
67
+ async def observe(self, channels: list[str] | None = None) -> dict[str, Any]:
68
+ """Read node state. Returns {channel_name: data}."""
69
+
70
+ @abstractmethod
71
+ async def act(self, command: str, args: dict[str, Any]) -> Any:
72
+ """Execute a command. Returns result."""
73
+
74
+ @abstractmethod
75
+ def shield_rules(self) -> list[ShieldRule]:
76
+ """Safety limits. Enforced by control plane."""
77
+
78
+ # ─── Events ─────────────────────────────────────
79
+
80
+ async def emit(self, name: str, severity: str = Severity.INFO, **data: Any) -> None:
81
+ """Push an event to the control plane."""
82
+ if self._event_bus and self._nrp_id:
83
+ event = NRPEvent(
84
+ source=self._nrp_id.uri,
85
+ name=name,
86
+ severity=severity,
87
+ data=data,
88
+ )
89
+ await self._event_bus.emit(event)
90
+
91
+ async def emit_emergency(self, name: str, **data: Any) -> None:
92
+ """Push an emergency event. Bypasses all queues."""
93
+ await self.emit(name, Severity.EMERGENCY, **data)
94
+
95
+ # ─── Lifecycle ──────────────────────────────────
96
+
97
+ async def connect(self) -> bool:
98
+ """Establish connection. Override for networked devices."""
99
+ return True
100
+
101
+ async def disconnect(self) -> None:
102
+ """Clean shutdown."""
103
+ pass
104
+
105
+ async def heartbeat(self) -> dict[str, Any]:
106
+ """Health check."""
107
+ try:
108
+ state = await self.observe(["status"])
109
+ return {"alive": True, **state}
110
+ except Exception as e:
111
+ return {"alive": False, "error": str(e)[:200]}
112
+
@@ -0,0 +1,234 @@
1
+ # Copyright (c) 2026 Elmadani SALKA. All rights reserved.
2
+ # Licensed under the MIT License. See LICENSE file.
3
+ """
4
+ NRP Events — Asynchronous event bus with severity-based routing.
5
+
6
+ Nodes emit events. The control plane dispatches to subscribers
7
+ based on pattern matching. Emergency events bypass the queue.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import json
14
+ import logging
15
+ import time
16
+ from collections import defaultdict
17
+ from dataclasses import dataclass, field
18
+ from typing import Any, Callable, Awaitable
19
+
20
+ log = logging.getLogger("jarvis.events")
21
+
22
+ EventHandler = Callable[["NRPEvent"], Awaitable[None] | None]
23
+
24
+
25
+ class Severity:
26
+ """Event severity levels."""
27
+ DEBUG = "debug"
28
+ INFO = "info"
29
+ WARNING = "warning"
30
+ CRITICAL = "critical"
31
+ EMERGENCY = "emergency" # Bypasses all queues
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class NRPEvent:
36
+ """Single event payload."""
37
+ source: str # NRP ID or node name
38
+ name: str # "temperature_changed", "battery_low", "collision"
39
+ severity: str # Severity level
40
+ data: dict[str, Any] = field(default_factory=dict)
41
+ timestamp: float = field(default_factory=time.time)
42
+
43
+ def to_dict(self) -> dict[str, Any]:
44
+ return {
45
+ "source": self.source,
46
+ "name": self.name,
47
+ "severity": self.severity,
48
+ "data": self.data,
49
+ "timestamp": self.timestamp,
50
+ }
51
+
52
+ def to_json(self) -> str:
53
+ return json.dumps(self.to_dict(), default=str, ensure_ascii=False)
54
+
55
+
56
+ class EventBus:
57
+ """Central event routing. Subscribe by pattern, receive matching events."""
58
+
59
+ __slots__ = ("_handlers", "_history", "_max_history", "_queue")
60
+
61
+ def __init__(self, max_history: int = 10_000) -> None:
62
+ self._handlers: dict[str, list[EventHandler]] = defaultdict(list)
63
+ self._history: list[NRPEvent] = []
64
+ self._max_history = max_history
65
+ self._queue: asyncio.Queue[NRPEvent] = asyncio.Queue(maxsize=50_000)
66
+
67
+ def subscribe(self, pattern: str, handler: EventHandler) -> None:
68
+ """
69
+ Subscribe to events matching a pattern.
70
+
71
+ Patterns:
72
+ "*" — all events
73
+ "battery_low" — exact event name
74
+ "nrp://farm/*" — all events from farm scope
75
+ "temperature_*" — wildcard on event name
76
+ """
77
+ self._handlers[pattern].append(handler)
78
+ log.debug("events.subscribe pattern=%s", pattern)
79
+
80
+ def unsubscribe(self, pattern: str, handler: EventHandler) -> None:
81
+ if pattern in self._handlers:
82
+ self._handlers[pattern] = [h for h in self._handlers[pattern] if h is not handler]
83
+
84
+ async def emit(self, event: NRPEvent) -> int:
85
+ """
86
+ Emit an event. Routes to all matching handlers.
87
+ Returns number of handlers that received it.
88
+
89
+ EMERGENCY events are processed synchronously (no queue).
90
+ All others go through the async queue.
91
+ """
92
+ self._record(event)
93
+
94
+ if event.severity == Severity.EMERGENCY:
95
+ return await self._dispatch_now(event)
96
+
97
+ try:
98
+ self._queue.put_nowait(event)
99
+ except asyncio.QueueFull:
100
+ log.error("events.queue_full dropping=%s", event.name)
101
+ return 0 # Will be dispatched by process_loop
102
+
103
+ async def emit_simple(
104
+ self, source: str, name: str, severity: str = Severity.INFO, **data: Any
105
+ ) -> int:
106
+ """Shorthand for emitting events."""
107
+ return await self.emit(NRPEvent(source=source, name=name, severity=severity, data=data))
108
+
109
+ async def process_loop(self) -> None:
110
+ """Background loop that processes queued events. Run as asyncio task."""
111
+ log.info("events.loop_started")
112
+ while True:
113
+ try:
114
+ event = await asyncio.wait_for(self._queue.get(), timeout=1.0)
115
+ await self._dispatch_now(event)
116
+ except asyncio.TimeoutError:
117
+ continue
118
+ except Exception as exc:
119
+ log.exception("events.loop_error: %s", exc)
120
+
121
+ def recent(self, n: int = 50, source: str = "", name: str = "", severity: str = "") -> list[NRPEvent]:
122
+ """Query recent events with optional filters."""
123
+ events = self._history
124
+ if source:
125
+ events = [e for e in events if source in e.source]
126
+ if name:
127
+ events = [e for e in events if name in e.name]
128
+ if severity:
129
+ events = [e for e in events if e.severity == severity]
130
+ return events[-n:]
131
+
132
+ @property
133
+ def pending(self) -> int:
134
+ return self._queue.qsize()
135
+
136
+ @property
137
+ def total(self) -> int:
138
+ return len(self._history)
139
+
140
+ # ─── Internal ──────────────────────────────────
141
+
142
+ async def _dispatch_now(self, event: NRPEvent) -> int:
143
+ """Dispatch to all matching handlers immediately."""
144
+ import fnmatch
145
+ count = 0
146
+ for pattern, handlers in self._handlers.items():
147
+ matched = (
148
+ pattern == "*"
149
+ or pattern == event.name
150
+ or fnmatch.fnmatch(event.name, pattern)
151
+ or fnmatch.fnmatch(event.source, pattern)
152
+ )
153
+ if matched:
154
+ for handler in handlers:
155
+ try:
156
+ result = handler(event)
157
+ if asyncio.iscoroutine(result):
158
+ await result
159
+ count += 1
160
+ except Exception as exc:
161
+ log.error("events.handler_error pattern=%s error=%s", pattern, exc)
162
+ return count
163
+
164
+ def _record(self, event: NRPEvent) -> None:
165
+ """Store in history ring buffer."""
166
+ self._history.append(event)
167
+ if len(self._history) > self._max_history:
168
+ self._history = self._history[-self._max_history // 2:]
169
+
170
+ lvl = {
171
+ Severity.DEBUG: logging.DEBUG,
172
+ Severity.INFO: logging.INFO,
173
+ Severity.WARNING: logging.WARNING,
174
+ Severity.CRITICAL: logging.ERROR,
175
+ Severity.EMERGENCY: logging.CRITICAL,
176
+ }.get(event.severity, logging.INFO)
177
+ log.log(lvl, "event.%s source=%s data=%s", event.name, event.source,
178
+ json.dumps(event.data, default=str)[:200])
179
+
180
+
181
+ class EventSSE:
182
+ """Server-Sent Events endpoint. Streams events to HTTP clients in real-time."""
183
+
184
+ def __init__(self, bus: EventBus) -> None:
185
+ self.bus = bus
186
+ self._clients: list[asyncio.Queue[str]] = []
187
+
188
+ async def handler(self, request: Any) -> Any:
189
+ """aiohttp SSE handler. Each client gets a queue."""
190
+ from aiohttp import web
191
+ from aiohttp.web import StreamResponse
192
+
193
+ response = StreamResponse()
194
+ response.content_type = "text/event-stream"
195
+ response.headers["Cache-Control"] = "no-cache"
196
+ response.headers["X-Accel-Buffering"] = "no"
197
+ await response.prepare(request)
198
+
199
+ queue: asyncio.Queue[str] = asyncio.Queue(maxsize=1000)
200
+ self._clients.append(queue)
201
+
202
+ try:
203
+ while True:
204
+ try:
205
+ data = await asyncio.wait_for(queue.get(), timeout=30.0)
206
+ await response.write(f"data: {data}\n\n".encode())
207
+ except asyncio.TimeoutError:
208
+ # Keepalive
209
+ await response.write(b": keepalive\n\n")
210
+ except (ConnectionResetError, asyncio.CancelledError):
211
+ pass
212
+ finally:
213
+ self._clients.remove(queue)
214
+
215
+ return response
216
+
217
+ async def broadcast(self, event: NRPEvent) -> None:
218
+ """Send event to all SSE clients."""
219
+ data = event.to_json()
220
+ dead: list[asyncio.Queue[str]] = []
221
+ for client in self._clients:
222
+ try:
223
+ client.put_nowait(data)
224
+ except asyncio.QueueFull:
225
+ dead.append(client)
226
+ for d in dead:
227
+ self._clients.remove(d)
228
+
229
+ def wire(self, bus: EventBus) -> None:
230
+ """Subscribe to all events and broadcast to SSE clients."""
231
+ async def _forward(event: NRPEvent) -> None:
232
+ await self.broadcast(event)
233
+ bus.subscribe("*", _forward)
234
+
@@ -0,0 +1,71 @@
1
+ # Copyright (c) 2026 Elmadani SALKA. All rights reserved.
2
+ # Licensed under the MIT License. See LICENSE file.
3
+ """
4
+ NRP Identity — Universal node addressing.
5
+
6
+ Stable addressing: nrp://scope/kind/name
7
+ Survives IP changes, network moves, device replacement.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+ from dataclasses import dataclass
14
+
15
+ # nrp://factory-3/robot/arm-7
16
+ # nrp://home/sensor/kitchen-temp
17
+ # nrp://fleet/vehicle/truck-42
18
+ # nrp://farm/drone/survey-1
19
+ _NRP_PATTERN = re.compile(
20
+ r"^nrp://(?P<scope>[a-z0-9][a-z0-9._-]*)/"
21
+ r"(?P<kind>[a-z0-9][a-z0-9_-]*)/"
22
+ r"(?P<name>[a-z0-9][a-z0-9._-]*)$"
23
+ )
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class NRPId:
28
+ """Universal node identifier."""
29
+ scope: str # Location/org: "factory-3", "home", "fleet", "farm"
30
+ kind: str # Type: "robot", "sensor", "server", "vehicle", "drone"
31
+ name: str # Instance: "arm-7", "kitchen-temp", "truck-42"
32
+
33
+ @property
34
+ def uri(self) -> str:
35
+ return f"nrp://{self.scope}/{self.kind}/{self.name}"
36
+
37
+ @property
38
+ def short(self) -> str:
39
+ """Short form for display: kind/name."""
40
+ return f"{self.kind}/{self.name}"
41
+
42
+ def matches(self, pattern: str) -> bool:
43
+ """Match against glob patterns. E.g. 'factory-*/robot/*'."""
44
+ import fnmatch
45
+ return fnmatch.fnmatch(self.uri, f"nrp://{pattern}")
46
+
47
+ @classmethod
48
+ def parse(cls, uri: str) -> NRPId:
49
+ """Parse 'nrp://scope/kind/name' into NRPId."""
50
+ m = _NRP_PATTERN.match(uri.lower().strip())
51
+ if not m:
52
+ raise ValueError(
53
+ f"Invalid NRP ID: {uri!r}. "
54
+ f"Format: nrp://scope/kind/name (lowercase, alphanumeric, hyphens)"
55
+ )
56
+ return cls(scope=m["scope"], kind=m["kind"], name=m["name"])
57
+
58
+ @classmethod
59
+ def create(cls, scope: str, kind: str, name: str) -> NRPId:
60
+ """Create and validate an NRP ID."""
61
+ nid = cls(scope=scope.lower(), kind=kind.lower(), name=name.lower())
62
+ # Validate by roundtripping through parse
63
+ NRPId.parse(nid.uri)
64
+ return nid
65
+
66
+ def __str__(self) -> str:
67
+ return self.uri
68
+
69
+ def __repr__(self) -> str:
70
+ return f"NRPId({self.uri!r})"
71
+
@@ -0,0 +1,166 @@
1
+ # Copyright (c) 2026 Elmadani SALKA. All rights reserved.
2
+ # Licensed under the MIT License. See LICENSE file.
3
+ """
4
+ NRP Manifest — Self-describing nodes.
5
+
6
+ Structured capability declaration for NRP nodes.
7
+ Channels (observe), actions (act), and constraints (shield)
8
+ are declared once and consumed by any control plane.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import time
15
+ from dataclasses import dataclass, field
16
+ from typing import Any
17
+
18
+ from .identity import NRPId
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class ChannelSpec:
23
+ """One observable channel (sensor, metric, state)."""
24
+ name: str
25
+ type: str # "float", "int", "bool", "string", "float[]", "image", "json"
26
+ unit: str = "" # "°C", "rad", "m/s", "percent", "Pa", ""
27
+ rate: str = "" # "100Hz", "1Hz", "on_change", "on_request"
28
+ description: str = ""
29
+ range: list[float] | None = None # [min, max] if applicable
30
+
31
+ def to_dict(self) -> dict[str, Any]:
32
+ d: dict[str, Any] = {"name": self.name, "type": self.type}
33
+ if self.unit: d["unit"] = self.unit
34
+ if self.rate: d["rate"] = self.rate
35
+ if self.description: d["description"] = self.description
36
+ if self.range: d["range"] = self.range
37
+ return d
38
+
39
+
40
+ @dataclass(slots=True)
41
+ class ActionSpec:
42
+ """One action the node can perform."""
43
+ name: str
44
+ args: dict[str, str] = field(default_factory=dict) # arg_name -> "type description"
45
+ description: str = ""
46
+ dangerous: bool = False
47
+ priority: str = "normal" # "normal", "high", "critical"
48
+ returns: str = "" # What it returns
49
+
50
+ def to_dict(self) -> dict[str, Any]:
51
+ d: dict[str, Any] = {"name": self.name}
52
+ if self.args: d["args"] = self.args
53
+ if self.description: d["description"] = self.description
54
+ if self.dangerous: d["dangerous"] = True
55
+ if self.priority != "normal": d["priority"] = self.priority
56
+ if self.returns: d["returns"] = self.returns
57
+ return d
58
+
59
+
60
+ @dataclass(slots=True)
61
+ class ShieldSpec:
62
+ """One safety constraint."""
63
+ name: str
64
+ type: str # "limit", "zone", "threshold", "pattern", "confirm"
65
+ value: Any = None
66
+ unit: str = ""
67
+ description: str = ""
68
+
69
+ def to_dict(self) -> dict[str, Any]:
70
+ d: dict[str, Any] = {"name": self.name, "type": self.type}
71
+ if self.value is not None: d["value"] = self.value
72
+ if self.unit: d["unit"] = self.unit
73
+ if self.description: d["description"] = self.description
74
+ return d
75
+
76
+
77
+ @dataclass(slots=True)
78
+ class NRPManifest:
79
+ """Complete capability descriptor for one NRP node."""
80
+
81
+ # Identity
82
+ nrp_id: NRPId
83
+ manufacturer: str = ""
84
+ model: str = ""
85
+ firmware: str = ""
86
+
87
+ # Capabilities
88
+ observe: list[ChannelSpec] = field(default_factory=list)
89
+ act: list[ActionSpec] = field(default_factory=list)
90
+ shield: list[ShieldSpec] = field(default_factory=list)
91
+
92
+ # Metadata
93
+ tags: dict[str, str] = field(default_factory=dict)
94
+ registered_at: float = field(default_factory=time.time)
95
+
96
+ def to_dict(self) -> dict[str, Any]:
97
+ return {
98
+ "nrp_id": self.nrp_id.uri,
99
+ "manufacturer": self.manufacturer,
100
+ "model": self.model,
101
+ "firmware": self.firmware,
102
+ "observe": [c.to_dict() for c in self.observe],
103
+ "act": [a.to_dict() for a in self.act],
104
+ "shield": [s.to_dict() for s in self.shield],
105
+ "tags": self.tags,
106
+ }
107
+
108
+ def to_json(self, indent: int = 2) -> str:
109
+ return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)
110
+
111
+ def to_llm_description(self) -> str:
112
+ """Structured text summary suitable for LLM context injection."""
113
+ lines = [f"Node: {self.nrp_id.uri}"]
114
+ if self.manufacturer:
115
+ lines.append(f" Device: {self.manufacturer} {self.model}")
116
+
117
+ if self.observe:
118
+ lines.append(" Observe (read state):")
119
+ for ch in self.observe:
120
+ unit = f" ({ch.unit})" if ch.unit else ""
121
+ desc = f" — {ch.description}" if ch.description else ""
122
+ lines.append(f" {ch.name}: {ch.type}{unit}{desc}")
123
+
124
+ if self.act:
125
+ lines.append(" Act (commands):")
126
+ for a in self.act:
127
+ args_str = ", ".join(f"{k}: {v}" for k, v in a.args.items()) if a.args else "none"
128
+ danger = " [DANGEROUS]" if a.dangerous else ""
129
+ prio = f" [PRIORITY={a.priority}]" if a.priority != "normal" else ""
130
+ desc = f" — {a.description}" if a.description else ""
131
+ lines.append(f" {a.name}({args_str}){danger}{prio}{desc}")
132
+
133
+ if self.shield:
134
+ lines.append(" Shield (safety limits):")
135
+ for s in self.shield:
136
+ unit = f" {s.unit}" if s.unit else ""
137
+ lines.append(f" {s.name}: {s.type} = {s.value}{unit}")
138
+
139
+ return "\n".join(lines)
140
+
141
+ @classmethod
142
+ def from_dict(cls, data: dict[str, Any]) -> NRPManifest:
143
+ """Parse a manifest from JSON/dict."""
144
+ nrp_id = NRPId.parse(data["nrp_id"])
145
+ return cls(
146
+ nrp_id=nrp_id,
147
+ manufacturer=data.get("manufacturer", ""),
148
+ model=data.get("model", ""),
149
+ firmware=data.get("firmware", ""),
150
+ observe=[ChannelSpec(**c) for c in data.get("observe", [])],
151
+ act=[ActionSpec(
152
+ name=a["name"],
153
+ args=a.get("args", {}),
154
+ description=a.get("description", ""),
155
+ dangerous=a.get("dangerous", False),
156
+ priority=a.get("priority", "normal"),
157
+ returns=a.get("returns", ""),
158
+ ) for a in data.get("act", [])],
159
+ shield=[ShieldSpec(**s) for s in data.get("shield", [])],
160
+ tags=data.get("tags", {}),
161
+ )
162
+
163
+ @classmethod
164
+ def from_json(cls, text: str) -> NRPManifest:
165
+ return cls.from_dict(json.loads(text))
166
+
File without changes
@@ -0,0 +1,223 @@
1
+ Metadata-Version: 2.4
2
+ Name: nrprotocol
3
+ Version: 0.1.0
4
+ Summary: Node Reach Protocol — The universal standard for AI-to-world control.
5
+ Author-email: Elmadani SALKA <Elmadani.SALKA@proton.me>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ElmadaniS/nrp
8
+ Project-URL: Repository, https://github.com/ElmadaniS/nrp
9
+ Project-URL: Issues, https://github.com/ElmadaniS/nrp/issues
10
+ Keywords: ai,robotics,iot,protocol,mcp,nrp,llm,edge
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
15
+ Classifier: Topic :: System :: Networking
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Dynamic: license-file
20
+
21
+ <div align="center">
22
+
23
+ <img src="logo.svg" width="80" alt="Halyn">
24
+
25
+ # NRP
26
+
27
+ ### Node Reach Protocol
28
+
29
+ **The universal standard for AI-to-world control.**
30
+
31
+ MCP connects LLMs to software.<br>
32
+ **NRP connects LLMs to everything else.**
33
+
34
+ Servers · Robots · Drones · Sensors · Vehicles · APIs · Factories · Smart Homes
35
+
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
37
+ [![Python 3.10+](https://img.shields.io/badge/Python-3.10+-green.svg)](https://python.org)
38
+ [![Tests](https://img.shields.io/badge/Tests-48%20passed-brightgreen.svg)]()
39
+
40
+ </div>
41
+
42
+ ---
43
+
44
+ ## The Problem
45
+
46
+ Every device speaks a different language. SSH for servers. ROS2 for robots. MQTT for sensors. OPC-UA for factories. REST for APIs. Hundreds of protocols. Thousands of SDKs. No standard.
47
+
48
+ MCP standardized software integration. NRP does the same for hardware and physical systems.
49
+
50
+ ## The Protocol
51
+
52
+ 3 methods. That is the interface.
53
+
54
+ ```
55
+ OBSERVE → read state (sensors, metrics, cameras, APIs)
56
+ ACT → change state (commands, movements, writes, calls)
57
+ SHIELD → safety limits (boundaries the AI cannot cross)
58
+ ```
59
+
60
+ Every device becomes a **node**. Every node has an address:
61
+
62
+ ```
63
+ nrp://factory/robot/arm-7
64
+ nrp://farm/sensor/soil-north
65
+ nrp://cloud/api/stripe
66
+ nrp://home/light/kitchen
67
+ nrp://fleet/vehicle/truck-42
68
+ ```
69
+
70
+ Every node **describes itself**. The AI reads the manifest and knows what to do. Zero configuration. Zero documentation.
71
+
72
+ ## Write a Driver in 100 Lines
73
+
74
+ ```python
75
+ from nrp import NRPDriver, NRPManifest, ChannelSpec, ActionSpec, ShieldSpec
76
+
77
+ class MyRobot(NRPDriver):
78
+
79
+ def manifest(self) -> NRPManifest:
80
+ return NRPManifest(
81
+ nrp_id=self._nrp_id,
82
+ manufacturer="Unitree", model="G1",
83
+ observe=[
84
+ ChannelSpec("joints", "float[]", unit="rad", rate="100Hz"),
85
+ ChannelSpec("battery", "int", unit="percent"),
86
+ ChannelSpec("camera", "image", rate="30Hz"),
87
+ ],
88
+ act=[
89
+ ActionSpec("walk", {"speed": "float m/s"}, "Walk forward", dangerous=True),
90
+ ActionSpec("pick", {"target": "string"}, "Pick an object", dangerous=True),
91
+ ActionSpec("stand", {}, "Stand still"),
92
+ ],
93
+ shield=[
94
+ ShieldSpec("max_speed", "limit", 1.5, "m/s"),
95
+ ShieldSpec("workspace", "zone", [0, 0, 10, 10], "meters"),
96
+ ],
97
+ )
98
+
99
+ async def observe(self, channels=None):
100
+ return {"joints": self.read_joints(), "battery": self.read_battery()}
101
+
102
+ async def act(self, command, args):
103
+ if command == "walk":
104
+ return self.walk(args["speed"])
105
+ if command == "pick":
106
+ return self.pick(args["target"])
107
+
108
+ def shield_rules(self):
109
+ return [ShieldRule("max_speed", ShieldType.LIMIT, 1.5)]
110
+ ```
111
+
112
+
113
+
114
+ ## What the AI Sees
115
+
116
+ When a node connects, the AI receives a human-readable description:
117
+
118
+ ```
119
+ Node: nrp://factory/robot/g1-01
120
+ Device: Unitree G1
121
+ Observe (read state):
122
+ joints: float[] (rad) — Joint angles at 100Hz
123
+ battery: int (percent) — Battery level
124
+ camera: image — Camera feed at 30Hz
125
+ Act (commands):
126
+ walk(speed: float m/s) [DANGEROUS] — Walk forward
127
+ pick(target: string) [DANGEROUS] — Pick an object
128
+ stand() — Stand still
129
+ Shield (safety limits):
130
+ max_speed: limit = 1.5 m/s
131
+ workspace: zone = [0, 0, 10, 10] meters
132
+ ```
133
+
134
+
135
+
136
+ ## Real-Time Events
137
+
138
+ Nodes push events. The AI reacts without polling.
139
+
140
+ ```python
141
+ # The node pushes
142
+ await driver.emit("battery_low", Severity.WARNING, percent=8)
143
+ await driver.emit("collision", Severity.EMERGENCY, force=45.2)
144
+
145
+ # The control plane routes
146
+ bus.subscribe("battery_*", alert_handler)
147
+ bus.subscribe("nrp://factory/*", factory_monitor)
148
+ ```
149
+
150
+ **Emergency events bypass all queues** and are processed synchronously.
151
+
152
+ ## Specification
153
+
154
+ | Document | Description |
155
+ |----------|-------------|
156
+ | [IDENTITY.md](spec/IDENTITY.md) | Universal addressing: `nrp://scope/kind/name` |
157
+ | [MANIFEST.md](spec/MANIFEST.md) | Self-describing nodes: channels, actions, shields |
158
+ | [EVENTS.md](spec/EVENTS.md) | Real-time push: severity levels, emergency bypass |
159
+ | [NRP_SPEC.md](spec/NRP_SPEC.md) | Protocol overview |
160
+
161
+ ## Install
162
+
163
+ ```bash
164
+ pip install nrp
165
+ ```
166
+
167
+ ## Examples
168
+
169
+ - [`hello_ssh.py`](examples/hello_ssh.py) — Connect to a server in 60 lines
170
+ - [`multi_node.py`](examples/multi_node.py) — 3 sensors, 1 conversation
171
+
172
+ ## Built With NRP
173
+
174
+ | Project | Description |
175
+ |---------|-------------|
176
+ | [Halyn](https://github.com/ElmadaniS/halyn) | NRP control plane with domain-scoped authorization |
177
+
178
+ ## Why Not Just Use MCP?
179
+
180
+ MCP is brilliant for software. But:
181
+
182
+ - MCP has no concept of **physical safety** (shield rules)
183
+ - MCP has no concept of **real-time events** (push, not pull)
184
+ - MCP has no concept of **self-describing hardware** (manifests)
185
+ - MCP has no concept of **universal device identity** (`nrp://`)
186
+ - MCP tools are defined by the server. NRP tools are declared by the device.
187
+
188
+ NRP complements MCP. A control plane exposes NRP nodes as MCP tools — transparent to the LLM.
189
+
190
+ ## Architecture
191
+
192
+ ```
193
+ Any LLM (Claude, GPT, Ollama, local)
194
+
195
+ │ MCP (software)
196
+
197
+
198
+ Control Plane (e.g. Halyn)
199
+
200
+ │ NRP (physical + digital world)
201
+
202
+ ├──→ Servers (SSH)
203
+ ├──→ Robots (ROS2, Unitree, DJI)
204
+ ├──→ Sensors (MQTT)
205
+ ├──→ APIs (REST, GraphQL — auto-introspected)
206
+ ├──→ Containers (Docker)
207
+ ├──→ Browsers (Chrome CDP)
208
+ ├──→ Factories (OPC-UA, Modbus)
209
+ ├──→ Vehicles (CAN, DDS)
210
+ └──→ Anything with an interface
211
+ ```
212
+
213
+ ## Contributing
214
+
215
+ See [CONTRIBUTING.md](CONTRIBUTING.md). A driver is 4 methods, ~100 lines. If it has an interface, it can be an NRP node.
216
+
217
+ ## License
218
+
219
+ MIT — Free forever. Use it. Build on it.
220
+
221
+ ## Author
222
+
223
+ **Elmadani SALKA**
@@ -0,0 +1,13 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ sdk/python/nrp/__init__.py
5
+ sdk/python/nrp/driver.py
6
+ sdk/python/nrp/events.py
7
+ sdk/python/nrp/identity.py
8
+ sdk/python/nrp/manifest.py
9
+ sdk/python/nrp/py.typed
10
+ sdk/python/nrprotocol.egg-info/PKG-INFO
11
+ sdk/python/nrprotocol.egg-info/SOURCES.txt
12
+ sdk/python/nrprotocol.egg-info/dependency_links.txt
13
+ sdk/python/nrprotocol.egg-info/top_level.txt
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+