chore: run format and fix sandbox browser timeouts
This commit is contained in:
@@ -191,8 +191,8 @@ extension CronJobEditor {
|
|||||||
func applyDeleteAfterRun(
|
func applyDeleteAfterRun(
|
||||||
to root: inout [String: Any],
|
to root: inout [String: Any],
|
||||||
scheduleKind: ScheduleKind? = nil,
|
scheduleKind: ScheduleKind? = nil,
|
||||||
deleteAfterRun: Bool? = nil
|
deleteAfterRun: Bool? = nil)
|
||||||
) {
|
{
|
||||||
let resolvedSchedule = scheduleKind ?? self.scheduleKind
|
let resolvedSchedule = scheduleKind ?? self.scheduleKind
|
||||||
let resolvedDelete = deleteAfterRun ?? self.deleteAfterRun
|
let resolvedDelete = deleteAfterRun ?? self.deleteAfterRun
|
||||||
if resolvedSchedule == .at {
|
if resolvedSchedule == .at {
|
||||||
|
|||||||
@@ -58,9 +58,7 @@ export function stripThoughtSignatures<T>(
|
|||||||
if (!block || typeof block !== "object") return block;
|
if (!block || typeof block !== "object") return block;
|
||||||
const rec = block as ContentBlockWithSignature;
|
const rec = block as ContentBlockWithSignature;
|
||||||
const stripSnake = shouldStripSignature(rec.thought_signature);
|
const stripSnake = shouldStripSignature(rec.thought_signature);
|
||||||
const stripCamel = includeCamelCase
|
const stripCamel = includeCamelCase ? shouldStripSignature(rec.thoughtSignature) : false;
|
||||||
? shouldStripSignature(rec.thoughtSignature)
|
|
||||||
: false;
|
|
||||||
if (!stripSnake && !stripCamel) {
|
if (!stripSnake && !stripCamel) {
|
||||||
return block;
|
return block;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ describe("sanitizeSessionHistory (google thinking)", () => {
|
|||||||
id: "call_1",
|
id: "call_1",
|
||||||
name: "read",
|
name: "read",
|
||||||
arguments: { path: "/tmp/foo" },
|
arguments: { path: "/tmp/foo" },
|
||||||
thoughtSignature: "{\"id\":1}",
|
thoughtSignature: '{"id":1}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "toolCall",
|
type: "toolCall",
|
||||||
@@ -192,7 +192,7 @@ describe("sanitizeSessionHistory (google thinking)", () => {
|
|||||||
{ type: "text", text: "ok" },
|
{ type: "text", text: "ok" },
|
||||||
{
|
{
|
||||||
type: "text",
|
type: "text",
|
||||||
text: "[Tool Call: read (ID: call_1)]\nArguments: {\n \"path\": \"/tmp/foo\"\n}",
|
text: '[Tool Call: read (ID: call_1)]\nArguments: {\n "path": "/tmp/foo"\n}',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "toolCall",
|
type: "toolCall",
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ vi.mock("./pi-embedded-helpers.js", async () => {
|
|||||||
...actual,
|
...actual,
|
||||||
isGoogleModelApi: vi.fn(),
|
isGoogleModelApi: vi.fn(),
|
||||||
downgradeGeminiHistory: vi.fn(),
|
downgradeGeminiHistory: vi.fn(),
|
||||||
sanitizeSessionMessagesImages: vi
|
sanitizeSessionMessagesImages: vi.fn().mockImplementation(async (msgs) => msgs),
|
||||||
.fn()
|
|
||||||
.mockImplementation(async (msgs) => msgs),
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -30,9 +28,7 @@ describe("sanitizeSessionHistory", () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
vi.mocked(helpers.sanitizeSessionMessagesImages).mockImplementation(
|
vi.mocked(helpers.sanitizeSessionMessagesImages).mockImplementation(async (msgs) => msgs);
|
||||||
async (msgs) => msgs,
|
|
||||||
);
|
|
||||||
// Default mock implementation
|
// Default mock implementation
|
||||||
vi.mocked(helpers.downgradeGeminiHistory).mockImplementation((msgs) => {
|
vi.mocked(helpers.downgradeGeminiHistory).mockImplementation((msgs) => {
|
||||||
if (!msgs) return [];
|
if (!msgs) return [];
|
||||||
|
|||||||
@@ -82,12 +82,13 @@ function findUnsupportedSchemaKeywords(schema: unknown, path: string): string[]
|
|||||||
return violations;
|
return violations;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeToolsForGoogle<TSchemaType extends TSchema = TSchema, TResult = unknown>(
|
export function sanitizeToolsForGoogle<
|
||||||
params: {
|
TSchemaType extends TSchema = TSchema,
|
||||||
tools: AgentTool<TSchemaType, TResult>[];
|
TResult = unknown,
|
||||||
provider: string;
|
>(params: {
|
||||||
},
|
tools: AgentTool<TSchemaType, TResult>[];
|
||||||
): AgentTool<TSchemaType, TResult>[] {
|
provider: string;
|
||||||
|
}): AgentTool<TSchemaType, TResult>[] {
|
||||||
if (params.provider !== "google-antigravity" && params.provider !== "google-gemini-cli") {
|
if (params.provider !== "google-antigravity" && params.provider !== "google-gemini-cli") {
|
||||||
return params.tools;
|
return params.tools;
|
||||||
}
|
}
|
||||||
@@ -95,7 +96,9 @@ export function sanitizeToolsForGoogle<TSchemaType extends TSchema = TSchema, TR
|
|||||||
if (!tool.parameters || typeof tool.parameters !== "object") return tool;
|
if (!tool.parameters || typeof tool.parameters !== "object") return tool;
|
||||||
return {
|
return {
|
||||||
...tool,
|
...tool,
|
||||||
parameters: cleanToolSchemaForGemini(tool.parameters as Record<string, unknown>) as TSchemaType,
|
parameters: cleanToolSchemaForGemini(
|
||||||
|
tool.parameters as Record<string, unknown>,
|
||||||
|
) as TSchemaType,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,10 +22,7 @@ export type SubscribeEmbeddedPiSessionParams = {
|
|||||||
blockReplyChunking?: BlockReplyChunking;
|
blockReplyChunking?: BlockReplyChunking;
|
||||||
onPartialReply?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise<void>;
|
onPartialReply?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise<void>;
|
||||||
onAssistantMessageStart?: () => void | Promise<void>;
|
onAssistantMessageStart?: () => void | Promise<void>;
|
||||||
onAgentEvent?: (evt: {
|
onAgentEvent?: (evt: { stream: string; data: Record<string, unknown> }) => void | Promise<void>;
|
||||||
stream: string;
|
|
||||||
data: Record<string, unknown>;
|
|
||||||
}) => void | Promise<void>;
|
|
||||||
enforceFinalTag?: boolean;
|
enforceFinalTag?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ function buildSandboxBrowserResolvedConfig(params: {
|
|||||||
cdpProtocol: "http",
|
cdpProtocol: "http",
|
||||||
cdpHost,
|
cdpHost,
|
||||||
cdpIsLoopback: true,
|
cdpIsLoopback: true,
|
||||||
|
remoteCdpTimeoutMs: 1500,
|
||||||
|
remoteCdpHandshakeTimeoutMs: 3000,
|
||||||
color: DEFAULT_CLAWD_BROWSER_COLOR,
|
color: DEFAULT_CLAWD_BROWSER_COLOR,
|
||||||
executablePath: undefined,
|
executablePath: undefined,
|
||||||
headless: params.headless,
|
headless: params.headless,
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ describe("cdp.helpers", () => {
|
|||||||
|
|
||||||
it("adds basic auth headers when credentials are present", () => {
|
it("adds basic auth headers when credentials are present", () => {
|
||||||
const headers = getHeadersWithAuth("https://user:pass@example.com");
|
const headers = getHeadersWithAuth("https://user:pass@example.com");
|
||||||
expect(headers.Authorization).toBe(
|
expect(headers.Authorization).toBe(`Basic ${Buffer.from("user:pass").toString("base64")}`);
|
||||||
`Basic ${Buffer.from("user:pass").toString("base64")}`,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps preexisting authorization headers", () => {
|
it("keeps preexisting authorization headers", () => {
|
||||||
|
|||||||
@@ -28,15 +28,10 @@ export function isLoopbackHost(host: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHeadersWithAuth(
|
export function getHeadersWithAuth(url: string, headers: Record<string, string> = {}) {
|
||||||
url: string,
|
|
||||||
headers: Record<string, string> = {},
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
const hasAuthHeader = Object.keys(headers).some(
|
const hasAuthHeader = Object.keys(headers).some((key) => key.toLowerCase() === "authorization");
|
||||||
(key) => key.toLowerCase() === "authorization",
|
|
||||||
);
|
|
||||||
if (hasAuthHeader) return headers;
|
if (hasAuthHeader) return headers;
|
||||||
if (parsed.username || parsed.password) {
|
if (parsed.username || parsed.password) {
|
||||||
const auth = Buffer.from(`${parsed.username}:${parsed.password}`).toString("base64");
|
const auth = Buffer.from(`${parsed.username}:${parsed.password}`).toString("base64");
|
||||||
@@ -103,18 +98,11 @@ function createCdpSender(ws: WebSocket) {
|
|||||||
return { send, closeWithError };
|
return { send, closeWithError };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchJson<T>(
|
export async function fetchJson<T>(url: string, timeoutMs = 1500, init?: RequestInit): Promise<T> {
|
||||||
url: string,
|
|
||||||
timeoutMs = 1500,
|
|
||||||
init?: RequestInit,
|
|
||||||
): Promise<T> {
|
|
||||||
const ctrl = new AbortController();
|
const ctrl = new AbortController();
|
||||||
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
||||||
try {
|
try {
|
||||||
const headers = getHeadersWithAuth(
|
const headers = getHeadersWithAuth(url, (init?.headers as Record<string, string>) || {});
|
||||||
url,
|
|
||||||
(init?.headers as Record<string, string>) || {},
|
|
||||||
);
|
|
||||||
const res = await fetch(url, { ...init, headers, signal: ctrl.signal });
|
const res = await fetch(url, { ...init, headers, signal: ctrl.signal });
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
return (await res.json()) as T;
|
return (await res.json()) as T;
|
||||||
@@ -123,18 +111,11 @@ export async function fetchJson<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchOk(
|
export async function fetchOk(url: string, timeoutMs = 1500, init?: RequestInit): Promise<void> {
|
||||||
url: string,
|
|
||||||
timeoutMs = 1500,
|
|
||||||
init?: RequestInit,
|
|
||||||
): Promise<void> {
|
|
||||||
const ctrl = new AbortController();
|
const ctrl = new AbortController();
|
||||||
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
||||||
try {
|
try {
|
||||||
const headers = getHeadersWithAuth(
|
const headers = getHeadersWithAuth(url, (init?.headers as Record<string, string>) || {});
|
||||||
url,
|
|
||||||
(init?.headers as Record<string, string>) || {},
|
|
||||||
);
|
|
||||||
const res = await fetch(url, { ...init, headers, signal: ctrl.signal });
|
const res = await fetch(url, { ...init, headers, signal: ctrl.signal });
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,9 +1,4 @@
|
|||||||
import {
|
import { appendCdpPath, fetchJson, isLoopbackHost, withCdpSocket } from "./cdp.helpers.js";
|
||||||
appendCdpPath,
|
|
||||||
fetchJson,
|
|
||||||
isLoopbackHost,
|
|
||||||
withCdpSocket,
|
|
||||||
} from "./cdp.helpers.js";
|
|
||||||
|
|
||||||
export { appendCdpPath, fetchJson, fetchOk, getHeadersWithAuth } from "./cdp.helpers.js";
|
export { appendCdpPath, fetchJson, fetchOk, getHeadersWithAuth } from "./cdp.helpers.js";
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ describe("browser default executable detection", () => {
|
|||||||
vi.mocked(execFileSync).mockImplementation((cmd, args) => {
|
vi.mocked(execFileSync).mockImplementation((cmd, args) => {
|
||||||
const argsStr = Array.isArray(args) ? args.join(" ") : "";
|
const argsStr = Array.isArray(args) ? args.join(" ") : "";
|
||||||
if (cmd === "/usr/bin/plutil" && argsStr.includes("LSHandlers")) {
|
if (cmd === "/usr/bin/plutil" && argsStr.includes("LSHandlers")) {
|
||||||
return JSON.stringify([{ LSHandlerURLScheme: "http", LSHandlerRoleAll: "com.google.Chrome" }]);
|
return JSON.stringify([
|
||||||
|
{ LSHandlerURLScheme: "http", LSHandlerRoleAll: "com.google.Chrome" },
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
if (cmd === "/usr/bin/osascript" && argsStr.includes("path to application id")) {
|
if (cmd === "/usr/bin/osascript" && argsStr.includes("path to application id")) {
|
||||||
return "/Applications/Google Chrome.app";
|
return "/Applications/Google Chrome.app";
|
||||||
@@ -55,7 +57,9 @@ describe("browser default executable detection", () => {
|
|||||||
vi.mocked(execFileSync).mockImplementation((cmd, args) => {
|
vi.mocked(execFileSync).mockImplementation((cmd, args) => {
|
||||||
const argsStr = Array.isArray(args) ? args.join(" ") : "";
|
const argsStr = Array.isArray(args) ? args.join(" ") : "";
|
||||||
if (cmd === "/usr/bin/plutil" && argsStr.includes("LSHandlers")) {
|
if (cmd === "/usr/bin/plutil" && argsStr.includes("LSHandlers")) {
|
||||||
return JSON.stringify([{ LSHandlerURLScheme: "http", LSHandlerRoleAll: "com.apple.Safari" }]);
|
return JSON.stringify([
|
||||||
|
{ LSHandlerURLScheme: "http", LSHandlerRoleAll: "com.apple.Safari" },
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -119,7 +119,12 @@ function inferKindFromIdentifier(identifier: string): BrowserExecutable["kind"]
|
|||||||
if (id.includes("edge")) return "edge";
|
if (id.includes("edge")) return "edge";
|
||||||
if (id.includes("chromium")) return "chromium";
|
if (id.includes("chromium")) return "chromium";
|
||||||
if (id.includes("canary")) return "canary";
|
if (id.includes("canary")) return "canary";
|
||||||
if (id.includes("opera") || id.includes("vivaldi") || id.includes("yandex") || id.includes("thebrowser")) {
|
if (
|
||||||
|
id.includes("opera") ||
|
||||||
|
id.includes("vivaldi") ||
|
||||||
|
id.includes("yandex") ||
|
||||||
|
id.includes("thebrowser")
|
||||||
|
) {
|
||||||
return "chromium";
|
return "chromium";
|
||||||
}
|
}
|
||||||
return "chrome";
|
return "chrome";
|
||||||
@@ -131,13 +136,12 @@ function inferKindFromExecutableName(name: string): BrowserExecutable["kind"] {
|
|||||||
if (lower.includes("edge") || lower.includes("msedge")) return "edge";
|
if (lower.includes("edge") || lower.includes("msedge")) return "edge";
|
||||||
if (lower.includes("chromium")) return "chromium";
|
if (lower.includes("chromium")) return "chromium";
|
||||||
if (lower.includes("canary") || lower.includes("sxs")) return "canary";
|
if (lower.includes("canary") || lower.includes("sxs")) return "canary";
|
||||||
if (lower.includes("opera") || lower.includes("vivaldi") || lower.includes("yandex")) return "chromium";
|
if (lower.includes("opera") || lower.includes("vivaldi") || lower.includes("yandex"))
|
||||||
|
return "chromium";
|
||||||
return "chrome";
|
return "chrome";
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectDefaultChromiumExecutable(
|
function detectDefaultChromiumExecutable(platform: NodeJS.Platform): BrowserExecutable | null {
|
||||||
platform: NodeJS.Platform,
|
|
||||||
): BrowserExecutable | null {
|
|
||||||
if (platform === "darwin") return detectDefaultChromiumExecutableMac();
|
if (platform === "darwin") return detectDefaultChromiumExecutableMac();
|
||||||
if (platform === "linux") return detectDefaultChromiumExecutableLinux();
|
if (platform === "linux") return detectDefaultChromiumExecutableLinux();
|
||||||
if (platform === "win32") return detectDefaultChromiumExecutableWindows();
|
if (platform === "win32") return detectDefaultChromiumExecutableWindows();
|
||||||
@@ -227,8 +231,7 @@ function detectDefaultChromiumExecutableLinux(): BrowserExecutable | null {
|
|||||||
function detectDefaultChromiumExecutableWindows(): BrowserExecutable | null {
|
function detectDefaultChromiumExecutableWindows(): BrowserExecutable | null {
|
||||||
const progId = readWindowsProgId();
|
const progId = readWindowsProgId();
|
||||||
const command =
|
const command =
|
||||||
(progId ? readWindowsCommandForProgId(progId) : null) ||
|
(progId ? readWindowsCommandForProgId(progId) : null) || readWindowsCommandForProgId("http");
|
||||||
readWindowsCommandForProgId("http");
|
|
||||||
if (!command) return null;
|
if (!command) return null;
|
||||||
const expanded = expandWindowsEnvVars(command);
|
const expanded = expandWindowsEnvVars(command);
|
||||||
const exePath = extractWindowsExecutablePath(expanded);
|
const exePath = extractWindowsExecutablePath(expanded);
|
||||||
@@ -285,7 +288,7 @@ function splitExecLine(line: string): string[] {
|
|||||||
let quoteChar = "";
|
let quoteChar = "";
|
||||||
for (let i = 0; i < line.length; i += 1) {
|
for (let i = 0; i < line.length; i += 1) {
|
||||||
const ch = line[i];
|
const ch = line[i];
|
||||||
if ((ch === "\"" || ch === "'") && (!inQuotes || ch === quoteChar)) {
|
if ((ch === '"' || ch === "'") && (!inQuotes || ch === quoteChar)) {
|
||||||
if (inQuotes) {
|
if (inQuotes) {
|
||||||
inQuotes = false;
|
inQuotes = false;
|
||||||
quoteChar = "";
|
quoteChar = "";
|
||||||
@@ -342,7 +345,7 @@ function readWindowsCommandForProgId(progId: string): string | null {
|
|||||||
function expandWindowsEnvVars(value: string): string {
|
function expandWindowsEnvVars(value: string): string {
|
||||||
return value.replace(/%([^%]+)%/g, (_match, name) => {
|
return value.replace(/%([^%]+)%/g, (_match, name) => {
|
||||||
const key = String(name ?? "").trim();
|
const key = String(name ?? "").trim();
|
||||||
return key ? process.env[key] ?? `%${key}%` : _match;
|
return key ? (process.env[key] ?? `%${key}%`) : _match;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -267,9 +267,7 @@ async function connectBrowser(cdpUrl: string): Promise<ConnectedBrowser> {
|
|||||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||||
try {
|
try {
|
||||||
const timeout = 5000 + attempt * 2000;
|
const timeout = 5000 + attempt * 2000;
|
||||||
const wsUrl = await getChromeWebSocketUrl(normalized, timeout).catch(
|
const wsUrl = await getChromeWebSocketUrl(normalized, timeout).catch(() => null);
|
||||||
() => null,
|
|
||||||
);
|
|
||||||
const endpoint = wsUrl ?? normalized;
|
const endpoint = wsUrl ?? normalized;
|
||||||
const headers = getHeadersWithAuth(endpoint);
|
const headers = getHeadersWithAuth(endpoint);
|
||||||
const browser = await chromium.connectOverCDP(endpoint, { timeout, headers });
|
const browser = await chromium.connectOverCDP(endpoint, { timeout, headers });
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
|
||||||
import {
|
import { appendCdpPath, createTargetViaCdp, getHeadersWithAuth, normalizeCdpWsUrl } from "./cdp.js";
|
||||||
appendCdpPath,
|
|
||||||
createTargetViaCdp,
|
|
||||||
getHeadersWithAuth,
|
|
||||||
normalizeCdpWsUrl,
|
|
||||||
} from "./cdp.js";
|
|
||||||
import {
|
import {
|
||||||
isChromeCdpReady,
|
isChromeCdpReady,
|
||||||
isChromeReachable,
|
isChromeReachable,
|
||||||
@@ -55,10 +50,7 @@ async function fetchJson<T>(url: string, timeoutMs = 1500, init?: RequestInit):
|
|||||||
const ctrl = new AbortController();
|
const ctrl = new AbortController();
|
||||||
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
||||||
try {
|
try {
|
||||||
const headers = getHeadersWithAuth(
|
const headers = getHeadersWithAuth(url, (init?.headers as Record<string, string>) || {});
|
||||||
url,
|
|
||||||
(init?.headers as Record<string, string>) || {},
|
|
||||||
);
|
|
||||||
const res = await fetch(url, { ...init, headers, signal: ctrl.signal });
|
const res = await fetch(url, { ...init, headers, signal: ctrl.signal });
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
return (await res.json()) as T;
|
return (await res.json()) as T;
|
||||||
@@ -71,10 +63,7 @@ async function fetchOk(url: string, timeoutMs = 1500, init?: RequestInit): Promi
|
|||||||
const ctrl = new AbortController();
|
const ctrl = new AbortController();
|
||||||
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
||||||
try {
|
try {
|
||||||
const headers = getHeadersWithAuth(
|
const headers = getHeadersWithAuth(url, (init?.headers as Record<string, string>) || {});
|
||||||
url,
|
|
||||||
(init?.headers as Record<string, string>) || {},
|
|
||||||
);
|
|
||||||
const res = await fetch(url, { ...init, headers, signal: ctrl.signal });
|
const res = await fetch(url, { ...init, headers, signal: ctrl.signal });
|
||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -238,7 +238,11 @@ export function registerConfigCli(program: Command) {
|
|||||||
defaultRuntime.log(JSON.stringify(res.value ?? null, null, 2));
|
defaultRuntime.log(JSON.stringify(res.value ?? null, null, 2));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (typeof res.value === "string" || typeof res.value === "number" || typeof res.value === "boolean") {
|
if (
|
||||||
|
typeof res.value === "string" ||
|
||||||
|
typeof res.value === "number" ||
|
||||||
|
typeof res.value === "boolean"
|
||||||
|
) {
|
||||||
defaultRuntime.log(String(res.value));
|
defaultRuntime.log(String(res.value));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,7 +225,11 @@ describe("daemon-cli coverage", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{"));
|
const jsonLine = runtimeLogs.find((line) => line.trim().startsWith("{"));
|
||||||
const parsed = JSON.parse(jsonLine ?? "{}") as { ok?: boolean; action?: string; result?: string };
|
const parsed = JSON.parse(jsonLine ?? "{}") as {
|
||||||
|
ok?: boolean;
|
||||||
|
action?: string;
|
||||||
|
result?: string;
|
||||||
|
};
|
||||||
expect(parsed.ok).toBe(true);
|
expect(parsed.ok).toBe(true);
|
||||||
expect(parsed.action).toBe("install");
|
expect(parsed.action).toBe("install");
|
||||||
expect(parsed.result).toBe("installed");
|
expect(parsed.result).toBe("installed");
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import type { Command } from "commander";
|
import type { Command } from "commander";
|
||||||
import {
|
import { listPairingChannels, notifyPairingApproved } from "../channels/plugins/pairing.js";
|
||||||
listPairingChannels,
|
|
||||||
notifyPairingApproved,
|
|
||||||
} from "../channels/plugins/pairing.js";
|
|
||||||
import { normalizeChannelId } from "../channels/plugins/index.js";
|
import { normalizeChannelId } from "../channels/plugins/index.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import { loadConfig } from "../config/config.js";
|
||||||
import { resolvePairingIdLabel } from "../pairing/pairing-labels.js";
|
import { resolvePairingIdLabel } from "../pairing/pairing-labels.js";
|
||||||
|
|||||||
@@ -378,9 +378,7 @@ export function registerPluginsCli(program: Command) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (record.source !== "npm") {
|
if (record.source !== "npm") {
|
||||||
defaultRuntime.log(
|
defaultRuntime.log(chalk.yellow(`Skipping "${pluginId}" (source: ${record.source}).`));
|
||||||
chalk.yellow(`Skipping "${pluginId}" (source: ${record.source}).`),
|
|
||||||
);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!record.spec) {
|
if (!record.spec) {
|
||||||
@@ -412,9 +410,7 @@ export function registerPluginsCli(program: Command) {
|
|||||||
if (currentVersion && probe.version && currentVersion === probe.version) {
|
if (currentVersion && probe.version && currentVersion === probe.version) {
|
||||||
defaultRuntime.log(`${pluginId} is up to date (${currentLabel}).`);
|
defaultRuntime.log(`${pluginId} is up to date (${currentLabel}).`);
|
||||||
} else {
|
} else {
|
||||||
defaultRuntime.log(
|
defaultRuntime.log(`Would update ${pluginId}: ${currentLabel} → ${nextVersion}.`);
|
||||||
`Would update ${pluginId}: ${currentLabel} → ${nextVersion}.`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -372,8 +372,6 @@ async function resolveSshTarget(
|
|||||||
});
|
});
|
||||||
if (!target) return { target: rawTarget, identity: identity ?? undefined };
|
if (!target) return { target: rawTarget, identity: identity ?? undefined };
|
||||||
const identityFile =
|
const identityFile =
|
||||||
identity ??
|
identity ?? config.identityFiles.find((entry) => entry.trim().length > 0)?.trim() ?? undefined;
|
||||||
config.identityFiles.find((entry) => entry.trim().length > 0)?.trim() ??
|
|
||||||
undefined;
|
|
||||||
return { target, identity: identityFile };
|
return { target, identity: identityFile };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,9 +291,7 @@ export async function statusAllCommand(
|
|||||||
: gatewayProbe?.error
|
: gatewayProbe?.error
|
||||||
? `unreachable (${gatewayProbe.error})`
|
? `unreachable (${gatewayProbe.error})`
|
||||||
: "unreachable";
|
: "unreachable";
|
||||||
const gatewayAuth = gatewayReachable
|
const gatewayAuth = gatewayReachable ? ` · auth ${formatGatewayAuthUsed(probeAuth)}` : "";
|
||||||
? ` · auth ${formatGatewayAuthUsed(probeAuth)}`
|
|
||||||
: "";
|
|
||||||
const gatewaySelfLine =
|
const gatewaySelfLine =
|
||||||
gatewaySelf?.host || gatewaySelf?.ip || gatewaySelf?.version || gatewaySelf?.platform
|
gatewaySelf?.host || gatewaySelf?.ip || gatewaySelf?.version || gatewaySelf?.platform
|
||||||
? [
|
? [
|
||||||
|
|||||||
@@ -28,18 +28,18 @@ describe("config discord", () => {
|
|||||||
dm: {
|
dm: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
allowFrom: ["steipete"],
|
allowFrom: ["steipete"],
|
||||||
groupEnabled: true,
|
groupEnabled: true,
|
||||||
groupChannels: ["clawd-dm"],
|
groupChannels: ["clawd-dm"],
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
emojiUploads: true,
|
emojiUploads: true,
|
||||||
stickerUploads: false,
|
stickerUploads: false,
|
||||||
channels: true,
|
channels: true,
|
||||||
},
|
},
|
||||||
guilds: {
|
guilds: {
|
||||||
"123": {
|
"123": {
|
||||||
slug: "friends-of-clawd",
|
slug: "friends-of-clawd",
|
||||||
requireMention: false,
|
requireMention: false,
|
||||||
users: ["steipete"],
|
users: ["steipete"],
|
||||||
channels: {
|
channels: {
|
||||||
general: { allow: true },
|
general: { allow: true },
|
||||||
|
|||||||
@@ -306,7 +306,8 @@ const FIELD_HELP: Record<string, string> = {
|
|||||||
"plugins.installs.*.source": 'Install source ("npm", "archive", or "path").',
|
"plugins.installs.*.source": 'Install source ("npm", "archive", or "path").',
|
||||||
"plugins.installs.*.spec": "Original npm spec used for install (if source is npm).",
|
"plugins.installs.*.spec": "Original npm spec used for install (if source is npm).",
|
||||||
"plugins.installs.*.sourcePath": "Original archive/path used for install (if any).",
|
"plugins.installs.*.sourcePath": "Original archive/path used for install (if any).",
|
||||||
"plugins.installs.*.installPath": "Resolved install directory (usually ~/.clawdbot/extensions/<id>).",
|
"plugins.installs.*.installPath":
|
||||||
|
"Resolved install directory (usually ~/.clawdbot/extensions/<id>).",
|
||||||
"plugins.installs.*.version": "Version recorded at install time (if available).",
|
"plugins.installs.*.version": "Version recorded at install time (if available).",
|
||||||
"plugins.installs.*.installedAt": "ISO timestamp of last install/update.",
|
"plugins.installs.*.installedAt": "ISO timestamp of last install/update.",
|
||||||
"agents.defaults.model.primary": "Primary model (provider/model).",
|
"agents.defaults.model.primary": "Primary model (provider/model).",
|
||||||
|
|||||||
@@ -10,11 +10,9 @@ import {
|
|||||||
export const SessionSchema = z
|
export const SessionSchema = z
|
||||||
.object({
|
.object({
|
||||||
scope: z.union([z.literal("per-sender"), z.literal("global")]).optional(),
|
scope: z.union([z.literal("per-sender"), z.literal("global")]).optional(),
|
||||||
dmScope: z.union([
|
dmScope: z
|
||||||
z.literal("main"),
|
.union([z.literal("main"), z.literal("per-peer"), z.literal("per-channel-peer")])
|
||||||
z.literal("per-peer"),
|
.optional(),
|
||||||
z.literal("per-channel-peer"),
|
|
||||||
]).optional(),
|
|
||||||
resetTriggers: z.array(z.string()).optional(),
|
resetTriggers: z.array(z.string()).optional(),
|
||||||
idleMinutes: z.number().int().positive().optional(),
|
idleMinutes: z.number().int().positive().optional(),
|
||||||
heartbeatIdleMinutes: z.number().int().positive().optional(),
|
heartbeatIdleMinutes: z.number().int().positive().optional(),
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ describe("parseSystemdExecStart", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("preserves quoted arguments", () => {
|
it("preserves quoted arguments", () => {
|
||||||
const execStart = "/usr/bin/clawdbot gateway start --name \"My Bot\"";
|
const execStart = '/usr/bin/clawdbot gateway start --name "My Bot"';
|
||||||
expect(parseSystemdExecStart(execStart)).toEqual([
|
expect(parseSystemdExecStart(execStart)).toEqual([
|
||||||
"/usr/bin/clawdbot",
|
"/usr/bin/clawdbot",
|
||||||
"gateway",
|
"gateway",
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import { timingSafeEqual } from "node:crypto";
|
import { timingSafeEqual } from "node:crypto";
|
||||||
import type { IncomingMessage } from "node:http";
|
import type { IncomingMessage } from "node:http";
|
||||||
import type {
|
import type { GatewayAuthConfig, GatewayTailscaleMode } from "../config/config.js";
|
||||||
GatewayAuthConfig,
|
|
||||||
GatewayTailscaleMode,
|
|
||||||
} from "../config/config.js";
|
|
||||||
export type ResolvedGatewayAuthMode = "none" | "token" | "password";
|
export type ResolvedGatewayAuthMode = "none" | "token" | "password";
|
||||||
|
|
||||||
export type ResolvedGatewayAuth = {
|
export type ResolvedGatewayAuth = {
|
||||||
@@ -62,14 +59,13 @@ function isLocalDirectRequest(req?: IncomingMessage): boolean {
|
|||||||
if (!isLoopbackAddress(clientIp)) return false;
|
if (!isLoopbackAddress(clientIp)) return false;
|
||||||
|
|
||||||
const host = getHostName(req.headers?.host);
|
const host = getHostName(req.headers?.host);
|
||||||
const hostIsLocal =
|
const hostIsLocal = host === "localhost" || host === "127.0.0.1" || host === "::1";
|
||||||
host === "localhost" || host === "127.0.0.1" || host === "::1";
|
|
||||||
const hostIsTailscaleServe = host.endsWith(".ts.net");
|
const hostIsTailscaleServe = host.endsWith(".ts.net");
|
||||||
|
|
||||||
const hasForwarded = Boolean(
|
const hasForwarded = Boolean(
|
||||||
req.headers?.["x-forwarded-for"] ||
|
req.headers?.["x-forwarded-for"] ||
|
||||||
req.headers?.["x-real-ip"] ||
|
req.headers?.["x-real-ip"] ||
|
||||||
req.headers?.["x-forwarded-host"],
|
req.headers?.["x-forwarded-host"],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (hostIsLocal || hostIsTailscaleServe) && !hasForwarded;
|
return (hostIsLocal || hostIsTailscaleServe) && !hasForwarded;
|
||||||
@@ -81,17 +77,11 @@ function getTailscaleUser(req?: IncomingMessage): TailscaleUser | null {
|
|||||||
if (typeof login !== "string" || !login.trim()) return null;
|
if (typeof login !== "string" || !login.trim()) return null;
|
||||||
const nameRaw = req.headers["tailscale-user-name"];
|
const nameRaw = req.headers["tailscale-user-name"];
|
||||||
const profilePic = req.headers["tailscale-user-profile-pic"];
|
const profilePic = req.headers["tailscale-user-profile-pic"];
|
||||||
const name =
|
const name = typeof nameRaw === "string" && nameRaw.trim() ? nameRaw.trim() : login.trim();
|
||||||
typeof nameRaw === "string" && nameRaw.trim()
|
|
||||||
? nameRaw.trim()
|
|
||||||
: login.trim();
|
|
||||||
return {
|
return {
|
||||||
login: login.trim(),
|
login: login.trim(),
|
||||||
name,
|
name,
|
||||||
profilePic:
|
profilePic: typeof profilePic === "string" && profilePic.trim() ? profilePic.trim() : undefined,
|
||||||
typeof profilePic === "string" && profilePic.trim()
|
|
||||||
? profilePic.trim()
|
|
||||||
: undefined,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,17 +89,14 @@ function hasTailscaleProxyHeaders(req?: IncomingMessage): boolean {
|
|||||||
if (!req) return false;
|
if (!req) return false;
|
||||||
return Boolean(
|
return Boolean(
|
||||||
req.headers["x-forwarded-for"] &&
|
req.headers["x-forwarded-for"] &&
|
||||||
req.headers["x-forwarded-proto"] &&
|
req.headers["x-forwarded-proto"] &&
|
||||||
req.headers["x-forwarded-host"],
|
req.headers["x-forwarded-host"],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTailscaleProxyRequest(req?: IncomingMessage): boolean {
|
function isTailscaleProxyRequest(req?: IncomingMessage): boolean {
|
||||||
if (!req) return false;
|
if (!req) return false;
|
||||||
return (
|
return isLoopbackAddress(req.socket?.remoteAddress) && hasTailscaleProxyHeaders(req);
|
||||||
isLoopbackAddress(req.socket?.remoteAddress) &&
|
|
||||||
hasTailscaleProxyHeaders(req)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function resolveGatewayAuth(params: {
|
export function resolveGatewayAuth(params: {
|
||||||
@@ -120,13 +107,11 @@ export function resolveGatewayAuth(params: {
|
|||||||
const authConfig = params.authConfig ?? {};
|
const authConfig = params.authConfig ?? {};
|
||||||
const env = params.env ?? process.env;
|
const env = params.env ?? process.env;
|
||||||
const token = authConfig.token ?? env.CLAWDBOT_GATEWAY_TOKEN ?? undefined;
|
const token = authConfig.token ?? env.CLAWDBOT_GATEWAY_TOKEN ?? undefined;
|
||||||
const password =
|
const password = authConfig.password ?? env.CLAWDBOT_GATEWAY_PASSWORD ?? undefined;
|
||||||
authConfig.password ?? env.CLAWDBOT_GATEWAY_PASSWORD ?? undefined;
|
|
||||||
const mode: ResolvedGatewayAuth["mode"] =
|
const mode: ResolvedGatewayAuth["mode"] =
|
||||||
authConfig.mode ?? (password ? "password" : token ? "token" : "none");
|
authConfig.mode ?? (password ? "password" : token ? "token" : "none");
|
||||||
const allowTailscale =
|
const allowTailscale =
|
||||||
authConfig.allowTailscale ??
|
authConfig.allowTailscale ?? (params.tailscaleMode === "serve" && mode !== "password");
|
||||||
(params.tailscaleMode === "serve" && mode !== "password");
|
|
||||||
return {
|
return {
|
||||||
mode,
|
mode,
|
||||||
token,
|
token,
|
||||||
@@ -142,9 +127,7 @@ export function assertGatewayAuthConfigured(auth: ResolvedGatewayAuth): void {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (auth.mode === "password" && !auth.password) {
|
if (auth.mode === "password" && !auth.password) {
|
||||||
throw new Error(
|
throw new Error("gateway auth mode is password, but no password was configured");
|
||||||
"gateway auth mode is password, but no password was configured",
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -162,7 +162,9 @@ export async function startGatewayNodeBridge(params: {
|
|||||||
});
|
});
|
||||||
if (started.port > 0) {
|
if (started.port > 0) {
|
||||||
const scheme = params.bridgeTls?.enabled ? "tls" : "tcp";
|
const scheme = params.bridgeTls?.enabled ? "tls" : "tcp";
|
||||||
params.logBridge.info(`listening on ${scheme}://${params.bridgeHost}:${started.port} (node)`);
|
params.logBridge.info(
|
||||||
|
`listening on ${scheme}://${params.bridgeHost}:${started.port} (node)`,
|
||||||
|
);
|
||||||
return { bridge: started, nodePresenceTimers };
|
return { bridge: started, nodePresenceTimers };
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ export async function startNodeBridgeServer(opts: NodeBridgeServerOpts): Promise
|
|||||||
const loopbackHost = "127.0.0.1";
|
const loopbackHost = "127.0.0.1";
|
||||||
|
|
||||||
const listeners: Array<{ host: string; server: net.Server }> = [];
|
const listeners: Array<{ host: string; server: net.Server }> = [];
|
||||||
const createServer = () => (opts.tls ? tls.createServer(opts.tls, onConnection) : net.createServer(onConnection));
|
const createServer = () =>
|
||||||
|
opts.tls ? tls.createServer(opts.tls, onConnection) : net.createServer(onConnection);
|
||||||
const primary = createServer();
|
const primary = createServer();
|
||||||
await new Promise<void>((resolve, reject) => {
|
await new Promise<void>((resolve, reject) => {
|
||||||
const onError = (err: Error) => reject(err);
|
const onError = (err: Error) => reject(err);
|
||||||
|
|||||||
@@ -145,9 +145,7 @@ function parseBinProbePayload(payloadJSON: string | null | undefined): string[]
|
|||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(payloadJSON) as { stdout?: unknown; bins?: unknown };
|
const parsed = JSON.parse(payloadJSON) as { stdout?: unknown; bins?: unknown };
|
||||||
if (Array.isArray(parsed.bins)) {
|
if (Array.isArray(parsed.bins)) {
|
||||||
return parsed.bins
|
return parsed.bins.map((bin) => String(bin).trim()).filter(Boolean);
|
||||||
.map((bin) => String(bin).trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
}
|
}
|
||||||
if (typeof parsed.stdout === "string") {
|
if (typeof parsed.stdout === "string") {
|
||||||
return parsed.stdout
|
return parsed.stdout
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import type { PluginInstallRecord } from "../config/types.plugins.js";
|
|||||||
|
|
||||||
export type PluginInstallUpdate = PluginInstallRecord & { pluginId: string };
|
export type PluginInstallUpdate = PluginInstallRecord & { pluginId: string };
|
||||||
|
|
||||||
export function recordPluginInstall(cfg: ClawdbotConfig, update: PluginInstallUpdate): ClawdbotConfig {
|
export function recordPluginInstall(
|
||||||
|
cfg: ClawdbotConfig,
|
||||||
|
update: PluginInstallUpdate,
|
||||||
|
): ClawdbotConfig {
|
||||||
const { pluginId, ...record } = update;
|
const { pluginId, ...record } = update;
|
||||||
const installs = {
|
const installs = {
|
||||||
...cfg.plugins?.installs,
|
...cfg.plugins?.installs,
|
||||||
|
|||||||
@@ -70,11 +70,9 @@ export const registerTelegramNativeCommands = ({
|
|||||||
) => Promise<unknown>;
|
) => Promise<unknown>;
|
||||||
};
|
};
|
||||||
if (typeof api.setMyCommands === "function") {
|
if (typeof api.setMyCommands === "function") {
|
||||||
api
|
api.setMyCommands(allCommands).catch((err) => {
|
||||||
.setMyCommands(allCommands)
|
runtime.error?.(danger(`telegram setMyCommands failed: ${String(err)}`));
|
||||||
.catch((err) => {
|
});
|
||||||
runtime.error?.(danger(`telegram setMyCommands failed: ${String(err)}`));
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
logVerbose("telegram: setMyCommands unavailable; skipping registration");
|
logVerbose("telegram: setMyCommands unavailable; skipping registration");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,9 +235,7 @@ describe("createTelegramBot", () => {
|
|||||||
expect(nativeStatus).toBeDefined();
|
expect(nativeStatus).toBeDefined();
|
||||||
expect(registered).toContainEqual({ command: "custom_backup", description: "Git backup" });
|
expect(registered).toContainEqual({ command: "custom_backup", description: "Git backup" });
|
||||||
expect(registered).not.toContainEqual({ command: "status", description: "Custom status" });
|
expect(registered).not.toContainEqual({ command: "status", description: "Custom status" });
|
||||||
expect(registered.filter((command) => command.command === "status")).toEqual([
|
expect(registered.filter((command) => command.command === "status")).toEqual([nativeStatus]);
|
||||||
nativeStatus,
|
|
||||||
]);
|
|
||||||
expect(errorSpy).toHaveBeenCalled();
|
expect(errorSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user