Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 0 additions & 60 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
20 changes: 17 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
8 changes: 4 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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 \
Expand All @@ -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 \
Expand All @@ -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"
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down Expand Up @@ -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
Expand Down
36 changes: 18 additions & 18 deletions sentinel-tui/src/agent_bridge.rs
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -28,7 +28,7 @@ use crate::app::{
StepStatus,
};

// ── AgentConfig ───────────────────────────────────────────────────────────────
// ── AgentConfig ───────────────────────────────────────────────────────────────

/// Configuration for a single agent session, passed to [`run_agent_session`].
///
Expand All @@ -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.
///
Expand All @@ -70,7 +70,7 @@ pub async fn run_agent_session(
}
}

// ── Session driver ────────────────────────────────────────────────────────────
// ── Session driver ────────────────────────────────────────────────────────────

async fn run_inner(
config: AgentConfig,
Expand All @@ -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<dyn LlmBackend> = match config.backend_name.as_str() {
"anthropic" => {
let key = config
Expand All @@ -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);

Expand All @@ -119,7 +119,7 @@ async fn run_inner(
)
.with_capabilities(caps);

// ── 3. Investigate ────────────────────────────────────────────────────────
// ── 3. Investigate ────────────────────────────────────────────────────────
emit(
update_tx,
SessionUpdate::PhaseChanged(SessionPhase::Investigating),
Expand All @@ -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),
Expand All @@ -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".
Expand Down Expand Up @@ -230,7 +230,7 @@ async fn run_inner(
}
}

// ── 7. Execute ────────────────────────────────────────────────────────────
// ── 7. Execute ────────────────────────────────────────────────────────────
emit(
update_tx,
SessionUpdate::PhaseChanged(SessionPhase::Executing),
Expand All @@ -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
),
Expand Down Expand Up @@ -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,
Expand All @@ -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<SessionUpdate>, update: SessionUpdate) {
if tx.send(update).await.is_err() {
error!("TUI update channel closed — dropping update");
error!("TUI update channel closed dropping update");
}
}

Expand Down
Loading
Loading