From 702386a122f028674990d6aa6b3b923c4fc2b3a0 Mon Sep 17 00:00:00 2001 From: Eve Date: Fri, 8 May 2026 09:34:15 +0800 Subject: [PATCH] feat: wire minimal governance contract path --- plugins/reporting-governance/README.md | 57 ++++++++- plugins/reporting-governance/package.json | 2 +- .../src/core/execute-governance-contract.mjs | 38 ++++++ .../reporting-governance/src/core/index.mjs | 1 + plugins/reporting-governance/src/index.mjs | 2 +- .../exports-boundary.integration.test.mjs | 2 + .../governance-contract.integration.test.mjs | 109 ++++++++++++++++++ 7 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 plugins/reporting-governance/src/core/execute-governance-contract.mjs create mode 100644 plugins/reporting-governance/test/governance-contract.integration.test.mjs diff --git a/plugins/reporting-governance/README.md b/plugins/reporting-governance/README.md index a11085b..15913ae 100644 --- a/plugins/reporting-governance/README.md +++ b/plugins/reporting-governance/README.md @@ -24,6 +24,7 @@ plugins/reporting-governance/ index.mjs policy-evaluator.mjs decision-runner.mjs + execute-governance-contract.mjs adapters/ storage/ reference/ @@ -68,6 +69,42 @@ Reference runtime compositions and migration notes. **The watchdog reference runtime composition belongs here**, as a reference implementation for OpenClaw rather than as package core logic. +## Public surface and compatibility + +Current **public package surface** is intentionally narrow: + +- root export: `@openclaw/plugin-reporting-governance` +- adapter exports: + - `@openclaw/plugin-reporting-governance/adapters` + - `@openclaw/plugin-reporting-governance/adapters/watchdog` + - `@openclaw/plugin-reporting-governance/adapters/dispatcher` + - `@openclaw/plugin-reporting-governance/adapters/bridge-supervisor` + - `@openclaw/plugin-reporting-governance/adapters/sender-binding` + - `@openclaw/plugin-reporting-governance/adapters/orchestrator` + +What is currently exposed from the root export: + +- `evaluatePolicyPack(...)` +- `evaluatePolicies(...)` +- `planDecisionExecution(...)` +- `executeGovernanceContract(...)` +- package metadata helpers such as `packageName` +- package-owned adapter entrypoints and `runWatchdogChain(...)` + +Compatibility posture for this slice: + +- `0.1.0-mainline` should be treated as **pre-1.0, surface-tightening phase**. +- Deep imports into `src/` are **not supported API** even if files exist in-repo. +- Tests now explicitly enforce that private paths like `src/adapters/runtime-binding.mjs` stay outside `exports`. +- Adding a symbol to a file under `src/` does **not** mean it is public unless wired through package `exports`. +- Future tightening of root/adapters exports may still be a breaking change until a stable `1.0` surface is declared. + +Practical migration rule: + +- depend on package root exports or declared adapter subpaths only +- do not couple runtime integrations to repo-private file paths +- treat capability descriptors and schemas as package artifacts, but not as guaranteed JS import entrypoints unless exported later + ## Current reference composition The current reference composition is the OpenClaw watchdog chain: @@ -93,6 +130,7 @@ The current package now includes a small but runnable `core/` implementation: - `src/core/policy-evaluator.mjs` - `src/core/decision-runner.mjs` +- `src/core/execute-governance-contract.mjs` - `src/core/index.mjs` Current package-core responsibilities: @@ -103,6 +141,7 @@ Current package-core responsibilities: - choose the highest-precedence decision when multiple rules match - convert a canonical decision into an execution plan, enforcement intent, and receipt skeleton - truthfully degrade unsupported enforcement paths based on the capability descriptor +- provide one minimal contract path from `capability descriptor -> policy decision -> execution planning` Still **runtime-adapter responsibility** at this stage: @@ -114,6 +153,22 @@ Still **runtime-adapter responsibility** at this stage: This means `core/` now owns evaluation and planning semantics, while adapters still own actual enforcement side effects. +## Minimal end-to-end contract slice now included + +This slice now has one small but testable contract path: + +1. capability descriptor advertises real enforcement support +2. policy evaluator emits a canonical decision from event/evidence/context +3. decision runner converts that decision into execution planning +4. the result declares: + - adapter-dispatch actions required + - package-core actions possible locally + - blocked mandatory actions when capability support is missing + - truthful delivery / receipt state + +This is intentionally **planning-level end-to-end**, not full live inline interception. +It proves contract alignment without pretending all runtime enforcement is already extracted. + ## Not yet included This package still does **not** claim full implementation of: @@ -124,4 +179,4 @@ This package still does **not** claim full implementation of: - complete rewrite / placeholder / review / status-downgrade adapter execution - non-watchdog full runtime governance interception -It now provides the first package-mainline evaluator / decision-runner core, but the remaining enforcement surface is still intentionally honest about adapter gaps. +It now provides the first package-mainline evaluator / decision-runner core, plus a minimal end-to-end contract proof, but the remaining enforcement surface is still intentionally honest about adapter gaps. diff --git a/plugins/reporting-governance/package.json b/plugins/reporting-governance/package.json index 2424239..43f0940 100644 --- a/plugins/reporting-governance/package.json +++ b/plugins/reporting-governance/package.json @@ -14,6 +14,6 @@ "./adapters/orchestrator": "./src/adapters/orchestrator.mjs" }, "scripts": { - "test": "node --test test/package-structure.test.mjs test/policy-evaluator.test.mjs test/decision-runner.test.mjs test/watchdog-chain.integration.test.mjs test/exports-boundary.integration.test.mjs" + "test": "node --test test/package-structure.test.mjs test/policy-evaluator.test.mjs test/decision-runner.test.mjs test/governance-contract.integration.test.mjs test/watchdog-chain.integration.test.mjs test/exports-boundary.integration.test.mjs" } } diff --git a/plugins/reporting-governance/src/core/execute-governance-contract.mjs b/plugins/reporting-governance/src/core/execute-governance-contract.mjs new file mode 100644 index 0000000..00f758b --- /dev/null +++ b/plugins/reporting-governance/src/core/execute-governance-contract.mjs @@ -0,0 +1,38 @@ +import { evaluatePolicies } from './policy-evaluator.mjs'; +import { planDecisionExecution } from './decision-runner.mjs'; + +export function executeGovernanceContract({ + event, + evidence = [], + capabilityDescriptor = {}, + policyPacks = [], + context = {}, +} = {}) { + const evaluation = evaluatePolicies({ + event, + evidence, + capabilityDescriptor, + policyPacks, + context, + }); + + const planning = planDecisionExecution({ + decision: evaluation.decision, + capabilityDescriptor, + }); + + return { + evaluation, + planning, + contract: { + runtime: capabilityDescriptor?.metadata?.id ?? capabilityDescriptor?.runtime?.name ?? 'unknown-runtime', + policy_id: evaluation.decision.policy_id, + decision: evaluation.decision.decision, + adapter_actions: planning.enforcement_intent.runtime_adapter_required, + package_actions: planning.enforcement_intent.package_core_actions, + blocked_actions: planning.enforcement_intent.blocked_actions.map((action) => action.action), + delivery_state: planning.receipt.delivery_state, + receipt_status: planning.receipt.status, + }, + }; +} diff --git a/plugins/reporting-governance/src/core/index.mjs b/plugins/reporting-governance/src/core/index.mjs index e11c1eb..ab6c084 100644 --- a/plugins/reporting-governance/src/core/index.mjs +++ b/plugins/reporting-governance/src/core/index.mjs @@ -1,2 +1,3 @@ export { evaluatePolicyPack, evaluatePolicies } from './policy-evaluator.mjs'; export { planDecisionExecution } from './decision-runner.mjs'; +export { executeGovernanceContract } from './execute-governance-contract.mjs'; diff --git a/plugins/reporting-governance/src/index.mjs b/plugins/reporting-governance/src/index.mjs index 25b36ee..e6e4bc8 100644 --- a/plugins/reporting-governance/src/index.mjs +++ b/plugins/reporting-governance/src/index.mjs @@ -29,7 +29,7 @@ export const packageBoundaries = { ] }; -export { evaluatePolicyPack, evaluatePolicies, planDecisionExecution } from './core/index.mjs'; +export { evaluatePolicyPack, evaluatePolicies, planDecisionExecution, executeGovernanceContract } from './core/index.mjs'; export { createRuntimeBinding, runWatchdogAdapter, diff --git a/plugins/reporting-governance/test/exports-boundary.integration.test.mjs b/plugins/reporting-governance/test/exports-boundary.integration.test.mjs index f694d0f..0d3d752 100644 --- a/plugins/reporting-governance/test/exports-boundary.integration.test.mjs +++ b/plugins/reporting-governance/test/exports-boundary.integration.test.mjs @@ -68,12 +68,14 @@ test('package root export resolves public package surface only', () => { packageName: plugin.packageName, hasRunWatchdogChain: typeof plugin.runWatchdogChain, hasPlanDecisionExecution: typeof plugin.planDecisionExecution, + hasExecuteGovernanceContract: typeof plugin.executeGovernanceContract, })); `); assert.equal(result.packageName, '@openclaw/plugin-reporting-governance'); assert.equal(result.hasRunWatchdogChain, 'function'); assert.equal(result.hasPlanDecisionExecution, 'function'); + assert.equal(result.hasExecuteGovernanceContract, 'function'); } finally { fs.rmSync(root, { recursive: true, force: true }); } diff --git a/plugins/reporting-governance/test/governance-contract.integration.test.mjs b/plugins/reporting-governance/test/governance-contract.integration.test.mjs new file mode 100644 index 0000000..a04eb4c --- /dev/null +++ b/plugins/reporting-governance/test/governance-contract.integration.test.mjs @@ -0,0 +1,109 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { executeGovernanceContract } from '../src/core/execute-governance-contract.mjs'; +import capabilityDescriptor from '../capabilities/openclaw-watchdog-reference.json' with { type: 'json' }; + +const noSilencePack = { + metadata: { id: 'no-silence', severity_default: 'high' }, + spec: { + evaluation_mode: 'any_rule_match', + rules: [ + { + id: 'no-silence.missed-checkpoint', + title: 'Missed checkpoint requires visible recovery', + intent: 'Prevent overdue checkpoints from becoming invisible.', + triggers: { event_types: ['silence_timeout'] }, + conditions: { + all: [ + { fact: 'checkpoint.is_overdue', equals: true } + ] + }, + decision_output: { + decision: 'force_checkpoint', + severity: 'high', + reason: 'checkpoint overdue triggered forced operator-visible recovery', + suggested_status: 'in_progress', + required_actions: [ + { action: 'notify_operator', target: 'operator_channel', mandatory: true }, + { action: 'emit_event', target: 'event_stream', mandatory: true } + ], + operator_notice: { + required: true, + channel: 'telegram', + urgency: 'high', + message: 'Required update: checkpoint overdue.', + deadline: '2026-01-01T00:00:00.000Z' + } + }, + operator_message_templates: { + checkpoint_forced: 'Required update: task exceeded allowed silence window.' + } + } + ] + } +}; + +test('capability descriptor -> policy evaluation -> decision planning yields adapter-compatible contract', () => { + const result = executeGovernanceContract({ + event: { + type: 'silence_timeout', + payload: { + checkpoint_overdue: true, + result_available: true, + result_forwarded: false, + } + }, + evidence: [ + { id: 'ev-watchdog', quality: 'moderate', is_new: true } + ], + capabilityDescriptor, + policyPacks: [noSilencePack], + context: { + signals: ['checkpoint_overdue'], + operator_context: { report_anchor_present: true } + } + }); + + assert.equal(result.evaluation.decision.decision, 'force_checkpoint'); + 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']); + assert.deepEqual(result.contract.blocked_actions, []); + assert.equal(result.contract.receipt_status, 'planned'); + assert.equal(result.contract.runtime, 'openclaw-watchdog-reference'); +}); + +test('contract truthfully degrades when capability descriptor cannot satisfy mandatory action', () => { + const limitedDescriptor = { + ...capabilityDescriptor, + metadata: { + ...capabilityDescriptor.metadata, + id: 'limited-openclaw-watchdog-reference' + }, + capabilities: { + ...capabilityDescriptor.capabilities, + enforcement: { + ...capabilityDescriptor.capabilities.enforcement, + force_checkpoint: { supported: false, level: 'none' } + } + } + }; + + 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, []); + assert.deepEqual(result.contract.blocked_actions, ['notify_operator']); + assert.equal(result.contract.receipt_status, 'degraded'); +});