diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c4f688..a24846f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,10 +8,6 @@ on: tag: description: "Existing tag to (re-)release, e.g. v0.1.0" required: true - publish_crates: - description: "Publish to crates.io (requires CARGO_REGISTRY_TOKEN). Leave false for a dry run." - type: boolean - default: false permissions: contents: read @@ -146,59 +142,3 @@ jobs: generate_release_notes: ${{ steps.notes.outputs.generated == 'true' }} prerelease: ${{ contains(github.event.inputs.tag || github.ref_name, '-') }} files: artifacts/* - - publish: - name: crates.io - runs-on: ubuntu-latest - needs: release - # Publishing is opt-in and manual only. A tag push builds binaries and cuts - # the GitHub Release; re-run this workflow via workflow_dispatch to dry-run - # or (with publish_crates=true) actually publish to crates.io. - if: github.event_name == 'workflow_dispatch' - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.event.inputs.tag || github.ref }} - - - name: Install Rust stable - uses: dtolnay/rust-toolchain@stable - - - uses: Swatinem/rust-cache@v2 - - - name: Publish workspace crates in dependency order - shell: bash - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - DO_PUBLISH: ${{ github.event.inputs.publish_crates == 'true' }} - run: | - set -euo pipefail - # Order matters: each crate must already be on the registry before a - # dependent crate is published. - CRATES=( - sentinel-core - sentinel-exec - sentinel-policy - sentinel-capabilities - sentinel-audit - sentinel-agent-llm - sentinel-fleet - sentinel-tui - ) - if [ "$DO_PUBLISH" != "true" ]; then - echo "::notice::Dry run — re-run this workflow with publish_crates=true to publish." - for c in "${CRATES[@]}"; do - cargo publish --dry-run --locked -p "$c" --allow-dirty - done - exit 0 - fi - if [ -z "${CARGO_REGISTRY_TOKEN:-}" ]; then - echo "::error::CARGO_REGISTRY_TOKEN is not set." - exit 1 - fi - for c in "${CRATES[@]}"; do - echo "::group::publish $c" - cargo publish --locked -p "$c" - # Give the index time to propagate before the next crate resolves it. - sleep 45 - echo "::endgroup::" - done diff --git a/CHANGELOG.md b/CHANGELOG.md index b65794e..259d010 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,16 +13,30 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Tag-driven release automation: verification (fmt, clippy, tests, `cargo audit`), four-target binary builds (`x86_64-unknown-linux-gnu`, `x86_64-unknown-linux-musl`, `aarch64-apple-darwin`, `x86_64-apple-darwin`) - with SHA-256 sums, GitHub Release creation from the CHANGELOG section, and an - opt-in crates.io publish that walks the workspace in dependency order + with SHA-256 sums, and GitHub Release creation from the CHANGELOG section +- `rust-version = "1.86"` on `workspace.package`, so cargo reports the real + minimum supported Rust version instead of failing deep inside a dependency ### Changed - License declaration reconciled to **MIT**, matching `LICENSE` and the README badge — `workspace.package.license` previously declared Apache-2.0 - Workspace-internal dependencies now carry an explicit `version` alongside - `path`; without it `cargo publish` rejects every crate in the workspace + `path` (required of any crate that is ever published) +- Removed the crates.io badge and publish step. `sentinel-agent` — the crate the + badge advertised — has never existed, and `sentinel-core` and `sentinel-tui` are + registered to other authors, so the workspace cannot be published under these + names. Releases ship binaries and container images only +- Documented minimum Rust version corrected from 1.75 to **1.86** in the README + and CONTRIBUTING.md ### Fixed +- `docker build` could not succeed: the builder image was `rust:1.82-slim`, but + `ratatui 0.30` requires Rust 1.86 and `clap 4.6` requires 1.85, so cargo refused + the workspace before compiling anything. Builder bumped to `rust:1.86-slim` +- Terminal output and log messages in `sentinel-tui` printed mojibake: box-drawing + rules, em dashes, arrows and ellipses had been committed as double-encoded UTF-8 + (`â` sequences), so `sentinel run` rendered `â──â──` instead of `──`. 3,096 + sequences repaired across the four TUI sources and the Dockerfile - `cargo clippy --workspace --all-targets -- -D warnings` — the exact command the CI lint step runs — failed on current stable with eight `collapsible_match` errors in `sentinel-tui`. The TUI key handler now uses match guards. Behaviour diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a415ab3..341a27b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -14,7 +14,8 @@ explicit security review in the PR description — see below. Requirements: -- Rust **1.75** or newer (stable) +- Rust **1.86** or newer (stable) — set as `rust-version` in the workspace manifest; + `ratatui 0.30` and `clap 4.6` are what put the floor there - `cargo clippy`, `cargo fmt` (`rustup component add clippy rustfmt`) - `cargo audit` (`cargo install cargo-audit`) for dependency checks - Docker, only if you are changing the image diff --git a/Cargo.toml b/Cargo.toml index 11170c0..61f4c17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ resolver = "2" [workspace.package] version = "0.1.0" edition = "2021" +rust-version = "1.86" license = "MIT" repository = "https://github.com/marcuspat/Sentinel" authors = ["Sentinel Contributors"] diff --git a/Dockerfile b/Dockerfile index a9846e8..54ef3b2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -# ── Stage 1: build ────────────────────────────────────────────────────────── -FROM rust:1.82-slim AS builder +# ── Stage 1: build ────────────────────────────────────────────────────────── +FROM rust:1.86-slim AS builder RUN apt-get update && apt-get install -y --no-install-recommends \ pkg-config \ @@ -12,7 +12,7 @@ COPY . . # Build the release binary in one layer so CI cache is maximally effective. RUN cargo build --release --bin sentinel -# ── Stage 2: runtime ───────────────────────────────────────────────────────── +# ── Stage 2: runtime ───────────────────────────────────────────────────────── FROM debian:bookworm-slim AS runtime RUN apt-get update && apt-get install -y --no-install-recommends \ @@ -22,7 +22,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ COPY --from=builder /app/target/release/sentinel /usr/local/bin/sentinel -# Operators override these at runtime — never bake keys into the image. +# Operators override these at runtime — never bake keys into the image. ENV ANTHROPIC_API_KEY="" \ OPENAI_API_KEY="" \ RUST_LOG="info" diff --git a/README.md b/README.md index 9e8c3d1..9c9ed51 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,5 @@ # Sentinel -[![Crates.io](https://img.shields.io/crates/v/sentinel-agent.svg)](https://crates.io/crates/sentinel-agent) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Build](https://github.com/marcuspat/Sentinel/actions/workflows/ci.yml/badge.svg)](https://github.com/marcuspat/Sentinel/actions) @@ -50,7 +49,7 @@ sentinel --backend ollama --model llama3 run "Check CPU load" ## Build ```bash -# Requires Rust 1.75+ +# Requires Rust 1.86+ (ratatui 0.30 and clap 4.6 set the floor) cargo build --release # Run all tests diff --git a/sentinel-tui/src/agent_bridge.rs b/sentinel-tui/src/agent_bridge.rs index 46b78b8..6877a8a 100644 --- a/sentinel-tui/src/agent_bridge.rs +++ b/sentinel-tui/src/agent_bridge.rs @@ -1,6 +1,6 @@ //! Bridge between the TUI event loop and the [`ReasoningLoop`]. //! -//! [`run_agent_session`] drives the full investigate → plan → approve → act +//! [`run_agent_session`] drives the full investigate → plan → approve → act //! lifecycle in a background tokio task, emitting [`SessionUpdate`]s into the //! TUI's mpsc channel and surfacing a plan-gate [`ApprovalRequest`] for //! operator sign-off before execution begins. @@ -28,7 +28,7 @@ use crate::app::{ StepStatus, }; -// ── AgentConfig ─────────────────────────────────────────────────────────────── +// ── AgentConfig ─────────────────────────────────────────────────────────────── /// Configuration for a single agent session, passed to [`run_agent_session`]. /// @@ -52,7 +52,7 @@ pub struct AgentConfig { pub model: String, } -// ── Public entry point ──────────────────────────────────────────────────────── +// ── Public entry point ──────────────────────────────────────────────────────── /// Drive a full agent session in the background, emitting [`SessionUpdate`]s. /// @@ -70,7 +70,7 @@ pub async fn run_agent_session( } } -// ── Session driver ──────────────────────────────────────────────────────────── +// ── Session driver ──────────────────────────────────────────────────────────── async fn run_inner( config: AgentConfig, @@ -79,7 +79,7 @@ async fn run_inner( ) -> Result<()> { let session_id = Uuid::new_v4(); - // ── 1. Build the LLM backend ────────────────────────────────────────────── + // ── 1. Build the LLM backend ────────────────────────────────────────────── let backend: Box = match config.backend_name.as_str() { "anthropic" => { let key = config @@ -96,7 +96,7 @@ async fn run_inner( other => return Err(anyhow::anyhow!("unknown backend '{other}'")), }; - // ── 2. Assemble capabilities, registry, policy, and audit log ───────────── + // ── 2. Assemble capabilities, registry, policy, and audit log ───────────── let executor = Arc::new(RealCommandExecutor); let caps = all_capabilities(executor); @@ -119,7 +119,7 @@ async fn run_inner( ) .with_capabilities(caps); - // ── 3. Investigate ──────────────────────────────────────────────────────── + // ── 3. Investigate ──────────────────────────────────────────────────────── emit( update_tx, SessionUpdate::PhaseChanged(SessionPhase::Investigating), @@ -141,13 +141,13 @@ async fn run_inner( update_tx, LogLevel::Info, format!( - "Investigation complete — {} observation(s) collected.", + "Investigation complete — {} observation(s) collected.", observations.len() ), ) .await; - // ── 4. Plan ─────────────────────────────────────────────────────────────── + // ── 4. Plan ─────────────────────────────────────────────────────────────── emit( update_tx, SessionUpdate::PhaseChanged(SessionPhase::Planning), @@ -163,25 +163,25 @@ async fn run_inner( plan_id = %core_plan.id, steps = core_plan.steps.len(), overall_risk = ?core_plan.overall_risk, - "plan ready — sending to TUI" + "plan ready — sending to TUI" ); let app_plan = core_plan_to_app(&core_plan); emit(update_tx, SessionUpdate::PlanProposed(app_plan)).await; - // ── 5. Dry-run short-circuit ────────────────────────────────────────────── + // ── 5. Dry-run short-circuit ────────────────────────────────────────────── if config.dry_run { log_entry( update_tx, LogLevel::Info, - "Dry-run mode — plan generated but NOT executed.".to_string(), + "Dry-run mode — plan generated but NOT executed.".to_string(), ) .await; emit(update_tx, SessionUpdate::SessionCompleted).await; return Ok(()); } - // ── 6. Operator approval gate ───────────────────────────────────────────── + // ── 6. Operator approval gate ───────────────────────────────────────────── // // Surface a single plan-gate ApprovalRequest so the TUI's y/n modal handles // it uniformly. The gate step represents "approve the full execution plan". @@ -230,7 +230,7 @@ async fn run_inner( } } - // ── 7. Execute ──────────────────────────────────────────────────────────── + // ── 7. Execute ──────────────────────────────────────────────────────────── emit( update_tx, SessionUpdate::PhaseChanged(SessionPhase::Executing), @@ -240,7 +240,7 @@ async fn run_inner( update_tx, LogLevel::Info, format!( - "Executing {} step(s) on {}…", + "Executing {} step(s) on {}…", core_plan.steps.len(), config.host ), @@ -274,7 +274,7 @@ async fn run_inner( update_tx, LogLevel::Info, format!( - "Execution complete — {} succeeded, {} failed, {} rolled back in {}ms.", + "Execution complete — {} succeeded, {} failed, {} rolled back in {}ms.", summary.steps_completed, summary.steps_failed, summary.steps_rolled_back, @@ -287,12 +287,12 @@ async fn run_inner( Ok(()) } -// ── Helpers ─────────────────────────────────────────────────────────────────── +// ── Helpers ─────────────────────────────────────────────────────────────────── /// Send a [`SessionUpdate`], ignoring send errors (TUI may have exited). async fn emit(tx: &mpsc::Sender, update: SessionUpdate) { if tx.send(update).await.is_err() { - error!("TUI update channel closed — dropping update"); + error!("TUI update channel closed — dropping update"); } } diff --git a/sentinel-tui/src/app.rs b/sentinel-tui/src/app.rs index 1fe99e3..6733640 100644 --- a/sentinel-tui/src/app.rs +++ b/sentinel-tui/src/app.rs @@ -4,7 +4,7 @@ use uuid::Uuid; use sentinel_core::{RiskTier, SessionPhase}; -// ── Local plan / session types ──────────────────────────────────────────────── +// ── Local plan / session types ──────────────────────────────────────────────── // These mirror what the agent-llm crate will eventually expose. They are // defined here so the TUI can compile independently while that crate is a // placeholder. @@ -99,14 +99,14 @@ pub enum ApprovalDecision { Reject { reason: String }, } -// ── Interactive per-step approval ───────────────────────────────────────────── +// ── Interactive per-step approval ───────────────────────────────────────────── /// The operator's answer to a blocking per-step approval prompt. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ApprovalOutcome { - /// Approve this step — the agent may proceed. + /// Approve this step — the agent may proceed. Approve, - /// Abort — the agent must not run this step (and should stop the plan). + /// Abort — the agent must not run this step (and should stop the plan). Abort, } @@ -127,7 +127,7 @@ pub struct ApprovalRequest { /// High-level interaction state of the TUI. #[derive(Debug)] pub enum AppState { - /// Normal browsing/editing — tabs and inputs are active. + /// Normal browsing/editing — tabs and inputs are active. Normal, /// A modal is blocking input, awaiting approval of the contained step. ApprovingPlan(PlanStep), @@ -197,7 +197,7 @@ impl Session { } } -// ── SessionUpdate ───────────────────────────────────────────────────────────── +// ── SessionUpdate ───────────────────────────────────────────────────────────── /// An update pushed from the background agent task into the TUI event loop. #[derive(Debug, Clone)] @@ -213,7 +213,7 @@ pub enum SessionUpdate { Error(String), } -// ── Plan view ───────────────────────────────────────────────────────────────── +// ── Plan view ───────────────────────────────────────────────────────────────── /// Per-step display state in the Plan tab. #[derive(Debug, Clone)] @@ -285,7 +285,7 @@ impl PlanView { } } -// ── Tab enum ────────────────────────────────────────────────────────────────── +// ── Tab enum ────────────────────────────────────────────────────────────────── /// Top-level navigation tabs. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -332,7 +332,7 @@ impl Tab { } } -// ── App ─────────────────────────────────────────────────────────────────────── +// ── App ─────────────────────────────────────────────────────────────────────── /// Top-level TUI application state. pub struct App { @@ -352,10 +352,10 @@ pub struct App { pub status_message: Option, /// Cursor position within the goal input field. pub input_cursor: usize, - /// Current interaction state — drives modal/blocking input handling. + /// Current interaction state — drives modal/blocking input handling. pub state: AppState, - // ── New fields for live agent integration ───────────────────────────────── + // ── New fields for live agent integration ───────────────────────────────── /// Goal that was just submitted; `run_app()` drains this each tick to /// spawn the background agent task. @@ -365,7 +365,7 @@ pub struct App { /// If `true`, sessions run in plan-only (dry-run) mode. pub dry_run: bool, - // ── Private channels ────────────────────────────────────────────────────── + // ── Private channels ────────────────────────────────────────────────────── /// Channel responder for the in-flight approval modal, if any. approval_responder: Option>, @@ -404,7 +404,7 @@ impl App { } } - // ── Session update channel ──────────────────────────────────────────────── + // ── Session update channel ──────────────────────────────────────────────── /// Attach the channel on which the background agent emits [`SessionUpdate`]s. pub fn set_session_update_channel(&mut self, rx: mpsc::Receiver) { @@ -430,7 +430,7 @@ impl App { } } - // ── Interactive approval ────────────────────────────────────────────────── + // ── Interactive approval ────────────────────────────────────────────────── /// Attach the channel on which the agent will send approval requests. pub fn set_approval_channel(&mut self, rx: mpsc::Receiver) { @@ -510,7 +510,7 @@ impl App { self.state = AppState::Normal; } - // ── Navigation ──────────────────────────────────────────────────────────── + // ── Navigation ──────────────────────────────────────────────────────────── pub fn next_tab(&mut self) { self.current_tab = self.current_tab.next(); @@ -520,7 +520,7 @@ impl App { self.current_tab = self.current_tab.prev(); } - // ── Log scrolling ───────────────────────────────────────────────────────── + // ── Log scrolling ───────────────────────────────────────────────────────── pub fn scroll_log_down(&mut self) { self.log_scroll = self.log_scroll.saturating_add(1); @@ -530,7 +530,7 @@ impl App { self.log_scroll = self.log_scroll.saturating_sub(1); } - // ── Plan approval ───────────────────────────────────────────────────────── + // ── Plan approval ───────────────────────────────────────────────────────── /// Approve every step in the current plan at once. pub fn approve_all(&mut self) { @@ -561,7 +561,7 @@ impl App { self.status_message = Some(format!("Plan rejected: {}", reason)); } - // ── Goal / session helpers ──────────────────────────────────────────────── + // ── Goal / session helpers ──────────────────────────────────────────────── pub fn set_status(&mut self, msg: impl Into) { self.status_message = Some(msg.into()); @@ -584,10 +584,10 @@ impl App { // Signal run_app() to spawn the agent task on the next tick. self.pending_goal = Some(goal.clone()); let mut session = Session::new(goal.clone(), host, dry_run); - session.log(LogLevel::Info, format!("Session started — goal: {}", goal)); + session.log(LogLevel::Info, format!("Session started — goal: {}", goal)); self.session = Some(session); self.current_tab = Tab::Investigation; - self.status_message = Some("Session started. Connecting to agent…".into()); + self.status_message = Some("Session started. Connecting to agent…".into()); } /// Apply a `SessionUpdate` received from the background agent. @@ -606,7 +606,7 @@ impl App { SessionUpdate::PlanProposed(plan) => { self.plan_view.load_plan(&plan); if let Some(s) = &mut self.session { - s.log(LogLevel::Info, "Plan proposed — review required."); + s.log(LogLevel::Info, "Plan proposed — review required."); s.current_plan = Some(plan); } self.current_tab = Tab::Plan; @@ -615,7 +615,7 @@ impl App { } SessionUpdate::PlanApproved => { if let Some(s) = &mut self.session { - s.log(LogLevel::Info, "Plan approved — beginning execution."); + s.log(LogLevel::Info, "Plan approved — beginning execution."); } self.current_tab = Tab::Execution; } @@ -672,7 +672,7 @@ impl App { } } - // ── Plan view delegation ────────────────────────────────────────────────── + // ── Plan view delegation ────────────────────────────────────────────────── pub fn plan_scroll_down(&mut self) { self.plan_view.move_down(); @@ -683,12 +683,12 @@ impl App { } } -// ───────────────────────────────────────────────────────────────────────────── +// ───────────────────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; - // ── Tab navigation ──────────────────────────────────────────────────────── + // ── Tab navigation ──────────────────────────────────────────────────────── #[test] fn new_app_starts_on_goal_tab() { @@ -742,7 +742,7 @@ mod tests { assert_eq!(titles.len(), unique.len()); } - // ── Log scroll ──────────────────────────────────────────────────────────── + // ── Log scroll ──────────────────────────────────────────────────────────── #[test] fn scroll_down_and_up() { @@ -760,7 +760,7 @@ mod tests { assert_eq!(app.log_scroll, 0); } - // ── Plan approval ───────────────────────────────────────────────────────── + // ── Plan approval ───────────────────────────────────────────────────────── fn make_plan() -> Plan { Plan::new( @@ -812,7 +812,7 @@ mod tests { )); } - // ── Session ─────────────────────────────────────────────────────────────── + // ── Session ─────────────────────────────────────────────────────────────── #[test] fn start_session_without_goal_shows_message() { @@ -859,7 +859,7 @@ mod tests { ); } - // ── poll_session_updates ────────────────────────────────────────────────── + // ── poll_session_updates ────────────────────────────────────────────────── #[tokio::test] async fn poll_session_updates_applies_updates() { @@ -876,7 +876,7 @@ mod tests { assert_eq!(app.current_tab, Tab::Audit); } - // ── Status message ──────────────────────────────────────────────────────── + // ── Status message ──────────────────────────────────────────────────────── #[test] fn set_and_clear_status() { @@ -887,7 +887,7 @@ mod tests { assert!(app.status_message.is_none()); } - // ── PlanView ────────────────────────────────────────────────────────────── + // ── PlanView ────────────────────────────────────────────────────────────── #[test] fn plan_view_move_down_clamps_at_end() { @@ -910,7 +910,7 @@ mod tests { assert!(!pv.steps[0].expanded); } - // ── Interactive approval ────────────────────────────────────────────────── + // ── Interactive approval ────────────────────────────────────────────────── fn approval_step() -> PlanStep { PlanStep::new( diff --git a/sentinel-tui/src/event_handler.rs b/sentinel-tui/src/event_handler.rs index 3529194..cfaf3bf 100644 --- a/sentinel-tui/src/event_handler.rs +++ b/sentinel-tui/src/event_handler.rs @@ -33,7 +33,7 @@ pub async fn handle_events( match event { AppEvent::Key(key) => handle_key(app, key), AppEvent::Tick => { - // Periodic tick — nothing to do beyond the approval poll above. + // Periodic tick — nothing to do beyond the approval poll above. } AppEvent::SessionUpdate(update) => { app.apply_session_update(update); @@ -76,7 +76,7 @@ fn handle_key(app: &mut App, key: KeyEvent) { } match key.code { - // ── Global quit ─────────────────────────────────────────────────── + // ── Global quit ─────────────────────────────────────────────────── KeyCode::Char('q') | KeyCode::Esc => { // On the Goal tab, Esc clears the input; on other tabs it quits. if app.current_tab == Tab::Goal && key.code == KeyCode::Esc { @@ -87,11 +87,11 @@ fn handle_key(app: &mut App, key: KeyEvent) { } } - // ── Tab navigation ──────────────────────────────────────────────── + // ── Tab navigation ──────────────────────────────────────────────── KeyCode::Tab => app.next_tab(), KeyCode::BackTab => app.prev_tab(), - // ── Scrolling ───────────────────────────────────────────────────── + // ── Scrolling ───────────────────────────────────────────────────── KeyCode::Down | KeyCode::Char('j') => { match app.current_tab { Tab::Plan => app.plan_scroll_down(), @@ -105,7 +105,7 @@ fn handle_key(app: &mut App, key: KeyEvent) { } } - // ── Plan approval actions ───────────────────────────────────────── + // ── Plan approval actions ───────────────────────────────────────── KeyCode::Char('a') if app.current_tab == Tab::Plan => { app.approve_all(); } @@ -122,7 +122,7 @@ fn handle_key(app: &mut App, key: KeyEvent) { // change behaviour: 'a', 's' and 'r' would start inserting characters. KeyCode::Char('a') | KeyCode::Char('s') | KeyCode::Char('r') => {} - // ── Goal input (only on Goal tab) ───────────────────────────────── + // ── Goal input (only on Goal tab) ───────────────────────────────── KeyCode::Enter if app.current_tab == Tab::Goal => { // Use the host and dry_run stored in app state (set from CLI args // in run_tui) rather than hardcoded defaults. @@ -173,7 +173,7 @@ mod tests { } } - // ── Quit ────────────────────────────────────────────────────────────────── + // ── Quit ────────────────────────────────────────────────────────────────── #[test] fn q_sets_should_quit_on_non_goal_tab() { @@ -193,7 +193,7 @@ mod tests { assert!(app.should_quit); } - // ── Tab navigation ──────────────────────────────────────────────────────── + // ── Tab navigation ──────────────────────────────────────────────────────── #[test] fn tab_key_advances_tab() { @@ -211,7 +211,7 @@ mod tests { assert_eq!(app.current_tab, Tab::Investigation); } - // ── Goal input ──────────────────────────────────────────────────────────── + // ── Goal input ──────────────────────────────────────────────────────────── #[test] fn char_keys_append_to_goal_on_goal_tab() { @@ -252,7 +252,7 @@ mod tests { assert_eq!(session.host, "prod-web-01"); } - // ── Plan approval ───────────────────────────────────────────────────────── + // ── Plan approval ───────────────────────────────────────────────────────── #[test] fn a_key_approves_all_on_plan_tab() { @@ -286,7 +286,7 @@ mod tests { )); } - // ── Scroll ──────────────────────────────────────────────────────────────── + // ── Scroll ──────────────────────────────────────────────────────────────── #[test] fn j_key_scrolls_down_on_investigation_tab() { @@ -305,7 +305,7 @@ mod tests { assert_eq!(app.log_scroll, 2); } - // ── Interactive approval modal ──────────────────────────────────────────── + // ── Interactive approval modal ──────────────────────────────────────────── fn approving_app() -> (App, tokio::sync::oneshot::Receiver) { use crate::app::PlanStep; @@ -356,7 +356,7 @@ mod tests { assert!(app.is_approving()); } - // ── Async handle_events ─────────────────────────────────────────────────── + // ── Async handle_events ─────────────────────────────────────────────────── #[tokio::test] async fn handle_tick_is_noop() { diff --git a/sentinel-tui/src/main.rs b/sentinel-tui/src/main.rs index 8ce676c..9f8b7c5 100644 --- a/sentinel-tui/src/main.rs +++ b/sentinel-tui/src/main.rs @@ -164,7 +164,7 @@ async fn main() -> Result<()> { Ok(()) } -// ── TUI entry point ─────────────────────────────────────────────────────────── +// ── TUI entry point ─────────────────────────────────────────────────────────── async fn run_tui( host: String, @@ -221,11 +221,11 @@ async fn run_app( model: String, ) -> Result<()> { loop { - // ── Drain live agent updates ────────────────────────────────────── + // ── Drain live agent updates ────────────────────────────────────── app.poll_session_updates(); app.poll_approval(); - // ── Spawn agent task when a new goal arrives ────────────────────── + // ── Spawn agent task when a new goal arrives ────────────────────── if let Some(goal) = app.pending_goal.take() { let (update_tx, update_rx) = mpsc::channel(128); let (approval_tx, approval_rx) = mpsc::channel(4); @@ -245,10 +245,10 @@ async fn run_app( tokio::spawn(run_agent_session(config, update_tx, approval_tx)); } - // ── Render ──────────────────────────────────────────────────────── + // ── Render ──────────────────────────────────────────────────────── terminal.draw(|f| ui::draw(f, app))?; - // ── Input ───────────────────────────────────────────────────────── + // ── Input ───────────────────────────────────────────────────────── if event::poll(Duration::from_millis(50))? { if let Event::Key(key) = event::read()? { handle_events(app, AppEvent::Key(key)).await?; @@ -264,10 +264,10 @@ async fn run_app( Ok(()) } -// ── Subcommand handlers ─────────────────────────────────────────────────────── +// ── Subcommand handlers ─────────────────────────────────────────────────────── -/// Wire the full agent stack — LLM backend, executor, capabilities, registry, -/// policy, audit log — and drive an investigate → plan → approve → act session. +/// Wire the full agent stack — LLM backend, executor, capabilities, registry, +/// policy, audit log — and drive an investigate → plan → approve → act session. #[allow(clippy::too_many_arguments)] async fn run_agent( goal: String, @@ -335,12 +335,12 @@ async fn run_agent( println!(); // Investigate. - println!("── Investigating ──"); + println!("── Investigating ──"); let observations = agent.investigate(session_id, &goal, &host).await?; println!("Collected {} observation(s).", observations.len()); // Plan. - println!("\n── Planning ──"); + println!("\n── Planning ──"); let mut plan = agent.plan(session_id, &goal, &observations).await?; println!("Rationale : {}", plan.rationale); println!("Overall risk : {:?}", plan.overall_risk); @@ -363,7 +363,7 @@ async fn run_agent( // Approve. let approval = if auto_approve { - println!("\nAuto-approve enabled — executing plan."); + println!("\nAuto-approve enabled — executing plan."); ApprovalDecision::FullApproval } else { use std::io::Write as _; @@ -387,7 +387,7 @@ async fn run_agent( } // Act. - println!("\n── Executing ──"); + println!("\n── Executing ──"); let summary = agent .execute_plan(session_id, &host, &mut plan, approval) .await?; @@ -441,18 +441,18 @@ async fn run_fleet( match &results[hostname] { CapabilityResult::Success { output } => { ok += 1; - println!("✔ {hostname}: success"); + println!("✔ {hostname}: success"); if let Ok(pretty) = serde_json::to_string(output) { println!(" {pretty}"); } } CapabilityResult::Failure { error, .. } => { failed += 1; - println!("x {hostname}: FAILED — {error}"); + println!("x {hostname}: FAILED — {error}"); } CapabilityResult::DryRun { predicted_effect } => { ok += 1; - println!("• {hostname}: dry-run"); + println!("• {hostname}: dry-run"); if let Ok(pretty) = serde_json::to_string(predicted_effect) { println!(" {pretty}"); } @@ -487,7 +487,7 @@ fn show_policy() { let rules = evaluator.rules(); println!( - "Default Sentinel policy (deny-by-default) — {} rule(s):", + "Default Sentinel policy (deny-by-default) — {} rule(s):", rules.len() ); println!("{:-<78}", ""); @@ -570,12 +570,12 @@ fn verify_audit(path: &std::path::Path) -> Result<()> { if result.valid { println!( - "Audit log VALID — {} event(s) verified.", + "Audit log VALID — {} event(s) verified.", result.events_checked ); } else { eprintln!( - "Audit log INVALID — chain broken at sequence {}.", + "Audit log INVALID — chain broken at sequence {}.", result.first_broken_at.unwrap_or(0) ); if let Some(err) = &result.error {