4runr-os 2.10.63 → 2.10.65

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.
@@ -199,20 +199,20 @@
199
199
  }
200
200
  },
201
201
  "node_modules/@aws-sdk/client-kms": {
202
- "version": "3.1058.0",
203
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-kms/-/client-kms-3.1058.0.tgz",
204
- "integrity": "sha512-sZ+vpNFa7ACfV0sMPCYR4dIMN9LUH/M/7OStsTADSNzTKiRQKTt//Vsfu7ku0tHEjjVX5fOc6oSeu9LRu4tD6Q==",
202
+ "version": "3.1059.0",
203
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-kms/-/client-kms-3.1059.0.tgz",
204
+ "integrity": "sha512-4FPu9hM3Lvb98hWpuwLeaGrFcuc59tdP2tpTLpONTTaf8H3umJNNriSAu/0FDK656XIh9pzj4E4a3qLK4RkQEw==",
205
205
  "license": "Apache-2.0",
206
206
  "dependencies": {
207
207
  "@aws-crypto/sha256-browser": "5.2.0",
208
208
  "@aws-crypto/sha256-js": "5.2.0",
209
- "@aws-sdk/core": "^3.974.15",
210
- "@aws-sdk/credential-provider-node": "^3.972.48",
211
- "@aws-sdk/types": "^3.973.9",
212
- "@smithy/core": "^3.24.5",
213
- "@smithy/fetch-http-handler": "^5.4.5",
214
- "@smithy/node-http-handler": "^4.7.5",
215
- "@smithy/types": "^4.14.2",
209
+ "@aws-sdk/core": "^3.974.16",
210
+ "@aws-sdk/credential-provider-node": "^3.972.49",
211
+ "@aws-sdk/types": "^3.973.10",
212
+ "@smithy/core": "^3.24.6",
213
+ "@smithy/fetch-http-handler": "^5.4.6",
214
+ "@smithy/node-http-handler": "^4.7.6",
215
+ "@smithy/types": "^4.14.3",
216
216
  "tslib": "^2.6.2"
217
217
  },
218
218
  "engines": {
@@ -220,20 +220,20 @@
220
220
  }
221
221
  },
222
222
  "node_modules/@aws-sdk/client-secrets-manager": {
223
- "version": "3.1058.0",
224
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1058.0.tgz",
225
- "integrity": "sha512-m6u7ns0aA1dJGKVhqz7HQFEyvMu5ae67bxVmolSH3rQwHnvjRxWMCgGKWljgHI/Pd2OiRtodZlLikafjo65EPg==",
223
+ "version": "3.1059.0",
224
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.1059.0.tgz",
225
+ "integrity": "sha512-gAxWLTS5DKdIEDUgc6ulyGZTG6ExvGjer/yQCPR02SrQ4e2957t0dsFBVfVk+qgXKLcHit4NPJJCC5BLdxPvpA==",
226
226
  "license": "Apache-2.0",
227
227
  "dependencies": {
228
228
  "@aws-crypto/sha256-browser": "5.2.0",
229
229
  "@aws-crypto/sha256-js": "5.2.0",
230
- "@aws-sdk/core": "^3.974.15",
231
- "@aws-sdk/credential-provider-node": "^3.972.48",
232
- "@aws-sdk/types": "^3.973.9",
233
- "@smithy/core": "^3.24.5",
234
- "@smithy/fetch-http-handler": "^5.4.5",
235
- "@smithy/node-http-handler": "^4.7.5",
236
- "@smithy/types": "^4.14.2",
230
+ "@aws-sdk/core": "^3.974.16",
231
+ "@aws-sdk/credential-provider-node": "^3.972.49",
232
+ "@aws-sdk/types": "^3.973.10",
233
+ "@smithy/core": "^3.24.6",
234
+ "@smithy/fetch-http-handler": "^5.4.6",
235
+ "@smithy/node-http-handler": "^4.7.6",
236
+ "@smithy/types": "^4.14.3",
237
237
  "tslib": "^2.6.2"
238
238
  },
239
239
  "engines": {
@@ -241,20 +241,20 @@
241
241
  }
242
242
  },
243
243
  "node_modules/@aws-sdk/client-ssm": {
244
- "version": "3.1058.0",
245
- "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.1058.0.tgz",
246
- "integrity": "sha512-xvJ0IahS3aepnIW1400dApwH3i/eJk+ggXNbNxnjHxjN8gozTFKn80HaboT/husAImv8qzJhZG/9ZeeT7o2USg==",
244
+ "version": "3.1059.0",
245
+ "resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.1059.0.tgz",
246
+ "integrity": "sha512-c6VaL8BKDeDSHWz59ELsYZcQit7r4aofVm07acWnxw1DQy5amQagOymPDhYwIpB6H3wZNMDu4FK3FxecNDzGww==",
247
247
  "license": "Apache-2.0",
248
248
  "dependencies": {
249
249
  "@aws-crypto/sha256-browser": "5.2.0",
250
250
  "@aws-crypto/sha256-js": "5.2.0",
251
- "@aws-sdk/core": "^3.974.15",
252
- "@aws-sdk/credential-provider-node": "^3.972.48",
253
- "@aws-sdk/types": "^3.973.9",
254
- "@smithy/core": "^3.24.5",
255
- "@smithy/fetch-http-handler": "^5.4.5",
256
- "@smithy/node-http-handler": "^4.7.5",
257
- "@smithy/types": "^4.14.2",
251
+ "@aws-sdk/core": "^3.974.16",
252
+ "@aws-sdk/credential-provider-node": "^3.972.49",
253
+ "@aws-sdk/types": "^3.973.10",
254
+ "@smithy/core": "^3.24.6",
255
+ "@smithy/fetch-http-handler": "^5.4.6",
256
+ "@smithy/node-http-handler": "^4.7.6",
257
+ "@smithy/types": "^4.14.3",
258
258
  "tslib": "^2.6.2"
259
259
  },
260
260
  "engines": {
@@ -262,17 +262,17 @@
262
262
  }
263
263
  },
264
264
  "node_modules/@aws-sdk/core": {
265
- "version": "3.974.15",
266
- "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.15.tgz",
267
- "integrity": "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw==",
265
+ "version": "3.974.16",
266
+ "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.16.tgz",
267
+ "integrity": "sha512-WXPvTfG7J2H4Ae6ewhd0285UC+8+9p/pKoibXXQlbXSqHexFLGM0oXHTwDfQEPmrNnvuWPpVjgoAUfW+cUFbXw==",
268
268
  "license": "Apache-2.0",
269
269
  "dependencies": {
270
- "@aws-sdk/types": "^3.973.9",
271
- "@aws-sdk/xml-builder": "^3.972.26",
270
+ "@aws-sdk/types": "^3.973.10",
271
+ "@aws-sdk/xml-builder": "^3.972.27",
272
272
  "@aws/lambda-invoke-store": "^0.2.2",
273
- "@smithy/core": "^3.24.5",
274
- "@smithy/signature-v4": "^5.4.5",
275
- "@smithy/types": "^4.14.2",
273
+ "@smithy/core": "^3.24.6",
274
+ "@smithy/signature-v4": "^5.4.6",
275
+ "@smithy/types": "^4.14.3",
276
276
  "bowser": "^2.11.0",
277
277
  "tslib": "^2.6.2"
278
278
  },
@@ -281,15 +281,15 @@
281
281
  }
282
282
  },
283
283
  "node_modules/@aws-sdk/credential-provider-env": {
284
- "version": "3.972.41",
285
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.41.tgz",
286
- "integrity": "sha512-n1EbJ98yvPWWdHZZv8bRBMqqDQJrtgtxyJ4xLy2Uqrh25BCOZQ7nnS1CsFXvuH8r0b0KVHDZEGEH5FxmEMP8jg==",
284
+ "version": "3.972.42",
285
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.42.tgz",
286
+ "integrity": "sha512-0+MCOYqeHyADFdeVr5/e1G8JJoWm7/szZAiKssqJS84E9+tO07509YNyQRJ5a7x5wO9YFAV324syrwm6yIcs5w==",
287
287
  "license": "Apache-2.0",
288
288
  "dependencies": {
289
- "@aws-sdk/core": "^3.974.15",
290
- "@aws-sdk/types": "^3.973.9",
291
- "@smithy/core": "^3.24.5",
292
- "@smithy/types": "^4.14.2",
289
+ "@aws-sdk/core": "^3.974.16",
290
+ "@aws-sdk/types": "^3.973.10",
291
+ "@smithy/core": "^3.24.6",
292
+ "@smithy/types": "^4.14.3",
293
293
  "tslib": "^2.6.2"
294
294
  },
295
295
  "engines": {
@@ -297,17 +297,17 @@
297
297
  }
298
298
  },
299
299
  "node_modules/@aws-sdk/credential-provider-http": {
300
- "version": "3.972.43",
301
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.43.tgz",
302
- "integrity": "sha512-TT76RN1NkI9WoyZqCNxOw6/WBMF7pYOTJcXbMokNFU+euSG40Kaf/t/FhDACVZWP+43wEM6ZynIPIkzS1wR1iA==",
300
+ "version": "3.972.44",
301
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.44.tgz",
302
+ "integrity": "sha512-auuhqlnv4PUdfqcdHLKGTdoCceXOuby6WNeCMoxtZmQGWNCibbz95/lSYzNWq9cExN17UlRFqo1nvTcC+zHfEg==",
303
303
  "license": "Apache-2.0",
304
304
  "dependencies": {
305
- "@aws-sdk/core": "^3.974.15",
306
- "@aws-sdk/types": "^3.973.9",
307
- "@smithy/core": "^3.24.5",
308
- "@smithy/fetch-http-handler": "^5.4.5",
309
- "@smithy/node-http-handler": "^4.7.5",
310
- "@smithy/types": "^4.14.2",
305
+ "@aws-sdk/core": "^3.974.16",
306
+ "@aws-sdk/types": "^3.973.10",
307
+ "@smithy/core": "^3.24.6",
308
+ "@smithy/fetch-http-handler": "^5.4.6",
309
+ "@smithy/node-http-handler": "^4.7.6",
310
+ "@smithy/types": "^4.14.3",
311
311
  "tslib": "^2.6.2"
312
312
  },
313
313
  "engines": {
@@ -315,23 +315,23 @@
315
315
  }
316
316
  },
317
317
  "node_modules/@aws-sdk/credential-provider-ini": {
318
- "version": "3.972.46",
319
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.46.tgz",
320
- "integrity": "sha512-hvcgcwOiS0nb2XFb5Op1Pz/vYaWz5K8kKullziGpdNRuG0NwzRXseuPt2CoBqknHGaSPVesu1aOn2OcctEYdCA==",
318
+ "version": "3.972.47",
319
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.47.tgz",
320
+ "integrity": "sha512-G+8JuG0CfLcC2IQXFVqMR1+KDF1rksebr+YL6+HHYbNjs/hiwX53ye45sU3pJKljpS2uIXqOrOsicHIv02vWrw==",
321
321
  "license": "Apache-2.0",
322
322
  "dependencies": {
323
- "@aws-sdk/core": "^3.974.15",
324
- "@aws-sdk/credential-provider-env": "^3.972.41",
325
- "@aws-sdk/credential-provider-http": "^3.972.43",
326
- "@aws-sdk/credential-provider-login": "^3.972.45",
327
- "@aws-sdk/credential-provider-process": "^3.972.41",
328
- "@aws-sdk/credential-provider-sso": "^3.972.45",
329
- "@aws-sdk/credential-provider-web-identity": "^3.972.45",
330
- "@aws-sdk/nested-clients": "^3.997.13",
331
- "@aws-sdk/types": "^3.973.9",
332
- "@smithy/core": "^3.24.5",
333
- "@smithy/credential-provider-imds": "^4.3.6",
334
- "@smithy/types": "^4.14.2",
323
+ "@aws-sdk/core": "^3.974.16",
324
+ "@aws-sdk/credential-provider-env": "^3.972.42",
325
+ "@aws-sdk/credential-provider-http": "^3.972.44",
326
+ "@aws-sdk/credential-provider-login": "^3.972.46",
327
+ "@aws-sdk/credential-provider-process": "^3.972.42",
328
+ "@aws-sdk/credential-provider-sso": "^3.972.46",
329
+ "@aws-sdk/credential-provider-web-identity": "^3.972.46",
330
+ "@aws-sdk/nested-clients": "^3.997.14",
331
+ "@aws-sdk/types": "^3.973.10",
332
+ "@smithy/core": "^3.24.6",
333
+ "@smithy/credential-provider-imds": "^4.3.7",
334
+ "@smithy/types": "^4.14.3",
335
335
  "tslib": "^2.6.2"
336
336
  },
337
337
  "engines": {
@@ -339,16 +339,16 @@
339
339
  }
340
340
  },
341
341
  "node_modules/@aws-sdk/credential-provider-login": {
342
- "version": "3.972.45",
343
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.45.tgz",
344
- "integrity": "sha512-MZQv4SNjByk1iOKmrqmzcUF/uCB05wjvEHyXKxmGQTUANTIVayX6HPUF0bzkWLvtnkH7sAn9kUCfkXbSpj9sDA==",
342
+ "version": "3.972.46",
343
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.46.tgz",
344
+ "integrity": "sha512-+H6h2/1Q+iIJ4w+FPCKM//xwy4C9yADknDTR68+K3PbOtB/uLup3zIH8PUKn6QwnttME4VM4ftSTnbvoDf5wXg==",
345
345
  "license": "Apache-2.0",
346
346
  "dependencies": {
347
- "@aws-sdk/core": "^3.974.15",
348
- "@aws-sdk/nested-clients": "^3.997.13",
349
- "@aws-sdk/types": "^3.973.9",
350
- "@smithy/core": "^3.24.5",
351
- "@smithy/types": "^4.14.2",
347
+ "@aws-sdk/core": "^3.974.16",
348
+ "@aws-sdk/nested-clients": "^3.997.14",
349
+ "@aws-sdk/types": "^3.973.10",
350
+ "@smithy/core": "^3.24.6",
351
+ "@smithy/types": "^4.14.3",
352
352
  "tslib": "^2.6.2"
353
353
  },
354
354
  "engines": {
@@ -356,21 +356,21 @@
356
356
  }
357
357
  },
358
358
  "node_modules/@aws-sdk/credential-provider-node": {
359
- "version": "3.972.48",
360
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.48.tgz",
361
- "integrity": "sha512-QIbtJP0olSLZ2ImEu636pP+7JJbPfaL3xSJIFXhu472CWuondCc4bGOa8OeyhOFet8z4H1D/ZFKXc39FboWwYA==",
359
+ "version": "3.972.49",
360
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.49.tgz",
361
+ "integrity": "sha512-hlyoc+2352BhC+HF91t9avIDJu1+EvQGGltxFB8ADCpAHGYNNLEQAZJIPzw3O/bRiiDSE2B8pdj/fFCXlDTEDg==",
362
362
  "license": "Apache-2.0",
363
363
  "dependencies": {
364
- "@aws-sdk/credential-provider-env": "^3.972.41",
365
- "@aws-sdk/credential-provider-http": "^3.972.43",
366
- "@aws-sdk/credential-provider-ini": "^3.972.46",
367
- "@aws-sdk/credential-provider-process": "^3.972.41",
368
- "@aws-sdk/credential-provider-sso": "^3.972.45",
369
- "@aws-sdk/credential-provider-web-identity": "^3.972.45",
370
- "@aws-sdk/types": "^3.973.9",
371
- "@smithy/core": "^3.24.5",
372
- "@smithy/credential-provider-imds": "^4.3.6",
373
- "@smithy/types": "^4.14.2",
364
+ "@aws-sdk/credential-provider-env": "^3.972.42",
365
+ "@aws-sdk/credential-provider-http": "^3.972.44",
366
+ "@aws-sdk/credential-provider-ini": "^3.972.47",
367
+ "@aws-sdk/credential-provider-process": "^3.972.42",
368
+ "@aws-sdk/credential-provider-sso": "^3.972.46",
369
+ "@aws-sdk/credential-provider-web-identity": "^3.972.46",
370
+ "@aws-sdk/types": "^3.973.10",
371
+ "@smithy/core": "^3.24.6",
372
+ "@smithy/credential-provider-imds": "^4.3.7",
373
+ "@smithy/types": "^4.14.3",
374
374
  "tslib": "^2.6.2"
375
375
  },
376
376
  "engines": {
@@ -378,15 +378,15 @@
378
378
  }
379
379
  },
380
380
  "node_modules/@aws-sdk/credential-provider-process": {
381
- "version": "3.972.41",
382
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.41.tgz",
383
- "integrity": "sha512-7I/n1zkysouLOWvkEhjNEP4vMnD2v4kzzr3/3QBdrripEpn7ap1/I5DF3Hou1SUqkKWo1f3oPGMyFAA1FAMvsQ==",
381
+ "version": "3.972.42",
382
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.42.tgz",
383
+ "integrity": "sha512-r996IYVtQ7rWa5UfSs3fZLT/Dq/SSgH9wv9zahx9lcg6vvaPPQHFpTy45nZajZu/+OUdEQxEjTn9rOMons59mA==",
384
384
  "license": "Apache-2.0",
385
385
  "dependencies": {
386
- "@aws-sdk/core": "^3.974.15",
387
- "@aws-sdk/types": "^3.973.9",
388
- "@smithy/core": "^3.24.5",
389
- "@smithy/types": "^4.14.2",
386
+ "@aws-sdk/core": "^3.974.16",
387
+ "@aws-sdk/types": "^3.973.10",
388
+ "@smithy/core": "^3.24.6",
389
+ "@smithy/types": "^4.14.3",
390
390
  "tslib": "^2.6.2"
391
391
  },
392
392
  "engines": {
@@ -394,17 +394,17 @@
394
394
  }
395
395
  },
396
396
  "node_modules/@aws-sdk/credential-provider-sso": {
397
- "version": "3.972.45",
398
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.45.tgz",
399
- "integrity": "sha512-oHgbz/eFD8IKiksqDsz9ZMU4A59BpQq4QwJedBnGD80ZqYcHPPHZBwjBnxLVkB7iRVVHWpDclR8yWdD2PkQIUA==",
397
+ "version": "3.972.46",
398
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.46.tgz",
399
+ "integrity": "sha512-vd+UqRbiYLvXXH3kTAuTxq+Vdb3rOgg29/FeB35ETsgdJTTB7x3YuqtpRckATsL5bDRXFVQo0uBj0/vxCJ8hHg==",
400
400
  "license": "Apache-2.0",
401
401
  "dependencies": {
402
- "@aws-sdk/core": "^3.974.15",
403
- "@aws-sdk/nested-clients": "^3.997.13",
404
- "@aws-sdk/token-providers": "3.1056.0",
405
- "@aws-sdk/types": "^3.973.9",
406
- "@smithy/core": "^3.24.5",
407
- "@smithy/types": "^4.14.2",
402
+ "@aws-sdk/core": "^3.974.16",
403
+ "@aws-sdk/nested-clients": "^3.997.14",
404
+ "@aws-sdk/token-providers": "3.1059.0",
405
+ "@aws-sdk/types": "^3.973.10",
406
+ "@smithy/core": "^3.24.6",
407
+ "@smithy/types": "^4.14.3",
408
408
  "tslib": "^2.6.2"
409
409
  },
410
410
  "engines": {
@@ -412,16 +412,16 @@
412
412
  }
413
413
  },
414
414
  "node_modules/@aws-sdk/credential-provider-web-identity": {
415
- "version": "3.972.45",
416
- "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.45.tgz",
417
- "integrity": "sha512-CDhzKdb2onv5bpnjn/acgdNmJOQthPDLsPizU7rZflsEcgMMp8Mlri+U5hdxf8ldvZJpvM3vLU6D56vfJm5AMQ==",
415
+ "version": "3.972.46",
416
+ "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.46.tgz",
417
+ "integrity": "sha512-xuxBbMorygYsbDV6E/tUGQUgIDfhnzxz8uJYX8rByzehI6gLUu8RPsgOpLmh9YWerqeHZxJbqzgheBSB7tpooQ==",
418
418
  "license": "Apache-2.0",
419
419
  "dependencies": {
420
- "@aws-sdk/core": "^3.974.15",
421
- "@aws-sdk/nested-clients": "^3.997.13",
422
- "@aws-sdk/types": "^3.973.9",
423
- "@smithy/core": "^3.24.5",
424
- "@smithy/types": "^4.14.2",
420
+ "@aws-sdk/core": "^3.974.16",
421
+ "@aws-sdk/nested-clients": "^3.997.14",
422
+ "@aws-sdk/types": "^3.973.10",
423
+ "@smithy/core": "^3.24.6",
424
+ "@smithy/types": "^4.14.3",
425
425
  "tslib": "^2.6.2"
426
426
  },
427
427
  "engines": {
@@ -429,20 +429,20 @@
429
429
  }
430
430
  },
431
431
  "node_modules/@aws-sdk/nested-clients": {
432
- "version": "3.997.13",
433
- "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.13.tgz",
434
- "integrity": "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg==",
432
+ "version": "3.997.14",
433
+ "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.14.tgz",
434
+ "integrity": "sha512-T5CS1r4P27FjkBYIWwVibWqEuq32BbCga2Z5m5OBSdSdi2wPfW2vl6zLWAB/5MeeyC4s2pY/MY3cj2Gd3rgSkg==",
435
435
  "license": "Apache-2.0",
436
436
  "dependencies": {
437
437
  "@aws-crypto/sha256-browser": "5.2.0",
438
438
  "@aws-crypto/sha256-js": "5.2.0",
439
- "@aws-sdk/core": "^3.974.15",
440
- "@aws-sdk/signature-v4-multi-region": "^3.996.30",
441
- "@aws-sdk/types": "^3.973.9",
442
- "@smithy/core": "^3.24.5",
443
- "@smithy/fetch-http-handler": "^5.4.5",
444
- "@smithy/node-http-handler": "^4.7.5",
445
- "@smithy/types": "^4.14.2",
439
+ "@aws-sdk/core": "^3.974.16",
440
+ "@aws-sdk/signature-v4-multi-region": "^3.996.31",
441
+ "@aws-sdk/types": "^3.973.10",
442
+ "@smithy/core": "^3.24.6",
443
+ "@smithy/fetch-http-handler": "^5.4.6",
444
+ "@smithy/node-http-handler": "^4.7.6",
445
+ "@smithy/types": "^4.14.3",
446
446
  "tslib": "^2.6.2"
447
447
  },
448
448
  "engines": {
@@ -450,14 +450,14 @@
450
450
  }
451
451
  },
452
452
  "node_modules/@aws-sdk/signature-v4-multi-region": {
453
- "version": "3.996.30",
454
- "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.30.tgz",
455
- "integrity": "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw==",
453
+ "version": "3.996.31",
454
+ "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.31.tgz",
455
+ "integrity": "sha512-Kn2up9SlG1KC6wRtwf0d7waTGF6rvp9DxYqB54x6UCKdQ6kyaXCqHL4WGb5vUJga5kS8FxnjhY0LqM28aMvnNQ==",
456
456
  "license": "Apache-2.0",
457
457
  "dependencies": {
458
- "@aws-sdk/types": "^3.973.9",
459
- "@smithy/signature-v4": "^5.4.5",
460
- "@smithy/types": "^4.14.2",
458
+ "@aws-sdk/types": "^3.973.10",
459
+ "@smithy/signature-v4": "^5.4.6",
460
+ "@smithy/types": "^4.14.3",
461
461
  "tslib": "^2.6.2"
462
462
  },
463
463
  "engines": {
@@ -465,16 +465,16 @@
465
465
  }
466
466
  },
467
467
  "node_modules/@aws-sdk/token-providers": {
468
- "version": "3.1056.0",
469
- "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1056.0.tgz",
470
- "integrity": "sha512-81duvlltQlsfn5K+o8zILcystBRdbT1G2JJYVCML5NZHBz4CL/zf+sAemCtBh/uh6RQUMyInGeZLQ7/8igZhbA==",
468
+ "version": "3.1059.0",
469
+ "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1059.0.tgz",
470
+ "integrity": "sha512-xql+7YBAE7WYb9xJfY0vcAXM8rJXfClmB2wkt+g/EoLUMog0pOb7o741fE96wFqha0uQTEgTo/5lGGguzavxmw==",
471
471
  "license": "Apache-2.0",
472
472
  "dependencies": {
473
- "@aws-sdk/core": "^3.974.15",
474
- "@aws-sdk/nested-clients": "^3.997.13",
475
- "@aws-sdk/types": "^3.973.9",
476
- "@smithy/core": "^3.24.5",
477
- "@smithy/types": "^4.14.2",
473
+ "@aws-sdk/core": "^3.974.16",
474
+ "@aws-sdk/nested-clients": "^3.997.14",
475
+ "@aws-sdk/types": "^3.973.10",
476
+ "@smithy/core": "^3.24.6",
477
+ "@smithy/types": "^4.14.3",
478
478
  "tslib": "^2.6.2"
479
479
  },
480
480
  "engines": {
@@ -482,12 +482,12 @@
482
482
  }
483
483
  },
484
484
  "node_modules/@aws-sdk/types": {
485
- "version": "3.973.9",
486
- "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.9.tgz",
487
- "integrity": "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg==",
485
+ "version": "3.973.10",
486
+ "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.10.tgz",
487
+ "integrity": "sha512-992QrTO7G9qCvKD0fx1rMlqcL14plUcRAbwmqqYVsuF3GrqcvlAL9qxR+baMafarEZ+l7DUQ5lCMmt5mbMhF7g==",
488
488
  "license": "Apache-2.0",
489
489
  "dependencies": {
490
- "@smithy/types": "^4.14.2",
490
+ "@smithy/types": "^4.14.3",
491
491
  "tslib": "^2.6.2"
492
492
  },
493
493
  "engines": {
@@ -507,12 +507,12 @@
507
507
  }
508
508
  },
509
509
  "node_modules/@aws-sdk/xml-builder": {
510
- "version": "3.972.26",
511
- "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.26.tgz",
512
- "integrity": "sha512-cDbrqvDS73whl6YAPSPq0U6whzG6UWI9PuWh0wrUuGoZexhWEqhdunbukV7iBoaWnFV1AODutM5hOD6rtn439g==",
510
+ "version": "3.972.27",
511
+ "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.27.tgz",
512
+ "integrity": "sha512-hpsCXCOI436kxWpjtRuIHVvuPP81MOw8f18jzfZeg+UOiiOvlqWcmWChzEhJEu16cOC6+ku4ncBN+7rdt+DZ9g==",
513
513
  "license": "Apache-2.0",
514
514
  "dependencies": {
515
- "@smithy/types": "^4.14.2",
515
+ "@smithy/types": "^4.14.3",
516
516
  "fast-xml-parser": "5.7.3",
517
517
  "tslib": "^2.6.2"
518
518
  },
@@ -4123,9 +4123,9 @@
4123
4123
  }
4124
4124
  },
4125
4125
  "node_modules/electron-to-chromium": {
4126
- "version": "1.5.364",
4127
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.364.tgz",
4128
- "integrity": "sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==",
4126
+ "version": "1.5.366",
4127
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.366.tgz",
4128
+ "integrity": "sha512-OlRuhb688YTCzzU3gXPLn6nGyd+F+53INE1qaKKlu6kETErE8FYsyDh0XqXEU+uBRn0MpCzz2vfNwORhkap8qg==",
4129
4129
  "dev": true,
4130
4130
  "license": "ISC"
4131
4131
  },
@@ -6860,9 +6860,9 @@
6860
6860
  "license": "MIT"
6861
6861
  },
6862
6862
  "node_modules/node-releases": {
6863
- "version": "2.0.46",
6864
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz",
6865
- "integrity": "sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==",
6863
+ "version": "2.0.47",
6864
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
6865
+ "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==",
6866
6866
  "dev": true,
6867
6867
  "license": "MIT",
6868
6868
  "engines": {
@@ -0,0 +1,81 @@
1
+ /**
2
+ * One-shot: prove sentinel-limits.json round-trip changes Sentinel.getConfig()
3
+ * and policy thresholds (simulates Gateway restart without HTTP).
4
+ *
5
+ * Usage: node scripts/verify-sentinel-persist.mjs
6
+ */
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as os from 'os';
10
+
11
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), '4runr-persist-verify-'));
12
+ process.env['APPDATA'] = tmp;
13
+
14
+ const { Sentinel, policyManager } = await import('@4runr/sentinel');
15
+ const {
16
+ saveSentinelLimitsToDisk,
17
+ loadSentinelLimitsFromDisk,
18
+ applyPersistedSentinelConfig,
19
+ getSentinelLimitsFilePath,
20
+ } = await import('../dist/security/sentinel-config-store.js');
21
+
22
+ const distinctive = {
23
+ enabled: true,
24
+ runMaxDurationMs: 111_000,
25
+ runMaxTokens: 22_000,
26
+ runIdleMs: 12_345,
27
+ loopWindow: 9,
28
+ loopMax: 4,
29
+ runMaxCost: 2.75,
30
+ };
31
+
32
+ saveSentinelLimitsToDisk(distinctive);
33
+ const filePath = getSentinelLimitsFilePath();
34
+ if (!fs.existsSync(filePath)) {
35
+ console.error('FAIL: file not written', filePath);
36
+ process.exit(1);
37
+ }
38
+ const onDisk = JSON.parse(fs.readFileSync(filePath, 'utf8'));
39
+ console.log('OK: wrote', filePath);
40
+ console.log(' config.runIdleMs on disk:', onDisk.config?.runIdleMs);
41
+
42
+ const loaded = loadSentinelLimitsFromDisk();
43
+ if (loaded?.runIdleMs !== distinctive.runIdleMs) {
44
+ console.error('FAIL: load from disk', loaded);
45
+ process.exit(1);
46
+ }
47
+ console.log('OK: loadSentinelLimitsFromDisk matches');
48
+
49
+ Sentinel.resetInstance();
50
+ const sentinel = Sentinel.getInstance();
51
+ const before = sentinel.getConfig().runIdleMs;
52
+ applyPersistedSentinelConfig(sentinel);
53
+ const after = sentinel.getConfig().runIdleMs;
54
+ if (after !== distinctive.runIdleMs) {
55
+ console.error('FAIL: after boot apply', { before, after, expected: distinctive.runIdleMs });
56
+ process.exit(1);
57
+ }
58
+ console.log('OK: fresh singleton after applyPersistedSentinelConfig', { before, after });
59
+
60
+ const violations = policyManager.evaluatePolicies(
61
+ 'verify-run',
62
+ {
63
+ runId: 'verify-run',
64
+ createdAt: Date.now(),
65
+ startedAt: Date.now() - 30_000,
66
+ lastTokenAt: Date.now() - 30_000,
67
+ tokenCount: 0,
68
+ status: 'running',
69
+ events: [],
70
+ },
71
+ sentinel.getConfig()
72
+ );
73
+ const idle = violations.find((v) => v.policy === 'idle');
74
+ if (!idle || idle.threshold !== distinctive.runIdleMs) {
75
+ console.error('FAIL: policy threshold', idle);
76
+ process.exit(1);
77
+ }
78
+ console.log('OK: idle policy threshold = persisted runIdleMs', idle.threshold);
79
+
80
+ fs.rmSync(tmp, { recursive: true, force: true });
81
+ console.log('\nAll persistence checks passed (fundamental, not UI-only).');
@@ -0,0 +1,93 @@
1
+ /**
2
+ * HTTP apply → disk → simulated restart (proves route wiring, not UI-only).
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
6
+ import Fastify from 'fastify';
7
+ import * as fs from 'fs';
8
+ import * as path from 'path';
9
+ import * as os from 'os';
10
+ import { Sentinel } from '@4runr/sentinel';
11
+ import {
12
+ getSentinelLimitsFilePath,
13
+ applyPersistedSentinelConfig,
14
+ hydrateSentinelConfigFromDisk,
15
+ } from '../security/sentinel-config-store.js';
16
+
17
+ jest.mock('../middleware/rateLimit.js', () => ({
18
+ writeRateLimit: async () => {},
19
+ readRateLimit: async () => {},
20
+ }));
21
+
22
+ jest.mock('../middleware/auth.js', () => ({
23
+ requireAuth: async () => {},
24
+ }));
25
+
26
+ describe('POST /api/sentinel/policies/custom persistence', () => {
27
+ const originalAppData = process.env['APPDATA'];
28
+ let tmpDir: string;
29
+ let app: ReturnType<typeof Fastify>;
30
+
31
+ beforeEach(async () => {
32
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '4runr-sentinel-http-'));
33
+ process.env['APPDATA'] = tmpDir;
34
+ delete process.env['RUN_IDLE_MS'];
35
+ delete process.env['SENTINEL_IDLE_MS'];
36
+ Sentinel.resetInstance();
37
+
38
+ const { sentinelPolicyRoutes } = await import('../routes/sentinel-policies.js');
39
+ app = Fastify();
40
+ await sentinelPolicyRoutes(app);
41
+ await app.ready();
42
+ });
43
+
44
+ afterEach(async () => {
45
+ await app.close();
46
+ Sentinel.resetInstance();
47
+ if (originalAppData === undefined) delete process.env['APPDATA'];
48
+ else process.env['APPDATA'] = originalAppData;
49
+ fs.rmSync(tmpDir, { recursive: true, force: true });
50
+ });
51
+
52
+ it('writes sentinel-limits.json and reloads after simulated Gateway restart', async () => {
53
+ const distinctiveIdle = 77_777;
54
+
55
+ const res = await app.inject({
56
+ method: 'POST',
57
+ url: '/api/sentinel/policies/custom',
58
+ payload: {
59
+ enabled: true,
60
+ runMaxDurationMs: 120_000,
61
+ runMaxTokens: 16_000,
62
+ runIdleMs: distinctiveIdle,
63
+ loopWindow: 15,
64
+ loopMax: 5,
65
+ runMaxCost: 1.5,
66
+ },
67
+ });
68
+
69
+ expect(res.statusCode).toBe(200);
70
+ const body = res.json() as { success?: boolean; config?: { runIdleMs?: number } };
71
+ expect(body.success).toBe(true);
72
+ expect(body.config?.runIdleMs).toBe(distinctiveIdle);
73
+
74
+ const filePath = getSentinelLimitsFilePath();
75
+ expect(fs.existsSync(filePath)).toBe(true);
76
+ const onDisk = JSON.parse(fs.readFileSync(filePath, 'utf8')) as {
77
+ config: { runIdleMs: number };
78
+ };
79
+ expect(onDisk.config.runIdleMs).toBe(distinctiveIdle);
80
+
81
+ Sentinel.resetInstance();
82
+ const sentinel = Sentinel.getInstance();
83
+ expect(applyPersistedSentinelConfig(sentinel)).toBe(true);
84
+ expect(sentinel.getConfig().runIdleMs).toBe(distinctiveIdle);
85
+
86
+ Sentinel.resetInstance();
87
+ const stale = Sentinel.getInstance();
88
+ expect(stale.getConfig().runIdleMs).not.toBe(distinctiveIdle);
89
+
90
+ hydrateSentinelConfigFromDisk(stale);
91
+ expect(stale.getConfig().runIdleMs).toBe(distinctiveIdle);
92
+ });
93
+ });
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Sentinel limits disk persistence
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
6
+ import * as fs from 'fs';
7
+ import * as path from 'path';
8
+ import * as os from 'os';
9
+ import {
10
+ getSentinelLimitsFilePath,
11
+ loadSentinelLimitsFromDisk,
12
+ saveSentinelLimitsToDisk,
13
+ applyPersistedSentinelConfig,
14
+ } from '../security/sentinel-config-store.js';
15
+ import { Sentinel, policyManager } from '@4runr/sentinel';
16
+ import type { RunState } from '@4runr/sentinel';
17
+
18
+ describe('sentinel-config-store', () => {
19
+ const originalAppData = process.env['APPDATA'];
20
+ let tmpDir: string;
21
+
22
+ beforeEach(() => {
23
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '4runr-sentinel-'));
24
+ process.env['APPDATA'] = tmpDir;
25
+ Sentinel.resetInstance();
26
+ });
27
+
28
+ afterEach(() => {
29
+ Sentinel.resetInstance();
30
+ if (originalAppData === undefined) {
31
+ delete process.env['APPDATA'];
32
+ } else {
33
+ process.env['APPDATA'] = originalAppData;
34
+ }
35
+ fs.rmSync(tmpDir, { recursive: true, force: true });
36
+ });
37
+
38
+ it('round-trips config to disk', () => {
39
+ const config = {
40
+ enabled: true,
41
+ runMaxDurationMs: 90_000,
42
+ runMaxTokens: 8_000,
43
+ runIdleMs: 45_000,
44
+ loopWindow: 10,
45
+ loopMax: 3,
46
+ runMaxCost: 1.25,
47
+ };
48
+ saveSentinelLimitsToDisk(config);
49
+ expect(fs.existsSync(getSentinelLimitsFilePath())).toBe(true);
50
+ const loaded = loadSentinelLimitsFromDisk();
51
+ expect(loaded).toMatchObject(config);
52
+ });
53
+
54
+ it('applyPersistedSentinelConfig merges when env field unset', () => {
55
+ delete process.env['RUN_IDLE_MS'];
56
+ delete process.env['SENTINEL_IDLE_MS'];
57
+ saveSentinelLimitsToDisk({
58
+ enabled: true,
59
+ runMaxDurationMs: 60_000,
60
+ runMaxTokens: 16_000,
61
+ runIdleMs: 99_000,
62
+ loopWindow: 15,
63
+ loopMax: 5,
64
+ runMaxCost: 2.0,
65
+ });
66
+ const sentinel = Sentinel.getInstance();
67
+ applyPersistedSentinelConfig(sentinel);
68
+ expect(sentinel.getConfig().runIdleMs).toBe(99_000);
69
+ });
70
+
71
+ it('simulates Gateway restart: disk limits reload into a fresh Sentinel singleton', () => {
72
+ delete process.env['RUN_IDLE_MS'];
73
+ delete process.env['SENTINEL_IDLE_MS'];
74
+
75
+ const distinctiveIdle = 12_345;
76
+ saveSentinelLimitsToDisk({
77
+ enabled: true,
78
+ runMaxDurationMs: 600_000,
79
+ runMaxTokens: 50_000,
80
+ runIdleMs: distinctiveIdle,
81
+ loopWindow: 15,
82
+ loopMax: 5,
83
+ runMaxCost: 3.5,
84
+ });
85
+
86
+ Sentinel.resetInstance();
87
+ const sentinel = Sentinel.getInstance();
88
+ expect(sentinel.getConfig().runIdleMs).not.toBe(distinctiveIdle);
89
+
90
+ expect(applyPersistedSentinelConfig(sentinel)).toBe(true);
91
+ expect(sentinel.getConfig().runIdleMs).toBe(distinctiveIdle);
92
+ expect(sentinel.getConfig().runMaxCost).toBe(3.5);
93
+ });
94
+
95
+ it('persisted limits drive real policy evaluation (not display-only)', () => {
96
+ delete process.env['RUN_IDLE_MS'];
97
+ delete process.env['SENTINEL_IDLE_MS'];
98
+
99
+ const idleMs = 8_000;
100
+ saveSentinelLimitsToDisk({
101
+ enabled: true,
102
+ runMaxDurationMs: 600_000,
103
+ runMaxTokens: 50_000,
104
+ runIdleMs: idleMs,
105
+ loopWindow: 15,
106
+ loopMax: 5,
107
+ runMaxCost: 1.0,
108
+ });
109
+
110
+ Sentinel.resetInstance();
111
+ const sentinel = Sentinel.getInstance();
112
+ applyPersistedSentinelConfig(sentinel);
113
+
114
+ const runState: RunState = {
115
+ runId: 'run-persist-verify',
116
+ createdAt: Date.now(),
117
+ startedAt: Date.now() - 20_000,
118
+ lastTokenAt: Date.now() - 20_000,
119
+ tokenCount: 0,
120
+ status: 'running',
121
+ events: [],
122
+ };
123
+
124
+ const violations = policyManager.evaluatePolicies(
125
+ 'run-persist-verify',
126
+ runState,
127
+ sentinel.getConfig()
128
+ );
129
+ const idleViolation = violations.find((v) => v.policy === 'idle');
130
+ expect(idleViolation).toBeDefined();
131
+ expect(idleViolation!.threshold).toBe(idleMs);
132
+ });
133
+ });
@@ -81,6 +81,7 @@ import { ddosProtection } from './middleware/ddos-protection.js';
81
81
  import { wrapLoggerForMonitoring } from './middleware/log-capture.js';
82
82
  import { initializeDatabase, shutdownDatabase } from './db/init.js';
83
83
  import type { PrismaClient } from '@prisma/client';
84
+ import { applyPersistedSentinelConfig } from './security/sentinel-config-store.js';
84
85
 
85
86
  // Initialize no-persistence mode BEFORE any other initialization (only in memory mode)
86
87
  // This must happen before any filesystem writes or database connections
@@ -364,6 +365,7 @@ const sseConnections = new Map<string, Set<any>>();
364
365
  // Sentinel integration - event streams for each run
365
366
  const sentinelEventStreams = new Map<string, GatewayEventStream>();
366
367
  const sentinel = Sentinel.getInstance();
368
+ applyPersistedSentinelConfig(sentinel, baseLogger);
367
369
 
368
370
  // Shield integration - AI Safety Layer (mode from SENTINEL_SHIELD_MODE — see shield-config.ts)
369
371
  const shield = Shield.getInstance();
@@ -12,6 +12,11 @@ import {
12
12
  } from '@4runr/sentinel';
13
13
  import { requireAuth } from '../middleware/auth.js';
14
14
  import { readRateLimit, writeRateLimit } from '../middleware/rateLimit.js';
15
+ import {
16
+ persistSentinelConfigAfterApply,
17
+ hydrateSentinelConfigFromDisk,
18
+ getSentinelLimitsFilePath,
19
+ } from '../security/sentinel-config-store.js';
15
20
 
16
21
  /**
17
22
  * Register Sentinel policy management routes
@@ -170,11 +175,12 @@ export async function sentinelPolicyRoutes(fastify: FastifyInstance) {
170
175
 
171
176
  try {
172
177
  sentinel.updateConfig(config);
178
+ const savedTo = persistSentinelConfigAfterApply(sentinel, config);
173
179
  return {
174
180
  success: true,
175
181
  template: templateName,
176
182
  config: config,
177
- message: `Template '${templateName}' applied successfully. New limits are now active.`
183
+ message: `Template '${templateName}' applied. Limits active and saved to ${savedTo}.`,
178
184
  };
179
185
  } catch (error: any) {
180
186
  return reply.status(400).send({
@@ -229,10 +235,11 @@ export async function sentinelPolicyRoutes(fastify: FastifyInstance) {
229
235
 
230
236
  try {
231
237
  sentinel.updateConfig(config);
238
+ const savedTo = persistSentinelConfigAfterApply(sentinel, config);
232
239
  return {
233
240
  success: true,
234
241
  config,
235
- message: 'Custom Sentinel limits applied. Active until Gateway restart unless set via env.',
242
+ message: `Custom limits saved (${savedTo}). Reopen Sentinel or restart Gateway to reload; env vars override fields when set.`,
236
243
  };
237
244
  } catch (error: unknown) {
238
245
  const msg = error instanceof Error ? error.message : String(error);
@@ -267,10 +274,12 @@ export async function sentinelPolicyRoutes(fastify: FastifyInstance) {
267
274
  try {
268
275
  const { Sentinel } = await import('@4runr/sentinel');
269
276
  const sentinel = Sentinel.getInstance();
270
-
277
+ hydrateSentinelConfigFromDisk(sentinel);
278
+
271
279
  return {
272
280
  success: true,
273
- config: sentinel.getConfig()
281
+ config: sentinel.getConfig(),
282
+ persistedPath: getSentinelLimitsFilePath(),
274
283
  };
275
284
  } catch (error: any) {
276
285
  return reply.status(500).send({
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Persist operator Sentinel limits to disk (survives Gateway restart).
3
+ * File: {4Runr data dir}/config/sentinel-limits.json
4
+ *
5
+ * Boot order: env defaults (packages/sentinel) → overlay saved file when present.
6
+ * Explicit env vars for a field are not overwritten by the saved file.
7
+ */
8
+
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ import { get4RunrConfigDir } from '@4runr/shared';
12
+ import { Sentinel, type SentinelConfig } from '@4runr/sentinel';
13
+
14
+ const FILE_NAME = 'sentinel-limits.json';
15
+
16
+ const ENV_FIELD_KEYS: Array<{
17
+ field: keyof SentinelConfig;
18
+ envKeys: string[];
19
+ }> = [
20
+ { field: 'enabled', envKeys: ['SENTINEL_ENABLED'] },
21
+ { field: 'runMaxDurationMs', envKeys: ['RUN_MAX_DURATION_MS'] },
22
+ { field: 'runMaxTokens', envKeys: ['RUN_MAX_TOKENS'] },
23
+ { field: 'runIdleMs', envKeys: ['SENTINEL_IDLE_MS', 'RUN_IDLE_MS'] },
24
+ { field: 'loopWindow', envKeys: ['SENTINEL_LOOP_WINDOW'] },
25
+ { field: 'loopMax', envKeys: ['SENTINEL_LOOP_MAX'] },
26
+ { field: 'runMaxCost', envKeys: ['RUN_MAX_COST', 'SENTINEL_MAX_COST'] },
27
+ ];
28
+
29
+ export function getSentinelLimitsFilePath(): string {
30
+ return path.join(get4RunrConfigDir(), FILE_NAME);
31
+ }
32
+
33
+ function envDefinesField(field: keyof SentinelConfig): boolean {
34
+ const entry = ENV_FIELD_KEYS.find((e) => e.field === field);
35
+ if (!entry) return false;
36
+ return entry.envKeys.some((k) => {
37
+ const v = process.env[k];
38
+ return v !== undefined && v.trim() !== '';
39
+ });
40
+ }
41
+
42
+ function parseSavedPayload(raw: unknown): SentinelConfig | null {
43
+ if (!raw || typeof raw !== 'object') return null;
44
+ const o = raw as Record<string, unknown>;
45
+ const num = (key: string): number | undefined => {
46
+ const v = o[key];
47
+ if (typeof v === 'number' && Number.isFinite(v)) return v;
48
+ return undefined;
49
+ };
50
+ const enabled = o['enabled'];
51
+ if (typeof enabled !== 'boolean') return null;
52
+ const runMaxDurationMs = num('runMaxDurationMs');
53
+ const runMaxTokens = num('runMaxTokens');
54
+ const runIdleMs = num('runIdleMs');
55
+ const loopWindow = num('loopWindow');
56
+ const loopMax = num('loopMax');
57
+ if (
58
+ runMaxDurationMs === undefined ||
59
+ runMaxTokens === undefined ||
60
+ runIdleMs === undefined ||
61
+ loopWindow === undefined ||
62
+ loopMax === undefined
63
+ ) {
64
+ return null;
65
+ }
66
+ const runMaxCost = num('runMaxCost');
67
+ return {
68
+ enabled,
69
+ runMaxDurationMs,
70
+ runMaxTokens,
71
+ runIdleMs,
72
+ loopWindow,
73
+ loopMax,
74
+ ...(runMaxCost !== undefined ? { runMaxCost } : {}),
75
+ };
76
+ }
77
+
78
+ export function loadSentinelLimitsFromDisk(): SentinelConfig | null {
79
+ const filePath = getSentinelLimitsFilePath();
80
+ try {
81
+ if (!fs.existsSync(filePath)) return null;
82
+ const text = fs.readFileSync(filePath, 'utf8');
83
+ const parsed = JSON.parse(text) as unknown;
84
+ const inner =
85
+ parsed && typeof parsed === 'object' && 'config' in (parsed as object)
86
+ ? (parsed as { config: unknown }).config
87
+ : parsed;
88
+ return parseSavedPayload(inner);
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ export function saveSentinelLimitsToDisk(config: SentinelConfig): void {
95
+ const filePath = getSentinelLimitsFilePath();
96
+ const payload = {
97
+ version: 1,
98
+ updatedAt: new Date().toISOString(),
99
+ config: {
100
+ enabled: config.enabled,
101
+ runMaxDurationMs: config.runMaxDurationMs,
102
+ runMaxTokens: config.runMaxTokens,
103
+ runIdleMs: config.runIdleMs,
104
+ loopWindow: config.loopWindow,
105
+ loopMax: config.loopMax,
106
+ runMaxCost: config.runMaxCost ?? 1.0,
107
+ },
108
+ };
109
+ fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), { mode: 0o600 });
110
+ }
111
+
112
+ /** Merge saved limits from disk into the live Sentinel singleton (env wins per field). */
113
+ export function mergePersistedSentinelConfig(sentinel: Sentinel): boolean {
114
+ const saved = loadSentinelLimitsFromDisk();
115
+ if (!saved) return false;
116
+
117
+ const current = sentinel.getConfig();
118
+ const merged: SentinelConfig = { ...current };
119
+
120
+ for (const { field } of ENV_FIELD_KEYS) {
121
+ if (envDefinesField(field)) continue;
122
+ (merged as Record<string, unknown>)[field] = saved[field];
123
+ }
124
+
125
+ try {
126
+ sentinel.updateConfig(merged);
127
+ return true;
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Apply saved limits on Gateway boot. Env-defined fields keep env values.
135
+ */
136
+ export function applyPersistedSentinelConfig(sentinel: Sentinel, logger?: {
137
+ info: (msg: string, meta?: Record<string, unknown>) => void;
138
+ warn: (msg: string, meta?: Record<string, unknown>) => void;
139
+ }): boolean {
140
+ const ok = mergePersistedSentinelConfig(sentinel);
141
+ if (ok) {
142
+ logger?.info('Sentinel limits loaded from disk', {
143
+ path: getSentinelLimitsFilePath(),
144
+ });
145
+ }
146
+ return ok;
147
+ }
148
+
149
+ /** Call before returning policy config to clients so long-lived Gateway picks up disk saves. */
150
+ export function hydrateSentinelConfigFromDisk(sentinel: Sentinel): void {
151
+ mergePersistedSentinelConfig(sentinel);
152
+ }
153
+
154
+ export function persistSentinelConfigAfterApply(
155
+ sentinel: Sentinel,
156
+ config: SentinelConfig
157
+ ): string {
158
+ saveSentinelLimitsToDisk(config);
159
+ return getSentinelLimitsFilePath();
160
+ }
@@ -828,13 +828,30 @@ impl App {
828
828
  }
829
829
  }
830
830
 
831
+ pub fn open_sentinel_config(&mut self, ws: Option<&WebSocketClient>) {
832
+ self.state.pending_sentinel_load_id = None;
833
+ self.state.pending_sentinel_apply_id = None;
834
+ self.push_overlay(Screen::SentinelConfig);
835
+ self.state
836
+ .logs
837
+ .push_back("[NAV] Opening Sentinel Configuration...".into());
838
+ if self.state.operation_mode == OperationMode::Connected {
839
+ if let Some(ws) = ws {
840
+ self.begin_sentinel_load_request(ws);
841
+ }
842
+ } else {
843
+ self.state.sentinel_config.error = Some(
844
+ "Connect to Gateway first (connect portal).".to_string(),
845
+ );
846
+ }
847
+ self.request_immediate_render("open_sentinel_config");
848
+ }
849
+
831
850
  pub fn begin_sentinel_load_request(&mut self, ws: &WebSocketClient) {
832
851
  if self.state.operation_mode != OperationMode::Connected {
833
852
  return;
834
853
  }
835
- if self.state.pending_sentinel_load_id.is_some()
836
- || self.state.pending_sentinel_apply_id.is_some()
837
- {
854
+ if self.state.pending_sentinel_load_id.is_some() {
838
855
  return;
839
856
  }
840
857
  self.state.sentinel_config.loading = true;
@@ -1551,19 +1568,7 @@ impl App {
1551
1568
  }
1552
1569
  }
1553
1570
  "sentinel" | "sentinel config" | "sentinel policies" => {
1554
- self.push_overlay(Screen::SentinelConfig);
1555
- self.state
1556
- .logs
1557
- .push_back("[NAV] Opening Sentinel Configuration...".into());
1558
- if self.state.operation_mode == OperationMode::Connected {
1559
- if let Some(ws) = ws_client {
1560
- self.begin_sentinel_load_request(ws);
1561
- }
1562
- } else {
1563
- self.state.sentinel_config.error = Some(
1564
- "Connect to Gateway first (connect portal).".to_string(),
1565
- );
1566
- }
1571
+ self.open_sentinel_config(ws_client);
1567
1572
  }
1568
1573
  "config" | "settings" => {
1569
1574
  self.push_overlay(Screen::Settings);
@@ -2514,7 +2519,7 @@ impl App {
2514
2519
 
2515
2520
  match key.code {
2516
2521
  KeyCode::Tab => {
2517
- self.state.sentinel_config.toggle_view_mode();
2522
+ self.state.sentinel_config.cycle_view_mode();
2518
2523
  self.request_immediate_render("sentinel_tab");
2519
2524
  }
2520
2525
  KeyCode::Up => {
@@ -2567,6 +2572,10 @@ impl App {
2567
2572
  Some("Connect to Gateway first.".to_string());
2568
2573
  } else if let Some(ws) = ws_client {
2569
2574
  match self.state.sentinel_config.view_mode {
2575
+ SentinelViewMode::About => {
2576
+ self.begin_sentinel_load_request(ws);
2577
+ self.request_render("sentinel_about_refresh");
2578
+ }
2570
2579
  SentinelViewMode::Templates => {
2571
2580
  if let Some(t) = self.state.sentinel_config.selected_template() {
2572
2581
  let key = t.key.clone();
@@ -14,10 +14,29 @@ const BG_PANEL: Color = Color::Rgb(18, 18, 25);
14
14
 
15
15
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
16
16
  pub enum SentinelViewMode {
17
+ About,
17
18
  Templates,
18
19
  Custom,
19
20
  }
20
21
 
22
+ const ABOUT_SENTINEL_LINES: &[&str] = &[
23
+ "Sentinel is 4Runr's runtime safety layer for agent executions on this Gateway.",
24
+ "",
25
+ "What it does:",
26
+ " • Watches each run while it is running (queue processor path)",
27
+ " • Enforces limits: idle time, max duration, token cap, cost, loop detection",
28
+ " • On violation: kills the run (status killed, policy_violation:* in logs)",
29
+ "",
30
+ "Scope today:",
31
+ " • One policy for ALL runs on this Gateway (not per-agent yet)",
32
+ " • A run = one agent execution (POST /api/runs → start → agent runs)",
33
+ "",
34
+ "How to use this screen:",
35
+ " • Templates: apply a preset · Custom: tune fields · Enter: save to disk",
36
+ " • Saved file survives Gateway restart (see path in Custom help)",
37
+ " • Env vars in docker-compose override individual fields when set",
38
+ ];
39
+
21
40
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
22
41
  pub enum CustomField {
23
42
  Enabled,
@@ -165,13 +184,14 @@ impl SentinelConfigState {
165
184
  self.templates.get(self.selected_index)
166
185
  }
167
186
 
168
- pub fn toggle_view_mode(&mut self) {
187
+ pub fn cycle_view_mode(&mut self) {
169
188
  self.view_mode = match self.view_mode {
189
+ SentinelViewMode::About => SentinelViewMode::Templates,
170
190
  SentinelViewMode::Templates => {
171
191
  self.sync_custom_draft_from_current();
172
192
  SentinelViewMode::Custom
173
193
  }
174
- SentinelViewMode::Custom => SentinelViewMode::Templates,
194
+ SentinelViewMode::Custom => SentinelViewMode::About,
175
195
  };
176
196
  self.error = None;
177
197
  self.status_message = None;
@@ -191,6 +211,7 @@ impl SentinelConfigState {
191
211
 
192
212
  pub fn select_previous(&mut self) {
193
213
  match self.view_mode {
214
+ SentinelViewMode::About => {}
194
215
  SentinelViewMode::Templates => {
195
216
  if self.selected_index > 0 {
196
217
  self.selected_index -= 1;
@@ -210,6 +231,7 @@ impl SentinelConfigState {
210
231
 
211
232
  pub fn select_next(&mut self) {
212
233
  match self.view_mode {
234
+ SentinelViewMode::About => {}
213
235
  SentinelViewMode::Templates => {
214
236
  if self.selected_index + 1 < self.templates.len() {
215
237
  self.selected_index += 1;
@@ -489,6 +511,7 @@ pub fn render(f: &mut Frame, state: &AppState) {
489
511
  .split(area);
490
512
 
491
513
  let mode_label = match sc.view_mode {
514
+ SentinelViewMode::About => "About",
492
515
  SentinelViewMode::Templates => "Templates",
493
516
  SentinelViewMode::Custom => "Custom",
494
517
  };
@@ -514,6 +537,9 @@ pub fn render(f: &mut Frame, state: &AppState) {
514
537
  .split(chunks[1]);
515
538
 
516
539
  match sc.view_mode {
540
+ SentinelViewMode::About => {
541
+ render_about_panel(f, chunks[1], sc);
542
+ }
517
543
  SentinelViewMode::Templates => {
518
544
  render_active_panel(f, body[0], sc);
519
545
  render_templates_panel(f, body[1], sc);
@@ -525,11 +551,14 @@ pub fn render(f: &mut Frame, state: &AppState) {
525
551
  }
526
552
 
527
553
  let default_footer = match sc.view_mode {
554
+ SentinelViewMode::About => {
555
+ "Tab: Templates · R: reload from Gateway · ESC: Close"
556
+ }
528
557
  SentinelViewMode::Templates => {
529
- "Tab Custom · ↑/↓ Template · Enter Apply · R Refresh · ESC Close"
558
+ "Tab: next view · ↑/↓ Template · Enter Apply · R Refresh · ESC Close"
530
559
  }
531
560
  SentinelViewMode::Custom => {
532
- "Tab Templates · ↑/↓ Field · ←/→ or +/- Value · Space ON/OFF · Enter Apply · ESC Close"
561
+ "Tab: next view · ↑/↓ Field · ←/→ Value · Enter Apply (saves to disk) · ESC Close"
533
562
  }
534
563
  };
535
564
  const CUSTOM_SHORTCUTS: &str =
@@ -573,6 +602,48 @@ pub fn render(f: &mut Frame, state: &AppState) {
573
602
  );
574
603
  }
575
604
 
605
+ fn render_about_panel(f: &mut Frame, area: Rect, sc: &SentinelConfigState) {
606
+ let block = Block::default()
607
+ .title(" 📖 What is Sentinel? ")
608
+ .borders(Borders::ALL)
609
+ .border_style(Style::default().fg(CYBER_CYAN))
610
+ .style(Style::default().bg(BG_PANEL));
611
+ let inner = block.inner(area);
612
+ f.render_widget(block, area);
613
+
614
+ let mut lines: Vec<Line> = Vec::new();
615
+ for line in ABOUT_SENTINEL_LINES {
616
+ if line.is_empty() {
617
+ lines.push(Line::from(""));
618
+ continue;
619
+ }
620
+ let style = if line.starts_with(" •") {
621
+ Style::default().fg(TEXT_PRIMARY)
622
+ } else if line.ends_with(':') && !line.starts_with(' ') {
623
+ Style::default().fg(NEON_GREEN).bold()
624
+ } else {
625
+ Style::default().fg(TEXT_DIM)
626
+ };
627
+ lines.push(Line::from(Span::styled(*line, style)));
628
+ }
629
+ lines.push(Line::from(""));
630
+ lines.push(Line::from(vec![
631
+ Span::styled("Active limits now: ", Style::default().fg(TEXT_DIM)),
632
+ Span::styled(
633
+ if sc.current_enabled { "ON" } else { "OFF" },
634
+ Style::default().fg(NEON_GREEN),
635
+ ),
636
+ Span::styled(" · idle ", Style::default().fg(TEXT_DIM)),
637
+ Span::styled(fmt_ms(sc.current_idle_ms), Style::default().fg(TEXT_PRIMARY)),
638
+ Span::styled(" · max ", Style::default().fg(TEXT_DIM)),
639
+ Span::styled(fmt_ms(sc.current_duration_ms), Style::default().fg(TEXT_PRIMARY)),
640
+ Span::styled(" · cost ", Style::default().fg(TEXT_DIM)),
641
+ Span::styled(fmt_cost(sc.current_max_cost), Style::default().fg(AMBER_WARN)),
642
+ ]));
643
+
644
+ f.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
645
+ }
646
+
576
647
  fn render_active_panel(f: &mut Frame, area: Rect, sc: &SentinelConfigState) {
577
648
  let block = Block::default()
578
649
  .title(" ⚙️ Active limits (live) ")
@@ -627,7 +698,7 @@ fn render_active_panel(f: &mut Frame, area: Rect, sc: &SentinelConfigState) {
627
698
  ),
628
699
  ]),
629
700
  Line::from(""),
630
- Line::from("Press Tab → Custom to tune your own limits.").style(Style::default().fg(TEXT_MUTED)),
701
+ Line::from("Tab → About / Custom / Templates to switch views.").style(Style::default().fg(TEXT_MUTED)),
631
702
  ];
632
703
  f.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
633
704
  }
@@ -741,15 +812,15 @@ fn render_custom_help(f: &mut Frame, area: Rect, sc: &SentinelConfigState) {
741
812
  lines.push(Line::from(field.env_var()).style(Style::default().fg(CYBER_CYAN)));
742
813
  lines.push(Line::from(""));
743
814
  lines.push(
744
- Line::from("UI Apply = live in memory until Gateway restarts.")
745
- .style(Style::default().fg(TEXT_MUTED)),
815
+ Line::from("Enter Apply Gateway + disk (%APPDATA%\\4runr\\config\\sentinel-limits.json).")
816
+ .style(Style::default().fg(NEON_GREEN)),
746
817
  );
747
818
  lines.push(
748
- Line::from("D in 4r disconnect respawns Gateway env defaults return.")
819
+ Line::from("Survives Gateway restart. Env vars in docker-compose override fields when set.")
749
820
  .style(Style::default().fg(TEXT_MUTED)),
750
821
  );
751
822
  lines.push(
752
- Line::from("Set env in docker-compose / shell before connect to keep forever.")
823
+ Line::from("Scope: all agent runs on this Gateway (one policy per run execution).")
753
824
  .style(Style::default().fg(TEXT_MUTED)),
754
825
  );
755
826
  lines.push(Line::from(""));
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "4runr-os",
3
- "version": "2.10.63",
3
+ "version": "2.10.65",
4
4
  "type": "module",
5
- "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.63: Sentinel Custom M:SS time display, cent-level cost steps.",
5
+ "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.65: Sentinel limits reload from disk on open; About tab explains purpose.",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "4runr": "dist/index.js",