packages/cua-driver/docs/sdk-rust-source-of-truth-and-uniffi-evaluation-plan.md
Status: superseded by the final package-boundary decision in PR #2341
Applies to: PR #2341 and follow-up SDK/runtime work
Primary objective: eliminate manual contract/runtime bookkeeping before publishing the Python and TypeScript SDKs
Decision rationale: Cua Driver integration surfaces: MCP/CLI and SDK bindings
Final decision update (2026-07-21): the evaluation below records the sequence and evidence that led to the implementation, but its interim recommendation to retain Python and TypeScript MCP facades is no longer current. The language packages now expose only the Rust-backed UniFFI SDK at their package roots. Agents use
cua-driver mcpthrough their runtime's existing MCP client. The thin language MCP clients and their generator were removed before publication.
Proceed with the Rust source-of-truth work while separating the agent boundary from the imported SDK boundary. MCP/CLI is the baseline agent architecture; UniFFI remains a candidate for distributing a Rust SDK/server implementation.
The recommended sequence is:
Directly embedding the GUI engine is not part of the current client-generation work. The existing runtime assumes a shared daemon, process-global state, and OS-specific main-thread and permission behavior that a host-process library cannot safely inherit without a separate design. A UniFFI facade over a shared Rust daemon client and a UniFFI embedded/server SDK are distinct candidates.
cua-driver-contract declares fourteen typed tools: four session lifecycle
tools and ten portable desktop-loop tools.cua-driver mcp over stdio.tools/list access for runtime-discovered
and platform-specific tools.--check drift mode.CuaDriver and
AsyncCuaDriver in Python, and CuaDriver in TypeScript.The four session tools build their live ToolDef metadata from canonical
contracts through ToolDef::from_contract. Their invocation logic remains
handwritten, so this proves metadata parity, not behavioral or output parity.
The ten desktop contracts are separately authored portable_subset
declarations. The platform crates do not consume those input types and instead
own richer handwritten ToolDef schemas plus field-by-field ArgsExt parsing.
No test currently proves that each portable input schema is accepted by every
corresponding live platform schema.
Capabilities also have two declaration sites today: the runtime capability map
and each ToolContract. Existing tests compare them, but do not eliminate the
duplicate ownership.
Registration-based counts at this revision are approximately:
These counts must be verified against live tools/list on each OS before being
used as release evidence. They must not become fixed global assertions because
the rosters intentionally differ and evolve. For example, Windows registers
debug_window_info, while Linux registers four additional pointer primitives.
The daemon serves concurrent connections through one shared registry and tool state. Core session hooks, activity, element tokens, and other facilities use process-global singletons. On macOS, AppKit surfaces require the main thread and permission identity is tied to the executable. These constraints are central to the transport decision.
Fleet provides a strong precedent for official UniFFI targets such as Python, Kotlin, Swift, and Ruby: its checked-in generator uses pinned Rust dependencies and supports regeneration checking.
Fleet also contains generated TypeScript examples:
uniffi-bindgen-react-native, @ubjs/core, and
@ubjs/node to load a native cdylib; andThose TypeScript artifacts prove feasibility, not a production-ready generation
pipeline. In the inspected tree, TypeScript is absent from Fleet's main
generation/check script, the React Native generator is not pinned in the Rust
workspace, the example @ubjs/* dependencies use latest, and the browser
WASM glue depends on configuration and generation that are not checked in.
Therefore:
--check flow as the standard the
TypeScript UniFFI spike must meet; andTransport-free Rust ToolSpec + typed inputs/results
|
+--> live ToolDef and tools/list metadata
+--> runtime typed input parsing
+--> structured-result validation
+--> portable and platform-rich projections
+--> contract manifest
+--> optional typed MCP-client generation
+--> optional UniFFI implementation exports
Agent surface:
Codex / Claude / other agent ---- MCP stdio ---- native daemon ---- OS APIs
Shell automation ---------------- CLI call ----- native daemon ---- OS APIs
Imported application SDK surface:
Python/TS typed client ----------- MCP stdio ---- native daemon ---- OS APIs
or
Language binding ---------------- UniFFI ------- Rust daemon client ---- daemon
or, after a separate host-runtime design
Language binding ---------------- UniFFI ------- embedded Rust engine ---- OS APIs
The transport-free specifications should live in the existing
cua-driver-contract crate or a narrowly scoped sibling rlib. All platform
crates must be able to depend on the types without importing MCP framing or a
public SDK runtime.
The typed layer must distinguish:
structuredContent payload, when present;A blanket adapter must not pretend every tool result is one serializable success object. Agent examples must use the existing agent SDK's MCP client directly; otherwise they would obscure the distinction this plan depends on.
Add a mechanical subset-and-existence test for the ten desktop contracts before the larger refactor.
For every declared platform and portable desktop tool, the test must prove:
Implement a small schema-subset checker for the schema constructs used by the ten contracts. Do not claim general JSON Schema implication. Fail explicitly on an unsupported keyword so the test cannot silently accept an unproven case.
ToolDef, registry, or capability
map changes.Define a ToolSpec model containing:
Add the new dependency from the macOS, Windows, and Linux platform crates to the specification crate. Keep dependency direction acyclic and transport-free.
Do not commit to schemars up front. First run a schema-dialect spike on one
session input and one complex desktop input. It must reproduce the existing
required/optional, closed-object, enum/const, nullability, default, and bounds
semantics with a bounded override surface. If it cannot, keep the existing
schema builders while still making typed Rust inputs authoritative.
Introduce an adapter that:
For a migrated tool, the live ToolDef must be derived from its spec and the
platform implementation must not retain a second schema literal or field-by-
field ArgsExt parser.
Start with the four session tools because their live metadata already comes from canonical contracts. This proves the adapter and output model with the lowest platform risk.
The model must cover both field-level richness and tool-set divergence:
debug_window_info; andPortable SDK inputs must be derived projections of richer typed declarations,
not parallel handwritten schemas. If one public union would be misleading,
define separate typed commands such as DesktopClickInput and
WindowClickInput, but route them to the same implementation family.
Migrate:
start_session and end_session for lifecycle and structured output;get_desktop_state for image plus structured metadata;click for portable versus rich targeting and destructive annotations;get_window_state for window-focused richness; andThe slice is complete only when:
tools/list metadata is spec-derived;After the slice stabilizes, migrate all fourteen methods currently generated by
PR #2341. Remove duplicated schemas and ad hoc parsing for those tools. Keep
compatibility aliases only in dispatch and hidden from tools/list and SDKs.
Migrate stable tools in independently reviewable families:
Each runtime roster entry must be classified as stable typed, experimental generic-only, compatibility alias, or internal/hidden. Do not encode one global tool count.
Configure each agent runtime's existing MCP client with cua-driver mcp, or
use cua-driver call from shell-oriented agents. Do not require generated Cua
language clients in this path. Maintain runnable Python Claude Agent SDK and
TypeScript Codex SDK examples as proof.
This is the O(1) agent-protocol surface: one MCP server is consumable from N MCP-capable runtimes. Generated clients add ergonomics for applications, not interoperability for these agents.
Continue generating Python and TypeScript types/methods while small native transports speak MCP stdio directly.
This preserves:
tools/list / tools/call escape hatch;cdylib; andThe remaining handwritten language code is a few small transport, result, and facade modules. This is a typed remote-client SDK, not a native implementation binding. Measure it against the actual application SDK requirement.
Build a platform-neutral Rust client that owns MCP framing and result normalization while still launching or connecting to the native driver daemon. Export only the vertical-slice API through UniFFI.
Run two distinct experiments:
uniffi-bindgen-react-native and @ubjs/node.The TypeScript experiment starts from a green-field reproducibility assumption, not from Fleet's manual artifacts. It must add:
--check mode;cdylib resolution from Cargo compiler-artifact output; andDo not implement browser/WASM packaging for local desktop automation in this spike.
The purpose of this option is to implement application client behavior once in Rust and distribute that implementation to N runtimes. Merely moving JSON-RPC framing into Rust is insufficient benefit; the spike must identify the shared lifecycle, normalization, policy, or server-composition behavior it owns.
This is the path for application developers who want to create or embed a Cua server. Do not treat it as a fallback transport change. A separate proposal would first need to eliminate or explicitly host:
For SDK option 1 and each language-specific SDK option 2 spike, record:
Decide Python and TypeScript independently. UniFFI may pass for Python and fail for TypeScript.
Adopt SDK option 2 for a language only if:
For TypeScript, failure to provide pinned deterministic generation and CI checking is an automatic rejection regardless of runtime benchmark results.
Otherwise retain SDK option 1. The source-of-truth work is complete and valuable regardless of this decision. Neither outcome changes direct MCP/CLI as the recommended agent integration.
Cancellation is not currently a runtime capability: dispatch awaits a tool directly, serve-layer deadlines do not cancel a running action, and individual tools contain their own bounded retry/polling behavior.
Do not block A0 or the initial typed migration on inventing cancellation. Before claiming cancellation parity or adopting a UniFFI transport, make one explicit decision:
Tool trait and typed adapter, defining safe interruption points and action
completion semantics; orIn either case, transports must never automatically resend an ambiguous state-changing request. Tests must distinguish transport resend, caller cancellation, and bounded retries within one tool invocation.
ToolDef schema or ArgsExt parser.auto, window, and desktop) enforce identical
policy behavior through generic calls and every generated binding option.Client exports.The contract/SDK workflow must trigger on changes to:
ToolDef, registry, and migrated implementation code;Required jobs:
Do not publish either binding option during the comparison.
Keep commits independently reviewable:
test(cua-driver): prove portable desktop contracts match live schemasrefactor(cua-driver): collapse tool capability ownershiprefactor(cua-driver): introduce transport-free typed tool specsrefactor(cua-driver): route session tools through typed runtime adapterrefactor(cua-driver): migrate divergent desktop vertical slicerefactor(cua-driver): migrate current portable desktop SDK toolstest(cua-driver): spike UniFFI Python Rust daemon clienttest(cua-driver): evaluate reproducible UniFFI Node bindingsBefore PR #2341 merges, land A0 and update its description to state the exact guarantee. The complete typed-runtime migration can follow without presenting the current manifest as full runtime parity.
Every public runtime tool is either typed with explicit platform support or
intentionally classified as generic-only, with no unclassified tools/list
entry on any supported OS.
| Risk | Mitigation |
|---|---|
| Portable declarations drift before the refactor finishes | Land A0 first and trigger it on platform/runtime changes. |
| Derived schema dialect differs from existing contracts | Gate schemars on a dialect-reproduction spike; keep bounded explicit builders if needed. |
| Platform richness creates unusable public unions | Model separate typed commands and explicit per-platform presence from one spec family. |
| Mixed MCP results do not fit one typed output | Type only structured payloads and stable errors; preserve the content envelope. |
| Migration scope expands across large platform files | Start with session tools, prove one divergent slice, then migrate by family. |
| TypeScript UniFFI inherits an immature toolchain | Treat it as green-field, pin everything, require deterministic generation and CI checking. |
| Native client libraries complicate distribution | Measure the full matrix and retain thin MCP unless benefit is material. |
| Cancellation is claimed but not implemented | Complete Workstream D or document non-cancelling deadline semantics. |
| Direct embedding breaks permission or global-state assumptions | Keep it outside this plan and require a separate OS-specific proof. |
Claude Code Opus independently reviewed the repository, the original plan, the three platform registries, the current SDK generators, and Fleet's UniFFI artifacts. This revision incorporates its must-fix findings:
The implementation keeps MCP/CLI as the public agent boundary and makes typed Rust the source for the current fourteen-tool typed-client SDK surface:
Measurements were taken from this checkout at version 0.10.0 before package
publication:
| Candidate | Maintained source | Generated source | Universal package |
|---|---|---|---|
| Python MCP SDK | 676 lines | 331 lines | 10,666-byte pure-Python wheel |
| TypeScript MCP SDK | 278 lines | 275 lines | 4,923-byte npm tarball; 19,506 bytes unpacked |
Both clients retain generic call_tool access and preserve the full MCP
content/result envelope. Their executable fixture suites cover initialization,
typed calls, errors, images, and generic runtime-only calls.
The comparison used the latest checked-in Fleet precedent at
c2ba0b5e94d0f2c06d0c7efb0913803ca0a616af rather than creating a second
throwaway binding implementation with the same toolchain:
uniffi-bindgen-react-native; this is not merely WASM.@ubjs/core, @ubjs/node, and a colocated native cdylib,
and are not part of Fleet's pinned four-language generation/check script.
The runnable example declares those runtime/build dependencies as latest.Python: retain the typed MCP client in this pull request. UniFFI itself passes the feasibility and reproducibility gate, but Cua Driver does not yet have a shared Rust daemon client with meaningful application behavior. A facade over today's transport would add a per-platform, per-architecture cdylib and native-loader failure mode to a 10.4 KiB universal wheel while still speaking to the existing daemon.
TypeScript: retain the typed MCP client in this pull request. The current precedent fails the plan's automatic adoption gate: generation is outside the pinned drift-check script, the runtime packages are unpinned, and consumers must locate a host-native library. Technical feasibility does not yet make it a reproducible npm release pipeline.
This package decision does not reject a UniFFI embedded/server SDK and does not claim the generated clients improve MCP interoperability. MCP-capable agents should connect directly, as the agent examples demonstrate. A follow-up SDK proposal must decide whether its consumer needs a shared Rust daemon client or an embedded/server implementation, then evaluate UniFFI against that target.
No rejected UniFFI spike code is retained. Cold-start and steady-state comparisons are intentionally not claimed: neither native candidate passed the packaging/reproducibility pre-gates that justify producing a release-matrix prototype. Reconsider Python when a reusable Rust daemon client or supported embedded host exists; reconsider TypeScript after pinned generation, deterministic CI checking, and native package loading land in the repository.