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>
This commit is contained in:
dennis palatov 2026-01-28 11:36:06 -08:00 committed by GitHub
parent df22a4fff7
commit e8c36e52db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 859 additions and 1 deletions

View File

@ -19,6 +19,7 @@ import type { PolicyConfig, PolicyEvaluation } from "./src/policy-types.js";
import { RateLimiter } from "./src/rate-limiter.js";
import { resolvePreset, listPresets, isPresetName } from "./src/presets.js";
import { AuditReporter } from "./src/reporter.js";
import { PolicyRegistry, type PolicyEntityId } from "./src/policy-entity.js";
type PluginConfig = {
auditLevel?: string;
@ -54,6 +55,9 @@ const plugin = {
// Rate limiter (memory-only, shared across all sessions)
const rateLimiter = new RateLimiter();
// Policy entity registry (policies as first-class trust participants)
const policyRegistry = new PolicyRegistry();
// Resolve policy config: preset → merge with overrides
let resolvedPolicyConfig: Partial<PolicyConfig> = {};
if (config.policy) {
@ -85,6 +89,20 @@ const plugin = {
if (!entry) {
const sessionId = sessionKey || randomUUID();
const lct = createSoftLCT(sessionId);
// Register policy entity (policy as first-class trust participant)
let policyEntityId: PolicyEntityId | undefined;
const presetName = config.policy?.preset ?? "safety";
if (isPresetName(presetName)) {
const policyEntity = policyRegistry.registerPolicy({
name: presetName,
preset: presetName,
});
policyEntityId = policyEntity.entityId;
// Session witnesses operating under this policy
policyRegistry.witnessSession(policyEntity.entityId, sessionId);
}
const state: SessionState = {
sessionId,
lct,
@ -92,12 +110,14 @@ const plugin = {
startedAt: new Date().toISOString(),
toolCounts: {},
categoryCounts: {},
policyEntityId,
};
const audit = new AuditChain(storagePath, sessionId);
sessionStore.save(state);
entry = { state, audit };
sessions.set(sessionKey, entry);
logger.info(`[web4] Session ${lct.tokenId} initialized (${auditLevel} audit)`);
const policyInfo = policyEntityId ? ` [policy:${presetName}]` : "";
logger.info(`[web4] Session ${lct.tokenId} initialized (${auditLevel} audit)${policyInfo}`);
}
return entry;
}
@ -149,6 +169,7 @@ const plugin = {
state.actionIndex,
state.lastR6Id,
auditLevel,
state.policyEntityId,
);
// Record in audit chain with success (commands that reach hooks succeeded)
@ -220,6 +241,7 @@ const plugin = {
entry.state.actionIndex,
entry.state.lastR6Id,
auditLevel,
entry.state.policyEntityId,
);
// Write policy constraints to R6
@ -227,6 +249,19 @@ const plugin = {
r6.rules.constraints = policyEval.constraints;
}
// Witness policy decision (policy witnesses the outcome)
if (entry.state.policyEntityId) {
const decision = policyEval?.decision ?? "allow";
const success = !event.error;
policyRegistry.witnessDecision(
entry.state.policyEntityId as PolicyEntityId,
entry.state.sessionId,
event.toolName,
decision,
success
);
}
const result = {
status: (event.error ? "error" : "success") as "success" | "error",
outputHash: event.result ? hashOutput(event.result) : undefined,
@ -467,6 +502,29 @@ const plugin = {
}
console.log(`Usage: { "policy": { "preset": "<name>" } }`);
});
// --- Policy Entity CLI ---
policy
.command("entities")
.description("List registered policy entities (policies as trust participants)")
.action(() => {
const entities = policyRegistry.listPolicies();
if (entities.length === 0) {
console.log("No policy entities registered.");
return;
}
console.log(`${entities.length} policy entities:\n`);
for (const e of entities) {
console.log(` ${e.entityId}`);
console.log(` name: ${e.name} | source: ${e.source}`);
console.log(` hash: ${e.contentHash}`);
console.log(` created: ${e.createdAt}`);
const witnessedBy = policyRegistry.getWitnessedBy(e.entityId);
const hasWitnessed = policyRegistry.getHasWitnessed(e.entityId);
console.log(` witnessed by: ${witnessedBy.length} | has witnessed: ${hasWitnessed.length}`);
console.log();
}
});
},
{ commands: ["policy"] },
);

View File

@ -0,0 +1,425 @@
import { describe, it, expect, beforeEach } from "vitest";
import {
PolicyEntity,
PolicyRegistry,
computePolicyHash,
type PolicyEntityId,
} from "./policy-entity.js";
import type { PolicyConfig, PolicyRule, PolicyMatch } from "./policy-types.js";
import { RateLimiter } from "./rate-limiter.js";
describe("PolicyEntity", () => {
let registry: PolicyRegistry;
beforeEach(() => {
registry = new PolicyRegistry();
});
describe("entity creation", () => {
it("creates entity with preset", () => {
const entity = registry.registerPolicy({
name: "test",
preset: "safety",
});
expect(entity.name).toBe("test");
expect(entity.source).toBe("preset");
expect(entity.entityId).toMatch(/^policy:test:\d+:[a-f0-9]+$/);
expect(entity.contentHash).toHaveLength(16);
});
it("creates entity with custom config", () => {
const config: PolicyConfig = {
defaultPolicy: "allow",
enforce: true,
rules: [],
};
const entity = registry.registerPolicy({
name: "custom",
config,
});
expect(entity.source).toBe("custom");
expect(entity.config).toEqual(config);
});
it("throws when neither config nor preset provided", () => {
expect(() =>
registry.registerPolicy({ name: "invalid" })
).toThrow("Must provide either config or preset");
});
it("throws when both config and preset provided", () => {
expect(() =>
registry.registerPolicy({
name: "invalid",
config: { defaultPolicy: "allow", enforce: true, rules: [] },
preset: "safety",
})
).toThrow("Cannot provide both config and preset");
});
});
describe("hash uniqueness", () => {
it("different configs produce different hashes", () => {
const entity1 = registry.registerPolicy({
name: "safety",
preset: "safety",
});
const entity2 = registry.registerPolicy({
name: "permissive",
preset: "permissive",
});
expect(entity1.contentHash).not.toBe(entity2.contentHash);
expect(entity1.entityId).not.toBe(entity2.entityId);
});
it("same config returns cached entity", () => {
const entity1 = registry.registerPolicy({
name: "safety",
preset: "safety",
version: "v1",
});
const entity2 = registry.registerPolicy({
name: "safety",
preset: "safety",
version: "v1",
});
expect(entity1).toBe(entity2);
});
});
describe("entity ID format", () => {
it("follows policy:<name>:<version>:<hash> format", () => {
const entity = registry.registerPolicy({
name: "test",
preset: "safety",
version: "v1",
});
const parts = entity.entityId.split(":");
expect(parts).toHaveLength(4);
expect(parts[0]).toBe("policy");
expect(parts[1]).toBe("test");
expect(parts[2]).toBe("v1");
expect(parts[3]).toBe(entity.contentHash);
});
it("auto-generates version as timestamp", () => {
const entity = registry.registerPolicy({
name: "test",
preset: "safety",
});
const parts = entity.entityId.split(":");
const version = parts[2];
expect(version).toHaveLength(14);
expect(version).toMatch(/^\d+$/);
});
});
describe("serialization", () => {
it("toJSON returns entity data", () => {
const entity = registry.registerPolicy({
name: "test",
preset: "safety",
});
const json = entity.toJSON();
expect(json.entityId).toBe(entity.entityId);
expect(json.name).toBe("test");
expect(json.contentHash).toBe(entity.contentHash);
expect(json.config).toBeDefined();
});
});
});
describe("PolicyEntity evaluation", () => {
let registry: PolicyRegistry;
beforeEach(() => {
registry = new PolicyRegistry();
});
it("allows by default with permissive preset", () => {
const entity = registry.registerPolicy({
name: "permissive",
preset: "permissive",
});
const result = entity.evaluate("Read", "file_read", "/tmp/test.txt");
expect(result.decision).toBe("allow");
expect(result.matchedRule).toBeUndefined();
expect(result.reason).toContain("Default policy");
});
it("denies destructive commands with safety preset", () => {
const entity = registry.registerPolicy({
name: "safety",
preset: "safety",
});
const result = entity.evaluate("Bash", "command", "rm -rf /");
expect(result.decision).toBe("deny");
expect(result.matchedRule?.id).toBe("deny-destructive-commands");
expect(result.enforced).toBe(true);
});
it("denies secret file reads with safety preset", () => {
const entity = registry.registerPolicy({
name: "safety",
preset: "safety",
});
const result = entity.evaluate("Read", "file_read", "/app/.env");
expect(result.decision).toBe("deny");
expect(result.matchedRule?.id).toBe("deny-secret-files");
});
it("warns on network with safety preset", () => {
const entity = registry.registerPolicy({
name: "safety",
preset: "safety",
});
const result = entity.evaluate("WebFetch", "network", "https://example.com");
expect(result.decision).toBe("warn");
expect(result.matchedRule?.id).toBe("warn-network");
});
it("denies by default with strict preset", () => {
const entity = registry.registerPolicy({
name: "strict",
preset: "strict",
});
const result = entity.evaluate("Bash", "command", "ls");
expect(result.decision).toBe("deny");
expect(result.matchedRule).toBeUndefined();
expect(result.reason).toContain("Default policy");
});
it("allows read tools with strict preset", () => {
const entity = registry.registerPolicy({
name: "strict",
preset: "strict",
});
for (const tool of ["Read", "Glob", "Grep", "TodoWrite"]) {
const result = entity.evaluate(tool, "file_read", "/tmp/test.txt");
expect(result.decision).toBe("allow");
expect(result.matchedRule?.id).toBe("allow-read-tools");
}
});
it("includes constraints in evaluation", () => {
const entity = registry.registerPolicy({
name: "safety",
preset: "safety",
});
const result = entity.evaluate("Bash", "command", "rm -rf /");
expect(result.constraints).toContain(`policy:${entity.entityId}`);
expect(result.constraints).toContain("decision:deny");
expect(result.constraints).toContain("rule:deny-destructive-commands");
});
});
describe("PolicyEntity with rate limiting", () => {
let registry: PolicyRegistry;
beforeEach(() => {
registry = new PolicyRegistry();
});
it("allows under rate limit threshold", () => {
const config: PolicyConfig = {
defaultPolicy: "allow",
enforce: true,
rules: [
{
id: "rate-bash",
name: "Rate limit Bash",
priority: 1,
decision: "deny",
match: {
tools: ["Bash"],
rateLimit: { maxCount: 5, windowMs: 60000 },
},
},
],
};
const entity = registry.registerPolicy({ name: "rate-test", config });
const limiter = new RateLimiter();
// Under limit - rule doesn't fire, falls through to default
const result = entity.evaluate("Bash", "command", "ls", limiter);
expect(result.decision).toBe("allow");
expect(result.matchedRule).toBeUndefined();
});
it("denies when over rate limit threshold", () => {
const config: PolicyConfig = {
defaultPolicy: "allow",
enforce: true,
rules: [
{
id: "rate-bash",
name: "Rate limit Bash",
priority: 1,
decision: "deny",
match: {
tools: ["Bash"],
rateLimit: { maxCount: 2, windowMs: 60000 },
},
},
],
};
const entity = registry.registerPolicy({ name: "rate-test", config });
const limiter = new RateLimiter();
// Record actions to exceed limit
const key = "ratelimit:rate-bash:tool:Bash";
limiter.record(key);
limiter.record(key);
// Now at limit - rule fires
const result = entity.evaluate("Bash", "command", "ls", limiter);
expect(result.decision).toBe("deny");
expect(result.matchedRule?.id).toBe("rate-bash");
});
});
describe("PolicyRegistry", () => {
let registry: PolicyRegistry;
beforeEach(() => {
registry = new PolicyRegistry();
});
describe("listing policies", () => {
it("lists all registered policies", () => {
registry.registerPolicy({ name: "safety", preset: "safety" });
registry.registerPolicy({ name: "strict", preset: "strict" });
const policies = registry.listPolicies();
expect(policies).toHaveLength(2);
const names = policies.map((p) => p.name);
expect(names).toContain("safety");
expect(names).toContain("strict");
});
});
describe("retrieval", () => {
it("gets policy by entity ID", () => {
const entity = registry.registerPolicy({
name: "test",
preset: "safety",
});
const found = registry.getPolicy(entity.entityId);
expect(found).toBe(entity);
});
it("returns undefined for unknown entity ID", () => {
const found = registry.getPolicy("policy:unknown:v1:abc123" as PolicyEntityId);
expect(found).toBeUndefined();
});
it("gets policy by content hash", () => {
const entity = registry.registerPolicy({
name: "test",
preset: "safety",
});
const found = registry.getPolicyByHash(entity.contentHash);
expect(found).toBe(entity);
});
});
});
describe("PolicyRegistry witnessing", () => {
let registry: PolicyRegistry;
beforeEach(() => {
registry = new PolicyRegistry();
});
it("records session witnessing policy", () => {
const entity = registry.registerPolicy({
name: "safety",
preset: "safety",
});
registry.witnessSession(entity.entityId, "session-123");
const witnessedBy = registry.getWitnessedBy(entity.entityId);
expect(witnessedBy).toContain("session:session-123");
});
it("records policy witnessing decision", () => {
const entity = registry.registerPolicy({
name: "safety",
preset: "safety",
});
registry.witnessDecision(
entity.entityId,
"session-123",
"Read",
"allow",
true
);
const hasWitnessed = registry.getHasWitnessed(entity.entityId);
expect(hasWitnessed).toContain("session:session-123");
});
});
describe("computePolicyHash", () => {
it("computes consistent hash for same config", () => {
const config: PolicyConfig = {
defaultPolicy: "allow",
enforce: true,
rules: [],
};
const hash1 = computePolicyHash(config);
const hash2 = computePolicyHash(config);
expect(hash1).toBe(hash2);
expect(hash1).toHaveLength(16);
});
it("computes different hash for different configs", () => {
const config1: PolicyConfig = {
defaultPolicy: "allow",
enforce: true,
rules: [],
};
const config2: PolicyConfig = {
defaultPolicy: "deny",
enforce: true,
rules: [],
};
const hash1 = computePolicyHash(config1);
const hash2 = computePolicyHash(config2);
expect(hash1).not.toBe(hash2);
});
});

View File

@ -0,0 +1,369 @@
/**
* Policy Entity - Policy as a first-class participant in the trust network.
*
* Policy isn't just configuration - it's society's law. It has identity,
* can be witnessed, and is hash-tracked in the audit chain.
*
* Key concepts:
* - Policy is immutable once registered (changing = new entity)
* - Sessions witness operating under a policy
* - Policy witnesses agent decisions (allow/deny)
* - R6 records reference the policyHash in effect
*/
import { createHash } from "crypto";
import type {
PolicyConfig,
PolicyRule,
PolicyMatch,
PolicyDecision,
PolicyEvaluation,
} from "./policy-types.js";
import { resolvePreset } from "./presets.js";
import type { RateLimiter } from "./rate-limiter.js";
export type PolicyEntityId = `policy:${string}:${string}:${string}`;
export type PolicyEntityData = {
entityId: PolicyEntityId;
name: string;
version: string;
contentHash: string;
createdAt: string;
source: "preset" | "custom";
config: PolicyConfig;
};
/**
* A policy as a first-class entity in the trust network.
*
* Properties:
* - entityId: Unique identifier (policy:<name>:<version>:<hash>)
* - contentHash: SHA-256 of the policy document (first 16 chars)
* - config: The actual policy configuration
*/
export class PolicyEntity {
readonly entityId: PolicyEntityId;
readonly name: string;
readonly version: string;
readonly contentHash: string;
readonly createdAt: string;
readonly source: "preset" | "custom";
readonly config: PolicyConfig;
/** Rules sorted by priority (ascending) for evaluation */
private sortedRules: PolicyRule[];
constructor(data: PolicyEntityData) {
this.entityId = data.entityId;
this.name = data.name;
this.version = data.version;
this.contentHash = data.contentHash;
this.createdAt = data.createdAt;
this.source = data.source;
this.config = data.config;
// Sort rules by priority (lower = evaluated first)
this.sortedRules = [...data.config.rules].sort(
(a, b) => a.priority - b.priority
);
}
/**
* Evaluate a tool call against this policy.
*/
evaluate(
toolName: string,
category: string,
target?: string,
rateLimiter?: RateLimiter
): PolicyEvaluation {
for (const rule of this.sortedRules) {
if (this.matchesRule(toolName, category, target, rule.match)) {
// Check rate limit if specified
if (rule.match.rateLimit && rateLimiter) {
const key = this.rateLimitKey(rule, toolName, category);
const result = rateLimiter.check(
key,
rule.match.rateLimit.maxCount,
rule.match.rateLimit.windowMs
);
if (result.allowed) {
continue; // Under limit, rule doesn't fire
}
}
const enforced = rule.decision !== "deny" || this.config.enforce;
return {
decision: rule.decision,
matchedRule: rule,
enforced,
reason: rule.reason ?? `Matched rule: ${rule.name}`,
constraints: [
`policy:${this.entityId}`,
`decision:${rule.decision}`,
`rule:${rule.id}`,
],
};
}
}
// No rule matched - default policy
return {
decision: this.config.defaultPolicy,
matchedRule: undefined,
enforced: true,
reason: `Default policy: ${this.config.defaultPolicy}`,
constraints: [
`policy:${this.entityId}`,
`decision:${this.config.defaultPolicy}`,
"rule:default",
],
};
}
private matchesRule(
toolName: string,
category: string,
target: string | undefined,
match: PolicyMatch
): boolean {
// Tool match
if (match.tools && !match.tools.includes(toolName)) {
return false;
}
// Category match
if (match.categories && !match.categories.includes(category as any)) {
return false;
}
// Target pattern match
if (match.targetPatterns) {
if (!target) {
return false;
}
let matched = false;
for (const pattern of match.targetPatterns) {
if (match.targetPatternsAreRegex) {
if (new RegExp(pattern).test(target)) {
matched = true;
break;
}
} else {
// Simple glob matching (convert * to .*)
const regexPattern = pattern
.replace(/\*\*/g, ".*")
.replace(/\*/g, "[^/]*")
.replace(/\?/g, ".");
if (new RegExp(`^${regexPattern}$`).test(target)) {
matched = true;
break;
}
}
}
if (!matched) {
return false;
}
}
return true;
}
private rateLimitKey(
rule: PolicyRule,
toolName: string,
category: string
): string {
if (rule.match.tools) {
return `ratelimit:${rule.id}:tool:${toolName}`;
}
if (rule.match.categories) {
return `ratelimit:${rule.id}:category:${category}`;
}
return `ratelimit:${rule.id}:global`;
}
toJSON(): PolicyEntityData {
return {
entityId: this.entityId,
name: this.name,
version: this.version,
contentHash: this.contentHash,
createdAt: this.createdAt,
source: this.source,
config: this.config,
};
}
}
/**
* Registry of policy entities with hash-tracking.
*
* Policies are registered once and become immutable. Changing a policy
* creates a new entity with a new hash.
*/
export class PolicyRegistry {
/** In-memory cache of loaded policies */
private cache = new Map<PolicyEntityId, PolicyEntity>();
/** Witnessing records: entity -> set of witnesses */
private witnessedBy = new Map<string, Set<string>>();
/** Witnessing records: entity -> set of entities witnessed */
private hasWitnessed = new Map<string, Set<string>>();
/**
* Register a policy and create its entity.
*/
registerPolicy(options: {
name: string;
config?: PolicyConfig;
preset?: string;
version?: string;
}): PolicyEntity {
const { name, config: providedConfig, preset, version } = options;
if (!providedConfig && !preset) {
throw new Error("Must provide either config or preset");
}
if (providedConfig && preset) {
throw new Error("Cannot provide both config and preset");
}
// Resolve config
let config: PolicyConfig;
let source: "preset" | "custom";
if (preset) {
config = resolvePreset(preset);
source = "preset";
} else {
config = providedConfig!;
source = "custom";
}
// Generate version if not provided
const versionStr = version ?? new Date().toISOString().replace(/[-:T.]/g, "").slice(0, 14);
// Compute content hash
const contentStr = JSON.stringify(config, Object.keys(config).sort());
const contentHash = createHash("sha256")
.update(contentStr)
.digest("hex")
.slice(0, 16);
// Build entity ID
const entityId: PolicyEntityId = `policy:${name}:${versionStr}:${contentHash}`;
// Check cache
if (this.cache.has(entityId)) {
return this.cache.get(entityId)!;
}
// Create entity
const entity = new PolicyEntity({
entityId,
name,
version: versionStr,
contentHash,
createdAt: new Date().toISOString(),
source,
config,
});
// Cache
this.cache.set(entityId, entity);
return entity;
}
/**
* Get a policy by entity ID.
*/
getPolicy(entityId: PolicyEntityId): PolicyEntity | undefined {
return this.cache.get(entityId);
}
/**
* Get a policy by content hash.
*/
getPolicyByHash(contentHash: string): PolicyEntity | undefined {
for (const entity of this.cache.values()) {
if (entity.contentHash === contentHash) {
return entity;
}
}
return undefined;
}
/**
* List all registered policies.
*/
listPolicies(): PolicyEntity[] {
return [...this.cache.values()];
}
/**
* Record that a session is operating under this policy.
*
* Creates bidirectional witnessing:
* - Session witnesses the policy (I operate under these rules)
* - Policy witnesses the session (this session uses me)
*/
witnessSession(policyEntityId: PolicyEntityId, sessionId: string): void {
const sessionEntity = `session:${sessionId}`;
// Policy is witnessed by session
if (!this.witnessedBy.has(policyEntityId)) {
this.witnessedBy.set(policyEntityId, new Set());
}
this.witnessedBy.get(policyEntityId)!.add(sessionEntity);
// Session has witnessed policy
if (!this.hasWitnessed.has(sessionEntity)) {
this.hasWitnessed.set(sessionEntity, new Set());
}
this.hasWitnessed.get(sessionEntity)!.add(policyEntityId);
}
/**
* Record a policy decision in the witnessing chain.
*/
witnessDecision(
policyEntityId: PolicyEntityId,
sessionId: string,
_toolName: string,
_decision: PolicyDecision,
_success: boolean
): void {
const sessionEntity = `session:${sessionId}`;
// Policy has witnessed the session's action
if (!this.hasWitnessed.has(policyEntityId)) {
this.hasWitnessed.set(policyEntityId, new Set());
}
this.hasWitnessed.get(policyEntityId)!.add(sessionEntity);
}
/**
* Get entities that have witnessed a policy.
*/
getWitnessedBy(entityId: string): string[] {
return [...(this.witnessedBy.get(entityId) ?? [])];
}
/**
* Get entities that a policy has witnessed.
*/
getHasWitnessed(entityId: string): string[] {
return [...(this.hasWitnessed.get(entityId) ?? [])];
}
}
/**
* Compute a policy content hash.
*/
export function computePolicyHash(config: PolicyConfig): string {
const contentStr = JSON.stringify(config, Object.keys(config).sort());
return createHash("sha256").update(contentStr).digest("hex").slice(0, 16);
}

View File

@ -23,6 +23,8 @@ export type R6Request = {
export type R6Rules = {
auditLevel: string;
constraints: string[];
/** Policy entity ID (policy as first-class trust participant) */
policyEntityId?: string;
};
export type R6Role = {
@ -114,6 +116,7 @@ export function createR6Request(
actionIndex: number,
prevR6Id: string | undefined,
auditLevel: string,
policyEntityId?: string,
): R6Request {
return {
id: `r6:${randomUUID().slice(0, 8)}`,
@ -121,6 +124,7 @@ export function createR6Request(
rules: {
auditLevel,
constraints: [],
policyEntityId,
},
role: {
sessionId,

View File

@ -14,6 +14,8 @@ export type SessionState = {
startedAt: string;
toolCounts: Record<string, number>;
categoryCounts: Record<string, number>;
/** Policy entity ID (policy:<name>:<version>:<hash>) */
policyEntityId?: string;
};
export class SessionStore {