feat: add reporting governance preflight contract
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
const CANONICAL_SCHEMA_PATHS = Object.freeze({
|
||||
event_schema: 'schemas/reporting-governance/event-envelope.schema.json',
|
||||
evidence_schema: 'schemas/reporting-governance/evidence.schema.json',
|
||||
decision_schema: 'schemas/reporting-governance/decision.schema.json',
|
||||
capabilities_schema: 'schemas/reporting-governance/capability-descriptor.schema.json'
|
||||
});
|
||||
|
||||
const EXPECTATION_CAPABILITY_PATHS = Object.freeze({
|
||||
emit_canonical_events: ['capabilities.normalization.canonical_events'],
|
||||
evaluate_watchdog_overdue: ['capabilities.watchdog.watchdog_evaluation'],
|
||||
create_queue_items: ['capabilities.notification_path.queue_items'],
|
||||
create_spool_handoff: ['capabilities.notification_path.spool_handoff'],
|
||||
write_bridge_receipts: ['capabilities.notification_path.receipts'],
|
||||
direct_sender_binding: ['capabilities.notification_path.sender_binding'],
|
||||
final_delivery_ack: ['capabilities.notification_path.final_delivery_proof'],
|
||||
inline_dispatch_blocking: ['capabilities.enforcement.block_transition']
|
||||
});
|
||||
|
||||
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 isSupported(node) {
|
||||
return Boolean(node?.supported) && (node?.level === 'partial' || node?.level === 'full');
|
||||
}
|
||||
|
||||
function assessCapabilityExpectation(capabilityDescriptor, expectation) {
|
||||
const candidatePaths = EXPECTATION_CAPABILITY_PATHS[expectation] ?? [];
|
||||
const matchedPath = candidatePaths.find((candidatePath) => isSupported(getByPath(capabilityDescriptor, candidatePath)));
|
||||
return {
|
||||
expectation,
|
||||
supported: Boolean(matchedPath),
|
||||
matched_path: matchedPath ?? candidatePaths[0] ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function collectExpectedActions(profile = {}) {
|
||||
const actions = [];
|
||||
const overdueAction = profile?.spec?.policies?.overrides?.checkpoints?.overdueAction;
|
||||
if (overdueAction) {
|
||||
actions.push(overdueAction);
|
||||
}
|
||||
if (profile?.spec?.notifications?.operatorVisibleRecoveryRequired) {
|
||||
actions.push('notify_operator');
|
||||
}
|
||||
return [...new Set(actions)];
|
||||
}
|
||||
|
||||
function evaluateActionSupport(capabilityDescriptor = {}, action) {
|
||||
switch (action) {
|
||||
case 'force_checkpoint':
|
||||
return {
|
||||
action,
|
||||
supported: isSupported(getByPath(capabilityDescriptor, 'capabilities.enforcement.force_checkpoint')),
|
||||
required_paths: ['capabilities.enforcement.force_checkpoint'],
|
||||
mode: 'fail_closed'
|
||||
};
|
||||
case 'notify_operator': {
|
||||
const queueSupported = isSupported(getByPath(capabilityDescriptor, 'capabilities.notification_path.queue_items'));
|
||||
const senderSupported = isSupported(getByPath(capabilityDescriptor, 'capabilities.notification_path.sender_binding'))
|
||||
|| isSupported(getByPath(capabilityDescriptor, 'capabilities.notification_path.direct_send'));
|
||||
return {
|
||||
action,
|
||||
supported: queueSupported,
|
||||
degraded: queueSupported && !senderSupported,
|
||||
required_paths: ['capabilities.notification_path.queue_items'],
|
||||
advisory_paths: ['capabilities.notification_path.sender_binding', 'capabilities.notification_path.direct_send'],
|
||||
mode: queueSupported && !senderSupported ? 'honest_degrade' : 'pass'
|
||||
};
|
||||
}
|
||||
case 'dispatch_message': {
|
||||
const directSendSupported = isSupported(getByPath(capabilityDescriptor, 'capabilities.notification_path.direct_send'));
|
||||
const senderSupported = isSupported(getByPath(capabilityDescriptor, 'capabilities.notification_path.sender_binding'));
|
||||
return {
|
||||
action,
|
||||
supported: directSendSupported || senderSupported,
|
||||
required_paths: ['capabilities.notification_path.direct_send', 'capabilities.notification_path.sender_binding'],
|
||||
mode: directSendSupported || senderSupported ? 'pass' : 'fail_closed'
|
||||
};
|
||||
}
|
||||
default:
|
||||
return {
|
||||
action,
|
||||
supported: true,
|
||||
required_paths: [],
|
||||
mode: 'pass'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function runCompatibilityPreflight({ capabilityDescriptor = {}, profile = {}, packageVersion } = {}) {
|
||||
const runtimeId = capabilityDescriptor?.metadata?.id ?? capabilityDescriptor?.runtime?.name ?? 'unknown-runtime';
|
||||
const requestedPluginVersion = profile?.spec?.package?.pluginVersion ?? packageVersion ?? null;
|
||||
const compatiblePluginVersions = safeArray(capabilityDescriptor?.compatibility?.plugin_spec_versions);
|
||||
|
||||
const errors = [];
|
||||
const warnings = [];
|
||||
const notes = [];
|
||||
|
||||
const schemaChecks = Object.entries(CANONICAL_SCHEMA_PATHS).map(([key, expectedPath]) => {
|
||||
const actualPath = capabilityDescriptor?.compatibility?.[key] ?? null;
|
||||
const ok = actualPath === expectedPath;
|
||||
if (!ok) {
|
||||
errors.push(`schema mismatch: ${key} expected ${expectedPath} but got ${actualPath ?? 'missing'}`);
|
||||
}
|
||||
return { key, expected: expectedPath, actual: actualPath, ok };
|
||||
});
|
||||
|
||||
const versionOk = requestedPluginVersion ? compatiblePluginVersions.includes(requestedPluginVersion) : false;
|
||||
if (!versionOk) {
|
||||
errors.push(`plugin version ${requestedPluginVersion ?? 'missing'} is not declared compatible by runtime ${runtimeId}`);
|
||||
}
|
||||
|
||||
const requiredExpectations = safeArray(profile?.capability_expectations?.required).map((expectation) =>
|
||||
assessCapabilityExpectation(capabilityDescriptor, expectation)
|
||||
);
|
||||
const preferredExpectations = safeArray(profile?.capability_expectations?.preferred).map((expectation) =>
|
||||
assessCapabilityExpectation(capabilityDescriptor, expectation)
|
||||
);
|
||||
|
||||
for (const result of requiredExpectations) {
|
||||
if (!result.supported) {
|
||||
errors.push(`required capability expectation not satisfied: ${result.expectation}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const result of preferredExpectations) {
|
||||
if (!result.supported) {
|
||||
warnings.push(`preferred capability expectation not satisfied: ${result.expectation}`);
|
||||
}
|
||||
}
|
||||
|
||||
const actionChecks = collectExpectedActions(profile).map((action) => evaluateActionSupport(capabilityDescriptor, action));
|
||||
for (const result of actionChecks) {
|
||||
if (!result.supported) {
|
||||
errors.push(`required action is not supportable by runtime: ${result.action}`);
|
||||
} else if (result.mode === 'honest_degrade') {
|
||||
warnings.push(`action ${result.action} only supports deferred truth path; treat final delivery as pending_external_send until acked`);
|
||||
notes.push(`degrade:${result.action}=pending_external_send`);
|
||||
}
|
||||
}
|
||||
|
||||
const status = errors.length > 0 ? 'fail_closed' : warnings.length > 0 ? 'degraded' : 'pass';
|
||||
|
||||
return {
|
||||
status,
|
||||
runtime: runtimeId,
|
||||
requested_profile: profile?.metadata?.id ?? null,
|
||||
requested_plugin_version: requestedPluginVersion,
|
||||
compatibility: {
|
||||
version_ok: versionOk,
|
||||
compatible_plugin_versions: compatiblePluginVersions,
|
||||
schema_checks: schemaChecks,
|
||||
required_expectations: requiredExpectations,
|
||||
preferred_expectations: preferredExpectations,
|
||||
action_checks: actionChecks
|
||||
},
|
||||
errors,
|
||||
warnings,
|
||||
notes
|
||||
};
|
||||
}
|
||||
|
||||
export const __testables = {
|
||||
CANONICAL_SCHEMA_PATHS,
|
||||
EXPECTATION_CAPABILITY_PATHS,
|
||||
collectExpectedActions,
|
||||
evaluateActionSupport,
|
||||
assessCapabilityExpectation
|
||||
};
|
||||
|
||||
export default runCompatibilityPreflight;
|
||||
@@ -42,13 +42,53 @@ function mapActionToCapability(action) {
|
||||
case 'block_transition':
|
||||
return 'block_transition';
|
||||
case 'notify_operator':
|
||||
return 'notify_operator';
|
||||
case 'dispatch_message':
|
||||
return 'force_checkpoint';
|
||||
return 'dispatch_message';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function actionSupport(capabilityDescriptor = {}, actionName) {
|
||||
switch (actionName) {
|
||||
case 'notify_operator': {
|
||||
const queue = capabilityDescriptor?.capabilities?.notification_path?.queue_items ?? { supported: false, level: 'none' };
|
||||
const sender = capabilityDescriptor?.capabilities?.notification_path?.sender_binding ?? { supported: false, level: 'none' };
|
||||
const directSend = capabilityDescriptor?.capabilities?.notification_path?.direct_send ?? { supported: false, level: 'none' };
|
||||
const queueSupported = queue.supported && isTruthySupportLevel(queue.level);
|
||||
const senderSupported = (sender.supported && isTruthySupportLevel(sender.level)) || (directSend.supported && isTruthySupportLevel(directSend.level));
|
||||
return {
|
||||
supported: queueSupported,
|
||||
level: queueSupported && senderSupported ? 'full' : queueSupported ? 'partial' : 'none',
|
||||
capability: 'notify_operator',
|
||||
mode: queueSupported && !senderSupported ? 'runtime_adapter_dispatch_deferred' : 'runtime_adapter_dispatch'
|
||||
};
|
||||
}
|
||||
case 'dispatch_message': {
|
||||
const directSend = capabilityDescriptor?.capabilities?.notification_path?.direct_send ?? { supported: false, level: 'none' };
|
||||
const sender = capabilityDescriptor?.capabilities?.notification_path?.sender_binding ?? { supported: false, level: 'none' };
|
||||
const supported = (directSend.supported && isTruthySupportLevel(directSend.level)) || (sender.supported && isTruthySupportLevel(sender.level));
|
||||
return {
|
||||
supported,
|
||||
level: supported ? (directSend.level === 'full' || sender.level === 'full' ? 'full' : 'partial') : 'none',
|
||||
capability: 'dispatch_message',
|
||||
mode: 'runtime_adapter_dispatch'
|
||||
};
|
||||
}
|
||||
default: {
|
||||
const capabilityName = mapActionToCapability(actionName);
|
||||
const capability = capabilityName ? enforcementSupport(capabilityDescriptor, capabilityName) : { supported: true, level: 'full' };
|
||||
return {
|
||||
supported: capabilityName ? capability.supported && isTruthySupportLevel(capability.level) : true,
|
||||
level: capability.level ?? 'full',
|
||||
capability: capabilityName,
|
||||
mode: 'package_core_signal'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function planDecisionExecution({ decision, capabilityDescriptor = {} }) {
|
||||
if (!decision) {
|
||||
throw new Error('decision is required');
|
||||
@@ -61,26 +101,22 @@ export function planDecisionExecution({ decision, capabilityDescriptor = {} }) {
|
||||
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;
|
||||
const support = actionSupport(capabilityDescriptor, requiredAction.action);
|
||||
|
||||
if (supported) {
|
||||
if (support.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'
|
||||
execution_mode: support.mode,
|
||||
capability: support.capability,
|
||||
support_level: support.level
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
blockedActions.push({
|
||||
...requiredAction,
|
||||
capability: capabilityName,
|
||||
support_level: capability.level ?? 'none',
|
||||
capability: support.capability,
|
||||
support_level: support.level ?? 'none',
|
||||
reason: 'runtime capability descriptor does not support this enforcement path'
|
||||
});
|
||||
}
|
||||
@@ -93,6 +129,10 @@ export function planDecisionExecution({ decision, capabilityDescriptor = {} }) {
|
||||
delivery_state = 'blocked';
|
||||
}
|
||||
|
||||
if (actionPlans.some((action) => action.execution_mode === 'runtime_adapter_dispatch_deferred')) {
|
||||
notes.push('Some operator notice paths are only queue/bridge capable; final delivery must remain pending_external_send until acked.');
|
||||
}
|
||||
|
||||
if (blockedActions.some((action) => action.mandatory)) {
|
||||
notes.push('One or more mandatory actions could not be planned truthfully from the advertised runtime capabilities.');
|
||||
}
|
||||
@@ -104,7 +144,9 @@ export function planDecisionExecution({ decision, capabilityDescriptor = {} }) {
|
||||
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),
|
||||
runtime_adapter_required: actionPlans
|
||||
.filter((action) => action.execution_mode === 'runtime_adapter_dispatch' || action.execution_mode === 'runtime_adapter_dispatch_deferred')
|
||||
.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 })
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { evaluatePolicyPack, evaluatePolicies } from './policy-evaluator.mjs';
|
||||
export { planDecisionExecution } from './decision-runner.mjs';
|
||||
export { executeGovernanceContract } from './execute-governance-contract.mjs';
|
||||
export { runCompatibilityPreflight } from './compatibility-preflight.mjs';
|
||||
|
||||
@@ -29,7 +29,13 @@ export const packageBoundaries = {
|
||||
]
|
||||
};
|
||||
|
||||
export { evaluatePolicyPack, evaluatePolicies, planDecisionExecution, executeGovernanceContract } from './core/index.mjs';
|
||||
export {
|
||||
evaluatePolicyPack,
|
||||
evaluatePolicies,
|
||||
planDecisionExecution,
|
||||
executeGovernanceContract,
|
||||
runCompatibilityPreflight,
|
||||
} from './core/index.mjs';
|
||||
export {
|
||||
createRuntimeBinding,
|
||||
runWatchdogAdapter,
|
||||
|
||||
Reference in New Issue
Block a user