4runr-os 2.10.72 → 2.10.73
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 +110 -110
- package/apps/gateway/scripts/verify-sentinel-persist.mjs +81 -81
- package/apps/gateway/src/__tests__/integration/sentinel.test.ts +79 -0
- package/apps/gateway/src/__tests__/sentinel-config-persist-http.test.ts +93 -93
- package/apps/gateway/src/__tests__/sentinel-config-store.test.ts +133 -133
- package/apps/gateway/src/security/sentinel-config-store.ts +208 -208
- package/dist/tui-handlers.js +57 -67
- package/dist/tui-handlers.js.map +1 -1
- package/dist/watchdog.d.ts +5 -0
- package/dist/watchdog.d.ts.map +1 -1
- package/dist/watchdog.js +82 -75
- package/dist/watchdog.js.map +1 -1
- package/mk3-tui/src/main.rs +7 -0
- package/package.json +2 -2
- package/scripts/os-tools-smoke.cjs +1 -1
|
@@ -1,93 +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
|
-
});
|
|
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
|
+
});
|
|
@@ -1,133 +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
|
-
});
|
|
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
|
+
});
|