Skip to content

Commit d738b6b

Browse files
authored
Feat/cerberus erep (#940)
* feat: reference mapping * feat: sync * feat: reference
1 parent d22457f commit d738b6b

10 files changed

Lines changed: 405 additions & 8 deletions

File tree

platforms/cerberus/client/src/database/data-source.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import path from "path";
99
import { UserEVaultMapping } from "./entities/UserEVaultMapping";
1010
import { VotingObservation } from "./entities/VotingObservation";
1111
import { CharterSignature } from "./entities/CharterSignature";
12+
import { Reference } from "./entities/Reference";
1213

1314
config({ path: path.resolve(__dirname, "../../../../.env") });
1415

@@ -25,6 +26,7 @@ export const AppDataSource = new DataSource({
2526
UserEVaultMapping,
2627
VotingObservation,
2728
CharterSignature,
29+
Reference,
2830
],
2931
migrations: [path.join(__dirname, "migrations", "*.ts")],
3032
subscribers: [PostgresSubscriber],
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, CreateDateColumn, UpdateDateColumn } from "typeorm";
2+
import { User } from "./User";
3+
4+
@Entity("references")
5+
export class Reference {
6+
@PrimaryGeneratedColumn("uuid")
7+
id!: string;
8+
9+
@Column()
10+
targetType!: string; // "user", "group", "platform"
11+
12+
@Column()
13+
targetId!: string;
14+
15+
@Column()
16+
targetName!: string;
17+
18+
@Column("text")
19+
content!: string;
20+
21+
@Column()
22+
referenceType!: string; // "general", "violation", etc.
23+
24+
@Column("int", { nullable: true })
25+
numericScore?: number; // 1-5 score
26+
27+
@Column()
28+
authorId!: string;
29+
30+
@ManyToOne(() => User)
31+
@JoinColumn({ name: "authorId" })
32+
author!: User;
33+
34+
@Column({ default: "signed" })
35+
status!: string; // "signed", "revoked"
36+
37+
@Column({ default: false })
38+
anonymous!: boolean;
39+
40+
@CreateDateColumn()
41+
createdAt!: Date;
42+
43+
@UpdateDateColumn()
44+
updatedAt!: Date;
45+
}

platforms/cerberus/client/src/services/CerberusTriggerService.ts

Lines changed: 84 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { GroupService } from "./GroupService";
55
import { UserService } from "./UserService";
66
import { PlatformEVaultService } from "./PlatformEVaultService";
77
import { VotingContextService } from "./VotingContextService";
8+
import { ReferenceWriterService } from "./ReferenceWriterService";
89

910
interface CharterViolation {
1011
violation: string;
@@ -28,6 +29,7 @@ export class CerberusTriggerService {
2829
private userService: UserService;
2930
private platformService: PlatformEVaultService;
3031
private votingContextService: VotingContextService;
32+
private referenceWriterService: ReferenceWriterService;
3133
private openaiApiKey: string;
3234

3335
constructor() {
@@ -36,6 +38,7 @@ export class CerberusTriggerService {
3638
this.userService = new UserService();
3739
this.platformService = PlatformEVaultService.getInstance();
3840
this.votingContextService = new VotingContextService();
41+
this.referenceWriterService = new ReferenceWriterService();
3942
this.openaiApiKey = process.env.OPENAI_API_KEY || "";
4043
}
4144

@@ -319,6 +322,14 @@ export class CerberusTriggerService {
319322
*/
320323
async analyzeCharterViolations(messages: Message[], group: Group, lastVoteMessage?: Message): Promise<{
321324
violations: string[];
325+
userViolations: Array<{
326+
userId: string;
327+
userName: string;
328+
userEname: string | null;
329+
violation: string;
330+
severity: "low" | "medium" | "high";
331+
score: number;
332+
}>;
322333
summary: string;
323334
hasVotingIssues: boolean;
324335
votingStatus: string;
@@ -327,6 +338,7 @@ export class CerberusTriggerService {
327338
if (!this.openaiApiKey) {
328339
return {
329340
violations: [],
341+
userViolations: [],
330342
summary: "⚠️ OpenAI API key not configured. Cannot analyze charter violations.",
331343
hasVotingIssues: false,
332344
votingStatus: "Not analyzed",
@@ -385,10 +397,13 @@ VOTING CONTEXT:
385397
console.log(votingContext);
386398
}
387399

388-
// Format messages for analysis
389-
const messagesText = messages.map(msg =>
390-
`[${msg.createdAt.toLocaleString()}] ${msg.sender ? msg.sender.name : 'System'}: ${msg.text}`
391-
).join('\n');
400+
// Format messages for analysis — include sender ID and ename for structured violation reporting
401+
const messagesText = messages.map(msg => {
402+
const senderInfo = msg.sender
403+
? `${msg.sender.name || 'Unknown'} (id:${msg.sender.id}${msg.sender.ename ? ', ename:' + msg.sender.ename : ''})`
404+
: 'System';
405+
return `[${msg.createdAt.toLocaleString()}] ${senderInfo}: ${msg.text}`;
406+
}).join('\n');
392407

393408
const charterText = group.charter || "No charter defined for this group.";
394409

@@ -430,6 +445,16 @@ CHARTER ENFORCEMENT:
430445
RESPOND WITH ONLY PURE JSON - NO MARKDOWN, NO CODE BLOCKS:
431446
{
432447
"violations": ["array of detailed violation descriptions with justifications"],
448+
"userViolations": [
449+
{
450+
"userId": "the user's id from the message (id:xxx)",
451+
"userName": "the user's display name",
452+
"userEname": "the user's ename if available, or null",
453+
"violation": "one-sentence description of what rule was violated",
454+
"severity": "low" | "medium" | "high",
455+
"score": 1-5 (1 = severe, 5 = minor/warning)
456+
}
457+
],
433458
"summary": "comprehensive summary with specific examples and actionable recommendations",
434459
"hasVotingIssues": boolean,
435460
"votingStatus": "string describing current voting situation with reasoning",
@@ -486,8 +511,14 @@ Be thorough and justify your reasoning. Provide clear, actionable recommendation
486511

487512
// Parse the JSON response
488513
try {
489-
const analysis = JSON.parse(content);
490-
514+
// Strip markdown code fences if present
515+
let jsonContent = content.trim();
516+
if (jsonContent.startsWith("```")) {
517+
jsonContent = jsonContent.replace(/^```(?:json)?\s*/, "").replace(/```\s*$/, "").trim();
518+
}
519+
520+
const analysis = JSON.parse(jsonContent);
521+
491522
// Validate required fields
492523
if (!Array.isArray(analysis.violations) ||
493524
typeof analysis.summary !== 'string' ||
@@ -497,6 +528,11 @@ Be thorough and justify your reasoning. Provide clear, actionable recommendation
497528
throw new Error("Invalid JSON structure from OpenAI");
498529
}
499530

531+
// Ensure userViolations exists and is an array
532+
if (!Array.isArray(analysis.userViolations)) {
533+
analysis.userViolations = [];
534+
}
535+
500536
return analysis;
501537

502538
} catch (parseError) {
@@ -505,6 +541,7 @@ Be thorough and justify your reasoning. Provide clear, actionable recommendation
505541

506542
return {
507543
violations: [],
544+
userViolations: [],
508545
summary: "❌ Error analyzing messages. Please check the logs.",
509546
hasVotingIssues: false,
510547
votingStatus: "Error occurred",
@@ -516,6 +553,7 @@ Be thorough and justify your reasoning. Provide clear, actionable recommendation
516553
console.error("Error analyzing with AI:", error);
517554
return {
518555
violations: [],
556+
userViolations: [],
519557
summary: "❌ Error analyzing charter violations. Please check the logs.",
520558
hasVotingIssues: false,
521559
votingStatus: "Error occurred",
@@ -589,11 +627,50 @@ Be thorough and justify your reasoning. Provide clear, actionable recommendation
589627
});
590628
}
591629

630+
// Write violation references for users who violated the charter
631+
if (analysis.userViolations && analysis.userViolations.length > 0) {
632+
try {
633+
// Resolve a Cerberus system user as the author of the reference
634+
// Use the first group participant as a fallback author, or try "cerberus" ename
635+
let authorId: string | null = null;
636+
const cerberusUser = await this.referenceWriterService.resolveUserId("cerberus");
637+
if (cerberusUser) {
638+
authorId = cerberusUser.id;
639+
} else {
640+
// Use the trigger message sender or first available user
641+
const allUsers = await this.userService.getAllUsers();
642+
if (allUsers.length > 0) authorId = allUsers[0].id;
643+
}
644+
645+
if (authorId) {
646+
const violationRefs = analysis.userViolations.map((uv: any) => ({
647+
targetId: uv.userId,
648+
targetName: uv.userName,
649+
targetEname: uv.userEname || undefined,
650+
content: `[Cerberus - Charter Violation in ${groupWithCharter.name}] ${uv.violation}`,
651+
numericScore: Math.max(1, Math.min(5, uv.score || 2))
652+
}));
653+
654+
await this.referenceWriterService.writeViolationReferences(
655+
violationRefs,
656+
triggerMessage.group.id,
657+
groupWithCharter.name,
658+
authorId
659+
);
660+
console.log(`📝 Wrote ${violationRefs.length} violation references`);
661+
} else {
662+
console.warn("⚠️ No author user found for violation references");
663+
}
664+
} catch (refError) {
665+
console.error("❌ Error writing violation references:", refError);
666+
}
667+
}
668+
592669
// Build the final analysis text
593670
let analysisText: string;
594671
if (analysis.violations.length > 0) {
595672
analysisText = `🚨 CHARTER VIOLATIONS DETECTED!\n\n${analysis.summary}`;
596-
673+
597674
// Add enforcement information if available
598675
if (analysis.enforcement && analysis.enforcement !== "No enforcement possible - API not configured") {
599676
analysisText += `\n\n⚖️ ENFORCEMENT:\n${analysis.enforcement}`;
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { AppDataSource } from "../database/data-source";
2+
import { Reference } from "../database/entities/Reference";
3+
import { User } from "../database/entities/User";
4+
5+
interface ViolationReference {
6+
targetId: string; // User ID of the violator
7+
targetName: string; // Display name of the violator
8+
targetEname?: string; // ename of the violator
9+
content: string; // Description of the violation
10+
numericScore: number; // 1-5 (1 = severe violation, 5 = minor)
11+
}
12+
13+
export class ReferenceWriterService {
14+
private referenceRepository = AppDataSource.getRepository(Reference);
15+
private userRepository = AppDataSource.getRepository(User);
16+
private eReputationBaseUrl: string;
17+
private platformSecret: string;
18+
19+
constructor() {
20+
this.eReputationBaseUrl = process.env.PUBLIC_EREPUTATION_BASE_URL || "http://localhost:8765";
21+
this.platformSecret = process.env.PLATFORM_SHARED_SECRET || "";
22+
}
23+
24+
/**
25+
* Write violation references for users who violated the charter.
26+
* Writes to both local DB (for eVault sync) and eReputation API directly.
27+
*/
28+
async writeViolationReferences(
29+
violations: ViolationReference[],
30+
groupId: string,
31+
groupName: string,
32+
authorId: string
33+
): Promise<void> {
34+
for (const violation of violations) {
35+
try {
36+
// 1. Write to local DB (triggers web3adapter → eVault sync)
37+
await this.writeLocalReference(violation, authorId);
38+
39+
// 2. Write directly to eReputation API
40+
await this.writeToEReputationApi(violation, authorId);
41+
42+
console.log(`✅ Violation reference written for ${violation.targetName} (${violation.targetId})`);
43+
} catch (error) {
44+
console.error(`❌ Failed to write violation reference for ${violation.targetName}:`, error);
45+
}
46+
}
47+
}
48+
49+
/**
50+
* Write reference to local Cerberus DB — the web3adapter subscriber
51+
* will pick it up and sync to eVault automatically.
52+
*/
53+
private async writeLocalReference(violation: ViolationReference, authorId: string): Promise<Reference> {
54+
const reference = this.referenceRepository.create({
55+
targetType: "user",
56+
targetId: violation.targetId,
57+
targetName: violation.targetName,
58+
content: violation.content,
59+
referenceType: "violation",
60+
numericScore: violation.numericScore,
61+
authorId,
62+
status: "signed",
63+
anonymous: false
64+
});
65+
66+
return await this.referenceRepository.save(reference);
67+
}
68+
69+
/**
70+
* Write reference directly to eReputation API using the platform shared secret.
71+
*/
72+
private async writeToEReputationApi(violation: ViolationReference, authorId: string): Promise<void> {
73+
if (!this.platformSecret) {
74+
console.warn("⚠️ PLATFORM_SHARED_SECRET not set, skipping eReputation API call");
75+
return;
76+
}
77+
78+
try {
79+
const response = await fetch(`${this.eReputationBaseUrl}/api/references/system`, {
80+
method: "POST",
81+
headers: {
82+
"Content-Type": "application/json",
83+
"X-Platform-Secret": this.platformSecret
84+
},
85+
body: JSON.stringify({
86+
targetType: "user",
87+
targetId: violation.targetId,
88+
targetName: violation.targetName,
89+
content: violation.content,
90+
referenceType: "violation",
91+
numericScore: violation.numericScore,
92+
authorId,
93+
anonymous: false
94+
})
95+
});
96+
97+
if (!response.ok) {
98+
const errorText = await response.text();
99+
console.error(`❌ eReputation API error (${response.status}): ${errorText}`);
100+
}
101+
} catch (error) {
102+
console.error("❌ Failed to call eReputation API:", error);
103+
}
104+
}
105+
106+
/**
107+
* Resolve a user name/ename to their user ID in the local database.
108+
*/
109+
async resolveUserId(nameOrEname: string): Promise<User | null> {
110+
// Try ename first
111+
let user = await this.userRepository.findOne({ where: { ename: nameOrEname } });
112+
if (user) return user;
113+
114+
// Try name
115+
user = await this.userRepository.findOne({ where: { name: nameOrEname } });
116+
return user;
117+
}
118+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"tableName": "references",
3+
"schemaId": "550e8400-e29b-41d4-a716-446655440010",
4+
"ownerEnamePath": "users(author.ename)",
5+
"localToUniversalMap": {
6+
"id": "id",
7+
"targetType": "targetType",
8+
"targetId": "targetId",
9+
"targetName": "targetName",
10+
"content": "content",
11+
"referenceType": "referenceType",
12+
"numericScore": "numericScore",
13+
"authorId": "users(author.id),author",
14+
"status": "status",
15+
"anonymous": "anonymous",
16+
"createdAt": "__date(createdAt)",
17+
"updatedAt": "__date(updatedAt)"
18+
}
19+
}

platforms/cerberus/client/src/web3adapter/watchers/subscriber.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,8 @@ export class PostgresSubscriber implements EntitySubscriberInterface {
448448
return ["sender", "group"];
449449
case "CharterSignature":
450450
return ["user", "group"];
451+
case "Reference":
452+
return ["author"];
451453
default:
452454
return [];
453455
}

0 commit comments

Comments
 (0)