@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.
Files changed (74) hide show
  1. package/README.md +8 -2
  2. package/dist/adapters/engine.d.ts.map +1 -1
  3. package/dist/adapters/engine.js +10 -0
  4. package/dist/adapters/engine.js.map +1 -1
  5. package/dist/adapters/openclaw-plugin.d.ts.map +1 -1
  6. package/dist/adapters/openclaw-plugin.js +6 -18
  7. package/dist/adapters/openclaw-plugin.js.map +1 -1
  8. package/dist/cli.js +525 -42
  9. package/dist/cli.js.map +1 -1
  10. package/dist/cloud/client.d.ts +16 -2
  11. package/dist/cloud/client.d.ts.map +1 -1
  12. package/dist/cloud/client.js +56 -9
  13. package/dist/cloud/client.js.map +1 -1
  14. package/dist/cloud/openclaw-notify.d.ts +18 -0
  15. package/dist/cloud/openclaw-notify.d.ts.map +1 -0
  16. package/dist/cloud/openclaw-notify.js +69 -0
  17. package/dist/cloud/openclaw-notify.js.map +1 -0
  18. package/dist/config.d.ts +14 -0
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +51 -0
  21. package/dist/config.js.map +1 -1
  22. package/dist/feed/cron.d.ts +18 -0
  23. package/dist/feed/cron.d.ts.map +1 -1
  24. package/dist/feed/cron.js +499 -25
  25. package/dist/feed/cron.js.map +1 -1
  26. package/dist/feed/selfcheck.d.ts.map +1 -1
  27. package/dist/feed/selfcheck.js +470 -23
  28. package/dist/feed/selfcheck.js.map +1 -1
  29. package/dist/feed/types.d.ts +7 -8
  30. package/dist/feed/types.d.ts.map +1 -1
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +3 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/installers.d.ts.map +1 -1
  36. package/dist/installers.js +101 -2
  37. package/dist/installers.js.map +1 -1
  38. package/dist/runtime/evaluator.d.ts.map +1 -1
  39. package/dist/runtime/evaluator.js +45 -3
  40. package/dist/runtime/evaluator.js.map +1 -1
  41. package/dist/runtime/protect.d.ts.map +1 -1
  42. package/dist/runtime/protect.js +15 -2
  43. package/dist/runtime/protect.js.map +1 -1
  44. package/dist/runtime/self-command.d.ts +2 -0
  45. package/dist/runtime/self-command.d.ts.map +1 -0
  46. package/dist/runtime/self-command.js +73 -0
  47. package/dist/runtime/self-command.js.map +1 -0
  48. package/dist/tests/cli-checkup.test.js +1 -1
  49. package/dist/tests/cli-checkup.test.js.map +1 -1
  50. package/dist/tests/cli-connect.test.d.ts +2 -0
  51. package/dist/tests/cli-connect.test.d.ts.map +1 -0
  52. package/dist/tests/cli-connect.test.js +326 -0
  53. package/dist/tests/cli-connect.test.js.map +1 -0
  54. package/dist/tests/cli-init.test.js +141 -0
  55. package/dist/tests/cli-init.test.js.map +1 -1
  56. package/dist/tests/cli-policy.test.js +72 -0
  57. package/dist/tests/cli-policy.test.js.map +1 -1
  58. package/dist/tests/cli-subscribe.test.js +295 -2
  59. package/dist/tests/cli-subscribe.test.js.map +1 -1
  60. package/dist/tests/feed-cloud.test.js +45 -1
  61. package/dist/tests/feed-cloud.test.js.map +1 -1
  62. package/dist/tests/feed-cron.test.js +506 -10
  63. package/dist/tests/feed-cron.test.js.map +1 -1
  64. package/dist/tests/feed-selfcheck.test.js +61 -13
  65. package/dist/tests/feed-selfcheck.test.js.map +1 -1
  66. package/dist/tests/installer.test.js +69 -0
  67. package/dist/tests/installer.test.js.map +1 -1
  68. package/dist/tests/integration.test.js +41 -9
  69. package/dist/tests/integration.test.js.map +1 -1
  70. package/dist/tests/runtime-cloud.test.js +148 -0
  71. package/dist/tests/runtime-cloud.test.js.map +1 -1
  72. package/openclaw.plugin.json +4 -0
  73. package/package.json +1 -1
  74. 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 announce-last delivery and cron schedule', async () => {
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: 'announce', channel: 'last' });
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-notify-run`/);
110
- strict_1.default.match(job.payload.message, /agentguard subscribe --cron-notify-run/);
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('--announce'));
205
- strict_1.default.ok(calls[1].args.includes('--channel'));
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: 'announce', channel: 'last' });
395
- strict_1.default.match(gateway.calls[2].params.payload.message, /Command: `agentguard subscribe --quiet --cron-notify-run`/);
396
- strict_1.default.match(gateway.calls[2].params.payload.message, /agentguard subscribe --quiet --cron-notify-run/);
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