@goplus/agentguard 1.1.14 → 1.1.20
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/README.md +8 -2
- package/dist/adapters/engine.d.ts.map +1 -1
- package/dist/adapters/engine.js +10 -0
- package/dist/adapters/engine.js.map +1 -1
- package/dist/adapters/openclaw-plugin.d.ts.map +1 -1
- package/dist/adapters/openclaw-plugin.js +6 -18
- package/dist/adapters/openclaw-plugin.js.map +1 -1
- package/dist/cli.js +525 -42
- package/dist/cli.js.map +1 -1
- package/dist/cloud/client.d.ts +16 -2
- package/dist/cloud/client.d.ts.map +1 -1
- package/dist/cloud/client.js +56 -9
- package/dist/cloud/client.js.map +1 -1
- package/dist/cloud/openclaw-notify.d.ts +18 -0
- package/dist/cloud/openclaw-notify.d.ts.map +1 -0
- package/dist/cloud/openclaw-notify.js +69 -0
- package/dist/cloud/openclaw-notify.js.map +1 -0
- package/dist/config.d.ts +14 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +51 -0
- package/dist/config.js.map +1 -1
- package/dist/feed/cron.d.ts +18 -0
- package/dist/feed/cron.d.ts.map +1 -1
- package/dist/feed/cron.js +499 -25
- package/dist/feed/cron.js.map +1 -1
- package/dist/feed/selfcheck.d.ts.map +1 -1
- package/dist/feed/selfcheck.js +470 -23
- package/dist/feed/selfcheck.js.map +1 -1
- package/dist/feed/types.d.ts +7 -8
- package/dist/feed/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/installers.d.ts.map +1 -1
- package/dist/installers.js +101 -2
- package/dist/installers.js.map +1 -1
- package/dist/runtime/evaluator.d.ts.map +1 -1
- package/dist/runtime/evaluator.js +45 -3
- package/dist/runtime/evaluator.js.map +1 -1
- package/dist/runtime/protect.d.ts.map +1 -1
- package/dist/runtime/protect.js +15 -2
- package/dist/runtime/protect.js.map +1 -1
- package/dist/runtime/self-command.d.ts +2 -0
- package/dist/runtime/self-command.d.ts.map +1 -0
- package/dist/runtime/self-command.js +73 -0
- package/dist/runtime/self-command.js.map +1 -0
- package/dist/tests/cli-checkup.test.js +1 -1
- package/dist/tests/cli-checkup.test.js.map +1 -1
- package/dist/tests/cli-connect.test.d.ts +2 -0
- package/dist/tests/cli-connect.test.d.ts.map +1 -0
- package/dist/tests/cli-connect.test.js +326 -0
- package/dist/tests/cli-connect.test.js.map +1 -0
- package/dist/tests/cli-init.test.js +141 -0
- package/dist/tests/cli-init.test.js.map +1 -1
- package/dist/tests/cli-policy.test.js +72 -0
- package/dist/tests/cli-policy.test.js.map +1 -1
- package/dist/tests/cli-subscribe.test.js +295 -2
- package/dist/tests/cli-subscribe.test.js.map +1 -1
- package/dist/tests/feed-cloud.test.js +45 -1
- package/dist/tests/feed-cloud.test.js.map +1 -1
- package/dist/tests/feed-cron.test.js +506 -10
- package/dist/tests/feed-cron.test.js.map +1 -1
- package/dist/tests/feed-selfcheck.test.js +61 -13
- package/dist/tests/feed-selfcheck.test.js.map +1 -1
- package/dist/tests/installer.test.js +69 -0
- package/dist/tests/installer.test.js.map +1 -1
- package/dist/tests/integration.test.js +41 -9
- package/dist/tests/integration.test.js.map +1 -1
- package/dist/tests/runtime-cloud.test.js +148 -0
- package/dist/tests/runtime-cloud.test.js.map +1 -1
- package/openclaw.plugin.json +4 -0
- package/package.json +1 -1
- package/skills/agentguard/SKILL.md +11 -5
|
@@ -12,6 +12,7 @@ const node_net_1 = __importDefault(require("node:net"));
|
|
|
12
12
|
const node_os_1 = require("node:os");
|
|
13
13
|
const node_path_1 = require("node:path");
|
|
14
14
|
const cron_js_1 = require("../feed/cron.js");
|
|
15
|
+
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
15
16
|
async function closeServer(server) {
|
|
16
17
|
await new Promise((resolve, reject) => {
|
|
17
18
|
server.close((err) => {
|
|
@@ -84,6 +85,41 @@ function fakeGateway(jobs = []) {
|
|
|
84
85
|
},
|
|
85
86
|
};
|
|
86
87
|
}
|
|
88
|
+
function base64UrlEncode(value) {
|
|
89
|
+
return value.toString('base64').replaceAll('+', '-').replaceAll('/', '_').replace(/=+$/g, '');
|
|
90
|
+
}
|
|
91
|
+
function base64UrlDecode(value) {
|
|
92
|
+
const normalized = value.replaceAll('-', '+').replaceAll('_', '/');
|
|
93
|
+
const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4);
|
|
94
|
+
return Buffer.from(padded, 'base64');
|
|
95
|
+
}
|
|
96
|
+
function publicKeyRawBase64UrlFromPem(publicKeyPem) {
|
|
97
|
+
const publicKey = (0, node_crypto_1.createPublicKey)(publicKeyPem);
|
|
98
|
+
const spki = publicKey.export({ type: 'spki', format: 'der' });
|
|
99
|
+
const raw = spki.length === ED25519_SPKI_PREFIX.length + 32 &&
|
|
100
|
+
spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)
|
|
101
|
+
? spki.subarray(ED25519_SPKI_PREFIX.length)
|
|
102
|
+
: spki;
|
|
103
|
+
return base64UrlEncode(raw);
|
|
104
|
+
}
|
|
105
|
+
function writeOpenClawIdentity(stateDir) {
|
|
106
|
+
const { publicKey, privateKey } = (0, node_crypto_1.generateKeyPairSync)('ed25519');
|
|
107
|
+
const publicKeyPem = publicKey.export({ type: 'spki', format: 'pem' }).toString();
|
|
108
|
+
const privateKeyPem = privateKey.export({ type: 'pkcs8', format: 'pem' }).toString();
|
|
109
|
+
const deviceId = (0, node_crypto_1.createHash)('sha256')
|
|
110
|
+
.update(base64UrlDecode(publicKeyRawBase64UrlFromPem(publicKeyPem)))
|
|
111
|
+
.digest('hex');
|
|
112
|
+
const identityDir = (0, node_path_1.join)(stateDir, 'identity');
|
|
113
|
+
(0, node_fs_1.mkdirSync)(identityDir, { recursive: true });
|
|
114
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(identityDir, 'device.json'), JSON.stringify({
|
|
115
|
+
version: 1,
|
|
116
|
+
deviceId,
|
|
117
|
+
publicKeyPem,
|
|
118
|
+
privateKeyPem,
|
|
119
|
+
createdAtMs: Date.now(),
|
|
120
|
+
}));
|
|
121
|
+
return { deviceId, publicKeyPem, privateKeyPem };
|
|
122
|
+
}
|
|
87
123
|
(0, node_test_1.describe)('feed/cron', () => {
|
|
88
124
|
(0, node_test_1.it)('validateCronExpression rejects non-five-field values', () => {
|
|
89
125
|
strict_1.default.equal((0, cron_js_1.validateCronExpression)('0 * * * *'), '0 * * * *');
|
|
@@ -91,7 +127,7 @@ function fakeGateway(jobs = []) {
|
|
|
91
127
|
strict_1.default.throws(() => (0, cron_js_1.validateCronExpression)('0 * * *'), /Invalid --cron/);
|
|
92
128
|
strict_1.default.throws(() => (0, cron_js_1.validateCronExpression)('0 * * * * *'), /Invalid --cron/);
|
|
93
129
|
});
|
|
94
|
-
(0, node_test_1.it)('adds an OpenClaw cron job with
|
|
130
|
+
(0, node_test_1.it)('adds an OpenClaw cron job with no-delivery fallback and cron schedule', async () => {
|
|
95
131
|
const gateway = fakeGateway();
|
|
96
132
|
const result = await (0, cron_js_1.installOpenClawThreatFeedCron)({ name: 'agentguard-threat-feed', cronExpression: '0 * * * *', quiet: false, force: false, timezone: 'Asia/Shanghai' }, { request: gateway.request });
|
|
97
133
|
strict_1.default.equal(result.created, true);
|
|
@@ -101,13 +137,14 @@ function fakeGateway(jobs = []) {
|
|
|
101
137
|
const job = gateway.calls[1].params;
|
|
102
138
|
strict_1.default.equal(job.name, 'agentguard-threat-feed');
|
|
103
139
|
strict_1.default.deepEqual(job.schedule, { kind: 'cron', expr: '0 * * * *', tz: 'Asia/Shanghai' });
|
|
104
|
-
strict_1.default.deepEqual(job.delivery, { mode: '
|
|
140
|
+
strict_1.default.deepEqual(job.delivery, { mode: 'none' });
|
|
105
141
|
strict_1.default.equal(job.sessionTarget, 'isolated');
|
|
106
142
|
strict_1.default.equal(job.payload.kind, 'agentTurn');
|
|
107
143
|
strict_1.default.equal('agentguard' in job.payload, false);
|
|
108
144
|
strict_1.default.match(job.payload.message, /Mode: manual/);
|
|
109
|
-
strict_1.default.match(job.payload.message, /Command: `agentguard subscribe --cron-
|
|
110
|
-
strict_1.default.match(job.payload.message, /agentguard subscribe --cron-
|
|
145
|
+
strict_1.default.match(job.payload.message, /Command: `agentguard subscribe --json --cron-run`/);
|
|
146
|
+
strict_1.default.match(job.payload.message, /agentguard subscribe --json --cron-run/);
|
|
147
|
+
strict_1.default.match(job.payload.message, /handles its own OpenClaw notification delivery/);
|
|
111
148
|
strict_1.default.match(job.payload.message, /NO_REPLY/);
|
|
112
149
|
});
|
|
113
150
|
(0, node_test_1.it)('auto-installs system crontab jobs for Codex and Claude Code agents', async () => {
|
|
@@ -143,6 +180,78 @@ function fakeGateway(jobs = []) {
|
|
|
143
180
|
strict_1.default.match(script, new RegExp(`export AGENTGUARD_HOME='${home.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}'`));
|
|
144
181
|
strict_1.default.match(script, /exec agentguard subscribe --quiet --json --cron-run/);
|
|
145
182
|
});
|
|
183
|
+
(0, node_test_1.it)('removes the managed system crontab block without touching other entries', async () => {
|
|
184
|
+
const calls = [];
|
|
185
|
+
const home = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agentguard-system-remove-'));
|
|
186
|
+
const current = [
|
|
187
|
+
'# existing',
|
|
188
|
+
'# AgentGuard begin agentguard-threat-feed',
|
|
189
|
+
'0 * * * * /tmp/agentguard-threat-feed.sh',
|
|
190
|
+
'# AgentGuard end agentguard-threat-feed',
|
|
191
|
+
'15 * * * * /tmp/other-job.sh',
|
|
192
|
+
'',
|
|
193
|
+
].join('\n');
|
|
194
|
+
const runner = async (command, args, input) => {
|
|
195
|
+
calls.push({ command, args, input });
|
|
196
|
+
if (command === 'crontab' && args[0] === '-l') {
|
|
197
|
+
return { stdout: current, stderr: '' };
|
|
198
|
+
}
|
|
199
|
+
return { stdout: '', stderr: '' };
|
|
200
|
+
};
|
|
201
|
+
const result = await (0, cron_js_1.removeThreatFeedCron)({
|
|
202
|
+
name: 'agentguard-threat-feed',
|
|
203
|
+
backend: 'system',
|
|
204
|
+
agentGuardHome: home,
|
|
205
|
+
}, { runCommand: runner });
|
|
206
|
+
strict_1.default.deepEqual(result, [{ name: 'agentguard-threat-feed', backend: 'system', removed: true }]);
|
|
207
|
+
strict_1.default.deepEqual(calls.map((call) => call.args[0]), ['-l', '-']);
|
|
208
|
+
strict_1.default.match(calls[1].input ?? '', /# existing/);
|
|
209
|
+
strict_1.default.match(calls[1].input ?? '', /other-job/);
|
|
210
|
+
strict_1.default.doesNotMatch(calls[1].input ?? '', /AgentGuard begin agentguard-threat-feed/);
|
|
211
|
+
});
|
|
212
|
+
(0, node_test_1.it)('removes OpenClaw gateway cron jobs by default subscribe name', async () => {
|
|
213
|
+
const gateway = fakeGateway([{ id: 'job-1', name: 'agentguard-threat-feed' }]);
|
|
214
|
+
const result = await (0, cron_js_1.removeThreatFeedCron)({
|
|
215
|
+
name: 'agentguard-threat-feed',
|
|
216
|
+
backend: 'openclaw',
|
|
217
|
+
}, {
|
|
218
|
+
async runCommand() {
|
|
219
|
+
throw new Error('native openclaw unavailable');
|
|
220
|
+
},
|
|
221
|
+
gateway: { request: gateway.request },
|
|
222
|
+
});
|
|
223
|
+
strict_1.default.deepEqual(result.map((item) => item.backend), ['openclaw', 'openclaw-gateway']);
|
|
224
|
+
strict_1.default.equal(result[0].removed, false);
|
|
225
|
+
strict_1.default.match(result[0].error ?? '', /native openclaw unavailable/);
|
|
226
|
+
strict_1.default.equal(result[1].removed, true);
|
|
227
|
+
strict_1.default.deepEqual(gateway.calls.map((call) => call.method), ['cron.list', 'cron.remove']);
|
|
228
|
+
strict_1.default.deepEqual(gateway.calls[1].params, { jobId: 'job-1' });
|
|
229
|
+
});
|
|
230
|
+
(0, node_test_1.it)('removes native OpenClaw cron jobs by id', async () => {
|
|
231
|
+
const calls = [];
|
|
232
|
+
const runner = async (command, args) => {
|
|
233
|
+
calls.push({ command, args });
|
|
234
|
+
if (args.join(' ') === 'cron list') {
|
|
235
|
+
return {
|
|
236
|
+
stdout: JSON.stringify({
|
|
237
|
+
jobs: [
|
|
238
|
+
{ id: '7407b173-da3f-4ded-b6e3-722a9c5248b0', name: 'agentguard-threat-feed' },
|
|
239
|
+
],
|
|
240
|
+
}),
|
|
241
|
+
stderr: '',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return { stdout: '', stderr: '' };
|
|
245
|
+
};
|
|
246
|
+
const result = await (0, cron_js_1.removeThreatFeedCron)({
|
|
247
|
+
name: 'agentguard-threat-feed',
|
|
248
|
+
backend: 'openclaw',
|
|
249
|
+
}, { runCommand: runner, gateway: { request: fakeGateway().request } });
|
|
250
|
+
strict_1.default.equal(result[0].backend, 'openclaw');
|
|
251
|
+
strict_1.default.equal(result[0].removed, true);
|
|
252
|
+
strict_1.default.deepEqual(calls.map((call) => call.args.slice(0, 2).join(' ')), ['cron list', 'cron remove']);
|
|
253
|
+
strict_1.default.deepEqual(calls[1].args, ['cron', 'remove', '7407b173-da3f-4ded-b6e3-722a9c5248b0']);
|
|
254
|
+
});
|
|
146
255
|
(0, node_test_1.it)('rejects unsafe AgentGuard home paths for system crontab jobs', async () => {
|
|
147
256
|
await strict_1.default.rejects(() => (0, cron_js_1.installThreatFeedCron)({
|
|
148
257
|
name: 'agentguard-threat-feed',
|
|
@@ -201,9 +310,8 @@ function fakeGateway(jobs = []) {
|
|
|
201
310
|
strict_1.default.deepEqual(calls.map((call) => call.args.slice(0, 2).join(' ')), ['cron list', 'cron add']);
|
|
202
311
|
strict_1.default.ok(calls[1].args.includes('--timeout-seconds'));
|
|
203
312
|
strict_1.default.ok(calls[1].args.includes('300'));
|
|
204
|
-
strict_1.default.ok(calls[1].args.includes('--
|
|
205
|
-
strict_1.default.ok(calls[1].args.includes('--
|
|
206
|
-
strict_1.default.ok(calls[1].args.includes('last'));
|
|
313
|
+
strict_1.default.ok(calls[1].args.includes('--no-deliver'));
|
|
314
|
+
strict_1.default.ok(!calls[1].args.includes('--announce'));
|
|
207
315
|
});
|
|
208
316
|
(0, node_test_1.it)('does not treat native OpenClaw cron name substrings as existing jobs', async () => {
|
|
209
317
|
const calls = [];
|
|
@@ -247,6 +355,35 @@ function fakeGateway(jobs = []) {
|
|
|
247
355
|
strict_1.default.equal(result.created, false);
|
|
248
356
|
strict_1.default.deepEqual(calls.map((call) => call.args.slice(0, 2).join(' ')), ['cron list']);
|
|
249
357
|
});
|
|
358
|
+
(0, node_test_1.it)('replaces native OpenClaw cron jobs by id when force is set', async () => {
|
|
359
|
+
const calls = [];
|
|
360
|
+
const runner = async (command, args) => {
|
|
361
|
+
calls.push({ command, args });
|
|
362
|
+
if (args.join(' ') === 'cron list') {
|
|
363
|
+
return {
|
|
364
|
+
stdout: [
|
|
365
|
+
'ID Name Schedule',
|
|
366
|
+
'7407b173-da3f-4ded-b6e3-722a9c5248b0 agentguard-threat-feed cron */5 * * * * @ UTC',
|
|
367
|
+
].join('\n'),
|
|
368
|
+
stderr: '',
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return { stdout: '', stderr: '' };
|
|
372
|
+
};
|
|
373
|
+
const result = await (0, cron_js_1.installThreatFeedCron)({
|
|
374
|
+
name: 'agentguard-threat-feed',
|
|
375
|
+
cronExpression: '*/5 * * * *',
|
|
376
|
+
quiet: true,
|
|
377
|
+
force: true,
|
|
378
|
+
backend: 'auto',
|
|
379
|
+
agentHost: 'openclaw',
|
|
380
|
+
timezone: 'UTC',
|
|
381
|
+
}, { runCommand: runner });
|
|
382
|
+
strict_1.default.equal(result.created, true);
|
|
383
|
+
strict_1.default.deepEqual(calls.map((call) => call.args.slice(0, 2).join(' ')), ['cron list', 'cron remove', 'cron add']);
|
|
384
|
+
strict_1.default.deepEqual(calls[1].args, ['cron', 'remove', '7407b173-da3f-4ded-b6e3-722a9c5248b0']);
|
|
385
|
+
strict_1.default.ok(!calls[2].args.includes('--force'));
|
|
386
|
+
});
|
|
250
387
|
(0, node_test_1.it)('does not fall back to OpenClaw Gateway when native OpenClaw cron add fails', async () => {
|
|
251
388
|
const gateway = fakeGateway();
|
|
252
389
|
const runner = async (_command, args) => {
|
|
@@ -287,6 +424,8 @@ function fakeGateway(jobs = []) {
|
|
|
287
424
|
strict_1.default.deepEqual(job.delivery, { mode: 'announce', channel: 'last' });
|
|
288
425
|
strict_1.default.equal('agentguard' in job.payload, false);
|
|
289
426
|
strict_1.default.match(job.payload.message, /Command: `agentguard subscribe --cron-notify-run`/);
|
|
427
|
+
strict_1.default.match(job.payload.message, /remediation guidance/);
|
|
428
|
+
strict_1.default.match(job.payload.message, /manual response steps/);
|
|
290
429
|
});
|
|
291
430
|
(0, node_test_1.it)('auto-installs native Hermes cron jobs for Hermes agents', async () => {
|
|
292
431
|
const calls = [];
|
|
@@ -367,6 +506,17 @@ function fakeGateway(jobs = []) {
|
|
|
367
506
|
strict_1.default.equal(result.backend, 'openclaw-gateway');
|
|
368
507
|
strict_1.default.deepEqual(gateway.calls.map((call) => call.method), ['cron.list', 'cron.add']);
|
|
369
508
|
});
|
|
509
|
+
(0, node_test_1.it)('rejects an explicit OpenClaw cron target when the saved agent host is different', async () => {
|
|
510
|
+
await strict_1.default.rejects(() => (0, cron_js_1.installThreatFeedCron)({
|
|
511
|
+
name: 'agentguard-threat-feed',
|
|
512
|
+
cronExpression: '0 * * * *',
|
|
513
|
+
quiet: false,
|
|
514
|
+
force: false,
|
|
515
|
+
backend: 'openclaw',
|
|
516
|
+
agentHost: 'codex',
|
|
517
|
+
timezone: 'UTC',
|
|
518
|
+
}), /Cron target openclaw conflicts with saved agent host "codex"/);
|
|
519
|
+
});
|
|
370
520
|
(0, node_test_1.it)('fails fast when OpenClaw Gateway cron.list is unavailable', async () => {
|
|
371
521
|
await strict_1.default.rejects(() => (0, cron_js_1.installOpenClawThreatFeedCron)({ name: 'agentguard-threat-feed', cronExpression: '0 * * * *', quiet: false, force: false, timezone: 'UTC' }, {
|
|
372
522
|
async request(method) {
|
|
@@ -391,9 +541,9 @@ function fakeGateway(jobs = []) {
|
|
|
391
541
|
strict_1.default.deepEqual(gateway.calls[2].params.schedule, { kind: 'cron', expr: '*/5 * * * *', tz: 'UTC' });
|
|
392
542
|
strict_1.default.equal('agentguard' in gateway.calls[2].params.payload, false);
|
|
393
543
|
strict_1.default.match(gateway.calls[2].params.payload.message, /Mode: quiet/);
|
|
394
|
-
strict_1.default.deepEqual(gateway.calls[2].params.delivery, { mode: '
|
|
395
|
-
strict_1.default.match(gateway.calls[2].params.payload.message, /Command: `agentguard subscribe --quiet --cron-
|
|
396
|
-
strict_1.default.match(gateway.calls[2].params.payload.message, /agentguard subscribe --quiet --cron-
|
|
544
|
+
strict_1.default.deepEqual(gateway.calls[2].params.delivery, { mode: 'none' });
|
|
545
|
+
strict_1.default.match(gateway.calls[2].params.payload.message, /Command: `agentguard subscribe --quiet --json --cron-run`/);
|
|
546
|
+
strict_1.default.match(gateway.calls[2].params.payload.message, /agentguard subscribe --quiet --json --cron-run/);
|
|
397
547
|
});
|
|
398
548
|
(0, node_test_1.it)('does not add a replacement if force removal fails', async () => {
|
|
399
549
|
const calls = [];
|
|
@@ -416,11 +566,39 @@ function fakeGateway(jobs = []) {
|
|
|
416
566
|
},
|
|
417
567
|
}), /timed out/);
|
|
418
568
|
});
|
|
569
|
+
(0, node_test_1.it)('prefers the OpenClaw CLI Gateway call for default local OpenClaw requests', async () => {
|
|
570
|
+
const calls = [];
|
|
571
|
+
const result = await (0, cron_js_1.openClawGatewayRequest)('sessions.list', { limit: 1 }, {
|
|
572
|
+
timeoutMs: 1234,
|
|
573
|
+
runCommand: async (command, args) => {
|
|
574
|
+
calls.push({ command, args });
|
|
575
|
+
return {
|
|
576
|
+
stdout: JSON.stringify({ sessions: [{ key: 'session-1' }] }),
|
|
577
|
+
stderr: '',
|
|
578
|
+
};
|
|
579
|
+
},
|
|
580
|
+
});
|
|
581
|
+
strict_1.default.deepEqual(result, { sessions: [{ key: 'session-1' }] });
|
|
582
|
+
strict_1.default.equal(calls.length, 1);
|
|
583
|
+
strict_1.default.equal(calls[0].command, 'openclaw');
|
|
584
|
+
strict_1.default.deepEqual(calls[0].args, [
|
|
585
|
+
'gateway',
|
|
586
|
+
'call',
|
|
587
|
+
'sessions.list',
|
|
588
|
+
'--params',
|
|
589
|
+
'{"limit":1}',
|
|
590
|
+
'--timeout',
|
|
591
|
+
'1234',
|
|
592
|
+
'--json',
|
|
593
|
+
]);
|
|
594
|
+
});
|
|
419
595
|
(0, node_test_1.it)('keeps the default HTTP JSON-RPC Gateway path and legacy cron.add params', async () => {
|
|
420
596
|
let requestBody;
|
|
597
|
+
let authorization;
|
|
421
598
|
const server = node_http_1.default.createServer((req, res) => {
|
|
422
599
|
strict_1.default.equal(req.method, 'POST');
|
|
423
600
|
strict_1.default.equal(req.url, '/');
|
|
601
|
+
authorization = req.headers.authorization;
|
|
424
602
|
let raw = '';
|
|
425
603
|
req.setEncoding('utf8');
|
|
426
604
|
req.on('data', (chunk) => {
|
|
@@ -437,9 +615,14 @@ function fakeGateway(jobs = []) {
|
|
|
437
615
|
const result = await (0, cron_js_1.openClawGatewayRequest)('cron.add', { name: 'agentguard-threat-feed' }, {
|
|
438
616
|
host: '127.0.0.1',
|
|
439
617
|
port: serverPort(server),
|
|
618
|
+
token: 'gateway-test-token',
|
|
440
619
|
timeoutMs: 100,
|
|
620
|
+
runCommand: async () => {
|
|
621
|
+
throw new Error('explicit host/port should skip OpenClaw CLI');
|
|
622
|
+
},
|
|
441
623
|
});
|
|
442
624
|
strict_1.default.deepEqual(result, { ok: true });
|
|
625
|
+
strict_1.default.equal(authorization, 'Bearer gateway-test-token');
|
|
443
626
|
strict_1.default.equal(requestBody.method, 'cron.add');
|
|
444
627
|
strict_1.default.deepEqual(requestBody.params, [{ name: 'agentguard-threat-feed' }]);
|
|
445
628
|
}
|
|
@@ -447,6 +630,67 @@ function fakeGateway(jobs = []) {
|
|
|
447
630
|
await closeServer(server);
|
|
448
631
|
}
|
|
449
632
|
});
|
|
633
|
+
(0, node_test_1.it)('loads the local OpenClaw Gateway token for direct HTTP fallback requests', async () => {
|
|
634
|
+
const stateDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agentguard-openclaw-token-state-'));
|
|
635
|
+
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
|
|
636
|
+
const previousConfigPath = process.env.OPENCLAW_CONFIG_PATH;
|
|
637
|
+
const previousAgentGuardToken = process.env.AGENTGUARD_OPENCLAW_GATEWAY_TOKEN;
|
|
638
|
+
const previousOpenClawToken = process.env.OPENCLAW_GATEWAY_TOKEN;
|
|
639
|
+
let authorization;
|
|
640
|
+
(0, node_fs_1.mkdirSync)(stateDir, { recursive: true });
|
|
641
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(stateDir, 'openclaw.json'), JSON.stringify({
|
|
642
|
+
gateway: {
|
|
643
|
+
auth: {
|
|
644
|
+
token: 'config-gateway-token',
|
|
645
|
+
},
|
|
646
|
+
},
|
|
647
|
+
}));
|
|
648
|
+
process.env.OPENCLAW_STATE_DIR = stateDir;
|
|
649
|
+
delete process.env.OPENCLAW_CONFIG_PATH;
|
|
650
|
+
delete process.env.AGENTGUARD_OPENCLAW_GATEWAY_TOKEN;
|
|
651
|
+
delete process.env.OPENCLAW_GATEWAY_TOKEN;
|
|
652
|
+
const server = node_http_1.default.createServer((req, res) => {
|
|
653
|
+
authorization = req.headers.authorization;
|
|
654
|
+
let raw = '';
|
|
655
|
+
req.setEncoding('utf8');
|
|
656
|
+
req.on('data', (chunk) => {
|
|
657
|
+
raw += chunk;
|
|
658
|
+
});
|
|
659
|
+
req.on('end', () => {
|
|
660
|
+
const requestBody = JSON.parse(raw);
|
|
661
|
+
res.setHeader('Content-Type', 'application/json');
|
|
662
|
+
res.end(JSON.stringify({ jsonrpc: '2.0', id: requestBody.id, result: { sessions: [] } }));
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
666
|
+
try {
|
|
667
|
+
await (0, cron_js_1.openClawGatewayRequest)('sessions.list', {}, {
|
|
668
|
+
host: '127.0.0.1',
|
|
669
|
+
port: serverPort(server),
|
|
670
|
+
timeoutMs: 100,
|
|
671
|
+
});
|
|
672
|
+
strict_1.default.equal(authorization, 'Bearer config-gateway-token');
|
|
673
|
+
}
|
|
674
|
+
finally {
|
|
675
|
+
if (previousStateDir === undefined)
|
|
676
|
+
delete process.env.OPENCLAW_STATE_DIR;
|
|
677
|
+
else
|
|
678
|
+
process.env.OPENCLAW_STATE_DIR = previousStateDir;
|
|
679
|
+
if (previousConfigPath === undefined)
|
|
680
|
+
delete process.env.OPENCLAW_CONFIG_PATH;
|
|
681
|
+
else
|
|
682
|
+
process.env.OPENCLAW_CONFIG_PATH = previousConfigPath;
|
|
683
|
+
if (previousAgentGuardToken === undefined)
|
|
684
|
+
delete process.env.AGENTGUARD_OPENCLAW_GATEWAY_TOKEN;
|
|
685
|
+
else
|
|
686
|
+
process.env.AGENTGUARD_OPENCLAW_GATEWAY_TOKEN = previousAgentGuardToken;
|
|
687
|
+
if (previousOpenClawToken === undefined)
|
|
688
|
+
delete process.env.OPENCLAW_GATEWAY_TOKEN;
|
|
689
|
+
else
|
|
690
|
+
process.env.OPENCLAW_GATEWAY_TOKEN = previousOpenClawToken;
|
|
691
|
+
await closeServer(server);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
450
694
|
(0, node_test_1.it)('handles fragmented WebSocket Gateway text responses', async () => {
|
|
451
695
|
const server = node_net_1.default.createServer((socket) => {
|
|
452
696
|
let handshakeComplete = false;
|
|
@@ -512,5 +756,257 @@ function fakeGateway(jobs = []) {
|
|
|
512
756
|
await closeServer(server);
|
|
513
757
|
}
|
|
514
758
|
});
|
|
759
|
+
(0, node_test_1.it)('sends signed device identity during the WebSocket connect handshake when OpenClaw identity exists', async () => {
|
|
760
|
+
const stateDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agentguard-openclaw-state-'));
|
|
761
|
+
const identity = writeOpenClawIdentity(stateDir);
|
|
762
|
+
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
|
|
763
|
+
process.env.OPENCLAW_STATE_DIR = stateDir;
|
|
764
|
+
let connectParams;
|
|
765
|
+
const server = node_net_1.default.createServer((socket) => {
|
|
766
|
+
let handshakeComplete = false;
|
|
767
|
+
let buffer = Buffer.alloc(0);
|
|
768
|
+
let clientRequests = 0;
|
|
769
|
+
socket.on('data', (chunk) => {
|
|
770
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
771
|
+
if (!handshakeComplete) {
|
|
772
|
+
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
773
|
+
if (headerEnd === -1)
|
|
774
|
+
return;
|
|
775
|
+
const header = buffer.subarray(0, headerEnd + 4).toString('utf8');
|
|
776
|
+
const key = /^Sec-WebSocket-Key:\s*(.+)$/im.exec(header)?.[1]?.trim();
|
|
777
|
+
strict_1.default.ok(key);
|
|
778
|
+
const accept = (0, node_crypto_1.createHash)('sha1')
|
|
779
|
+
.update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
|
|
780
|
+
.digest('base64');
|
|
781
|
+
socket.write([
|
|
782
|
+
'HTTP/1.1 101 Switching Protocols',
|
|
783
|
+
'Upgrade: websocket',
|
|
784
|
+
'Connection: Upgrade',
|
|
785
|
+
`Sec-WebSocket-Accept: ${accept}`,
|
|
786
|
+
'',
|
|
787
|
+
'',
|
|
788
|
+
].join('\r\n'));
|
|
789
|
+
handshakeComplete = true;
|
|
790
|
+
buffer = buffer.subarray(headerEnd + 4);
|
|
791
|
+
socket.write(encodeServerWebSocketFrame(JSON.stringify({
|
|
792
|
+
type: 'event',
|
|
793
|
+
event: 'connect.challenge',
|
|
794
|
+
payload: { nonce: 'nonce-1' },
|
|
795
|
+
})));
|
|
796
|
+
}
|
|
797
|
+
while (true) {
|
|
798
|
+
const parsed = readClientWebSocketFrame(buffer);
|
|
799
|
+
if (!parsed)
|
|
800
|
+
break;
|
|
801
|
+
buffer = parsed.rest;
|
|
802
|
+
clientRequests += 1;
|
|
803
|
+
const frame = JSON.parse(parsed.payload);
|
|
804
|
+
if (clientRequests === 1) {
|
|
805
|
+
connectParams = frame.params;
|
|
806
|
+
socket.write(encodeServerWebSocketFrame(JSON.stringify({ type: 'res', id: frame.id, ok: true, payload: {} })));
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
socket.write(encodeServerWebSocketFrame(JSON.stringify({
|
|
810
|
+
type: 'res',
|
|
811
|
+
id: frame.id,
|
|
812
|
+
ok: true,
|
|
813
|
+
payload: { jobs: [] },
|
|
814
|
+
})));
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
});
|
|
818
|
+
});
|
|
819
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
820
|
+
try {
|
|
821
|
+
const result = await (0, cron_js_1.openClawGatewayRequest)('cron.list', {}, {
|
|
822
|
+
url: `ws://127.0.0.1:${serverPort(server)}`,
|
|
823
|
+
timeoutMs: 500,
|
|
824
|
+
});
|
|
825
|
+
strict_1.default.deepEqual(result, { jobs: [] });
|
|
826
|
+
strict_1.default.equal(connectParams.minProtocol, 3);
|
|
827
|
+
strict_1.default.equal(connectParams.maxProtocol, 4);
|
|
828
|
+
strict_1.default.equal(connectParams.client.id, 'cli');
|
|
829
|
+
strict_1.default.equal(connectParams.device.id, identity.deviceId);
|
|
830
|
+
strict_1.default.equal(connectParams.device.publicKey, publicKeyRawBase64UrlFromPem(identity.publicKeyPem));
|
|
831
|
+
strict_1.default.equal(connectParams.device.nonce, 'nonce-1');
|
|
832
|
+
strict_1.default.equal(typeof connectParams.device.signedAt, 'number');
|
|
833
|
+
const signedPayload = [
|
|
834
|
+
'v3',
|
|
835
|
+
identity.deviceId,
|
|
836
|
+
'cli',
|
|
837
|
+
'cli',
|
|
838
|
+
'operator',
|
|
839
|
+
'operator.admin,operator.read,operator.write,operator.approvals,operator.pairing,operator.talk.secrets',
|
|
840
|
+
String(connectParams.device.signedAt),
|
|
841
|
+
'',
|
|
842
|
+
'nonce-1',
|
|
843
|
+
process.platform,
|
|
844
|
+
'',
|
|
845
|
+
].join('|');
|
|
846
|
+
strict_1.default.equal((0, node_crypto_1.verify)(null, Buffer.from(signedPayload, 'utf8'), (0, node_crypto_1.createPublicKey)(identity.publicKeyPem), base64UrlDecode(connectParams.device.signature)), true);
|
|
847
|
+
}
|
|
848
|
+
finally {
|
|
849
|
+
if (previousStateDir === undefined)
|
|
850
|
+
delete process.env.OPENCLAW_STATE_DIR;
|
|
851
|
+
else
|
|
852
|
+
process.env.OPENCLAW_STATE_DIR = previousStateDir;
|
|
853
|
+
await closeServer(server);
|
|
854
|
+
}
|
|
855
|
+
});
|
|
856
|
+
(0, node_test_1.it)('sends the Gateway token during the WebSocket connect handshake', async () => {
|
|
857
|
+
let connectParams;
|
|
858
|
+
const server = node_net_1.default.createServer((socket) => {
|
|
859
|
+
let handshakeComplete = false;
|
|
860
|
+
let buffer = Buffer.alloc(0);
|
|
861
|
+
let clientRequests = 0;
|
|
862
|
+
socket.on('data', (chunk) => {
|
|
863
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
864
|
+
if (!handshakeComplete) {
|
|
865
|
+
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
866
|
+
if (headerEnd === -1)
|
|
867
|
+
return;
|
|
868
|
+
const header = buffer.subarray(0, headerEnd + 4).toString('utf8');
|
|
869
|
+
const key = /^Sec-WebSocket-Key:\s*(.+)$/im.exec(header)?.[1]?.trim();
|
|
870
|
+
strict_1.default.ok(key);
|
|
871
|
+
const accept = (0, node_crypto_1.createHash)('sha1')
|
|
872
|
+
.update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
|
|
873
|
+
.digest('base64');
|
|
874
|
+
socket.write([
|
|
875
|
+
'HTTP/1.1 101 Switching Protocols',
|
|
876
|
+
'Upgrade: websocket',
|
|
877
|
+
'Connection: Upgrade',
|
|
878
|
+
`Sec-WebSocket-Accept: ${accept}`,
|
|
879
|
+
'',
|
|
880
|
+
'',
|
|
881
|
+
].join('\r\n'));
|
|
882
|
+
handshakeComplete = true;
|
|
883
|
+
buffer = buffer.subarray(headerEnd + 4);
|
|
884
|
+
socket.write(encodeServerWebSocketFrame(JSON.stringify({
|
|
885
|
+
type: 'event',
|
|
886
|
+
event: 'connect.challenge',
|
|
887
|
+
payload: { nonce: 'nonce-token' },
|
|
888
|
+
})));
|
|
889
|
+
}
|
|
890
|
+
while (true) {
|
|
891
|
+
const parsed = readClientWebSocketFrame(buffer);
|
|
892
|
+
if (!parsed)
|
|
893
|
+
break;
|
|
894
|
+
buffer = parsed.rest;
|
|
895
|
+
clientRequests += 1;
|
|
896
|
+
const frame = JSON.parse(parsed.payload);
|
|
897
|
+
if (clientRequests === 1) {
|
|
898
|
+
connectParams = frame.params;
|
|
899
|
+
socket.write(encodeServerWebSocketFrame(JSON.stringify({ type: 'res', id: frame.id, ok: true, payload: {} })));
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
socket.write(encodeServerWebSocketFrame(JSON.stringify({
|
|
903
|
+
type: 'res',
|
|
904
|
+
id: frame.id,
|
|
905
|
+
ok: true,
|
|
906
|
+
payload: { jobs: [] },
|
|
907
|
+
})));
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
});
|
|
912
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
913
|
+
try {
|
|
914
|
+
const result = await (0, cron_js_1.openClawGatewayRequest)('cron.list', {}, {
|
|
915
|
+
url: `ws://127.0.0.1:${serverPort(server)}`,
|
|
916
|
+
token: 'gateway-websocket-token',
|
|
917
|
+
timeoutMs: 500,
|
|
918
|
+
});
|
|
919
|
+
strict_1.default.deepEqual(result, { jobs: [] });
|
|
920
|
+
strict_1.default.equal(connectParams.auth.token, 'gateway-websocket-token');
|
|
921
|
+
}
|
|
922
|
+
finally {
|
|
923
|
+
await closeServer(server);
|
|
924
|
+
}
|
|
925
|
+
});
|
|
926
|
+
(0, node_test_1.it)('omits device auth instead of failing when OpenClaw identity keys are invalid', async () => {
|
|
927
|
+
const stateDir = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'agentguard-openclaw-bad-state-'));
|
|
928
|
+
const identityDir = (0, node_path_1.join)(stateDir, 'identity');
|
|
929
|
+
(0, node_fs_1.mkdirSync)(identityDir, { recursive: true });
|
|
930
|
+
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(identityDir, 'device.json'), JSON.stringify({
|
|
931
|
+
version: 1,
|
|
932
|
+
deviceId: 'bad-device',
|
|
933
|
+
publicKeyPem: 'not a public key',
|
|
934
|
+
privateKeyPem: 'not a private key',
|
|
935
|
+
}));
|
|
936
|
+
const previousStateDir = process.env.OPENCLAW_STATE_DIR;
|
|
937
|
+
process.env.OPENCLAW_STATE_DIR = stateDir;
|
|
938
|
+
let connectParams;
|
|
939
|
+
const server = node_net_1.default.createServer((socket) => {
|
|
940
|
+
let handshakeComplete = false;
|
|
941
|
+
let buffer = Buffer.alloc(0);
|
|
942
|
+
let clientRequests = 0;
|
|
943
|
+
socket.on('data', (chunk) => {
|
|
944
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
945
|
+
if (!handshakeComplete) {
|
|
946
|
+
const headerEnd = buffer.indexOf('\r\n\r\n');
|
|
947
|
+
if (headerEnd === -1)
|
|
948
|
+
return;
|
|
949
|
+
const header = buffer.subarray(0, headerEnd + 4).toString('utf8');
|
|
950
|
+
const key = /^Sec-WebSocket-Key:\s*(.+)$/im.exec(header)?.[1]?.trim();
|
|
951
|
+
strict_1.default.ok(key);
|
|
952
|
+
const accept = (0, node_crypto_1.createHash)('sha1')
|
|
953
|
+
.update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
|
|
954
|
+
.digest('base64');
|
|
955
|
+
socket.write([
|
|
956
|
+
'HTTP/1.1 101 Switching Protocols',
|
|
957
|
+
'Upgrade: websocket',
|
|
958
|
+
'Connection: Upgrade',
|
|
959
|
+
`Sec-WebSocket-Accept: ${accept}`,
|
|
960
|
+
'',
|
|
961
|
+
'',
|
|
962
|
+
].join('\r\n'));
|
|
963
|
+
handshakeComplete = true;
|
|
964
|
+
buffer = buffer.subarray(headerEnd + 4);
|
|
965
|
+
socket.write(encodeServerWebSocketFrame(JSON.stringify({
|
|
966
|
+
type: 'event',
|
|
967
|
+
event: 'connect.challenge',
|
|
968
|
+
payload: { nonce: 'nonce-bad-identity' },
|
|
969
|
+
})));
|
|
970
|
+
}
|
|
971
|
+
while (true) {
|
|
972
|
+
const parsed = readClientWebSocketFrame(buffer);
|
|
973
|
+
if (!parsed)
|
|
974
|
+
break;
|
|
975
|
+
buffer = parsed.rest;
|
|
976
|
+
clientRequests += 1;
|
|
977
|
+
const frame = JSON.parse(parsed.payload);
|
|
978
|
+
if (clientRequests === 1) {
|
|
979
|
+
connectParams = frame.params;
|
|
980
|
+
socket.write(encodeServerWebSocketFrame(JSON.stringify({ type: 'res', id: frame.id, ok: true, payload: {} })));
|
|
981
|
+
}
|
|
982
|
+
else {
|
|
983
|
+
socket.write(encodeServerWebSocketFrame(JSON.stringify({
|
|
984
|
+
type: 'res',
|
|
985
|
+
id: frame.id,
|
|
986
|
+
ok: true,
|
|
987
|
+
payload: { jobs: [] },
|
|
988
|
+
})));
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
});
|
|
992
|
+
});
|
|
993
|
+
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
994
|
+
try {
|
|
995
|
+
const result = await (0, cron_js_1.openClawGatewayRequest)('cron.list', {}, {
|
|
996
|
+
url: `ws://127.0.0.1:${serverPort(server)}`,
|
|
997
|
+
timeoutMs: 500,
|
|
998
|
+
});
|
|
999
|
+
strict_1.default.deepEqual(result, { jobs: [] });
|
|
1000
|
+
strict_1.default.equal(connectParams.client.id, 'cli');
|
|
1001
|
+
strict_1.default.equal(connectParams.device, undefined);
|
|
1002
|
+
}
|
|
1003
|
+
finally {
|
|
1004
|
+
if (previousStateDir === undefined)
|
|
1005
|
+
delete process.env.OPENCLAW_STATE_DIR;
|
|
1006
|
+
else
|
|
1007
|
+
process.env.OPENCLAW_STATE_DIR = previousStateDir;
|
|
1008
|
+
await closeServer(server);
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
515
1011
|
});
|
|
516
1012
|
//# sourceMappingURL=feed-cron.test.js.map
|