diff --git a/plugins/reporting-governance/src/core/action-support.mjs b/plugins/reporting-governance/src/core/action-support.mjs new file mode 100644 index 0000000..681862d --- /dev/null +++ b/plugins/reporting-governance/src/core/action-support.mjs @@ -0,0 +1,121 @@ +function getByPath(source, dottedPath) { + if (!source || !dottedPath) return undefined; + return dottedPath.split('.').reduce((acc, key) => (acc == null ? undefined : acc[key]), source); +} + +function isTruthySupportLevel(level) { + return level === 'partial' || level === 'full'; +} + +function isSupported(node) { + return Boolean(node?.supported) && isTruthySupportLevel(node?.level); +} + +function enforcementSupport(capabilityDescriptor = {}, actionName) { + return capabilityDescriptor?.capabilities?.enforcement?.[actionName] ?? { supported: false, level: 'none' }; +} + +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': + return 'notify_operator'; + case 'dispatch_message': + return 'dispatch_message'; + default: + return null; + } +} + +export function evaluateActionSupport(capabilityDescriptor = {}, action) { + switch (action) { + case 'force_checkpoint': { + const capability = enforcementSupport(capabilityDescriptor, 'force_checkpoint'); + const supported = isSupported(capability); + return { + action, + supported, + level: capability.level ?? 'none', + capability: 'force_checkpoint', + required_paths: ['capabilities.enforcement.force_checkpoint'], + execution_mode: 'package_core_signal', + mode: supported ? 'pass' : 'fail_closed' + }; + } + 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 = isSupported(queue); + const senderSupported = isSupported(sender) || isSupported(directSend); + return { + action, + supported: queueSupported, + degraded: queueSupported && !senderSupported, + level: queueSupported && senderSupported ? 'full' : queueSupported ? 'partial' : 'none', + capability: 'notify_operator', + required_paths: ['capabilities.notification_path.queue_items'], + advisory_paths: ['capabilities.notification_path.sender_binding', 'capabilities.notification_path.direct_send'], + execution_mode: queueSupported && !senderSupported ? 'runtime_adapter_dispatch_deferred' : 'runtime_adapter_dispatch', + mode: queueSupported && !senderSupported ? 'honest_degrade' : queueSupported ? 'pass' : 'fail_closed' + }; + } + 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 = isSupported(directSend) || isSupported(sender); + return { + action, + supported, + level: supported ? (directSend.level === 'full' || sender.level === 'full' ? 'full' : 'partial') : 'none', + capability: 'dispatch_message', + required_paths: ['capabilities.notification_path.direct_send', 'capabilities.notification_path.sender_binding'], + execution_mode: 'runtime_adapter_dispatch', + mode: supported ? 'pass' : 'fail_closed' + }; + } + default: { + const capabilityName = mapActionToCapability(action); + const capability = capabilityName ? enforcementSupport(capabilityDescriptor, capabilityName) : { supported: true, level: 'full' }; + const supported = capabilityName ? isSupported(capability) : true; + return { + action, + supported, + level: capability.level ?? 'full', + capability: capabilityName, + required_paths: capabilityName ? [`capabilities.enforcement.${capabilityName}`] : [], + execution_mode: 'package_core_signal', + mode: supported ? 'pass' : 'fail_closed' + }; + } + } +} + +export function assessCapabilityExpectation(capabilityDescriptor, expectation, expectationPaths = {}) { + const candidatePaths = expectationPaths[expectation] ?? []; + const matchedPath = candidatePaths.find((candidatePath) => isSupported(getByPath(capabilityDescriptor, candidatePath))); + return { + expectation, + supported: Boolean(matchedPath), + matched_path: matchedPath ?? candidatePaths[0] ?? null + }; +} + +export const __testables = { + getByPath, + isTruthySupportLevel, + isSupported, + enforcementSupport, + mapActionToCapability +}; diff --git a/plugins/reporting-governance/src/core/compatibility-preflight.mjs b/plugins/reporting-governance/src/core/compatibility-preflight.mjs index df7a48c..5606e79 100644 --- a/plugins/reporting-governance/src/core/compatibility-preflight.mjs +++ b/plugins/reporting-governance/src/core/compatibility-preflight.mjs @@ -1,3 +1,5 @@ +import { assessCapabilityExpectation, evaluateActionSupport } from './action-support.mjs'; + const CANONICAL_SCHEMA_PATHS = Object.freeze({ event_schema: 'schemas/reporting-governance/event-envelope.schema.json', evidence_schema: 'schemas/reporting-governance/evidence.schema.json', @@ -20,25 +22,6 @@ 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; @@ -51,48 +34,6 @@ function collectExpectedActions(profile = {}) { 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; @@ -117,10 +58,10 @@ export function runCompatibilityPreflight({ capabilityDescriptor = {}, profile = } const requiredExpectations = safeArray(profile?.capability_expectations?.required).map((expectation) => - assessCapabilityExpectation(capabilityDescriptor, expectation) + assessCapabilityExpectation(capabilityDescriptor, expectation, EXPECTATION_CAPABILITY_PATHS) ); const preferredExpectations = safeArray(profile?.capability_expectations?.preferred).map((expectation) => - assessCapabilityExpectation(capabilityDescriptor, expectation) + assessCapabilityExpectation(capabilityDescriptor, expectation, EXPECTATION_CAPABILITY_PATHS) ); for (const result of requiredExpectations) { diff --git a/plugins/reporting-governance/src/core/decision-runner.mjs b/plugins/reporting-governance/src/core/decision-runner.mjs index 40e9b7f..45a8384 100644 --- a/plugins/reporting-governance/src/core/decision-runner.mjs +++ b/plugins/reporting-governance/src/core/decision-runner.mjs @@ -1,19 +1,13 @@ +import { evaluateActionSupport } from './action-support.mjs'; + 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, @@ -27,68 +21,6 @@ function createReceipt({ decision, actionPlans, blockedActions, delivery_state, }; } -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': - return 'notify_operator'; - case 'dispatch_message': - 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'); @@ -101,12 +33,12 @@ export function planDecisionExecution({ decision, capabilityDescriptor = {} }) { const notes = []; for (const requiredAction of safeArray(decision.required_actions)) { - const support = actionSupport(capabilityDescriptor, requiredAction.action); + const support = evaluateActionSupport(capabilityDescriptor, requiredAction.action); if (support.supported) { actionPlans.push({ ...requiredAction, - execution_mode: support.mode, + execution_mode: support.execution_mode, capability: support.capability, support_level: support.level }); diff --git a/plugins/reporting-governance/src/core/execute-governance-contract.mjs b/plugins/reporting-governance/src/core/execute-governance-contract.mjs index 00f758b..115faf0 100644 --- a/plugins/reporting-governance/src/core/execute-governance-contract.mjs +++ b/plugins/reporting-governance/src/core/execute-governance-contract.mjs @@ -1,12 +1,56 @@ import { evaluatePolicies } from './policy-evaluator.mjs'; +import { runCompatibilityPreflight } from './compatibility-preflight.mjs'; import { planDecisionExecution } from './decision-runner.mjs'; +function createBlockedContract({ capabilityDescriptor = {}, evaluation, preflight }) { + return { + evaluation, + preflight, + planning: { + decision: evaluation.decision, + enforcement_intent: { + policy_id: evaluation.decision.policy_id, + decision: evaluation.decision.decision, + planned_actions: [], + blocked_actions: [], + runtime_adapter_required: [], + package_core_actions: [] + }, + receipt: { + policy_id: evaluation.decision.policy_id, + decision: evaluation.decision.decision, + status: 'failed', + delivery_state: 'blocked', + enforcement_intent: [], + blocked_actions: [], + operator_notice_required: Boolean(evaluation.decision.operator_notice?.required), + notes: [ + 'Compatibility preflight failed closed; execution planning was blocked before any runnable contract could be produced.', + ...preflight.errors + ] + } + }, + contract: { + runtime: capabilityDescriptor?.metadata?.id ?? capabilityDescriptor?.runtime?.name ?? 'unknown-runtime', + policy_id: evaluation.decision.policy_id, + decision: evaluation.decision.decision, + adapter_actions: [], + package_actions: [], + blocked_actions: [], + delivery_state: 'blocked', + receipt_status: 'failed' + } + }; +} + export function executeGovernanceContract({ event, evidence = [], capabilityDescriptor = {}, policyPacks = [], context = {}, + profile = {}, + packageVersion, } = {}) { const evaluation = evaluatePolicies({ event, @@ -16,6 +60,16 @@ export function executeGovernanceContract({ context, }); + const preflight = runCompatibilityPreflight({ + capabilityDescriptor, + profile, + packageVersion, + }); + + if (preflight.status === 'fail_closed') { + return createBlockedContract({ capabilityDescriptor, evaluation, preflight }); + } + const planning = planDecisionExecution({ decision: evaluation.decision, capabilityDescriptor, @@ -23,6 +77,7 @@ export function executeGovernanceContract({ return { evaluation, + preflight, planning, contract: { runtime: capabilityDescriptor?.metadata?.id ?? capabilityDescriptor?.runtime?.name ?? 'unknown-runtime', diff --git a/plugins/reporting-governance/test/governance-contract.integration.test.mjs b/plugins/reporting-governance/test/governance-contract.integration.test.mjs index 56f2aaa..72ad000 100644 --- a/plugins/reporting-governance/test/governance-contract.integration.test.mjs +++ b/plugins/reporting-governance/test/governance-contract.integration.test.mjs @@ -94,10 +94,13 @@ test('capability descriptor -> policy evaluation -> decision planning yields ada context: { signals: ['checkpoint_overdue'], operator_context: { report_anchor_present: true } - } + }, + profile: strictProfile, + packageVersion: '0.1.0-mainline' }); assert.equal(result.evaluation.decision.decision, 'force_checkpoint'); + assert.equal(result.preflight.status, 'pass'); assert.equal(result.planning.receipt.delivery_state, 'pending_external_send'); assert.deepEqual(result.contract.adapter_actions, ['notify_operator']); assert.deepEqual(result.contract.package_actions, ['emit_event']); @@ -139,10 +142,13 @@ test('contract truthfully degrades when notify path can queue but cannot directl policyPacks: [noSilencePack], context: { signals: ['checkpoint_overdue'] - } + }, + profile: strictProfile, + packageVersion: '0.1.0-mainline' }); assert.equal(result.evaluation.decision.decision, 'force_checkpoint'); + assert.equal(result.preflight.status, 'degraded'); assert.deepEqual(result.contract.adapter_actions, ['notify_operator']); assert.deepEqual(result.contract.blocked_actions, []); assert.equal(result.contract.receipt_status, 'planned'); @@ -181,10 +187,18 @@ test('contract fails closed when capability descriptor cannot satisfy mandatory policyPacks: [noSilencePack], context: { signals: ['checkpoint_overdue'] - } + }, + profile: strictProfile, + packageVersion: '0.1.0-mainline' }); assert.equal(result.evaluation.decision.decision, 'force_checkpoint'); - assert.deepEqual(result.contract.adapter_actions, ['notify_operator']); - assert.equal(result.contract.receipt_status, 'planned'); + assert.equal(result.preflight.status, 'fail_closed'); + assert.deepEqual(result.planning.enforcement_intent.planned_actions, []); + assert.deepEqual(result.contract.adapter_actions, []); + assert.deepEqual(result.contract.package_actions, []); + assert.deepEqual(result.contract.blocked_actions, []); + assert.equal(result.contract.delivery_state, 'blocked'); + assert.equal(result.contract.receipt_status, 'failed'); + assert.ok(result.planning.receipt.notes.some((note) => note.includes('failed closed'))); });