@inquiryon/amp-governance 1.1.5 → 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 +10 -70
- 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,13 +56,10 @@ 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
|
|
|
58
|
-
// AMP reachability state — flips to false
|
|
59
|
-
// back to true
|
|
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.
|
|
60
61
|
let _ampReachable = true;
|
|
61
62
|
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
63
|
|
|
66
64
|
// ── HOOK AUTO-DEPLOY ─────────────────────────────────────────────────────────
|
|
67
65
|
|
|
@@ -223,39 +221,13 @@ async function notifyUser(sender, message) {
|
|
|
223
221
|
}
|
|
224
222
|
|
|
225
223
|
/**
|
|
226
|
-
*
|
|
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.
|
|
224
|
+
* Called whenever a real AMP call fails (init or hitl/request).
|
|
252
225
|
* Sends a clear channel notification on the first occurrence, then stays quiet
|
|
253
226
|
* until AMP recovers (to avoid flooding the user with repeated messages).
|
|
254
227
|
*/
|
|
255
228
|
async function notifyAmpDown(tool, errDetail) {
|
|
256
229
|
const ampUrl = (() => { try { return buildAmpUrl('/'); } catch (_) { return config?.AMP_BACKEND_URL || 'unknown'; } })();
|
|
257
230
|
|
|
258
|
-
startRecoveryPoller();
|
|
259
231
|
if (!_ampDownNotified) {
|
|
260
232
|
_ampDownNotified = true;
|
|
261
233
|
const msg =
|
|
@@ -275,14 +247,13 @@ async function notifyAmpDown(tool, errDetail) {
|
|
|
275
247
|
}
|
|
276
248
|
|
|
277
249
|
/**
|
|
278
|
-
* Called when AMP
|
|
279
|
-
* Clears the down state
|
|
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.
|
|
280
252
|
*/
|
|
281
253
|
async function notifyAmpRecovered() {
|
|
282
254
|
if (!_ampReachable) {
|
|
283
255
|
_ampReachable = true;
|
|
284
256
|
_ampDownNotified = false;
|
|
285
|
-
stopRecoveryPoller();
|
|
286
257
|
await notifyUser(_lastSender,
|
|
287
258
|
`✅ *AMP Governance Service Restored*\n\nThe AMP Governance service is back online. OpenClaw is resuming normal operation — your next request will be processed.`
|
|
288
259
|
);
|
|
@@ -290,27 +261,6 @@ async function notifyAmpRecovered() {
|
|
|
290
261
|
}
|
|
291
262
|
}
|
|
292
263
|
|
|
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
|
-
|
|
314
264
|
// ── HITL POLICY ENGINE ───────────────────────────────────────────────────────
|
|
315
265
|
|
|
316
266
|
/**
|
|
@@ -319,6 +269,7 @@ function startRecoveryPoller() {
|
|
|
319
269
|
*/
|
|
320
270
|
async function requestHitlEval(instanceId, tool, params) {
|
|
321
271
|
const apiKey = getAmpApiKey();
|
|
272
|
+
const { tool: normTool, action: normAction } = normalizeTool(tool, params);
|
|
322
273
|
const res = await fetch(buildAmpUrl('/api/hitl/request'), {
|
|
323
274
|
method: 'POST',
|
|
324
275
|
headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
|
|
@@ -327,8 +278,8 @@ async function requestHitlEval(instanceId, tool, params) {
|
|
|
327
278
|
instance_id: instanceId,
|
|
328
279
|
org_id: config.AMP_ORG_ID,
|
|
329
280
|
agent_name: config.AGENT_NAME,
|
|
330
|
-
tool,
|
|
331
|
-
action:
|
|
281
|
+
tool: normTool,
|
|
282
|
+
action: normAction,
|
|
332
283
|
context: params || {},
|
|
333
284
|
hitl: {
|
|
334
285
|
enable: true,
|
|
@@ -584,23 +535,12 @@ export default {
|
|
|
584
535
|
_instanceId = null;
|
|
585
536
|
});
|
|
586
537
|
|
|
587
|
-
// ── INBOUND MESSAGE: cache sender
|
|
538
|
+
// ── INBOUND MESSAGE: cache sender for HITL/outage notifications ──────────
|
|
588
539
|
api.on('message_received', async (event, ctx) => {
|
|
589
540
|
if (event.from && ctx.channelId) {
|
|
590
541
|
_lastSender = { from: event.from, channelId: ctx.channelId };
|
|
591
542
|
console.log(`[AMP Governance] Sender cached: ${event.from} on ${ctx.channelId}`);
|
|
592
543
|
}
|
|
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
|
-
}
|
|
604
544
|
});
|
|
605
545
|
|
|
606
546
|
// ── BEFORE TOOL CALL: governance check + logging ─────────────────────────
|
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
|
+
}
|