4runr-os 2.10.62 → 2.10.64

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,85 @@
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
+ } from '../security/sentinel-config-store.js';
15
+
16
+ jest.mock('../middleware/rateLimit.js', () => ({
17
+ writeRateLimit: async () => {},
18
+ readRateLimit: async () => {},
19
+ }));
20
+
21
+ jest.mock('../middleware/auth.js', () => ({
22
+ requireAuth: async () => {},
23
+ }));
24
+
25
+ describe('POST /api/sentinel/policies/custom persistence', () => {
26
+ const originalAppData = process.env['APPDATA'];
27
+ let tmpDir: string;
28
+ let app: ReturnType<typeof Fastify>;
29
+
30
+ beforeEach(async () => {
31
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), '4runr-sentinel-http-'));
32
+ process.env['APPDATA'] = tmpDir;
33
+ delete process.env['RUN_IDLE_MS'];
34
+ delete process.env['SENTINEL_IDLE_MS'];
35
+ Sentinel.resetInstance();
36
+
37
+ const { sentinelPolicyRoutes } = await import('../routes/sentinel-policies.js');
38
+ app = Fastify();
39
+ await sentinelPolicyRoutes(app);
40
+ await app.ready();
41
+ });
42
+
43
+ afterEach(async () => {
44
+ await app.close();
45
+ Sentinel.resetInstance();
46
+ if (originalAppData === undefined) delete process.env['APPDATA'];
47
+ else process.env['APPDATA'] = originalAppData;
48
+ fs.rmSync(tmpDir, { recursive: true, force: true });
49
+ });
50
+
51
+ it('writes sentinel-limits.json and reloads after simulated Gateway restart', async () => {
52
+ const distinctiveIdle = 77_777;
53
+
54
+ const res = await app.inject({
55
+ method: 'POST',
56
+ url: '/api/sentinel/policies/custom',
57
+ payload: {
58
+ enabled: true,
59
+ runMaxDurationMs: 120_000,
60
+ runMaxTokens: 16_000,
61
+ runIdleMs: distinctiveIdle,
62
+ loopWindow: 15,
63
+ loopMax: 5,
64
+ runMaxCost: 1.5,
65
+ },
66
+ });
67
+
68
+ expect(res.statusCode).toBe(200);
69
+ const body = res.json() as { success?: boolean; config?: { runIdleMs?: number } };
70
+ expect(body.success).toBe(true);
71
+ expect(body.config?.runIdleMs).toBe(distinctiveIdle);
72
+
73
+ const filePath = getSentinelLimitsFilePath();
74
+ expect(fs.existsSync(filePath)).toBe(true);
75
+ const onDisk = JSON.parse(fs.readFileSync(filePath, 'utf8')) as {
76
+ config: { runIdleMs: number };
77
+ };
78
+ expect(onDisk.config.runIdleMs).toBe(distinctiveIdle);
79
+
80
+ Sentinel.resetInstance();
81
+ const sentinel = Sentinel.getInstance();
82
+ expect(applyPersistedSentinelConfig(sentinel)).toBe(true);
83
+ expect(sentinel.getConfig().runIdleMs).toBe(distinctiveIdle);
84
+ });
85
+ });
@@ -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,7 @@ 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 { persistSentinelConfigAfterApply } from '../security/sentinel-config-store.js';
15
16
 
16
17
  /**
17
18
  * Register Sentinel policy management routes
@@ -170,11 +171,12 @@ export async function sentinelPolicyRoutes(fastify: FastifyInstance) {
170
171
 
171
172
  try {
172
173
  sentinel.updateConfig(config);
174
+ const savedTo = persistSentinelConfigAfterApply(sentinel, config);
173
175
  return {
174
176
  success: true,
175
177
  template: templateName,
176
178
  config: config,
177
- message: `Template '${templateName}' applied successfully. New limits are now active.`
179
+ message: `Template '${templateName}' applied. Limits active and saved to ${savedTo}.`,
178
180
  };
179
181
  } catch (error: any) {
180
182
  return reply.status(400).send({
@@ -229,10 +231,11 @@ export async function sentinelPolicyRoutes(fastify: FastifyInstance) {
229
231
 
230
232
  try {
231
233
  sentinel.updateConfig(config);
234
+ const savedTo = persistSentinelConfigAfterApply(sentinel, config);
232
235
  return {
233
236
  success: true,
234
237
  config,
235
- message: 'Custom Sentinel limits applied. Active until Gateway restart unless set via env.',
238
+ message: `Custom Sentinel limits applied and saved to ${savedTo}. Survives Gateway restart; env vars override individual fields when set.`,
236
239
  };
237
240
  } catch (error: unknown) {
238
241
  const msg = error instanceof Error ? error.message : String(error);
@@ -0,0 +1,150 @@
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
+ /**
113
+ * Apply saved limits on Gateway boot. Env-defined fields keep env values.
114
+ */
115
+ export function applyPersistedSentinelConfig(sentinel: Sentinel, logger?: {
116
+ info: (msg: string, meta?: Record<string, unknown>) => void;
117
+ warn: (msg: string, meta?: Record<string, unknown>) => void;
118
+ }): boolean {
119
+ const saved = loadSentinelLimitsFromDisk();
120
+ if (!saved) return false;
121
+
122
+ const current = sentinel.getConfig();
123
+ const merged: SentinelConfig = { ...current };
124
+
125
+ for (const { field } of ENV_FIELD_KEYS) {
126
+ if (envDefinesField(field)) continue;
127
+ (merged as Record<string, unknown>)[field] = saved[field];
128
+ }
129
+
130
+ try {
131
+ sentinel.updateConfig(merged);
132
+ logger?.info('Sentinel limits loaded from disk', {
133
+ path: getSentinelLimitsFilePath(),
134
+ });
135
+ return true;
136
+ } catch (err) {
137
+ logger?.warn('Failed to apply saved Sentinel limits', {
138
+ error: err instanceof Error ? err.message : String(err),
139
+ });
140
+ return false;
141
+ }
142
+ }
143
+
144
+ export function persistSentinelConfigAfterApply(
145
+ sentinel: Sentinel,
146
+ config: SentinelConfig
147
+ ): string {
148
+ saveSentinelLimitsToDisk(config);
149
+ return getSentinelLimitsFilePath();
150
+ }
@@ -256,11 +256,11 @@ impl SentinelConfigState {
256
256
  }
257
257
  }
258
258
  CustomField::MaxCost => {
259
- let step = 0.25;
259
+ let step = 0.05;
260
260
  if increase {
261
- d.max_cost = (d.max_cost + step).min(100.0);
261
+ d.max_cost = round_cost((d.max_cost + step).min(100.0));
262
262
  } else {
263
- d.max_cost = (d.max_cost - step).max(0.01);
263
+ d.max_cost = round_cost((d.max_cost - step).max(0.05));
264
264
  }
265
265
  }
266
266
  CustomField::LoopWindow => {
@@ -430,10 +430,28 @@ pub fn parse_health(v: &Value) -> SentinelHealthSnapshot {
430
430
  }
431
431
 
432
432
  fn fmt_ms(ms: u64) -> String {
433
- if ms >= 60_000 {
434
- format!("{:.0}m", ms as f64 / 60_000.0)
433
+ let total_secs = ms / 1000;
434
+ let mins = total_secs / 60;
435
+ let secs = total_secs % 60;
436
+ if mins > 0 {
437
+ format!("{}:{:02}", mins, secs)
435
438
  } else {
436
- format!("{:.0}s", ms as f64 / 1000.0)
439
+ format!("{}s", secs)
440
+ }
441
+ }
442
+
443
+ fn round_cost(usd: f64) -> f64 {
444
+ (usd * 100.0).round() / 100.0
445
+ }
446
+
447
+ fn fmt_cost(usd: f64) -> String {
448
+ let cents = (round_cost(usd) * 100.0).round() as i64;
449
+ let dollars = cents / 100;
450
+ let rem = cents.abs() % 100;
451
+ if rem == 0 {
452
+ format!("${}", dollars)
453
+ } else {
454
+ format!("${}.{:02}", dollars, rem)
437
455
  }
438
456
  }
439
457
 
@@ -449,7 +467,7 @@ fn custom_field_value(d: &SentinelCurrentConfig, field: CustomField) -> String {
449
467
  CustomField::IdleMs => fmt_ms(d.idle_ms),
450
468
  CustomField::MaxDurationMs => fmt_ms(d.duration_ms),
451
469
  CustomField::MaxTokens => format!("{}", d.max_tokens),
452
- CustomField::MaxCost => format!("${:.2}", d.max_cost),
470
+ CustomField::MaxCost => fmt_cost(d.max_cost),
453
471
  CustomField::LoopWindow => format!("{}s", d.loop_window),
454
472
  CustomField::LoopMax => format!("{}", d.loop_max),
455
473
  }
@@ -511,7 +529,7 @@ pub fn render(f: &mut Frame, state: &AppState) {
511
529
  "Tab Custom · ↑/↓ Template · Enter Apply · R Refresh · ESC Close"
512
530
  }
513
531
  SentinelViewMode::Custom => {
514
- "Tab Templates · ↑/↓ Field · ←/→ or +/- Value · Space ON/OFF · Enter Apply · ESC Close"
532
+ "Tab Templates · ↑/↓ Field · ←/→ Value · Enter Apply (saves to disk) · ESC Close"
515
533
  }
516
534
  };
517
535
  const CUSTOM_SHORTCUTS: &str =
@@ -599,10 +617,7 @@ fn render_active_panel(f: &mut Frame, area: Rect, sc: &SentinelConfigState) {
599
617
  ]),
600
618
  Line::from(vec![
601
619
  Span::styled("Max cost: ", Style::default().fg(TEXT_DIM)),
602
- Span::styled(
603
- format!("${:.2}", sc.current_max_cost),
604
- Style::default().fg(AMBER_WARN),
605
- ),
620
+ Span::styled(fmt_cost(sc.current_max_cost), Style::default().fg(AMBER_WARN)),
606
621
  ]),
607
622
  Line::from(vec![
608
623
  Span::styled("Loop detect: ", Style::default().fg(TEXT_DIM)),
@@ -726,15 +741,15 @@ fn render_custom_help(f: &mut Frame, area: Rect, sc: &SentinelConfigState) {
726
741
  lines.push(Line::from(field.env_var()).style(Style::default().fg(CYBER_CYAN)));
727
742
  lines.push(Line::from(""));
728
743
  lines.push(
729
- Line::from("UI Apply = live in memory until Gateway restarts.")
730
- .style(Style::default().fg(TEXT_MUTED)),
744
+ Line::from("Enter Apply Gateway + disk (%APPDATA%\\4runr\\config\\sentinel-limits.json).")
745
+ .style(Style::default().fg(NEON_GREEN)),
731
746
  );
732
747
  lines.push(
733
- Line::from("D in 4r disconnect respawns Gateway env defaults return.")
748
+ Line::from("Survives Gateway restart. Env vars in docker-compose override fields when set.")
734
749
  .style(Style::default().fg(TEXT_MUTED)),
735
750
  );
736
751
  lines.push(
737
- Line::from("Set env in docker-compose / shell before connect to keep forever.")
752
+ Line::from("Scope: all agent runs on this Gateway (one policy per run execution).")
738
753
  .style(Style::default().fg(TEXT_MUTED)),
739
754
  );
740
755
  lines.push(Line::from(""));
@@ -745,3 +760,25 @@ fn render_custom_help(f: &mut Frame, area: Rect, sc: &SentinelConfigState) {
745
760
 
746
761
  f.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
747
762
  }
763
+
764
+ #[cfg(test)]
765
+ mod format_tests {
766
+ use super::{fmt_cost, fmt_ms};
767
+
768
+ #[test]
769
+ fn fmt_ms_shows_minutes_and_seconds() {
770
+ assert_eq!(fmt_ms(30_000), "30s");
771
+ assert_eq!(fmt_ms(60_000), "1:00");
772
+ assert_eq!(fmt_ms(75_000), "1:15");
773
+ assert_eq!(fmt_ms(240_000), "4:00");
774
+ assert_eq!(fmt_ms(255_000), "4:15");
775
+ }
776
+
777
+ #[test]
778
+ fn fmt_cost_shows_cents_when_present() {
779
+ assert_eq!(fmt_cost(1.0), "$1");
780
+ assert_eq!(fmt_cost(1.05), "$1.05");
781
+ assert_eq!(fmt_cost(1.15), "$1.15");
782
+ assert_eq!(fmt_cost(1.25), "$1.25");
783
+ }
784
+ }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "4runr-os",
3
- "version": "2.10.62",
3
+ "version": "2.10.64",
4
4
  "type": "module",
5
- "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.62: Sentinel Custom tab edit limits with arrow keys, +/-, and mouse wheel.",
5
+ "description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.64: Sentinel limits persist to disk; Custom tab edit with arrow keys and M:SS display.",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "4runr": "dist/index.js",