@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 +86 -10
- package/package.json +2 -1
- package/toolNormalization.js +26 -0
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
|
-
|
|
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:
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
492
|
-
|
|
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.
|
|
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
|
+
}
|