@inquiryon/amp-governance 1.1.4 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +144 -8
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -55,6 +55,14 @@ let _lastSender = null; // { from: string, channelId: string }
|
|
|
55
55
|
// Set by register() so notifyUser can use the runtime API directly
|
|
56
56
|
let _runtime = null;
|
|
57
57
|
|
|
58
|
+
// AMP reachability state — flips to false on first connectivity failure,
|
|
59
|
+
// back to true when a request succeeds again.
|
|
60
|
+
let _ampReachable = true;
|
|
61
|
+
let _ampDownNotified = false; // prevent notification spam while AMP is down
|
|
62
|
+
let _recoveryPoller = null; // setInterval handle for recovery polling
|
|
63
|
+
|
|
64
|
+
const RECOVERY_POLL_INTERVAL_MS = 15000; // 15 seconds
|
|
65
|
+
|
|
58
66
|
// ── HOOK AUTO-DEPLOY ─────────────────────────────────────────────────────────
|
|
59
67
|
|
|
60
68
|
/**
|
|
@@ -151,6 +159,7 @@ async function ensureInstance() {
|
|
|
151
159
|
_instanceId = id;
|
|
152
160
|
writeSession(id);
|
|
153
161
|
console.log(`[AMP Governance] AMP instance created: ${id}`);
|
|
162
|
+
await notifyAmpRecovered();
|
|
154
163
|
}
|
|
155
164
|
return _instanceId || null;
|
|
156
165
|
} catch (err) {
|
|
@@ -159,6 +168,7 @@ async function ensureInstance() {
|
|
|
159
168
|
let target = '';
|
|
160
169
|
try { target = buildAmpUrl('/api/agent/init'); } catch (_) {}
|
|
161
170
|
console.error(`[AMP Governance] ensureInstance failed: ${em}${ec ? ` | cause=${ec}` : ''}${target ? ` | url=${target}` : ''}`);
|
|
171
|
+
_ampReachable = false;
|
|
162
172
|
return null;
|
|
163
173
|
}
|
|
164
174
|
}
|
|
@@ -199,12 +209,108 @@ async function notifyUser(sender, message) {
|
|
|
199
209
|
const prefixed = `[AMP]\n${message}`;
|
|
200
210
|
console.log(`[AMP Governance] Sending notification to ${sender.from}: ${message}`);
|
|
201
211
|
try {
|
|
202
|
-
|
|
212
|
+
const channelId = sender.channelId || '';
|
|
213
|
+
if (channelId === 'slack' || sender.from.startsWith('slack:')) {
|
|
214
|
+
// Strip the 'slack:' prefix — sendMessageSlack expects 'channel:<id>' or 'user:<id>', not 'slack:channel:<id>'
|
|
215
|
+
const slackTarget = sender.from.startsWith('slack:') ? sender.from.slice('slack:'.length) : sender.from;
|
|
216
|
+
await _runtime.channel.slack.sendMessageSlack(slackTarget, prefixed);
|
|
217
|
+
} else {
|
|
218
|
+
await _runtime.channel.whatsapp.sendMessageWhatsApp(sender.from, prefixed, { verbose: false });
|
|
219
|
+
}
|
|
203
220
|
} catch (err) {
|
|
204
221
|
console.warn('[AMP Governance] notifyUser failed:', err.message);
|
|
205
222
|
}
|
|
206
223
|
}
|
|
207
224
|
|
|
225
|
+
/**
|
|
226
|
+
* Lightweight AMP reachability check — does a quick HTTP GET to a known
|
|
227
|
+
* endpoint. Any HTTP response (even 4xx) means AMP is up; a network-level
|
|
228
|
+
* error (ECONNREFUSED, ETIMEDOUT, etc.) means it is down.
|
|
229
|
+
* Returns true if reachable, false if not.
|
|
230
|
+
*/
|
|
231
|
+
async function pingAmp() {
|
|
232
|
+
try {
|
|
233
|
+
const apiKey = getAmpApiKey();
|
|
234
|
+
const controller = new AbortController();
|
|
235
|
+
const timer = setTimeout(() => controller.abort(), 4000); // 4 s timeout
|
|
236
|
+
try {
|
|
237
|
+
await fetch(buildAmpUrl('/api/amp/sse-status'), {
|
|
238
|
+
method: 'GET',
|
|
239
|
+
signal: controller.signal,
|
|
240
|
+
});
|
|
241
|
+
} finally {
|
|
242
|
+
clearTimeout(timer);
|
|
243
|
+
}
|
|
244
|
+
return true; // any HTTP response = server is up
|
|
245
|
+
} catch (_) {
|
|
246
|
+
return false; // connection refused / timeout / DNS failure
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Called whenever AMP is found to be unreachable.
|
|
252
|
+
* Sends a clear channel notification on the first occurrence, then stays quiet
|
|
253
|
+
* until AMP recovers (to avoid flooding the user with repeated messages).
|
|
254
|
+
*/
|
|
255
|
+
async function notifyAmpDown(tool, errDetail) {
|
|
256
|
+
const ampUrl = (() => { try { return buildAmpUrl('/'); } catch (_) { return config?.AMP_BACKEND_URL || 'unknown'; } })();
|
|
257
|
+
|
|
258
|
+
startRecoveryPoller();
|
|
259
|
+
if (!_ampDownNotified) {
|
|
260
|
+
_ampDownNotified = true;
|
|
261
|
+
const msg =
|
|
262
|
+
`🚨 *AMP Governance Service Unreachable*\n\n` +
|
|
263
|
+
`OpenClaw cannot connect to the AMP Governance service at:\n${ampUrl}\n\n` +
|
|
264
|
+
`*All agent activities are now blocked* until AMP is back online. ` +
|
|
265
|
+
`This is a safety measure — no tools (file reads, shell commands, web searches, etc.) ` +
|
|
266
|
+
`will be executed while governance is unavailable.\n\n` +
|
|
267
|
+
`*What to do:*\n` +
|
|
268
|
+
`• Verify the AMP backend is running (check pm2 or your server)\n` +
|
|
269
|
+
`• Confirm the AMP_BACKEND_URL in your amp_config.json is correct\n` +
|
|
270
|
+
`• Once AMP is restored, send your request again and OpenClaw will resume normally\n\n` +
|
|
271
|
+
`_Technical detail: ${errDetail}_`;
|
|
272
|
+
await notifyUser(_lastSender, msg);
|
|
273
|
+
console.error(`[AMP Governance] AMP unreachable — all tool calls blocked. URL: ${ampUrl} | Error: ${errDetail}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Called when AMP responds successfully after a period of being down.
|
|
279
|
+
* Clears the down state and stops the recovery poller.
|
|
280
|
+
*/
|
|
281
|
+
async function notifyAmpRecovered() {
|
|
282
|
+
if (!_ampReachable) {
|
|
283
|
+
_ampReachable = true;
|
|
284
|
+
_ampDownNotified = false;
|
|
285
|
+
stopRecoveryPoller();
|
|
286
|
+
await notifyUser(_lastSender,
|
|
287
|
+
`✅ *AMP Governance Service Restored*\n\nThe AMP Governance service is back online. OpenClaw is resuming normal operation — your next request will be processed.`
|
|
288
|
+
);
|
|
289
|
+
console.log('[AMP Governance] AMP service recovered — resuming normal operation.');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function stopRecoveryPoller() {
|
|
294
|
+
if (_recoveryPoller !== null) {
|
|
295
|
+
clearInterval(_recoveryPoller);
|
|
296
|
+
_recoveryPoller = null;
|
|
297
|
+
console.log('[AMP Governance] Recovery poller stopped.');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function startRecoveryPoller() {
|
|
302
|
+
if (_recoveryPoller !== null) return; // already running
|
|
303
|
+
console.log(`[AMP Governance] Starting recovery poller (every ${RECOVERY_POLL_INTERVAL_MS / 1000}s)...`);
|
|
304
|
+
_recoveryPoller = setInterval(async () => {
|
|
305
|
+
const reachable = await pingAmp();
|
|
306
|
+
if (reachable) {
|
|
307
|
+
await notifyAmpRecovered();
|
|
308
|
+
} else {
|
|
309
|
+
console.log('[AMP Governance] Recovery poll: AMP still unreachable.');
|
|
310
|
+
}
|
|
311
|
+
}, RECOVERY_POLL_INTERVAL_MS);
|
|
312
|
+
}
|
|
313
|
+
|
|
208
314
|
// ── HITL POLICY ENGINE ───────────────────────────────────────────────────────
|
|
209
315
|
|
|
210
316
|
/**
|
|
@@ -275,10 +381,15 @@ async function checkToolPolicy(instanceId, tool, params) {
|
|
|
275
381
|
let hitlResponse;
|
|
276
382
|
try {
|
|
277
383
|
hitlResponse = await requestHitlEval(instanceId, tool, params);
|
|
384
|
+
// Successful contact with AMP — clear any prior down state
|
|
385
|
+
await notifyAmpRecovered();
|
|
278
386
|
} catch (err) {
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
387
|
+
const em = err && err.message ? err.message : String(err);
|
|
388
|
+
console.error(`[AMP Governance] HITL request failed — blocking tool "${tool}": ${em}`);
|
|
389
|
+
await ampLog(instanceId, `AMP governance check failed — tool "${tool}" blocked: ${em}`, 'ERROR');
|
|
390
|
+
_ampReachable = false;
|
|
391
|
+
await notifyAmpDown(tool, em);
|
|
392
|
+
return { block: true, blockReason: `AMP Governance service is unreachable. Tool "${tool}" was blocked as a safety measure. Restore the AMP backend and retry.` };
|
|
282
393
|
}
|
|
283
394
|
|
|
284
395
|
const status = hitlResponse.status;
|
|
@@ -473,12 +584,23 @@ export default {
|
|
|
473
584
|
_instanceId = null;
|
|
474
585
|
});
|
|
475
586
|
|
|
476
|
-
// ── INBOUND MESSAGE: cache sender
|
|
477
|
-
api.on('message_received', (event, ctx) => {
|
|
587
|
+
// ── INBOUND MESSAGE: cache sender + probe AMP health ─────────────────────
|
|
588
|
+
api.on('message_received', async (event, ctx) => {
|
|
478
589
|
if (event.from && ctx.channelId) {
|
|
479
590
|
_lastSender = { from: event.from, channelId: ctx.channelId };
|
|
480
591
|
console.log(`[AMP Governance] Sender cached: ${event.from} on ${ctx.channelId}`);
|
|
481
592
|
}
|
|
593
|
+
// Probe AMP on every inbound message so we catch outages even when the
|
|
594
|
+
// agent responds without using any tools (e.g. simple greetings).
|
|
595
|
+
// pingAmp() does a real HTTP check — bypasses any cached instance state.
|
|
596
|
+
const reachable = await pingAmp();
|
|
597
|
+
if (!reachable) {
|
|
598
|
+
_ampReachable = false;
|
|
599
|
+
await notifyAmpDown('(inbound message)',
|
|
600
|
+
'Could not reach AMP Governance service — backend may be down or unreachable.');
|
|
601
|
+
} else if (!_ampReachable) {
|
|
602
|
+
await notifyAmpRecovered();
|
|
603
|
+
}
|
|
482
604
|
});
|
|
483
605
|
|
|
484
606
|
// ── BEFORE TOOL CALL: governance check + logging ─────────────────────────
|
|
@@ -488,8 +610,22 @@ export default {
|
|
|
488
610
|
|
|
489
611
|
const instanceId = await ensureInstance();
|
|
490
612
|
if (!instanceId) {
|
|
491
|
-
|
|
492
|
-
|
|
613
|
+
const reason = !config
|
|
614
|
+
? 'AMP Governance plugin is not configured (amp_config.json missing or invalid).'
|
|
615
|
+
: 'AMP Governance service is unreachable.';
|
|
616
|
+
console.error(`[AMP Governance] No instance available — blocking tool "${tool}". Reason: ${reason}`);
|
|
617
|
+
if (!config) {
|
|
618
|
+
await notifyUser(_lastSender,
|
|
619
|
+
`🚨 *AMP Governance Not Configured*\n\n` +
|
|
620
|
+
`The AMP Governance plugin has no valid configuration. ` +
|
|
621
|
+
`All OpenClaw activities are blocked until amp_config.json is set up correctly.\n\n` +
|
|
622
|
+
`Please configure AMP_BACKEND_URL, AMP_API_KEY, and AMP_ORG_ID in:\n` +
|
|
623
|
+
`~/.openclaw/hooks/amp/amp_config.json`
|
|
624
|
+
);
|
|
625
|
+
} else {
|
|
626
|
+
await notifyAmpDown(tool, 'Could not establish an AMP session — backend may be down or unreachable.');
|
|
627
|
+
}
|
|
628
|
+
return { block: true, blockReason: reason };
|
|
493
629
|
}
|
|
494
630
|
|
|
495
631
|
// Log the incoming tool call
|