From 828b62c3f5fb285d92a6cb5380d1ccfcd0e47b6c Mon Sep 17 00:00:00 2001 From: ThorstenHellert Date: Wed, 12 Aug 2026 15:16:47 +0200 Subject: [PATCH 1/2] feat(bluesky): panel-confirmed start requests for tokenless agents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deployed web terminals never hold the scan launch token, so a tokenless queue_start previously dead-ended in a refusal that read like a config bug. It now files a start request the bridge publishes on the queue summary and SSE stream; the BLUESKY queue panel renders it beside the queue it would drain with Confirm start (the panel's own token-gated start) and Dismiss controls. The request routes are ungated and arm nothing — the only start path remains the token-gated POST /queue/start, and the kill switch still refuses before a request is filed. Skill guidance, tool docstrings, refusal hints, deploy hints, and the bluesky how-to pages now all state the posture consistently: the agent never holds the token in a deployed terminal, the human's panel click is the arming decision, and config surgery is never the fix. --- CHANGELOG.md | 10 + docs/source/how-to/bluesky/queue.rst | 32 ++- docs/source/how-to/bluesky/run-first-scan.rst | 6 +- src/osprey/cli/build_injectors.py | 4 +- .../web_terminals/env_production.py | 14 +- .../bluesky_panels/panels/bluesky/index.html | 16 ++ .../bluesky_panels/panels/bluesky/panel.css | 21 ++ .../panels/bluesky/queue-client.js | 62 ++++++ .../panels/bluesky/queue-view.js | 43 ++++ .../interfaces/bluesky_panels/queue_relay.py | 26 +++ src/osprey/mcp_server/bluesky/tools/queue.py | 154 +++++++++++--- src/osprey/services/bluesky_bridge/queue.py | 144 ++++++++++++- .../skills/operating-bluesky-scans/SKILL.md | 56 ++++- .../skills/writing-bluesky-plans/SKILL.md | 6 +- .../test_bluesky_queue_contract.py | 198 +++++++++++++++++- .../bluesky_panels/bluesky-panel.test.mjs | 71 +++++++ .../bluesky_panels/test_app_integration.py | 9 +- .../bluesky_panels/test_queue_relay.py | 31 +++ tests/mcp_server/bluesky/test_queue_tools.py | 50 ++++- tests/mcp_server/test_backend_emit_sites.py | 34 ++- 20 files changed, 910 insertions(+), 77 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a7a578f3..24d10f22d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,16 @@ Compatibility is documented in release notes, not encoded in the version string. ### Added +- A tokenless `queue_start` now files a **start request** the operator confirms + in the BLUESKY queue panel, instead of dead-ending in a refusal. Deployed + web terminals never hold the scan launch token by design; the agent stages + and queues, the request appears in the queue panel beside the queue it would + drain, and the human's *Confirm start* click — the panel's own token-gated + start — is what arms it. Dismissing the request is always available and + starts nothing. Skill and error-message guidance across the scan stack now + explains this posture so agents hand the start to the human instead of + chasing the token in configuration. + - An archiver read that comes back empty now says why: the response carries a coverage verdict — the window predates or postdates the archive, the channel was never recorded, or the window holds a genuine gap — with the archive's diff --git a/docs/source/how-to/bluesky/queue.rst b/docs/source/how-to/bluesky/queue.rst index cc3d1fd3a..c5b4991c2 100644 --- a/docs/source/how-to/bluesky/queue.rst +++ b/docs/source/how-to/bluesky/queue.rst @@ -66,12 +66,13 @@ One queue, three ways to drive it .. code-block:: text - POST /queue/items add the current draft revision - POST /queue/start start draining (needs the launch token) - POST /queue/stop stop after the running item - POST /queue/abort abort the running plan — never gated - GET /queue what is queued and running - GET /runs recent runs; /runs//data for the numbers + POST /queue/items add the current draft revision + POST /queue/start start draining (needs the launch token) + POST /queue/start-request ask a token holder to start — arms nothing + POST /queue/stop stop after the running item + POST /queue/abort abort the running plan — never gated + GET /queue what is queued and running + GET /runs recent runs; /runs//data for the numbers Every refusal comes back with a ``detail`` object of the form ``{"code": ..., "detail": ...}`` — a stable code for software to @@ -115,6 +116,9 @@ quirks worth knowing: - The launch token — this hands work straight to a moving machine. * - Start the queue - The launch token. + * - Ask for a start (file a start request) + - Nothing — the request arms nothing. Confirming it *is* the + token-gated start, done from the queue panel. * - Stop the queue / abort the running plan - Nothing. Ever. Anywhere. * - Withdraw a pending stop @@ -126,6 +130,14 @@ quirks worth knowing: anything, even on an idle queue. Its halts and its read tools are never taken away. + In a deployed control room the agent's environment never holds the launch + token at all — the token lives with the operator panels. The agent's + ``queue_start`` then files a **start request**: it appears in the BLUESKY + queue panel beside the queue it would drain, with *Confirm start* and + *Dismiss* controls. Confirming fires the panel's own token-carrying start; + dismissing starts nothing. Either way, the human's click is the arming + decision. + .. dropdown:: When something is refused :color: info :icon: alert @@ -138,7 +150,13 @@ quirks worth knowing: for a repeat. ``launch_token_required`` - The operation was armed and the deployment is not. Nothing was started. + The operation was armed and the caller held no valid token. Nothing was + started. (An agent asking for a plain start never hits this — it files a + start request for the panel instead.) + + ``queue_empty`` + A start was requested with nothing queued, so there was nothing a + confirmation could run. Stage and add a plan first. ``browse_only_connector`` This deployment cannot execute scans at all — it is pointed at the diff --git a/docs/source/how-to/bluesky/run-first-scan.rst b/docs/source/how-to/bluesky/run-first-scan.rst index f008a023c..dad3c97f1 100644 --- a/docs/source/how-to/bluesky/run-first-scan.rst +++ b/docs/source/how-to/bluesky/run-first-scan.rst @@ -108,8 +108,10 @@ token, no switch can disable them: process with its own copy of the devices. That is why the queue survives restarts of everything around it. - **Start queue** is checked against a **launch token** the deployment - holds. For the agent, starting is additionally switched off entirely - whenever the project's control-system writes are disabled. + holds. In deployed control rooms the agent never holds it — asking the + agent to start a scan gets you a **start request** to confirm in the + queue panel. For the agent, starting is additionally switched off + entirely whenever the project's control-system writes are disabled. .. dropdown:: First-run hiccups :color: info diff --git a/src/osprey/cli/build_injectors.py b/src/osprey/cli/build_injectors.py index c43d3f909..1469d01e5 100644 --- a/src/osprey/cli/build_injectors.py +++ b/src/osprey/cli/build_injectors.py @@ -519,7 +519,9 @@ def _inject_bluesky(bluesky: BlueskyConfig, project_path: Path) -> None: logger.info(" ✓ Injected Bluesky scan bridge (port %d)", bluesky.port) logger.info( " Token: `osprey deploy up` writes BLUESKY_LAUNCH_TOKEN to .env; " - "the `bluesky` MCP server's queue tools read it automatically." + "a host-run agent's queue tools read it automatically. Deployed web " + "terminals never receive it — their agents file a start request the " + "operator confirms in the BLUESKY queue panel." ) logger.info( " Images: `osprey deploy up` builds the bluesky-bridge image locally " diff --git a/src/osprey/deployment/web_terminals/env_production.py b/src/osprey/deployment/web_terminals/env_production.py index 6a6600fda..d2ca8c30a 100644 --- a/src/osprey/deployment/web_terminals/env_production.py +++ b/src/osprey/deployment/web_terminals/env_production.py @@ -185,10 +185,16 @@ def _build_env_production_subset( :mod:`osprey.deployment.container_lifecycle`, minted per deploy under those fixed names). Neither kind is anything a web terminal presents to anyone: the containers that need a service token read the deploy ``.env`` - the main compose file hands them, and nothing in a web terminal reads one - at all. This is the security spec for this function: a var absent from the - enumerated list above can never appear in the returned dict, regardless of - what the input ``.env`` contains. + the main compose file hands them. The one web-terminal consumer that + WOULD read a service token if present — the bluesky MCP server's + ``${BLUESKY_LAUNCH_TOKEN:-}`` — is tokenless here on purpose: an agent + container must never hold a write-arming bearer credential (any Bash or + Python it runs could read it and arm the queue with no approval), so its + ``queue_start`` files a panel start request and the operator's panels + sidecar, which does receive the token, answers it. This is the security + spec for this function: a var absent from the enumerated list above can + never appear in the returned dict, regardless of what the input ``.env`` + contains. :param config: Raw deploy config (facility fields merged in — see ``modules.web_terminals.image_source`` in :func:`ensure_env_production`). diff --git a/src/osprey/interfaces/bluesky_panels/panels/bluesky/index.html b/src/osprey/interfaces/bluesky_panels/panels/bluesky/index.html index 8e20551ee..60c4652aa 100644 --- a/src/osprey/interfaces/bluesky_panels/panels/bluesky/index.html +++ b/src/osprey/interfaces/bluesky_panels/panels/bluesky/index.html @@ -241,6 +241,22 @@

Queue

+ + +