@velanir/openclaw-participation-gate 0.1.1 → 0.1.2
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 +113 -21
- 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.5.
|
|
13
|
+
- Target OpenClaw plugin API: `>=2026.5.5`
|
|
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,23 +2210,30 @@ 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();
|
|
2231
2239
|
for (const message2 of [...params.localHistory, ...params.inboundHistory]) {
|
|
@@ -2272,8 +2280,6 @@ function buildClassifierInput(params) {
|
|
|
2272
2280
|
historyBody: threadHistoryBody
|
|
2273
2281
|
} : void 0,
|
|
2274
2282
|
recentMessages: mergeRecentMessages({
|
|
2275
|
-
event: params.event,
|
|
2276
|
-
ctx: params.ctx,
|
|
2277
2283
|
inboundHistory,
|
|
2278
2284
|
localHistory: params.recentMessages,
|
|
2279
2285
|
maxMessages: params.maxMessages
|
|
@@ -2340,6 +2346,12 @@ async function decideParticipation(params) {
|
|
|
2340
2346
|
classifierOutputLength: classifierDecision.outputLength,
|
|
2341
2347
|
classifierParseError: classifierDecision.parseError,
|
|
2342
2348
|
classifierRecentMessageCount: classifierInput.recentMessages.length,
|
|
2349
|
+
classifierRecentUserMessageCount: classifierInput.recentMessages.filter(
|
|
2350
|
+
(message2) => message2.role !== "assistant"
|
|
2351
|
+
).length,
|
|
2352
|
+
classifierRecentAssistantMessageCount: classifierInput.recentMessages.filter(
|
|
2353
|
+
(message2) => message2.role === "assistant"
|
|
2354
|
+
).length,
|
|
2343
2355
|
classifierThreadContext: Boolean(classifierInput.thread?.starterBody || classifierInput.thread?.historyBody),
|
|
2344
2356
|
classifierCurrentMessageLength: classifierInput.currentMessage.content.length,
|
|
2345
2357
|
...params.config.logging.classifierDebug ? {
|
|
@@ -2440,15 +2452,33 @@ function outboundKeys(event, ctx) {
|
|
|
2440
2452
|
return keys.length > 0 ? keys : ["unknown"];
|
|
2441
2453
|
}
|
|
2442
2454
|
function publicMessages(messages, maxMessages) {
|
|
2443
|
-
return messages.sort((left, right) => left.sequence - right.sequence).slice(-maxMessages).map(({ sequence: _sequence, ...message2 }) => message2);
|
|
2455
|
+
return messages.sort((left, right) => left.sequence - right.sequence).slice(-maxMessages).map(({ sequence: _sequence, dedupeKey: _dedupeKey, ...message2 }) => message2);
|
|
2456
|
+
}
|
|
2457
|
+
function outboundDedupeKey(event, content) {
|
|
2458
|
+
if (event.runId) {
|
|
2459
|
+
return ["run", event.runId, content].join("\0");
|
|
2460
|
+
}
|
|
2461
|
+
if (event.sessionKey) {
|
|
2462
|
+
return ["session", event.sessionKey, content].join("\0");
|
|
2463
|
+
}
|
|
2464
|
+
return ["message", event.messageId ?? "", event.to ?? "", content].join("\0");
|
|
2444
2465
|
}
|
|
2445
2466
|
function createParticipationHistoryStore() {
|
|
2446
2467
|
const messagesByConversation = /* @__PURE__ */ new Map();
|
|
2447
2468
|
let nextSequence = 1;
|
|
2448
|
-
function recordForKeys(keys, message2, maxMessages) {
|
|
2469
|
+
function recordForKeys(keys, message2, maxMessages, dedupeKey) {
|
|
2470
|
+
if (dedupeKey) {
|
|
2471
|
+
for (const key of keys) {
|
|
2472
|
+
const current = messagesByConversation.get(key) ?? [];
|
|
2473
|
+
if (current.some((entry) => entry.dedupeKey === dedupeKey)) {
|
|
2474
|
+
return false;
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2449
2478
|
const stored = {
|
|
2450
2479
|
...message2,
|
|
2451
|
-
sequence: nextSequence
|
|
2480
|
+
sequence: nextSequence,
|
|
2481
|
+
...dedupeKey ? { dedupeKey } : {}
|
|
2452
2482
|
};
|
|
2453
2483
|
nextSequence += 1;
|
|
2454
2484
|
for (const key of keys) {
|
|
@@ -2459,6 +2489,7 @@ function createParticipationHistoryStore() {
|
|
|
2459
2489
|
}
|
|
2460
2490
|
messagesByConversation.set(key, current);
|
|
2461
2491
|
}
|
|
2492
|
+
return true;
|
|
2462
2493
|
}
|
|
2463
2494
|
function recordInbound(event, ctx, maxMessages) {
|
|
2464
2495
|
if (maxMessages <= 0 || event.isGroup !== true) {
|
|
@@ -2468,7 +2499,7 @@ function createParticipationHistoryStore() {
|
|
|
2468
2499
|
if (!content) {
|
|
2469
2500
|
return false;
|
|
2470
2501
|
}
|
|
2471
|
-
recordForKeys(
|
|
2502
|
+
return recordForKeys(
|
|
2472
2503
|
inboundKeys(event, ctx),
|
|
2473
2504
|
{
|
|
2474
2505
|
role: "user",
|
|
@@ -2479,7 +2510,6 @@ function createParticipationHistoryStore() {
|
|
|
2479
2510
|
},
|
|
2480
2511
|
maxMessages
|
|
2481
2512
|
);
|
|
2482
|
-
return true;
|
|
2483
2513
|
}
|
|
2484
2514
|
return {
|
|
2485
2515
|
recent(event, ctx, maxMessages) {
|
|
@@ -2507,7 +2537,7 @@ function createParticipationHistoryStore() {
|
|
|
2507
2537
|
if (!content) {
|
|
2508
2538
|
return false;
|
|
2509
2539
|
}
|
|
2510
|
-
recordForKeys(
|
|
2540
|
+
return recordForKeys(
|
|
2511
2541
|
outboundKeys(event, ctx),
|
|
2512
2542
|
{
|
|
2513
2543
|
role: "assistant",
|
|
@@ -2515,9 +2545,9 @@ function createParticipationHistoryStore() {
|
|
|
2515
2545
|
senderName: "This coworker",
|
|
2516
2546
|
content
|
|
2517
2547
|
},
|
|
2518
|
-
maxMessages
|
|
2548
|
+
maxMessages,
|
|
2549
|
+
outboundDedupeKey(event, content)
|
|
2519
2550
|
);
|
|
2520
|
-
return true;
|
|
2521
2551
|
},
|
|
2522
2552
|
record(event, ctx, maxMessages) {
|
|
2523
2553
|
return recordInbound(event, ctx, maxMessages);
|
|
@@ -2569,6 +2599,8 @@ function logParticipationDecision(params) {
|
|
|
2569
2599
|
field("outputHash", params.decision.classifierOutputHash),
|
|
2570
2600
|
field("outputLength", params.decision.classifierOutputLength),
|
|
2571
2601
|
field("recentMessages", params.decision.classifierRecentMessageCount),
|
|
2602
|
+
field("recentUserMessages", params.decision.classifierRecentUserMessageCount),
|
|
2603
|
+
field("recentAssistantMessages", params.decision.classifierRecentAssistantMessageCount),
|
|
2572
2604
|
field("threadContext", params.decision.classifierThreadContext),
|
|
2573
2605
|
field("currentLength", params.decision.classifierCurrentMessageLength),
|
|
2574
2606
|
contentLoggingEnabled(params.config) ? field("parseError", params.decision.classifierParseError) : void 0,
|
|
@@ -2590,6 +2622,8 @@ function logParticipationDecision(params) {
|
|
|
2590
2622
|
outputHash: params.decision.classifierOutputHash,
|
|
2591
2623
|
outputLength: params.decision.classifierOutputLength,
|
|
2592
2624
|
recentMessages: params.decision.classifierRecentMessageCount,
|
|
2625
|
+
recentUserMessages: params.decision.classifierRecentUserMessageCount,
|
|
2626
|
+
recentAssistantMessages: params.decision.classifierRecentAssistantMessageCount,
|
|
2593
2627
|
threadContext: params.decision.classifierThreadContext,
|
|
2594
2628
|
currentMessageLength: params.decision.classifierCurrentMessageLength,
|
|
2595
2629
|
outcome
|
|
@@ -2634,6 +2668,31 @@ function logParticipationHistoryEvent(params) {
|
|
|
2634
2668
|
|
|
2635
2669
|
// src/index.ts
|
|
2636
2670
|
var PLUGIN_ID = "velanir-participation-gate";
|
|
2671
|
+
function replyPayloadContent(event) {
|
|
2672
|
+
const text = event.payload?.text;
|
|
2673
|
+
if (typeof text !== "string") {
|
|
2674
|
+
return void 0;
|
|
2675
|
+
}
|
|
2676
|
+
const trimmed = text.trim();
|
|
2677
|
+
return trimmed || void 0;
|
|
2678
|
+
}
|
|
2679
|
+
function messageSentEventFromReplyPayload(event, ctx, content) {
|
|
2680
|
+
return {
|
|
2681
|
+
to: ctx.conversationId ?? ctx.channelId ?? event.channel,
|
|
2682
|
+
content,
|
|
2683
|
+
success: true,
|
|
2684
|
+
sessionKey: event.sessionKey ?? ctx.sessionKey,
|
|
2685
|
+
runId: event.runId ?? ctx.runId
|
|
2686
|
+
};
|
|
2687
|
+
}
|
|
2688
|
+
function messageSentContextFromReplyPayload(event, ctx) {
|
|
2689
|
+
return {
|
|
2690
|
+
...ctx,
|
|
2691
|
+
...event.channel && !ctx.channelId ? { channelId: event.channel } : {},
|
|
2692
|
+
...event.sessionKey && !ctx.sessionKey ? { sessionKey: event.sessionKey } : {},
|
|
2693
|
+
...event.runId && !ctx.runId ? { runId: event.runId } : {}
|
|
2694
|
+
};
|
|
2695
|
+
}
|
|
2637
2696
|
var index_default = definePluginEntry({
|
|
2638
2697
|
id: PLUGIN_ID,
|
|
2639
2698
|
name: "Velanir Participation Gate",
|
|
@@ -2713,6 +2772,39 @@ var index_default = definePluginEntry({
|
|
|
2713
2772
|
});
|
|
2714
2773
|
return void 0;
|
|
2715
2774
|
});
|
|
2775
|
+
api.on("reply_payload_sending", (event, ctx) => {
|
|
2776
|
+
const replyEvent = event;
|
|
2777
|
+
if (replyEvent.kind !== void 0 && replyEvent.kind !== "final") {
|
|
2778
|
+
return void 0;
|
|
2779
|
+
}
|
|
2780
|
+
const content = replyPayloadContent(replyEvent);
|
|
2781
|
+
if (!content) {
|
|
2782
|
+
return void 0;
|
|
2783
|
+
}
|
|
2784
|
+
const replyContext = ctx;
|
|
2785
|
+
const messageSentEvent = messageSentEventFromReplyPayload(replyEvent, replyContext, content);
|
|
2786
|
+
const messageSentContext = messageSentContextFromReplyPayload(replyEvent, replyContext);
|
|
2787
|
+
const recorded = history.recordOutbound(
|
|
2788
|
+
messageSentEvent,
|
|
2789
|
+
messageSentContext,
|
|
2790
|
+
config.context.maxMessages
|
|
2791
|
+
);
|
|
2792
|
+
logParticipationHistoryEvent({
|
|
2793
|
+
logger: runtimeApi.logger,
|
|
2794
|
+
config,
|
|
2795
|
+
event: "reply_payload_outbound",
|
|
2796
|
+
fields: {
|
|
2797
|
+
recorded,
|
|
2798
|
+
kind: replyEvent.kind,
|
|
2799
|
+
channel: messageSentContext.channelId,
|
|
2800
|
+
conversation: messageSentContext.conversationId ?? messageSentEvent.to,
|
|
2801
|
+
session: messageSentContext.sessionKey ?? messageSentEvent.sessionKey,
|
|
2802
|
+
runId: messageSentEvent.runId
|
|
2803
|
+
},
|
|
2804
|
+
content
|
|
2805
|
+
});
|
|
2806
|
+
return void 0;
|
|
2807
|
+
});
|
|
2716
2808
|
}
|
|
2717
2809
|
});
|
|
2718
2810
|
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.2",
|
|
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.5.
|
|
37
|
+
"openclaw": ">=2026.5.5"
|
|
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.5.
|
|
60
|
+
"pluginApi": ">=2026.5.5",
|
|
61
|
+
"minGatewayVersion": "2026.5.5"
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
}
|