Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dca0dfa894 |
@@ -1,5 +1,12 @@
|
||||
# Changelog
|
||||
|
||||
## v0.12.2 — 2026-09-25 08:44
|
||||
|
||||
- New compose service `forwarder` (`ops/forwarder`, `python:3.12-slim`, stdlib only, no pip installs): runs the notify forwarder that was previously a host-side `nohup` python script — receives the bot's outbound notify webhook POSTs (`{text, from?}` on `MESH_NOTIFY_URL`) and forwards each one as a message to an opencode API session (Basic auth).
|
||||
- `network_mode: host` → listens on the host's `0.0.0.0:4097`, so `MESH_NOTIFY_URL=http://172.24.0.1:4097/notify` keeps working unchanged from the bot container, and it reaches opencode at `127.0.0.1:9898`. No `networks:` key (host mode), no Traefik labels; `restart: unless-stopped`.
|
||||
- Config moves out of the script into compose environment from `.env` (gitignored): `FORWARDER_BASE` / `FORWARDER_SESSION` keep their current values as defaults, `FORWARDER_USER` / `FORWARDER_PASS` are required — the container exits on startup if either is missing. The real credentials are no longer hardcoded anywhere in the repo; nothing sensitive committed.
|
||||
- No app code changes — existing 59 tests stay green.
|
||||
|
||||
## v0.12.1 — 2026-09-25 07:57
|
||||
|
||||
- Hotfix for the v0.12.0 bootstrap crash (`UnknownDependenciesException`): `MeshModule` now exports `MeshService`, and `WeatherService`'s `import type { MeshService }` became a value import — with `emitDecoratorMetadata` the type-only import is erased, so the constructor's DI token was `undefined` at runtime even with the export (the unit suite instantiates services directly and never booted the module graph, which is why 58 green tests still shipped a broken app).
|
||||
|
||||
@@ -97,6 +97,16 @@ Hourly cron job (`src/weather`): fetches current conditions for Lublin from [Ope
|
||||
- `WEATHER_LAT` (default `51.2465`) / `WEATHER_LON` (default `22.5684`) — coordinates queried from Open-Meteo
|
||||
- `WEATHER_CITY` (default `"Lublin"`) — city name in the report line
|
||||
|
||||
## Notify forwarder
|
||||
|
||||
Second compose service (`ops/forwarder`, `python:3.12-slim`, stdlib only): receives the bot's outbound notify webhook POSTs (`{text, from?}` on `MESH_NOTIFY_URL`) and forwards each one as a message to an opencode API session (Basic auth). Runs with `network_mode: host` — it listens on the host's `0.0.0.0:4097`, which is why `MESH_NOTIFY_URL=http://172.24.0.1:4097/notify` works from the bot container, and reaches opencode at `127.0.0.1:9898`. `restart: unless-stopped`; no Traefik labels (host networking).
|
||||
|
||||
Config lives in `.env` (gitignored) and is passed via compose environment — credentials are never committed to the repo:
|
||||
|
||||
- `FORWARDER_BASE` (default `http://127.0.0.1:9898/workspaces/679fd6f6-b237-4a46-bf69-64cb0fc865b0/instance`) — opencode instance base URL
|
||||
- `FORWARDER_SESSION` (default `ses_f31037caeffeioRwixqGV7VS6m`) — target session ID
|
||||
- `FORWARDER_USER` / `FORWARDER_PASS` — Basic auth credentials; **required**, the container exits on startup if either is missing
|
||||
|
||||
## Env vars
|
||||
|
||||
- `MESH_HOST` (default `192.168.1.4`) — bridge host
|
||||
|
||||
@@ -19,6 +19,15 @@ services:
|
||||
- 'traefik.http.routers.mesh-bot.rule=Host(`bot.dev.kkhost.pl`)'
|
||||
- 'traefik.http.routers.mesh-bot.entrypoints=web'
|
||||
- 'traefik.http.services.mesh-bot.loadbalancer.server.port=3000'
|
||||
forwarder:
|
||||
build: ./ops/forwarder
|
||||
network_mode: host
|
||||
environment:
|
||||
FORWARDER_BASE: ${FORWARDER_BASE:-http://127.0.0.1:9898/workspaces/679fd6f6-b237-4a46-bf69-64cb0fc865b0/instance}
|
||||
FORWARDER_SESSION: ${FORWARDER_SESSION:-ses_f31037caeffeioRwixqGV7VS6m}
|
||||
FORWARDER_USER: ${FORWARDER_USER:-}
|
||||
FORWARDER_PASS: ${FORWARDER_PASS:-}
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
mesh_data:
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
COPY forwarder.py .
|
||||
CMD ["python3", "forwarder.py"]
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
"""mesh-bot notify forwarder: POST /notify {text, from?} -> opencode session message (basic auth).
|
||||
|
||||
Note on semantics: the opencode API accepts a session message immediately but holds the HTTP
|
||||
response until the agent turn completes — so a read timeout here means DELIVERED, not failed.
|
||||
"""
|
||||
import json, base64, urllib.request
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
import os
|
||||
|
||||
BASE = os.environ.get("FORWARDER_BASE", "http://127.0.0.1:9898/workspaces/679fd6f6-b237-4a46-bf69-64cb0fc865b0/instance")
|
||||
SESSION = os.environ.get("FORWARDER_SESSION", "ses_f31037caeffeioRwixqGV7VS6m")
|
||||
USER = os.environ["FORWARDER_USER"] # required; fail fast if missing
|
||||
PASS = os.environ["FORWARDER_PASS"] # required; fail fast if missing
|
||||
|
||||
class H(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
if self.path != "/notify":
|
||||
self.send_response(404); self.end_headers(); return
|
||||
n = int(self.headers.get("Content-Length", 0))
|
||||
try:
|
||||
body = json.loads(self.rfile.read(n) or b"{}")
|
||||
text, frm = body.get("text"), body.get("from")
|
||||
msg = 'New mesh message' + (f' from node {frm}' if frm is not None else '') + f': "{text}" — please react to it.'
|
||||
req = urllib.request.Request(
|
||||
f"{BASE}/session/{SESSION}/message",
|
||||
data=json.dumps({"parts": [{"type": "text", "text": msg}]}).encode(),
|
||||
headers={"Content-Type": "application/json",
|
||||
"Authorization": "Basic " + base64.b64encode(f"{USER}:{PASS}".encode()).decode()},
|
||||
method="POST")
|
||||
try:
|
||||
r = urllib.request.urlopen(req, timeout=15)
|
||||
code = r.status # headers received => opencode accepted the message
|
||||
try: out = r.read(200).decode(errors='replace')
|
||||
except Exception as e2: out = f'(body read failed after delivery: {e2!r})'
|
||||
print(f"forwarded to opencode session {SESSION}: HTTP {code} {out[:150]!r}", flush=True)
|
||||
self._reply(code if 200 <= code < 300 else 502, b'{"ok":true}' if 200 <= code < 300 else str(code).encode())
|
||||
except TimeoutError:
|
||||
# request was sent; opencode holds the response until the agent turn finishes => delivered
|
||||
print(f"forwarded to opencode session {SESSION}: accepted (response still pending after 15 s)", flush=True)
|
||||
self._reply(202, b'{"ok":true,"note":"accepted; opencode response pending"}')
|
||||
except Exception as e:
|
||||
print(f"opencode call failed: {e!r}", flush=True)
|
||||
self._reply(502, repr(e).encode())
|
||||
|
||||
def _reply(self, code, payload):
|
||||
try:
|
||||
self.send_response(code); self.end_headers(); self.wfile.write(payload)
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass # client went away; delivery state already logged above
|
||||
|
||||
def log_message(self, fmt, *args):
|
||||
print("forwarder:", fmt % args, flush=True)
|
||||
|
||||
HTTPServer(("0.0.0.0", 4097), H).serve_forever()
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mesh-bot",
|
||||
"version": "0.12.1",
|
||||
"version": "0.12.2",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
Reference in New Issue
Block a user