feat(reporting-governance): wire profile artifacts into contract and orchestrator
This commit is contained in:
@@ -10,6 +10,7 @@ Current purpose:
|
|||||||
- 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
|
- add one minimal package-owned deployment profile artifact / loader / binding contract slice that is executable in tests
|
||||||
|
- let profile artifacts drive one real orchestrator adapter entrypoint instead of staying test-only
|
||||||
|
|
||||||
## Package skeleton
|
## Package skeleton
|
||||||
|
|
||||||
@@ -118,9 +119,6 @@ Practical migration rule:
|
|||||||
|
|
||||||
- new integrations should always send a profile artifact or package version pin.
|
- 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.
|
- 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
|
- depend on package root exports or declared adapter subpaths only
|
||||||
- do not couple runtime integrations to repo-private file paths
|
- do not couple runtime integrations to repo-private file paths
|
||||||
- treat capability descriptors and schemas as package artifacts, but not as guaranteed JS import entrypoints unless exported later
|
- treat capability descriptors and schemas as package artifacts, but not as guaranteed JS import entrypoints unless exported later
|
||||||
@@ -151,14 +149,17 @@ This round adds one small but real package artifact path:
|
|||||||
|
|
||||||
- package artifact: `profiles/strict-manager-mode.profile.json`
|
- package artifact: `profiles/strict-manager-mode.profile.json`
|
||||||
- loader: `src/storage/profile-artifact.mjs#loadDeploymentProfileArtifact(...)`
|
- loader: `src/storage/profile-artifact.mjs#loadDeploymentProfileArtifact(...)`
|
||||||
|
- validator: `src/storage/profile-artifact.mjs#validateDeploymentProfileArtifact(...)`
|
||||||
- binding contract: `src/storage/profile-artifact.mjs#createDeploymentBindingContract(...)`
|
- binding contract: `src/storage/profile-artifact.mjs#createDeploymentBindingContract(...)`
|
||||||
|
|
||||||
What this slice does:
|
What this slice does:
|
||||||
|
|
||||||
1. package ships a profile artifact snapshot under package boundary
|
1. package ships a profile artifact snapshot under package boundary
|
||||||
2. loader resolves that artifact from package-local path
|
2. loader resolves that artifact from package-local path
|
||||||
3. binding contract translates profile-declared script/artifact roots into concrete repo/runtime paths
|
3. validator fail-closes minimal boundary drift on `kind`, `apiVersion`, `spec.bindings.entrypoint`, `scripts`, `artifact_roots`, and `spec.package.pluginVersion`
|
||||||
4. adapter runtime binding can be instantiated from that contract in tests
|
4. binding contract translates profile-declared script/artifact roots into concrete repo/runtime paths
|
||||||
|
5. adapter runtime binding can be instantiated from that contract in tests
|
||||||
|
6. orchestrator adapter can now bootstrap from package profile artifact input directly
|
||||||
|
|
||||||
What this slice does **not** claim yet:
|
What this slice does **not** claim yet:
|
||||||
|
|
||||||
@@ -169,14 +170,6 @@ What this slice does **not** claim yet:
|
|||||||
|
|
||||||
It is intentionally the smallest verifiable step that proves package profile artifacts are executable inputs rather than documentation only.
|
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:
|
||||||
@@ -195,6 +188,7 @@ Current package-core responsibilities:
|
|||||||
- convert a canonical decision into an execution plan, enforcement intent, and receipt skeleton
|
- convert a canonical decision into an execution plan, enforcement intent, and receipt skeleton
|
||||||
- truthfully degrade unsupported enforcement paths based on the capability descriptor
|
- truthfully degrade unsupported enforcement paths based on the capability descriptor
|
||||||
- provide one minimal contract path from `capability descriptor -> policy decision -> execution planning`
|
- provide one minimal contract path from `capability descriptor -> policy decision -> execution planning`
|
||||||
|
- surface deployment binding metadata when caller passes a validated profile artifact
|
||||||
|
|
||||||
Still **runtime-adapter responsibility** at this stage:
|
Still **runtime-adapter responsibility** at this stage:
|
||||||
|
|
||||||
@@ -213,13 +207,15 @@ This slice now has one small but testable contract path:
|
|||||||
1. capability descriptor advertises real enforcement support
|
1. capability descriptor advertises real enforcement support
|
||||||
2. policy evaluator emits a canonical decision from event/evidence/context
|
2. policy evaluator emits a canonical decision from event/evidence/context
|
||||||
3. decision runner converts that decision into execution planning
|
3. decision runner converts that decision into execution planning
|
||||||
4. the result declares:
|
4. validated profile artifact can supply deployment binding metadata
|
||||||
|
5. orchestrator adapter can consume profile artifact bindings and run one real runtime layer
|
||||||
|
6. the result declares:
|
||||||
- adapter-dispatch actions required
|
- adapter-dispatch actions required
|
||||||
- package-core actions possible locally
|
- package-core actions possible locally
|
||||||
- blocked mandatory actions when capability support is missing
|
- blocked mandatory actions when capability support is missing
|
||||||
- truthful delivery / receipt state
|
- truthful delivery / receipt state
|
||||||
|
|
||||||
This is intentionally **planning-level end-to-end**, not full live inline interception.
|
This is intentionally **planning-level end-to-end plus one adapter bootstrap layer**, not full live inline interception.
|
||||||
It proves contract alignment without pretending all runtime enforcement is already extracted.
|
It proves contract alignment without pretending all runtime enforcement is already extracted.
|
||||||
|
|
||||||
## Not yet included
|
## Not yet included
|
||||||
@@ -232,4 +228,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, a compatibility-envelope boundary, and a minimal package profile artifact/binding slice, 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, a minimal package profile artifact/binding slice, and one profile-driven orchestrator path, but the remaining enforcement surface is still intentionally honest about adapter gaps.
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { ensureSuccess, parseJsonStdout, runNodeScript } from './_script-runner.mjs';
|
import { ensureSuccess, parseJsonStdout, runNodeScript } from './_script-runner.mjs';
|
||||||
import { createRuntimeBinding, resolveScriptPath } from './runtime-binding.mjs';
|
import { createRuntimeBinding, resolveScriptPath } from './runtime-binding.mjs';
|
||||||
|
import { loadDeploymentProfileArtifact, createDeploymentBindingContract } from '../storage/profile-artifact.mjs';
|
||||||
|
|
||||||
export function runOrchestratorAdapter({
|
export function runOrchestratorAdapter({
|
||||||
scriptPath = null,
|
scriptPath = null,
|
||||||
runtimeBinding = null,
|
runtimeBinding = null,
|
||||||
|
profileArtifact = null,
|
||||||
|
profileArtifactPath = null,
|
||||||
|
profileId = null,
|
||||||
|
repoRootOverride = null,
|
||||||
state,
|
state,
|
||||||
evidenceDir,
|
evidenceDir,
|
||||||
eventDir,
|
eventDir,
|
||||||
@@ -23,8 +28,18 @@ export function runOrchestratorAdapter({
|
|||||||
claim = false,
|
claim = false,
|
||||||
dryRun = false,
|
dryRun = false,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const binding = runtimeBinding ?? createRuntimeBinding();
|
const deploymentBinding = profileArtifact || profileArtifactPath || profileId
|
||||||
const resolvedScriptPath = path.resolve(scriptPath ?? resolveScriptPath('orchestrator', { runtimeBinding: binding }));
|
? createDeploymentBindingContract({
|
||||||
|
artifact: profileArtifact ?? loadDeploymentProfileArtifact({ artifactPath: profileArtifactPath, profileId }).artifact,
|
||||||
|
repoRootOverride,
|
||||||
|
})
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const binding = runtimeBinding ?? createRuntimeBinding({
|
||||||
|
cwd: repoRootOverride,
|
||||||
|
scripts: deploymentBinding?.scripts,
|
||||||
|
});
|
||||||
|
const resolvedScriptPath = path.resolve(scriptPath ?? deploymentBinding?.entrypoint ?? resolveScriptPath('orchestrator', { runtimeBinding: binding }));
|
||||||
const resolvedWatchdogScript = path.resolve(watchdogScript ?? resolveScriptPath('watchdog', { runtimeBinding: binding }));
|
const resolvedWatchdogScript = path.resolve(watchdogScript ?? resolveScriptPath('watchdog', { runtimeBinding: binding }));
|
||||||
const resolvedDispatcherScript = path.resolve(dispatcherScript ?? resolveScriptPath('dispatcher', { runtimeBinding: binding }));
|
const resolvedDispatcherScript = path.resolve(dispatcherScript ?? resolveScriptPath('dispatcher', { runtimeBinding: binding }));
|
||||||
const resolvedSupervisorScript = path.resolve(supervisorScript ?? resolveScriptPath('bridgeSupervisor', { runtimeBinding: binding }));
|
const resolvedSupervisorScript = path.resolve(supervisorScript ?? resolveScriptPath('bridgeSupervisor', { runtimeBinding: binding }));
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { evaluatePolicies } from './policy-evaluator.mjs';
|
import { evaluatePolicies } from './policy-evaluator.mjs';
|
||||||
import { runCompatibilityPreflight } from './compatibility-preflight.mjs';
|
import { runCompatibilityPreflight } from './compatibility-preflight.mjs';
|
||||||
import { planDecisionExecution } from './decision-runner.mjs';
|
import { planDecisionExecution } from './decision-runner.mjs';
|
||||||
|
import { createDeploymentBindingContract } from '../storage/profile-artifact.mjs';
|
||||||
|
|
||||||
function createBlockedReceipt({ evaluation, preflight }) {
|
function createBlockedReceipt({ evaluation, preflight }) {
|
||||||
return {
|
return {
|
||||||
@@ -57,6 +58,7 @@ export function executeGovernanceContract({
|
|||||||
context = {},
|
context = {},
|
||||||
profile = {},
|
profile = {},
|
||||||
packageVersion,
|
packageVersion,
|
||||||
|
repoRootOverride,
|
||||||
} = {}) {
|
} = {}) {
|
||||||
const evaluation = evaluatePolicies({
|
const evaluation = evaluatePolicies({
|
||||||
event,
|
event,
|
||||||
@@ -81,10 +83,15 @@ export function executeGovernanceContract({
|
|||||||
capabilityDescriptor,
|
capabilityDescriptor,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const deploymentBinding = profile?.spec?.bindings
|
||||||
|
? createDeploymentBindingContract({ artifact: profile, repoRootOverride })
|
||||||
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
evaluation,
|
evaluation,
|
||||||
preflight,
|
preflight,
|
||||||
planning,
|
planning,
|
||||||
|
deploymentBinding,
|
||||||
contract: {
|
contract: {
|
||||||
runtime: capabilityDescriptor?.metadata?.id ?? capabilityDescriptor?.runtime?.name ?? 'unknown-runtime',
|
runtime: capabilityDescriptor?.metadata?.id ?? capabilityDescriptor?.runtime?.name ?? 'unknown-runtime',
|
||||||
policy_id: evaluation.decision.policy_id,
|
policy_id: evaluation.decision.policy_id,
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ scripts/long_task_watchdog.mjs
|
|||||||
|
|
||||||
## Package mapping
|
## Package mapping
|
||||||
|
|
||||||
- `src/adapters/watchdog-adapter.mjs` → watchdog trigger + canonical event seeding
|
- `src/adapters/watchdog.mjs` → watchdog trigger + canonical event seeding
|
||||||
- `src/adapters/dispatcher-adapter.mjs` → queue to spool handoff
|
- `src/adapters/dispatcher.mjs` → queue to spool handoff
|
||||||
- `src/adapters/bridge-adapter.mjs` → spool consumption + receipt writeback
|
- `src/adapters/bridge-supervisor.mjs` → spool consumption + receipt writeback
|
||||||
- `src/adapters/sender-binding-adapter.mjs` → sender contract boundary
|
- `src/adapters/sender-binding.mjs` → sender contract boundary
|
||||||
- `src/adapters/orchestrator-adapter.mjs` → deterministic composition entrypoint
|
- `src/adapters/orchestrator.mjs` → deterministic composition entrypoint
|
||||||
|
|
||||||
## Runtime artifact classes
|
## Runtime artifact classes
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,58 @@ import path from 'node:path';
|
|||||||
|
|
||||||
const packageRoot = path.resolve(import.meta.dirname, '..', '..');
|
const packageRoot = path.resolve(import.meta.dirname, '..', '..');
|
||||||
const repoRoot = path.resolve(packageRoot, '..', '..');
|
const repoRoot = path.resolve(packageRoot, '..', '..');
|
||||||
|
const EXPECTED_KIND = 'DeploymentProfileArtifact';
|
||||||
|
const EXPECTED_API_VERSION = 'reporting-governance/v1alpha1';
|
||||||
|
|
||||||
function readJsonFile(filePath) {
|
function readJsonFile(filePath) {
|
||||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertNonEmptyString(value, label) {
|
||||||
|
if (typeof value !== 'string' || value.trim() === '') {
|
||||||
|
throw new Error(`${label} must be a non-empty string`);
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertObjectRecord(value, label) {
|
||||||
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||||
|
throw new Error(`${label} must be an object record`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateDeploymentProfileArtifact(artifact) {
|
||||||
|
if (!artifact || typeof artifact !== 'object' || Array.isArray(artifact)) {
|
||||||
|
throw new Error('deployment profile artifact must be an object');
|
||||||
|
}
|
||||||
|
if (artifact.kind !== EXPECTED_KIND) {
|
||||||
|
throw new Error(`deployment profile artifact kind must be ${EXPECTED_KIND}`);
|
||||||
|
}
|
||||||
|
if (artifact.apiVersion !== EXPECTED_API_VERSION) {
|
||||||
|
throw new Error(`deployment profile artifact apiVersion must be ${EXPECTED_API_VERSION}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bindings = artifact?.spec?.bindings;
|
||||||
|
if (!bindings || typeof bindings !== 'object' || Array.isArray(bindings)) {
|
||||||
|
throw new Error('deployment profile artifact bindings are required');
|
||||||
|
}
|
||||||
|
|
||||||
|
assertNonEmptyString(bindings.entrypoint, 'deployment profile artifact spec.bindings.entrypoint');
|
||||||
|
const scripts = assertObjectRecord(bindings.scripts, 'deployment profile artifact spec.bindings.scripts');
|
||||||
|
const artifactRoots = assertObjectRecord(bindings.artifact_roots, 'deployment profile artifact spec.bindings.artifact_roots');
|
||||||
|
assertNonEmptyString(artifact?.spec?.package?.pluginVersion, 'deployment profile artifact spec.package.pluginVersion');
|
||||||
|
|
||||||
|
for (const [key, relativePath] of Object.entries(scripts)) {
|
||||||
|
assertNonEmptyString(relativePath, `deployment profile artifact spec.bindings.scripts.${key}`);
|
||||||
|
}
|
||||||
|
for (const [key, relativePath] of Object.entries(artifactRoots)) {
|
||||||
|
assertNonEmptyString(relativePath, `deployment profile artifact spec.bindings.artifact_roots.${key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return artifact;
|
||||||
|
}
|
||||||
|
|
||||||
export function resolvePackageArtifactPath(...segments) {
|
export function resolvePackageArtifactPath(...segments) {
|
||||||
return path.resolve(packageRoot, ...segments);
|
return path.resolve(packageRoot, ...segments);
|
||||||
}
|
}
|
||||||
@@ -17,7 +64,7 @@ export function loadDeploymentProfileArtifact({ artifactPath, profileId } = {})
|
|||||||
artifactPath
|
artifactPath
|
||||||
?? resolvePackageArtifactPath('profiles', `${profileId ?? 'strict-manager-mode'}.profile.json`)
|
?? resolvePackageArtifactPath('profiles', `${profileId ?? 'strict-manager-mode'}.profile.json`)
|
||||||
);
|
);
|
||||||
const artifact = readJsonFile(resolvedPath);
|
const artifact = validateDeploymentProfileArtifact(readJsonFile(resolvedPath));
|
||||||
return {
|
return {
|
||||||
artifactPath: resolvedPath,
|
artifactPath: resolvedPath,
|
||||||
artifact,
|
artifact,
|
||||||
@@ -25,30 +72,30 @@ export function loadDeploymentProfileArtifact({ artifactPath, profileId } = {})
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function createDeploymentBindingContract({ artifact, repoRootOverride } = {}) {
|
export function createDeploymentBindingContract({ artifact, repoRootOverride } = {}) {
|
||||||
if (!artifact?.spec?.bindings) {
|
const validatedArtifact = validateDeploymentProfileArtifact(artifact);
|
||||||
throw new Error('deployment profile artifact bindings are required');
|
|
||||||
}
|
|
||||||
|
|
||||||
const root = path.resolve(repoRootOverride ?? repoRoot);
|
const root = path.resolve(repoRootOverride ?? repoRoot);
|
||||||
const scripts = Object.fromEntries(
|
const scripts = Object.fromEntries(
|
||||||
Object.entries(artifact.spec.bindings.scripts ?? {}).map(([key, relativePath]) => [key, path.resolve(root, relativePath)])
|
Object.entries(validatedArtifact.spec.bindings.scripts).map(([key, relativePath]) => [key, path.resolve(root, relativePath)])
|
||||||
);
|
);
|
||||||
const artifactRoots = Object.fromEntries(
|
const artifactRoots = Object.fromEntries(
|
||||||
Object.entries(artifact.spec.bindings.artifact_roots ?? {}).map(([key, relativePath]) => [key, path.resolve(root, relativePath)])
|
Object.entries(validatedArtifact.spec.bindings.artifact_roots).map(([key, relativePath]) => [key, path.resolve(root, relativePath)])
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
runtime: artifact.spec.bindings.runtime ?? artifact.metadata?.runtime ?? 'unknown-runtime',
|
runtime: validatedArtifact.spec.bindings.runtime ?? validatedArtifact.metadata?.runtime ?? 'unknown-runtime',
|
||||||
entrypoint: path.resolve(root, artifact.spec.bindings.entrypoint),
|
entrypoint: path.resolve(root, validatedArtifact.spec.bindings.entrypoint),
|
||||||
pluginVersion: artifact.spec?.package?.pluginVersion ?? null,
|
pluginVersion: validatedArtifact.spec.package.pluginVersion,
|
||||||
compatibilityMode: artifact.metadata?.compatibility_mode ?? 'strict_envelope',
|
compatibilityMode: validatedArtifact.metadata?.compatibility_mode ?? 'strict_envelope',
|
||||||
scripts,
|
scripts,
|
||||||
artifactRoots,
|
artifactRoots,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const __testables = {
|
export const __testables = {
|
||||||
|
EXPECTED_KIND,
|
||||||
|
EXPECTED_API_VERSION,
|
||||||
packageRoot,
|
packageRoot,
|
||||||
repoRoot,
|
repoRoot,
|
||||||
readJsonFile,
|
readJsonFile,
|
||||||
|
validateDeploymentProfileArtifact,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
import { executeGovernanceContract, runCompatibilityPreflight } from '../src/core/index.mjs';
|
import { executeGovernanceContract, runCompatibilityPreflight } from '../src/core/index.mjs';
|
||||||
import capabilityDescriptor from '../capabilities/openclaw-watchdog-reference.json' with { type: 'json' };
|
import capabilityDescriptor from '../capabilities/openclaw-watchdog-reference.json' with { type: 'json' };
|
||||||
|
|
||||||
|
const packageRoot = path.resolve(import.meta.dirname, '..');
|
||||||
|
const repoRoot = path.resolve(packageRoot, '..', '..');
|
||||||
|
|
||||||
const noSilencePack = {
|
const noSilencePack = {
|
||||||
metadata: { id: 'no-silence', severity_default: 'high' },
|
metadata: { id: 'no-silence', severity_default: 'high' },
|
||||||
spec: {
|
spec: {
|
||||||
@@ -294,3 +298,53 @@ test('schema/version mismatch blocks contract before any runnable plan is produc
|
|||||||
assert.ok(result.planning.receipt.notes.some((note) => note.includes('schema mismatch: decision_schema')));
|
assert.ok(result.planning.receipt.notes.some((note) => note.includes('schema mismatch: decision_schema')));
|
||||||
assert.ok(result.planning.receipt.notes.some((note) => note.includes('plugin version 0.1.0-mainline is not declared compatible')));
|
assert.ok(result.planning.receipt.notes.some((note) => note.includes('plugin version 0.1.0-mainline is not declared compatible')));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('executeGovernanceContract exposes deployment binding when profile artifact bindings are provided', () => {
|
||||||
|
const profileArtifact = {
|
||||||
|
...strictProfile,
|
||||||
|
kind: 'DeploymentProfileArtifact',
|
||||||
|
apiVersion: 'reporting-governance/v1alpha1',
|
||||||
|
metadata: {
|
||||||
|
...strictProfile.metadata,
|
||||||
|
runtime: 'openclaw',
|
||||||
|
compatibility_mode: 'strict_envelope',
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
...strictProfile.spec,
|
||||||
|
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: {
|
||||||
|
queueItems: 'state/operator-notify-queue'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = executeGovernanceContract({
|
||||||
|
event: {
|
||||||
|
type: 'silence_timeout',
|
||||||
|
payload: { checkpoint_overdue: true }
|
||||||
|
},
|
||||||
|
capabilityDescriptor,
|
||||||
|
policyPacks: [noSilencePack],
|
||||||
|
context: {
|
||||||
|
signals: ['checkpoint_overdue']
|
||||||
|
},
|
||||||
|
profile: profileArtifact,
|
||||||
|
packageVersion: '0.1.0-mainline',
|
||||||
|
repoRootOverride: repoRoot,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.preflight.status, 'pass');
|
||||||
|
assert.equal(result.deploymentBinding.entrypoint, path.resolve(repoRoot, 'scripts/watchdog_auto_notify_orchestrator.mjs'));
|
||||||
|
assert.equal(result.deploymentBinding.scripts.dispatcher, path.resolve(repoRoot, 'scripts/operator_notify_dispatcher.mjs'));
|
||||||
|
assert.equal(result.deploymentBinding.artifactRoots.queueItems, path.resolve(repoRoot, 'state/operator-notify-queue'));
|
||||||
|
});
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import assert from 'node:assert/strict';
|
|||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
import { loadDeploymentProfileArtifact, createDeploymentBindingContract } from '../src/storage/profile-artifact.mjs';
|
import {
|
||||||
|
loadDeploymentProfileArtifact,
|
||||||
|
createDeploymentBindingContract,
|
||||||
|
validateDeploymentProfileArtifact,
|
||||||
|
} from '../src/storage/profile-artifact.mjs';
|
||||||
import { createRuntimeBinding } from '../src/adapters/index.mjs';
|
import { createRuntimeBinding } from '../src/adapters/index.mjs';
|
||||||
|
|
||||||
const packageRoot = path.resolve(import.meta.dirname, '..');
|
const packageRoot = path.resolve(import.meta.dirname, '..');
|
||||||
@@ -45,3 +49,58 @@ test('runtime binding can be instantiated from profile artifact binding contract
|
|||||||
assert.equal(runtimeBinding.scripts.bridgeSupervisor, contract.scripts.bridgeSupervisor);
|
assert.equal(runtimeBinding.scripts.bridgeSupervisor, contract.scripts.bridgeSupervisor);
|
||||||
assert.equal(runtimeBinding.scripts.senderBinding, contract.scripts.senderBinding);
|
assert.equal(runtimeBinding.scripts.senderBinding, contract.scripts.senderBinding);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('deployment profile artifact validation fails closed on boundary drift', () => {
|
||||||
|
assert.throws(
|
||||||
|
() => validateDeploymentProfileArtifact({}),
|
||||||
|
/kind must be DeploymentProfileArtifact/
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => validateDeploymentProfileArtifact({
|
||||||
|
kind: 'DeploymentProfileArtifact',
|
||||||
|
apiVersion: 'reporting-governance/v1alpha1',
|
||||||
|
spec: {
|
||||||
|
package: { pluginVersion: '0.1.0-mainline' },
|
||||||
|
bindings: {
|
||||||
|
entrypoint: '',
|
||||||
|
scripts: { watchdog: 'scripts/long_task_watchdog.mjs' },
|
||||||
|
artifact_roots: { queueItems: 'state/operator-notify-queue' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
/spec\.bindings\.entrypoint must be a non-empty string/
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => validateDeploymentProfileArtifact({
|
||||||
|
kind: 'DeploymentProfileArtifact',
|
||||||
|
apiVersion: 'reporting-governance/v1alpha1',
|
||||||
|
spec: {
|
||||||
|
package: { pluginVersion: '' },
|
||||||
|
bindings: {
|
||||||
|
entrypoint: 'scripts/watchdog_auto_notify_orchestrator.mjs',
|
||||||
|
scripts: { watchdog: '' },
|
||||||
|
artifact_roots: { queueItems: 'state/operator-notify-queue' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
/spec\.package\.pluginVersion must be a non-empty string/
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() => validateDeploymentProfileArtifact({
|
||||||
|
kind: 'DeploymentProfileArtifact',
|
||||||
|
apiVersion: 'reporting-governance/v1alpha1',
|
||||||
|
spec: {
|
||||||
|
package: { pluginVersion: '0.1.0-mainline' },
|
||||||
|
bindings: {
|
||||||
|
entrypoint: 'scripts/watchdog_auto_notify_orchestrator.mjs',
|
||||||
|
scripts: [],
|
||||||
|
artifact_roots: { queueItems: 'state/operator-notify-queue' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
/spec\.bindings\.scripts must be an object record/
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import {
|
|||||||
runWatchdogChain,
|
runWatchdogChain,
|
||||||
} from '../src/index.mjs';
|
} from '../src/index.mjs';
|
||||||
|
|
||||||
|
const packageRoot = path.resolve(import.meta.dirname, '..');
|
||||||
|
|
||||||
function createFixtureRoot() {
|
function createFixtureRoot() {
|
||||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'reporting-governance-plugin-'));
|
return fs.mkdtempSync(path.join(os.tmpdir(), 'reporting-governance-plugin-'));
|
||||||
}
|
}
|
||||||
@@ -115,3 +117,35 @@ test('dry-run path produces verifiable pending receipt via package adapter', ()
|
|||||||
fs.rmSync(root, { recursive: true, force: true });
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('orchestrator adapter can bootstrap from profile artifact loader path', () => {
|
||||||
|
const root = createFixtureRoot();
|
||||||
|
try {
|
||||||
|
mkdirs(root, ['evidence', 'events', 'queue', 'spool', 'receipts']);
|
||||||
|
const statePath = writeState(root);
|
||||||
|
|
||||||
|
const result = runOrchestratorAdapter({
|
||||||
|
profileId: 'strict-manager-mode',
|
||||||
|
repoRootOverride: path.resolve(packageRoot, '..', '..'),
|
||||||
|
state: statePath,
|
||||||
|
evidenceDir: path.join(root, 'evidence'),
|
||||||
|
eventDir: path.join(root, 'events'),
|
||||||
|
queueDir: path.join(root, 'queue'),
|
||||||
|
spoolDir: path.join(root, 'spool'),
|
||||||
|
receiptDir: path.join(root, 'receipts'),
|
||||||
|
writeState: true,
|
||||||
|
dryRun: true,
|
||||||
|
now: '2026-05-07T08:20:00.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.result.watchdog.notificationCount, 1);
|
||||||
|
assert.equal(result.result.dispatcher.dispatchedCount, 1);
|
||||||
|
assert.equal(result.result.supervisor.pendingCount, 1);
|
||||||
|
|
||||||
|
const receipt = readSingleJson(path.join(root, 'receipts')).payload;
|
||||||
|
assert.equal(receipt.state, 'pending_external_send');
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user