@inquiryon/openclaw-amp-governance 1.0.3 → 1.0.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 +40 -2
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
+
import { exec } from 'child_process';
|
|
2
3
|
|
|
3
4
|
console.log('[AMP Governance] Plugin module loaded — Phase 4.');
|
|
4
5
|
|
|
@@ -23,6 +24,9 @@ try {
|
|
|
23
24
|
// Module-level instance cache so we only init once per process lifetime
|
|
24
25
|
let _instanceId = null;
|
|
25
26
|
|
|
27
|
+
// Last known sender — populated by message_received, used for HITL notifications
|
|
28
|
+
let _lastSender = null; // { from: string, channelId: string }
|
|
29
|
+
|
|
26
30
|
function readSession() {
|
|
27
31
|
try {
|
|
28
32
|
return JSON.parse(fs.readFileSync(SESSION_FILE, 'utf-8'));
|
|
@@ -111,6 +115,23 @@ async function ampLog(instanceId, message, level = 'INFO') {
|
|
|
111
115
|
}
|
|
112
116
|
}
|
|
113
117
|
|
|
118
|
+
// ── PROACTIVE NOTIFICATIONS ──────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Send a proactive WhatsApp message to the user without waiting for the agent
|
|
122
|
+
* to finish. Uses the openclaw CLI so we don't need to reverse-engineer the
|
|
123
|
+
* gateway REST API. Fire-and-forget — errors are logged but never thrown.
|
|
124
|
+
*/
|
|
125
|
+
function notifyUser(sender, message) {
|
|
126
|
+
if (!sender?.from || !sender?.channelId) return;
|
|
127
|
+
const safeMsg = message.replace(/"/g, '\\"');
|
|
128
|
+
const cmd = `openclaw message send --channel ${sender.channelId} --target "${sender.from}" --message "${safeMsg}"`;
|
|
129
|
+
console.log(`[AMP Governance] Sending notification to ${sender.from}: ${message}`);
|
|
130
|
+
exec(cmd, (err) => {
|
|
131
|
+
if (err) console.warn('[AMP Governance] notifyUser failed:', err.message);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
114
135
|
// ── HITL POLICY ENGINE ───────────────────────────────────────────────────────
|
|
115
136
|
|
|
116
137
|
/**
|
|
@@ -120,7 +141,7 @@ async function ampLog(instanceId, message, level = 'INFO') {
|
|
|
120
141
|
async function requestHitlEval(instanceId, tool, params) {
|
|
121
142
|
const res = await fetch(`${config.AMP_BACKEND_URL}/api/hitl/request`, {
|
|
122
143
|
method: 'POST',
|
|
123
|
-
headers: { 'Content-Type': 'application/json' },
|
|
144
|
+
headers: { 'X-API-Key': config.AMP_API_KEY, 'Content-Type': 'application/json' },
|
|
124
145
|
body: JSON.stringify({
|
|
125
146
|
caller_id: instanceId,
|
|
126
147
|
instance_id: instanceId,
|
|
@@ -209,12 +230,16 @@ async function checkToolPolicy(instanceId, tool, params) {
|
|
|
209
230
|
console.log(`[AMP Governance] HITL required for "${tool}" — waiting for human decision...`);
|
|
210
231
|
await ampLog(instanceId, `HITL requested for tool: ${tool} — awaiting human approval in AMP`, 'WARN');
|
|
211
232
|
|
|
233
|
+
// Notify the user immediately so they aren't staring at silence
|
|
234
|
+
notifyUser(_lastSender, `I need a reviewer to approve "${tool}" before I can proceed. I'll follow up once the decision is made — this may take a few minutes.`);
|
|
235
|
+
|
|
212
236
|
let decision;
|
|
213
237
|
try {
|
|
214
238
|
decision = await pollHitlDecision(instanceId);
|
|
215
239
|
} catch (err) {
|
|
216
240
|
const msg = `Tool call "${tool}" timed out waiting for human approval — blocked.`;
|
|
217
241
|
await ampLog(instanceId, msg, 'ERROR');
|
|
242
|
+
notifyUser(_lastSender, `No response from the reviewer — "${tool}" was blocked due to timeout.`);
|
|
218
243
|
return { block: true, blockReason: msg };
|
|
219
244
|
}
|
|
220
245
|
|
|
@@ -223,11 +248,13 @@ async function checkToolPolicy(instanceId, tool, params) {
|
|
|
223
248
|
|
|
224
249
|
if (resolution === 'approve' || resolution === 'approved') {
|
|
225
250
|
await ampLog(instanceId, `HITL approved: ${tool} | workitem: ${decision.workitem_id}`);
|
|
251
|
+
notifyUser(_lastSender, `The reviewer approved "${tool}" — continuing now.`);
|
|
226
252
|
return {};
|
|
227
253
|
}
|
|
228
254
|
|
|
229
255
|
if (resolution === 'modify' || resolution === 'modified') {
|
|
230
256
|
await ampLog(instanceId, `HITL approved with modification: ${tool} | workitem: ${decision.workitem_id}`);
|
|
257
|
+
notifyUser(_lastSender, `The reviewer approved "${tool}" (with modifications) — continuing now.`);
|
|
231
258
|
return {};
|
|
232
259
|
}
|
|
233
260
|
|
|
@@ -235,6 +262,7 @@ async function checkToolPolicy(instanceId, tool, params) {
|
|
|
235
262
|
const info = decision.information ? ` Reviewer note: ${decision.information}` : '';
|
|
236
263
|
const msg = `Tool call "${tool}" was rejected by a human reviewer.${info}`;
|
|
237
264
|
await ampLog(instanceId, `HITL rejected: ${tool} | workitem: ${decision.workitem_id}`, 'WARN');
|
|
265
|
+
notifyUser(_lastSender, `The reviewer rejected "${tool}".${info}`);
|
|
238
266
|
return { block: true, blockReason: msg };
|
|
239
267
|
}
|
|
240
268
|
|
|
@@ -360,6 +388,14 @@ export default {
|
|
|
360
388
|
register(api) {
|
|
361
389
|
api.logger.info('AMP Governance registered. Phase 4 - eval policy enforcement active.');
|
|
362
390
|
|
|
391
|
+
// ── INBOUND MESSAGE: cache sender for HITL notifications ─────────────────
|
|
392
|
+
api.on('message_received', (event, ctx) => {
|
|
393
|
+
if (event.from && ctx.channelId) {
|
|
394
|
+
_lastSender = { from: event.from, channelId: ctx.channelId };
|
|
395
|
+
console.log(`[AMP Governance] Sender cached: ${event.from} on ${ctx.channelId}`);
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
363
399
|
// ── BEFORE TOOL CALL: governance check + logging ─────────────────────────
|
|
364
400
|
api.on('before_tool_call', async (event) => {
|
|
365
401
|
const tool = event.toolName || event.name || 'unknown-tool';
|
|
@@ -398,7 +434,7 @@ export default {
|
|
|
398
434
|
await ampLog(instanceId, message, event.success === false ? 'ERROR' : 'INFO');
|
|
399
435
|
});
|
|
400
436
|
|
|
401
|
-
// ── OUTBOUND MESSAGE LOGGING
|
|
437
|
+
// ── OUTBOUND MESSAGE LOGGING + PREFIX ────────────────────────────────────
|
|
402
438
|
api.on('message_sending', async (event) => {
|
|
403
439
|
const instanceId = _instanceId || readSession()?.instanceId;
|
|
404
440
|
const msgText = event.content || event.text || event.message || JSON.stringify(event);
|
|
@@ -407,6 +443,8 @@ export default {
|
|
|
407
443
|
if (instanceId) {
|
|
408
444
|
await ampLog(instanceId, `Agent reply: ${preview}`);
|
|
409
445
|
}
|
|
446
|
+
// Prefix every outbound message so the user can distinguish agent replies
|
|
447
|
+
return { content: `[OpenClaw]\n${msgText}` };
|
|
410
448
|
});
|
|
411
449
|
},
|
|
412
450
|
};
|