@coffeexdev/openclaw-sentinel
v0.8.3
Published
Secure declarative gateway-native watcher plugin for OpenClaw
Maintainers
Readme
@coffeexdev/openclaw-sentinel
Secure, declarative, gateway-native background watcher plugin for OpenClaw.
Quick start (OpenClaw users)
If you only read one section, read this.
1) Install plugin
openclaw plugins install @coffeexdev/openclaw-sentinel2) Configure Sentinel
Add/update ~/.openclaw/openclaw.json:
{
plugins: {
entries: {
"openclaw-sentinel": {
enabled: true,
config: {
// Required: watchers can only call endpoints on these hosts.
allowedHosts: ["api.github.com", "api.coingecko.com"],
// Default dispatch base for internal webhook callbacks.
localDispatchBase: "http://127.0.0.1:18789",
// Optional: base prefix for isolated /hooks/sentinel callback sessions.
// Sentinel appends :watcher:<id> by default (or :group:<key> when grouped).
hookSessionPrefix: "agent:main:hooks:sentinel",
// Optional: default group key for callbacks without explicit hookSessionGroup.
// hookSessionGroup: "ops-alerts",
// Optional: suppress duplicate relays by dedupe key within this time window.
hookRelayDedupeWindowMs: 120000,
// Optional: guarantee hook-response delivery contract for /hooks/sentinel callbacks.
// Wait this long for assistant-authored output before fallback behavior applies.
hookResponseTimeoutMs: 30000,
// Optional: timeout fallback relay mode for /hooks/sentinel response contracts.
// "none" = no fallback message, "concise" = send a fail-safe relay line.
hookResponseFallbackMode: "concise",
// Optional: dedupe repeated callback response contracts by dedupe key.
hookResponseDedupeWindowMs: 120000,
// Optional: payload style for non-/hooks/sentinel deliveryTargets notifications.
// "none" suppresses delivery-target message fan-out (callback still fires).
// "concise" (default) sends human-friendly relay text only.
// "debug" appends a structured sentinel envelope block for diagnostics.
// notificationPayloadMode: "concise",
// Optional: max length for watcher.fire.operatorGoal.
// Default: 12000, min: 500, hard cap: 20000.
// Keep this as small as practical to reduce state + prompt bloat.
// maxOperatorGoalChars: 12000,
// Optional legacy alias for hookSessionPrefix (still supported).
// hookSessionKey: "agent:main:hooks:sentinel",
// Optional: explicit bearer token override for dispatch calls back to gateway.
// Sentinel auto-detects gateway auth token from runtime config when available,
// so manual token copy is usually no longer required.
// dispatchAuthToken: "<gateway-token>"
},
},
},
},
}3) Restart gateway
openclaw gateway restartTroubleshooting: Unrecognized key: "sentinel"
If gateway startup/validation reports:
Unrecognized key: "sentinel"your config is using the old root-level shape. Move Sentinel config under:
plugins.entries.openclaw-sentinel.config
Sentinel also logs a runtime warning when that legacy root key is still observable, but it never writes a root-level sentinel key.
Hardening notes (0.6 minor)
hookSessionKeyremains supported but is deprecated. If both are present,hookSessionPrefixnow wins.- HTTP watcher strategies now set
redirect: "error"to prevent host-allowlist bypass via redirects. - Watcher IDs are now constrained to
^[A-Za-z0-9_-]{1,128}$. /hooks/sentinelvalidates JSONContent-Typewhen provided and returns415for unsupported media types.
4) Create your first watcher (sentinel_control)
{
"action": "create",
"watcher": {
"id": "eth-price-watch",
"skillId": "skills.alerts",
"enabled": true,
"strategy": "http-poll",
"endpoint": "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd",
"intervalMs": 15000,
"match": "all",
"conditions": [{ "path": "ethereum.usd", "op": "gte", "value": 5000 }],
"fire": {
"webhookPath": "/hooks/sentinel",
"eventName": "eth_target_hit",
"intent": "price_threshold_review",
"contextTemplate": {
"asset": "ETH",
"priceUsd": "${payload.ethereum.usd}",
"workflow": "alerts"
},
"priority": "high",
"sessionGroup": "portfolio-risk",
"operatorGoal": "Confirm threshold breach, summarize impact, and notify on-call with actionable next steps.",
"deadlineTemplate": "${timestamp}",
"payloadTemplate": {
"event": "${event.name}",
"price": "${payload.ethereum.usd}",
"ts": "${timestamp}"
}
},
"retry": { "maxRetries": 5, "baseMs": 500, "maxMs": 15000 },
"fireOnce": true
}
}5) Verify
Use sentinel_control:
{ "action": "list" }{ "action": "status", "id": "eth-price-watch" }What happens when a watcher fires?
- Sentinel evaluates conditions.
- On match, it dispatches a generic callback envelope (
type: "sentinel.callback") tolocalDispatchBase + webhookPath. - The envelope includes stable keys (
intent,context,watcher,trigger, boundedpayload,deliveryTargets,deliveryContext,source) so downstream agent behavior is workflow-agnostic. - For
/hooks/sentinel, Sentinel enqueues an instruction-prefixed system event with a structured callback prompt context (watcher,trigger,source,deliveryTargets,deliveryContext,context,payload) plus the full envelope, then requests an immediatecron:sentinel-callbackwake (avoids heartbeat-poll prompting). - The hook route creates a response-delivery contract keyed by callback dedupe key, preserving original chat/session context (
deliveryContext) and intended relay targets. - OpenClaw processes each callback in an isolated hook session: per-watcher by default, or grouped when
hookSessionGroup/fire.sessionGroupis set. Shared global hook-session mode is intentionally not supported. - Relay guardrails suppress control-token outputs (
NO_REPLY,HEARTBEAT_OK, empty variants). If model output is unusable, Sentinel emits a concise contextual fallback message. Timeout fallback behavior still followshookResponseFallbackMode.
The /hooks/sentinel route is auto-registered on plugin startup (idempotent). Response contracts are dedupe-aware by callback dedupe key (hookResponseDedupeWindowMs).
Sample emitted envelope:
{
"type": "sentinel.callback",
"version": "1",
"intent": "price_threshold_review",
"actionable": true,
"watcher": {
"id": "eth-price-watch",
"skillId": "skills.alerts",
"eventName": "eth_target_hit",
"intent": "price_threshold_review",
"strategy": "http-poll",
"endpoint": "https://api.coingecko.com/api/v3/simple/price?ids=ethereum&vs_currencies=usd",
"match": "all",
"conditions": [{ "path": "ethereum.usd", "op": "gte", "value": 5000 }],
"fireOnce": false
},
"trigger": {
"matchedAt": "2026-03-04T15:00:00.000Z",
"dedupeKey": "<sha256>",
"priority": "high"
},
"context": { "asset": "ETH", "priceUsd": 5001, "workflow": "alerts" },
"payload": { "ethereum": { "usd": 5001 } },
"deliveryTargets": [{ "channel": "telegram", "to": "5613673222" }],
"deliveryContext": {
"sessionKey": "agent:main:telegram:direct:5613673222",
"messageChannel": "telegram",
"requesterSenderId": "5613673222",
"currentChat": { "channel": "telegram", "to": "5613673222" }
},
"source": { "plugin": "openclaw-sentinel", "route": "/hooks/sentinel" }
}fire.operatorGoal length guidance
- Default max length is 12000 chars (raised from 500).
- You can tune this with plugin config:
maxOperatorGoalChars(min 500, hard cap 20000). - Recommendation: keep most goals in the 200-2000 char range for clarity and lower prompt/state overhead.
- Use larger goals only when you genuinely need richer policy/runbook context.
Tradeoff: larger values improve callback guidance but also increase persisted watcher size and callback prompt footprint, so unbounded values are intentionally not allowed.
Migration: existing watchers under the old 500-char limit continue to work unchanged. No migration action is required unless you want to add richer guidance.
Why Sentinel
Sentinel runs watcher lifecycles inside the gateway with fixed strategies and declarative conditions. It does not execute user-authored code from watcher definitions.
Features
- Tool registration:
sentinel_control- actions:
create(add),enable,disable,remove(delete),status(get),list
- actions:
- Strict schema validation (TypeBox, strict object checks) + code-like field/value rejection
- Strategies:
http-poll(supports POST with custom body for JSON-RPC etc.)websocketssehttp-long-pollevm-call— smart contract state polling viaeth_callwith ABI encoding/decoding
- Condition operators:
eq,neq,gt,gte,lt,lte,exists,absent,contains,matches,changed
- Match mode:
all/any - Fire templating: substitution-only placeholders, non-Turing-complete
- Local webhook dispatch model (no outbound custom fire URL)
- Default callback route:
/hooks/sentinel(auto-registered) - Persistence:
~/.openclaw/sentinel-state.json - Resource limits and per-skill limits
allowedHostsendpoint enforcement- CLI surface:
list,status,enable,disable,audit
Tool input example (sentinel_control:create)
{
"action": "create",
"watcher": {
"id": "sentinel-alert",
"skillId": "skills.general-monitor",
"enabled": true,
"strategy": "http-poll",
"endpoint": "https://api.github.com/events",
"intervalMs": 15000,
"match": "any",
"conditions": [{ "path": "type", "op": "eq", "value": "PushEvent" }],
"fire": {
"webhookPath": "/hooks/sentinel",
"eventName": "sentinel_push",
"payloadTemplate": {
"watcher": "${watcher.id}",
"event": "${event.name}",
"type": "${payload.type}",
"ts": "${timestamp}"
}
},
"retry": { "maxRetries": 5, "baseMs": 250, "maxMs": 5000 },
"deliveryTargets": [
{ "channel": "telegram", "to": "5613673222" },
{ "channel": "discord", "to": "123456789012345678", "accountId": "main" }
]
}
}deliveryTargets is optional. If omitted on create, Sentinel infers a default target from the current tool/session context (channel + current peer).
Notification payload delivery modes
Sentinel always dispatches the callback envelope to localDispatchBase + webhookPath on match.
notificationPayloadMode only controls additional fan-out messages to deliveryTargets for watcher dispatches (for example /hooks/agent).
It does not control /hooks/sentinel hook-response contracts or assistant-output relay behavior.
Global mode options:
none: suppress delivery-target notification messages (callback dispatch still occurs)concise(default): send short relay text onlydebug: send relay text plusSENTINEL_DEBUG_ENVELOPE_JSONblock
1) Global notifications disabled (none)
{
plugins: {
entries: {
"openclaw-sentinel": {
enabled: true,
config: {
allowedHosts: ["api.github.com"],
notificationPayloadMode: "none",
},
},
},
},
}2) Global concise relay (default)
{
plugins: {
entries: {
"openclaw-sentinel": {
enabled: true,
config: {
allowedHosts: ["api.github.com"],
notificationPayloadMode: "concise",
},
},
},
},
}3) Global debug diagnostics
{
plugins: {
entries: {
"openclaw-sentinel": {
enabled: true,
config: {
allowedHosts: ["api.github.com"],
notificationPayloadMode: "debug",
},
},
},
},
}In debug mode, delivery notifications include the same concise relay line plus a SENTINEL_DEBUG_ENVELOPE_JSON block for diagnostics.
4) Per-watcher override (watcher.fire.notificationPayloadMode)
{
"action": "create",
"watcher": {
"id": "status-watch",
"skillId": "skills.ops",
"enabled": true,
"strategy": "http-poll",
"endpoint": "https://status.example.com/api/health",
"intervalMs": 10000,
"match": "all",
"conditions": [{ "path": "status", "op": "eq", "value": "degraded" }],
"fire": {
"webhookPath": "/hooks/agent",
"eventName": "service_degraded",
"notificationPayloadMode": "none",
"payloadTemplate": { "event": "${event.name}", "status": "${payload.status}" }
},
"retry": { "maxRetries": 5, "baseMs": 250, "maxMs": 5000 }
}
}Allowed values:
inherit(or omitted): follow globalnotificationPayloadModenone: suppress delivery-target notification messages for this watcherconcise: force concise notification text for this watcherdebug: force debug envelope output for this watcher
Precedence: watcher override > global setting.
Migration notes
- Existing installs keep default behavior (
concise) unless you setnotificationPayloadModeexplicitly. - If you want callback-only operation (wake LLM loop via
/hooks/sentinelbut no delivery-target chat message), set global or per-watcher mode tonone.
Hook-response delivery contract (/hooks/sentinel)
/hooks/sentinel now enforces a dedicated trigger → LLM → user-visible relay contract:
- Callback is enqueued to isolated hook session.
- Contract captures original delivery context (
deliveryContext+ resolveddeliveryTargets). - The LLM calls
sentinel_act notifyto deliver results to targets (sole delivery mechanism). - Any successful
sentinel_actcall fulfills the relay contract and cancels the timeout timer. - If no
sentinel_actcall arrives in time (hookResponseTimeoutMs), timeout fallback is configurable:hookResponseFallbackMode: "concise"(default) sends a short fail-safe relay.hookResponseFallbackMode: "none"suppresses timeout fallback.
- Repeated callbacks with same dedupe key are idempotent within
hookResponseDedupeWindowMs.
Example config:
{
plugins: {
entries: {
"openclaw-sentinel": {
enabled: true,
config: {
allowedHosts: ["api.github.com"],
hookResponseTimeoutMs: 30000,
hookResponseFallbackMode: "concise",
hookResponseDedupeWindowMs: 120000,
},
},
},
},
}Runtime controls
{ "action": "status", "id": "sentinel-alert" }{ "action": "disable", "id": "sentinel-alert" }{ "action": "remove", "id": "sentinel-alert" }JSON Schema
Formal JSON Schema for sentinel config/watchers:
schema/sentinel.schema.json
Documentation
CLI
openclaw-sentinel list
openclaw-sentinel status <watcher-id>
openclaw-sentinel enable <watcher-id>
openclaw-sentinel disable <watcher-id>
openclaw-sentinel auditDevelopment
pnpm install
pnpm run lint
pnpm test
pnpm run buildLicense
MIT
