From 6366f70491db606a4b421cc0620b643d0ce46400 Mon Sep 17 00:00:00 2001 From: Eve Date: Fri, 8 May 2026 10:07:26 +0800 Subject: [PATCH] feat(reporting-governance): add profile artifact binding slice --- .../agent-reporting-governance-plugin.md | 54 +++- plugins/reporting-governance/README.md | 55 +++- .../openclaw-watchdog-reference.json | 258 ++++++++++++++++++ ...watchdog-reference.descriptor.example.json | 81 ++++++ plugins/reporting-governance/package.json | 2 +- .../profiles/strict-manager-mode.profile.json | 36 +++ .../src/adapters/index.mjs | 1 + .../src/reference/openclaw-watchdog-chain.md | 49 ++++ .../src/storage/index.mjs | 1 + .../src/storage/profile-artifact.mjs | 54 ++++ .../test/compatibility-preflight.test.mjs | 25 +- .../exports-boundary.integration.test.mjs | 4 +- .../governance-contract.integration.test.mjs | 30 ++ .../test/package-structure.test.mjs | 4 +- .../test/profile-artifact.test.mjs | 47 ++++ 15 files changed, 695 insertions(+), 6 deletions(-) create mode 100644 plugins/reporting-governance/capabilities/openclaw-watchdog-reference.json create mode 100644 plugins/reporting-governance/examples/openclaw-watchdog-reference.descriptor.example.json create mode 100644 plugins/reporting-governance/profiles/strict-manager-mode.profile.json create mode 100644 plugins/reporting-governance/src/reference/openclaw-watchdog-chain.md create mode 100644 plugins/reporting-governance/src/storage/index.mjs create mode 100644 plugins/reporting-governance/src/storage/profile-artifact.mjs create mode 100644 plugins/reporting-governance/test/profile-artifact.test.mjs diff --git a/docs/architecture/agent-reporting-governance-plugin.md b/docs/architecture/agent-reporting-governance-plugin.md index 392f8ff..9e40862 100644 --- a/docs/architecture/agent-reporting-governance-plugin.md +++ b/docs/architecture/agent-reporting-governance-plugin.md @@ -177,7 +177,59 @@ This matters architecturally because it proves the plugin can: - preserve honest delivery semantics instead of collapsing `dispatched` into false success - provide a migration path from repo scripts toward package-level adapters and deployment profiles -The mainline next step is therefore not “more watchdog patching,” but formalizing this runtime composition inside the plugin’s adapter and capability model so future package extraction has a stable target. +## Compatibility envelope and legacy compatibility mode + +The architecture now draws a hard line between two caller postures: + +### Compatibility envelope + +A caller is inside the compatibility envelope once it supplies either: + +- a deployment profile / profile artifact, or +- a package version pin + +Inside this envelope, runtime compatibility is enforced against the descriptor as a truth contract: + +- canonical schema paths must match +- requested plugin version must be declared compatible +- required capability expectations must be satisfied +- requested actions must be supportable or honestly degraded + +If not, the system fails closed before producing a runnable enforcement contract. + +### Legacy compatibility mode + +Legacy compatibility mode exists only so older callers that still invoke package core without profile/package metadata do not break immediately. + +Behavior in this mode: + +- no version pin is assumed +- schema mismatch is surfaced in `schema_checks` but does not hard-fail by itself +- preflight records migration debt as notes +- truth semantics for actual planning are still preserved + +This is a migration concession, not a long-term steady state. +New callers should move to profile/package-backed invocation. + +## Minimal package profile artifact trajectory + +Architecture is also now advanced one notch from “profiles are external YAML docs” toward “profiles are package artifacts with a loader boundary”. + +Current minimal slice: + +- package-owned profile artifact snapshot: `plugins/reporting-governance/profiles/strict-manager-mode.profile.json` +- package loader: `src/storage/profile-artifact.mjs#loadDeploymentProfileArtifact(...)` +- binding contract projector: `src/storage/profile-artifact.mjs#createDeploymentBindingContract(...)` + +Architectural meaning: + +- package can carry one portable profile artifact under its own boundary +- storage layer owns loading/package-artifact interpretation +- runtime binding can be derived from the artifact rather than hardcoded entirely in docs +- tests prove the artifact resolves into concrete script and runtime-artifact paths + +This is intentionally still a **minimal verifiable slice**, not the full deployment system. +It proves the package boundary can own profile artifacts and bind them into runtime execution inputs. Primary follow-on specs: diff --git a/plugins/reporting-governance/README.md b/plugins/reporting-governance/README.md index 15913ae..2e58c06 100644 --- a/plugins/reporting-governance/README.md +++ b/plugins/reporting-governance/README.md @@ -9,6 +9,7 @@ Current purpose: - fix boundaries between `core/`, `adapters/`, `storage/`, and reference implementations - prepare the next implementation round for evaluator / decision-runner extraction - provide a minimal package-level policy evaluator and decision runner skeleton that can be verified in isolation +- add one minimal package-owned deployment profile artifact / loader / binding contract slice that is executable in tests ## Package skeleton @@ -17,6 +18,7 @@ plugins/reporting-governance/ package.json README.md capabilities/ + profiles/ docs/ examples/ src/ @@ -62,6 +64,7 @@ Durable I/O contracts for governance artifacts: - queue items - spool artifacts - receipts +- decision/profile/package artifacts - future decisions / audit manifests ### `src/reference/` @@ -99,6 +102,23 @@ Compatibility posture for this slice: - 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. +### Compatibility envelope vs legacy compatibility mode + +This slice now makes the boundary explicit: + +- **compatibility envelope present** = caller provides a deployment profile and/or package version pin, so `runCompatibilityPreflight(...)` must enforce canonical schema paths, declared plugin compatibility, required expectations, and action support **fail-closed**. +- **legacy compatibility mode** = caller omits profile + package version entirely, so preflight keeps old call sites alive, records the missing version pin as a note, and does **not** fail only because descriptor schema/version metadata drifted. + +Hard rule: + +- legacy mode is a caller-compatibility concession, **not** a relaxed truth model. +- once any profile/package compatibility envelope is supplied, schema mismatch becomes blocking again. + +Practical migration rule: + +- new integrations should always send a profile artifact or package version pin. +- old integrations may temporarily call without one, but should treat returned notes as migration debt. + Practical migration rule: - depend on package root exports or declared adapter subpaths only @@ -117,6 +137,7 @@ Package-home documentation: - `src/reference/openclaw-watchdog-chain.md` - `capabilities/openclaw-watchdog-reference.json` +- `profiles/strict-manager-mode.profile.json` Mainline background specs remain in: @@ -124,6 +145,38 @@ Mainline background specs remain in: - `docs/specs/reporting-governance-adapter-interface.md` - `docs/specs/reporting-governance-deployment-model.md` +## Minimal profile artifact / loader / binding contract slice + +This round adds one small but real package artifact path: + +- package artifact: `profiles/strict-manager-mode.profile.json` +- loader: `src/storage/profile-artifact.mjs#loadDeploymentProfileArtifact(...)` +- binding contract: `src/storage/profile-artifact.mjs#createDeploymentBindingContract(...)` + +What this slice does: + +1. package ships a profile artifact snapshot under package boundary +2. loader resolves that artifact from package-local path +3. binding contract translates profile-declared script/artifact roots into concrete repo/runtime paths +4. adapter runtime binding can be instantiated from that contract in tests + +What this slice does **not** claim yet: + +- full profile schema validation pipeline +- automatic YAML -> artifact generation +- generalized multi-profile packaging +- production deployment installer + +It is intentionally the smallest verifiable step that proves package profile artifacts are executable inputs rather than documentation only. + +## Current reference composition + +The current reference composition is the OpenClaw watchdog chain: + +```text +watchdog -> queue -> dispatcher -> bridge -> sender binding -> acked|blocked|pending_external_send +``` + ## Minimal evaluator / decision runner now included The current package now includes a small but runnable `core/` implementation: @@ -179,4 +232,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, plus a minimal end-to-end contract proof, but the remaining enforcement surface is still intentionally honest about adapter gaps. +It now provides the first package-mainline evaluator / decision-runner core, a compatibility-envelope boundary, and a minimal package profile artifact/binding slice, but the remaining enforcement surface is still intentionally honest about adapter gaps. diff --git a/plugins/reporting-governance/capabilities/openclaw-watchdog-reference.json b/plugins/reporting-governance/capabilities/openclaw-watchdog-reference.json new file mode 100644 index 0000000..2ed8eeb --- /dev/null +++ b/plugins/reporting-governance/capabilities/openclaw-watchdog-reference.json @@ -0,0 +1,258 @@ +{ + "$schema": "../../../schemas/reporting-governance/capability-descriptor.schema.json", + "apiVersion": "reporting-governance/v1alpha1", + "kind": "AdapterCapabilities", + "metadata": { + "id": "openclaw-watchdog-reference", + "title": "OpenClaw Watchdog Reference Runtime Composition", + "version": "1.0.0", + "owner": "openclaw", + "summary": "Reference capability descriptor for the OpenClaw watchdog -> queue -> dispatcher -> bridge -> sender-binding composition.", + "tags": [ + "openclaw", + "watchdog", + "reference-runtime", + "queue-bridge-sender" + ] + }, + "runtime": { + "name": "openclaw", + "mode": "hybrid", + "surfaces": [ + "watchdog", + "queue", + "dispatcher", + "bridge", + "sender_binding", + "orchestrator", + "scheduler", + "storage" + ], + "entrypoint": "scripts/watchdog_auto_notify_orchestrator.mjs" + }, + "compatibility": { + "plugin_spec_versions": [ + "0.1.0-mainline" + ], + "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" + }, + "capabilities": { + "observation": { + "task_lifecycle": { + "supported": true, + "level": "partial", + "notes": [ + "Current reference path is strongest for overdue/watchdog-related task state rather than all inline lifecycle hooks." + ] + }, + "subagent_lifecycle": { + "supported": true, + "level": "partial", + "notes": [ + "Indirectly supported through watchdog/governor state and runtime artifacts, not yet a full dedicated package module." + ] + }, + "checkpoint_obligations": { + "supported": true, + "level": "partial", + "notes": [ + "Current reference composition enforces overdue visibility but not yet all checkpoint-shape checks inline." + ] + }, + "outgoing_report_attempts": { + "supported": true, + "level": "partial", + "notes": [ + "Visible in notification path artifacts, but not yet generalized as a full package-wide hook surface." + ] + }, + "watchdog_state": { + "supported": true, + "level": "full" + }, + "queue_spool_receipts": { + "supported": true, + "level": "full" + } + }, + "normalization": { + "canonical_events": { + "supported": true, + "level": "full" + }, + "canonical_evidence": { + "supported": true, + "level": "partial", + "notes": [ + "Watchdog evidence and delivery receipts are strong; broader non-watchdog evidence shaping is still package-next work." + ] + }, + "decision_inputs": { + "supported": true, + "level": "partial", + "notes": [ + "Enough for watchdog-driven enforcement; not yet a generalized evaluator package input pipeline." + ] + }, + "correlation_propagation": { + "supported": true, + "level": "partial", + "notes": [ + "Correlation is preserved across the watchdog queue/bridge chain where available." + ] + } + }, + "enforcement": { + "block_transition": { + "supported": true, + "level": "partial", + "notes": [ + "Blocking exists in queue/dispatch/send paths, but not yet as a universal inline transition gate." + ] + }, + "rewrite_message": { + "supported": false, + "level": "none", + "notes": [ + "Not part of the reference watchdog composition yet." + ] + }, + "annotate_placeholder": { + "supported": false, + "level": "none" + }, + "force_checkpoint": { + "supported": true, + "level": "partial", + "notes": [ + "The composition can force operator-visible recovery/notice, but not yet a generalized checkpoint runner." + ] + }, + "request_review": { + "supported": false, + "level": "none" + }, + "downgrade_status": { + "supported": false, + "level": "none" + }, + "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", + "notes": [ + "Supported only when the deployment provides a working sender runtime such as openclaw-cli." + ] + }, + "receipts": { + "supported": true, + "level": "full" + }, + "final_delivery_proof": { + "supported": true, + "level": "partial", + "notes": [ + "Ack proof exists only when sender runtime returns proven sent state." + ] + }, + "truth_model": { + "delivery_states": [ + "prepared", + "queued", + "dispatched", + "pending_external_send", + "acked", + "blocked" + ], + "ack_requires_proven_send": true, + "pending_external_send_supported": true + } + }, + "watchdog": { + "scheduler_installation": { + "supported": true, + "level": "partial", + "notes": [ + "Cron wrapper exists, but installation remains environment-bound rather than package-core." + ] + }, + "watchdog_evaluation": { + "supported": true, + "level": "full" + }, + "watchdog_event_emission": { + "supported": true, + "level": "full" + }, + "operator_recovery_path": { + "supported": true, + "level": "full" + }, + "watchdog_closure": { + "supported": true, + "level": "full" + } + }, + "storage": { + "persist_events": { + "supported": true, + "level": "full" + }, + "persist_evidence": { + "supported": true, + "level": "full" + }, + "persist_decisions": { + "supported": false, + "level": "none", + "notes": [ + "Decision-store extraction is still the next stage." + ] + }, + "persist_receipts": { + "supported": true, + "level": "full" + }, + "preserve_original_attempted_message": { + "supported": true, + "level": "partial", + "notes": [ + "Notification payloads and sender-attempt artifacts are retained for the reference path." + ] + } + } + }, + "defaults": { + "reporting_mode": "watchdog-auto-notify", + "channel": "telegram", + "policy_ids": [ + "no-silence", + "mandatory-checkpoint-structure" + ], + "watchdog_policy_id": "no-silence" + }, + "notes": [ + "This descriptor intentionally describes the current OpenClaw watchdog reference composition, not the full future plugin capability surface.", + "Evaluator and decision-runner extraction should consume this descriptor as the runtime truth contract." + ] +} diff --git a/plugins/reporting-governance/examples/openclaw-watchdog-reference.descriptor.example.json b/plugins/reporting-governance/examples/openclaw-watchdog-reference.descriptor.example.json new file mode 100644 index 0000000..ffb5a85 --- /dev/null +++ b/plugins/reporting-governance/examples/openclaw-watchdog-reference.descriptor.example.json @@ -0,0 +1,81 @@ +{ + "$schema": "../../../schemas/reporting-governance/capability-descriptor.schema.json", + "apiVersion": "reporting-governance/v1alpha1", + "kind": "AdapterCapabilities", + "metadata": { + "id": "example-openclaw-watchdog-reference", + "title": "Example OpenClaw Watchdog Reference Descriptor", + "version": "1.0.0" + }, + "runtime": { + "name": "openclaw", + "mode": "hybrid", + "surfaces": [ + "watchdog", + "queue", + "dispatcher", + "bridge", + "sender_binding", + "orchestrator", + "storage" + ], + "entrypoint": "scripts/watchdog_auto_notify_orchestrator.mjs" + }, + "compatibility": { + "plugin_spec_versions": [ + "0.1.0-mainline" + ] + }, + "capabilities": { + "observation": { + "task_lifecycle": {"supported": true, "level": "partial"}, + "subagent_lifecycle": {"supported": true, "level": "partial"}, + "checkpoint_obligations": {"supported": true, "level": "partial"}, + "outgoing_report_attempts": {"supported": true, "level": "partial"}, + "watchdog_state": {"supported": true, "level": "full"}, + "queue_spool_receipts": {"supported": true, "level": "full"} + }, + "normalization": { + "canonical_events": {"supported": true, "level": "full"}, + "canonical_evidence": {"supported": true, "level": "partial"}, + "decision_inputs": {"supported": true, "level": "partial"}, + "correlation_propagation": {"supported": true, "level": "partial"} + }, + "enforcement": { + "block_transition": {"supported": true, "level": "partial"}, + "rewrite_message": {"supported": false, "level": "none"}, + "annotate_placeholder": {"supported": false, "level": "none"}, + "force_checkpoint": {"supported": true, "level": "partial"}, + "request_review": {"supported": false, "level": "none"}, + "downgrade_status": {"supported": false, "level": "none"}, + "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"}, + "receipts": {"supported": true, "level": "full"}, + "final_delivery_proof": {"supported": true, "level": "partial"}, + "truth_model": { + "delivery_states": ["prepared", "queued", "dispatched", "pending_external_send", "acked", "blocked"], + "ack_requires_proven_send": true, + "pending_external_send_supported": true + } + }, + "watchdog": { + "scheduler_installation": {"supported": true, "level": "partial"}, + "watchdog_evaluation": {"supported": true, "level": "full"}, + "watchdog_event_emission": {"supported": true, "level": "full"}, + "operator_recovery_path": {"supported": true, "level": "full"}, + "watchdog_closure": {"supported": true, "level": "full"} + }, + "storage": { + "persist_events": {"supported": true, "level": "full"}, + "persist_evidence": {"supported": true, "level": "full"}, + "persist_decisions": {"supported": false, "level": "none"}, + "persist_receipts": {"supported": true, "level": "full"}, + "preserve_original_attempted_message": {"supported": true, "level": "partial"} + } + } +} diff --git a/plugins/reporting-governance/package.json b/plugins/reporting-governance/package.json index 2b1747b..ffc4522 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/compatibility-preflight.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" + "test": "node --test test/package-structure.test.mjs test/policy-evaluator.test.mjs test/compatibility-preflight.test.mjs test/profile-artifact.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/profiles/strict-manager-mode.profile.json b/plugins/reporting-governance/profiles/strict-manager-mode.profile.json new file mode 100644 index 0000000..16fbdec --- /dev/null +++ b/plugins/reporting-governance/profiles/strict-manager-mode.profile.json @@ -0,0 +1,36 @@ +{ + "$schema": "../../../schemas/reporting-governance/deployment-profile.schema.json", + "apiVersion": "reporting-governance/v1alpha1", + "kind": "DeploymentProfileArtifact", + "metadata": { + "id": "strict-manager-mode", + "version": "1.0.0", + "runtime": "openclaw", + "source_profile": "profiles/strict-manager-mode.yaml", + "compatibility_mode": "strict_envelope" + }, + "spec": { + "package": { + "pluginVersion": "0.1.0-mainline" + }, + "bindings": { + "runtime": "openclaw", + "entrypoint": "scripts/watchdog_auto_notify_orchestrator.mjs", + "scripts": { + "watchdog": "scripts/long_task_watchdog.mjs", + "dispatcher": "scripts/operator_notify_dispatcher.mjs", + "bridgeSupervisor": "scripts/operator_notify_bridge_supervisor.mjs", + "senderBinding": "scripts/operator_notify_sender_binding.mjs", + "orchestrator": "scripts/watchdog_auto_notify_orchestrator.mjs" + }, + "artifact_roots": { + "watchdogEvidence": "state/long-task-watchdog", + "canonicalEvents": "state/long-task-watchdog-events", + "queueItems": "state/operator-notify-queue", + "spoolArtifacts": "state/operator-notify-dispatch-spool", + "bridgeReceipts": "state/operator-notify-bridge-receipts", + "senderAttempts": "state/operator-notify-sender-attempts" + } + } + } +} diff --git a/plugins/reporting-governance/src/adapters/index.mjs b/plugins/reporting-governance/src/adapters/index.mjs index b9cf8af..1d161fb 100644 --- a/plugins/reporting-governance/src/adapters/index.mjs +++ b/plugins/reporting-governance/src/adapters/index.mjs @@ -5,3 +5,4 @@ export { runSenderBindingAdapter } from './sender-binding.mjs'; export { runOrchestratorAdapter } from './orchestrator.mjs'; export { createRuntimeBinding } from './runtime-binding.mjs'; +export { loadDeploymentProfileArtifact, createDeploymentBindingContract } from '../storage/profile-artifact.mjs'; diff --git a/plugins/reporting-governance/src/reference/openclaw-watchdog-chain.md b/plugins/reporting-governance/src/reference/openclaw-watchdog-chain.md new file mode 100644 index 0000000..b1fa2a0 --- /dev/null +++ b/plugins/reporting-governance/src/reference/openclaw-watchdog-chain.md @@ -0,0 +1,49 @@ +# OpenClaw Watchdog Reference Runtime Composition + +This file places the existing watchdog auto-notify chain inside the reporting-governance package skeleton as a **reference implementation**. + +## Placement decision + +The watchdog chain is **not** part of `src/core/`. +It is categorized under `src/reference/` and represented through package adapter boundaries in `src/adapters/`. + +## Why + +The chain is runtime-specific and deployment-shaped: + +- it depends on OpenClaw execution patterns +- it depends on queue / spool / bridge / sender boundaries +- it reflects truthful delivery-state handling across runtime hops + +Those are exactly the properties of a reference adapter composition, not runtime-agnostic governance core. + +## Reference chain + +```text +scripts/long_task_watchdog.mjs + -> scripts/operator_notify_dispatcher.mjs + -> scripts/operator_notify_bridge_supervisor.mjs + -> scripts/operator_notify_sender_binding.mjs + -> acked | blocked | pending_external_send +``` + +## Package mapping + +- `src/adapters/watchdog-adapter.mjs` → watchdog trigger + canonical event seeding +- `src/adapters/dispatcher-adapter.mjs` → queue to spool handoff +- `src/adapters/bridge-adapter.mjs` → spool consumption + receipt writeback +- `src/adapters/sender-binding-adapter.mjs` → sender contract boundary +- `src/adapters/orchestrator-adapter.mjs` → deterministic composition entrypoint + +## Runtime artifact classes + +The current reference implementation still writes runtime artifacts in repo-level state directories such as: + +- `state/long-task-watchdog/` +- `state/long-task-watchdog-events/` +- `state/operator-notify-queue/` +- `state/operator-notify-dispatch-spool/` +- `state/operator-notify-bridge-receipts/` +- `state/operator-notify-sender-attempts/` + +Future package extraction should preserve these artifact classes through `src/storage/` contracts rather than raw script coupling. diff --git a/plugins/reporting-governance/src/storage/index.mjs b/plugins/reporting-governance/src/storage/index.mjs new file mode 100644 index 0000000..78afa2c --- /dev/null +++ b/plugins/reporting-governance/src/storage/index.mjs @@ -0,0 +1 @@ +export { loadDeploymentProfileArtifact, createDeploymentBindingContract } from './profile-artifact.mjs'; diff --git a/plugins/reporting-governance/src/storage/profile-artifact.mjs b/plugins/reporting-governance/src/storage/profile-artifact.mjs new file mode 100644 index 0000000..811a93d --- /dev/null +++ b/plugins/reporting-governance/src/storage/profile-artifact.mjs @@ -0,0 +1,54 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +const packageRoot = path.resolve(import.meta.dirname, '..', '..'); +const repoRoot = path.resolve(packageRoot, '..', '..'); + +function readJsonFile(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +export function resolvePackageArtifactPath(...segments) { + return path.resolve(packageRoot, ...segments); +} + +export function loadDeploymentProfileArtifact({ artifactPath, profileId } = {}) { + const resolvedPath = path.resolve( + artifactPath + ?? resolvePackageArtifactPath('profiles', `${profileId ?? 'strict-manager-mode'}.profile.json`) + ); + const artifact = readJsonFile(resolvedPath); + return { + artifactPath: resolvedPath, + artifact, + }; +} + +export function createDeploymentBindingContract({ artifact, repoRootOverride } = {}) { + if (!artifact?.spec?.bindings) { + throw new Error('deployment profile artifact bindings are required'); + } + + const root = path.resolve(repoRootOverride ?? repoRoot); + const scripts = Object.fromEntries( + Object.entries(artifact.spec.bindings.scripts ?? {}).map(([key, relativePath]) => [key, path.resolve(root, relativePath)]) + ); + const artifactRoots = Object.fromEntries( + Object.entries(artifact.spec.bindings.artifact_roots ?? {}).map(([key, relativePath]) => [key, path.resolve(root, relativePath)]) + ); + + return { + runtime: artifact.spec.bindings.runtime ?? artifact.metadata?.runtime ?? 'unknown-runtime', + entrypoint: path.resolve(root, artifact.spec.bindings.entrypoint), + pluginVersion: artifact.spec?.package?.pluginVersion ?? null, + compatibilityMode: artifact.metadata?.compatibility_mode ?? 'strict_envelope', + scripts, + artifactRoots, + }; +} + +export const __testables = { + packageRoot, + repoRoot, + readJsonFile, +}; diff --git a/plugins/reporting-governance/test/compatibility-preflight.test.mjs b/plugins/reporting-governance/test/compatibility-preflight.test.mjs index 1573734..fe7e556 100644 --- a/plugins/reporting-governance/test/compatibility-preflight.test.mjs +++ b/plugins/reporting-governance/test/compatibility-preflight.test.mjs @@ -43,7 +43,30 @@ test('runCompatibilityPreflight passes strict profile against reference descript assert.equal(result.errors.length, 0); }); -test('runCompatibilityPreflight fails closed on schema/version mismatch', () => { +test('runCompatibilityPreflight keeps legacy compatibility mode open when caller provides no compatibility envelope', () => { + 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 + }); + + assert.equal(result.status, 'pass'); + assert.equal(result.requested_profile, null); + assert.equal(result.requested_plugin_version, null); + assert.equal(result.compatibility.version_ok, true); + assert.equal(result.errors.length, 0); + assert.ok(result.compatibility.schema_checks.some((entry) => entry.key === 'decision_schema' && entry.ok === false)); + assert.ok(result.notes.some((note) => note.includes('skipped plugin version pin'))); +}); + +test('runCompatibilityPreflight fails closed on schema/version mismatch once compatibility envelope is present', () => { const brokenDescriptor = { ...capabilityDescriptor, compatibility: { diff --git a/plugins/reporting-governance/test/exports-boundary.integration.test.mjs b/plugins/reporting-governance/test/exports-boundary.integration.test.mjs index 0d3d752..8afad97 100644 --- a/plugins/reporting-governance/test/exports-boundary.integration.test.mjs +++ b/plugins/reporting-governance/test/exports-boundary.integration.test.mjs @@ -81,7 +81,7 @@ test('package root export resolves public package surface only', () => { } }); -test('adapters subpath export resolves package-owned adapter index', () => { +test('adapters subpath export resolves package-owned adapter index plus profile artifact loader helpers', () => { const root = createFixtureRoot(); try { installPackageAlias(root); @@ -93,7 +93,9 @@ test('adapters subpath export resolves package-owned adapter index', () => { `); assert.deepEqual(result.adapterKeys, [ + 'createDeploymentBindingContract', 'createRuntimeBinding', + 'loadDeploymentProfileArtifact', 'runBridgeSupervisorAdapter', 'runDispatcherAdapter', 'runOrchestratorAdapter', diff --git a/plugins/reporting-governance/test/governance-contract.integration.test.mjs b/plugins/reporting-governance/test/governance-contract.integration.test.mjs index 5623e62..b3c085f 100644 --- a/plugins/reporting-governance/test/governance-contract.integration.test.mjs +++ b/plugins/reporting-governance/test/governance-contract.integration.test.mjs @@ -131,6 +131,36 @@ test('executeGovernanceContract stays compatible for legacy callers without prof assert.deepEqual(result.contract.package_actions, ['emit_event']); }); +test('legacy caller still fails once profile supplies compatibility envelope against mismatched descriptor', () => { + 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 = executeGovernanceContract({ + event: { + type: 'silence_timeout', + payload: { checkpoint_overdue: true } + }, + capabilityDescriptor: brokenDescriptor, + policyPacks: [noSilencePack], + context: { + signals: ['checkpoint_overdue'] + }, + profile: strictProfile, + packageVersion: '0.1.0-mainline' + }); + + assert.equal(result.preflight.status, 'fail_closed'); + assert.equal(result.contract.delivery_state, 'blocked'); + assert.equal(result.contract.receipt_status, 'blocked'); + assert.ok(result.planning.receipt.notes.some((note) => note.includes('schema mismatch: decision_schema'))); +}); + test('contract truthfully degrades when notify path can queue but cannot directly dispatch', () => { const limitedDescriptor = { ...capabilityDescriptor, diff --git a/plugins/reporting-governance/test/package-structure.test.mjs b/plugins/reporting-governance/test/package-structure.test.mjs index 6a45e6e..6dae6d8 100644 --- a/plugins/reporting-governance/test/package-structure.test.mjs +++ b/plugins/reporting-governance/test/package-structure.test.mjs @@ -19,9 +19,11 @@ const requiredPaths = [ 'src/adapters/sender-binding.mjs', 'src/adapters/orchestrator.mjs', 'src/storage', + 'src/storage/profile-artifact.mjs', 'src/reference/openclaw-watchdog-chain.md', 'capabilities/openclaw-watchdog-reference.json', - 'examples/openclaw-watchdog-reference.descriptor.example.json' + 'examples/openclaw-watchdog-reference.descriptor.example.json', + 'profiles/strict-manager-mode.profile.json' ]; test('reporting-governance package skeleton paths exist', () => { diff --git a/plugins/reporting-governance/test/profile-artifact.test.mjs b/plugins/reporting-governance/test/profile-artifact.test.mjs new file mode 100644 index 0000000..d6371e9 --- /dev/null +++ b/plugins/reporting-governance/test/profile-artifact.test.mjs @@ -0,0 +1,47 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; + +import { loadDeploymentProfileArtifact, createDeploymentBindingContract } from '../src/storage/profile-artifact.mjs'; +import { createRuntimeBinding } from '../src/adapters/index.mjs'; + +const packageRoot = path.resolve(import.meta.dirname, '..'); +const repoRoot = path.resolve(packageRoot, '..', '..'); + +test('deployment profile artifact loads from package profiles and preserves compatibility envelope metadata', () => { + const { artifactPath, artifact } = loadDeploymentProfileArtifact({ profileId: 'strict-manager-mode' }); + + assert.equal(path.relative(packageRoot, artifactPath), path.join('profiles', 'strict-manager-mode.profile.json')); + assert.equal(artifact.kind, 'DeploymentProfileArtifact'); + assert.equal(artifact.metadata.id, 'strict-manager-mode'); + assert.equal(artifact.metadata.compatibility_mode, 'strict_envelope'); + assert.equal(artifact.spec.package.pluginVersion, '0.1.0-mainline'); +}); + +test('deployment binding contract resolves package artifact into real repo script and artifact paths', () => { + const { artifact } = loadDeploymentProfileArtifact({ profileId: 'strict-manager-mode' }); + const binding = createDeploymentBindingContract({ artifact }); + + assert.equal(binding.runtime, 'openclaw'); + assert.equal(binding.pluginVersion, '0.1.0-mainline'); + assert.equal(binding.compatibilityMode, 'strict_envelope'); + assert.equal(binding.entrypoint, path.resolve(repoRoot, 'scripts/watchdog_auto_notify_orchestrator.mjs')); + assert.equal(binding.scripts.watchdog, path.resolve(repoRoot, 'scripts/long_task_watchdog.mjs')); + assert.equal(binding.artifactRoots.queueItems, path.resolve(repoRoot, 'state/operator-notify-queue')); + assert.equal(fs.existsSync(binding.scripts.orchestrator), true); +}); + +test('runtime binding can be instantiated from profile artifact binding contract', () => { + const { artifact } = loadDeploymentProfileArtifact({ profileId: 'strict-manager-mode' }); + const contract = createDeploymentBindingContract({ artifact }); + const runtimeBinding = createRuntimeBinding({ + cwd: repoRoot, + scripts: contract.scripts, + }); + + assert.equal(runtimeBinding.cwd, repoRoot); + assert.equal(runtimeBinding.scripts.dispatcher, contract.scripts.dispatcher); + assert.equal(runtimeBinding.scripts.bridgeSupervisor, contract.scripts.bridgeSupervisor); + assert.equal(runtimeBinding.scripts.senderBinding, contract.scripts.senderBinding); +});