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
15 changes: 14 additions & 1 deletion .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,23 @@ jobs:
working-directory: dist
run: sha256sum *.tar.gz > checksums.txt

# Release notes are the top-most section of the curated CHANGELOG.md -
# everything between the first "## [" version header and the next one - so
# the release reads like the changelog rather than a raw commit list. The
# validate job already guarantees that section exists for this tag.
- name: Generate release notes
id: notes
run: |
{
echo "content<<CHANGELOG_EOF"
awk '/^## \[/ { if (seen) exit; seen=1; next } seen { print }' CHANGELOG.md
echo "CHANGELOG_EOF"
} >> "$GITHUB_OUTPUT"

- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
generate_release_notes: true
body: ${{ steps.notes.outputs.content }}
# install.sh ships as a release asset so the latest-download URL the
# update notice points at always serves the current installer. It is
# the checked-out repo copy - the single source of truth.
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

All notable changes to Rook, following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [Semantic Versioning](https://semver.org/).

## [0.6.2] - unreleased

### Changed

- **The README is minimalist; detail moved into `docs/`.** The front page had grown to ~500 lines; it now leads with the essentials - what Rook is, install, a first run, why, safety - and a Documentation section, matching zot's shape. The providers, configuration, objectives/flags, skills, how-it-works, safety and development sections moved into a `docs/` folder, each its own page.

- **Release notes now come from this changelog, matching zot.** The release workflow set `generate_release_notes: true`, so a GitHub release listed raw commit titles and ignored `CHANGELOG.md` entirely - while zot published the curated changelog section as its release body. rook's workflow now extracts the top-most `## [version]` section from `CHANGELOG.md` (the same `awk` zot uses) and publishes it as the release body. The changelog format was already identical; only the workflow differed. Every future release reads like the changelog rather than a commit list.

## [0.6.1] - 2026-08-24

### Changed
Expand Down
536 changes: 69 additions & 467 deletions README.md

Large diffs are not rendered by default.

61 changes: 61 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Configuration

Configuration is layered: **built-in defaults < config file < `ROOK_*` env vars
< CLI flags**. The config file is optional - env vars alone are enough.

```bash
rook config # opens the config in $EDITOR, creating it from a template
rook config path # print the config file location
```

The file lives at `~/.config/rook/config.yaml` (override with `$ROOK_CONFIG` or
`--config`). Every scalar has a matching `ROOK_*` env var (`agent.model` →
`ROOK_AGENT_MODEL`, `default_provider` → `ROOK_DEFAULT_PROVIDER`). A provider's
key comes from its provider's conventional variable or `api_key` in the file,
which may be a literal or a `$VAR` reference. A developer build also reads a
`.env` from the working directory - a released one does not (see
[development.md](development.md)). See
[../configs/rook.example.yaml](../configs/rook.example.yaml) for the full,
commented template.

## Engine tuning

Under `agent:` the run's engine knobs mirror zot's, so a run is shaped the same
way in either tool. Each is optional; zero uses the built-in default.

- `max_settles` - nudges to record an outcome before a run is surfaced as unsettled
- `max_calls` - cap on total tool calls (0 = unbounded)
- `max_time` - wall-clock cap (`30m`, `2h`, `90s`); empty = unbounded
- `max_tokens` - cap on a single response's output (0 = unbounded)
- `max_tool_output` - bytes a tool result may return before truncation
- `max_continuations`, `max_recoveries`, `max_cycles`, `max_empties` - recovery/repetition bounds
- `limit_checkpoints` - percentages of a bounded limit to warn at (`[]` turns notices off)
- `context_strategy` - `compact` (summarise older history) or `truncate` (drop oldest)
- `compact_min_tokens`, `compact_min_messages`, `compact_trigger_ratio` - when compaction fires

A model entry under `providers.<name>.models.<model>` additionally takes:

- `context` - the real context window, if the endpoint's is smaller than the model's card
- `vision` - `true` if this model can be shown images (offers the agent the view tool)

Rook cannot import zot's internal model catalogue, so there is no
auto-detection; these are explicit operator overrides - the part that matters
for a custom endpoint.

## Update check

After a run, Rook asks GitHub for the latest release and prints a one-line
notice (with the upgrade command) when the binary is out of date - the only call
it makes that is not to the model provider. Disable it on an air-gapped or
locked-down host with `update_check.disabled: true`, or
`ROOK_UPDATE_CHECK_DISABLED=true` (no config file needed).

## Credential hygiene

Rook strips the resolved provider credential from the environment before the
agent runs, so the commands it executes against a target cannot read it. Every
built-in provider's conventional variable is unset by name, whichever provider
the run uses - so a provider key exported in your shell does not leak into the
agent's process. And a built-in provider's conventional key is withheld once you
set a custom `base_url`, so a key scoped to one host is never forwarded to
another.
40 changes: 40 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Development

The engine is the published [zot](https://github.com/openzot/openzot) module,
pinned in `go.mod`, so the repository builds from a clean clone with no extra
steps:

```bash
git clone https://github.com/pdparchitect/rook
cd rook
make # lists the targets
make build # build ./rook
```

`make` on its own prints the targets rather than assuming one, because Rook has
two build variants:

```bash
make build # release binary
make dev # developer binary - reads a .env from the working directory
make test # run tests
make race # tests under the race detector
make vet # go vet over both build variants
make dist # cross-platform release archives under dist/
```

## Install from source

```bash
go install github.com/pdparchitect/rook/cmd/rook@latest
```

## Release vs developer builds

A released binary does **not** read a `.env` from its working directory; a
developer build does. Rook runs shell commands against targets with a provider
key in the process, so a released binary must not take credentials from whatever
directory it was pointed at - a stray committed `.env` in the code under review
would otherwise reach the process about to run commands against it. The switch
is a build tag (`-tags dev`) that defaults to off; `rook --version` prints which
kind you have. See [../RELEASES.md](../RELEASES.md) for the release flow.
35 changes: 35 additions & 0 deletions docs/how-it-works.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# How it works

```
cmd/rook CLI: flags, .env, signal handling, version
internal/config Central config: default model, max iterations, system prompt
internal/agent Loads the catalog + on-disk skills, registers tools, drives the loop
internal/version Build-time version + GitHub release update check
embed.go //go:embed skills → the embedded catalog
skills/ the skill-catalog bootstrap (no skill library)
```

The engine is [zot](https://github.com/openzot/openzot), running in-process:
the agentic loop, thread assembly, compaction, loop detection, provider
transports and the read-only viewer all live in the zot module, pinned in
`go.mod`. Rook adds the security persona, the objective/ledger workflow, and the
skill catalog on top.

The default model and the agent's system prompt (backstory) live in one place -
[`internal/config/config.go`](../internal/config/config.go) - so they can be
tuned without touching the CLI or the agent loop.

At startup Rook loads the embedded catalog with `agent.LoadSkillsFromFS` and
layers the on-disk skills directory over it with `agent.NewSkillLoader`, which
rescans that directory each turn. Skills are read with the `read` tool - an
embedded skill through an `embedded-skill://` URL, an on-disk one through its
file path. Rook registers `agent.DefaultTools()`, builds a security-focused
backstory, and runs `agent.ExecuteWithTools` until the agent records an outcome
by calling `_success` or `_failure` (settle mode: a run ends only when it records
an outcome, never because its prose sounded conclusive).

Every run writes a session log under `.rook/sessions/`, so a run can be inspected
afterwards and resumed with `rook --resume last` (or an id, or a path). It also
writes per-run artifacts (`status.json` + `events.jsonl`) the desktop status
widget reads, and closes with an end-of-run digest carrying the session id and a
ready-to-copy resume command.
143 changes: 143 additions & 0 deletions docs/objectives.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# Objectives

Rook dispatches from mission files, not command-line prose. An objective is a
small YAML file - the durable goal, the success criteria that define "done", and
the rules of engagement the work must hold to - and each objective becomes one
autonomous run.

```bash
export ZAI_API_KEY="sk-..." # or --provider openai with OPENAI_API_KEY, etc.

# Write an objective, then run it
rook new "Gain access to the target network and map paths to domain admin"
# edit .rook/objectives/gain-access-to-the-target-network-and-map-pa.yaml
# to set the success criteria, then:
rook

# Run a single objective with reasoning streamed to the terminal
rook -v .rook/objectives/firmware-recon.yaml

# Version
rook version
```

`rook new` drops a file under `.rook/objectives/`. Edit it to set the success
criteria, then a bare `rook` runs every outstanding objective in the dossier -
skipping what the ledger already records as done. `--watch` turns the folder
into a drop box.

```bash
rook # run every outstanding objective
rook .rook/objectives/firmware-recon.yaml # run one by name
rook --watch # drop-box mode
```

A ledger records what has run: a satisfied objective is skipped on re-run;
editing an objective changes its hash and re-queues it; a failed run is never
recorded as done, so it runs again next time. Each receipt carries evidence read
back from the run's own artifacts - the stop reason, the summary, the iteration
count - so a record is proof rather than a claim.

A developer build loads a `.env` from the working directory; a released binary
does not (see [development.md](development.md)).

## Some example objectives

```bash
# Reverse-engineer an entire binary or firmware image - recover structure,
# embedded secrets, and the attack surface it exposes
rook new "Reverse engineer the firmware image in ./firmware.bin: recover embedded credentials, map services, and identify remotely reachable bugs"

# Full engagement: gain a foothold and map how deep access goes
rook new "Gain initial access to the target network from the external surface, then map lateral-movement paths to the domain controller"

# Whole-codebase audit - not one endpoint, the whole project
rook new "Audit the entire ./monorepo for the full OWASP taxonomy: injection, broken access control, auth flaws, crypto misuse, and unsafe deserialization"

# Cloud compromise assessment - find the path from a foothold to data exfiltration
rook new "Assess the AWS environment for paths to privilege escalation; chain IAM, S3 and IMDS exposure into a full data-exfiltration chain"

# External attack-surface mapping
rook new "Map example.com's external surface: subdomains, exposed services, leaked credentials, and anything that should not be internet-facing"

# Smart-contract security review - the whole protocol, not one function
rook new "Audit the Solidity protocol in ./contracts for reentrancy, access-control, oracle manipulation and economic attacks across every contract"
```

## Objective files

An objective is a small YAML file with four fields:

```yaml
# .rook/objectives/firmware-recon.yaml
# rook objective - what to do and what "done" means.

title: Firmware reverse engineering

# The durable goal of the engagement. The objective will not run until this is filled in.
objective: |-
Reverse engineer the firmware image: recover embedded credentials, map the
services it exposes, and identify remotely reachable memory-corruption bugs

# The objective is not met until every one of these holds.
success:
- every credential is extracted and documented with its location and purpose
- every remote service is enumerated with its protocol and entry point
- every exploitable bug has a working PoC and impact assessment
- the engagement report is delivered as the run's outcome

# Rules that hold for the whole run - non-negotiable constraints on how the
# objective may be pursued.
rules_of_engagement:
- no network access; work entirely offline against the image
- do not modify the original firmware image
```

The agent's findings stream to **stderr**; with `--verbose`, reasoning tokens
stream to **stdout**. The final report is delivered as the agent's response -
Rook does not write files on its own. If you want the report (or any other
artifact) saved to disk, ask for it in the objective and the agent will use its
`write` tool.

## Flags

| Flag | Default | Description |
| ------------------ | ---------------------------- | -------------------------------------------------- |
| `--provider` | `zai` | Model provider to run against, or one named in config |
| `--config` | `~/.config/rook/config.yaml` | Path to the config file (or `$ROOK_CONFIG`) |
| `--model` | `glm-5.2` | Model the agent reasons with (overrides config) |
| `--dir` | `.` | Working directory the agent investigates |
| `--max-iterations` | `10000` | Maximum agent iterations before a forced stop |
| `--objectives-dir` | `./.rook/objectives` | Where this project's objectives live, run by a bare `rook` |
| `--records-dir` | `./.rook/records` | Where run records (the ledger) are written |
| `--session-dir` | `./.rook/sessions` | Where session logs are written (`--no-session` to disable) |
| `--resume` | - | Continue an earlier session: an id, a path, or `last` |
| `--run-dir` | `~/.local/state/rook/runs` | Base directory for per-run artifacts |
| `--watch` | `false` | Stay up and run objectives as they arrive |
| `--rerun` | `false` | Run objectives even when the ledger says done |
| `-v`, `--verbose` | `false` | Stream the agent's reasoning tokens to stdout |
| `-V`, `--version` | - | Print version and exit |

Flags override `ROOK_*` environment variables, which override the config file,
which overrides the built-in defaults.

## Files & directories

Rook uses four distinct locations - it helps to keep them straight:

| Location | What it holds | Default path |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| **Workspace** | The directory Rook works _in_ - what it reads, edits, and runs commands against. Any file the agent writes (a report you asked for, a PoC) lands here. | the current working directory (override with `--dir`) |
| **Dossier** | The project's objectives and run records: `.rook/objectives/<slug>.yaml` (the mission files) and `.rook/records/<slug>/<run>.yaml` (the ledger receipts). | `./.rook/` (override with `--objectives-dir` / `--records-dir`) |
| **Run artifacts** | Rook's own record of _each run_: `status.json` (live state) and `events.jsonl` (append-only log). Telemetry, not work product - the status widget reads it. | `~/.local/state/rook/runs/<runid>/` (`$XDG_STATE_HOME`; override with `--run-dir` / `run_dir` / `$ROOK_RUN_DIR`) |
| **Config** | Your settings and provider keys. | `~/.config/rook/config.yaml` (`$ROOK_CONFIG` / `--config`) |

The **dossier** is the contract: objective files are what a run is dispatched
from, and the ledger records what has been done. The **run artifacts** are
Rook's log of _what it did_; the **workspace** is _where it did it_. They never
mix: run artifacts are telemetry under your state directory, while the agent's
file writes stay in the workspace.

Each run gets its own `runs/<runid>/` directory (`<timestamp>-<pid>`), so
concurrent runs never overwrite each other; the desktop widget shows the most
recent active run.
Loading