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

+4 -1
View File
@@ -28,6 +28,9 @@ work=$(mktemp -d)
export XDG_CONFIG_HOME="$work/config" XDG_DATA_HOME="$work/data" XDG_CACHE_HOME="$work/cache"
mkdir -p "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_CACHE_HOME"
unset WGPU_BACKEND WGPU_BACKENDS
# The control channel's token (docs/control-protocol.md), set here so this client knows it.
SOLIDCRAFT_CONTROL_TOKEN=$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')
export SOLIDCRAFT_CONTROL_TOKEN
solidcraft --control 18837 >"$work/app.log" 2>&1 &
app=$!
cleanup() {
@@ -44,7 +47,7 @@ for ((attempt=0; attempt<300; attempt++)); do
sleep .1
done
test "$ready" = 1
for request in '{"id":1,"method":"ui.inspect","params":{}}' '{"id":2,"method":"engine.execute","params":{"command":"solid.box","params":{"length":30,"width":20,"height":10}}}' '{"id":3,"method":"document.inspect","params":{"measure":true}}'; do
for request in "{\"id\":1,\"method\":\"ui.inspect\",\"params\":{},\"token\":\"$SOLIDCRAFT_CONTROL_TOKEN\"}" '{"id":2,"method":"engine.execute","params":{"command":"solid.box","params":{"length":30,"width":20,"height":10}}}' '{"id":3,"method":"document.inspect","params":{"measure":true}}'; do
printf '%s\n' "$request" >&3
IFS= read -r -t 30 reply <&3
printf '%s\n' "$reply"
+5 -2
View File
@@ -17,6 +17,7 @@ import signal
import socket
import subprocess
import sys
import secrets
import tempfile
import time
@@ -26,8 +27,10 @@ flatpak_smoke = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(flatpak_smoke)
PORTS = {"x11": 18839, "wayland": 18840}
# The control channel's token (docs/control-protocol.md), given to the app so this client knows it.
TOKEN = secrets.token_hex(32)
REQUESTS = [
{"id": 1, "method": "ui.inspect", "params": {}},
{"id": 1, "method": "ui.inspect", "params": {}, "token": TOKEN},
{"id": 2, "method": "engine.execute",
"params": {"command": "solid.box", "params": {"length": 30, "width": 20, "height": 10}}},
{"id": 3, "method": "document.inspect", "params": {"measure": True}},
@@ -67,7 +70,7 @@ def run_backend(backend, out):
runtime = Path(f"/run/user/{os.getuid()}")
if not runtime.is_dir():
raise RuntimeError(f"{runtime} is missing: snaps expect the login session's runtime directory")
env = dict(os.environ, XDG_RUNTIME_DIR=str(runtime))
env = dict(os.environ, XDG_RUNTIME_DIR=str(runtime), SOLIDCRAFT_CONTROL_TOKEN=TOKEN)
for name in ("WAYLAND_DISPLAY", "WAYLAND_SOCKET", "DISPLAY", "SOLIDCRAFT_WAYLAND", "SOLVECRAFT_WAYLAND"):
env.pop(name, None)
# The snap can write to the (non-hidden) home folder, not to the host's /tmp.