Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: CI

on:
push:
branches: ["main", "feat/**", "fix/**", "chore/**"]
branches: ["main", "feat/**", "fix/**", "chore/**", "docs/**"]
pull_request:
branches: ["main"]

Expand Down
204 changes: 204 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
name: Release

on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
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

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1

jobs:
verify:
name: Verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.tag || github.ref }}

- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
components: clippy

- uses: Swatinem/rust-cache@v2

- name: cargo clippy
run: cargo clippy --workspace --all-targets -- -D warnings

- name: cargo test
run: cargo test --workspace

- name: cargo audit
uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}

binaries:
name: ${{ matrix.target }}
runs-on: ${{ matrix.os }}
needs: verify
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
musl: true
- target: aarch64-apple-darwin
os: macos-latest
- target: x86_64-apple-darwin
os: macos-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.tag || github.ref }}

- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}

- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}

- name: Install musl tooling
if: matrix.musl
run: sudo apt-get update && sudo apt-get install -y musl-tools

- name: Build
run: cargo build --release --locked --target ${{ matrix.target }} --bin sentinel

- name: Package
shell: bash
run: |
set -euo pipefail
mkdir -p dist
cp "target/${{ matrix.target }}/release/sentinel" "dist/sentinel-${{ matrix.target }}"
cd dist
shasum -a 256 "sentinel-${{ matrix.target }}" > "sentinel-${{ matrix.target }}.sha256"

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: sentinel-${{ matrix.target }}
path: dist/

release:
name: GitHub Release
runs-on: ubuntu-latest
needs: binaries
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.tag || github.ref }}

- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
merge-multiple: true

- name: Extract release notes from CHANGELOG
id: notes
shell: bash
run: |
set -euo pipefail
TAG="${{ github.event.inputs.tag || github.ref_name }}"
VERSION="${TAG#v}"
# Pull the section for this version out of CHANGELOG.md.
awk -v ver="$VERSION" '
$0 ~ "^## \\[" ver "\\]" { found=1; next }
found && /^## \[/ { exit }
found { print }
' CHANGELOG.md > release-notes.md
if [ ! -s release-notes.md ]; then
echo "No CHANGELOG.md section for $VERSION; falling back to generated notes." >&2
echo "generated=true" >> "$GITHUB_OUTPUT"
else
echo "generated=false" >> "$GITHUB_OUTPUT"
fi

- name: Create release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ github.event.inputs.tag || github.ref_name }}
name: Sentinel ${{ github.event.inputs.tag || github.ref_name }}
body_path: ${{ steps.notes.outputs.generated == 'false' && 'release-notes.md' || '' }}
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
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,31 @@ All notable changes to this project will be documented in this file.

The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Added
- `CONTRIBUTING.md` covering development setup, workspace layout and bounded
contexts, code style, testing expectations, ADR process, and the
security-sensitive areas that get extra review
- 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

### 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

### Fixed
- `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
is unchanged: `a`, `s` and `r` keep being swallowed off the Plan tab via an
explicit no-op arm rather than falling through to the Goal-tab text input

## [0.1.0] - 2026-05-26

### Added
Expand Down
123 changes: 123 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Contributing to Sentinel

Thanks for your interest. This document covers what you need to get a change
merged.

## Scope

Sentinel executes privileged system administration actions under LLM direction.
Every change is evaluated against one question first: **does this widen what the
agent can do without an operator saying yes?** If it does, it needs an ADR and an
explicit security review in the PR description — see below.

## Development Setup

Requirements:

- Rust **1.75** or newer (stable)
- `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
- `musl-tools` if you are building the static Linux binary

```bash
git clone https://github.com/marcuspat/Sentinel.git
cd Sentinel
cargo check --workspace --all-targets
cargo test --workspace
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all -- --check
```

## Before You Open a PR

```bash
cargo fmt --all
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cargo audit
```

The first three must pass; CI runs the same commands. `cargo audit` findings
should be resolved or explained in the PR.

## Workspace Layout

The workspace is eight crates with a unidirectional dependency graph — no
circular dependencies. Keep it that way.

| Crate | Bounded context |
|---|---|
| `sentinel-core` | Domain types, capability traits, shared errors |
| `sentinel-exec` | Sandboxed command execution (rlimits, timeouts, output caps) |
| `sentinel-policy` | Deny-by-default policy evaluation, risk tiers, kill switch |
| `sentinel-capabilities` | Concrete system capabilities |
| `sentinel-agent-llm` | Investigate–Plan–Approve–Act reasoning loop, LLM backends |
| `sentinel-audit` | SHA-256 hash-chained audit log, verification, metrics |
| `sentinel-fleet` | mTLS controller/agent fleet management |
| `sentinel-tui` | Terminal UI and the `sentinel` binary |

A new capability belongs in `sentinel-capabilities` and must be registered with a
risk tier. A capability with no risk tier is a bug.

## Code Style

- `rustfmt` defaults; `clippy` with `-D warnings`. No `#[allow(...)]` without a
comment explaining why.
- Errors: `thiserror` for library error enums, `anyhow` at the binary boundary.
Never `unwrap()` or `expect()` on a path reachable from LLM input or from an
operator-supplied config.
- No `unsafe` outside of the sandbox syscall layer, and any new `unsafe` block
carries a `// SAFETY:` comment.
- Public items get doc comments. Security-relevant invariants get them in prose,
not just in types.

## Testing

- Unit tests live in-crate; the workspace carries 382 of them and that number
should not go down.
- `mockall` for trait mocks, `tempfile` for filesystem tests, `wiremock` for HTTP,
`criterion` for benchmarks (`cargo bench`, HTML reports under `target/criterion`).
- Anything touching the policy engine, the command allowlist, the sandbox, the
audit chain, or the approval gate **needs a test that demonstrates the deny
path**, not just the allow path.
- New risk-tier routing or resource-guard entries need a test proving the guard
actually blocks.

## Architecture Decisions

Significant design changes get an ADR under `docs/adr/` (`ADR-013`, `ADR-014`, …)
following the existing format: context, decision, consequences. Reference the ADR
number in the PR description.

## Pull Requests

- One logical change per PR. Mechanical reformatting goes in its own commit.
- Update `CHANGELOG.md` under an `## [Unreleased]` heading using
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/) categories.
- Update `README.md` if you add or change a CLI command or a documented capability.
- CI must be green before review.

## Security-Sensitive Changes

Do not open a public PR for a vulnerability fix. Follow [SECURITY.md](SECURITY.md)
and report it privately first.

These areas get extra scrutiny — explain your reasoning in the PR description and
expect questions:

- the deny-by-default evaluator, risk tiers, or kill switch (`sentinel-policy`)
- the exact-match command allowlist or shell-free execution path (`sentinel-exec`)
- rlimit sandbox configuration, timeouts, or output caps
- the PID guard, signal allowlist, or path validation (`sentinel-capabilities`)
- the audit hash chain, its genesis constant, or the verifier (`sentinel-audit`)
- mTLS setup or certificate pinning (`sentinel-fleet`)
- the approval gate and capability-ID validation (`sentinel-agent-llm`)

Loosening any of these defaults is a breaking change even if the types do not
change.

## License

By contributing you agree that your contributions are licensed under the MIT
License, matching [LICENSE](LICENSE).
Loading
Loading