Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
05f05ed
Merge origin/develop into iOS Kokoro onboarding
Dexploarer May 20, 2026
20ed54b
Merge latest origin/develop into iOS Kokoro onboarding
Dexploarer May 20, 2026
f60d04f
fix(local-inference): skip iOS-only OmniVoice patch for desktop builds
Dexploarer May 20, 2026
7a197ef
fix(ci): align remote plugin and mobile build gates
Dexploarer May 20, 2026
a3c5859
Merge origin/develop into iOS Kokoro onboarding
Dexploarer May 20, 2026
bbff559
Merge branch 'develop' into codex/ios-kokoro-tts-onboarding
Dexploarer May 20, 2026
435307b
fix: address remote runner review feedback
Dexploarer May 20, 2026
c096648
Merge remote-tracking branch 'origin/codex/ios-kokoro-tts-onboarding'…
Dexploarer May 20, 2026
b3ed949
Merge remote-tracking branch 'origin/develop' into codex/ios-kokoro-t…
Dexploarer May 20, 2026
875ad59
fix: address codefactor review findings
Dexploarer May 20, 2026
9c8018d
fix: format homepage files
Dexploarer May 20, 2026
4c6966c
fix: update capability naming audit self-test
Dexploarer May 20, 2026
65739d6
fix: repair remaining ci smoke failures
Dexploarer May 20, 2026
877a7a6
fix: stabilize server and inference ci gates
Dexploarer May 20, 2026
1ff4a31
fix: align dflash ios fused target tests
Dexploarer May 20, 2026
0ccfd3c
fix: align capability router guardrail tests
Dexploarer May 20, 2026
ad466ae
fix: align hearwear view parity
Dexploarer May 21, 2026
76ba012
Merge remote-tracking branch 'origin/develop' into codex/ios-kokoro-t…
May 21, 2026
e75827d
refactor: complete satellite/carrot → remote-plugin vocabulary rename
May 21, 2026
e08421b
Merge remote-tracking branch 'origin/develop' into codex/ios-kokoro-t…
May 21, 2026
3957818
fix(ci): align voice prefix onboarding with blue/white brand and lock…
May 21, 2026
fb60a29
fix(tests): correct first-party remotes path + align xr view-id parity
May 21, 2026
27c1f0e
Merge remote-tracking branch 'origin/develop' into codex/ios-kokoro-t…
May 21, 2026
fc7d8c6
fix(test): drop duplicate remotePluginId key in remote-plugin-host ev…
May 21, 2026
39a9921
fix(lint): biome auto-sort imports in electrobun host after rename
May 21, 2026
e50c98c
fix: biome lint nits, remove duplicate pluginWorkerRuntimeSrc/remoteP…
May 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/local-inference-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -167,14 +167,14 @@ jobs:
set -euo pipefail
APT_ARGS=(-o Acquire::ForceIPv4=true -o Acquire::Retries=5 -o Acquire::http::Timeout=30)
# Vulkan target additionally needs libvulkan-dev (loader headers
# path) and glslang-tools (provides glslc for the
# path), glslang-tools, and glslc for the
# Vulkan_GLSLC_EXECUTABLE flag the build script passes).
# The build script's prepareVulkanHeaders() fetches Khronos
# vulkan.hpp + spirv.hpp regardless, so the only "extra" packages
# CI needs from apt are libvulkan-dev + glslang-tools.
# CI needs from apt are libvulkan-dev + glslang-tools + glslc.
EXTRA_PKGS=""
if [[ "${{ matrix.backend }}" == "vulkan" ]]; then
EXTRA_PKGS="libvulkan-dev glslang-tools"
EXTRA_PKGS="libvulkan-dev glslang-tools glslc"
fi
for attempt in 1 2 3; do
if sudo apt-get "${APT_ARGS[@]}" update && \
Expand Down
2 changes: 1 addition & 1 deletion apps/app-xr/e2e/all-views-crud.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* opened, rendered, and closed via the agent view-host route.
*/

import { test, expect } from "@playwright/test";
import { expect, test } from "@playwright/test";

const BASE_URL = process.env.XR_BASE_URL ?? "http://localhost:31337";

Expand Down
2 changes: 1 addition & 1 deletion knip.json
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@
"src/**/*.ts",
"examples/**/*.test.ts",
"examples/**/worker.mjs",
"examples/**/carrot.json"
"examples/**/plugin.json"
],
"ignore": ["dist/**"]
},
Expand Down
31 changes: 21 additions & 10 deletions packages/agent/src/__tests__/view-memory-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ function makePlugin(
return { name: pluginName, description: `Test plugin ${pluginName}`, views };
}

function requirePluginViews(plugin: Plugin): ViewDeclaration[] {
if (!plugin.views) throw new Error(`Expected ${plugin.name} test views`);
return plugin.views;
}

/** Collect all view ids currently in the registry that start with `prefix`. */
function viewsWithPrefix(prefix: string): string[] {
return listViews({ developerMode: true })
Expand Down Expand Up @@ -101,7 +106,9 @@ describe("repeated register/unregister cycles", () => {

it("re-registering after unregister yields exactly the original view ids", async () => {
const plugin = makePlugin("bounded-plugin", 3);
const expectedIds = plugin.views?.map((v) => v.id).sort();
const expectedIds = requirePluginViews(plugin)
.map((v) => v.id)
.sort();

for (let i = 0; i < 10; i++) {
await register(plugin);
Expand All @@ -126,7 +133,7 @@ describe("module cache isolation", () => {
unregister("isolation-plugin");

const allIds = listViews({ developerMode: true }).map((e) => e.id);
for (const view of plugin.views!) {
for (const view of requirePluginViews(plugin)) {
expect(allIds).not.toContain(view.id);
}
});
Expand Down Expand Up @@ -157,7 +164,7 @@ describe("bundle URL cleanup", () => {

await register(plugin);

for (const view of plugin.views!) {
for (const view of requirePluginViews(plugin)) {
const entry = getView(view.id);
// bundleUrl is present when bundlePath is set (no real pluginDir, so
// available=false, but bundleUrl is still assigned from the path).
Expand All @@ -167,7 +174,7 @@ describe("bundle URL cleanup", () => {

unregister("bundle-plugin");

for (const view of plugin.views!) {
for (const view of requirePluginViews(plugin)) {
expect(getView(view.id)).toBeUndefined();
}
});
Expand Down Expand Up @@ -233,20 +240,23 @@ describe("WeakRef collectability after unregister", () => {
const plugin = makePlugin("weakref-plugin", 1);
await register(plugin);

const viewId = plugin.views?.[0].id;
const viewId = requirePluginViews(plugin)[0]?.id;
if (!viewId) {
throw new Error("Expected test plugin to register a view");
}
const entry = getView(viewId);
expect(entry).toBeDefined();
if (!entry) {
throw new Error("Expected registry entry");
}

// Hold a WeakRef to the entry object. After the plugin is unregistered the
// registry Map drops its reference; if no other strong refs exist the entry
// becomes eligible for collection.
// NOTE: `entry` is a local variable that is a strong reference — we must
// copy the ref and then null out all our local references to make the
// object truly unreachable.
const weakEntry = new WeakRef(entry!);
const weakEntry = new WeakRef(entry);

unregister("weakref-plugin");

Expand Down Expand Up @@ -288,7 +298,7 @@ describe("no EventEmitter listener accumulation", () => {

const listenerFns: Array<() => void> = [];

async function loadCycle(_i: number): Promise<void> {
async function loadCycle(): Promise<void> {
const fn = () => {};
listenerFns.push(fn);
emitter.on(EVENT, fn);
Expand All @@ -301,9 +311,10 @@ describe("no EventEmitter listener accumulation", () => {

const baseline = emitter.listenerCount(EVENT);

const cycles = 10;
for (let i = 0; i < cycles; i++) {
await loadCycle(i);
let remainingCycles = 10;
while (remainingCycles > 0) {
remainingCycles -= 1;
await loadCycle();
unloadCycle();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import nodePath from "node:path";
import type { IAgentRuntime, UUID } from "@elizaos/core";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
createHandler,
ensureWorkspace,
loadConfig,
} from "../../../cloud-services/coding-remote-runner/src/index.ts";
import { E2BRemoteCapabilityRouterService } from "./e2b-capability-router.ts";

const REMOTE_RUNNER_URL = "https://coding-remote-runner.test";
const REMOTE_RUNNER_TOKEN = "sat-token";

let workspaceRoot = "";
let originalFetch: typeof fetch;

function replaceGlobalFetch(fetchImpl: typeof fetch): void {
Object.defineProperty(globalThis, "fetch", {
configurable: true,
writable: true,
value: fetchImpl,
});
}

beforeEach(async () => {
workspaceRoot = await mkdtemp(
nodePath.join(tmpdir(), "agent-remote-runner-"),
);
originalFetch = globalThis.fetch;
});

afterEach(async () => {
replaceGlobalFetch(originalFetch);
await rm(workspaceRoot, { recursive: true, force: true });
});

function makeRuntime(): IAgentRuntime {
const runtime: Partial<IAgentRuntime> = {
agentId: "11111111-1111-1111-1111-111111111111" as UUID,
character: { name: "Remote runner Proof" },
getSetting: () => null,
getService: () => null,
};
return runtime as IAgentRuntime;
}

async function installCodingRemoteRunnerFetch(): Promise<void> {
const config = loadConfig({
ELIZA_CODING_WORKSPACE: workspaceRoot,
ELIZA_REMOTE_RUNNER_HTTP_TOKEN: REMOTE_RUNNER_TOKEN,
});
await ensureWorkspace(config);
const handler = createHandler(config);
const fetchMock: typeof fetch = Object.assign(
async (
input: Parameters<typeof fetch>[0],
init?: Parameters<typeof fetch>[1],
): Promise<Response> => {
const request = new Request(input, init);
const url = new URL(request.url);
if (url.origin !== REMOTE_RUNNER_URL) {
return originalFetch(input, init);
}
return handler(request);
},
{ preconnect: originalFetch.preconnect },
);
replaceGlobalFetch(fetchMock);
}

describe("E2B remote runner router with the Coding remote runner HTTP runner", () => {
it("runs coding commands through the remote runner workspace instead of the caller host", async () => {
await installCodingRemoteRunnerFetch();
const service = new E2BRemoteCapabilityRouterService(makeRuntime(), {
enabled: true,
provider: "home",
remoteHttpBaseUrl: REMOTE_RUNNER_URL,
remoteHttpToken: REMOTE_RUNNER_TOKEN,
agentRunners: ["codex", "claude-code", "opencode"],
workdir: "/workspace",
hostWorkspaceRoot: workspaceRoot,
timeoutMs: 30_000,
requestTimeoutMs: 10_000,
keepAlive: true,
allowInternetAccess: false,
envs: {},
metadata: {},
});

const result = await service.pty.runCommand({
command: "sh",
args: ["-lc", "printf remote-coded > mobile-proof.txt"],
cwd: "/workspace",
timeoutMs: 10_000,
});
const read = await service.fs.readText({ path: "mobile-proof.txt" });

expect(result.exitCode).toBe(0);
expect(result.timedOut).toBe(false);
expect(read).toMatchObject({
path: "/workspace/mobile-proof.txt",
text: "remote-coded",
truncated: false,
});
await expect(
readFile(nodePath.join(workspaceRoot, "mobile-proof.txt"), "utf8"),
).resolves.toBe("remote-coded");
});
});
35 changes: 30 additions & 5 deletions packages/agent/src/services/remote-plugin-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,18 @@ export class RemotePluginBridge {
plugin.dependencies = (descriptor.dependencies as string[]) ?? [];
}

this.attachFunctionContributions(plugin, descriptor);
this.attachServiceContributions(plugin, descriptor);
this.attachRouteContributions(plugin, descriptor);
this.attachViewContributions(plugin, descriptor);

return plugin;
}

private attachFunctionContributions(
plugin: Plugin,
descriptor: JsonObject,
): void {
const actions = descriptor.actions as
| Array<JsonObject & { name: string; handler: RemoteFunctionRef }>
| undefined;
Expand Down Expand Up @@ -206,7 +218,12 @@ export class RemotePluginBridge {
}
plugin.models = modelMap;
}
}

private attachServiceContributions(
plugin: Plugin,
descriptor: JsonObject,
): void {
// Services: opt-in via `static rpcMethods`. The descriptor carries
// one entry per service with the methods list and per-method rpc
// ids; we synthesise a ServiceClass with dynamic methods.
Expand All @@ -224,7 +241,12 @@ export class RemotePluginBridge {
this.makeServiceClassStub(svc),
) as Plugin["services"];
}
}

private attachRouteContributions(
plugin: Plugin,
descriptor: JsonObject,
): void {
// Routes: the agent's existing plugin-route lifecycle will pick
// these up. Each routeHandler is wrapped to forward
// RouteHandlerContext via worker-rpc and return RouteHandlerResult.
Expand All @@ -236,7 +258,12 @@ export class RemotePluginBridge {
.map((r) => this.makeRouteStub(r))
.filter((r): r is NonNullable<Plugin["routes"]>[number] => r !== null);
}
}

private attachViewContributions(
plugin: Plugin,
descriptor: JsonObject,
): void {
// Views/widgets/componentTypes are pure JSON metadata; pass them
// through unchanged so the existing view registry serves the
// remote plugin's bundle the same way it does direct plugins'.
Expand All @@ -248,8 +275,6 @@ export class RemotePluginBridge {
plugin.componentTypes =
descriptor.componentTypes as unknown as Plugin["componentTypes"];
}

return plugin;
}

private makeActionStub(
Expand Down Expand Up @@ -386,11 +411,11 @@ export class RemotePluginBridge {
static readonly serviceType = serviceType;
static readonly capabilityDescription = description;
readonly capabilityDescription = description;
static async start(runtime: IAgentRuntime): Promise<RemoteServiceProxy> {
const instance = new RemoteServiceProxy(runtime);
static async start(): Promise<RemoteServiceProxy> {
const instance = new RemoteServiceProxy();
return instance;
}
constructor(_runtime: IAgentRuntime) {
constructor() {
for (const method of descriptor.rpcMethods) {
const id = methodIdMap.get(method);
if (!id) continue;
Expand Down
12 changes: 12 additions & 0 deletions packages/agent/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,18 @@
"@elizaos/plugin-registry": [
"../../plugins/plugin-registry/src/index.ts"
],
"@elizaos/plugin-remote-manifest": [
"../plugin-remote-manifest/src/index.ts"
],
"@elizaos/plugin-remote-manifest/*": [
"../plugin-remote-manifest/src/*"
],
"@elizaos/plugin-worker-runtime": [
"../plugin-worker-runtime/src/index.ts"
],
"@elizaos/plugin-worker-runtime/*": [
"../plugin-worker-runtime/src/*"
],
"@elizaos/plugin-wallet": ["../../plugins/plugin-wallet/src/index.ts"],
"@elizaos/plugin-wifi": ["../../plugins/plugin-wifi/src/index.ts"],
"@elizaos/app-core": ["../app-core/src/index.ts"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,6 @@
android:value="local-agent-runtime" />
</service>

<!--
Continuous-chat foreground service (R10 §6.2). Owns the long-
lived microphone capture path so VAD-gated / always-on chat
survives screen lock + backgrounding. foregroundServiceType
"microphone" is required on API 34+ alongside the
FOREGROUND_SERVICE_MICROPHONE permission.
-->
<service
android:name=".ElizaVoiceCaptureService"
android:exported="false"
Expand Down Expand Up @@ -137,12 +130,10 @@
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths"></meta-data>
android:resource="@xml/file_paths" />
</provider>
</application>

<!-- Permissions -->

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
Expand All @@ -157,10 +148,6 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
<!--
ElizaVoiceCaptureService runs continuous-chat background mic capture
(R10 §6.2). Required on API 34+ alongside RECORD_AUDIO.
-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
Expand Down
Loading
Loading