feat(reporting-governance): add profile artifact binding slice
This commit is contained in:
@@ -177,7 +177,59 @@ This matters architecturally because it proves the plugin can:
|
|||||||
- preserve honest delivery semantics instead of collapsing `dispatched` into false success
|
- 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
|
- 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:
|
Primary follow-on specs:
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Current purpose:
|
|||||||
- fix boundaries between `core/`, `adapters/`, `storage/`, and reference implementations
|
- fix boundaries between `core/`, `adapters/`, `storage/`, and reference implementations
|
||||||
- prepare the next implementation round for evaluator / decision-runner extraction
|
- 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
|
- 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
|
## Package skeleton
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ plugins/reporting-governance/
|
|||||||
package.json
|
package.json
|
||||||
README.md
|
README.md
|
||||||
capabilities/
|
capabilities/
|
||||||
|
profiles/
|
||||||
docs/
|
docs/
|
||||||
examples/
|
examples/
|
||||||
src/
|
src/
|
||||||
@@ -62,6 +64,7 @@ Durable I/O contracts for governance artifacts:
|
|||||||
- queue items
|
- queue items
|
||||||
- spool artifacts
|
- spool artifacts
|
||||||
- receipts
|
- receipts
|
||||||
|
- decision/profile/package artifacts
|
||||||
- future decisions / audit manifests
|
- future decisions / audit manifests
|
||||||
|
|
||||||
### `src/reference/`
|
### `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`.
|
- 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.
|
- 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:
|
Practical migration rule:
|
||||||
|
|
||||||
- depend on package root exports or declared adapter subpaths only
|
- depend on package root exports or declared adapter subpaths only
|
||||||
@@ -117,6 +137,7 @@ Package-home documentation:
|
|||||||
|
|
||||||
- `src/reference/openclaw-watchdog-chain.md`
|
- `src/reference/openclaw-watchdog-chain.md`
|
||||||
- `capabilities/openclaw-watchdog-reference.json`
|
- `capabilities/openclaw-watchdog-reference.json`
|
||||||
|
- `profiles/strict-manager-mode.profile.json`
|
||||||
|
|
||||||
Mainline background specs remain in:
|
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-adapter-interface.md`
|
||||||
- `docs/specs/reporting-governance-deployment-model.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
|
## Minimal evaluator / decision runner now included
|
||||||
|
|
||||||
The current package now includes a small but runnable `core/` implementation:
|
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
|
- complete rewrite / placeholder / review / status-downgrade adapter execution
|
||||||
- non-watchdog full runtime governance interception
|
- 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"
|
"./adapters/orchestrator": "./src/adapters/orchestrator.mjs"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"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 { runOrchestratorAdapter } from './orchestrator.mjs';
|
||||||
|
|
||||||
export { createRuntimeBinding } from './runtime-binding.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);
|
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 = {
|
const brokenDescriptor = {
|
||||||
...capabilityDescriptor,
|
...capabilityDescriptor,
|
||||||
compatibility: {
|
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();
|
const root = createFixtureRoot();
|
||||||
try {
|
try {
|
||||||
installPackageAlias(root);
|
installPackageAlias(root);
|
||||||
@@ -93,7 +93,9 @@ test('adapters subpath export resolves package-owned adapter index', () => {
|
|||||||
`);
|
`);
|
||||||
|
|
||||||
assert.deepEqual(result.adapterKeys, [
|
assert.deepEqual(result.adapterKeys, [
|
||||||
|
'createDeploymentBindingContract',
|
||||||
'createRuntimeBinding',
|
'createRuntimeBinding',
|
||||||
|
'loadDeploymentProfileArtifact',
|
||||||
'runBridgeSupervisorAdapter',
|
'runBridgeSupervisorAdapter',
|
||||||
'runDispatcherAdapter',
|
'runDispatcherAdapter',
|
||||||
'runOrchestratorAdapter',
|
'runOrchestratorAdapter',
|
||||||
|
|||||||
@@ -131,6 +131,36 @@ test('executeGovernanceContract stays compatible for legacy callers without prof
|
|||||||
assert.deepEqual(result.contract.package_actions, ['emit_event']);
|
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', () => {
|
test('contract truthfully degrades when notify path can queue but cannot directly dispatch', () => {
|
||||||
const limitedDescriptor = {
|
const limitedDescriptor = {
|
||||||
...capabilityDescriptor,
|
...capabilityDescriptor,
|
||||||
|
|||||||
@@ -19,9 +19,11 @@ const requiredPaths = [
|
|||||||
'src/adapters/sender-binding.mjs',
|
'src/adapters/sender-binding.mjs',
|
||||||
'src/adapters/orchestrator.mjs',
|
'src/adapters/orchestrator.mjs',
|
||||||
'src/storage',
|
'src/storage',
|
||||||
|
'src/storage/profile-artifact.mjs',
|
||||||
'src/reference/openclaw-watchdog-chain.md',
|
'src/reference/openclaw-watchdog-chain.md',
|
||||||
'capabilities/openclaw-watchdog-reference.json',
|
'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', () => {
|
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