mirror of
https://github.com/lahfir/agent-desktop.git
synced 2026-08-05 13:40:17 +00:00
Lands the full agent-desktop FFI layer with every PR #22 review finding resolved, modular refactor applied, todo-resolve batches (011 + 006) closed, and a production release pipeline that bundles the prebuilt cdylib for 5 target triples alongside the CLI on every GitHub Release. **FFI surface** - Panic-unwind cdylib boundary via `trap_panic` / `trap_panic_ptr` / `trap_panic_const_ptr` / `trap_panic_void` - Runtime main-thread enforcement on every macOS-sensitive entrypoint; TLS errno-style last-error lifetime - Every `#[repr(i32)]` field validated at the C boundary via `try_from_c_enum!` — arbitrary bit patterns return `ErrInvalidArgs` without UB - BFS flat-tree layout with `child_start` / `child_count`; iterative traversal - Opaque list handles (`AdAppList`, `AdWindowList`, `AdSurfaceList`, `AdNotificationList`); opaque `AdImageBuffer` with `_data` / `_size` / `_width` / `_height` / `_format` accessors - `AdNativeHandle` single-owner single-thread contract; zero-on-free makes double-call deterministic - Fail-closed UTF-8 for optional filter pointers (`try_c_to_string` tri-state) - `AdTreeOptions` fully honored in `ad_get_tree` (include_bounds / interactive_only / compact) - Verified notification action identity (`NotificationIdentity` fingerprint) — refuses to press if NC reordered between list and act **Release pipeline** - New `build-ffi` matrix in `.github/workflows/release.yml` producing `libagent_desktop_ffi.{dylib,so,dll}` tarballs for aarch64/x86_64-apple-darwin, x86_64/aarch64-unknown-linux-gnu, x86_64-pc-windows-msvc - macOS `install_name = @rpath/libagent_desktop_ffi.dylib` baked in by `build.rs` and CI-verified via `otool -D` - `actions/attest-build-provenance@v4.1.0` — keyless Sigstore provenance over every release artifact; `gh attestation verify` - `checksums.txt` covers both CLI and FFI assets; asset-count assertion bumped 3 → 8 - npm package stays CLI-only; Python/Swift/Go/Ruby/Node/C hosts pull dylib tarball directly from the Release - README gains a "Language bindings (FFI)" section with platform→artifact table **Docs** - `skills/agent-desktop-ffi/` — SKILL.md + build-and-link.md + ownership.md + threading.md + error-handling.md - `docs/solutions/best-practices/` — two new solution docs (deterministic build-artifact marker, identity fingerprint against OS reorder) **Verification** - `cargo clippy --all-targets -- -D warnings` — clean - `cargo test --lib --workspace` — 139 passed (5 suites) - `cargo test -p agent-desktop-ffi --tests` — 87 passed (4 suites, including new `c_header_compile` C-ABI harness) - macOS PR CI green on multiple runs; latest: 24561160455 - Python `ctypes.CDLL` round-trip from a freshly extracted FFI tarball confirmed; adapter lifecycle + `ad_list_apps` enumeration clean; `install_name` survives the tarball Closes #22 (superseded).
124 lines
4.3 KiB
YAML
124 lines
4.3 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main, master]
|
|
pull_request:
|
|
workflow_dispatch:
|
|
|
|
# Cancel in-progress runs for the same ref so stale PR builds don't burn minutes.
|
|
concurrency:
|
|
group: ci-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
# Deny all permissions at workflow level; each job declares only what it needs.
|
|
permissions: {}
|
|
|
|
env:
|
|
CARGO_TERM_COLOR: always
|
|
RUST_BACKTRACE: 1
|
|
|
|
jobs:
|
|
fmt:
|
|
name: Format
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 10
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
- run: rustup component add rustfmt
|
|
- run: cargo fmt --all -- --check
|
|
|
|
test:
|
|
name: Test
|
|
runs-on: macos-latest
|
|
timeout-minutes: 30
|
|
permissions:
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
|
|
- name: Install Rust toolchain
|
|
run: rustup show
|
|
|
|
- name: Cache cargo registry
|
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
with:
|
|
path: |
|
|
~/.cargo/registry/index/
|
|
~/.cargo/registry/cache/
|
|
~/.cargo/git/db/
|
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}
|
|
restore-keys: ${{ runner.os }}-cargo-
|
|
|
|
- name: Cache build artifacts
|
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
with:
|
|
path: target/
|
|
key: ${{ runner.os }}-build-ci-${{ hashFiles('**/Cargo.toml') }}-${{ hashFiles('**/*.rs') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-build-ci-${{ hashFiles('**/Cargo.toml') }}-
|
|
${{ runner.os }}-build-ci-
|
|
|
|
- name: Check dependency isolation
|
|
run: |
|
|
if cargo tree -p agent-desktop-core 2>/dev/null | grep -E 'agent-desktop-(macos|windows|linux)'; then
|
|
echo "FAIL: core crate depends on platform crates"
|
|
exit 1
|
|
fi
|
|
echo "OK: core crate has no platform dependencies"
|
|
|
|
- name: Clippy
|
|
run: cargo clippy --all-targets -- -D warnings
|
|
|
|
- name: Unit tests
|
|
run: cargo test --lib --workspace
|
|
|
|
# The FFI crate ships integration harnesses under crates/ffi/tests/ that
|
|
# exercise the C-ABI from outside the crate (raw extern "C" decls, enum
|
|
# fuzzing, out-param zeroing, last-error lifetimes). The workspace --lib
|
|
# run skips them, so wire them in explicitly so regressions fail PR CI.
|
|
- name: FFI integration tests
|
|
run: cargo test -p agent-desktop-ffi --tests
|
|
|
|
# Use the ci profile (no LTO, opt-level 1) — fast compile, still checks the binary builds.
|
|
- name: Build binary (ci profile)
|
|
run: cargo build --profile ci
|
|
|
|
- name: Check binary size
|
|
run: |
|
|
SIZE=$(stat -f%z target/ci/agent-desktop)
|
|
LIMIT=$((15 * 1024 * 1024))
|
|
echo "Binary size: $(du -sh target/ci/agent-desktop | cut -f1)"
|
|
if [ "$SIZE" -gt "$LIMIT" ]; then
|
|
echo "FAIL: binary exceeds 15MB limit (${SIZE} bytes)"
|
|
exit 1
|
|
fi
|
|
echo "OK: binary within 15MB limit"
|
|
|
|
- name: FFI cdylib build (release-ffi profile)
|
|
run: cargo build --profile release-ffi -p agent-desktop-ffi
|
|
|
|
- name: FFI header drift check
|
|
run: |
|
|
# build.rs stamps target/ffi-header-path.txt with the absolute path
|
|
# to the header produced by the most recent build, so the drift
|
|
# check always compares against the just-generated artifact rather
|
|
# than an arbitrary match from `find | head -1` over a warm cache.
|
|
STAMP=target/ffi-header-path.txt
|
|
if [ ! -f "$STAMP" ]; then
|
|
echo "FAIL: $STAMP missing; build.rs did not stamp header path"
|
|
exit 1
|
|
fi
|
|
GENERATED=$(cat "$STAMP")
|
|
if [ ! -f "$GENERATED" ]; then
|
|
echo "FAIL: stamped header path does not exist: $GENERATED"
|
|
exit 1
|
|
fi
|
|
if ! diff -u crates/ffi/include/agent_desktop.h "$GENERATED"; then
|
|
echo "FAIL: crates/ffi/include/agent_desktop.h is out of date"
|
|
echo "Run 'scripts/update-ffi-header.sh' locally and commit the regenerated header."
|
|
exit 1
|
|
fi
|
|
echo "OK: FFI header is in sync with source"
|