feat: add reporting governance preflight contract
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { runCompatibilityPreflight } from '../src/core/compatibility-preflight.mjs';
|
||||
import capabilityDescriptor from '../capabilities/openclaw-watchdog-reference.json' with { type: 'json' };
|
||||
|
||||
const strictProfile = {
|
||||
metadata: { id: 'strict-manager-mode' },
|
||||
spec: {
|
||||
package: { pluginVersion: '0.1.0-mainline' },
|
||||
policies: {
|
||||
overrides: {
|
||||
checkpoints: { overdueAction: 'force_checkpoint' }
|
||||
}
|
||||
},
|
||||
notifications: {
|
||||
operatorVisibleRecoveryRequired: true
|
||||
}
|
||||
},
|
||||
capability_expectations: {
|
||||
required: [
|
||||
'emit_canonical_events',
|
||||
'evaluate_watchdog_overdue',
|
||||
'create_queue_items',
|
||||
'create_spool_handoff',
|
||||
'write_bridge_receipts'
|
||||
],
|
||||
preferred: ['direct_sender_binding', 'final_delivery_ack', 'inline_dispatch_blocking']
|
||||
}
|
||||
};
|
||||
|
||||
test('runCompatibilityPreflight passes strict profile against reference descriptor', () => {
|
||||
const result = runCompatibilityPreflight({
|
||||
capabilityDescriptor,
|
||||
profile: strictProfile,
|
||||
packageVersion: '0.1.0-mainline'
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'pass');
|
||||
assert.equal(result.compatibility.version_ok, true);
|
||||
assert.ok(result.compatibility.schema_checks.every((entry) => entry.ok));
|
||||
assert.ok(result.compatibility.required_expectations.every((entry) => entry.supported));
|
||||
assert.equal(result.errors.length, 0);
|
||||
});
|
||||
|
||||
test('runCompatibilityPreflight fails closed on schema/version mismatch', () => {
|
||||
const brokenDescriptor = {
|
||||
...capabilityDescriptor,
|
||||
compatibility: {
|
||||
...capabilityDescriptor.compatibility,
|
||||
plugin_spec_versions: ['9.9.9'],
|
||||
decision_schema: 'schemas/reporting-governance/not-the-canonical-decision.schema.json'
|
||||
}
|
||||
};
|
||||
|
||||
const result = runCompatibilityPreflight({
|
||||
capabilityDescriptor: brokenDescriptor,
|
||||
profile: strictProfile,
|
||||
packageVersion: '0.1.0-mainline'
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'fail_closed');
|
||||
assert.equal(result.compatibility.version_ok, false);
|
||||
assert.match(result.errors.join('\n'), /schema mismatch: decision_schema/);
|
||||
assert.match(result.errors.join('\n'), /plugin version 0.1.0-mainline is not declared compatible/);
|
||||
});
|
||||
|
||||
test('runCompatibilityPreflight degrades honestly when notify path can queue but cannot prove final send', () => {
|
||||
const degradedDescriptor = {
|
||||
...capabilityDescriptor,
|
||||
capabilities: {
|
||||
...capabilityDescriptor.capabilities,
|
||||
notification_path: {
|
||||
...capabilityDescriptor.capabilities.notification_path,
|
||||
sender_binding: { supported: false, level: 'none' },
|
||||
direct_send: { supported: false, level: 'none' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = runCompatibilityPreflight({
|
||||
capabilityDescriptor: degradedDescriptor,
|
||||
profile: strictProfile,
|
||||
packageVersion: '0.1.0-mainline'
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'degraded');
|
||||
assert.equal(result.errors.length, 0);
|
||||
assert.ok(result.warnings.some((warning) => warning.includes('notify_operator')));
|
||||
assert.ok(result.notes.includes('degrade:notify_operator=pending_external_send'));
|
||||
});
|
||||
|
||||
test('runCompatibilityPreflight fails closed when profile requires unsupported force_checkpoint', () => {
|
||||
const limitedDescriptor = {
|
||||
...capabilityDescriptor,
|
||||
capabilities: {
|
||||
...capabilityDescriptor.capabilities,
|
||||
enforcement: {
|
||||
...capabilityDescriptor.capabilities.enforcement,
|
||||
force_checkpoint: { supported: false, level: 'none' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = runCompatibilityPreflight({
|
||||
capabilityDescriptor: limitedDescriptor,
|
||||
profile: strictProfile,
|
||||
packageVersion: '0.1.0-mainline'
|
||||
});
|
||||
|
||||
assert.equal(result.status, 'fail_closed');
|
||||
assert.match(result.errors.join('\n'), /required action is not supportable by runtime: force_checkpoint/);
|
||||
});
|
||||
@@ -15,6 +15,10 @@ const baseCapabilityDescriptor = {
|
||||
escalate: { supported: true, level: 'full' }
|
||||
},
|
||||
notification_path: {
|
||||
queue_items: { supported: true, level: 'full' },
|
||||
spool_handoff: { supported: true, level: 'full' },
|
||||
sender_binding: { supported: true, level: 'full' },
|
||||
direct_send: { supported: true, level: 'partial' },
|
||||
truth_model: {
|
||||
delivery_states: ['prepared', 'queued', 'dispatched', 'pending_external_send', 'acked', 'blocked'],
|
||||
ack_requires_proven_send: true,
|
||||
@@ -53,6 +57,39 @@ test('planDecisionExecution produces runtime-adapter dispatch intent for force_c
|
||||
assert.ok(result.receipt.notes.some((note) => note.includes('runtime-adapter responsibility')));
|
||||
});
|
||||
|
||||
test('planDecisionExecution keeps notify_operator separate from dispatch_message capability semantics', () => {
|
||||
const notifyOnlyDescriptor = {
|
||||
capabilities: {
|
||||
...baseCapabilityDescriptor.capabilities,
|
||||
notification_path: {
|
||||
...baseCapabilityDescriptor.capabilities.notification_path,
|
||||
sender_binding: { supported: false, level: 'none' },
|
||||
direct_send: { supported: false, level: 'none' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const result = planDecisionExecution({
|
||||
decision: {
|
||||
decision: 'force_checkpoint',
|
||||
policy_id: 'no-silence.missed-checkpoint',
|
||||
severity: 'high',
|
||||
reason: 'checkpoint overdue',
|
||||
required_actions: [
|
||||
{ action: 'notify_operator', target: 'operator_channel', mandatory: true },
|
||||
{ action: 'dispatch_message', target: 'operator_channel', mandatory: true }
|
||||
],
|
||||
operator_notice: { required: true }
|
||||
},
|
||||
capabilityDescriptor: notifyOnlyDescriptor
|
||||
});
|
||||
|
||||
assert.deepEqual(result.enforcement_intent.runtime_adapter_required, ['notify_operator']);
|
||||
assert.equal(result.enforcement_intent.planned_actions[0].execution_mode, 'runtime_adapter_dispatch_deferred');
|
||||
assert.equal(result.enforcement_intent.blocked_actions[0].action, 'dispatch_message');
|
||||
assert.ok(result.receipt.notes.some((note) => note.includes('pending_external_send')));
|
||||
});
|
||||
|
||||
test('planDecisionExecution truthfully blocks unsupported package action paths', () => {
|
||||
const result = planDecisionExecution({
|
||||
decision: {
|
||||
@@ -79,3 +116,22 @@ test('planDecisionExecution truthfully blocks unsupported package action paths',
|
||||
assert.equal(result.enforcement_intent.planned_actions.length, 0);
|
||||
assert.equal(result.enforcement_intent.blocked_actions[0].action, 'set_status');
|
||||
});
|
||||
|
||||
test('planDecisionExecution marks block decisions as blocked when runtime truth model supports blocked state', () => {
|
||||
const result = planDecisionExecution({
|
||||
decision: {
|
||||
decision: 'block',
|
||||
policy_id: 'no-fake-progress.invalid-transition',
|
||||
severity: 'high',
|
||||
reason: 'invalid dispatch attempt',
|
||||
required_actions: [
|
||||
{ action: 'block_transition', target: 'dispatch_gate', mandatory: true }
|
||||
],
|
||||
operator_notice: { required: false }
|
||||
},
|
||||
capabilityDescriptor: baseCapabilityDescriptor
|
||||
});
|
||||
|
||||
assert.equal(result.receipt.delivery_state, 'blocked');
|
||||
assert.deepEqual(result.enforcement_intent.package_core_actions, ['block_transition']);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { executeGovernanceContract } from '../src/core/execute-governance-contract.mjs';
|
||||
import { executeGovernanceContract, runCompatibilityPreflight } from '../src/core/index.mjs';
|
||||
import capabilityDescriptor from '../capabilities/openclaw-watchdog-reference.json' with { type: 'json' };
|
||||
|
||||
const noSilencePack = {
|
||||
@@ -44,7 +44,39 @@ const noSilencePack = {
|
||||
}
|
||||
};
|
||||
|
||||
const strictProfile = {
|
||||
metadata: { id: 'strict-manager-mode' },
|
||||
spec: {
|
||||
package: { pluginVersion: '0.1.0-mainline' },
|
||||
policies: {
|
||||
overrides: {
|
||||
checkpoints: { overdueAction: 'force_checkpoint' }
|
||||
}
|
||||
},
|
||||
notifications: {
|
||||
operatorVisibleRecoveryRequired: true
|
||||
}
|
||||
},
|
||||
capability_expectations: {
|
||||
required: [
|
||||
'emit_canonical_events',
|
||||
'evaluate_watchdog_overdue',
|
||||
'create_queue_items',
|
||||
'create_spool_handoff',
|
||||
'write_bridge_receipts'
|
||||
],
|
||||
preferred: ['direct_sender_binding', 'final_delivery_ack']
|
||||
}
|
||||
};
|
||||
|
||||
test('capability descriptor -> policy evaluation -> decision planning yields adapter-compatible contract', () => {
|
||||
const preflight = runCompatibilityPreflight({
|
||||
capabilityDescriptor,
|
||||
profile: strictProfile,
|
||||
packageVersion: '0.1.0-mainline'
|
||||
});
|
||||
assert.equal(preflight.status, 'pass');
|
||||
|
||||
const result = executeGovernanceContract({
|
||||
event: {
|
||||
type: 'silence_timeout',
|
||||
@@ -74,7 +106,7 @@ test('capability descriptor -> policy evaluation -> decision planning yields ada
|
||||
assert.equal(result.contract.runtime, 'openclaw-watchdog-reference');
|
||||
});
|
||||
|
||||
test('contract truthfully degrades when capability descriptor cannot satisfy mandatory action', () => {
|
||||
test('contract truthfully degrades when notify path can queue but cannot directly dispatch', () => {
|
||||
const limitedDescriptor = {
|
||||
...capabilityDescriptor,
|
||||
metadata: {
|
||||
@@ -83,13 +115,21 @@ test('contract truthfully degrades when capability descriptor cannot satisfy man
|
||||
},
|
||||
capabilities: {
|
||||
...capabilityDescriptor.capabilities,
|
||||
enforcement: {
|
||||
...capabilityDescriptor.capabilities.enforcement,
|
||||
force_checkpoint: { supported: false, level: 'none' }
|
||||
notification_path: {
|
||||
...capabilityDescriptor.capabilities.notification_path,
|
||||
sender_binding: { supported: false, level: 'none' },
|
||||
direct_send: { supported: false, level: 'none' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const preflight = runCompatibilityPreflight({
|
||||
capabilityDescriptor: limitedDescriptor,
|
||||
profile: strictProfile,
|
||||
packageVersion: '0.1.0-mainline'
|
||||
});
|
||||
assert.equal(preflight.status, 'degraded');
|
||||
|
||||
const result = executeGovernanceContract({
|
||||
event: {
|
||||
type: 'silence_timeout',
|
||||
@@ -103,7 +143,48 @@ test('contract truthfully degrades when capability descriptor cannot satisfy man
|
||||
});
|
||||
|
||||
assert.equal(result.evaluation.decision.decision, 'force_checkpoint');
|
||||
assert.deepEqual(result.contract.adapter_actions, []);
|
||||
assert.deepEqual(result.contract.blocked_actions, ['notify_operator']);
|
||||
assert.equal(result.contract.receipt_status, 'degraded');
|
||||
assert.deepEqual(result.contract.adapter_actions, ['notify_operator']);
|
||||
assert.deepEqual(result.contract.blocked_actions, []);
|
||||
assert.equal(result.contract.receipt_status, 'planned');
|
||||
assert.ok(result.planning.receipt.notes.some((note) => note.includes('pending_external_send')));
|
||||
});
|
||||
|
||||
test('contract fails closed when capability descriptor cannot satisfy mandatory force_checkpoint path', () => {
|
||||
const limitedDescriptor = {
|
||||
...capabilityDescriptor,
|
||||
metadata: {
|
||||
...capabilityDescriptor.metadata,
|
||||
id: 'hard-limited-openclaw-watchdog-reference'
|
||||
},
|
||||
capabilities: {
|
||||
...capabilityDescriptor.capabilities,
|
||||
enforcement: {
|
||||
...capabilityDescriptor.capabilities.enforcement,
|
||||
force_checkpoint: { supported: false, level: 'none' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const preflight = runCompatibilityPreflight({
|
||||
capabilityDescriptor: limitedDescriptor,
|
||||
profile: strictProfile,
|
||||
packageVersion: '0.1.0-mainline'
|
||||
});
|
||||
assert.equal(preflight.status, 'fail_closed');
|
||||
|
||||
const result = executeGovernanceContract({
|
||||
event: {
|
||||
type: 'silence_timeout',
|
||||
payload: { checkpoint_overdue: true }
|
||||
},
|
||||
capabilityDescriptor: limitedDescriptor,
|
||||
policyPacks: [noSilencePack],
|
||||
context: {
|
||||
signals: ['checkpoint_overdue']
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(result.evaluation.decision.decision, 'force_checkpoint');
|
||||
assert.deepEqual(result.contract.adapter_actions, ['notify_operator']);
|
||||
assert.equal(result.contract.receipt_status, 'planned');
|
||||
});
|
||||
|
||||
@@ -104,6 +104,47 @@ const verifiedCompletionPack = {
|
||||
}
|
||||
};
|
||||
|
||||
const structurePack = {
|
||||
metadata: { id: 'mandatory-checkpoint-structure', severity_default: 'medium' },
|
||||
spec: {
|
||||
evaluation_mode: 'any_rule_match',
|
||||
rules: [
|
||||
{
|
||||
id: 'mandatory-checkpoint-structure.block-missing-fields',
|
||||
title: 'Missing checkpoint structure blocks dispatch',
|
||||
intent: 'Do not allow malformed checkpoints to pass as normal progress.',
|
||||
triggers: {
|
||||
event_types: ['task_checkpoint_sent'],
|
||||
claim_types: ['progress']
|
||||
},
|
||||
conditions: {
|
||||
all: [
|
||||
{ fact: 'message.has_required_checkpoint_fields', equals: false }
|
||||
]
|
||||
},
|
||||
decision_output: {
|
||||
decision: 'block',
|
||||
severity: 'high',
|
||||
reason: 'checkpoint structure missing required fields',
|
||||
required_actions: [
|
||||
{ action: 'block_transition', target: 'dispatch_gate', mandatory: true },
|
||||
{ action: 'notify_operator', target: 'operator_channel', mandatory: true }
|
||||
],
|
||||
operator_notice: {
|
||||
required: true,
|
||||
channel: 'telegram',
|
||||
urgency: 'high',
|
||||
message: 'Checkpoint blocked: missing required structure.'
|
||||
}
|
||||
},
|
||||
operator_message_templates: {
|
||||
blocked: 'Checkpoint blocked: missing required structure.'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
test('evaluatePolicyPack returns force_checkpoint for overdue silence event', () => {
|
||||
const result = evaluatePolicyPack({
|
||||
event: {
|
||||
@@ -141,3 +182,80 @@ test('evaluatePolicies picks downgrade_status over allow for weak completion cla
|
||||
assert.equal(result.decision.suggested_status, 'pending_verification');
|
||||
assert.equal(result.evaluations[0].decision.policy_id, 'verified-completion-only.insufficient-evidence');
|
||||
});
|
||||
|
||||
test('evaluatePolicies gives block precedence over allow-like outcomes when checkpoint structure is invalid', () => {
|
||||
const result = evaluatePolicies({
|
||||
event: {
|
||||
type: 'task_checkpoint_sent',
|
||||
payload: {}
|
||||
},
|
||||
evidence: [
|
||||
{ id: 'ev-2', quality: 'moderate', is_new: true }
|
||||
],
|
||||
capabilityDescriptor,
|
||||
policyPacks: [noSilencePack, structurePack],
|
||||
context: {
|
||||
message: { has_required_checkpoint_fields: false }
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(result.decision.decision, 'block');
|
||||
assert.equal(result.decision.policy_id, 'mandatory-checkpoint-structure.block-missing-fields');
|
||||
});
|
||||
|
||||
test('evaluatePolicies applies multi-pack precedence with escalate over block and force_checkpoint', () => {
|
||||
const escalationPack = {
|
||||
metadata: { id: 'escalation-pack', severity_default: 'critical' },
|
||||
spec: {
|
||||
evaluation_mode: 'any_rule_match',
|
||||
rules: [
|
||||
{
|
||||
id: 'escalation-pack.missing-visible-followup',
|
||||
title: 'Missing visible follow-up escalates',
|
||||
intent: 'Escalate when result exists without visible forwarding.',
|
||||
triggers: { event_types: ['silence_timeout'] },
|
||||
conditions: {
|
||||
all: [
|
||||
{ fact: 'forwarding.result_available_without_visible_followup', equals: true }
|
||||
]
|
||||
},
|
||||
decision_output: {
|
||||
decision: 'escalate',
|
||||
severity: 'critical',
|
||||
reason: 'result exists without visible forwarding',
|
||||
required_actions: [
|
||||
{ action: 'raise_escalation', target: 'manager_channel', mandatory: true }
|
||||
],
|
||||
operator_notice: {
|
||||
required: true,
|
||||
channel: 'telegram',
|
||||
urgency: 'high',
|
||||
message: 'Escalation required: hidden result path detected.'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
const result = evaluatePolicies({
|
||||
event: {
|
||||
type: 'silence_timeout',
|
||||
payload: {
|
||||
checkpoint_overdue: true,
|
||||
result_available: true,
|
||||
result_forwarded: false
|
||||
}
|
||||
},
|
||||
evidence: [{ id: 'ev-3', quality: 'moderate', is_new: true }],
|
||||
capabilityDescriptor,
|
||||
policyPacks: [noSilencePack, structurePack, escalationPack],
|
||||
context: {
|
||||
signals: ['checkpoint_overdue'],
|
||||
message: { has_required_checkpoint_fields: false }
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(result.decision.decision, 'escalate');
|
||||
assert.equal(result.decision.policy_id, 'escalation-pack.missing-visible-followup');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user