diff --git a/docs/mcp/approve-from-your-assistant.mdx b/docs/mcp/approve-from-your-assistant.mdx index 8f73d04..b183f03 100644 --- a/docs/mcp/approve-from-your-assistant.mdx +++ b/docs/mcp/approve-from-your-assistant.mdx @@ -17,16 +17,56 @@ approver's assistant ──▶ ctrlrun mcp-operator ──▶ the approval r and the store your agents already use. It speaks MCP revision `2026-07-28` and accepts `2025-11-25`, `2025-06-18` and `2025-03-26`. -## Start it +## From the assistant on your own machine + +If the person answering is the one at the keyboard, there is nothing to proxy. Desktop +assistants, Cursor and the editors launch an MCP server as a subprocess and speak to it on stdin and stdout, +and `--stdio` is that server: + +```json +{ + "mcpServers": { + "ctrlrun-operator": { + "command": "uvx", + "args": ["ctrlrun", "mcp-operator", "--stdio"], + "env": { "CTRLRUN_CONFIG": "/path/to/your/ctrlrun.yaml" } + } + } +} +``` + +The core install is enough; the operator imports nothing from an extra. `CTRLRUN_CONFIG` names +the policy your agents run against, and the store is found beside it. + +It opens no socket. The approver is the **account the process runs as**, read from the real +uid and from nothing the client sends or sets: not the environment, not `clientInfo`, not +`SUDO_USER`. Answers are recorded as `mcp-operator:` with the issuer +`os-login:`, so the evidence says how the answer arrived. Two things to know before you +rely on it: + +- **The login has no expiry.** This process holds `approve`, `deny` and `resolve` under your + name for as long as it runs, so the confirmation your client shows before a write is the human + step. Leave it on for these three tools. +- **Root is an account, not a person.** Under `sudo` or in a container running as root, every + write is refused and reads still answer. + +A login carries no roles, so a control that names an `approver_role` refuses over stdio. It +takes no header, JWT or origin flag; each is refused by name, because a flag that cannot take +effect is a flag you would believe took effect. + +## Start it behind a proxy + +For an approver who is not at the keyboard of the machine the store is on: ```bash ctrlrun mcp-operator --principal-header x-approver --user-header x-approver-user ``` -It binds `127.0.0.1:8901` and there is **no flag that changes that**. Its read tools answer -without a credential, so it must not be the process that opens a port to a network; put a proxy -in front of it on the same host, terminating authentication there and overwriting both headers -on every request. That proxy is what makes the headers worth anything. +Over HTTP it binds `127.0.0.1:8901` and there is **no flag that changes that**; `--stdio` binds +nothing at all. Its read tools answer without a credential, so it must not be the process that +opens a port to a network; put a proxy in front of it on the same host, terminating +authentication there and overwriting both headers on every request. That proxy is what makes +the headers worth anything. There is no `--principal` either. A fixed name would attribute every approval to the same string whoever gave it, and an approver that distinguishes nobody is not attribution. With @@ -100,6 +140,9 @@ The agent then runs the refund it was waiting on, and the receipt says who let i - **Approve an action other than the one the request names.** The grant carries the `action_hash` stored when the request was created; change an argument and the call is refused. - **Take an answer from a machine.** A credential naming an agent and no person is `-41013`. +- **Take a name from the client that launched it.** Over `--stdio` the approver is the OS login + of the real uid. `clientInfo`, the environment and every argument are the client's word, and + none of them is used. - **Take an expired credential.** `-41014`, distinct from `-41007`, because refreshing a token and obtaining one are different fixes. - **Decide whether you were allowed to answer.** It authenticates *who* answered and records it. @@ -111,6 +154,11 @@ The agent then runs the refund it was waiting on, and the receipt says who let i - Exits at start naming `--user-header` or `--identity-jwt-user-claim`: a write tool refuses a credential that names no person, so a configuration that could never write is refused early. - Exits at start naming loopback: there is no `--allow-remote`; put a proxy in front instead. +- Exits at start naming a flag beside `--stdio`: there are no headers over a pipe, so a header, + JWT or origin flag cannot take effect and is refused rather than ignored. +- Your client reports output that is not JSON: the startup block goes to stderr, and a client + that shows it there is showing you information, not an error. A release from before + `--stdio` existed has no such mode at all; upgrade. - `-41003` with reason `expired`: the request timed out. The agent proposes it again. - `-32020`: a mirrored MCP header disagrees with the body. The body is believed and the request is refused rather than guessed at. diff --git a/docs/mcp/overview.mdx b/docs/mcp/overview.mdx index b8116aa..c8c5145 100644 --- a/docs/mcp/overview.mdx +++ b/docs/mcp/overview.mdx @@ -41,9 +41,14 @@ answers from these pages rather than from memory. ## Discovery: the registries -The gateway is what you run to put CTRLRun in front of an MCP server, and it is listed in the -MCP registries as such once the listing is live. Until then this section carries no link rather -than a placeholder; the registry entry is a launch-day item. +The operator server is the MCP server this project publishes, and it is listed in the +[official MCP registry](https://registry.modelcontextprotocol.io/v0/servers/io.github.CTRLRun%2Fctrlrun-mcp-operator/versions) +as `io.github.CTRLRun/ctrlrun-mcp-operator`, and on [Glama](https://glama.ai/mcp/servers/CTRLRun/ctrlrun). +A client that installs from a registry gets `ctrlrun mcp-operator --stdio`, which is the +configuration in [Approve from your assistant](/docs/mcp/approve-from-your-assistant). + +The gateway is not listed, and the reason is what it is: a proxy you run in front of an MCP +server you already have, not a server a registry can point a client at. ## Answering: the operator server @@ -66,8 +71,9 @@ approver's assistant ──▶ ctrlrun mcp-operator ──▶ the same appro binding, single use and expiry. - Nothing here can make an agent act. There is no tool that proposes or executes an action, and no auto-approve. -- It binds loopback and has no flag that changes that, because its read tools answer without a - credential. +- Over HTTP it binds loopback and has no flag that changes that, because its read tools answer + without a credential. With `--stdio` it binds nothing: the client that launched it is its only + client, and the approver is that client's OS login. [Approve from your assistant](/docs/mcp/approve-from-your-assistant) is the page with the configuration and a transcript. diff --git a/docs/reference/cli.mdx b/docs/reference/cli.mdx index 6c8c391..07de7eb 100644 --- a/docs/reference/cli.mdx +++ b/docs/reference/cli.mdx @@ -32,7 +32,7 @@ Commands: hold Refuse to prune a range of receipts, until a person says... init Write a starter ctrlrun.yaml and create .ctrlrun/. inspect Show one action's whole history: proposal, decision,... - mcp-operator Answer approvals from an MCP client, over loopback... + mcp-operator Answer approvals from an MCP client, over loopback or stdio... policy Propose a policy change, or replay one against what already... prune Delete receipts from the start of the chain, leaving it... receipts Show the receipts this store holds. @@ -410,16 +410,25 @@ Options: ```text Usage: ctrlrun mcp-operator [OPTIONS] - Answer approvals from an MCP client, over loopback (SPEC-mcp-operator.md). + Answer approvals from an MCP client, over loopback or stdio (SPEC-mcp- + operator.md). There is no --principal and no --allow-remote, and both absences are load- bearing: a static principal cannot attribute an answer to a person (§3.1), and a server whose read tools answer without a credential must not be the one that - opens a port (§2.1). + opens a port (§2.1). --stdio opens none at all, and its approver is the one + name the launching client cannot set: the OS login of the process (§2.3). Options: --listen TEXT HOST:PORT. [default: 127.0.0.1:8901] --path TEXT The MCP endpoint path. [default: /mcp] + --stdio Speak MCP on stdin and stdout to the client + that launched this process (a desktop + assistant, Cursor, an editor). Opens no + socket. The approver is the account this + process runs as, read from the real uid; takes + no header, JWT or origin flag (SPEC-mcp- + operator §2.3). --principal-header TEXT Take the approver's agent from this header, set by a proxy that authenticates them. --user-header TEXT Take the approver's name from this header.