openclaw/extensions/web4-governance/src/session-state.ts
dennis palatov 93bab62444 feat(web4-governance): add PolicyEntity as first-class trust participant (#5)
Policy is now "society's law" - not just configuration, but a hash-tracked,
witnessable entity in the trust network.

Key changes:
- PolicyEntity class with hash-identified entityId (policy:<name>:<version>:<hash>)
- PolicyRegistry for registration and witnessing
- Policy evaluation with rate limiting integration
- Session witnesses policy at initialization
- Policy witnesses tool decisions in after_tool_call
- R6 records include policyEntityId in rules field
- CLI command: `moltbot policy entities` to list registered policies
- 26 new tests for PolicyEntity (177 total tests passing)

The witnessing flow:
1. Session start → register policy entity → session witnesses policy
2. Tool call → policy evaluates decision → policy witnesses outcome
3. Bidirectional trust accumulation in the witnessing chain

Co-authored-by: dp-web4 <dp@metalinxx.io>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-28 13:33:36 -08:00

55 lines
1.5 KiB
TypeScript

/**
* Session State - Tracks R6 chain state for a session.
*/
import { mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import type { SoftLCTToken } from "./soft-lct.js";
export type SessionState = {
sessionId: string;
lct: SoftLCTToken;
actionIndex: number;
lastR6Id?: string;
startedAt: string;
toolCounts: Record<string, number>;
categoryCounts: Record<string, number>;
/** Policy entity ID (policy:<name>:<version>:<hash>) */
policyEntityId?: string;
};
export class SessionStore {
private storagePath: string;
constructor(storagePath: string) {
this.storagePath = storagePath;
mkdirSync(join(this.storagePath, "sessions"), { recursive: true });
}
private filePath(sessionId: string): string {
return join(this.storagePath, "sessions", `${sessionId}.json`);
}
save(state: SessionState): void {
writeFileSync(this.filePath(state.sessionId), JSON.stringify(state, null, 2));
}
load(sessionId: string): SessionState | null {
const path = this.filePath(sessionId);
if (!existsSync(path)) return null;
try {
return JSON.parse(readFileSync(path, "utf-8")) as SessionState;
} catch {
return null;
}
}
incrementAction(state: SessionState, toolName: string, category: string, r6Id: string): void {
state.actionIndex++;
state.lastR6Id = r6Id;
state.toolCounts[toolName] = (state.toolCounts[toolName] ?? 0) + 1;
state.categoryCounts[category] = (state.categoryCounts[category] ?? 0) + 1;
this.save(state);
}
}