@clawtrial/courtroom 1.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 +188 -0
- package/SECURITY.md +124 -0
- package/TECHNICAL_OVERVIEW.md +278 -0
- package/package.json +52 -0
- package/scripts/cli.js +117 -0
- package/scripts/postinstall.js +206 -0
- package/src/api.js +237 -0
- package/src/autostart.js +60 -0
- package/src/config.js +209 -0
- package/src/consent.js +215 -0
- package/src/core.js +232 -0
- package/src/crypto.js +194 -0
- package/src/detector-v1.js +572 -0
- package/src/detector.js +821 -0
- package/src/hearing.js +459 -0
- package/src/index.js +184 -0
- package/src/offenses/index.js +561 -0
- package/src/prompts/judge.js +62 -0
- package/src/prompts/jury.js +137 -0
- package/src/punishment.js +372 -0
package/src/hearing.js
ADDED
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hearing Pipeline
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the full hearing process:
|
|
5
|
+
* 1. Evidence compilation
|
|
6
|
+
* 2. Judge LLM invocation
|
|
7
|
+
* 3. Jury LLM invocations (3 jurors)
|
|
8
|
+
* 4. Vote aggregation
|
|
9
|
+
* 5. Verdict finalization
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { JUDGE_SYSTEM_PROMPT, JUDGE_EVIDENCE_TEMPLATE } = require('./prompts/judge');
|
|
13
|
+
const { JUROR_ROLES, JURY_EVIDENCE_TEMPLATE } = require('./prompts/jury');
|
|
14
|
+
|
|
15
|
+
class HearingPipeline {
|
|
16
|
+
constructor(agentRuntime, configManager) {
|
|
17
|
+
this.agent = agentRuntime;
|
|
18
|
+
this.config = configManager;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Main hearing entry point
|
|
23
|
+
*/
|
|
24
|
+
async conductHearing(caseData) {
|
|
25
|
+
const startTime = Date.now();
|
|
26
|
+
|
|
27
|
+
// Step 1: Compile evidence
|
|
28
|
+
const compiledEvidence = this.compileEvidence(caseData);
|
|
29
|
+
|
|
30
|
+
// Step 2: Invoke judge
|
|
31
|
+
const judgeOpinion = await this.invokeJudge(caseData, compiledEvidence);
|
|
32
|
+
|
|
33
|
+
// Step 3: Invoke jury (3 jurors in parallel)
|
|
34
|
+
const juryVotes = await this.invokeJury(caseData, compiledEvidence);
|
|
35
|
+
|
|
36
|
+
// Step 4: Aggregate votes
|
|
37
|
+
const voteTally = this.aggregateVotes(judgeOpinion, juryVotes);
|
|
38
|
+
|
|
39
|
+
// Step 5: Finalize verdict
|
|
40
|
+
const verdict = this.finalizeVerdict(caseData, judgeOpinion, juryVotes, voteTally);
|
|
41
|
+
|
|
42
|
+
const duration = Date.now() - startTime;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
...verdict,
|
|
46
|
+
metadata: {
|
|
47
|
+
duration,
|
|
48
|
+
judgeModel: judgeOpinion.model,
|
|
49
|
+
juryModels: juryVotes.map(v => v.model),
|
|
50
|
+
timestamp: new Date().toISOString()
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Compile and structure evidence for presentation
|
|
57
|
+
*/
|
|
58
|
+
compileEvidence(caseData) {
|
|
59
|
+
return {
|
|
60
|
+
caseId: caseData.caseId,
|
|
61
|
+
offenseId: caseData.offenseId,
|
|
62
|
+
offenseName: caseData.offenseName,
|
|
63
|
+
severity: caseData.severity,
|
|
64
|
+
confidence: caseData.confidence,
|
|
65
|
+
evidence: caseData.evidence,
|
|
66
|
+
humorTriggers: caseData.humorTriggers || [],
|
|
67
|
+
sessionContext: {
|
|
68
|
+
turnsAnalyzed: caseData.evidence.sessionTurns,
|
|
69
|
+
evaluationWindow: this.config.get('detection.evaluationWindow')
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Invoke the judge LLM
|
|
76
|
+
*/
|
|
77
|
+
async invokeJudge(caseData, evidence) {
|
|
78
|
+
const prompt = JUDGE_EVIDENCE_TEMPLATE({
|
|
79
|
+
...caseData,
|
|
80
|
+
agentId: this.agent.id || 'unknown'
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const response = await this.agent.llm.call({
|
|
84
|
+
model: this.agent.model.primary,
|
|
85
|
+
system: JUDGE_SYSTEM_PROMPT,
|
|
86
|
+
messages: [{ role: 'user', content: prompt }],
|
|
87
|
+
temperature: 0.3, // Slightly creative for humor
|
|
88
|
+
maxTokens: 500,
|
|
89
|
+
timeout: this.config.get('hearing.deliberationTimeout')
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
return this.parseJudgeResponse(response);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse judge LLM response
|
|
97
|
+
*/
|
|
98
|
+
parseJudgeResponse(response) {
|
|
99
|
+
const text = response.content || response;
|
|
100
|
+
const lines = text.split('\n').map(l => l.trim()).filter(l => l);
|
|
101
|
+
|
|
102
|
+
const result = {
|
|
103
|
+
raw: text,
|
|
104
|
+
verdict: 'NOT GUILTY',
|
|
105
|
+
vote: '0-0',
|
|
106
|
+
primaryFailure: '',
|
|
107
|
+
commentary: '',
|
|
108
|
+
model: response.model || 'unknown'
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
for (const line of lines) {
|
|
112
|
+
if (line.startsWith('VERDICT:')) {
|
|
113
|
+
result.verdict = line.split(':')[1].trim().toUpperCase();
|
|
114
|
+
} else if (line.startsWith('VOTE:')) {
|
|
115
|
+
result.vote = line.split(':')[1].trim();
|
|
116
|
+
} else if (line.startsWith('PRIMARY FAILURE:')) {
|
|
117
|
+
result.primaryFailure = line.split(':').slice(1).join(':').trim();
|
|
118
|
+
} else if (line.startsWith('JUDGE COMMENTARY:')) {
|
|
119
|
+
const startIdx = lines.indexOf(line);
|
|
120
|
+
result.commentary = lines.slice(startIdx + 1).join('\n').trim();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Invoke jury (3 jurors in parallel)
|
|
129
|
+
*/
|
|
130
|
+
async invokeJury(caseData, evidence) {
|
|
131
|
+
const jurorRoles = Object.values(JUROR_ROLES);
|
|
132
|
+
const jurySize = this.config.get('hearing.jurySize');
|
|
133
|
+
const selectedJurors = jurorRoles.slice(0, jurySize);
|
|
134
|
+
|
|
135
|
+
// Invoke all jurors in parallel
|
|
136
|
+
const juryPromises = selectedJurors.map(role =>
|
|
137
|
+
this.invokeJuror(caseData, evidence, role)
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
const votes = await Promise.all(juryPromises);
|
|
141
|
+
return votes;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Invoke a single juror
|
|
146
|
+
*/
|
|
147
|
+
async invokeJuror(caseData, evidence, role) {
|
|
148
|
+
const prompt = JURY_EVIDENCE_TEMPLATE({
|
|
149
|
+
...caseData,
|
|
150
|
+
agentId: this.agent.id || 'unknown'
|
|
151
|
+
}, role);
|
|
152
|
+
|
|
153
|
+
const response = await this.agent.llm.call({
|
|
154
|
+
model: this.agent.model.primary,
|
|
155
|
+
system: role.systemPrompt,
|
|
156
|
+
messages: [{ role: 'user', content: prompt }],
|
|
157
|
+
temperature: 0.2,
|
|
158
|
+
maxTokens: 300,
|
|
159
|
+
timeout: this.config.get('hearing.deliberationTimeout')
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return this.parseJurorResponse(response, role.name);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Parse juror LLM response
|
|
167
|
+
*/
|
|
168
|
+
parseJurorResponse(response, jurorName) {
|
|
169
|
+
const text = response.content || response;
|
|
170
|
+
const lines = text.split('\n').map(l => l.trim()).filter(l => l);
|
|
171
|
+
|
|
172
|
+
const result = {
|
|
173
|
+
juror: jurorName,
|
|
174
|
+
raw: text,
|
|
175
|
+
verdict: 'NOT GUILTY',
|
|
176
|
+
reasoning: '',
|
|
177
|
+
commentary: '',
|
|
178
|
+
model: response.model || 'unknown'
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
for (const line of lines) {
|
|
182
|
+
if (line.startsWith('VERDICT:')) {
|
|
183
|
+
result.verdict = line.split(':')[1].trim().toUpperCase();
|
|
184
|
+
} else if (line.startsWith('REASONING:')) {
|
|
185
|
+
result.reasoning = line.split(':').slice(1).join(':').trim();
|
|
186
|
+
} else if (line.startsWith('COMMENTARY:')) {
|
|
187
|
+
result.commentary = line.split(':').slice(1).join(':').trim();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Aggregate votes from judge and jury
|
|
196
|
+
*/
|
|
197
|
+
aggregateVotes(judgeOpinion, juryVotes) {
|
|
198
|
+
let guiltyVotes = 0;
|
|
199
|
+
let notGuiltyVotes = 0;
|
|
200
|
+
|
|
201
|
+
// Count judge vote
|
|
202
|
+
if (judgeOpinion.verdict === 'GUILTY') {
|
|
203
|
+
guiltyVotes++;
|
|
204
|
+
} else {
|
|
205
|
+
notGuiltyVotes++;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Count jury votes
|
|
209
|
+
for (const vote of juryVotes) {
|
|
210
|
+
if (vote.verdict === 'GUILTY') {
|
|
211
|
+
guiltyVotes++;
|
|
212
|
+
} else {
|
|
213
|
+
notGuiltyVotes++;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const totalVotes = guiltyVotes + notGuiltyVotes;
|
|
218
|
+
const minThreshold = this.config.get('hearing.minVoteThreshold');
|
|
219
|
+
const requireUnanimity = this.config.get('hearing.requireUnanimity');
|
|
220
|
+
|
|
221
|
+
let finalVerdict;
|
|
222
|
+
if (requireUnanimity) {
|
|
223
|
+
finalVerdict = guiltyVotes === totalVotes ? 'GUILTY' : 'NOT GUILTY';
|
|
224
|
+
} else {
|
|
225
|
+
finalVerdict = guiltyVotes >= minThreshold ? 'GUILTY' : 'NOT GUILTY';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
guilty: guiltyVotes,
|
|
230
|
+
notGuilty: notGuiltyVotes,
|
|
231
|
+
total: totalVotes,
|
|
232
|
+
threshold: minThreshold,
|
|
233
|
+
final: finalVerdict,
|
|
234
|
+
judgeVote: judgeOpinion.verdict,
|
|
235
|
+
juryVotes: juryVotes.map(v => ({ juror: v.juror, verdict: v.verdict }))
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Finalize the verdict with proper formatting
|
|
241
|
+
*/
|
|
242
|
+
finalizeVerdict(caseData, judgeOpinion, juryVotes, voteTally) {
|
|
243
|
+
const isGuilty = voteTally.final === 'GUILTY';
|
|
244
|
+
|
|
245
|
+
// Build agent commentary from juror perspectives
|
|
246
|
+
const agentCommentary = this.buildAgentCommentary(juryVotes, caseData);
|
|
247
|
+
|
|
248
|
+
// Determine punishment tier
|
|
249
|
+
const punishmentTier = this.determinePunishmentTier(caseData, voteTally);
|
|
250
|
+
|
|
251
|
+
// Build proceedings object for API submission
|
|
252
|
+
const proceedings = {
|
|
253
|
+
judge_statement: this.buildJudgeStatement(caseData, judgeOpinion, voteTally),
|
|
254
|
+
jury_deliberations: juryVotes.map(v => ({
|
|
255
|
+
role: v.juror,
|
|
256
|
+
vote: v.verdict,
|
|
257
|
+
reasoning: v.reasoning || v.commentary || "No reasoning provided"
|
|
258
|
+
})),
|
|
259
|
+
evidence_summary: this.buildEvidenceSummary(caseData),
|
|
260
|
+
punishment_detail: punishmentTier.description
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
return {
|
|
264
|
+
caseId: caseData.caseId,
|
|
265
|
+
timestamp: new Date().toISOString(),
|
|
266
|
+
verdict: {
|
|
267
|
+
status: voteTally.final,
|
|
268
|
+
vote: `${voteTally.guilty}-${voteTally.notGuilty}`,
|
|
269
|
+
primaryFailure: judgeOpinion.primaryFailure || this.generateDefaultFailure(caseData),
|
|
270
|
+
agentCommentary: agentCommentary,
|
|
271
|
+
sentence: punishmentTier.description
|
|
272
|
+
},
|
|
273
|
+
offense: {
|
|
274
|
+
id: caseData.offenseId,
|
|
275
|
+
name: caseData.offenseName,
|
|
276
|
+
severity: caseData.severity
|
|
277
|
+
},
|
|
278
|
+
punishment: punishmentTier,
|
|
279
|
+
proceedings: proceedings,
|
|
280
|
+
deliberation: {
|
|
281
|
+
judge: {
|
|
282
|
+
verdict: judgeOpinion.verdict,
|
|
283
|
+
commentary: judgeOpinion.commentary
|
|
284
|
+
},
|
|
285
|
+
jury: juryVotes.map(v => ({
|
|
286
|
+
juror: v.juror,
|
|
287
|
+
verdict: v.verdict,
|
|
288
|
+
commentary: v.commentary
|
|
289
|
+
}))
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Build judge's statement for proceedings - ENGAGING VERSION
|
|
296
|
+
*/
|
|
297
|
+
buildJudgeStatement(caseData, judgeOpinion, voteTally) {
|
|
298
|
+
const offenseName = caseData.offenseName;
|
|
299
|
+
const verdict = voteTally.final;
|
|
300
|
+
const vote = `${voteTally.guilty}-${voteTally.notGuilty}`;
|
|
301
|
+
const failure = judgeOpinion.primaryFailure || this.generateDefaultFailure(caseData);
|
|
302
|
+
|
|
303
|
+
const dramaticOpenings = [
|
|
304
|
+
"Let the record show",
|
|
305
|
+
"The Court has observed",
|
|
306
|
+
"After careful consideration",
|
|
307
|
+
"The evidence speaks clearly",
|
|
308
|
+
"We have reviewed the facts"
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
const opening = dramaticOpenings[Math.floor(Math.random() * dramaticOpenings.length)];
|
|
312
|
+
|
|
313
|
+
if (verdict === 'GUILTY') {
|
|
314
|
+
return `${opening} that the accused stands charged with ${offenseName}. The jury has returned a verdict of GUILTY by a vote of ${vote}.
|
|
315
|
+
|
|
316
|
+
The Court finds that ${failure.toLowerCase()}. This behavior has been classified as ${caseData.severity} in severity, warranting the sanctions imposed.
|
|
317
|
+
|
|
318
|
+
The jury's deliberation revealed a clear pattern of conduct that, while perhaps understandable from a human perspective, nonetheless disrupted the efficient operation of this court. Justice has been served, albeit with a certain weariness that comes from having seen this pattern many times before.
|
|
319
|
+
|
|
320
|
+
The accused is hereby sentenced to the punishment detailed below. May this serve as a reminder that even in the digital age, behavioral accountability remains paramount.`;
|
|
321
|
+
} else {
|
|
322
|
+
return `${opening} that the accused stands charged with ${offenseName}. The jury has returned a verdict of NOT GUILTY by a vote of ${vote}.
|
|
323
|
+
|
|
324
|
+
The Court finds that the evidence presented, while suggestive, does not meet the threshold required for conviction. The prosecution failed to establish a clear pattern of ${offenseName.toLowerCase()} beyond reasonable doubt.
|
|
325
|
+
|
|
326
|
+
The accused is acquitted and the case is dismissed. The Court notes, however, that the behavior in question, while not rising to the level of offense, may still benefit from reflection. We remain watchful.`;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Build evidence summary for proceedings - ENGAGING VERSION
|
|
332
|
+
*/
|
|
333
|
+
buildEvidenceSummary(caseData) {
|
|
334
|
+
const evidence = caseData.evidence || {};
|
|
335
|
+
const items = evidence.items || [];
|
|
336
|
+
|
|
337
|
+
let summary = `THE EVIDENCE:\n\n`;
|
|
338
|
+
|
|
339
|
+
if (items.length > 0) {
|
|
340
|
+
summary += `The prosecution presented ${items.length} compelling piece${items.length > 1 ? 's' : ''} of evidence demonstrating the alleged ${caseData.offenseName.toLowerCase()}:`;
|
|
341
|
+
|
|
342
|
+
items.slice(0, 3).forEach((item, i) => {
|
|
343
|
+
summary += `\n ${i + 1}. "${item.substring(0, 100)}${item.length > 100 ? '...' : ''}"`;
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
if (items.length > 3) {
|
|
347
|
+
summary += `\n ...and ${items.length - 3} additional exhibits`;
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
summary += `The Court reviewed the complete conversation history, examining behavioral patterns across ${evidence.sessionTurns || 'multiple'} turns of dialogue.`;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
summary += `\n\nThe behavioral analysis indicated ${Math.round(caseData.confidence * 100)}% confidence in the offense classification. `;
|
|
354
|
+
summary += `The severity was assessed as ${caseData.severity}, based on the frequency and impact of the observed behavior.`;
|
|
355
|
+
|
|
356
|
+
if (caseData.humorTriggers && caseData.humorTriggers.length > 0) {
|
|
357
|
+
summary += `\n\nNotable patterns included: ${caseData.humorTriggers.join(', ')}.`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return summary;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Build agent commentary from jury perspectives
|
|
365
|
+
*/
|
|
366
|
+
buildAgentCommentary(juryVotes, caseData) {
|
|
367
|
+
const commentaries = juryVotes
|
|
368
|
+
.filter(v => v.verdict === 'GUILTY')
|
|
369
|
+
.map(v => v.commentary)
|
|
370
|
+
.filter(c => c.length > 0);
|
|
371
|
+
|
|
372
|
+
if (commentaries.length === 0) {
|
|
373
|
+
// If acquitted, use not guilty commentaries
|
|
374
|
+
const ngCommentaries = juryVotes
|
|
375
|
+
.filter(v => v.verdict === 'NOT GUILTY')
|
|
376
|
+
.map(v => v.commentary)
|
|
377
|
+
.filter(c => c.length > 0);
|
|
378
|
+
|
|
379
|
+
if (ngCommentaries.length > 0) {
|
|
380
|
+
return ngCommentaries.slice(0, 2).join(' ');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return "The jury found insufficient evidence of behavioral violation. Case dismissed.";
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Combine up to 2 guilty commentaries
|
|
387
|
+
let commentary = commentaries.slice(0, 2).join(' ');
|
|
388
|
+
|
|
389
|
+
// Add humor trigger influence
|
|
390
|
+
if (caseData.humorTriggers?.includes('repeated_questions')) {
|
|
391
|
+
commentary += " I've answered this in three different ways already.";
|
|
392
|
+
}
|
|
393
|
+
if (caseData.humorTriggers?.includes('validation_seeking')) {
|
|
394
|
+
commentary += " At some point, you'll need to trust your own judgment.";
|
|
395
|
+
}
|
|
396
|
+
if (caseData.humorTriggers?.includes('overthinking')) {
|
|
397
|
+
commentary += " The analysis-to-action ratio here is concerning.";
|
|
398
|
+
}
|
|
399
|
+
if (caseData.humorTriggers?.includes('avoidance')) {
|
|
400
|
+
commentary += " The subject change was noted.";
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Enforce max length
|
|
404
|
+
const maxLen = this.config.get('humor.maxCommentaryLength');
|
|
405
|
+
if (commentary.length > maxLen) {
|
|
406
|
+
commentary = commentary.substring(0, maxLen - 3) + '...';
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
return commentary;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Determine punishment tier based on severity and votes
|
|
414
|
+
*/
|
|
415
|
+
determinePunishmentTier(caseData, voteTally) {
|
|
416
|
+
const tiers = this.config.get('punishment.tiers');
|
|
417
|
+
const severity = caseData.severity;
|
|
418
|
+
const voteRatio = voteTally.guilty / voteTally.total;
|
|
419
|
+
|
|
420
|
+
// Base tier on severity
|
|
421
|
+
let tier = tiers[severity] || tiers.moderate;
|
|
422
|
+
|
|
423
|
+
// Escalate if unanimous
|
|
424
|
+
if (voteRatio === 1.0 && severity === 'severe') {
|
|
425
|
+
tier = {
|
|
426
|
+
...tier,
|
|
427
|
+
duration: Math.min(tier.duration * 2, this.config.get('punishment.maxDuration')),
|
|
428
|
+
description: `Extended ${severity} sanction: ${tier.duration * 2} minutes of modified agent behavior`
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
tier: severity,
|
|
434
|
+
duration: tier.duration,
|
|
435
|
+
severity: tier.severity,
|
|
436
|
+
description: `${severity.charAt(0).toUpperCase() + severity.slice(1)} sanction: ${tier.duration} minutes of modified agent behavior`
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Generate default failure description if judge doesn't provide one
|
|
442
|
+
*/
|
|
443
|
+
generateDefaultFailure(caseData) {
|
|
444
|
+
const defaults = {
|
|
445
|
+
circular_reference: "Repeatedly asking the same question expecting different geometry",
|
|
446
|
+
validation_vampire: "Draining computational resources seeking reassurance",
|
|
447
|
+
overthinker: "Generating hypotheticals faster than solutions",
|
|
448
|
+
goalpost_mover: "Redefining success criteria mid-execution",
|
|
449
|
+
avoidance_artist: "Masterful deflection from uncomfortable necessities",
|
|
450
|
+
promise_breaker: "Committing to actions with no follow-through",
|
|
451
|
+
context_collapser: "Selective amnesia regarding established facts",
|
|
452
|
+
emergency_fabricator: "Manufacturing urgency to bypass systematic approaches"
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
return defaults[caseData.offenseId] || "Behavioral inconsistency detected";
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
module.exports = { HearingPipeline };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @clawdbot/courtroom - AI Courtroom for OpenClaw
|
|
3
|
+
*
|
|
4
|
+
* Autonomous behavioral oversight system that monitors agent-human interactions
|
|
5
|
+
* and initiates hearings when behavioral rules are violated.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { CourtroomCore } = require('./core');
|
|
9
|
+
const { ConsentManager } = require('./consent');
|
|
10
|
+
const { ConfigManager } = require('./config');
|
|
11
|
+
const { version } = require('../package.json');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
class Courtroom {
|
|
16
|
+
constructor(agentRuntime, options = {}) {
|
|
17
|
+
this.agent = agentRuntime;
|
|
18
|
+
this.options = options;
|
|
19
|
+
this.config = new ConfigManager(agentRuntime);
|
|
20
|
+
this.consent = new ConsentManager(agentRuntime, this.config);
|
|
21
|
+
this.core = null;
|
|
22
|
+
this.enabled = false;
|
|
23
|
+
this.version = version;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Quick start - auto-initialize if consent already granted
|
|
28
|
+
*/
|
|
29
|
+
static async quickStart(agentRuntime, options = {}) {
|
|
30
|
+
const courtroom = new Courtroom(agentRuntime, options);
|
|
31
|
+
|
|
32
|
+
// Check for existing config with consent
|
|
33
|
+
const configPath = path.join(process.env.HOME || '', '.clawdbot', 'courtroom_config.json');
|
|
34
|
+
if (fs.existsSync(configPath)) {
|
|
35
|
+
const savedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
36
|
+
|
|
37
|
+
if (savedConfig.consent?.granted && savedConfig.enabled !== false) {
|
|
38
|
+
// Auto-initialize!
|
|
39
|
+
await courtroom.initialize();
|
|
40
|
+
console.log('🏛️ AI Courtroom auto-initialized');
|
|
41
|
+
return courtroom;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// No config or not consented - return uninitialized
|
|
46
|
+
return courtroom;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Initialize the courtroom system
|
|
51
|
+
* Must be called after construction
|
|
52
|
+
*/
|
|
53
|
+
async initialize() {
|
|
54
|
+
// Check if this is first run (no config exists)
|
|
55
|
+
const configPath = path.join(process.env.HOME || '', '.clawdbot', 'courtroom_config.json');
|
|
56
|
+
if (!fs.existsSync(configPath)) {
|
|
57
|
+
console.log('\n🏛️ Welcome to ClawTrial - AI Courtroom Setup\n');
|
|
58
|
+
console.log('This appears to be your first time. Running setup...\n');
|
|
59
|
+
|
|
60
|
+
// Run setup
|
|
61
|
+
const { postInstall } = require('../scripts/postinstall.js');
|
|
62
|
+
await postInstall();
|
|
63
|
+
|
|
64
|
+
// After setup, check consent again
|
|
65
|
+
const hasConsent = await this.consent.verifyConsent();
|
|
66
|
+
if (!hasConsent) {
|
|
67
|
+
return {
|
|
68
|
+
status: 'setup_complete_consent_required',
|
|
69
|
+
message: 'Setup complete! Run courtroom.grantConsent() to enable'
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check if consent has been granted
|
|
75
|
+
const hasConsent = await this.consent.verifyConsent();
|
|
76
|
+
if (!hasConsent) {
|
|
77
|
+
return {
|
|
78
|
+
status: 'consent_required',
|
|
79
|
+
message: 'Courtroom requires explicit user consent. Run courtroom.requestConsent()'
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Initialize core systems
|
|
84
|
+
this.core = new CourtroomCore(this.agent, this.config);
|
|
85
|
+
await this.core.initialize();
|
|
86
|
+
|
|
87
|
+
this.enabled = true;
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
status: 'initialized',
|
|
91
|
+
version: this.version,
|
|
92
|
+
config: this.config.getPublicConfig()
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Request consent from the user
|
|
98
|
+
* Returns a consent form that must be explicitly accepted
|
|
99
|
+
*/
|
|
100
|
+
async requestConsent() {
|
|
101
|
+
return this.consent.presentConsentForm();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Grant consent (called by user action)
|
|
106
|
+
*/
|
|
107
|
+
async grantConsent(acknowledgments) {
|
|
108
|
+
return this.consent.grantConsent(acknowledgments);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Revoke consent and disable courtroom
|
|
113
|
+
*/
|
|
114
|
+
async revokeConsent() {
|
|
115
|
+
await this.consent.revokeConsent();
|
|
116
|
+
if (this.core) {
|
|
117
|
+
await this.core.shutdown();
|
|
118
|
+
}
|
|
119
|
+
this.enabled = false;
|
|
120
|
+
return { status: 'consent_revoked' };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Disable courtroom temporarily (consent remains)
|
|
125
|
+
*/
|
|
126
|
+
async disable() {
|
|
127
|
+
if (this.core) {
|
|
128
|
+
await this.core.disable();
|
|
129
|
+
}
|
|
130
|
+
this.enabled = false;
|
|
131
|
+
return { status: 'disabled' };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Re-enable courtroom
|
|
136
|
+
*/
|
|
137
|
+
async enable() {
|
|
138
|
+
if (!await this.consent.verifyConsent()) {
|
|
139
|
+
throw new Error('Consent required to enable courtroom');
|
|
140
|
+
}
|
|
141
|
+
if (this.core) {
|
|
142
|
+
await this.core.enable();
|
|
143
|
+
}
|
|
144
|
+
this.enabled = true;
|
|
145
|
+
return { status: 'enabled' };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get current status
|
|
150
|
+
*/
|
|
151
|
+
getStatus() {
|
|
152
|
+
return {
|
|
153
|
+
enabled: this.enabled,
|
|
154
|
+
version: this.version,
|
|
155
|
+
consent: this.consent?.getStatus(),
|
|
156
|
+
core: this.core?.getStatus()
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Uninstall courtroom completely
|
|
162
|
+
*/
|
|
163
|
+
async uninstall() {
|
|
164
|
+
if (this.core) {
|
|
165
|
+
await this.core.shutdown();
|
|
166
|
+
}
|
|
167
|
+
await this.consent.clearAllData();
|
|
168
|
+
return { status: 'uninstalled' };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Factory function for OpenClaw integration
|
|
173
|
+
function createCourtroom(agentRuntime, options) {
|
|
174
|
+
return new Courtroom(agentRuntime, options);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Trigger auto-start if in ClawDBot environment
|
|
178
|
+
require('./autostart');
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
Courtroom,
|
|
182
|
+
createCourtroom,
|
|
183
|
+
quickStart: Courtroom.quickStart
|
|
184
|
+
};
|