packages/cua-driver/docs/sdk-first-runtime-north-star.md
Status: Draft for architecture discussion
Direction: Make the typed SDK and its owned native runtime the product boundary. Keep daemon and server topologies as optional adapters.
RFC: RFC 2549
Supersedes: RFC 2447
Related: permission adapters and session modes, #2385, and #2437.
Cua Driver is a stateful native runtime exposed through typed SDKs. An application creates the runtime, uses it, and shuts it down. MCP, HTTP, a private worker, and a long-lived daemon can expose that runtime when a consumer needs a transport or process boundary.
The daemon remains supported, but it stops defining the application contract. The migration preserves the released CLI, MCP, Rust, Python, and TypeScript interfaces. Runtime ownership may change behind them only after compatibility and behavior-parity gates pass.
The runtime and public SDK stay transport-free. gRPC, MCP, HTTP, local sockets, and environment forwarding carry generated Driver envelopes outside the core.
flowchart TB
subgraph Consumers["Applications and adapters"]
APP["Native application"]
PYAPP["Python application"]
TSAPP["TypeScript application"]
MCP["MCP stdio adapter"]
HTTP["HTTP adapter"]
CLI["CLI"]
end
subgraph Contract["Public product boundary"]
SDK["Typed CuaDriver SDK"]
end
subgraph Runtime["Owned runtime"]
AUTH["Authorization coordinator"]
SESSIONS["Sessions and browser bindings"]
UI["Cursor, capture, and recording"]
CORE["Native platform core"]
end
APP --> SDK
PYAPP --> SDK
TSAPP --> SDK
MCP --> SDK
HTTP --> SDK
CLI --> SDK
SDK --> AUTH
AUTH --> SESSIONS
AUTH --> UI
SESSIONS --> CORE
UI --> CORE
The topology changes ownership and isolation. It does not change tool semantics, authorization, results, or generated SDK types.
flowchart LR
subgraph Direct["Default: direct SDK"]
DAPP["Host application"]
DSDK["CuaDriver object"]
DRUNTIME["In-process runtime"]
DAPP --> DSDK --> DRUNTIME
end
subgraph Worker["Optional: private worker"]
WAPP["Host application"]
WSDK["CuaDriver object"]
CHANNEL["Inherited private channel"]
WRUNTIME["Supervised worker runtime"]
WAPP --> WSDK --> CHANNEL --> WRUNTIME
end
subgraph Service["Optional: explicit service"]
CLIENTS["External clients"]
ADAPTER["gRPC, MCP, HTTP, or daemon adapter"]
SRUNTIME["Service-owned runtime"]
CLIENTS --> ADAPTER --> SRUNTIME
end
| Need | Preferred topology |
|---|---|
| One application owns automation | Direct SDK |
| The host wants native crash containment | Private supervised worker |
| An MCP client owns one stdio process on Windows or Linux | MCP adapter with its own runtime |
| Standalone installed MCP runs on macOS | QwenCuaDriver.app-owned service runtime |
| Several external clients must share state | Explicit service or daemon |
| QwenCuaDriver.app must own a stable macOS permission identity | App-owned runtime or child |
| Short-lived scripts need persistent shared state | Explicit service or daemon |
The runtime owns state until its SDK object shuts down. Removing the required daemon does not turn tools into independent stateless functions.
stateDiagram-v2
[*] --> Created: CuaDriver.create(options)
Created --> Running: initialize native runtime
Running --> Running: sessions, actions, capture, grants
Running --> Draining: shutdown()
Draining --> Stopped: finish admitted work and cleanup
Stopped --> [*]
Running --> Stopped: host or worker exits
The runtime owns:
The architecture is not permission to disrupt current users. The CLI keeps its
commands, defaults, flags, socket behavior, platform identity, exit codes, and
machine-readable output. The SDKs keep create, connect, typed operation
names, package exports, result envelopes, structured errors, and lifecycle
behavior.
Private-worker options and new runtime configuration are additive. Changing a released interface or an observably different default requires a separate compatibility decision and the appropriate semantic-versioning release. Browser-use improvements can continue shipping on the current daemon-backed CLI while the SDK-owned refactor proceeds behind these gates.
TCC needs a stable responsible process. It does not require a global socket daemon.
flowchart TB
subgraph Embedded["Host-owned identity"]
HOST["Signed Host.app
owns TCC grants"]
INPROC["In-process Cua runtime"]
CHILD["Private child runtime"]
HOST -->|"same process"| INPROC
HOST -->|"direct spawn keeps responsibility chain"| CHILD
end
subgraph Standalone["Cua-owned identity"]
CUAAPP["Signed QwenCuaDriver.app
owns TCC grants"]
CUARUNTIME["App-owned runtime or child"]
CUAAPP --> CUARUNTIME
end
subgraph Avoid["Unsupported ownership"]
RANDOM["Unrelated launcher"]
GLOBAL["Discoverable global daemon"]
RANDOM -->|"identity or responsibility is ambiguous"| GLOBAL
end
The host must load the runtime directly or spawn its private worker without breaking the macOS responsibility chain. QwenCuaDriver.app can own the identity for standalone use. LaunchServices handoffs remain outside the supported embedding contract, but standalone installed MCP may use LaunchServices to reach QwenCuaDriver.app and preserve its stable TCC identity.
Direct permission checks are read-only. The embedding host owns permission UX and any restart required after Accessibility or Screen Recording grants change.
The trusted host chooses an immutable authorization ceiling when it creates a runtime. It then creates immutable session contexts beneath that ceiling before actions begin. Effective authority is the intersection of the runtime ceiling, the session context, managed policy, and user policy. A session can narrow but never widen the ceiling.
Released callers that do not use a trusted host session API inherit a compatibility session with today's daemon or runtime behavior. This preserves the current CLI and SDK contract while the permission model becomes portable across direct, worker, and service topologies.
flowchart LR
HOST["Trusted host"]
CEILING["Runtime authorization ceiling"]
RUNTIME["CuaDriver runtime"]
SESSION["Immutable session authorization"]
BINDING["Bound action surface or
authenticated connection"]
MODEL["Model or automation caller"]
TOOLS["Authorized tool execution"]
HOST -->|"constructs"| CEILING
CEILING --> RUNTIME
HOST -->|"creates before actions"| SESSION
RUNTIME --> SESSION
SESSION --> BINDING
MODEL --> BINDING --> TOOLS
MODEL -.->|"cannot create, select, or widen"| SESSION
BINDING -.->|"public session IDs are not authority"| CEILING
An unrestricted session suppresses Cua approval prompts only. Managed policy, user policy, hard invariants, TCC, resource ownership, revocation, and cleanup still apply. Its explicit acknowledgement must come from trusted host construction, not an agent-visible request.
A gateway may host several authorization modes beneath one runtime ceiling only when trusted host code creates the sessions and each action is bound to the corresponding authenticated connection or in-process action surface.
flowchart TB
GATEWAY["Trusted gateway"]
CEILING["Runtime authorization ceiling"]
subgraph RUNTIME["One runtime generation"]
STANDARD["Session A
standard"]
BOUNDED["Session B
bounded manifest"]
UNRESTRICTED["Session C
unrestricted acknowledgement"]
end
A["Bound action surface A"]
B["Authenticated connection B"]
C["Authenticated connection C"]
GATEWAY -->|"constructs"| CEILING
CEILING --> STANDARD --> A
CEILING --> BOUNDED --> B
CEILING --> UNRESTRICTED --> C
The model receives only the action surface already bound to its effective context. It never receives the host-only session factory, mode setter, connection proof, or serialized authority value. Public session IDs remain lifecycle labels and cannot select permission modes.
If a topology cannot provide a trusted binding, it exposes the compatibility session or uses a separate runtime-owner process per mode. Multiple in-process runtimes are a possible later optimization after every process-global facility has been isolated; they are not a security boundary against arbitrary host code.
The generated SDKs carry one typed contract into each supported language. Transports consume this contract instead of defining a second one.
flowchart LR
CORE["Private native core"]
ABI["Versioned C ABI"]
RUST["Safe Rust SDK"]
UNIFFI["Generated UniFFI bridge"]
PY["Python SDK"]
TS["TypeScript SDK"]
RUSTAPP["Rust applications and adapters"]
PYAPP["Python applications and adapters"]
TSAPP["TypeScript applications and adapters"]
CORE --> ABI --> RUST
RUST --> UNIFFI
UNIFFI --> PY
UNIFFI --> TS
RUST --> RUSTAPP
PY --> PYAPP
TS --> TSAPP
MCP stdio can create and own CuaDriver directly on Windows, Linux, and
embedded macOS paths. Standalone installed macOS MCP keeps the signed
QwenCuaDriver.app service identity by default. It does not add a second proxy layer
unless the deployment asks for service ownership or shared state.
Remote applications use the same generated SDK through an internal remote connection backend. The backend exchanges generated Driver envelopes through a minimal authenticated channel. gRPC may implement that channel, but it does not become the native core, public SDK contract, or a second tool vocabulary.
The current implementation still has process-global authorization and platform state. First, canonical dispatch receives a portable effective session context. Then one direct runtime per process becomes explicit and generation-scoped authorization and resource ownership move behind that runtime. Complete same-process multi-runtime isolation is later work.
flowchart LR
subgraph Today
GLOBALS["Process globals
permission mode, manifest, hooks"]
CLIENTA["Runtime object A"]
CLIENTB["Runtime object B"]
CLIENTA --> GLOBALS
CLIENTB --> GLOBALS
end
subgraph FirstMilestone["First milestone"]
PA["Process A
one runtime"]
COMPAT["Compatibility session
current behavior"]
PB["Process B
one runtime"]
SA["Authenticated session A"]
SB["Authenticated session B"]
FA["Process A facilities"]
FB["Process B facilities"]
PA --> COMPAT
PB --> SA
PB --> SB
PA --> FA
PB --> FB
end
GLOBALS -->|"make ownership explicit"| PA
GLOBALS -->|"isolate by process"| PB
State that must become runtime-owned includes:
Shared process facilities must be read-only, synchronized, or represented by an explicit process coordinator. A second direct runtime returns a structured conflict until those facilities can no longer merge authority.
flowchart LR
P0["1. Canonical authorization
at runtime dispatch"]
P1["2. Portable session
authorization"]
P2["3. Trusted session
creation and binding"]
P3["4. Runtime ownership
and single-runtime guard"]
P4["5. Make direct SDK
the documented default"]
P5["6. Platform-aware
MCP ownership"]
P6["7. Add private worker
for isolation"]
P7["8. Authenticate service mode
and measure compatibility use"]
P0 --> P1 --> P2 --> P3 --> P4 --> P5 --> P6 --> P7
These are target-state requirements. The compatibility daemon must meet the service-authentication invariant before it is presented as a mixed-trust service.
The SDK-first architecture is ready to become the default when:
connect() and the
long-lived daemon.CuaDriver.connect() remain a supported compatibility path?