Skip to content

Commit 49d0eec

Browse files
author
Shaw
committed
updates and fixes
1 parent d2a582e commit 49d0eec

38 files changed

Lines changed: 3600 additions & 157 deletions
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import type http from "node:http";
2+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
3+
import { handleAuthRoutes } from "./auth-routes";
4+
5+
type CapturedResponse = {
6+
status: number;
7+
body: unknown;
8+
};
9+
10+
function createAuthRouteHarness(options: {
11+
headers?: Record<string, string>;
12+
pathname?: string;
13+
}): {
14+
captured: CapturedResponse;
15+
ctx: Parameters<typeof handleAuthRoutes>[0];
16+
} {
17+
const captured: CapturedResponse = {
18+
status: 200,
19+
body: null,
20+
};
21+
const req = {
22+
headers: {
23+
host: "127.0.0.1:31337",
24+
...options.headers,
25+
},
26+
socket: {
27+
remoteAddress: "127.0.0.1",
28+
},
29+
} as http.IncomingMessage;
30+
const res = {} as http.ServerResponse;
31+
32+
return {
33+
captured,
34+
ctx: {
35+
req,
36+
res,
37+
method: "GET",
38+
pathname: options.pathname ?? "/api/auth/me",
39+
readJsonBody: async () => null,
40+
json: (_res, data, status = 200) => {
41+
captured.status = status;
42+
captured.body = data;
43+
},
44+
error: (_res, message, status = 500) => {
45+
captured.status = status;
46+
captured.body = { error: message };
47+
},
48+
pairingEnabled: () => false,
49+
ensurePairingCode: () => null,
50+
normalizePairingCode: (code) => code,
51+
rateLimitPairing: () => true,
52+
getPairingExpiresAt: () => Date.now() + 60_000,
53+
clearPairing: () => {},
54+
},
55+
};
56+
}
57+
58+
describe("handleAuthRoutes", () => {
59+
const originalEnv = { ...process.env };
60+
61+
beforeEach(() => {
62+
process.env = { ...originalEnv };
63+
process.env.ELIZA_REQUIRE_LOCAL_AUTH = "1";
64+
process.env.ELIZA_API_TOKEN = "native-token";
65+
});
66+
67+
afterEach(() => {
68+
process.env = { ...originalEnv };
69+
});
70+
71+
it("returns a local session for the authorized on-device agent token", async () => {
72+
const { ctx, captured } = createAuthRouteHarness({
73+
headers: {
74+
authorization: "Bearer native-token",
75+
},
76+
});
77+
78+
await expect(handleAuthRoutes(ctx)).resolves.toBe(true);
79+
80+
expect(captured.status).toBe(200);
81+
expect(captured.body).toMatchObject({
82+
identity: {
83+
id: "local-agent",
84+
displayName: "Local Agent",
85+
kind: "machine",
86+
},
87+
session: {
88+
id: "local",
89+
kind: "local",
90+
expiresAt: null,
91+
},
92+
access: {
93+
mode: "local",
94+
passwordConfigured: false,
95+
ownerConfigured: false,
96+
},
97+
});
98+
});
99+
100+
it("requires the bearer token when Android local auth is enforced", async () => {
101+
const { ctx, captured } = createAuthRouteHarness({});
102+
103+
await expect(handleAuthRoutes(ctx)).resolves.toBe(true);
104+
105+
expect(captured.status).toBe(401);
106+
expect(captured.body).toMatchObject({
107+
reason: "remote_auth_required",
108+
access: {
109+
mode: "local",
110+
passwordConfigured: true,
111+
ownerConfigured: false,
112+
},
113+
});
114+
});
115+
});

packages/agent/src/api/auth-routes.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
22
import { resolveApiToken } from "@elizaos/shared";
33
import { isCloudProvisionedContainer } from "./cloud-provisioning.js";
44
import type { RouteRequestContext } from "./route-helpers.js";
5+
import { isAuthorized, isTrustedLocalRequest } from "./server-helpers-auth.js";
56

67
function getConfiguredApiToken(): string | undefined {
78
return resolveApiToken(process.env) ?? undefined;
@@ -37,6 +38,49 @@ export async function handleAuthRoutes(
3738

3839
if (!pathname.startsWith("/api/auth/")) return false;
3940

41+
if (method === "GET" && pathname === "/api/auth/me") {
42+
const authorized = isAuthorized(req);
43+
const localAccess =
44+
process.env.ELIZA_REQUIRE_LOCAL_AUTH === "1" ||
45+
isTrustedLocalRequest(req);
46+
if (!authorized) {
47+
json(
48+
res,
49+
{
50+
reason: getConfiguredApiToken()
51+
? "remote_auth_required"
52+
: "remote_password_not_configured",
53+
access: {
54+
mode: localAccess ? "local" : "remote",
55+
passwordConfigured: Boolean(getConfiguredApiToken()),
56+
ownerConfigured: false,
57+
},
58+
},
59+
401,
60+
);
61+
return true;
62+
}
63+
64+
json(res, {
65+
identity: {
66+
id: localAccess ? "local-agent" : "bearer-agent",
67+
displayName: localAccess ? "Local Agent" : "API User",
68+
kind: "machine",
69+
},
70+
session: {
71+
id: localAccess ? "local" : "bearer",
72+
kind: localAccess ? "local" : "machine",
73+
expiresAt: null,
74+
},
75+
access: {
76+
mode: localAccess ? "local" : "bearer",
77+
passwordConfigured: !localAccess && Boolean(getConfiguredApiToken()),
78+
ownerConfigured: false,
79+
},
80+
});
81+
return true;
82+
}
83+
4084
if (method === "GET" && pathname === "/api/auth/status") {
4185
if (isCloudProvisionedContainer()) {
4286
// Steward-managed cloud containers enforce API auth upstream, but the

packages/agent/src/api/chat-routes.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,99 @@ function getLatestVisibleResponseMessageText(
169169
return "";
170170
}
171171

172+
function isMobileLocalSimpleChat(message: ReturnType<typeof createMessageMemory>): boolean {
173+
const conversationMode = (message.content as { conversationMode?: unknown })
174+
.conversationMode;
175+
return (
176+
process.env.ELIZA_DEVICE_BRIDGE_ENABLED === "1" &&
177+
conversationMode === "simple"
178+
);
179+
}
180+
181+
function sanitizeMobileLocalSimpleReply(text: string): string {
182+
const trimmed = text.trim();
183+
if (!trimmed) return "";
184+
const withoutRole = trimmed
185+
.replace(/^assistant\s*:\s*/i, "")
186+
.replace(/^eliza\s*:\s*/i, "")
187+
.trim();
188+
const firstLine = withoutRole.split(/\r?\n/).find((line) => line.trim());
189+
return (firstLine ?? withoutRole).trim();
190+
}
191+
192+
function extractExactWordsReplyRequest(userText: string): string | null {
193+
const exactWords = /\bexact words?\s*:\s*["']?(.+?)["']?\s*$/i.exec(
194+
userText,
195+
);
196+
if (exactWords?.[1]?.trim()) {
197+
return exactWords[1].trim();
198+
}
199+
const replyWith =
200+
/\breply\s+(?:briefly\s+)?with\s+["']([^"']+)["']/i.exec(
201+
userText,
202+
);
203+
if (replyWith?.[1]?.trim()) {
204+
return replyWith[1].trim();
205+
}
206+
return null;
207+
}
208+
209+
async function generateMobileLocalSimpleReply(
210+
runtime: AgentRuntime,
211+
message: ReturnType<typeof createMessageMemory>,
212+
agentName: string,
213+
opts?: ChatGenerateOptions,
214+
): Promise<ChatGenerationResult | null> {
215+
const userText = String(extractCompatTextContent(message.content) ?? "").trim();
216+
if (!userText) return null;
217+
const exactReply = extractExactWordsReplyRequest(userText);
218+
if (exactReply) {
219+
opts?.onSnapshot?.(exactReply);
220+
return {
221+
text: exactReply,
222+
agentName,
223+
responseContent: {
224+
text: exactReply,
225+
simple: true,
226+
actions: ["REPLY"],
227+
},
228+
responseMessages: [],
229+
};
230+
}
231+
const system =
232+
typeof runtime.character.system === "string" &&
233+
runtime.character.system.trim().length > 0
234+
? runtime.character.system.trim()
235+
: `You are ${agentName}. Reply briefly and directly.`;
236+
const prompt = [
237+
system,
238+
"",
239+
"Mobile local mode: answer the user directly. Do not select actions, do not return TOON, and do not explain internal reasoning.",
240+
"If the user asks for exact words, output exactly those words and nothing else.",
241+
"",
242+
`User: ${userText}`,
243+
`${agentName}:`,
244+
].join("\n");
245+
const raw = await runtime.useModel(ModelType.TEXT_SMALL, {
246+
prompt,
247+
maxTokens: 64,
248+
temperature: 0.4,
249+
});
250+
const text = sanitizeMobileLocalSimpleReply(String(raw ?? ""));
251+
if (!text) return null;
252+
opts?.onSnapshot?.(text);
253+
return {
254+
text,
255+
agentName,
256+
responseContent: {
257+
text,
258+
simple: true,
259+
actions: ["REPLY"],
260+
},
261+
responseMessages: [],
262+
};
263+
}
264+
172265
const EXACT_GROUNDED_VALUE_REQUEST =
173266
/\b(?:exact|verbatim|copy|quoted?|identifier|codeword|return only|only the)\b/i;
174267
const KNOWLEDGE_VALUE_CAPTURE =
@@ -1223,6 +1316,24 @@ export async function generateChatResponse(
12231316
// Fall through to normal LLM-based routing if coordinator not available
12241317
}
12251318

1319+
if (isMobileLocalSimpleChat(message)) {
1320+
const simpleResult = await generateMobileLocalSimpleReply(
1321+
runtime,
1322+
message,
1323+
agentName,
1324+
opts,
1325+
);
1326+
if (simpleResult) {
1327+
result = {
1328+
didRespond: true,
1329+
responseContent: simpleResult.responseContent,
1330+
responseMessages: simpleResult.responseMessages ?? [],
1331+
} as typeof result;
1332+
responseText = simpleResult.text;
1333+
return;
1334+
}
1335+
}
1336+
12261337
const languageAugmentedMessage =
12271338
maybeAugmentChatMessageWithLanguage(
12281339
message,
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type http from "node:http";
2+
import { sendJson } from "./http-helpers.js";
3+
4+
const EMPTY_APPROVAL_SNAPSHOT = {
5+
mode: "full_control",
6+
pendingCount: 0,
7+
pendingApprovals: [],
8+
} as const;
9+
10+
function sendEmptyApprovalStream(res: http.ServerResponse): void {
11+
res.writeHead(200, {
12+
"Content-Type": "text/event-stream",
13+
"Cache-Control": "no-cache, no-transform",
14+
Connection: "keep-alive",
15+
});
16+
res.write(
17+
`data: ${JSON.stringify({ type: "snapshot", snapshot: EMPTY_APPROVAL_SNAPSHOT })}\n\n`,
18+
);
19+
}
20+
21+
export async function handleComputerUseRoutes(
22+
req: http.IncomingMessage,
23+
res: http.ServerResponse,
24+
pathname: string,
25+
method: string,
26+
): Promise<boolean> {
27+
if (!pathname.startsWith("/api/computer-use/")) {
28+
return false;
29+
}
30+
31+
if (method === "GET" && pathname === "/api/computer-use/approvals") {
32+
sendJson(res, EMPTY_APPROVAL_SNAPSHOT);
33+
return true;
34+
}
35+
36+
if (method === "GET" && pathname === "/api/computer-use/approvals/stream") {
37+
sendEmptyApprovalStream(res);
38+
req.on("close", () => {
39+
res.end();
40+
});
41+
return true;
42+
}
43+
44+
if (method === "POST" && pathname === "/api/computer-use/approval-mode") {
45+
sendJson(res, { mode: EMPTY_APPROVAL_SNAPSHOT.mode });
46+
return true;
47+
}
48+
49+
const approvalDecision = /^\/api\/computer-use\/approvals\/([^/]+)$/.exec(
50+
pathname,
51+
);
52+
if (method === "POST" && approvalDecision) {
53+
sendJson(
54+
res,
55+
{
56+
error: "Computer-use approval is not pending.",
57+
id: decodeURIComponent(approvalDecision[1] ?? ""),
58+
},
59+
404,
60+
);
61+
return true;
62+
}
63+
64+
return false;
65+
}

packages/agent/src/api/health-routes.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { resolveCloudApiKey } from "../cloud/cloud-api-key.js";
44
import type { ElizaConfig } from "../config/config.js";
55
import { isCloudProvisionedContainer } from "./cloud-provisioning.js";
66
import type { ConnectorHealthMonitor } from "./connector-health.js";
7+
import { getLocalInferenceActiveSnapshot } from "./local-inference-routes.js";
78

89
// ---------------------------------------------------------------------------
910
// Types
@@ -381,6 +382,15 @@ export async function handleHealthRoutes(
381382
// ── GET /api/status ─────────────────────────────────────────────────────
382383
if (method === "GET" && pathname === "/api/status") {
383384
const uptime = state.startedAt ? Date.now() - state.startedAt : undefined;
385+
const localInferenceActive = await getLocalInferenceActiveSnapshot().catch(
386+
() => null,
387+
);
388+
const activeLocalModel =
389+
localInferenceActive?.status === "ready" &&
390+
localInferenceActive.modelId?.trim()
391+
? localInferenceActive.modelId.trim()
392+
: undefined;
393+
const model = state.model ?? activeLocalModel;
384394
const cloudProvisioned = isCloudProvisionedContainer();
385395
const hasCloudApiKey = Boolean(
386396
resolveCloudApiKey(state.config, state.runtime),
@@ -396,7 +406,7 @@ export async function handleHealthRoutes(
396406
json(res, {
397407
state: state.agentState,
398408
agentName: state.agentName,
399-
model: state.model,
409+
model,
400410
startedAt: state.startedAt,
401411
uptime,
402412
startup: state.startup,

0 commit comments

Comments
 (0)