@velanir/openclaw-participation-gate 0.1.1 → 0.1.3
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 +10 -7
- package/dist/index.d.ts +8 -4
- package/dist/index.js +134 -29
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -8,9 +8,9 @@ The plugin is designed for the `requireMention: false` use case: the coworker ca
|
|
|
8
8
|
|
|
9
9
|
- Package: `@velanir/openclaw-participation-gate`
|
|
10
10
|
- OpenClaw plugin id: `velanir-participation-gate`
|
|
11
|
-
- Runtime hooks: `before_dispatch`, `message_sent`
|
|
11
|
+
- Runtime hooks: `before_dispatch`, `message_sent`, `reply_payload_sending`
|
|
12
12
|
- Default mode: `shadow`
|
|
13
|
-
- Target OpenClaw plugin API: `>=2026.
|
|
13
|
+
- Target OpenClaw plugin API: `>=2026.6.6`
|
|
14
14
|
|
|
15
15
|
The platform monorepo owns the source package in `packages/openclaw-plugins/participation-gate`. Customer machines install the built package into their OpenClaw runtime.
|
|
16
16
|
|
|
@@ -31,8 +31,10 @@ For group and channel messages, the plugin:
|
|
|
31
31
|
|
|
32
32
|
Explicit mentions still bypass classifier enforcement, but the inbound turn is
|
|
33
33
|
recorded before bypass so later unmentioned follow-ups have context. Successful
|
|
34
|
-
`message_sent` events
|
|
35
|
-
classifier recognize follow-ups
|
|
34
|
+
`message_sent` events and final `reply_payload_sending` payloads are also
|
|
35
|
+
recorded as coworker replies, which lets the classifier recognize follow-ups
|
|
36
|
+
after this coworker already answered. Duplicate outbound hook records are
|
|
37
|
+
deduped before they reach classifier context.
|
|
36
38
|
|
|
37
39
|
Any missing runtime dependency, missing context, invalid classifier output, classifier timeout, or classifier error fails open and lets the coworker respond. This is deliberate because false silence is worse than an occasional extra reply while the plugin is being rolled out.
|
|
38
40
|
|
|
@@ -58,9 +60,10 @@ logs `classifier_malformed` instead of treating it as a clean `classifier_true`.
|
|
|
58
60
|
|
|
59
61
|
Normal decision logs include the participation score, threshold, classifier
|
|
60
62
|
prompt version, prompt hash, input hash, parse status, attempt count, output
|
|
61
|
-
hash,
|
|
62
|
-
|
|
63
|
-
`logging.includeContent` are
|
|
63
|
+
hash, output length, recent message count, and recent user/assistant role
|
|
64
|
+
counts. Exact rendered prompt/input/raw classifier output and parse errors are
|
|
65
|
+
logged only when `logging.classifierDebug` and `logging.includeContent` are
|
|
66
|
+
both enabled.
|
|
64
67
|
|
|
65
68
|
## Context Contract
|
|
66
69
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
type OpenClawPluginApi = {
|
|
2
|
+
on: (hook: "before_dispatch" | "message_sent" | "reply_payload_sending", handler: (event: unknown, ctx: unknown) => unknown | Promise<unknown>, options?: {
|
|
3
|
+
priority?: number;
|
|
4
|
+
timeoutMs?: number;
|
|
5
|
+
}) => void;
|
|
6
|
+
};
|
|
2
7
|
|
|
3
8
|
declare const PLUGIN_ID = "velanir-participation-gate";
|
|
4
9
|
declare const _default: {
|
|
5
10
|
id: string;
|
|
6
11
|
name: string;
|
|
7
12
|
description: string;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} & Pick<openclaw_plugin_sdk_plugin_entry.OpenClawPluginDefinition, "kind" | "reload" | "nodeHostCommands" | "securityAuditCollectors">;
|
|
13
|
+
register(api: OpenClawPluginApi): void;
|
|
14
|
+
};
|
|
11
15
|
|
|
12
16
|
export { PLUGIN_ID, _default as default };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// src/api.ts
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import { definePluginEntry as defineOpenClawPluginEntry } from "openclaw/plugin-sdk/plugin-entry";
|
|
3
|
+
function definePluginEntry(entry) {
|
|
4
|
+
return defineOpenClawPluginEntry(entry);
|
|
5
|
+
}
|
|
5
6
|
|
|
6
7
|
// ../../oct8-secrets/dist/index.js
|
|
7
8
|
import { createHash, randomUUID } from "crypto";
|
|
@@ -2209,40 +2210,60 @@ function messageIsThread(event, ctx) {
|
|
|
2209
2210
|
event.messageThreadId || ctx.messageThreadId || event.parentSessionKey || ctx.parentSessionKey || event.threadStarterBody || threadHistoryText(event) || sessionKey.includes(":thread:") || sessionKey.includes("thread")
|
|
2210
2211
|
);
|
|
2211
2212
|
}
|
|
2213
|
+
function normalizeInboundHistoryMessage(message2) {
|
|
2214
|
+
const content = typeof message2.content === "string" ? message2.content.trim() : typeof message2.body === "string" ? message2.body.trim() : "";
|
|
2215
|
+
if (!content) {
|
|
2216
|
+
return void 0;
|
|
2217
|
+
}
|
|
2218
|
+
const senderName = message2.senderName ?? message2.sender;
|
|
2219
|
+
return {
|
|
2220
|
+
...message2.role ? { role: message2.role } : {},
|
|
2221
|
+
...message2.senderId ? { senderId: message2.senderId } : {},
|
|
2222
|
+
...senderName ? { senderName } : {},
|
|
2223
|
+
content,
|
|
2224
|
+
...message2.timestamp !== void 0 ? { timestamp: message2.timestamp } : {}
|
|
2225
|
+
};
|
|
2226
|
+
}
|
|
2212
2227
|
function eventRecentMessages(event, maxMessages) {
|
|
2213
2228
|
if (!Array.isArray(event.inboundHistory) || maxMessages <= 0) {
|
|
2214
2229
|
return [];
|
|
2215
2230
|
}
|
|
2216
|
-
return event.inboundHistory.filter((message2) => message2
|
|
2217
|
-
}
|
|
2218
|
-
function providerIsSlack(event, ctx) {
|
|
2219
|
-
const provider = event.provider ?? ctx.provider ?? event.surface ?? ctx.surface ?? event.channel;
|
|
2220
|
-
return provider?.toLowerCase() === "slack";
|
|
2231
|
+
return event.inboundHistory.map(normalizeInboundHistoryMessage).filter((message2) => Boolean(message2)).slice(-maxMessages);
|
|
2221
2232
|
}
|
|
2222
2233
|
function mergeRecentMessages(params) {
|
|
2223
2234
|
if (params.inboundHistory.length === 0) {
|
|
2224
2235
|
return params.localHistory;
|
|
2225
2236
|
}
|
|
2226
|
-
if (providerIsSlack(params.event, params.ctx)) {
|
|
2227
|
-
return params.inboundHistory;
|
|
2228
|
-
}
|
|
2229
2237
|
const merged = [];
|
|
2230
2238
|
const seen = /* @__PURE__ */ new Set();
|
|
2239
|
+
const messageKey = (message2) => [
|
|
2240
|
+
message2.role ?? "user",
|
|
2241
|
+
message2.senderId ?? "",
|
|
2242
|
+
message2.senderName ?? "",
|
|
2243
|
+
message2.timestamp ?? "",
|
|
2244
|
+
message2.content
|
|
2245
|
+
].join("\0");
|
|
2231
2246
|
for (const message2 of [...params.localHistory, ...params.inboundHistory]) {
|
|
2232
|
-
const key =
|
|
2233
|
-
message2.role ?? "user",
|
|
2234
|
-
message2.senderId ?? "",
|
|
2235
|
-
message2.senderName ?? "",
|
|
2236
|
-
message2.timestamp ?? "",
|
|
2237
|
-
message2.content
|
|
2238
|
-
].join("\0");
|
|
2247
|
+
const key = messageKey(message2);
|
|
2239
2248
|
if (seen.has(key)) {
|
|
2240
2249
|
continue;
|
|
2241
2250
|
}
|
|
2242
2251
|
seen.add(key);
|
|
2243
2252
|
merged.push(message2);
|
|
2244
2253
|
}
|
|
2245
|
-
|
|
2254
|
+
const selectedKeys = new Set(merged.slice(-params.maxMessages).map(messageKey));
|
|
2255
|
+
const localAssistantKeys = params.localHistory.filter((message2) => message2.role === "assistant").map(messageKey).filter((key, index, keys) => keys.indexOf(key) === index).slice(-params.maxMessages);
|
|
2256
|
+
for (const assistantKey of localAssistantKeys) {
|
|
2257
|
+
if (selectedKeys.has(assistantKey)) {
|
|
2258
|
+
continue;
|
|
2259
|
+
}
|
|
2260
|
+
const keyToDrop = merged.map(messageKey).find((key) => selectedKeys.has(key) && !localAssistantKeys.includes(key));
|
|
2261
|
+
if (keyToDrop) {
|
|
2262
|
+
selectedKeys.delete(keyToDrop);
|
|
2263
|
+
}
|
|
2264
|
+
selectedKeys.add(assistantKey);
|
|
2265
|
+
}
|
|
2266
|
+
return merged.filter((message2) => selectedKeys.has(messageKey(message2)));
|
|
2246
2267
|
}
|
|
2247
2268
|
function buildClassifierInput(params) {
|
|
2248
2269
|
const inboundHistory = eventRecentMessages(params.event, params.maxMessages);
|
|
@@ -2272,8 +2293,6 @@ function buildClassifierInput(params) {
|
|
|
2272
2293
|
historyBody: threadHistoryBody
|
|
2273
2294
|
} : void 0,
|
|
2274
2295
|
recentMessages: mergeRecentMessages({
|
|
2275
|
-
event: params.event,
|
|
2276
|
-
ctx: params.ctx,
|
|
2277
2296
|
inboundHistory,
|
|
2278
2297
|
localHistory: params.recentMessages,
|
|
2279
2298
|
maxMessages: params.maxMessages
|
|
@@ -2340,6 +2359,12 @@ async function decideParticipation(params) {
|
|
|
2340
2359
|
classifierOutputLength: classifierDecision.outputLength,
|
|
2341
2360
|
classifierParseError: classifierDecision.parseError,
|
|
2342
2361
|
classifierRecentMessageCount: classifierInput.recentMessages.length,
|
|
2362
|
+
classifierRecentUserMessageCount: classifierInput.recentMessages.filter(
|
|
2363
|
+
(message2) => message2.role !== "assistant"
|
|
2364
|
+
).length,
|
|
2365
|
+
classifierRecentAssistantMessageCount: classifierInput.recentMessages.filter(
|
|
2366
|
+
(message2) => message2.role === "assistant"
|
|
2367
|
+
).length,
|
|
2343
2368
|
classifierThreadContext: Boolean(classifierInput.thread?.starterBody || classifierInput.thread?.historyBody),
|
|
2344
2369
|
classifierCurrentMessageLength: classifierInput.currentMessage.content.length,
|
|
2345
2370
|
...params.config.logging.classifierDebug ? {
|
|
@@ -2440,15 +2465,33 @@ function outboundKeys(event, ctx) {
|
|
|
2440
2465
|
return keys.length > 0 ? keys : ["unknown"];
|
|
2441
2466
|
}
|
|
2442
2467
|
function publicMessages(messages, maxMessages) {
|
|
2443
|
-
return messages.sort((left, right) => left.sequence - right.sequence).slice(-maxMessages).map(({ sequence: _sequence, ...message2 }) => message2);
|
|
2468
|
+
return messages.sort((left, right) => left.sequence - right.sequence).slice(-maxMessages).map(({ sequence: _sequence, dedupeKey: _dedupeKey, ...message2 }) => message2);
|
|
2469
|
+
}
|
|
2470
|
+
function outboundDedupeKey(event, content) {
|
|
2471
|
+
if (event.runId) {
|
|
2472
|
+
return ["run", event.runId, content].join("\0");
|
|
2473
|
+
}
|
|
2474
|
+
if (event.sessionKey) {
|
|
2475
|
+
return ["session", event.sessionKey, content].join("\0");
|
|
2476
|
+
}
|
|
2477
|
+
return ["message", event.messageId ?? "", event.to ?? "", content].join("\0");
|
|
2444
2478
|
}
|
|
2445
2479
|
function createParticipationHistoryStore() {
|
|
2446
2480
|
const messagesByConversation = /* @__PURE__ */ new Map();
|
|
2447
2481
|
let nextSequence = 1;
|
|
2448
|
-
function recordForKeys(keys, message2, maxMessages) {
|
|
2482
|
+
function recordForKeys(keys, message2, maxMessages, dedupeKey) {
|
|
2483
|
+
if (dedupeKey) {
|
|
2484
|
+
for (const key of keys) {
|
|
2485
|
+
const current = messagesByConversation.get(key) ?? [];
|
|
2486
|
+
if (current.some((entry) => entry.dedupeKey === dedupeKey)) {
|
|
2487
|
+
return false;
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2449
2491
|
const stored = {
|
|
2450
2492
|
...message2,
|
|
2451
|
-
sequence: nextSequence
|
|
2493
|
+
sequence: nextSequence,
|
|
2494
|
+
...dedupeKey ? { dedupeKey } : {}
|
|
2452
2495
|
};
|
|
2453
2496
|
nextSequence += 1;
|
|
2454
2497
|
for (const key of keys) {
|
|
@@ -2459,6 +2502,7 @@ function createParticipationHistoryStore() {
|
|
|
2459
2502
|
}
|
|
2460
2503
|
messagesByConversation.set(key, current);
|
|
2461
2504
|
}
|
|
2505
|
+
return true;
|
|
2462
2506
|
}
|
|
2463
2507
|
function recordInbound(event, ctx, maxMessages) {
|
|
2464
2508
|
if (maxMessages <= 0 || event.isGroup !== true) {
|
|
@@ -2468,7 +2512,7 @@ function createParticipationHistoryStore() {
|
|
|
2468
2512
|
if (!content) {
|
|
2469
2513
|
return false;
|
|
2470
2514
|
}
|
|
2471
|
-
recordForKeys(
|
|
2515
|
+
return recordForKeys(
|
|
2472
2516
|
inboundKeys(event, ctx),
|
|
2473
2517
|
{
|
|
2474
2518
|
role: "user",
|
|
@@ -2479,7 +2523,6 @@ function createParticipationHistoryStore() {
|
|
|
2479
2523
|
},
|
|
2480
2524
|
maxMessages
|
|
2481
2525
|
);
|
|
2482
|
-
return true;
|
|
2483
2526
|
}
|
|
2484
2527
|
return {
|
|
2485
2528
|
recent(event, ctx, maxMessages) {
|
|
@@ -2507,7 +2550,7 @@ function createParticipationHistoryStore() {
|
|
|
2507
2550
|
if (!content) {
|
|
2508
2551
|
return false;
|
|
2509
2552
|
}
|
|
2510
|
-
recordForKeys(
|
|
2553
|
+
return recordForKeys(
|
|
2511
2554
|
outboundKeys(event, ctx),
|
|
2512
2555
|
{
|
|
2513
2556
|
role: "assistant",
|
|
@@ -2515,9 +2558,9 @@ function createParticipationHistoryStore() {
|
|
|
2515
2558
|
senderName: "This coworker",
|
|
2516
2559
|
content
|
|
2517
2560
|
},
|
|
2518
|
-
maxMessages
|
|
2561
|
+
maxMessages,
|
|
2562
|
+
outboundDedupeKey(event, content)
|
|
2519
2563
|
);
|
|
2520
|
-
return true;
|
|
2521
2564
|
},
|
|
2522
2565
|
record(event, ctx, maxMessages) {
|
|
2523
2566
|
return recordInbound(event, ctx, maxMessages);
|
|
@@ -2569,6 +2612,8 @@ function logParticipationDecision(params) {
|
|
|
2569
2612
|
field("outputHash", params.decision.classifierOutputHash),
|
|
2570
2613
|
field("outputLength", params.decision.classifierOutputLength),
|
|
2571
2614
|
field("recentMessages", params.decision.classifierRecentMessageCount),
|
|
2615
|
+
field("recentUserMessages", params.decision.classifierRecentUserMessageCount),
|
|
2616
|
+
field("recentAssistantMessages", params.decision.classifierRecentAssistantMessageCount),
|
|
2572
2617
|
field("threadContext", params.decision.classifierThreadContext),
|
|
2573
2618
|
field("currentLength", params.decision.classifierCurrentMessageLength),
|
|
2574
2619
|
contentLoggingEnabled(params.config) ? field("parseError", params.decision.classifierParseError) : void 0,
|
|
@@ -2590,6 +2635,8 @@ function logParticipationDecision(params) {
|
|
|
2590
2635
|
outputHash: params.decision.classifierOutputHash,
|
|
2591
2636
|
outputLength: params.decision.classifierOutputLength,
|
|
2592
2637
|
recentMessages: params.decision.classifierRecentMessageCount,
|
|
2638
|
+
recentUserMessages: params.decision.classifierRecentUserMessageCount,
|
|
2639
|
+
recentAssistantMessages: params.decision.classifierRecentAssistantMessageCount,
|
|
2593
2640
|
threadContext: params.decision.classifierThreadContext,
|
|
2594
2641
|
currentMessageLength: params.decision.classifierCurrentMessageLength,
|
|
2595
2642
|
outcome
|
|
@@ -2634,6 +2681,31 @@ function logParticipationHistoryEvent(params) {
|
|
|
2634
2681
|
|
|
2635
2682
|
// src/index.ts
|
|
2636
2683
|
var PLUGIN_ID = "velanir-participation-gate";
|
|
2684
|
+
function replyPayloadContent(event) {
|
|
2685
|
+
const text = event.payload?.text;
|
|
2686
|
+
if (typeof text !== "string") {
|
|
2687
|
+
return void 0;
|
|
2688
|
+
}
|
|
2689
|
+
const trimmed = text.trim();
|
|
2690
|
+
return trimmed || void 0;
|
|
2691
|
+
}
|
|
2692
|
+
function messageSentEventFromReplyPayload(event, ctx, content) {
|
|
2693
|
+
return {
|
|
2694
|
+
to: ctx.conversationId ?? ctx.channelId ?? event.channel,
|
|
2695
|
+
content,
|
|
2696
|
+
success: true,
|
|
2697
|
+
sessionKey: event.sessionKey ?? ctx.sessionKey,
|
|
2698
|
+
runId: event.runId ?? ctx.runId
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2701
|
+
function messageSentContextFromReplyPayload(event, ctx) {
|
|
2702
|
+
return {
|
|
2703
|
+
...ctx,
|
|
2704
|
+
...event.channel && !ctx.channelId ? { channelId: event.channel } : {},
|
|
2705
|
+
...event.sessionKey && !ctx.sessionKey ? { sessionKey: event.sessionKey } : {},
|
|
2706
|
+
...event.runId && !ctx.runId ? { runId: event.runId } : {}
|
|
2707
|
+
};
|
|
2708
|
+
}
|
|
2637
2709
|
var index_default = definePluginEntry({
|
|
2638
2710
|
id: PLUGIN_ID,
|
|
2639
2711
|
name: "Velanir Participation Gate",
|
|
@@ -2713,6 +2785,39 @@ var index_default = definePluginEntry({
|
|
|
2713
2785
|
});
|
|
2714
2786
|
return void 0;
|
|
2715
2787
|
});
|
|
2788
|
+
api.on("reply_payload_sending", (event, ctx) => {
|
|
2789
|
+
const replyEvent = event;
|
|
2790
|
+
if (replyEvent.kind !== void 0 && replyEvent.kind !== "final") {
|
|
2791
|
+
return void 0;
|
|
2792
|
+
}
|
|
2793
|
+
const content = replyPayloadContent(replyEvent);
|
|
2794
|
+
if (!content) {
|
|
2795
|
+
return void 0;
|
|
2796
|
+
}
|
|
2797
|
+
const replyContext = ctx;
|
|
2798
|
+
const messageSentEvent = messageSentEventFromReplyPayload(replyEvent, replyContext, content);
|
|
2799
|
+
const messageSentContext = messageSentContextFromReplyPayload(replyEvent, replyContext);
|
|
2800
|
+
const recorded = history.recordOutbound(
|
|
2801
|
+
messageSentEvent,
|
|
2802
|
+
messageSentContext,
|
|
2803
|
+
config.context.maxMessages
|
|
2804
|
+
);
|
|
2805
|
+
logParticipationHistoryEvent({
|
|
2806
|
+
logger: runtimeApi.logger,
|
|
2807
|
+
config,
|
|
2808
|
+
event: "reply_payload_outbound",
|
|
2809
|
+
fields: {
|
|
2810
|
+
recorded,
|
|
2811
|
+
kind: replyEvent.kind,
|
|
2812
|
+
channel: messageSentContext.channelId,
|
|
2813
|
+
conversation: messageSentContext.conversationId ?? messageSentEvent.to,
|
|
2814
|
+
session: messageSentContext.sessionKey ?? messageSentEvent.sessionKey,
|
|
2815
|
+
runId: messageSentEvent.runId
|
|
2816
|
+
},
|
|
2817
|
+
content
|
|
2818
|
+
});
|
|
2819
|
+
return void 0;
|
|
2820
|
+
});
|
|
2716
2821
|
}
|
|
2717
2822
|
});
|
|
2718
2823
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@velanir/openclaw-participation-gate",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "OpenClaw plugin that gates group/channel participation for Velanir digital coworkers.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"test": "pnpm --filter=@oct8/oct8-secrets build && vitest run"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"openclaw": ">=2026.
|
|
37
|
+
"openclaw": ">=2026.6.6"
|
|
38
38
|
},
|
|
39
39
|
"peerDependenciesMeta": {
|
|
40
40
|
"openclaw": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@oct8/oct8-secrets": "workspace:*",
|
|
46
46
|
"@types/node": "^22.19.1",
|
|
47
|
-
"openclaw": "2026.
|
|
47
|
+
"openclaw": "2026.6.6",
|
|
48
48
|
"tsup": "^8.4.0",
|
|
49
49
|
"typescript": "^5.7.0",
|
|
50
50
|
"vitest": "^4.0.0"
|
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
"./dist/index.js"
|
|
58
58
|
],
|
|
59
59
|
"compat": {
|
|
60
|
-
"pluginApi": ">=2026.
|
|
60
|
+
"pluginApi": ">=2026.6.6",
|
|
61
|
+
"minGatewayVersion": "2026.6.6"
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
}
|