fix: control port needs a per-launch token; MCP tool annotations tell the whole truth (#205) (#901)

solidcraft --control PORT writes a random per-launch token to a user-only file (<data>/control/PORT.token; 0700/0600 on Unix, icacls on Windows; SOLIDCRAFT_CONTROL_TOKEN overrides) and refuses any connection whose first request lacks it. mcp --connect, the demo and packaging smoke clients send it. MCP execute/batch/open/save/export/screenshot are annotated destructive and open-world, new_design destructive. Docs: control-protocol.md, mcp.md, AGENTS.md, pending release note in releasing.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bherbruckandClaude Opus 5.5 authored and GitHub committed 2026-10-13 18:51:19 -04:00
1 parent d9237d004e
commit fe3717f2e4
19 files changed
+465 -53

No files matched your search

+39 -3
View File
@@ -6,6 +6,42 @@ line `{"id": …, "ok": true, "result": …}` or `{"id": …, "ok": false, "erro
handled on the UI thread between frames; pointer and keyboard input is injected as real egui
events, one pointer event per frame.
### Token
Every connection must authenticate. At start the app makes a random 256-bit token for this
launch and writes it, as one line of hex, to a file only the user can read:
| System | Token file |
|---|---|
| Linux | `$XDG_DATA_HOME/solidcraft/control/PORT.token` (`~/.local/share/solidcraft/control/PORT.token`) |
| macOS | `~/Library/Application Support/SolidCraft/control/PORT.token` |
| Windows | `%LOCALAPPDATA%\SolidCraft\control\PORT.token` |
On Unix the folder is `0700` and the file `0600`; on Windows the file's inherited permissions are
replaced by a single entry for the user. `SOLIDCRAFT_CONTROL_DIR` moves the folder. The file is
removed when the app exits, and the next launch writes a new token. The token file's path is
printed to stderr at start-up.
The first request on a connection must carry the token as a top-level `"token"` member
(`{"id": 1, "method": "ui.inspect", "params": {}, "token": "…"}`); later requests on the same
connection may leave it out. A first line without the right token is answered with
`{"ok": false, "error": "unauthorized: …"}` and the connection is closed, without running
anything. Tokens are compared in constant time.
`solidcraft-cli mcp --connect 127.0.0.1:PORT` reads the token file for that port by itself
(`--token T` or `SOLIDCRAFT_CONTROL_TOKEN` give one explicitly). For a test harness, or a
sandboxed package whose data folder the client can't see, start the app with
`SOLIDCRAFT_CONTROL_TOKEN` set to at least 32 hex characters (for example
`openssl rand -hex 32`) and the app uses that token instead of making one; the file is still
written. A one-line client in Python:
```python
token = (Path.home() / ".local/share/solidcraft/control/7878.token").read_text().strip()
sock.sendall((json.dumps({"id": 1, "method": "ui.inspect", "params": {}, "token": token}) + "\n").encode())
```
### Limits
The channel is untrusted input. A request line may be at most 16 MiB; a longer one is answered
with `{"ok": false, "error": "request too large"}` and the connection is closed, as are
connections that send eight lines that are not JSON. At most 16 connections are served at once
@@ -19,9 +55,9 @@ reads, writes or deletes (command paths such as `file.export`'s `path` or `doc.r
`dir`, `ui.render`, `ui.screenshot`) must then lie inside one of those folders after `..` and
symlinks are resolved, or the request fails with `… outside the folders this agent may use …`.
The app adds its own recovery folder and, when it lies safely inside the home folder and not in
a hidden folder, the open design's folder (whose hidden files stay out of reach). The bridged MCP server sends its folders this way on every request ([mcp.md](mcp.md)). Without
`allow_paths` file paths are not limited, and the channel itself has no authentication beyond
listening on loopback only.
a hidden folder, the open design's folder (whose hidden files stay out of reach). The bridged
MCP server sends its folders this way on every request ([mcp.md](mcp.md)). Without
`allow_paths` file paths are not limited: a client holding the token can do what the user can.
## Engine
+14
View File
@@ -19,6 +19,11 @@ solidcraft --control 7878 &
solidcraft-cli mcp --connect 127.0.0.1:7878
```
Bridged, the server finds the app's control token in the token file the app wrote for that port
([control-protocol.md](control-protocol.md#token)); `--token T` or `SOLIDCRAFT_CONTROL_TOKEN` give
it explicitly when the app runs in a sandbox whose data folder the server can't see (prefer the
variable: command-line arguments are visible to other users in the process list).
stdout carries only protocol messages; errors go to stderr. Bridged screenshots are written to a
temporary file by the app and read back by the server, so both must run on the same machine.
@@ -67,6 +72,15 @@ folder of the design open in it, under the same rules, so give absolute paths: a
resolves against the app's working folder, not the server's. The app's own
file dialogs are never limited.
### Tool annotations
Each tool says what it may do, for clients that ask before running tools (or auto-approve on
these hints). `list_commands`, `inspect_design`, `measure`, `body_topology` and `crash_reports`
are read-only. `set_parameter`, `undo` and `redo` change the design undoably. `new_design`
replaces it (destructive). `execute`, `batch`, `open`, `save`, `export` and `screenshot` (whose
`path` writes a file) can read, write, overwrite or delete files: they are not read-only,
`destructiveHint` and `openWorldHint` are true.
### Client configuration
Claude Code (project `.mcp.json`, or `claude mcp add solidcraft -- solidcraft-cli mcp`; a setup
+16
View File
@@ -98,6 +98,22 @@ new commits were added. An existing tag or published release is never overwritte
version after publication. Do not push the version tag yourself: publishing the draft creates
it at the draft's target commit.
## Notes for the next release
Items to carry into the next release's notes (step 3 below), then remove from here:
- **Security (agents and the control channel, [#205](https://github.com/bherbruck/solidcraft/issues/205)).**
Upgrading is recommended for anyone who drives SolidCraft from an AI agent or runs it with
`--control` on a shared machine. MCP tool calls now reach files only in the open design's
folder (inside home, without its hidden files), SolidCraft's recovery folder and folders
given with `--allow-path`; `..`, symlinks that lead out and Windows UNC, device, stream and
short-name paths are refused ([#578](https://github.com/bherbruck/solidcraft/pull/578), thanks
@kravens). `doc.recovery_discard` deletes only real recovery entries. The control port now
needs a per-launch token from a file only the user can read, so other local users can't drive
the app; `solidcraft-cli mcp --connect` finds it by itself, other clients must send it (see
docs/control-protocol.md). MCP tool annotations no longer understate side effects. Reported by
@CalvinEthos.
## Verify and publish
When the task authorizes shipping a release, agents should complete these steps without