@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.
Files changed (2) hide show
  1. package/index.js +144 -8
  2. 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
- await _runtime.channel.whatsapp.sendMessageWhatsApp(sender.from, prefixed, { verbose: false });
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
- console.warn(`[AMP Governance] HITL request failed (fail open): ${err.message}`);
280
- await ampLog(instanceId, `AMP governance check failed (fail open): ${err.message}`, 'WARN');
281
- return {};
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 for HITL notifications ─────────────────
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
- console.warn('[AMP Governance] No instance available — skipping governance check.');
492
- return {};
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inquiryon/amp-governance",
3
- "version": "1.1.4",
3
+ "version": "1.1.5",
4
4
  "description": "AMP governance plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "files": [