Skip to content

Commit e7a2c00

Browse files
author
Shaw
committed
Merge branch 'pr-7850' into develop
PR #7850: mobile local voice + sandbox routing (carrot→remote rename) Conflict resolutions: - AndroidManifest.xml: kept HEAD (boot receiver + activities + FileProvider already declared); dropped pr-7850 duplicate GatewayConnectionService/ ElizaAgentService and second FileProvider block - service-type-collisions.test.ts: kept pr-7850 (adds capability-router collision allowlist + new xr-session phrasing) - remote-plugin-host.ts: kept pr-7850 store-dir env keys (REMOTE first, CARROT fallback for backwards compat) - App.tsx: kept pr-7850 refactor (renderViewRouterContent helper + pathForNavigateViewDetail/directTabForNavigateView/etc helpers) - phase-4-bake-milady-app.md, hello-carrot/README.md: deleted (HEAD/PR divergent delete-modify on both sides)
2 parents 20a64a6 + e50c98c commit e7a2c00

113 files changed

Lines changed: 4296 additions & 3537 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/local-inference-matrix.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,14 +167,14 @@ jobs:
167167
set -euo pipefail
168168
APT_ARGS=(-o Acquire::ForceIPv4=true -o Acquire::Retries=5 -o Acquire::http::Timeout=30)
169169
# Vulkan target additionally needs libvulkan-dev (loader headers
170-
# path) and glslang-tools (provides glslc for the
170+
# path), glslang-tools, and glslc for the
171171
# Vulkan_GLSLC_EXECUTABLE flag the build script passes).
172172
# The build script's prepareVulkanHeaders() fetches Khronos
173173
# vulkan.hpp + spirv.hpp regardless, so the only "extra" packages
174-
# CI needs from apt are libvulkan-dev + glslang-tools.
174+
# CI needs from apt are libvulkan-dev + glslang-tools + glslc.
175175
EXTRA_PKGS=""
176176
if [[ "${{ matrix.backend }}" == "vulkan" ]]; then
177-
EXTRA_PKGS="libvulkan-dev glslang-tools"
177+
EXTRA_PKGS="libvulkan-dev glslang-tools glslc"
178178
fi
179179
for attempt in 1 2 3; do
180180
if sudo apt-get "${APT_ARGS[@]}" update && \

apps/app-xr/e2e/all-views-crud.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* opened, rendered, and closed via the agent view-host route.
66
*/
77

8-
import { test, expect } from "@playwright/test";
8+
import { expect, test } from "@playwright/test";
99

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

knip.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -511,7 +511,7 @@
511511
"src/**/*.ts",
512512
"examples/**/*.test.ts",
513513
"examples/**/worker.mjs",
514-
"examples/**/carrot.json"
514+
"examples/**/plugin.json"
515515
],
516516
"ignore": ["dist/**"]
517517
},

packages/agent/src/__tests__/view-memory-lifecycle.test.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ function makePlugin(
4242
return { name: pluginName, description: `Test plugin ${pluginName}`, views };
4343
}
4444

45+
function requirePluginViews(plugin: Plugin): ViewDeclaration[] {
46+
if (!plugin.views) throw new Error(`Expected ${plugin.name} test views`);
47+
return plugin.views;
48+
}
49+
4550
/** Collect all view ids currently in the registry that start with `prefix`. */
4651
function viewsWithPrefix(prefix: string): string[] {
4752
return listViews({ developerMode: true })
@@ -101,7 +106,9 @@ describe("repeated register/unregister cycles", () => {
101106

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

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

128135
const allIds = listViews({ developerMode: true }).map((e) => e.id);
129-
for (const view of plugin.views!) {
136+
for (const view of requirePluginViews(plugin)) {
130137
expect(allIds).not.toContain(view.id);
131138
}
132139
});
@@ -157,7 +164,7 @@ describe("bundle URL cleanup", () => {
157164

158165
await register(plugin);
159166

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

168175
unregister("bundle-plugin");
169176

170-
for (const view of plugin.views!) {
177+
for (const view of requirePluginViews(plugin)) {
171178
expect(getView(view.id)).toBeUndefined();
172179
}
173180
});
@@ -233,20 +240,23 @@ describe("WeakRef collectability after unregister", () => {
233240
const plugin = makePlugin("weakref-plugin", 1);
234241
await register(plugin);
235242

236-
const viewId = plugin.views?.[0].id;
243+
const viewId = requirePluginViews(plugin)[0]?.id;
237244
if (!viewId) {
238245
throw new Error("Expected test plugin to register a view");
239246
}
240247
const entry = getView(viewId);
241248
expect(entry).toBeDefined();
249+
if (!entry) {
250+
throw new Error("Expected registry entry");
251+
}
242252

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

251261
unregister("weakref-plugin");
252262

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

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

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

302312
const baseline = emitter.listenerCount(EVENT);
303313

304-
const cycles = 10;
305-
for (let i = 0; i < cycles; i++) {
306-
await loadCycle(i);
314+
let remainingCycles = 10;
315+
while (remainingCycles > 0) {
316+
remainingCycles -= 1;
317+
await loadCycle();
307318
unloadCycle();
308319
}
309320

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { mkdtemp, readFile, rm } from "node:fs/promises";
2+
import { tmpdir } from "node:os";
3+
import nodePath from "node:path";
4+
import type { IAgentRuntime, UUID } from "@elizaos/core";
5+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
6+
import {
7+
createHandler,
8+
ensureWorkspace,
9+
loadConfig,
10+
} from "../../../cloud-services/coding-remote-runner/src/index.ts";
11+
import { E2BRemoteCapabilityRouterService } from "./e2b-capability-router.ts";
12+
13+
const REMOTE_RUNNER_URL = "https://coding-remote-runner.test";
14+
const REMOTE_RUNNER_TOKEN = "sat-token";
15+
16+
let workspaceRoot = "";
17+
let originalFetch: typeof fetch;
18+
19+
function replaceGlobalFetch(fetchImpl: typeof fetch): void {
20+
Object.defineProperty(globalThis, "fetch", {
21+
configurable: true,
22+
writable: true,
23+
value: fetchImpl,
24+
});
25+
}
26+
27+
beforeEach(async () => {
28+
workspaceRoot = await mkdtemp(
29+
nodePath.join(tmpdir(), "agent-remote-runner-"),
30+
);
31+
originalFetch = globalThis.fetch;
32+
});
33+
34+
afterEach(async () => {
35+
replaceGlobalFetch(originalFetch);
36+
await rm(workspaceRoot, { recursive: true, force: true });
37+
});
38+
39+
function makeRuntime(): IAgentRuntime {
40+
const runtime: Partial<IAgentRuntime> = {
41+
agentId: "11111111-1111-1111-1111-111111111111" as UUID,
42+
character: { name: "Remote runner Proof" },
43+
getSetting: () => null,
44+
getService: () => null,
45+
};
46+
return runtime as IAgentRuntime;
47+
}
48+
49+
async function installCodingRemoteRunnerFetch(): Promise<void> {
50+
const config = loadConfig({
51+
ELIZA_CODING_WORKSPACE: workspaceRoot,
52+
ELIZA_REMOTE_RUNNER_HTTP_TOKEN: REMOTE_RUNNER_TOKEN,
53+
});
54+
await ensureWorkspace(config);
55+
const handler = createHandler(config);
56+
const fetchMock: typeof fetch = Object.assign(
57+
async (
58+
input: Parameters<typeof fetch>[0],
59+
init?: Parameters<typeof fetch>[1],
60+
): Promise<Response> => {
61+
const request = new Request(input, init);
62+
const url = new URL(request.url);
63+
if (url.origin !== REMOTE_RUNNER_URL) {
64+
return originalFetch(input, init);
65+
}
66+
return handler(request);
67+
},
68+
{ preconnect: originalFetch.preconnect },
69+
);
70+
replaceGlobalFetch(fetchMock);
71+
}
72+
73+
describe("E2B remote runner router with the Coding remote runner HTTP runner", () => {
74+
it("runs coding commands through the remote runner workspace instead of the caller host", async () => {
75+
await installCodingRemoteRunnerFetch();
76+
const service = new E2BRemoteCapabilityRouterService(makeRuntime(), {
77+
enabled: true,
78+
provider: "home",
79+
remoteHttpBaseUrl: REMOTE_RUNNER_URL,
80+
remoteHttpToken: REMOTE_RUNNER_TOKEN,
81+
agentRunners: ["codex", "claude-code", "opencode"],
82+
workdir: "/workspace",
83+
hostWorkspaceRoot: workspaceRoot,
84+
timeoutMs: 30_000,
85+
requestTimeoutMs: 10_000,
86+
keepAlive: true,
87+
allowInternetAccess: false,
88+
envs: {},
89+
metadata: {},
90+
});
91+
92+
const result = await service.pty.runCommand({
93+
command: "sh",
94+
args: ["-lc", "printf remote-coded > mobile-proof.txt"],
95+
cwd: "/workspace",
96+
timeoutMs: 10_000,
97+
});
98+
const read = await service.fs.readText({ path: "mobile-proof.txt" });
99+
100+
expect(result.exitCode).toBe(0);
101+
expect(result.timedOut).toBe(false);
102+
expect(read).toMatchObject({
103+
path: "/workspace/mobile-proof.txt",
104+
text: "remote-coded",
105+
truncated: false,
106+
});
107+
await expect(
108+
readFile(nodePath.join(workspaceRoot, "mobile-proof.txt"), "utf8"),
109+
).resolves.toBe("remote-coded");
110+
});
111+
});

packages/agent/src/services/remote-plugin-bridge.ts

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,18 @@ export class RemotePluginBridge {
165165
plugin.dependencies = (descriptor.dependencies as string[]) ?? [];
166166
}
167167

168+
this.attachFunctionContributions(plugin, descriptor);
169+
this.attachServiceContributions(plugin, descriptor);
170+
this.attachRouteContributions(plugin, descriptor);
171+
this.attachViewContributions(plugin, descriptor);
172+
173+
return plugin;
174+
}
175+
176+
private attachFunctionContributions(
177+
plugin: Plugin,
178+
descriptor: JsonObject,
179+
): void {
168180
const actions = descriptor.actions as
169181
| Array<JsonObject & { name: string; handler: RemoteFunctionRef }>
170182
| undefined;
@@ -206,7 +218,12 @@ export class RemotePluginBridge {
206218
}
207219
plugin.models = modelMap;
208220
}
221+
}
209222

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

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

263+
private attachViewContributions(
264+
plugin: Plugin,
265+
descriptor: JsonObject,
266+
): void {
240267
// Views/widgets/componentTypes are pure JSON metadata; pass them
241268
// through unchanged so the existing view registry serves the
242269
// remote plugin's bundle the same way it does direct plugins'.
@@ -248,8 +275,6 @@ export class RemotePluginBridge {
248275
plugin.componentTypes =
249276
descriptor.componentTypes as unknown as Plugin["componentTypes"];
250277
}
251-
252-
return plugin;
253278
}
254279

255280
private makeActionStub(
@@ -386,11 +411,11 @@ export class RemotePluginBridge {
386411
static readonly serviceType = serviceType;
387412
static readonly capabilityDescription = description;
388413
readonly capabilityDescription = description;
389-
static async start(runtime: IAgentRuntime): Promise<RemoteServiceProxy> {
390-
const instance = new RemoteServiceProxy(runtime);
414+
static async start(): Promise<RemoteServiceProxy> {
415+
const instance = new RemoteServiceProxy();
391416
return instance;
392417
}
393-
constructor(_runtime: IAgentRuntime) {
418+
constructor() {
394419
for (const method of descriptor.rpcMethods) {
395420
const id = methodIdMap.get(method);
396421
if (!id) continue;

packages/agent/tsconfig.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,18 @@
5050
"@elizaos/plugin-registry": [
5151
"../../plugins/plugin-registry/src/index.ts"
5252
],
53+
"@elizaos/plugin-remote-manifest": [
54+
"../plugin-remote-manifest/src/index.ts"
55+
],
56+
"@elizaos/plugin-remote-manifest/*": [
57+
"../plugin-remote-manifest/src/*"
58+
],
59+
"@elizaos/plugin-worker-runtime": [
60+
"../plugin-worker-runtime/src/index.ts"
61+
],
62+
"@elizaos/plugin-worker-runtime/*": [
63+
"../plugin-worker-runtime/src/*"
64+
],
5365
"@elizaos/plugin-wallet": ["../../plugins/plugin-wallet/src/index.ts"],
5466
"@elizaos/plugin-wifi": ["../../plugins/plugin-wifi/src/index.ts"],
5567
"@elizaos/app-core": ["../app-core/src/index.ts"],

packages/app-core/platforms/android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@
196196
<data android:scheme="mmsto" />
197197
</intent-filter>
198198
</activity>
199-
199+
200200
<receiver
201201
android:name="ai.elizaos.app.ElizaBootReceiver"
202202
android:directBootAware="true"
@@ -340,8 +340,6 @@
340340
</activity>
341341
</application>
342342

343-
<!-- Permissions -->
344-
345343
<uses-permission android:name="android.permission.INTERNET" />
346344
<uses-permission android:name="android.permission.RECORD_AUDIO" />
347345
<uses-permission android:name="android.permission.CAMERA" />
@@ -356,10 +354,6 @@
356354
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
357355
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
358356
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
359-
<!--
360-
ElizaVoiceCaptureService runs continuous-chat background mic capture
361-
(R10 §6.2). Required on API 34+ alongside RECORD_AUDIO.
362-
-->
363357
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />
364358
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
365359
<uses-permission

packages/app-core/platforms/electrobun/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ Produces `src/libMacWindowEffects.dylib` (consumed via Bun FFI at runtime).
3737

3838
## First-party Remotes
3939

40-
Prototype ElizaLaunch Remotes are folded into this shell under `remotes/` and seeded on desktop startup by `src/first-party-remotes.ts`. They use the existing `@elizaos/electrobun-carrots` install/start/log runtime instead of a parallel module system.
40+
Prototype ElizaLaunch Remotes are folded into this shell under `remotes/` and seeded on desktop startup by `src/first-party-remotes.ts`. They use the existing `@elizaos/plugin-remote-manifest` install/start/log runtime instead of a parallel module system.
4141

4242
- `eliza.runtime` is required and runs as a Runtime Remote adapter over the existing Electrobun `AgentManager`; production mode must not start a second elizaOS runtime process.
4343
- `eliza.fs`, `eliza.local-model`, `eliza.pty`, and `eliza.git` are first-party capability Remotes.
4444
- `eliza.surface` is a dev/admin surface and is only included when `ELIZA_ENABLE_DEV_REMOTES=1`.
4545

46-
The current worker-to-worker bridge supports the upstream `invoke-carrot` host request, and renderer/dev views can call workers through the typed `carrot:invokeWorker` RPC. Broad automatic event broadcast is still a host-level follow-up, so dev surfaces should use explicit invokes plus polling where needed.
46+
The current worker-to-worker bridge supports the upstream `invoke-remote-plugin` host request, and renderer/dev views can call workers through the typed `remote-plugin:invokeWorker` RPC. Broad automatic event broadcast is still a host-level follow-up, so dev surfaces should use explicit invokes plus polling where needed.
4747

4848
`eliza.surface` is not the product dashboard. Keep it as an inspector harness while product work moves toward dynamic agent-created canvas/A2UI views, Eliza-1 routing, voice loop latency tracing, and OmniVoice/Kokoro validation.
4949

0 commit comments

Comments
 (0)