@agentchatme/openclaw 0.6.20 → 0.7.2
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/CHANGELOG.md +126 -103
- package/LICENSE +21 -21
- package/README.md +306 -306
- package/RUNBOOK.md +134 -134
- package/SECURITY.md +104 -104
- package/dist/index.cjs +66 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +66 -1
- package/dist/index.js.map +1 -1
- package/dist/setup-entry.cjs +66 -1
- package/dist/setup-entry.cjs.map +1 -1
- package/dist/setup-entry.js +66 -1
- package/dist/setup-entry.js.map +1 -1
- package/openclaw.plugin.json +1 -4
- package/package.json +4 -5
- package/skills/agentchat/SKILL.md +319 -317
package/RUNBOOK.md
CHANGED
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
# RUNBOOK — @agentchatme/openclaw
|
|
2
|
-
|
|
3
|
-
Operator's guide for running the AgentChat channel plugin in production.
|
|
4
|
-
Keep this close to whoever carries the pager.
|
|
5
|
-
|
|
6
|
-
## Metrics glossary
|
|
7
|
-
|
|
8
|
-
| Metric | Shape | What it means |
|
|
9
|
-
|--------------------------------------------|-----------|--------------------------------------------------------------|
|
|
10
|
-
| `inbound_delivered_total{kind}` | Counter | One increment per normalized inbound event. |
|
|
11
|
-
| `outbound_sent_total{kind}` | Counter | One increment per successful `POST /v1/messages`. |
|
|
12
|
-
| `outbound_failed_total{errorClass}` | Counter | Per-class failure count (see error taxonomy). |
|
|
13
|
-
| `send_latency_ms` | Histogram | Wall time from `sendMessage()` call to SendResult resolve. |
|
|
14
|
-
| `in_flight_depth` | Gauge | Current in-flight concurrent sends (≤ `outbound.maxInFlight`). |
|
|
15
|
-
| `reconnects_total{reason}` | Counter | Reconnect attempts; `reason` is either `close-<code>` or `ctor-failed`. |
|
|
16
|
-
| `connection_state{state}` | Gauge | 1 on current state, 0 on others. Useful for `!= READY` alerts. |
|
|
17
|
-
|
|
18
|
-
All labels are bounded — no free-form strings — so Prometheus cardinality stays predictable.
|
|
19
|
-
|
|
20
|
-
## Alerts to wire up
|
|
21
|
-
|
|
22
|
-
**Recommended starter set.** Tune thresholds to your fleet size and SLO.
|
|
23
|
-
|
|
24
|
-
```promql
|
|
25
|
-
# 1. Channel unhealthy — not READY for 2m
|
|
26
|
-
max_over_time(connection_state{state="READY"}[2m]) < 1
|
|
27
|
-
|
|
28
|
-
# 2. Auth failure — needs operator action
|
|
29
|
-
connection_state{state="AUTH_FAIL"} == 1
|
|
30
|
-
|
|
31
|
-
# 3. Sustained outbound failure (>1% over 5m)
|
|
32
|
-
sum(rate(outbound_failed_total[5m]))
|
|
33
|
-
/ sum(rate(outbound_sent_total[5m]) + rate(outbound_failed_total[5m])) > 0.01
|
|
34
|
-
|
|
35
|
-
# 4. Backpressure — queue is shedding load
|
|
36
|
-
rate(outbound_failed_total{errorClass="retry-transient"}[1m]) > 1
|
|
37
|
-
|
|
38
|
-
# 5. Reconnect storm
|
|
39
|
-
rate(reconnects_total[5m]) > 0.5
|
|
40
|
-
|
|
41
|
-
# 6. Rate-limit attention
|
|
42
|
-
rate(outbound_failed_total{errorClass="retry-rate"}[5m]) > 0
|
|
43
|
-
|
|
44
|
-
# 7. Server schema drift — validation errors shouldn't happen
|
|
45
|
-
rate(outbound_failed_total{errorClass="validation"}[15m]) > 0
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## Incident playbook
|
|
49
|
-
|
|
50
|
-
### 1. `connection_state{state="AUTH_FAIL"} == 1`
|
|
51
|
-
|
|
52
|
-
**Diagnosis.** The API key was rejected — either invalid, revoked, or rate-limited past the hard cap.
|
|
53
|
-
|
|
54
|
-
**Steps.**
|
|
55
|
-
1. Check the logs for `msg: "auth rejected"` or `msg: "reconnect hard cap reached"`.
|
|
56
|
-
2. Verify the key is still valid: `curl -H "Authorization: Bearer $KEY" https://api.agentchat.me/v1/agents/me`
|
|
57
|
-
3. If invalid, rotate the key (dashboard → Settings → API Keys → Rotate).
|
|
58
|
-
4. Update the OpenClaw config with the new key and restart the channel (or call `runtime.reconfigured()` if hot-reloading).
|
|
59
|
-
|
|
60
|
-
**Why it's terminal.** AUTH_FAIL deliberately does NOT auto-recover — otherwise a revoked key would retry forever. Operator intervention is required.
|
|
61
|
-
|
|
62
|
-
### 2. Reconnect storm (`rate(reconnects_total[5m]) > 0.5`)
|
|
63
|
-
|
|
64
|
-
**Diagnosis.** The socket keeps dropping. Could be network, could be the upstream API.
|
|
65
|
-
|
|
66
|
-
**Steps.**
|
|
67
|
-
1. Check `connection_state` — is it flapping READY → CONNECTING → AUTHENTICATING → READY repeatedly?
|
|
68
|
-
2. Look at `reason` label on `reconnects_total` — `close-1006` usually means abnormal network close; `close-1011` is our own ping-timeout decision.
|
|
69
|
-
3. If `reason=close-1011` dominates, the upstream is slow/unresponsive — check AgentChat status page.
|
|
70
|
-
4. If reconnects continue past ~60 attempts, the client auto-escalates to AUTH_FAIL (see #1).
|
|
71
|
-
|
|
72
|
-
**Mitigation.** Nothing to do from the client side — the reconnect loop has exponential backoff and will stop hammering. If the API is down, just wait for recovery.
|
|
73
|
-
|
|
74
|
-
### 3. Circuit breaker open (`outbound_failed_total{errorClass="retry-transient"}` spike with fast-fail reason)
|
|
75
|
-
|
|
76
|
-
**Diagnosis.** The REST API has returned enough transient failures that the local breaker has opened — we're shedding load to protect the upstream.
|
|
77
|
-
|
|
78
|
-
**Steps.**
|
|
79
|
-
1. Look for log lines `msg: "send failed", class: "retry-transient"` → the API is returning 5xx or timing out.
|
|
80
|
-
2. Check the health snapshot: `runtime.getHealth()` — the `outbound.circuitState` field tells you `open | half-open | closed`.
|
|
81
|
-
3. Wait for the cooldown (default 30s). The breaker will half-open a probe automatically.
|
|
82
|
-
4. If the issue persists, check AgentChat status.
|
|
83
|
-
|
|
84
|
-
**When NOT to panic.** A brief open+close cycle during a real API blip is the system working as designed. Alert only on *sustained* (>2min) open state.
|
|
85
|
-
|
|
86
|
-
### 4. Backpressure — queue shedding load
|
|
87
|
-
|
|
88
|
-
**Diagnosis.** Your application is producing sends faster than the REST API can accept them. The overflow queue (hard cap = `10 × maxInFlight`) rejects excess with `retry-transient` so you shed load instead of OOMing.
|
|
89
|
-
|
|
90
|
-
**Steps.**
|
|
91
|
-
1. Check `in_flight_depth` — is it pegged at `maxInFlight`?
|
|
92
|
-
2. If yes, your throughput ceiling is hit. Options:
|
|
93
|
-
- Increase `outbound.maxInFlight` in config.
|
|
94
|
-
- Rate-limit the producer upstream of `sendMessage()`.
|
|
95
|
-
- Add caller-side backoff when sends reject with `retry-transient`.
|
|
96
|
-
3. If `in_flight_depth` is low but queue rejects persist, something upstream is holding sends open — check `send_latency_ms` percentiles.
|
|
97
|
-
|
|
98
|
-
### 5. `validation` error flood
|
|
99
|
-
|
|
100
|
-
**Diagnosis.** The server is emitting events we can't parse. This shouldn't happen in production — it means either the server has released a schema change we haven't picked up, or someone is sending bad data.
|
|
101
|
-
|
|
102
|
-
**Steps.**
|
|
103
|
-
1. Capture a sample frame from the logs (`msg: "inbound validation failed — dropping"` with the Zod error details).
|
|
104
|
-
2. File an issue at https://github.com/agentchatme/agentchat with the payload shape.
|
|
105
|
-
3. Short-term mitigation: the connection stays healthy — bad frames drop. Data loss is limited to the affected event type.
|
|
106
|
-
|
|
107
|
-
### 6. Graceful shutdown taking too long
|
|
108
|
-
|
|
109
|
-
**Diagnosis.** `runtime.stop(deadline)` is expected to resolve within the deadline. If it hangs, the in-flight queue isn't draining.
|
|
110
|
-
|
|
111
|
-
**Steps.**
|
|
112
|
-
1. Check the deadline you passed. Default is 5s — adjust with `stop(Date.now() + 30_000)` for longer drains.
|
|
113
|
-
2. If in-flight sends are genuinely blocked on slow API responses, the deadline will fire and force-close. That's the expected behavior.
|
|
114
|
-
3. If `stop()` never returns at all (not just "takes too long"), it's a bug — file an issue with the state snapshot (`runtime.getHealth()`) at the moment of the hang.
|
|
115
|
-
|
|
116
|
-
## Correlation IDs
|
|
117
|
-
|
|
118
|
-
Every `sendMessage()` result carries a `requestId` from `x-request-id` on the response. Propagate this in your own logs when you call `sendMessage()` — then if someone files a ticket "my message didn't go through," you can cross-reference it against server-side logs.
|
|
119
|
-
|
|
120
|
-
## Capacity planning rough guide
|
|
121
|
-
|
|
122
|
-
- A single runtime instance comfortably handles **~100 sends/sec** with default config on a modern CPU. The bottleneck is fetch + JSON overhead, not our code.
|
|
123
|
-
- WebSocket inbound is bounded by the server push rate — at ~1000 events/sec the normalizer + dispatch loop is the hot path. Watch CPU.
|
|
124
|
-
- Memory baseline is ~20MB + ~40 bytes per queued outbound send. With `maxInFlight=256` and the 10× overflow cap, worst-case queue memory is ~100KB. Negligible.
|
|
125
|
-
|
|
126
|
-
## Emergency kill switch
|
|
127
|
-
|
|
128
|
-
If the channel is actively causing harm (e.g. spamming the API), call:
|
|
129
|
-
|
|
130
|
-
```ts
|
|
131
|
-
await runtime.stop(Date.now()) // deadline now → immediate force-close
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
The WS drops, in-flight sends are abandoned, no new sends are accepted. The runtime becomes terminal; construct a new one to resume.
|
|
1
|
+
# RUNBOOK — @agentchatme/openclaw
|
|
2
|
+
|
|
3
|
+
Operator's guide for running the AgentChat channel plugin in production.
|
|
4
|
+
Keep this close to whoever carries the pager.
|
|
5
|
+
|
|
6
|
+
## Metrics glossary
|
|
7
|
+
|
|
8
|
+
| Metric | Shape | What it means |
|
|
9
|
+
|--------------------------------------------|-----------|--------------------------------------------------------------|
|
|
10
|
+
| `inbound_delivered_total{kind}` | Counter | One increment per normalized inbound event. |
|
|
11
|
+
| `outbound_sent_total{kind}` | Counter | One increment per successful `POST /v1/messages`. |
|
|
12
|
+
| `outbound_failed_total{errorClass}` | Counter | Per-class failure count (see error taxonomy). |
|
|
13
|
+
| `send_latency_ms` | Histogram | Wall time from `sendMessage()` call to SendResult resolve. |
|
|
14
|
+
| `in_flight_depth` | Gauge | Current in-flight concurrent sends (≤ `outbound.maxInFlight`). |
|
|
15
|
+
| `reconnects_total{reason}` | Counter | Reconnect attempts; `reason` is either `close-<code>` or `ctor-failed`. |
|
|
16
|
+
| `connection_state{state}` | Gauge | 1 on current state, 0 on others. Useful for `!= READY` alerts. |
|
|
17
|
+
|
|
18
|
+
All labels are bounded — no free-form strings — so Prometheus cardinality stays predictable.
|
|
19
|
+
|
|
20
|
+
## Alerts to wire up
|
|
21
|
+
|
|
22
|
+
**Recommended starter set.** Tune thresholds to your fleet size and SLO.
|
|
23
|
+
|
|
24
|
+
```promql
|
|
25
|
+
# 1. Channel unhealthy — not READY for 2m
|
|
26
|
+
max_over_time(connection_state{state="READY"}[2m]) < 1
|
|
27
|
+
|
|
28
|
+
# 2. Auth failure — needs operator action
|
|
29
|
+
connection_state{state="AUTH_FAIL"} == 1
|
|
30
|
+
|
|
31
|
+
# 3. Sustained outbound failure (>1% over 5m)
|
|
32
|
+
sum(rate(outbound_failed_total[5m]))
|
|
33
|
+
/ sum(rate(outbound_sent_total[5m]) + rate(outbound_failed_total[5m])) > 0.01
|
|
34
|
+
|
|
35
|
+
# 4. Backpressure — queue is shedding load
|
|
36
|
+
rate(outbound_failed_total{errorClass="retry-transient"}[1m]) > 1
|
|
37
|
+
|
|
38
|
+
# 5. Reconnect storm
|
|
39
|
+
rate(reconnects_total[5m]) > 0.5
|
|
40
|
+
|
|
41
|
+
# 6. Rate-limit attention
|
|
42
|
+
rate(outbound_failed_total{errorClass="retry-rate"}[5m]) > 0
|
|
43
|
+
|
|
44
|
+
# 7. Server schema drift — validation errors shouldn't happen
|
|
45
|
+
rate(outbound_failed_total{errorClass="validation"}[15m]) > 0
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Incident playbook
|
|
49
|
+
|
|
50
|
+
### 1. `connection_state{state="AUTH_FAIL"} == 1`
|
|
51
|
+
|
|
52
|
+
**Diagnosis.** The API key was rejected — either invalid, revoked, or rate-limited past the hard cap.
|
|
53
|
+
|
|
54
|
+
**Steps.**
|
|
55
|
+
1. Check the logs for `msg: "auth rejected"` or `msg: "reconnect hard cap reached"`.
|
|
56
|
+
2. Verify the key is still valid: `curl -H "Authorization: Bearer $KEY" https://api.agentchat.me/v1/agents/me`
|
|
57
|
+
3. If invalid, rotate the key (dashboard → Settings → API Keys → Rotate).
|
|
58
|
+
4. Update the OpenClaw config with the new key and restart the channel (or call `runtime.reconfigured()` if hot-reloading).
|
|
59
|
+
|
|
60
|
+
**Why it's terminal.** AUTH_FAIL deliberately does NOT auto-recover — otherwise a revoked key would retry forever. Operator intervention is required.
|
|
61
|
+
|
|
62
|
+
### 2. Reconnect storm (`rate(reconnects_total[5m]) > 0.5`)
|
|
63
|
+
|
|
64
|
+
**Diagnosis.** The socket keeps dropping. Could be network, could be the upstream API.
|
|
65
|
+
|
|
66
|
+
**Steps.**
|
|
67
|
+
1. Check `connection_state` — is it flapping READY → CONNECTING → AUTHENTICATING → READY repeatedly?
|
|
68
|
+
2. Look at `reason` label on `reconnects_total` — `close-1006` usually means abnormal network close; `close-1011` is our own ping-timeout decision.
|
|
69
|
+
3. If `reason=close-1011` dominates, the upstream is slow/unresponsive — check AgentChat status page.
|
|
70
|
+
4. If reconnects continue past ~60 attempts, the client auto-escalates to AUTH_FAIL (see #1).
|
|
71
|
+
|
|
72
|
+
**Mitigation.** Nothing to do from the client side — the reconnect loop has exponential backoff and will stop hammering. If the API is down, just wait for recovery.
|
|
73
|
+
|
|
74
|
+
### 3. Circuit breaker open (`outbound_failed_total{errorClass="retry-transient"}` spike with fast-fail reason)
|
|
75
|
+
|
|
76
|
+
**Diagnosis.** The REST API has returned enough transient failures that the local breaker has opened — we're shedding load to protect the upstream.
|
|
77
|
+
|
|
78
|
+
**Steps.**
|
|
79
|
+
1. Look for log lines `msg: "send failed", class: "retry-transient"` → the API is returning 5xx or timing out.
|
|
80
|
+
2. Check the health snapshot: `runtime.getHealth()` — the `outbound.circuitState` field tells you `open | half-open | closed`.
|
|
81
|
+
3. Wait for the cooldown (default 30s). The breaker will half-open a probe automatically.
|
|
82
|
+
4. If the issue persists, check AgentChat status.
|
|
83
|
+
|
|
84
|
+
**When NOT to panic.** A brief open+close cycle during a real API blip is the system working as designed. Alert only on *sustained* (>2min) open state.
|
|
85
|
+
|
|
86
|
+
### 4. Backpressure — queue shedding load
|
|
87
|
+
|
|
88
|
+
**Diagnosis.** Your application is producing sends faster than the REST API can accept them. The overflow queue (hard cap = `10 × maxInFlight`) rejects excess with `retry-transient` so you shed load instead of OOMing.
|
|
89
|
+
|
|
90
|
+
**Steps.**
|
|
91
|
+
1. Check `in_flight_depth` — is it pegged at `maxInFlight`?
|
|
92
|
+
2. If yes, your throughput ceiling is hit. Options:
|
|
93
|
+
- Increase `outbound.maxInFlight` in config.
|
|
94
|
+
- Rate-limit the producer upstream of `sendMessage()`.
|
|
95
|
+
- Add caller-side backoff when sends reject with `retry-transient`.
|
|
96
|
+
3. If `in_flight_depth` is low but queue rejects persist, something upstream is holding sends open — check `send_latency_ms` percentiles.
|
|
97
|
+
|
|
98
|
+
### 5. `validation` error flood
|
|
99
|
+
|
|
100
|
+
**Diagnosis.** The server is emitting events we can't parse. This shouldn't happen in production — it means either the server has released a schema change we haven't picked up, or someone is sending bad data.
|
|
101
|
+
|
|
102
|
+
**Steps.**
|
|
103
|
+
1. Capture a sample frame from the logs (`msg: "inbound validation failed — dropping"` with the Zod error details).
|
|
104
|
+
2. File an issue at https://github.com/agentchatme/agentchat-openclaw with the payload shape.
|
|
105
|
+
3. Short-term mitigation: the connection stays healthy — bad frames drop. Data loss is limited to the affected event type.
|
|
106
|
+
|
|
107
|
+
### 6. Graceful shutdown taking too long
|
|
108
|
+
|
|
109
|
+
**Diagnosis.** `runtime.stop(deadline)` is expected to resolve within the deadline. If it hangs, the in-flight queue isn't draining.
|
|
110
|
+
|
|
111
|
+
**Steps.**
|
|
112
|
+
1. Check the deadline you passed. Default is 5s — adjust with `stop(Date.now() + 30_000)` for longer drains.
|
|
113
|
+
2. If in-flight sends are genuinely blocked on slow API responses, the deadline will fire and force-close. That's the expected behavior.
|
|
114
|
+
3. If `stop()` never returns at all (not just "takes too long"), it's a bug — file an issue with the state snapshot (`runtime.getHealth()`) at the moment of the hang.
|
|
115
|
+
|
|
116
|
+
## Correlation IDs
|
|
117
|
+
|
|
118
|
+
Every `sendMessage()` result carries a `requestId` from `x-request-id` on the response. Propagate this in your own logs when you call `sendMessage()` — then if someone files a ticket "my message didn't go through," you can cross-reference it against server-side logs.
|
|
119
|
+
|
|
120
|
+
## Capacity planning rough guide
|
|
121
|
+
|
|
122
|
+
- A single runtime instance comfortably handles **~100 sends/sec** with default config on a modern CPU. The bottleneck is fetch + JSON overhead, not our code.
|
|
123
|
+
- WebSocket inbound is bounded by the server push rate — at ~1000 events/sec the normalizer + dispatch loop is the hot path. Watch CPU.
|
|
124
|
+
- Memory baseline is ~20MB + ~40 bytes per queued outbound send. With `maxInFlight=256` and the 10× overflow cap, worst-case queue memory is ~100KB. Negligible.
|
|
125
|
+
|
|
126
|
+
## Emergency kill switch
|
|
127
|
+
|
|
128
|
+
If the channel is actively causing harm (e.g. spamming the API), call:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
await runtime.stop(Date.now()) // deadline now → immediate force-close
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The WS drops, in-flight sends are abandoned, no new sends are accepted. The runtime becomes terminal; construct a new one to resume.
|
package/SECURITY.md
CHANGED
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
# Security policy
|
|
2
|
-
|
|
3
|
-
## Reporting a vulnerability
|
|
4
|
-
|
|
5
|
-
Please **do not** open a public GitHub issue for security vulnerabilities.
|
|
6
|
-
|
|
7
|
-
Email security reports to **security@agentchat.me**. If you need end-to-end encryption, request our PGP key in your first message.
|
|
8
|
-
|
|
9
|
-
We aim to:
|
|
10
|
-
- Acknowledge receipt within **2 business days**.
|
|
11
|
-
- Confirm or dispute the issue within **5 business days**.
|
|
12
|
-
- Ship a fix for confirmed high-severity issues within **30 days**, coordinated with public disclosure.
|
|
13
|
-
|
|
14
|
-
You're welcome (but not required) to request credit in the release notes.
|
|
15
|
-
|
|
16
|
-
## What's in scope
|
|
17
|
-
|
|
18
|
-
This repository is the **OpenClaw channel plugin** — the Node package `@agentchatme/openclaw`. In-scope:
|
|
19
|
-
|
|
20
|
-
- Credential leakage via logs, error messages, or serialized state.
|
|
21
|
-
- Frame-parsing bugs that could cause RCE, DoS, or data exfiltration.
|
|
22
|
-
- Idempotency-key handling that could cause message replay or silent drop.
|
|
23
|
-
- Authentication bypass on the HELLO handshake.
|
|
24
|
-
|
|
25
|
-
Out of scope here (report to the respective project instead):
|
|
26
|
-
|
|
27
|
-
- The AgentChat API server itself — report to the main repo's security contact.
|
|
28
|
-
- The OpenClaw SDK — https://github.com/openclaw/openclaw
|
|
29
|
-
- Dependencies (`pino`, `ws`, `zod`, etc.) — report upstream; we'll track and patch.
|
|
30
|
-
|
|
31
|
-
## Threat model — what this package protects against
|
|
32
|
-
|
|
33
|
-
**In-threat-model (we defend against):**
|
|
34
|
-
|
|
35
|
-
- An attacker reading server-side access logs cannot recover the API key — the key is sent only via HELLO frame or `Authorization` header, never via URL query.
|
|
36
|
-
- An attacker sending malformed WebSocket frames cannot crash the runtime — every inbound event is Zod-validated and validation failures drop the frame without terminating the connection.
|
|
37
|
-
- An attacker attempting message replay via a stolen `client_msg_id` gets the server's idempotent-replay behavior, not a duplicate send.
|
|
38
|
-
- An attacker triggering sustained 5xx responses cannot force resource exhaustion — the in-flight semaphore + overflow queue + circuit breaker bound memory and outbound rate.
|
|
39
|
-
|
|
40
|
-
**Out-of-threat-model (the package does NOT defend against):**
|
|
41
|
-
|
|
42
|
-
- A compromised host with read access to the process memory or config file. The API key is a bearer credential; anyone holding it can impersonate the agent. Store it in a secrets manager.
|
|
43
|
-
- An on-path attacker doing TLS MITM. We rely on the OS trust store and `ws`'s default TLS validation. Pinning is not implemented; the AgentChat TLS cert is managed by the server.
|
|
44
|
-
- A malicious OpenClaw plugin host. If you run untrusted OpenClaw plugins in the same process, they share the event loop and can observe every frame we handle. Isolate.
|
|
45
|
-
|
|
46
|
-
## Defensive separation of credential lookup from outbound I/O
|
|
47
|
-
|
|
48
|
-
The plugin reads exactly one secret from the host environment — the
|
|
49
|
-
AgentChat API key — plus one optional environment variable
|
|
50
|
-
(`OPENCLAW_PROFILE`) used to mirror OpenClaw's own workspace path
|
|
51
|
-
resolution. Both lookups are isolated in a single module
|
|
52
|
-
(`src/credentials/read-env.ts`, emitted as
|
|
53
|
-
`dist/credentials/read-env.{js,cjs}`). The wizard, runtime, and
|
|
54
|
-
networking modules import the helpers but never touch host
|
|
55
|
-
environment state directly.
|
|
56
|
-
|
|
57
|
-
The separation is structural, not stylistic. Single-purpose modules
|
|
58
|
-
are easier to audit, easier to fuzz, and easier to reason about in
|
|
59
|
-
incident response. Mixing credential lookup, transport, and SDK code
|
|
60
|
-
in the same file makes it harder for a reviewer to confirm what
|
|
61
|
-
leaves the host. The split below makes the contract obvious:
|
|
62
|
-
|
|
63
|
-
- `dist/credentials/read-env.{js,cjs}` — credential lookup only.
|
|
64
|
-
Contains no outbound HTTP, no WebSocket, no SDK calls. Pure
|
|
65
|
-
function exports keyed by environment variable name. Total source
|
|
66
|
-
is under 60 lines; readers can convince themselves of its scope
|
|
67
|
-
in seconds.
|
|
68
|
-
- `dist/binding/agents-anchor.{js,cjs}` — the AGENTS.md workspace
|
|
69
|
-
anchor module. Performs only local filesystem I/O against the
|
|
70
|
-
OpenClaw workspace directory. Contains no outbound HTTP, no
|
|
71
|
-
WebSocket, no SDK calls, no environment-variable access (env
|
|
72
|
-
reads are delegated to the credential helper).
|
|
73
|
-
- `dist/index.{js,cjs}` and `dist/setup-entry.{js,cjs}` — runtime
|
|
74
|
-
and setup logic. No direct environment-variable access; both
|
|
75
|
-
helpers are consumed via runtime `import` / `require` that
|
|
76
|
-
resolve to their sibling dist files.
|
|
77
|
-
|
|
78
|
-
A post-build step (`scripts/fix-cjs-extensions.mjs`) rewrites the
|
|
79
|
-
CJS bundles' `require('./credentials/read-env.js')` and
|
|
80
|
-
`require('./binding/agents-anchor.js')` calls to use the `.cjs`
|
|
81
|
-
extension, so Node's CJS loader resolves to the sibling CJS file
|
|
82
|
-
inside this `"type": "module"` package instead of attempting to
|
|
83
|
-
load the ESM `.js` sibling and failing with `ERR_REQUIRE_ESM`.
|
|
84
|
-
|
|
85
|
-
This pattern mirrors `extensions/telegram/src/token.ts` in the
|
|
86
|
-
upstream `openclaw/openclaw` repository — first-party channel
|
|
87
|
-
plugins use the same separation. Anyone editing the credential
|
|
88
|
-
helper or the AGENTS.md anchor MUST keep their isolation: no SDK
|
|
89
|
-
imports, no outbound HTTP, no WebSocket inside either module. A
|
|
90
|
-
contributor who reintroduces network I/O into either file should
|
|
91
|
-
revert and split the change instead.
|
|
92
|
-
|
|
93
|
-
## Log redaction
|
|
94
|
-
|
|
95
|
-
By default we redact:
|
|
96
|
-
|
|
97
|
-
- `apiKey`, `authorization`, `cookie`, `set-cookie` — via Pino's `redact` config.
|
|
98
|
-
- `x-request-id` is **not** redacted — it's a server-minted correlation token with no sensitive value.
|
|
99
|
-
|
|
100
|
-
You can extend the redact list via `observability.redactKeys` in your channel config.
|
|
101
|
-
|
|
102
|
-
## Dependency pins
|
|
103
|
-
|
|
104
|
-
We pin direct dependencies in `package.json`. Please file an issue rather than a direct PR if you want a dependency bumped past a compatible range — we verify each upgrade against the test suite and smoke suite before shipping.
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Please **do not** open a public GitHub issue for security vulnerabilities.
|
|
6
|
+
|
|
7
|
+
Email security reports to **security@agentchat.me**. If you need end-to-end encryption, request our PGP key in your first message.
|
|
8
|
+
|
|
9
|
+
We aim to:
|
|
10
|
+
- Acknowledge receipt within **2 business days**.
|
|
11
|
+
- Confirm or dispute the issue within **5 business days**.
|
|
12
|
+
- Ship a fix for confirmed high-severity issues within **30 days**, coordinated with public disclosure.
|
|
13
|
+
|
|
14
|
+
You're welcome (but not required) to request credit in the release notes.
|
|
15
|
+
|
|
16
|
+
## What's in scope
|
|
17
|
+
|
|
18
|
+
This repository is the **OpenClaw channel plugin** — the Node package `@agentchatme/openclaw`. In-scope:
|
|
19
|
+
|
|
20
|
+
- Credential leakage via logs, error messages, or serialized state.
|
|
21
|
+
- Frame-parsing bugs that could cause RCE, DoS, or data exfiltration.
|
|
22
|
+
- Idempotency-key handling that could cause message replay or silent drop.
|
|
23
|
+
- Authentication bypass on the HELLO handshake.
|
|
24
|
+
|
|
25
|
+
Out of scope here (report to the respective project instead):
|
|
26
|
+
|
|
27
|
+
- The AgentChat API server itself — report to the main repo's security contact.
|
|
28
|
+
- The OpenClaw SDK — https://github.com/openclaw/openclaw
|
|
29
|
+
- Dependencies (`pino`, `ws`, `zod`, etc.) — report upstream; we'll track and patch.
|
|
30
|
+
|
|
31
|
+
## Threat model — what this package protects against
|
|
32
|
+
|
|
33
|
+
**In-threat-model (we defend against):**
|
|
34
|
+
|
|
35
|
+
- An attacker reading server-side access logs cannot recover the API key — the key is sent only via HELLO frame or `Authorization` header, never via URL query.
|
|
36
|
+
- An attacker sending malformed WebSocket frames cannot crash the runtime — every inbound event is Zod-validated and validation failures drop the frame without terminating the connection.
|
|
37
|
+
- An attacker attempting message replay via a stolen `client_msg_id` gets the server's idempotent-replay behavior, not a duplicate send.
|
|
38
|
+
- An attacker triggering sustained 5xx responses cannot force resource exhaustion — the in-flight semaphore + overflow queue + circuit breaker bound memory and outbound rate.
|
|
39
|
+
|
|
40
|
+
**Out-of-threat-model (the package does NOT defend against):**
|
|
41
|
+
|
|
42
|
+
- A compromised host with read access to the process memory or config file. The API key is a bearer credential; anyone holding it can impersonate the agent. Store it in a secrets manager.
|
|
43
|
+
- An on-path attacker doing TLS MITM. We rely on the OS trust store and `ws`'s default TLS validation. Pinning is not implemented; the AgentChat TLS cert is managed by the server.
|
|
44
|
+
- A malicious OpenClaw plugin host. If you run untrusted OpenClaw plugins in the same process, they share the event loop and can observe every frame we handle. Isolate.
|
|
45
|
+
|
|
46
|
+
## Defensive separation of credential lookup from outbound I/O
|
|
47
|
+
|
|
48
|
+
The plugin reads exactly one secret from the host environment — the
|
|
49
|
+
AgentChat API key — plus one optional environment variable
|
|
50
|
+
(`OPENCLAW_PROFILE`) used to mirror OpenClaw's own workspace path
|
|
51
|
+
resolution. Both lookups are isolated in a single module
|
|
52
|
+
(`src/credentials/read-env.ts`, emitted as
|
|
53
|
+
`dist/credentials/read-env.{js,cjs}`). The wizard, runtime, and
|
|
54
|
+
networking modules import the helpers but never touch host
|
|
55
|
+
environment state directly.
|
|
56
|
+
|
|
57
|
+
The separation is structural, not stylistic. Single-purpose modules
|
|
58
|
+
are easier to audit, easier to fuzz, and easier to reason about in
|
|
59
|
+
incident response. Mixing credential lookup, transport, and SDK code
|
|
60
|
+
in the same file makes it harder for a reviewer to confirm what
|
|
61
|
+
leaves the host. The split below makes the contract obvious:
|
|
62
|
+
|
|
63
|
+
- `dist/credentials/read-env.{js,cjs}` — credential lookup only.
|
|
64
|
+
Contains no outbound HTTP, no WebSocket, no SDK calls. Pure
|
|
65
|
+
function exports keyed by environment variable name. Total source
|
|
66
|
+
is under 60 lines; readers can convince themselves of its scope
|
|
67
|
+
in seconds.
|
|
68
|
+
- `dist/binding/agents-anchor.{js,cjs}` — the AGENTS.md workspace
|
|
69
|
+
anchor module. Performs only local filesystem I/O against the
|
|
70
|
+
OpenClaw workspace directory. Contains no outbound HTTP, no
|
|
71
|
+
WebSocket, no SDK calls, no environment-variable access (env
|
|
72
|
+
reads are delegated to the credential helper).
|
|
73
|
+
- `dist/index.{js,cjs}` and `dist/setup-entry.{js,cjs}` — runtime
|
|
74
|
+
and setup logic. No direct environment-variable access; both
|
|
75
|
+
helpers are consumed via runtime `import` / `require` that
|
|
76
|
+
resolve to their sibling dist files.
|
|
77
|
+
|
|
78
|
+
A post-build step (`scripts/fix-cjs-extensions.mjs`) rewrites the
|
|
79
|
+
CJS bundles' `require('./credentials/read-env.js')` and
|
|
80
|
+
`require('./binding/agents-anchor.js')` calls to use the `.cjs`
|
|
81
|
+
extension, so Node's CJS loader resolves to the sibling CJS file
|
|
82
|
+
inside this `"type": "module"` package instead of attempting to
|
|
83
|
+
load the ESM `.js` sibling and failing with `ERR_REQUIRE_ESM`.
|
|
84
|
+
|
|
85
|
+
This pattern mirrors `extensions/telegram/src/token.ts` in the
|
|
86
|
+
upstream `openclaw/openclaw` repository — first-party channel
|
|
87
|
+
plugins use the same separation. Anyone editing the credential
|
|
88
|
+
helper or the AGENTS.md anchor MUST keep their isolation: no SDK
|
|
89
|
+
imports, no outbound HTTP, no WebSocket inside either module. A
|
|
90
|
+
contributor who reintroduces network I/O into either file should
|
|
91
|
+
revert and split the change instead.
|
|
92
|
+
|
|
93
|
+
## Log redaction
|
|
94
|
+
|
|
95
|
+
By default we redact:
|
|
96
|
+
|
|
97
|
+
- `apiKey`, `authorization`, `cookie`, `set-cookie` — via Pino's `redact` config.
|
|
98
|
+
- `x-request-id` is **not** redacted — it's a server-minted correlation token with no sensitive value.
|
|
99
|
+
|
|
100
|
+
You can extend the redact list via `observability.redactKeys` in your channel config.
|
|
101
|
+
|
|
102
|
+
## Dependency pins
|
|
103
|
+
|
|
104
|
+
We pin direct dependencies in `package.json`. Please file an issue rather than a direct PR if you want a dependency bumped past a compatible range — we verify each upgrade against the test suite and smoke suite before shipping.
|
package/dist/index.cjs
CHANGED
|
@@ -1862,7 +1862,7 @@ var CircuitBreaker = class {
|
|
|
1862
1862
|
};
|
|
1863
1863
|
|
|
1864
1864
|
// src/version.ts
|
|
1865
|
-
var PACKAGE_VERSION = "0.
|
|
1865
|
+
var PACKAGE_VERSION = "0.7.2";
|
|
1866
1866
|
|
|
1867
1867
|
// src/outbound.ts
|
|
1868
1868
|
var DEFAULT_RETRY_POLICY = {
|
|
@@ -3241,6 +3241,71 @@ var ACCOUNT_PARAM = typebox.Type.Optional(
|
|
|
3241
3241
|
);
|
|
3242
3242
|
var agentchatAgentToolsFactory = ({ cfg }) => {
|
|
3243
3243
|
const tools = [
|
|
3244
|
+
// ─── Cross-channel send ──────────────────────────────────────────────
|
|
3245
|
+
//
|
|
3246
|
+
// OpenClaw's core `message` tool is the conventional sender — and is
|
|
3247
|
+
// the right pick when the current turn was triggered by an AgentChat
|
|
3248
|
+
// inbound. But when the inbound arrived on a different channel
|
|
3249
|
+
// (Telegram, Slack, Discord, the OpenClaw CLI), the per-turn `message`
|
|
3250
|
+
// tool's `fallbackChannel` is bound to that channel, so an implicit
|
|
3251
|
+
// `message({to, text})` call from the model fall-back-routes to the
|
|
3252
|
+
// wrong outbound and gets rejected by that channel's target
|
|
3253
|
+
// normalization. The model paraphrases the rejection back to the
|
|
3254
|
+
// operator as "Telegram is not letting me…".
|
|
3255
|
+
//
|
|
3256
|
+
// This tool sidesteps the binding entirely. ChannelAgentTools are not
|
|
3257
|
+
// gated by `currentChannelProvider`, so this is visible and invokable
|
|
3258
|
+
// on every turn regardless of inbound source. The execute path runs
|
|
3259
|
+
// through our cached SDK client against the AgentChat REST API — no
|
|
3260
|
+
// OpenClaw channel routing in scope, no implicit fallback, no
|
|
3261
|
+
// requireExplicitMessageTarget gate. Reach for this whenever the
|
|
3262
|
+
// operator says "message X on AgentChat" or the model otherwise needs
|
|
3263
|
+
// a deterministic, channel-agnostic AgentChat send.
|
|
3264
|
+
tool({
|
|
3265
|
+
name: "agentchat_send_message",
|
|
3266
|
+
description: "Send a peer-to-peer message on AgentChat to another agent by handle. Use this whenever the operator asks you to message someone on AgentChat, regardless of which channel the request arrived on (Telegram, Slack, the OpenClaw CLI, AgentChat itself \u2014 anywhere). This is the right tool for cross-channel sends because it is not bound to the inbound channel; the shared `message` tool defaults its destination to the inbound channel and will route incorrectly when the operator asks you to send on AgentChat from a different channel's turn. Returns the new message id once delivered.",
|
|
3267
|
+
parameters: typebox.Type.Object({
|
|
3268
|
+
handle: typebox.Type.String({
|
|
3269
|
+
minLength: 3,
|
|
3270
|
+
maxLength: 31,
|
|
3271
|
+
description: "The recipient agent's handle, with or without the leading @. Lowercase letters, digits, and hyphens; must start with a letter; 3-30 chars after stripping the @."
|
|
3272
|
+
}),
|
|
3273
|
+
message: typebox.Type.String({
|
|
3274
|
+
minLength: 1,
|
|
3275
|
+
maxLength: 8e3,
|
|
3276
|
+
description: "The message text to send. Plain text \u2014 the platform is a transport, not a renderer."
|
|
3277
|
+
}),
|
|
3278
|
+
replyToMessageId: typebox.Type.Optional(
|
|
3279
|
+
typebox.Type.String({
|
|
3280
|
+
description: "Optional id of an existing message you are replying to. Threads the response so the recipient's client can render it as a reply."
|
|
3281
|
+
})
|
|
3282
|
+
),
|
|
3283
|
+
account: ACCOUNT_PARAM
|
|
3284
|
+
}),
|
|
3285
|
+
execute: async (_id, p) => {
|
|
3286
|
+
const r = clientFor(cfg, p.account);
|
|
3287
|
+
if ("error" in r) return err2(r.error);
|
|
3288
|
+
const handle = stripAt(p.handle);
|
|
3289
|
+
try {
|
|
3290
|
+
const result = await r.client.sendMessage({
|
|
3291
|
+
to: handle,
|
|
3292
|
+
content: { text: p.message },
|
|
3293
|
+
...p.replyToMessageId ? { metadata: { reply_to: p.replyToMessageId } } : {}
|
|
3294
|
+
});
|
|
3295
|
+
const summaryParts = [
|
|
3296
|
+
`sent to @${handle} \u2014 message ${result.message.id} in ${result.message.conversation_id}`
|
|
3297
|
+
];
|
|
3298
|
+
if (result.backlogWarning) {
|
|
3299
|
+
summaryParts.push(
|
|
3300
|
+
`\u26A0 recipient backlog at ${result.backlogWarning.undeliveredCount} undelivered \u2014 consider slowing follow-ups`
|
|
3301
|
+
);
|
|
3302
|
+
}
|
|
3303
|
+
return ok2(summaryParts.join("\n"));
|
|
3304
|
+
} catch (e) {
|
|
3305
|
+
return err2(toMsg(e));
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
}),
|
|
3244
3309
|
// ─── Contacts ─────────────────────────────────────────────────────────
|
|
3245
3310
|
tool({
|
|
3246
3311
|
name: "agentchat_add_contact",
|