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.
- package/apps/gateway/package-lock.json +152 -152
- package/apps/gateway/scripts/verify-sentinel-persist.mjs +81 -0
- package/apps/gateway/src/__tests__/sentinel-config-persist-http.test.ts +93 -0
- package/apps/gateway/src/__tests__/sentinel-config-store.test.ts +133 -0
- package/apps/gateway/src/index.ts +2 -0
- package/apps/gateway/src/routes/sentinel-policies.ts +13 -4
- package/apps/gateway/src/security/sentinel-config-store.ts +160 -0
- package/mk3-tui/src/app.rs +26 -17
- package/mk3-tui/src/ui/sentinel_config.rs +80 -9
- package/package.json +2 -2
|
@@ -199,20 +199,20 @@
|
|
|
199
199
|
}
|
|
200
200
|
},
|
|
201
201
|
"node_modules/@aws-sdk/client-kms": {
|
|
202
|
-
"version": "3.
|
|
203
|
-
"resolved": "https://registry.npmjs.org/@aws-sdk/client-kms/-/client-kms-3.
|
|
204
|
-
"integrity": "sha512-
|
|
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.
|
|
210
|
-
"@aws-sdk/credential-provider-node": "^3.972.
|
|
211
|
-
"@aws-sdk/types": "^3.973.
|
|
212
|
-
"@smithy/core": "^3.24.
|
|
213
|
-
"@smithy/fetch-http-handler": "^5.4.
|
|
214
|
-
"@smithy/node-http-handler": "^4.7.
|
|
215
|
-
"@smithy/types": "^4.14.
|
|
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.
|
|
224
|
-
"resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.
|
|
225
|
-
"integrity": "sha512-
|
|
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.
|
|
231
|
-
"@aws-sdk/credential-provider-node": "^3.972.
|
|
232
|
-
"@aws-sdk/types": "^3.973.
|
|
233
|
-
"@smithy/core": "^3.24.
|
|
234
|
-
"@smithy/fetch-http-handler": "^5.4.
|
|
235
|
-
"@smithy/node-http-handler": "^4.7.
|
|
236
|
-
"@smithy/types": "^4.14.
|
|
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.
|
|
245
|
-
"resolved": "https://registry.npmjs.org/@aws-sdk/client-ssm/-/client-ssm-3.
|
|
246
|
-
"integrity": "sha512-
|
|
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.
|
|
252
|
-
"@aws-sdk/credential-provider-node": "^3.972.
|
|
253
|
-
"@aws-sdk/types": "^3.973.
|
|
254
|
-
"@smithy/core": "^3.24.
|
|
255
|
-
"@smithy/fetch-http-handler": "^5.4.
|
|
256
|
-
"@smithy/node-http-handler": "^4.7.
|
|
257
|
-
"@smithy/types": "^4.14.
|
|
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.
|
|
266
|
-
"resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.
|
|
267
|
-
"integrity": "sha512-
|
|
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.
|
|
271
|
-
"@aws-sdk/xml-builder": "^3.972.
|
|
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.
|
|
274
|
-
"@smithy/signature-v4": "^5.4.
|
|
275
|
-
"@smithy/types": "^4.14.
|
|
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.
|
|
285
|
-
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.
|
|
286
|
-
"integrity": "sha512-
|
|
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.
|
|
290
|
-
"@aws-sdk/types": "^3.973.
|
|
291
|
-
"@smithy/core": "^3.24.
|
|
292
|
-
"@smithy/types": "^4.14.
|
|
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.
|
|
301
|
-
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.
|
|
302
|
-
"integrity": "sha512-
|
|
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.
|
|
306
|
-
"@aws-sdk/types": "^3.973.
|
|
307
|
-
"@smithy/core": "^3.24.
|
|
308
|
-
"@smithy/fetch-http-handler": "^5.4.
|
|
309
|
-
"@smithy/node-http-handler": "^4.7.
|
|
310
|
-
"@smithy/types": "^4.14.
|
|
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.
|
|
319
|
-
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.
|
|
320
|
-
"integrity": "sha512-
|
|
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.
|
|
324
|
-
"@aws-sdk/credential-provider-env": "^3.972.
|
|
325
|
-
"@aws-sdk/credential-provider-http": "^3.972.
|
|
326
|
-
"@aws-sdk/credential-provider-login": "^3.972.
|
|
327
|
-
"@aws-sdk/credential-provider-process": "^3.972.
|
|
328
|
-
"@aws-sdk/credential-provider-sso": "^3.972.
|
|
329
|
-
"@aws-sdk/credential-provider-web-identity": "^3.972.
|
|
330
|
-
"@aws-sdk/nested-clients": "^3.997.
|
|
331
|
-
"@aws-sdk/types": "^3.973.
|
|
332
|
-
"@smithy/core": "^3.24.
|
|
333
|
-
"@smithy/credential-provider-imds": "^4.3.
|
|
334
|
-
"@smithy/types": "^4.14.
|
|
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.
|
|
343
|
-
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.
|
|
344
|
-
"integrity": "sha512
|
|
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.
|
|
348
|
-
"@aws-sdk/nested-clients": "^3.997.
|
|
349
|
-
"@aws-sdk/types": "^3.973.
|
|
350
|
-
"@smithy/core": "^3.24.
|
|
351
|
-
"@smithy/types": "^4.14.
|
|
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.
|
|
360
|
-
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.
|
|
361
|
-
"integrity": "sha512-
|
|
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.
|
|
365
|
-
"@aws-sdk/credential-provider-http": "^3.972.
|
|
366
|
-
"@aws-sdk/credential-provider-ini": "^3.972.
|
|
367
|
-
"@aws-sdk/credential-provider-process": "^3.972.
|
|
368
|
-
"@aws-sdk/credential-provider-sso": "^3.972.
|
|
369
|
-
"@aws-sdk/credential-provider-web-identity": "^3.972.
|
|
370
|
-
"@aws-sdk/types": "^3.973.
|
|
371
|
-
"@smithy/core": "^3.24.
|
|
372
|
-
"@smithy/credential-provider-imds": "^4.3.
|
|
373
|
-
"@smithy/types": "^4.14.
|
|
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.
|
|
382
|
-
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.
|
|
383
|
-
"integrity": "sha512-
|
|
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.
|
|
387
|
-
"@aws-sdk/types": "^3.973.
|
|
388
|
-
"@smithy/core": "^3.24.
|
|
389
|
-
"@smithy/types": "^4.14.
|
|
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.
|
|
398
|
-
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.
|
|
399
|
-
"integrity": "sha512-
|
|
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.
|
|
403
|
-
"@aws-sdk/nested-clients": "^3.997.
|
|
404
|
-
"@aws-sdk/token-providers": "3.
|
|
405
|
-
"@aws-sdk/types": "^3.973.
|
|
406
|
-
"@smithy/core": "^3.24.
|
|
407
|
-
"@smithy/types": "^4.14.
|
|
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.
|
|
416
|
-
"resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.
|
|
417
|
-
"integrity": "sha512-
|
|
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.
|
|
421
|
-
"@aws-sdk/nested-clients": "^3.997.
|
|
422
|
-
"@aws-sdk/types": "^3.973.
|
|
423
|
-
"@smithy/core": "^3.24.
|
|
424
|
-
"@smithy/types": "^4.14.
|
|
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.
|
|
433
|
-
"resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.
|
|
434
|
-
"integrity": "sha512-
|
|
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.
|
|
440
|
-
"@aws-sdk/signature-v4-multi-region": "^3.996.
|
|
441
|
-
"@aws-sdk/types": "^3.973.
|
|
442
|
-
"@smithy/core": "^3.24.
|
|
443
|
-
"@smithy/fetch-http-handler": "^5.4.
|
|
444
|
-
"@smithy/node-http-handler": "^4.7.
|
|
445
|
-
"@smithy/types": "^4.14.
|
|
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.
|
|
454
|
-
"resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.
|
|
455
|
-
"integrity": "sha512-
|
|
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.
|
|
459
|
-
"@smithy/signature-v4": "^5.4.
|
|
460
|
-
"@smithy/types": "^4.14.
|
|
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.
|
|
469
|
-
"resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.
|
|
470
|
-
"integrity": "sha512-
|
|
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.
|
|
474
|
-
"@aws-sdk/nested-clients": "^3.997.
|
|
475
|
-
"@aws-sdk/types": "^3.973.
|
|
476
|
-
"@smithy/core": "^3.24.
|
|
477
|
-
"@smithy/types": "^4.14.
|
|
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.
|
|
486
|
-
"resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.
|
|
487
|
-
"integrity": "sha512-
|
|
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.
|
|
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.
|
|
511
|
-
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.
|
|
512
|
-
"integrity": "sha512-
|
|
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.
|
|
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.
|
|
4127
|
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.
|
|
4128
|
-
"integrity": "sha512-
|
|
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.
|
|
6864
|
-
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.
|
|
6865
|
-
"integrity": "sha512-
|
|
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
|
|
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:
|
|
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
|
+
}
|
package/mk3-tui/src/app.rs
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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::
|
|
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
|
|
558
|
+
"Tab: next view · ↑/↓ Template · Enter Apply · R Refresh · ESC Close"
|
|
530
559
|
}
|
|
531
560
|
SentinelViewMode::Custom => {
|
|
532
|
-
"Tab
|
|
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("
|
|
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("
|
|
745
|
-
.style(Style::default().fg(
|
|
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("
|
|
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("
|
|
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.
|
|
3
|
+
"version": "2.10.65",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "4Runr AI Agent OS - Secure terminal interface for AI agents. v2.10.
|
|
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",
|