4runr-os 2.10.65 → 2.10.67

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. package/apps/gateway/dist/apps/gateway/src/index.js +2 -0
  2. package/apps/gateway/dist/apps/gateway/src/index.js.map +1 -1
  3. package/apps/gateway/dist/apps/gateway/src/routes/sentinel-policies.d.ts.map +1 -1
  4. package/apps/gateway/dist/apps/gateway/src/routes/sentinel-policies.js +8 -3
  5. package/apps/gateway/dist/apps/gateway/src/routes/sentinel-policies.js.map +1 -1
  6. package/apps/gateway/dist/apps/gateway/src/security/sentinel-config-store.d.ts +14 -0
  7. package/apps/gateway/dist/apps/gateway/src/security/sentinel-config-store.d.ts.map +1 -0
  8. package/apps/gateway/dist/apps/gateway/src/security/sentinel-config-store.js +168 -0
  9. package/apps/gateway/dist/apps/gateway/src/security/sentinel-config-store.js.map +1 -0
  10. package/apps/gateway/package-lock.json +125 -125
  11. package/apps/gateway/src/__tests__/run-kill.test.ts +80 -80
  12. package/apps/gateway/src/__tests__/sentinel-events.test.ts +95 -95
  13. package/apps/gateway/src/__tests__/sentinel-execute-watch.test.ts +45 -45
  14. package/apps/gateway/src/__tests__/sentinel-policies.test.ts +90 -90
  15. package/apps/gateway/src/__tests__/sentinel-publish-kill.test.ts +30 -30
  16. package/apps/gateway/src/__tests__/sentinel-run-failure.test.ts +89 -89
  17. package/apps/gateway/src/adapters/gateway-cancel-adapter.ts +32 -32
  18. package/apps/gateway/src/queue/sentinel-execute-watch.ts +42 -42
  19. package/apps/gateway/src/routes/sentinel-policies.ts +2 -2
  20. package/apps/gateway/src/runs/run-kill.ts +117 -117
  21. package/apps/gateway/src/security/sentinel-config-store.ts +53 -5
  22. package/apps/gateway/src/security/sentinel-run-failure.ts +85 -85
  23. package/mk3-tui/src/app.rs +4 -19
  24. package/mk3-tui/src/ui/sentinel_config.rs +86 -113
  25. package/package.json +4 -4
  26. package/scripts/os-tools-smoke.cjs +460 -460
@@ -1,95 +1,95 @@
1
- /**
2
- * Sentinel Redis event payload tests (N3)
3
- */
4
-
5
- import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
6
- import {
7
- buildAuditLogEvent,
8
- buildKillEvent,
9
- buildPolicyViolationEvent,
10
- } from '../adapters/redis-sentinel-publisher.js';
11
- import {
12
- resetMetrics,
13
- sentinelKillsTotal,
14
- sentinelPolicyViolationsTotal,
15
- } from '../metrics/index.js';
16
-
17
- describe('Sentinel Redis events (N3)', () => {
18
- beforeEach(() => {
19
- resetMetrics();
20
- });
21
-
22
- afterEach(() => {
23
- resetMetrics();
24
- });
25
-
26
- describe('event payloads', () => {
27
- it('buildPolicyViolationEvent includes deny action and policy fields', () => {
28
- const event = buildPolicyViolationEvent('run-1', {
29
- policy: 'timeout',
30
- reason: 'Run exceeded maximum duration',
31
- threshold: 60_000,
32
- actual: 120_000,
33
- action: 'deny',
34
- });
35
-
36
- expect(event.type).toBe('policy.violation');
37
- expect(event.runId).toBe('run-1');
38
- expect(event.data).toMatchObject({
39
- policy: 'timeout',
40
- action: 'deny',
41
- threshold: 60_000,
42
- actual: 120_000,
43
- });
44
- });
45
-
46
- it('buildKillEvent includes stable output and trigger', () => {
47
- const event = buildKillEvent('run-2', 'policy_violation:idle', 'policy');
48
-
49
- expect(event.type).toBe('kill');
50
- expect(event.data).toMatchObject({
51
- reason: 'policy_violation:idle',
52
- trigger: 'policy',
53
- source: 'sentinel',
54
- policy: 'idle',
55
- output: {
56
- error: 'Run terminated by Sentinel',
57
- source: 'sentinel',
58
- action: 'deny',
59
- reason: 'policy_violation:idle',
60
- policy: 'idle',
61
- },
62
- });
63
- });
64
-
65
- it('buildAuditLogEvent wraps kill audit records', () => {
66
- const event = buildAuditLogEvent('run-3', 'kill', {
67
- reason: 'manual_cancellation',
68
- trigger: 'manual',
69
- });
70
-
71
- expect(event.type).toBe('audit.log');
72
- expect(event.data.action).toBe('kill');
73
- expect(event.data.reason).toBe('manual_cancellation');
74
- });
75
- });
76
-
77
- describe('Prometheus counters', () => {
78
- it('sentinelPolicyViolationsTotal labels by policy', async () => {
79
- sentinelPolicyViolationsTotal.inc({ policy: 'token_cap' });
80
-
81
- const metrics = await sentinelPolicyViolationsTotal.get();
82
- const row = metrics.values.find((v) => v.labels.policy === 'token_cap');
83
- expect(row?.value).toBe(1);
84
- });
85
-
86
- it('sentinelKillsTotal labels by trigger', async () => {
87
- sentinelKillsTotal.inc({ trigger: 'manual' });
88
- sentinelKillsTotal.inc({ trigger: 'policy' });
89
-
90
- const metrics = await sentinelKillsTotal.get();
91
- expect(metrics.values.find((v) => v.labels.trigger === 'manual')?.value).toBe(1);
92
- expect(metrics.values.find((v) => v.labels.trigger === 'policy')?.value).toBe(1);
93
- });
94
- });
95
- });
1
+ /**
2
+ * Sentinel Redis event payload tests (N3)
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
6
+ import {
7
+ buildAuditLogEvent,
8
+ buildKillEvent,
9
+ buildPolicyViolationEvent,
10
+ } from '../adapters/redis-sentinel-publisher.js';
11
+ import {
12
+ resetMetrics,
13
+ sentinelKillsTotal,
14
+ sentinelPolicyViolationsTotal,
15
+ } from '../metrics/index.js';
16
+
17
+ describe('Sentinel Redis events (N3)', () => {
18
+ beforeEach(() => {
19
+ resetMetrics();
20
+ });
21
+
22
+ afterEach(() => {
23
+ resetMetrics();
24
+ });
25
+
26
+ describe('event payloads', () => {
27
+ it('buildPolicyViolationEvent includes deny action and policy fields', () => {
28
+ const event = buildPolicyViolationEvent('run-1', {
29
+ policy: 'timeout',
30
+ reason: 'Run exceeded maximum duration',
31
+ threshold: 60_000,
32
+ actual: 120_000,
33
+ action: 'deny',
34
+ });
35
+
36
+ expect(event.type).toBe('policy.violation');
37
+ expect(event.runId).toBe('run-1');
38
+ expect(event.data).toMatchObject({
39
+ policy: 'timeout',
40
+ action: 'deny',
41
+ threshold: 60_000,
42
+ actual: 120_000,
43
+ });
44
+ });
45
+
46
+ it('buildKillEvent includes stable output and trigger', () => {
47
+ const event = buildKillEvent('run-2', 'policy_violation:idle', 'policy');
48
+
49
+ expect(event.type).toBe('kill');
50
+ expect(event.data).toMatchObject({
51
+ reason: 'policy_violation:idle',
52
+ trigger: 'policy',
53
+ source: 'sentinel',
54
+ policy: 'idle',
55
+ output: {
56
+ error: 'Run terminated by Sentinel',
57
+ source: 'sentinel',
58
+ action: 'deny',
59
+ reason: 'policy_violation:idle',
60
+ policy: 'idle',
61
+ },
62
+ });
63
+ });
64
+
65
+ it('buildAuditLogEvent wraps kill audit records', () => {
66
+ const event = buildAuditLogEvent('run-3', 'kill', {
67
+ reason: 'manual_cancellation',
68
+ trigger: 'manual',
69
+ });
70
+
71
+ expect(event.type).toBe('audit.log');
72
+ expect(event.data.action).toBe('kill');
73
+ expect(event.data.reason).toBe('manual_cancellation');
74
+ });
75
+ });
76
+
77
+ describe('Prometheus counters', () => {
78
+ it('sentinelPolicyViolationsTotal labels by policy', async () => {
79
+ sentinelPolicyViolationsTotal.inc({ policy: 'token_cap' });
80
+
81
+ const metrics = await sentinelPolicyViolationsTotal.get();
82
+ const row = metrics.values.find((v) => v.labels.policy === 'token_cap');
83
+ expect(row?.value).toBe(1);
84
+ });
85
+
86
+ it('sentinelKillsTotal labels by trigger', async () => {
87
+ sentinelKillsTotal.inc({ trigger: 'manual' });
88
+ sentinelKillsTotal.inc({ trigger: 'policy' });
89
+
90
+ const metrics = await sentinelKillsTotal.get();
91
+ expect(metrics.values.find((v) => v.labels.trigger === 'manual')?.value).toBe(1);
92
+ expect(metrics.values.find((v) => v.labels.trigger === 'policy')?.value).toBe(1);
93
+ });
94
+ });
95
+ });
@@ -1,45 +1,45 @@
1
- import { describe, it, expect, jest, afterEach } from '@jest/globals';
2
- import { resetPolicyViolationPublisher } from '@4runr/sentinel';
3
- import { executeWithSentinelWatch, SENTINEL_KILL_CHECK_INTERVAL_MS } from '../queue/sentinel-execute-watch.js';
4
- import { formatSentinelKillError } from '../security/sentinel-run-failure.js';
5
-
6
- describe('executeWithSentinelWatch', () => {
7
- afterEach(() => {
8
- resetPolicyViolationPublisher();
9
- });
10
-
11
- it('uses 250ms poll interval during execute', () => {
12
- expect(SENTINEL_KILL_CHECK_INTERVAL_MS).toBe(250);
13
- });
14
-
15
- it('rejects when Sentinel marks run killed during execute', async () => {
16
- let calls = 0;
17
- const getRunStatus = jest.fn(() => {
18
- calls += 1;
19
- if (calls >= 2) {
20
- return { status: 'killed', killReason: 'policy_violation:timeout' };
21
- }
22
- return { status: 'running' };
23
- });
24
-
25
- await expect(
26
- executeWithSentinelWatch(
27
- 'run-1',
28
- () => new Promise((resolve) => setTimeout(() => resolve('done'), 2500)),
29
- getRunStatus
30
- )
31
- ).rejects.toThrow(formatSentinelKillError('policy_violation:timeout'));
32
- });
33
-
34
- it('returns execute result when run stays running', async () => {
35
- const getRunStatus = jest.fn(() => ({ status: 'running' }));
36
-
37
- const result = await executeWithSentinelWatch(
38
- 'run-2',
39
- async () => 'ok',
40
- getRunStatus
41
- );
42
-
43
- expect(result).toBe('ok');
44
- });
45
- });
1
+ import { describe, it, expect, jest, afterEach } from '@jest/globals';
2
+ import { resetPolicyViolationPublisher } from '@4runr/sentinel';
3
+ import { executeWithSentinelWatch, SENTINEL_KILL_CHECK_INTERVAL_MS } from '../queue/sentinel-execute-watch.js';
4
+ import { formatSentinelKillError } from '../security/sentinel-run-failure.js';
5
+
6
+ describe('executeWithSentinelWatch', () => {
7
+ afterEach(() => {
8
+ resetPolicyViolationPublisher();
9
+ });
10
+
11
+ it('uses 250ms poll interval during execute', () => {
12
+ expect(SENTINEL_KILL_CHECK_INTERVAL_MS).toBe(250);
13
+ });
14
+
15
+ it('rejects when Sentinel marks run killed during execute', async () => {
16
+ let calls = 0;
17
+ const getRunStatus = jest.fn(() => {
18
+ calls += 1;
19
+ if (calls >= 2) {
20
+ return { status: 'killed', killReason: 'policy_violation:timeout' };
21
+ }
22
+ return { status: 'running' };
23
+ });
24
+
25
+ await expect(
26
+ executeWithSentinelWatch(
27
+ 'run-1',
28
+ () => new Promise((resolve) => setTimeout(() => resolve('done'), 2500)),
29
+ getRunStatus
30
+ )
31
+ ).rejects.toThrow(formatSentinelKillError('policy_violation:timeout'));
32
+ });
33
+
34
+ it('returns execute result when run stays running', async () => {
35
+ const getRunStatus = jest.fn(() => ({ status: 'running' }));
36
+
37
+ const result = await executeWithSentinelWatch(
38
+ 'run-2',
39
+ async () => 'ok',
40
+ getRunStatus
41
+ );
42
+
43
+ expect(result).toBe('ok');
44
+ });
45
+ });
@@ -1,90 +1,90 @@
1
- /**
2
- * Sentinel policy evaluation — deny (kill) failure modes (N2)
3
- */
4
-
5
- import { describe, it, expect } from '@jest/globals';
6
- import { policyManager } from '@4runr/sentinel';
7
- import type { RunState, SentinelConfig } from '@4runr/sentinel';
8
-
9
- const baseConfig: SentinelConfig = {
10
- enabled: true,
11
- runMaxDurationMs: 60_000,
12
- runMaxTokens: 1000,
13
- runIdleMs: 30_000,
14
- loopWindow: 15,
15
- loopMax: 5,
16
- runMaxCost: 1.0,
17
- };
18
-
19
- function runningState(overrides: Partial<RunState> = {}): RunState {
20
- const now = Date.now();
21
- return {
22
- runId: 'run-test',
23
- createdAt: now - 5000,
24
- startedAt: now - 5000,
25
- tokenCount: 0,
26
- status: 'running',
27
- events: [],
28
- ...overrides,
29
- };
30
- }
31
-
32
- describe('Sentinel policies (N2 deny modes)', () => {
33
- it('timeout policy returns violation when duration exceeded', () => {
34
- const runState = runningState({
35
- startedAt: Date.now() - 120_000,
36
- });
37
- const violations = policyManager.evaluatePolicies('run-test', runState, baseConfig);
38
- expect(violations.some(v => v.policy === 'timeout')).toBe(true);
39
- });
40
-
41
- it('token_cap policy returns violation when over limit', () => {
42
- const runState = runningState({ tokenCount: 2000 });
43
- const violations = policyManager.evaluatePolicies('run-test', runState, baseConfig);
44
- expect(violations.some(v => v.policy === 'token_cap')).toBe(true);
45
- });
46
-
47
- it('idle policy returns violation when no tokens after idle threshold', () => {
48
- const runState = runningState({
49
- startedAt: Date.now() - 60_000,
50
- lastTokenAt: undefined,
51
- });
52
- const violations = policyManager.evaluatePolicies('run-test', runState, baseConfig);
53
- expect(violations.some(v => v.policy === 'idle')).toBe(true);
54
- });
55
-
56
- it('cost policy returns violation when cost exceeds cap', () => {
57
- const runState = runningState({ cost: 2.5 });
58
- const violations = policyManager.evaluatePolicies('run-test', runState, baseConfig);
59
- expect(violations.some(v => v.policy === 'cost')).toBe(true);
60
- });
61
-
62
- it('returns no violations for healthy running state', () => {
63
- const runState = runningState({
64
- startedAt: Date.now() - 1000,
65
- lastTokenAt: Date.now() - 500,
66
- tokenCount: 100,
67
- cost: 0.01,
68
- });
69
- const violations = policyManager.evaluatePolicies('run-test', runState, baseConfig);
70
- expect(violations).toHaveLength(0);
71
- });
72
-
73
- it('loop_detection policy returns violation for repetitive token pattern', () => {
74
- const now = Date.now();
75
- const tokenEvents = Array.from({ length: 10 }, (_, i) => ({
76
- type: 'token' as const,
77
- runId: 'run-test',
78
- at: now - (10 - i) * 1000,
79
- tokens: 42,
80
- }));
81
- const runState = runningState({
82
- startedAt: now - 20_000,
83
- lastTokenAt: now - 1000,
84
- tokenCount: 420,
85
- events: tokenEvents,
86
- });
87
- const violations = policyManager.evaluatePolicies('run-test', runState, baseConfig);
88
- expect(violations.some(v => v.policy === 'loop_detection')).toBe(true);
89
- });
90
- });
1
+ /**
2
+ * Sentinel policy evaluation — deny (kill) failure modes (N2)
3
+ */
4
+
5
+ import { describe, it, expect } from '@jest/globals';
6
+ import { policyManager } from '@4runr/sentinel';
7
+ import type { RunState, SentinelConfig } from '@4runr/sentinel';
8
+
9
+ const baseConfig: SentinelConfig = {
10
+ enabled: true,
11
+ runMaxDurationMs: 60_000,
12
+ runMaxTokens: 1000,
13
+ runIdleMs: 30_000,
14
+ loopWindow: 15,
15
+ loopMax: 5,
16
+ runMaxCost: 1.0,
17
+ };
18
+
19
+ function runningState(overrides: Partial<RunState> = {}): RunState {
20
+ const now = Date.now();
21
+ return {
22
+ runId: 'run-test',
23
+ createdAt: now - 5000,
24
+ startedAt: now - 5000,
25
+ tokenCount: 0,
26
+ status: 'running',
27
+ events: [],
28
+ ...overrides,
29
+ };
30
+ }
31
+
32
+ describe('Sentinel policies (N2 deny modes)', () => {
33
+ it('timeout policy returns violation when duration exceeded', () => {
34
+ const runState = runningState({
35
+ startedAt: Date.now() - 120_000,
36
+ });
37
+ const violations = policyManager.evaluatePolicies('run-test', runState, baseConfig);
38
+ expect(violations.some(v => v.policy === 'timeout')).toBe(true);
39
+ });
40
+
41
+ it('token_cap policy returns violation when over limit', () => {
42
+ const runState = runningState({ tokenCount: 2000 });
43
+ const violations = policyManager.evaluatePolicies('run-test', runState, baseConfig);
44
+ expect(violations.some(v => v.policy === 'token_cap')).toBe(true);
45
+ });
46
+
47
+ it('idle policy returns violation when no tokens after idle threshold', () => {
48
+ const runState = runningState({
49
+ startedAt: Date.now() - 60_000,
50
+ lastTokenAt: undefined,
51
+ });
52
+ const violations = policyManager.evaluatePolicies('run-test', runState, baseConfig);
53
+ expect(violations.some(v => v.policy === 'idle')).toBe(true);
54
+ });
55
+
56
+ it('cost policy returns violation when cost exceeds cap', () => {
57
+ const runState = runningState({ cost: 2.5 });
58
+ const violations = policyManager.evaluatePolicies('run-test', runState, baseConfig);
59
+ expect(violations.some(v => v.policy === 'cost')).toBe(true);
60
+ });
61
+
62
+ it('returns no violations for healthy running state', () => {
63
+ const runState = runningState({
64
+ startedAt: Date.now() - 1000,
65
+ lastTokenAt: Date.now() - 500,
66
+ tokenCount: 100,
67
+ cost: 0.01,
68
+ });
69
+ const violations = policyManager.evaluatePolicies('run-test', runState, baseConfig);
70
+ expect(violations).toHaveLength(0);
71
+ });
72
+
73
+ it('loop_detection policy returns violation for repetitive token pattern', () => {
74
+ const now = Date.now();
75
+ const tokenEvents = Array.from({ length: 10 }, (_, i) => ({
76
+ type: 'token' as const,
77
+ runId: 'run-test',
78
+ at: now - (10 - i) * 1000,
79
+ tokens: 42,
80
+ }));
81
+ const runState = runningState({
82
+ startedAt: now - 20_000,
83
+ lastTokenAt: now - 1000,
84
+ tokenCount: 420,
85
+ events: tokenEvents,
86
+ });
87
+ const violations = policyManager.evaluatePolicies('run-test', runState, baseConfig);
88
+ expect(violations.some(v => v.policy === 'loop_detection')).toBe(true);
89
+ });
90
+ });
@@ -1,30 +1,30 @@
1
- /**
2
- * publishSentinelRunKill drives metrics (N3)
3
- */
4
-
5
- import { describe, it, expect, beforeEach, jest } from '@jest/globals';
6
- import { publishSentinelRunKill } from '../adapters/redis-sentinel-publisher.js';
7
- import { resetMetrics, sentinelKillsTotal } from '../metrics/index.js';
8
-
9
- const mockPublish = jest.fn<() => Promise<number>>().mockResolvedValue(1);
10
-
11
- jest.mock('../db/redis.js', () => ({
12
- getRedisClient: jest.fn(() => Promise.resolve({ publish: mockPublish })),
13
- }));
14
-
15
- describe('publishSentinelRunKill', () => {
16
- beforeEach(() => {
17
- resetMetrics();
18
- mockPublish.mockClear();
19
- });
20
-
21
- it('increments sentinelKillsTotal and publishes kill + audit channels', async () => {
22
- await publishSentinelRunKill('run-9', 'manual_cancellation');
23
-
24
- expect(mockPublish).toHaveBeenCalledTimes(2);
25
-
26
- const metrics = await sentinelKillsTotal.get();
27
- const manual = metrics.values.find((v) => v.labels.trigger === 'manual');
28
- expect(manual?.value).toBe(1);
29
- });
30
- });
1
+ /**
2
+ * publishSentinelRunKill drives metrics (N3)
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, jest } from '@jest/globals';
6
+ import { publishSentinelRunKill } from '../adapters/redis-sentinel-publisher.js';
7
+ import { resetMetrics, sentinelKillsTotal } from '../metrics/index.js';
8
+
9
+ const mockPublish = jest.fn<() => Promise<number>>().mockResolvedValue(1);
10
+
11
+ jest.mock('../db/redis.js', () => ({
12
+ getRedisClient: jest.fn(() => Promise.resolve({ publish: mockPublish })),
13
+ }));
14
+
15
+ describe('publishSentinelRunKill', () => {
16
+ beforeEach(() => {
17
+ resetMetrics();
18
+ mockPublish.mockClear();
19
+ });
20
+
21
+ it('increments sentinelKillsTotal and publishes kill + audit channels', async () => {
22
+ await publishSentinelRunKill('run-9', 'manual_cancellation');
23
+
24
+ expect(mockPublish).toHaveBeenCalledTimes(2);
25
+
26
+ const metrics = await sentinelKillsTotal.get();
27
+ const manual = metrics.values.find((v) => v.labels.trigger === 'manual');
28
+ expect(manual?.value).toBe(1);
29
+ });
30
+ });