chore: format sources and update protocol outputs
This commit is contained in:
@@ -845,42 +845,67 @@ public struct ConfigGetParams: Codable, Sendable {
|
||||
|
||||
public struct ConfigSetParams: Codable, Sendable {
|
||||
public let raw: String
|
||||
public let basehash: String?
|
||||
|
||||
public init(
|
||||
raw: String
|
||||
raw: String,
|
||||
basehash: String?
|
||||
) {
|
||||
self.raw = raw
|
||||
self.basehash = basehash
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case raw
|
||||
case basehash = "baseHash"
|
||||
}
|
||||
}
|
||||
|
||||
public struct ConfigApplyParams: Codable, Sendable {
|
||||
public let raw: String
|
||||
public let basehash: String?
|
||||
public let sessionkey: String?
|
||||
public let note: String?
|
||||
public let restartdelayms: Int?
|
||||
|
||||
public init(
|
||||
raw: String,
|
||||
basehash: String?,
|
||||
sessionkey: String?,
|
||||
note: String?,
|
||||
restartdelayms: Int?
|
||||
) {
|
||||
self.raw = raw
|
||||
self.basehash = basehash
|
||||
self.sessionkey = sessionkey
|
||||
self.note = note
|
||||
self.restartdelayms = restartdelayms
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case raw
|
||||
case basehash = "baseHash"
|
||||
case sessionkey = "sessionKey"
|
||||
case note
|
||||
case restartdelayms = "restartDelayMs"
|
||||
}
|
||||
}
|
||||
|
||||
public struct ConfigPatchParams: Codable, Sendable {
|
||||
public let raw: String
|
||||
public let basehash: String?
|
||||
|
||||
public init(
|
||||
raw: String,
|
||||
basehash: String?
|
||||
) {
|
||||
self.raw = raw
|
||||
self.basehash = basehash
|
||||
}
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case raw
|
||||
case basehash = "baseHash"
|
||||
}
|
||||
}
|
||||
|
||||
public struct ConfigSchemaParams: Codable, Sendable {
|
||||
}
|
||||
|
||||
|
||||
@@ -24,9 +24,9 @@ describe("sanitizeSessionHistory (google thinking)", () => {
|
||||
sessionId: "session:google",
|
||||
});
|
||||
|
||||
const assistant = out.find(
|
||||
(msg) => (msg as { role?: string }).role === "assistant",
|
||||
) as { content?: Array<{ type?: string; text?: string }> };
|
||||
const assistant = out.find((msg) => (msg as { role?: string }).role === "assistant") as {
|
||||
content?: Array<{ type?: string; text?: string }>;
|
||||
};
|
||||
expect(assistant.content?.map((block) => block.type)).toEqual(["text"]);
|
||||
expect(assistant.content?.[0]?.text).toBe("reasoning");
|
||||
});
|
||||
@@ -51,9 +51,9 @@ describe("sanitizeSessionHistory (google thinking)", () => {
|
||||
sessionId: "session:google",
|
||||
});
|
||||
|
||||
const assistant = out.find(
|
||||
(msg) => (msg as { role?: string }).role === "assistant",
|
||||
) as { content?: Array<{ type?: string; thinking?: string; thinkingSignature?: string }> };
|
||||
const assistant = out.find((msg) => (msg as { role?: string }).role === "assistant") as {
|
||||
content?: Array<{ type?: string; thinking?: string; thinkingSignature?: string }>;
|
||||
};
|
||||
expect(assistant.content?.map((block) => block.type)).toEqual(["thinking"]);
|
||||
expect(assistant.content?.[0]?.thinking).toBe("reasoning");
|
||||
expect(assistant.content?.[0]?.thinkingSignature).toBe("sig");
|
||||
@@ -83,9 +83,9 @@ describe("sanitizeSessionHistory (google thinking)", () => {
|
||||
sessionId: "session:google-mixed",
|
||||
});
|
||||
|
||||
const assistant = out.find(
|
||||
(msg) => (msg as { role?: string }).role === "assistant",
|
||||
) as { content?: Array<{ type?: string; text?: string }> };
|
||||
const assistant = out.find((msg) => (msg as { role?: string }).role === "assistant") as {
|
||||
content?: Array<{ type?: string; text?: string }>;
|
||||
};
|
||||
expect(assistant.content?.map((block) => block.type)).toEqual(["text", "text", "text"]);
|
||||
expect(assistant.content?.[1]?.text).toBe("internal note");
|
||||
});
|
||||
@@ -113,9 +113,9 @@ describe("sanitizeSessionHistory (google thinking)", () => {
|
||||
sessionId: "session:google-mixed-signatures",
|
||||
});
|
||||
|
||||
const assistant = out.find(
|
||||
(msg) => (msg as { role?: string }).role === "assistant",
|
||||
) as { content?: Array<{ type?: string; thinking?: string; text?: string }> };
|
||||
const assistant = out.find((msg) => (msg as { role?: string }).role === "assistant") as {
|
||||
content?: Array<{ type?: string; thinking?: string; text?: string }>;
|
||||
};
|
||||
expect(assistant.content?.map((block) => block.type)).toEqual(["thinking", "text"]);
|
||||
expect(assistant.content?.[0]?.thinking).toBe("signed");
|
||||
expect(assistant.content?.[1]?.text).toBe("unsigned");
|
||||
@@ -141,9 +141,7 @@ describe("sanitizeSessionHistory (google thinking)", () => {
|
||||
sessionId: "session:google-empty",
|
||||
});
|
||||
|
||||
const assistant = out.find(
|
||||
(msg) => (msg as { role?: string }).role === "assistant",
|
||||
);
|
||||
const assistant = out.find((msg) => (msg as { role?: string }).role === "assistant");
|
||||
expect(assistant).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -167,9 +165,9 @@ describe("sanitizeSessionHistory (google thinking)", () => {
|
||||
sessionId: "session:openai",
|
||||
});
|
||||
|
||||
const assistant = out.find(
|
||||
(msg) => (msg as { role?: string }).role === "assistant",
|
||||
) as { content?: Array<{ type?: string }> };
|
||||
const assistant = out.find((msg) => (msg as { role?: string }).role === "assistant") as {
|
||||
content?: Array<{ type?: string }>;
|
||||
};
|
||||
expect(assistant.content?.map((block) => block.type)).toEqual(["thinking"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -299,9 +299,7 @@ async function waitForSubagentCompletion(runId: string, waitTimeoutMs: number) {
|
||||
mutated = true;
|
||||
}
|
||||
entry.outcome =
|
||||
wait.status === "error"
|
||||
? { status: "error", error: wait.error }
|
||||
: { status: "ok" };
|
||||
wait.status === "error" ? { status: "error", error: wait.error } : { status: "ok" };
|
||||
mutated = true;
|
||||
if (mutated) persistSubagentRuns();
|
||||
if (!beginSubagentAnnounce(runId)) return;
|
||||
|
||||
@@ -138,10 +138,7 @@ export function createGatewayTool(opts?: {
|
||||
} else {
|
||||
const rawSnapshot = (snapshot as { raw?: unknown }).raw;
|
||||
if (typeof rawSnapshot === "string") {
|
||||
baseHash = crypto
|
||||
.createHash("sha256")
|
||||
.update(rawSnapshot)
|
||||
.digest("hex");
|
||||
baseHash = crypto.createHash("sha256").update(rawSnapshot).digest("hex");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,9 +105,7 @@ function resolveFetchEnabled(params: { fetch?: WebFetchConfig; sandboxed?: boole
|
||||
|
||||
function resolveSearchApiKey(search?: WebSearchConfig): string | undefined {
|
||||
const fromConfig =
|
||||
search && "apiKey" in search && typeof search.apiKey === "string"
|
||||
? search.apiKey.trim()
|
||||
: "";
|
||||
search && "apiKey" in search && typeof search.apiKey === "string" ? search.apiKey.trim() : "";
|
||||
const fromEnv = (process.env.BRAVE_API_KEY ?? "").trim();
|
||||
return fromConfig || fromEnv || undefined;
|
||||
}
|
||||
@@ -160,12 +158,7 @@ function readCache<T>(
|
||||
return { value: entry.value, cached: true };
|
||||
}
|
||||
|
||||
function writeCache<T>(
|
||||
cache: Map<string, CacheEntry<T>>,
|
||||
key: string,
|
||||
value: T,
|
||||
ttlMs: number,
|
||||
) {
|
||||
function writeCache<T>(cache: Map<string, CacheEntry<T>>, key: string, value: T, ttlMs: number) {
|
||||
if (ttlMs <= 0) return;
|
||||
if (cache.size >= DEFAULT_CACHE_MAX_ENTRIES) {
|
||||
const oldest = cache.keys().next();
|
||||
@@ -319,7 +312,7 @@ async function runWebSearch(params: {
|
||||
}
|
||||
|
||||
const data = (await res.json()) as BraveSearchResponse;
|
||||
const results = Array.isArray(data.web?.results) ? data.web?.results ?? [] : [];
|
||||
const results = Array.isArray(data.web?.results) ? (data.web?.results ?? []) : [];
|
||||
const mapped = results.map((entry) => ({
|
||||
title: entry.title ?? "",
|
||||
url: entry.url ?? "",
|
||||
@@ -463,8 +456,7 @@ export function createWebFetchTool(options?: {
|
||||
execute: async (_toolCallId, args) => {
|
||||
const params = args as Record<string, unknown>;
|
||||
const url = readStringParam(params, "url", { required: true });
|
||||
const extractMode =
|
||||
readStringParam(params, "extractMode") === "text" ? "text" : "markdown";
|
||||
const extractMode = readStringParam(params, "extractMode") === "text" ? "text" : "markdown";
|
||||
const maxChars = readNumberParam(params, "maxChars", { integer: true });
|
||||
const result = await runWebFetch({
|
||||
url,
|
||||
|
||||
@@ -159,10 +159,7 @@ function axValue(v: unknown): string {
|
||||
return "";
|
||||
}
|
||||
|
||||
export function formatAriaSnapshot(
|
||||
nodes: RawAXNode[],
|
||||
limit: number,
|
||||
): AriaSnapshotNode[] {
|
||||
export function formatAriaSnapshot(nodes: RawAXNode[], limit: number): AriaSnapshotNode[] {
|
||||
const byId = new Map<string, RawAXNode>();
|
||||
for (const n of nodes) {
|
||||
if (n.nodeId) byId.set(n.nodeId, n);
|
||||
|
||||
@@ -72,14 +72,11 @@ export async function fetchBrowserJson<T>(
|
||||
}
|
||||
return h;
|
||||
})();
|
||||
res = await fetch(
|
||||
url,
|
||||
{
|
||||
...init,
|
||||
...(mergedHeaders ? { headers: mergedHeaders } : {}),
|
||||
signal: ctrl.signal,
|
||||
} as RequestInit,
|
||||
);
|
||||
res = await fetch(url, {
|
||||
...init,
|
||||
...(mergedHeaders ? { headers: mergedHeaders } : {}),
|
||||
signal: ctrl.signal,
|
||||
} as RequestInit);
|
||||
} catch (err) {
|
||||
throw enhanceBrowserFetchError(url, err, timeoutMs);
|
||||
} finally {
|
||||
|
||||
@@ -159,20 +159,17 @@ export async function browserCreateProfile(
|
||||
driver?: "clawd" | "extension";
|
||||
},
|
||||
): Promise<BrowserCreateProfileResult> {
|
||||
return await fetchBrowserJson<BrowserCreateProfileResult>(
|
||||
`${baseUrl}/profiles/create`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: opts.name,
|
||||
color: opts.color,
|
||||
cdpUrl: opts.cdpUrl,
|
||||
driver: opts.driver,
|
||||
}),
|
||||
timeoutMs: 10000,
|
||||
},
|
||||
);
|
||||
return await fetchBrowserJson<BrowserCreateProfileResult>(`${baseUrl}/profiles/create`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name: opts.name,
|
||||
color: opts.color,
|
||||
cdpUrl: opts.cdpUrl,
|
||||
driver: opts.driver,
|
||||
}),
|
||||
timeoutMs: 10000,
|
||||
});
|
||||
}
|
||||
|
||||
export type BrowserDeleteProfileResult = {
|
||||
|
||||
@@ -105,9 +105,7 @@ describe("chrome extension relay server", () => {
|
||||
cdpUrl = `http://127.0.0.1:${port}`;
|
||||
await ensureChromeExtensionRelayServer({ cdpUrl });
|
||||
|
||||
const v1 = (await fetch(`${cdpUrl}/json/version`).then((r) =>
|
||||
r.json(),
|
||||
)) as {
|
||||
const v1 = (await fetch(`${cdpUrl}/json/version`).then((r) => r.json())) as {
|
||||
webSocketDebuggerUrl?: string;
|
||||
};
|
||||
expect(v1.webSocketDebuggerUrl).toBeUndefined();
|
||||
@@ -115,9 +113,7 @@ describe("chrome extension relay server", () => {
|
||||
const ext = new WebSocket(`ws://127.0.0.1:${port}/extension`);
|
||||
await waitForOpen(ext);
|
||||
|
||||
const v2 = (await fetch(`${cdpUrl}/json/version`).then((r) =>
|
||||
r.json(),
|
||||
)) as {
|
||||
const v2 = (await fetch(`${cdpUrl}/json/version`).then((r) => r.json())) as {
|
||||
webSocketDebuggerUrl?: string;
|
||||
};
|
||||
expect(String(v2.webSocketDebuggerUrl ?? "")).toContain(`/cdp`);
|
||||
@@ -153,15 +149,11 @@ describe("chrome extension relay server", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
const list = (await fetch(`${cdpUrl}/json/list`).then((r) =>
|
||||
r.json(),
|
||||
)) as Array<{
|
||||
const list = (await fetch(`${cdpUrl}/json/list`).then((r) => r.json())) as Array<{
|
||||
id?: string;
|
||||
url?: string;
|
||||
}>;
|
||||
expect(
|
||||
list.some((t) => t.id === "t1" && t.url === "https://example.com"),
|
||||
).toBe(true);
|
||||
expect(list.some((t) => t.id === "t1" && t.url === "https://example.com")).toBe(true);
|
||||
|
||||
const cdp = new WebSocket(`ws://127.0.0.1:${port}/cdp`);
|
||||
await waitForOpen(cdp);
|
||||
|
||||
@@ -114,21 +114,13 @@ function parseBaseUrl(raw: string): {
|
||||
} {
|
||||
const parsed = new URL(raw.trim().replace(/\/$/, ""));
|
||||
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||
throw new Error(
|
||||
`extension relay cdpUrl must be http(s), got ${parsed.protocol}`,
|
||||
);
|
||||
throw new Error(`extension relay cdpUrl must be http(s), got ${parsed.protocol}`);
|
||||
}
|
||||
const host = parsed.hostname;
|
||||
const port =
|
||||
parsed.port?.trim() !== ""
|
||||
? Number(parsed.port)
|
||||
: parsed.protocol === "https:"
|
||||
? 443
|
||||
: 80;
|
||||
parsed.port?.trim() !== "" ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : 80;
|
||||
if (!Number.isFinite(port) || port <= 0 || port > 65535) {
|
||||
throw new Error(
|
||||
`extension relay cdpUrl has invalid port: ${parsed.port || "(empty)"}`,
|
||||
);
|
||||
throw new Error(`extension relay cdpUrl has invalid port: ${parsed.port || "(empty)"}`);
|
||||
}
|
||||
return { host, port, baseUrl: parsed.toString().replace(/\/$/, "") };
|
||||
}
|
||||
@@ -162,9 +154,7 @@ export async function ensureChromeExtensionRelayServer(opts: {
|
||||
}): Promise<ChromeExtensionRelayServer> {
|
||||
const info = parseBaseUrl(opts.cdpUrl);
|
||||
if (!isLoopbackHost(info.host)) {
|
||||
throw new Error(
|
||||
`extension relay requires loopback cdpUrl host (got ${info.host})`,
|
||||
);
|
||||
throw new Error(`extension relay requires loopback cdpUrl host (got ${info.host})`);
|
||||
}
|
||||
|
||||
const existing = serversByPort.get(info.port);
|
||||
@@ -184,9 +174,7 @@ export async function ensureChromeExtensionRelayServer(opts: {
|
||||
>();
|
||||
let nextExtensionId = 1;
|
||||
|
||||
const sendToExtension = async (
|
||||
payload: ExtensionForwardCommandMessage,
|
||||
): Promise<unknown> => {
|
||||
const sendToExtension = async (payload: ExtensionForwardCommandMessage): Promise<unknown> => {
|
||||
const ws = extensionWs;
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
||||
throw new Error("Chrome extension not connected");
|
||||
@@ -195,9 +183,7 @@ export async function ensureChromeExtensionRelayServer(opts: {
|
||||
return await new Promise<unknown>((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
pendingExtension.delete(payload.id);
|
||||
reject(
|
||||
new Error(`extension request timeout: ${payload.params.method}`),
|
||||
);
|
||||
reject(new Error(`extension request timeout: ${payload.params.method}`));
|
||||
}, 30_000);
|
||||
pendingExtension.set(payload.id, { resolve, reject, timer });
|
||||
});
|
||||
@@ -216,10 +202,7 @@ export async function ensureChromeExtensionRelayServer(opts: {
|
||||
ws.send(JSON.stringify(res));
|
||||
};
|
||||
|
||||
const ensureTargetEventsForClient = (
|
||||
ws: WebSocket,
|
||||
mode: "autoAttach" | "discover",
|
||||
) => {
|
||||
const ensureTargetEventsForClient = (ws: WebSocket, mode: "autoAttach" | "discover") => {
|
||||
for (const target of connectedTargets.values()) {
|
||||
if (mode === "autoAttach") {
|
||||
ws.send(
|
||||
@@ -267,8 +250,7 @@ export async function ensureChromeExtensionRelayServer(opts: {
|
||||
};
|
||||
case "Target.getTargetInfo": {
|
||||
const params = (cmd.params ?? {}) as { targetId?: string };
|
||||
const targetId =
|
||||
typeof params.targetId === "string" ? params.targetId : undefined;
|
||||
const targetId = typeof params.targetId === "string" ? params.targetId : undefined;
|
||||
if (targetId) {
|
||||
for (const t of connectedTargets.values()) {
|
||||
if (t.targetId === targetId) return { targetInfo: t.targetInfo };
|
||||
@@ -283,8 +265,7 @@ export async function ensureChromeExtensionRelayServer(opts: {
|
||||
}
|
||||
case "Target.attachToTarget": {
|
||||
const params = (cmd.params ?? {}) as { targetId?: string };
|
||||
const targetId =
|
||||
typeof params.targetId === "string" ? params.targetId : undefined;
|
||||
const targetId = typeof params.targetId === "string" ? params.targetId : undefined;
|
||||
if (!targetId) throw new Error("targetId required");
|
||||
for (const t of connectedTargets.values()) {
|
||||
if (t.targetId === targetId) return { sessionId: t.sessionId };
|
||||
@@ -458,9 +439,7 @@ export async function ensureChromeExtensionRelayServer(opts: {
|
||||
|
||||
const ping = setInterval(() => {
|
||||
if (ws.readyState !== WebSocket.OPEN) return;
|
||||
ws.send(
|
||||
JSON.stringify({ method: "ping" } satisfies ExtensionPingMessage),
|
||||
);
|
||||
ws.send(JSON.stringify({ method: "ping" } satisfies ExtensionPingMessage));
|
||||
}, 5000);
|
||||
|
||||
ws.on("message", (data) => {
|
||||
@@ -471,21 +450,12 @@ export async function ensureChromeExtensionRelayServer(opts: {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
parsed &&
|
||||
typeof parsed === "object" &&
|
||||
"id" in parsed &&
|
||||
typeof parsed.id === "number"
|
||||
) {
|
||||
if (parsed && typeof parsed === "object" && "id" in parsed && typeof parsed.id === "number") {
|
||||
const pending = pendingExtension.get(parsed.id);
|
||||
if (!pending) return;
|
||||
pendingExtension.delete(parsed.id);
|
||||
clearTimeout(pending.timer);
|
||||
if (
|
||||
"error" in parsed &&
|
||||
typeof parsed.error === "string" &&
|
||||
parsed.error.trim()
|
||||
) {
|
||||
if ("error" in parsed && typeof parsed.error === "string" && parsed.error.trim()) {
|
||||
pending.reject(new Error(parsed.error));
|
||||
} else {
|
||||
pending.resolve((parsed as ExtensionResponseMessage).result);
|
||||
@@ -495,10 +465,7 @@ export async function ensureChromeExtensionRelayServer(opts: {
|
||||
|
||||
if (parsed && typeof parsed === "object" && "method" in parsed) {
|
||||
if ((parsed as ExtensionPongMessage).method === "pong") return;
|
||||
if (
|
||||
(parsed as ExtensionForwardEventMessage).method !== "forwardCDPEvent"
|
||||
)
|
||||
return;
|
||||
if ((parsed as ExtensionForwardEventMessage).method !== "forwardCDPEvent") return;
|
||||
const evt = parsed as ExtensionForwardEventMessage;
|
||||
const method = evt.params?.method;
|
||||
const params = evt.params?.params;
|
||||
@@ -591,8 +558,7 @@ export async function ensureChromeExtensionRelayServer(opts: {
|
||||
}
|
||||
if (cmd.method === "Target.attachToTarget") {
|
||||
const params = (cmd.params ?? {}) as { targetId?: string };
|
||||
const targetId =
|
||||
typeof params.targetId === "string" ? params.targetId : undefined;
|
||||
const targetId = typeof params.targetId === "string" ? params.targetId : undefined;
|
||||
if (targetId) {
|
||||
const target = Array.from(connectedTargets.values()).find(
|
||||
(t) => t.targetId === targetId,
|
||||
@@ -669,9 +635,7 @@ export async function ensureChromeExtensionRelayServer(opts: {
|
||||
return relay;
|
||||
}
|
||||
|
||||
export async function stopChromeExtensionRelayServer(opts: {
|
||||
cdpUrl: string;
|
||||
}): Promise<boolean> {
|
||||
export async function stopChromeExtensionRelayServer(opts: { cdpUrl: string }): Promise<boolean> {
|
||||
const info = parseBaseUrl(opts.cdpUrl);
|
||||
const existing = serversByPort.get(info.port);
|
||||
if (!existing) return false;
|
||||
|
||||
@@ -111,9 +111,10 @@ export function registerBrowserBasicRoutes(app: express.Express, ctx: BrowserRou
|
||||
const name = toStringOrEmpty((req.body as { name?: unknown })?.name);
|
||||
const color = toStringOrEmpty((req.body as { color?: unknown })?.color);
|
||||
const cdpUrl = toStringOrEmpty((req.body as { cdpUrl?: unknown })?.cdpUrl);
|
||||
const driver = toStringOrEmpty(
|
||||
(req.body as { driver?: unknown })?.driver,
|
||||
) as "clawd" | "extension" | "";
|
||||
const driver = toStringOrEmpty((req.body as { driver?: unknown })?.driver) as
|
||||
| "clawd"
|
||||
| "extension"
|
||||
| "";
|
||||
|
||||
if (!name) return jsonError(res, 400, "name is required");
|
||||
|
||||
|
||||
@@ -342,9 +342,7 @@ function createProfileContext(
|
||||
|
||||
const resetProfile = async () => {
|
||||
if (profile.driver === "extension") {
|
||||
await stopChromeExtensionRelayServer({ cdpUrl: profile.cdpUrl }).catch(
|
||||
() => {},
|
||||
);
|
||||
await stopChromeExtensionRelayServer({ cdpUrl: profile.cdpUrl }).catch(() => {});
|
||||
return { moved: false, from: profile.cdpUrl };
|
||||
}
|
||||
if (!profile.cdpIsLoopback) {
|
||||
|
||||
@@ -3,11 +3,7 @@ import express from "express";
|
||||
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { createSubsystemLogger } from "../logging.js";
|
||||
import {
|
||||
resolveBrowserConfig,
|
||||
resolveProfile,
|
||||
shouldStartLocalBrowserServer,
|
||||
} from "./config.js";
|
||||
import { resolveBrowserConfig, resolveProfile, shouldStartLocalBrowserServer } from "./config.js";
|
||||
import { ensureChromeExtensionRelayServer } from "./extension-relay.js";
|
||||
import { registerBrowserRoutes } from "./routes/index.js";
|
||||
import { type BrowserServerState, createBrowserRouteContext } from "./server-context.js";
|
||||
@@ -61,13 +57,9 @@ export async function startBrowserControlServerFromConfig(): Promise<BrowserServ
|
||||
for (const name of Object.keys(resolved.profiles)) {
|
||||
const profile = resolveProfile(resolved, name);
|
||||
if (!profile || profile.driver !== "extension") continue;
|
||||
await ensureChromeExtensionRelayServer({ cdpUrl: profile.cdpUrl }).catch(
|
||||
(err) => {
|
||||
logServer.warn(
|
||||
`Chrome extension relay init failed for profile "${name}": ${String(err)}`,
|
||||
);
|
||||
},
|
||||
);
|
||||
await ensureChromeExtensionRelayServer({ cdpUrl: profile.cdpUrl }).catch((err) => {
|
||||
logServer.warn(`Chrome extension relay init failed for profile "${name}": ${String(err)}`);
|
||||
});
|
||||
}
|
||||
|
||||
logServer.info(`Browser control listening on http://127.0.0.1:${port}/`);
|
||||
|
||||
@@ -34,9 +34,7 @@ export function listChannelPluginCatalogEntries(): ChannelPluginCatalogEntry[] {
|
||||
return [...CATALOG];
|
||||
}
|
||||
|
||||
export function getChannelPluginCatalogEntry(
|
||||
id: string,
|
||||
): ChannelPluginCatalogEntry | undefined {
|
||||
export function getChannelPluginCatalogEntry(id: string): ChannelPluginCatalogEntry | undefined {
|
||||
const trimmed = id.trim();
|
||||
if (!trimmed) return undefined;
|
||||
return CATALOG.find((entry) => entry.id === trimmed);
|
||||
|
||||
@@ -17,4 +17,3 @@ describe("browser extension install", () => {
|
||||
expect(result.path.includes("node_modules")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -75,20 +75,20 @@ export function registerBrowserExtensionCommands(
|
||||
defaultRuntime.log(JSON.stringify({ ok: true, path: installed.path }, null, 2));
|
||||
return;
|
||||
}
|
||||
defaultRuntime.log(installed.path);
|
||||
defaultRuntime.error(
|
||||
info(
|
||||
[
|
||||
"Next:",
|
||||
`- Chrome → chrome://extensions → enable “Developer mode”`,
|
||||
`- “Load unpacked” → select: ${installed.path}`,
|
||||
`- Pin “Clawdbot Browser Relay”, then click it on the tab (badge shows ON)`,
|
||||
"",
|
||||
`${theme.muted("Docs:")} ${formatDocsLink("/tools/chrome-extension", "docs.clawd.bot/tools/chrome-extension")}`,
|
||||
].join("\n"),
|
||||
),
|
||||
);
|
||||
});
|
||||
defaultRuntime.log(installed.path);
|
||||
defaultRuntime.error(
|
||||
info(
|
||||
[
|
||||
"Next:",
|
||||
`- Chrome → chrome://extensions → enable “Developer mode”`,
|
||||
`- “Load unpacked” → select: ${installed.path}`,
|
||||
`- Pin “Clawdbot Browser Relay”, then click it on the tab (badge shows ON)`,
|
||||
"",
|
||||
`${theme.muted("Docs:")} ${formatDocsLink("/tools/chrome-extension", "docs.clawd.bot/tools/chrome-extension")}`,
|
||||
].join("\n"),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
ext
|
||||
.command("path")
|
||||
|
||||
@@ -384,10 +384,7 @@ export function registerBrowserManageCommands(
|
||||
.option("--cdp-url <url>", "CDP URL for remote Chrome (http/https)")
|
||||
.option("--driver <driver>", "Profile driver (clawd|extension). Default: clawd")
|
||||
.action(
|
||||
async (
|
||||
opts: { name: string; color?: string; cdpUrl?: string; driver?: string },
|
||||
cmd,
|
||||
) => {
|
||||
async (opts: { name: string; color?: string; cdpUrl?: string; driver?: string }, cmd) => {
|
||||
const parent = parentOpts(cmd);
|
||||
const baseUrl = resolveBrowserControlUrl(parent?.url);
|
||||
try {
|
||||
@@ -401,9 +398,7 @@ export function registerBrowserManageCommands(
|
||||
defaultRuntime.log(JSON.stringify(result, null, 2));
|
||||
return;
|
||||
}
|
||||
const loc = result.isRemote
|
||||
? ` cdpUrl: ${result.cdpUrl}`
|
||||
: ` port: ${result.cdpPort}`;
|
||||
const loc = result.isRemote ? ` cdpUrl: ${result.cdpUrl}` : ` port: ${result.cdpPort}`;
|
||||
defaultRuntime.log(
|
||||
info(
|
||||
`🦞 Created profile "${result.profile}"\n${loc}\n color: ${result.color}${
|
||||
|
||||
@@ -80,8 +80,10 @@ export function registerSecurityCli(program: Command) {
|
||||
for (const action of fixResult.actions) {
|
||||
const mode = action.mode.toString(8).padStart(3, "0");
|
||||
if (action.ok) lines.push(muted(` chmod ${mode} ${action.path}`));
|
||||
else if (action.skipped) lines.push(muted(` skip chmod ${mode} ${action.path} (${action.skipped})`));
|
||||
else if (action.error) lines.push(muted(` chmod ${mode} ${action.path} failed: ${action.error}`));
|
||||
else if (action.skipped)
|
||||
lines.push(muted(` skip chmod ${mode} ${action.path} (${action.skipped})`));
|
||||
else if (action.error)
|
||||
lines.push(muted(` chmod ${mode} ${action.path} failed: ${action.error}`));
|
||||
}
|
||||
if (fixResult.errors.length > 0) {
|
||||
for (const err of fixResult.errors) lines.push(muted(` error: ${err}`));
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
import { resolveAgentDir, resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import {
|
||||
resolveAgentDir,
|
||||
resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId,
|
||||
} from "../agents/agent-scope.js";
|
||||
import { ensureAuthProfileStore } from "../agents/auth-profiles.js";
|
||||
import { resolveAuthStorePath } from "../agents/auth-profiles/paths.js";
|
||||
import { CONFIG_PATH_CLAWDBOT, writeConfigFile } from "../config/config.js";
|
||||
@@ -223,9 +227,12 @@ export async function agentsAddCommand(
|
||||
const sourceAuthPath = resolveAuthStorePath(resolveAgentDir(cfg, defaultAgentId));
|
||||
const destAuthPath = resolveAuthStorePath(agentDir);
|
||||
const sameAuthPath =
|
||||
path.resolve(sourceAuthPath).toLowerCase() ===
|
||||
path.resolve(destAuthPath).toLowerCase();
|
||||
if (!sameAuthPath && (await fileExists(sourceAuthPath)) && !(await fileExists(destAuthPath))) {
|
||||
path.resolve(sourceAuthPath).toLowerCase() === path.resolve(destAuthPath).toLowerCase();
|
||||
if (
|
||||
!sameAuthPath &&
|
||||
(await fileExists(sourceAuthPath)) &&
|
||||
!(await fileExists(destAuthPath))
|
||||
) {
|
||||
const shouldCopy = await prompter.confirm({
|
||||
message: `Copy auth profiles from "${defaultAgentId}"?`,
|
||||
initialValue: false,
|
||||
|
||||
@@ -140,9 +140,7 @@ export async function setupChannels(
|
||||
quickstartScore: 0,
|
||||
}));
|
||||
const combinedStatuses = [...statusEntries, ...catalogStatuses];
|
||||
const statusByChannel = new Map(
|
||||
combinedStatuses.map((entry) => [entry.channel, entry]),
|
||||
);
|
||||
const statusByChannel = new Map(combinedStatuses.map((entry) => [entry.channel, entry]));
|
||||
const statusLines = combinedStatuses.flatMap((entry) => entry.statusLines);
|
||||
if (statusLines.length > 0) {
|
||||
await prompter.note(statusLines.join("\n"), "Channel status");
|
||||
@@ -216,9 +214,7 @@ export async function setupChannels(
|
||||
})) as ChannelChoice[];
|
||||
}
|
||||
|
||||
const catalogById = new Map(
|
||||
catalogEntries.map((entry) => [entry.id as ChannelChoice, entry]),
|
||||
);
|
||||
const catalogById = new Map(catalogEntries.map((entry) => [entry.id as ChannelChoice, entry]));
|
||||
if (selection.some((channel) => catalogById.has(channel))) {
|
||||
const workspaceDir = resolveAgentWorkspaceDir(next, resolveDefaultAgentId(next));
|
||||
for (const channel of selection) {
|
||||
@@ -248,16 +244,10 @@ export async function setupChannels(
|
||||
|
||||
const selectionNotes = new Map<string, string>();
|
||||
for (const plugin of installedPlugins) {
|
||||
selectionNotes.set(
|
||||
plugin.id,
|
||||
formatChannelSelectionLine(plugin.meta, formatDocsLink),
|
||||
);
|
||||
selectionNotes.set(plugin.id, formatChannelSelectionLine(plugin.meta, formatDocsLink));
|
||||
}
|
||||
for (const entry of catalogEntries) {
|
||||
selectionNotes.set(
|
||||
entry.id,
|
||||
formatChannelSelectionLine(entry.meta, formatDocsLink),
|
||||
);
|
||||
selectionNotes.set(entry.id, formatChannelSelectionLine(entry.meta, formatDocsLink));
|
||||
}
|
||||
const selectedLines = selection
|
||||
.map((channel) => selectionNotes.get(channel))
|
||||
|
||||
@@ -12,9 +12,7 @@ export const makeRuntime = (overrides: Partial<RuntimeEnv> = {}): RuntimeEnv =>
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const makePrompter = (
|
||||
overrides: Partial<WizardPrompter> = {},
|
||||
): WizardPrompter => ({
|
||||
export const makePrompter = (overrides: Partial<WizardPrompter> = {}): WizardPrompter => ({
|
||||
intro: vi.fn(async () => {}),
|
||||
outro: vi.fn(async () => {}),
|
||||
note: vi.fn(async () => {}),
|
||||
|
||||
@@ -170,8 +170,7 @@ export function reloadOnboardingPluginRegistry(params: {
|
||||
workspaceDir?: string;
|
||||
}): void {
|
||||
const workspaceDir =
|
||||
params.workspaceDir ??
|
||||
resolveAgentWorkspaceDir(params.cfg, resolveDefaultAgentId(params.cfg));
|
||||
params.workspaceDir ?? resolveAgentWorkspaceDir(params.cfg, resolveDefaultAgentId(params.cfg));
|
||||
const log = createSubsystemLogger("plugins");
|
||||
loadClawdbotPlugins({
|
||||
config: params.cfg,
|
||||
|
||||
@@ -52,7 +52,10 @@ const SHELL_ENV_EXPECTED_KEYS = [
|
||||
export type ParseConfigJson5Result = { ok: true; parsed: unknown } | { ok: false; error: string };
|
||||
|
||||
function hashConfigRaw(raw: string | null): string {
|
||||
return crypto.createHash("sha256").update(raw ?? "").digest("hex");
|
||||
return crypto
|
||||
.createHash("sha256")
|
||||
.update(raw ?? "")
|
||||
.digest("hex");
|
||||
}
|
||||
|
||||
export type ConfigIoDeps = {
|
||||
|
||||
@@ -82,9 +82,7 @@ export const ClawdbotSchema = z
|
||||
.object({
|
||||
cdpPort: z.number().int().min(1).max(65535).optional(),
|
||||
cdpUrl: z.string().optional(),
|
||||
driver: z
|
||||
.union([z.literal("clawd"), z.literal("extension")])
|
||||
.optional(),
|
||||
driver: z.union([z.literal("clawd"), z.literal("extension")]).optional(),
|
||||
color: HexColorSchema,
|
||||
})
|
||||
.refine((value) => value.cdpPort || value.cdpUrl, {
|
||||
|
||||
@@ -227,7 +227,11 @@ export const configHandlers: GatewayRequestHandlers = {
|
||||
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, parsedRes.error));
|
||||
return;
|
||||
}
|
||||
if (!parsedRes.parsed || typeof parsedRes.parsed !== "object" || Array.isArray(parsedRes.parsed)) {
|
||||
if (
|
||||
!parsedRes.parsed ||
|
||||
typeof parsedRes.parsed !== "object" ||
|
||||
Array.isArray(parsedRes.parsed)
|
||||
) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
|
||||
@@ -28,7 +28,10 @@ describe("gateway config.patch", () => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
const setRes = await onceMessage<{ ok: boolean }>(ws, (o) => o.type === "res" && o.id === setId);
|
||||
const setRes = await onceMessage<{ ok: boolean }>(
|
||||
ws,
|
||||
(o) => o.type === "res" && o.id === setId,
|
||||
);
|
||||
expect(setRes.ok).toBe(true);
|
||||
|
||||
const getId = "req-get";
|
||||
@@ -85,7 +88,9 @@ describe("gateway config.patch", () => {
|
||||
);
|
||||
const get2Res = await onceMessage<{
|
||||
ok: boolean;
|
||||
payload?: { config?: { gateway?: { mode?: string }; channels?: { telegram?: { botToken?: string } } } };
|
||||
payload?: {
|
||||
config?: { gateway?: { mode?: string }; channels?: { telegram?: { botToken?: string } } };
|
||||
};
|
||||
}>(ws, (o) => o.type === "res" && o.id === get2Id);
|
||||
expect(get2Res.ok).toBe(true);
|
||||
expect(get2Res.payload?.config?.gateway?.mode).toBe("local");
|
||||
@@ -112,7 +117,10 @@ describe("gateway config.patch", () => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
const setRes = await onceMessage<{ ok: boolean }>(ws, (o) => o.type === "res" && o.id === setId);
|
||||
const setRes = await onceMessage<{ ok: boolean }>(
|
||||
ws,
|
||||
(o) => o.type === "res" && o.id === setId,
|
||||
);
|
||||
expect(setRes.ok).toBe(true);
|
||||
|
||||
const patchId = "req-patch-2";
|
||||
@@ -154,7 +162,10 @@ describe("gateway config.patch", () => {
|
||||
},
|
||||
}),
|
||||
);
|
||||
const setRes = await onceMessage<{ ok: boolean }>(ws, (o) => o.type === "res" && o.id === setId);
|
||||
const setRes = await onceMessage<{ ok: boolean }>(
|
||||
ws,
|
||||
(o) => o.type === "res" && o.id === setId,
|
||||
);
|
||||
expect(setRes.ok).toBe(true);
|
||||
|
||||
const set2Id = "req-set-4";
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { PluginRegistry } from "../../../plugins/registry.js";
|
||||
|
||||
export const createTestRegistry = (
|
||||
overrides: Partial<PluginRegistry> = {},
|
||||
): PluginRegistry => {
|
||||
export const createTestRegistry = (overrides: Partial<PluginRegistry> = {}): PluginRegistry => {
|
||||
const base: PluginRegistry = {
|
||||
plugins: [],
|
||||
tools: [],
|
||||
|
||||
@@ -44,7 +44,9 @@ describe("createGatewayPluginRequestHandler", () => {
|
||||
{ pluginId: "second", handler: second, source: "second" },
|
||||
],
|
||||
}),
|
||||
log: { warn: vi.fn() } as unknown as Parameters<typeof createGatewayPluginRequestHandler>[0]["log"],
|
||||
log: { warn: vi.fn() } as unknown as Parameters<
|
||||
typeof createGatewayPluginRequestHandler
|
||||
>[0]["log"],
|
||||
});
|
||||
|
||||
const { res } = makeResponse();
|
||||
@@ -78,10 +80,7 @@ describe("createGatewayPluginRequestHandler", () => {
|
||||
expect(handled).toBe(true);
|
||||
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining("boom"));
|
||||
expect(res.statusCode).toBe(500);
|
||||
expect(setHeader).toHaveBeenCalledWith(
|
||||
"Content-Type",
|
||||
"text/plain; charset=utf-8",
|
||||
);
|
||||
expect(setHeader).toHaveBeenCalledWith("Content-Type", "text/plain; charset=utf-8");
|
||||
expect(end).toHaveBeenCalledWith("Internal Server Error");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -170,7 +170,10 @@ vi.mock("../config/config.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../config/config.js")>("../config/config.js");
|
||||
const resolveConfigPath = () => path.join(testConfigRoot.value, "clawdbot.json");
|
||||
const hashConfigRaw = (raw: string | null) =>
|
||||
crypto.createHash("sha256").update(raw ?? "").digest("hex");
|
||||
crypto
|
||||
.createHash("sha256")
|
||||
.update(raw ?? "")
|
||||
.digest("hex");
|
||||
|
||||
const readConfigFileSnapshot = async () => {
|
||||
if (testState.legacyIssues.length > 0) {
|
||||
|
||||
@@ -152,10 +152,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
record.gatewayMethods.push(trimmed);
|
||||
};
|
||||
|
||||
const registerHttpHandler = (
|
||||
record: PluginRecord,
|
||||
handler: ClawdbotPluginHttpHandler,
|
||||
) => {
|
||||
const registerHttpHandler = (record: PluginRecord, handler: ClawdbotPluginHttpHandler) => {
|
||||
record.httpHandlers += 1;
|
||||
registry.httpHandlers.push({
|
||||
pluginId: record.id,
|
||||
|
||||
@@ -84,7 +84,10 @@ describe("security audit", () => {
|
||||
|
||||
expect(res.findings).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ checkId: "browser.control_remote_no_token", severity: "critical" }),
|
||||
expect.objectContaining({
|
||||
checkId: "browser.control_remote_no_token",
|
||||
severity: "critical",
|
||||
}),
|
||||
]),
|
||||
);
|
||||
} finally {
|
||||
|
||||
@@ -351,7 +351,9 @@ function collectBrowserControlFindings(cfg: ClawdbotConfig): SecurityAuditFindin
|
||||
const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off";
|
||||
const gatewayAuth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, tailscaleMode });
|
||||
const gatewayToken =
|
||||
gatewayAuth.mode === "token" && typeof gatewayAuth.token === "string" && gatewayAuth.token.trim()
|
||||
gatewayAuth.mode === "token" &&
|
||||
typeof gatewayAuth.token === "string" &&
|
||||
gatewayAuth.token.trim()
|
||||
? gatewayAuth.token.trim()
|
||||
: null;
|
||||
|
||||
|
||||
@@ -157,10 +157,11 @@ function setWhatsAppGroupAllowFromFromStore(params: {
|
||||
}
|
||||
}
|
||||
|
||||
function applyConfigFixes(params: {
|
||||
function applyConfigFixes(params: { cfg: ClawdbotConfig; env: NodeJS.ProcessEnv }): {
|
||||
cfg: ClawdbotConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): { cfg: ClawdbotConfig; changes: string[]; policyFlips: Set<string> } {
|
||||
changes: string[];
|
||||
policyFlips: Set<string>;
|
||||
} {
|
||||
const next = structuredClone(params.cfg ?? {});
|
||||
const changes: string[] = [];
|
||||
const policyFlips = new Set<string>();
|
||||
@@ -170,7 +171,15 @@ function applyConfigFixes(params: {
|
||||
changes.push('logging.redactSensitive=off -> "tools"');
|
||||
}
|
||||
|
||||
for (const channel of ["telegram", "whatsapp", "discord", "signal", "imessage", "slack", "msteams"]) {
|
||||
for (const channel of [
|
||||
"telegram",
|
||||
"whatsapp",
|
||||
"discord",
|
||||
"signal",
|
||||
"imessage",
|
||||
"slack",
|
||||
"msteams",
|
||||
]) {
|
||||
setGroupPolicyAllowlist({ cfg: next, channel, changes, policyFlips });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user