feat(sdk): minimal uniffi setup for cross language sdk (#9593)

This commit is contained in:
Alex Hancock
2026-06-04 09:58:31 -04:00
committed by GitHub
parent dc59e41945
commit 6251e56347
24 changed files with 514 additions and 24 deletions
Generated
+277 -6
View File
@@ -268,6 +268,48 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16"
[[package]]
name = "askama"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7"
dependencies = [
"askama_derive",
"itoa",
"percent-encoding",
"serde",
"serde_json",
]
[[package]]
name = "askama_derive"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac"
dependencies = [
"askama_parser",
"basic-toml",
"memchr",
"proc-macro2",
"quote",
"rustc-hash 2.1.2",
"serde",
"serde_derive",
"syn 2.0.117",
]
[[package]]
name = "askama_parser"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f"
dependencies = [
"memchr",
"serde",
"serde_derive",
"winnow 0.7.15",
]
[[package]]
name = "asn1-rs"
version = "0.7.2"
@@ -945,7 +987,7 @@ dependencies = [
"arc-swap",
"bytes",
"either",
"fs-err",
"fs-err 3.3.0",
"http 1.4.1",
"http-body 1.0.1",
"hyper",
@@ -1028,6 +1070,15 @@ version = "1.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
[[package]]
name = "basic-toml"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
dependencies = [
"serde",
]
[[package]]
name = "bat"
version = "0.26.1"
@@ -1831,6 +1882,29 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "cargo-platform"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
dependencies = [
"serde",
]
[[package]]
name = "cargo_metadata"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba"
dependencies = [
"camino",
"cargo-platform",
"semver",
"serde",
"serde_json",
"thiserror 2.0.18",
]
[[package]]
name = "castaway"
version = "0.2.4"
@@ -3920,6 +3994,15 @@ dependencies = [
"syn 2.0.117",
]
[[package]]
name = "fs-err"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41"
dependencies = [
"autocfg",
]
[[package]]
name = "fs-err"
version = "3.3.0"
@@ -4444,6 +4527,17 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "goblin"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47"
dependencies = [
"log",
"plain",
"scroll",
]
[[package]]
name = "goose"
version = "1.37.0"
@@ -4475,12 +4569,12 @@ dependencies = [
"encoding_rs",
"env-lock",
"etcetera 0.11.0",
"fs-err",
"fs-err 3.3.0",
"fs2",
"futures",
"goose-acp-macros",
"goose-mcp",
"goose-sdk",
"goose-sdk-types",
"goose-test-support",
"http 1.4.1",
"http-body-util",
@@ -4671,14 +4765,25 @@ dependencies = [
[[package]]
name = "goose-sdk"
version = "1.37.0"
dependencies = [
"agent-client-protocol",
"agent-client-protocol-schema",
"goose-sdk-types",
"thiserror 2.0.18",
"tokio",
"tokio-util",
"uniffi",
]
[[package]]
name = "goose-sdk-types"
version = "1.37.0"
dependencies = [
"agent-client-protocol",
"agent-client-protocol-schema",
"schemars 1.2.1",
"serde",
"serde_json",
"tokio",
"tokio-util",
]
[[package]]
@@ -7692,7 +7797,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b"
dependencies = [
"anyhow",
"itertools 0.13.0",
"itertools 0.14.0",
"proc-macro2",
"quote",
"syn 2.0.117",
@@ -8690,6 +8795,26 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "scroll"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6"
dependencies = [
"scroll_derive",
]
[[package]]
name = "scroll_derive"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "scrypt"
version = "0.11.0"
@@ -8777,6 +8902,10 @@ name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
dependencies = [
"serde",
"serde_core",
]
[[package]]
name = "seq-macro"
@@ -10943,6 +11072,15 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "toml"
version = "0.9.12+spec-1.1.0"
@@ -11562,6 +11700,127 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "uniffi"
version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3291800a6b06569f7d3e15bdb6dc235e0f0c8bd3eb07177f430057feb076415f"
dependencies = [
"anyhow",
"camino",
"cargo_metadata",
"clap",
"uniffi_bindgen",
"uniffi_core",
"uniffi_macros",
"uniffi_pipeline",
]
[[package]]
name = "uniffi_bindgen"
version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a04b99fa7796eaaa7b87976a0dbdd1178dc1ee702ea00aca2642003aef9b669e"
dependencies = [
"anyhow",
"askama",
"camino",
"cargo_metadata",
"fs-err 2.11.0",
"glob",
"goblin",
"heck",
"indexmap 2.14.0",
"once_cell",
"serde",
"tempfile",
"textwrap",
"toml 0.5.11",
"uniffi_internal_macros",
"uniffi_meta",
"uniffi_pipeline",
"uniffi_udl",
]
[[package]]
name = "uniffi_core"
version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38a9a27529ccff732f8efddb831b65b1e07f7dea3fd4cacd4a35a8c4b253b98"
dependencies = [
"anyhow",
"bytes",
"once_cell",
"static_assertions",
]
[[package]]
name = "uniffi_internal_macros"
version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09acd2ce09c777dd65ee97c251d33c8a972afc04873f1e3b21eb3492ade16933"
dependencies = [
"anyhow",
"indexmap 2.14.0",
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "uniffi_macros"
version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5596f178c4f7aafa1a501c4e0b96236a96bc2ef92bdb453d83e609dad0040152"
dependencies = [
"camino",
"fs-err 2.11.0",
"once_cell",
"proc-macro2",
"quote",
"serde",
"syn 2.0.117",
"toml 0.5.11",
"uniffi_meta",
]
[[package]]
name = "uniffi_meta"
version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "beadc1f460eb2e209263c49c4f5b19e9a02e00a3b2b393f78ad10d766346ecff"
dependencies = [
"anyhow",
"siphasher 0.3.11",
"uniffi_internal_macros",
"uniffi_pipeline",
]
[[package]]
name = "uniffi_pipeline"
version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd76b3ac8a2d964ca9fce7df21c755afb4c77b054a85ad7a029ad179cc5abb8a"
dependencies = [
"anyhow",
"heck",
"indexmap 2.14.0",
"tempfile",
"uniffi_internal_macros",
]
[[package]]
name = "uniffi_udl"
version = "0.29.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4319cf905911d70d5b97ce0f46f101619a22e9a189c8c46d797a9955e9233716"
dependencies = [
"anyhow",
"textwrap",
"uniffi_meta",
"weedle2",
]
[[package]]
name = "unit-prefix"
version = "0.5.2"
@@ -11990,6 +12249,15 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "weedle2"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e"
dependencies = [
"nom 7.1.3",
]
[[package]]
name = "weezl"
version = "0.1.12"
@@ -12480,6 +12748,9 @@ name = "winnow"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]]
name = "winnow"
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "goose-sdk-types"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
description = "Shared types for the Goose SDK"
[dependencies]
agent-client-protocol = { workspace = true, features = ["unstable"] }
agent-client-protocol-schema = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
schemars = { workspace = true, features = ["derive"] }
[package.metadata.cargo-machete]
# Used to provide extras imports for agent-client-protocol
ignored = ["agent-client-protocol-schema"]
+8
View File
@@ -0,0 +1,8 @@
//! Shared types for the Goose SDK.
//!
//! These wire types are used by both the ACP client/server path and the
//! in-process uniffi bindings, keeping a single source of truth for Goose's
//! custom `_goose/*` JSON-RPC methods.
pub mod custom_notifications;
pub mod custom_requests;
+2
View File
@@ -0,0 +1,2 @@
generated
examples/uniffi/*.jar
+18 -4
View File
@@ -6,14 +6,28 @@ rust-version.workspace = true
authors.workspace = true
license.workspace = true
repository.workspace = true
description = "Rust SDK for talking to Goose over the Agent Client Protocol (ACP)"
description = "Rust SDK for Goose with optional uniffi bindings for Python/Kotlin"
[lib]
name = "goose_sdk"
crate-type = ["cdylib", "staticlib", "rlib"]
[[bin]]
name = "goose-uniffi-bindgen"
path = "src/bin/uniffi-bindgen.rs"
required-features = ["uniffi"]
[features]
default = []
uniffi = ["dep:uniffi", "dep:thiserror"]
[dependencies]
goose-sdk-types = { path = "../goose-sdk-types" }
agent-client-protocol = { workspace = true, features = ["unstable"] }
agent-client-protocol-schema = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
schemars = { workspace = true, features = ["derive"] }
uniffi = { version = "0.29", features = ["cli"], optional = true }
thiserror = { version = "2", optional = true }
[dev-dependencies]
tokio = { workspace = true }
+16
View File
@@ -0,0 +1,16 @@
# goose-sdk
The bindings layer for Goose. It houses the shared types used for both ACP and
SDK access, and exposes a cross-language version of the Goose API.
With `--features uniffi` the crate compiles to native bindings for Python and
Kotlin (namespace `aaif_goose` / `aaif.goose`). The published surface is
currently a `ping` -> `pong` stub in `src/bindings.rs` — the scaffold for the
real implementation.
```bash
just python # build bindings + run examples/uniffi/ping.py
just kotlin # build bindings + run examples/uniffi/Ping.kt
```
Both print `pong: aaif.io`.
+9
View File
@@ -0,0 +1,9 @@
package aaif.example
import aaif.goose.Client
fun main() {
val client = Client()
val pong = client.ping("aaif.io")
println(pong.message)
}
+21
View File
@@ -0,0 +1,21 @@
"""Minimal Goose SDK demo: ping the SDK and print the pong."""
from __future__ import annotations
import sys
from pathlib import Path
HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent.parent / "generated"))
from aaif_goose import Client # noqa: E402
def main() -> None:
client = Client()
pong = client.ping("aaif.io")
print(pong.message)
if __name__ == "__main__":
main()
+38
View File
@@ -0,0 +1,38 @@
set shell := ["bash", "-cu"]
set working-directory := '../..'
lib_ext := if os() == "macos" { "dylib" } else if os() == "windows" { "dll" } else { "so" }
lib_dir := "./target/debug"
lib_path := lib_dir / "libgoose_sdk." + lib_ext
bindgen := "./target/debug/goose-uniffi-bindgen"
config := "./crates/goose-sdk/uniffi.toml"
gen_dir := "./crates/goose-sdk/generated"
examples_dir := "./crates/goose-sdk/examples/uniffi"
default:
@just --list --justfile {{justfile()}}
_build:
cargo build -p goose-sdk --features uniffi -q
_generate lang: _build
{{bindgen}} generate --library {{lib_path}} --config {{config}} --language {{lang}} --no-format --out-dir {{gen_dir}} 2>/dev/null
cp {{lib_path}} {{gen_dir}}/
touch {{gen_dir}}/__init__.py
python: (_generate "python")
DYLD_LIBRARY_PATH={{lib_dir}} LD_LIBRARY_PATH={{lib_dir}} \
python3 {{examples_dir}}/ping.py
kotlin: (_generate "kotlin")
@if [ ! -f {{examples_dir}}/jna.jar ]; then \
curl -sSL -o {{examples_dir}}/jna.jar \
https://repo1.maven.org/maven2/net/java/dev/jna/jna/5.14.0/jna-5.14.0.jar; \
fi
kotlinc -cp {{examples_dir}}/jna.jar -nowarn \
{{gen_dir}}/aaif/goose/aaif_goose.kt \
{{examples_dir}}/Ping.kt \
-include-runtime -d {{examples_dir}}/ping.jar 2>/dev/null
java -Djna.library.path={{lib_dir}} \
--enable-native-access=ALL-UNNAMED \
-cp {{examples_dir}}/ping.jar:{{examples_dir}}/jna.jar aaif.example.PingKt
@@ -0,0 +1,3 @@
fn main() {
uniffi::uniffi_bindgen_main()
}
+69
View File
@@ -0,0 +1,69 @@
//! In-process uniffi bindings for the Goose SDK.
//!
//! This is the published API surface exposed to Python and Kotlin. Right now it
//! is a minimal `ping` -> `pong` round-trip that proves the uniffi
//! infrastructure end to end without depending on the `goose` core crate.
//!
//! To build the real SDK, add `goose` (and whatever else you need) as
//! dependencies and replace the [`Client`] methods below with the actual
//! agent surface.
use std::sync::Arc;
/// Errors surfaced across the uniffi boundary.
#[derive(Debug, thiserror::Error, uniffi::Error)]
pub enum GooseError {
#[error("{0}")]
Generic(String),
}
/// A reply to a [`Client::ping`] call.
#[derive(Debug, Clone, uniffi::Record)]
pub struct Pong {
/// Echo of the message that was pinged.
pub message: String,
}
/// The top-level entry point for the Goose SDK.
///
/// This is the object that consuming languages instantiate. Today it only knows
/// how to answer a ping; extend it with the real agent API.
#[derive(uniffi::Object)]
pub struct Client {}
#[uniffi::export]
impl Client {
#[uniffi::constructor]
pub fn new() -> Arc<Self> {
Arc::new(Self {})
}
/// Round-trip a message through the SDK. Returns a [`Pong`] echoing the
/// supplied `message`, prefixed with `pong: `.
pub fn ping(&self, message: String) -> Result<Pong, GooseError> {
if message.is_empty() {
return Err(GooseError::Generic("message must not be empty".into()));
}
Ok(Pong {
message: format!("pong: {message}"),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ping_returns_pong() {
let client = Client::new();
let pong = client.ping("aaif.io".into()).expect("ping should succeed");
assert_eq!(pong.message, "pong: aaif.io");
}
#[test]
fn empty_ping_errors() {
let client = Client::new();
assert!(client.ping(String::new()).is_err());
}
}
+21 -2
View File
@@ -1,2 +1,21 @@
pub mod custom_notifications;
pub mod custom_requests;
//! Goose SDK.
//!
//! With default features this crate re-exports the shared SDK wire types from
//! `goose-sdk-types` so you can build an Agent Client Protocol (ACP) client
//! that talks to `goose acp` over stdio.
//!
//! With `--features uniffi` the crate additionally compiles as a
//! `cdylib`/`staticlib` and exposes a small in-process API to Python and Kotlin
//! via [uniffi-rs](https://github.com/mozilla/uniffi-rs).
//!
//! The published uniffi surface is intentionally a single `ping` -> `pong`
//! round-trip. It exists as a working scaffold for adding the real Goose SDK
//! API: replace [`bindings`] with the actual implementation.
pub use goose_sdk_types::{custom_notifications, custom_requests};
#[cfg(feature = "uniffi")]
uniffi::setup_scaffolding!("aaif_goose");
#[cfg(feature = "uniffi")]
pub mod bindings;
+2
View File
@@ -0,0 +1,2 @@
[bindings.kotlin]
package_name = "aaif.goose"
+1 -1
View File
@@ -122,7 +122,7 @@ strum = { workspace = true }
once_cell = { workspace = true }
etcetera = { workspace = true }
fs-err = { version = "3.1", default-features = false }
goose-sdk = { path = "../goose-sdk", default-features = false }
goose-sdk-types = { path = "../goose-sdk-types" }
rand = { workspace = true }
utoipa = { workspace = true, features = ["chrono"] }
tokio-cron-scheduler = { version = "0.15", default-features = false }
+1 -2
View File
@@ -10,8 +10,7 @@ pub(crate) mod tools;
pub mod transport;
pub use common::{map_permission_response, PermissionDecision};
pub use goose_sdk::custom_notifications;
pub use goose_sdk::custom_requests;
pub use goose_sdk_types::{custom_notifications, custom_requests};
pub use provider::{
extension_configs_to_mcp_servers, AcpProvider, AcpProviderConfig, ACP_CURRENT_MODEL,
};
@@ -16,7 +16,7 @@ use crate::sources::parse_frontmatter;
use crate::utils::safe_truncate;
use anyhow::Result;
use async_trait::async_trait;
use goose_sdk::custom_requests::{SourceEntry, SourceType};
use goose_sdk_types::custom_requests::{SourceEntry, SourceType};
use rmcp::model::{
CallToolResult, Content, Implementation, InitializeResult, JsonObject, ListToolsResult, Meta,
ServerCapabilities, ServerNotification, Tool,
+1 -1
View File
@@ -7,7 +7,7 @@
use crate::sources::parse_frontmatter;
use anyhow::{anyhow, bail, Context, Result};
use goose_sdk::custom_requests::{SourceEntry, SourceType};
use goose_sdk_types::custom_requests::{SourceEntry, SourceType};
use serde::Deserialize;
use std::collections::{BTreeMap, HashMap};
use std::fs;
+1 -2
View File
@@ -2,8 +2,7 @@
compile_error!("Features `rustls-tls` and `native-tls` are mutually exclusive");
pub mod acp;
pub use goose_sdk::custom_notifications;
pub use goose_sdk::custom_requests;
pub use goose_sdk_types::{custom_notifications, custom_requests};
pub mod action_required_manager;
pub mod agents;
pub mod builtin_extension;
+1 -1
View File
@@ -4,7 +4,7 @@ use crate::agents::extension::PlatformExtensionContext;
use crate::agents::mcp_client::{Error, McpClientTrait};
use crate::agents::ToolCallContext;
use async_trait::async_trait;
use goose_sdk::custom_requests::{SourceEntry, SourceType};
use goose_sdk_types::custom_requests::{SourceEntry, SourceType};
use rmcp::model::{
CallToolResult, Content, Implementation, InitializeResult, JsonObject, ListToolsResult,
ServerCapabilities, ServerNotification, Tool,
+1 -1
View File
@@ -14,7 +14,7 @@ use crate::sources::parse_frontmatter;
use agent_client_protocol::Error;
use anyhow::Result;
use arguments::apply_skill_arguments;
use goose_sdk::custom_requests::{SourceEntry, SourceType};
use goose_sdk_types::custom_requests::{SourceEntry, SourceType};
use serde::Deserialize;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
@@ -1,6 +1,6 @@
use std::path::Path;
use goose_sdk::custom_requests::{SourceEntry, SourceType};
use goose_sdk_types::custom_requests::{SourceEntry, SourceType};
use super::types::{SlashCommandEntry, SlashCommandSource};
use super::util::normalize_command_name;
@@ -82,7 +82,7 @@ pub(super) fn commands_from_sources(sources: Vec<SourceEntry>) -> Vec<SlashComma
#[cfg(test)]
mod tests {
use super::*;
use goose_sdk::custom_requests::SourceType;
use goose_sdk_types::custom_requests::SourceType;
use std::collections::HashMap;
use tempfile::TempDir;
+1 -1
View File
@@ -11,7 +11,7 @@ use crate::skills::{
use crate::source_roots::SourceRoot;
use agent_client_protocol::Error;
use fs_err as fs;
use goose_sdk::custom_requests::{SourceEntry, SourceType};
use goose_sdk_types::custom_requests::{SourceEntry, SourceType};
use serde::Deserialize;
use std::collections::HashMap;
use std::path::{Path, PathBuf};