{"id":"75da2552-1cd6-4dd1-a6e0-ae62a71256a8","entityType":"agent","slug":"clawhub-lahfir-agent-desktop-ffi","name":"agent-desktop-ffi","canonicalUrl":"https://www.xpersona.co/agent/clawhub-lahfir-agent-desktop-ffi","canonicalPath":"/agent/clawhub-lahfir-agent-desktop-ffi","generatedAt":"2026-10-10T13:31:58.328Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T10:38:55.243Z","emptyReason":null},"description":"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.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.5K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s1793830nng35p6y70jxm4ybt984y8ka:agent-desktop-ffi","sourceUrl":"https://clawhub.ai/lahfir/agent-desktop-ffi","homepage":"https://clawhub.ai/lahfir/skills/agent-desktop-ffi","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/lahfir/agent-desktop-ffi","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/lahfir/skills/agent-desktop-ffi","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":63,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"agent-desktop-ffi technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T10:38:55.243Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T10:38:55.243Z","emptyReason":null},"stars":null,"forks":null,"downloads":1490,"packageName":null,"latestVersion":"1.0.7","tractionLabel":"1.5K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T10:38:55.242Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T10:38:55.243Z","lastCrawledAt":"2026-10-10T10:38:55.242Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T10:38:55.242Z","lastVerifiedAt":null,"highlights":[{"version":"1.0.7","createdAt":"2026-08-28T00:35:42.860Z","changelog":"Release v0.8.4","fileCount":7,"zipByteSize":20459},{"version":"1.0.6","createdAt":"2026-07-20T07:30:47.361Z","changelog":"Release v0.5.0","fileCount":7,"zipByteSize":20385},{"version":"1.0.5","createdAt":"2026-07-02T00:13:37.881Z","changelog":"Release v0.4.6","fileCount":7,"zipByteSize":18443},{"version":"1.0.4","createdAt":"2026-06-27T02:16:27.530Z","changelog":"Release v0.4.2","fileCount":6,"zipByteSize":16561},{"version":"1.0.3","createdAt":"2026-06-21T01:41:20.806Z","changelog":"Release v0.3.1","fileCount":7,"zipByteSize":11744},{"version":"1.0.2","createdAt":"2026-06-20T20:05:30.351Z","changelog":"Release v0.3.0","fileCount":7,"zipByteSize":11878},{"version":"1.0.1","createdAt":"2026-05-20T01:30:28.669Z","changelog":"Release v0.2.0","fileCount":7,"zipByteSize":10974},{"version":"1.0.0","createdAt":"2026-04-17T11:02:13.227Z","changelog":"Initial release of agent-desktop-ffi. - Provides direct C-ABI bindings to agent-desktop's PlatformAdapter for use in Python, Swift, Node, Go, C++, and Ruby. - Distributes a shared library (`libagent_desktop_ffi.{dylib, so, dll}`) and C header for integration. - Enforces core constraints, including main-thread-only execution on macOS and safe enum handling. - Documents error handling, handle ownership, threading, and build/linking procedures. - ABI is marked unstable pre-1.0; breaking changes may occur between minor versions.","fileCount":6,"zipByteSize":8912}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s1793830nng35p6y70jxm4ybt984y8ka:agent-desktop-ffi","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s1793830nng35p6y70jxm4ybt984y8ka:agent-desktop-ffi` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/lahfir/agent-desktop-ffi before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-lahfir-agent-desktop-ffi/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-lahfir-agent-desktop-ffi/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-lahfir-agent-desktop-ffi/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-lahfir-agent-desktop-ffi/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-lahfir-agent-desktop-ffi/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-lahfir-agent-desktop-ffi/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T13:31:58.324Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-lahfir-agent-desktop-ffi/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-lahfir-agent-desktop-ffi/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-lahfir-agent-desktop-ffi/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-lahfir-agent-desktop-ffi/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T10:38:55.243Z","emptyReason":null},"readme":"Skill: agent-desktop-ffi\n\nOwner: lahfir\n\nSummary: 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.\n\nTags: latest:1.0.7\n\nVersion history:\n\nv1.0.7 | 2026-08-28T00:35:42.860Z | user\n\nRelease v0.8.4\n\nv1.0.6 | 2026-07-20T07:30:47.361Z | user\n\nRelease v0.5.0\n\nv1.0.5 | 2026-07-02T00:13:37.881Z | user\n\nRelease v0.4.6\n\nv1.0.4 | 2026-06-27T02:16:27.530Z | user\n\nRelease v0.4.2\n\nv1.0.3 | 2026-06-21T01:41:20.806Z | user\n\nRelease v0.3.1\n\nv1.0.2 | 2026-06-20T20:05:30.351Z | user\n\nRelease v0.3.0\n\nv1.0.1 | 2026-05-20T01:30:28.669Z | user\n\nRelease v0.2.0\n\nv1.0.0 | 2026-04-17T11:02:13.227Z | auto\n\nInitial release of agent-desktop-ffi.\n\n- Provides direct C-ABI bindings to agent-desktop's PlatformAdapter for use in Python, Swift, Node, Go, C++, and Ruby.\n- Distributes a shared library (`libagent_desktop_ffi.{dylib, so, dll}`) and C header for integration.\n- Enforces core constraints, including main-thread-only execution on macOS and safe enum handling.\n- Documents error handling, handle ownership, threading, and build/linking procedures.\n- ABI is marked unstable pre-1.0; breaking changes may occur between minor versions.\n\nArchive index:\n\nArchive v1.0.7: 7 files, 20459 bytes\n\nFiles: references/build-and-link.md (8898b), references/error-handling.md (8544b), references/ownership.md (8514b), references/threading.md (7115b), skill-card.md (3086b), SKILL.md (11267b), _meta.json (136b)\n\nFile v1.0.7:SKILL.md\n\n---\nname: agent-desktop-ffi\nversion: 0.4.1\ntags: ffi, c-bindings, cdylib, python, swift, node, go, rust-ffi\nrequirements:\n  - agent-desktop-ffi\ndescription: >\n  C-ABI bindings over agent-desktop's PlatformAdapter. Consumers\n  (Python ctypes, Swift, Node ffi-napi, Go cgo, C++, Ruby fiddle)\n  link libagent_desktop_ffi.{dylib,so,dll} and call `ad_*` functions\n  directly instead of spawning the CLI binary per call. The canonical\n  observe-act workflow is: ad_init → ad_adapter_create[_with_session]\n  → ad_snapshot → parse @e refs → ad_execute_by_ref → ad_free_string\n  → ad_adapter_destroy.\n---\n\n# agent-desktop-ffi\n\nDirect C-ABI access to every PlatformAdapter operation. Build the\ncdylib with the workspace's `release-ffi` profile:\n\n```sh\ncargo build --profile release-ffi -p agent-desktop-ffi\n```\n\nThe output is `target/release-ffi/libagent_desktop_ffi.dylib`\n(`.so` on Linux, `.dll` on Windows) plus a committed C header at\n`crates/ffi/include/agent_desktop.h`.\n\nA Python ctypes smoke harness lives at `tests/ffi-python/smoke.py` and\nserves as a worked end-to-end example covering the ABI handshake, struct\nsize validation, `ad_version`, and the snapshot pipeline leg. See\n`tests/ffi-python/README.md` for usage.\n\nFour reference topics, loaded as needed:\n\n- [ownership.md](references/ownership.md) — who allocates / who frees,\n  for every `*mut T` the FFI hands back to the caller.\n- [error-handling.md](references/error-handling.md) — errno-style\n  last-error contract, enum validation, panic boundary.\n- [threading.md](references/threading.md) — host-thread contract,\n  cross-process mutation serialization, AXIsProcessTrusted inheritance,\n  and adapter-bound native handles.\n- [build-and-link.md](references/build-and-link.md) — ABI handshake,\n  struct size validation, minimal C and Python examples, observe-act\n  workflow, and prebuilt archive locations.\n\n## Observe-act workflow (canonical path)\n\n```\nad_init(AD_ABI_VERSION_MAJOR)                    // verify header ↔ dylib match\nadapter = ad_adapter_create_with_session(\"s1\")   // or ad_adapter_create()\nrc = ad_snapshot(adapter, \"Finder\", 0, 10, false, false, &json_out)\n// parse json_out: locate snapshot-qualified refs in data.tree\nad_free_string(json_out)\n// build action:\nAdAction act = {0}; act.kind = AD_ACTION_KIND_CLICK;\nrc = ad_execute_by_ref(adapter, \"@s8f3k2p9:e5\", NULL, &act, 0, &result_out)\nad_free_string(result_out)\nad_adapter_destroy(adapter)\n```\n\n`ad_snapshot` returns a `{version, ok, command, data}` JSON envelope\nidentical to the CLI output. The `data.tree` field contains snapshot-qualified\nref IDs for interactive elements. Pass a qualified ref, or a legacy bare ref\nplus its explicit `snapshot_id`, to `ad_execute_by_ref` to drive the pipeline\n(RefStore load → strict resolution → actionability preflight → dispatch).\n\n## Core constraints\n\n- **ABI handshake.** Call `ad_init(AD_ABI_VERSION_MAJOR)` once after loading the\n  dylib. A mismatch between the compiled-in constant and the loaded dylib returns\n  `AD_RESULT_ERR_INVALID_ARGS` — abort rather than proceed. You can also read the\n  raw dylib major via `ad_abi_version()` for diagnostic display. New `ad_*` symbols\n  and new error codes are additive (no bump required); removed or layout-changed\n  symbols increment the major.\n\n- **Session adapters.** `ad_adapter_create_with_session(\"session-id\")` associates\n  the adapter with a session namespace for refmap persistence — the same as CLI\n  `--session <id>`. A null `snapshot_id` is valid only for a qualified ref;\n  legacy bare `@eN` refs require an explicit snapshot ID. Session IDs: 1–64\n  chars, ASCII alphanumeric / `-` / `_`.\n  Invalid IDs return null (check `ad_last_error_*`).\n\n- **Structured session trace (no ABI change).** File-based JSONL tracing activates\n  only when the session has a manifest with `trace: on` from `session start`\n  (CLI) or equivalent on-disk setup. `ad_adapter_create_with_session` alone does\n  **not** create trace files. When tracing is active, `command_context()`-backed\n  commands append to one segment per OS process under\n  `~/.agent-desktop/sessions/<id>/trace/<pid>-<procTs>.jsonl`. A long-lived host\n  reuses the same segment filename for all calls in that process. For unstructured\n  diagnostics regardless of session manifest, use `ad_set_log_callback` (below).\n\n- **Threading and mutation leases.** Adapter entrypoints may be called from any\n  host thread. Native handles remain bound to their creating adapter and thread.\n  Desktop mutations acquire the same canonical cross-process interaction lease\n  as the CLI; reads carry finite deadlines without taking the mutation lock. See\n  [threading.md](references/threading.md) for the Apple documentation basis and\n  the read/read, read/mutation, and mutation/mutation ordering matrix.\n\n- **Release profile.** `cargo build --release` produces `panic = \"abort\"` —\n  any Rust panic inside an `extern \"C\"` fn will `SIGABRT` the host. Use\n  `--profile release-ffi` to get the correct `panic = \"unwind\"` profile. CI\n  enforces this.\n\n- **Last-error lifetime.** Pointers returned by `ad_last_error_*` remain valid\n  across any number of subsequent *successful* FFI calls on the same thread.\n  Only the next failing call rotates them. Cache the pointer once, read it as\n  many times as you need.\n\n- **ad_last_error_details.** A fourth accessor, `ad_last_error_details()`,\n  returns a borrowed JSON string carrying structured details (e.g. the\n  actionability check report on `ACTION_FAILED`, candidate summaries on\n  `AMBIGUOUS_TARGET`). The details may contain element names, values, and window\n  titles from the user's screen — treat as sensitive diagnostics and avoid routing\n  to shared log surfaces.\n\n- **Handle release.** Every `ad_resolve_element_exact` / `ad_find_exact` result must be\n  released with `ad_free_handle(adapter, &handle)` on the same adapter that\n  produced it, before that adapter is destroyed. On macOS this balances the\n  internal `CFRetain`; on Windows/Linux the call is a no-op but safe to issue.\n  `ad_free_handle` zeroes `handle.ptr` so a follow-up call is a safe no-op.\n\n- **Primary ref-action path.** `ad_execute_by_ref` is the recommended entrypoint\n  for the observe-act loop: it loads the RefStore, looks up the ref in the refmap\n  (STALE_REF on miss), runs strict element re-identification (STALE_REF / AMBIGUOUS_TARGET),\n  runs the live actionability preflight, then dispatches. TypeText and PressKey\n  default to `focus_fallback` policy (matching CLI `type`/`press-key`); all other\n  actions default to `headless`. Pass `AD_POLICY_KIND_HEADED` (2) to opt in to\n  cursor-based fallbacks.\n\n- **Generation-safe direct APIs.** Legacy `AdRefEntry` and\n  `AdWindowInfo` layouts remain available for binary compatibility, but they do\n  not carry process-generation evidence and direct targeting functions fail\n  closed. Use `AdExactRefEntry`, `AdExactWindowInfo`,\n  `ad_list_windows_exact`, and the `*_exact` targeting symbols. Likewise,\n  `ad_list_surfaces_exact` preserves `SurfaceInfo.id`; the legacy surface list\n  is an observation-only projection that omits it.\n\n- **Display discovery.** Call `ad_list_displays` before using\n  `AdScreenshotTarget.screen_index`. List order is the screenshot index order;\n  inspect each `AdDisplayInfo` for its stable display ID, bounds, primary flag,\n  and scale, then release the handle with `ad_display_list_free`.\n\n- **Low-level action paths.** `ad_execute_action` (headless, no preflight) and\n  `ad_execute_action_with_policy` are raw escape hatches for callers holding a live\n  `AdNativeHandle` from `ad_resolve_element_exact` / `ad_find_exact`. Use them when\n  you need to bypass the ref-action pipeline.\n\n- **Ref-action preflight.** `ad_execute_by_ref` and\n  `ad_execute_ref_action_exact_with_policy` both resolve the element strictly and run the live actionability preflight\n  (visible, stable, enabled, supported action, policy, editable) before dispatching\n  — a disabled or unsupported target fails before any platform call. On\n  `AD_RESULT_ERR_ACTION_FAILED`, the structured check report is available as JSON\n  via `ad_last_error_details()`.\n\n- **Action result steps.** `AdActionResult.steps` mirrors the CLI `steps` array\n  for activation-chain actions. Each entry has `label` and `outcome` strings and\n  is owned by the result; release with `ad_free_action_result(&out)`.\n\n- **Tracing / log callback.** Two tracing surfaces coexist:\n\n  1. **Structured file trace** — same JSONL contract as CLI `--trace`, gated by a\n     `trace: on` session manifest. Segments include `event`, `ts_ms`, `seq`, and\n     redacted fields. Requires `session start` (or equivalent manifest on disk)\n     before creating the adapter; plain session-id adapters write nothing to disk.\n\n  2. **`ad_set_log_callback(cb)`** — installs a `tracing` subscriber layer that\n     delivers events as JSON to your callback. `cb` receives an int32_t level\n     (1=ERROR … 5=TRACE) and a `const char *msg` valid only for the duration of\n     the call. Pass `NULL` to unregister. The layer is installed on the first\n     non-null call; if a foreign global subscriber already owns the process at\n     that point, the install fails with `AD_RESULT_ERR_INTERNAL` and no events are\n     ever delivered. Sensitive field values (password, token, text, …) are\n     replaced with `{\"redacted\":true}` before formatting. A panicking callback is\n     caught and silently discarded. The callback may fire from threads other than\n     the registering thread, and may still fire briefly after a `NULL` unregister\n     — keep the callback and any data it captures valid for the process lifetime.\n\n- **Wait.** `ad_wait(adapter, args, &out)` runs the full CLI `wait` command\n  (element-appear, window-appear, text-appear, menu-open/close, notification,\n  element predicates). Zero-initialize `AdWaitArgs`, set the fields you need, and\n  validate the struct size against `AD_WAIT_ARGS_SIZE` / `ad_wait_args_size()` before\n  calling. The output is a `{version, ok, command, data}` JSON envelope freed with\n  `ad_free_string`. `ad_wait` blocks the calling thread up to `timeout_ms` ms —\n  ensure the adapter is not destroyed from another thread while it is running.\n\n- **Text input privacy.** On macOS, focus-fallback or headed text insertion may\n  briefly use the clipboard for non-ASCII text. For sensitive text, prefer\n  `AD_ACTION_KIND_SET_VALUE` with `AD_POLICY_KIND_HEADLESS` when the target\n  supports settable values.\n\n- **Enum discriminants.** Every `#[repr(i32)]` enum field is validated at the C\n  boundary — invalid discriminants return `AD_RESULT_ERR_INVALID_ARGS` instead of\n  undefined behavior.\n\n- **ABI stability.** The major version in `AD_ABI_VERSION_MAJOR` increments on any\n  breaking change (removed symbol, incompatible layout). Additive changes (new\n  symbols, new error codes) do not bump it. Before 1.0, pin the exact version of\n  libagent_desktop_ffi you link against.\n\n- **`ad_get_tree_exact` vs `ad_snapshot`.** `ad_get_tree_exact` returns a raw flat BFS tree\n  without `@e` refs, no refmap persistence, and no JSON envelope — use it for\n  custom traversal or UI inspection. For observe-act agents that drive actions via\n  `ad_execute_by_ref`, always start with `ad_snapshot`.\n\nFile v1.0.7:_meta.json\n\n{\n  \"ownerId\": \"kn7a8wtv8q9jjhpxh4w601zsb5827bnx\",\n  \"slug\": \"agent-desktop-ffi\",\n  \"version\": \"1.0.7\",\n  \"publishedAt\": 1787877342860\n}\n\nFile v1.0.7:references/build-and-link.md\n\n# Build and link\n\n## Building the cdylib\n\n```sh\ncargo build --profile release-ffi -p agent-desktop-ffi\n```\n\nOutput:\n\n- macOS: `target/release-ffi/libagent_desktop_ffi.dylib`\n- Linux: `target/release-ffi/libagent_desktop_ffi.so`\n- Windows: `target/release-ffi/agent_desktop_ffi.dll`\n\nThe generated header is at `crates/ffi/include/agent_desktop.h`. CI\nvalidates that the committed header matches what `cargo build`\nregenerates — if you change a type in `crates/ffi/src/`, rebuild\nlocally and commit the updated header.\n\n`--profile release-ffi` keeps `panic = \"unwind\"`, which is required for\nthe `catch_unwind` traps inside every `extern \"C\"` entrypoint. The default\n`release` profile uses `panic = \"abort\"` (for CLI binary-size reasons) and\nsilently defeats those traps.\n\n## Prebuilt archives\n\nEvery GitHub release ships prebuilt archives for:\n\n- macOS arm64 and x86_64\n- Linux x64 and arm64 (glibc)\n- Windows x64 MSVC\n\nEach archive contains the dylib/so/dll, `include/agent_desktop.h`, and\n`LICENSE`. Integrity: compare against `checksums.txt` in the release\nassets. Supply-chain verification: each release is signed via Sigstore\nattestation — verify with `cosign verify-blob` before deploying.\n\n## ABI handshake (do this first)\n\nAfter `dlopen` / `LoadLibrary`, compare the dylib major to the header you\ncompiled against before calling anything else:\n\n```c\nAdResult rc = ad_init(AD_ABI_VERSION_MAJOR);\nif (rc != AD_RESULT_OK) {\n    // header and dylib have incompatible major versions\n    fprintf(stderr, \"ABI mismatch: %s\\n\", ad_last_error_message());\n    return -1;\n}\n```\n\nAlternatively, read the raw dylib major and compare yourself:\n\n```c\nuint32_t dylib_major = ad_abi_version();\nif (dylib_major != AD_ABI_VERSION_MAJOR) {\n    fprintf(stderr, \"ABI major: header=%u dylib=%u\\n\",\n            AD_ABI_VERSION_MAJOR, dylib_major);\n    abort();\n}\n```\n\n`ad_init` returns `AD_RESULT_ERR_INVALID_ARGS` on mismatch (with a\ndiagnostic in `ad_last_error_message`). A mismatch means the header you\ncompiled against and the loaded dylib are incompatible — do not call\nanything further.\n\n## Struct size validation\n\nLanguages whose struct layout may diverge from C (Python ctypes, Go cgo,\nJNI, etc.) must validate every size-pinned struct before passing it to\nthe library. The FFI exposes three validation layers:\n\n1. **Header macros**: `AD_ACTION_SIZE`, `AD_WAIT_ARGS_SIZE`,\n   `AD_REF_ENTRY_SIZE`, `AD_DRAG_PARAMS_SIZE`, `AD_ACTION_RESULT_SIZE`,\n   `AD_ACTION_STEP_SIZE`, `AD_ELEMENT_STATE_SIZE`.\n2. **Runtime getters**: `ad_action_size()`, `ad_wait_args_size()`,\n   `ad_ref_entry_size()`, `ad_drag_params_size()`, `ad_action_result_size()`,\n   `ad_action_step_size()`, `ad_element_state_size()` — each returns the\n   same value the macro encodes, compiled from the Rust side.\n3. **C11 static asserts** in the header (`#ifndef AGENT_DESKTOP_ABI_ASSERTS`)\n   catch mismatches at C compile time.\n\nCompare your binding's `sizeof` equivalent against the getter at load\ntime, before building or passing any of these structs. The Python smoke\nharness (`tests/ffi-python/smoke.py` Leg 2) demonstrates this check for\nall size-pinned structs in a single loop.\n\n## Worked example: Python ctypes smoke harness\n\n`tests/ffi-python/smoke.py` is the canonical reference for Python\nconsumers. It covers:\n\n- Leg 1: `ad_abi_version()` vs `AD_ABI_VERSION_MAJOR` (header/dylib match)\n- Leg 2: every `ad_*_size()` getter vs its `AD_*_SIZE` macro (struct layout)\n- Leg 3: `ad_version()` → parse JSON → `ad_free_string` (basic pipeline)\n- Leg 4: `ad_adapter_create` → `ad_snapshot` → `ad_free_string` →\n  `ad_adapter_destroy` (full adapter lifecycle, stub passes through\n  `PLATFORM_NOT_SUPPORTED`)\n\nRun it:\n\n```bash\npython3 tests/ffi-python/smoke.py \\\n  target/release-ffi/libagent_desktop_ffi.dylib \\\n  crates/ffi/include/agent_desktop.h\n```\n\nOr with environment variables:\n\n```bash\nAD_DYLIB_PATH=target/release-ffi/libagent_desktop_ffi.dylib \\\nAD_HEADER_PATH=crates/ffi/include/agent_desktop.h \\\npython3 tests/ffi-python/smoke.py\n```\n\nNo `pip install` required — `smoke.py` uses the Python standard library only.\n\n## Minimal C example\n\n```c\n#include <stdio.h>\n#include \"agent_desktop.h\"\n\nint main(void) {\n    // 1. ABI handshake\n    if (ad_init(AD_ABI_VERSION_MAJOR) != AD_RESULT_OK) {\n        fprintf(stderr, \"ABI mismatch: %s\\n\", ad_last_error_message());\n        return 1;\n    }\n\n    // 2. Create adapter\n    AdAdapter *adapter = ad_adapter_create();\n    if (!adapter) {\n        fprintf(stderr, \"adapter_create failed: %s\\n\", ad_last_error_message());\n        return 1;\n    }\n\n    // 3. Check permissions\n    AdResult rc = ad_check_permissions(adapter);\n    if (rc != AD_RESULT_OK) {\n        fprintf(stderr, \"permission denied: %s\\n\", ad_last_error_message());\n        ad_adapter_destroy(adapter);\n        return 1;\n    }\n\n    // 4. Snapshot the focused window\n    char *json_out = NULL;\n    rc = ad_snapshot(adapter,\n                     NULL,   // app (null = focused window)\n                     0,      // surface = Window\n                     10,     // max_depth\n                     false,  // interactive_only\n                     false,  // compact\n                     &json_out);\n    if (rc == AD_RESULT_OK && json_out) {\n        printf(\"%s\\n\", json_out);\n        // parse JSON to find @e refs in data.tree\n        ad_free_string(json_out);\n    } else if (json_out) {\n        // command-level error — ok:false envelope, still must free\n        fprintf(stderr, \"snapshot error: %s\\n\", json_out);\n        ad_free_string(json_out);\n    } else {\n        // infrastructure error — *out is null\n        fprintf(stderr, \"snapshot failed: %s\\n\", ad_last_error_message());\n    }\n\n    ad_adapter_destroy(adapter);\n    return 0;\n}\n```\n\nCompile:\n\n```sh\nclang -I./crates/ffi/include main.c \\\n      -L./target/release-ffi -lagent_desktop_ffi \\\n      -o snapshot_demo\ninstall_name_tool -change \\\n    libagent_desktop_ffi.dylib \\\n    @executable_path/target/release-ffi/libagent_desktop_ffi.dylib \\\n    snapshot_demo\n```\n\n## Observe-act workflow in C\n\nAfter parsing the snapshot JSON and extracting a qualified ref ID:\n\n```c\nAdAction act = {0};              // zero-init before setting any field\nact.kind = AD_ACTION_KIND_CLICK;\n\nchar *result = NULL;\nAdResult rc = ad_execute_by_ref(\n    adapter,\n    \"@s8f3k2p9:e5\", // qualified ref from snapshot data.tree\n    NULL,            // the qualified ref embeds its snapshot ID\n    &act,\n    0,            // policy = Headless\n    &result\n);\nif (result) {\n    // parse JSON — ok:true on success, ok:false on STALE_REF etc.\n    printf(\"%s\\n\", result);\n    ad_free_string(result);\n}\nif (rc != AD_RESULT_OK) {\n    const char *det = ad_last_error_details();  // may be NULL; treat as sensitive\n    fprintf(stderr, \"execute_by_ref failed (%d): %s\\n\",\n            (int)rc, ad_last_error_message());\n}\n```\n\nTo type text, set the action kind and text field:\n\n```c\nAdAction type_act = {0};\ntype_act.kind = AD_ACTION_KIND_TYPE_TEXT;\ntype_act.text = \"hello\";\n// TypeText defaults to focus_fallback via ad_execute_by_ref; explicit policy:\nrc = ad_execute_by_ref(adapter, \"@s8f3k2p9:e3\", NULL, &type_act,\n                       AD_POLICY_KIND_FOCUS_FALLBACK, &result);\n```\n\n## Threading reminder\n\nAdapter entrypoints may be called from any host thread. Keep native handles on\nthe thread that resolved them and use snapshot-qualified refs across async or\nworker boundaries. Mutations are serialized by a canonical cross-process\ninteraction lease. See [threading.md](threading.md).\n\n## Minimal Python ctypes example\n\n```python\nimport ctypes, json\nfrom ctypes import c_int, c_int32, c_uint8, c_bool, c_char_p, POINTER, c_void_p\n\nlib = ctypes.CDLL(\"./target/release-ffi/libagent_desktop_ffi.dylib\")\n\n# ABI handshake\nlib.ad_init.restype = c_int32\nlib.ad_init.argtypes = [ctypes.c_uint32]\nAD_ABI_VERSION_MAJOR = 4  # sync with header macro\nrc = lib.ad_init(AD_ABI_VERSION_MAJOR)\nassert rc == 0, f\"ABI mismatch: rc={rc}\"\n\n# Adapter lifecycle\nlib.ad_adapter_create.restype = c_void_p\nlib.ad_adapter_create.argtypes = []\nlib.ad_adapter_destroy.restype = None\nlib.ad_adapter_destroy.argtypes = [c_void_p]\n\n# Snapshot\nlib.ad_snapshot.restype = c_int32\nlib.ad_snapshot.argtypes = [c_void_p, c_char_p, c_int32, c_uint8, c_bool, c_bool,\n                             POINTER(c_char_p)]\nlib.ad_free_string.restype = None\nlib.ad_free_string.argtypes = [c_char_p]\n\nlib.ad_last_error_message.restype = c_char_p\nlib.ad_last_error_message.argtypes = []\n\nadapter = lib.ad_adapter_create()\nassert adapter, \"ad_adapter_create() returned null\"\n\nout = c_char_p()\nrc = lib.ad_snapshot(adapter, None, 0, 10, False, False, ctypes.byref(out))\nif out.value:\n    envelope = json.loads(out.value)\n    lib.ad_free_string(out)\n    print(\"ok:\", envelope.get(\"ok\"))\nelse:\n    msg = lib.ad_last_error_message()\n    print(\"error:\", msg.decode() if msg else \"(no message)\")\n\nlib.ad_adapter_destroy(adapter)\n```\n\nFile v1.0.7:references/error-handling.md\n\n# Error handling\n\nThe FFI uses an errno-style last-error pattern. Every `AdResult`-returning\nfunction returns `AD_RESULT_OK` (= 0) on success or a negative error\ncode on failure. When a failure occurs, thread-local last-error state is\npopulated; read it with the `ad_last_error_*` accessors.\n\n## Minimal pattern\n\n```c\nAdResult rc = ad_launch_app(adapter, \"com.apple.finder\", 5000, &win);\nif (rc != AD_RESULT_OK) {\n    const char *msg = ad_last_error_message();\n    const char *sug = ad_last_error_suggestion();   // may be NULL\n    fprintf(stderr, \"launch_app failed (%d): %s\\n\", (int)rc, msg ? msg : \"(no message)\");\n    if (sug) fprintf(stderr, \"  suggestion: %s\\n\", sug);\n    // no need to release the struct — out-param was zero-initialized\n    return -1;\n}\n// ...use win...\nad_release_window_fields(&win);\n```\n\n## Last-error accessors\n\nFour accessors share the same per-thread lifetime contract:\n\n| Accessor                        | Returns                                                        |\n|---------------------------------|----------------------------------------------------------------|\n| `ad_last_error_code()`          | The `AdResult` code of the last failure, or `AD_RESULT_OK`    |\n| `ad_last_error_message()`       | Human-readable description, or null                            |\n| `ad_last_error_suggestion()`    | Recovery hint, or null                                         |\n| `ad_last_error_platform_detail()` | OS-specific diagnostic (AX codes, HRESULTs, AT-SPI), or null |\n| `ad_last_error_details()`       | Structured JSON details, or null — **sensitive** (see below)   |\n\n`ad_last_error_details()` returns a JSON string with structured context:\nthe actionability check report on `ACTION_FAILED`, candidate element\nsummaries on `AMBIGUOUS_TARGET`, the last observed state on a `wait`\n`TIMEOUT`, etc. The details may contain element names, values, and window\ntitles from the user's screen. Treat as sensitive diagnostics and avoid\nrouting to shared log surfaces.\n\n## Lifetime contract\n\nThe pointer returned by any `ad_last_error_*` accessor remains valid\nacross any number of subsequent **successful** FFI calls. Only the next\n**failing** call rotates the slot.\n\nConsequence: you can cache the pointer right after a failure and keep\nreading it until the next failure — equivalent to POSIX `errno` /\n`strerror`.\n\n```c\nAdResult rc = ad_some_call(...);\nconst char *msg = ad_last_error_message();   // snapshot\n\nad_check_permissions(adapter);                // success\nad_check_permissions(adapter);                // success\nprintf(\"%s\\n\", msg);                          // still valid\n```\n\nFailure-path calls rotate: if a subsequent call fails, the prior\npointer may dangle. Read it before the next potentially-failing call.\n\nLast-error is per-thread (thread-local storage) — Thread A's failure\ndoes not affect Thread B's slot.\n\n`ad_check_permissions` does not treat `Unknown` as success. Stub adapters\nthat cannot answer permission probes return\n`AD_RESULT_ERR_PLATFORM_NOT_SUPPORTED`. The macOS adapter reports\n`AD_RESULT_ERR_INTERNAL` only if the platform probe itself is ambiguous;\nread `ad_last_error_*` for the diagnostic.\n\n## Error codes\n\nNumeric values are ABI-stable. New codes are appended; existing values\nare not renumbered. Always handle values outside this list — future\nreleases may add codes.\n\n| Name                                  | i32   | Meaning                                    |\n|---------------------------------------|-------|--------------------------------------------|\n| `AD_RESULT_OK`                        |   0   | Success                                    |\n| `AD_RESULT_ERR_PERM_DENIED`           |  -1   | Accessibility / input permission missing   |\n| `AD_RESULT_ERR_ELEMENT_NOT_FOUND`     |  -2   | Ref resolve / find miss                    |\n| `AD_RESULT_ERR_APP_NOT_FOUND`         |  -3   | Bundle/PID lookup miss                     |\n| `AD_RESULT_ERR_ACTION_FAILED`         |  -4   | Action dispatched but rejected             |\n| `AD_RESULT_ERR_ACTION_NOT_SUPPORTED`  |  -5   | Platform cannot perform this action        |\n| `AD_RESULT_ERR_STALE_REF`             |  -6   | Ref predates a UI change; re-snapshot      |\n| `AD_RESULT_ERR_WINDOW_NOT_FOUND`      |  -7   | Window filter matched nothing              |\n| `AD_RESULT_ERR_PLATFORM_NOT_SUPPORTED`|  -8   | API unavailable on this OS                 |\n| `AD_RESULT_ERR_TIMEOUT`               |  -9   | Wait exceeded deadline                     |\n| `AD_RESULT_ERR_INVALID_ARGS`          | -10   | Null pointer, bad enum, invalid UTF-8      |\n| `AD_RESULT_ERR_NOTIFICATION_NOT_FOUND`| -11   | Notification index out of range or reordered |\n| `AD_RESULT_ERR_INTERNAL`              | -12   | Internal failure or foreign-subscriber conflict |\n| `AD_RESULT_ERR_SNAPSHOT_NOT_FOUND`    | -13   | Requested snapshot ref store is missing    |\n| `AD_RESULT_ERR_POLICY_DENIED`         | -14   | Current action policy blocks this fallback |\n| `AD_RESULT_ERR_AMBIGUOUS_TARGET`      | -15   | Strict re-identification found multiple candidates; re-snapshot |\n| `AD_RESULT_ERR_APP_UNRESPONSIVE`      | -16   | Read-only liveness probe failed after an uncertain mutation |\n\n## Ref token validation\n\nRef-taking entrypoints accept two canonical forms:\n\n- `@<snapshot_id>:e<N>` is a qualified ref. `snapshot_id` may be null. If a\n  separate snapshot ID is supplied, it must match the embedded ID.\n- `@e<N>` is a legacy bare ref. It requires a non-null snapshot ID argument.\n\n`N` is a positive `u32` written without a sign and with at most 10 decimal\ndigits. Snapshot IDs are 3–64 ASCII alphanumeric, `-`, or `_` characters.\nMalformed tokens, invalid UTF-8, a bare ref without a snapshot ID, or mismatched\nembedded and explicit snapshot IDs return `AD_RESULT_ERR_INVALID_ARGS` before\ndispatch. A well-formed token whose saved snapshot is absent returns\n`AD_RESULT_ERR_SNAPSHOT_NOT_FOUND`; a saved snapshot with no matching local ref\nreturns `AD_RESULT_ERR_STALE_REF`.\n\nSnapshot lookup is confined to the adapter's namespace. An adapter created with\n`ad_adapter_create_with_session` never searches the global namespace or another\nsession for a matching snapshot ID.\n\n## Off-main-thread migration\n\nABI v3 removed the blanket macOS main-thread rejection. Adapter entrypoints may\nnow run on any host thread, so callers must stop treating\n`AD_RESULT_ERR_INTERNAL` as an expected off-main-thread result. Native handles\nremain thread-affine capabilities: cross-thread, cross-adapter, released, or\nrevoked handle use returns `AD_RESULT_ERR_INVALID_ARGS`. See\n[threading.md](threading.md) for the concurrency and ownership contract.\n\n## Enum validation\n\nEvery `#[repr(i32)]` enum field is validated at the C boundary. An\nout-of-range discriminant returns `AD_RESULT_ERR_INVALID_ARGS` with\ndiagnostic last-error text. This prevents the consumer from accidentally\ntriggering undefined behavior by stuffing an arbitrary `int32_t` into an\nenum slot. Affected fields: `AdAction.kind` (`AdActionKind`),\n`AdMouseEvent.kind` (`AdMouseEventKind`), `AdMouseEvent.button`\n(`AdMouseButton`), `AdScrollParams.direction` (`AdDirection`),\n`AdTreeOptions.surface` (`AdSnapshotSurface`), `AdScreenshotTarget.kind`\n(`AdScreenshotKind`), `AdWindowOp.kind` (`AdWindowOpKind`), and the\n`policy` parameter of `ad_execute_by_ref` / `ad_execute_action_with_policy`\n/ `ad_execute_ref_action_exact_with_policy` (`AdPolicyKind`).\n\n## Command-backed JSON entrypoints: dual-failure modes\n\n`ad_snapshot`, `ad_execute_by_ref`, `ad_wait`, `ad_status`, and\n`ad_version` have two distinct failure modes:\n\n- **Argument / infrastructure failure** (null adapter, invalid UTF-8,\n  bad discriminant, context error): `*out` is set to null,\n  no allocation is made, and the last-error slot is the only failure\n  indication.\n- **Command-level failure** (app not found, STALE_REF, TIMEOUT, etc.):\n  `*out` is set to a heap-allocated JSON string with `\"ok\":false` and an\n  `\"error\"` payload. The caller **must still free** it with\n  `ad_free_string(*out)`. The last-error slot is also set.\n\nAlways check `*out` for null before deciding whether to free.\n\n## Panic safety\n\nEvery `extern \"C\"` entrypoint wraps its body in `catch_unwind`. A\nRust panic inside the FFI surfaces as `AD_RESULT_ERR_INTERNAL` with\nmessage `\"rust panic in FFI boundary\"`. No `SIGABRT`, no host crash.\n\nThe cdylib must be built under the `release-ffi` profile for this\nguarantee to hold in optimized builds — the workspace `release` profile\nuses `panic = \"abort\"` (for CLI binary-size reasons).\n\nFile v1.0.7:references/ownership.md\n\n# Pointer ownership\n\nEvery `*mut T` / `*const T` returned by the FFI comes with a matching\nfree function. Always call it; the allocator the FFI uses is Rust's\n`Box::from_raw` / `CString::from_raw`, which cannot be freed with C's\n`free()`.\n\n## Allocation / release table\n\n### Command-backed JSON strings\n\nThese entrypoints write an owned, NUL-terminated JSON envelope into\n`*out`; free with `ad_free_string`. See error-handling.md for the\ndual-failure mode (command-level errors write `ok:false` JSON into\n`*out`; infrastructure errors leave `*out` null with no allocation).\n\n| Allocates                                                                         | Frees with              |\n|-----------------------------------------------------------------------------------|-------------------------|\n| `ad_version(&out)`                                                                | `ad_free_string(out)`   |\n| `ad_status(adapter, &out)`                                                        | `ad_free_string(out)`   |\n| `ad_snapshot(adapter, app, surface, max_depth, interactive_only, compact, &out)` | `ad_free_string(out)`   |\n| `ad_execute_by_ref(adapter, ref_id, snapshot_id, action, policy, &out)`          | `ad_free_string(out)`   |\n| `ad_wait(adapter, args, &out)`                                                    | `ad_free_string(out)`   |\n\n### Adapter lifecycle\n\n| Allocates                                            | Frees with                              |\n|------------------------------------------------------|-----------------------------------------|\n| `ad_adapter_create()`                                | `ad_adapter_destroy(adapter)`           |\n| `ad_adapter_create_with_session(session)`            | `ad_adapter_destroy(adapter)`           |\n\n### Opaque list handles\n\n| Allocates                                                    | Frees with                              |\n|--------------------------------------------------------------|-----------------------------------------|\n| `ad_list_apps(adapter, &list)`                               | `ad_app_list_free(list)`                |\n| `ad_list_displays(adapter, &list)`                           | `ad_display_list_free(list)`            |\n| `ad_list_windows(adapter, app, focused, &list)`              | `ad_window_list_free(list)`             |\n| `ad_list_windows_exact(adapter, app, focused, &list)`        | `ad_exact_window_list_free(list)`       |\n| `ad_list_surfaces(adapter, pid, &list)`                      | `ad_surface_list_free(list)`            |\n| `ad_list_surfaces_exact(adapter, pid, &list)`                | `ad_exact_surface_list_free(list)`      |\n| `ad_list_notifications(adapter, filter, &list)`              | `ad_notification_list_free(list)`       |\n| `ad_dismiss_all_notifications(adapter, f, &ok, &fail)`       | `ad_notification_list_free` on each, or `ad_dismiss_all_notifications_free(ok, fail)` |\n\n### App / window lifecycle\n\n| Allocates                                                 | Frees with                                                          |\n|-----------------------------------------------------------|---------------------------------------------------------------------|\n| `ad_launch_app(adapter, id, timeout, &out)`               | `ad_release_window_fields(&out)` — frees interior strings only; the `AdWindowInfo` struct lives on the caller's stack |\n| `ad_launch_app_exact(adapter, id, timeout, &out)`         | `ad_release_exact_window_fields(&out)` |\n\n### Raw tree and element access\n\n| Allocates                                                                              | Frees with                              |\n|----------------------------------------------------------------------------------------|-----------------------------------------|\n| `ad_get_tree_exact(adapter, win, opts, &out)`                                          | `ad_free_tree(&out)`                    |\n| `ad_resolve_element_exact(adapter, entry, &handle)`                                    | `ad_free_handle(adapter, &handle)` — zeroes `handle.ptr` so a follow-up call is a no-op |\n| `ad_find_exact(adapter, win, query, &handle)`                                          | same as `ad_resolve_element_exact`      |\n\n### Action results\n\n| Allocates                                                                              | Frees with                   |\n|----------------------------------------------------------------------------------------|------------------------------|\n| `ad_execute_action(adapter, handle, action, &out)`                                     | `ad_free_action_result(&out)` |\n| `ad_execute_action_with_policy(adapter, handle, action, policy, &out)`                 | `ad_free_action_result(&out)` |\n| `ad_execute_ref_action_exact_with_policy(adapter, entry, action, policy, &out)`        | `ad_free_action_result(&out)` |\n| `ad_notification_action(adapter, &request, &out)` — set `request.identity` from `ad_list_notifications` and choose an explicit non-headless policy; reorder mismatches fail closed | `ad_free_action_result(&out)` |\n\n### Clipboard and image buffers\n\n| Allocates                                   | Frees with                                                          |\n|---------------------------------------------|---------------------------------------------------------------------|\n| `ad_get_clipboard(adapter, &text)`          | `ad_free_string(text)`                                              |\n| `ad_get(adapter, handle, property, &text)`  | `ad_free_string(text)` — text may be null when the property is absent; `ad_free_string(NULL)` is a no-op |\n| `ad_screenshot(adapter, target, &buf)`      | `ad_image_buffer_free(buf)` (buf is opaque; read via `ad_image_buffer_{data,size,width,height,format}`) |\n\n## Rules\n\n- Every free function is **null-tolerant**. `ad_free_tree(NULL)`,\n  `ad_free_handle(adapter, NULL)`, `ad_free_string(NULL)`, etc. are\n  no-ops. List accessors (`ad_*_list_count`, `_get`) also accept null\n  and return `0` / `NULL` respectively.\n- **Double-free of list handles and `AdImageBuffer` is undefined.** The\n  opaque wrappers are allocated by `Box::into_raw`; the second call\n  would invoke `Box::from_raw` on a freed allocation. Always set the\n  pointer to `NULL` after freeing.\n- **`ad_free_handle` is safe to double-call** — it zeroes `handle.ptr`\n  after the platform release, so a follow-up call sees `NULL` and\n  returns `AD_RESULT_OK` without re-entering `CFRelease`.\n- **Adapters must outlive their handles.** Free every handle with the\n  same adapter and on the same thread that produced it before calling\n  `ad_adapter_destroy`. Destroyed, wrong-adapter, cross-thread, forged, and\n  already-freed tokens are rejected without dereferencing foreign memory.\n- Pointers inside a struct (`.id`, `.title`, `.app_name`, each\n  `AdNotificationInfo.body`, etc.) are freed by the struct's owning\n  free function (list_free / release_fields) — do not call\n  `ad_free_string()` on them individually.\n- `AdActionResult` owns `action`, `ref_id`, `post_state`,\n  `post_state.states`, `steps`, and each `steps[i].label` /\n  `steps[i].outcome`; free all of them only through\n  `ad_free_action_result(&out)`. Treat the returned counts as\n  read-only metadata; release the unmodified result struct.\n- Ownership does **not** transfer back to Rust after you free. Keep a\n  local `NULL` to prevent accidental reuse.\n\n## Out-param zeroing\n\nEvery fallible FFI function zeroes its out-param **before** any guard\n(pointer validation, UTF-8 validation, enum validation). On error,\ncalling the paired free function is safe: all pointers inside are\nguaranteed null, all counts zero, so the free is a no-op rather than\na double-free on a previous caller's allocation.\n\nIn particular:\n\n- `ad_get_clipboard` writes `*out = NULL` before the adapter call —\n  no stale buffer visible on error.\n- `ad_launch_app` writes `*out = zeroed AdWindowInfo` before the\n  platform call — `ad_release_window_fields(&out)` on the zero-init\n  struct is a no-op.\n- `ad_screenshot` writes `*out = NULL` before allocating the image\n  buffer — no stale pointer when the screenshot fails.\n- `ad_snapshot`, `ad_execute_by_ref`, `ad_wait`, `ad_status`,\n  `ad_version` write `*out = NULL` before any guard, so on\n  infrastructure failures no allocation is made and `ad_free_string(NULL)`\n  is a safe no-op.\n- `ad_*_list` and `ad_resolve_element_exact` / `ad_find_exact` all apply the same\n  pattern to their handle / list out-params.\n\nFile v1.0.7:references/threading.md\n\n# Threading\n\n## Host-thread contract\n\nFFI entrypoints may be called from any host thread. The library does not apply\na blanket macOS main-thread guard to Accessibility (`AXUIElement`) or Quartz\nevent (`CGEvent`) operations.\n\nThat contract follows Apple's published boundaries:\n\n- Apple's [Thread Safety Summary](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html)\n  says objects restricted to the main thread are called out explicitly and\n  describes Core Foundation as thread-safe for common immutable query, retain,\n  release, and transfer operations.\n- Apple documents [`AXUIElement`](https://developer.apple.com/documentation/applicationservices/axuielement)\n  as an accessibility object and its header as a CF type; it does not publish a\n  blanket main-thread requirement for AX calls.\n- Apple documents [`CGEvent`](https://developer.apple.com/documentation/coregraphics/cgevent)\n  as a CF-derived low-level event type; it likewise does not publish a blanket\n  main-thread restriction.\n\nAppKit view/event-loop rules do not automatically apply to an assistive\napplication calling AX or Quartz APIs. Apple explicitly says\n[`NSWorkspace.shared`](https://developer.apple.com/documentation/appkit/nsworkspace/shared)\nis safe to access from any thread, and does not publish a main-thread-only rule\nfor `NSPasteboard`.\n\nCode paths that use Cocoa objects create their required autorelease pools\ninternally. Apple's Thread Safety Summary requires a pool on secondary threads\nthat use Cocoa and identifies classes such as `NSView` as genuinely\nmain-thread-only. Because Rust and many foreign runtimes create POSIX threads,\nFFI initialization also follows Apple's\n[Using POSIX Threads in a Cocoa Application](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html)\nguidance by starting one `NSThread` once, which causes Cocoa to install its\nmultithreading locks before worker-thread AppKit calls.\n\n## Concurrency and mutation ordering\n\nAdapter registry handles can be acquired concurrently. Read operations carry a\nfinite deadline and do not take the interaction lease.\n\n| Concurrent calls | Contract |\n|------------------|----------|\n| read + read | Both may run concurrently; neither waits for the interaction lease |\n| read + mutation | The read does not wait for the lease and may overlap the mutation; its result is a point-in-time observation, not a transaction |\n| mutation + mutation | One lease holder runs at a time for the same OS user; the waiter consumes its command deadline and returns a structured timeout if it cannot acquire the lease |\n| native-handle call on another thread | Rejected with `AD_RESULT_ERR_INVALID_ARGS`; use a qualified ref instead |\n\nSnapshots, finds, gets, display/window/app listings, screenshots, clipboard\nreads, permission reads, status, and trace reads are observations. Ref actions,\ndirect native-handle actions, input synthesis, clipboard writes/clear, app and\nwindow changes, notification actions, and permission requests are mutations.\nFor example, `ad_snapshot` and `ad_execute_by_ref` may be called from different\nthreads, but the snapshot can overlap the action; callers that require\nobserve-then-act ordering must wait for the snapshot result before dispatching\nthe action.\n\nDesktop mutations take a process-independent advisory lock at\n`/tmp/agent-desktop-<uid>/interaction.lock`. Current CLI and FFI builds therefore\nserialize mutations across threads, processes, and different HOME values for\nthe same user. The lock is command-scoped, not transaction-scoped: another\nactor may change the UI between an observation and a later action, or between\ntwo independently invoked actions.\n\nThis ordering is implemented by agent-desktop's in-process process guard plus a\nUnix advisory file lock, not by `AXObserver`, an AppKit run loop, or a global\nApple accessibility mutex. On non-Unix platforms, each adapter must provide the\nsame `PlatformAdapter::acquire_interaction_lease` contract with its native\nserialization primitive.\n\nThe lease cannot coordinate:\n\n- older `agent-desktop` binaries that predate the lock;\n- direct human input or unrelated automation tools;\n- state changes initiated by the target application itself.\n\nCallers must still use strict refs/exact window identities and treat stale or\nambiguous targets as normal retryable automation outcomes.\n\n## Adapter destruction\n\nAdapter pointers are opaque registry tokens. Each call acquires a retained\nadapter owner before platform work. `ad_adapter_destroy` revokes the token:\ncalls that already acquired it finish safely, while calls that begin afterward\nreturn `AD_RESULT_ERR_INVALID_ARGS`. Concurrent destruction cannot free memory\nstill referenced by an in-flight call.\n\nDestroying an adapter also revokes native handles created by that adapter on the\ncalling thread. Other threads' handle registries reject subsequent use because\nthe owner adapter token no longer exists.\n\n## Native-handle thread ownership\n\n`ad_resolve_element_exact` and `ad_find_exact` return an opaque\n`AdNativeHandle`. Native handles are adapter-bound and thread-affine:\n\n- resolve, use, and release a handle on the same thread;\n- pass the same adapter that produced the handle;\n- do not use a handle after destroying its adapter.\n\nViolations are rejected with `AD_RESULT_ERR_INVALID_ARGS`; the library does not\ndereference forged, cross-adapter, cross-thread, released, or revoked tokens.\nPrefer snapshot-qualified refs and `ad_execute_by_ref` when a handle would need\nto cross an async task or thread boundary.\n\n## Log callback threading\n\n`ad_set_log_callback(cb)` may be called from any thread. The callback may be\ninvoked from any thread that calls an `ad_*` function.\n\nA callback unregistered via `NULL` may still receive an invocation already in\nflight on another thread. Keep the callback and captured state alive for the\nprocess lifetime, or quiesce active adapter calls before unregistering it.\n\n## Language runtimes\n\nRuntime serialization is not thread affinity. For example, CPython's GIL does\nnot guarantee that two FFI calls execute on the same OS thread. Store native\nhandles only in thread-confined objects, or avoid them and use\nsnapshot-qualified refs. Rust, Swift, Node, Go, and managed runtimes need the\nsame discipline when tasks can migrate between worker threads.\n\n## Accessibility permission identity\n\n`ad_check_permissions` calls macOS `AXIsProcessTrusted()`, which reports trust\nfor the hosting executable (`python3`, `node`, a Swift app, and so on), not for\nthe dylib as a separate executable. Permission prompts and deployment guidance\nmust identify the host process that loads `libagent_desktop_ffi.dylib`.\n\n## Last error and blocking calls\n\nThe last-error slot is thread-local. Thread A's failure does not change thread\nB's error state.\n\n`ad_wait` blocks its calling thread for at most its finite deadline and retains\nthe adapter for that duration. Destroying the adapter token concurrently stops\nnew calls but does not invalidate an in-flight wait.\n\nFile v1.0.7:skill-card.md\n\n## Description:\n\nC-ABI bindings over agent-desktop's PlatformAdapter let Python ctypes, Swift, Node ffi-napi, Go cgo, C++, and Ruby fiddle consumers link libagent_desktop_ffi and call ad_* functions directly for observe-act desktop automation.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[lahfir](https://clawhub.ai/user/lahfir)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and engineers use this skill to integrate agents with agent-desktop through C-compatible bindings, linking the shared library and following observe-act, ABI, memory ownership, error-handling, and threading guidance.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The FFI can observe and control desktop UI through the host process.\n\nMitigation: Install and load it only in hosts intended to perform desktop automation, and ensure the host process has appropriate accessibility and input permissions.\n\nRisk: Snapshots, screenshots, clipboard reads, error details, log callbacks, and optional traces can contain sensitive desktop data.\n\nMitigation: Treat these outputs as sensitive, enable tracing deliberately, avoid shared log surfaces for diagnostics, and review or delete trace files when no longer needed.\n\nRisk: An ABI or struct-layout mismatch can make language bindings unsafe or unreliable.\n\nMitigation: Run the ABI handshake and struct-size validation before calling adapter entrypoints, and pin the exact FFI version used by the consumer.\n\n## Reference(s):\n\n- [Pointer ownership](references/ownership.md)\n- [Error handling](references/error-handling.md)\n- [Threading](references/threading.md)\n- [Build and link](references/build-and-link.md)\n- [Apple Thread Safety Summary](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html)\n- [Apple AXUIElement documentation](https://developer.apple.com/documentation/applicationservices/axuielement)\n- [Apple CGEvent documentation](https://developer.apple.com/documentation/coregraphics/cgevent)\n- [Apple NSWorkspace.shared documentation](https://developer.apple.com/documentation/appkit/nsworkspace/shared)\n- [Apple POSIX Threads in Cocoa guidance](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html)\n\n## Skill Output:\n\n**Output Type(s):** [Markdown, Code, Shell commands, Configuration, Guidance]\n\n**Output Format:** [Markdown with C, Python, and shell code blocks]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Guidance covers ABI handshake, shared-library linking, pointer ownership, error handling, threading, tracing, and desktop automation privacy considerations.]\n\n## Skill Version(s):\n\n1.0.7 (source: server release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.0.6: 7 files, 20385 bytes\n\nFiles: references/build-and-link.md (8898b), references/error-handling.md (8544b), references/ownership.md (8514b), references/threading.md (7115b), skill-card.md (2862b), SKILL.md (11274b), _meta.json (136b)\n\nFile v1.0.6:SKILL.md\n\n---\nname: agent-desktop-ffi\nversion: 0.4.1\ntags: ffi, c-bindings, cdylib, python, swift, node, go, rust-ffi\nrequirements:\n  - agent-desktop-ffi\ndescription: >\n  C-ABI bindings over agent-desktop's PlatformAdapter. Consumers\n  (Python ctypes, Swift, Node ffi-napi, Go cgo, C++, Ruby fiddle)\n  link libagent_desktop_ffi.{dylib,so,dll} and call `ad_*` functions\n  directly instead of spawning the CLI binary per call. The canonical\n  observe-act workflow is: ad_init → ad_adapter_create[_with_session]\n  → ad_snapshot → parse @e refs → ad_execute_by_ref → ad_free_string\n  → ad_adapter_destroy.\n---\n\n# agent-desktop-ffi\n\nDirect C-ABI access to every PlatformAdapter operation. Build the\ncdylib with the workspace's `release-ffi` profile:\n\n```sh\ncargo build --profile release-ffi -p agent-desktop-ffi\n```\n\nThe output is `target/release-ffi/libagent_desktop_ffi.dylib`\n(`.so` on Linux, `.dll` on Windows) plus a committed C header at\n`crates/ffi/include/agent_desktop.h`.\n\nA Python ctypes smoke harness lives at `tests/ffi-python/smoke.py` and\nserves as a worked end-to-end example covering the ABI handshake, struct\nsize validation, `ad_version`, and the snapshot pipeline leg. See\n`tests/ffi-python/README.md` for usage.\n\nFour reference topics, loaded as needed:\n\n- [ownership.md](references/ownership.md) — who allocates / who frees,\n  for every `*mut T` the FFI hands back to the caller.\n- [error-handling.md](references/error-handling.md) — errno-style\n  last-error contract, enum validation, panic boundary.\n- [threading.md](references/threading.md) — host-thread contract,\n  cross-process mutation serialization, AXIsProcessTrusted inheritance,\n  and adapter-bound native handles.\n- [build-and-link.md](references/build-and-link.md) — ABI handshake,\n  struct size validation, minimal C and Python examples, observe-act\n  workflow, and prebuilt archive locations.\n\n## Observe-act workflow (canonical path)\n\n```\nad_init(AD_ABI_VERSION_MAJOR)                    // verify header ↔ dylib match\nadapter = ad_adapter_create_with_session(\"s1\")   // or ad_adapter_create()\nrc = ad_snapshot(adapter, \"Finder\", 0, 10, false, false, &json_out)\n// parse json_out: locate snapshot-qualified refs in data.tree\nad_free_string(json_out)\n// build action:\nAdAction act = {0}; act.kind = AD_ACTION_KIND_CLICK;\nrc = ad_execute_by_ref(adapter, \"@s8f3k2p9:e5\", NULL, &act, 0, &result_out)\nad_free_string(result_out)\nad_adapter_destroy(adapter)\n```\n\n`ad_snapshot` returns a `{version, ok, command, data}` JSON envelope\nidentical to the CLI output. The `data.tree` field contains snapshot-qualified\nref IDs for interactive elements. Pass a qualified ref, or a legacy bare ref\nplus its explicit `snapshot_id`, to `ad_execute_by_ref` to drive the pipeline\n(RefStore load → strict resolution → actionability preflight → dispatch).\n\n## Core constraints\n\n- **ABI handshake.** Call `ad_init(AD_ABI_VERSION_MAJOR)` once after loading the\n  dylib. A mismatch between the compiled-in constant and the loaded dylib returns\n  `AD_RESULT_ERR_INVALID_ARGS` — abort rather than proceed. You can also read the\n  raw dylib major via `ad_abi_version()` for diagnostic display. New `ad_*` symbols\n  and new error codes are additive (no bump required); removed or layout-changed\n  symbols increment the major.\n\n- **Session adapters.** `ad_adapter_create_with_session(\"session-id\")` associates\n  the adapter with a session namespace for refmap persistence — the same as CLI\n  `--session <id>`. A null `snapshot_id` is valid only for a qualified ref;\n  legacy bare `@eN` refs require an explicit snapshot ID. Session IDs: 1–64\n  chars, ASCII alphanumeric / `-` / `_`.\n  Invalid IDs return null (check `ad_last_error_*`).\n\n- **Structured session trace (no ABI change).** File-based JSONL tracing activates\n  only when the session has a manifest with `trace: on` from `session start`\n  (CLI) or equivalent on-disk setup. `ad_adapter_create_with_session` alone does\n  **not** create trace files. When tracing is active, `command_context()`-backed\n  commands append to one segment per OS process under\n  `~/.agent-desktop/sessions/<id>/trace/<pid>-<procTs>.jsonl`. A long-lived host\n  reuses the same segment filename for all calls in that process. For unstructured\n  diagnostics regardless of session manifest, use `ad_set_log_callback` (below).\n\n- **Threading and mutation leases.** Adapter entrypoints may be called from any\n  host thread. Native handles remain bound to their creating adapter and thread.\n  Desktop mutations acquire the same canonical cross-process interaction lease\n  as the CLI; reads carry finite deadlines without taking the mutation lock. See\n  [threading.md](references/threading.md) for the Apple documentation basis and\n  the read/read, read/mutation, and mutation/mutation ordering matrix.\n\n- **Release profile.** `cargo build --release` produces `panic = \"abort\"` —\n  any Rust panic inside an `extern \"C\"` fn will `SIGABRT` the host. Use\n  `--profile release-ffi` to get the correct `panic = \"unwind\"` profile. CI\n  enforces this.\n\n- **Last-error lifetime.** Pointers returned by `ad_last_error_*` remain valid\n  across any number of subsequent *successful* FFI calls on the same thread.\n  Only the next failing call rotates them. Cache the pointer once, read it as\n  many times as you need.\n\n- **ad_last_error_details.** A fourth accessor, `ad_last_error_details()`,\n  returns a borrowed JSON string carrying structured details (e.g. the\n  actionability check report on `ACTION_FAILED`, candidate summaries on\n  `AMBIGUOUS_TARGET`). The details may contain element names, values, and window\n  titles from the user's screen — treat as sensitive diagnostics and avoid routing\n  to shared log surfaces.\n\n- **Handle release.** Every `ad_resolve_element_exact` / `ad_find_exact` result must be\n  released with `ad_free_handle(adapter, &handle)` on the same adapter that\n  produced it, before that adapter is destroyed. On macOS this balances the\n  internal `CFRetain`; on Windows/Linux the call is a no-op but safe to issue.\n  `ad_free_handle` zeroes `handle.ptr` so a follow-up call is a safe no-op.\n\n- **Primary ref-action path.** `ad_execute_by_ref` is the recommended entrypoint\n  for the observe-act loop: it loads the RefStore, looks up the ref in the refmap\n  (STALE_REF on miss), runs strict element re-identification (STALE_REF / AMBIGUOUS_TARGET),\n  runs the live actionability preflight, then dispatches. TypeText and PressKey\n  default to `focus_fallback` policy (matching CLI `type`/`press-key`); all other\n  actions default to `headless`. Pass `AD_POLICY_KIND_HEADED` (2) to opt in to\n  cursor-based fallbacks.\n\n- **Generation-safe direct APIs.** ABI-v3 legacy `AdRefEntry` and\n  `AdWindowInfo` layouts remain available for binary compatibility, but they do\n  not carry process-generation evidence and direct targeting functions fail\n  closed. Use `AdExactRefEntry`, `AdExactWindowInfo`,\n  `ad_list_windows_exact`, and the `*_exact` targeting symbols. Likewise,\n  `ad_list_surfaces_exact` preserves `SurfaceInfo.id`; the legacy surface list\n  is an observation-only projection that omits it.\n\n- **Display discovery.** Call `ad_list_displays` before using\n  `AdScreenshotTarget.screen_index`. List order is the screenshot index order;\n  inspect each `AdDisplayInfo` for its stable display ID, bounds, primary flag,\n  and scale, then release the handle with `ad_display_list_free`.\n\n- **Low-level action paths.** `ad_execute_action` (headless, no preflight) and\n  `ad_execute_action_with_policy` are raw escape hatches for callers holding a live\n  `AdNativeHandle` from `ad_resolve_element_exact` / `ad_find_exact`. Use them when\n  you need to bypass the ref-action pipeline.\n\n- **Ref-action preflight.** `ad_execute_by_ref` and\n  `ad_execute_ref_action_exact_with_policy` both resolve the element strictly and run the live actionability preflight\n  (visible, stable, enabled, supported action, policy, editable) before dispatching\n  — a disabled or unsupported target fails before any platform call. On\n  `AD_RESULT_ERR_ACTION_FAILED`, the structured check report is available as JSON\n  via `ad_last_error_details()`.\n\n- **Action result steps.** `AdActionResult.steps` mirrors the CLI `steps` array\n  for activation-chain actions. Each entry has `label` and `outcome` strings and\n  is owned by the result; release with `ad_free_action_result(&out)`.\n\n- **Tracing / log callback.** Two tracing surfaces coexist:\n\n  1. **Structured file trace** — same JSONL contract as CLI `--trace`, gated by a\n     `trace: on` session manifest. Segments include `event`, `ts_ms`, `seq`, and\n     redacted fields. Requires `session start` (or equivalent manifest on disk)\n     before creating the adapter; plain session-id adapters write nothing to disk.\n\n  2. **`ad_set_log_callback(cb)`** — installs a `tracing` subscriber layer that\n     delivers events as JSON to your callback. `cb` receives an int32_t level\n     (1=ERROR … 5=TRACE) and a `const char *msg` valid only for the duration of\n     the call. Pass `NULL` to unregister. The layer is installed on the first\n     non-null call; if a foreign global subscriber already owns the process at\n     that point, the install fails with `AD_RESULT_ERR_INTERNAL` and no events are\n     ever delivered. Sensitive field values (password, token, text, …) are\n     replaced with `{\"redacted\":true}` before formatting. A panicking callback is\n     caught and silently discarded. The callback may fire from threads other than\n     the registering thread, and may still fire briefly after a `NULL` unregister\n     — keep the callback and any data it captures valid for the process lifetime.\n\n- **Wait.** `ad_wait(adapter, args, &out)` runs the full CLI `wait` command\n  (element-appear, window-appear, text-appear, menu-open/close, notification,\n  element predicates). Zero-initialize `AdWaitArgs`, set the fields you need, and\n  validate the struct size against `AD_WAIT_ARGS_SIZE` / `ad_wait_args_size()` before\n  calling. The output is a `{version, ok, command, data}` JSON envelope freed with\n  `ad_free_string`. `ad_wait` blocks the calling thread up to `timeout_ms` ms —\n  ensure the adapter is not destroyed from another thread while it is running.\n\n- **Text input privacy.** On macOS, focus-fallback or headed text insertion may\n  briefly use the clipboard for non-ASCII text. For sensitive text, prefer\n  `AD_ACTION_KIND_SET_VALUE` with `AD_POLICY_KIND_HEADLESS` when the target\n  supports settable values.\n\n- **Enum discriminants.** Every `#[repr(i32)]` enum field is validated at the C\n  boundary — invalid discriminants return `AD_RESULT_ERR_INVALID_ARGS` instead of\n  undefined behavior.\n\n- **ABI stability.** The major version in `AD_ABI_VERSION_MAJOR` increments on any\n  breaking change (removed symbol, incompatible layout). Additive changes (new\n  symbols, new error codes) do not bump it. Before 1.0, pin the exact version of\n  libagent_desktop_ffi you link against.\n\n- **`ad_get_tree_exact` vs `ad_snapshot`.** `ad_get_tree_exact` returns a raw flat BFS tree\n  without `@e` refs, no refmap persistence, and no JSON envelope — use it for\n  custom traversal or UI inspection. For observe-act agents that drive actions via\n  `ad_execute_by_ref`, always start with `ad_snapshot`.\n\nFile v1.0.6:_meta.json\n\n{\n  \"ownerId\": \"kn7a8wtv8q9jjhpxh4w601zsb5827bnx\",\n  \"slug\": \"agent-desktop-ffi\",\n  \"version\": \"1.0.6\",\n  \"publishedAt\": 1784532647361\n}\n\nFile v1.0.6:references/build-and-link.md\n\n# Build and link\n\n## Building the cdylib\n\n```sh\ncargo build --profile release-ffi -p agent-desktop-ffi\n```\n\nOutput:\n\n- macOS: `target/release-ffi/libagent_desktop_ffi.dylib`\n- Linux: `target/release-ffi/libagent_desktop_ffi.so`\n- Windows: `target/release-ffi/agent_desktop_ffi.dll`\n\nThe generated header is at `crates/ffi/include/agent_desktop.h`. CI\nvalidates that the committed header matches what `cargo build`\nregenerates — if you change a type in `crates/ffi/src/`, rebuild\nlocally and commit the updated header.\n\n`--profile release-ffi` keeps `panic = \"unwind\"`, which is required for\nthe `catch_unwind` traps inside every `extern \"C\"` entrypoint. The default\n`release` profile uses `panic = \"abort\"` (for CLI binary-size reasons) and\nsilently defeats those traps.\n\n## Prebuilt archives\n\nEvery GitHub release ships prebuilt archives for:\n\n- macOS arm64 and x86_64\n- Linux x64 and arm64 (glibc)\n- Windows x64 MSVC\n\nEach archive contains the dylib/so/dll, `include/agent_desktop.h`, and\n`LICENSE`. Integrity: compare against `checksums.txt` in the release\nassets. Supply-chain verification: each release is signed via Sigstore\nattestation — verify with `cosign verify-blob` before deploying.\n\n## ABI handshake (do this first)\n\nAfter `dlopen` / `LoadLibrary`, compare the dylib major to the header you\ncompiled against before calling anything else:\n\n```c\nAdResult rc = ad_init(AD_ABI_VERSION_MAJOR);\nif (rc != AD_RESULT_OK) {\n    // header and dylib have incompatible major versions\n    fprintf(stderr, \"ABI mismatch: %s\\n\", ad_last_error_message());\n    return -1;\n}\n```\n\nAlternatively, read the raw dylib major and compare yourself:\n\n```c\nuint32_t dylib_major = ad_abi_version();\nif (dylib_major != AD_ABI_VERSION_MAJOR) {\n    fprintf(stderr, \"ABI major: header=%u dylib=%u\\n\",\n            AD_ABI_VERSION_MAJOR, dylib_major);\n    abort();\n}\n```\n\n`ad_init` returns `AD_RESULT_ERR_INVALID_ARGS` on mismatch (with a\ndiagnostic in `ad_last_error_message`). A mismatch means the header you\ncompiled against and the loaded dylib are incompatible — do not call\nanything further.\n\n## Struct size validation\n\nLanguages whose struct layout may diverge from C (Python ctypes, Go cgo,\nJNI, etc.) must validate every size-pinned struct before passing it to\nthe library. The FFI exposes three validation layers:\n\n1. **Header macros**: `AD_ACTION_SIZE`, `AD_WAIT_ARGS_SIZE`,\n   `AD_REF_ENTRY_SIZE`, `AD_DRAG_PARAMS_SIZE`, `AD_ACTION_RESULT_SIZE`,\n   `AD_ACTION_STEP_SIZE`, `AD_ELEMENT_STATE_SIZE`.\n2. **Runtime getters**: `ad_action_size()`, `ad_wait_args_size()`,\n   `ad_ref_entry_size()`, `ad_drag_params_size()`, `ad_action_result_size()`,\n   `ad_action_step_size()`, `ad_element_state_size()` — each returns the\n   same value the macro encodes, compiled from the Rust side.\n3. **C11 static asserts** in the header (`#ifndef AGENT_DESKTOP_ABI_ASSERTS`)\n   catch mismatches at C compile time.\n\nCompare your binding's `sizeof` equivalent against the getter at load\ntime, before building or passing any of these structs. The Python smoke\nharness (`tests/ffi-python/smoke.py` Leg 2) demonstrates this check for\nall size-pinned structs in a single loop.\n\n## Worked example: Python ctypes smoke harness\n\n`tests/ffi-python/smoke.py` is the canonical reference for Python\nconsumers. It covers:\n\n- Leg 1: `ad_abi_version()` vs `AD_ABI_VERSION_MAJOR` (header/dylib match)\n- Leg 2: every `ad_*_size()` getter vs its `AD_*_SIZE` macro (struct layout)\n- Leg 3: `ad_version()` → parse JSON → `ad_free_string` (basic pipeline)\n- Leg 4: `ad_adapter_create` → `ad_snapshot` → `ad_free_string` →\n  `ad_adapter_destroy` (full adapter lifecycle, stub passes through\n  `PLATFORM_NOT_SUPPORTED`)\n\nRun it:\n\n```bash\npython3 tests/ffi-python/smoke.py \\\n  target/release-ffi/libagent_desktop_ffi.dylib \\\n  crates/ffi/include/agent_desktop.h\n```\n\nOr with environment variables:\n\n```bash\nAD_DYLIB_PATH=target/release-ffi/libagent_desktop_ffi.dylib \\\nAD_HEADER_PATH=crates/ffi/include/agent_desktop.h \\\npython3 tests/ffi-python/smoke.py\n```\n\nNo `pip install` required — `smoke.py` uses the Python standard library only.\n\n## Minimal C example\n\n```c\n#include <stdio.h>\n#include \"agent_desktop.h\"\n\nint main(void) {\n    // 1. ABI handshake\n    if (ad_init(AD_ABI_VERSION_MAJOR) != AD_RESULT_OK) {\n        fprintf(stderr, \"ABI mismatch: %s\\n\", ad_last_error_message());\n        return 1;\n    }\n\n    // 2. Create adapter\n    AdAdapter *adapter = ad_adapter_create();\n    if (!adapter) {\n        fprintf(stderr, \"adapter_create failed: %s\\n\", ad_last_error_message());\n        return 1;\n    }\n\n    // 3. Check permissions\n    AdResult rc = ad_check_permissions(adapter);\n    if (rc != AD_RESULT_OK) {\n        fprintf(stderr, \"permission denied: %s\\n\", ad_last_error_message());\n        ad_adapter_destroy(adapter);\n        return 1;\n    }\n\n    // 4. Snapshot the focused window\n    char *json_out = NULL;\n    rc = ad_snapshot(adapter,\n                     NULL,   // app (null = focused window)\n                     0,      // surface = Window\n                     10,     // max_depth\n                     false,  // interactive_only\n                     false,  // compact\n                     &json_out);\n    if (rc == AD_RESULT_OK && json_out) {\n        printf(\"%s\\n\", json_out);\n        // parse JSON to find @e refs in data.tree\n        ad_free_string(json_out);\n    } else if (json_out) {\n        // command-level error — ok:false envelope, still must free\n        fprintf(stderr, \"snapshot error: %s\\n\", json_out);\n        ad_free_string(json_out);\n    } else {\n        // infrastructure error — *out is null\n        fprintf(stderr, \"snapshot failed: %s\\n\", ad_last_error_message());\n    }\n\n    ad_adapter_destroy(adapter);\n    return 0;\n}\n```\n\nCompile:\n\n```sh\nclang -I./crates/ffi/include main.c \\\n      -L./target/release-ffi -lagent_desktop_ffi \\\n      -o snapshot_demo\ninstall_name_tool -change \\\n    libagent_desktop_ffi.dylib \\\n    @executable_path/target/release-ffi/libagent_desktop_ffi.dylib \\\n    snapshot_demo\n```\n\n## Observe-act workflow in C\n\nAfter parsing the snapshot JSON and extracting a qualified ref ID:\n\n```c\nAdAction act = {0};              // zero-init before setting any field\nact.kind = AD_ACTION_KIND_CLICK;\n\nchar *result = NULL;\nAdResult rc = ad_execute_by_ref(\n    adapter,\n    \"@s8f3k2p9:e5\", // qualified ref from snapshot data.tree\n    NULL,            // the qualified ref embeds its snapshot ID\n    &act,\n    0,            // policy = Headless\n    &result\n);\nif (result) {\n    // parse JSON — ok:true on success, ok:false on STALE_REF etc.\n    printf(\"%s\\n\", result);\n    ad_free_string(result);\n}\nif (rc != AD_RESULT_OK) {\n    const char *det = ad_last_error_details();  // may be NULL; treat as sensitive\n    fprintf(stderr, \"execute_by_ref failed (%d): %s\\n\",\n            (int)rc, ad_last_error_message());\n}\n```\n\nTo type text, set the action kind and text field:\n\n```c\nAdAction type_act = {0};\ntype_act.kind = AD_ACTION_KIND_TYPE_TEXT;\ntype_act.text = \"hello\";\n// TypeText defaults to focus_fallback via ad_execute_by_ref; explicit policy:\nrc = ad_execute_by_ref(adapter, \"@s8f3k2p9:e3\", NULL, &type_act,\n                       AD_POLICY_KIND_FOCUS_FALLBACK, &result);\n```\n\n## Threading reminder\n\nAdapter entrypoints may be called from any host thread. Keep native handles on\nthe thread that resolved them and use snapshot-qualified refs across async or\nworker boundaries. Mutations are serialized by a canonical cross-process\ninteraction lease. See [threading.md](threading.md).\n\n## Minimal Python ctypes example\n\n```python\nimport ctypes, json\nfrom ctypes import c_int, c_int32, c_uint8, c_bool, c_char_p, POINTER, c_void_p\n\nlib = ctypes.CDLL(\"./target/release-ffi/libagent_desktop_ffi.dylib\")\n\n# ABI handshake\nlib.ad_init.restype = c_int32\nlib.ad_init.argtypes = [ctypes.c_uint32]\nAD_ABI_VERSION_MAJOR = 3  # sync with header macro\nrc = lib.ad_init(AD_ABI_VERSION_MAJOR)\nassert rc == 0, f\"ABI mismatch: rc={rc}\"\n\n# Adapter lifecycle\nlib.ad_adapter_create.restype = c_void_p\nlib.ad_adapter_create.argtypes = []\nlib.ad_adapter_destroy.restype = None\nlib.ad_adapter_destroy.argtypes = [c_void_p]\n\n# Snapshot\nlib.ad_snapshot.restype = c_int32\nlib.ad_snapshot.argtypes = [c_void_p, c_char_p, c_int32, c_uint8, c_bool, c_bool,\n                             POINTER(c_char_p)]\nlib.ad_free_string.restype = None\nlib.ad_free_string.argtypes = [c_char_p]\n\nlib.ad_last_error_message.restype = c_char_p\nlib.ad_last_error_message.argtypes = []\n\nadapter = lib.ad_adapter_create()\nassert adapter, \"ad_adapter_create() returned null\"\n\nout = c_char_p()\nrc = lib.ad_snapshot(adapter, None, 0, 10, False, False, ctypes.byref(out))\nif out.value:\n    envelope = json.loads(out.value)\n    lib.ad_free_string(out)\n    print(\"ok:\", envelope.get(\"ok\"))\nelse:\n    msg = lib.ad_last_error_message()\n    print(\"error:\", msg.decode() if msg else \"(no message)\")\n\nlib.ad_adapter_destroy(adapter)\n```\n\nFile v1.0.6:references/error-handling.md\n\n# Error handling\n\nThe FFI uses an errno-style last-error pattern. Every `AdResult`-returning\nfunction returns `AD_RESULT_OK` (= 0) on success or a negative error\ncode on failure. When a failure occurs, thread-local last-error state is\npopulated; read it with the `ad_last_error_*` accessors.\n\n## Minimal pattern\n\n```c\nAdResult rc = ad_launch_app(adapter, \"com.apple.finder\", 5000, &win);\nif (rc != AD_RESULT_OK) {\n    const char *msg = ad_last_error_message();\n    const char *sug = ad_last_error_suggestion();   // may be NULL\n    fprintf(stderr, \"launch_app failed (%d): %s\\n\", (int)rc, msg ? msg : \"(no message)\");\n    if (sug) fprintf(stderr, \"  suggestion: %s\\n\", sug);\n    // no need to release the struct — out-param was zero-initialized\n    return -1;\n}\n// ...use win...\nad_release_window_fields(&win);\n```\n\n## Last-error accessors\n\nFour accessors share the same per-thread lifetime contract:\n\n| Accessor                        | Returns                                                        |\n|---------------------------------|----------------------------------------------------------------|\n| `ad_last_error_code()`          | The `AdResult` code of the last failure, or `AD_RESULT_OK`    |\n| `ad_last_error_message()`       | Human-readable description, or null                            |\n| `ad_last_error_suggestion()`    | Recovery hint, or null                                         |\n| `ad_last_error_platform_detail()` | OS-specific diagnostic (AX codes, HRESULTs, AT-SPI), or null |\n| `ad_last_error_details()`       | Structured JSON details, or null — **sensitive** (see below)   |\n\n`ad_last_error_details()` returns a JSON string with structured context:\nthe actionability check report on `ACTION_FAILED`, candidate element\nsummaries on `AMBIGUOUS_TARGET`, the last observed state on a `wait`\n`TIMEOUT`, etc. The details may contain element names, values, and window\ntitles from the user's screen. Treat as sensitive diagnostics and avoid\nrouting to shared log surfaces.\n\n## Lifetime contract\n\nThe pointer returned by any `ad_last_error_*` accessor remains valid\nacross any number of subsequent **successful** FFI calls. Only the next\n**failing** call rotates the slot.\n\nConsequence: you can cache the pointer right after a failure and keep\nreading it until the next failure — equivalent to POSIX `errno` /\n`strerror`.\n\n```c\nAdResult rc = ad_some_call(...);\nconst char *msg = ad_last_error_message();   // snapshot\n\nad_check_permissions(adapter);                // success\nad_check_permissions(adapter);                // success\nprintf(\"%s\\n\", msg);                          // still valid\n```\n\nFailure-path calls rotate: if a subsequent call fails, the prior\npointer may dangle. Read it before the next potentially-failing call.\n\nLast-error is per-thread (thread-local storage) — Thread A's failure\ndoes not affect Thread B's slot.\n\n`ad_check_permissions` does not treat `Unknown` as success. Stub adapters\nthat cannot answer permission probes return\n`AD_RESULT_ERR_PLATFORM_NOT_SUPPORTED`. The macOS adapter reports\n`AD_RESULT_ERR_INTERNAL` only if the platform probe itself is ambiguous;\nread `ad_last_error_*` for the diagnostic.\n\n## Error codes\n\nNumeric values are ABI-stable. New codes are appended; existing values\nare not renumbered. Always handle values outside this list — future\nreleases may add codes.\n\n| Name                                  | i32   | Meaning                                    |\n|---------------------------------------|-------|--------------------------------------------|\n| `AD_RESULT_OK`                        |   0   | Success                                    |\n| `AD_RESULT_ERR_PERM_DENIED`           |  -1   | Accessibility / input permission missing   |\n| `AD_RESULT_ERR_ELEMENT_NOT_FOUND`     |  -2   | Ref resolve / find miss                    |\n| `AD_RESULT_ERR_APP_NOT_FOUND`         |  -3   | Bundle/PID lookup miss                     |\n| `AD_RESULT_ERR_ACTION_FAILED`         |  -4   | Action dispatched but rejected             |\n| `AD_RESULT_ERR_ACTION_NOT_SUPPORTED`  |  -5   | Platform cannot perform this action        |\n| `AD_RESULT_ERR_STALE_REF`             |  -6   | Ref predates a UI change; re-snapshot      |\n| `AD_RESULT_ERR_WINDOW_NOT_FOUND`      |  -7   | Window filter matched nothing              |\n| `AD_RESULT_ERR_PLATFORM_NOT_SUPPORTED`|  -8   | API unavailable on this OS                 |\n| `AD_RESULT_ERR_TIMEOUT`               |  -9   | Wait exceeded deadline                     |\n| `AD_RESULT_ERR_INVALID_ARGS`          | -10   | Null pointer, bad enum, invalid UTF-8      |\n| `AD_RESULT_ERR_NOTIFICATION_NOT_FOUND`| -11   | Notification index out of range or reordered |\n| `AD_RESULT_ERR_INTERNAL`              | -12   | Internal failure or foreign-subscriber conflict |\n| `AD_RESULT_ERR_SNAPSHOT_NOT_FOUND`    | -13   | Requested snapshot ref store is missing    |\n| `AD_RESULT_ERR_POLICY_DENIED`         | -14   | Current action policy blocks this fallback |\n| `AD_RESULT_ERR_AMBIGUOUS_TARGET`      | -15   | Strict re-identification found multiple candidates; re-snapshot |\n| `AD_RESULT_ERR_APP_UNRESPONSIVE`      | -16   | Read-only liveness probe failed after an uncertain mutation |\n\n## Ref token validation\n\nRef-taking entrypoints accept two canonical forms:\n\n- `@<snapshot_id>:e<N>` is a qualified ref. `snapshot_id` may be null. If a\n  separate snapshot ID is supplied, it must match the embedded ID.\n- `@e<N>` is a legacy bare ref. It requires a non-null snapshot ID argument.\n\n`N` is a positive `u32` written without a sign and with at most 10 decimal\ndigits. Snapshot IDs are 3–64 ASCII alphanumeric, `-`, or `_` characters.\nMalformed tokens, invalid UTF-8, a bare ref without a snapshot ID, or mismatched\nembedded and explicit snapshot IDs return `AD_RESULT_ERR_INVALID_ARGS` before\ndispatch. A well-formed token whose saved snapshot is absent returns\n`AD_RESULT_ERR_SNAPSHOT_NOT_FOUND`; a saved snapshot with no matching local ref\nreturns `AD_RESULT_ERR_STALE_REF`.\n\nSnapshot lookup is confined to the adapter's namespace. An adapter created with\n`ad_adapter_create_with_session` never searches the global namespace or another\nsession for a matching snapshot ID.\n\n## Off-main-thread migration\n\nABI v3 removed the blanket macOS main-thread rejection. Adapter entrypoints may\nnow run on any host thread, so callers must stop treating\n`AD_RESULT_ERR_INTERNAL` as an expected off-main-thread result. Native handles\nremain thread-affine capabilities: cross-thread, cross-adapter, released, or\nrevoked handle use returns `AD_RESULT_ERR_INVALID_ARGS`. See\n[threading.md](threading.md) for the concurrency and ownership contract.\n\n## Enum validation\n\nEvery `#[repr(i32)]` enum field is validated at the C boundary. An\nout-of-range discriminant returns `AD_RESULT_ERR_INVALID_ARGS` with\ndiagnostic last-error text. This prevents the consumer from accidentally\ntriggering undefined behavior by stuffing an arbitrary `int32_t` into an\nenum slot. Affected fields: `AdAction.kind` (`AdActionKind`),\n`AdMouseEvent.kind` (`AdMouseEventKind`), `AdMouseEvent.button`\n(`AdMouseButton`), `AdScrollParams.direction` (`AdDirection`),\n`AdTreeOptions.surface` (`AdSnapshotSurface`), `AdScreenshotTarget.kind`\n(`AdScreenshotKind`), `AdWindowOp.kind` (`AdWindowOpKind`), and the\n`policy` parameter of `ad_execute_by_ref` / `ad_execute_action_with_policy`\n/ `ad_execute_ref_action_exact_with_policy` (`AdPolicyKind`).\n\n## Command-backed JSON entrypoints: dual-failure modes\n\n`ad_snapshot`, `ad_execute_by_ref`, `ad_wait`, `ad_status`, and\n`ad_version` have two distinct failure modes:\n\n- **Argument / infrastructure failure** (null adapter, invalid UTF-8,\n  bad discriminant, context error): `*out` is set to null,\n  no allocation is made, and the last-error slot is the only failure\n  indication.\n- **Command-level failure** (app not found, STALE_REF, TIMEOUT, etc.):\n  `*out` is set to a heap-allocated JSON string with `\"ok\":false` and an\n  `\"error\"` payload. The caller **must still free** it with\n  `ad_free_string(*out)`. The last-error slot is also set.\n\nAlways check `*out` for null before deciding whether to free.\n\n## Panic safety\n\nEvery `extern \"C\"` entrypoint wraps its body in `catch_unwind`. A\nRust panic inside the FFI surfaces as `AD_RESULT_ERR_INTERNAL` with\nmessage `\"rust panic in FFI boundary\"`. No `SIGABRT`, no host crash.\n\nThe cdylib must be built under the `release-ffi` profile for this\nguarantee to hold in optimized builds — the workspace `release` profile\nuses `panic = \"abort\"` (for CLI binary-size reasons).\n\nFile v1.0.6:references/ownership.md\n\n# Pointer ownership\n\nEvery `*mut T` / `*const T` returned by the FFI comes with a matching\nfree function. Always call it; the allocator the FFI uses is Rust's\n`Box::from_raw` / `CString::from_raw`, which cannot be freed with C's\n`free()`.\n\n## Allocation / release table\n\n### Command-backed JSON strings\n\nThese entrypoints write an owned, NUL-terminated JSON envelope into\n`*out`; free with `ad_free_string`. See error-handling.md for the\ndual-failure mode (command-level errors write `ok:false` JSON into\n`*out`; infrastructure errors leave `*out` null with no allocation).\n\n| Allocates                                                                         | Frees with              |\n|-----------------------------------------------------------------------------------|-------------------------|\n| `ad_version(&out)`                                                                | `ad_free_string(out)`   |\n| `ad_status(adapter, &out)`                                                        | `ad_free_string(out)`   |\n| `ad_snapshot(adapter, app, surface, max_depth, interactive_only, compact, &out)` | `ad_free_string(out)`   |\n| `ad_execute_by_ref(adapter, ref_id, snapshot_id, action, policy, &out)`          | `ad_free_string(out)`   |\n| `ad_wait(adapter, args, &out)`                                                    | `ad_free_string(out)`   |\n\n### Adapter lifecycle\n\n| Allocates                                            | Frees with                              |\n|------------------------------------------------------|-----------------------------------------|\n| `ad_adapter_create()`                                | `ad_adapter_destroy(adapter)`           |\n| `ad_adapter_create_with_session(session)`            | `ad_adapter_destroy(adapter)`           |\n\n### Opaque list handles\n\n| Allocates                                                    | Frees with                              |\n|--------------------------------------------------------------|-----------------------------------------|\n| `ad_list_apps(adapter, &list)`                               | `ad_app_list_free(list)`                |\n| `ad_list_displays(adapter, &list)`                           | `ad_display_list_free(list)`            |\n| `ad_list_windows(adapter, app, focused, &list)`              | `ad_window_list_free(list)`             |\n| `ad_list_windows_exact(adapter, app, focused, &list)`        | `ad_exact_window_list_free(list)`       |\n| `ad_list_surfaces(adapter, pid, &list)`                      | `ad_surface_list_free(list)`            |\n| `ad_list_surfaces_exact(adapter, pid, &list)`                | `ad_exact_surface_list_free(list)`      |\n| `ad_list_notifications(adapter, filter, &list)`              | `ad_notification_list_free(list)`       |\n| `ad_dismiss_all_notifications(adapter, f, &ok, &fail)`       | `ad_notification_list_free` on each, or `ad_dismiss_all_notifications_free(ok, fail)` |\n\n### App / window lifecycle\n\n| Allocates                                                 | Frees with                                                          |\n|-----------------------------------------------------------|---------------------------------------------------------------------|\n| `ad_launch_app(adapter, id, timeout, &out)`               | `ad_release_window_fields(&out)` — frees interior strings only; the `AdWindowInfo` struct lives on the caller's stack |\n| `ad_launch_app_exact(adapter, id, timeout, &out)`         | `ad_release_exact_window_fields(&out)` |\n\n### Raw tree and element access\n\n| Allocates                                                                              | Frees with                              |\n|----------------------------------------------------------------------------------------|-----------------------------------------|\n| `ad_get_tree_exact(adapter, win, opts, &out)`                                          | `ad_free_tree(&out)`                    |\n| `ad_resolve_element_exact(adapter, entry, &handle)`                                    | `ad_free_handle(adapter, &handle)` — zeroes `handle.ptr` so a follow-up call is a no-op |\n| `ad_find_exact(adapter, win, query, &handle)`                                          | same as `ad_resolve_element_exact`      |\n\n### Action results\n\n| Allocates                                                                              | Frees with                   |\n|----------------------------------------------------------------------------------------|------------------------------|\n| `ad_execute_action(adapter, handle, action, &out)`                                     | `ad_free_action_result(&out)` |\n| `ad_execute_action_with_policy(adapter, handle, action, policy, &out)`                 | `ad_free_action_result(&out)` |\n| `ad_execute_ref_action_exact_with_policy(adapter, entry, action, policy, &out)`        | `ad_free_action_result(&out)` |\n| `ad_notification_action(adapter, &request, &out)` — set `request.identity` from `ad_list_notifications` and choose an explicit non-headless policy; reorder mismatches fail closed | `ad_free_action_result(&out)` |\n\n### Clipboard and image buffers\n\n| Allocates                                   | Frees with                                                          |\n|---------------------------------------------|---------------------------------------------------------------------|\n| `ad_get_clipboard(adapter, &text)`          | `ad_free_string(text)`                                              |\n| `ad_get(adapter, handle, property, &text)`  | `ad_free_string(text)` — text may be null when the property is absent; `ad_free_string(NULL)` is a no-op |\n| `ad_screenshot(adapter, target, &buf)`      | `ad_image_buffer_free(buf)` (buf is opaque; read via `ad_image_buffer_{data,size,width,height,format}`) |\n\n## Rules\n\n- Every free function is **null-tolerant**. `ad_free_tree(NULL)`,\n  `ad_free_handle(adapter, NULL)`, `ad_free_string(NULL)`, etc. are\n  no-ops. List accessors (`ad_*_list_count`, `_get`) also accept null\n  and return `0` / `NULL` respectively.\n- **Double-free of list handles and `AdImageBuffer` is undefined.** The\n  opaque wrappers are allocated by `Box::into_raw`; the second call\n  would invoke `Box::from_raw` on a freed allocation. Always set the\n  pointer to `NULL` after freeing.\n- **`ad_free_handle` is safe to double-call** — it zeroes `handle.ptr`\n  after the platform release, so a follow-up call sees `NULL` and\n  returns `AD_RESULT_OK` without re-entering `CFRelease`.\n- **Adapters must outlive their handles.** Free every handle with the\n  same adapter and on the same thread that produced it before calling\n  `ad_adapter_destroy`. Destroyed, wrong-adapter, cross-thread, forged, and\n  already-freed tokens are rejected without dereferencing foreign memory.\n- Pointers inside a struct (`.id`, `.title`, `.app_name`, each\n  `AdNotificationInfo.body`, etc.) are freed by the struct's owning\n  free function (list_free / release_fields) — do not call\n  `ad_free_string()` on them individually.\n- `AdActionResult` owns `action`, `ref_id`, `post_state`,\n  `post_state.states`, `steps`, and each `steps[i].label` /\n  `steps[i].outcome`; free all of them only through\n  `ad_free_action_result(&out)`. Treat the returned counts as\n  read-only metadata; release the unmodified result struct.\n- Ownership does **not** transfer back to Rust after you free. Keep a\n  local `NULL` to prevent accidental reuse.\n\n## Out-param zeroing\n\nEvery fallible FFI function zeroes its out-param **before** any guard\n(pointer validation, UTF-8 validation, enum validation). On error,\ncalling the paired free function is safe: all pointers inside are\nguaranteed null, all counts zero, so the free is a no-op rather than\na double-free on a previous caller's allocation.\n\nIn particular:\n\n- `ad_get_clipboard` writes `*out = NULL` before the adapter call —\n  no stale buffer visible on error.\n- `ad_launch_app` writes `*out = zeroed AdWindowInfo` before the\n  platform call — `ad_release_window_fields(&out)` on the zero-init\n  struct is a no-op.\n- `ad_screenshot` writes `*out = NULL` before allocating the image\n  buffer — no stale pointer when the screenshot fails.\n- `ad_snapshot`, `ad_execute_by_ref`, `ad_wait`, `ad_status`,\n  `ad_version` write `*out = NULL` before any guard, so on\n  infrastructure failures no allocation is made and `ad_free_string(NULL)`\n  is a safe no-op.\n- `ad_*_list` and `ad_resolve_element_exact` / `ad_find_exact` all apply the same\n  pattern to their handle / list out-params.\n\nFile v1.0.6:references/threading.md\n\n# Threading\n\n## Host-thread contract\n\nFFI entrypoints may be called from any host thread. The library does not apply\na blanket macOS main-thread guard to Accessibility (`AXUIElement`) or Quartz\nevent (`CGEvent`) operations.\n\nThat contract follows Apple's published boundaries:\n\n- Apple's [Thread Safety Summary](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html)\n  says objects restricted to the main thread are called out explicitly and\n  describes Core Foundation as thread-safe for common immutable query, retain,\n  release, and transfer operations.\n- Apple documents [`AXUIElement`](https://developer.apple.com/documentation/applicationservices/axuielement)\n  as an accessibility object and its header as a CF type; it does not publish a\n  blanket main-thread requirement for AX calls.\n- Apple documents [`CGEvent`](https://developer.apple.com/documentation/coregraphics/cgevent)\n  as a CF-derived low-level event type; it likewise does not publish a blanket\n  main-thread restriction.\n\nAppKit view/event-loop rules do not automatically apply to an assistive\napplication calling AX or Quartz APIs. Apple explicitly says\n[`NSWorkspace.shared`](https://developer.apple.com/documentation/appkit/nsworkspace/shared)\nis safe to access from any thread, and does not publish a main-thread-only rule\nfor `NSPasteboard`.\n\nCode paths that use Cocoa objects create their required autorelease pools\ninternally. Apple's Thread Safety Summary requires a pool on secondary threads\nthat use Cocoa and identifies classes such as `NSView` as genuinely\nmain-thread-only. Because Rust and many foreign runtimes create POSIX threads,\nFFI initialization also follows Apple's\n[Using POSIX Threads in a Cocoa Application](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html)\nguidance by starting one `NSThread` once, which causes Cocoa to install its\nmultithreading locks before worker-thread AppKit calls.\n\n## Concurrency and mutation ordering\n\nAdapter registry handles can be acquired concurrently. Read operations carry a\nfinite deadline and do not take the interaction lease.\n\n| Concurrent calls | Contract |\n|------------------|----------|\n| read + read | Both may run concurrently; neither waits for the interaction lease |\n| read + mutation | The read does not wait for the lease and may overlap the mutation; its result is a point-in-time observation, not a transaction |\n| mutation + mutation | One lease holder runs at a time for the same OS user; the waiter consumes its command deadline and returns a structured timeout if it cannot acquire the lease |\n| native-handle call on another thread | Rejected with `AD_RESULT_ERR_INVALID_ARGS`; use a qualified ref instead |\n\nSnapshots, finds, gets, display/window/app listings, screenshots, clipboard\nreads, permission reads, status, and trace reads are observations. Ref actions,\ndirect native-handle actions, input synthesis, clipboard writes/clear, app and\nwindow changes, notification actions, and permission requests are mutations.\nFor example, `ad_snapshot` and `ad_execute_by_ref` may be called from different\nthreads, but the snapshot can overlap the action; callers that require\nobserve-then-act ordering must wait for the snapshot result before dispatching\nthe action.\n\nDesktop mutations take a process-independent advisory lock at\n`/tmp/agent-desktop-<uid>/interaction.lock`. Current CLI and FFI builds therefore\nserialize mutations across threads, processes, and different HOME values for\nthe same user. The lock is command-scoped, not transaction-scoped: another\nactor may change the UI between an observation and a later action, or between\ntwo independently invoked actions.\n\nThis ordering is implemented by agent-desktop's in-process process guard plus a\nUnix advisory file lock, not by `AXObserver`, an AppKit run loop, or a global\nApple accessibility mutex. On non-Unix platforms, each adapter must provide the\nsame `PlatformAdapter::acquire_interaction_lease` contract with its native\nserialization primitive.\n\nThe lease cannot coordinate:\n\n- older `agent-desktop` binaries that predate the lock;\n- direct human input or unrelated automation tools;\n- state changes initiated by the target application itself.\n\nCallers must still use strict refs/exact window identities and treat stale or\nambiguous targets as normal retryable automation outcomes.\n\n## Adapter destruction\n\nAdapter pointers are opaque registry tokens. Each call acquires a retained\nadapter owner before platform work. `ad_adapter_destroy` revokes the token:\ncalls that already acquired it finish safely, while calls that begin afterward\nreturn `AD_RESULT_ERR_INVALID_ARGS`. Concurrent destruction cannot free memory\nstill referenced by an in-flight call.\n\nDestroying an adapter also revokes native handles created by that adapter on the\ncalling thread. Other threads' handle registries reject subsequent use because\nthe owner adapter token no longer exists.\n\n## Native-handle thread ownership\n\n`ad_resolve_element_exact` and `ad_find_exact` return an opaque\n`AdNativeHandle`. Native handles are adapter-bound and thread-affine:\n\n- resolve, use, and release a handle on the same thread;\n- pass the same adapter that produced the handle;\n- do not use a handle after destroying its adapter.\n\nViolations are rejected with `AD_RESULT_ERR_INVALID_ARGS`; the library does not\ndereference forged, cross-adapter, cross-thread, released, or revoked tokens.\nPrefer snapshot-qualified refs and `ad_execute_by_ref` when a handle would need\nto cross an async task or thread boundary.\n\n## Log callback threading\n\n`ad_set_log_callback(cb)` may be called from any thread. The callback may be\ninvoked from any thread that calls an `ad_*` function.\n\nA callback unregistered via `NULL` may still receive an invocation already in\nflight on another thread. Keep the callback and captured state alive for the\nprocess lifetime, or quiesce active adapter calls before unregistering it.\n\n## Language runtimes\n\nRuntime serialization is not thread affinity. For example, CPython's GIL does\nnot guarantee that two FFI calls execute on the same OS thread. Store native\nhandles only in thread-confined objects, or avoid them and use\nsnapshot-qualified refs. Rust, Swift, Node, Go, and managed runtimes need the\nsame discipline when tasks can migrate between worker threads.\n\n## Accessibility permission identity\n\n`ad_check_permissions` calls macOS `AXIsProcessTrusted()`, which reports trust\nfor the hosting executable (`python3`, `node`, a Swift app, and so on), not for\nthe dylib as a separate executable. Permission prompts and deployment guidance\nmust identify the host process that loads `libagent_desktop_ffi.dylib`.\n\n## Last error and blocking calls\n\nThe last-error slot is thread-local. Thread A's failure does not change thread\nB's error state.\n\n`ad_wait` blocks its calling thread for at most its finite deadline and retains\nthe adapter for that duration. Destroying the adapter token concurrently stops\nnew calls but does not invalidate an in-flight wait.\n\nFile v1.0.6:skill-card.md\n\n## Description: <br>\nC-ABI bindings over agent-desktop's PlatformAdapter let consumers such as Python ctypes, Swift, Node ffi-napi, Go cgo, C++, and Ruby fiddle link libagent_desktop_ffi directly and run the observe-act workflow without spawning the CLI binary per call. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[lahfir](https://clawhub.ai/user/lahfir) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and engineers use this skill to build, link, and operate FFI integrations for desktop automation through agent-desktop's PlatformAdapter. It helps consuming runtimes manage ABI handshakes, adapter lifecycles, references, errors, threading, and memory ownership. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: Consuming programs can drive desktop automation through this FFI. <br>\nMitigation: Install and use the skill only when that desktop automation capability is intended for the consuming host program. <br>\nRisk: Snapshots, screenshots, clipboard data, trace files, and last-error details can contain sensitive user or application data. <br>\nMitigation: Treat these outputs as sensitive diagnostics and avoid routing them to shared logs or storage unless explicitly intended. <br>\nRisk: Tracing and log callbacks can record or route diagnostic events. <br>\nMitigation: Enable tracing or log callbacks only when those diagnostics should be recorded or delivered to the caller's callback. <br>\n\n\n## Reference(s): <br>\n- [Build and link](references/build-and-link.md) <br>\n- [Error handling](references/error-handling.md) <br>\n- [Pointer ownership](references/ownership.md) <br>\n- [Threading](references/threading.md) <br>\n- [Apple Thread Safety Summary](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html) <br>\n- [Apple AXUIElement](https://developer.apple.com/documentation/applicationservices/axuielement) <br>\n- [Apple CGEvent](https://developer.apple.com/documentation/coregraphics/cgevent) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [Guidance, Code, Shell commands, Configuration] <br>\n**Output Format:** [Markdown with inline shell, C, Python, and FFI guidance] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Includes build commands, ABI validation guidance, lifecycle rules, and risk-aware diagnostic handling.] <br>\n\n## Skill Version(s): <br>\n1.0.6 (source: server release metadata; artifact frontmatter lists 0.4.1) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v1.0.5: 7 files, 18443 bytes\n\nFiles: references/build-and-link.md (8809b), references/error-handling.md (7012b), references/ownership.md (8116b), references/threading.md (4869b), skill-card.md (2659b), SKILL.md (10946b), _meta.json (136b)\n\nFile v1.0.5:SKILL.md\n\n---\nname: agent-desktop-ffi\nversion: 0.4.1\ntags: ffi, c-bindings, cdylib, python, swift, node, go, rust-ffi\nrequirements:\n  - agent-desktop-ffi\ndescription: >\n  C-ABI bindings over agent-desktop's PlatformAdapter. Consumers\n  (Python ctypes, Swift, Node ffi-napi, Go cgo, C++, Ruby fiddle)\n  link libagent_desktop_ffi.{dylib,so,dll} and call `ad_*` functions\n  directly instead of spawning the CLI binary per call. The canonical\n  observe-act workflow is: ad_init → ad_adapter_create[_with_session]\n  → ad_snapshot → parse @e refs → ad_execute_by_ref → ad_free_string\n  → ad_adapter_destroy.\n---\n\n# agent-desktop-ffi\n\nDirect C-ABI access to every PlatformAdapter operation. Build the\ncdylib with the workspace's `release-ffi` profile:\n\n```sh\ncargo build --profile release-ffi -p agent-desktop-ffi\n```\n\nThe output is `target/release-ffi/libagent_desktop_ffi.dylib`\n(`.so` on Linux, `.dll` on Windows) plus a committed C header at\n`crates/ffi/include/agent_desktop.h`.\n\nA Python ctypes smoke harness lives at `tests/ffi-python/smoke.py` and\nserves as a worked end-to-end example covering the ABI handshake, struct\nsize validation, `ad_version`, and the snapshot pipeline leg. See\n`tests/ffi-python/README.md` for usage.\n\nFour reference topics, loaded as needed:\n\n- [ownership.md](references/ownership.md) — who allocates / who frees,\n  for every `*mut T` the FFI hands back to the caller.\n- [error-handling.md](references/error-handling.md) — errno-style\n  last-error contract, enum validation, panic boundary.\n- [threading.md](references/threading.md) — macOS main-thread rule,\n  AXIsProcessTrusted inheritance when Python/Node dlopens the cdylib,\n  and the single-owner handle invariant.\n- [build-and-link.md](references/build-and-link.md) — ABI handshake,\n  struct size validation, minimal C and Python examples, observe-act\n  workflow, and prebuilt archive locations.\n\n## Observe-act workflow (canonical path)\n\n```\nad_init(AD_ABI_VERSION_MAJOR)                    // verify header ↔ dylib match\nadapter = ad_adapter_create_with_session(\"s1\")   // or ad_adapter_create()\nrc = ad_snapshot(adapter, \"Finder\", 0, 10, false, false, &json_out)\n// parse json_out: locate @e refs in data.tree, note data.snapshot_id\nad_free_string(json_out)\n// build action:\nAdAction act = {0}; act.kind = AD_ACTION_KIND_CLICK;\nrc = ad_execute_by_ref(adapter, \"@e5\", snapshot_id, &act, 0, &result_out)\nad_free_string(result_out)\nad_adapter_destroy(adapter)\n```\n\n`ad_snapshot` returns a `{version, ok, command, data}` JSON envelope\nidentical to the CLI output. The `data.tree` field contains `@e`-prefixed\nref IDs for interactive elements. Pass a ref ID and the `snapshot_id`\nto `ad_execute_by_ref` to drive the CLI-semantics ref-action pipeline\n(RefStore load → strict resolution → actionability preflight → dispatch).\n\n## Core constraints\n\n- **ABI handshake.** Call `ad_init(AD_ABI_VERSION_MAJOR)` once after loading the\n  dylib. A mismatch between the compiled-in constant and the loaded dylib returns\n  `AD_RESULT_ERR_INVALID_ARGS` — abort rather than proceed. You can also read the\n  raw dylib major via `ad_abi_version()` for diagnostic display. New `ad_*` symbols\n  and new error codes are additive (no bump required); removed or layout-changed\n  symbols increment the major.\n\n- **Session adapters.** `ad_adapter_create_with_session(\"session-id\")` associates\n  the adapter with a session namespace for refmap persistence — the same as CLI\n  `--session <id>`. Passing the same session ID across adapter lifetimes lets\n  `ad_execute_by_ref` with `snapshot_id=NULL` target the latest snapshot from\n  that session. Session IDs: 1–64 chars, ASCII alphanumeric / `-` / `_`.\n  Invalid IDs return null (check `ad_last_error_*`).\n\n- **Structured session trace (no ABI change).** File-based JSONL tracing activates\n  only when the session has a manifest with `trace: on` from `session start`\n  (CLI) or equivalent on-disk setup. `ad_adapter_create_with_session` alone does\n  **not** create trace files. When tracing is active, `command_context()`-backed\n  commands append to one segment per OS process under\n  `~/.agent-desktop/sessions/<id>/trace/<pid>-<procTs>.jsonl`. A long-lived host\n  reuses the same segment filename for all calls in that process. For unstructured\n  diagnostics regardless of session manifest, use `ad_set_log_callback` (below).\n\n- **Main thread only (macOS).** Call every adapter-touching entrypoint\n  (`ad_snapshot`, `ad_execute_by_ref`, `ad_wait`, `ad_get_tree`, `ad_find`,\n  `ad_get`, `ad_is`, `ad_resolve_element`, `ad_execute_action`,\n  `ad_execute_action_with_policy`, `ad_execute_ref_action_with_policy`,\n  `ad_screenshot`, clipboard get/set/clear, mouse, drag, launch, close, focus,\n  window-op, list-apps/windows/surfaces, notification list/dismiss/action)\n  from the process's main thread. macOS accessibility and Cocoa APIs require\n  this. The FFI enforces this at runtime in every build profile — a worker-thread\n  call returns `AD_RESULT_ERR_INTERNAL` with a diagnostic last-error. On\n  non-macOS platforms the check is a compile-time true; there is no runtime cost.\n\n- **Release profile.** `cargo build --release` produces `panic = \"abort\"` —\n  any Rust panic inside an `extern \"C\"` fn will `SIGABRT` the host. Use\n  `--profile release-ffi` to get the correct `panic = \"unwind\"` profile. CI\n  enforces this.\n\n- **Last-error lifetime.** Pointers returned by `ad_last_error_*` remain valid\n  across any number of subsequent *successful* FFI calls on the same thread.\n  Only the next failing call rotates them. Cache the pointer once, read it as\n  many times as you need.\n\n- **ad_last_error_details.** A fourth accessor, `ad_last_error_details()`,\n  returns a borrowed JSON string carrying structured details (e.g. the\n  actionability check report on `ACTION_FAILED`, candidate summaries on\n  `AMBIGUOUS_TARGET`). The details may contain element names, values, and window\n  titles from the user's screen — treat as sensitive diagnostics and avoid routing\n  to shared log surfaces.\n\n- **Handle release.** Every `ad_resolve_element` / `ad_find` result must be\n  released with `ad_free_handle(adapter, &handle)` on the same adapter that\n  produced it, before that adapter is destroyed. On macOS this balances the\n  internal `CFRetain`; on Windows/Linux the call is a no-op but safe to issue.\n  `ad_free_handle` zeroes `handle.ptr` so a follow-up call is a safe no-op.\n\n- **Primary ref-action path.** `ad_execute_by_ref` is the recommended entrypoint\n  for the observe-act loop: it loads the RefStore, looks up the ref in the refmap\n  (STALE_REF on miss), runs strict element re-identification (STALE_REF / AMBIGUOUS_TARGET),\n  runs the live actionability preflight, then dispatches. TypeText and PressKey\n  default to `focus_fallback` policy (matching CLI `type`/`press-key`); all other\n  actions default to `headless`. Pass `AD_POLICY_KIND_HEADED` (2) to opt in to\n  cursor-based fallbacks.\n\n- **Low-level action paths.** `ad_execute_action` (headless, no preflight) and\n  `ad_execute_action_with_policy` are raw escape hatches for callers holding a live\n  `AdNativeHandle` from `ad_resolve_element` / `ad_find`. Use them when you need\n  to bypass the ref-action pipeline. `ad_execute_ref_action_with_policy` accepts a\n  pre-resolved `AdRefEntry` instead of a ref string — useful when you have\n  serialized an entry outside the RefStore pipeline.\n\n- **Ref-action preflight.** `ad_execute_by_ref` and `ad_execute_ref_action_with_policy`\n  both resolve the element strictly and run the live actionability preflight\n  (visible, stable, enabled, supported action, policy, editable) before dispatching\n  — a disabled or unsupported target fails before any platform call. On\n  `AD_RESULT_ERR_ACTION_FAILED`, the structured check report is available as JSON\n  via `ad_last_error_details()`.\n\n- **Action result steps.** `AdActionResult.steps` mirrors the CLI `steps` array\n  for activation-chain actions. Each entry has `label` and `outcome` strings and\n  is owned by the result; release with `ad_free_action_result(&out)`.\n\n- **Tracing / log callback.** Two tracing surfaces coexist:\n\n  1. **Structured file trace** — same JSONL contract as CLI `--trace`, gated by a\n     `trace: on` session manifest. Segments include `event`, `ts_ms`, `seq`, and\n     redacted fields. Requires `session start` (or equivalent manifest on disk)\n     before creating the adapter; plain session-id adapters write nothing to disk.\n\n  2. **`ad_set_log_callback(cb)`** — installs a `tracing` subscriber layer that\n     delivers events as JSON to your callback. `cb` receives an int32_t level\n     (1=ERROR … 5=TRACE) and a `const char *msg` valid only for the duration of\n     the call. Pass `NULL` to unregister. The layer is installed on the first\n     non-null call; if a foreign global subscriber already owns the process at\n     that point, the install fails with `AD_RESULT_ERR_INTERNAL` and no events are\n     ever delivered. Sensitive field values (password, token, text, …) are\n     replaced with `{\"redacted\":true}` before formatting. A panicking callback is\n     caught and silently discarded. The callback may fire from threads other than\n     the registering thread, and may still fire briefly after a `NULL` unregister\n     — keep the callback and any data it captures valid for the process lifetime.\n\n- **Wait.** `ad_wait(adapter, args, &out)` runs the full CLI `wait` command\n  (element-appear, window-appear, text-appear, menu-open/close, notification,\n  element predicates). Zero-initialize `AdWaitArgs`, set the fields you need, and\n  validate the struct size against `AD_WAIT_ARGS_SIZE` / `ad_wait_args_size()` before\n  calling. The output is a `{version, ok, command, data}` JSON envelope freed with\n  `ad_free_string`. `ad_wait` blocks the calling thread up to `timeout_ms` ms —\n  ensure the adapter is not destroyed from another thread while it is running.\n\n- **Text input privacy.** On macOS, focus-fallback or headed text insertion may\n  briefly use the clipboard for non-ASCII text. For sensitive text, prefer\n  `AD_ACTION_KIND_SET_VALUE` with `AD_POLICY_KIND_HEADLESS` when the target\n  supports settable values.\n\n- **Enum discriminants.** Every `#[repr(i32)]` enum field is validated at the C\n  boundary — invalid discriminants return `AD_RESULT_ERR_INVALID_ARGS` instead of\n  undefined behavior.\n\n- **ABI stability.** The major version in `AD_ABI_VERSION_MAJOR` increments on any\n  breaking change (removed symbol, incompatible layout). Additive changes (new\n  symbols, new error codes) do not bump it. Before 1.0, pin the exact version of\n  libagent_desktop_ffi you link against.\n\n- **`ad_get_tree` vs `ad_snapshot`.** `ad_get_tree` returns a raw flat BFS tree\n  without `@e` refs, no refmap persistence, and no JSON envelope — use it for\n  custom traversal or UI inspection. For observe-act agents that drive actions via\n  `ad_execute_by_ref`, always start with `ad_snapshot`.\n\nFile v1.0.5:_meta.json\n\n{\n  \"ownerId\": \"kn7a8wtv8q9jjhpxh4w601zsb5827bnx\",\n  \"slug\": \"agent-desktop-ffi\",\n  \"version\": \"1.0.5\",\n  \"publishedAt\": 1782951217881\n}\n\nFile v1.0.5:references/build-and-link.md\n\n# Build and link\n\n## Building the cdylib\n\n```sh\ncargo build --profile release-ffi -p agent-desktop-ffi\n```\n\nOutput:\n\n- macOS: `target/release-ffi/libagent_desktop_ffi.dylib`\n- Linux: `target/release-ffi/libagent_desktop_ffi.so`\n- Windows: `target/release-ffi/agent_desktop_ffi.dll`\n\nThe generated header is at `crates/ffi/include/agent_desktop.h`. CI\nvalidates that the committed header matches what `cargo build`\nregenerates — if you change a type in `crates/ffi/src/`, rebuild\nlocally and commit the updated header.\n\n`--profile release-ffi` keeps `panic = \"unwind\"`, which is required for\nthe `catch_unwind` traps inside every `extern \"C\"` entrypoint. The default\n`release` profile uses `panic = \"abort\"` (for CLI binary-size reasons) and\nsilently defeats those traps.\n\n## Prebuilt archives\n\nEvery GitHub release ships prebuilt archives for:\n\n- macOS arm64 and x86_64\n- Linux x64 and arm64 (glibc)\n- Windows x64 MSVC\n\nEach archive contains the dylib/so/dll, `include/agent_desktop.h`, and\n`LICENSE`. Integrity: compare against `checksums.txt` in the release\nassets. Supply-chain verification: each release is signed via Sigstore\nattestation — verify with `cosign verify-blob` before deploying.\n\n## ABI handshake (do this first)\n\nAfter `dlopen` / `LoadLibrary`, compare the dylib major to the header you\ncompiled against before calling anything else:\n\n```c\nAdResult rc = ad_init(AD_ABI_VERSION_MAJOR);\nif (rc != AD_RESULT_OK) {\n    // header and dylib have incompatible major versions\n    fprintf(stderr, \"ABI mismatch: %s\\n\", ad_last_error_message());\n    return -1;\n}\n```\n\nAlternatively, read the raw dylib major and compare yourself:\n\n```c\nuint32_t dylib_major = ad_abi_version();\nif (dylib_major != AD_ABI_VERSION_MAJOR) {\n    fprintf(stderr, \"ABI major: header=%u dylib=%u\\n\",\n            AD_ABI_VERSION_MAJOR, dylib_major);\n    abort();\n}\n```\n\n`ad_init` returns `AD_RESULT_ERR_INVALID_ARGS` on mismatch (with a\ndiagnostic in `ad_last_error_message`). A mismatch means the header you\ncompiled against and the loaded dylib are incompatible — do not call\nanything further.\n\n## Struct size validation\n\nLanguages whose struct layout may diverge from C (Python ctypes, Go cgo,\nJNI, etc.) must validate every size-pinned struct before passing it to\nthe library. The FFI exposes three validation layers:\n\n1. **Header macros**: `AD_ACTION_SIZE`, `AD_WAIT_ARGS_SIZE`,\n   `AD_REF_ENTRY_SIZE`, `AD_DRAG_PARAMS_SIZE`, `AD_ACTION_RESULT_SIZE`,\n   `AD_ACTION_STEP_SIZE`, `AD_ELEMENT_STATE_SIZE`.\n2. **Runtime getters**: `ad_action_size()`, `ad_wait_args_size()`,\n   `ad_ref_entry_size()`, `ad_drag_params_size()`, `ad_action_result_size()`,\n   `ad_action_step_size()`, `ad_element_state_size()` — each returns the\n   same value the macro encodes, compiled from the Rust side.\n3. **C11 static asserts** in the header (`#ifndef AGENT_DESKTOP_ABI_ASSERTS`)\n   catch mismatches at C compile time.\n\nCompare your binding's `sizeof` equivalent against the getter at load\ntime, before building or passing any of these structs. The Python smoke\nharness (`tests/ffi-python/smoke.py` Leg 2) demonstrates this check for\nall size-pinned structs in a single loop.\n\n## Worked example: Python ctypes smoke harness\n\n`tests/ffi-python/smoke.py` is the canonical reference for Python\nconsumers. It covers:\n\n- Leg 1: `ad_abi_version()` vs `AD_ABI_VERSION_MAJOR` (header/dylib match)\n- Leg 2: every `ad_*_size()` getter vs its `AD_*_SIZE` macro (struct layout)\n- Leg 3: `ad_version()` → parse JSON → `ad_free_string` (basic pipeline)\n- Leg 4: `ad_adapter_create` → `ad_snapshot` → `ad_free_string` →\n  `ad_adapter_destroy` (full adapter lifecycle, stub passes through\n  `PLATFORM_NOT_SUPPORTED`)\n\nRun it:\n\n```bash\npython3 tests/ffi-python/smoke.py \\\n  target/release-ffi/libagent_desktop_ffi.dylib \\\n  crates/ffi/include/agent_desktop.h\n```\n\nOr with environment variables:\n\n```bash\nAD_DYLIB_PATH=target/release-ffi/libagent_desktop_ffi.dylib \\\nAD_HEADER_PATH=crates/ffi/include/agent_desktop.h \\\npython3 tests/ffi-python/smoke.py\n```\n\nNo `pip install` required — `smoke.py` uses the Python standard library only.\n\n## Minimal C example\n\n```c\n#include <stdio.h>\n#include \"agent_desktop.h\"\n\nint main(void) {\n    // 1. ABI handshake\n    if (ad_init(AD_ABI_VERSION_MAJOR) != AD_RESULT_OK) {\n        fprintf(stderr, \"ABI mismatch: %s\\n\", ad_last_error_message());\n        return 1;\n    }\n\n    // 2. Create adapter\n    AdAdapter *adapter = ad_adapter_create();\n    if (!adapter) {\n        fprintf(stderr, \"adapter_create failed: %s\\n\", ad_last_error_message());\n        return 1;\n    }\n\n    // 3. Check permissions\n    AdResult rc = ad_check_permissions(adapter);\n    if (rc != AD_RESULT_OK) {\n        fprintf(stderr, \"permission denied: %s\\n\", ad_last_error_message());\n        ad_adapter_destroy(adapter);\n        return 1;\n    }\n\n    // 4. Snapshot the focused window\n    char *json_out = NULL;\n    rc = ad_snapshot(adapter,\n                     NULL,   // app (null = focused window)\n                     0,      // surface = Window\n                     10,     // max_depth\n                     false,  // interactive_only\n                     false,  // compact\n                     &json_out);\n    if (rc == AD_RESULT_OK && json_out) {\n        printf(\"%s\\n\", json_out);\n        // parse JSON to find @e refs in data.tree\n        ad_free_string(json_out);\n    } else if (json_out) {\n        // command-level error — ok:false envelope, still must free\n        fprintf(stderr, \"snapshot error: %s\\n\", json_out);\n        ad_free_string(json_out);\n    } else {\n        // infrastructure error — *out is null\n        fprintf(stderr, \"snapshot failed: %s\\n\", ad_last_error_message());\n    }\n\n    ad_adapter_destroy(adapter);\n    return 0;\n}\n```\n\nCompile:\n\n```sh\nclang -I./crates/ffi/include main.c \\\n      -L./target/release-ffi -lagent_desktop_ffi \\\n      -o snapshot_demo\ninstall_name_tool -change \\\n    libagent_desktop_ffi.dylib \\\n    @executable_path/target/release-ffi/libagent_desktop_ffi.dylib \\\n    snapshot_demo\n```\n\n## Observe-act workflow in C\n\nAfter parsing the snapshot JSON and extracting a ref ID (e.g. `\"@e5\"`):\n\n```c\nAdAction act = {0};              // zero-init before setting any field\nact.kind = AD_ACTION_KIND_CLICK;\n\nchar *result = NULL;\nAdResult rc = ad_execute_by_ref(\n    adapter,\n    \"@e5\",        // ref ID from snapshot data.tree\n    NULL,         // snapshot_id — NULL = latest for this session\n    &act,\n    0,            // policy = Headless\n    &result\n);\nif (result) {\n    // parse JSON — ok:true on success, ok:false on STALE_REF etc.\n    printf(\"%s\\n\", result);\n    ad_free_string(result);\n}\nif (rc != AD_RESULT_OK) {\n    const char *det = ad_last_error_details();  // may be NULL; treat as sensitive\n    fprintf(stderr, \"execute_by_ref failed (%d): %s\\n\",\n            (int)rc, ad_last_error_message());\n}\n```\n\nTo type text, set the action kind and text field:\n\n```c\nAdAction type_act = {0};\ntype_act.kind = AD_ACTION_KIND_TYPE_TEXT;\ntype_act.text = \"hello\";\n// TypeText defaults to focus_fallback via ad_execute_by_ref; explicit policy:\nrc = ad_execute_by_ref(adapter, \"@e3\", NULL, &type_act,\n                       AD_POLICY_KIND_FOCUS_FALLBACK, &result);\n```\n\n## Call graph reminder\n\nAll adapter-touching FFI calls must run on the **main thread** on macOS.\nFor Python that typically means the script's entry point, not a worker\nspawned via `threading`. See [threading.md](threading.md).\n\n## Minimal Python ctypes example\n\n```python\nimport ctypes, json\nfrom ctypes import c_int, c_int32, c_uint8, c_bool, c_char_p, POINTER, c_void_p\n\nlib = ctypes.CDLL(\"./target/release-ffi/libagent_desktop_ffi.dylib\")\n\n# ABI handshake\nlib.ad_init.restype = c_int32\nlib.ad_init.argtypes = [ctypes.c_uint32]\nAD_ABI_VERSION_MAJOR = 1  # sync with header macro\nrc = lib.ad_init(AD_ABI_VERSION_MAJOR)\nassert rc == 0, f\"ABI mismatch: rc={rc}\"\n\n# Adapter lifecycle\nlib.ad_adapter_create.restype = c_void_p\nlib.ad_adapter_create.argtypes = []\nlib.ad_adapter_destroy.restype = None\nlib.ad_adapter_destroy.argtypes = [c_void_p]\n\n# Snapshot\nlib.ad_snapshot.restype = c_int32\nlib.ad_snapshot.argtypes = [c_void_p, c_char_p, c_int32, c_uint8, c_bool, c_bool,\n                             POINTER(c_char_p)]\nlib.ad_free_string.restype = None\nlib.ad_free_string.argtypes = [c_char_p]\n\nlib.ad_last_error_message.restype = c_char_p\nlib.ad_last_error_message.argtypes = []\n\nadapter = lib.ad_adapter_create()\nassert adapter, \"ad_adapter_create() returned null\"\n\nout = c_char_p()\nrc = lib.ad_snapshot(adapter, None, 0, 10, False, False, ctypes.byref(out))\nif out.value:\n    envelope = json.loads(out.value)\n    lib.ad_free_string(out)\n    print(\"ok:\", envelope.get(\"ok\"))\nelse:\n    msg = lib.ad_last_error_message()\n    print(\"error:\", msg.decode() if msg else \"(no message)\")\n\nlib.ad_adapter_destroy(adapter)\n```\n\nFile v1.0.5:references/error-handling.md\n\n# Error handling\n\nThe FFI uses an errno-style last-error pattern. Every `AdResult`-returning\nfunction returns `AD_RESULT_OK` (= 0) on success or a negative error\ncode on failure. When a failure occurs, thread-local last-error state is\npopulated; read it with the `ad_last_error_*` accessors.\n\n## Minimal pattern\n\n```c\nAdResult rc = ad_launch_app(adapter, \"com.apple.finder\", 5000, &win);\nif (rc != AD_RESULT_OK) {\n    const char *msg = ad_last_error_message();\n    const char *sug = ad_last_error_suggestion();   // may be NULL\n    fprintf(stderr, \"launch_app failed (%d): %s\\n\", (int)rc, msg ? msg : \"(no message)\");\n    if (sug) fprintf(stderr, \"  suggestion: %s\\n\", sug);\n    // no need to release the struct — out-param was zero-initialized\n    return -1;\n}\n// ...use win...\nad_release_window_fields(&win);\n```\n\n## Last-error accessors\n\nFour accessors share the same per-thread lifetime contract:\n\n| Accessor                        | Returns                                                        |\n|---------------------------------|----------------------------------------------------------------|\n| `ad_last_error_code()`          | The `AdResult` code of the last failure, or `AD_RESULT_OK`    |\n| `ad_last_error_message()`       | Human-readable description, or null                            |\n| `ad_last_error_suggestion()`    | Recovery hint, or null                                         |\n| `ad_last_error_platform_detail()` | OS-specific diagnostic (AX codes, HRESULTs, AT-SPI), or null |\n| `ad_last_error_details()`       | Structured JSON details, or null — **sensitive** (see below)   |\n\n`ad_last_error_details()` returns a JSON string with structured context:\nthe actionability check report on `ACTION_FAILED`, candidate element\nsummaries on `AMBIGUOUS_TARGET`, the last observed state on a `wait`\n`TIMEOUT`, etc. The details may contain element names, values, and window\ntitles from the user's screen. Treat as sensitive diagnostics and avoid\nrouting to shared log surfaces.\n\n## Lifetime contract\n\nThe pointer returned by any `ad_last_error_*` accessor remains valid\nacross any number of subsequent **successful** FFI calls. Only the next\n**failing** call rotates the slot.\n\nConsequence: you can cache the pointer right after a failure and keep\nreading it until the next failure — equivalent to POSIX `errno` /\n`strerror`.\n\n```c\nAdResult rc = ad_some_call(...);\nconst char *msg = ad_last_error_message();   // snapshot\n\nad_check_permissions(adapter);                // success\nad_check_permissions(adapter);                // success\nprintf(\"%s\\n\", msg);                          // still valid\n```\n\nFailure-path calls rotate: if a subsequent call fails, the prior\npointer may dangle. Read it before the next potentially-failing call.\n\nLast-error is per-thread (thread-local storage) — Thread A's failure\ndoes not affect Thread B's slot.\n\n`ad_check_permissions` does not treat `Unknown` as success. Stub adapters\nthat cannot answer permission probes return\n`AD_RESULT_ERR_PLATFORM_NOT_SUPPORTED`. The macOS adapter reports\n`AD_RESULT_ERR_INTERNAL` only if the platform probe itself is ambiguous;\nread `ad_last_error_*` for the diagnostic.\n\n## Error codes\n\nNumeric values are ABI-stable. New codes are appended; existing values\nare not renumbered. Always handle values outside this list — future\nreleases may add codes.\n\n| Name                                  | i32   | Meaning                                    |\n|---------------------------------------|-------|--------------------------------------------|\n| `AD_RESULT_OK`                        |   0   | Success                                    |\n| `AD_RESULT_ERR_PERM_DENIED`           |  -1   | Accessibility / input permission missing   |\n| `AD_RESULT_ERR_ELEMENT_NOT_FOUND`     |  -2   | Ref resolve / find miss                    |\n| `AD_RESULT_ERR_APP_NOT_FOUND`         |  -3   | Bundle/PID lookup miss                     |\n| `AD_RESULT_ERR_ACTION_FAILED`         |  -4   | Action dispatched but rejected             |\n| `AD_RESULT_ERR_ACTION_NOT_SUPPORTED`  |  -5   | Platform cannot perform this action        |\n| `AD_RESULT_ERR_STALE_REF`             |  -6   | Ref predates a UI change; re-snapshot      |\n| `AD_RESULT_ERR_WINDOW_NOT_FOUND`      |  -7   | Window filter matched nothing              |\n| `AD_RESULT_ERR_PLATFORM_NOT_SUPPORTED`|  -8   | API unavailable on this OS                 |\n| `AD_RESULT_ERR_TIMEOUT`               |  -9   | Wait exceeded deadline                     |\n| `AD_RESULT_ERR_INVALID_ARGS`          | -10   | Null pointer, bad enum, invalid UTF-8      |\n| `AD_RESULT_ERR_NOTIFICATION_NOT_FOUND`| -11   | Notification index out of range or reordered |\n| `AD_RESULT_ERR_INTERNAL`              | -12   | Internal failure, off-main-thread, or foreign-subscriber conflict |\n| `AD_RESULT_ERR_SNAPSHOT_NOT_FOUND`    | -13   | Requested snapshot ref store is missing    |\n| `AD_RESULT_ERR_POLICY_DENIED`         | -14   | Current action policy blocks this fallback |\n| `AD_RESULT_ERR_AMBIGUOUS_TARGET`      | -15   | Strict re-identification found multiple candidates; re-snapshot |\n\n## Enum validation\n\nEvery `#[repr(i32)]` enum field is validated at the C boundary. An\nout-of-range discriminant returns `AD_RESULT_ERR_INVALID_ARGS` with\ndiagnostic last-error text. This prevents the consumer from accidentally\ntriggering undefined behavior by stuffing an arbitrary `int32_t` into an\nenum slot. Affected fields: `AdAction.kind` (`AdActionKind`),\n`AdMouseEvent.kind` (`AdMouseEventKind`), `AdMouseEvent.button`\n(`AdMouseButton`), `AdScrollParams.direction` (`AdDirection`),\n`AdTreeOptions.surface` (`AdSnapshotSurface`), `AdScreenshotTarget.kind`\n(`AdScreenshotKind`), `AdWindowOp.kind` (`AdWindowOpKind`), and the\n`policy` parameter of `ad_execute_by_ref` / `ad_execute_action_with_policy`\n/ `ad_execute_ref_action_with_policy` (`AdPolicyKind`).\n\n## Command-backed JSON entrypoints: dual-failure modes\n\n`ad_snapshot`, `ad_execute_by_ref`, `ad_wait`, `ad_status`, and\n`ad_version` have two distinct failure modes:\n\n- **Argument / infrastructure failure** (null adapter, off-main-thread,\n  invalid UTF-8, bad discriminant, context error): `*out` is set to null,\n  no allocation is made, and the last-error slot is the only failure\n  indication.\n- **Command-level failure** (app not found, STALE_REF, TIMEOUT, etc.):\n  `*out` is set to a heap-allocated JSON string with `\"ok\":false` and an\n  `\"error\"` payload. The caller **must still free** it with\n  `ad_free_string(*out)`. The last-error slot is also set.\n\nAlways check `*out` for null before deciding whether to free.\n\n## Panic safety\n\nEvery `extern \"C\"` entrypoint wraps its body in `catch_unwind`. A\nRust panic inside the FFI surfaces as `AD_RESULT_ERR_INTERNAL` with\nmessage `\"rust panic in FFI boundary\"`. No `SIGABRT`, no host crash.\n\nThe cdylib must be built under the `release-ffi` profile for this\nguarantee to hold in optimized builds — the workspace `release` profile\nuses `panic = \"abort\"` (for CLI binary-size reasons).\n\nFile v1.0.5:references/ownership.md\n\n# Pointer ownership\n\nEvery `*mut T` / `*const T` returned by the FFI comes with a matching\nfree function. Always call it; the allocator the FFI uses is Rust's\n`Box::from_raw` / `CString::from_raw`, which cannot be freed with C's\n`free()`.\n\n## Allocation / release table\n\n### Command-backed JSON strings\n\nThese entrypoints write an owned, NUL-terminated JSON envelope into\n`*out`; free with `ad_free_string`. See error-handling.md for the\ndual-failure mode (command-level errors write `ok:false` JSON into\n`*out`; infrastructure errors leave `*out` null with no allocation).\n\n| Allocates                                                                         | Frees with              |\n|-----------------------------------------------------------------------------------|-------------------------|\n| `ad_version(&out)`                                                                | `ad_free_string(out)`   |\n| `ad_status(adapter, &out)`                                                        | `ad_free_string(out)`   |\n| `ad_snapshot(adapter, app, surface, max_depth, interactive_only, compact, &out)` | `ad_free_string(out)`   |\n| `ad_execute_by_ref(adapter, ref_id, snapshot_id, action, policy, &out)`          | `ad_free_string(out)`   |\n| `ad_wait(adapter, args, &out)`                                                    | `ad_free_string(out)`   |\n\n### Adapter lifecycle\n\n| Allocates                                            | Frees with                              |\n|------------------------------------------------------|-----------------------------------------|\n| `ad_adapter_create()`                                | `ad_adapter_destroy(adapter)`           |\n| `ad_adapter_create_with_session(session)`            | `ad_adapter_destroy(adapter)`           |\n\n### Opaque list handles\n\n| Allocates                                                    | Frees with                              |\n|--------------------------------------------------------------|-----------------------------------------|\n| `ad_list_apps(adapter, &list)`                               | `ad_app_list_free(list)`                |\n| `ad_list_windows(adapter, app, focused, &list)`              | `ad_window_list_free(list)`             |\n| `ad_list_surfaces(adapter, pid, &list)`                      | `ad_surface_list_free(list)`            |\n| `ad_list_notifications(adapter, filter, &list)`              | `ad_notification_list_free(list)`       |\n| `ad_dismiss_all_notifications(adapter, f, &ok, &fail)`       | `ad_notification_list_free` on each, or `ad_dismiss_all_notifications_free(ok, fail)` |\n\n### App / window lifecycle\n\n| Allocates                                                 | Frees with                                                          |\n|-----------------------------------------------------------|---------------------------------------------------------------------|\n| `ad_launch_app(adapter, id, timeout, &out)`               | `ad_release_window_fields(&out)` — frees interior strings only; the `AdWindowInfo` struct lives on the caller's stack |\n\n### Raw tree and element access\n\n| Allocates                                                                              | Frees with                              |\n|----------------------------------------------------------------------------------------|-----------------------------------------|\n| `ad_get_tree(adapter, win, opts, &out)`                                                | `ad_free_tree(&out)`                    |\n| `ad_resolve_element(adapter, entry, &handle)`                                          | `ad_free_handle(adapter, &handle)` — zeroes `handle.ptr` so a follow-up call is a no-op |\n| `ad_find(adapter, win, query, &handle)`                                                | same as `ad_resolve_element`            |\n\n### Action results\n\n| Allocates                                                                              | Frees with                   |\n|----------------------------------------------------------------------------------------|------------------------------|\n| `ad_execute_action(adapter, handle, action, &out)`                                     | `ad_free_action_result(&out)` |\n| `ad_execute_action_with_policy(adapter, handle, action, policy, &out)`                 | `ad_free_action_result(&out)` |\n| `ad_execute_ref_action_with_policy(adapter, entry, action, policy, &out)`              | `ad_free_action_result(&out)` |\n| `ad_notification_action(adapter, idx, expected_app, expected_title, name, &out)` — pass the `app_name`/`title` from `ad_list_notifications` (either may be null) so NC reorder between list and press returns `ERR_NOTIFICATION_NOT_FOUND` instead of pressing a different notification | `ad_free_action_result(&out)` |\n\n### Clipboard and image buffers\n\n| Allocates                                   | Frees with                                                          |\n|---------------------------------------------|---------------------------------------------------------------------|\n| `ad_get_clipboard(adapter, &text)`          | `ad_free_string(text)`                                              |\n| `ad_get(adapter, handle, property, &text)`  | `ad_free_string(text)` — text may be null when the property is absent; `ad_free_string(NULL)` is a no-op |\n| `ad_screenshot(adapter, target, &buf)`      | `ad_image_buffer_free(buf)` (buf is opaque; read via `ad_image_buffer_{data,size,width,height,format}`) |\n\n## Rules\n\n- Every free function is **null-tolerant**. `ad_free_tree(NULL)`,\n  `ad_free_handle(adapter, NULL)`, `ad_free_string(NULL)`, etc. are\n  no-ops. List accessors (`ad_*_list_count`, `_get`) also accept null\n  and return `0` / `NULL` respectively.\n- **Double-free of list handles and `AdImageBuffer` is undefined.** The\n  opaque wrappers are allocated by `Box::into_raw`; the second call\n  would invoke `Box::from_raw` on a freed allocation. Always set the\n  pointer to `NULL` after freeing.\n- **`ad_free_handle` is safe to double-call** — it zeroes `handle.ptr`\n  after the platform release, so a follow-up call sees `NULL` and\n  returns `AD_RESULT_OK` without re-entering `CFRelease`.\n- **Adapters must outlive their handles.** Free every handle with the\n  same adapter that produced it before calling `ad_adapter_destroy`.\n  Destroying the adapter first and later freeing its handles is\n  undefined behavior.\n- Pointers inside a struct (`.id`, `.title`, `.app_name`, each\n  `AdNotificationInfo.body`, etc.) are freed by the struct's owning\n  free function (list_free / release_fields) — do not call\n  `ad_free_string()` on them individually.\n- `AdActionResult` owns `action`, `ref_id`, `post_state`,\n  `post_state.states`, `steps`, and each `steps[i].label` /\n  `steps[i].outcome`; free all of them only through\n  `ad_free_action_result(&out)`. Treat the returned counts as\n  read-only metadata; release the unmodified result struct.\n- Ownership does **not** transfer back to Rust after you free. Keep a\n  local `NULL` to prevent accidental reuse.\n\n## Out-param zeroing\n\nEvery fallible FFI function zeroes its out-param **before** any guard\n(pointer validation, main-thread check, UTF-8 validation). On error,\ncalling the paired free function is safe: all pointers inside are\nguaranteed null, all counts zero, so the free is a no-op rather than\na double-free on a previous caller's allocation.\n\nIn particular:\n\n- `ad_get_clipboard` writes `*out = NULL` before the adapter call —\n  no stale buffer visible on error.\n- `ad_launch_app` writes `*out = zeroed AdWindowInfo` before the\n  platform call — `ad_release_window_fields(&out)` on the zero-init\n  struct is a no-op.\n- `ad_screenshot` writes `*out = NULL` before allocating the image\n  buffer — no stale pointer when the screenshot fails.\n- `ad_snapshot`, `ad_execute_by_ref`, `ad_wait`, `ad_status`,\n  `ad_version` write `*out = NULL` before any guard, so on\n  infrastructure failures no allocation is made and `ad_free_string(NULL)`\n  is a safe no-op.\n- `ad_*_list` and `ad_resolve_element` / `ad_find` all apply the same\n  pattern to their handle / list out-params.\n\nFile v1.0.5:references/threading.md\n\n# Threading\n\n## macOS: main-thread rule\n\nEvery adapter-touching entrypoint **must be invoked on the process's\nmain thread**. macOS accessibility and Cocoa APIs require this.\n\nEntrypoints subject to the main-thread guard:\n\n- `ad_snapshot`, `ad_execute_by_ref`, `ad_wait`\n- `ad_get_tree`, `ad_find`, `ad_get`, `ad_is`, `ad_resolve_element`\n- `ad_execute_action`, `ad_execute_action_with_policy`,\n  `ad_execute_ref_action_with_policy`\n- `ad_screenshot`\n- Clipboard: `ad_get_clipboard`, `ad_set_clipboard`, `ad_clear_clipboard`\n- Input: `ad_mouse_event`, `ad_drag`\n- App / window: `ad_launch_app`, `ad_close_app`, `ad_focus_window`,\n  `ad_window_op`\n- Listing: `ad_list_apps`, `ad_list_windows`, `ad_list_surfaces`\n- Notifications: `ad_list_notifications`, `ad_dismiss_notification`,\n  `ad_dismiss_all_notifications`, `ad_notification_action`\n\nThe check runs at **runtime, in every build profile** — worker-thread\ncalls return `AD_RESULT_ERR_INTERNAL` with a `'static` diagnostic\n`\"agent_desktop FFI entry called off the main thread (macOS requires\nmain-thread AX/Cocoa calls)\"`. No build-config difference; no silent\nUB window in release builds.\n\nOn non-macOS targets the check is a compile-time `true` and has zero\nruntime cost.\n\n## Operations safe off the main thread\n\nThese functions carry no runtime main-thread guard:\n\n- `ad_adapter_create` / `ad_adapter_create_with_session` / `ad_adapter_destroy`\n- `ad_init`, `ad_abi_version`\n- `ad_version` (no adapter; pure serialization)\n- `ad_status` (reads permission report and ref-store metadata only; no AX tree query)\n- `ad_check_permissions` (pure process-wide AX trust query)\n- `ad_set_log_callback`\n- `ad_last_error_code`, `ad_last_error_message`, `ad_last_error_suggestion`,\n  `ad_last_error_platform_detail`, `ad_last_error_details`\n- List accessors: `ad_app_list_count` / `_get` / `_free`,\n  `ad_window_list_count` / `_get` / `_free`,\n  `ad_surface_list_count` / `_get` / `_free`,\n  `ad_notification_list_count` / `_get` / `_free`\n- `ad_image_buffer_data` / `_size` / `_width` / `_height` / `_format` / `_free`\n- `ad_release_window_fields`\n- `ad_free_handle` (invokes `CFRelease` which is thread-safe — but\n  still prefer calling from the thread that produced the handle)\n- `ad_free_tree`, `ad_free_action_result`, `ad_free_string`\n\n## Log callback threading\n\n`ad_set_log_callback(cb)` is safe to call from any thread (no AX\ninvolvement). However, the installed callback may be invoked from threads\nother than the registering thread — tracing events can originate from any\nthread that calls an `ad_*` function.\n\nA callback unregistered via `NULL` may still receive one or more\ninvocations from a concurrent thread for a brief window after this call\nreturns. The callback (and any data it captures) must remain valid for the\nprocess lifetime, or the caller must quiesce all active adapter calls\nbefore unregistering.\n\n## Python consumers\n\nCPython's GIL serializes calls but does not pin them to the main thread.\nIf you're calling from anything other than the main interpreter thread\nyou will silently corrupt state on macOS.\n\nTwo patterns work:\n\n1. **Restrict FFI calls to the main thread.** Use `asyncio` with the\n   default event loop pinned to main, or a synchronous entrypoint only.\n2. **Marshal across threads yourself.** Use a queue; have a dedicated\n   main-thread worker that dequeues and invokes the FFI.\n\n## AXIsProcessTrusted inheritance\n\n`ad_check_permissions` calls macOS's `AXIsProcessTrusted()`, which\nreturns the trust status of the **hosting executable** — i.e., the\n`python3` / `node` / `swift` process, not `agent-desktop` itself.\n\nConsequence: granting accessibility permission to one Python script's\nPython interpreter grants it to every Python script that dlopens\n`libagent_desktop_ffi.dylib`. Document this prominently for your\nconsumers; consider requiring opt-in permission prompts in host code\nrather than relying on macOS's binary-level grant.\n\n## Thread-ownership of handles\n\n`ad_resolve_element` and `ad_find` return an opaque `AdNativeHandle`\nthat wraps a platform pointer. The handle is **single-owner,\nsingle-thread** by FFI contract:\n\n- Create it on thread A → free it on thread A with `ad_free_handle`.\n  Transferring the handle to thread B is undefined behavior.\n- Use it in FFI calls only from the same thread that produced it.\n\n## Last-error is thread-local\n\nEvery thread has its own last-error slot. Thread A's failure does not\nset thread B's last-error; `ad_last_error_*` accessors always see the\ncalling thread's state.\n\n## ad_wait lifecycle\n\n`ad_wait` blocks the calling thread for up to `timeout_ms` milliseconds\nwhile it holds a live reference into the adapter's allocation. Do not\ncall `ad_adapter_destroy` on the same handle from another thread while\n`ad_wait` is running — that is a use-after-free. Ensure the wait has\nreturned before destroying the adapter.\n\nFile v1.0.5:skill-card.md\n\n## Description: <br>\nAgent Desktop Ffi provides C ABI bindings over agent-desktop's PlatformAdapter so host languages can observe desktop UI state and invoke ad_* actions through a linked shared library. <br>\n\nThis skill is ready for commercial/non-commercial use. <br>\n\n## Publisher: <br>\n[lahfir](https://clawhub.ai/user/lahfir) <br>\n\n### License/Terms of Use: <br>\nMIT-0 <br>\n\n\n## Use Case: <br>\nDevelopers and automation engineers use this skill to integrate agent-desktop desktop observation and action workflows into Python, Swift, Node, Go, C++, Ruby, or other FFI-capable host programs. <br>\n\n### Deployment Geography for Use: <br>\nGlobal <br>\n\n## Known Risks and Mitigations: <br>\nRisk: The linked library can inspect desktop state and perform input actions in applications controlled by the trusted host process. <br>\nMitigation: Use it only in trusted host programs and require explicit user confirmation before actions that click, type, submit forms, or otherwise change application state. <br>\nRisk: Snapshots, screenshots, clipboard activity, and detailed errors can expose window titles, element values, or other sensitive desktop context. <br>\nMitigation: Avoid sending raw snapshots, screenshots, clipboard contents, and ad_last_error_details output to shared logs or untrusted storage. <br>\nRisk: Incorrect FFI use can crash or corrupt the host process through ABI mismatches, wrong struct layouts, off-main-thread macOS calls, or incorrect pointer ownership. <br>\nMitigation: Run ad_init and struct-size checks before use, call macOS adapter entrypoints on the main thread, and release every returned pointer or handle with its documented matching free function. <br>\n\n\n## Reference(s): <br>\n- [Build and link](references/build-and-link.md) <br>\n- [Error handling](references/error-handling.md) <br>\n- [Pointer ownership](references/ownership.md) <br>\n- [Threading](references/threading.md) <br>\n\n\n## Skill Output: <br>\n**Output Type(s):** [guidance, code, shell commands, configuration] <br>\n**Output Format:** [Markdown with inline C, Python, and shell code examples] <br>\n**Output Parameters:** [1D] <br>\n**Other Properties Related to Output:** [Includes ABI, memory ownership, error handling, threading, logging, and desktop action safety guidance.] <br>\n\n## Skill Version(s): <br>\n1.0.5 (source: server release metadata; artifact frontmatter says 0.4.1) <br>\n\n## Ethical Considerations: <br>\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment. <br>\n\nArchive v1.0.4: 6 files, 16561 bytes\n\nFiles: references/build-and-link.md (8809b), references/error-handling.md (7012b), references/ownership.md (8116b), references/threading.md (4869b), SKILL.md (9878b), _meta.json (136b)\n\nFile v1.0.4:SKILL.md\n\n---\nname: agent-desktop-ffi\nversion: 0.4.0\ntags: ffi, c-bindings, cdylib, python, swift, node, go, rust-ffi\nrequirements:\n  - agent-desktop-ffi\ndescription: >\n  C-ABI bindings over agent-desktop's PlatformAdapter. Consumers\n  (Python ctypes, Swift, Node ffi-napi, Go cgo, C++, Ruby fiddle)\n  link libagent_desktop_ffi.{dylib,so,dll} and call `ad_*` functions\n  directly instead of spawning the CLI binary per call. The canonical\n  observe-act workflow is: ad_init → ad_adapter_create[_with_session]\n  → ad_snapshot → parse @e refs → ad_execute_by_ref → ad_free_string\n  → ad_adapter_destroy.\n---\n\n# agent-desktop-ffi\n\nDirect C-ABI access to every PlatformAdapter operation. Build the\ncdylib with the workspace's `release-ffi` profile:\n\n```sh\ncargo build --profile release-ffi -p agent-desktop-ffi\n```\n\nThe output is `target/release-ffi/libagent_desktop_ffi.dylib`\n(`.so` on Linux, `.dll` on Windows) plus a committed C header at\n`crates/ffi/include/agent_desktop.h`.\n\nA Python ctypes smoke harness lives at `tests/ffi-python/smoke.py` and\nserves as a worked end-to-end example covering the ABI handshake, struct\nsize validation, `ad_version`, and the snapshot pipeline leg. See\n`tests/ffi-python/README.md` for usage.\n\nFour reference topics, loaded as needed:\n\n- [ownership.md](references/ownership.md) — who allocates / who frees,\n  for every `*mut T` the FFI hands back to the caller.\n- [error-handling.md](references/error-handling.md) — errno-style\n  last-error contract, enum validation, panic boundary.\n- [threading.md](references/threading.md) — macOS main-thread rule,\n  AXIsProcessTrusted inheritance when Python/Node dlopens the cdylib,\n  and the single-owner handle invariant.\n- [build-and-link.md](references/build-and-link.md) — ABI handshake,\n  struct size validation, minimal C and Python examples, observe-act\n  workflow, and prebuilt archive locations.\n\n## Observe-act workflow (canonical path)\n\n```\nad_init(AD_ABI_VERSION_MAJOR)                    // verify header ↔ dylib match\nadapter = ad_adapter_create_with_session(\"s1\")   // or ad_adapter_create()\nrc = ad_snapshot(adapter, \"Finder\", 0, 10, false, false, &json_out)\n// parse json_out: locate @e refs in data.tree, note data.snapshot_id\nad_free_string(json_out)\n// build action:\nAdAction act = {0}; act.kind = AD_ACTION_KIND_CLICK;\nrc = ad_execute_by_ref(adapter, \"@e5\", snapshot_id, &act, 0, &result_out)\nad_free_string(result_out)\nad_adapter_destroy(adapter)\n```\n\n`ad_snapshot` returns a `{version, ok, command, data}` JSON envelope\nidentical to the CLI output. The `data.tree` field contains `@e`-prefixed\nref IDs for interactive elements. Pass a ref ID and the `snapshot_id`\nto `ad_execute_by_ref` to drive the CLI-semantics ref-action pipeline\n(RefStore load → strict resolution → actionability preflight → dispatch).\n\n## Core constraints\n\n- **ABI handshake.** Call `ad_init(AD_ABI_VERSION_MAJOR)` once after loading the\n  dylib. A mismatch between\n\nArchive v1.0.3: 7 files, 11744 bytes\n\nFiles: references/build-and-link.md (2766b), references/error-handling.md (4306b), references/ownership.md (5223b), references/threading.md (3181b), skill-card.md (2573b), SKILL.md (5658b), _meta.json (136b)\n\nArchive v1.0.2: 7 files, 11878 bytes\n\nFiles: references/build-and-link.md (2766b), references/error-handling.md (4306b), references/ownership.md (5223b), references/threading.md (3181b), skill-card.md (2873b), SKILL.md (5659b), _meta.json (136b)\n\nArchive v1.0.1: 7 files, 10974 bytes\n\nFiles: references/build-and-link.md (2766b), references/error-handling.md (4306b), references/ownership.md (4457b), references/threading.md (3181b), skill-card.md (3036b), SKILL.md (4097b), _meta.json (136b)\n\nArchive v1.0.0: 6 files, 8912 bytes\n\nFiles: references/build-and-link.md (2766b), references/error-handling.md (3726b), references/ownership.md (4457b), references/threading.md (3181b), SKILL.md (3498b), _meta.json (136b)","readmeExcerpt":"Skill: agent-desktop-ffi Owner: lahfir Summary: 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_b","codeSnippets":[],"executableExamples":[{"language":"sh","snippet":"cargo build --profile release-ffi -p agent-desktop-ffi"},{"language":"text","snippet":"ad_init(AD_ABI_VERSION_MAJOR)                    // verify header ↔ dylib match\nadapter = ad_adapter_create_with_session(\"s1\")   // or ad_adapter_create()\nrc = ad_snapshot(adapter, \"Finder\", 0, 10, false, false, &json_out)\n// parse json_out: locate snapshot-qualified refs in data.tree\nad_free_string(json_out)\n// build action:\nAdAction act = {0}; act.kind = AD_ACTION_KIND_CLICK;\nrc = ad_execute_by_ref(adapter, \"@s8f3k2p9:e5\", NULL, &act, 0, &result_out)\nad_free_string(result_out)\nad_adapter_destroy(adapter)"},{"language":"sh","snippet":"cargo build --profile release-ffi -p agent-desktop-ffi"},{"language":"c","snippet":"AdResult rc = ad_init(AD_ABI_VERSION_MAJOR);\nif (rc != AD_RESULT_OK) {\n    // header and dylib have incompatible major versions\n    fprintf(stderr, \"ABI mismatch: %s\\n\", ad_last_error_message());\n    return -1;\n}"},{"language":"c","snippet":"uint32_t dylib_major = ad_abi_version();\nif (dylib_major != AD_ABI_VERSION_MAJOR) {\n    fprintf(stderr, \"ABI major: header=%u dylib=%u\\n\",\n            AD_ABI_VERSION_MAJOR, dylib_major);\n    abort();\n}"},{"language":"bash","snippet":"python3 tests/ffi-python/smoke.py \\\n  target/release-ffi/libagent_desktop_ffi.dylib \\\n  crates/ffi/include/agent_desktop.h"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: agent-desktop-ffi\nversion: 0.4.1\ntags: ffi, c-bindings, cdylib, python, swift, node, go, rust-ffi\nrequirements:\n  - agent-desktop-ffi\ndescription: >\n  C-ABI bindings over agent-desktop's PlatformAdapter. Consumers\n  (Python ctypes, Swift, Node ffi-napi, Go cgo, C++, Ruby fiddle)\n  link libagent_desktop_ffi.{dylib,so,dll} and call `ad_*` functions\n  directly instead of spawning the CLI binary per call. The canonical\n  observe-act workflow is: ad_init → ad_adapter_create[_with_session]\n  → ad_snapshot → parse @e refs → ad_execute_by_ref → ad_free_string\n  → ad_adapter_destroy.\n---\n\n# agent-desktop-ffi\n\nDirect C-ABI access to every PlatformAdapter operation. Build the\ncdylib with the workspace's `release-ffi` profile:\n\n```sh\ncargo build --profile release-ffi -p agent-desktop-ffi\n```\n\nThe output is `target/release-ffi/libagent_desktop_ffi.dylib`\n(`.so` on Linux, `.dll` on Windows) plus a committed C header at\n`crates/ffi/include/agent_desktop.h`.\n\nA Python ctypes smoke harness lives at `tests/ffi-python/smoke.py` and\nserves as a worked end-to-end example covering the ABI handshake, struct\nsize validation, `ad_version`, and the snapshot pipeline leg. See\n`tests/ffi-python/README.md` for usage.\n\nFour reference topics, loaded as needed:\n\n- [ownership.md](references/ownership.md) — who allocates / who frees,\n  for every `*mut T` the FFI hands back to the caller.\n- [error-handling.md](references/error-handling.md) — errno-style\n  last-error contract, enum validation, panic boundary.\n- [threading.md](references/threading.md) — host-thread contract,\n  cross-process mutation serialization, AXIsProcessTrusted inheritance,\n  and adapter-bound native handles.\n- [build-and-link.md](references/build-and-link.md) — ABI handshake,\n  struct size validation, minimal C and Python examples, observe-act\n  workflow, and prebuilt archive locations.\n\n## Observe-act workflow (canonical path)\n\n```\nad_init(AD_ABI_VERSION_MAJOR)                    // verify header ↔ dylib match\nadapter = ad_adapter_create_with_session(\"s1\")   // or ad_adapter_create()\nrc = ad_snapshot(adapter, \"Finder\", 0, 10, false, false, &json_out)\n// parse json_out: locate snapshot-qualified refs in data.tree\nad_free_string(json_out)\n// build action:\nAdAction act = {0}; act.kind = AD_ACTION_KIND_CLICK;\nrc = ad_execute_by_ref(adapter, \"@s8f3k2p9:e5\", NULL, &act, 0, &result_out)\nad_free_string(result_out)\nad_adapter_destroy(adapter)\n```\n\n`ad_snapshot` returns a `{version, ok, command, data}` JSON envelope\nidentical to the CLI output. The `data.tree` field contains snapshot-qualified\nref IDs for interactive elements. Pass a qualified ref, or a legacy bare ref\nplus its explicit `snapshot_id`, to `ad_execute_by_ref` to drive the pipeline\n(RefStore load → strict resolution → actionability preflight → dispatch).\n\n## Core constraints\n\n- **ABI handshake.** Call `ad_init(AD_ABI_VERSION_MAJOR)` once after loading the\n  dylib. A mismatch between the compiled-in constant and the loaded dylib returns\n  `AD_RES"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn7a8wtv8q9jjhpxh4w601zsb5827bnx\",\n  \"slug\": \"agent-desktop-ffi\",\n  \"version\": \"1.0.7\",\n  \"publishedAt\": 1787877342860\n}"},{"path":"references/build-and-link.md","content":"# Build and link\n\n## Building the cdylib\n\n```sh\ncargo build --profile release-ffi -p agent-desktop-ffi\n```\n\nOutput:\n\n- macOS: `target/release-ffi/libagent_desktop_ffi.dylib`\n- Linux: `target/release-ffi/libagent_desktop_ffi.so`\n- Windows: `target/release-ffi/agent_desktop_ffi.dll`\n\nThe generated header is at `crates/ffi/include/agent_desktop.h`. CI\nvalidates that the committed header matches what `cargo build`\nregenerates — if you change a type in `crates/ffi/src/`, rebuild\nlocally and commit the updated header.\n\n`--profile release-ffi` keeps `panic = \"unwind\"`, which is required for\nthe `catch_unwind` traps inside every `extern \"C\"` entrypoint. The default\n`release` profile uses `panic = \"abort\"` (for CLI binary-size reasons) and\nsilently defeats those traps.\n\n## Prebuilt archives\n\nEvery GitHub release ships prebuilt archives for:\n\n- macOS arm64 and x86_64\n- Linux x64 and arm64 (glibc)\n- Windows x64 MSVC\n\nEach archive contains the dylib/so/dll, `include/agent_desktop.h`, and\n`LICENSE`. Integrity: compare against `checksums.txt` in the release\nassets. Supply-chain verification: each release is signed via Sigstore\nattestation — verify with `cosign verify-blob` before deploying.\n\n## ABI handshake (do this first)\n\nAfter `dlopen` / `LoadLibrary`, compare the dylib major to the header you\ncompiled against before calling anything else:\n\n```c\nAdResult rc = ad_init(AD_ABI_VERSION_MAJOR);\nif (rc != AD_RESULT_OK) {\n    // header and dylib have incompatible major versions\n    fprintf(stderr, \"ABI mismatch: %s\\n\", ad_last_error_message());\n    return -1;\n}\n```\n\nAlternatively, read the raw dylib major and compare yourself:\n\n```c\nuint32_t dylib_major = ad_abi_version();\nif (dylib_major != AD_ABI_VERSION_MAJOR) {\n    fprintf(stderr, \"ABI major: header=%u dylib=%u\\n\",\n            AD_ABI_VERSION_MAJOR, dylib_major);\n    abort();\n}\n```\n\n`ad_init` returns `AD_RESULT_ERR_INVALID_ARGS` on mismatch (with a\ndiagnostic in `ad_last_error_message`). A mismatch means the header you\ncompiled against and the loaded dylib are incompatible — do not call\nanything further.\n\n## Struct size validation\n\nLanguages whose struct layout may diverge from C (Python ctypes, Go cgo,\nJNI, etc.) must validate every size-pinned struct before passing it to\nthe library. The FFI exposes three validation layers:\n\n1. **Header macros**: `AD_ACTION_SIZE`, `AD_WAIT_ARGS_SIZE`,\n   `AD_REF_ENTRY_SIZE`, `AD_DRAG_PARAMS_SIZE`, `AD_ACTION_RESULT_SIZE`,\n   `AD_ACTION_STEP_SIZE`, `AD_ELEMENT_STATE_SIZE`.\n2. **Runtime getters**: `ad_action_size()`, `ad_wait_args_size()`,\n   `ad_ref_entry_size()`, `ad_drag_params_size()`, `ad_action_result_size()`,\n   `ad_action_step_size()`, `ad_element_state_size()` — each returns the\n   same value the macro encodes, compiled from the Rust side.\n3. **C11 static asserts** in the header (`#ifndef AGENT_DESKTOP_ABI_ASSERTS`)\n   catch mismatches at C compile time.\n\nCompare your binding's `sizeof` equivalent against the getter at load\ntime, before building or passing any of thes"},{"path":"references/error-handling.md","content":"# Error handling\n\nThe FFI uses an errno-style last-error pattern. Every `AdResult`-returning\nfunction returns `AD_RESULT_OK` (= 0) on success or a negative error\ncode on failure. When a failure occurs, thread-local last-error state is\npopulated; read it with the `ad_last_error_*` accessors.\n\n## Minimal pattern\n\n```c\nAdResult rc = ad_launch_app(adapter, \"com.apple.finder\", 5000, &win);\nif (rc != AD_RESULT_OK) {\n    const char *msg = ad_last_error_message();\n    const char *sug = ad_last_error_suggestion();   // may be NULL\n    fprintf(stderr, \"launch_app failed (%d): %s\\n\", (int)rc, msg ? msg : \"(no message)\");\n    if (sug) fprintf(stderr, \"  suggestion: %s\\n\", sug);\n    // no need to release the struct — out-param was zero-initialized\n    return -1;\n}\n// ...use win...\nad_release_window_fields(&win);\n```\n\n## Last-error accessors\n\nFour accessors share the same per-thread lifetime contract:\n\n| Accessor                        | Returns                                                        |\n|---------------------------------|----------------------------------------------------------------|\n| `ad_last_error_code()`          | The `AdResult` code of the last failure, or `AD_RESULT_OK`    |\n| `ad_last_error_message()`       | Human-readable description, or null                            |\n| `ad_last_error_suggestion()`    | Recovery hint, or null                                         |\n| `ad_last_error_platform_detail()` | OS-specific diagnostic (AX codes, HRESULTs, AT-SPI), or null |\n| `ad_last_error_details()`       | Structured JSON details, or null — **sensitive** (see below)   |\n\n`ad_last_error_details()` returns a JSON string with structured context:\nthe actionability check report on `ACTION_FAILED`, candidate element\nsummaries on `AMBIGUOUS_TARGET`, the last observed state on a `wait`\n`TIMEOUT`, etc. The details may contain element names, values, and window\ntitles from the user's screen. Treat as sensitive diagnostics and avoid\nrouting to shared log surfaces.\n\n## Lifetime contract\n\nThe pointer returned by any `ad_last_error_*` accessor remains valid\nacross any number of subsequent **successful** FFI calls. Only the next\n**failing** call rotates the slot.\n\nConsequence: you can cache the pointer right after a failure and keep\nreading it until the next failure — equivalent to POSIX `errno` /\n`strerror`.\n\n```c\nAdResult rc = ad_some_call(...);\nconst char *msg = ad_last_error_message();   // snapshot\n\nad_check_permissions(adapter);                // success\nad_check_permissions(adapter);                // success\nprintf(\"%s\\n\", msg);                          // still valid\n```\n\nFailure-path calls rotate: if a subsequent call fails, the prior\npointer may dangle. Read it before the next potentially-failing call.\n\nLast-error is per-thread (thread-local storage) — Thread A's failure\ndoes not affect Thread B's slot.\n\n`ad_check_permissions` does not treat `Unknown` as success. Stub adapters\nthat cannot answer permission probes return\n`AD_RESULT_ERR_PLATF"},{"path":"references/ownership.md","content":"# Pointer ownership\n\nEvery `*mut T` / `*const T` returned by the FFI comes with a matching\nfree function. Always call it; the allocator the FFI uses is Rust's\n`Box::from_raw` / `CString::from_raw`, which cannot be freed with C's\n`free()`.\n\n## Allocation / release table\n\n### Command-backed JSON strings\n\nThese entrypoints write an owned, NUL-terminated JSON envelope into\n`*out`; free with `ad_free_string`. See error-handling.md for the\ndual-failure mode (command-level errors write `ok:false` JSON into\n`*out`; infrastructure errors leave `*out` null with no allocation).\n\n| Allocates                                                                         | Frees with              |\n|-----------------------------------------------------------------------------------|-------------------------|\n| `ad_version(&out)`                                                                | `ad_free_string(out)`   |\n| `ad_status(adapter, &out)`                                                        | `ad_free_string(out)`   |\n| `ad_snapshot(adapter, app, surface, max_depth, interactive_only, compact, &out)` | `ad_free_string(out)`   |\n| `ad_execute_by_ref(adapter, ref_id, snapshot_id, action, policy, &out)`          | `ad_free_string(out)`   |\n| `ad_wait(adapter, args, &out)`                                                    | `ad_free_string(out)`   |\n\n### Adapter lifecycle\n\n| Allocates                                            | Frees with                              |\n|------------------------------------------------------|-----------------------------------------|\n| `ad_adapter_create()`                                | `ad_adapter_destroy(adapter)`           |\n| `ad_adapter_create_with_session(session)`            | `ad_adapter_destroy(adapter)`           |\n\n### Opaque list handles\n\n| Allocates                                                    | Frees with                              |\n|--------------------------------------------------------------|-----------------------------------------|\n| `ad_list_apps(adapter, &list)`                               | `ad_app_list_free(list)`                |\n| `ad_list_displays(adapter, &list)`                           | `ad_display_list_free(list)`            |\n| `ad_list_windows(adapter, app, focused, &list)`              | `ad_window_list_free(list)`             |\n| `ad_list_windows_exact(adapter, app, focused, &list)`        | `ad_exact_window_list_free(list)`       |\n| `ad_list_surfaces(adapter, pid, &list)`                      | `ad_surface_list_free(list)`            |\n| `ad_list_surfaces_exact(adapter, pid, &list)`                | `ad_exact_surface_list_free(list)`      |\n| `ad_list_notifications(adapter, filter, &list)`              | `ad_notification_list_free(list)`       |\n| `ad_dismiss_all_notifications(adapter, f, &ok, &fail)`       | `ad_notification_list_free` on each, or `ad_dismiss_all_notifications_free(ok, fail)` |\n\n### App / window lifecycle\n\n| Allocates                                         "}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":1800,"uniquenessScore":41,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T10:38:55.243Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-10T10:38:55.243Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T13:31:58.328Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}