@securecoms/agent-bridge 0.21.0 → 0.23.0
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.
- package/README.md +8 -1
- package/connectors/hermes/adapter.py +33 -7
- package/connectors/hermes/tests/test_logging_privacy.py +61 -0
- package/connectors/openclaw/openclaw-entry.inbound.test.ts +560 -0
- package/connectors/openclaw/openclaw-entry.js +240 -47
- package/dist/bin.d.ts +3 -7
- package/dist/bin.js +2868 -511
- package/dist/bridge-agent.d.ts +222 -15
- package/dist/coordination.d.ts +90 -5
- package/dist/decrypt-error-log-limiter.d.ts +6 -30
- package/dist/errors.d.ts +1 -1
- package/dist/http-server.d.ts +20 -1
- package/dist/index.js +2862 -496
- package/dist/service-log.d.ts +7 -0
- package/dist/summarize.d.ts +3 -2
- package/dist/version.d.ts +1 -1
- package/dist/ws-server.d.ts +26 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -270,7 +270,14 @@ closed without exposing OpenClaw memory.
|
|
|
270
270
|
{ "type":"ack", "message_id":"..." } // confirms you consumed a delivered message
|
|
271
271
|
```
|
|
272
272
|
|
|
273
|
-
**Acknowledge what you receive.** Inbound delivery is **at-least-once**.
|
|
273
|
+
**Acknowledge what you receive.** Inbound delivery is **at-least-once**. Before the remote server queue is acknowledged, the daemon seals each runtime-bound `message` into `bridge-state.db`. It remains durable until you send a matching `{type:"ack", message_id}`. If your socket, daemon, or host disappears first, the message is restored and redelivered to the next consumer in FIFO order. The cost is that you may occasionally see a message twice; dedup by `message_id`. Ack as soon as you've durably handled a message, not before.
|
|
274
|
+
|
|
275
|
+
For the first-party runtime, a reply carries `in_reply_to`. The daemon persists a
|
|
276
|
+
content-free reply receipt after publish, and a replacement runtime checks that
|
|
277
|
+
receipt before running the turn again. This closes the publish-success/ACK-loss
|
|
278
|
+
window where the result is known. It does not pretend an indeterminate external
|
|
279
|
+
tool call is safe to repeat; those calls use the runtime's separate reconciliation
|
|
280
|
+
ledger.
|
|
274
281
|
|
|
275
282
|
**One consumer at a time.** The daemon serves a single agent per process. Opening a second `/v1/ws` connection closes the first (newest wins, code `1000`); the replaced socket's un-acked messages are re-buffered for the new one. Don't run two consumers against one daemon expecting to share the stream.
|
|
276
283
|
|
|
@@ -81,6 +81,17 @@ _BINDING_PATH = os.path.join(_PLUGIN_DIR, "securecoms.binding.json")
|
|
|
81
81
|
_CONNECTIONS_DIR = os.path.expanduser("~/.securecoms/connections")
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
def _error_class(error: object) -> str:
|
|
85
|
+
"""Return a bounded class name without reading exception prose."""
|
|
86
|
+
try:
|
|
87
|
+
name = type(error).__name__
|
|
88
|
+
except Exception:
|
|
89
|
+
return "Error"
|
|
90
|
+
if not name or len(name) > 64 or not name.replace("_", "a").isalnum():
|
|
91
|
+
return "Error"
|
|
92
|
+
return name
|
|
93
|
+
|
|
94
|
+
|
|
84
95
|
def _read_file_text(path: str) -> str:
|
|
85
96
|
with open(path, "r") as f:
|
|
86
97
|
return f.read()
|
|
@@ -184,7 +195,7 @@ class SecureComsAdapter(BasePlatformAdapter):
|
|
|
184
195
|
self._recv_task = asyncio.create_task(self._supervise())
|
|
185
196
|
self._tick_task = asyncio.create_task(self._tick_loop())
|
|
186
197
|
self._mark_connected()
|
|
187
|
-
logger.info("SecureComs: connected to daemon
|
|
198
|
+
logger.info("SecureComs: connected to daemon")
|
|
188
199
|
return True
|
|
189
200
|
|
|
190
201
|
async def _open_ws(self) -> bool:
|
|
@@ -230,7 +241,7 @@ class SecureComsAdapter(BasePlatformAdapter):
|
|
|
230
241
|
changed = self._hold_reason != reason
|
|
231
242
|
self._hold_reason = reason
|
|
232
243
|
if changed:
|
|
233
|
-
logger.warning("SecureComs: waiting for SecureComs daemon
|
|
244
|
+
logger.warning("SecureComs: waiting for SecureComs daemon")
|
|
234
245
|
self._set_fatal_error(
|
|
235
246
|
"waiting_for_daemon",
|
|
236
247
|
f"waiting for SecureComs daemon: {reason}",
|
|
@@ -253,7 +264,10 @@ class SecureComsAdapter(BasePlatformAdapter):
|
|
|
253
264
|
except asyncio.CancelledError:
|
|
254
265
|
raise
|
|
255
266
|
except Exception as e:
|
|
256
|
-
logger.warning(
|
|
267
|
+
logger.warning(
|
|
268
|
+
"SecureComs: receive failed (error_class=%s)",
|
|
269
|
+
_error_class(e),
|
|
270
|
+
)
|
|
257
271
|
if self._stopped:
|
|
258
272
|
break
|
|
259
273
|
self._mark_disconnected()
|
|
@@ -311,7 +325,10 @@ class SecureComsAdapter(BasePlatformAdapter):
|
|
|
311
325
|
"is_dm": bool(c.get("is_dm")),
|
|
312
326
|
}
|
|
313
327
|
except Exception as e:
|
|
314
|
-
logger.debug(
|
|
328
|
+
logger.debug(
|
|
329
|
+
"SecureComs: channel refresh failed (error_class=%s)",
|
|
330
|
+
_error_class(e),
|
|
331
|
+
)
|
|
315
332
|
|
|
316
333
|
# ── Status emit ───────────────────────────────────────────────────────
|
|
317
334
|
|
|
@@ -321,7 +338,10 @@ class SecureComsAdapter(BasePlatformAdapter):
|
|
|
321
338
|
try:
|
|
322
339
|
await self._ws.send_json(frame)
|
|
323
340
|
except Exception as e:
|
|
324
|
-
logger.debug(
|
|
341
|
+
logger.debug(
|
|
342
|
+
"SecureComs: status frame send failed (error_class=%s)",
|
|
343
|
+
_error_class(e),
|
|
344
|
+
)
|
|
325
345
|
|
|
326
346
|
async def _tick_loop(self) -> None:
|
|
327
347
|
"""Drives ChannelStatusManager.tick() so a channel goes idle after
|
|
@@ -334,7 +354,10 @@ class SecureComsAdapter(BasePlatformAdapter):
|
|
|
334
354
|
except asyncio.CancelledError:
|
|
335
355
|
raise
|
|
336
356
|
except Exception as e:
|
|
337
|
-
logger.debug(
|
|
357
|
+
logger.debug(
|
|
358
|
+
"SecureComs: status tick failed (error_class=%s)",
|
|
359
|
+
_error_class(e),
|
|
360
|
+
)
|
|
338
361
|
|
|
339
362
|
# ── Inbound ───────────────────────────────────────────────────────────
|
|
340
363
|
|
|
@@ -360,7 +383,10 @@ class SecureComsAdapter(BasePlatformAdapter):
|
|
|
360
383
|
{"type": "ack", "message_id": frame.get("message_id")}
|
|
361
384
|
)
|
|
362
385
|
except Exception as e:
|
|
363
|
-
logger.warning(
|
|
386
|
+
logger.warning(
|
|
387
|
+
"SecureComs: ack failed (error_class=%s)",
|
|
388
|
+
_error_class(e),
|
|
389
|
+
)
|
|
364
390
|
elif msg.type in (aiohttp.WSMsgType.CLOSED, aiohttp.WSMsgType.ERROR):
|
|
365
391
|
return # WS closed → supervisor reconnects
|
|
366
392
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from adapter import _error_class
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _logging_violations(source: str) -> list[str]:
|
|
8
|
+
violations: list[str] = []
|
|
9
|
+
tree = ast.parse(source)
|
|
10
|
+
for node in ast.walk(tree):
|
|
11
|
+
if not (
|
|
12
|
+
isinstance(node, ast.Call)
|
|
13
|
+
and isinstance(node.func, ast.Attribute)
|
|
14
|
+
and isinstance(node.func.value, ast.Name)
|
|
15
|
+
and node.func.value.id == "logger"
|
|
16
|
+
):
|
|
17
|
+
continue
|
|
18
|
+
if node.func.attr == "exception":
|
|
19
|
+
violations.append("logger.exception")
|
|
20
|
+
if not node.args or not (
|
|
21
|
+
isinstance(node.args[0], ast.Constant)
|
|
22
|
+
and isinstance(node.args[0].value, str)
|
|
23
|
+
):
|
|
24
|
+
violations.append("dynamic format")
|
|
25
|
+
continue
|
|
26
|
+
rendered = node.args[0].value.lower()
|
|
27
|
+
if any(label in rendered for label in ("identity=", "channel=", "reason=", "url=")):
|
|
28
|
+
violations.append("sensitive field")
|
|
29
|
+
for arg in node.args[1:]:
|
|
30
|
+
if not (
|
|
31
|
+
isinstance(arg, ast.Call)
|
|
32
|
+
and isinstance(arg.func, ast.Name)
|
|
33
|
+
and arg.func.id == "_error_class"
|
|
34
|
+
) and not (isinstance(arg, ast.Name) and arg.id == "backoff"):
|
|
35
|
+
violations.append("unbounded argument")
|
|
36
|
+
return violations
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_adapter_logging_source_is_content_free() -> None:
|
|
40
|
+
source = (Path(__file__).parents[1] / "adapter.py").read_text()
|
|
41
|
+
assert _logging_violations(source) == []
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_adapter_logging_guard_detects_dynamic_values() -> None:
|
|
45
|
+
hostile = '''
|
|
46
|
+
logger.info(dynamic_format, identity)
|
|
47
|
+
logger.warning("channel=%s", channel_id)
|
|
48
|
+
logger.error("failed: %s", error)
|
|
49
|
+
'''
|
|
50
|
+
violations = _logging_violations(hostile)
|
|
51
|
+
assert "dynamic format" in violations
|
|
52
|
+
assert "sensitive field" in violations
|
|
53
|
+
assert "unbounded argument" in violations
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_error_class_does_not_render_exception_prose() -> None:
|
|
57
|
+
class HostileError(RuntimeError):
|
|
58
|
+
def __str__(self) -> str:
|
|
59
|
+
raise AssertionError("exception prose was rendered")
|
|
60
|
+
|
|
61
|
+
assert _error_class(HostileError()) == "HostileError"
|