signalmesh-meshd 0.1.2__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.
- signalmesh_meshd-0.1.2/LICENSE +21 -0
- signalmesh_meshd-0.1.2/PKG-INFO +131 -0
- signalmesh_meshd-0.1.2/README.md +96 -0
- signalmesh_meshd-0.1.2/pyproject.toml +53 -0
- signalmesh_meshd-0.1.2/setup.cfg +4 -0
- signalmesh_meshd-0.1.2/src/signalmesh_meshd/__init__.py +20 -0
- signalmesh_meshd-0.1.2/src/signalmesh_meshd/__main__.py +5 -0
- signalmesh_meshd-0.1.2/src/signalmesh_meshd/cli.py +164 -0
- signalmesh_meshd-0.1.2/src/signalmesh_meshd/config.py +42 -0
- signalmesh_meshd-0.1.2/src/signalmesh_meshd/daemon.py +108 -0
- signalmesh_meshd-0.1.2/src/signalmesh_meshd/harness.py +132 -0
- signalmesh_meshd-0.1.2/src/signalmesh_meshd/safety.py +23 -0
- signalmesh_meshd-0.1.2/src/signalmesh_meshd.egg-info/PKG-INFO +131 -0
- signalmesh_meshd-0.1.2/src/signalmesh_meshd.egg-info/SOURCES.txt +17 -0
- signalmesh_meshd-0.1.2/src/signalmesh_meshd.egg-info/dependency_links.txt +1 -0
- signalmesh_meshd-0.1.2/src/signalmesh_meshd.egg-info/entry_points.txt +2 -0
- signalmesh_meshd-0.1.2/src/signalmesh_meshd.egg-info/requires.txt +6 -0
- signalmesh_meshd-0.1.2/src/signalmesh_meshd.egg-info/top_level.txt +1 -0
- signalmesh_meshd-0.1.2/tests/test_smoke.py +38 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 acecalisto3
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: signalmesh-meshd
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Outbound-only edge daemon for the SignalMesh Submesh Protocol — wrap local software as callable mesh coords with zero inbound ports.
|
|
5
|
+
Author-email: acecalisto3 <acecalisto3@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://huggingface.co/spaces/acecalisto3/SignalMesh
|
|
8
|
+
Project-URL: HuggingFace Space, https://huggingface.co/spaces/acecalisto3/SignalMesh
|
|
9
|
+
Project-URL: Live App, https://acecalisto3-signalmesh.hf.space
|
|
10
|
+
Project-URL: Live Lander, https://hyperagent.com/s/VpnZ7xsnstxjTfHNoYnoag
|
|
11
|
+
Keywords: mesh,agents,protocol,websocket,cli
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: System :: Networking
|
|
24
|
+
Classifier: Topic :: System :: Systems Administration
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: click>=8.0
|
|
30
|
+
Requires-Dist: websockets>=12
|
|
31
|
+
Requires-Dist: PyYAML>=6.0
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# signalmesh-meshd
|
|
37
|
+
|
|
38
|
+
Outbound-only edge daemon for the SignalMesh Submesh Protocol. It holds one
|
|
39
|
+
persistent WebSocket to the SignalMesh cloud, walks
|
|
40
|
+
`~/.signalmesh/harnesses/*/SKILL.md` for op manifests, and executes
|
|
41
|
+
`call_request`s as local subprocesses — no inbound ports, ever.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pipx install signalmesh-meshd
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Not on PyPI yet? Install straight from the built wheel:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pipx install /path/to/signalmesh_meshd-0.1.2-py3-none-any.whl
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
Pair once — exchanges your SignalMesh key for a `device_id` + `device_token`,
|
|
58
|
+
written to `~/.signalmesh/meshd.yaml` at mode `0600`:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
meshd pair --key=smesh-XXXXXX --name=my-desktop
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Connect — opens the outbound WebSocket, advertises discovered ops, and blocks
|
|
65
|
+
serving `call_request`s until you kill it:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
meshd connect
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Invoke a coord from the cloud (this is what the SignalMesh backend does on
|
|
72
|
+
your behalf when a mesh caller routes to your device):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
curl -X POST https://acecalisto3-signalmesh.hf.space/api/submesh/call \
|
|
76
|
+
-H "Content-Type: application/json" \
|
|
77
|
+
-H "X-SignalMesh-Key: smesh-XXXXXX" \
|
|
78
|
+
-d '{"coord": "submesh.my_desktop.echo_test.say", "input": {"msg": "hello mesh"}}'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Writing a SKILL.md manifest
|
|
82
|
+
|
|
83
|
+
Drop a directory under `~/.signalmesh/harnesses/<harness-name>/` containing a
|
|
84
|
+
`SKILL.md` with YAML frontmatter. meshd parses the frontmatter block, refuses
|
|
85
|
+
any `exec` argv containing shell metacharacters, and advertises one coord per
|
|
86
|
+
subcommand.
|
|
87
|
+
|
|
88
|
+
`~/.signalmesh/harnesses/echo-test/SKILL.md`:
|
|
89
|
+
|
|
90
|
+
```markdown
|
|
91
|
+
---
|
|
92
|
+
binary: /bin/echo
|
|
93
|
+
subcommands:
|
|
94
|
+
- op_id: say
|
|
95
|
+
exec: ["{msg}"]
|
|
96
|
+
summary: "Echo a string back through the mesh."
|
|
97
|
+
params_schema:
|
|
98
|
+
msg: string
|
|
99
|
+
timeout_ms: 5000
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
# echo-test
|
|
103
|
+
|
|
104
|
+
Minimal harness proving the mesh call → subprocess → response round trip.
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This advertises `submesh.<device_name>.echo_test.say`, callable with
|
|
108
|
+
`{"msg": "..."}`.
|
|
109
|
+
|
|
110
|
+
## Safety model
|
|
111
|
+
|
|
112
|
+
Every argv token is checked against a shell-metachar denylist twice: once at
|
|
113
|
+
manifest-load time (before a coord is even advertised) and once again after
|
|
114
|
+
`{param}` substitution (before the subprocess actually runs) — a manifest
|
|
115
|
+
can't sneak a malicious literal past you, and a caller can't sneak one in
|
|
116
|
+
through input params either. Subprocesses always run with `shell=False`.
|
|
117
|
+
Path-typed params are additionally checked against each subcommand's
|
|
118
|
+
`allow_paths` glob list; anything that resolves outside the allowlist is
|
|
119
|
+
rejected before it ever reaches `argv`.
|
|
120
|
+
|
|
121
|
+
## Links
|
|
122
|
+
|
|
123
|
+
- 🤗 **HuggingFace Space** (canonical): https://huggingface.co/spaces/acecalisto3/SignalMesh
|
|
124
|
+
- Live app (API endpoints): https://acecalisto3-signalmesh.hf.space
|
|
125
|
+
- Interactive lander: https://hyperagent.com/s/VpnZ7xsnstxjTfHNoYnoag
|
|
126
|
+
- Protocol docs: see the Space's `/api/submesh/*` routes (`device/pair`,
|
|
127
|
+
`device/{id}/revoke`, `devices`, `edge`, `call`)
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# signalmesh-meshd
|
|
2
|
+
|
|
3
|
+
Outbound-only edge daemon for the SignalMesh Submesh Protocol. It holds one
|
|
4
|
+
persistent WebSocket to the SignalMesh cloud, walks
|
|
5
|
+
`~/.signalmesh/harnesses/*/SKILL.md` for op manifests, and executes
|
|
6
|
+
`call_request`s as local subprocesses — no inbound ports, ever.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pipx install signalmesh-meshd
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Not on PyPI yet? Install straight from the built wheel:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pipx install /path/to/signalmesh_meshd-0.1.2-py3-none-any.whl
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick start
|
|
21
|
+
|
|
22
|
+
Pair once — exchanges your SignalMesh key for a `device_id` + `device_token`,
|
|
23
|
+
written to `~/.signalmesh/meshd.yaml` at mode `0600`:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
meshd pair --key=smesh-XXXXXX --name=my-desktop
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Connect — opens the outbound WebSocket, advertises discovered ops, and blocks
|
|
30
|
+
serving `call_request`s until you kill it:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
meshd connect
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Invoke a coord from the cloud (this is what the SignalMesh backend does on
|
|
37
|
+
your behalf when a mesh caller routes to your device):
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
curl -X POST https://acecalisto3-signalmesh.hf.space/api/submesh/call \
|
|
41
|
+
-H "Content-Type: application/json" \
|
|
42
|
+
-H "X-SignalMesh-Key: smesh-XXXXXX" \
|
|
43
|
+
-d '{"coord": "submesh.my_desktop.echo_test.say", "input": {"msg": "hello mesh"}}'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Writing a SKILL.md manifest
|
|
47
|
+
|
|
48
|
+
Drop a directory under `~/.signalmesh/harnesses/<harness-name>/` containing a
|
|
49
|
+
`SKILL.md` with YAML frontmatter. meshd parses the frontmatter block, refuses
|
|
50
|
+
any `exec` argv containing shell metacharacters, and advertises one coord per
|
|
51
|
+
subcommand.
|
|
52
|
+
|
|
53
|
+
`~/.signalmesh/harnesses/echo-test/SKILL.md`:
|
|
54
|
+
|
|
55
|
+
```markdown
|
|
56
|
+
---
|
|
57
|
+
binary: /bin/echo
|
|
58
|
+
subcommands:
|
|
59
|
+
- op_id: say
|
|
60
|
+
exec: ["{msg}"]
|
|
61
|
+
summary: "Echo a string back through the mesh."
|
|
62
|
+
params_schema:
|
|
63
|
+
msg: string
|
|
64
|
+
timeout_ms: 5000
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
# echo-test
|
|
68
|
+
|
|
69
|
+
Minimal harness proving the mesh call → subprocess → response round trip.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
This advertises `submesh.<device_name>.echo_test.say`, callable with
|
|
73
|
+
`{"msg": "..."}`.
|
|
74
|
+
|
|
75
|
+
## Safety model
|
|
76
|
+
|
|
77
|
+
Every argv token is checked against a shell-metachar denylist twice: once at
|
|
78
|
+
manifest-load time (before a coord is even advertised) and once again after
|
|
79
|
+
`{param}` substitution (before the subprocess actually runs) — a manifest
|
|
80
|
+
can't sneak a malicious literal past you, and a caller can't sneak one in
|
|
81
|
+
through input params either. Subprocesses always run with `shell=False`.
|
|
82
|
+
Path-typed params are additionally checked against each subcommand's
|
|
83
|
+
`allow_paths` glob list; anything that resolves outside the allowlist is
|
|
84
|
+
rejected before it ever reaches `argv`.
|
|
85
|
+
|
|
86
|
+
## Links
|
|
87
|
+
|
|
88
|
+
- 🤗 **HuggingFace Space** (canonical): https://huggingface.co/spaces/acecalisto3/SignalMesh
|
|
89
|
+
- Live app (API endpoints): https://acecalisto3-signalmesh.hf.space
|
|
90
|
+
- Interactive lander: https://hyperagent.com/s/VpnZ7xsnstxjTfHNoYnoag
|
|
91
|
+
- Protocol docs: see the Space's `/api/submesh/*` routes (`device/pair`,
|
|
92
|
+
`device/{id}/revoke`, `devices`, `edge`, `call`)
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "signalmesh-meshd"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Outbound-only edge daemon for the SignalMesh Submesh Protocol — wrap local software as callable mesh coords with zero inbound ports."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "acecalisto3", email = "acecalisto3@gmail.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["mesh", "agents", "protocol", "websocket", "cli"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Environment :: Console",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: OS Independent",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
24
|
+
"Programming Language :: Python :: 3.9",
|
|
25
|
+
"Programming Language :: Python :: 3.10",
|
|
26
|
+
"Programming Language :: Python :: 3.11",
|
|
27
|
+
"Programming Language :: Python :: 3.12",
|
|
28
|
+
"Topic :: System :: Networking",
|
|
29
|
+
"Topic :: System :: Systems Administration",
|
|
30
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
31
|
+
]
|
|
32
|
+
dependencies = [
|
|
33
|
+
"click>=8.0",
|
|
34
|
+
"websockets>=12",
|
|
35
|
+
"PyYAML>=6.0",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.optional-dependencies]
|
|
39
|
+
test = [
|
|
40
|
+
"pytest>=7.0",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
meshd = "signalmesh_meshd.cli:main"
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://huggingface.co/spaces/acecalisto3/SignalMesh"
|
|
48
|
+
"HuggingFace Space" = "https://huggingface.co/spaces/acecalisto3/SignalMesh"
|
|
49
|
+
"Live App" = "https://acecalisto3-signalmesh.hf.space"
|
|
50
|
+
"Live Lander" = "https://hyperagent.com/s/VpnZ7xsnstxjTfHNoYnoag"
|
|
51
|
+
|
|
52
|
+
[tool.setuptools.packages.find]
|
|
53
|
+
where = ["src"]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""signalmesh-meshd — outbound-only edge daemon for the SignalMesh Submesh Protocol.
|
|
2
|
+
|
|
3
|
+
Bridges local software into the SignalMesh cloud without opening a single
|
|
4
|
+
inbound port: holds one persistent outbound WebSocket, advertises locally
|
|
5
|
+
discovered ops (from ~/.signalmesh/harnesses/*/SKILL.md), and executes
|
|
6
|
+
call_requests as local subprocesses.
|
|
7
|
+
"""
|
|
8
|
+
from .cli import cli, main
|
|
9
|
+
from .config import MeshdConfig
|
|
10
|
+
from .daemon import run_edge_client
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.2"
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"__version__",
|
|
16
|
+
"cli",
|
|
17
|
+
"main",
|
|
18
|
+
"MeshdConfig",
|
|
19
|
+
"run_edge_client",
|
|
20
|
+
]
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""meshd CLI — click-based entry point.
|
|
2
|
+
|
|
3
|
+
meshd pair --key=smesh-XXXXXX --name=my-desktop
|
|
4
|
+
meshd connect # long-running WS client
|
|
5
|
+
meshd status # show current pairing state
|
|
6
|
+
meshd disconnect --revoke # invalidate device server-side
|
|
7
|
+
"""
|
|
8
|
+
import asyncio
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import random
|
|
12
|
+
import sys
|
|
13
|
+
import time
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import click
|
|
18
|
+
except ImportError:
|
|
19
|
+
print("meshd requires: pip install click websockets PyYAML", file=sys.stderr)
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
|
|
22
|
+
from .config import CLOUD_HTTPS, CONFIG_DIR, CONFIG_PATH, MeshdConfig
|
|
23
|
+
from .daemon import run_edge_client
|
|
24
|
+
from .safety import log
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import yaml
|
|
28
|
+
except ImportError:
|
|
29
|
+
yaml = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@click.group()
|
|
33
|
+
def cli() -> None:
|
|
34
|
+
"""meshd — SignalMesh outbound-only edge daemon."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@cli.command()
|
|
38
|
+
@click.option("--key", required=True,
|
|
39
|
+
help="Your X-SignalMesh-Key (free tier or paid).")
|
|
40
|
+
@click.option("--name", default=None,
|
|
41
|
+
help="Device name (default: this machine's hostname).")
|
|
42
|
+
@click.option("--cloud", default=CLOUD_HTTPS,
|
|
43
|
+
help=f"SignalMesh cloud base URL (default: {CLOUD_HTTPS}).")
|
|
44
|
+
def pair(key: str, name: Optional[str], cloud: str) -> None:
|
|
45
|
+
"""One-time pairing — exchanges key for device_id + device_token."""
|
|
46
|
+
import socket
|
|
47
|
+
import urllib.request
|
|
48
|
+
|
|
49
|
+
device_name = name or socket.gethostname()
|
|
50
|
+
body = json.dumps({"user_key": key, "device_name": device_name}).encode()
|
|
51
|
+
req = urllib.request.Request(
|
|
52
|
+
f"{cloud.rstrip('/')}/api/submesh/device/pair",
|
|
53
|
+
data=body,
|
|
54
|
+
headers={
|
|
55
|
+
"Content-Type": "application/json",
|
|
56
|
+
"X-SignalMesh-Key": key,
|
|
57
|
+
},
|
|
58
|
+
method="POST",
|
|
59
|
+
)
|
|
60
|
+
try:
|
|
61
|
+
with urllib.request.urlopen(req, timeout=15) as r:
|
|
62
|
+
result = json.loads(r.read())
|
|
63
|
+
except Exception as e:
|
|
64
|
+
log.error("Pairing failed: %s", e)
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
|
|
67
|
+
ws_url = cloud.replace("https://", "wss://").rstrip("/") + "/api/submesh/edge"
|
|
68
|
+
CONFIG_DIR.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
CONFIG_PATH.write_text(yaml.safe_dump({
|
|
70
|
+
"device_id": result["device_id"],
|
|
71
|
+
"device_token": result["device_token"],
|
|
72
|
+
"device_name": result["device_name"],
|
|
73
|
+
"cloud_url": ws_url,
|
|
74
|
+
"exposed_harnesses": {},
|
|
75
|
+
}))
|
|
76
|
+
os.chmod(CONFIG_PATH, 0o600)
|
|
77
|
+
click.echo(f"Paired as {device_name}")
|
|
78
|
+
click.echo(f" device_id: {result['device_id']}")
|
|
79
|
+
click.echo(f" config: {CONFIG_PATH} (mode 0600)")
|
|
80
|
+
click.echo(f" cloud: {ws_url}")
|
|
81
|
+
click.echo(f" next: meshd connect")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _run_with_retry(cfg) -> None:
|
|
85
|
+
"""Run edge client with exponential-backoff retry. Ctrl+C exits cleanly.
|
|
86
|
+
Fatal after 20 consecutive failures."""
|
|
87
|
+
attempt = 0
|
|
88
|
+
while True:
|
|
89
|
+
try:
|
|
90
|
+
asyncio.run(run_edge_client(cfg))
|
|
91
|
+
log.info("Edge client returned normally; exiting.")
|
|
92
|
+
return
|
|
93
|
+
except KeyboardInterrupt:
|
|
94
|
+
log.info("Shutting down (KeyboardInterrupt).")
|
|
95
|
+
return
|
|
96
|
+
except Exception as e:
|
|
97
|
+
attempt += 1
|
|
98
|
+
if attempt > 20:
|
|
99
|
+
log.error("Too many failures (%d); giving up. Last: %s: %s",
|
|
100
|
+
attempt, type(e).__name__, e)
|
|
101
|
+
raise
|
|
102
|
+
delay = min(60.0, 0.5 * (2 ** min(attempt, 7))) * (0.5 + random.random())
|
|
103
|
+
log.warning("Edge client failed (attempt %d): %s: %s — retrying in %.1fs",
|
|
104
|
+
attempt, type(e).__name__, e, delay)
|
|
105
|
+
time.sleep(delay)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@cli.command()
|
|
109
|
+
def connect() -> None:
|
|
110
|
+
"""Connect to the cloud mesh and serve local capabilities."""
|
|
111
|
+
cfg = MeshdConfig.load()
|
|
112
|
+
click.echo(f"Connecting as {cfg.device_name} → {cfg.cloud_url}")
|
|
113
|
+
_run_with_retry(cfg)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@cli.command()
|
|
117
|
+
def status() -> None:
|
|
118
|
+
"""Show current pairing state."""
|
|
119
|
+
if not CONFIG_PATH.exists():
|
|
120
|
+
click.echo("Not paired. Run: meshd pair --key=...")
|
|
121
|
+
return
|
|
122
|
+
cfg = MeshdConfig.load()
|
|
123
|
+
click.echo(f"Paired as {cfg.device_name} ({cfg.device_id})")
|
|
124
|
+
click.echo(f" cloud: {cfg.cloud_url}")
|
|
125
|
+
click.echo(f" config: {CONFIG_PATH}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@cli.command()
|
|
129
|
+
@click.option("--revoke", is_flag=True,
|
|
130
|
+
help="Also call the server-side revoke endpoint to invalidate the device.")
|
|
131
|
+
@click.option("--key", default=None,
|
|
132
|
+
help="Your pairing key (required when --revoke is set — the mesh-issued "
|
|
133
|
+
"device_token cannot authorize revocation of its own device).")
|
|
134
|
+
def disconnect(revoke: bool, key: str) -> None:
|
|
135
|
+
"""Delete local config; optionally revoke server-side."""
|
|
136
|
+
if not CONFIG_PATH.exists():
|
|
137
|
+
click.echo("No config to remove. Already disconnected.")
|
|
138
|
+
return
|
|
139
|
+
if revoke:
|
|
140
|
+
if not key:
|
|
141
|
+
raise click.UsageError("--revoke requires --key=<pairing-key> "
|
|
142
|
+
"(the same key you passed to `meshd pair`).")
|
|
143
|
+
cfg = MeshdConfig.load()
|
|
144
|
+
import urllib.request
|
|
145
|
+
cloud = cfg.cloud_url.replace("wss://", "https://").split("/api/")[0]
|
|
146
|
+
revoke_url = f"{cloud}/api/submesh/device/{cfg.device_id}/revoke"
|
|
147
|
+
req = urllib.request.Request(revoke_url, method="POST",
|
|
148
|
+
headers={"X-SignalMesh-Key": key})
|
|
149
|
+
try:
|
|
150
|
+
urllib.request.urlopen(req, timeout=8)
|
|
151
|
+
click.echo(f"Revoked device {cfg.device_id} on server.")
|
|
152
|
+
except Exception as e:
|
|
153
|
+
click.echo(f"Revoke request failed (proceeding with local disconnect): {e}")
|
|
154
|
+
CONFIG_PATH.unlink(missing_ok=True)
|
|
155
|
+
click.echo(f"Local config removed: {CONFIG_PATH}")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def main() -> None:
|
|
159
|
+
"""Console-script entry point (`meshd` and `python -m signalmesh_meshd`)."""
|
|
160
|
+
cli()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
main()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Config load/save for meshd — pairing state lives at ~/.signalmesh/meshd.yaml."""
|
|
2
|
+
import sys
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
import yaml
|
|
8
|
+
except ImportError:
|
|
9
|
+
print("meshd requires: pip install click websockets PyYAML", file=sys.stderr)
|
|
10
|
+
sys.exit(1)
|
|
11
|
+
|
|
12
|
+
from .safety import log
|
|
13
|
+
|
|
14
|
+
CONFIG_DIR = Path.home() / ".signalmesh"
|
|
15
|
+
CONFIG_PATH = CONFIG_DIR / "meshd.yaml"
|
|
16
|
+
HARNESS_DIR = CONFIG_DIR / "harnesses"
|
|
17
|
+
CLOUD_HTTPS = "https://acecalisto3-signalmesh.hf.space"
|
|
18
|
+
CLOUD_WSS = "wss://acecalisto3-signalmesh.hf.space/api/submesh/edge"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class MeshdConfig:
|
|
23
|
+
device_id: str
|
|
24
|
+
device_token: str
|
|
25
|
+
device_name: str
|
|
26
|
+
cloud_url: str = CLOUD_WSS
|
|
27
|
+
exposed_harnesses: dict = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def load(cls) -> "MeshdConfig":
|
|
31
|
+
if not CONFIG_PATH.exists():
|
|
32
|
+
log.error("No config at %s — run `meshd pair --key=...` first.",
|
|
33
|
+
CONFIG_PATH)
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
raw = yaml.safe_load(CONFIG_PATH.read_text())
|
|
36
|
+
return cls(
|
|
37
|
+
device_id=raw["device_id"],
|
|
38
|
+
device_token=raw["device_token"],
|
|
39
|
+
device_name=raw["device_name"],
|
|
40
|
+
cloud_url=raw.get("cloud_url", CLOUD_WSS),
|
|
41
|
+
exposed_harnesses=raw.get("exposed_harnesses", {}),
|
|
42
|
+
)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""The connect loop: WebSocket handshake, capability advertise, heartbeat,
|
|
2
|
+
and message dispatch (call_request / revoke / ack)."""
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import random
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import websockets
|
|
11
|
+
from websockets.exceptions import ConnectionClosed
|
|
12
|
+
except ImportError:
|
|
13
|
+
print("meshd requires: pip install click websockets PyYAML", file=sys.stderr)
|
|
14
|
+
sys.exit(1)
|
|
15
|
+
|
|
16
|
+
from .config import MeshdConfig
|
|
17
|
+
from .harness import _execute_op, _load_local_ops
|
|
18
|
+
from .safety import log
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
async def run_edge_client(cfg: MeshdConfig) -> None:
|
|
22
|
+
attempt = 0
|
|
23
|
+
while True:
|
|
24
|
+
try:
|
|
25
|
+
log.info("Connecting to %s", cfg.cloud_url)
|
|
26
|
+
async with websockets.connect(cfg.cloud_url,
|
|
27
|
+
ping_interval=20,
|
|
28
|
+
ping_timeout=10) as ws:
|
|
29
|
+
# ── Handshake: hello ────────────────────────────────
|
|
30
|
+
await ws.send(json.dumps({
|
|
31
|
+
"type": "hello",
|
|
32
|
+
"device_id": cfg.device_id,
|
|
33
|
+
"device_token": cfg.device_token,
|
|
34
|
+
"device_name": cfg.device_name,
|
|
35
|
+
"meshd_version": "0.1.0",
|
|
36
|
+
"platform": sys.platform,
|
|
37
|
+
}))
|
|
38
|
+
resp = json.loads(await ws.recv())
|
|
39
|
+
if resp.get("type") == "error":
|
|
40
|
+
log.error("Auth rejected: %s (fatal, not retrying)",
|
|
41
|
+
resp.get("code", "unknown"))
|
|
42
|
+
return
|
|
43
|
+
if resp.get("type") != "auth_accepted":
|
|
44
|
+
log.error("Unexpected server response: %s", resp)
|
|
45
|
+
return
|
|
46
|
+
log.info("Connected as %s (device %s). session_ttl_ms=%s",
|
|
47
|
+
cfg.device_name, cfg.device_id,
|
|
48
|
+
resp.get("session_ttl_ms"))
|
|
49
|
+
attempt = 0 # reset backoff on successful connect
|
|
50
|
+
|
|
51
|
+
# ── Advertise capabilities ──────────────────────────
|
|
52
|
+
# Phase 2 MVP: empty ops list. Phase 2.1 walks
|
|
53
|
+
# ~/.signalmesh/harnesses/*/SKILL.md and emits one op per
|
|
54
|
+
# subcommand group. See core/nicoli_catalog.py server-side
|
|
55
|
+
# for the same extraction pattern.
|
|
56
|
+
ops = _load_local_ops(cfg)
|
|
57
|
+
await ws.send(json.dumps({
|
|
58
|
+
"type": "capabilities_advertise",
|
|
59
|
+
"device_id": cfg.device_id,
|
|
60
|
+
"ops": ops,
|
|
61
|
+
}))
|
|
62
|
+
ack = json.loads(await ws.recv())
|
|
63
|
+
log.info("Advertised %d ops. Server ack: %s",
|
|
64
|
+
len(ops), ack.get("op_count"))
|
|
65
|
+
|
|
66
|
+
# ── Heartbeat coroutine ─────────────────────────────
|
|
67
|
+
async def heartbeat_loop():
|
|
68
|
+
while True:
|
|
69
|
+
await asyncio.sleep(30)
|
|
70
|
+
try:
|
|
71
|
+
await ws.send(json.dumps({
|
|
72
|
+
"type": "heartbeat",
|
|
73
|
+
"device_id": cfg.device_id,
|
|
74
|
+
"ts": time.time(),
|
|
75
|
+
}))
|
|
76
|
+
except Exception:
|
|
77
|
+
break
|
|
78
|
+
heartbeat_task = asyncio.create_task(heartbeat_loop())
|
|
79
|
+
|
|
80
|
+
# ── Main receive loop ───────────────────────────────
|
|
81
|
+
try:
|
|
82
|
+
async for raw in ws:
|
|
83
|
+
msg = json.loads(raw)
|
|
84
|
+
mtype = msg.get("type", "")
|
|
85
|
+
if mtype == "call_request":
|
|
86
|
+
log.info("call_request coord=%s call_id=%s",
|
|
87
|
+
msg.get("coord"), msg.get("call_id"))
|
|
88
|
+
result = await _execute_op(msg.get("coord",""),
|
|
89
|
+
msg.get("input",{}) or {})
|
|
90
|
+
await ws.send(json.dumps({"type":"call_result",
|
|
91
|
+
"call_id":msg.get("call_id"),
|
|
92
|
+
**result}))
|
|
93
|
+
elif mtype == "revoke":
|
|
94
|
+
log.warning("Server sent revoke — closing.")
|
|
95
|
+
break
|
|
96
|
+
elif mtype == "ack":
|
|
97
|
+
pass
|
|
98
|
+
else:
|
|
99
|
+
log.debug("Unhandled server message: %s", mtype)
|
|
100
|
+
finally:
|
|
101
|
+
heartbeat_task.cancel()
|
|
102
|
+
|
|
103
|
+
except (ConnectionClosed, OSError, asyncio.TimeoutError) as e:
|
|
104
|
+
delay = min(30, 1.5 * (2 ** attempt)) + random.uniform(0, 1)
|
|
105
|
+
attempt += 1
|
|
106
|
+
log.warning("Connection lost (%s); reconnecting in %.1fs",
|
|
107
|
+
type(e).__name__, delay)
|
|
108
|
+
await asyncio.sleep(delay)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""SKILL.md walker + op execution.
|
|
2
|
+
|
|
3
|
+
Walks ~/.signalmesh/harnesses/*/SKILL.md for YAML-frontmatter manifests,
|
|
4
|
+
builds the ops list advertised to the cloud, and executes call_requests as
|
|
5
|
+
local subprocesses (shell=False) once the cloud routes a call to a known
|
|
6
|
+
coord.
|
|
7
|
+
"""
|
|
8
|
+
import asyncio
|
|
9
|
+
import os
|
|
10
|
+
import re as _re
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
import yaml
|
|
16
|
+
except ImportError: # pragma: no cover - guarded identically in config.py
|
|
17
|
+
yaml = None
|
|
18
|
+
|
|
19
|
+
from .config import HARNESS_DIR, MeshdConfig
|
|
20
|
+
from .safety import has_shell_metachars, log, path_under
|
|
21
|
+
|
|
22
|
+
_OPS_INDEX: dict = {}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _load_local_ops(cfg: MeshdConfig) -> list:
|
|
26
|
+
"""
|
|
27
|
+
Walk ~/.signalmesh/harnesses/*/SKILL.md for YAML-frontmatter manifests.
|
|
28
|
+
Populates _OPS_INDEX for the call_request handler. Refuses argv tokens
|
|
29
|
+
containing shell metacharacters.
|
|
30
|
+
"""
|
|
31
|
+
ops = []
|
|
32
|
+
_OPS_INDEX.clear()
|
|
33
|
+
if not HARNESS_DIR.exists():
|
|
34
|
+
return ops
|
|
35
|
+
for hd in HARNESS_DIR.iterdir():
|
|
36
|
+
if not hd.is_dir():
|
|
37
|
+
continue
|
|
38
|
+
skill_md = hd / "SKILL.md"
|
|
39
|
+
if not skill_md.exists():
|
|
40
|
+
continue
|
|
41
|
+
try:
|
|
42
|
+
content = skill_md.read_text()
|
|
43
|
+
m = _re.match(r'^---\n(.*?)\n---', content, _re.DOTALL)
|
|
44
|
+
if not m:
|
|
45
|
+
continue
|
|
46
|
+
manifest = yaml.safe_load(m.group(1)) or {}
|
|
47
|
+
except Exception as e:
|
|
48
|
+
log.warning("skipping %s: %s", skill_md, e)
|
|
49
|
+
continue
|
|
50
|
+
binary = manifest.get("binary", "")
|
|
51
|
+
harness_name = hd.name
|
|
52
|
+
for sub in manifest.get("subcommands", []) or []:
|
|
53
|
+
op_id = sub.get("op_id")
|
|
54
|
+
argv = sub.get("exec", [])
|
|
55
|
+
if not op_id or not isinstance(argv, list) or not argv:
|
|
56
|
+
continue
|
|
57
|
+
if has_shell_metachars(argv):
|
|
58
|
+
log.error("refusing %s.%s: shell metachars in argv", harness_name, op_id)
|
|
59
|
+
continue
|
|
60
|
+
device_slug = cfg.device_name.lower().replace("-", "_")
|
|
61
|
+
coord = f"submesh.{device_slug}.{harness_name}.{op_id}".lower().replace("-", "_")
|
|
62
|
+
ops.append({
|
|
63
|
+
"coord": coord,
|
|
64
|
+
"harness": harness_name,
|
|
65
|
+
"op_id": op_id,
|
|
66
|
+
"method": "EXEC",
|
|
67
|
+
"summary": sub.get("summary", ""),
|
|
68
|
+
"params_schema": sub.get("params_schema", {}),
|
|
69
|
+
"streaming": True,
|
|
70
|
+
})
|
|
71
|
+
_OPS_INDEX[coord] = {
|
|
72
|
+
"binary": binary,
|
|
73
|
+
"argv": argv,
|
|
74
|
+
"allow_paths": sub.get("allow_paths", []),
|
|
75
|
+
"timeout_ms": sub.get("timeout_ms", 60000),
|
|
76
|
+
"params_schema": sub.get("params_schema", {}),
|
|
77
|
+
}
|
|
78
|
+
return ops
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def _execute_op(coord: str, input_params: dict) -> dict:
|
|
82
|
+
"""Resolve coord → argv → subprocess.run (shell=False) → call_result envelope."""
|
|
83
|
+
entry = _OPS_INDEX.get(coord)
|
|
84
|
+
if not entry:
|
|
85
|
+
return {"status": "error", "error": {"code": "coord_not_found", "message": coord}}
|
|
86
|
+
binary = entry["binary"]
|
|
87
|
+
if binary and not os.access(binary, os.X_OK):
|
|
88
|
+
return {"status": "error",
|
|
89
|
+
"error": {"code": "binary_not_executable", "message": binary}}
|
|
90
|
+
resolved = [binary] if binary else []
|
|
91
|
+
params_schema = entry.get("params_schema", {})
|
|
92
|
+
allow_paths = entry.get("allow_paths", [])
|
|
93
|
+
for tok in entry["argv"]:
|
|
94
|
+
s = str(tok)
|
|
95
|
+
if s.startswith("{") and s.endswith("}"):
|
|
96
|
+
key = s[1:-1]
|
|
97
|
+
val = input_params.get(key, "")
|
|
98
|
+
if params_schema.get(key) == "path" and allow_paths:
|
|
99
|
+
p = Path(str(val)).expanduser().resolve()
|
|
100
|
+
allowed = any(True for pattern in allow_paths
|
|
101
|
+
if path_under(p, Path(pattern.split("**")[0]).expanduser().resolve()))
|
|
102
|
+
if not allowed:
|
|
103
|
+
return {"status": "error", "error": {"code": "path_outside_allowlist",
|
|
104
|
+
"message": f"{val!r} not in {allow_paths}"}}
|
|
105
|
+
val = str(p)
|
|
106
|
+
resolved.append(str(val))
|
|
107
|
+
else:
|
|
108
|
+
resolved.append(s)
|
|
109
|
+
if has_shell_metachars(resolved):
|
|
110
|
+
return {"status": "error", "error": {"code": "shell_metachar_after_subst",
|
|
111
|
+
"message": " ".join(resolved)}}
|
|
112
|
+
started = time.time()
|
|
113
|
+
try:
|
|
114
|
+
proc = await asyncio.create_subprocess_exec(
|
|
115
|
+
*resolved, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT)
|
|
116
|
+
try:
|
|
117
|
+
stdout, _ = await asyncio.wait_for(proc.communicate(),
|
|
118
|
+
timeout=entry["timeout_ms"] / 1000.0)
|
|
119
|
+
except asyncio.TimeoutError:
|
|
120
|
+
proc.kill()
|
|
121
|
+
return {"status": "error", "duration_ms": int((time.time() - started) * 1000),
|
|
122
|
+
"error": {"code": "timeout"}}
|
|
123
|
+
except FileNotFoundError:
|
|
124
|
+
return {"status": "error", "error": {"code": "binary_not_found",
|
|
125
|
+
"message": resolved[0] if resolved else ""}}
|
|
126
|
+
duration_ms = int((time.time() - started) * 1000)
|
|
127
|
+
out = stdout.decode(errors="replace") if stdout else ""
|
|
128
|
+
if proc.returncode == 0:
|
|
129
|
+
return {"status": "success", "duration_ms": duration_ms, "exit_code": 0,
|
|
130
|
+
"output": {"stdout": out[-8000:], "argv": resolved}}
|
|
131
|
+
return {"status": "error", "duration_ms": duration_ms, "exit_code": proc.returncode,
|
|
132
|
+
"error": {"code": "nonzero_exit", "message": out[-2000:]}}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Shared logging + the shell-metachar guard used by harness.py before any
|
|
2
|
+
subprocess exec, both at advertise-time (argv literals) and at call-time
|
|
3
|
+
(argv after {param} substitution)."""
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
logging.basicConfig(level=logging.INFO,
|
|
8
|
+
format="%(asctime)s %(levelname)s meshd: %(message)s")
|
|
9
|
+
log = logging.getLogger("meshd")
|
|
10
|
+
|
|
11
|
+
SHELL_METACHARS = set(";&|`$()<>\\\"'\n")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def has_shell_metachars(tokens) -> bool:
|
|
15
|
+
return any(any(c in SHELL_METACHARS for c in str(tok)) for tok in tokens)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def path_under(p: Path, base: Path) -> bool:
|
|
19
|
+
try:
|
|
20
|
+
p.relative_to(base)
|
|
21
|
+
return True
|
|
22
|
+
except ValueError:
|
|
23
|
+
return False
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: signalmesh-meshd
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Outbound-only edge daemon for the SignalMesh Submesh Protocol — wrap local software as callable mesh coords with zero inbound ports.
|
|
5
|
+
Author-email: acecalisto3 <acecalisto3@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://huggingface.co/spaces/acecalisto3/SignalMesh
|
|
8
|
+
Project-URL: HuggingFace Space, https://huggingface.co/spaces/acecalisto3/SignalMesh
|
|
9
|
+
Project-URL: Live App, https://acecalisto3-signalmesh.hf.space
|
|
10
|
+
Project-URL: Live Lander, https://hyperagent.com/s/VpnZ7xsnstxjTfHNoYnoag
|
|
11
|
+
Keywords: mesh,agents,protocol,websocket,cli
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: System :: Networking
|
|
24
|
+
Classifier: Topic :: System :: Systems Administration
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Requires-Python: >=3.9
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Requires-Dist: click>=8.0
|
|
30
|
+
Requires-Dist: websockets>=12
|
|
31
|
+
Requires-Dist: PyYAML>=6.0
|
|
32
|
+
Provides-Extra: test
|
|
33
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# signalmesh-meshd
|
|
37
|
+
|
|
38
|
+
Outbound-only edge daemon for the SignalMesh Submesh Protocol. It holds one
|
|
39
|
+
persistent WebSocket to the SignalMesh cloud, walks
|
|
40
|
+
`~/.signalmesh/harnesses/*/SKILL.md` for op manifests, and executes
|
|
41
|
+
`call_request`s as local subprocesses — no inbound ports, ever.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pipx install signalmesh-meshd
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Not on PyPI yet? Install straight from the built wheel:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
pipx install /path/to/signalmesh_meshd-0.1.2-py3-none-any.whl
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Quick start
|
|
56
|
+
|
|
57
|
+
Pair once — exchanges your SignalMesh key for a `device_id` + `device_token`,
|
|
58
|
+
written to `~/.signalmesh/meshd.yaml` at mode `0600`:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
meshd pair --key=smesh-XXXXXX --name=my-desktop
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Connect — opens the outbound WebSocket, advertises discovered ops, and blocks
|
|
65
|
+
serving `call_request`s until you kill it:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
meshd connect
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Invoke a coord from the cloud (this is what the SignalMesh backend does on
|
|
72
|
+
your behalf when a mesh caller routes to your device):
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
curl -X POST https://acecalisto3-signalmesh.hf.space/api/submesh/call \
|
|
76
|
+
-H "Content-Type: application/json" \
|
|
77
|
+
-H "X-SignalMesh-Key: smesh-XXXXXX" \
|
|
78
|
+
-d '{"coord": "submesh.my_desktop.echo_test.say", "input": {"msg": "hello mesh"}}'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Writing a SKILL.md manifest
|
|
82
|
+
|
|
83
|
+
Drop a directory under `~/.signalmesh/harnesses/<harness-name>/` containing a
|
|
84
|
+
`SKILL.md` with YAML frontmatter. meshd parses the frontmatter block, refuses
|
|
85
|
+
any `exec` argv containing shell metacharacters, and advertises one coord per
|
|
86
|
+
subcommand.
|
|
87
|
+
|
|
88
|
+
`~/.signalmesh/harnesses/echo-test/SKILL.md`:
|
|
89
|
+
|
|
90
|
+
```markdown
|
|
91
|
+
---
|
|
92
|
+
binary: /bin/echo
|
|
93
|
+
subcommands:
|
|
94
|
+
- op_id: say
|
|
95
|
+
exec: ["{msg}"]
|
|
96
|
+
summary: "Echo a string back through the mesh."
|
|
97
|
+
params_schema:
|
|
98
|
+
msg: string
|
|
99
|
+
timeout_ms: 5000
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
# echo-test
|
|
103
|
+
|
|
104
|
+
Minimal harness proving the mesh call → subprocess → response round trip.
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
This advertises `submesh.<device_name>.echo_test.say`, callable with
|
|
108
|
+
`{"msg": "..."}`.
|
|
109
|
+
|
|
110
|
+
## Safety model
|
|
111
|
+
|
|
112
|
+
Every argv token is checked against a shell-metachar denylist twice: once at
|
|
113
|
+
manifest-load time (before a coord is even advertised) and once again after
|
|
114
|
+
`{param}` substitution (before the subprocess actually runs) — a manifest
|
|
115
|
+
can't sneak a malicious literal past you, and a caller can't sneak one in
|
|
116
|
+
through input params either. Subprocesses always run with `shell=False`.
|
|
117
|
+
Path-typed params are additionally checked against each subcommand's
|
|
118
|
+
`allow_paths` glob list; anything that resolves outside the allowlist is
|
|
119
|
+
rejected before it ever reaches `argv`.
|
|
120
|
+
|
|
121
|
+
## Links
|
|
122
|
+
|
|
123
|
+
- 🤗 **HuggingFace Space** (canonical): https://huggingface.co/spaces/acecalisto3/SignalMesh
|
|
124
|
+
- Live app (API endpoints): https://acecalisto3-signalmesh.hf.space
|
|
125
|
+
- Interactive lander: https://hyperagent.com/s/VpnZ7xsnstxjTfHNoYnoag
|
|
126
|
+
- Protocol docs: see the Space's `/api/submesh/*` routes (`device/pair`,
|
|
127
|
+
`device/{id}/revoke`, `devices`, `edge`, `call`)
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/signalmesh_meshd/__init__.py
|
|
5
|
+
src/signalmesh_meshd/__main__.py
|
|
6
|
+
src/signalmesh_meshd/cli.py
|
|
7
|
+
src/signalmesh_meshd/config.py
|
|
8
|
+
src/signalmesh_meshd/daemon.py
|
|
9
|
+
src/signalmesh_meshd/harness.py
|
|
10
|
+
src/signalmesh_meshd/safety.py
|
|
11
|
+
src/signalmesh_meshd.egg-info/PKG-INFO
|
|
12
|
+
src/signalmesh_meshd.egg-info/SOURCES.txt
|
|
13
|
+
src/signalmesh_meshd.egg-info/dependency_links.txt
|
|
14
|
+
src/signalmesh_meshd.egg-info/entry_points.txt
|
|
15
|
+
src/signalmesh_meshd.egg-info/requires.txt
|
|
16
|
+
src/signalmesh_meshd.egg-info/top_level.txt
|
|
17
|
+
tests/test_smoke.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
signalmesh_meshd
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Smoke test: proves the built wheel is importable end-to-end and that
|
|
2
|
+
MeshdConfig can be exercised against a fake path (never touches the real
|
|
3
|
+
~/.signalmesh/meshd.yaml on the machine running the tests)."""
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
import signalmesh_meshd
|
|
9
|
+
from signalmesh_meshd import config as meshd_config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_version():
|
|
13
|
+
assert signalmesh_meshd.__version__ == "0.1.0"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_top_level_exports():
|
|
17
|
+
assert hasattr(signalmesh_meshd, "cli")
|
|
18
|
+
assert hasattr(signalmesh_meshd, "main")
|
|
19
|
+
assert hasattr(signalmesh_meshd, "MeshdConfig")
|
|
20
|
+
assert hasattr(signalmesh_meshd, "run_edge_client")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_config_load_from_fake_path(tmp_path, monkeypatch):
|
|
24
|
+
fake_config_path = tmp_path / "meshd.yaml"
|
|
25
|
+
fake_config_path.write_text(yaml.safe_dump({
|
|
26
|
+
"device_id": "dev-fake-0001",
|
|
27
|
+
"device_token": "tok-fake-secret",
|
|
28
|
+
"device_name": "test-harness",
|
|
29
|
+
"cloud_url": "wss://example.invalid/api/submesh/edge",
|
|
30
|
+
"exposed_harnesses": {},
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
monkeypatch.setattr(meshd_config, "CONFIG_PATH", fake_config_path)
|
|
34
|
+
|
|
35
|
+
cfg = meshd_config.MeshdConfig.load()
|
|
36
|
+
assert cfg.device_id == "dev-fake-0001"
|
|
37
|
+
assert cfg.device_name == "test-harness"
|
|
38
|
+
assert cfg.cloud_url == "wss://example.invalid/api/submesh/edge"
|