The Machine (TM)
A local AI agent that can control your machine, extensible and permission-gated.
TM is a Python agent harness inspired by the architecture of pi, rebuilt around explicit permission approval for filesystem, shell, and network access.
Status
Core agent, providers, built-in tools, permissions, and CLI are working. See the phase table below.
Installation
the-machine is on PyPI. uv
is recommended (it can also install Python for you).
Install uv (once):
# Windows (PowerShell)
irm https://astral.sh/uv/install.ps1 | iex
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh
Install the tm command (recommended)
uv tool install "the-machine[all]"
uv tool update-shell # add the tool bin dir to PATH; restart the terminal afterwards
tm --help
Or with pip, inside a virtual environment:
pip install "the-machine[all]"
[all] pulls in the Anthropic, Google, and TUI extras. Drop it for a minimal
install (OpenAI-compatible providers only; --tui then needs textual).
From source
uv tool install "the-machine[all] @ git+https://github.com/beyondalbert/tm"
# without git:
uv tool install "the-machine[all] @ https://github.com/beyondalbert/tm/archive/refs/heads/main.tar.gz"
From a clone (development)
git clone https://github.com/beyondalbert/tm
cd tm
uv sync --all-extras
uv run tm --help
First run
tm --login deepseek # store an API key (input visible)
tm "list the files in this folder"
Requires Python 3.11+. uv installs a suitable interpreter automatically.
Updating
tm --update
tm --update checks PyPI and upgrades in place. It uses whichever installer owns
the environment (uv tool, pipx, or pip), so uv is not required. On Windows the
running tm.exe cannot replace itself, so the upgrade runs in a small detached
helper (log: <config>/update.log); exit tm and start it again when it finishes.
Providers
Multi-provider via official SDK adapters. Domestic providers first (DeepSeek, Qwen, Kimi, Zhipu GLM), then OpenAI and any OpenAI-compatible endpoint (Ollama, vLLM, LM Studio).
Set the API key for the provider you want, then run:
$env:DEEPSEEK_API_KEY = "sk-..."
python -m uv run tm "list the files in this folder"
Where to set the API key
Three options, in order of convenience:
-
Store it once (recommended). Prompts for the key (visible, so you can confirm it) and saves it to the credentials file:
python -m uv run tm --login deepseek
After saving, TM echoes back a masked preview (for example
sk-000...0000 (35 chars)) so you can confirm it was entered correctly.Saved to
<config>/credentials.toml:- Windows:
%APPDATA%\the-machine\credentials.toml - Linux/macOS:
~/.config/the-machine/credentials.toml
The file looks like:
[providers] deepseek = "sk-..." qwen = "sk-..."
- Windows:
-
Environment variable (session or persistent):
# current shell only $env:DEEPSEEK_API_KEY = "sk-..." # persistent for your user (restart the terminal afterwards) [Environment]::SetEnvironmentVariable("DEEPSEEK_API_KEY", "sk-...", "User")
-
Override the config location with
TM_CONFIG_DIRif you do not want the default directory.
Stored credentials take precedence over environment variables.
Providers and env vars:
| Provider | Id | Env var | Default model |
|---|---|---|---|
| DeepSeek | deepseek |
DEEPSEEK_API_KEY |
deepseek-v4-pro |
| Qwen (DashScope) | qwen |
DASHSCOPE_API_KEY |
qwen-plus |
| Moonshot (Kimi) | moonshot |
MOONSHOT_API_KEY |
moonshot-v1-32k |
| Zhipu (GLM) | zhipu |
ZHIPUAI_API_KEY |
glm-4-plus |
| SiliconFlow | siliconflow |
SILICONFLOW_API_KEY |
DeepSeek-V3 |
| Anthropic | anthropic |
ANTHROPIC_API_KEY |
claude-3-5-sonnet-latest |
| Google Gemini | google |
GEMINI_API_KEY |
gemini-2.0-flash |
| OpenAI | openai |
OPENAI_API_KEY |
gpt-4o |
| Groq | groq |
GROQ_API_KEY |
llama-3.3-70b-versatile |
| OpenRouter | openrouter |
OPENROUTER_API_KEY |
claude-3.5-sonnet |
| Together AI | together |
TOGETHER_API_KEY |
Llama-3.3-70B |
| xAI | xai |
XAI_API_KEY |
grok-2-latest |
| Ollama (local) | ollama |
– | qwen2.5:7b |
Anthropic and Google use their official SDKs; the rest use the OpenAI-compatible
adapter. ollama and other local servers need no key.
List models: tm --list-models.
Custom providers and models
Define your own provider (any OpenAI-compatible, Anthropic, or Google endpoint)
or extra models in <config>/models.toml (Windows:
%APPDATA%\the-machine\models.toml). It is merged over the built-in catalog, so
tm --list-models and /model pick it up without editing code.
[providers.myprovider]
name = "My Provider"
api = "openai-completions" # or anthropic-messages / google-generative-ai
base_url = "https://api.example.com/v1"
api_key_env = "MYPROVIDER_API_KEY" # a name, or a list of names
default_model = "my-model"
[[providers.myprovider.models]]
id = "my-model"
context_window = 32768
max_tokens = 4096
input_cost = 0.5 # optional: USD per 1M tokens
output_cost = 1.5
cache_read_cost = 0.05
# add or override models on a built-in provider
[[providers.deepseek.models]]
id = "deepseek-v4-lite"
context_window = 65536
Store the key with tm --login myprovider, or set the env var named above.
Switch to a custom model with /model my-model or tm --model my-model.
Modes
| Command | Behavior |
|---|---|
tm |
Interactive TUI (Textual) with tools and permissions |
tm "prompt" |
One-shot agent run |
tm --no-tui |
Plain console REPL instead of the TUI |
tm --tui |
Force the TUI (default when interactive) |
tm --read-only |
Only read/grep/find/ls tools |
tm --no-python |
Disable the python tool (shell stays) |
tm --dry-run |
Show what would change without mutating the machine |
tm --no-tools |
Plain chat, no machine control |
tm --yolo |
Auto-approve every action |
tm -p |
One-shot, reads stdin when no prompt is given |
tm --json |
Emit agent events as JSON lines (for integration) |
tm -c |
Continue the most recent session here (the default) |
tm --new-session |
Start a fresh session instead of continuing the latest |
tm -r |
Pick a saved session to resume (--all-sessions to include other dirs) |
tm --no-extensions |
Skip loading .aiagent/extensions |
tm --no-auto-compact |
Disable automatic context compaction |
tm --max-turns N |
Max agent turns per prompt (default 300) |
tm --max-retries N |
Retries for transient network errors (default 3) |
tm --telemetry |
Write redacted spans to <config>/telemetry.jsonl |
tm --durable |
Persist a durable restart point per run to <config>/state.jsonl |
tm --no-mouse |
Let the terminal handle mouse selection/copy |
tm --approve |
Trust project-local files for this run (-a) |
tm --no-approve |
Ignore project-local files for this run (-na) |
Context files (AGENTS.md / CLAUDE.md, walking up from cwd, plus the global
config dir) are appended to the system prompt. Disable with --no-context-files.
When stdout is not a terminal (piped or redirected) or textual is not
installed, tm falls back to the console REPL automatically.
Copying text. The TUI captures the mouse for in-app selection and scrolling.
Drag to select text and press Ctrl+Shift+C to copy it. Copying uses the real
system clipboard (clip/Set-Clipboard on Windows, pbcopy, wl-copy, or
xclip/xsel), with an OSC52 fallback for terminals that support it. With no
selection, Ctrl+Shift+C copies the last reply. You can also run /copy [n] to
copy the nth-from-last reply. If copying does nothing (some terminals intercept
Ctrl+Shift+C), start the TUI with --no-mouse and use the terminal's own
selection (Shift+drag on most terminals). Set mouse = false in settings to make
that the default.
Permissions
Every file, shell, and network action is evaluated against a policy. Deny rules
win, then allow rules, then the default decision. Reads are allowed by
default (default_read = "allow"); writes, shell, and network default to
ask. Undecided actions prompt the user; a (always) remembers the scope and
persists it to <config>/approvals.json, so it survives a restart. All decisions
are appended to <config>/audit.jsonl.
To reduce prompts during a session, /auto on (or Ctrl+Y in the TUI) auto
approves everything that is not explicitly denied for the rest of the session;
/auto off returns to prompting, and tm --yolo starts in that mode. A built-in
list of destructive commands always asks and is never remembered, as does
elevate.
Policy files are merged from <config>/policy.toml (global) and
<cwd>/.aiagent/policy.toml (project). Example:
default = "ask" # writes, shell, network
default_read = "allow" # reads (set "ask" to be prompted)
[files.read]
allow = ["**"]
deny = ["**/.env", "**/id_rsa"]
[files.write]
allow = ["src/**", "tests/**"]
deny = [".git/**"]
[shell]
allow = ["git *", "ls *", "pytest *"]
deny = ["rm -rf *", "shutdown*", "format *"]
[network]
allow = ["api.deepseek.com"]
File rules are folder-aware: a rule naming a directory (src, src/, or
src/**) covers everything under it, so you can control access per folder
instead of per file. When the policy leaves an action as ask and you answer
a (always), TM remembers the scope: the containing folder for file actions,
the program for shell commands (git covers git status, git log, ...),
and the host for network actions.
Note: shell commands are matched textually. TM cannot reliably stop a shell command from making network calls; use a sandbox/container when you need a hard network boundary.
Project trust. Project-local resources that can run code or change the policy
(.aiagent/extensions, .aiagent/skills, .aiagent/prompts, .agents/skills,
.aiagent/policy.toml, .aiagent/SYSTEM.md) are loaded only after you trust the
directory. TM asks on first use, remembers the decision in <config>/trust.json,
and honors --approve/--no-approve, /trust [off], and the
default_project_trust setting (ask | always | never). Context files such
as AGENTS.md are not gated. Non-interactive runs (-p, --json, --mode rpc)
do not prompt and decline by default, so allowlist what they may do in
policy.toml or start them with --yolo.
Changes and undo. Mutating tools append to <config>/changes/changes.jsonl.
File writes/edits snapshot the previous content; package installs and service
start/stops record enough to reverse them, so /undo [n] restores the previous
state. Destructive shell commands (a built-in deny list) always need an explicit,
non-remembered approval. elevate is never auto-approved or remembered. Use
tm --dry-run to preview mutations without applying them.
Interactive commands
Inside tm (agent REPL or TUI) type / for commands:
| Command | Description |
|---|---|
/help |
list commands |
/model [pattern] |
list models or switch model |
/new |
start a new session |
/session |
show current session id/path |
/resume [n|id] |
resume a saved session (/resume opens a picker) |
/tree [n] |
list conversation points, or branch from point n |
/fork [n] |
fork the session (at point n) into a new file |
/compact [note] |
summarize older context |
/recover |
reconcile interrupted durable operations |
/undo [n] |
roll back the last n reversible changes |
/copy [n] |
copy the nth-from-last reply to the clipboard |
/skills |
list available skills |
/skill:<name> |
load a skill into the conversation |
/prompts |
list prompt templates |
/<template> [args] |
expand a prompt template |
/exit |
quit |
Completion. In the TUI, press Tab to accept a suggestion: type / to
complete a command, or @ to fuzzy-search a file path. Up/Down move the
selection and Escape dismisses it.
Input. The TUI prompt is multi-line: Enter submits, Shift+Enter (or
Ctrl+J) inserts a newline, and pasting keeps every line. While a turn is
running, Esc or Ctrl+C stops it and Ctrl+P pauses/resumes it (the status
line shows which). Stopping also kills a running shell/python command.
Network errors. Transient provider failures (timeouts, connection resets,
429/5xx) are retried up to max_retries times with a visible "network problem:
retrying" notice. Non-transient errors (auth, bad request) stop the run.
Sessions
Sessions are appended to JSONL files under <config>/sessions/, tagged with the
working directory. tm continues the most recent session for the current
directory by default (in the TUI, its history is rendered on startup).
Each session file is a full store with three durable forms — an append-only
entry tree, bound values/lists, and an append-only usage ledger.
Writes commit atomically (one transaction per JSONL line; a torn final line
is discarded whole), and the durable operation restart point from --durable
lives in the same store.
tm— continue the most recent session for this directory.tm --new-session— start a fresh session (also/newinside a session).tm -c— explicit form of the default.tm -r— list saved sessions (number, message count, updated time, preview) and pick one;--all-sessionsincludes other directories.tm --session <file>— open a specific file.tm --no-session— run without persisting.
Inside a session, /resume [n|id] switches sessions (a picker modal in the TUI),
/tree navigates conversation points, and /fork copies a branch to a new file.
The TUI re-renders the resumed conversation's history on startup and after
/resume, /new, /tree, and /fork.
The provider/model you switch to with /model is remembered per session and
restored when that session is resumed, overriding the settings default.
Settings
<config>/settings.toml (Windows: %APPDATA%\the-machine\settings.toml):
provider = "deepseek"
model = "deepseek-v4-pro"
temperature = 0.2
max_tokens = 8192
system_prompt = "Extra instructions appended to the system prompt."
auto_compact = true # summarize older context when nearing the limit
compact_threshold = 0.8 # fraction of the context window that triggers it
compact_keep_recent = 6 # recent messages kept verbatim
max_turns = 300 # max agent turns per prompt (also `tm --max-turns N`)
max_retries = 3 # retries for transient network errors (also `--max-retries N`)
default_project_trust = "ask" # ask | always | never
cache_retention = "short" # short | long (provider prompt cache; TM_CACHE_RETENTION overrides)
env_probe = true # add a machine summary to the system prompt
python_executable = "" # interpreter for the python tool (default: the one running TM)
python_timeout = 120 # seconds per python script
workspace_dir = "" # where python scripts are saved (default: <config>/workspace)
auto_install = true # install missing pip packages declared by the python tool
Automatic compaction runs before a prompt when the estimated context exceeds
context_window * compact_threshold. Disable per run with --no-auto-compact.
If a turn ends truncated or with an overflow error, TM compacts and retries once.
Compaction is stored as a durable entry, so the summarized context survives a
restart.
Per-model pricing (input_cost, output_cost, cache_read_cost, USD per 1M
tokens) can be set in models.toml; the footer then shows $cost and the cache
hit rate (CH%).
Machine awareness and the Python fallback
TM inspects the machine before assuming anything, and writes Python when shell one-liners are not enough.
- Environment probe. At startup TM appends a compact summary of the machine
(OS, CPU/memory/disk, GPUs, Python, installed tools) to the system prompt. The
system_infotool returns the same facts on demand (passfullfor tool paths). Disable withenv_probe = false. pythontool. Runs a Python script with a configurable interpreter (default: the one running TM), saves it under<config>/workspace/scripts/so it can be re-run or edited, streams output, enforces a timeout, and returns the traceback on failure. Declarepackagesto have missing pip dependencies installed first (auto-install, gated by the network permission). Disable with--no-python.- Solve ladder. The system prompt tells the model to prefer, in order: an installed tool, the OS-native command, the Python standard library, a package, then a system-level change; to verify every step; and to try before asking.
The architecture and the multi-phase plan live in docs/design/device-management.md.
Tool output. Command and file output is capped at 2000 lines or 50KB,
whichever is hit first. Command output (shell, python, elevate) keeps the
end, where errors and the exit code appear, and a non-zero exit code marks
the result as an error; file and search output keeps the beginning (reads add
Use offset=N to continue, grep caps each match line at 500 characters). When
output is truncated, the full text is saved under <config>/workspace/spill/ and
the notice names that file.
Customization
Context files. AGENTS.md / CLAUDE.md (and AGENTS.override.md) are
loaded from the global config dir and walking up from the working directory, and
appended to the system prompt. Disable with --no-context-files.
Skills. A skill is a directory with a SKILL.md (optional frontmatter with
name and description). Load from <config>/skills/, .agents/skills/, or
.aiagent/skills/. Their names/descriptions are advertised in the system prompt;
load one with /skill:<name>.
---
name: code-review
description: Review code for bugs and security issues
---
Review the target for bugs, security issues, and missing tests.
Prompt templates. Markdown files in <config>/prompts/ or
.aiagent/prompts/, invoked as /<filename>; {{input}} is replaced by the
text after the command.
<!-- prompts/review.md -->
Review this for bugs, security, and performance: {{input}}
Extensions. Python modules in <config>/extensions/ or
.aiagent/extensions/. Each defines setup(api) and can register tools, event
listeners, and slash commands. Extensions run with full process permissions.
from tm.tools.base import Tool, text_result
def setup(api):
api.register_tool(MyTool())
api.on("tool_execution_end", lambda event: None)
api.register_command("greet", lambda arg: f"hello {arg}")
Telemetry
Opt in with --telemetry (or telemetry = true in settings). Spans are written
to <config>/telemetry.jsonl. The contract (tm/telemetry/) is deliberately
narrow and never carries prompts, message content, tool arguments, tool
output, or credentials — only structural metadata and counts. redact() is
the single choke point, and adapters (NoopTelemetry, MemoryTelemetry,
FileTelemetry) can only ever see redacted spans.
Durable operations
Opt in with --durable (or durable = true). Each tm run is a durable
operation tracked in <config>/state.jsonl: before an uncertain external
effect (a tool call) the intent is recorded, and it is settled afterwards.
Messages are persisted to the session as they are produced, so the in-flight
call is durable too.
On the next start, TM automatically recovers interrupted operations:
- a
replay_safeeffect (read-only tools) is re-run from the recorded intent and the run continues; - an effect that is not replay-safe is not re-run; it is surfaced to the model as an error result telling it to check the effects;
- an operation with no effect in flight is settled as aborted.
Recovered re-runs still go through the permission gate. See
tm/core/operation.py and tm/core/agent.py.
Architecture
tm/ai– unified multi-provider streaming LLM layer (types, event stream, OpenAI-compatible + Anthropic + Google adapters, registry/catalog)tm/core– agent loop, high-levelAgent, events, sessions, compaction, and the durablestore/operationrestart pointtm/telemetry– vendor-neutral telemetry contract, redaction, adapterstm/tools– built-in machine-control tools (read/write/edit/shell/python/grep/find/ls/system_info, plus process/service/ package/elevate for lifecycle, services, packages, and guided elevation)tm/host– cross-platform machine probing and control (identity, resources, software, processes, services, packages, elevation)tm/safety– change journal, reversible snapshots, dangerous-command guardtm/permissions– policy, allow/deny/ask checker, approval, audit logtm/skills.py,tm/prompts.py,tm/extensions.py,tm/context– resourcestests/evals– faux-provider eval scenariostm/tui– terminal UI (Textual)tm/cli– Typer entry point and slash commands
Development
Set up once (installs the project editable, so source edits take effect with no reinstall):
python -m uv sync --all-extras
Run the app from source:
python -m uv run tm # TUI
python -m uv run tm --no-tui # console REPL
python -m uv run tm "prompt" # one-shot agent run
python -m uv run tm --json "prompt" # stream agent events as JSON (great for debugging)
python -m uv run tm --login deepseek # store an API key
On Windows, start.bat (cmd) and run.ps1 (PowerShell) wrap the above and set
up dependencies on first run:
.\start.bat # or: .\run.ps1
.\start.bat --no-tui # console REPL
.\start.bat "prompt" # one-shot
Tests and quality gates (same as CI):
python -m uv run pytest -q # full suite
python -m uv run pytest tests/evals -q -m eval # offline eval scenarios
python -m uv run python scripts/run-evals.py # eval report
python -m uv run pytest tests/test_tui.py -q # one file
python -m uv run pytest -q -k tool # by name
python -m uv run ruff check . # lint
python -m uv run mypy # types
python -m uv build # wheel + sdist
Before a release, scripts/release-smoke.ps1 builds, validates metadata in a
throwaway venv, and runs tm --version / tm --list-models.
Secret scanning. CI runs gitleaks (.github/workflows/secret-scan.yml,
configured by .gitleaks.toml), and tests/test_no_secrets.py fails the suite
if a tracked file looks like it contains a committed secret. Never put real keys
in tests, docs, or fixtures; use an obvious placeholder such as sk-xxxx.
Notes:
- The live DeepSeek test only runs when
DEEPSEEK_API_KEYis set;scripts/smoke_deepseek.pyis a standalone live smoke test. - TUI behavior is tested headlessly with Textual's
run_test(seetests/test_tui.py): set the#promptvalue, press enter, wait for workers, then assert onapp.query(".assistant")etc. - Do not use
uv tool installwhile developing: it copies the code (edits won't show) and can fail with a file lock iftmis running. To test a real install, use a throwaway tool dir:UV_TOOL_DIR=$env:TEMP\tm-tool uv tool install ".[all]".
uv.lock is committed. CI (.github/workflows/ci.yml) runs ruff and mypy on
Linux, and the test suite on Linux, Windows, and macOS for Python 3.11 and 3.12.
See RELEASE.md for publishing to PyPI.
Roadmap
| Phase | Scope | Status |
|---|---|---|
| P0 | scaffolding, config, CLI | done |
| P1 | ai layer + domestic providers | done |
| P2 | agent loop, tools, events | done |
| P3 | built-in tools | done |
| P4 | permission system + CLI wiring | done |
| P5 | Textual TUI | done |
| P6 | session persistence (JSONL tree) | done |
| P7 | Anthropic + Ollama providers | done |
| P8 | context files, skills, prompt templates, extensions | done |
| P9 | compaction, print/json mode, packaging | done |
| P10 | sessions: resume picker, fork, tree navigation | done |
| P11 | Google Gemini + extra providers, TUI streaming/status | done |
| P12 | device management A: host probe, python tool, solve ladder | done |
| P13 | device management B: process/service/package lifecycle | done |
| P14 | device management C: guided elevation, change journal, /undo, dry-run |
done |
Device management phases D-G are planned in docs/design/device-management.md: dynamic monitoring, OS-native scheduling, remote SSH, and reuse/specialization. Possible future work: more providers, provider-side prompt caching controls, a web UI, and multi-agent orchestration.
License
MIT, see LICENSE.
Release files for the-machine 0.3.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| the_machine-0.3.5.tar.gz | 221.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| the_machine-0.3.5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 365.3 kB
Release files / the_machine-0.3.5.tar.gz
| Download URL | the_machine-0.3.5.tar.gz |
|---|---|
| Size | 221.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d24366e8c1a56650a1599827cae1ad18d85f4ab3fa5d70f4f79deef12ef86799
|
|
BLAKE2b-256 checksum How to use checksums |
ff0c02c90135541c88516d493135ee8317612027cbb5bef0c647a642019e9be3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / the_machine-0.3.5-py3-none-any.whl
| Download URL | the_machine-0.3.5-py3-none-any.whl |
|---|---|
| Size | 143.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
115079f90a012c811c3550a84a1a76b09058e6e9347ce3256a8e921391af71c9
|
|
BLAKE2b-256 checksum How to use checksums |
7cb4c3c31f5e7e8281cc565bce58029c18e8595c6f7a8dfda953121a09340d0f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|