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>
151 lines
3.4 KiB
TypeScript
151 lines
3.4 KiB
TypeScript
/**
|
|
* R6 Framework - Intent → Action → Result
|
|
*
|
|
* R6 = Rules + Role + Request + Reference + Resource → Result
|
|
*
|
|
* Every tool call gets a structured R6 record that captures intent,
|
|
* context, and outcome for audit and trust evaluation.
|
|
*/
|
|
|
|
import { createHash, randomUUID } from "node:crypto";
|
|
|
|
export type R6Request = {
|
|
id: string;
|
|
timestamp: string;
|
|
rules: R6Rules;
|
|
role: R6Role;
|
|
request: R6RequestDetail;
|
|
reference: R6Reference;
|
|
resource: R6Resource;
|
|
result?: R6Result;
|
|
};
|
|
|
|
export type R6Rules = {
|
|
auditLevel: string;
|
|
constraints: string[];
|
|
/** Policy entity ID (policy as first-class trust participant) */
|
|
policyEntityId?: string;
|
|
};
|
|
|
|
export type R6Role = {
|
|
sessionId: string;
|
|
agentId?: string;
|
|
actionIndex: number;
|
|
bindingType: "soft-lct";
|
|
};
|
|
|
|
export type R6RequestDetail = {
|
|
toolName: string;
|
|
category: ToolCategory;
|
|
target?: string;
|
|
inputHash: string;
|
|
};
|
|
|
|
export type R6Reference = {
|
|
sessionId: string;
|
|
prevR6Id?: string;
|
|
chainPosition: number;
|
|
};
|
|
|
|
export type R6Resource = {
|
|
estimatedTokens?: number;
|
|
approvalRequired: boolean;
|
|
};
|
|
|
|
export type R6Result = {
|
|
status: "success" | "error" | "blocked";
|
|
outputHash?: string;
|
|
errorMessage?: string;
|
|
durationMs?: number;
|
|
};
|
|
|
|
export type ToolCategory =
|
|
| "file_read"
|
|
| "file_write"
|
|
| "command"
|
|
| "network"
|
|
| "delegation"
|
|
| "state"
|
|
| "mcp"
|
|
| "unknown";
|
|
|
|
const TOOL_CATEGORIES: Record<string, ToolCategory> = {
|
|
Read: "file_read",
|
|
Glob: "file_read",
|
|
Grep: "file_read",
|
|
Write: "file_write",
|
|
Edit: "file_write",
|
|
NotebookEdit: "file_write",
|
|
Bash: "command",
|
|
WebFetch: "network",
|
|
WebSearch: "network",
|
|
Task: "delegation",
|
|
TodoWrite: "state",
|
|
};
|
|
|
|
export function classifyTool(toolName: string): ToolCategory {
|
|
return TOOL_CATEGORIES[toolName] ?? "unknown";
|
|
}
|
|
|
|
export function hashInput(input: Record<string, unknown>): string {
|
|
return createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16);
|
|
}
|
|
|
|
export function hashOutput(output: unknown): string {
|
|
const str = typeof output === "string" ? output : JSON.stringify(output);
|
|
return createHash("sha256").update(str).digest("hex").slice(0, 16);
|
|
}
|
|
|
|
export function extractTarget(toolName: string, params: Record<string, unknown>): string | undefined {
|
|
if (params.file_path) return String(params.file_path);
|
|
if (params.path) return String(params.path);
|
|
if (params.pattern) return String(params.pattern);
|
|
if (params.command) {
|
|
const cmd = String(params.command);
|
|
return cmd.length > 80 ? cmd.slice(0, 80) + "..." : cmd;
|
|
}
|
|
if (params.url) return String(params.url);
|
|
return undefined;
|
|
}
|
|
|
|
export function createR6Request(
|
|
sessionId: string,
|
|
agentId: string | undefined,
|
|
toolName: string,
|
|
params: Record<string, unknown>,
|
|
actionIndex: number,
|
|
prevR6Id: string | undefined,
|
|
auditLevel: string,
|
|
policyEntityId?: string,
|
|
): R6Request {
|
|
return {
|
|
id: `r6:${randomUUID().slice(0, 8)}`,
|
|
timestamp: new Date().toISOString(),
|
|
rules: {
|
|
auditLevel,
|
|
constraints: [],
|
|
policyEntityId,
|
|
},
|
|
role: {
|
|
sessionId,
|
|
agentId,
|
|
actionIndex,
|
|
bindingType: "soft-lct",
|
|
},
|
|
request: {
|
|
toolName,
|
|
category: classifyTool(toolName),
|
|
target: extractTarget(toolName, params),
|
|
inputHash: hashInput(params),
|
|
},
|
|
reference: {
|
|
sessionId,
|
|
prevR6Id,
|
|
chainPosition: actionIndex,
|
|
},
|
|
resource: {
|
|
approvalRequired: false,
|
|
},
|
|
};
|
|
}
|