Skip to content
codecoradevPublic

About

Durable Rust agent harness: chat-first personal assistant with mission mode — resumable after crashes, replayable forever

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

tole

CI crates.io crates.io License: MIT

Durable Rust agent harness: a conversational agent with risk-tiered approval gates, a write-once JSONL session log, and a register state machine — resumable after crashes, replayable forever.

Status: v0.9.0 is the latest release: tole doctor, soft interrupts, per-turn token stats, sibling change notices and the typed child-agent handoff, a provider failover chain, the user config layer, --trust workspace, command-level allow/deny patterns, a global session index (sessions --all) and context compaction, plus the 0.8.2 tole serve hardening (see the CHANGELOG). develop is working toward 0.10.0, a pi-style terminal UI (tole-tui, #464). v0.8.1 itself brought token-usage reporting and a capped, resumable read_file (#211), on top of v0.8.0, which added the per-project config .tole/config.toml (trusted before it applies; see the Configuration section) on top of v0.7.x (rescan-2 hardening, one tool-call authorization gate; see the CHANGELOG for the behavior changes). Four faces on one durable core: the CLI (run / chat / resume / sessions / jobs / mission), tole mcp (tool server), tole acp (editor agent), and tole serve (REST + multi-session MCP-over-HTTP daemon). Identity (owner-approved): a chat-first personal assistant WITH a mission mode for autonomous work — budgeted turn-chaining (tole mission --max-steps/--max-minutes/ --max-tokens/--verify), durable task-list tools (todo_write/ todo_read), cost reports, and remote approvals via the serve face (the 0.9.0 mobile track consumes them from a phone). Shipped in 0.7.0: ACP intra-turn visibility + true text streaming, ACP auto model picker, run ergonomics (--prompt-file/--name/--timeout), read-only web_fetch/web_search (SSRF-guarded), and startup update notification + tole upgrade. See CHANGELOG.md and docs/epics.md.

Ecosystem position

tole is the agent harness of the CodeCora ecosystem — the hands:

  • cora-code (cora) — code intelligence. Attached automatically when the cora binary is on PATH: tole detects the built-in cora mcp server and registers its 18-tool surface (brain search, callers, impact, affected tests, dead-code, review — registry names mcp_cora_*). Opt out with --no-auto-mcp. Code review (cora ≥ 0.19): with the review tools attached, the system prompt names cora's agent-as-reviewer flow — the model passes a diff to mcp_cora_cora_review_prepare, judges it itself (no LLM key in cora), then calls mcp_cora_cora_review_finalize with the same diff and its findings. --trust internal allows the mcp_cora_* tools without prompting.
  • uteke (uteke) — semantic memory. uteke_recall (recall) and uteke_document (markdown → durable knowledge) register automatically when the uteke binary is on PATH, and the optional memory loop closes the circle.
  • Supported versions: cora ≥ 0.19.0 and uteke ≥ 0.21.1 (both optional). tole doctor reports each one as supported, too old (with the upgrade command) or not installed.
  • Anything else that speaks MCP over stdio joins the same registry via repeatable --mcp-server name=command [args...] flags.
  • tole as an MCP server: tole mcp serves the registry's hardened tools (jailed file ops, git, jobs, memory) to any MCP client. ReadOnly tools are always callable; Write tools require --allow patterns; Destructive tools are structurally absent.
  • tole as an ACP agent: tole acp speaks the Agent Client Protocol — editors (Zed et al.) drive durable tole sessions, and tool approvals surface as permission requests in the editor. Native pickers via ACP config options (#176): TOLE_MODELS (comma-separated ids) advertises a model switch that persists durably per session across resumes, and an approval selector (ask / auto, session-scoped) plus an allow_always option on Write permission requests let the human relax the gate from the editor without weakening the Destructive tier.
  • tole as an HTTP daemon: tole serve (issue #96) exposes the session host over token-authenticated REST — remote clients create sessions, run turns, and poll status without SSH-ing into the box. Binds 127.0.0.1 by default. POST /sessions/{id}/cancel (#178, REST face) stops an in-flight turn — it settles stopReason: "cancelled". POST /sessions/{id}/interrupt (#377) steers the running turn instead, without losing its work: the {"text", "urgent"} message is injected at the turn's next safe point as an attributed entry (409 when no turn is running). --transport mcp (issue #137) serves multi-session MCP over Streamable HTTP instead: one authenticated MCP connection addresses N durable tole sessions (tole_session_new/prompt/cancel/interrupt/status/list + the registry tools routed to each session's workspace jail).

Every ecosystem integration is probe-first: a missing binary degrades to a one-line warning, never a phantom tool.

Install

cargo install tole-cli        # the `tole` binary from crates.io
# or build from source:
git clone https://github.com/codecoradev/tole && cd tole
cargo build --release -p tole-cli && ./target/release/tole --help
# `mcp-http` (needed by `tole serve --transport mcp`) is a default
# feature since #168 — a plain build/install already includes it.

Quickstart

# Any OpenAI-compatible endpoint works; TOLE_* is canonical,
# OPENAI_* is read as a fallback.
export TOLE_BASE_URL=https://api.openai.com/v1
export TOLE_MODEL=gpt-4o-mini
export TOLE_API_KEY=...

tole run "summarize the last commit"        # one durable turn
tole chat                                   # multi-turn REPL on one durable session
tole chat --memory uteke                    # …with cross-session memory
tole chat --resume <id>                     # pick the thread back up (also: --last)
tole resume <id> "next instruction"         # continue a settled session
tole sessions                               # list durable sessions
tole sessions --all [--grep <text>]         # every project's sessions + where they live
tole status <id>                            # pc / seq / turns / token + cache usage / request-size split

Sessions live in .tole/sessions/<id>.jsonl (override with --sessions-dir): append-only, crash-safe, replayable — kill the process mid-turn and resume exactly where it stopped.

Useful flags (all subcommands): --system (persona, pinned in the session header), --workspace <dir> (file-tools jail root), --allow <glob> (repeatable pre-authorization for Write tools, e.g. --allow 'write_*'; run_command:<argv> matches commands by argument, e.g. --allow 'run_command:git status *' — a lone * is "any further arguments", the program is matched literally; a leading ! denies and wins over every allow and --yes, e.g. --allow '!run_command:rm *'; with a command deny set, only commands an explicit run_command: allow names run unattended — everything else asks), --yes (auto-allow every Write; Destructive still prompts), --mcp-server, --no-auto-mcp (skip the cora auto-preset), --trust <preset> (one-word trust: internal / read_only / workspace — read_only plus write_file/edit_file auto-allowed only inside an explicit --workspace jail, and not for files another tool may execute — dot-paths (.git/hooks, .envrc, .vscode/), build and task files (build.rs, Makefile, package.json, ...) and scripts keep prompting, as do delete_file and run_command; a home or filesystem-root workspace is refused, and the guard is also available as --allow 'write_file@workspace'. Not a sandbox: review what it wrote before you run it), --skill <path> (load a SKILL.md), --plan-mode (read-only wire), --on-turnend <cmd> (final-message gate).

Configuration (environment)

Variable Purpose
TOLE_BASE_URL, TOLE_MODEL, TOLE_API_KEY LLM provider — any OpenAI-compatible endpoint (OPENAI_* equivalents read as fallback)
TOLE_FALLBACK_BASE_URL, TOLE_FALLBACK_API_KEY, TOLE_FALLBACK_MODEL optional reserve provider (#209): a step that fails because the primary is unavailable (connection failure, timeout, 5xx, 429) is retried on the reserve, and the switch is recorded in the session log (tole_route). The reserve needs its own key (the primary's is never sent to another host); the model defaults to the primary's. Request errors (401/403/404/400) never fail over
TOLE_SYSTEM_PROMPT default persona when --system is absent
TOLE_CONTEXT_WINDOW the model's context window in tokens, for context compaction (#440). Without it, tole reads the window from the provider's /models (context_length, max_model_len, ...) once a prompt passes 8k tokens; with neither, compaction stays off. chat, resume and mission compact between turns once a prompt reaches --compact-at (default 0.8) of the window: the settled turns before the last one become one durable compaction entry (a fenced summary), the session file keeps everything, and tole status lists each compaction. --no-compaction (or config no_compaction = true) turns it off; config keys context_window, compact_at
TOLE_SKILL skill files to preload (SKILL.md paths, separated like PATH); same as repeated --skill, which replaces it; refused by the server faces like --skill
TOLE_MEMORY memory loop backend (uteke) — same as --memory uteke
TOLE_NO_UPDATE_CHECK 1 disables the startup update-check banner (issue #220)
TOLE_VERBOSE 1 prints the startup probe misses (uteke / cora binary not found, no GitHub origin), which are silent by default; tole doctor reports the sibling binaries either way
TOLE_MEMORY_NAMESPACE override the loop's uteke room (default: repo-<directory name>; the name predates rooms)

Provider doctor

tole doctor answers "is this provider/model actually usable?" in tiers:

--tier Checks
offline config resolves, base URL shape, key present (no network)
catalog (default) live GET /models; the configured model must be served
full a chat call, a streamed call and a real tool-call loop (spends a few tokens)

Tiers are cumulative and stop at the first blocker, which is printed with the command that fixes it. The exit code is non-zero when the requested tier is not reached, so tole doctor --tier full is the gate for "ready to release" claims. API keys and URL credentials never appear in the output.

After the provider tiers, an ecosystem: section reports the sibling binaries against the supported floor (cora ≥ 0.19.0, uteke ≥ 0.21.1). It is informational: an old or missing sibling never fails the run.

Every run is recorded in a coverage ledger ($CODECORA_HOME/tole/doctor-ledger.json, owner-readable only: level reached, first blocker, fix, timestamp per provider

  • model; --no-ledger skips it). tole doctor --summary prints READY n-of-m plus the first blocker and its fix command without running anything; the latest run wins per provider + model, so re-run --tier full before a release claim.

Configuration (project and user files)

<cwd>/.tole/config.toml (no parent-directory walk) holds per-project defaults. It is loaded by every command except config, upgrade and approvals; use --config <path> to name another file (a path given on the command line is trusted without a trust record) or --no-config to ignore any file entirely (no discovery, no trust check, no output). When a config is loaded, one line tole: using config <path> goes to stderr. Precedence: flag > env > config > default, and a project without a config file behaves exactly as before.

User config (#433). $CODECORA_HOME/tole/config.toml (default ~/.codecora/tole/config.toml) holds a user's global defaults with the same keys and validation, e.g. a skill to preload everywhere. It sits below the project file: flag > env > config > user config > default, key by key (a project key replaces the user key, lists included). It needs no trust record (it lives outside any repository, like the trust store), prints tole: using user config <path>, and --no-config ignores it too. tole config check lists both files and shows which layer wins each key.

Keys. Every key is optional; the schema is strict (an unknown key, a wrong type or a secret-like key is an error with file:line; the file is capped at 64 KiB).

Key Mirrors Effective value
model, base_url, system_prompt, memory the provider env, --system, --memory flag > env > config
sessions_dir, workspace --sessions-dir, --workspace flag > config > default
[mission] max_steps, max_minutes, max_tokens mission --max-* flag > config > budget tier
[mission] verify, verify_timeout mission --verify, --verify-timeout flag > config > default (300)
trust (list of presets) --trust flag > TOLE_TRUST env > config
allow (list of globs) the subcommand's own --allow flag > config
mcp_server (list) --mcp-server flag > config
on_pretool, on_posttool, on_turnend (lists) --on-pretool / --on-posttool / --on-turnend flag > config
skill (list of paths) --skill flag > config (paths resolve against the cwd)
plan_mode, no_auto_mcp, no_skills (booleans) --plan-mode, --no-auto-mcp, --no-skills flag || config

The API key only ever comes from the environment (a secret-like key in the file is an error). An empty env variable counts as unset.

  • Lists are replaced wholesale. A higher layer replaces the whole list for that key; lists are never merged. A non-empty --allow list replaces the config allow list entirely (an empty flag list means "not given"), exactly like --trust over TOLE_TRUST. Every subcommand has its own --allow; the config allow is the fallback when THAT subcommand's --allow is empty.
  • Booleans can only be turned on. effective = flag || config: a flag can turn a setting on but there is no flag that forces it off, so a config true is dropped only with --no-config (or by editing / untrusting the file). tole config check says so next to each boolean.
  • Same effect as the flag. plan_mode = true removes the write/delete/run tools from the wire like --plan-mode; allow/trust feed the same allowlist as the flags, so Destructive tools still always prompt (they can never be allowed through the file, and on the non-interactive faces they stay unregistered).
  • Faces that refuse a flag refuse it from the config too. serve, acp, mcp and mission refuse --on-pretool/--on-posttool (and --on-turnend where the flag is refused), --skill, --no-skills, --mcp-server (and --plan-mode on mission). A value that arrives from the config is refused with the same message plus use --no-config to ignore the project config; tole never silently drops a project's safety hook.
  • Feature-less builds. In a build without the mcp feature, mcp_server / no_auto_mcp in the config is an error naming the key and the feature; without shell-tools the same holds for the hook keys and memory.

tole config check prints every key that is set with its effective value and where it came from (flag, env VAR, config, default), using the same resolution code as startup (lists show the effective list, booleans effective: true (config) or (flag)).

The file is untrusted input (it lives in a cloned repo), so it needs a content-bound approval before it takes effect:

  • On an interactive command (run, chat, resume, mission) with a terminal on both stdin and stderr, an untrusted or changed config is shown in full and you are asked trust this config? [y/N]. Everywhere else (sessions, status, serve, acp, mcp, no terminal, or --prompt-file -) tole FAILS CLOSED with the exact instruction (tole config trust) and runs nothing; serve/acp/mcp never ask because their stdin/stdout are protocol channels.

  • tole config trust [--config <path>] [--yes] validates the file, prints its path and FULL content (plus a line diff against the previously trusted version) with control and bidi characters escaped, then asks [y/N]. Without a terminal it refuses unless --yes is given (the content is still printed).

  • The approval is stored outside the repo in $CODECORA_HOME/tole/trusted-configs.json (default ~/.codecora, mode 0600) as a snapshot of the file keyed by canonical project directory. A config is trusted only while its bytes are identical to that snapshot; any edit makes it untrusted again.

  • tole config untrust [--config <path>] removes the record (no error if absent). tole config check prints trust: trusted, trust: NOT trusted or trust: CHANGED since it was trusted; its exit code stays 0 for a valid file whatever the trust state.

  • A corrupt or unsupported-version store is an error naming the file; tole never overwrites it.

The schema is strict (unknown keys, wrong types, malformed TOML, unknown trust presets and files over 64 KiB are errors, reported as path:line:col: message). Flat snake_case keys mirror the global flags (model, base_url, sessions_dir, workspace, plan_mode, memory, trust, allow, mcp_server, no_auto_mcp, on_pretool, on_posttool, on_turnend, skill, no_skills, system_prompt) plus one [mission] table (max_steps, max_minutes, max_tokens, verify, verify_timeout). Secrets never belong in a project file: any key whose name contains secret, password, token or api_key (any depth, any case) is a hard error.

Tools

Tool Risk Notes
read_file, write_file, edit_file RO / Write jailed to --workspace (TOCTOU-safe, symlink-refusing); read_file returns at most 20,000 chars per call (offset/limit in chars, next_offset to continue)
delete_file Destructive always prompts; never allowlistable, even with --yes
git Write status / diff / add / commit only — push stays human
gh Write read-only ops, argv-validated per op; repo defaults to the checkout's GitHub remote, optional per-call override (validated)
run_command Write argv-split (no shell), cwd-jailed, timeout + output cap
verify_package RO crates.io / npm registry check before any install — hallucinated names get NOT FOUND + candidates, edit-distance-1 candidates get a typo-squat warning
load_skill RO loads a discovered SKILL.md on demand (--skill pins one upfront; --no-skills disables)
gitea Write Gitea counterpart of gh over the instance REST API — registers when origin is a Gitea remote AND TOLE_GITEA_TOKEN/GITEA_TOKEN is set; same six ops
agent_start, agent_poll Write / Write depth-1 child agents: spawn durable child sessions (structurally no grandchildren), results via per-child uteke mailboxes (ephemeral by default); agent_poll is Write because a settled poll consumes (cleans) the mailbox, and --trust internal allowlists it by name; --agents-worktree gives each child its own git worktree; siblings are told (#380) when another child changes a file they read or wrote, at their next safe point; role (explore/implement/verify/fix) and consumes (artifacts by reference) give children a typed handoff, and an implement/fix child without passing verification is not closed (#383)
systemone_decide RO typed decisions (choice/score/noul + confidence) from a System One backend — active when SYSTEMONE_API_KEY is set; SYSTEMONE_BASE_URL picks the backend (hosted Jev default, self-hosted compatible)
job_start, job_poll Write / RO detached long-running jobs with log tailing
cora_search RO hybrid codebase search via cora brain; native fallback — skipped when the cora MCP surface is attached
uteke_recall, uteke_document RO / Write semantic memory recall / markdown → room
mcp_* (from --mcp-server) Write server metadata is never trusted for risk; approval gate always applies
web_fetch, web_search RO text-only internet (no JS/browser): fetch is direct HTTPS, size-capped, content-type allowlisted, HTML→text; search needs TOLE_WEB_SEARCH_URL (default contract GET ?q= → {"results":[{title,url,snippet}]}; TOLE_WEB_SEARCH_METHOD=POST sends {"query","limit"}, TOLE_WEB_SEARCH_HEADERS='Authorization=Bearer ${KEY}' adds headers sent only to the backend, TOLE_WEB_SEARCH_FIELDS='snippet=description,results=items' maps fields; a malformed value fails startup) — no backend, no tool; results are model content, never executed
todo_write, todo_read Write / RO durable mission task list (at most one in_progress); state lives in session entries — write results are the record, crash-resume restores the last settled list; covered by --trust internal

Remote approvals

tole serve --token $TOLE_SERVE_TOKEN          # on the box
tole approvals list --url http://box:7801 --token $TOKEN
tole approvals allow apr-... --url http://box:7801 --token $TOKEN

When a serve-face session hits a non-preauthorized Write (#200), the pending decision becomes a queue entry and the turn settles resumably (fail-closed). A remote operator lists and decides: allow stores a one-shot approval and resumes the session (the replayed effect re-consults the gate — exactly once), deny records the verdict. Entries expire to denied (15 min) so nothing hangs silently; every decision lands a durable audit register on the session. MCP/ACP parity is a follow-up. This is also the surface uteke-mobile drives — docs/mobile-control.md is the mobile-control guide (#202).

Mission mode

tole mission "ship the feature" --max-steps 48 --max-minutes 15 \
  --verify "cargo test" --yes

Autonomous turn-chaining toward a goal (#199): the existing turn machinery looped until the model declares MISSION_COMPLETE (and --verify exits 0, when set) or a budget trips (--max-steps, --max-minutes, --max-tokens). Every chained turn is a normal durable turn — crash mid-mission resumes exactly where it stopped (tole mission --resume <id>, or plain tole resume), cancel works unchanged, Destructive tools stay un-auto-allowable, and a durable summary lands on the session either way. Plans ride the todo_write/todo_read tools (#198); budget exhaustion settles resumably, never a dead session. Run ergonomics (#216): run --prompt-file <path> (- = stdin), --name <alias> (stored in the session header — tole sessions shows it and resume accepts the alias), and --timeout <secs> (wall-clock cap; expiry cancels at a checkpoint — resumable, never dead). Budget tiers (#201): conservative defaults (48 steps / 15 min / 200k tokens) with headroom under --trust internal (96 / 30 / 500k) — explicit flags always win; a durable cost report (turns, steps, tokens, per-risk-tier tool calls, cached tokens) lands on the session and tole status renders it. tole status (and GET /sessions/{id}/status, as usage_report) also shows the usage derived from the ledger for any session: prompt / completion / reasoning / cached tokens, the cache-hit rate, and the request size in characters split into system+tools vs history at the first and last step (#211; recorded per step under tole_wire; sessions recorded before it read n/a). It also lists per-user-turn stats (#379: input / output / cache-read / cache-write / reasoning tokens, turns in the status JSON), summed from the same ledger so they survive restarts; a value no step reported is n/a / null, never 0. Reported only — --max-tokens accounting is unchanged.

Approval gates

Every non-ReadOnly call goes through an Approver. Destructive tools are denied structurally: the registry refuses them without an interactive approver, allowlists deny them on sight, and --yes never covers them. There is no bypass path. Secret redaction is wire-only (the durable local log keeps the original text by design).

Memory loop

--memory uteke (or TOLE_MEMORY=uteke) turns the harness into a cross-session memory loop — host-initiated on both ends, the model can neither trigger nor suppress it:

  • Before the first turn of a fresh session, memories relevant to the prompt are recalled from the owner's uteke store and injected into the message inside a clearly marked fenced block; the durable log records exactly what the provider saw.
  • When the session settles with a final answer, a compact summary is stored back — the next session's recall can find it.

Memory lives in the uteke room repo-<dir> (uteke ≥ 0.21.1; rooms replace the deprecated namespaces; TOLE_MEMORY_NAMESPACE overrides the room). Memory stored by earlier tole versions in the namespace of the same name is still recalled while the room is empty. The injected batch is hard-capped at 24 items / 64 KiB (a larger limit is clamped; a hit is never cut mid-way) and deduplicated. A batch is all-or-nothing: an unparseable recall payload fails the whole batch with a stderr note rather than injecting fewer items, and the turn proceeds without memory.

Workspace layout

  • crates/tole-core — platform-agnostic library: state machine, JSONL storage, Tool/Approver/Storage/Provider traits, tools, and the OpenAI-compatible provider (crates.io; embedders build with --no-default-features — CI keeps the mobile targets compile-checked).
  • crates/tole-cli — headless CLI host (the tole binary).
  • evals/ — agentic eval harness: Tier 1 CI contracts, Tier 2 live missions, Tier 3 release baselines (see evals/README.md).

Documentation

Development

cargo fmt --all
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
cora review --staged      # ecosystem-standard review gate before every commit

CI enforces the same set across Linux, plus cross-compile checks for aarch64-apple-ios and aarch64-linux-android.

Design sources

Research notes and design sources: adapted from pi's harness spec (MIT).

License

MIT.

About

Durable Rust agent harness: chat-first personal assistant with mission mode — resumable after crashes, replayable forever

Resources

Code of conduct

Contributing

Security policy

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages