@coffeexdev/openclaw-sentinel 0.3.0 → 0.4.1

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 CHANGED
@@ -58,6 +58,14 @@ openclaw gateway restart
58
58
  "fire": {
59
59
  "webhookPath": "/hooks/sentinel",
60
60
  "eventName": "eth_target_hit",
61
+ "intent": "price_threshold_review",
62
+ "contextTemplate": {
63
+ "asset": "ETH",
64
+ "priceUsd": "${payload.ethereum.usd}",
65
+ "workflow": "alerts"
66
+ },
67
+ "priority": "high",
68
+ "deadlineTemplate": "${timestamp}",
61
69
  "payloadTemplate": {
62
70
  "event": "${event.name}",
63
71
  "price": "${payload.ethereum.usd}",
@@ -87,25 +95,35 @@ Use `sentinel_control`:
87
95
  ## What happens when a watcher fires?
88
96
 
89
97
  1. Sentinel evaluates conditions.
90
- 2. On match, it dispatches to `localDispatchBase + webhookPath`.
91
- 3. It also sends a notification message to each configured `deliveryTargets` destination (defaults to the current chat context when watcher is created from a channel session).
92
- 4. For `/hooks/sentinel`, the plugin route enqueues a system event (instruction prefix + structured JSON envelope) and requests heartbeat wake.
93
- 5. OpenClaw wakes and processes that event in the configured session (`hookSessionKey`, default `agent:main:main`).
98
+ 2. On match, it dispatches a generic callback envelope (`type: "sentinel.callback"`) to `localDispatchBase + webhookPath`.
99
+ 3. The envelope includes stable keys (`intent`, `context`, `watcher`, `trigger`, bounded `payload`, `deliveryTargets`, `source`) so downstream agent behavior is workflow-agnostic.
100
+ 4. It also sends a notification message to each configured `deliveryTargets` destination (defaults to the current chat context when watcher is created from a channel session).
101
+ 5. For `/hooks/sentinel`, the plugin route enqueues an instruction-prefixed system event plus structured JSON envelope and requests heartbeat wake.
102
+ 6. OpenClaw wakes and processes that event in the configured session (`hookSessionKey`, default `agent:main:main`).
94
103
 
95
104
  The `/hooks/sentinel` route is auto-registered on plugin startup (idempotent).
96
105
 
97
- ### `/hooks/sentinel` wake event format
106
+ Sample emitted envelope:
98
107
 
99
- Sentinel enqueues deterministic system-event text in this shape:
100
-
101
- ```text
102
- SENTINEL_TRIGGER: This system event came from /hooks/sentinel. Evaluate action policy, decide whether to notify configured deliveryTargets, and execute safe follow-up actions.
103
- SENTINEL_ENVELOPE_JSON:
104
- { ...stable JSON envelope... }
108
+ ```json
109
+ {
110
+ "type": "sentinel.callback",
111
+ "version": "1",
112
+ "intent": "price_threshold_review",
113
+ "actionable": true,
114
+ "watcher": { "id": "eth-price-watch", "skillId": "skills.alerts", "eventName": "eth_target_hit" },
115
+ "trigger": {
116
+ "matchedAt": "2026-03-04T15:00:00.000Z",
117
+ "dedupeKey": "<sha256>",
118
+ "priority": "high"
119
+ },
120
+ "context": { "asset": "ETH", "priceUsd": 5001, "workflow": "alerts" },
121
+ "payload": { "ethereum": { "usd": 5001 } },
122
+ "deliveryTargets": [{ "channel": "telegram", "to": "5613673222" }],
123
+ "source": { "plugin": "openclaw-sentinel", "route": "/hooks/sentinel" }
124
+ }
105
125
  ```
106
126
 
107
- Envelope keys: `watcherId`, `eventName`, `skillId` (if present), `matchedAt`, `payload` (bounded with truncation marker when clipped), `dedupeKey`, `correlationId`, `deliveryTargets` (if present), `source` (`route`, `plugin`).
108
-
109
127
  ## Why Sentinel
110
128
 
111
129
  Sentinel runs watcher lifecycles inside the gateway with fixed strategies and declarative conditions.
@@ -0,0 +1,8 @@
1
+ import { WatcherDefinition } from "./types.js";
2
+ export declare function createCallbackEnvelope(args: {
3
+ watcher: WatcherDefinition;
4
+ payload: unknown;
5
+ payloadBody: Record<string, unknown>;
6
+ matchedAt: string;
7
+ webhookPath: string;
8
+ }): Record<string, unknown>;
@@ -0,0 +1,100 @@
1
+ import { createHash } from "node:crypto";
2
+ import { renderTemplate } from "./template.js";
3
+ const MAX_PAYLOAD_JSON_CHARS = 4000;
4
+ function toIntent(eventName) {
5
+ return (eventName
6
+ .replace(/[^a-zA-Z0-9]+/g, " ")
7
+ .trim()
8
+ .toLowerCase()
9
+ .replace(/\s+/g, "_") || "sentinel_event");
10
+ }
11
+ function summarizePayload(payload) {
12
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
13
+ return { summary: String(payload) };
14
+ }
15
+ const obj = payload;
16
+ const out = {};
17
+ for (const key of Object.keys(obj).slice(0, 12))
18
+ out[key] = obj[key];
19
+ return out;
20
+ }
21
+ function truncatePayload(payload) {
22
+ const serialized = JSON.stringify(payload);
23
+ if (!serialized)
24
+ return payload;
25
+ if (serialized.length <= MAX_PAYLOAD_JSON_CHARS)
26
+ return payload;
27
+ return {
28
+ truncated: true,
29
+ maxChars: MAX_PAYLOAD_JSON_CHARS,
30
+ preview: serialized.slice(0, MAX_PAYLOAD_JSON_CHARS),
31
+ };
32
+ }
33
+ function getPath(obj, path) {
34
+ return path.split(".").reduce((acc, part) => acc?.[part], obj);
35
+ }
36
+ function getTemplateString(value, context) {
37
+ if (!value)
38
+ return undefined;
39
+ if (!value.includes("${"))
40
+ return value;
41
+ if (/^\$\{[^}]+\}$/.test(value)) {
42
+ const rendered = renderTemplate({ value }, context);
43
+ const resolved = rendered.value;
44
+ if (resolved === undefined || resolved === null)
45
+ return undefined;
46
+ return String(resolved);
47
+ }
48
+ return value.replaceAll(/\$\{([^}]+)\}/g, (_full, path) => {
49
+ if (!/^(watcher\.(id|skillId)|event\.(name)|payload\.[a-zA-Z0-9_.-]+|timestamp)$/.test(path)) {
50
+ throw new Error(`Template placeholder not allowed: $\{${path}\}`);
51
+ }
52
+ const resolved = getPath(context, path);
53
+ if (resolved === undefined || resolved === null) {
54
+ throw new Error(`Template placeholder unresolved: $\{${path}\}`);
55
+ }
56
+ return String(resolved);
57
+ });
58
+ }
59
+ export function createCallbackEnvelope(args) {
60
+ const { watcher, payload, payloadBody, matchedAt, webhookPath } = args;
61
+ const context = {
62
+ watcher,
63
+ event: { name: watcher.fire.eventName },
64
+ payload,
65
+ timestamp: matchedAt,
66
+ };
67
+ const intent = watcher.fire.intent ?? toIntent(watcher.fire.eventName);
68
+ const renderedContext = watcher.fire.contextTemplate
69
+ ? renderTemplate(watcher.fire.contextTemplate, context)
70
+ : payloadBody;
71
+ const priority = watcher.fire.priority ?? "normal";
72
+ const deadline = getTemplateString(watcher.fire.deadlineTemplate, context);
73
+ const dedupeSeed = getTemplateString(watcher.fire.dedupeKeyTemplate, context) ??
74
+ `${watcher.id}|${watcher.fire.eventName}|${matchedAt}`;
75
+ const dedupeKey = createHash("sha256").update(dedupeSeed).digest("hex");
76
+ return {
77
+ type: "sentinel.callback",
78
+ version: "1",
79
+ intent,
80
+ actionable: true,
81
+ watcher: {
82
+ id: watcher.id,
83
+ skillId: watcher.skillId,
84
+ eventName: watcher.fire.eventName,
85
+ },
86
+ trigger: {
87
+ matchedAt,
88
+ dedupeKey,
89
+ priority,
90
+ ...(deadline ? { deadline } : {}),
91
+ },
92
+ context: renderedContext ?? summarizePayload(payload),
93
+ payload: truncatePayload(payload),
94
+ deliveryTargets: watcher.deliveryTargets ?? [],
95
+ source: {
96
+ plugin: "openclaw-sentinel",
97
+ route: webhookPath,
98
+ },
99
+ };
100
+ }
@@ -1 +1,4 @@
1
- export declare function renderTemplate(template: Record<string, string | number | boolean | null>, context: Record<string, unknown>): Record<string, unknown>;
1
+ export type TemplateValue = string | number | boolean | null | {
2
+ [key: string]: TemplateValue;
3
+ } | TemplateValue[];
4
+ export declare function renderTemplate(template: Record<string, TemplateValue>, context: Record<string, unknown>): Record<string, unknown>;
package/dist/template.js CHANGED
@@ -2,17 +2,12 @@ const placeholderPattern = /^\$\{(watcher\.(id|skillId)|event\.(name)|payload\.[
2
2
  function getPath(obj, path) {
3
3
  return path.split(".").reduce((acc, part) => acc?.[part], obj);
4
4
  }
5
- export function renderTemplate(template, context) {
6
- const out = {};
7
- for (const [key, value] of Object.entries(template)) {
8
- if (typeof value !== "string") {
9
- out[key] = value;
10
- continue;
11
- }
12
- if (!value.startsWith("${")) {
13
- out[key] = value;
14
- continue;
15
- }
5
+ function renderValue(value, context) {
6
+ if (value === null || typeof value === "number" || typeof value === "boolean")
7
+ return value;
8
+ if (typeof value === "string") {
9
+ if (!value.startsWith("${"))
10
+ return value;
16
11
  if (!placeholderPattern.test(value)) {
17
12
  throw new Error(`Template placeholder not allowed: ${value}`);
18
13
  }
@@ -20,7 +15,16 @@ export function renderTemplate(template, context) {
20
15
  const resolved = getPath(context, path);
21
16
  if (resolved === undefined)
22
17
  throw new Error(`Template placeholder unresolved: ${value}`);
23
- out[key] = resolved;
18
+ return resolved;
19
+ }
20
+ if (Array.isArray(value))
21
+ return value.map((item) => renderValue(item, context));
22
+ const out = {};
23
+ for (const [key, child] of Object.entries(value)) {
24
+ out[key] = renderValue(child, context);
24
25
  }
25
26
  return out;
26
27
  }
28
+ export function renderTemplate(template, context) {
29
+ return renderValue(template, context);
30
+ }
@@ -0,0 +1,2 @@
1
+ export declare const TEMPLATE_VALUE_SCHEMA_ID = "https://schemas.coffeexcoin.dev/openclaw-sentinel/template-value.json";
2
+ export declare const TemplateValueSchema: any;
@@ -0,0 +1,10 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ export const TEMPLATE_VALUE_SCHEMA_ID = "https://schemas.coffeexcoin.dev/openclaw-sentinel/template-value.json";
3
+ export const TemplateValueSchema = Type.Recursive((Self) => Type.Union([
4
+ Type.String(),
5
+ Type.Number(),
6
+ Type.Boolean(),
7
+ Type.Null(),
8
+ Type.Array(Self),
9
+ Type.Record(Type.String(), Self),
10
+ ]), { $id: TEMPLATE_VALUE_SCHEMA_ID });
@@ -21,7 +21,12 @@ export declare const SentinelToolSchema: import("@sinclair/typebox").TObject<{
21
21
  fire: import("@sinclair/typebox").TObject<{
22
22
  webhookPath: import("@sinclair/typebox").TString;
23
23
  eventName: import("@sinclair/typebox").TString;
24
- payloadTemplate: import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TString, import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TBoolean, import("@sinclair/typebox").TNull]>>;
24
+ payloadTemplate: import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, any>;
25
+ intent: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
26
+ contextTemplate: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, any>>;
27
+ priority: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"low">, import("@sinclair/typebox").TLiteral<"normal">, import("@sinclair/typebox").TLiteral<"high">, import("@sinclair/typebox").TLiteral<"critical">]>>;
28
+ deadlineTemplate: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
29
+ dedupeKeyTemplate: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
25
30
  }>;
26
31
  retry: import("@sinclair/typebox").TObject<{
27
32
  maxRetries: import("@sinclair/typebox").TNumber;
@@ -1,4 +1,5 @@
1
1
  import { Type } from "@sinclair/typebox";
2
+ import { TemplateValueSchema } from "./templateValueSchema.js";
2
3
  const ConditionSchema = Type.Object({
3
4
  path: Type.String({ description: "JSONPath expression to evaluate against the response" }),
4
5
  op: Type.Union([
@@ -23,9 +24,16 @@ const FireConfigSchema = Type.Object({
23
24
  description: "Path appended to localDispatchBase for webhook delivery",
24
25
  }),
25
26
  eventName: Type.String({ description: "Event name included in the dispatched payload" }),
26
- payloadTemplate: Type.Record(Type.String(), Type.Union([Type.String(), Type.Number(), Type.Boolean(), Type.Null()]), {
27
+ payloadTemplate: Type.Record(Type.String(), TemplateValueSchema, {
27
28
  description: "Key-value template for the webhook payload. Supports ${...} interpolation from matched response data.",
28
29
  }),
30
+ intent: Type.Optional(Type.String({ description: "Generic callback intent for downstream agent routing" })),
31
+ contextTemplate: Type.Optional(Type.Record(Type.String(), TemplateValueSchema, {
32
+ description: "Structured callback context template. Supports ${...} interpolation from matched response data.",
33
+ })),
34
+ priority: Type.Optional(Type.Union([Type.Literal("low"), Type.Literal("normal"), Type.Literal("high"), Type.Literal("critical")], { description: "Callback urgency hint" })),
35
+ deadlineTemplate: Type.Optional(Type.String({ description: "Optional templated deadline string for callback consumers" })),
36
+ dedupeKeyTemplate: Type.Optional(Type.String({ description: "Optional template to derive deterministic trigger dedupe key" })),
29
37
  });
30
38
  const RetryPolicySchema = Type.Object({
31
39
  maxRetries: Type.Number({ description: "Maximum number of retry attempts" }),
package/dist/types.d.ts CHANGED
@@ -6,10 +6,16 @@ export interface Condition {
6
6
  value?: unknown;
7
7
  }
8
8
  export declare const DEFAULT_SENTINEL_WEBHOOK_PATH = "/hooks/sentinel";
9
+ export type PriorityLevel = "low" | "normal" | "high" | "critical";
9
10
  export interface FireConfig {
10
11
  webhookPath?: string;
11
12
  eventName: string;
12
- payloadTemplate: Record<string, string | number | boolean | null>;
13
+ payloadTemplate: Record<string, import("./template.js").TemplateValue>;
14
+ intent?: string;
15
+ contextTemplate?: Record<string, import("./template.js").TemplateValue>;
16
+ priority?: PriorityLevel;
17
+ deadlineTemplate?: string;
18
+ dedupeKeyTemplate?: string;
13
19
  }
14
20
  export interface RetryPolicy {
15
21
  maxRetries: number;
@@ -1,2 +1,42 @@
1
1
  import { WatcherDefinition } from "./types.js";
2
+ export declare const WatcherSchema: import("@sinclair/typebox").TObject<{
3
+ id: import("@sinclair/typebox").TString;
4
+ skillId: import("@sinclair/typebox").TString;
5
+ enabled: import("@sinclair/typebox").TBoolean;
6
+ strategy: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"http-poll">, import("@sinclair/typebox").TLiteral<"websocket">, import("@sinclair/typebox").TLiteral<"sse">, import("@sinclair/typebox").TLiteral<"http-long-poll">]>;
7
+ endpoint: import("@sinclair/typebox").TString;
8
+ method: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"GET">, import("@sinclair/typebox").TLiteral<"POST">]>>;
9
+ headers: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
10
+ body: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
11
+ intervalMs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
12
+ timeoutMs: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TInteger>;
13
+ match: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"all">, import("@sinclair/typebox").TLiteral<"any">]>;
14
+ conditions: import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
15
+ path: import("@sinclair/typebox").TString;
16
+ op: import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"eq">, import("@sinclair/typebox").TLiteral<"neq">, import("@sinclair/typebox").TLiteral<"gt">, import("@sinclair/typebox").TLiteral<"gte">, import("@sinclair/typebox").TLiteral<"lt">, import("@sinclair/typebox").TLiteral<"lte">, import("@sinclair/typebox").TLiteral<"exists">, import("@sinclair/typebox").TLiteral<"absent">, import("@sinclair/typebox").TLiteral<"contains">, import("@sinclair/typebox").TLiteral<"matches">, import("@sinclair/typebox").TLiteral<"changed">]>;
17
+ value: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnknown>;
18
+ }>>;
19
+ fire: import("@sinclair/typebox").TObject<{
20
+ webhookPath: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
21
+ eventName: import("@sinclair/typebox").TString;
22
+ payloadTemplate: import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, any>;
23
+ intent: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
24
+ contextTemplate: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, any>>;
25
+ priority: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TLiteral<"low">, import("@sinclair/typebox").TLiteral<"normal">, import("@sinclair/typebox").TLiteral<"high">, import("@sinclair/typebox").TLiteral<"critical">]>>;
26
+ deadlineTemplate: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
27
+ dedupeKeyTemplate: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
28
+ }>;
29
+ retry: import("@sinclair/typebox").TObject<{
30
+ maxRetries: import("@sinclair/typebox").TInteger;
31
+ baseMs: import("@sinclair/typebox").TInteger;
32
+ maxMs: import("@sinclair/typebox").TInteger;
33
+ }>;
34
+ fireOnce: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
35
+ deliveryTargets: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TObject<{
36
+ channel: import("@sinclair/typebox").TString;
37
+ to: import("@sinclair/typebox").TString;
38
+ accountId: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
39
+ }>>>;
40
+ metadata: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TRecord<import("@sinclair/typebox").TString, import("@sinclair/typebox").TString>>;
41
+ }>;
2
42
  export declare function validateWatcherDefinition(input: unknown): WatcherDefinition;
package/dist/validator.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { Value } from "@sinclair/typebox/value";
3
+ import { TemplateValueSchema } from "./templateValueSchema.js";
3
4
  import { DEFAULT_SENTINEL_WEBHOOK_PATH } from "./types.js";
4
5
  const codeyKeyPattern = /(script|code|eval|handler|function|import|require)/i;
5
6
  const codeyValuePattern = /(=>|\bfunction\b|\bimport\s+|\brequire\s*\(|\beval\s*\()/i;
@@ -20,7 +21,7 @@ const ConditionSchema = Type.Object({
20
21
  ]),
21
22
  value: Type.Optional(Type.Unknown()),
22
23
  }, { additionalProperties: false });
23
- const WatcherSchema = Type.Object({
24
+ export const WatcherSchema = Type.Object({
24
25
  id: Type.String({ minLength: 1 }),
25
26
  skillId: Type.String({ minLength: 1 }),
26
27
  enabled: Type.Boolean(),
@@ -41,7 +42,17 @@ const WatcherSchema = Type.Object({
41
42
  fire: Type.Object({
42
43
  webhookPath: Type.Optional(Type.String({ pattern: "^/" })),
43
44
  eventName: Type.String({ minLength: 1 }),
44
- payloadTemplate: Type.Record(Type.String(), Type.Union([Type.String(), Type.Number(), Type.Boolean(), Type.Null()])),
45
+ payloadTemplate: Type.Record(Type.String(), TemplateValueSchema),
46
+ intent: Type.Optional(Type.String({ minLength: 1 })),
47
+ contextTemplate: Type.Optional(Type.Record(Type.String(), TemplateValueSchema)),
48
+ priority: Type.Optional(Type.Union([
49
+ Type.Literal("low"),
50
+ Type.Literal("normal"),
51
+ Type.Literal("high"),
52
+ Type.Literal("critical"),
53
+ ])),
54
+ deadlineTemplate: Type.Optional(Type.String({ minLength: 1 })),
55
+ dedupeKeyTemplate: Type.Optional(Type.String({ minLength: 1 })),
45
56
  }, { additionalProperties: false }),
46
57
  retry: Type.Object({
47
58
  maxRetries: Type.Integer({ minimum: 0, maximum: 20 }),
@@ -3,6 +3,7 @@ import { assertHostAllowed, assertWatcherLimits } from "./limits.js";
3
3
  import { defaultStatePath, loadState, saveState } from "./stateStore.js";
4
4
  import { renderTemplate } from "./template.js";
5
5
  import { validateWatcherDefinition } from "./validator.js";
6
+ import { createCallbackEnvelope } from "./callbackEnvelope.js";
6
7
  import { httpPollStrategy } from "./strategies/httpPoll.js";
7
8
  import { httpLongPollStrategy } from "./strategies/httpLongPoll.js";
8
9
  import { sseStrategy } from "./strategies/sse.js";
@@ -171,13 +172,22 @@ export class WatcherManager {
171
172
  rt.lastError = undefined;
172
173
  this.runtime[id] = rt;
173
174
  if (matched) {
174
- const body = renderTemplate(watcher.fire.payloadTemplate, {
175
+ const matchedAt = new Date().toISOString();
176
+ const payloadBody = renderTemplate(watcher.fire.payloadTemplate, {
175
177
  watcher,
176
178
  event: { name: watcher.fire.eventName },
177
179
  payload,
178
- timestamp: new Date().toISOString(),
180
+ timestamp: matchedAt,
179
181
  });
180
- await this.dispatcher.dispatch(watcher.fire.webhookPath ?? DEFAULT_SENTINEL_WEBHOOK_PATH, body);
182
+ const webhookPath = watcher.fire.webhookPath ?? DEFAULT_SENTINEL_WEBHOOK_PATH;
183
+ const body = createCallbackEnvelope({
184
+ watcher,
185
+ payload,
186
+ payloadBody,
187
+ matchedAt,
188
+ webhookPath,
189
+ });
190
+ await this.dispatcher.dispatch(webhookPath, body);
181
191
  if (watcher.deliveryTargets?.length && this.notifier) {
182
192
  const attemptedAt = new Date().toISOString();
183
193
  const message = JSON.stringify(body);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coffeexdev/openclaw-sentinel",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "Secure declarative gateway-native watcher plugin for OpenClaw",
5
5
  "keywords": [
6
6
  "openclaw",