Skip to main content
By default the SDK downloads its runtime from a GitHub release on first run. For a signed, notarized application that is the wrong shape: your bundle is one signed artifact, and an executable that appears in userData afterwards is outside it. Instead, install the runtime from npm, copy it into your bundle, sign it with your own identity, and turn the downloader off.

Install the platform package

@ade-dev/runtime is a meta package. It lists five platform packages as optionalDependencies, each carrying os and cpu, so npm installs exactly the one that matches the machine. This is the pattern esbuild and swc use. Install a specific platform package directly when you cross-build: npm install @ade-dev/runtime-darwin-arm64 --force on a Linux CI runner, for instance.
Install with a version that matches the ADE release you tested against. The npm version of a runtime package is the ADE release version.

The layout

The binary alone is not runnable. It dlopens its native modules out of native/node_modules and resolves that directory from ADE_RUNTIME_NODE_MODULES, so both halves must travel together and both must be reachable at runtime.

Resolve it with no configuration

With the package installed, the SDK finds it on its own. resolveBundledRuntime() is step 2 of the resolution order and sits above the cache, PATH, and any download.
allowDownload: false is the important half. Without it a packaging mistake works on your machine through a silent download and fails on a locked-down user’s. With it, the same mistake throws runtime_unavailable in QA with a message listing everything that was tried.

Inside a packaged app

Inside a packaged app the runtime is no longer under node_modules. resolvePackagedRuntime finds the copy under your resources directory, so the layout (bin/ade, native/, native/node_modules) is not hardcoded in your app:
resolvePackagedRuntime(resourcesPath, { dir = "ade-runtime" }) returns null when the directory has no runtime binary, or when the binary is for a different CPU architecture than the running app. It throws binary_not_found when the binary is present but native/node_modules is not. A client created from it reports doctor().runtime.source === "packaged". You can still pass the three paths by hand:
That form reports source: "explicit". runtimeRoot defaults to the parent of runtimeNodeModules, so you only pass it when you split the two halves. Both directories are validated at createAdeChat, not at spawn: a wrong path fails with binary_not_found naming the path, rather than with a dlopen error from inside a child process. Paths with spaces are safe. /Applications/My App.app/Contents/Resources/… needs no quoting anywhere: every path is composed with path.join, every spawn is argv-based, and no shell string is ever built.

Checksums

The binaryPath and bundled-package routes deliberately skip SHA256SUMS. That check exists to establish the provenance of bytes fetched over the network. Bytes you signed into your own bundle are verified by the OS, and re-checking them against a GitHub release would prove nothing about what you shipped. doctor().runtime.checksumVerified is false on those routes by design.

macOS: signing

Sign the runtime with your own Developer ID, as part of your own bundle. The packages ship the released binaries, which carry ADE’s signature; a binary signed by ADE inside your app invites a Team ID mismatch and forces disable-library-validation on your host app. The binary is a Node single-executable build, so it needs the JIT entitlement:
with entitlements.runtime.plist carrying exactly one key:
Then sign every Mach-O file under native/ individually and with no entitlements. --deep is not the answer here, and Apple says so:
Find those files by magic number rather than by extension. ADE’s own notarizer sniffs feedface, feedfacf, cefaedfe, cffaedfe, cafebabe, bebafeca, cafebabf, bfbafeca; the .node addons, node-pty’s spawn-helper, and the cr-sqlite .dylib are all in that set. Verify with codesign --verify --strict --verbose=4.

macOS: entitlements, precisely

The question every embedder asks, answered against ADE’s own build:

macOS: notarization

The embedded runtime is covered by your app’s notarization submission. It is inside the bundle you submit, so it is scanned and ticketed with everything else. You do not notarize it separately.
Do not plan on stapling a ticket to the bare runtime binary. xcrun stapler rejects non-bundle executables, and ADE’s own release treats stapling the standalone binary as best-effort for exactly that reason. Offline Gatekeeper validation rides on your app bundle’s own stapled ticket, which is another reason to embed rather than to ship the binary loose.

Windows: Authenticode

Sign the runtime as part of your normal signing step, the same way you sign your own .exe and DLLs. Under electron-builder, azureSignOptions (or your signtoolOptions equivalent) sits above electron-builder’s single signing chokepoint, so files under extraResources are covered without a separate pass. You do not need signtool remove. ADE’s build strips the vendor Node signature before injecting the SEA blob, because signing a postject-modified binary that still carries the original signature fails with 0x800700C1 (ERROR_BAD_EXE_FORMAT). The binary you install from npm is already stripped and re-signed, so it signs cleanly as-is. Timestamp the signature. An expired certificate invalidates every signature it made unless they were timestamped.

electron-builder

Copy the platform package into Contents/Resources (or the Windows resources directory) with extraResources, sign it in afterPack, and tell electron-builder not to sign it again:
The order is the whole point, so here it is in full:
  1. afterPack runs after the app is assembled and before electron-builder signs it. The hook signs the runtime with its own entitlements.
  2. electron-builder signs the app. It walks every Mach-O file under Contents, including extraResources, and would give each one your entitlementsInherit. signIgnore stops it at the runtime, so the runtime keeps the signature and the entitlements from step 1.
  3. electron-builder seals the app and notarizes it. The seal covers the runtime as signed in step 1.
Do not sign the runtime in afterSign. electron-builder has already sealed and notarized the app when afterSign runs, so a signature changed there invalidates the seal and does not match the notarized bytes. Guides before SDK 0.3 said that electron-builder does not sign extraResources. That is not true for electron-builder 26: @electron/osx-sign walks all of Contents.
The filter matters: it keeps package.json, README.md, LICENSE, RUNTIME-EMBEDDING-EXCEPTION.md and signing/ out of the bundle, so the only files inside are ones you sign. The two license documents still have to reach your users. Put them wherever your application shows its third-party notices.

The signing kit

From runtime 1.2.81, each macOS platform package carries a signing/ directory: The hook reads the manifest, and walks the tree itself for an older package. For that fallback, save the one-key plist from macOS: signing as build/entitlements.runtime.plist in your project.
After the build, check the result:
The last command must show com.apple.security.cs.allow-jit and nothing else. Then point the client at the copied tree with resolvePackagedRuntime, as in Inside a packaged app above.

Intel Macs

Each platform package carries one architecture. An app that ships one runtime (for example darwin-arm64 only) must refuse to start chat on the other architecture, or ship a second build. A universal app needs both packages: copy @ade-dev/runtime-darwin-arm64 and @ade-dev/runtime-darwin-x64 to two directories and choose one at run time from process.arch. There is no universal runtime binary. On an Intel Mac, resolvePackagedRuntime returns null when only an arm64 tree is present, so createAdeChat fails at start with runtime_unavailable instead of at spawn.

Confirm what shipped

doctor().runtime names the path that was actually taken. Put source and signature in your support bundle on day one: together they separate “this app is running the runtime we signed and shipped” from “this app quietly downloaded one on this machine”, which you cannot otherwise see.
A packaged build that reports source: "downloaded" is a packaging bug. allowDownload: false turns that bug into a runtime_unavailable throw at startup instead.

Before you redistribute it

The runtime binary is AGPL-3.0-only, and putting it inside your bundle is redistribution. The ADE Runtime Embedding Exception permits it when the binary is unmodified and your application consumes it through the documented @ade-dev/sdk interface. Re-signing it with your own identity, as this page instructs, does not count as modifying it. Read the exact conditions before you redistribute.

License

What each artifact is licensed under, and the conditions the embedding exception sets.

Next

Runtime

The sidecar’s guest rules, pidfile reclaim, and the rest of doctor().