@coffeexdev/openclaw-sentinel 0.5.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -17
- package/dist/callbackEnvelope.js +7 -3
- package/dist/configSchema.js +80 -18
- package/dist/evaluator.js +1 -3
- package/dist/index.js +270 -54
- package/dist/strategies/httpLongPoll.js +29 -3
- package/dist/strategies/httpPoll.js +36 -5
- package/dist/strategies/sse.js +45 -13
- package/dist/strategies/websocket.js +40 -5
- package/dist/template.js +1 -3
- package/dist/toolSchema.js +16 -3
- package/dist/types.d.ts +2 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +9 -0
- package/dist/validator.js +2 -1
- package/dist/watcherManager.d.ts +7 -0
- package/dist/watcherManager.js +57 -32
- package/openclaw.plugin.json +9 -9
- package/package.json +1 -1
package/dist/strategies/sse.js
CHANGED
|
@@ -1,41 +1,73 @@
|
|
|
1
|
+
function isAbortError(err) {
|
|
2
|
+
if (!(err instanceof Error))
|
|
3
|
+
return false;
|
|
4
|
+
return err.name === "AbortError" || err.message.toLowerCase().includes("aborted");
|
|
5
|
+
}
|
|
1
6
|
export const sseStrategy = async (watcher, onPayload, onError) => {
|
|
2
7
|
let active = true;
|
|
8
|
+
let inFlightAbort;
|
|
9
|
+
let sleepTimer;
|
|
10
|
+
const wait = (ms) => new Promise((resolve) => {
|
|
11
|
+
sleepTimer = setTimeout(() => {
|
|
12
|
+
sleepTimer = undefined;
|
|
13
|
+
resolve();
|
|
14
|
+
}, ms);
|
|
15
|
+
});
|
|
3
16
|
const loop = async () => {
|
|
4
17
|
while (active) {
|
|
5
18
|
try {
|
|
19
|
+
inFlightAbort = new AbortController();
|
|
6
20
|
const response = await fetch(watcher.endpoint, {
|
|
7
21
|
headers: { Accept: "text/event-stream", ...(watcher.headers ?? {}) },
|
|
8
|
-
signal: AbortSignal.
|
|
22
|
+
signal: AbortSignal.any([
|
|
23
|
+
inFlightAbort.signal,
|
|
24
|
+
AbortSignal.timeout(watcher.timeoutMs ?? 60000),
|
|
25
|
+
]),
|
|
26
|
+
redirect: "error",
|
|
9
27
|
});
|
|
10
28
|
if (!response.ok)
|
|
11
29
|
throw new Error(`sse non-2xx: ${response.status}`);
|
|
12
30
|
const contentType = response.headers.get("content-type") ?? "";
|
|
13
|
-
if (!contentType.toLowerCase().includes("text/event-stream"))
|
|
31
|
+
if (!contentType.toLowerCase().includes("text/event-stream")) {
|
|
14
32
|
throw new Error(`sse expected text/event-stream, got: ${contentType || "unknown"}`);
|
|
33
|
+
}
|
|
15
34
|
const text = await response.text();
|
|
16
35
|
for (const line of text.split("\n")) {
|
|
17
|
-
if (line.startsWith("data:"))
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
36
|
+
if (!line.startsWith("data:"))
|
|
37
|
+
continue;
|
|
38
|
+
const raw = line.slice(5).trim();
|
|
39
|
+
if (!raw)
|
|
40
|
+
continue;
|
|
41
|
+
try {
|
|
42
|
+
await onPayload(JSON.parse(raw));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
await onPayload({ message: raw });
|
|
27
46
|
}
|
|
28
47
|
}
|
|
29
|
-
|
|
48
|
+
if (active) {
|
|
49
|
+
await wait(watcher.intervalMs ?? 1000);
|
|
50
|
+
}
|
|
30
51
|
}
|
|
31
52
|
catch (err) {
|
|
53
|
+
if (!active && isAbortError(err))
|
|
54
|
+
return;
|
|
32
55
|
await onError(err);
|
|
33
56
|
return;
|
|
34
57
|
}
|
|
58
|
+
finally {
|
|
59
|
+
inFlightAbort = undefined;
|
|
60
|
+
}
|
|
35
61
|
}
|
|
36
62
|
};
|
|
37
63
|
void loop();
|
|
38
64
|
return async () => {
|
|
39
65
|
active = false;
|
|
66
|
+
inFlightAbort?.abort();
|
|
67
|
+
inFlightAbort = undefined;
|
|
68
|
+
if (sleepTimer) {
|
|
69
|
+
clearTimeout(sleepTimer);
|
|
70
|
+
sleepTimer = undefined;
|
|
71
|
+
}
|
|
40
72
|
};
|
|
41
73
|
};
|
|
@@ -1,14 +1,42 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
|
+
const DEFAULT_CONNECT_TIMEOUT_MS = 30_000;
|
|
2
3
|
export const websocketStrategy = async (watcher, onPayload, onError, callbacks) => {
|
|
3
4
|
let active = true;
|
|
4
5
|
let ws = null;
|
|
6
|
+
let connectTimer;
|
|
7
|
+
const clearConnectTimer = () => {
|
|
8
|
+
if (!connectTimer)
|
|
9
|
+
return;
|
|
10
|
+
clearTimeout(connectTimer);
|
|
11
|
+
connectTimer = undefined;
|
|
12
|
+
};
|
|
13
|
+
const connectTimeoutMs = Math.max(1, watcher.timeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS);
|
|
5
14
|
const connect = () => {
|
|
6
15
|
let pendingError = null;
|
|
7
16
|
let failureReported = false;
|
|
8
|
-
|
|
17
|
+
const reportFailure = (reason) => {
|
|
18
|
+
if (!active || failureReported)
|
|
19
|
+
return;
|
|
20
|
+
failureReported = true;
|
|
21
|
+
clearConnectTimer();
|
|
22
|
+
void onError(reason);
|
|
23
|
+
};
|
|
24
|
+
ws = new WebSocket(watcher.endpoint, {
|
|
25
|
+
headers: watcher.headers,
|
|
26
|
+
handshakeTimeout: connectTimeoutMs,
|
|
27
|
+
});
|
|
28
|
+
connectTimer = setTimeout(() => {
|
|
29
|
+
if (!active || !ws)
|
|
30
|
+
return;
|
|
31
|
+
if (ws.readyState === WebSocket.CONNECTING) {
|
|
32
|
+
pendingError = new Error(`websocket connect timeout after ${connectTimeoutMs}ms`);
|
|
33
|
+
ws.terminate();
|
|
34
|
+
}
|
|
35
|
+
}, connectTimeoutMs);
|
|
9
36
|
ws.on("open", () => {
|
|
10
37
|
if (!active)
|
|
11
38
|
return;
|
|
39
|
+
clearConnectTimer();
|
|
12
40
|
callbacks?.onConnect?.();
|
|
13
41
|
});
|
|
14
42
|
ws.on("message", async (data) => {
|
|
@@ -28,17 +56,24 @@ export const websocketStrategy = async (watcher, onPayload, onError, callbacks)
|
|
|
28
56
|
pendingError = err instanceof Error ? err : new Error(String(err));
|
|
29
57
|
});
|
|
30
58
|
ws.on("close", (code) => {
|
|
31
|
-
if (!active
|
|
59
|
+
if (!active)
|
|
32
60
|
return;
|
|
33
|
-
failureReported = true;
|
|
34
61
|
const reason = pendingError?.message ?? `websocket closed: ${code}`;
|
|
35
|
-
|
|
62
|
+
reportFailure(new Error(reason));
|
|
36
63
|
});
|
|
37
64
|
};
|
|
38
65
|
connect();
|
|
39
66
|
return async () => {
|
|
40
67
|
active = false;
|
|
41
|
-
|
|
68
|
+
clearConnectTimer();
|
|
69
|
+
if (!ws)
|
|
70
|
+
return;
|
|
71
|
+
if (ws.readyState === WebSocket.CONNECTING) {
|
|
72
|
+
ws.terminate();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
42
76
|
ws.close();
|
|
77
|
+
}
|
|
43
78
|
};
|
|
44
79
|
};
|
package/dist/template.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
+
import { getPath } from "./utils.js";
|
|
1
2
|
const placeholderPattern = /^\$\{(watcher\.(id|skillId)|event\.(name)|payload\.[a-zA-Z0-9_.-]+|timestamp)\}$/;
|
|
2
|
-
function getPath(obj, path) {
|
|
3
|
-
return path.split(".").reduce((acc, part) => acc?.[part], obj);
|
|
4
|
-
}
|
|
5
3
|
function renderValue(value, context) {
|
|
6
4
|
if (value === null || typeof value === "number" || typeof value === "boolean")
|
|
7
5
|
return value;
|
package/dist/toolSchema.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Type } from "@sinclair/typebox";
|
|
2
2
|
import { TemplateValueSchema } from "./templateValueSchema.js";
|
|
3
3
|
const TemplateValueRefSchema = Type.Ref(TemplateValueSchema);
|
|
4
|
+
const WATCHER_ID_PATTERN = "^[A-Za-z0-9_-]{1,128}$";
|
|
4
5
|
const ConditionSchema = Type.Object({
|
|
5
6
|
path: Type.String({ description: "JSONPath expression to evaluate against the response" }),
|
|
6
7
|
op: Type.Union([
|
|
@@ -58,7 +59,11 @@ const DeliveryTargetSchema = Type.Object({
|
|
|
58
59
|
accountId: Type.Optional(Type.String({ description: "Optional account id for multi-account channels" })),
|
|
59
60
|
}, { additionalProperties: false });
|
|
60
61
|
const WatcherSchema = Type.Object({
|
|
61
|
-
id: Type.String({
|
|
62
|
+
id: Type.String({
|
|
63
|
+
pattern: WATCHER_ID_PATTERN,
|
|
64
|
+
maxLength: 128,
|
|
65
|
+
description: "Unique watcher identifier (letters, numbers, hyphen, underscore)",
|
|
66
|
+
}),
|
|
62
67
|
skillId: Type.String({ description: "ID of the skill that owns this watcher" }),
|
|
63
68
|
enabled: Type.Boolean({ description: "Whether the watcher is actively polling" }),
|
|
64
69
|
strategy: Type.Union([
|
|
@@ -114,7 +119,11 @@ const CreateActionSchema = Type.Object({
|
|
|
114
119
|
}, { additionalProperties: false });
|
|
115
120
|
const IdActionSchema = Type.Object({
|
|
116
121
|
action: IdActionNameSchema,
|
|
117
|
-
id: Type.String({
|
|
122
|
+
id: Type.String({
|
|
123
|
+
pattern: WATCHER_ID_PATTERN,
|
|
124
|
+
maxLength: 128,
|
|
125
|
+
description: "Watcher ID for action target",
|
|
126
|
+
}),
|
|
118
127
|
}, { additionalProperties: false });
|
|
119
128
|
const ListActionSchema = Type.Object({
|
|
120
129
|
action: ListActionNameSchema,
|
|
@@ -127,7 +136,11 @@ export const SentinelToolValidationSchema = Type.Union([CreateActionSchema, IdAc
|
|
|
127
136
|
export const SentinelToolSchema = Type.Object({
|
|
128
137
|
action: AnyActionNameSchema,
|
|
129
138
|
watcher: Type.Optional(WatcherSchema),
|
|
130
|
-
id: Type.Optional(Type.String({
|
|
139
|
+
id: Type.Optional(Type.String({
|
|
140
|
+
pattern: WATCHER_ID_PATTERN,
|
|
141
|
+
maxLength: 128,
|
|
142
|
+
description: "Watcher ID for action target",
|
|
143
|
+
})),
|
|
131
144
|
}, {
|
|
132
145
|
additionalProperties: false,
|
|
133
146
|
$defs: {
|
package/dist/types.d.ts
CHANGED
|
@@ -67,6 +67,8 @@ export interface WatcherRuntimeState {
|
|
|
67
67
|
lastConnectAt?: string;
|
|
68
68
|
lastDisconnectAt?: string;
|
|
69
69
|
lastDisconnectReason?: string;
|
|
70
|
+
lastDispatchError?: string;
|
|
71
|
+
lastDispatchErrorAt?: string;
|
|
70
72
|
lastDelivery?: {
|
|
71
73
|
attemptedAt: string;
|
|
72
74
|
successCount: number;
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getPath(obj: unknown, path: string): unknown;
|
package/dist/utils.js
ADDED
package/dist/validator.js
CHANGED
|
@@ -5,6 +5,7 @@ import { DEFAULT_SENTINEL_WEBHOOK_PATH } from "./types.js";
|
|
|
5
5
|
const TemplateValueRefSchema = Type.Ref(TemplateValueSchema);
|
|
6
6
|
const codeyKeyPattern = /(script|code|eval|handler|function|import|require)/i;
|
|
7
7
|
const codeyValuePattern = /(=>|\bfunction\b|\bimport\s+|\brequire\s*\(|\beval\s*\()/i;
|
|
8
|
+
const WATCHER_ID_PATTERN = "^[A-Za-z0-9_-]{1,128}$";
|
|
8
9
|
const ConditionSchema = Type.Object({
|
|
9
10
|
path: Type.String({ minLength: 1 }),
|
|
10
11
|
op: Type.Union([
|
|
@@ -23,7 +24,7 @@ const ConditionSchema = Type.Object({
|
|
|
23
24
|
value: Type.Optional(Type.Unknown()),
|
|
24
25
|
}, { additionalProperties: false });
|
|
25
26
|
export const WatcherSchema = Type.Object({
|
|
26
|
-
id: Type.String({
|
|
27
|
+
id: Type.String({ pattern: WATCHER_ID_PATTERN, maxLength: 128 }),
|
|
27
28
|
skillId: Type.String({ minLength: 1 }),
|
|
28
29
|
enabled: Type.Boolean(),
|
|
29
30
|
strategy: Type.Union([
|
package/dist/watcherManager.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ export interface WatcherCreateContext {
|
|
|
6
6
|
export interface WatcherNotifier {
|
|
7
7
|
notify(target: DeliveryTarget, message: string): Promise<void>;
|
|
8
8
|
}
|
|
9
|
+
export interface WatcherLogger {
|
|
10
|
+
info?(message: string): void;
|
|
11
|
+
warn?(message: string): void;
|
|
12
|
+
error?(message: string): void;
|
|
13
|
+
}
|
|
9
14
|
export declare const backoff: (base: number, max: number, failures: number) => number;
|
|
10
15
|
export declare class WatcherManager {
|
|
11
16
|
private config;
|
|
@@ -16,6 +21,7 @@ export declare class WatcherManager {
|
|
|
16
21
|
private stops;
|
|
17
22
|
private retryTimers;
|
|
18
23
|
private statePath;
|
|
24
|
+
private logger?;
|
|
19
25
|
private webhookRegistration;
|
|
20
26
|
constructor(config: SentinelConfig, dispatcher: GatewayWebhookDispatcher, notifier?: WatcherNotifier | undefined);
|
|
21
27
|
init(): Promise<void>;
|
|
@@ -23,6 +29,7 @@ export declare class WatcherManager {
|
|
|
23
29
|
list(): WatcherDefinition[];
|
|
24
30
|
status(id: string): WatcherRuntimeState | undefined;
|
|
25
31
|
setNotifier(notifier: WatcherNotifier | undefined): void;
|
|
32
|
+
setLogger(logger: WatcherLogger | undefined): void;
|
|
26
33
|
setWebhookRegistrationStatus(status: "ok" | "error", message?: string, path?: string): void;
|
|
27
34
|
enable(id: string): Promise<void>;
|
|
28
35
|
disable(id: string): Promise<void>;
|
package/dist/watcherManager.js
CHANGED
|
@@ -50,6 +50,7 @@ export class WatcherManager {
|
|
|
50
50
|
stops = new Map();
|
|
51
51
|
retryTimers = new Map();
|
|
52
52
|
statePath;
|
|
53
|
+
logger;
|
|
53
54
|
webhookRegistration = {
|
|
54
55
|
path: DEFAULT_SENTINEL_WEBHOOK_PATH,
|
|
55
56
|
status: "pending",
|
|
@@ -81,6 +82,8 @@ export class WatcherManager {
|
|
|
81
82
|
lastEvaluated: prev?.lastEvaluated,
|
|
82
83
|
lastPayloadHash: prev?.lastPayloadHash,
|
|
83
84
|
lastPayload: prev?.lastPayload,
|
|
85
|
+
lastDispatchError: prev?.lastDispatchError,
|
|
86
|
+
lastDispatchErrorAt: prev?.lastDispatchErrorAt,
|
|
84
87
|
};
|
|
85
88
|
}
|
|
86
89
|
}
|
|
@@ -112,6 +115,9 @@ export class WatcherManager {
|
|
|
112
115
|
setNotifier(notifier) {
|
|
113
116
|
this.notifier = notifier;
|
|
114
117
|
}
|
|
118
|
+
setLogger(logger) {
|
|
119
|
+
this.logger = logger;
|
|
120
|
+
}
|
|
115
121
|
setWebhookRegistrationStatus(status, message, path) {
|
|
116
122
|
this.webhookRegistration = {
|
|
117
123
|
path: path ?? this.webhookRegistration.path,
|
|
@@ -213,39 +219,58 @@ export class WatcherManager {
|
|
|
213
219
|
matchedAt,
|
|
214
220
|
webhookPath,
|
|
215
221
|
});
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
const message =
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
failures.push({
|
|
234
|
-
target,
|
|
235
|
-
error: String(err?.message ?? err),
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
}));
|
|
239
|
-
rt.lastDelivery = {
|
|
240
|
-
attemptedAt,
|
|
241
|
-
successCount,
|
|
242
|
-
failureCount: failures.length,
|
|
243
|
-
failures: failures.length > 0 ? failures : undefined,
|
|
244
|
-
};
|
|
222
|
+
let dispatchSucceeded = false;
|
|
223
|
+
try {
|
|
224
|
+
await this.dispatcher.dispatch(webhookPath, body);
|
|
225
|
+
dispatchSucceeded = true;
|
|
226
|
+
rt.lastDispatchError = undefined;
|
|
227
|
+
rt.lastDispatchErrorAt = undefined;
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
const message = String(err?.message ?? err);
|
|
231
|
+
const status = err?.status;
|
|
232
|
+
rt.lastDispatchError = message;
|
|
233
|
+
rt.lastDispatchErrorAt = new Date().toISOString();
|
|
234
|
+
rt.lastError = message;
|
|
235
|
+
this.logger?.warn?.(`[openclaw-sentinel] Dispatch failed for watcher=${watcher.id} webhookPath=${webhookPath}: ${message}`);
|
|
236
|
+
if (status === 401 || status === 403) {
|
|
237
|
+
this.logger?.warn?.("[openclaw-sentinel] Dispatch authorization rejected (401/403). dispatchAuthToken may be missing or invalid. Sentinel now auto-detects gateway auth token when possible; explicit config/env overrides still take precedence.");
|
|
238
|
+
}
|
|
245
239
|
}
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
|
|
240
|
+
if (dispatchSucceeded) {
|
|
241
|
+
const deliveryMode = resolveNotificationPayloadMode(this.config, watcher);
|
|
242
|
+
const isSentinelWebhook = webhookPath === DEFAULT_SENTINEL_WEBHOOK_PATH;
|
|
243
|
+
if (deliveryMode !== "none" &&
|
|
244
|
+
watcher.deliveryTargets?.length &&
|
|
245
|
+
this.notifier &&
|
|
246
|
+
!isSentinelWebhook) {
|
|
247
|
+
const attemptedAt = new Date().toISOString();
|
|
248
|
+
const message = buildDeliveryNotificationMessage(watcher, body, deliveryMode);
|
|
249
|
+
const failures = [];
|
|
250
|
+
let successCount = 0;
|
|
251
|
+
await Promise.all(watcher.deliveryTargets.map(async (target) => {
|
|
252
|
+
try {
|
|
253
|
+
await this.notifier?.notify(target, message);
|
|
254
|
+
successCount += 1;
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
failures.push({
|
|
258
|
+
target,
|
|
259
|
+
error: String(err?.message ?? err),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}));
|
|
263
|
+
rt.lastDelivery = {
|
|
264
|
+
attemptedAt,
|
|
265
|
+
successCount,
|
|
266
|
+
failureCount: failures.length,
|
|
267
|
+
failures: failures.length > 0 ? failures : undefined,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
if (watcher.fireOnce) {
|
|
271
|
+
watcher.enabled = false;
|
|
272
|
+
await this.stopWatcher(id);
|
|
273
|
+
}
|
|
249
274
|
}
|
|
250
275
|
}
|
|
251
276
|
await this.persist();
|
package/openclaw.plugin.json
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dispatchAuthToken": {
|
|
21
21
|
"type": "string",
|
|
22
|
-
"description": "
|
|
22
|
+
"description": "Optional bearer token override for webhook dispatch auth. Sentinel auto-detects gateway auth token when available."
|
|
23
23
|
},
|
|
24
24
|
"hookSessionKey": {
|
|
25
25
|
"type": "string",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"description": "Optional default session group key. When set, callbacks without explicit hookSessionGroup are routed to this group session."
|
|
36
36
|
},
|
|
37
37
|
"hookRelayDedupeWindowMs": {
|
|
38
|
-
"type": "
|
|
38
|
+
"type": "integer",
|
|
39
39
|
"minimum": 0,
|
|
40
40
|
"description": "Suppress duplicate relay messages for the same dedupe key within this window (milliseconds)",
|
|
41
41
|
"default": 120000
|
|
@@ -56,29 +56,29 @@
|
|
|
56
56
|
"description": "Resource limits for watcher creation",
|
|
57
57
|
"properties": {
|
|
58
58
|
"maxWatchersTotal": {
|
|
59
|
-
"type": "
|
|
59
|
+
"type": "integer",
|
|
60
60
|
"description": "Maximum total watchers across all skills",
|
|
61
61
|
"default": 200
|
|
62
62
|
},
|
|
63
63
|
"maxWatchersPerSkill": {
|
|
64
|
-
"type": "
|
|
64
|
+
"type": "integer",
|
|
65
65
|
"description": "Maximum watchers per skill",
|
|
66
66
|
"default": 20
|
|
67
67
|
},
|
|
68
68
|
"maxConditionsPerWatcher": {
|
|
69
|
-
"type": "
|
|
69
|
+
"type": "integer",
|
|
70
70
|
"description": "Maximum conditions per watcher definition",
|
|
71
71
|
"default": 25
|
|
72
72
|
},
|
|
73
73
|
"maxIntervalMsFloor": {
|
|
74
|
-
"type": "
|
|
74
|
+
"type": "integer",
|
|
75
75
|
"description": "Minimum allowed polling interval in milliseconds",
|
|
76
76
|
"default": 1000
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
},
|
|
80
80
|
"hookResponseTimeoutMs": {
|
|
81
|
-
"type": "
|
|
81
|
+
"type": "integer",
|
|
82
82
|
"minimum": 0,
|
|
83
83
|
"description": "Milliseconds to wait for an assistant-authored hook response before optional fallback relay",
|
|
84
84
|
"default": 30000
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"default": "concise"
|
|
91
91
|
},
|
|
92
92
|
"hookResponseDedupeWindowMs": {
|
|
93
|
-
"type": "
|
|
93
|
+
"type": "integer",
|
|
94
94
|
"minimum": 0,
|
|
95
95
|
"description": "Deduplicate hook response-delivery contracts by dedupe key within this window (milliseconds)",
|
|
96
96
|
"default": 120000
|
|
@@ -108,7 +108,7 @@
|
|
|
108
108
|
},
|
|
109
109
|
"dispatchAuthToken": {
|
|
110
110
|
"label": "Dispatch Auth Token",
|
|
111
|
-
"help": "
|
|
111
|
+
"help": "Optional override for webhook dispatch authentication. Sentinel auto-detects gateway auth token when available.",
|
|
112
112
|
"sensitive": true,
|
|
113
113
|
"placeholder": "sk-..."
|
|
114
114
|
},
|