4runr-os 2.10.65 → 2.10.66
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.
|
@@ -175,7 +175,7 @@ export async function sentinelPolicyRoutes(fastify: FastifyInstance) {
|
|
|
175
175
|
|
|
176
176
|
try {
|
|
177
177
|
sentinel.updateConfig(config);
|
|
178
|
-
const savedTo = persistSentinelConfigAfterApply(sentinel, config);
|
|
178
|
+
const savedTo = persistSentinelConfigAfterApply(sentinel, config, fastify.log);
|
|
179
179
|
return {
|
|
180
180
|
success: true,
|
|
181
181
|
template: templateName,
|
|
@@ -235,7 +235,7 @@ export async function sentinelPolicyRoutes(fastify: FastifyInstance) {
|
|
|
235
235
|
|
|
236
236
|
try {
|
|
237
237
|
sentinel.updateConfig(config);
|
|
238
|
-
const savedTo = persistSentinelConfigAfterApply(sentinel, config);
|
|
238
|
+
const savedTo = persistSentinelConfigAfterApply(sentinel, config, fastify.log);
|
|
239
239
|
return {
|
|
240
240
|
success: true,
|
|
241
241
|
config,
|
|
@@ -119,7 +119,7 @@ export function mergePersistedSentinelConfig(sentinel: Sentinel): boolean {
|
|
|
119
119
|
|
|
120
120
|
for (const { field } of ENV_FIELD_KEYS) {
|
|
121
121
|
if (envDefinesField(field)) continue;
|
|
122
|
-
(merged as
|
|
122
|
+
(merged as any)[field] = saved[field];
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
try {
|
|
@@ -137,12 +137,49 @@ export function applyPersistedSentinelConfig(sentinel: Sentinel, logger?: {
|
|
|
137
137
|
info: (msg: string, meta?: Record<string, unknown>) => void;
|
|
138
138
|
warn: (msg: string, meta?: Record<string, unknown>) => void;
|
|
139
139
|
}): boolean {
|
|
140
|
+
const filePath = getSentinelLimitsFilePath();
|
|
141
|
+
const saved = loadSentinelLimitsFromDisk();
|
|
142
|
+
|
|
143
|
+
if (!saved) {
|
|
144
|
+
logger?.info('No Sentinel limits file found on disk (will use env defaults)', {
|
|
145
|
+
expectedPath: filePath,
|
|
146
|
+
});
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check which env vars are set and will override the disk values
|
|
151
|
+
const envOverrides: string[] = [];
|
|
152
|
+
const diskApplied: string[] = [];
|
|
153
|
+
|
|
154
|
+
for (const { field, envKeys } of ENV_FIELD_KEYS) {
|
|
155
|
+
if (envDefinesField(field)) {
|
|
156
|
+
const activeEnv = envKeys.find(k => process.env[k] !== undefined && process.env[k]?.trim() !== '');
|
|
157
|
+
if (activeEnv) {
|
|
158
|
+
envOverrides.push(`${field} (via ${activeEnv}=${process.env[activeEnv]})`);
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
diskApplied.push(`${field}=${saved[field]}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
140
165
|
const ok = mergePersistedSentinelConfig(sentinel);
|
|
166
|
+
|
|
141
167
|
if (ok) {
|
|
142
|
-
|
|
143
|
-
|
|
168
|
+
const finalConfig = sentinel.getConfig();
|
|
169
|
+
logger?.info('✅ Sentinel limits loaded from disk and merged', {
|
|
170
|
+
diskPath: filePath,
|
|
171
|
+
diskValues: saved,
|
|
172
|
+
envOverrides: envOverrides.length > 0 ? envOverrides : 'none',
|
|
173
|
+
diskAppliedFields: diskApplied.length > 0 ? diskApplied : 'none (all env-overridden)',
|
|
174
|
+
finalActiveConfig: finalConfig,
|
|
175
|
+
});
|
|
176
|
+
} else {
|
|
177
|
+
logger?.warn('Failed to apply Sentinel limits from disk', {
|
|
178
|
+
diskPath: filePath,
|
|
179
|
+
savedConfig: saved,
|
|
144
180
|
});
|
|
145
181
|
}
|
|
182
|
+
|
|
146
183
|
return ok;
|
|
147
184
|
}
|
|
148
185
|
|
|
@@ -153,8 +190,19 @@ export function hydrateSentinelConfigFromDisk(sentinel: Sentinel): void {
|
|
|
153
190
|
|
|
154
191
|
export function persistSentinelConfigAfterApply(
|
|
155
192
|
sentinel: Sentinel,
|
|
156
|
-
config: SentinelConfig
|
|
193
|
+
config: SentinelConfig,
|
|
194
|
+
logger?: {
|
|
195
|
+
info: (msg: string, meta?: Record<string, unknown>) => void;
|
|
196
|
+
}
|
|
157
197
|
): string {
|
|
198
|
+
const filePath = getSentinelLimitsFilePath();
|
|
158
199
|
saveSentinelLimitsToDisk(config);
|
|
159
|
-
|
|
200
|
+
|
|
201
|
+
logger?.info('✅ Sentinel limits saved to disk', {
|
|
202
|
+
path: filePath,
|
|
203
|
+
config: config,
|
|
204
|
+
note: 'Survives Gateway restart. Env vars in docker-compose override individual fields when set.',
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
return filePath;
|
|
160
208
|
}
|
package/mk3-tui/src/app.rs
CHANGED
|
@@ -2571,25 +2571,10 @@ impl App {
|
|
|
2571
2571
|
self.state.sentinel_config.error =
|
|
2572
2572
|
Some("Connect to Gateway first.".to_string());
|
|
2573
2573
|
} else if let Some(ws) = ws_client {
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
}
|
|
2579
|
-
SentinelViewMode::Templates => {
|
|
2580
|
-
if let Some(t) = self.state.sentinel_config.selected_template() {
|
|
2581
|
-
let key = t.key.clone();
|
|
2582
|
-
let name = t.name.clone();
|
|
2583
|
-
self.begin_sentinel_apply_request(ws, &key);
|
|
2584
|
-
self.add_log(format!("[SENTINEL] Applying template: {}", name));
|
|
2585
|
-
}
|
|
2586
|
-
}
|
|
2587
|
-
SentinelViewMode::Custom => {
|
|
2588
|
-
let draft = self.state.sentinel_config.custom_draft.clone();
|
|
2589
|
-
self.begin_sentinel_apply_custom_request(ws, &draft);
|
|
2590
|
-
self.add_log("[SENTINEL] Applying custom limits".to_string());
|
|
2591
|
-
}
|
|
2592
|
-
}
|
|
2574
|
+
// Apply custom configuration
|
|
2575
|
+
let draft = self.state.sentinel_config.custom_draft.clone();
|
|
2576
|
+
self.begin_sentinel_apply_custom_request(ws, &draft);
|
|
2577
|
+
self.add_log("[SENTINEL] Applying custom limits...".to_string());
|
|
2593
2578
|
}
|
|
2594
2579
|
self.request_render("sentinel_apply");
|
|
2595
2580
|
}
|
|
@@ -14,8 +14,6 @@ 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,
|
|
18
|
-
Templates,
|
|
19
17
|
Custom,
|
|
20
18
|
}
|
|
21
19
|
|
|
@@ -27,13 +25,13 @@ const ABOUT_SENTINEL_LINES: &[&str] = &[
|
|
|
27
25
|
" • Enforces limits: idle time, max duration, token cap, cost, loop detection",
|
|
28
26
|
" • On violation: kills the run (status killed, policy_violation:* in logs)",
|
|
29
27
|
"",
|
|
30
|
-
"Scope
|
|
28
|
+
"Scope:",
|
|
31
29
|
" • One policy for ALL runs on this Gateway (not per-agent yet)",
|
|
32
30
|
" • A run = one agent execution (POST /api/runs → start → agent runs)",
|
|
33
31
|
"",
|
|
34
|
-
"
|
|
35
|
-
" •
|
|
36
|
-
" •
|
|
32
|
+
"Persistence:",
|
|
33
|
+
" • Enter: saves to disk (%APPDATA%\\4runr\\config\\sentinel-limits.json)",
|
|
34
|
+
" • Survives Gateway restart",
|
|
37
35
|
" • Env vars in docker-compose override individual fields when set",
|
|
38
36
|
];
|
|
39
37
|
|
|
@@ -157,7 +155,7 @@ pub struct SentinelConfigState {
|
|
|
157
155
|
impl Default for SentinelConfigState {
|
|
158
156
|
fn default() -> Self {
|
|
159
157
|
Self {
|
|
160
|
-
view_mode: SentinelViewMode::
|
|
158
|
+
view_mode: SentinelViewMode::Custom,
|
|
161
159
|
templates: Vec::new(),
|
|
162
160
|
selected_index: 0,
|
|
163
161
|
custom_draft: SentinelCurrentConfig::default(),
|
|
@@ -185,14 +183,9 @@ impl SentinelConfigState {
|
|
|
185
183
|
}
|
|
186
184
|
|
|
187
185
|
pub fn cycle_view_mode(&mut self) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
self.sync_custom_draft_from_current();
|
|
192
|
-
SentinelViewMode::Custom
|
|
193
|
-
}
|
|
194
|
-
SentinelViewMode::Custom => SentinelViewMode::About,
|
|
195
|
-
};
|
|
186
|
+
// Only one view now, so cycling does nothing
|
|
187
|
+
self.view_mode = SentinelViewMode::Custom;
|
|
188
|
+
self.sync_custom_draft_from_current();
|
|
196
189
|
self.error = None;
|
|
197
190
|
self.status_message = None;
|
|
198
191
|
}
|
|
@@ -210,42 +203,22 @@ impl SentinelConfigState {
|
|
|
210
203
|
}
|
|
211
204
|
|
|
212
205
|
pub fn select_previous(&mut self) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
SentinelViewMode::Custom => {
|
|
221
|
-
let idx = CustomField::ALL
|
|
222
|
-
.iter()
|
|
223
|
-
.position(|f| *f == self.custom_field)
|
|
224
|
-
.unwrap_or(0);
|
|
225
|
-
if idx > 0 {
|
|
226
|
-
self.custom_field = CustomField::ALL[idx - 1];
|
|
227
|
-
}
|
|
228
|
-
}
|
|
206
|
+
let idx = CustomField::ALL
|
|
207
|
+
.iter()
|
|
208
|
+
.position(|f| *f == self.custom_field)
|
|
209
|
+
.unwrap_or(0);
|
|
210
|
+
if idx > 0 {
|
|
211
|
+
self.custom_field = CustomField::ALL[idx - 1];
|
|
229
212
|
}
|
|
230
213
|
}
|
|
231
214
|
|
|
232
215
|
pub fn select_next(&mut self) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
SentinelViewMode::Custom => {
|
|
241
|
-
let idx = CustomField::ALL
|
|
242
|
-
.iter()
|
|
243
|
-
.position(|f| *f == self.custom_field)
|
|
244
|
-
.unwrap_or(0);
|
|
245
|
-
if idx + 1 < CustomField::ALL.len() {
|
|
246
|
-
self.custom_field = CustomField::ALL[idx + 1];
|
|
247
|
-
}
|
|
248
|
-
}
|
|
216
|
+
let idx = CustomField::ALL
|
|
217
|
+
.iter()
|
|
218
|
+
.position(|f| *f == self.custom_field)
|
|
219
|
+
.unwrap_or(0);
|
|
220
|
+
if idx + 1 < CustomField::ALL.len() {
|
|
221
|
+
self.custom_field = CustomField::ALL[idx + 1];
|
|
249
222
|
}
|
|
250
223
|
}
|
|
251
224
|
|
|
@@ -510,11 +483,6 @@ pub fn render(f: &mut Frame, state: &AppState) {
|
|
|
510
483
|
])
|
|
511
484
|
.split(area);
|
|
512
485
|
|
|
513
|
-
let mode_label = match sc.view_mode {
|
|
514
|
-
SentinelViewMode::About => "About",
|
|
515
|
-
SentinelViewMode::Templates => "Templates",
|
|
516
|
-
SentinelViewMode::Custom => "Custom",
|
|
517
|
-
};
|
|
518
486
|
let health_txt = if sc.health_status.is_empty() {
|
|
519
487
|
"—".to_string()
|
|
520
488
|
} else {
|
|
@@ -522,10 +490,7 @@ pub fn render(f: &mut Frame, state: &AppState) {
|
|
|
522
490
|
};
|
|
523
491
|
|
|
524
492
|
let header = Block::default()
|
|
525
|
-
.title(format!(
|
|
526
|
-
" 🛡️ Sentinel — {} · {} ",
|
|
527
|
-
mode_label, health_txt
|
|
528
|
-
))
|
|
493
|
+
.title(format!(" 🛡️ Sentinel — {} ", health_txt))
|
|
529
494
|
.borders(Borders::ALL)
|
|
530
495
|
.border_style(Style::default().fg(BRAND_PURPLE))
|
|
531
496
|
.style(Style::default().bg(BG_PANEL));
|
|
@@ -536,31 +501,12 @@ pub fn render(f: &mut Frame, state: &AppState) {
|
|
|
536
501
|
.constraints([Constraint::Percentage(45), Constraint::Percentage(55)])
|
|
537
502
|
.split(chunks[1]);
|
|
538
503
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
}
|
|
543
|
-
SentinelViewMode::Templates => {
|
|
544
|
-
render_active_panel(f, body[0], sc);
|
|
545
|
-
render_templates_panel(f, body[1], sc);
|
|
546
|
-
}
|
|
547
|
-
SentinelViewMode::Custom => {
|
|
548
|
-
render_custom_editor(f, body[0], sc);
|
|
549
|
-
render_custom_help(f, body[1], sc);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
504
|
+
// Unified view: Custom editor on left, About + Help on right
|
|
505
|
+
render_custom_editor(f, body[0], sc);
|
|
506
|
+
render_custom_help_with_about(f, body[1], sc);
|
|
552
507
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
"Tab: Templates · R: reload from Gateway · ESC: Close"
|
|
556
|
-
}
|
|
557
|
-
SentinelViewMode::Templates => {
|
|
558
|
-
"Tab: next view · ↑/↓ Template · Enter Apply · R Refresh · ESC Close"
|
|
559
|
-
}
|
|
560
|
-
SentinelViewMode::Custom => {
|
|
561
|
-
"Tab: next view · ↑/↓ Field · ←/→ Value · Enter Apply (saves to disk) · ESC Close"
|
|
562
|
-
}
|
|
563
|
-
};
|
|
508
|
+
const DEFAULT_FOOTER: &str =
|
|
509
|
+
"↑/↓ Field · ←/→ Value · Enter Apply (saves to disk) · R Refresh · ESC Close";
|
|
564
510
|
const CUSTOM_SHORTCUTS: &str =
|
|
565
511
|
"←/→ or +/- adjust value · wheel · Space ON/OFF · Enter apply · ESC close";
|
|
566
512
|
|
|
@@ -571,13 +517,9 @@ pub fn render(f: &mut Frame, state: &AppState) {
|
|
|
571
517
|
} else if let Some(e) = &sc.error {
|
|
572
518
|
e.clone()
|
|
573
519
|
} else if let Some(m) = &sc.status_message {
|
|
574
|
-
|
|
575
|
-
format!("{}\n{}", m, CUSTOM_SHORTCUTS)
|
|
576
|
-
} else {
|
|
577
|
-
m.clone()
|
|
578
|
-
}
|
|
520
|
+
format!("{}\n{}", m, CUSTOM_SHORTCUTS)
|
|
579
521
|
} else {
|
|
580
|
-
|
|
522
|
+
DEFAULT_FOOTER.to_string()
|
|
581
523
|
};
|
|
582
524
|
|
|
583
525
|
let footer = Block::default()
|
|
@@ -786,50 +728,81 @@ fn render_custom_editor(f: &mut Frame, area: Rect, sc: &SentinelConfigState) {
|
|
|
786
728
|
f.render_widget(List::new(items), inner);
|
|
787
729
|
}
|
|
788
730
|
|
|
789
|
-
fn
|
|
790
|
-
|
|
791
|
-
|
|
731
|
+
fn render_custom_help_with_about(f: &mut Frame, area: Rect, sc: &SentinelConfigState) {
|
|
732
|
+
use ratatui::layout::{Constraint, Direction, Layout};
|
|
733
|
+
|
|
734
|
+
// Split into About section (top) and field help (bottom)
|
|
735
|
+
let chunks = Layout::default()
|
|
736
|
+
.direction(Direction::Vertical)
|
|
737
|
+
.constraints([Constraint::Length(15), Constraint::Min(10)])
|
|
738
|
+
.split(area);
|
|
739
|
+
|
|
740
|
+
// Top: About Sentinel
|
|
741
|
+
let about_block = Block::default()
|
|
742
|
+
.title(" 📖 What is Sentinel? ")
|
|
792
743
|
.borders(Borders::ALL)
|
|
793
|
-
.border_style(Style::default().fg(
|
|
744
|
+
.border_style(Style::default().fg(CYBER_CYAN))
|
|
794
745
|
.style(Style::default().bg(BG_PANEL));
|
|
795
|
-
let
|
|
796
|
-
f.render_widget(
|
|
746
|
+
let about_inner = about_block.inner(chunks[0]);
|
|
747
|
+
f.render_widget(about_block, chunks[0]);
|
|
748
|
+
|
|
749
|
+
let mut about_lines: Vec<Line> = Vec::new();
|
|
750
|
+
for line in ABOUT_SENTINEL_LINES {
|
|
751
|
+
if line.is_empty() {
|
|
752
|
+
about_lines.push(Line::from(""));
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
let style = if line.starts_with(" •") {
|
|
756
|
+
Style::default().fg(TEXT_PRIMARY)
|
|
757
|
+
} else if line.ends_with(':') && !line.starts_with(' ') {
|
|
758
|
+
Style::default().fg(NEON_GREEN).bold()
|
|
759
|
+
} else {
|
|
760
|
+
Style::default().fg(TEXT_DIM)
|
|
761
|
+
};
|
|
762
|
+
about_lines.push(Line::from(Span::styled(*line, style)));
|
|
763
|
+
}
|
|
764
|
+
f.render_widget(
|
|
765
|
+
Paragraph::new(about_lines).wrap(Wrap { trim: false }),
|
|
766
|
+
about_inner,
|
|
767
|
+
);
|
|
797
768
|
|
|
769
|
+
// Bottom: Field-specific help
|
|
798
770
|
let field = sc.custom_field;
|
|
799
|
-
let
|
|
771
|
+
let help_block = Block::default()
|
|
772
|
+
.title(format!(" ℹ️ {} ", field.label()))
|
|
773
|
+
.borders(Borders::ALL)
|
|
774
|
+
.border_style(Style::default().fg(BRAND_PURPLE))
|
|
775
|
+
.style(Style::default().bg(BG_PANEL));
|
|
776
|
+
let help_inner = help_block.inner(chunks[1]);
|
|
777
|
+
f.render_widget(help_block, chunks[1]);
|
|
778
|
+
|
|
779
|
+
let mut help_lines = vec![
|
|
800
780
|
Line::from(""),
|
|
801
781
|
Line::from(field.help()).style(Style::default().fg(TEXT_PRIMARY)),
|
|
802
782
|
Line::from(""),
|
|
803
783
|
];
|
|
804
784
|
if let Some(reason) = field.kill_reason() {
|
|
805
|
-
|
|
785
|
+
help_lines.push(Line::from(vec![
|
|
806
786
|
Span::styled("Kill log reason: ", Style::default().fg(TEXT_DIM)),
|
|
807
787
|
Span::styled(reason, Style::default().fg(AMBER_WARN)),
|
|
808
788
|
]));
|
|
809
|
-
|
|
789
|
+
help_lines.push(Line::from(""));
|
|
810
790
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
lines.push(
|
|
815
|
-
Line::from("Enter Apply → Gateway + disk (%APPDATA%\\4runr\\config\\sentinel-limits.json).")
|
|
816
|
-
.style(Style::default().fg(NEON_GREEN)),
|
|
817
|
-
);
|
|
818
|
-
lines.push(
|
|
819
|
-
Line::from("Survives Gateway restart. Env vars in docker-compose override fields when set.")
|
|
820
|
-
.style(Style::default().fg(TEXT_MUTED)),
|
|
791
|
+
help_lines.push(
|
|
792
|
+
Line::from("Persistent via env (Gateway boot):")
|
|
793
|
+
.style(Style::default().fg(NEON_GREEN).bold()),
|
|
821
794
|
);
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
lines.push(Line::from(""));
|
|
827
|
-
lines.push(
|
|
828
|
-
Line::from("Adjust draft: ←/→ or +/- · mouse wheel · Space toggles Enforcement.")
|
|
795
|
+
help_lines.push(Line::from(field.env_var()).style(Style::default().fg(CYBER_CYAN)));
|
|
796
|
+
help_lines.push(Line::from(""));
|
|
797
|
+
help_lines.push(
|
|
798
|
+
Line::from("Adjust: ←/→ or +/- · mouse wheel · Space toggles Enforcement.")
|
|
829
799
|
.style(Style::default().fg(NEON_GREEN)),
|
|
830
800
|
);
|
|
831
801
|
|
|
832
|
-
f.render_widget(
|
|
802
|
+
f.render_widget(
|
|
803
|
+
Paragraph::new(help_lines).wrap(Wrap { trim: false }),
|
|
804
|
+
help_inner,
|
|
805
|
+
);
|
|
833
806
|
}
|
|
834
807
|
|
|
835
808
|
#[cfg(test)]
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "4runr-os",
|
|
3
|
-
"version": "2.10.
|
|
3
|
+
"version": "2.10.66",
|
|
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.66: Sentinel unified Custom+About view (Templates removed); enhanced persistence logging.",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"bin": {
|
|
8
8
|
"4runr": "dist/index.js",
|