Architecture
runspec is a contract for command-line tools. A runspec.toml next to the
code declares a tool's interface: arguments, types, validation, which arguments
are secrets, and how much autonomy an AI agent may have with it. Everything
else — a CLI, an MCP tool, a form, a scheduled job — is generated from that one
declaration.
The design rests on one principle:
The spec declares. Executors enforce.
A declaration can't stop anything on its own. Autonomy, credentials and "who may run this" are enforced by whatever actually runs the tool. runspec ships several executors that enforce the same declarations, each suited to a different kind of user.
The layers
┌───────────────────────────────────────────────────────────────┐
│ Front-ends │
│ CLI · runspec serve (stdio MCP) · MCP gateway · console │
├───────────────────────────────────────────────────────────────┤
│ Executors — enforce autonomy, credentials, identity │
│ local process · SSH to remote hosts (agentless) · sudo/su │
├───────────────────────────────────────────────────────────────┤
│ Contract — runspec (stdlib only) │
│ runspec.toml · parser · validation · secret args · logging │
└───────────────────────────────────────────────────────────────┘
│ The host's own controls: Unix accounts · sudoers · auditd │
The bottom line matters most. runspec doesn't invent its own users, permissions or audit trail. It runs tools as real Unix accounts, through sudo, so everything the organisation already trusts keeps working.
Two kinds of user
| Tool author / local user | Operator with a fleet | |
|---|---|---|
| Runs tools | on their own machine | on their machine and on remote hosts over SSH |
| Front-ends | the CLI; any MCP client via runspec serve (Claude Code, Copilot, …) |
the console (with or without AI); any MCP client via the console's MCP server |
| Autonomy | advertised in the tool schema; honoured by clients that support it | enforced: confirm cards, manual tools refused |
| Credentials | environment variables, .runspec_env, or an interactive prompt |
the OS keychain, injected per run and never on a command line |
| Needs | pip install runspec |
+ runspec-console on the operator's machine; nothing new on hosts except the tools themselves |
A tool written once serves both users unchanged.
Remote execution: agentless, per-user identity
The console reaches a host over SSH as the operator's own account. It runs the tool as a short-lived process, and nothing stays running on the host.
- Identity is the operator's. A tool runs as whoever logged in, or as a
service account via
sudo -u, where sudoers allows it. sudo refuses anyone it doesn't allow; runspec doesn't keep its own list of who may run what. - Secrets never touch a command line. Values travel on the process's
standard input and are read by a fixed shell prelude, so they're absent from
psand from the sudo log (SPEC → "Remote Execution → Command building"). This is tested on every change with real accounts. - Audit is the host's. sshd, sudo and, where enabled, auditd record who ran what. auditd records the original login account even through sudo, and can forward to a central SIEM.
- Kill switch. A per-venv denylist lets an administrator disable some or all tools on a host at once, and it can't be used to lock itself out.
Why SSH rather than an MCP server on every host
| SSH + runspec | HTTP MCP server per host | |
|---|---|---|
| Whose identity a tool runs as | each operator's, with their own sudo rights | the server's single account |
| On each host | the tools, nothing running | a daemon, a port, TLS, tokens, upgrades |
| Access control | existing accounts and sudoers | per-server tokens or OAuth |
| Audit | existing (sshd, sudo, auditd) | whatever each server logs |
HTTP servers suit a tool that is really a service owned by one team, or users who must not have shell access. For many operators with different rights on many existing Linux hosts, SSH reuses controls the organisation already runs. It's the same argument that made agentless configuration management (Ansible) the norm.
Shared tool environments
A tool environment (a Python venv) on a host is either personal or shared:
- Personal: a venv in a user's home, run only by that user.
- Shared: a venv owned by a service account. Every run happens as that account, via sudo, and sudoers — typically a Unix group — decides who may run it. Adding or removing a person is an administrator's group change, not a runspec change.
Either way, each venv has exactly one account running its tools, so its logs and run history have a single writer and need no special permissions.
This is the default. A tool's run_as defaults to "@owner", the account that
owns its venv. The console, runspec serve and the MCP gateway escalate with
sudo -n -u <owner>, and escalate nothing when you are the owner. A tool run
directly by anyone but the owner refuses to start and names the account to
sudo -u to. run_as = "" opts a package out, so its tools run as the
invoker.
Autonomy and AI
Every tool declares an autonomy level: autonomous, confirm, supervised or
manual. The console enforces it for its own agent and for external agents
through its local MCP server. A confirm call from Claude Code or Copilot
waits for an approval card in the console. The console also runs with
ai = false, as a plain operations tool: forms, schedules, history and fleet
management, with no model involved.
What's deliberately out of scope
- A central server. There's no runspec service to host, patch or secure; the console runs on the operator's machine.
- Its own identity system. Unix accounts, sudoers and SSH keys are the identity system.
- Long-running tools on hosts. Tools are commands. Services are better served by their own daemons.
Roadmap items that affect this picture
- A shared executor library. The console and the MCP gateway currently carry separate copies of the SSH, credential and escalation code. They should share one package.
- The MCP gateway (today
runspec-mcp). It needs a client-agnostic setup that works in Copilot as well as Claude Code, simpler credential configuration, and a clearer name (runspec-mcp-gateway). - Run history. Simplifying it follows from the single-writer rule above; see
design/venv-run-history-assessment.md.