agent-desktop-ffi
Direct C-ABI access to every PlatformAdapter operation. Build the
cdylib with the workspace's release-ffi profile:
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 — who allocates / who frees,
for every
*mut T the FFI hands back to the caller.
- error-handling.md — errno-style
last-error contract, enum validation, panic boundary.
- threading.md — host-thread contract,
cross-process mutation serialization, AXIsProcessTrusted inheritance,
and adapter-bound native handles.
- 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 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 fallbacks.
Generation-safe direct APIs. Legacy AdRefEntry and
AdWindowInfo layouts remain available for binary compatibility, but they do
not carry process-generation evidence and direct targeting functions fail
closed. Use AdExactRefEntry, AdExactWindowInfo,
ad_list_windows_exact, and the *_exact targeting symbols. Likewise,
ad_list_surfaces_exact preserves SurfaceInfo.id; the legacy surface list
is an observation-only projection that omits it.
Display discovery. Call ad_list_displays before using
AdScreenshotTarget.screen_index. List order is the screenshot index order;
inspect each AdDisplayInfo for its stable display ID, bounds, primary flag,
and scale, then release the handle with ad_display_list_free.
Low-level action paths. ad_execute_action (headless, no preflight) and
ad_execute_action_with_policy are raw escape hatches for callers holding a live
AdNativeHandle from ad_resolve_element_exact / ad_find_exact. Use them when
you need to bypass the ref-action pipeline.
Ref-action preflight. ad_execute_by_ref and
ad_execute_ref_action_exact_with_policy both resolve the element strictly and run the live actionability preflight
(visible, stable, enabled, supported action, policy, editable) before dispatching
— a disabled or unsupported target fails before any platform call. On
AD_RESULT_ERR_ACTION_FAILED, the structured check report is available as JSON
via ad_last_error_details().
Action result steps. AdActionResult.steps mirrors the CLI steps array
for activation-chain actions. Each entry has label and outcome strings and
is owned by the result; release with ad_free_action_result(&out).
Tracing / log callback. Two tracing surfaces coexist:
Structured file trace — same JSONL contract as CLI --trace, gated by a
trace: on session manifest. Segments include event, ts_ms, seq, and
redacted fields. Requires session start (or equivalent manifest on disk)
before creating the adapter; plain session-id adapters write nothing to disk.
ad_set_log_callback(cb) — installs a tracing subscriber layer that
delivers events as JSON to your callback. cb receives an int32_t level
(1=ERROR … 5=TRACE) and a const char *msg valid only for the duration of
the call. Pass NULL to unregister. The layer is installed on the first
non-null call; if a foreign global subscriber already owns the process at
that point, the install fails with AD_RESULT_ERR_INTERNAL and no events are
ever delivered. Sensitive field values (password, token, text, …) are
replaced with {"redacted":true} before formatting. A panicking callback is
caught and silently discarded. The callback may fire from threads other than
the registering thread, and may still fire briefly after a NULL unregister
— keep the callback and any data it captures valid for the process lifetime.
Wait. ad_wait(adapter, args, &out) runs the full CLI wait command
(element-appear, window-appear, text-appear, menu-open/close, notification,
element predicates). Zero-initialize AdWaitArgs, set the fields you need, and
validate the struct size against AD_WAIT_ARGS_SIZE / ad_wait_args_size() before
calling. The output is a {version, ok, command, data} JSON envelope freed with
ad_free_string. ad_wait blocks the calling thread up to timeout_ms ms —
ensure the adapter is not destroyed from another thread while it is running.
Text input privacy. On macOS, focus-fallback or headed text insertion may
briefly use the clipboard for non-ASCII text. For sensitive text, prefer
AD_ACTION_KIND_SET_VALUE with AD_POLICY_KIND_HEADLESS when the target
supports settable values.
Enum discriminants. Every #[repr(i32)] enum field is validated at the C
boundary — invalid discriminants return AD_RESULT_ERR_INVALID_ARGS instead of
undefined behavior.
ABI stability. The major version in AD_ABI_VERSION_MAJOR increments on any
breaking change (removed symbol, incompatible layout). Additive changes (new
symbols, new error codes) do not bump it. Before 1.0, pin the exact version of
libagent_desktop_ffi you link against.
ad_get_tree_exact vs ad_snapshot. ad_get_tree_exact returns a raw flat BFS tree
without @e refs, no refmap persistence, and no JSON envelope — use it for
custom traversal or UI inspection. For observe-act agents that drive actions via
ad_execute_by_ref, always start with ad_snapshot.
1---2name: agent-desktop-ffi3description: C-ABI bindings over agent-desktop's PlatformAdapter. Consumers (Python ctypes, Swift, Node ffi-napi, Go cgo, C++, Ruby fiddle) link libagent_desktop_ffi.{dylib,so,dll} and call `ad_*` functions directly instead of spawning the CLI binary per call. The canonical observe-act workflow is: ad_init → ad_adapter_create[_with_session] → ad_snapshot → parse @e refs → ad_execute_by_ref → ad_free_string → ad_adapter_destroy.4---56# agent-desktop-ffi78Direct C-ABI access to every PlatformAdapter operation. Build the9cdylib with the workspace's `release-ffi` profile:1011```sh12cargo build --profile release-ffi -p agent-desktop-ffi13```1415The output is `target/release-ffi/libagent_desktop_ffi.dylib`16(`.so` on Linux, `.dll` on Windows) plus a committed C header at17`crates/ffi/include/agent_desktop.h`.1819A Python ctypes smoke harness lives at `tests/ffi-python/smoke.py` and20serves as a worked end-to-end example covering the ABI handshake, struct21size validation, `ad_version`, and the snapshot pipeline leg. See22`tests/ffi-python/README.md` for usage.2324Four reference topics, loaded as needed:2526- [ownership.md](references/ownership.md) — who allocates / who frees,27 for every `*mut T` the FFI hands back to the caller.28- [error-handling.md](references/error-handling.md) — errno-style29 last-error contract, enum validation, panic boundary.30- [threading.md](references/threading.md) — host-thread contract,31 cross-process mutation serialization, AXIsProcessTrusted inheritance,32 and adapter-bound native handles.33- [build-and-link.md](references/build-and-link.md) — ABI handshake,34 struct size validation, minimal C and Python examples, observe-act35 workflow, and prebuilt archive locations.3637## Observe-act workflow (canonical path)3839```40ad_init(AD_ABI_VERSION_MAJOR) // verify header ↔ dylib match41adapter = ad_adapter_create_with_session("s1") // or ad_adapter_create()42rc = ad_snapshot(adapter, "Finder", 0, 10, false, false, &json_out)43// parse json_out: locate snapshot-qualified refs in data.tree44ad_free_string(json_out)45// build action:46AdAction act = {0}; act.kind = AD_ACTION_KIND_CLICK;47rc = ad_execute_by_ref(adapter, "@s8f3k2p9:e5", NULL, &act, 0, &result_out)48ad_free_string(result_out)49ad_adapter_destroy(adapter)50```5152`ad_snapshot` returns a `{version, ok, command, data}` JSON envelope53identical to the CLI output. The `data.tree` field contains snapshot-qualified54ref IDs for interactive elements. Pass a qualified ref, or a legacy bare ref55plus its explicit `snapshot_id`, to `ad_execute_by_ref` to drive the pipeline56(RefStore load → strict resolution → actionability preflight → dispatch).5758## Core constraints5960- **ABI handshake.** Call `ad_init(AD_ABI_VERSION_MAJOR)` once after loading the61 dylib. A mismatch between the compiled-in constant and the loaded dylib returns62 `AD_RESULT_ERR_INVALID_ARGS` — abort rather than proceed. You can also read the63 raw dylib major via `ad_abi_version()` for diagnostic display. New `ad_*` symbols64 and new error codes are additive (no bump required); removed or layout-changed65 symbols increment the major.6667- **Session adapters.** `ad_adapter_create_with_session("session-id")` associates68 the adapter with a session namespace for refmap persistence — the same as CLI69 `--session <id>`. A null `snapshot_id` is valid only for a qualified ref;70 legacy bare `@eN` refs require an explicit snapshot ID. Session IDs: 1–6471 chars, ASCII alphanumeric / `-` / `_`.72 Invalid IDs return null (check `ad_last_error_*`).7374- **Structured session trace (no ABI change).** File-based JSONL tracing activates75 only when the session has a manifest with `trace: on` from `session start`76 (CLI) or equivalent on-disk setup. `ad_adapter_create_with_session` alone does77 **not** create trace files. When tracing is active, `command_context()`-backed78 commands append to one segment per OS process under79 `~/.agent-desktop/sessions/<id>/trace/<pid>-<procTs>.jsonl`. A long-lived host80 reuses the same segment filename for all calls in that process. For unstructured81 diagnostics regardless of session manifest, use `ad_set_log_callback` (below).8283- **Threading and mutation leases.** Adapter entrypoints may be called from any84 host thread. Native handles remain bound to their creating adapter and thread.85 Desktop mutations acquire the same canonical cross-process interaction lease86 as the CLI; reads carry finite deadlines without taking the mutation lock. See87 [threading.md](references/threading.md) for the Apple documentation basis and88 the read/read, read/mutation, and mutation/mutation ordering matrix.8990- **Release profile.** `cargo build --release` produces `panic = "abort"` —91 any Rust panic inside an `extern "C"` fn will `SIGABRT` the host. Use92 `--profile release-ffi` to get the correct `panic = "unwind"` profile. CI93 enforces this.9495- **Last-error lifetime.** Pointers returned by `ad_last_error_*` remain valid96 across any number of subsequent *successful* FFI calls on the same thread.97 Only the next failing call rotates them. Cache the pointer once, read it as98 many times as you need.99100- **ad_last_error_details.** A fourth accessor, `ad_last_error_details()`,101 returns a borrowed JSON string carrying structured details (e.g. the102 actionability check report on `ACTION_FAILED`, candidate summaries on103 `AMBIGUOUS_TARGET`). The details may contain element names, values, and window104 titles from the user's screen — treat as sensitive diagnostics and avoid routing105 to shared log surfaces.106107- **Handle release.** Every `ad_resolve_element_exact` / `ad_find_exact` result must be108 released with `ad_free_handle(adapter, &handle)` on the same adapter that109 produced it, before that adapter is destroyed. On macOS this balances the110 internal `CFRetain`; on Windows/Linux the call is a no-op but safe to issue.111 `ad_free_handle` zeroes `handle.ptr` so a follow-up call is a safe no-op.112113- **Primary ref-action path.** `ad_execute_by_ref` is the recommended entrypoint114 for the observe-act loop: it loads the RefStore, looks up the ref in the refmap115 (STALE_REF on miss), runs strict element re-identification (STALE_REF / AMBIGUOUS_TARGET),116 runs the live actionability preflight, then dispatches. TypeText and PressKey117 default to `focus_fallback` policy (matching CLI `type`/`press-key`); all other118 actions default to `headless`. Pass `AD_POLICY_KIND_HEADED` (2) to opt in to119 cursor-based fallbacks.120121- **Generation-safe direct APIs.** Legacy `AdRefEntry` and122 `AdWindowInfo` layouts remain available for binary compatibility, but they do123 not carry process-generation evidence and direct targeting functions fail124 closed. Use `AdExactRefEntry`, `AdExactWindowInfo`,125 `ad_list_windows_exact`, and the `*_exact` targeting symbols. Likewise,126 `ad_list_surfaces_exact` preserves `SurfaceInfo.id`; the legacy surface list127 is an observation-only projection that omits it.128129- **Display discovery.** Call `ad_list_displays` before using130 `AdScreenshotTarget.screen_index`. List order is the screenshot index order;131 inspect each `AdDisplayInfo` for its stable display ID, bounds, primary flag,132 and scale, then release the handle with `ad_display_list_free`.133134- **Low-level action paths.** `ad_execute_action` (headless, no preflight) and135 `ad_execute_action_with_policy` are raw escape hatches for callers holding a live136 `AdNativeHandle` from `ad_resolve_element_exact` / `ad_find_exact`. Use them when137 you need to bypass the ref-action pipeline.138139- **Ref-action preflight.** `ad_execute_by_ref` and140 `ad_execute_ref_action_exact_with_policy` both resolve the element strictly and run the live actionability preflight141 (visible, stable, enabled, supported action, policy, editable) before dispatching142 — a disabled or unsupported target fails before any platform call. On143 `AD_RESULT_ERR_ACTION_FAILED`, the structured check report is available as JSON144 via `ad_last_error_details()`.145146- **Action result steps.** `AdActionResult.steps` mirrors the CLI `steps` array147 for activation-chain actions. Each entry has `label` and `outcome` strings and148 is owned by the result; release with `ad_free_action_result(&out)`.149150- **Tracing / log callback.** Two tracing surfaces coexist:151152 1. **Structured file trace** — same JSONL contract as CLI `--trace`, gated by a153 `trace: on` session manifest. Segments include `event`, `ts_ms`, `seq`, and154 redacted fields. Requires `session start` (or equivalent manifest on disk)155 before creating the adapter; plain session-id adapters write nothing to disk.156157 2. **`ad_set_log_callback(cb)`** — installs a `tracing` subscriber layer that158 delivers events as JSON to your callback. `cb` receives an int32_t level159 (1=ERROR … 5=TRACE) and a `const char *msg` valid only for the duration of160 the call. Pass `NULL` to unregister. The layer is installed on the first161 non-null call; if a foreign global subscriber already owns the process at162 that point, the install fails with `AD_RESULT_ERR_INTERNAL` and no events are163 ever delivered. Sensitive field values (password, token, text, …) are164 replaced with `{"redacted":true}` before formatting. A panicking callback is165 caught and silently discarded. The callback may fire from threads other than166 the registering thread, and may still fire briefly after a `NULL` unregister167 — keep the callback and any data it captures valid for the process lifetime.168169- **Wait.** `ad_wait(adapter, args, &out)` runs the full CLI `wait` command170 (element-appear, window-appear, text-appear, menu-open/close, notification,171 element predicates). Zero-initialize `AdWaitArgs`, set the fields you need, and172 validate the struct size against `AD_WAIT_ARGS_SIZE` / `ad_wait_args_size()` before173 calling. The output is a `{version, ok, command, data}` JSON envelope freed with174 `ad_free_string`. `ad_wait` blocks the calling thread up to `timeout_ms` ms —175 ensure the adapter is not destroyed from another thread while it is running.176177- **Text input privacy.** On macOS, focus-fallback or headed text insertion may178 briefly use the clipboard for non-ASCII text. For sensitive text, prefer179 `AD_ACTION_KIND_SET_VALUE` with `AD_POLICY_KIND_HEADLESS` when the target180 supports settable values.181182- **Enum discriminants.** Every `#[repr(i32)]` enum field is validated at the C183 boundary — invalid discriminants return `AD_RESULT_ERR_INVALID_ARGS` instead of184 undefined behavior.185186- **ABI stability.** The major version in `AD_ABI_VERSION_MAJOR` increments on any187 breaking change (removed symbol, incompatible layout). Additive changes (new188 symbols, new error codes) do not bump it. Before 1.0, pin the exact version of189 libagent_desktop_ffi you link against.190191- **`ad_get_tree_exact` vs `ad_snapshot`.** `ad_get_tree_exact` returns a raw flat BFS tree192 without `@e` refs, no refmap persistence, and no JSON envelope — use it for193 custom traversal or UI inspection. For observe-act agents that drive actions via194 `ad_execute_by_ref`, always start with `ad_snapshot`.