Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/web/content/docs/concepts/internals.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ without JSON.

## HTTP surface

Project [desktop sessions](/docs/guides/desktops) reuse the daemon's controller,
approval, artifact, and input-receipt services. Oblien catalog operations persist
project associations in the workspace registry; per-run MCP tools and the native
viewer select the same saved desktop. No separate agent desktop process or idle
catalog worker is required.

Public: `GET /healthz`. Everything else sits behind the bearer-token middleware. Every
agent-specific route accepts `?agent=<type>` (default when omitted); turns/runs/chats are shared,
auth/config are per type.
Expand Down
104 changes: 104 additions & 0 deletions apps/web/content/docs/guides/desktops.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
---
title: Project desktops
description: Saved desktop sessions for projects, native viewing, and shared agent control.
---

Mindwire can open a saved desktop on an Oblien workspace and let an agent work in
the same screen you watch from the app. Each Linux project can keep its own apps
and desktop profile. Closing the viewer releases control and leaves those apps
running.

## Open a desktop

In the app, open **Workspace → Desktop** to list saved desktops, or **Project →
Desktop** to see the desktops linked to that project. Chat tools provide the same
shortcut. Create a desktop with a name and supported resolution, then select it
to connect. Reopening it uses the same saved session ID.

Desktop settings support renaming, starting, stopping, and deleting a session.
The current Oblien runtime requires a desktop to be stopped before its resolution
can change. Mindwire does not stop apps implicitly for a resize. Renaming works
while the desktop is running.

| Action | What remains |
| --- | --- |
| Close the viewer or finish an agent run | Apps, profile, and project files |
| Stop the desktop | Profile and project files; apps close |
| Delete the desktop | Project files; the desktop is stopped and its private profile is removed |

Oblien Mac workspaces expose one shared `console` screen with an OS-managed size.
Projects on that workspace share the console. A paired personal Mac keeps its
local opt-in flow: run `mindwire desktop enable` on the Mac and complete the macOS
setup. Display traffic uses its existing encrypted SSH connection.

## Let an agent use it

On an authorized Oblien workspace, Mindwire supplies desktop instructions and
tools to project chat runs. Codex and Claude use a temporary MCP configuration for
that run. Other supported command-capable harnesses use a temporary command helper
that calls the same service. Global harness configuration is unchanged.

| Tool | Purpose |
| --- | --- |
| `surface_desktops` | List this project's saved desktops and session limits |
| `surface_project_desktop` | Create or reuse the project desktop and open view/control access |
| `surface_status` | Inspect desktop availability and its current controller |
| `surface_open` | Open desktop access using the same project selection and approval flow |
| `surface_capture` | Return a PNG and a fresh frame ID |
| `surface_action` | Click, double-click, drag, scroll, type, or use supported clipboard operations |
| `surface_release` | End this run's control session while leaving apps running |

Approval happens before the agent creates, enables, or opens a desktop. Repeated
requests reuse the same creation and approval intent. A tool result opens the
same saved desktop in the app, and the user can take control at any time.

The agent starts a development server with its normal command tool. To open a
browser in the correct Linux desktop, it uses Alt+F2, types
`xdg-open http://localhost:3000`, and presses Return. It should capture the screen
again to verify the result. It must not guess a `DISPLAY` value or start another
VNC server.

## SDK and transport

Saved project desktops require `surfaceProtocolVersion >= 2`. The client supplies
an expiring, workspace-scoped runtime authorization through `surfaces.bindRuntime`.
This is a private binding; an Oblien account login token is never passed to the
agent or stored in project metadata. The app prepares this binding before cloud
chat runs and when managing project desktops.

Once the workspace binding and project exist:

```ts
const { session: saved } = await mw.surfaces.ensureProjectDesktop(projectId);
const desktop = mw.surfaces.desktop(saved.id);
const viewer = await desktop.open({
requestId: crypto.randomUUID(),
mode: "view",
});
const status = await desktop.status(true);
// Close access when the viewer leaves; keep the saved desktop and its apps.
await desktop.close(viewer.id);
```

For a named desktop with a custom size, use `surfaces.createDesktop` with
`projectId`, `name`, `resolution`, and a stable `requestId`. Persist the request
before sending and retry the same key and body if its response is lost.
`listDesktops(projectId)`, `getDesktop`, `updateDesktop`, `startDesktop`,
`stopDesktop`, and `deleteDesktop` manage its lifecycle. Deletion also releases
that desktop's controllers and can safely be retried after completion.

The Go SDK exposes the same catalog through `client.Surfaces`; select a saved
desktop with `ForDesktop(id)`. HTTP viewers select it with `?desktopId=...` on the
existing `/surfaces/desktop` routes. A saved desktop ID and an individual control
session ID identify different resources.

Each saved desktop has one shared controller. Captures and input receipts are
bound to that desktop and run; a screenshot from another desktop cannot authorize
pointer input. When delivery is uncertain, inspect the screen or the existing
receipt rather than sending the action again with a new ID.

Catalog management has no idle polling worker. The app caches lists and only
polls visible sessions while they are starting, stopping, or deleting. Slow
management operations do not block input or lease renewal on other open desktops.
Desktop profiles share the workspace's files and permissions; they are not
security sandboxes.
1 change: 1 addition & 0 deletions apps/web/content/docs/guides/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"authentication",
"destinations",
"personal-computers",
"desktops",
"project-sync"
]
}
111 changes: 90 additions & 21 deletions daemon/DESKTOP.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Workspace desktop control

Protocol version 1 is implemented in the daemon, Go and TypeScript SDKs, and the
Protocol version 2 is implemented in the daemon, Go and TypeScript SDKs, and the
Mindwire iOS client. These source changes require a daemon release before existing
workspaces can use them. `/healthz.surfaceProtocolVersion` is the compatibility gate;
clients do not infer support from the app version.
Expand All @@ -10,15 +10,16 @@ clients do not infer support from the app version.
| Concern | Authority |
| --- | --- |
| Workspace existence, power, execution targets | Oblien |
| Desktop support, enablement, availability and expiring SSH grants | Oblien image/runtime desktop API |
| Desktop support, saved GUI sessions, resolution and expiring SSH grants | Oblien image/runtime desktop API |
| Personal Mac desktop opt-in and local account login | Owner through `mindwire desktop` on the Mac |
| Sessions, current controller, approval and input receipts | Mindwire's workspace surface service |
| Project associations, control sessions, approvals and input receipts | Mindwire's workspace surface service |
| Agent execution and permission responses | Existing run/interaction services |
| Screenshots referenced by chat tools | Workspace artifact store |
| Navigation, zoom, keyboard, cached snapshots and collapsed tool cards | Client |

Desktop scope is the whole workspace. Projects and agent profiles in the same
workspace share one controller. The provider's cloud workspace ID and the registry
Each saved desktop has one controller shared by agents and phone viewers on that
display. Projects on an Oblien Linux VM can use separate saved desktops; a Mac
console remains one shared screen. The provider's cloud workspace ID and the registry
workspace ID are distinct; the binding validates both. Registry schema 3 adds
`surface_records` to the existing SQLite database. Provider secrets stay in the
existing private credential store, never registry snapshots or chat messages.
Expand All @@ -28,11 +29,11 @@ existing private credential store, never registry snapshots or chat messages.
Both transports carry native VNC/RFB. The iOS viewer uses Oblien's documented
**desktop-only SSH tunnel**:

1. Read `/desktop/status` with a workspace gateway token.
1. Read `/desktop/sessions/:desktopId/status` with a workspace gateway token.
2. Enable `/desktop/enable` only after an explicit Connect action when needed.
3. The signed-in app obtains `POST /workspace/:id/desktop/ssh` through the management
API. Its owner session JWT remains in the app.
4. Validate the grant's workspace, expiry, destination and SHA-256 host fingerprint.
API with `{session_id: desktopId}`. Its owner session JWT remains in the app.
4. Validate the grant's workspace, saved session ID, expiry, destination and SHA-256 host fingerprint.
5. Connect to `ssh.oblien.com:22`, pin the host key, and open `direct-tcpip` to
`127.0.0.1:5900`. The returned VNC authentication is `none` inside that tunnel.

Expand All @@ -47,12 +48,12 @@ initialization, backpressure and closure are shared by both halves of the tunnel
The viewer itself sends no keyboard, pointer or clipboard input.

The workspace daemon uses the provider's authenticated binary WebSocket endpoint,
`/desktop/ws`, with a bearer header and the `binary` subprotocol, matching Oblien's
`/desktop/sessions/:desktopId/ws`, with a bearer header and the `binary` subprotocol, matching Oblien's
desktop tunnel SDK. On macOS images, QEMU's display is a private socket in the Linux
launcher; it is not a VNC server inside the Mac, and outbound SSH to the public
gateway can be unavailable. WSS carries RFB bytes to that display. It does not
change the iOS viewer or introduce a browser renderer. The daemon shares one RFB
connection across its sessions; individual agents use the shared tools rather than
connection per saved desktop across its control sessions; individual agents use the shared tools rather than
opening their own display connections.

The Go RFB adapter handles standard RFB 3.8 None security results, explicit BGR
Expand Down Expand Up @@ -133,10 +134,10 @@ Mindwire's controller lease.

## Sessions, permission and input

Opening the iOS screen reads a cached capability check; it does not install a
service, enable access, create a VNC stream or take control. Connect opens a **view**
session. Taking control is explicit, with confirmation if another controller owns
it. Multiple viewers can stay connected. Exclusivity applies to Mindwire clients;
Opening a saved desktop connects a **view** session and acquires a free controller
by default. A busy controller requires explicit takeover. The picker does not
install services or start hidden display streams. Multiple viewers can stay
connected. Exclusivity applies to Mindwire clients;
Oblien's dashboard or an external VNC app is outside this lease protocol.

Agents request desktop permission through the same structured interaction that
Expand Down Expand Up @@ -218,16 +219,18 @@ are preserved, and the built-in server name cannot be shadowed.

The service owns desktop approval. Its private Codex MCP overlay uses the native
[`default_tools_approval_mode = "approve"`](https://developers.openai.com/codex/mcp/)
setting; Claude receives exact allow rules for these five tools. This only lets
setting; Claude receives exact allow rules for these seven tools. This only lets
calls reach the service's permission gate, avoiding a second native prompt per
operation. Existing user settings and explicit deny rules remain in force.

The same five tools serve both harnesses:
The same tools serve both harnesses:

| Tool | Result |
| --- | --- |
| `surface_status` | Workspace desktop capability/state and current controller |
| `surface_open` | Approved view/control session |
| `surface_desktops` | Project desktops and provider limits |
| `surface_project_desktop` | Approved create/reuse/start of the project's desktop, with saved ID and control session |
| `surface_status` | Selected desktop capability/state and current controller |
| `surface_open` | Approved view/control session; project-aware when a catalog is bound |
| `surface_capture` | Real image content, frame ID, geometry and durable artifact ID |
| `surface_action` | Input receipt; transient clipboard output when requested |
| `surface_release` | Close this run's session and release its input |
Expand All @@ -239,7 +242,12 @@ and would discard accompanying image content. Shared normalization produces
capture references. Native Codex/Claude history is merged with service-owned
permissions and artifact metadata. Images remain in history after the turn ends,
cancellation, reconnect or service restart; opening history does not expand cards.
Other harnesses are not currently given the built-in MCP server.
Harnesses without per-turn MCP support receive a run-scoped command helper using
the same MCP service. `"$MINDWIRE_DESKTOP_HELPER" --desktop-tool list` lists the
schemas; `--desktop-tool TOOL 'JSON'` calls one. Screenshots become private PNG
files for the harness's image tool. The loopback URL and run token come from the
environment; no global harness configuration changes. Files and authorization are
removed when the run finishes.

PNG artifacts are authenticated via `/artifacts/:id`, stored with private file
permissions and bounded to 8 MiB / 16 million pixels per image and 512 MiB per
Expand All @@ -248,6 +256,45 @@ lazily after 24 hours. Input receipt deduplication is retained for at least 24 h
and capped at 100,000 retained actions. Temporary grounding metadata is pruned
independently, so pruning does not remove chat screenshots.

## Saved project desktops

Oblien SDK 2.8 supplies `desktop.sessions` on management and runtime clients. The
app uses its typed Swift equivalent for workspace discovery and creation. The
daemon uses the root runtime `/desktop/sessions` API, never an execution-target
path. Capabilities decide virtual versus console mode, creation limits and sizes.

`PUT /surfaces/desktop/runtime` registers an expiring workspace runtime token in
the existing private credentials file. Renewing it preserves active controllers.
The app registers it before project desktop operations and agent turns, including
when no viewer has opened. Saved desktops require surface protocol 2.

Project links and create requests persist in `surface_records`. Creation is
serialized; its original body and hashed idempotency key survive lost responses
and daemon restarts. Retry the same request ID and body. Provider keys are 64
bytes and names at most 80 UTF-8 bytes. Captures, receipts, control and tool cards
retain the saved desktop ID. Agent tools reject another project's desktop.

Approval precedes enabling or creating the project's desktop. On Linux, use
Alt+F2 followed by `xdg-open http://localhost:3000` to open the browser in that
specific GUI session. Start the development server with the harness's normal
command tool; never guess DISPLAY or start another VNC server. The phone opens
the same ID from the project or tool card. Profiles share workspace files and
are not security sandboxes.

Viewer close/run completion releases control and keeps apps running. Stop closes
apps but retains the profile. Delete first stops a virtual desktop, releases its
controllers, and removes its private profile; project files remain. Repeating a
completed delete succeeds. The current Oblien runtime requires a virtual desktop
to be stopped before changing its resolution; Mindwire never implicitly stops apps
for a resize. Renaming still works while running. A Mac console uses its OS size.
A saved console cannot compete with an open
legacy default viewer on that same screen.

The catalog has no background refresh worker. Children are created only when
selected and use the root service's existing lease reaper. The app coalesces
operations, caches navigation reads, persists pending create requests, and polls
only visible sessions that are starting, stopping or deleting.

## HTTP and SDK contract

All routes use existing daemon authentication and transport. They are workspace
Expand All @@ -260,6 +307,11 @@ scoped, regardless of `?agent=` or `withAgent()`.
| GET | `/surfaces/desktop/local` | Safe local Mac setup status |
| PUT | `/surfaces/desktop/local` | Write-only setup; requires the local CLI control credential |
| PUT | `/surfaces/desktop/binding` | Write-only scoped provider authorization |
| PUT | `/surfaces/desktop/runtime` | Write-only runtime authorization for saved desktops and agent tools |
| GET / POST | `/surfaces/desktop/catalog` | List/create saved desktops; optional project association |
| GET / PATCH / DELETE | `/surfaces/desktop/catalog/:desktopId` | Inspect, rename/resize or stop-and-delete a desktop |
| POST | `/surfaces/desktop/catalog/:desktopId/start` or `/stop` | Start/stop the saved desktop |
| POST | `/surfaces/desktop/projects/:projectId/ensure` | Reuse or create the project's desktop |
| GET | `/surfaces/desktop/events` | Current snapshot, then live revisions/heartbeats |
| POST | `/surfaces/desktop/sessions` | Open view or control session |
| POST | `/surfaces/desktop/sessions/:id/control` | Acquire, renew, release or user takeover |
Expand All @@ -274,14 +326,20 @@ exposes `client.surfaces`, including a cancellable status watch. Embedded Go err
and HTTP errors use the same code/status mapping. Provider failures are reflected
in status snapshots; failed control actions return actionable protocol errors.

Select a display using HTTP `?desktopId=`, TypeScript
`client.surfaces.desktop(id)`, or Go `Client.Surfaces.ForDesktop(id)`. This scopes
status, events, control, captures and receipts. Catalog operations stay workspace
scoped. Saved provider desktop IDs differ from ephemeral control-session IDs.

Binding refreshes preserve active control. A fresh app gateway token does not
replace an unexpired SSH grant unnecessarily. Authorization eventually expires;
opening Desktop obtains another scoped grant. The app never refreshes by silently
re-enabling a disabled Runtime API. Closing/backgrounding the viewer releases only
its human session, leaving an authorized agent's independent session running.
Temporary iOS inactive states keep the viewer connected. Foreground return opens
a fresh view session automatically, without reclaiming a control lease or
replaying input. Explicit Disconnect/Done cancels that intent.
a fresh view session and reacquires a free controller only if the user still wants
control. It never takes an occupied lease or replays input. Explicit Release,
Disconnect or Done cancels automatic control intent.

## Browser preview

Expand Down Expand Up @@ -330,6 +388,17 @@ Opt-in tests: `WorkspaceDesktopLiveTests` on the signed-in iPhone,
`CODEX_LOCAL=1 go test ./internal/agent/codex -run '^TestAppServerDesktopMCP$'`.
Fixtures stay outside source control and must be removed after verification.

Saved-desktop tests cover concurrent creation, uncertain replies and restart,
independent controllers/captures, project isolation, denied approval, console
reuse, stop-before-delete and private helper cleanup. Native project-tool checks
use `TestHarnessProjectDesktopMCP` with `MINDWIRE_DESKTOP_HARNESS=codex` or
`claude-code`. For Claude, `MINDWIRE_DESKTOP_MODEL_FIXTURE=1` scripts only the
Messages API while exercising the installed CLI and real MCP service.
`TestProjectDesktopOblienLive` uses a private `MINDWIRE_PROJECT_DESKTOP_FIXTURE`
with workspace ID and runtime token. It creates a disposable virtual desktop,
checks resolution, MCP screenshot/input, shared viewer, stop/start and durable
reassociation, then deletes that test desktop.

Personal Mac coverage includes ARD authentication, local opt-in/disable, restart
restoration, credential isolation, input receipts, display revocation and actual
paired SSH forwarding over TCP and WebSocket. `MacDesktopTests` verifies cached and
Expand Down
Loading
Loading