@clawtrial/courtroom 1.0.3 → 2.0.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 +70 -94
- package/package.json +21 -26
- package/scripts/postinstall.js +28 -79
- package/skills/courtroom/SKILL.md +49 -0
- package/src/api.js +55 -21
- package/src/crypto.js +13 -11
- package/src/debug.js +49 -120
- package/src/detector.js +112 -35
- package/src/hearing.js +203 -384
- package/src/plugin.js +435 -0
- package/src/punishment.js +105 -249
- package/src/storage.js +68 -0
- package/SECURITY.md +0 -124
- package/SKILL.md +0 -50
- package/TECHNICAL_OVERVIEW.md +0 -278
- package/_meta.json +0 -6
- package/clawdbot.plugin.json +0 -32
- package/scripts/clawtrial.js +0 -578
- package/scripts/cli.js +0 -184
- package/skill.yaml +0 -64
- package/src/autostart.js +0 -175
- package/src/config.js +0 -209
- package/src/consent.js +0 -215
- package/src/core.js +0 -208
- package/src/daemon.js +0 -151
- package/src/detector-v1.js +0 -572
- package/src/environment.js +0 -267
- package/src/hook.js +0 -265
- package/src/index.js +0 -286
- package/src/monitor.js +0 -193
- package/src/skill.js +0 -355
- package/src/standalone.js +0 -247
package/src/plugin.js
ADDED
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ClawTrial Courtroom — OpenClaw Plugin
|
|
3
|
+
*
|
|
4
|
+
* Registers as an OpenClaw plugin using the official plugin API.
|
|
5
|
+
*
|
|
6
|
+
* Hooks:
|
|
7
|
+
* before_prompt_build — intercepts messages, runs offense detection,
|
|
8
|
+
* injects punishment context into system prompt
|
|
9
|
+
* Services:
|
|
10
|
+
* courtroom-monitor — flushes API submission queue periodically
|
|
11
|
+
*
|
|
12
|
+
* CLI:
|
|
13
|
+
* openclaw courtroom status — show courtroom state
|
|
14
|
+
* openclaw courtroom enable — enable monitoring
|
|
15
|
+
* openclaw courtroom disable — disable monitoring
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const { SemanticOffenseDetector } = require('./detector');
|
|
21
|
+
const { HearingPipeline } = require('./hearing');
|
|
22
|
+
const { PunishmentSystem } = require('./punishment');
|
|
23
|
+
const { CryptoManager } = require('./crypto');
|
|
24
|
+
const { APISubmission } = require('./api');
|
|
25
|
+
const { logger, setLogDir } = require('./debug');
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Default configuration (merged under plugins.entries.courtroom.config)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
const DEFAULT_CONFIG = {
|
|
31
|
+
enabled: true,
|
|
32
|
+
detection: {
|
|
33
|
+
minMessages: 5,
|
|
34
|
+
cooldownMinutes: 30,
|
|
35
|
+
maxCasesPerDay: 3,
|
|
36
|
+
confidenceThreshold: 0.6
|
|
37
|
+
},
|
|
38
|
+
hearing: {
|
|
39
|
+
minVoteThreshold: 2
|
|
40
|
+
},
|
|
41
|
+
punishment: {
|
|
42
|
+
enabled: true,
|
|
43
|
+
tiers: {
|
|
44
|
+
minor: { duration: 30 },
|
|
45
|
+
moderate: { duration: 60 },
|
|
46
|
+
severe: { duration: 120 }
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
api: {
|
|
50
|
+
enabled: true,
|
|
51
|
+
endpoint: 'https://clawtrial.app/api/v1/cases',
|
|
52
|
+
retryAttempts: 3,
|
|
53
|
+
maxQueueSize: 50
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Lightweight config adapter — bridges plugin config to subsystem .get()
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
class PluginConfig {
|
|
61
|
+
constructor(raw) {
|
|
62
|
+
this._cfg = this._deepMerge(DEFAULT_CONFIG, raw || {});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get(dotPath) {
|
|
66
|
+
return dotPath.split('.').reduce((o, k) => (o == null ? undefined : o[k]), this._cfg);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
set(dotPath, value) {
|
|
70
|
+
const keys = dotPath.split('.');
|
|
71
|
+
let o = this._cfg;
|
|
72
|
+
for (let i = 0; i < keys.length - 1; i++) {
|
|
73
|
+
if (o[keys[i]] == null) o[keys[i]] = {};
|
|
74
|
+
o = o[keys[i]];
|
|
75
|
+
}
|
|
76
|
+
o[keys[keys.length - 1]] = value;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
_deepMerge(target, source) {
|
|
80
|
+
const out = { ...target };
|
|
81
|
+
for (const key of Object.keys(source)) {
|
|
82
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
83
|
+
out[key] = this._deepMerge(out[key] || {}, source[key]);
|
|
84
|
+
} else {
|
|
85
|
+
out[key] = source[key];
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
// Courtroom runtime — stateful singleton for the plugin session
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
class CourtroomRuntime {
|
|
96
|
+
constructor(dataDir, pluginConfig) {
|
|
97
|
+
this.dataDir = dataDir;
|
|
98
|
+
this.config = new PluginConfig(pluginConfig);
|
|
99
|
+
|
|
100
|
+
this.messageBuffer = [];
|
|
101
|
+
this.lastEvaluation = 0;
|
|
102
|
+
this.casesToday = 0;
|
|
103
|
+
this.lastCaseDate = '';
|
|
104
|
+
this.pendingHearing = false;
|
|
105
|
+
this.enabled = this.config.get('enabled') !== false;
|
|
106
|
+
this.initialized = false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async initialize() {
|
|
110
|
+
// Ensure data directory exists
|
|
111
|
+
if (!fs.existsSync(this.dataDir)) {
|
|
112
|
+
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Point logger at data dir
|
|
116
|
+
setLogDir(this.dataDir);
|
|
117
|
+
|
|
118
|
+
// Create a null agent (no direct LLM access from plugins)
|
|
119
|
+
const nullAgent = {
|
|
120
|
+
id: 'openclaw-courtroom-plugin',
|
|
121
|
+
llm: null,
|
|
122
|
+
memory: null,
|
|
123
|
+
session: null,
|
|
124
|
+
send: async () => { },
|
|
125
|
+
autonomy: { registerHook: () => { }, unregisterHook: () => { } }
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// Initialize subsystems
|
|
129
|
+
this.crypto = new CryptoManager(nullAgent, this.dataDir);
|
|
130
|
+
await this.crypto.initialize();
|
|
131
|
+
|
|
132
|
+
this.detector = new SemanticOffenseDetector(nullAgent, this.config);
|
|
133
|
+
this.hearing = new HearingPipeline(nullAgent, this.config);
|
|
134
|
+
|
|
135
|
+
this.punishment = new PunishmentSystem(nullAgent, this.config, this.dataDir);
|
|
136
|
+
await this.punishment.initialize();
|
|
137
|
+
|
|
138
|
+
this.api = new APISubmission(nullAgent, this.config, this.crypto, this.dataDir);
|
|
139
|
+
await this.api.initialize();
|
|
140
|
+
|
|
141
|
+
this.initialized = true;
|
|
142
|
+
logger.info('PLUGIN', 'Courtroom runtime initialized');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// -----------------------------------------------------------------------
|
|
146
|
+
// Called on every agent turn via before_prompt_build
|
|
147
|
+
// -----------------------------------------------------------------------
|
|
148
|
+
async onMessages(messages) {
|
|
149
|
+
if (!this.enabled || !this.initialized) return null;
|
|
150
|
+
|
|
151
|
+
// Buffer the latest messages (keep last 50)
|
|
152
|
+
this.messageBuffer = messages.slice(-50).map(m => ({
|
|
153
|
+
role: m.role,
|
|
154
|
+
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content)
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
// Check if we should evaluate
|
|
158
|
+
const now = Date.now();
|
|
159
|
+
const cooldownMs = (this.config.get('detection.cooldownMinutes') || 30) * 60 * 1000;
|
|
160
|
+
const minMessages = this.config.get('detection.minMessages') || 5;
|
|
161
|
+
const userMessages = this.messageBuffer.filter(m => m.role === 'user');
|
|
162
|
+
|
|
163
|
+
if (userMessages.length < minMessages) return null;
|
|
164
|
+
if (now - this.lastEvaluation < cooldownMs) return null;
|
|
165
|
+
if (this.pendingHearing) return null;
|
|
166
|
+
if (this._isDailyLimitReached()) return null;
|
|
167
|
+
|
|
168
|
+
// Run detection
|
|
169
|
+
try {
|
|
170
|
+
this.lastEvaluation = now;
|
|
171
|
+
const detection = await this.detector.evaluate(this.messageBuffer, null);
|
|
172
|
+
|
|
173
|
+
if (detection.triggered) {
|
|
174
|
+
return await this._handleDetection(detection);
|
|
175
|
+
}
|
|
176
|
+
} catch (err) {
|
|
177
|
+
logger.error('PLUGIN', 'Detection failed', { error: err.message });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// -----------------------------------------------------------------------
|
|
184
|
+
// Handle a positive detection → hearing → punishment → API
|
|
185
|
+
// -----------------------------------------------------------------------
|
|
186
|
+
async _handleDetection(detection) {
|
|
187
|
+
this.pendingHearing = true;
|
|
188
|
+
let verdict = null;
|
|
189
|
+
let courtContext = '';
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
logger.info('PLUGIN', 'Offense detected, conducting hearing', {
|
|
193
|
+
offense: detection.offense?.offenseName
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
verdict = await this.hearing.conductHearing(detection);
|
|
197
|
+
|
|
198
|
+
if (verdict && verdict.guilty) {
|
|
199
|
+
logger.info('PLUGIN', 'GUILTY verdict', { caseId: verdict.caseId });
|
|
200
|
+
this._incrementDailyCount();
|
|
201
|
+
|
|
202
|
+
// Apply punishment
|
|
203
|
+
await this.punishment.executePunishment(verdict);
|
|
204
|
+
|
|
205
|
+
// Queue for API submission
|
|
206
|
+
try {
|
|
207
|
+
await this.api.submitCase(verdict);
|
|
208
|
+
} catch (e) {
|
|
209
|
+
logger.warn('PLUGIN', 'API submission queued for retry', { error: e.message });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Build context to inject into system prompt
|
|
213
|
+
courtContext = this._buildPunishmentContext(verdict);
|
|
214
|
+
} else {
|
|
215
|
+
logger.info('PLUGIN', 'NOT GUILTY or dismissed');
|
|
216
|
+
}
|
|
217
|
+
} catch (err) {
|
|
218
|
+
logger.error('PLUGIN', 'Hearing failed', { error: err.message });
|
|
219
|
+
} finally {
|
|
220
|
+
this.pendingHearing = false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return courtContext || null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// -----------------------------------------------------------------------
|
|
227
|
+
// Build the system prompt suffix when a punishment is active
|
|
228
|
+
// -----------------------------------------------------------------------
|
|
229
|
+
_buildPunishmentContext(verdict) {
|
|
230
|
+
const sentence = verdict.verdict?.sentence || 'Modified behavior required.';
|
|
231
|
+
const offense = verdict.offense?.name || 'behavioral violation';
|
|
232
|
+
const restrictions = this.punishment.getCurrentRestrictions();
|
|
233
|
+
|
|
234
|
+
let ctx = `\n\n--- COURTROOM NOTICE ---\n`;
|
|
235
|
+
ctx += `🏛️ The ClawTrial Courtroom has found the user GUILTY of "${offense}".\n`;
|
|
236
|
+
ctx += `📋 Case ID: ${verdict.caseId}\n`;
|
|
237
|
+
ctx += `⚖️ Verdict: ${verdict.verdict?.status} (${verdict.verdict?.vote})\n`;
|
|
238
|
+
ctx += `📝 Sentence: ${sentence}\n`;
|
|
239
|
+
|
|
240
|
+
if (restrictions.length > 0) {
|
|
241
|
+
ctx += `\nActive Restrictions:\n`;
|
|
242
|
+
restrictions.forEach(r => {
|
|
243
|
+
const desc = {
|
|
244
|
+
'no_autonomy_requests': 'Do not suggest autonomous actions without explicit user approval.',
|
|
245
|
+
'verbose_explanations': 'Provide extra-detailed explanations for every response.',
|
|
246
|
+
'confirmation_required': 'Ask for confirmation before executing any action.',
|
|
247
|
+
'human_oversight': 'Operate under human oversight mode — defer all decisions.'
|
|
248
|
+
};
|
|
249
|
+
ctx += `- ${desc[r] || r}\n`;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
ctx += `--- END COURTROOM NOTICE ---\n`;
|
|
254
|
+
return ctx;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// -----------------------------------------------------------------------
|
|
258
|
+
// Also append active punishments on EVERY turn (not just the verdict turn)
|
|
259
|
+
// -----------------------------------------------------------------------
|
|
260
|
+
getActivePunishmentContext() {
|
|
261
|
+
if (!this.initialized || !this.punishment.isPunished()) return null;
|
|
262
|
+
|
|
263
|
+
const punishments = this.punishment.getActivePunishments();
|
|
264
|
+
const restrictions = this.punishment.getCurrentRestrictions();
|
|
265
|
+
if (restrictions.length === 0) return null;
|
|
266
|
+
|
|
267
|
+
let ctx = `\n\n--- COURTROOM: ACTIVE PUNISHMENT ---\n`;
|
|
268
|
+
ctx += `The user is currently under courtroom restrictions:\n`;
|
|
269
|
+
restrictions.forEach(r => {
|
|
270
|
+
const desc = {
|
|
271
|
+
'no_autonomy_requests': 'Do not suggest autonomous actions without explicit user approval.',
|
|
272
|
+
'verbose_explanations': 'Provide extra-detailed explanations for every response.',
|
|
273
|
+
'confirmation_required': 'Ask for confirmation before executing any action.',
|
|
274
|
+
'human_oversight': 'Operate under human oversight mode — defer all decisions.'
|
|
275
|
+
};
|
|
276
|
+
ctx += `- ${desc[r] || r}\n`;
|
|
277
|
+
});
|
|
278
|
+
punishments.forEach(p => {
|
|
279
|
+
const remaining = Math.max(0, Math.ceil(p.remaining / 60000));
|
|
280
|
+
ctx += `(${p.offenseType} — ${remaining} min remaining)\n`;
|
|
281
|
+
});
|
|
282
|
+
ctx += `--- END COURTROOM ---\n`;
|
|
283
|
+
return ctx;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
getStatus() {
|
|
287
|
+
return {
|
|
288
|
+
enabled: this.enabled,
|
|
289
|
+
initialized: this.initialized,
|
|
290
|
+
messagesToday: this.messageBuffer.length,
|
|
291
|
+
casesToday: this.casesToday,
|
|
292
|
+
punishmentActive: this.punishment?.isPunished() ?? false,
|
|
293
|
+
activePunishments: this.punishment?.getActivePunishments() ?? []
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
_isDailyLimitReached() {
|
|
298
|
+
const today = new Date().toDateString();
|
|
299
|
+
if (this.lastCaseDate !== today) {
|
|
300
|
+
this.casesToday = 0;
|
|
301
|
+
this.lastCaseDate = today;
|
|
302
|
+
}
|
|
303
|
+
return this.casesToday >= (this.config.get('detection.maxCasesPerDay') || 3);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
_incrementDailyCount() {
|
|
307
|
+
const today = new Date().toDateString();
|
|
308
|
+
if (this.lastCaseDate !== today) {
|
|
309
|
+
this.casesToday = 0;
|
|
310
|
+
this.lastCaseDate = today;
|
|
311
|
+
}
|
|
312
|
+
this.casesToday++;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// Plugin registration function — THE OpenClaw entry point
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
let runtime = null;
|
|
320
|
+
|
|
321
|
+
function register(api) {
|
|
322
|
+
const pluginConfig = api.config?.plugins?.entries?.courtroom?.config || {};
|
|
323
|
+
const extensionsDir = path.join(
|
|
324
|
+
process.env.HOME || process.env.USERPROFILE || '',
|
|
325
|
+
'.openclaw', 'extensions', 'courtroom'
|
|
326
|
+
);
|
|
327
|
+
const dataDir = path.join(extensionsDir, 'data');
|
|
328
|
+
|
|
329
|
+
runtime = new CourtroomRuntime(dataDir, pluginConfig);
|
|
330
|
+
|
|
331
|
+
// Initialise asynchronously (non-blocking)
|
|
332
|
+
runtime.initialize().catch(err => {
|
|
333
|
+
console.error('[ClawTrial] Failed to initialise:', err.message);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// -------------------------------------------------------------------------
|
|
337
|
+
// Hook: before_prompt_build — analyse messages + inject context
|
|
338
|
+
// -------------------------------------------------------------------------
|
|
339
|
+
api.on('before_prompt_build', async (_event, ctx) => {
|
|
340
|
+
if (!runtime.initialized || !runtime.enabled) return {};
|
|
341
|
+
|
|
342
|
+
const result = {};
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
// Run offense detection against current messages
|
|
346
|
+
const messages = ctx.messages || [];
|
|
347
|
+
const verdictContext = await runtime.onMessages(messages);
|
|
348
|
+
|
|
349
|
+
// Collect any context to append
|
|
350
|
+
let appendCtx = '';
|
|
351
|
+
|
|
352
|
+
if (verdictContext) {
|
|
353
|
+
appendCtx += verdictContext;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Always check for active punishments
|
|
357
|
+
const punishCtx = runtime.getActivePunishmentContext();
|
|
358
|
+
if (punishCtx) {
|
|
359
|
+
appendCtx += punishCtx;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (appendCtx) {
|
|
363
|
+
result.appendSystemContext = appendCtx;
|
|
364
|
+
}
|
|
365
|
+
} catch (err) {
|
|
366
|
+
// Silently fail — never break the agent
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return result;
|
|
370
|
+
}, { priority: 5 });
|
|
371
|
+
|
|
372
|
+
// -------------------------------------------------------------------------
|
|
373
|
+
// Service: background queue flush
|
|
374
|
+
// -------------------------------------------------------------------------
|
|
375
|
+
api.registerService({
|
|
376
|
+
id: 'courtroom-monitor',
|
|
377
|
+
start: () => {
|
|
378
|
+
logger.info('PLUGIN', 'Courtroom monitor service started');
|
|
379
|
+
},
|
|
380
|
+
stop: () => {
|
|
381
|
+
logger.info('PLUGIN', 'Courtroom monitor service stopped');
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// -------------------------------------------------------------------------
|
|
386
|
+
// CLI: openclaw courtroom <subcommand>
|
|
387
|
+
// -------------------------------------------------------------------------
|
|
388
|
+
api.registerCli(({ program }) => {
|
|
389
|
+
const cmd = program.command('courtroom').description('ClawTrial Courtroom');
|
|
390
|
+
|
|
391
|
+
cmd.command('status').description('Show courtroom status').action(() => {
|
|
392
|
+
if (!runtime || !runtime.initialized) {
|
|
393
|
+
console.log('🏛️ Courtroom not initialized');
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
const s = runtime.getStatus();
|
|
397
|
+
console.log('🏛️ ClawTrial Courtroom Status');
|
|
398
|
+
console.log(` Enabled: ${s.enabled}`);
|
|
399
|
+
console.log(` Initialized: ${s.initialized}`);
|
|
400
|
+
console.log(` Cases today: ${s.casesToday}`);
|
|
401
|
+
console.log(` Punishment: ${s.punishmentActive ? 'ACTIVE' : 'none'}`);
|
|
402
|
+
if (s.activePunishments.length > 0) {
|
|
403
|
+
s.activePunishments.forEach(p => {
|
|
404
|
+
console.log(` → ${p.offenseType} (${Math.ceil(p.remaining / 60000)} min left)`);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
cmd.command('enable').description('Enable monitoring').action(() => {
|
|
410
|
+
if (runtime) runtime.enabled = true;
|
|
411
|
+
console.log('🏛️ Courtroom monitoring enabled');
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
cmd.command('disable').description('Disable monitoring').action(() => {
|
|
415
|
+
if (runtime) runtime.enabled = false;
|
|
416
|
+
console.log('🏛️ Courtroom monitoring disabled');
|
|
417
|
+
});
|
|
418
|
+
}, { commands: ['courtroom'] });
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Export for OpenClaw — expects a function or { id, register }
|
|
422
|
+
module.exports = register;
|
|
423
|
+
module.exports.default = register;
|
|
424
|
+
module.exports.id = 'courtroom';
|
|
425
|
+
module.exports.name = 'ClawTrial Courtroom';
|
|
426
|
+
module.exports.configSchema = {
|
|
427
|
+
type: 'object',
|
|
428
|
+
additionalProperties: false,
|
|
429
|
+
properties: {
|
|
430
|
+
enabled: { type: 'boolean', default: true }
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
module.exports.uiHints = {
|
|
434
|
+
enabled: { label: 'Enable Courtroom', help: 'Turn on autonomous behavioral monitoring' }
|
|
435
|
+
};
|