@velanir/openclaw-participation-gate 0.1.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 ADDED
@@ -0,0 +1,151 @@
1
+ # Velanir OpenClaw Participation Gate
2
+
3
+ OpenClaw plugin that decides whether a digital coworker should participate in a shared channel, group, room, or thread before the main agent runs.
4
+
5
+ The plugin is designed for the `requireMention: false` use case: the coworker can observe channel traffic, but should only answer when the current turn is actually for that coworker.
6
+
7
+ ## Package
8
+
9
+ - Package: `@velanir/openclaw-participation-gate`
10
+ - OpenClaw plugin id: `velanir-participation-gate`
11
+ - Runtime hooks: `before_dispatch`, `message_sent`
12
+ - Default mode: `shadow`
13
+ - Target OpenClaw plugin API: `>=2026.5.4`
14
+
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
+
17
+ ## Behavior
18
+
19
+ Direct messages always pass through. The gate only evaluates group and channel messages.
20
+
21
+ For group and channel messages, the plugin:
22
+
23
+ 1. Records recent shared-room user and coworker messages for fallback context.
24
+ 2. Loads participation context for the current coworker.
25
+ 3. Applies deterministic rules:
26
+ - respond when the message clearly addresses this coworker
27
+ - skip when the message clearly addresses another known coworker
28
+ - treat sentence-start requests such as `Albus I need you to...` as direct address without hardcoding coworker names
29
+ 4. Calls a lightweight classifier when deterministic rules do not decide the turn.
30
+ 5. Returns `{ handled: true }` only when the decision is skip and `mode` is `enforce`.
31
+
32
+ Explicit mentions still bypass classifier enforcement, but the inbound turn is
33
+ recorded before bypass so later unmentioned follow-ups have context. Successful
34
+ `message_sent` events are also recorded as coworker replies, which lets the
35
+ classifier recognize follow-ups after this coworker already answered.
36
+
37
+ 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
+
39
+ ## Classifier Contract
40
+
41
+ The classifier returns a scored participation decision:
42
+
43
+ ```json
44
+ { "participationScore": 0.83 }
45
+ ```
46
+
47
+ The plugin participates when `participationScore >= classifier.threshold`. The default threshold is `0.7`.
48
+
49
+ Legacy `{ "shouldRespond": true|false }` output is still accepted for
50
+ compatibility, but the prompt asks for `participationScore` only.
51
+ If the classifier output is malformed, the plugin retries once with a strict
52
+ JSON repair prompt. If the score can still be recovered from truncated output,
53
+ the recovered score is used for the skip/respond decision. If the retry is still
54
+ malformed with no recoverable score, the plugin fails open with score `1.0` and
55
+ logs `classifier_malformed` instead of treating it as a clean `classifier_true`.
56
+
57
+ `classifier.maxOutputTokens` limits the classifier response only. It does not cap input context.
58
+
59
+ Normal decision logs include the participation score, threshold, classifier
60
+ prompt version, prompt hash, input hash, parse status, attempt count, output
61
+ hash, and output length. Exact rendered prompt/input/raw classifier output and
62
+ parse errors are logged only when `logging.classifierDebug` and
63
+ `logging.includeContent` are both enabled.
64
+
65
+ ## Context Contract
66
+
67
+ Production context should come from the platform:
68
+
69
+ ```http
70
+ GET /v1/runtime/coworkers/{coworkerId}/participation-context
71
+ Authorization: DPoP <scoped runtime token>
72
+ DPoP: <proof for this GET request>
73
+ Accept: application/json
74
+ ```
75
+
76
+ Expected response:
77
+
78
+ ```json
79
+ {
80
+ "data": {
81
+ "self": {
82
+ "id": "coworker_albus",
83
+ "names": ["Albus", "Albus Dumbledore"],
84
+ "roleSummary": "Executive assistant for Diagon leadership"
85
+ },
86
+ "coworkers": [
87
+ {
88
+ "id": "coworker_tanya",
89
+ "names": ["Tanya", "Tanya Dean", "Tanya Morales"],
90
+ "roleSummary": "Executive assistant"
91
+ }
92
+ ]
93
+ }
94
+ }
95
+ ```
96
+
97
+ The Platform endpoint must use the runtime identity protocol. The provider defaults to `platform.authMode: "runtime"`, requests `scope=participation-context:read` from `/v1/runtime/token`, and signs a DPoP proof for each context read.
98
+
99
+ The endpoint must be derived from platform-owned coworker and organization data. Adding or removing a digital coworker should not require editing plugin configuration on customer machines.
100
+
101
+ `names` should include display names, short names, and channel-specific aliases or mention tokens when the platform knows them. The deterministic direct-address rules depend on these aliases, and the classifier receives the same identity set.
102
+
103
+ Static context exists only for tests and local development.
104
+
105
+ ## Security
106
+
107
+ The platform provider intentionally does not fall back to a broad `OCT8_API_SECRET`.
108
+
109
+ The production credential is a short-lived, DPoP-bound runtime token that can only read the current coworker's participation context. Static `platform.token` / `OCT8_PARTICIPATION_CONTEXT_TOKEN` is only read when `platform.authMode` is explicitly set to `static-token`; that mode remains prototype-only and must not be used to bypass runtime identity in live customer deployments.
110
+
111
+ Runtime mode expects the same runtime identity environment used by `oct8-secrets-runtime-v2`: `OCT8_API_URL`, `OCT8_RUNTIME_IDENTITY_ID`, `OCT8_RUNTIME_STATE_DIR`, and optionally `OCT8_RUNTIME_TOKEN_ISSUER` / `OCT8_RUNTIME_KEY_ID`. It does not read `OCT8_API_SECRET`.
112
+
113
+ ## Example Config
114
+
115
+ ```json
116
+ {
117
+ "mode": "shadow",
118
+ "classifier": {
119
+ "provider": "openai-codex",
120
+ "model": "gpt-5.5",
121
+ "timeoutMs": 5000,
122
+ "maxOutputTokens": 32,
123
+ "threshold": 0.7
124
+ },
125
+ "context": {
126
+ "source": "platform",
127
+ "maxMessages": 5,
128
+ "refreshMs": 300000
129
+ },
130
+ "platform": {
131
+ "authMode": "runtime",
132
+ "baseUrl": "https://api.velanir.ai",
133
+ "coworkerId": "coworker_albus"
134
+ },
135
+ "logging": {
136
+ "decisions": true,
137
+ "includeContent": false,
138
+ "classifierDebug": false
139
+ }
140
+ }
141
+ ```
142
+
143
+ Use `shadow` first to validate decisions in logs. Move to `enforce` only after the classifier and context source are behaving as expected. Platform-rendered configs should choose the coworker's configured OpenClaw model/provider unless an explicit classifier override is present.
144
+
145
+ ## Development
146
+
147
+ ```sh
148
+ pnpm --filter=@velanir/openclaw-participation-gate check-types
149
+ pnpm --filter=@velanir/openclaw-participation-gate test
150
+ pnpm --filter=@velanir/openclaw-participation-gate build
151
+ ```
@@ -0,0 +1,12 @@
1
+ import * as openclaw_plugin_sdk_plugin_entry from 'openclaw/plugin-sdk/plugin-entry';
2
+
3
+ declare const PLUGIN_ID = "velanir-participation-gate";
4
+ declare const _default: {
5
+ id: string;
6
+ name: string;
7
+ description: string;
8
+ configSchema: openclaw_plugin_sdk_plugin_entry.OpenClawPluginConfigSchema;
9
+ register: NonNullable<openclaw_plugin_sdk_plugin_entry.OpenClawPluginDefinition["register"]>;
10
+ } & Pick<openclaw_plugin_sdk_plugin_entry.OpenClawPluginDefinition, "kind" | "reload" | "nodeHostCommands" | "securityAuditCollectors">;
11
+
12
+ export { PLUGIN_ID, _default as default };