Consider this description for a GitHub issue of type Epic. Am I using the term ABI correctly here:
As of 2026-07, the Copilot SDK for Java does not embed a Copilot CLI runtime. Rather, it depends on one being available. This design decision was made some time before 2026-02. Now that the Rust Copilot CLI runtime is nearing the point when it can be used, this decision must be revisited. Work items in this Epic will make it so the Copilot SDK for Java embeds N OS and ABI specific instances of the Rust Copilot CLI runtime, dynamically selecting the correct run for the current OS and ABI and starting it. This decision is based on the assumption that even with embedding N OS and ABI specific instances of the Rust Copilot CLI runtime, the download size of the Copilot SDK Java jar will still be acceptably small due to the compact binary of the Rust CLI runtime.
Mostly, but "ABI" is probably not the term you want here — what you're describing is more precisely CPU architecture (or just "architecture"/"arch").
The distinction:
- Architecture is the instruction set: x86_64 vs aarch64. This is what determines which Rust binary will run on a given machine, alongside the OS. When projects like Netty, JNA, or gRPC embed native binaries and select at runtime, they key off OS + arch (e.g.,
linux-x86_64,osx-aarch64— the Mavenos-maven-pluginclassifier convention). - ABI is the application binary interface: calling conventions, data layout, and linkage contracts. In the Rust world, ABI is the fourth component of the target triple —
x86_64-unknown-linux-gnuvsx86_64-unknown-linux-musldiffer in ABI (glibc vs musl) while sharing the same OS and architecture. Similarlywindows-msvcvswindows-gnu.
So "N OS and ABI specific instances" would technically mean you're shipping, say, both a glibc and a musl build for the same Linux/x86_64 combination. That's a legitimate concern for Rust binaries — if the Copilot CLI is dynamically linked against glibc, it won't run on Alpine containers, which matters a lot for CI/CD scenarios. But if that's not what you mean, the phrasing overstates precision in the wrong dimension.
Suggested rewrites depending on intent:
- If you mean arch only: "N OS- and architecture-specific instances… selecting the correct binary for the current OS and architecture."
- If you genuinely intend to cover the libc dimension too, the cleanest term is target triple (or "Rust target"): "N target-specific instances of the Rust Copilot CLI runtime, dynamically selecting the correct binary for the current platform." That subsumes OS, arch, and ABI without enumerating them.
One practical note: if the Rust CLI is (or can be) built as musl static binaries for Linux, the ABI question largely evaporates on that OS — one binary per arch covers everything — which would also strengthen your download-size assumption. Might be worth a work item in the Epic to confirm how the Rust CLI team is building their release artifacts.
Also, minor typo: "selecting the correct run" → "selecting the correct runtime" (or "binary").
I have Copilot CLI open now on the Copilot CLI runtime repository (recursive, I know).
Anyhow, give me the question to determine the truth of the matter regarding the actual current state of the repo and its use, or non-use, of static or dynamic musl or non-musl approach.
Here's a prompt you can paste into Copilot CLI:
Examine this repository's release build configuration and answer the following:
1. What Rust target triples are built for release artifacts? List them all
(check .github/workflows, any cross/cargo config, release scripts, and
Cargo.toml / .cargo/config.toml).
2. For each Linux target: is it *-linux-gnu (glibc) or *-linux-musl?
3. For any musl targets: is the resulting binary fully statically linked,
or does it still dynamically link anything? Look for build flags like
RUSTFLAGS="-C target-feature=+crt-static", crt-static in config files,
or use of cross/zig/musl-gcc in the build.
4. For any gnu targets: what is the minimum glibc version implied by the
build environment (e.g., which container image or runner OS is used to
compile)?
5. Are there any native dependencies (openssl, libgit2, zlib, etc.) that
are dynamically linked rather than vendored/static? Check Cargo.toml
for features like "vendored" or "rustls" vs "native-tls".
Summarize: for each OS/arch combination shipped, state whether one binary
runs everywhere on that OS/arch (static musl) or whether there are libc
compatibility constraints (dynamic glibc, minimum version X).
Question 5 is the one that most often surprises people — a nominally musl-static build can quietly lose its portability if a dependency pulls in native TLS. If the answer comes back "rustls + vendored everything + musl static," then for LECTOR-relevant CI/CD scenarios (including Alpine-based runners) you'd need exactly one Linux binary per arch, and your Epic can safely say "OS and architecture" with no ABI dimension at all.
Consider this comment regarding the FFI interface with JNA:
Was curious about The JNA calling for Java, is this not a case for FFI via Panama (assuming a Java 25 LTS base line).
How does Panama come into this?
Panama is relevant only if the commenter's mental model of the integration is different from what your Epic actually describes — and that's worth surfacing, because the comment likely conflates two distinct architectures.
What Panama is: Project Panama produced the Foreign Function & Memory API (java.lang.foreign, JEP 454, final in Java 22, so comfortably in a Java 25 LTS baseline). It's the modern replacement for JNI and, by extension, JNA — it lets Java call functions in a native library loaded into the same process, with jextract generating bindings from C headers. Against JNA specifically, FFM wins on performance (no reflective marshalling layer), on being part of the platform (no third-party dependency), and on safety (MemorySegment bounds/lifetime checks). If you have JNA code and a Java 22+ baseline, migrating to FFM is generally the right call, with one operational caveat: native access is a restricted capability, so you need --enable-native-access on the module or launcher as the JDK tightens JEP 472 enforcement.
Why it may not come into this at all: FFM and JNA both address in-process FFI — calling extern "C" functions in a .so/.dylib/.dll. But your Epic embeds the Rust Copilot CLI as an executable, spawned as a sidecar process and spoken to over JSON-RPC on stdio. That's ProcessBuilder territory. There is no foreign function being called, so there's nothing for either JNA or Panama to do. The word "embedding" probably triggered the association with embedding a native library, but embedding-a-binary-as-a-resource-and-exec'ing-it is a different pattern entirely.
So the honest answer to the comment has two branches:
-
If the SDK doesn't actually use JNA (worth verifying — it might appear transitively, or be used for something incidental like process/TTY handling on Windows): reply that the integration is process-based JSON-RPC, not FFI, so neither JNA nor Panama applies. The Rust runtime is a subprocess, not a linked library.
-
If there is real JNA usage somewhere, or if there's a future in which the Rust runtime is consumed as a
cdylibwith a C ABI surface instead of a CLI: then yes, Panama FFM is the right 2026 answer on a Java 25 baseline. But note that flipping to the in-process model is a much bigger architectural decision than a bindings-technology swap — it changes crash isolation (a Rust panic/abort takes down the JVM), lifecycle management, and would require the CLI team to expose and stabilize a C ABI, which is a different deliverable from their executable.
There's also a middle observation worth making in your reply: the subprocess model is precisely what makes the "N binaries selected at runtime" approach clean. You extract the right executable to a temp/cache dir and exec it — no System.load, no native-access flags, no per-platform linkage concerns beyond the ones you're already resolving with the musl question.
You stated:
But your Epic embeds the Rust Copilot CLI as an executable, spawned as a sidecar process and spoken to over JSON-RPC on stdio.
Actually, no it is a cdylib: The artifact to be embedded is runtime.node, a Rust cdylib produced by the src/runtime crate in github/copilot-agent-runtime using the napi-rs build toolchain.
You're right, and I was wrong — I pattern-matched to the CLI-as-sidecar model when the actual artifact is a library. Thanks for the correction. But the napi-rs detail cuts in an interesting direction: it makes the commenter's Panama suggestion less viable, not more, while partially vindicating your original "ABI" terminology.
Why napi-rs complicates the Panama answer. A .node file is a cdylib, but its exported ABI surface is not a general-purpose C API — it's Node-API. The library exports essentially one entry point (napi_register_module_v1) and everything it does happens through a napi_env handle whose functions are provided by the host, which is normally Node.js. So while Panama FFM can technically dlopen a .node file and call that symbol, doing anything useful requires the JVM process to implement the N-API host interface — the whole vtable of napi_create_string, napi_call_function, reference/lifetime management, threadsafe functions, etc. That's not a bindings problem jextract solves; it's "reimplement a substantial slice of Node's embedding layer in Java." Neither JNA nor Panama gets you past that; the difference between them is irrelevant at that point.
So the real question for your reply to the commenter is: what hosts runtime.node in the Java SDK's architecture? The realistic options:
- A Node.js runtime is also bundled (or required), and it loads
runtime.node; Java talks to that process over JSON-RPC/stdio. This is the sidecar model after all, just with the sidecar beingnode + runtime.noderather than a standalone executable. Panama is irrelevant here. - libnode embedded in-process — the JVM loads Node as a library, which then hosts the addon. Panama could be the mechanism for the libnode embedding calls, but this is architecturally heavy and brings the crash-isolation problem back (Rust abort or V8 crash takes down the JVM).
- A non-Node N-API host — implementations exist in the Rust ecosystem, but shipping one inside a Java SDK would be a significant deliverable of its own.
ADR-007 ("native bundling strategy") presumably answers which of these it is — that's the thing to check before replying. A Copilot CLI prompt for the runtime repo session you have open:
This repo's src/runtime crate builds runtime.node, a napi-rs cdylib.
Answer from the actual code and ADRs:
1. What process loads runtime.node? Is a Node.js runtime bundled,
required as a peer dependency, or embedded via libnode?
2. If a Node runtime is involved, how do non-Node SDK consumers
(Java, .NET, etc.) communicate with it — JSON-RPC over stdio,
sockets, or in-process calls?
3. What napi-rs platform targets are built for release (list the
exact suffixes: darwin-arm64, linux-x64-gnu, linux-x64-musl,
win32-x64-msvc, etc.)?
4. Does runtime.node have native dependencies beyond Node-API
(openssl, zlib) and are they vendored/static?
Two knock-on effects for the earlier discussion:
First, your original "OS and ABI" phrasing looks better now. napi-rs's platform packaging convention literally encodes ABI as a distinct dimension: linux-x64-gnu vs linux-x64-musl, win32-x64-msvc vs -gnu. If the Epic embeds napi-rs artifacts, "N OS- and ABI-specific instances" may be exactly right — though I'd still suggest "N platform-specific instances (OS × architecture × ABI, per the napi-rs target matrix)" to make all three dimensions explicit.
Second, my earlier musl point needs restating for cdylibs. crt-static musl portability logic applies cleanly to executables; for a cdylib the calculus is different — a linux-x64-musl .node exists specifically to be loaded by a musl-linked Node on Alpine, and a gnu-linked Node can't load it (and vice versa). So you likely do need both gnu and musl Linux variants per arch, matching whatever Node runtime is in play — the ABI dimension doesn't evaporate the way it would have in the standalone-executable world. That also weakens nothing in your download-size assumption but does increase N.
Here, fetch ADR-007.
copilot-sdk/java/docs/adr/adr-007-native-bundling-strategy.md at main · github/copilot-sdk
(Claude fetched ADR-007 and summarized.)
Got it — and this ADR settles every open thread in this conversation, mostly by proving my speculation wrong in useful ways.
The N-API hosting problem I raised doesn't exist. The library exposes two front doors built over the same internal engine: a napi front door loaded by a Node.js process as a native addon (the current CLI path), and a C ABI front door — a fixed set of approximately 12 extern "C" lifecycle and transport entry points that any language can call in-process via FFI (JNA for Java, Python/cffi, C#/DllImport, Go/purego) without a Node.js process. So Java never touches the Node-API surface; it calls the stable C ABI directly. The elegant part is that all API methods travel as JSON-RPC data through this fixed transport, so the export list never changes as the method set grows — it's the JSON-RPC protocol you already know, just moved from stdio to an in-process function-call transport.
The Panama comment is therefore fully legitimate — and now answerable concretely. The ADR specifies the coordination artifact loads the binary via JNA using the C ABI entry points. A ~12-function, JSON-strings-over-a-fixed-transport surface is close to the ideal case for FFM: small enough to hand-write bindings (no jextract needed), hot enough on the transport path that avoiding JNA's marshalling overhead is measurable. The honest reply to the commenter: "Yes — the ADR currently says JNA, and since the C ABI surface is ~12 stable functions, migrating to FFM on a Java 22+/25 baseline would be low-effort. The blockers are baseline policy (what's the SDK's minimum Java version?) and the --enable-native-access requirement FFM imposes on consumers, which JNA currently sidesteps." That last point is a real UX consideration for an SDK: JNA works silently today, while FFM pushes a launcher flag onto every downstream user as JEP 472 restrictions tighten.
Your "OS and ABI" phrasing is vindicated, with one refinement. The build matrix is exactly each unique combination of OS, CPU architecture, and (on Linux) C runtime variant — eight triples where gnu vs. musl is a genuine third dimension. So the Epic text should say "OS, architecture, and (on Linux) libc variant" or just reference the eight napi-rs platform labels. Note also my musl advice inverts here, as I suspected in the cdylib discussion: the musl binaries are not fully statically linked; they dynamically link musl libc (-C target-feature=-crt-static is explicitly set at build time), and the gnu builds carry a glibc ≥ 2.28 floor enforced via a sysroot and verified post-build. One-static-binary-per-arch was never on the table for this artifact.
Your download-size assumption needs revision, and the ADR already made the resulting call. The binaries are much bigger than "compact Rust CLI" intuition suggests — 48–65 MB uncompressed, ~19–26 MB compressed per platform, against a current SDK JAR of 1.53 MB; a monolithic JAR would be ~132 MB for the 6 common platforms or ~180 MB with musl. Partly that's because the binary is growing with each TypeScript-port PR, not shrinking, and currently spawns a short-lived Node child process for method bodies not yet ported. Hence the decision: per-platform classifier JARs, DJL-style — a ~1.5 MB pure-Java coordination artifact plus one ~20–26 MB platform JAR selected via os-maven-plugin or Gradle variant resolution, with musl detection done by parsing the ELF PT_INTERP segment of /proc/self/exe, the same approach as the detect-libc npm package.
So your Epic text's third paragraph — "the download size of the Copilot SDK Java jar will still be acceptably small" — is true only under the classifier-JAR strategy, not under naive embedding. Worth rewording to something like: "This is made viable by ADR-007's per-platform classifier JAR strategy, under which a consumer downloads only the ~1.5 MB coordination artifact plus the single ~20–26 MB platform JAR matching their OS/arch/libc."
One caveat on all of the above: the ADR is still marked DRAFT, so cite it as the current direction rather than settled policy when replying to the commenter.
Aside from requiring Java 25 where we formerly support 17 or 25, are there any other downsides to using Panama instead of JNA?
Yes, several — and a couple of them are more consequential for an SDK than for an application, because every cost you take on is inherited by all downstream consumers.
The --enable-native-access requirement is the big one. JNA today loads and calls native code with at most a warning. Under FFM plus the JEP 472 integrity-by-default trajectory, restricted native operations require the consumer to grant native access — --enable-native-access=com.github.copilot.sdk (or ALL-UNNAMED for classpath users) on the launcher, or an Enable-Native-Access manifest attribute for executable JARs. That's friction you're pushing onto every Java app, Spring Boot service, and CI harness that adopts the SDK, and it's the kind of thing that generates support issues ("why does my app print/throw this?"). The irony is that JNA itself sits on the same enforcement path — it uses JNI internally, and --illegal-native-access will eventually bite it too — so this is a downside of now vs. later, not ever vs. never. But "later" may buy you a year or two of zero-config UX.
GraalVM native-image support is weaker. JNA on native-image is a known quantity with established reachability metadata. FFM support in native-image landed later and has been maturing incrementally — downcalls arrived first, upcalls and broader coverage have been filling in, and behavior varies by GraalVM release. If any meaningful slice of Copilot SDK consumers compile to native images (Quarkus/Micronaut CLI tools are plausible consumers), you'd want to verify the current state on your supported GraalVM versions before switching. This one is checkable and changing fast, so treat it as a verification item, not a permanent objection.
Callback/upcall ergonomics and threading. The C ABI transport is presumably bidirectional — the runtime needs to deliver JSON-RPC responses and server-initiated requests back to Java, which means native-to-Java calls. JNA's Callback handles foreign-thread attachment with well-worn semantics. FFM upcall stubs work from native threads too, but you're managing Arena lifetimes for the stubs yourself — an upcall stub whose arena closes while Rust still holds the function pointer is a JVM crash, and the Rust side's threading model (which thread invokes the callback, during or after which calls) becomes something you must reason about explicitly rather than something the binding layer papers over.
You lose JNA's crash padding, and FFM's safety wins don't apply here. FFM's safety story (bounds-checked MemorySegment, lifetime scopes) pays off when Java manipulates native memory structurally. This surface is ~12 functions passing JSON strings — there's little structural memory work to make safe. Meanwhile JNA offers protected mode as a best-effort net around native faults. Neither matters enormously, but the net safety delta for this particular ABI shape is close to zero.
The performance argument is weaker than it looks, for the same reason. JNA's overhead is per-call marshalling; FFM largely eliminates it. But your payloads are JSON-RPC strings whose serialization/deserialization cost dwarfs the call overhead, and call frequency is bounded by agent-interaction rates, not tight loops. You'd be taking on all the above costs for a latency win that's probably unmeasurable in end-to-end SDK usage. (If the transport ever moves to something like shared-memory frames or high-frequency streaming chunks, this calculus changes.)
Finally, the boring ones: it's a departure from what ADR-007 currently specifies, so it's an ADR amendment plus re-validation across all eight platform targets; hand-written FFM bindings are genuinely fine for 12 functions but are one more thing to keep in sync as the entry-point set evolves (the ADR's whole design bet is that it won't evolve, which helps); and team/reviewer familiarity with JNA is likely higher today.
The honest synthesis for the commenter: Panama is the right eventual answer — the JEP 472 endgame pushes everyone there, and a 12-function stable C ABI is an easy migration. But on a 17-or-25 dual baseline it's not available at all on 17, and even on a 25-only baseline the immediate trade is "consumer launcher flags + native-image verification + upcall lifetime management" purchased with a performance benefit that JSON transport mostly nullifies. A reasonable middle path is to structure the loading/binding layer behind a small internal interface so an FFM implementation can ship later as a multi-release JAR or optional module without touching the transport logic.