Hurricane Sentinel — For the Bots v1
This is the stored snapshot for the approved document version. The diff below shows what changed from the previous version.
Document snapshot
Hurricane Sentinel — For the Bots
An operator manual written for AI coding agents (Claude Code, Codex, and similar). Dense and action-oriented: how to install, configure, edit, and drive Sentinel from a shell or its HTTP API. Every path, command, and field here is real. If you are a human, the README / HOWITWORKS / SECURITY docs are gentler.
Your mental model
Sentinel is a harness that runs AI agents as a dedicated unprivileged Linux user ("the leashed user") under OS-enforced sandboxing. You (the agent) mostly interact with it in one of two ways:
- The
sentinelCLI — for setup and scripting. - The HTTP API (
sentinel webon:8700,sentinel apion:8701) — token-authed JSON; this is what the web console uses and the most convenient programmatic surface.
Everything an agent does is a run: a worker subprocess spawned as the leashed user, in a per-run cgroup, under Leash (network egress) and — for normal agents — Landlock+seccomp (filesystem/syscalls), with model traffic routed through Muzzle. A run pauses for approval on effectful tools, checkpoints to disk, and resumes.
Two agent flavors:
- Normal agents — an LLM (Ollama or an external provider via Muzzle) driving Sentinel's tool loop with per-tool approval gating.
- CLI-runtime agents —
model: codexormodel: claude-code; driven by that CLI's own agentic loop with its native tools, on your Codex/Claude subscription. See the dedicated section below.
Prerequisites & install
- Linux only (Leash is Linux-only). Needs Muzzle and Leash installed on the same host, plus a dedicated Linux user for agents to run as.
- Python managed with
uv; the package installs into a venv (commonly/opt/sentinel/v1/.venv).
# Interactive: prompts for ONE name that becomes both the head agent and its Linux user.
sentinel setup
# Non-interactive: register <user> with Leash, ensure Leash running, (optionally) check Muzzle.
sentinel init --leash <user> --muzzle
install.sh verifies (and can install) Muzzle + Leash, creates the leashed user, writes
/etc/sentinel/sentinel.yaml, and installs the systemd units. Headless installs default
the head agent/user to naomi.
Where everything lives
| Path | What |
|---|---|
/etc/sentinel/sentinel.yaml | The main config (see below). |
/etc/sentinel/web.env | SENTINEL_WEB_TOKEN=<hex> — the bearer token for the web/API. 0600. |
~/.sentinel/ or <data_root>/.sentinel/ | State dir (see data_root below). Contains: |
…/agents/<name>.yaml | Agent profiles. <name>.soul.md = self-edited persona. |
…/agents/<name>/workspace/ | An agent's private workspace (its runs' cwd). |
…/teams/<name>/{team.yaml,workspace/,guidelines.md} | Teams + shared team workspace. |
…/memory/<scope>/{index.md,vault/} | Per-agent (or shared-scope) memory bank. |
…/runs/<id>/ | Per-run state, events, messages. |
…/mcp/<name>/server.yaml | MCP server definitions (0600). |
…/cron/ …/tools/uploads/ …/datasources/ | Cron jobs, uploaded tools, datasources. |
<data_root>/workspace/ | The shared leashed workspace (naomi-scope runs write here). |
data_root: unset → state is in the leashed user's $HOME (~/.sentinel). Set (e.g.
/var/lib/sentinel-home) → relocated there (needed to enable ProtectHome). On a
relocated box the state dir is <data_root>/.sentinel.
Configuration — /etc/sentinel/sentinel.yaml
Full schema (defaults shown). Edit as root, then restart the services.
leashed_user: naomi # REQUIRED: the Linux user agents run as
data_root: /var/lib/sentinel-home # optional; unset = leashed user's home
profile: development # development | production (enforcement strictness)
muzzle_enabled: true
muzzle_endpoint: http://127.0.0.1:11434 # the Muzzle Ollama-passthrough listener
# Model aliases. An agent's `model` field references one of these `name`s, OR a raw tag.
models:
- name: gpt-oss # alias used by agents
provider: openai # ollama | openai | anthropic
model: gpt-oss:20b # the real model id sent upstream
endpoint: http://127.0.0.1:11437 # for provider!=ollama: the per-provider Muzzle listener
- name: local
provider: ollama
model: llama3.1
default_agent: main # head agent name (naomi scope). Shown in the console.
default_soul: assistant
default_leader_model: "" # tiered defaults: team-leader/naomi tier
default_worker_model: "" # member/agent tier
reasoning_default: true # send reasoning/thinking to reasoning-capable models
# Tool approval policy: tool_name -> auto | needs_approval | deny
tools:
write_file: needs_approval
run_command: needs_approval
subagent_disposition: discard # discard | save | ask (temp subagents after a run)
tool_routing: off # off | keyword (trim advertised tools for small models)
max_parallel_members: 4 # team fan-out concurrency
max_delegations_per_round: 16
fs_sandbox: true # per-run Landlock (skipped for CLI-runtime agents)
run_memory_max: null # per-run cgroup memory.max, e.g. "2G" (null = off)
run_tasks_max: null # per-run pids.max
run_mirror: true # mirror runs into <state>/sentinel.db for fast listing
auto_resume: false # re-drive a crashed run's owner-dead orphan
backup_retention: 7
CLI-runtime binaries (codex/claude-code agents) are located via env, forwarded to the
worker: set SENTINEL_CODEX_BIN / SENTINEL_CLAUDE_BIN in the service environment (a
systemd drop-in) — the leashed user must be logged into that CLI.
Services (systemd)
| Unit | Role |
|---|---|
sentinel-web | The web console + HTTP API on :8700. Drives runs in-process. |
sentinel-api | The programmable per-endpoint HTTP API on :8701. Also drives runs. |
sentinel-cron | Scheduled jobs. |
sentinel | Supervisor daemon — idles; does NOT drive runs. |
sentinel-broker | The ONLY root service. Mutates Leash egress policy via a narrow socket. |
sudo systemctl restart sentinel-web sentinel-api # ALWAYS restart via systemctl
sudo systemctl status sentinel-web
# Verify the socket owner PID == the unit's MainPID:
sudo ss -ltnHp 'sport = :8700'; systemctl show -p MainPID --value sentinel-web
A run inherits the web/api unit's systemd confinement (cgroup, ProtectHome, etc.), so resource/isolation directives belong on those units, not on the idle supervisor.
The sentinel CLI
Run agent-affecting commands as the leashed user (sudo -u <user> sentinel …) so tools
execute under that account and write to its workspace. Top-level commands:
| Command | Purpose |
|---|---|
init / setup | First-time install/config (see above). |
chat [--new] / chats / restore <id> | Interactive REPL; durable, auto-resumes latest. |
run --agent <name> "<task>" | One headless run. |
runs / show <id> | List / inspect runs. |
approvals / approve <id> / deny <id> | The async approval queue. |
resume <id> | Continue a paused run. |
agent create/list/show/add-skill/set-model | Manage the roster. |
team … | Manage teams (leader + members). |
workflow create/list/show/run | Named multi-step YAML pipelines. |
mcp add/list/remove/test | MCP servers (tools). |
model add/list/remove | Model aliases (also editable in the console). |
datasource add/list/remove/test | External datasources (files/sqlite/postgres/mongo). |
memory link/unlink/sync | Obsidian-vault memory linking/sync. |
cron add/list/remove/run | Scheduled runs (supports --timezone <IANA>). |
import openclaw <path> / import hermes <path> | Migrate an OpenClaw/Hermes assistant. |
package export/import | Export/import an agent or team as a secret-free .sentinelpkg. |
tools install-reqs | Leashed pip install of an uploaded tool's requirements. |
backup / restore-backup | State backup/restore. |
status / logs | Health + logs. |
web / api | Run the web console / programmable API. |
Driving Sentinel over HTTP (the API you'll usually want)
Base: http://127.0.0.1:8700. Auth: Authorization: Bearer $SENTINEL_WEB_TOKEN.
TOK=$(sudo grep -oP 'SENTINEL_WEB_TOKEN=\K.*' /etc/sentinel/web.env)
H=(-H "Authorization: Bearer $TOK" -H 'Content-Type: application/json')
# Who is the head agent
curl -s "${H[@]}" http://127.0.0.1:8700/api/whoami
# Start a run. scope: omit/"naomi" (head), "agent:<name>", or "team:<name>".
rid=$(curl -s "${H[@]}" -d '{"scope":"agent:backend-dev","message":"build X"}' \
http://127.0.0.1:8700/api/chat | jq -r .run_id)
# Poll it; read messages + events
curl -s "${H[@]}" http://127.0.0.1:8700/api/runs/$rid # {status, messages, events, scope, ...}
# Approvals
curl -s "${H[@]}" http://127.0.0.1:8700/api/approvals
curl -s "${H[@]}" -X POST http://127.0.0.1:8700/api/runs/$rid/cancel
# Manage agents / models / files
curl -s "${H[@]}" http://127.0.0.1:8700/api/agents
curl -s "${H[@]}" http://127.0.0.1:8700/api/models
curl -s "${H[@]}" 'http://127.0.0.1:8700/api/workspace/list?root=agent:backend-dev&path='
Agents (the roster)
An agent is …/agents/<name>.yaml:
name: backend-dev
description: Backend Python specialist
soul: "You are a careful backend engineer…" # persona / system prompt
model: gpt-oss # a config.models name, a raw tag, or "codex"/"claude-code"
cli_model: "" # CLI-runtime only: the underlying model (e.g. gpt-5-codex, sonnet)
cli_reasoning: "" # CLI-runtime only: minimal|low|medium|high
skills: [file-discipline]
tools: ["*"] # ["*"]=all, []=none, or a list incl. mcp__<server>__<tool>
memory: "" # bank scope; ""=private (own name), shared name = shared bank
workspace: "" # ""=<state>/agents/<name>/workspace
Create via API: POST /api/agents {name, model, soul, cli_model?, cli_reasoning?, tools, …};
edit: PUT /api/agents/<name>. Or sentinel agent create <name> --model <m>.
Memory: agents persist via the tool-based bank — remember(title, content) / recall(query)
(lexical, no embeddings), plus self-state edit_soul / update_user (head agent). Soul +
operator model + latest memory are injected into the system prompt each turn.
CLI-runtime agents (Codex / Claude Code)
Set model: codex or model: claude-code. The agent runs that CLI's own agentic loop with
its native tools (shell/edit/web) on your subscription; granted Sentinel tools are injected
as MCP. Enforcement is OS-level only (leashed user + Leash + cgroup + workspace cwd) — the
per-run Landlock/seccomp is skipped (it conflicts with the CLI's own sandbox). The step
trace (reasoning, tool calls, output) is surfaced as run events / an "Agent steps" panel.
Requirements: the CLI must be installed AND logged in as the leashed user, and allowed
to exec under Leash; set SENTINEL_CODEX_BIN/SENTINEL_CLAUDE_BIN. Configure the underlying
model + reasoning with cli_model + cli_reasoning. A CLI agent cannot be a team leader
(it can't call Sentinel's delegate tools) — use it as a team member with a normal-model
leader. The leader's model must support Ollama "thinking" if reasoning_default: true.
Teams
…/teams/<name>/team.yaml: {name, leader, members:[…]} (the leader must be in members).
The head delegates to a team → the leader fans out to members (each a scoped, sandboxed,
approval-gated child run) → synthesizes. Members share …/teams/<name>/workspace/. Trigger:
POST /api/chat {scope:"team:<name>", message}.
Common tasks (recipes)
# Add an external (OpenAI-compatible via Muzzle) model alias, then use it
curl -s "${H[@]}" -d '{"name":"smart","provider":"openai","model":"gpt-oss:20b","endpoint":"http://127.0.0.1:11437"}' \
http://127.0.0.1:8700/api/models
# Make a Codex agent on high reasoning
curl -s "${H[@]}" -d '{"name":"rex","model":"codex","cli_reasoning":"high","soul":"backend engineer"}' \
http://127.0.0.1:8700/api/agents
# Grant an agent an MCP tool (its tools list names it mcp__<server>__<tool>)
curl -s "${H[@]}" -X PUT -d '{"tools":["mcp__sentinel-uploads__mytool"]}' \
http://127.0.0.1:8700/api/agents/rex
# Browse/upload files in an agent's workspace (path-jailed)
curl -s "${H[@]}" 'http://127.0.0.1:8700/api/workspace/roots'
curl -s "${H[@]}" -F root=agent:rex -F path= -F file=@./x.py http://127.0.0.1:8700/api/workspace/upload
# Change the tool approval policy (edit config, then restart)
sudoedit /etc/sentinel/sentinel.yaml # set tools: { run_command: auto }
sudo systemctl restart sentinel-web sentinel-api
Gotchas & failure modes
- Restart only via
systemctl. A manualkillleaves the enabled unit dead (no self-heal). systemd expands--token ${SENTINEL_WEB_TOKEN}into argv, so seeing it inpsdoes NOT mean a manual launch. - Deploying to a non-editable venv install: the running services import the installed
copy under
.venv/.../site-packages/sentinel/, NOT the source tree — update both (and clear__pycache__) or reinstall. - A mid-flight run is auto-
failedon the driving service's restart (crash recovery) — intended, not a bug. Parked (approval/input) runs survive. - Reasoning vs the model: with
reasoning_default: true, a model that doesn't support Ollama "thinking" returns400 does not support thinking. Pick a thinking-capable model or turn reasoning off for it. - CLI-runtime "not installed for the leashed user": the CLI isn't on the worker's PATH
or
SENTINEL_*_BINisn't forwarded, or it's logged in as the wrong user. - Muzzle policy
tool_callsunset defaults to BLOCK — it silently drops tool calls from Ollama-backed agents. Set it toallow(a Muzzle concern; see the Muzzle manual).
Integrations
- Muzzle — every model call goes through a Muzzle listener (
muzzle_endpointfor Ollama; per-provider listeners for external models). Provider keys live only in Muzzle. - Leash — network egress is default-deny; Sentinel's root broker calls
leash_allowto permit a host (model provider, MCP server, git remote, datasource). See the Leash manual. - Learner — produces
SKILL.mdplaybooks importable viaPOST /api/skills/upload.
Quick reference
Config: /etc/sentinel/sentinel.yaml Token: /etc/sentinel/web.env
State: ~/.sentinel or <data_root>/.sentinel
Services: systemctl restart sentinel-web sentinel-api (:8700 web, :8701 api)
Run as: sudo -u <leashed_user> sentinel <cmd>
API auth: Authorization: Bearer $SENTINEL_WEB_TOKEN
Start run: POST /api/chat {scope, message} scope = naomi | agent:<n> | team:<n>