diff --git a/plugins/reporting-governance/package.json b/plugins/reporting-governance/package.json index c64a043..2424239 100644 --- a/plugins/reporting-governance/package.json +++ b/plugins/reporting-governance/package.json @@ -14,6 +14,6 @@ "./adapters/orchestrator": "./src/adapters/orchestrator.mjs" }, "scripts": { - "test": "node --test test/package-structure.test.mjs test/policy-evaluator.test.mjs test/decision-runner.test.mjs test/watchdog-chain.integration.test.mjs" + "test": "node --test test/package-structure.test.mjs test/policy-evaluator.test.mjs test/decision-runner.test.mjs test/watchdog-chain.integration.test.mjs test/exports-boundary.integration.test.mjs" } } diff --git a/plugins/reporting-governance/src/adapters/_script-runner.mjs b/plugins/reporting-governance/src/adapters/_script-runner.mjs index d8b2223..f43f32d 100644 --- a/plugins/reporting-governance/src/adapters/_script-runner.mjs +++ b/plugins/reporting-governance/src/adapters/_script-runner.mjs @@ -1,17 +1,11 @@ -import path from 'node:path'; import process from 'node:process'; import { spawnSync } from 'node:child_process'; - -const packageRoot = path.resolve(import.meta.dirname, '..', '..'); -const repoRoot = path.resolve(packageRoot, '..', '..'); - -export function resolveRepoPath(...segments) { - return path.join(repoRoot, ...segments); -} +import { createRuntimeBinding, packageRoot, repoRoot, resolveRepoPath } from './runtime-binding.mjs'; export function runNodeScript(scriptPath, args = [], options = {}) { + const runtimeBinding = options.runtimeBinding ?? createRuntimeBinding(options); return spawnSync(process.execPath, [scriptPath, ...args], { - cwd: repoRoot, + cwd: runtimeBinding.cwd, encoding: 'utf8', ...options, }); @@ -32,4 +26,4 @@ export function parseJsonStdout(label, result) { } } -export { packageRoot, repoRoot }; +export { packageRoot, repoRoot, resolveRepoPath }; diff --git a/plugins/reporting-governance/src/adapters/bridge-supervisor.mjs b/plugins/reporting-governance/src/adapters/bridge-supervisor.mjs index 82635da..7171e36 100644 --- a/plugins/reporting-governance/src/adapters/bridge-supervisor.mjs +++ b/plugins/reporting-governance/src/adapters/bridge-supervisor.mjs @@ -1,31 +1,34 @@ import path from 'node:path'; -import { ensureSuccess, parseJsonStdout, resolveRepoPath, runNodeScript } from './_script-runner.mjs'; - -const DEFAULT_SCRIPT = resolveRepoPath('scripts', 'operator_notify_bridge_supervisor.mjs'); -const DEFAULT_DISPATCHER_SCRIPT = resolveRepoPath('scripts', 'operator_notify_dispatcher.mjs'); +import { ensureSuccess, parseJsonStdout, runNodeScript } from './_script-runner.mjs'; +import { createRuntimeBinding, resolveScriptPath } from './runtime-binding.mjs'; export function runBridgeSupervisorAdapter({ - scriptPath = DEFAULT_SCRIPT, + scriptPath = null, + runtimeBinding = null, spoolDir, queueDir, receiptDir, - dispatcherScript = DEFAULT_DISPATCHER_SCRIPT, + dispatcherScript = null, senderCommand = null, now = null, compact = true, dryRun = false, } = {}) { + const binding = runtimeBinding ?? createRuntimeBinding(); + const resolvedScriptPath = path.resolve(scriptPath ?? resolveScriptPath('bridgeSupervisor', { runtimeBinding: binding })); + const resolvedDispatcherScript = path.resolve(dispatcherScript ?? resolveScriptPath('dispatcher', { runtimeBinding: binding })); + const args = []; if (spoolDir) args.push('--spool-dir', path.resolve(spoolDir)); if (queueDir) args.push('--queue-dir', path.resolve(queueDir)); if (receiptDir) args.push('--receipt-dir', path.resolve(receiptDir)); - if (dispatcherScript) args.push('--dispatcher-script', path.resolve(dispatcherScript)); + if (resolvedDispatcherScript) args.push('--dispatcher-script', resolvedDispatcherScript); if (senderCommand) args.push('--sender-command', senderCommand); if (now) args.push('--now', now); if (dryRun) args.push('--dry-run'); if (compact) args.push('--compact'); - const result = runNodeScript(path.resolve(scriptPath), args); + const result = runNodeScript(resolvedScriptPath, args, { runtimeBinding: binding }); ensureSuccess('bridge supervisor adapter', result); return parseJsonStdout('bridge supervisor adapter', result); } diff --git a/plugins/reporting-governance/src/adapters/dispatcher.mjs b/plugins/reporting-governance/src/adapters/dispatcher.mjs index 57164a2..8c41204 100644 --- a/plugins/reporting-governance/src/adapters/dispatcher.mjs +++ b/plugins/reporting-governance/src/adapters/dispatcher.mjs @@ -1,10 +1,10 @@ import path from 'node:path'; -import { ensureSuccess, parseJsonStdout, resolveRepoPath, runNodeScript } from './_script-runner.mjs'; - -const DEFAULT_SCRIPT = resolveRepoPath('scripts', 'operator_notify_dispatcher.mjs'); +import { ensureSuccess, parseJsonStdout, runNodeScript } from './_script-runner.mjs'; +import { createRuntimeBinding, resolveScriptPath } from './runtime-binding.mjs'; export function runDispatcherAdapter({ - scriptPath = DEFAULT_SCRIPT, + scriptPath = null, + runtimeBinding = null, queueDir, spoolDir, now = null, @@ -14,6 +14,9 @@ export function runDispatcherAdapter({ block = null, note = null, } = {}) { + const binding = runtimeBinding ?? createRuntimeBinding(); + const resolvedScriptPath = path.resolve(scriptPath ?? resolveScriptPath('dispatcher', { runtimeBinding: binding })); + const args = []; if (queueDir) args.push('--queue-dir', path.resolve(queueDir)); if (spoolDir) args.push('--spool-dir', path.resolve(spoolDir)); @@ -24,7 +27,7 @@ export function runDispatcherAdapter({ if (note) args.push('--note', note); if (compact) args.push('--compact'); - const result = runNodeScript(path.resolve(scriptPath), args); + const result = runNodeScript(resolvedScriptPath, args, { runtimeBinding: binding }); ensureSuccess('dispatcher adapter', result); return parseJsonStdout('dispatcher adapter', result); } diff --git a/plugins/reporting-governance/src/adapters/index.mjs b/plugins/reporting-governance/src/adapters/index.mjs index e4bbf3c..d8d526a 100644 --- a/plugins/reporting-governance/src/adapters/index.mjs +++ b/plugins/reporting-governance/src/adapters/index.mjs @@ -1,3 +1,4 @@ +export { createRuntimeBinding, resolveScriptPath, SCRIPT_ENV_KEYS, SCRIPT_NAMES } from './runtime-binding.mjs'; export { runWatchdogAdapter } from './watchdog.mjs'; export { runDispatcherAdapter } from './dispatcher.mjs'; export { runBridgeSupervisorAdapter } from './bridge-supervisor.mjs'; diff --git a/plugins/reporting-governance/src/adapters/orchestrator.mjs b/plugins/reporting-governance/src/adapters/orchestrator.mjs index 93f10de..80f54bf 100644 --- a/plugins/reporting-governance/src/adapters/orchestrator.mjs +++ b/plugins/reporting-governance/src/adapters/orchestrator.mjs @@ -1,22 +1,19 @@ import path from 'node:path'; -import { ensureSuccess, parseJsonStdout, resolveRepoPath, runNodeScript } from './_script-runner.mjs'; - -const DEFAULT_SCRIPT = resolveRepoPath('scripts', 'watchdog_auto_notify_orchestrator.mjs'); -const DEFAULT_WATCHDOG_SCRIPT = resolveRepoPath('scripts', 'long_task_watchdog.mjs'); -const DEFAULT_DISPATCHER_SCRIPT = resolveRepoPath('scripts', 'operator_notify_dispatcher.mjs'); -const DEFAULT_SUPERVISOR_SCRIPT = resolveRepoPath('scripts', 'operator_notify_bridge_supervisor.mjs'); +import { ensureSuccess, parseJsonStdout, runNodeScript } from './_script-runner.mjs'; +import { createRuntimeBinding, resolveScriptPath } from './runtime-binding.mjs'; export function runOrchestratorAdapter({ - scriptPath = DEFAULT_SCRIPT, + scriptPath = null, + runtimeBinding = null, state, evidenceDir, eventDir, queueDir, spoolDir, receiptDir, - watchdogScript = DEFAULT_WATCHDOG_SCRIPT, - dispatcherScript = DEFAULT_DISPATCHER_SCRIPT, - supervisorScript = DEFAULT_SUPERVISOR_SCRIPT, + watchdogScript = null, + dispatcherScript = null, + supervisorScript = null, senderCommand = null, senderMode = null, openclawBin = 'openclaw', @@ -26,6 +23,12 @@ export function runOrchestratorAdapter({ claim = false, dryRun = false, } = {}) { + const binding = runtimeBinding ?? createRuntimeBinding(); + const resolvedScriptPath = path.resolve(scriptPath ?? resolveScriptPath('orchestrator', { runtimeBinding: binding })); + const resolvedWatchdogScript = path.resolve(watchdogScript ?? resolveScriptPath('watchdog', { runtimeBinding: binding })); + const resolvedDispatcherScript = path.resolve(dispatcherScript ?? resolveScriptPath('dispatcher', { runtimeBinding: binding })); + const resolvedSupervisorScript = path.resolve(supervisorScript ?? resolveScriptPath('bridgeSupervisor', { runtimeBinding: binding })); + const args = []; if (state) args.push('--state', path.resolve(state)); if (evidenceDir) args.push('--evidence-dir', path.resolve(evidenceDir)); @@ -33,9 +36,9 @@ export function runOrchestratorAdapter({ if (queueDir) args.push('--queue-dir', path.resolve(queueDir)); if (spoolDir) args.push('--spool-dir', path.resolve(spoolDir)); if (receiptDir) args.push('--receipt-dir', path.resolve(receiptDir)); - if (watchdogScript) args.push('--watchdog-script', path.resolve(watchdogScript)); - if (dispatcherScript) args.push('--dispatcher-script', path.resolve(dispatcherScript)); - if (supervisorScript) args.push('--supervisor-script', path.resolve(supervisorScript)); + if (resolvedWatchdogScript) args.push('--watchdog-script', resolvedWatchdogScript); + if (resolvedDispatcherScript) args.push('--dispatcher-script', resolvedDispatcherScript); + if (resolvedSupervisorScript) args.push('--supervisor-script', resolvedSupervisorScript); if (senderCommand) args.push('--sender-command', senderCommand); if (senderMode) args.push('--sender-mode', senderMode); if (openclawBin) args.push('--openclaw-bin', openclawBin); @@ -45,7 +48,7 @@ export function runOrchestratorAdapter({ if (dryRun) args.push('--dry-run'); if (compact) args.push('--compact'); - const result = runNodeScript(path.resolve(scriptPath), args); + const result = runNodeScript(resolvedScriptPath, args, { runtimeBinding: binding }); ensureSuccess('orchestrator adapter', result); return parseJsonStdout('orchestrator adapter', result); } diff --git a/plugins/reporting-governance/src/adapters/runtime-binding.mjs b/plugins/reporting-governance/src/adapters/runtime-binding.mjs new file mode 100644 index 0000000..63fd64a --- /dev/null +++ b/plugins/reporting-governance/src/adapters/runtime-binding.mjs @@ -0,0 +1,59 @@ +import path from 'node:path'; +import process from 'node:process'; + +const ENV_PREFIX = 'OPENCLAW_REPORTING_GOVERNANCE_'; + +const packageRoot = path.resolve(import.meta.dirname, '..', '..'); +const repoRoot = path.resolve(packageRoot, '..', '..'); + +const SCRIPT_NAMES = { + watchdog: 'long_task_watchdog.mjs', + dispatcher: 'operator_notify_dispatcher.mjs', + bridgeSupervisor: 'operator_notify_bridge_supervisor.mjs', + senderBinding: 'operator_notify_sender_binding.mjs', + orchestrator: 'watchdog_auto_notify_orchestrator.mjs', +}; + +const SCRIPT_ENV_KEYS = { + watchdog: `${ENV_PREFIX}WATCHDOG_SCRIPT`, + dispatcher: `${ENV_PREFIX}DISPATCHER_SCRIPT`, + bridgeSupervisor: `${ENV_PREFIX}BRIDGE_SUPERVISOR_SCRIPT`, + senderBinding: `${ENV_PREFIX}SENDER_BINDING_SCRIPT`, + orchestrator: `${ENV_PREFIX}ORCHESTRATOR_SCRIPT`, +}; + +function normalizeString(value) { + return typeof value === 'string' && value.trim() ? value.trim() : null; +} + +export function resolveRepoPath(...segments) { + return path.join(repoRoot, ...segments); +} + +export function createRuntimeBinding(overrides = {}) { + const scripts = {}; + + for (const [key, fileName] of Object.entries(SCRIPT_NAMES)) { + const envValue = normalizeString(process.env[SCRIPT_ENV_KEYS[key]]); + const overrideValue = normalizeString(overrides?.scripts?.[key]); + scripts[key] = path.resolve(overrideValue ?? envValue ?? resolveRepoPath('scripts', fileName)); + } + + return { + packageRoot, + repoRoot, + cwd: path.resolve(normalizeString(overrides.cwd) ?? repoRoot), + scripts, + }; +} + +export function resolveScriptPath(name, options = {}) { + const runtimeBinding = options.runtimeBinding ?? createRuntimeBinding(options); + const scriptPath = runtimeBinding?.scripts?.[name]; + if (!scriptPath) { + throw new Error(`unknown runtime binding script: ${name}`); + } + return path.resolve(scriptPath); +} + +export { packageRoot, repoRoot, SCRIPT_ENV_KEYS, SCRIPT_NAMES }; diff --git a/plugins/reporting-governance/src/adapters/sender-binding.mjs b/plugins/reporting-governance/src/adapters/sender-binding.mjs index b196b74..6385cbf 100644 --- a/plugins/reporting-governance/src/adapters/sender-binding.mjs +++ b/plugins/reporting-governance/src/adapters/sender-binding.mjs @@ -1,10 +1,11 @@ import path from 'node:path'; -import { ensureSuccess, parseJsonStdout, resolveRepoPath, runNodeScript } from './_script-runner.mjs'; - -const DEFAULT_SCRIPT = resolveRepoPath('scripts', 'operator_notify_sender_binding.mjs'); +import process from 'node:process'; +import { ensureSuccess, parseJsonStdout, runNodeScript } from './_script-runner.mjs'; +import { createRuntimeBinding, resolveScriptPath } from './runtime-binding.mjs'; export function runSenderBindingAdapter({ - scriptPath = DEFAULT_SCRIPT, + scriptPath = null, + runtimeBinding = null, mode = 'shim', attemptDir, openclawBin = 'openclaw', @@ -12,16 +13,20 @@ export function runSenderBindingAdapter({ compact = true, env = {}, } = {}) { + const binding = runtimeBinding ?? createRuntimeBinding(); + const resolvedScriptPath = path.resolve(scriptPath ?? resolveScriptPath('senderBinding', { runtimeBinding: binding })); + const args = ['--mode', mode, '--openclaw-bin', openclawBin]; if (attemptDir) args.push('--attempt-dir', path.resolve(attemptDir)); if (now) args.push('--now', now); if (compact) args.push('--compact'); - const result = runNodeScript(path.resolve(scriptPath), args, { + const result = runNodeScript(resolvedScriptPath, args, { env: { ...process.env, ...env, }, + runtimeBinding: binding, }); ensureSuccess('sender binding adapter', result); return parseJsonStdout('sender binding adapter', result); diff --git a/plugins/reporting-governance/src/adapters/watchdog.mjs b/plugins/reporting-governance/src/adapters/watchdog.mjs index 83d989d..ba5cd3d 100644 --- a/plugins/reporting-governance/src/adapters/watchdog.mjs +++ b/plugins/reporting-governance/src/adapters/watchdog.mjs @@ -1,10 +1,10 @@ import path from 'node:path'; -import { ensureSuccess, parseJsonStdout, resolveRepoPath, runNodeScript } from './_script-runner.mjs'; - -const DEFAULT_SCRIPT = resolveRepoPath('scripts', 'long_task_watchdog.mjs'); +import { ensureSuccess, parseJsonStdout, runNodeScript } from './_script-runner.mjs'; +import { createRuntimeBinding, resolveScriptPath } from './runtime-binding.mjs'; export function runWatchdogAdapter({ - scriptPath = DEFAULT_SCRIPT, + scriptPath = null, + runtimeBinding = null, state, evidenceDir, eventDir, @@ -13,6 +13,9 @@ export function runWatchdogAdapter({ compact = true, writeState = false, } = {}) { + const binding = runtimeBinding ?? createRuntimeBinding(); + const resolvedScriptPath = path.resolve(scriptPath ?? resolveScriptPath('watchdog', { runtimeBinding: binding })); + const args = []; if (state) args.push('--state', path.resolve(state)); if (evidenceDir) args.push('--evidence-dir', path.resolve(evidenceDir)); @@ -22,7 +25,7 @@ export function runWatchdogAdapter({ if (writeState) args.push('--write-state'); if (compact) args.push('--compact'); - const result = runNodeScript(path.resolve(scriptPath), args); + const result = runNodeScript(resolvedScriptPath, args, { runtimeBinding: binding }); ensureSuccess('watchdog adapter', result); return parseJsonStdout('watchdog adapter', result); } diff --git a/plugins/reporting-governance/src/index.mjs b/plugins/reporting-governance/src/index.mjs index f816c73..6259473 100644 --- a/plugins/reporting-governance/src/index.mjs +++ b/plugins/reporting-governance/src/index.mjs @@ -31,6 +31,10 @@ export const packageBoundaries = { export { evaluatePolicyPack, evaluatePolicies, planDecisionExecution } from './core/index.mjs'; export { + createRuntimeBinding, + resolveScriptPath, + SCRIPT_ENV_KEYS, + SCRIPT_NAMES, runWatchdogAdapter, runDispatcherAdapter, runBridgeSupervisorAdapter, diff --git a/plugins/reporting-governance/test/exports-boundary.integration.test.mjs b/plugins/reporting-governance/test/exports-boundary.integration.test.mjs new file mode 100644 index 0000000..d6c323d --- /dev/null +++ b/plugins/reporting-governance/test/exports-boundary.integration.test.mjs @@ -0,0 +1,149 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; +import { spawnSync } from 'node:child_process'; + +const packageRoot = path.resolve(import.meta.dirname, '..'); + +function createFixtureRoot() { + return fs.mkdtempSync(path.join(os.tmpdir(), 'reporting-governance-exports-')); +} + +function installPackageAlias(root) { + const packageDir = path.join(root, 'node_modules', '@openclaw', 'plugin-reporting-governance'); + fs.mkdirSync(path.dirname(packageDir), { recursive: true }); + fs.symlinkSync(packageRoot, packageDir, 'dir'); + return packageDir; +} + +function writeState(root) { + const statePath = path.join(root, 'watchdog-state.json'); + fs.writeFileSync(statePath, `${JSON.stringify({ + version: 1, + watchdogs: [ + { + id: 'reporting-governance-plugin-watchdog', + task: 'reporting-governance plugin spec development', + status: 'active', + ownerSessionKey: 'agent:coder:main', + reportChannel: 'telegram', + reportTarget: '864811879', + intervalMinutes: 10, + lastMilestoneAt: '2026-05-07T08:00:00.000Z', + lastAlertAt: null + } + ] + }, null, 2)}\n`, 'utf8'); + return statePath; +} + +function runNodeEval(root, source, env = {}) { + const result = spawnSync(process.execPath, ['--input-type=module', '--eval', source], { + cwd: root, + encoding: 'utf8', + env: { + ...process.env, + ...env, + }, + }); + if (result.status !== 0) { + throw new Error(`node eval failed: ${(result.stderr ?? '').trim() || '(no stderr)'}`); + } + return JSON.parse((result.stdout ?? '').trim()); +} + +test('package root export resolves public package surface only', () => { + const root = createFixtureRoot(); + try { + installPackageAlias(root); + const result = runNodeEval(root, ` + import * as plugin from '@openclaw/plugin-reporting-governance'; + process.stdout.write(JSON.stringify({ + packageName: plugin.packageName, + hasRunWatchdogChain: typeof plugin.runWatchdogChain, + hasPlanDecisionExecution: typeof plugin.planDecisionExecution, + })); + `); + + assert.equal(result.packageName, '@openclaw/plugin-reporting-governance'); + assert.equal(result.hasRunWatchdogChain, 'function'); + assert.equal(result.hasPlanDecisionExecution, 'function'); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test('adapters subpath export resolves package-owned adapter index', () => { + const root = createFixtureRoot(); + try { + installPackageAlias(root); + const result = runNodeEval(root, ` + import * as adapters from '@openclaw/plugin-reporting-governance/adapters'; + process.stdout.write(JSON.stringify({ + adapterKeys: Object.keys(adapters).sort(), + })); + `); + + assert.deepEqual(result.adapterKeys, [ + 'SCRIPT_ENV_KEYS', + 'SCRIPT_NAMES', + 'createRuntimeBinding', + 'resolveScriptPath', + 'runBridgeSupervisorAdapter', + 'runDispatcherAdapter', + 'runOrchestratorAdapter', + 'runSenderBindingAdapter', + 'runWatchdogAdapter', + ]); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +}); + +test('leaf subpath export resolves and can execute through injected runtime binding', () => { + const root = createFixtureRoot(); + try { + installPackageAlias(root); + fs.mkdirSync(path.join(root, 'scripts'), { recursive: true }); + fs.mkdirSync(path.join(root, 'events'), { recursive: true }); + fs.mkdirSync(path.join(root, 'evidence'), { recursive: true }); + fs.mkdirSync(path.join(root, 'queue'), { recursive: true }); + const statePath = writeState(root); + + const stubScriptPath = path.join(root, 'scripts', 'custom-watchdog.mjs'); + fs.writeFileSync(stubScriptPath, ` + process.stdout.write(JSON.stringify({ + ok: true, + source: 'stub-watchdog', + argv: process.argv.slice(2), + })); + `, 'utf8'); + + const result = runNodeEval(root, ` + import { runWatchdogAdapter } from '@openclaw/plugin-reporting-governance/adapters/watchdog'; + const out = runWatchdogAdapter({ + state: ${JSON.stringify(statePath)}, + evidenceDir: ${JSON.stringify(path.join(root, 'evidence'))}, + eventDir: ${JSON.stringify(path.join(root, 'events'))}, + notificationDir: ${JSON.stringify(path.join(root, 'queue'))}, + runtimeBinding: { + cwd: ${JSON.stringify(root)}, + scripts: { + watchdog: ${JSON.stringify(stubScriptPath)}, + }, + }, + now: '2026-05-07T08:20:00.000Z', + }); + process.stdout.write(JSON.stringify(out)); + `); + + assert.equal(result.ok, true); + assert.equal(result.source, 'stub-watchdog'); + assert.ok(result.argv.includes('--state')); + assert.ok(result.argv.includes(path.resolve(statePath))); + } finally { + fs.rmSync(root, { recursive: true, force: true }); + } +});