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
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 undernode_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:
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
ThebinaryPath 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 forcesdisable-library-validation on your host app.
The binary is a Node single-executable build, so it needs the JIT entitlement:
entitlements.runtime.plist carrying exactly one key:
native/ individually and with no entitlements. --deep is not the answer here, and Apple says so:
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.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 intoContents/Resources (or the Windows resources directory) with extraResources, sign it in afterPack, and tell electron-builder not to sign it again:
afterPackruns after the app is assembled and before electron-builder signs it. The hook signs the runtime with its own entitlements.- electron-builder signs the app. It walks every Mach-O file under
Contents, includingextraResources, and would give each one yourentitlementsInherit.signIgnorestops it at the runtime, so the runtime keeps the signature and the entitlements from step 1. - electron-builder seals the app and notarizes it. The seal covers the runtime as signed in step 1.
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 asigning/ 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.
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 exampledarwin-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().