← Back to document history
Document version

Hurricane Sentinel — For the Bots v2

This is the stored snapshot for the approved document version. The diff below shows what changed from the previous version.

Preview
Source path
sentinel/forthebots.md
Source commit
No commit recorded
Created at
Aug 4, 2026, 2:35 AM UTC
Source digest
ce748efdba04cca2aff6c353412f93e93a6fe83bbfb7b478ef0a921e131c2360

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 sentinel CLI — for setup and scripting.
  • The HTTP API (sentinel web on :8700, sentinel api on :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:

  1. Normal agents — an LLM (Ollama or an external provider via Muzzle) driving Sentinel's tool loop with per-tool approval gating.
  2. CLI-runtime agentsmodel: codex or model: 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.
# Local model path (Ollama). --model defaults to qwen2.5:7b (a small, reliable tool-caller).
sentinel setup
sentinel setup --leash naomi --model qwen2.5:7b --yes      # non-interactive local

# Bring-your-own-key (no GPU): wire a hosted model end-to-end in one step. Provider-agnostic
# (openai | anthropic | gemini). Pass the key via env (preferred; --api-key is ps-visible) or
# a TTY prompt. Creates the Muzzle upstream + loopback listener + tool_calls override, adds a
# model alias, and binds the head agent to it. An explicit --model (the provider's id) is required.
SENTINEL_MODEL_API_KEY=sk-... sentinel setup --provider openai --model gpt-4o-mini --yes

# Register <user> with Leash + ensure Leash running (no model backend).
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. Setup ends by printing the console URL + login token and then running sentinel doctor (below) to confirm the box is healthy.

sentinel doctor — run this first when anything's off

sentinel doctor            # full check incl. a real end-to-end model ping
sentinel doctor --no-probe # skip the (slower) model call

Checks, each with the exact fix printed on failure (exit non-zero if any FAIL): models configured, Muzzle up, Muzzle allows tool_calls (an unset policy defaults to block and silently drops every agent tool call — the #1 first-run killer), model providers wired as Muzzle upstreams, an end-to-end backend ping (catches a dead upstream / an empty-reply model), Leash egress, the broker socket, and the console token. If backend fails with an "invalid model" error, your models[0] alias points at a model the backend doesn't serve — fix the alias/model id or reorder models so a served one is first.

Where everything lives

PathWhat
/etc/sentinel/sentinel.yamlThe main config (see below).
/etc/sentinel/web.envSENTINEL_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>.yamlAgent 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.yamlMCP 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 | gemini
    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)

UnitRole
sentinel-webThe web console + HTTP API on :8700. Drives runs in-process.
sentinel-apiThe programmable per-endpoint HTTP API on :8701. Also drives runs.
sentinel-cronScheduled jobs.
sentinelSupervisor daemon — idles; does NOT drive runs.
sentinel-brokerThe 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:

CommandPurpose
init / setupFirst-time install/config (see above). setup --provider … for a hosted key.
doctor [--no-probe]Actionable health check (models, Muzzle+tool_calls, backend ping, leash, token).
status / preflightConfig+service summary / gate the box against its security profile.
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-modelManage the roster.
team …Manage teams (leader + members).
workflow create/list/show/runNamed multi-step YAML pipelines.
mcp add/list/remove/testMCP servers (tools).
model add/list/removeModel aliases (also editable in the console).
datasource add/list/remove/testExternal datasources (files/sqlite/postgres/mongo).
memory link/unlink/syncObsidian-vault memory linking/sync.
cron add/list/remove/runScheduled runs (supports --timezone <IANA>).
import openclaw <path> / import hermes <path>Migrate an OpenClaw/Hermes assistant.
package export/importExport/import an agent or team as a secret-free .sentinelpkg.
tools install-reqsLeashed pip install of an uploaded tool's requirements.
backup / restore-backupState backup/restore.
status / logsHealth + logs.
web / apiRun 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 manual kill leaves the enabled unit dead (no self-heal). systemd expands --token ${SENTINEL_WEB_TOKEN} into argv, so seeing it in ps does 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-failed on 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" returns 400 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_*_BIN isn't forwarded, or it's logged in as the wrong user.
  • Muzzle policy tool_calls unset defaults to BLOCK — it silently drops tool calls from Ollama-backed agents. Set it to allow (a Muzzle concern; see the Muzzle manual).

Integrations

  • Muzzle — every model call goes through a Muzzle listener (muzzle_endpoint for 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_allow to permit a host (model provider, MCP server, git remote, datasource). See the Leash manual.
  • Learner — produces SKILL.md playbooks importable via POST /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>

Diff from previous

--- forthebots@1
+++ forthebots@2
@@ -35,15 +35,39 @@
 
 ```bash
 # Interactive: prompts for ONE name that becomes both the head agent and its Linux user.
+# Local model path (Ollama). --model defaults to qwen2.5:7b (a small, reliable tool-caller).
 sentinel setup
-
-# Non-interactive: register <user> with Leash, ensure Leash running, (optionally) check Muzzle.
+sentinel setup --leash naomi --model qwen2.5:7b --yes      # non-interactive local
+
+# Bring-your-own-key (no GPU): wire a hosted model end-to-end in one step. Provider-agnostic
+# (openai | anthropic | gemini). Pass the key via env (preferred; --api-key is ps-visible) or
+# a TTY prompt. Creates the Muzzle upstream + loopback listener + tool_calls override, adds a
+# model alias, and binds the head agent to it. An explicit --model (the provider's id) is required.
+SENTINEL_MODEL_API_KEY=sk-... sentinel setup --provider openai --model gpt-4o-mini --yes
+
+# Register <user> with Leash + ensure Leash running (no model backend).
 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`.
+the head agent/user to `naomi`. Setup ends by printing the **console URL + login token** and
+then running **`sentinel doctor`** (below) to confirm the box is healthy.
+
+### `sentinel doctor` — run this first when anything's off
+
+```bash
+sentinel doctor            # full check incl. a real end-to-end model ping
+sentinel doctor --no-probe # skip the (slower) model call
+```
+
+Checks, each with the exact fix printed on failure (exit non-zero if any FAIL): models
+configured, Muzzle up, **Muzzle allows `tool_calls`** (an *unset* policy defaults to `block`
+and silently drops every agent tool call — the #1 first-run killer), model providers wired as
+Muzzle upstreams, an **end-to-end backend ping** (catches a dead upstream / an empty-reply
+model), Leash egress, the broker socket, and the console token. If `backend` fails with an
+"invalid model" error, your `models[0]` alias points at a model the backend doesn't serve —
+fix the alias/`model` id or reorder `models` so a served one is first.
 
 ## Where everything lives
 
@@ -79,7 +103,7 @@
 # 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
+    provider: openai             # ollama | openai | anthropic | gemini
     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
@@ -140,7 +164,9 @@
 
 | Command | Purpose |
 |---------|---------|
-| `init` / `setup` | First-time install/config (see above). |
+| `init` / `setup` | First-time install/config (see above). `setup --provider …` for a hosted key. |
+| `doctor` [`--no-probe`] | Actionable health check (models, Muzzle+tool_calls, backend ping, leash, token). |
+| `status` / `preflight` | Config+service summary / gate the box against its security profile. |
 | `chat` [`--new`] / `chats` / `restore <id>` | Interactive REPL; durable, auto-resumes latest. |
 | `run --agent <name> "<task>"` | One headless run. |
 | `runs` / `show <id>` | List / inspect runs. |