Descartes is a local-first operations agent for one machine. It works as a maintenance agent, a system-administration assistant, and a gateway to system operations. It looks at the host with read-only tools. Then a private LLM agent reads the evidence. Descartes prints a diagnosis that shows the evidence and gives safe next checks.
Descartes makes no change to the host. A change needs a special command. That command does not exist yet. When it exists, a person must start it, and the policy must approve it.
descartes triage "my machine is slow"The name comes from Philip Kerr's The Second Angel: Descartes is the computer running the literal Blood Bank on the Moon. This project borrows the idea of a machine entrusted with keeping critical infrastructure alive, but starts humbly: observe first, notify clearly, act only when explicitly allowed.
Descartes is not "an LLM watching a server." It is meant to become a stratified machine
nervous system — cheap deterministic reflexes, statistical baselines, known-issue
signatures, and deliberative agent layers for escalation — all sitting on top of local
structured evidence rather than model guesswork. See AGENTS.md for the full project
identity, architecture, and conventions this repo follows.
- Capabilities at a glance
- Status
- Quick start
- Architecture
- Operational lifecycle
- What
triagedoes today - Local history daemon and deterministic alerts
- Self-learning and defensive detection — detectors · current limits
- Login and model selection
- JSON output
- Safety and privacy invariants
- Field-level data handling
- Supported platforms
- Descartes-owned paths
- Repository layout
- Building and testing
- Further reading
| Capability | What it does | Status |
|---|---|---|
| Triage | Answers a question about the host. The model leads. The diagnosis uses only read-only evidence envelopes (actions_taken: []). |
Live |
| Local history and alerts | Keeps a bounded local metric history. Raises alerts on memory, load, disk, and daemon staleness. Uses no LLM. | Live |
| Structural watching | Builds local fact history and runs deterministic novelty and statistical detectors. | On by default |
| Self-learning authoring | Mines candidate rules and baselines. A person must approve each one before it becomes active. | Opt-in |
| Defensive detection | Runs novelty detectors, deception canaries, and intrusion signals. See the detector list. | On by default |
| Containment recommendations | Recommends containment through a local notification. It only recommends. It makes no change. | Off by default |
| Evidence freeze | Makes a read-only forensic evidence bundle. An operator starts it (descartes incident freeze). |
Live |
| Remediation and host actions | No general action tool exists yet. Descartes changes only its own daemon service and its alert-state files. | Not yet |
Structural watching is on by default. The authoring and promotion layer stays opt-in. A detector that cannot establish a trustworthy baseline goes quiet instead of guessing, so it never fabricates a "first time seen" claim from lost or incomplete history. That is a specific guarantee, not a promise against every false positive or every missed attack. See the current limits.
This is version 0. It has these parts:
- a read-only triage CLI;
- a local-first alerting daemon with deterministic rules;
- structural watching (on by default);
- a self-learning authoring and promotion layer (opt-in).
Structural watching builds rules-free fact history and runs defensive detectors: novelty baselines, statistical baselines, deception canaries, and intrusion signals. The authoring layer mines candidate rules and baselines. A human must review and approve a candidate before it becomes active. A novelty detector that cannot trust its own history goes quiet rather than guessing, so it never fabricates a "first time seen" claim. Positive-evidence detectors (credential access, canaries) are not gated this way and fire on any matching metadata change, benign or not. See current limits.
The durable core will move to Rust over time. The current CLI uses Node.js, because this lets the tool ship quickly. The CLI includes the agent harness and the subscription login.
For the plan, see docs/ROADMAP.md. For the current state, see docs/HANDOFF.md.
On macOS, use Homebrew to install Descartes. Homebrew installs the CLI and the signed, notarized notification helper:
brew install lightless-labs/tap/descartesAn old npm install -g with Homebrew's Node.js can block brew link. Remove that old
install first. Then brew link can claim the descartes command:
npm uninstall -g @lightless-labs/descartesFor other systems, install with npm. This needs Node.js 22.19.0 or higher and a writable
npm global prefix. This install does not include the macOS notification helper. The
osascript channel still works. The --helper option stays available as a manual override.
npm install -g github:Lightless-Labs/descartes
descartes login
descartes triage "my machine is slow"
descartes triage "my machine is slow" --jsonTo install from an HTTPS tarball, use this command:
npm install -g https://github.com/Lightless-Labs/descartes/tarball/mainFor a root-owned system npm prefix, install into a user prefix instead:
npm install -g --prefix "$HOME/.local" github:Lightless-Labs/descartes
export PATH="$HOME/.local/bin:$PATH"Some Linux distributions have an old Node.js package. On these systems, install Node.js 22.19.0 or higher first with your usual version manager. Then install Descartes.
Descartes has layers. It is not one free autonomous shell. The lifecycle stages (below) are not the same as these layers. One stage can use several layers. One layer can support several stages.
| Layer | Purpose |
|---|---|
| L0 Deterministic System Tools | Get facts from local tools and platform APIs: OS, CPU, memory, disks, processes, services, logs, network, containers, VMs, scheduled jobs, certificates, sessions, and peers. |
| L1 Monitoring, Rules, Signatures | Find threshold breaches, repeated failures, drift, and known-issue patterns. Use no LLM. |
| L2 Deliberative Agents | Do escalated diagnosis, incident correlation, recommendations, and plans for new or unclear conditions. |
| L3 Federated Knowledge | Share anonymized signatures and outcomes across a fleet. This is optional and for the future. |
| Policy and Authority Plane | Control permissions, approvals, action plans, and audit logs for every change to the system. |
The model can route questions, ask for evidence, write explanations, audit gaps, and suggest improvements. The model is not the source of truth. The source of truth is the local structured evidence. Descartes returns this evidence as typed envelopes, not as prose:
{
"id": "system-overview",
"status": "ok",
"layer": "L0",
"source": "os",
"result": {},
"confidence": 1,
"review_hint": "none",
"trace": {
"tool": "collect_system",
"target": null,
"latency_ms": 18,
"ts": "2026-05-18T00:00:00Z"
}
}Observe → Notify → Diagnose → Recommend → Plan → Act → Learn
- Observe — Collect facts, logs, metrics, events, and machine state.
- Notify — Show important changes, risks, failures, and anomalies.
- Diagnose — Explain the probable causes from the evidence.
- Recommend — Suggest fixes, tradeoffs, and next checks.
- Plan — Build action plans that a person can audit and review.
- Act — Do actions only through the policy and authority gates.
- Learn — Turn confirmed findings into cheaper rules, signatures, tests, and tools.
In normal triage, the model leads. The steps are:
- The user asks a question about the machine.
- The private Descartes agent selects the read-only evidence tools to call.
- The deterministic collectors return structured evidence envelopes.
- The model writes a diagnosis. It uses only those envelopes and shows the evidence.
- Descartes prints the report. It records
actions_taken: [].
The collectors cover many areas:
- system identity, uptime, CPU, memory, and swap;
- disks, mounts, and filesystem type;
- top processes and the process parent tree;
- bounded time sampling;
- network basics: interfaces, routes, DNS, and listening sockets;
- service managers: launchd and systemd;
- recent logs (bounded);
- containers: Docker, Podman, Colima, Lima, and Apple
container; - VMs: Tart, Multipass, VirtualBox, libvirt, Parallels, VMware, UTM, and more;
- scheduled jobs;
- time sync;
- certificates;
- tmux and screen sessions;
- VPN and peer status: WireGuard, macOS VPN services, and Tailscale.
For the full list, see docs/reference/collectors.md.
--no-investigate is a fallback mode. It turns off the LLM-requested evidence tools. It
uses precollected facts to write the answer without tools.
When the local history daemon has fresh metrics, triage adds a bounded history summary.
This summary is one more evidence envelope. The default window is 24h:
descartes triage "How's my system doing?"
descartes triage --history-window 6h "Did anything change recently?" --json
descartes triage --no-history "Ignore local history for this question"Descartes can keep a bounded local metric history. Later CLI commands use this history to answer "what changed recently?". They use no LLM. Descartes can also raise deterministic alerts from this history. These alerts use no model.
descartes daemon install # write a user launchd/systemd service file (safe to repeat)
descartes daemon start # load and start the user service (safe to repeat)
descartes daemon status
descartes daemon stop
descartes daemon uninstall
descartes daemon run --foreground --once # one collection, for development
descartes history summary # short local metric summary, no LLM
descartes baseline status # watch phase for each detector domain
descartes baseline inspect # current established set, recomputed live
descartes alerts list # deterministic local alerts, no LLM
descartes alerts watch --interval 30s
descartes alerts ack alert_...The daemon collects only a few facts: the system overview, the top processes, and the disk usage. It writes only under Descartes-owned XDG state paths. It obeys the retention and size limits.
The daemon does not do these things:
- make background LLM calls (unless you enable alert intelligence);
- upload telemetry;
- give shell tools;
- do remediation actions.
The daemon runs at user level only: launchctl on macOS, systemctl --user on Linux.
This part is new. It needs more tests on real hosts and different launchd and systemd
versions.
The alert rules cover four conditions:
- a missing or stale daemon sample;
- sustained high memory pressure;
- sustained high load for the CPU count;
- disk pressure.
Alert intelligence is a separate option, off by default. When you enable it, an alert change can wake an LLM session. This session is rate-limited and audited. It has no remediation tools. It decides whether and how to notify.
Notification delivery is also a separate option, off by default. The channels are desktop, syslog, or an experimental native macOS channel:
descartes alerts intelligence enable --max-per-hour 3
descartes alerts notifications setup --channel desktopStructural watching is on by default. It uses watch.json and can do these things:
- read its own fact history;
- run deterministic novelty and statistical detectors;
- report whether each detector domain is still learning or watching.
The authoring and promotion layer uses learned.json. It stays off until a person enables it.
It can mine candidate rules and provenance and identity baselines. The two switches are
independent.
The baseline commands report and control structural watching:
descartes baseline status # show each domain's learning phase
descartes baseline status --json
descartes baseline inspect # show current established sets
descartes baseline inspect --domain service --json
descartes baseline enable # enable structural watching
descartes baseline disable # disable structural watchingbaseline inspect recomputes its answer from the fact-history window. Its answer can differ
slightly from the daemon's last tick. descartes alerts watch remains the live alert tail.
The authoring commands remain separate:
descartes learned enable # enable authoring and promotion
descartes learned mine # mine candidates from the facts
descartes learned soak # shadow test; never alerts on its own
descartes learned review # list candidates that wait for approval
descartes learned approve <constraint-id> --nonce <nonce>
descartes learned status # subsystem state and fact-store completeness
descartes provenance snapshot # process/identity baseline snapshot
descartes provenance baseline show
descartes containment recommend status # recommend-only containment surface (separate option, off by default)Each mined artifact moves through one lifecycle: draft → shadow → review-ready → active → retired. An active artifact feeds the same alert pipeline as the fixed rules. No promotion
happens without a human decision.
These detectors run inside the daemon tick. They are deterministic and use no LLM, except where noted. They read only the host's own facts. Field-level identity handling is not uniform — see Field-level data handling.
Each novelty detector is completeness-gated. When its fact history is incomplete, damaged, or changed, the detector emits nothing. This state is "cold-start". The detector does not make a false "first time" alert.
The positive-evidence detectors are different. Credential access and canary trips fire on a direct observation. They are not gated. Descartes never discards a real event.
| Detector | Detects | Basis |
|---|---|---|
| Session baseline | A mass session drop or churn (session.count_drop, session.churn). |
tmux and screen session census |
| Peer baseline | A burst or a drop of VPN or tailnet peer logins (peer.count_spike, peer.count_drop). |
WireGuard, Tailscale, and VPN peer census |
| Service baseline | A known service that disappears, or a new service that appears (service.disappeared, service.appeared). |
launchd and systemd census |
| Scheduled-job baseline | A new cron or scheduled job — a common persistence foothold (scheduled_job.appeared). |
cron, systemd-timer, and launchd census |
| Process lineage | A new exec-chain edge — an unusual child process (process.lineage_edge). |
process parent and child census |
| Credential access | A metadata change to a watched credential file: its mtime or inode changes, from a write or a replace (credential.access). It does not detect a plain read — a read does not change mtime or inode. It does not detect a permission change — a chmod changes only ctime, which the detector does not track. This is positive evidence, not gated. |
two lstat snapshots (mtime and inode); lstat only; never reads the contents |
| Deception canaries | A touch of a honey-token file, or a change to the canary manifest or store (canary.tripped, canary.tampered). |
filesystem tripwire |
| Incident correlation | A mass session drop together with an odd-hour, unknown peer login. | cross-stream join (optional, separately-gated LLM adjudication) |
| Provenance and identity | A process or a listening socket whose provenance or identity signature moves away from its baseline. | process ancestry and hashed identity signatures |
The fact-store completeness substrate supports these detectors. It makes the "never
fabricate" rule hold. A durable, tamper-aware integrity ledger records whether the fact
history is complete. So a detector can separate two cases: "I have never seen this" and "I
cannot trust my history". In the second case, the detector stays quiet. The state intact
needs positive proof. descartes baseline status shows the watch phase and
descartes learned status shows the authoring state and fact-store completeness.
The recommend-only containment surface turns a strong signal into a recommendation, for
example "throttle, block, or quarantine X". It sends the recommendation through the
local-notification path. It always adds the label RECOMMEND-ONLY. This surface cannot act.
It has no execution primitive, no capability token, and no way to change the host. Its
option does not turn on unless descartes learned enable is already on.
These are the limits of the defensive layer today:
- Watch phase. Structural watching is on by default. A detector stays quiet until it has
enough clean history for a baseline. Use
descartes baseline statusto see each domain. For alerts, you must also turn on notifications. - Cold-start and setup time. A detector stays quiet until it has enough clean history for a baseline. A new install reports nothing. A detector that recovers from a real history loss also reports nothing. Recovery after a real loss takes up to the fact-retention window. This delay is deliberate and safe.
- Ordinary retention churn can extend cold-start, not just a real loss. The fact-store byte cap evicts old points during routine use, and every eviction marks that stretch of history "degraded"; baseline readers reject degraded history. On a host whose full retention window churns before the cap clears, a detector can stay in cold-start for longer than the nominal recovery window, and today there is no separate status for "blocked by ordinary eviction" versus "recovering from a real loss".
- Host-edge scope. Descartes watches the local host. It does not watch the cloud plane or the cluster plane.
- Polling, not real-time. Detection runs on the daemon cycle. A fast action between two cycles can escape. A real-time event path exists in the design, but it is not built.
- On-host tamper has a bound. The completeness ledger stops accidental loss and non-root, out-of-band changes. But a root attacker who rewrites both the facts and the ledger together defeats the on-host check. An off-host attestation and a fleet dead-man's-switch are future design work. A canary detects tampering, but it cannot protect itself against local root.
- Recommend-only. Containment proposes an action. It never does the action. There is no general remediation tool yet.
- One instance only. Two daemon instances on the same state directory can race the fact store. This is a known, deferred item.
The design and the current build status are in
docs/plans/2026-07-09-self-learning-stratified-monitoring.md,
docs/plans/2026-08-21-fact-store-completeness-hardening.md, and docs/HANDOFF.md.
descartes login opens a browser for subscription OAuth, when possible. When the browser
callback cannot finish, use descartes login --no-open.
For a subscription login, Descartes picks a strong default model. It does not pick the first
model in the registry. It picks the highest openai-codex GPT model by version, or the
highest Anthropic Sonnet model. You can override the model:
descartes triage "my machine is slow" --model openai-codex/gpt-5.5 --thinking highUse --json on most commands for replay and debugging. For triage, the JSON output
includes these items:
- the diagnosis;
- the evidence envelopes;
- the deterministic findings;
- the diagnostics;
- the tool traces;
- the selected model metadata;
- the active tool names;
- the fallback state;
- and this field:
"actions_taken": []- Descartes is read-only by default. Local evidence collection makes no host change.
- No action changes the host without an explicit policy approval. Today Descartes changes only two things: its own user-level daemon service, and its alert-state files. There is no general remediation tool yet.
- Descartes does no raw telemetry, no background upload, and no federation, unless you turn it on.
- Alert-intelligence LLM wakeups are off by default. When on, they are rate-limited and
audited. That path has no remediation or shell tools (
enableTools:false). - Notification delivery is off by default. It needs an explicit setup and test step.
- Descartes hashes most detector identity values at the source with a domain-specific SHA-256 scheme before they are persisted. This is not universal — see Field-level data handling for what is hashed, what is only sanitized to a human-readable local form, and what a triage session can send to the selected model provider.
- Missing or damaged evidence becomes
unknownor a skip. Descartes never invents a security signal or a health signal. descartes incident freezesaves a Descartes-owned forensic evidence bundle. It calls only the registered read-only evidence tools. It changes nothing on the host. Only an operator can start it. Descartes never sends the bundle to an LLM. Seedocs/reference/incident-freeze.md.- Descartes can use Pi inside itself as a private agent harness. But Descartes does not read,
import, or change the user's own Pi setup. This includes
~/.pi, a project.pi, sessions, settings, auth, skills, prompts, themes, and model config. Descartes never writes to a Pi-owned path.
For the complete safety-invariant list, see AGENTS.md.
| Field | Local fact-store / metric history | Sent to the model during triage |
|---|---|---|
| tmux/screen session name | SHA-256 hash (session.name) | not sent |
| cron / systemd-timer / launchd job identity | SHA-256 hash | not sent |
| process exec-chain edge (parent/child comm) | SHA-256 hash | not sent |
| watched credential path | SHA-256 hash (path_hash); contents never read |
not sent |
| launchd/systemd service name | sanitized (charset-substituted, truncated) — human-readable form stays in local history | not sent |
| canary id | sanitized, not hashed | not sent |
| listening socket protocol/address/port and owning command | sanitized, not hashed — an IP and port stay legible in local history | not sent |
| top-process command | raw, in local metric history (sensitivity: process_identity) |
raw, in the compact evidence summary sent to the selected model provider |
| top-process PID and (truncated) args | not persisted to metric history | raw, in the compact evidence summary sent to the selected model provider |
| hostname | not persisted to metric history; appears in the transient triage evidence envelope and in an incident freeze bundle if you take one (freeze bundles stay local — never sent to an LLM) |
raw, in the compact evidence summary sent to the selected model provider |
| disk mount point | raw, in local metric history (sensitivity: path) |
raw, but only for a filesystem at ≥90% used, up to 8, as part of pressured_filesystems |
| disk filesystem device path | raw, in local metric history (sensitivity: path) |
not sent |
descartes incident freeze and the local history/alerts commands only ever touch the
local, Descartes-owned store — nothing there leaves the host. A triage request is
operator-initiated diagnosis: the compact evidence summary above is sent to whichever model
provider you logged into with descartes login, per request, only when you run triage.
Alert-intelligence wakeups (opt-in, descartes alerts intelligence enable) are a separate,
rate-limited, audited background disclosure path with no tool/shell access
(enableTools:false); it builds its own sanitized alert-record prompts and does not reuse
this same compact-evidence payload.
- Tier 1 (supported): macOS Apple Silicon and Linux x86_64.
- Best effort: macOS Intel and Linux ARM64.
- Not supported now: Windows, BSD, Android and Termux, remote hosts, and container-only introspection.
Descartes uses the XDG Base Directory conventions. It must not use a Pi-owned path:
| Purpose | Default |
|---|---|
| Config and auth | $XDG_CONFIG_HOME/descartes or $HOME/.config/descartes |
| Data | $XDG_DATA_HOME/descartes or $HOME/.local/share/descartes |
| State and session artifacts | $XDG_STATE_HOME/descartes or $HOME/.local/state/descartes |
| Cache | $XDG_CACHE_HOME/descartes or $HOME/.cache/descartes |
| Runtime | $XDG_RUNTIME_DIR/descartes when XDG_RUNTIME_DIR is set |
tools/descartes-cli/— the Node.js CLI. It holds triage, the history daemon, the alerts and notifications, the self-learning subsystem, and all L0 collectors (tools/descartes-cli/src/tools/).crates/— the Rust workspace. Today it holdsdescartes-root-helper. This is a/procresolver with no shell and a fixed argv. It supports an optional elevated-read path on Linux. It is a start, not the full durable core inAGENTS.md.docs/plans/— one implementation plan per slice of work. The team reviews and updates each plan.docs/reference/— collector and command reference docs (collectors.md,incident-freeze.md).docs/design/— durable design doctrine. These are living design principles, not point-in-time plans (for example, the autonomy doctrine).docs/research/,docs/reviews/,docs/solutions/,docs/use-cases/— research notes, real-host validation reports, durable learnings, and example scenarios.docs/ROADMAP.md— the longer-term capability roadmap and non-negotiables.docs/HANDOFF.md— the live handoff document. Start here for the current state.todos/— pending, tracked work items.
The Node.js CLI is the main tested surface today:
npm install
npm test # node --test tools/descartes-cli/test/*.test.js
npm run smoke:cli # descartes --helpThe Rust workspace in crates/ builds and tests on its own, apart from the Node CLI:
cargo check --workspace --all-targets
cargo test --workspaceSome Rust code (the elevated /proc paths) runs on Linux only. CI gates it for Linux. But
cargo check still compiles those paths on any host.
The larger Lightless Labs monorepo prefers Bazel. This repo builds with npm and Cargo directly for now. But it stays Bazel-friendly: clear manifests, no hidden generation steps, reproducible tests, and a clean crate graph.
AGENTS.md— the agent instructions: project identity, architecture, lifecycle, safety invariants, and conventions. Read this first.docs/HANDOFF.md— the current handoff document. It points to the live state and the next action.docs/plans/— per-slice implementation plans.docs/ROADMAP.md— the broader capability roadmap.
Two novels sit behind the project's sensibility:
- The Second Angel, Philip Kerr — the source of the name. Descartes is the computer running the literal Blood Bank on the Moon: a machine entrusted with keeping critical infrastructure alive. That is the whole posture, made humble — observe first, act only when allowed.
- Absolution Gap, Alastair Reynolds — for the layered approach. Survival there is a matter of stratified, defense-in-depth reflexes and escalation rather than one all-seeing intelligence, which is exactly how Descartes' L0–L3 layering is meant to work.