feat: add governance evaluator and decision runner skeleton

This commit is contained in:
Eve
2026-05-07 23:38:46 +08:00
parent 7fd02348cf
commit c2a775b62c
8 changed files with 761 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
function safeArray(value) {
return Array.isArray(value) ? value : [];
}
function enforcementSupport(capabilityDescriptor = {}, actionName) {
return capabilityDescriptor?.capabilities?.enforcement?.[actionName] ?? { supported: false, level: 'none' };
}
function notificationTruthModel(capabilityDescriptor = {}) {
return capabilityDescriptor?.capabilities?.notification_path?.truth_model ?? {};
}
function isTruthySupportLevel(level) {
return level === 'partial' || level === 'full';
}
function createReceipt({ decision, actionPlans, blockedActions, delivery_state, notes = [] }) {
return {
policy_id: decision.policy_id,
decision: decision.decision,
status: blockedActions.length > 0 ? 'degraded' : 'planned',
delivery_state,
enforcement_intent: actionPlans,
blocked_actions: blockedActions,
operator_notice_required: Boolean(decision.operator_notice?.required),
notes
};
}
function mapActionToCapability(action) {
switch (action) {
case 'rewrite_message':
return 'rewrite_message';
case 'record_placeholder':
return 'annotate_placeholder';
case 'set_status':
return 'downgrade_status';
case 'request_review':
return 'request_review';
case 'raise_escalation':
return 'escalate';
case 'block_transition':
return 'block_transition';
case 'notify_operator':
case 'dispatch_message':
return 'force_checkpoint';
default:
return null;
}
}
export function planDecisionExecution({ decision, capabilityDescriptor = {} }) {
if (!decision) {
throw new Error('decision is required');
}
const truthModel = notificationTruthModel(capabilityDescriptor);
const deliveryStates = safeArray(truthModel.delivery_states);
const actionPlans = [];
const blockedActions = [];
const notes = [];
for (const requiredAction of safeArray(decision.required_actions)) {
const capabilityName = mapActionToCapability(requiredAction.action);
const capability = capabilityName ? enforcementSupport(capabilityDescriptor, capabilityName) : { supported: true, level: 'full' };
const supported = capabilityName ? capability.supported && isTruthySupportLevel(capability.level) : true;
if (supported) {
actionPlans.push({
...requiredAction,
execution_mode: requiredAction.action === 'notify_operator' || requiredAction.action === 'dispatch_message'
? 'runtime_adapter_dispatch'
: 'package_core_signal',
capability: capabilityName,
support_level: capability.level ?? 'full'
});
continue;
}
blockedActions.push({
...requiredAction,
capability: capabilityName,
support_level: capability.level ?? 'none',
reason: 'runtime capability descriptor does not support this enforcement path'
});
}
let delivery_state = 'prepared';
if ((decision.decision === 'force_checkpoint' || decision.decision === 'annotate_placeholder') && deliveryStates.includes('pending_external_send')) {
delivery_state = 'pending_external_send';
notes.push('Operator-visible send remains a runtime-adapter responsibility until final sender delivery is acknowledged.');
} else if (decision.decision === 'block' && deliveryStates.includes('blocked')) {
delivery_state = 'blocked';
}
if (blockedActions.some((action) => action.mandatory)) {
notes.push('One or more mandatory actions could not be planned truthfully from the advertised runtime capabilities.');
}
return {
decision,
enforcement_intent: {
policy_id: decision.policy_id,
decision: decision.decision,
planned_actions: actionPlans,
blocked_actions: blockedActions,
runtime_adapter_required: actionPlans.filter((action) => action.execution_mode === 'runtime_adapter_dispatch').map((action) => action.action),
package_core_actions: actionPlans.filter((action) => action.execution_mode === 'package_core_signal').map((action) => action.action)
},
receipt: createReceipt({ decision, actionPlans, blockedActions, delivery_state, notes })
};
}

View File

@@ -0,0 +1,2 @@
export { evaluatePolicyPack, evaluatePolicies } from './policy-evaluator.mjs';
export { planDecisionExecution } from './decision-runner.mjs';

View File

@@ -0,0 +1,251 @@
const DECISION_PRECEDENCE = {
allow: 0,
annotate_placeholder: 1,
rewrite: 2,
require_review: 3,
downgrade_status: 4,
force_checkpoint: 5,
block: 6,
escalate: 7
};
const EVENT_TYPE_TO_CLAIM_TYPE = {
task_claimed_complete: 'completion',
task_verified_complete: 'verified_completion',
task_checkpoint_sent: 'progress',
forced_operator_update: 'progress'
};
const QUALITY_RANK = {
none: 0,
weak: 1,
moderate: 2,
strong: 3
};
function safeArray(value) {
return Array.isArray(value) ? value : [];
}
function getByPath(source, dottedPath) {
if (!source || !dottedPath) return undefined;
return dottedPath.split('.').reduce((acc, key) => (acc == null ? undefined : acc[key]), source);
}
function compareFact(actual, condition) {
if (Object.hasOwn(condition, 'equals')) return actual === condition.equals;
if (Object.hasOwn(condition, 'not_equals')) return actual !== condition.not_equals;
if (Object.hasOwn(condition, 'greater_than')) return Number(actual) > Number(condition.greater_than);
if (Object.hasOwn(condition, 'less_than')) return Number(actual) < Number(condition.less_than);
if (Object.hasOwn(condition, 'contains')) return Array.isArray(actual) ? actual.includes(condition.contains) : String(actual ?? '').includes(String(condition.contains));
if (Object.hasOwn(condition, 'in')) return safeArray(condition.in).includes(actual);
return Boolean(actual);
}
function matchesConditions(conditions = {}, facts = {}) {
const groups = Object.keys(conditions);
if (groups.length === 0) return true;
if (Array.isArray(conditions.all)) {
return conditions.all.every((entry) => evaluateConditionEntry(entry, facts));
}
if (Array.isArray(conditions.any)) {
return conditions.any.some((entry) => evaluateConditionEntry(entry, facts));
}
if (conditions.not) {
return !evaluateConditionEntry(conditions.not, facts);
}
return true;
}
function evaluateConditionEntry(entry, facts) {
if (!entry || typeof entry !== 'object') return false;
if (entry.all || entry.any || entry.not) {
return matchesConditions(entry, facts);
}
const actual = getByPath(facts, entry.fact);
return compareFact(actual, entry);
}
function matchesTriggers(rule = {}, event = {}, facts = {}) {
const triggers = rule.triggers ?? {};
const eventTypes = safeArray(triggers.event_types);
const derivedSignals = safeArray(triggers.derived_signals);
const claimTypes = safeArray(triggers.claim_types);
if (eventTypes.length > 0 && !eventTypes.includes(event.type)) return false;
if (derivedSignals.length > 0 && !derivedSignals.every((signal) => facts.signals?.includes?.(signal))) return false;
const claimType = facts.claim?.type;
if (claimTypes.length > 0 && !claimTypes.includes(claimType)) return false;
return true;
}
function findHighestEvidenceQuality(evidence = []) {
return safeArray(evidence).reduce((best, item) => {
const quality = item?.quality ?? 'none';
return QUALITY_RANK[quality] > QUALITY_RANK[best] ? quality : best;
}, 'none');
}
function countNewEvidence(evidence = []) {
return safeArray(evidence).filter((item) => item?.is_new !== false).length;
}
function buildFacts({ event, evidence, capabilityDescriptor, context }) {
const eventPayload = event?.payload ?? {};
const evidenceItems = safeArray(evidence);
const topQuality = findHighestEvidenceQuality(evidenceItems);
const newEvidenceCount = countNewEvidence(evidenceItems);
const promisedFollowupDue = Boolean(eventPayload.promised_followup_due ?? context?.promised_followup_due);
const hasRequiredCheckpointFields = Boolean(context?.message?.has_required_checkpoint_fields);
const resultForwarded = Boolean(eventPayload.result_forwarded ?? context?.forwarding?.result_forwarded);
const resultAvailable = Boolean(eventPayload.result_available ?? context?.forwarding?.result_available);
const isSilentTask = Boolean(eventPayload.silent_task ?? context?.task?.silent_task);
const hasReportAnchor = Boolean(context?.operator_context?.report_anchor_present ?? eventPayload.report_anchor_present);
const externalizedPathValid = Boolean(context?.checkpoint?.externalized_path_valid ?? eventPayload.externalized_path_valid);
const completionMinQuality = QUALITY_RANK[topQuality] >= QUALITY_RANK.moderate;
const verifiedCompletionMinQuality = QUALITY_RANK[topQuality] >= QUALITY_RANK.strong;
const forcedSignals = safeArray(context?.signals);
const supports = capabilityDescriptor?.capabilities?.enforcement ?? {};
return {
checkpoint: {
is_overdue: Boolean(eventPayload.checkpoint_overdue ?? context?.checkpoint?.is_overdue),
externalized_path_valid: externalizedPathValid
},
forwarding: {
result_available_without_visible_followup: resultAvailable && !resultForwarded,
result_available: resultAvailable,
result_forwarded: resultForwarded
},
evidence: {
new_items_since_last_checkpoint: newEvidenceCount,
completion_min_quality: completionMinQuality,
verified_completion_min_quality: verifiedCompletionMinQuality,
top_quality: topQuality
},
claim: {
type: eventPayload.claim_type ?? EVENT_TYPE_TO_CLAIM_TYPE[event?.type] ?? 'generic',
promised_followup_due: promisedFollowupDue,
next_step_has_supporting_evidence: newEvidenceCount > 0
},
message: {
has_required_checkpoint_fields: hasRequiredCheckpointFields
},
operator_context: {
report_anchor: {
present: hasReportAnchor
}
},
task: {
silent_task: isSilentTask
},
runtime: {
enforcement: supports
},
signals: forcedSignals
};
}
function createAllowDecision(defaultPolicyId = 'policy.allow') {
return {
decision: 'allow',
policy_id: defaultPolicyId,
severity: 'info',
reason: 'no policy rule matched the evaluated event and evidence',
rewritten_message: null,
suggested_status: null,
required_actions: [],
operator_notice: null
};
}
function normalizeDecision({ rule, pack, facts }) {
const output = rule.decision_output ?? {};
const operatorMessageTemplates = rule.operator_message_templates ?? {};
const message = output.rewritten_message
?? operatorMessageTemplates.blocked
?? operatorMessageTemplates.checkpoint_forced
?? operatorMessageTemplates.placeholder_rewrite
?? operatorMessageTemplates.review_required
?? operatorMessageTemplates.status_downgraded
?? null;
return {
decision: output.decision ?? 'allow',
policy_id: rule.id ?? pack?.metadata?.id ?? 'unknown-policy',
severity: output.severity ?? pack?.metadata?.severity_default ?? 'medium',
reason: output.reason ?? rule.intent ?? `${rule.title ?? 'policy rule'} matched`,
rewritten_message: message,
suggested_status: output.suggested_status ?? null,
required_actions: safeArray(output.required_actions),
operator_notice: output.operator_notice ?? null,
matched_rule: {
id: rule.id ?? null,
title: rule.title ?? null,
pack_id: pack?.metadata?.id ?? null
},
facts_snapshot: facts
};
}
export function evaluatePolicyPack({ event, evidence = [], capabilityDescriptor = {}, policyPack, context = {} }) {
if (!policyPack?.spec?.rules) {
throw new Error('policyPack.spec.rules is required');
}
const facts = buildFacts({ event, evidence, capabilityDescriptor, context });
const matches = [];
for (const rule of policyPack.spec.rules) {
if (!matchesTriggers(rule, event, facts)) continue;
if (!matchesConditions(rule.conditions, facts)) continue;
matches.push(normalizeDecision({ rule, pack: policyPack, facts }));
if (policyPack.spec.evaluation_mode === 'first_match') break;
}
const winningDecision = matches.sort((left, right) => (DECISION_PRECEDENCE[right.decision] ?? -1) - (DECISION_PRECEDENCE[left.decision] ?? -1))[0];
return {
facts,
matches,
decision: winningDecision ?? createAllowDecision(policyPack?.metadata?.id)
};
}
export function evaluatePolicies({ event, evidence = [], capabilityDescriptor = {}, policyPacks = [], context = {} }) {
const evaluations = safeArray(policyPacks).map((policyPack) => evaluatePolicyPack({
event,
evidence,
capabilityDescriptor,
policyPack,
context
}));
const matchedDecisions = evaluations
.map((evaluation) => evaluation.decision)
.filter((decision) => decision.decision !== 'allow');
const finalDecision = matchedDecisions.sort((left, right) => (DECISION_PRECEDENCE[right.decision] ?? -1) - (DECISION_PRECEDENCE[left.decision] ?? -1))[0]
?? createAllowDecision('policy.allow');
return {
evaluations,
decision: finalDecision
};
}
export const __testables = {
DECISION_PRECEDENCE,
buildFacts,
matchesConditions,
matchesTriggers,
findHighestEvidenceQuality,
countNewEvidence
};

View File

@@ -0,0 +1,32 @@
export const packageName = '@openclaw/plugin-reporting-governance';
export const packageVersion = '0.1.0-mainline';
export const packageBoundaries = {
core: [
'event normalization',
'evidence building',
'policy evaluation',
'decision running',
'capability/profile compatibility'
],
adapters: [
'watchdog adapter',
'dispatcher adapter',
'bridge adapter',
'sender-binding adapter',
'orchestrator adapter'
],
storage: [
'event store',
'evidence store',
'queue store',
'spool store',
'receipt store',
'decision store'
],
reference: [
'openclaw watchdog chain'
]
};
export { evaluatePolicyPack, evaluatePolicies, planDecisionExecution } from './core/index.mjs';