feat(reporting-governance): add profile artifact binding slice
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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."
|
||||
]
|
||||
}
|
||||
@@ -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"}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
1
plugins/reporting-governance/src/storage/index.mjs
Normal file
1
plugins/reporting-governance/src/storage/index.mjs
Normal file
@@ -0,0 +1 @@
|
||||
export { loadDeploymentProfileArtifact, createDeploymentBindingContract } from './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,
|
||||
};
|
||||
@@ -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: {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
47
plugins/reporting-governance/test/profile-artifact.test.mjs
Normal file
47
plugins/reporting-governance/test/profile-artifact.test.mjs
Normal file
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user