@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 +31 -13
- package/dist/callbackEnvelope.d.ts +8 -0
- package/dist/callbackEnvelope.js +100 -0
- package/dist/template.d.ts +4 -1
- package/dist/template.js +16 -12
- package/dist/templateValueSchema.d.ts +2 -0
- package/dist/templateValueSchema.js +10 -0
- package/dist/toolSchema.d.ts +6 -1
- package/dist/toolSchema.js +9 -1
- package/dist/types.d.ts +7 -1
- package/dist/validator.d.ts +40 -0
- package/dist/validator.js +13 -2
- package/dist/watcherManager.js +13 -3
- package/package.json +1 -1
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.
|
|
92
|
-
4.
|
|
93
|
-
5.
|
|
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
|
-
|
|
106
|
+
Sample emitted envelope:
|
|
98
107
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
+
}
|
package/dist/template.d.ts
CHANGED
|
@@ -1 +1,4 @@
|
|
|
1
|
-
export
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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,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 });
|
package/dist/toolSchema.d.ts
CHANGED
|
@@ -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,
|
|
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;
|
package/dist/toolSchema.js
CHANGED
|
@@ -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(),
|
|
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,
|
|
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;
|
package/dist/validator.d.ts
CHANGED
|
@@ -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(),
|
|
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 }),
|
package/dist/watcherManager.js
CHANGED
|
@@ -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
|
|
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:
|
|
180
|
+
timestamp: matchedAt,
|
|
179
181
|
});
|
|
180
|
-
|
|
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);
|