Skip to main content
ClaudeWave
Skill1.1k repo starsupdated 2d ago

agent-desktop-ffi

The agent-desktop-ffi library provides C-ABI bindings for desktop automation operations across macOS, Windows, and Linux, enabling direct C function calls to interact with platform accessibility APIs. Use this when integrating desktop automation capabilities into non-Rust applications via languages like Python or C that can load shared libraries, ensuring strict adherence to main-thread-only execution on macOS, proper handle lifecycle management, and the release-ffi build profile to prevent runtime panics.

Install in Claude Code
Copy
git clone --depth 1 https://github.com/lahfir/agent-desktop /tmp/agent-desktop-ffi && cp -r /tmp/agent-desktop-ffi/skills/agent-desktop-ffi ~/.claude/skills/agent-desktop-ffi
Then start a new Claude Code session; the skill loads automatically.

SKILL.md

# agent-desktop-ffi

Direct C-ABI access to every PlatformAdapter operation. Build the
cdylib with the workspace's `release-ffi` profile:

```sh
cargo build --profile release-ffi -p agent-desktop-ffi
```

The output is `target/release-ffi/libagent_desktop_ffi.dylib`
(`.so` on Linux, `.dll` on Windows) plus a committed C header at
`crates/ffi/include/agent_desktop.h`.

A Python ctypes smoke harness lives at `tests/ffi-python/smoke.py` and
serves as a worked end-to-end example covering the ABI handshake, struct
size validation, `ad_version`, and the snapshot pipeline leg. See
`tests/ffi-python/README.md` for usage.

Four reference topics, loaded as needed:

- [ownership.md](references/ownership.md) — who allocates / who frees,
  for every `*mut T` the FFI hands back to the caller.
- [error-handling.md](references/error-handling.md) — errno-style
  last-error contract, enum validation, panic boundary.
- [threading.md](references/threading.md) — host-thread contract,
  cross-process mutation serialization, AXIsProcessTrusted inheritance,
  and adapter-bound native handles.
- [build-and-link.md](references/build-and-link.md) — ABI handshake,
  struct size validation, minimal C and Python examples, observe-act
  workflow, and prebuilt archive locations.

## Observe-act workflow (canonical path)

```
ad_init(AD_ABI_VERSION_MAJOR)                    // verify header ↔ dylib match
adapter = ad_adapter_create_with_session("s1")   // or ad_adapter_create()
rc = ad_snapshot(adapter, "Finder", 0, 10, false, false, &json_out)
// parse json_out: locate snapshot-qualified refs in data.tree
ad_free_string(json_out)
// build action:
AdAction act = {0}; act.kind = AD_ACTION_KIND_CLICK;
rc = ad_execute_by_ref(adapter, "@s8f3k2p9:e5", NULL, &act, 0, &result_out)
ad_free_string(result_out)
ad_adapter_destroy(adapter)
```

`ad_snapshot` returns a `{version, ok, command, data}` JSON envelope
identical to the CLI output. The `data.tree` field contains snapshot-qualified
ref IDs for interactive elements. Pass a qualified ref, or a legacy bare ref
plus its explicit `snapshot_id`, to `ad_execute_by_ref` to drive the pipeline
(RefStore load → strict resolution → actionability preflight → dispatch).

## Core constraints

- **ABI handshake.** Call `ad_init(AD_ABI_VERSION_MAJOR)` once after loading the
  dylib. A mismatch between the compiled-in constant and the loaded dylib returns
  `AD_RESULT_ERR_INVALID_ARGS` — abort rather than proceed. You can also read the
  raw dylib major via `ad_abi_version()` for diagnostic display. New `ad_*` symbols
  and new error codes are additive (no bump required); removed or layout-changed
  symbols increment the major.

- **Session adapters.** `ad_adapter_create_with_session("session-id")` associates
  the adapter with a session namespace for refmap persistence — the same as CLI
  `--session <id>`. A null `snapshot_id` is valid only for a qualified ref;
  legacy bare `@eN` refs require an explicit snapshot ID. Session IDs: 1–64
  chars, ASCII alphanumeric / `-` / `_`.
  Invalid IDs return null (check `ad_last_error_*`).

- **Structured session trace (no ABI change).** File-based JSONL tracing activates
  only when the session has a manifest with `trace: on` from `session start`
  (CLI) or equivalent on-disk setup. `ad_adapter_create_with_session` alone does
  **not** create trace files. When tracing is active, `command_context()`-backed
  commands append to one segment per OS process under
  `~/.agent-desktop/sessions/<id>/trace/<pid>-<procTs>.jsonl`. A long-lived host
  reuses the same segment filename for all calls in that process. For unstructured
  diagnostics regardless of session manifest, use `ad_set_log_callback` (below).

- **Threading and mutation leases.** Adapter entrypoints may be called from any
  host thread. Native handles remain bound to their creating adapter and thread.
  Desktop mutations acquire the same canonical cross-process interaction lease
  as the CLI; reads carry finite deadlines without taking the mutation lock. See
  [threading.md](references/threading.md) for the Apple documentation basis and
  the read/read, read/mutation, and mutation/mutation ordering matrix.

- **Release profile.** `cargo build --release` produces `panic = "abort"` —
  any Rust panic inside an `extern "C"` fn will `SIGABRT` the host. Use
  `--profile release-ffi` to get the correct `panic = "unwind"` profile. CI
  enforces this.

- **Last-error lifetime.** Pointers returned by `ad_last_error_*` remain valid
  across any number of subsequent *successful* FFI calls on the same thread.
  Only the next failing call rotates them. Cache the pointer once, read it as
  many times as you need.

- **ad_last_error_details.** A fourth accessor, `ad_last_error_details()`,
  returns a borrowed JSON string carrying structured details (e.g. the
  actionability check report on `ACTION_FAILED`, candidate summaries on
  `AMBIGUOUS_TARGET`). The details may contain element names, values, and window
  titles from the user's screen — treat as sensitive diagnostics and avoid routing
  to shared log surfaces.

- **Handle release.** Every `ad_resolve_element_exact` / `ad_find_exact` result must be
  released with `ad_free_handle(adapter, &handle)` on the same adapter that
  produced it, before that adapter is destroyed. On macOS this balances the
  internal `CFRetain`; on Windows/Linux the call is a no-op but safe to issue.
  `ad_free_handle` zeroes `handle.ptr` so a follow-up call is a safe no-op.

- **Primary ref-action path.** `ad_execute_by_ref` is the recommended entrypoint
  for the observe-act loop: it loads the RefStore, looks up the ref in the refmap
  (STALE_REF on miss), runs strict element re-identification (STALE_REF / AMBIGUOUS_TARGET),
  runs the live actionability preflight, then dispatches. TypeText and PressKey
  default to `focus_fallback` policy (matching CLI `type`/`press-key`); all other
  actions default to `headless`. Pass `AD_POLICY_KIND_HEADED` (2) to opt in to
  cursor-based fa