@inquiryon/amp-governance 1.1.4 → 1.1.6

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 CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as fs from 'fs';
2
+ import { normalizeTool } from './toolNormalization.js';
2
3
 
3
4
  console.log('[AMP Governance] Plugin module loaded — Phase 4.');
4
5
 
@@ -55,6 +56,11 @@ let _lastSender = null; // { from: string, channelId: string }
55
56
  // Set by register() so notifyUser can use the runtime API directly
56
57
  let _runtime = null;
57
58
 
59
+ // AMP reachability state — flips to false when a real AMP call fails,
60
+ // back to true the next time a real AMP call succeeds.
61
+ let _ampReachable = true;
62
+ let _ampDownNotified = false; // prevent notification spam while AMP is down
63
+
58
64
  // ── HOOK AUTO-DEPLOY ─────────────────────────────────────────────────────────
59
65
 
60
66
  /**
@@ -151,6 +157,7 @@ async function ensureInstance() {
151
157
  _instanceId = id;
152
158
  writeSession(id);
153
159
  console.log(`[AMP Governance] AMP instance created: ${id}`);
160
+ await notifyAmpRecovered();
154
161
  }
155
162
  return _instanceId || null;
156
163
  } catch (err) {
@@ -159,6 +166,7 @@ async function ensureInstance() {
159
166
  let target = '';
160
167
  try { target = buildAmpUrl('/api/agent/init'); } catch (_) {}
161
168
  console.error(`[AMP Governance] ensureInstance failed: ${em}${ec ? ` | cause=${ec}` : ''}${target ? ` | url=${target}` : ''}`);
169
+ _ampReachable = false;
162
170
  return null;
163
171
  }
164
172
  }
@@ -199,12 +207,60 @@ async function notifyUser(sender, message) {
199
207
  const prefixed = `[AMP]\n${message}`;
200
208
  console.log(`[AMP Governance] Sending notification to ${sender.from}: ${message}`);
201
209
  try {
202
- await _runtime.channel.whatsapp.sendMessageWhatsApp(sender.from, prefixed, { verbose: false });
210
+ const channelId = sender.channelId || '';
211
+ if (channelId === 'slack' || sender.from.startsWith('slack:')) {
212
+ // Strip the 'slack:' prefix — sendMessageSlack expects 'channel:<id>' or 'user:<id>', not 'slack:channel:<id>'
213
+ const slackTarget = sender.from.startsWith('slack:') ? sender.from.slice('slack:'.length) : sender.from;
214
+ await _runtime.channel.slack.sendMessageSlack(slackTarget, prefixed);
215
+ } else {
216
+ await _runtime.channel.whatsapp.sendMessageWhatsApp(sender.from, prefixed, { verbose: false });
217
+ }
203
218
  } catch (err) {
204
219
  console.warn('[AMP Governance] notifyUser failed:', err.message);
205
220
  }
206
221
  }
207
222
 
223
+ /**
224
+ * Called whenever a real AMP call fails (init or hitl/request).
225
+ * Sends a clear channel notification on the first occurrence, then stays quiet
226
+ * until AMP recovers (to avoid flooding the user with repeated messages).
227
+ */
228
+ async function notifyAmpDown(tool, errDetail) {
229
+ const ampUrl = (() => { try { return buildAmpUrl('/'); } catch (_) { return config?.AMP_BACKEND_URL || 'unknown'; } })();
230
+
231
+ if (!_ampDownNotified) {
232
+ _ampDownNotified = true;
233
+ const msg =
234
+ `🚨 *AMP Governance Service Unreachable*\n\n` +
235
+ `OpenClaw cannot connect to the AMP Governance service at:\n${ampUrl}\n\n` +
236
+ `*All agent activities are now blocked* until AMP is back online. ` +
237
+ `This is a safety measure — no tools (file reads, shell commands, web searches, etc.) ` +
238
+ `will be executed while governance is unavailable.\n\n` +
239
+ `*What to do:*\n` +
240
+ `• Verify the AMP backend is running (check pm2 or your server)\n` +
241
+ `• Confirm the AMP_BACKEND_URL in your amp_config.json is correct\n` +
242
+ `• Once AMP is restored, send your request again and OpenClaw will resume normally\n\n` +
243
+ `_Technical detail: ${errDetail}_`;
244
+ await notifyUser(_lastSender, msg);
245
+ console.error(`[AMP Governance] AMP unreachable — all tool calls blocked. URL: ${ampUrl} | Error: ${errDetail}`);
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Called when a real AMP call succeeds after a period of being down.
251
+ * Clears the down state so the next failure notifies again.
252
+ */
253
+ async function notifyAmpRecovered() {
254
+ if (!_ampReachable) {
255
+ _ampReachable = true;
256
+ _ampDownNotified = false;
257
+ await notifyUser(_lastSender,
258
+ `✅ *AMP Governance Service Restored*\n\nThe AMP Governance service is back online. OpenClaw is resuming normal operation — your next request will be processed.`
259
+ );
260
+ console.log('[AMP Governance] AMP service recovered — resuming normal operation.');
261
+ }
262
+ }
263
+
208
264
  // ── HITL POLICY ENGINE ───────────────────────────────────────────────────────
209
265
 
210
266
  /**
@@ -213,6 +269,7 @@ async function notifyUser(sender, message) {
213
269
  */
214
270
  async function requestHitlEval(instanceId, tool, params) {
215
271
  const apiKey = getAmpApiKey();
272
+ const { tool: normTool, action: normAction } = normalizeTool(tool, params);
216
273
  const res = await fetch(buildAmpUrl('/api/hitl/request'), {
217
274
  method: 'POST',
218
275
  headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
@@ -221,8 +278,8 @@ async function requestHitlEval(instanceId, tool, params) {
221
278
  instance_id: instanceId,
222
279
  org_id: config.AMP_ORG_ID,
223
280
  agent_name: config.AGENT_NAME,
224
- tool,
225
- action: params?.action || '*',
281
+ tool: normTool,
282
+ action: normAction,
226
283
  context: params || {},
227
284
  hitl: {
228
285
  enable: true,
@@ -275,10 +332,15 @@ async function checkToolPolicy(instanceId, tool, params) {
275
332
  let hitlResponse;
276
333
  try {
277
334
  hitlResponse = await requestHitlEval(instanceId, tool, params);
335
+ // Successful contact with AMP — clear any prior down state
336
+ await notifyAmpRecovered();
278
337
  } 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 {};
338
+ const em = err && err.message ? err.message : String(err);
339
+ console.error(`[AMP Governance] HITL request failed blocking tool "${tool}": ${em}`);
340
+ await ampLog(instanceId, `AMP governance check failed — tool "${tool}" blocked: ${em}`, 'ERROR');
341
+ _ampReachable = false;
342
+ await notifyAmpDown(tool, em);
343
+ return { block: true, blockReason: `AMP Governance service is unreachable. Tool "${tool}" was blocked as a safety measure. Restore the AMP backend and retry.` };
282
344
  }
283
345
 
284
346
  const status = hitlResponse.status;
@@ -473,8 +535,8 @@ export default {
473
535
  _instanceId = null;
474
536
  });
475
537
 
476
- // ── INBOUND MESSAGE: cache sender for HITL notifications ─────────────────
477
- api.on('message_received', (event, ctx) => {
538
+ // ── INBOUND MESSAGE: cache sender for HITL/outage notifications ──────────
539
+ api.on('message_received', async (event, ctx) => {
478
540
  if (event.from && ctx.channelId) {
479
541
  _lastSender = { from: event.from, channelId: ctx.channelId };
480
542
  console.log(`[AMP Governance] Sender cached: ${event.from} on ${ctx.channelId}`);
@@ -488,8 +550,22 @@ export default {
488
550
 
489
551
  const instanceId = await ensureInstance();
490
552
  if (!instanceId) {
491
- console.warn('[AMP Governance] No instance available — skipping governance check.');
492
- return {};
553
+ const reason = !config
554
+ ? 'AMP Governance plugin is not configured (amp_config.json missing or invalid).'
555
+ : 'AMP Governance service is unreachable.';
556
+ console.error(`[AMP Governance] No instance available — blocking tool "${tool}". Reason: ${reason}`);
557
+ if (!config) {
558
+ await notifyUser(_lastSender,
559
+ `🚨 *AMP Governance Not Configured*\n\n` +
560
+ `The AMP Governance plugin has no valid configuration. ` +
561
+ `All OpenClaw activities are blocked until amp_config.json is set up correctly.\n\n` +
562
+ `Please configure AMP_BACKEND_URL, AMP_API_KEY, and AMP_ORG_ID in:\n` +
563
+ `~/.openclaw/hooks/amp/amp_config.json`
564
+ );
565
+ } else {
566
+ await notifyAmpDown(tool, 'Could not establish an AMP session — backend may be down or unreachable.');
567
+ }
568
+ return { block: true, blockReason: reason };
493
569
  }
494
570
 
495
571
  // Log the incoming tool call
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@inquiryon/amp-governance",
3
- "version": "1.1.4",
3
+ "version": "1.1.6",
4
4
  "description": "AMP governance plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "index.js",
8
+ "toolNormalization.js",
8
9
  "openclaw.plugin.json",
9
10
  "hook/handler.ts",
10
11
  "hook/HOOK.md",
@@ -0,0 +1,26 @@
1
+ // Normalizes OpenClaw's raw tool names into the same tool/action vocabulary
2
+ // used by AMP's Hermes plugin (AHP's policy.py), so a single AMP policy can
3
+ // govern both agent types identically instead of matching on each agent's
4
+ // own raw tool names.
5
+
6
+ const TOOL_MAP = {
7
+ web_search: { tool: 'exec', action: 'web_search' },
8
+ read: { tool: 'read', action: 'read' },
9
+ write: { tool: 'write', action: 'write' },
10
+ edit: { tool: 'write', action: 'edit' },
11
+ bash: { tool: 'exec', action: 'exec' },
12
+ shell: { tool: 'exec', action: 'exec' },
13
+ };
14
+
15
+ /**
16
+ * Returns { tool, action } for a raw OpenClaw tool name.
17
+ * Unlike Hermes's normalizer (which returns null for anything outside its
18
+ * known set), unmapped tools fall back to the raw name with action '*' —
19
+ * so a not-yet-mapped OpenClaw tool still reaches AMP instead of being
20
+ * silently dropped.
21
+ */
22
+ export function normalizeTool(rawTool, params) {
23
+ const mapped = TOOL_MAP[rawTool];
24
+ if (mapped) return { tool: mapped.tool, action: mapped.action };
25
+ return { tool: rawTool, action: params?.action || '*' };
26
+ }